Skip to content

Internationalization

Sia edited this page May 31, 2026 · 2 revisions

Internationalization (i18n)

The admin SSR UI supports English (en) and Korean (ko). Default is English to match the public-facing documentation (CLAUDE.md §5 "public docs are English"), with per-user override.

Resolution chain

When rendering an SSR page the language is decided in this order:

  1. Per-user settingadmin_users.language (en / ko / null). Change at Settings → General → Language.
  2. Server defaulti18n.defaultLanguage in server.yml, or override via VIBECODER_DEFAULT_LANGUAGE environment variable.
  3. Fallbacken (built-in default).

So with no preference and no env override the UI is English. An operator who wants Korean by default can set VIBECODER_DEFAULT_LANGUAGE=ko in docker/.env; the per-account override still wins.

Per-user change (browser)

  1. Sign in to the admin web (http://localhost:17880/).
  2. Go to Settings (top nav).
  3. Under Language, pick one of:
    • Use server default (xx) — clears the per-user override.
    • English
    • 한국어 (Korean)
  4. Click Save language, then refresh the page.

The change is per-account and persists until you change it again. Logging out preserves the choice (the value lives in admin_users.language, not in the session cookie).

Server default (operator)

Via Docker compose

# in docker/.env
VIBECODER_DEFAULT_LANGUAGE=ko

Then docker compose up -d server — the env reaches the JVM at boot and overrides the server.yml value. No server.yml change needed; this is the preferred path for environment-specific defaults (dev vs. prod).

Via server.yml

# config/server.yml (or $VIBECODER_CONFIG_DIR/server.yml)
i18n:
  defaultLanguage: ko

Restart the server (the value is read once at boot). Saved through the Settings page or hand-edited.

Adding a new language

The bundles live in server/i18n/MessagesEn.kt and MessagesKo.kt.

To add Spanish (for example):

  1. Create server/i18n/MessagesEs.kt with the same key set as the English bundle (every missing key falls back to English at render time).
  2. Add "es" to MessagesEs.MAP to the BUNDLES map in Messages.kt.
  3. Add "es" to the SUPPORTED set in Messages.kt.
  4. Add an <option value="es">Español</option> to the language dropdown in AdminTemplates.settingsPage() and a matching settings.general.language.option.es key in all bundles.
  5. Allow the value in ConfigLoader.applyEnvOverrides (the env → defaultLanguage validator).

Missing keys at render time return the key string itself (e.g. nav.home) — this makes them easy to spot during QA rather than crashing the page.

Key naming convention

Dot notation with category prefix:

  • common.* — reusable single words (save, cancel, signOut)
  • nav.* — top-level sidebar (nav.home, nav.projects)
  • settings.* — Settings page (settings.title, settings.tab.general, settings.general.language.title)
  • home.*, projects.*, env.*, claude.*, mcp.*, gitint.*, error.* — per-page

When adding a new key:

  1. Add to both MessagesEn.kt and MessagesKo.kt. Missing keys log a fallback at render time; the page does not break, but QA should catch the asymmetry.

  2. Use %s for parameter substitution (String.format-style):

    "home.greeting" to "Welcome to %s"
    // call site
    Messages.t(lang, "home.greeting", serverName)

Architecture notes

  • The bundles are plain Kotlin object (no resource files) so they ship with the JAR and are accessible without I/O — no first-render penalty.
  • WebSession.language is computed once per request in requireSessionOrRedirect and passed through AdminTemplates.shell(lang = …).
  • <html lang="…"> is set so screen readers, browser auto-translate, and language-aware fonts work correctly.
  • The dropdown POSTs to /settings/language which calls AdminUserRepository.setLanguage and redirects back to /settings with the "saved" flash message in the new language (the redirect URL encodes the message after resolve(newLang, defaultLang)).

Coverage status

Page i18n?
Sidebar / top nav
Settings tab bar
Settings → General
Settings → other tabs partial — most labels still Korean (rolling cleanup)
Home / Projects / Env / Claude / MCP partial — keys reserved, in progress
Console / Builds / Git / Files not yet

The hard infrastructure is complete; remaining work is mechanical (string extraction + key mapping), rolled up a batch of pages at a time.

Troubleshooting

Q. I changed my language but the page is still in the old one. Hit refresh — the dropdown saves on submit but the response is a redirect to /settings, which renders in the new language. If your browser cached an earlier page, force-reload that tab.

Q. I set VIBECODER_DEFAULT_LANGUAGE=ko in .env but new users still see English. Make sure you restarted the container (docker compose up -d server). Also confirm the value is lowercase (ko, not KO) — the loader is case-insensitive but only accepts the lowercase form internally.

Q. A label shows up as nav.something literally. That's a missing key — please file an issue with the page name. The fallback chain is lang → en → key, so seeing the key means both bundles are missing the entry.

Q. Can I have different defaults per user? That's exactly what admin_users.language provides — each user picks their own at /settings. The server default is only for new users who haven't chosen yet.

Clone this wiki locally