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)
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"]fallbackLng:"en"nonExplicitSupportedLngs:true(e.g.vi-VNresolves tovi)- Detection order:
localStorage→navigator
Translation resources are stored per language and namespace:
client/src/i18n/locales/en/*.jsonclient/src/i18n/locales/zh/*.jsonclient/src/i18n/locales/vi/*.json
Active namespaces:
commonnavdashboardsessionsactivityanalyticsworkflowssettingskanbanerrors
erDiagram
LANGUAGE ||--o{ NAMESPACE : contains
NAMESPACE ||--o{ KEY : defines
KEY ||--o{ TRANSLATION : maps_to
LANGUAGE {
string code "en|zh|vi"
string locale "en-US|zh-CN|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,zh,vifiles for the same namespace. - Keep fallback behavior deterministic by ensuring
enis always complete.
Use stable semantic keys, not English sentence literals.
- Use namespace-scoped keys:
namespace:key - Use lower camelCase key segments
- Keep terminology consistent across locales (for example, keep
Agent/Subagentterms stable where required) - Use suffixes for plurals when needed (e.g.
_plural) - Group nested concepts by domain (e.g.
time.justNow,time.mAgo)
nav:dashboardnav:languageNames.vicommon:time.justNowcommon:time.mAgokanban:agentCountkanban:agentCount_plural
classDiagram
class I18nConfig {
+supportedLngs: ["en","zh","vi"]
+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
The sidebar language controls call i18n.changeLanguage() and 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: Click language button (EN/ZH/VI)
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_zh: localStorage/navigator resolves zh
Detecting --> Loaded_vi: localStorage/navigator resolves vi
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
Formatting utilities are centralized in client/src/lib/format.ts.
en→en-USzh→zh-CNvi→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 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"]
E --> H["toLocaleTimeString / toLocaleString"]
F --> H
G --> H
H --> I["Localized date/time/number output"]
Use client tests to verify translation correctness, fallback behavior, and locale formatting:
client/src/i18n/__tests__/i18n.test.tsclient/src/lib/__tests__/format.test.tsclient/src/components/__tests__/Sidebar.test.tsx
Run:
npm run test:client| Area | What to verify | Example |
|---|---|---|
| Resource parity | Same key coverage across en/zh/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 | zh-CN, vi-VN formatting |
| Runtime switching | UI rerenders without reload | Sidebar language toggle |
| 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 |
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 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
- Confirm all namespaces exist for
en,zh,vi - 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.,
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)
client/src/i18n/index.tsclient/src/components/Sidebar.tsxclient/src/lib/format.tsclient/src/i18n/__tests__/i18n.test.tsclient/src/lib/__tests__/format.test.ts