Internationalization (i18n) Architecture and Usage

August 10, 2026 · View on GitHub

This guide documents how localization works in the Agent Dashboard, including architecture, resources, runtime behavior, testing, and rollout.

Supported languages: English (en), Chinese (zh), Vietnamese (vi), Korean (ko), Spanish (es)


1) Architecture Overview

Localization is implemented in the frontend with i18next + react-i18next and browser language detection.

flowchart TB
    subgraph Browser
        User["User"]
        LS["localStorage<br/>i18nextLng"]
        Nav["navigator.language"]
    end

    subgraph ClientApp["React Client"]
        Detector["i18next-browser-languagedetector"]
        I18n["i18n init<br/>client/src/i18n/index.ts"]
        NS["Namespace bundles<br/>common/nav/dashboard/..."]
        UI["Pages + components<br/>useTranslation()"]
        Format["format.ts<br/>locale-aware date/number/model-name"]
    end

    User --> UI
    LS --> Detector
    Nav --> Detector
    Detector --> I18n
    I18n --> NS
    NS --> UI
    I18n --> Format

Key runtime facts

  • supportedLngs: ["en", "zh", "vi", "ko", "es"]
  • fallbackLng: "en"
  • nonExplicitSupportedLngs: true (e.g. es-ES resolves to es)
  • Detection order: localStoragenavigator

2) Resource and Namespace Strategy

Translation resources are stored per language and namespace:

  • client/src/i18n/locales/en/*.json
  • client/src/i18n/locales/zh/*.json
  • client/src/i18n/locales/vi/*.json
  • client/src/i18n/locales/ko/*.json
  • client/src/i18n/locales/es/*.json

Active namespaces:

  • common
  • nav
  • dashboard
  • sessions
  • activity
  • analytics
  • workflows
  • settings
  • kanban
  • errors
erDiagram
    LANGUAGE ||--o{ NAMESPACE : contains
    NAMESPACE ||--o{ KEY : defines
    KEY ||--o{ TRANSLATION : maps_to

    LANGUAGE {
        string code "en|zh|vi|ko|es"
        string locale "en-US|zh-CN|vi-VN|ko-KR|es-ES"
    }
    NAMESPACE {
        string name "common|nav|dashboard|..."
        string file_path "locales/{lang}/{namespace}.json"
    }
    KEY {
        string id "dot.notation.or.leaf"
        string type "string|pluralized"
    }
    TRANSLATION {
        string value "localized text"
    }

Strategy notes

  • Keep namespace boundaries page/domain focused.
  • Keep key parity across en, zh, vi, ko, and es files for the same namespace.
  • Keep fallback behavior deterministic by ensuring en is always complete.

Translation quality checklist

When adding user-visible copy:

  1. Write the English source string first, with enough nearby context for translators to understand where it appears.
  2. Add the same key to every supported locale in the same change; a fallback is a safety net, not a completed translation.
  3. Preserve interpolation tokens, Markdown fragments, code identifiers, command names, and environment-variable names exactly as written in English.
  4. Reuse established product terminology within each locale instead of translating the same concept differently on each page.
  5. Keep labels concise and test a narrow viewport after translation, especially for headings, buttons, and table actions.

3) Key Naming Conventions

Use stable semantic keys, not English sentence literals.

Convention rules

  1. Use namespace-scoped keys: namespace:key
  2. Use lower camelCase key segments
  3. Keep terminology consistent across locales (for example, keep Agent / Subagent terms stable where required)
  4. Use suffixes for plurals when needed (e.g. _plural)
  5. Group nested concepts by domain (e.g. time.justNow, time.mAgo)

Examples

  • nav:dashboard
  • nav:languageNames.vi
  • common:time.justNow
  • common:time.mAgo
  • kanban:agentCount
  • kanban:agentCount_plural
classDiagram
    class I18nConfig {
      +supportedLngs: ["en","zh","vi","ko","es"]
      +fallbackLng: "en"
      +defaultNS: "common"
      +detectionOrder: ["localStorage","navigator"]
    }

    class NamespaceResource {
      +languageCode
      +namespace
      +jsonFilePath
      +keys[]
    }

    class ReactComponent {
      +useTranslation(namespace)
      +t(key, params)
    }

    class SidebarLanguageSwitch {
      +SUPPORTED_LANGUAGES
      +normalizeLanguage()
      +changeLanguage()
    }

    class FormatUtils {
      +getCurrentLocale()
      +formatTime()
      +formatDateTime()
      +fmtCostFull()
      +formatModelName()
    }

    I18nConfig --> NamespaceResource
    ReactComponent --> I18nConfig
    ReactComponent --> NamespaceResource
    SidebarLanguageSwitch --> I18nConfig
    FormatUtils --> I18nConfig

4) Language Detection and Switching Flow

The sidebar language control is the shared custom Select dropdown used by the Run Claude page. It calls i18n.changeLanguage() and the UI updates reactively through useTranslation.

sequenceDiagram
    participant U as User
    participant SB as Sidebar.tsx
    participant I as i18next
    participant LD as LanguageDetector
    participant NS as Locale Resources
    participant UI as React Components

    U->>SB: Choose a language from the custom dropdown
    SB->>I: changeLanguage("es")
    I->>NS: Resolve namespace bundles
    NS-->>I: Return translations
    I->>LD: Persist i18nextLng in localStorage
    I-->>UI: Trigger rerender
    UI->>UI: Re-evaluate t(...) keys
    UI-->>U: Localized labels displayed
stateDiagram-v2
    [*] --> Detecting
    Detecting --> Loaded_en: localStorage/navigator resolves en
    Detecting --> Loaded_zh: localStorage/navigator resolves zh
    Detecting --> Loaded_vi: localStorage/navigator resolves vi
    Detecting --> Loaded_ko: localStorage/navigator resolves ko
    Detecting --> Loaded_es: localStorage/navigator resolves es
    Detecting --> Loaded_en: unsupported locale -> fallback en

    Loaded_en --> Loaded_zh: user switches to zh
    Loaded_en --> Loaded_vi: user switches to vi
    Loaded_zh --> Loaded_en: user switches to en
    Loaded_zh --> Loaded_vi: user switches to vi
    Loaded_vi --> Loaded_en: user switches to en
    Loaded_vi --> Loaded_zh: user switches to zh
    Loaded_en --> Loaded_ko: user switches to ko
    Loaded_ko --> Loaded_en: user switches to en
    Loaded_en --> Loaded_es: user switches to es
    Loaded_es --> Loaded_en: user switches to en

5) Date and Number Localization Behavior

Formatting utilities are centralized in client/src/lib/format.ts.

  • enen-US
  • zhzh-CN
  • vivi-VN
  • koko-KR
  • eses-ES

formatTime, formatDateTime, and fmtCostFull use locale-aware toLocale* APIs.
Timestamp parsing normalizes timezone-less SQLite datetime strings to UTC before display formatting.

formatModelName converts raw model identifiers (e.g. claude-opus-4-7-20260101, claude-opus-4-7[1m]) into human-friendly display names (e.g. "Claude Opus 4.7", "Claude Opus 4.7 (1M)"). This is locale-independent (brand names are proper nouns) and is applied across all UI surfaces except the Settings page (which shows raw patterns for pricing rule configuration).

flowchart LR
    A["Raw timestamp / numeric value"] --> B["parseDate() normalization"]
    B --> C["getCurrentLanguage()"]
    C --> D{"Language"}
    D -->|en| E["Locale en-US"]
    D -->|zh| F["Locale zh-CN"]
    D -->|vi| G["Locale vi-VN"]
    D -->|ko| H["Locale ko-KR"]
    D -->|es| J["Locale es-ES"]
    E --> K["toLocaleTimeString / toLocaleString"]
    F --> K
    G --> K
    H --> K
    J --> K
    K --> I["Localized date/time/number output"]

6) Testing Strategy

Use client tests to verify translation correctness, fallback behavior, and locale formatting:

  • client/src/i18n/__tests__/i18n.test.ts
  • client/src/lib/__tests__/format.test.ts
  • client/src/components/__tests__/Sidebar.test.tsx

Run:

npm run test:client
AreaWhat to verifyExample
Resource paritySame key coverage across en/zh/vi/ko/esMissing key detection in CI
Locale fallbackUnknown locales fall back to enes-ES resolves to es
Terminology consistencyCanonical terms stay stableAgent/Subagent expectations
Date/number formattingLocale-specific output shapezh-CN, vi-VN, ko-KR, es-ES formatting
Runtime switchingUI rerenders without reloadSidebar custom language dropdown

7) Troubleshooting

SymptomLikely causeResolution
UI stays in old language after switchCached key or stale component stateConfirm i18n.changeLanguage(...) is called and component uses useTranslation
Unexpected fallback to EnglishUnsupported locale codeEnsure code normalizes to `en
Missing text on one pageNamespace file key missingAdd key to all language files for that namespace
Date/time looks wrongLocale mapping or timezone parse issueVerify getCurrentLocale() and parseDate() behavior
Inconsistent term translationManual translation driftEnforce glossary and update locale tests

8) Rollout Checklist

gantt
    title i18n rollout plan
    dateFormat  YYYY-MM-DD
    axisFormat  %m/%d

    section Resource Preparation
    Lock key inventory              :a1, 2026-01-01, 3d
    Fill en/zh/vi/ko/es namespace files   :a2, after a1, 5d

    section Runtime Integration
    Wire detection + persistence    :b1, after a2, 2d
    Validate sidebar switching      :b2, after b1, 2d
    Validate locale formatting      :b3, after b1, 2d

    section Verification
    Add/refresh i18n tests          :c1, after b2, 3d
    Run regression suite            :c2, after c1, 2d

    section Release
    Staged release + monitoring     :d1, after c2, 2d
    Post-release translation audit  :d2, after d1, 3d

Operational checklist

  • Confirm all namespaces exist for en, zh, vi, ko, es
  • Confirm key parity across all locale JSON files
  • Confirm language switching works in collapsed and expanded sidebar modes
  • Confirm fallback behavior for region tags (e.g., es-ES, vi-VN, zh-CN)
  • Confirm date/time/currency formatting for all supported languages
  • Confirm client tests pass before release
  • Confirm docs references are updated (README, ARCHITECTURE, docs/README)

References

  • client/src/i18n/index.ts
  • client/src/components/Sidebar.tsx
  • client/src/lib/format.ts
  • client/src/i18n/__tests__/i18n.test.ts
  • client/src/lib/__tests__/format.test.ts