Files
AgentHub/docs/I18N.md
T
nntrivi2001 daf5b7be92 feat: edit in the Changes panel, git graph history, plan quota (v2.3.0)
- Changes panel: a working file opens in a CodeMirror editor with its diff
  inline (DiffEditor.tsx, @codemirror/merge, lazy-loaded); Changes folds the
  unchanged lines, Whole file shows them; side by side at 50/50 and full
  width; a Revert per block; original from HEAD or the branch's merge base
  (`mergeBase` on GET /git/changes?range=branch)
- Every diff: green = line added whole, red = removed whole, blue = edited
  in place (--diff-mod)
- History drawn as a commit graph (#58: `parents` on GET /git/log,
  --date-order; lib/commitGraph.ts)
- Per-workspace never-stage list, drawn orange in the Files tree (--kept);
  closed Terminal rows past three deleted
- Status bar plan quota for Claude (per account) and Codex
  (GET /api/pricing/quota, server/lib/quota.js); compact hover panel that
  stays open, Refresh button with a spinner
- Settings → Appearance holds language, theme and pet; Ctrl+Shift+C copies;
  compact sidebar navigation and short session times
2026-10-06 15:38:25 +07:00

8.9 KiB

Internationalization (i18n) Architecture and Usage

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

Supported languages: English (en), Vietnamese (vi)


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", "vi"]
  • fallbackLng: "en"
  • nonExplicitSupportedLngs: true (e.g. vi-VN resolves to vi)
  • Detection order: localStorage → navigator

2) Resource and Namespace Strategy

Translation resources are stored per language and namespace:

  • client/src/i18n/locales/en/*.json
  • client/src/i18n/locales/vi/*.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|vi"
        string locale "en-US|vi-VN"
    }
    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 and vi files for the same namespace.
  • Keep fallback behavior deterministic by ensuring en is always complete.

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","vi"]
      +fallbackLng: "en"
      +defaultNS: "common"
      +detectionOrder: ["localStorage","navigator"]
    }

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

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

    class SettingsLanguagePicker {
      +changeLanguage()
    }

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

    I18nConfig --> NamespaceResource
    ReactComponent --> I18nConfig
    ReactComponent --> NamespaceResource
    SettingsLanguagePicker --> I18nConfig
    FormatUtils --> I18nConfig

4) Language Detection and Switching Flow

The language picker in Settings → Appearance (client/src/components/settings/AppearanceSettings.tsx, a SettingsSegmented of English / Tiếng Việt, found by the settings search too) calls i18n.changeLanguage() and UI updates reactively through useTranslation. The sidebar has no language control.

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: Click language button (EN/ZH/VI/KO)
    SB->>I: changeLanguage("vi")
    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_vi: localStorage/navigator resolves vi
    Detecting --> Loaded_en: unsupported locale -> fallback en

    Loaded_en --> Loaded_vi: user switches to vi
    Loaded_vi --> Loaded_en: user switches to en

5) Date and Number Localization Behavior

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

  • en → en-US (also the default for any other language)
  • vi → vi-VN

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 Model Pricing table (Settings → Claude Code → Overview, Settings → Codex → Overview and Settings → Antigravity), 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 -->|vi| G["Locale vi-VN"]
    E --> H["toLocaleTimeString / toLocaleString"]
    G --> H
    H --> 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
Area What to verify Example
Resource parity Same key coverage across en/vi Missing key detection in CI
Locale fallback Unknown locales fall back to en vi-VN resolves to vi
Terminology consistency Canonical terms stay stable Agent/Subagent expectations
Date/number formatting Locale-specific output shape en-US, vi-VN formatting
Runtime switching UI rerenders without reload Settings → Appearance language picker

7) Troubleshooting

Symptom Likely cause Resolution
UI stays in old language after switch Cached key or stale component state Confirm i18n.changeLanguage(...) is called and component uses useTranslation
Unexpected fallback to English Unsupported locale code Ensure code normalizes to `en
Missing text on one page Namespace file key missing Add key to all language files for that namespace
Date/time looks wrong Locale mapping or timezone parse issue Verify getCurrentLocale() and parseDate() behavior
Inconsistent term translation Manual translation drift Enforce 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/vi 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 and vi
  • Confirm key parity across all locale JSON files
  • Confirm language switching from Settings → Appearance re-renders the whole app
  • Confirm fallback behavior for region tags (e.g., vi-VN, en-US)
  • 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