From cd99956995ce4caca72e3a3e55c053ec8ae8fe77 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Thu, 12 Jun 2025 19:17:53 +0200 Subject: [PATCH 01/13] feat: add comprehensive login page customization options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CLI arguments and environment variables to customize all login page elements: - Login title, subtitle, and welcome text - Password field placeholder and submit button text - Password instruction messages (config file, env var, hashed) - Error messages (rate limit, missing/incorrect password) New CLI options: --login-title, --login-below, --password-placeholder, --submit-text --login-password-msg, --login-env-password-msg, --login-hashed-password-msg --login-rate-limit-msg, --missing-password-msg, --incorrect-password-msg New environment variables: CS_LOGIN_TITLE, CS_LOGIN_BELOW, CS_PASSWORD_PLACEHOLDER, CS_SUBMIT_TEXT CS_LOGIN_PASSWORD_MSG, CS_LOGIN_ENV_PASSWORD_MSG, CS_LOGIN_HASHED_PASSWORD_MSG CS_LOGIN_RATE_LIMIT_MSG, CS_MISSING_PASSWORD_MSG, CS_INCORRECT_PASSWORD_MSG Features: - Full backwards compatibility with existing --app-name/--welcome-text - HTML escaping for security (prevents XSS) - Config file support (YAML) - Priority: CLI args > env vars > config file > defaults - Internationalization preserved for non-customized messages Perfect for Docker deployments and corporate branding. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/FAQ.md | 34 +++++++ docs/README.md | 2 + docs/customization.md | 147 ++++++++++++++++++++++++++++ docs/install.md | 24 +++++ src/node/cli.ts | 91 +++++++++++++++++ src/node/routes/login.ts | 31 +++--- test/unit/node/cli.test.ts | 30 ++++++ test/unit/node/routes/login.test.ts | 65 ++++++++++++ 8 files changed, 413 insertions(+), 11 deletions(-) create mode 100644 docs/customization.md diff --git a/docs/FAQ.md b/docs/FAQ.md index c46c003b8800..0f1e78f58bb6 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -81,6 +81,40 @@ You can change the config file's location using the `--config` flag or The default location respects `$XDG_CONFIG_HOME`. +### Login page customization + +You can customize the login page appearance using CLI flags or environment variables: + +**CLI flags:** +```bash +code-server --login-title "My Code Server" \ + --login-env-password-msg "Password set via environment" \ + --password-placeholder "Enter password" \ + --submit-text "LOGIN" +``` + +**Environment variables:** +```bash +export CS_LOGIN_TITLE="My Code Server" +export CS_LOGIN_ENV_PASSWORD_MSG="Password set via environment" +export CS_PASSWORD_PLACEHOLDER="Enter password" +export CS_SUBMIT_TEXT="LOGIN" +code-server +``` + +**Config file:** +```yaml +bind-addr: 127.0.0.1:8080 +auth: password +password: your-password +login-title: "My Code Server" +login-env-password-msg: "Password set via environment" +password-placeholder: "Enter password" +submit-text: "LOGIN" +``` + +CLI flags take priority over environment variables, which take priority over config file settings. + ## How do I make my keyboard shortcuts work? Many shortcuts will not work by default, since they'll be "caught" by the browser. diff --git a/docs/README.md b/docs/README.md index 5724c804c087..e80b7bfb23be 100644 --- a/docs/README.md +++ b/docs/README.md @@ -61,6 +61,8 @@ code-server. We also have an in-depth [setup and configuration](https://coder.com/docs/code-server/latest/guide) guide. +You can also customize the login page appearance - see our [customization guide](./customization.md). + ## Questions? See answers to [frequently asked diff --git a/docs/customization.md b/docs/customization.md new file mode 100644 index 000000000000..0dd8d9d4e1c0 --- /dev/null +++ b/docs/customization.md @@ -0,0 +1,147 @@ +# Login Page Customization + +code-server allows you to customize the login page appearance and messages through CLI arguments, environment variables, or configuration files. + +## Available Customization Options + +### Branding and Appearance +- **Login Title**: Customize the main title on the login page +- **Welcome Text**: Set custom welcome message +- **App Name**: Change the application branding throughout the interface + +### Login Messages +- **Password Instructions**: Customize the message explaining where to find the password +- **Environment Password Message**: Custom message when password is set via `$PASSWORD` +- **Hashed Password Message**: Custom message when password is set via `$HASHED_PASSWORD` + +### Form Elements +- **Password Placeholder**: Custom placeholder text for the password field +- **Submit Button**: Custom text for the login button + +### Error Messages +- **Rate Limit Message**: Custom message when login attempts are rate limited +- **Missing Password**: Custom message for empty password submissions +- **Incorrect Password**: Custom message for wrong password attempts + +## Configuration Methods + +### CLI Arguments + +```bash +code-server \ + --app-name "My Development Server" \ + --welcome-text "Welcome to the development environment" \ + --login-title "Secure Access Portal" \ + --login-below "Please authenticate to continue" \ + --password-placeholder "Enter your access code" \ + --submit-text "AUTHENTICATE" \ + --login-env-password-msg "Access code provided via environment variable" \ + --login-rate-limit-msg "Too many attempts. Please wait before trying again." \ + --missing-password-msg "Access code is required" \ + --incorrect-password-msg "Invalid access code" +``` + +### Environment Variables + +Perfect for Docker deployments and containerized environments: + +```bash +# Basic branding +export CS_APP_NAME="My Development Server" +export CS_WELCOME_TEXT="Welcome to the development environment" + +# Login page customization +export CS_LOGIN_TITLE="Secure Access Portal" +export CS_LOGIN_BELOW="Please authenticate to continue" +export CS_PASSWORD_PLACEHOLDER="Enter your access code" +export CS_SUBMIT_TEXT="AUTHENTICATE" + +# Message customization +export CS_LOGIN_ENV_PASSWORD_MSG="Access code provided via environment variable" +export CS_LOGIN_RATE_LIMIT_MSG="Too many attempts. Please wait before trying again." +export CS_MISSING_PASSWORD_MSG="Access code is required" +export CS_INCORRECT_PASSWORD_MSG="Invalid access code" + +code-server +``` + +### Configuration File + +Add to your `~/.config/code-server/config.yaml`: + +```yaml +bind-addr: 127.0.0.1:8080 +auth: password +password: your-password + +# Branding +app-name: "My Development Server" +welcome-text: "Welcome to the development environment" + +# Login page +login-title: "Secure Access Portal" +login-below: "Please authenticate to continue" +password-placeholder: "Enter your access code" +submit-text: "AUTHENTICATE" + +# Messages +login-env-password-msg: "Access code provided via environment variable" +login-rate-limit-msg: "Too many attempts. Please wait before trying again." +missing-password-msg: "Access code is required" +incorrect-password-msg: "Invalid access code" +``` + +## Docker Examples + +### Basic Docker Deployment with Customization + +```bash +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$PWD:/home/coder/project" \ + -e "CS_LOGIN_TITLE=Development Environment" \ + -e "CS_LOGIN_ENV_PASSWORD_MSG=Password configured in container environment" \ + -e "CS_PASSWORD_PLACEHOLDER=Enter development password" \ + -e "CS_SUBMIT_TEXT=ACCESS ENVIRONMENT" \ + codercom/code-server:latest +``` + +### Corporate Branding Example + +```bash +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$PWD:/home/coder/project" \ + -e "CS_APP_NAME=ACME Corporation Dev Portal" \ + -e "CS_LOGIN_TITLE=ACME Development Portal" \ + -e "CS_LOGIN_BELOW=Enter your corporate credentials" \ + -e "CS_PASSWORD_PLACEHOLDER=Corporate Password" \ + -e "CS_SUBMIT_TEXT=SIGN IN" \ + -e "CS_LOGIN_ENV_PASSWORD_MSG=Password managed by IT department" \ + codercom/code-server:latest +``` + +## Priority Order + +Settings are applied in the following priority order (highest to lowest): + +1. **CLI arguments** - Highest priority +2. **Environment variables** - Medium priority +3. **Config file** - Lowest priority + +This allows you to set defaults in your config file and override them with environment variables or CLI arguments as needed. + +## Complete Reference + +| CLI Argument | Environment Variable | Description | +|--------------|---------------------|-------------| +| `--app-name` | `CS_APP_NAME` | Application name used throughout the interface | +| `--welcome-text` | `CS_WELCOME_TEXT` | Welcome message on login page | +| `--login-title` | `CS_LOGIN_TITLE` | Main title on login page | +| `--login-below` | `CS_LOGIN_BELOW` | Text below the login title | +| `--password-placeholder` | `CS_PASSWORD_PLACEHOLDER` | Password field placeholder text | +| `--submit-text` | `CS_SUBMIT_TEXT` | Login button text | +| `--login-password-msg` | `CS_LOGIN_PASSWORD_MSG` | Message for config file password | +| `--login-env-password-msg` | `CS_LOGIN_ENV_PASSWORD_MSG` | Message when using `$PASSWORD` env var | +| `--login-hashed-password-msg` | `CS_LOGIN_HASHED_PASSWORD_MSG` | Message when using `$HASHED_PASSWORD` env var | +| `--login-rate-limit-msg` | `CS_LOGIN_RATE_LIMIT_MSG` | Rate limiting error message | +| `--missing-password-msg` | `CS_MISSING_PASSWORD_MSG` | Empty password error message | +| `--incorrect-password-msg` | `CS_INCORRECT_PASSWORD_MSG` | Wrong password error message | \ No newline at end of file diff --git a/docs/install.md b/docs/install.md index e2dd905f9401..b2b57ce9fa3e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -287,6 +287,30 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ codercom/code-server:latest ``` +### Customizing the login page + +You can customize the login page by setting environment variables: + +```bash +# Example with login customization +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$PWD:/home/coder/project" \ + -e "CS_LOGIN_TITLE=My Development Environment" \ + -e "CS_LOGIN_ENV_PASSWORD_MSG=Password configured via environment variable" \ + -e "CS_PASSWORD_PLACEHOLDER=Enter your secure password" \ + -e "CS_SUBMIT_TEXT=ACCESS" \ + codercom/code-server:latest +``` + +Available customization environment variables: +- `CS_LOGIN_TITLE` - Custom login page title +- `CS_LOGIN_BELOW` - Custom text below the login title +- `CS_PASSWORD_PLACEHOLDER` - Custom password field placeholder +- `CS_SUBMIT_TEXT` - Custom submit button text +- `CS_LOGIN_PASSWORD_MSG` - Custom message for config file password +- `CS_LOGIN_ENV_PASSWORD_MSG` - Custom message when using `$PASSWORD` env var +- `CS_LOGIN_HASHED_PASSWORD_MSG` - Custom message when using `$HASHED_PASSWORD` env var + 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). diff --git a/src/node/cli.ts b/src/node/cli.ts index a29ec591e0a4..0bdd36fd93e0 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -92,6 +92,16 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { verbose?: boolean "app-name"?: string "welcome-text"?: string + "login-title"?: string + "login-below"?: string + "password-placeholder"?: string + "submit-text"?: string + "login-password-msg"?: string + "login-env-password-msg"?: string + "login-hashed-password-msg"?: string + "login-rate-limit-msg"?: string + "missing-password-msg"?: string + "incorrect-password-msg"?: string "abs-proxy-base-path"?: string /* Positional arguments. */ _?: string[] @@ -291,6 +301,46 @@ export const options: Options> = { short: "w", description: "Text to show on login page", }, + "login-title": { + type: "string", + description: "Custom login page title", + }, + "login-below": { + type: "string", + description: "Custom text to show below login title", + }, + "password-placeholder": { + type: "string", + description: "Custom placeholder text for password field", + }, + "submit-text": { + type: "string", + description: "Custom text for login submit button", + }, + "login-password-msg": { + type: "string", + description: "Custom message for config file password", + }, + "login-env-password-msg": { + type: "string", + description: "Custom message when password is set from $PASSWORD environment variable", + }, + "login-hashed-password-msg": { + type: "string", + description: "Custom message when password is set from $HASHED_PASSWORD environment variable", + }, + "login-rate-limit-msg": { + type: "string", + description: "Custom message for login rate limiting", + }, + "missing-password-msg": { + type: "string", + description: "Custom message for missing password error", + }, + "incorrect-password-msg": { + type: "string", + description: "Custom message for incorrect password error", + }, "abs-proxy-base-path": { type: "string", description: "The base path to prefix to all absproxy requests", @@ -593,6 +643,47 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config args["disable-proxy"] = true } + // Login message customization via environment variables + if (process.env.CS_LOGIN_TITLE) { + args["login-title"] = process.env.CS_LOGIN_TITLE + } + + if (process.env.CS_LOGIN_BELOW) { + args["login-below"] = process.env.CS_LOGIN_BELOW + } + + if (process.env.CS_PASSWORD_PLACEHOLDER) { + args["password-placeholder"] = process.env.CS_PASSWORD_PLACEHOLDER + } + + if (process.env.CS_SUBMIT_TEXT) { + args["submit-text"] = process.env.CS_SUBMIT_TEXT + } + + if (process.env.CS_LOGIN_PASSWORD_MSG) { + args["login-password-msg"] = process.env.CS_LOGIN_PASSWORD_MSG + } + + if (process.env.CS_LOGIN_ENV_PASSWORD_MSG) { + args["login-env-password-msg"] = process.env.CS_LOGIN_ENV_PASSWORD_MSG + } + + if (process.env.CS_LOGIN_HASHED_PASSWORD_MSG) { + args["login-hashed-password-msg"] = process.env.CS_LOGIN_HASHED_PASSWORD_MSG + } + + if (process.env.CS_LOGIN_RATE_LIMIT_MSG) { + args["login-rate-limit-msg"] = process.env.CS_LOGIN_RATE_LIMIT_MSG + } + + if (process.env.CS_MISSING_PASSWORD_MSG) { + args["missing-password-msg"] = process.env.CS_MISSING_PASSWORD_MSG + } + + if (process.env.CS_INCORRECT_PASSWORD_MSG) { + args["incorrect-password-msg"] = process.env.CS_INCORRECT_PASSWORD_MSG + } + const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD if (process.env.HASHED_PASSWORD) { args["hashed-password"] = process.env.HASHED_PASSWORD diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index 29d51a59d13b..bbc1b945131f 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -31,23 +31,32 @@ const getRoot = async (req: Request, error?: Error): Promise => { 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) - let passwordMsg = i18n.t("LOGIN_PASSWORD", { configFile: req.args.config }) + const welcomeText = escapeHtml(req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string)) + + // Determine password message with custom overrides + let passwordMsg = req.args["login-password-msg"] || i18n.t("LOGIN_PASSWORD", { configFile: req.args.config }) if (req.args.usingEnvPassword) { - passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD") + passwordMsg = req.args["login-env-password-msg"] || i18n.t("LOGIN_USING_ENV_PASSWORD") } else if (req.args.usingEnvHashedPassword) { - passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD") + passwordMsg = req.args["login-hashed-password-msg"] || i18n.t("LOGIN_USING_HASHED_PASSWORD") } + passwordMsg = escapeHtml(passwordMsg) + + // Get custom messages or fall back to i18n (with HTML escaping for security) + const loginTitle = escapeHtml(req.args["login-title"] || i18n.t("LOGIN_TITLE", { app: appName })) + const loginBelow = escapeHtml(req.args["login-below"] || i18n.t("LOGIN_BELOW")) + const passwordPlaceholder = escapeHtml(req.args["password-placeholder"] || i18n.t("PASSWORD_PLACEHOLDER")) + const submitText = escapeHtml(req.args["submit-text"] || 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 ? `
${escapeHtml(error.message)}
` : ""), ) } @@ -75,11 +84,11 @@ router.post<{}, string, { password?: string; base?: string } | undefined, { to?: try { // Check to see if they exceeded their login attempts if (!limiter.canTry()) { - throw new Error(i18n.t("LOGIN_RATE_LIMIT") as string) + throw new Error(req.args["login-rate-limit-msg"] || (i18n.t("LOGIN_RATE_LIMIT") as string)) } if (!password) { - throw new Error(i18n.t("MISS_PASSWORD") as string) + throw new Error(req.args["missing-password-msg"] || (i18n.t("MISS_PASSWORD") as string)) } const passwordMethod = getPasswordMethod(hashedPasswordFromArgs) @@ -113,7 +122,7 @@ router.post<{}, string, { password?: string; base?: string } | undefined, { to?: }), ) - throw new Error(i18n.t("INCORRECT_PASSWORD") as string) + throw new Error(req.args["incorrect-password-msg"] || (i18n.t("INCORRECT_PASSWORD") as string)) } catch (error: any) { const renderedHtml = await getRoot(req, error) res.send(renderedHtml) diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index d62edb840464..f0e426814651 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -49,6 +49,10 @@ describe("parser", () => { delete process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE delete process.env.VSCODE_PROXY_URI delete process.env.CS_DISABLE_PROXY + delete process.env.CS_LOGIN_TITLE + delete process.env.CS_LOGIN_ENV_PASSWORD_MSG + delete process.env.CS_PASSWORD_PLACEHOLDER + delete process.env.CS_SUBMIT_TEXT console.log = jest.fn() }) @@ -75,6 +79,10 @@ describe("parser", () => { "--verbose", ["--app-name", "custom instance name"], ["--welcome-text", "welcome to code"], + ["--login-title", "Custom Login Portal"], + ["--login-env-password-msg", "Password from environment"], + ["--password-placeholder", "Enter code"], + ["--submit-text", "ACCESS"], "2", ["--locale", "ja"], @@ -145,6 +153,10 @@ describe("parser", () => { verbose: true, "app-name": "custom instance name", "welcome-text": "welcome to code", + "login-title": "Custom Login Portal", + "login-env-password-msg": "Password from environment", + "password-placeholder": "Enter code", + "submit-text": "ACCESS", version: true, "bind-addr": "192.169.0.1:8080", "session-socket": "/tmp/override-code-server-ipc-socket", @@ -347,6 +359,24 @@ describe("parser", () => { }) }) + it("should use env var login customization", async () => { + process.env.CS_LOGIN_TITLE = "Custom Portal" + process.env.CS_LOGIN_ENV_PASSWORD_MSG = "Password from env" + process.env.CS_PASSWORD_PLACEHOLDER = "Enter code here" + process.env.CS_SUBMIT_TEXT = "ACCESS NOW" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "login-title": "Custom Portal", + "login-env-password-msg": "Password from env", + "password-placeholder": "Enter code here", + "submit-text": "ACCESS NOW", + }) + }) + it("should use env var github token", async () => { process.env.GITHUB_TOKEN = "ga-foo" const args = parse([]) diff --git a/test/unit/node/routes/login.test.ts b/test/unit/node/routes/login.test.ts index 2835bad82354..06c8e1b22ceb 100644 --- a/test/unit/node/routes/login.test.ts +++ b/test/unit/node/routes/login.test.ts @@ -146,5 +146,70 @@ describe("login", () => { expect(resp.status).toBe(200) expect(htmlContent).toContain(`欒迎ζ₯到 code-server`) }) + + it("should return custom login title", async () => { + process.env.PASSWORD = previousEnvPassword + const loginTitle = "Custom Access Portal" + const codeServer = await integration.setup([`--login-title=${loginTitle}`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(`${loginTitle}`) + }) + + it("should return custom password placeholder", async () => { + process.env.PASSWORD = previousEnvPassword + const placeholder = "Enter access code" + const codeServer = await integration.setup([`--password-placeholder=${placeholder}`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(`placeholder="${placeholder}"`) + }) + + it("should return custom submit button text", async () => { + process.env.PASSWORD = previousEnvPassword + const submitText = "ACCESS PORTAL" + const codeServer = await integration.setup([`--submit-text=${submitText}`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(`value="${submitText}"`) + }) + + it("should return custom env password message", async () => { + const envMessage = "Password configured via container environment" + const codeServer = await integration.setup([`--login-env-password-msg=${envMessage}`, `--password=test123`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(envMessage) + }) + + it("should escape HTML in custom messages", async () => { + process.env.PASSWORD = previousEnvPassword + const maliciousTitle = "" + const codeServer = await integration.setup([`--login-title=${maliciousTitle}`, `--password=test123`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain("<script>alert('xss')</script>") + expect(htmlContent).not.toContain("") + }) + + it("should return custom error messages", async () => { + const customMissingMsg = "Access code required" + const codeServer = await integration.setup([`--missing-password-msg=${customMissingMsg}`, `--password=test123`], "") + const resp = await codeServer.fetch("/login", { method: "POST" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(customMissingMsg) + }) }) }) From 2553f4548be94e602fa968c6c8256f43462d2834 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 17 Jun 2025 14:37:45 +0200 Subject: [PATCH 02/13] feat: replace individual UI flags with unified --custom-strings flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace non-scalable individual flags (--login-title, --login-below, etc.) with a unified --custom-strings flag that accepts JSON file paths or inline JSON for UI customization. This leverages the existing i18n system for better scalability and maintainability. Changes: - Add --custom-strings flag with JSON validation - Extend i18n system to merge custom strings with defaults - Remove newly-added individual login/UI flags - Deprecate legacy --app-name and --welcome-text flags - Update login route to use unified i18n system - Add comprehensive tests for new functionality - Update documentation with migration guide and examples πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/FAQ.md | 29 +---- docs/customization.md | 254 +++++++++++++++++++++---------------- src/node/cli.ts | 112 ++++------------ src/node/i18n/index.ts | 83 +++++++++--- src/node/main.ts | 12 ++ src/node/routes/login.ts | 24 ++-- test/unit/node/cli.test.ts | 45 +++---- 7 files changed, 287 insertions(+), 272 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 0f1e78f58bb6..1d97d8c2e21d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -83,37 +83,20 @@ The default location respects `$XDG_CONFIG_HOME`. ### Login page customization -You can customize the login page appearance using CLI flags or environment variables: +You can customize the login page using the `--custom-strings` flag: -**CLI flags:** ```bash -code-server --login-title "My Code Server" \ - --login-env-password-msg "Password set via environment" \ - --password-placeholder "Enter password" \ - --submit-text "LOGIN" +code-server --custom-strings '{"LOGIN_TITLE": "My Code Server", "WELCOME": "Welcome to my portal"}' ``` -**Environment variables:** +Or use a JSON file: ```bash -export CS_LOGIN_TITLE="My Code Server" -export CS_LOGIN_ENV_PASSWORD_MSG="Password set via environment" -export CS_PASSWORD_PLACEHOLDER="Enter password" -export CS_SUBMIT_TEXT="LOGIN" -code-server +code-server --custom-strings /path/to/custom-strings.json ``` -**Config file:** -```yaml -bind-addr: 127.0.0.1:8080 -auth: password -password: your-password -login-title: "My Code Server" -login-env-password-msg: "Password set via environment" -password-placeholder: "Enter password" -submit-text: "LOGIN" -``` +Legacy individual flags (`--app-name`, `--welcome-text`) are still supported but deprecated. -CLI flags take priority over environment variables, which take priority over config file settings. +For detailed customization options and examples, see the [customization guide](./customization.md). ## How do I make my keyboard shortcuts work? diff --git a/docs/customization.md b/docs/customization.md index 0dd8d9d4e1c0..55797e12ec50 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,68 +1,39 @@ # Login Page Customization -code-server allows you to customize the login page appearance and messages through CLI arguments, environment variables, or configuration files. - -## Available Customization Options - -### Branding and Appearance -- **Login Title**: Customize the main title on the login page -- **Welcome Text**: Set custom welcome message -- **App Name**: Change the application branding throughout the interface - -### Login Messages -- **Password Instructions**: Customize the message explaining where to find the password -- **Environment Password Message**: Custom message when password is set via `$PASSWORD` -- **Hashed Password Message**: Custom message when password is set via `$HASHED_PASSWORD` - -### Form Elements -- **Password Placeholder**: Custom placeholder text for the password field -- **Submit Button**: Custom text for the login button - -### Error Messages -- **Rate Limit Message**: Custom message when login attempts are rate limited -- **Missing Password**: Custom message for empty password submissions -- **Incorrect Password**: Custom message for wrong password attempts - -## Configuration Methods - -### CLI Arguments +code-server allows you to customize the login page appearance and messages through a unified `--custom-strings` flag or legacy CLI arguments. + +## Recommended Approach: Custom Strings + +The `--custom-strings` flag provides a scalable way to customize any UI text by leveraging the built-in internationalization system. + +### Using JSON File + +Create a JSON file with your customizations: + +```json +{ + "WELCOME": "Welcome to {{app}} Development Portal", + "LOGIN_TITLE": "{{app}} Secure Access", + "LOGIN_BELOW": "Please authenticate to continue", + "PASSWORD_PLACEHOLDER": "Enter your access code", + "SUBMIT": "AUTHENTICATE", + "LOGIN_PASSWORD": "Check the config file at {{configFile}} for the password.", + "LOGIN_USING_ENV_PASSWORD": "Access code provided via environment variable", + "LOGIN_USING_HASHED_PASSWORD": "Access code configured securely", + "LOGIN_RATE_LIMIT": "Too many attempts. Please wait before trying again.", + "MISS_PASSWORD": "Access code is required", + "INCORRECT_PASSWORD": "Invalid access code" +} +``` ```bash -code-server \ - --app-name "My Development Server" \ - --welcome-text "Welcome to the development environment" \ - --login-title "Secure Access Portal" \ - --login-below "Please authenticate to continue" \ - --password-placeholder "Enter your access code" \ - --submit-text "AUTHENTICATE" \ - --login-env-password-msg "Access code provided via environment variable" \ - --login-rate-limit-msg "Too many attempts. Please wait before trying again." \ - --missing-password-msg "Access code is required" \ - --incorrect-password-msg "Invalid access code" +code-server --custom-strings /path/to/custom-strings.json ``` -### Environment Variables - -Perfect for Docker deployments and containerized environments: +### Using Inline JSON ```bash -# Basic branding -export CS_APP_NAME="My Development Server" -export CS_WELCOME_TEXT="Welcome to the development environment" - -# Login page customization -export CS_LOGIN_TITLE="Secure Access Portal" -export CS_LOGIN_BELOW="Please authenticate to continue" -export CS_PASSWORD_PLACEHOLDER="Enter your access code" -export CS_SUBMIT_TEXT="AUTHENTICATE" - -# Message customization -export CS_LOGIN_ENV_PASSWORD_MSG="Access code provided via environment variable" -export CS_LOGIN_RATE_LIMIT_MSG="Too many attempts. Please wait before trying again." -export CS_MISSING_PASSWORD_MSG="Access code is required" -export CS_INCORRECT_PASSWORD_MSG="Invalid access code" - -code-server +code-server --custom-strings '{"WELCOME": "Welcome to My Dev Portal", "LOGIN_TITLE": "Development Access", "SUBMIT": "SIGN IN"}' ``` ### Configuration File @@ -73,75 +44,142 @@ Add to your `~/.config/code-server/config.yaml`: bind-addr: 127.0.0.1:8080 auth: password password: your-password - -# Branding -app-name: "My Development Server" -welcome-text: "Welcome to the development environment" - -# Login page -login-title: "Secure Access Portal" -login-below: "Please authenticate to continue" -password-placeholder: "Enter your access code" -submit-text: "AUTHENTICATE" - -# Messages -login-env-password-msg: "Access code provided via environment variable" -login-rate-limit-msg: "Too many attempts. Please wait before trying again." -missing-password-msg: "Access code is required" -incorrect-password-msg: "Invalid access code" +custom-strings: | + { + "WELCOME": "Welcome to {{app}} Development Portal", + "LOGIN_TITLE": "{{app}} Secure Access", + "PASSWORD_PLACEHOLDER": "Enter your access code", + "SUBMIT": "AUTHENTICATE" + } ``` +## Available Customization Keys + +| Key | Description | Default | Supports {{app}} placeholder | +|-----|-------------|---------|------------------------------| +| `WELCOME` | Welcome message on login page | "Welcome to {{app}}" | βœ… | +| `LOGIN_TITLE` | Main title on login page | "{{app}} login" | βœ… | +| `LOGIN_BELOW` | Text below the login title | "Please log in below." | ❌ | +| `PASSWORD_PLACEHOLDER` | Password field placeholder text | "PASSWORD" | ❌ | +| `SUBMIT` | Login button text | "SUBMIT" | ❌ | +| `LOGIN_PASSWORD` | Message for config file password | "Check the config file at {{configFile}} for the password." | ❌ | +| `LOGIN_USING_ENV_PASSWORD` | Message when using `$PASSWORD` env var | "Password was set from $PASSWORD." | ❌ | +| `LOGIN_USING_HASHED_PASSWORD` | Message when using `$HASHED_PASSWORD` env var | "Password was set from $HASHED_PASSWORD." | ❌ | +| `LOGIN_RATE_LIMIT` | Rate limiting error message | "Login rate limited!" | ❌ | +| `MISS_PASSWORD` | Empty password error message | "Missing password" | ❌ | +| `INCORRECT_PASSWORD` | Wrong password error message | "Incorrect password" | ❌ | + ## Docker Examples -### Basic Docker Deployment with Customization +### Basic Docker Deployment ```bash docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$PWD:/home/coder/project" \ - -e "CS_LOGIN_TITLE=Development Environment" \ - -e "CS_LOGIN_ENV_PASSWORD_MSG=Password configured in container environment" \ - -e "CS_PASSWORD_PLACEHOLDER=Enter development password" \ - -e "CS_SUBMIT_TEXT=ACCESS ENVIRONMENT" \ - codercom/code-server:latest + -v "$PWD/custom-strings.json:/custom-strings.json" \ + codercom/code-server:latest --custom-strings /custom-strings.json ``` -### Corporate Branding Example +### Corporate Branding with Inline JSON ```bash docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$PWD:/home/coder/project" \ - -e "CS_APP_NAME=ACME Corporation Dev Portal" \ - -e "CS_LOGIN_TITLE=ACME Development Portal" \ - -e "CS_LOGIN_BELOW=Enter your corporate credentials" \ - -e "CS_PASSWORD_PLACEHOLDER=Corporate Password" \ - -e "CS_SUBMIT_TEXT=SIGN IN" \ - -e "CS_LOGIN_ENV_PASSWORD_MSG=Password managed by IT department" \ - codercom/code-server:latest + codercom/code-server:latest --custom-strings '{ + "WELCOME": "Welcome to ACME Corporation Development Portal", + "LOGIN_TITLE": "ACME Dev Portal Access", + "LOGIN_BELOW": "Enter your corporate credentials", + "PASSWORD_PLACEHOLDER": "Corporate Password", + "SUBMIT": "SIGN IN", + "LOGIN_USING_ENV_PASSWORD": "Password managed by IT department" + }' +``` + +## Legacy Support (Deprecated) + +The following individual flags are still supported but deprecated. Use `--custom-strings` for new deployments: + +```bash +# Deprecated - use --custom-strings instead +code-server \ + --app-name "My Development Server" \ + --welcome-text "Welcome to the development environment" +``` + +These legacy flags will show deprecation warnings and may be removed in future versions. + +## Migration Guide + +### From Individual Flags to Custom Strings + +**Old approach:** +```bash +code-server \ + --app-name "Dev Portal" \ + --welcome-text "Welcome to development" \ + --login-title "Portal Access" +``` + +**New approach:** +```bash +code-server --custom-strings '{ + "WELCOME": "Welcome to development", + "LOGIN_TITLE": "Portal Access" +}' +``` + +**Note:** The `--app-name` flag controls the `{{app}}` placeholder in templates. Use it alongside `--custom-strings` or customize the full text without placeholders. + +### From Environment Variables + +**Old approach:** +```bash +export CS_LOGIN_TITLE="Portal Access" +export CS_WELCOME_TEXT="Welcome message" +code-server +``` + +**New approach:** +```bash +echo '{"LOGIN_TITLE": "Portal Access", "WELCOME": "Welcome message"}' > strings.json +code-server --custom-strings strings.json ``` -## Priority Order +## Benefits of Custom Strings + +- βœ… **Scalable**: Add any new UI strings without new CLI flags +- βœ… **Flexible**: Supports both files and inline JSON +- βœ… **Future-proof**: Automatically supports new UI strings as they're added +- βœ… **Organized**: All customizations in one place +- βœ… **Version-controlled**: JSON files can be tracked in your repository -Settings are applied in the following priority order (highest to lowest): +## Advanced Usage -1. **CLI arguments** - Highest priority -2. **Environment variables** - Medium priority -3. **Config file** - Lowest priority +### Multi-language Support -This allows you to set defaults in your config file and override them with environment variables or CLI arguments as needed. +Create different JSON files for different languages: -## Complete Reference +```bash +# English +code-server --custom-strings /config/strings-en.json + +# Spanish +code-server --custom-strings /config/strings-es.json --locale es +``` -| CLI Argument | Environment Variable | Description | -|--------------|---------------------|-------------| -| `--app-name` | `CS_APP_NAME` | Application name used throughout the interface | -| `--welcome-text` | `CS_WELCOME_TEXT` | Welcome message on login page | -| `--login-title` | `CS_LOGIN_TITLE` | Main title on login page | -| `--login-below` | `CS_LOGIN_BELOW` | Text below the login title | -| `--password-placeholder` | `CS_PASSWORD_PLACEHOLDER` | Password field placeholder text | -| `--submit-text` | `CS_SUBMIT_TEXT` | Login button text | -| `--login-password-msg` | `CS_LOGIN_PASSWORD_MSG` | Message for config file password | -| `--login-env-password-msg` | `CS_LOGIN_ENV_PASSWORD_MSG` | Message when using `$PASSWORD` env var | -| `--login-hashed-password-msg` | `CS_LOGIN_HASHED_PASSWORD_MSG` | Message when using `$HASHED_PASSWORD` env var | -| `--login-rate-limit-msg` | `CS_LOGIN_RATE_LIMIT_MSG` | Rate limiting error message | -| `--missing-password-msg` | `CS_MISSING_PASSWORD_MSG` | Empty password error message | -| `--incorrect-password-msg` | `CS_INCORRECT_PASSWORD_MSG` | Wrong password error message | \ No newline at end of file +### Dynamic Customization + +Generate JSON dynamically in scripts: + +```bash +#!/bin/bash +COMPANY_NAME="ACME Corp" +cat > /tmp/strings.json << EOF +{ + "WELCOME": "Welcome to ${COMPANY_NAME} Development Portal", + "LOGIN_TITLE": "${COMPANY_NAME} Access Portal" +} +EOF + +code-server --custom-strings /tmp/strings.json +``` \ No newline at end of file diff --git a/src/node/cli.ts b/src/node/cli.ts index 0bdd36fd93e0..132f29dc7a2e 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -92,17 +92,8 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { verbose?: boolean "app-name"?: string "welcome-text"?: string - "login-title"?: string - "login-below"?: string - "password-placeholder"?: string - "submit-text"?: string - "login-password-msg"?: string - "login-env-password-msg"?: string - "login-hashed-password-msg"?: string - "login-rate-limit-msg"?: string - "missing-password-msg"?: string - "incorrect-password-msg"?: string "abs-proxy-base-path"?: string + "custom-strings"?: string /* Positional arguments. */ _?: string[] } @@ -295,56 +286,22 @@ export const options: Options> = { type: "string", short: "an", description: "The name to use in branding. Will be shown in titlebar and welcome message", + deprecated: true, }, "welcome-text": { type: "string", short: "w", description: "Text to show on login page", - }, - "login-title": { - type: "string", - description: "Custom login page title", - }, - "login-below": { - type: "string", - description: "Custom text to show below login title", - }, - "password-placeholder": { - type: "string", - description: "Custom placeholder text for password field", - }, - "submit-text": { - type: "string", - description: "Custom text for login submit button", - }, - "login-password-msg": { - type: "string", - description: "Custom message for config file password", - }, - "login-env-password-msg": { - type: "string", - description: "Custom message when password is set from $PASSWORD environment variable", - }, - "login-hashed-password-msg": { - type: "string", - description: "Custom message when password is set from $HASHED_PASSWORD environment variable", - }, - "login-rate-limit-msg": { - type: "string", - description: "Custom message for login rate limiting", - }, - "missing-password-msg": { - type: "string", - description: "Custom message for missing password error", - }, - "incorrect-password-msg": { - type: "string", - description: "Custom message for incorrect password error", + deprecated: true, }, "abs-proxy-base-path": { type: "string", description: "The base path to prefix to all absproxy requests", }, + "custom-strings": { + type: "string", + description: "Path to JSON file or raw JSON string with custom UI strings. Merges with default strings and supports all i18n keys.", + }, } export const optionDescriptions = (opts: Partial>> = options): string[] => { @@ -509,6 +466,21 @@ export const parse = ( throw new Error("--cert-key is missing") } + // Validate custom-strings flag + if (args["custom-strings"]) { + try { + // First try to parse as JSON directly + JSON.parse(args["custom-strings"]) + } catch (jsonError) { + // If JSON parsing fails, check if it's a valid file path + if (!args["custom-strings"].startsWith("{") && !args["custom-strings"].startsWith("[")) { + // Assume it's a file path - validation will happen later when the file is read + } else { + throw error(`--custom-strings contains invalid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`) + } + } + } + logger.debug(() => [`parsed ${opts?.configFile ? "config" : "command line"}`, field("args", redactArgs(args))]) return args @@ -643,46 +615,6 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config args["disable-proxy"] = true } - // Login message customization via environment variables - if (process.env.CS_LOGIN_TITLE) { - args["login-title"] = process.env.CS_LOGIN_TITLE - } - - if (process.env.CS_LOGIN_BELOW) { - args["login-below"] = process.env.CS_LOGIN_BELOW - } - - if (process.env.CS_PASSWORD_PLACEHOLDER) { - args["password-placeholder"] = process.env.CS_PASSWORD_PLACEHOLDER - } - - if (process.env.CS_SUBMIT_TEXT) { - args["submit-text"] = process.env.CS_SUBMIT_TEXT - } - - if (process.env.CS_LOGIN_PASSWORD_MSG) { - args["login-password-msg"] = process.env.CS_LOGIN_PASSWORD_MSG - } - - if (process.env.CS_LOGIN_ENV_PASSWORD_MSG) { - args["login-env-password-msg"] = process.env.CS_LOGIN_ENV_PASSWORD_MSG - } - - if (process.env.CS_LOGIN_HASHED_PASSWORD_MSG) { - args["login-hashed-password-msg"] = process.env.CS_LOGIN_HASHED_PASSWORD_MSG - } - - if (process.env.CS_LOGIN_RATE_LIMIT_MSG) { - args["login-rate-limit-msg"] = process.env.CS_LOGIN_RATE_LIMIT_MSG - } - - if (process.env.CS_MISSING_PASSWORD_MSG) { - args["missing-password-msg"] = process.env.CS_MISSING_PASSWORD_MSG - } - - if (process.env.CS_INCORRECT_PASSWORD_MSG) { - args["incorrect-password-msg"] = process.env.CS_INCORRECT_PASSWORD_MSG - } const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD if (process.env.HASHED_PASSWORD) { diff --git a/src/node/i18n/index.ts b/src/node/i18n/index.ts index 4ee718e13aa2..c78fcbbf4188 100644 --- a/src/node/i18n/index.ts +++ b/src/node/i18n/index.ts @@ -1,33 +1,82 @@ 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, + }, +} + +let customStrings: Record = {} + +export async function loadCustomStrings(customStringsArg?: string): Promise { + if (!customStringsArg) { + return + } + + try { + let customStringsData: Record + + // 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) + } + + customStrings = customStringsData + + // Re-initialize i18next with merged resources + const mergedResources = Object.keys(defaultResources).reduce((acc, lang) => { + const langKey = lang as keyof typeof defaultResources + acc[langKey] = { + translation: { + ...defaultResources[langKey].translation, + ...customStrings, + }, + } + return acc + }, {} as typeof defaultResources) + + await i18next.init({ + lng: "en", + fallbackLng: "en", + returnNull: false, + lowerCaseLng: true, + debug: process.env.NODE_ENV === "development", + resources: mergedResources, + }) + } 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 diff --git a/src/node/main.ts b/src/node/main.ts index 470ddeb25cc7..2969476fb6d8 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -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,17 @@ export const runCodeServer = async ( ): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => { logger.info(`code-server ${version} ${commit}`) + // Load custom strings if provided + if (args["custom-strings"]) { + try { + await loadCustomStrings(args["custom-strings"]) + logger.info("Loaded custom strings") + } catch (error) { + logger.error("Failed to load custom strings", field("error", error)) + throw error + } + } + logger.info(`Using user-data-dir ${args["user-data-dir"]}`) logger.debug(`Using extensions-dir ${args["extensions-dir"]}`) diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index bbc1b945131f..511d4817455e 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -33,20 +33,20 @@ const getRoot = async (req: Request, error?: Error): Promise => { const appName = req.args["app-name"] || "code-server" const welcomeText = escapeHtml(req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string)) - // Determine password message with custom overrides - let passwordMsg = req.args["login-password-msg"] || i18n.t("LOGIN_PASSWORD", { configFile: req.args.config }) + // Determine password message using i18n + let passwordMsg = i18n.t("LOGIN_PASSWORD", { configFile: req.args.config }) if (req.args.usingEnvPassword) { - passwordMsg = req.args["login-env-password-msg"] || i18n.t("LOGIN_USING_ENV_PASSWORD") + passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD") } else if (req.args.usingEnvHashedPassword) { - passwordMsg = req.args["login-hashed-password-msg"] || i18n.t("LOGIN_USING_HASHED_PASSWORD") + passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD") } passwordMsg = escapeHtml(passwordMsg) - // Get custom messages or fall back to i18n (with HTML escaping for security) - const loginTitle = escapeHtml(req.args["login-title"] || i18n.t("LOGIN_TITLE", { app: appName })) - const loginBelow = escapeHtml(req.args["login-below"] || i18n.t("LOGIN_BELOW")) - const passwordPlaceholder = escapeHtml(req.args["password-placeholder"] || i18n.t("PASSWORD_PLACEHOLDER")) - const submitText = escapeHtml(req.args["submit-text"] || i18n.t("SUBMIT")) + // 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, @@ -84,11 +84,11 @@ router.post<{}, string, { password?: string; base?: string } | undefined, { to?: try { // Check to see if they exceeded their login attempts if (!limiter.canTry()) { - throw new Error(req.args["login-rate-limit-msg"] || (i18n.t("LOGIN_RATE_LIMIT") as string)) + throw new Error(i18n.t("LOGIN_RATE_LIMIT") as string) } if (!password) { - throw new Error(req.args["missing-password-msg"] || (i18n.t("MISS_PASSWORD") as string)) + throw new Error(i18n.t("MISS_PASSWORD") as string) } const passwordMethod = getPasswordMethod(hashedPasswordFromArgs) @@ -122,7 +122,7 @@ router.post<{}, string, { password?: string; base?: string } | undefined, { to?: }), ) - throw new Error(req.args["incorrect-password-msg"] || (i18n.t("INCORRECT_PASSWORD") as string)) + throw new Error(i18n.t("INCORRECT_PASSWORD") as string) } catch (error: any) { const renderedHtml = await getRoot(req, error) res.send(renderedHtml) diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index f0e426814651..c62ec563a8fc 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -79,10 +79,7 @@ describe("parser", () => { "--verbose", ["--app-name", "custom instance name"], ["--welcome-text", "welcome to code"], - ["--login-title", "Custom Login Portal"], - ["--login-env-password-msg", "Password from environment"], - ["--password-placeholder", "Enter code"], - ["--submit-text", "ACCESS"], + ["--custom-strings", '{"LOGIN_TITLE": "Custom Portal"}'], "2", ["--locale", "ja"], @@ -153,10 +150,7 @@ describe("parser", () => { verbose: true, "app-name": "custom instance name", "welcome-text": "welcome to code", - "login-title": "Custom Login Portal", - "login-env-password-msg": "Password from environment", - "password-placeholder": "Enter code", - "submit-text": "ACCESS", + "custom-strings": '{"LOGIN_TITLE": "Custom Portal"}', version: true, "bind-addr": "192.169.0.1:8080", "session-socket": "/tmp/override-code-server-ipc-socket", @@ -359,21 +353,28 @@ describe("parser", () => { }) }) - it("should use env var login customization", async () => { - process.env.CS_LOGIN_TITLE = "Custom Portal" - process.env.CS_LOGIN_ENV_PASSWORD_MSG = "Password from env" - process.env.CS_PASSWORD_PLACEHOLDER = "Enter code here" - process.env.CS_SUBMIT_TEXT = "ACCESS NOW" - const args = parse([]) - expect(args).toEqual({}) + it("should parse custom-strings flag", async () => { + // Test with JSON string + const jsonString = '{"WELCOME": "Custom Welcome", "LOGIN_TITLE": "My App"}' + const args = parse(["--custom-strings", jsonString]) + expect(args).toEqual({ + "custom-strings": jsonString, + }) + }) - const defaultArgs = await setDefaults(args) - expect(defaultArgs).toEqual({ - ...defaults, - "login-title": "Custom Portal", - "login-env-password-msg": "Password from env", - "password-placeholder": "Enter code here", - "submit-text": "ACCESS NOW", + it("should validate custom-strings JSON", async () => { + // Test with invalid JSON + expect(() => parse(["--custom-strings", '{"invalid": json}'])).toThrowError(/contains invalid JSON/) + + // Test with valid JSON that looks like a file path + expect(() => parse(["--custom-strings", "/path/to/file.json"])).not.toThrow() + }) + + it("should support deprecated app-name and welcome-text flags", async () => { + const args = parse(["--app-name", "My App", "--welcome-text", "Welcome!"]) + expect(args).toEqual({ + "app-name": "My App", + "welcome-text": "Welcome!", }) }) From c2eb61d5f3e2f53e59df8094ea15d6b0a3c35e4c Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 17 Jun 2025 14:40:28 +0200 Subject: [PATCH 03/13] docs: simplify migration guide to only cover released flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove migration examples for flags that were never released. Only --app-name and --welcome-text were in the original codebase and might be used by existing users. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/customization.md | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 55797e12ec50..4a9f7998de4b 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -97,7 +97,7 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ ## Legacy Support (Deprecated) -The following individual flags are still supported but deprecated. Use `--custom-strings` for new deployments: +The following flags are still supported but deprecated. Use `--custom-strings` for new deployments: ```bash # Deprecated - use --custom-strings instead @@ -110,40 +110,25 @@ These legacy flags will show deprecation warnings and may be removed in future v ## Migration Guide -### From Individual Flags to Custom Strings +### From Legacy Flags to Custom Strings **Old approach:** ```bash code-server \ --app-name "Dev Portal" \ - --welcome-text "Welcome to development" \ - --login-title "Portal Access" + --welcome-text "Welcome to development" ``` **New approach:** ```bash code-server --custom-strings '{ - "WELCOME": "Welcome to development", - "LOGIN_TITLE": "Portal Access" + "WELCOME": "Welcome to development" }' ``` -**Note:** The `--app-name` flag controls the `{{app}}` placeholder in templates. Use it alongside `--custom-strings` or customize the full text without placeholders. - -### From Environment Variables - -**Old approach:** -```bash -export CS_LOGIN_TITLE="Portal Access" -export CS_WELCOME_TEXT="Welcome message" -code-server -``` - -**New approach:** -```bash -echo '{"LOGIN_TITLE": "Portal Access", "WELCOME": "Welcome message"}' > strings.json -code-server --custom-strings strings.json -``` +**Note:** The `--app-name` flag controls the `{{app}}` placeholder in templates. You can either: +1. Keep using `--app-name` alongside `--custom-strings` +2. Customize the full text without placeholders in your JSON ## Benefits of Custom Strings From c0189ed3faa9e05e18cb9dd792d5f6a41d10b7a0 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 17 Jun 2025 14:42:48 +0200 Subject: [PATCH 04/13] docs: update Docker customization examples to use --custom-strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace outdated CS_* environment variable examples with the new --custom-strings flag approach. Include both inline JSON and mounted file examples for Docker users. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/install.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/install.md b/docs/install.md index b2b57ce9fa3e..2cda3583605f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -289,27 +289,31 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ ### Customizing the login page -You can customize the login page by setting environment variables: +You can customize the login page using the `--custom-strings` flag: ```bash -# Example with login customization +# Example with inline JSON customization docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$PWD:/home/coder/project" \ - -e "CS_LOGIN_TITLE=My Development Environment" \ - -e "CS_LOGIN_ENV_PASSWORD_MSG=Password configured via environment variable" \ - -e "CS_PASSWORD_PLACEHOLDER=Enter your secure password" \ - -e "CS_SUBMIT_TEXT=ACCESS" \ - codercom/code-server:latest + codercom/code-server:latest --custom-strings '{ + "LOGIN_TITLE": "My Development Environment", + "WELCOME": "Welcome to your coding workspace", + "PASSWORD_PLACEHOLDER": "Enter your secure password", + "SUBMIT": "ACCESS" + }' +``` + +Or mount a JSON file: + +```bash +# Example with JSON file +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$PWD:/home/coder/project" \ + -v "$PWD/custom-strings.json:/config/strings.json" \ + codercom/code-server:latest --custom-strings /config/strings.json ``` -Available customization environment variables: -- `CS_LOGIN_TITLE` - Custom login page title -- `CS_LOGIN_BELOW` - Custom text below the login title -- `CS_PASSWORD_PLACEHOLDER` - Custom password field placeholder -- `CS_SUBMIT_TEXT` - Custom submit button text -- `CS_LOGIN_PASSWORD_MSG` - Custom message for config file password -- `CS_LOGIN_ENV_PASSWORD_MSG` - Custom message when using `$PASSWORD` env var -- `CS_LOGIN_HASHED_PASSWORD_MSG` - Custom message when using `$HASHED_PASSWORD` env var +For detailed customization options, see the [customization guide](./customization.md). Our official image supports `amd64` and `arm64`. For `arm32` support, you can use a [community-maintained code-server From 2b2819e3887863f79044aca24dc84d427a3ab051 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 19:09:51 +0200 Subject: [PATCH 05/13] docs: remove niche customization sections from FAQ and install guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove login page customization sections from FAQ.md and install.md as they were too specific for those documents. Move customization reference to guide.md where it's more appropriate. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/FAQ.md | 16 ---------------- docs/README.md | 1 - docs/guide.md | 4 ++++ docs/install.md | 27 --------------------------- 4 files changed, 4 insertions(+), 44 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 1d97d8c2e21d..cba4a400f97a 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -81,22 +81,6 @@ You can change the config file's location using the `--config` flag or The default location respects `$XDG_CONFIG_HOME`. -### Login page customization - -You can customize the login page using the `--custom-strings` flag: - -```bash -code-server --custom-strings '{"LOGIN_TITLE": "My Code Server", "WELCOME": "Welcome to my portal"}' -``` - -Or use a JSON file: -```bash -code-server --custom-strings /path/to/custom-strings.json -``` - -Legacy individual flags (`--app-name`, `--welcome-text`) are still supported but deprecated. - -For detailed customization options and examples, see the [customization guide](./customization.md). ## How do I make my keyboard shortcuts work? diff --git a/docs/README.md b/docs/README.md index e80b7bfb23be..d607ba5ab232 100644 --- a/docs/README.md +++ b/docs/README.md @@ -61,7 +61,6 @@ code-server. We also have an in-depth [setup and configuration](https://coder.com/docs/code-server/latest/guide) guide. -You can also customize the login page appearance - see our [customization guide](./customization.md). ## Questions? diff --git a/docs/guide.md b/docs/guide.md index 2835aac1567c..14e6ed9bf9d5 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -458,3 +458,7 @@ 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 + +You can customize some of code-server's strings for either internationalization or customization purposes. See our [customization guide](./customization.md). diff --git a/docs/install.md b/docs/install.md index 2cda3583605f..c60127a1022f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -287,33 +287,6 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ codercom/code-server:latest ``` -### Customizing the login page - -You can customize the login page using the `--custom-strings` flag: - -```bash -# Example with inline JSON customization -docker run -it --name code-server -p 127.0.0.1:8080:8080 \ - -v "$PWD:/home/coder/project" \ - codercom/code-server:latest --custom-strings '{ - "LOGIN_TITLE": "My Development Environment", - "WELCOME": "Welcome to your coding workspace", - "PASSWORD_PLACEHOLDER": "Enter your secure password", - "SUBMIT": "ACCESS" - }' -``` - -Or mount a JSON file: - -```bash -# Example with JSON file -docker run -it --name code-server -p 127.0.0.1:8080:8080 \ - -v "$PWD:/home/coder/project" \ - -v "$PWD/custom-strings.json:/config/strings.json" \ - codercom/code-server:latest --custom-strings /config/strings.json -``` - -For detailed customization options, see the [customization guide](./customization.md). Our official image supports `amd64` and `arm64`. For `arm32` support, you can use a [community-maintained code-server From b275d5281093da6a7a2e04bf5cc8a00df033a992 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 19:18:08 +0200 Subject: [PATCH 06/13] refactor: remove redundant custom-strings validation and global variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove duplicate validation in cli.ts since comprehensive validation already exists in loadCustomStrings(). Also eliminate unnecessary customStrings global variable by using customStringsData directly. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/node/cli.ts | 14 -------------- src/node/i18n/index.ts | 5 +---- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 132f29dc7a2e..12d9e5addfea 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -466,20 +466,6 @@ export const parse = ( throw new Error("--cert-key is missing") } - // Validate custom-strings flag - if (args["custom-strings"]) { - try { - // First try to parse as JSON directly - JSON.parse(args["custom-strings"]) - } catch (jsonError) { - // If JSON parsing fails, check if it's a valid file path - if (!args["custom-strings"].startsWith("{") && !args["custom-strings"].startsWith("[")) { - // Assume it's a file path - validation will happen later when the file is read - } else { - throw error(`--custom-strings contains invalid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`) - } - } - } logger.debug(() => [`parsed ${opts?.configFile ? "config" : "command line"}`, field("args", redactArgs(args))]) diff --git a/src/node/i18n/index.ts b/src/node/i18n/index.ts index c78fcbbf4188..a2941270faf5 100644 --- a/src/node/i18n/index.ts +++ b/src/node/i18n/index.ts @@ -24,7 +24,6 @@ const defaultResources = { }, } -let customStrings: Record = {} export async function loadCustomStrings(customStringsArg?: string): Promise { if (!customStringsArg) { @@ -43,15 +42,13 @@ export async function loadCustomStrings(customStringsArg?: string): Promise { const langKey = lang as keyof typeof defaultResources acc[langKey] = { translation: { ...defaultResources[langKey].translation, - ...customStrings, + ...customStringsData, }, } return acc From af2f599fb040cbf3ed6c7c654d689beeadc74351 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 19:20:40 +0200 Subject: [PATCH 07/13] refactor: simplify loadCustomStrings function signature and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make customStringsArg parameter required since caller already checks existence. Remove redundant try-catch block in main.ts to avoid duplicate error logging since loadCustomStrings already provides descriptive error messages. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/node/i18n/index.ts | 5 +---- src/node/main.ts | 9 ++------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/node/i18n/index.ts b/src/node/i18n/index.ts index a2941270faf5..916ec895a98d 100644 --- a/src/node/i18n/index.ts +++ b/src/node/i18n/index.ts @@ -25,10 +25,7 @@ const defaultResources = { } -export async function loadCustomStrings(customStringsArg?: string): Promise { - if (!customStringsArg) { - return - } +export async function loadCustomStrings(customStringsArg: string): Promise { try { let customStringsData: Record diff --git a/src/node/main.ts b/src/node/main.ts index 2969476fb6d8..0f590f9a9ed0 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -125,13 +125,8 @@ export const runCodeServer = async ( // Load custom strings if provided if (args["custom-strings"]) { - try { - await loadCustomStrings(args["custom-strings"]) - logger.info("Loaded custom strings") - } catch (error) { - logger.error("Failed to load custom strings", field("error", error)) - throw error - } + await loadCustomStrings(args["custom-strings"]) + logger.info("Loaded custom strings") } logger.info(`Using user-data-dir ${args["user-data-dir"]}`) From e89a9affa7475af4bfbe817bad82edf1d871671b Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 19:25:59 +0200 Subject: [PATCH 08/13] test: remove outdated tests for deprecated custom UI flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove tests for legacy individual flags (--login-title, --password-placeholder, --submit-text, etc.) that have been replaced by the unified --custom-strings flag. Also remove cleanup of non-existent environment variables in CLI tests. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- test/unit/node/cli.test.ts | 4 -- test/unit/node/routes/login.test.ts | 64 ----------------------------- 2 files changed, 68 deletions(-) diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index c62ec563a8fc..f5c6174a2de6 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -49,10 +49,6 @@ describe("parser", () => { delete process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE delete process.env.VSCODE_PROXY_URI delete process.env.CS_DISABLE_PROXY - delete process.env.CS_LOGIN_TITLE - delete process.env.CS_LOGIN_ENV_PASSWORD_MSG - delete process.env.CS_PASSWORD_PLACEHOLDER - delete process.env.CS_SUBMIT_TEXT console.log = jest.fn() }) diff --git a/test/unit/node/routes/login.test.ts b/test/unit/node/routes/login.test.ts index 06c8e1b22ceb..e947b9e41a36 100644 --- a/test/unit/node/routes/login.test.ts +++ b/test/unit/node/routes/login.test.ts @@ -147,69 +147,5 @@ describe("login", () => { expect(htmlContent).toContain(`欒迎ζ₯到 code-server`) }) - it("should return custom login title", async () => { - process.env.PASSWORD = previousEnvPassword - const loginTitle = "Custom Access Portal" - const codeServer = await integration.setup([`--login-title=${loginTitle}`], "") - const resp = await codeServer.fetch("/login", { method: "GET" }) - - const htmlContent = await resp.text() - expect(resp.status).toBe(200) - expect(htmlContent).toContain(`${loginTitle}`) - }) - - it("should return custom password placeholder", async () => { - process.env.PASSWORD = previousEnvPassword - const placeholder = "Enter access code" - const codeServer = await integration.setup([`--password-placeholder=${placeholder}`], "") - const resp = await codeServer.fetch("/login", { method: "GET" }) - - const htmlContent = await resp.text() - expect(resp.status).toBe(200) - expect(htmlContent).toContain(`placeholder="${placeholder}"`) - }) - - it("should return custom submit button text", async () => { - process.env.PASSWORD = previousEnvPassword - const submitText = "ACCESS PORTAL" - const codeServer = await integration.setup([`--submit-text=${submitText}`], "") - const resp = await codeServer.fetch("/login", { method: "GET" }) - - const htmlContent = await resp.text() - expect(resp.status).toBe(200) - expect(htmlContent).toContain(`value="${submitText}"`) - }) - - it("should return custom env password message", async () => { - const envMessage = "Password configured via container environment" - const codeServer = await integration.setup([`--login-env-password-msg=${envMessage}`, `--password=test123`], "") - const resp = await codeServer.fetch("/login", { method: "GET" }) - - const htmlContent = await resp.text() - expect(resp.status).toBe(200) - expect(htmlContent).toContain(envMessage) - }) - - it("should escape HTML in custom messages", async () => { - process.env.PASSWORD = previousEnvPassword - const maliciousTitle = "" - const codeServer = await integration.setup([`--login-title=${maliciousTitle}`, `--password=test123`], "") - const resp = await codeServer.fetch("/login", { method: "GET" }) - - const htmlContent = await resp.text() - expect(resp.status).toBe(200) - expect(htmlContent).toContain("<script>alert('xss')</script>") - expect(htmlContent).not.toContain("") - }) - - it("should return custom error messages", async () => { - const customMissingMsg = "Access code required" - const codeServer = await integration.setup([`--missing-password-msg=${customMissingMsg}`, `--password=test123`], "") - const resp = await codeServer.fetch("/login", { method: "POST" }) - - const htmlContent = await resp.text() - expect(resp.status).toBe(200) - expect(htmlContent).toContain(customMissingMsg) - }) }) }) From a8b7fbe07fb6c1f75cf9694b3ac8e952f5c5d8be Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 19:28:28 +0200 Subject: [PATCH 09/13] refactor: use addResourceBundle instead of re-initializing i18next MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace manual resource merging and re-initialization with i18next's built-in addResourceBundle API. This is more efficient, cleaner, and the idiomatic way to add custom translations dynamically. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/node/i18n/index.ts | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/node/i18n/index.ts b/src/node/i18n/index.ts index 916ec895a98d..69807247ddae 100644 --- a/src/node/i18n/index.ts +++ b/src/node/i18n/index.ts @@ -39,25 +39,9 @@ export async function loadCustomStrings(customStringsArg: string): Promise customStringsData = JSON.parse(fileContent) } - // Re-initialize i18next with merged resources - const mergedResources = Object.keys(defaultResources).reduce((acc, lang) => { - const langKey = lang as keyof typeof defaultResources - acc[langKey] = { - translation: { - ...defaultResources[langKey].translation, - ...customStringsData, - }, - } - return acc - }, {} as typeof defaultResources) - - await i18next.init({ - lng: "en", - fallbackLng: "en", - returnNull: false, - lowerCaseLng: true, - debug: process.env.NODE_ENV === "development", - resources: mergedResources, + // 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)}`) From 27ee7149f54a0212d87b19733c91c8e5e3f96f92 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 19:29:51 +0200 Subject: [PATCH 10/13] docs: improve custom-strings flag description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change "custom UI strings" to "custom translations" to better reflect that this feature uses the i18n system and could be used for more than just UI elements, making it more accurate and future-proof. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/node/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 12d9e5addfea..a81624e20f4c 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -300,7 +300,7 @@ export const options: Options> = { }, "custom-strings": { type: "string", - description: "Path to JSON file or raw JSON string with custom UI strings. Merges with default strings and supports all i18n keys.", + description: "Path to JSON file or raw JSON string with custom translations. Merges with default strings and supports all i18n keys.", }, } From d5c0c88b72f7ca5480072e82dc970c7cdf9454f4 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 19:34:03 +0200 Subject: [PATCH 11/13] refactor: keep --app-name flag as non-deprecated for {{app}} placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove deprecation from --app-name since it serves a valuable purpose for the {{app}} placeholder in custom strings, especially useful for internationalization. Update documentation to show how --app-name works with --custom-strings and clarify that only --welcome-text is deprecated. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/customization.md | 54 +++++++++++++++++++++++++------------- src/node/cli.ts | 3 +-- test/unit/node/cli.test.ts | 2 +- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 4a9f7998de4b..3fc8ce98fd95 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -95,40 +95,58 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ }' ``` -## Legacy Support (Deprecated) +## Using App Name with Custom Strings -The following flags are still supported but deprecated. Use `--custom-strings` for new deployments: +The `--app-name` flag works perfectly with `--custom-strings` to provide the `{{app}}` placeholder functionality: ```bash -# Deprecated - use --custom-strings instead code-server \ - --app-name "My Development Server" \ - --welcome-text "Welcome to the development environment" + --app-name "Dev Portal" \ + --custom-strings '{"WELCOME": "Welcome to {{app}} environment"}' ``` -These legacy flags will show deprecation warnings and may be removed in future versions. +This approach allows you to: +- Set a custom app name once with `--app-name` +- Use `{{app}}` placeholders in your custom strings +- Easily change the app name without updating all strings -## Migration Guide +### Examples with App Name -### From Legacy Flags to Custom Strings +**Corporate branding with dynamic app name:** +```bash +code-server \ + --app-name "ACME Development Platform" \ + --custom-strings '{ + "WELCOME": "Welcome to {{app}}", + "LOGIN_TITLE": "{{app}} Access Portal", + "LOGIN_BELOW": "Please authenticate to access {{app}}" + }' +``` -**Old approach:** +**Internationalization with app name:** ```bash code-server \ - --app-name "Dev Portal" \ - --welcome-text "Welcome to development" + --app-name "Mon Portail" \ + --custom-strings '{ + "WELCOME": "Bienvenue sur {{app}}", + "LOGIN_TITLE": "Connexion Γ  {{app}}", + "SUBMIT": "SE CONNECTER" + }' ``` -**New approach:** +## Legacy Flag Migration + +The `--welcome-text` flag is deprecated. Migrate to `--custom-strings`: + +**Old:** ```bash -code-server --custom-strings '{ - "WELCOME": "Welcome to development" -}' +code-server --welcome-text "Welcome to development" ``` -**Note:** The `--app-name` flag controls the `{{app}}` placeholder in templates. You can either: -1. Keep using `--app-name` alongside `--custom-strings` -2. Customize the full text without placeholders in your JSON +**New:** +```bash +code-server --custom-strings '{"WELCOME": "Welcome to development"}' +``` ## Benefits of Custom Strings diff --git a/src/node/cli.ts b/src/node/cli.ts index a81624e20f4c..a2edada4f925 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -285,8 +285,7 @@ export const options: Options> = { "app-name": { type: "string", short: "an", - description: "The name to use in branding. Will be shown in titlebar and welcome message", - deprecated: true, + description: "Will replace the {{app}} placeholder in any strings, which by default includes the title bar and welcome message", }, "welcome-text": { type: "string", diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index f5c6174a2de6..fea6ed3af6d0 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -366,7 +366,7 @@ describe("parser", () => { expect(() => parse(["--custom-strings", "/path/to/file.json"])).not.toThrow() }) - it("should support deprecated app-name and welcome-text flags", async () => { + 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", From fd6ca5181b08e58ca19c05a2a54f7d0e5af253fd Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 19:47:34 +0200 Subject: [PATCH 12/13] refactor: rename --custom-strings flag to --i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change CLI flag from --custom-strings to --i18n to use standard internationalization terminology. This is more accurate since the feature leverages the i18next system and follows industry conventions. Update all documentation, tests, and code references. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/customization.md | 36 ++++++++++++++++++------------------ src/node/cli.ts | 4 ++-- src/node/main.ts | 4 ++-- test/unit/node/cli.test.ts | 20 ++++++++++---------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 3fc8ce98fd95..01dbd856536e 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,10 +1,10 @@ # Login Page Customization -code-server allows you to customize the login page appearance and messages through a unified `--custom-strings` flag or legacy CLI arguments. +code-server allows you to customize the login page appearance and messages through the `--i18n` flag or legacy CLI arguments. -## Recommended Approach: Custom Strings +## Recommended Approach: Internationalization -The `--custom-strings` flag provides a scalable way to customize any UI text by leveraging the built-in internationalization system. +The `--i18n` flag provides a scalable way to customize any UI text by leveraging the built-in internationalization system. ### Using JSON File @@ -27,13 +27,13 @@ Create a JSON file with your customizations: ``` ```bash -code-server --custom-strings /path/to/custom-strings.json +code-server --i18n /path/to/custom-strings.json ``` ### Using Inline JSON ```bash -code-server --custom-strings '{"WELCOME": "Welcome to My Dev Portal", "LOGIN_TITLE": "Development Access", "SUBMIT": "SIGN IN"}' +code-server --i18n '{"WELCOME": "Welcome to My Dev Portal", "LOGIN_TITLE": "Development Access", "SUBMIT": "SIGN IN"}' ``` ### Configuration File @@ -44,7 +44,7 @@ Add to your `~/.config/code-server/config.yaml`: bind-addr: 127.0.0.1:8080 auth: password password: your-password -custom-strings: | +i18n: | { "WELCOME": "Welcome to {{app}} Development Portal", "LOGIN_TITLE": "{{app}} Secure Access", @@ -77,7 +77,7 @@ custom-strings: | docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$PWD:/home/coder/project" \ -v "$PWD/custom-strings.json:/custom-strings.json" \ - codercom/code-server:latest --custom-strings /custom-strings.json + codercom/code-server:latest --i18n /custom-strings.json ``` ### Corporate Branding with Inline JSON @@ -85,7 +85,7 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ ```bash docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$PWD:/home/coder/project" \ - codercom/code-server:latest --custom-strings '{ + codercom/code-server:latest --i18n '{ "WELCOME": "Welcome to ACME Corporation Development Portal", "LOGIN_TITLE": "ACME Dev Portal Access", "LOGIN_BELOW": "Enter your corporate credentials", @@ -95,14 +95,14 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ }' ``` -## Using App Name with Custom Strings +## Using App Name with Internationalization -The `--app-name` flag works perfectly with `--custom-strings` to provide the `{{app}}` placeholder functionality: +The `--app-name` flag works perfectly with `--i18n` to provide the `{{app}}` placeholder functionality: ```bash code-server \ --app-name "Dev Portal" \ - --custom-strings '{"WELCOME": "Welcome to {{app}} environment"}' + --i18n '{"WELCOME": "Welcome to {{app}} environment"}' ``` This approach allows you to: @@ -116,7 +116,7 @@ This approach allows you to: ```bash code-server \ --app-name "ACME Development Platform" \ - --custom-strings '{ + --i18n '{ "WELCOME": "Welcome to {{app}}", "LOGIN_TITLE": "{{app}} Access Portal", "LOGIN_BELOW": "Please authenticate to access {{app}}" @@ -127,7 +127,7 @@ code-server \ ```bash code-server \ --app-name "Mon Portail" \ - --custom-strings '{ + --i18n '{ "WELCOME": "Bienvenue sur {{app}}", "LOGIN_TITLE": "Connexion Γ  {{app}}", "SUBMIT": "SE CONNECTER" @@ -136,7 +136,7 @@ code-server \ ## Legacy Flag Migration -The `--welcome-text` flag is deprecated. Migrate to `--custom-strings`: +The `--welcome-text` flag is deprecated. Migrate to `--i18n`: **Old:** ```bash @@ -145,7 +145,7 @@ code-server --welcome-text "Welcome to development" **New:** ```bash -code-server --custom-strings '{"WELCOME": "Welcome to development"}' +code-server --i18n '{"WELCOME": "Welcome to development"}' ``` ## Benefits of Custom Strings @@ -164,10 +164,10 @@ Create different JSON files for different languages: ```bash # English -code-server --custom-strings /config/strings-en.json +code-server --i18n /config/strings-en.json # Spanish -code-server --custom-strings /config/strings-es.json --locale es +code-server --i18n /config/strings-es.json --locale es ``` ### Dynamic Customization @@ -184,5 +184,5 @@ cat > /tmp/strings.json << EOF } EOF -code-server --custom-strings /tmp/strings.json +code-server --i18n /tmp/strings.json ``` \ No newline at end of file diff --git a/src/node/cli.ts b/src/node/cli.ts index a2edada4f925..40b99c9e4321 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -93,7 +93,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { "app-name"?: string "welcome-text"?: string "abs-proxy-base-path"?: string - "custom-strings"?: string + i18n?: string /* Positional arguments. */ _?: string[] } @@ -297,7 +297,7 @@ export const options: Options> = { type: "string", description: "The base path to prefix to all absproxy requests", }, - "custom-strings": { + i18n: { type: "string", description: "Path to JSON file or raw JSON string with custom translations. Merges with default strings and supports all i18n keys.", }, diff --git a/src/node/main.ts b/src/node/main.ts index 0f590f9a9ed0..caff79535461 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -124,8 +124,8 @@ export const runCodeServer = async ( logger.info(`code-server ${version} ${commit}`) // Load custom strings if provided - if (args["custom-strings"]) { - await loadCustomStrings(args["custom-strings"]) + if (args.i18n) { + await loadCustomStrings(args.i18n) logger.info("Loaded custom strings") } diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index fea6ed3af6d0..44f75193478a 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -75,7 +75,7 @@ describe("parser", () => { "--verbose", ["--app-name", "custom instance name"], ["--welcome-text", "welcome to code"], - ["--custom-strings", '{"LOGIN_TITLE": "Custom Portal"}'], + ["--i18n", '{"LOGIN_TITLE": "Custom Portal"}'], "2", ["--locale", "ja"], @@ -146,7 +146,7 @@ describe("parser", () => { verbose: true, "app-name": "custom instance name", "welcome-text": "welcome to code", - "custom-strings": '{"LOGIN_TITLE": "Custom Portal"}', + i18n: '{"LOGIN_TITLE": "Custom Portal"}', version: true, "bind-addr": "192.169.0.1:8080", "session-socket": "/tmp/override-code-server-ipc-socket", @@ -349,21 +349,21 @@ describe("parser", () => { }) }) - it("should parse custom-strings flag", async () => { + it("should parse i18n flag", async () => { // Test with JSON string const jsonString = '{"WELCOME": "Custom Welcome", "LOGIN_TITLE": "My App"}' - const args = parse(["--custom-strings", jsonString]) + const args = parse(["--i18n", jsonString]) expect(args).toEqual({ - "custom-strings": jsonString, + i18n: jsonString, }) }) - it("should validate custom-strings JSON", async () => { - // Test with invalid JSON - expect(() => parse(["--custom-strings", '{"invalid": json}'])).toThrowError(/contains invalid JSON/) - + it("should parse i18n file paths and JSON", async () => { // Test with valid JSON that looks like a file path - expect(() => parse(["--custom-strings", "/path/to/file.json"])).not.toThrow() + 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 () => { From ca12a25f8496dae2dc7df99447daa49a62d2caa0 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 24 Jun 2025 22:18:35 +0200 Subject: [PATCH 13/13] docs: consolidate i18n documentation in guide.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace login-specific customization.md with generic internationalization section in guide.md. This approach is more maintainable, reduces file sprawl, points to source files as truth, and encourages community contributions. Removes repetitive examples that could get out of sync. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/customization.md | 188 ------------------------------------------ docs/guide.md | 33 +++++++- 2 files changed, 32 insertions(+), 189 deletions(-) delete mode 100644 docs/customization.md diff --git a/docs/customization.md b/docs/customization.md deleted file mode 100644 index 01dbd856536e..000000000000 --- a/docs/customization.md +++ /dev/null @@ -1,188 +0,0 @@ -# Login Page Customization - -code-server allows you to customize the login page appearance and messages through the `--i18n` flag or legacy CLI arguments. - -## Recommended Approach: Internationalization - -The `--i18n` flag provides a scalable way to customize any UI text by leveraging the built-in internationalization system. - -### Using JSON File - -Create a JSON file with your customizations: - -```json -{ - "WELCOME": "Welcome to {{app}} Development Portal", - "LOGIN_TITLE": "{{app}} Secure Access", - "LOGIN_BELOW": "Please authenticate to continue", - "PASSWORD_PLACEHOLDER": "Enter your access code", - "SUBMIT": "AUTHENTICATE", - "LOGIN_PASSWORD": "Check the config file at {{configFile}} for the password.", - "LOGIN_USING_ENV_PASSWORD": "Access code provided via environment variable", - "LOGIN_USING_HASHED_PASSWORD": "Access code configured securely", - "LOGIN_RATE_LIMIT": "Too many attempts. Please wait before trying again.", - "MISS_PASSWORD": "Access code is required", - "INCORRECT_PASSWORD": "Invalid access code" -} -``` - -```bash -code-server --i18n /path/to/custom-strings.json -``` - -### Using Inline JSON - -```bash -code-server --i18n '{"WELCOME": "Welcome to My Dev Portal", "LOGIN_TITLE": "Development Access", "SUBMIT": "SIGN IN"}' -``` - -### Configuration File - -Add to your `~/.config/code-server/config.yaml`: - -```yaml -bind-addr: 127.0.0.1:8080 -auth: password -password: your-password -i18n: | - { - "WELCOME": "Welcome to {{app}} Development Portal", - "LOGIN_TITLE": "{{app}} Secure Access", - "PASSWORD_PLACEHOLDER": "Enter your access code", - "SUBMIT": "AUTHENTICATE" - } -``` - -## Available Customization Keys - -| Key | Description | Default | Supports {{app}} placeholder | -|-----|-------------|---------|------------------------------| -| `WELCOME` | Welcome message on login page | "Welcome to {{app}}" | βœ… | -| `LOGIN_TITLE` | Main title on login page | "{{app}} login" | βœ… | -| `LOGIN_BELOW` | Text below the login title | "Please log in below." | ❌ | -| `PASSWORD_PLACEHOLDER` | Password field placeholder text | "PASSWORD" | ❌ | -| `SUBMIT` | Login button text | "SUBMIT" | ❌ | -| `LOGIN_PASSWORD` | Message for config file password | "Check the config file at {{configFile}} for the password." | ❌ | -| `LOGIN_USING_ENV_PASSWORD` | Message when using `$PASSWORD` env var | "Password was set from $PASSWORD." | ❌ | -| `LOGIN_USING_HASHED_PASSWORD` | Message when using `$HASHED_PASSWORD` env var | "Password was set from $HASHED_PASSWORD." | ❌ | -| `LOGIN_RATE_LIMIT` | Rate limiting error message | "Login rate limited!" | ❌ | -| `MISS_PASSWORD` | Empty password error message | "Missing password" | ❌ | -| `INCORRECT_PASSWORD` | Wrong password error message | "Incorrect password" | ❌ | - -## Docker Examples - -### Basic Docker Deployment - -```bash -docker run -it --name code-server -p 127.0.0.1:8080:8080 \ - -v "$PWD:/home/coder/project" \ - -v "$PWD/custom-strings.json:/custom-strings.json" \ - codercom/code-server:latest --i18n /custom-strings.json -``` - -### Corporate Branding with Inline JSON - -```bash -docker run -it --name code-server -p 127.0.0.1:8080:8080 \ - -v "$PWD:/home/coder/project" \ - codercom/code-server:latest --i18n '{ - "WELCOME": "Welcome to ACME Corporation Development Portal", - "LOGIN_TITLE": "ACME Dev Portal Access", - "LOGIN_BELOW": "Enter your corporate credentials", - "PASSWORD_PLACEHOLDER": "Corporate Password", - "SUBMIT": "SIGN IN", - "LOGIN_USING_ENV_PASSWORD": "Password managed by IT department" - }' -``` - -## Using App Name with Internationalization - -The `--app-name` flag works perfectly with `--i18n` to provide the `{{app}}` placeholder functionality: - -```bash -code-server \ - --app-name "Dev Portal" \ - --i18n '{"WELCOME": "Welcome to {{app}} environment"}' -``` - -This approach allows you to: -- Set a custom app name once with `--app-name` -- Use `{{app}}` placeholders in your custom strings -- Easily change the app name without updating all strings - -### Examples with App Name - -**Corporate branding with dynamic app name:** -```bash -code-server \ - --app-name "ACME Development Platform" \ - --i18n '{ - "WELCOME": "Welcome to {{app}}", - "LOGIN_TITLE": "{{app}} Access Portal", - "LOGIN_BELOW": "Please authenticate to access {{app}}" - }' -``` - -**Internationalization with app name:** -```bash -code-server \ - --app-name "Mon Portail" \ - --i18n '{ - "WELCOME": "Bienvenue sur {{app}}", - "LOGIN_TITLE": "Connexion Γ  {{app}}", - "SUBMIT": "SE CONNECTER" - }' -``` - -## Legacy Flag Migration - -The `--welcome-text` flag is deprecated. Migrate to `--i18n`: - -**Old:** -```bash -code-server --welcome-text "Welcome to development" -``` - -**New:** -```bash -code-server --i18n '{"WELCOME": "Welcome to development"}' -``` - -## Benefits of Custom Strings - -- βœ… **Scalable**: Add any new UI strings without new CLI flags -- βœ… **Flexible**: Supports both files and inline JSON -- βœ… **Future-proof**: Automatically supports new UI strings as they're added -- βœ… **Organized**: All customizations in one place -- βœ… **Version-controlled**: JSON files can be tracked in your repository - -## Advanced Usage - -### Multi-language Support - -Create different JSON files for different languages: - -```bash -# English -code-server --i18n /config/strings-en.json - -# Spanish -code-server --i18n /config/strings-es.json --locale es -``` - -### Dynamic Customization - -Generate JSON dynamically in scripts: - -```bash -#!/bin/bash -COMPANY_NAME="ACME Corp" -cat > /tmp/strings.json << EOF -{ - "WELCOME": "Welcome to ${COMPANY_NAME} Development Portal", - "LOGIN_TITLE": "${COMPANY_NAME} Access Portal" -} -EOF - -code-server --i18n /tmp/strings.json -``` \ No newline at end of file diff --git a/docs/guide.md b/docs/guide.md index 14e6ed9bf9d5..588b65b30005 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -461,4 +461,35 @@ through the proxy without authentication, use `--skip-auth-preflight`. ## Internationalization and customization -You can customize some of code-server's strings for either internationalization or customization purposes. See our [customization guide](./customization.md). +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.