mirror of
https://github.com/coder/code-server.git
synced 2026-05-08 13:27:25 +02:00
Add language customization flag (#7374)
This allows you to customize any string (that has a translation) or add your own translations.
This commit is contained in:
committed by
GitHub
parent
8b3d9b9e0a
commit
92fca0dcc3
@@ -93,6 +93,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs {
|
||||
"app-name"?: string
|
||||
"welcome-text"?: string
|
||||
"abs-proxy-base-path"?: string
|
||||
i18n?: string
|
||||
/* Positional arguments. */
|
||||
_?: string[]
|
||||
}
|
||||
@@ -284,17 +285,24 @@ export const options: Options<Required<UserProvidedArgs>> = {
|
||||
"app-name": {
|
||||
type: "string",
|
||||
short: "an",
|
||||
description: "The name to use in branding. Will be shown in titlebar and welcome message",
|
||||
description:
|
||||
"Will replace the {{app}} placeholder in any strings, which by default includes the title bar and welcome message",
|
||||
},
|
||||
"welcome-text": {
|
||||
type: "string",
|
||||
short: "w",
|
||||
description: "Text to show on login page",
|
||||
deprecated: true,
|
||||
},
|
||||
"abs-proxy-base-path": {
|
||||
type: "string",
|
||||
description: "The base path to prefix to all absproxy requests",
|
||||
},
|
||||
i18n: {
|
||||
type: "string",
|
||||
path: true,
|
||||
description: "Path to JSON file with custom translations. Merges with default strings and supports all i18n keys.",
|
||||
},
|
||||
}
|
||||
|
||||
export const optionDescriptions = (opts: Partial<Options<Required<UserProvidedArgs>>> = options): string[] => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { promises as fs } from "fs"
|
||||
import i18next, { init } from "i18next"
|
||||
import * as en from "./locales/en.json"
|
||||
import * as ja from "./locales/ja.json"
|
||||
@@ -5,29 +6,54 @@ import * as th from "./locales/th.json"
|
||||
import * as ur from "./locales/ur.json"
|
||||
import * as zhCn from "./locales/zh-cn.json"
|
||||
|
||||
const defaultResources = {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
"zh-cn": {
|
||||
translation: zhCn,
|
||||
},
|
||||
th: {
|
||||
translation: th,
|
||||
},
|
||||
ja: {
|
||||
translation: ja,
|
||||
},
|
||||
ur: {
|
||||
translation: ur,
|
||||
},
|
||||
}
|
||||
|
||||
export async function loadCustomStrings(filePath: string): Promise<void> {
|
||||
try {
|
||||
// Read custom strings from file path only
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
const customStringsData = JSON.parse(fileContent)
|
||||
|
||||
// User-provided strings override all languages.
|
||||
Object.keys(defaultResources).forEach((locale) => {
|
||||
i18next.addResourceBundle(locale, "translation", customStringsData)
|
||||
})
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
||||
throw new Error(`Custom strings file not found: ${filePath}\nPlease ensure the file exists and is readable.`)
|
||||
} else if (error instanceof SyntaxError) {
|
||||
throw new Error(`Invalid JSON in custom strings file: ${filePath}\n${error.message}`)
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to load custom strings from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init({
|
||||
lng: "en",
|
||||
fallbackLng: "en", // language to use if translations in user language are not available.
|
||||
returnNull: false,
|
||||
lowerCaseLng: true,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
resources: {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
"zh-cn": {
|
||||
translation: zhCn,
|
||||
},
|
||||
th: {
|
||||
translation: th,
|
||||
},
|
||||
ja: {
|
||||
translation: ja,
|
||||
},
|
||||
ur: {
|
||||
translation: ur,
|
||||
},
|
||||
},
|
||||
resources: defaultResources,
|
||||
})
|
||||
|
||||
export default i18next
|
||||
|
||||
@@ -7,6 +7,7 @@ import { plural } from "../common/util"
|
||||
import { createApp, ensureAddress } from "./app"
|
||||
import { AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli"
|
||||
import { commit, version, vsRootPath } from "./constants"
|
||||
import { loadCustomStrings } from "./i18n"
|
||||
import { register } from "./routes"
|
||||
import { VSCodeModule } from "./routes/vscode"
|
||||
import { isDirectory, open } from "./util"
|
||||
@@ -122,6 +123,12 @@ export const runCodeServer = async (
|
||||
): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => {
|
||||
logger.info(`code-server ${version} ${commit}`)
|
||||
|
||||
// Load custom strings if provided
|
||||
if (args.i18n) {
|
||||
await loadCustomStrings(args.i18n)
|
||||
logger.info("Loaded custom strings")
|
||||
}
|
||||
|
||||
logger.info(`Using user-data-dir ${args["user-data-dir"]}`)
|
||||
logger.debug(`Using extensions-dir ${args["extensions-dir"]}`)
|
||||
|
||||
|
||||
@@ -31,23 +31,32 @@ const getRoot = async (req: Request, error?: Error): Promise<string> => {
|
||||
const locale = req.args["locale"] || "en"
|
||||
i18n.changeLanguage(locale)
|
||||
const appName = req.args["app-name"] || "code-server"
|
||||
const welcomeText = req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string)
|
||||
const welcomeText = escapeHtml(req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string))
|
||||
|
||||
// Determine password message using i18n
|
||||
let passwordMsg = i18n.t("LOGIN_PASSWORD", { configFile: req.args.config })
|
||||
if (req.args.usingEnvPassword) {
|
||||
passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD")
|
||||
} else if (req.args.usingEnvHashedPassword) {
|
||||
passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD")
|
||||
}
|
||||
passwordMsg = escapeHtml(passwordMsg)
|
||||
|
||||
// Get messages from i18n (with HTML escaping for security)
|
||||
const loginTitle = escapeHtml(i18n.t("LOGIN_TITLE", { app: appName }))
|
||||
const loginBelow = escapeHtml(i18n.t("LOGIN_BELOW"))
|
||||
const passwordPlaceholder = escapeHtml(i18n.t("PASSWORD_PLACEHOLDER"))
|
||||
const submitText = escapeHtml(i18n.t("SUBMIT"))
|
||||
|
||||
return replaceTemplates(
|
||||
req,
|
||||
content
|
||||
.replace(/{{I18N_LOGIN_TITLE}}/g, i18n.t("LOGIN_TITLE", { app: appName }))
|
||||
.replace(/{{I18N_LOGIN_TITLE}}/g, loginTitle)
|
||||
.replace(/{{WELCOME_TEXT}}/g, welcomeText)
|
||||
.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
|
||||
.replace(/{{I18N_LOGIN_BELOW}}/g, i18n.t("LOGIN_BELOW"))
|
||||
.replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, i18n.t("PASSWORD_PLACEHOLDER"))
|
||||
.replace(/{{I18N_SUBMIT}}/g, i18n.t("SUBMIT"))
|
||||
.replace(/{{I18N_LOGIN_BELOW}}/g, loginBelow)
|
||||
.replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, passwordPlaceholder)
|
||||
.replace(/{{I18N_SUBMIT}}/g, submitText)
|
||||
.replace(/{{ERROR}}/, error ? `<div class="error">${escapeHtml(error.message)}</div>` : ""),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user