Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add comprehensive login page customization options #7374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ You can change the config file's location using the `--config` flag or

The default location respects `$XDG_CONFIG_HOME`.


## How do I make my keyboard shortcuts work?

Many shortcuts will not work by default, since they'll be "caught" by the browser.
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ code-server.
We also have an in-depth [setup and
configuration](https://coder.com/docs/code-server/latest/guide) guide.


## Questions?

See answers to [frequently asked
Expand Down
35 changes: 35 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,3 +458,38 @@ By default, if you have auth enabled, code-server will authenticate all proxied
requests including preflight requests. This can cause issues because preflight
requests do not typically include credentials. To allow all preflight requests
through the proxy without authentication, use `--skip-auth-preflight`.

## Internationalization and customization

code-server allows you to provide a language file or JSON to configure certain strings. This can be used for both internationalization and customization.

For example:

```shell
code-server --i18n /custom-strings.json
code-server --i18n '{"WELCOME": "{{app}} ログイン"}'
```

Or this can be done in the config file:

```yaml
i18n: |
{
"WELCOME": "Welcome to the {{app}} Development Portal"
}
```

You can combine this with the `--locale` flag to configure language support for both code-server and VS Code in cases where code-server has no support but VS Code does. If you are using this for internationalization, please consider sending us a pull request to contribute it to `src/node/i18n/locales`.

### Available keys and placeholders

Refer to [../src/node/i18n/locales/en.json](../src/node/i18n/locales/en.json) for a full list of the available keys for translations. Note that the only placeholders supported for each key are the ones used in the default string.

The `--app-name` flag controls the `{{app}}` placeholder in templates. If you want to change the name, you can either:

1. Set `--app-name` (potentially alongside `--i18n`)
2. Use `--i18n` and hardcode the name in your strings

### Legacy flag

The `--welcome-text` flag is now deprecated. Use the `WELCOME` key instead.
1 change: 1 addition & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \
codercom/code-server:latest
```


Our official image supports `amd64` and `arm64`. For `arm32` support, you can
use a [community-maintained code-server
alternative](https://hub.docker.com/r/linuxserver/code-server).
Expand Down
10 changes: 9 additions & 1 deletion src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
}
Expand Down Expand Up @@ -284,17 +285,22 @@ 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",
description: "Path to JSON file or raw JSON string with custom translations. Merges with default strings and supports all i18n keys.",
},
}

export const optionDescriptions = (opts: Partial<Options<Required<UserProvidedArgs>>> = options): string[] => {
Expand Down Expand Up @@ -459,6 +465,7 @@ export const parse = (
throw new Error("--cert-key is missing")
}


logger.debug(() => [`parsed ${opts?.configFile ? "config" : "command line"}`, field("args", redactArgs(args))])

return args
Expand Down Expand Up @@ -593,6 +600,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
args["disable-proxy"] = true
}


const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD
if (process.env.HASHED_PASSWORD) {
args["hashed-password"] = process.env.HASHED_PASSWORD
Expand Down
61 changes: 44 additions & 17 deletions src/node/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,60 @@
import i18next, { init } from "i18next"
import { promises as fs } from "fs"
import * as en from "./locales/en.json"
import * as ja from "./locales/ja.json"
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(customStringsArg: string): Promise<void> {

try {
let customStringsData: Record<string, any>

// Try to parse as JSON first
try {
customStringsData = JSON.parse(customStringsArg)
} catch {
// If JSON parsing fails, treat as file path
const fileContent = await fs.readFile(customStringsArg, "utf8")
customStringsData = JSON.parse(fileContent)
}

// User-provided strings override all languages.
Object.keys(defaultResources).forEach((locale) => {
i18next.addResourceBundle(locale, "translation", customStringsData)
})
} catch (error) {
throw new Error(`Failed to load custom strings: ${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 changes: 7 additions & 0 deletions src/node/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"]}`)

Expand Down
19 changes: 14 additions & 5 deletions src/node/routes/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>` : ""),
)
}
Expand Down
27 changes: 27 additions & 0 deletions test/unit/node/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe("parser", () => {
"--verbose",
["--app-name", "custom instance name"],
["--welcome-text", "welcome to code"],
["--i18n", '{"LOGIN_TITLE": "Custom Portal"}'],
"2",

["--locale", "ja"],
Expand Down Expand Up @@ -145,6 +146,7 @@ describe("parser", () => {
verbose: true,
"app-name": "custom instance name",
"welcome-text": "welcome to code",
i18n: '{"LOGIN_TITLE": "Custom Portal"}',
version: true,
"bind-addr": "192.169.0.1:8080",
"session-socket": "/tmp/override-code-server-ipc-socket",
Expand Down Expand Up @@ -347,6 +349,31 @@ describe("parser", () => {
})
})

it("should parse i18n flag", async () => {
// Test with JSON string
const jsonString = '{"WELCOME": "Custom Welcome", "LOGIN_TITLE": "My App"}'
const args = parse(["--i18n", jsonString])
expect(args).toEqual({
i18n: jsonString,
})
})

it("should parse i18n file paths and JSON", async () => {
// Test with valid JSON that looks like a file path
expect(() => parse(["--i18n", "/path/to/file.json"])).not.toThrow()

// Test with JSON string (no validation at CLI level)
expect(() => parse(["--i18n", '{"valid": "json"}'])).not.toThrow()
})

it("should support app-name and deprecated welcome-text flags", async () => {
const args = parse(["--app-name", "My App", "--welcome-text", "Welcome!"])
expect(args).toEqual({
"app-name": "My App",
"welcome-text": "Welcome!",
})
})

it("should use env var github token", async () => {
process.env.GITHUB_TOKEN = "ga-foo"
const args = parse([])
Expand Down
1 change: 1 addition & 0 deletions test/unit/node/routes/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,6 @@ describe("login", () => {
expect(resp.status).toBe(200)
expect(htmlContent).toContain(`欢迎来到 code-server`)
})

})
})