Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from Settings → Dashboard to the Claude Code tab's AgentHub sub-tab (ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub. A home that is or contains the OS home is refused (EBADHOME). Settings persistence: dashboard-saved settings go to ~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin cache an update wipes. Load order: real env > the checkout's .env > that file. The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put. C1: codex-usage.js reads Codex rollout journals into sessions (new column tool) and token_usage, tracked per file in codex_rollouts; forks deduped by total vector, sub-agent threads folded into the root; pricing-sync also prices OpenAI models; PricingSettings with a vendor filter. C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on a click, through a stable forwarder; Codex trust is never written. The hook route creates tool='codex' sessions and folds sub-agent hooks into the root. D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab; Gemini pricing. Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=, clear-data covering the new tables, Codex child-thread events moving no status, hooks owning status only while current, EFOREIGN for a forwarder that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE honoured from .env. Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
8.7 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-VNresolves tovi)- Detection order:
localStorage→navigator
2) Resource and Namespace Strategy
Translation resources are stored per language and namespace:
client/src/i18n/locales/en/*.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|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
enandvifiles for the same namespace. - Keep fallback behavior deterministic by ensuring
enis always complete.
3) Key Naming Conventions
Use stable semantic keys, not English sentence literals.
Convention rules
- 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)
Examples
nav:dashboardnav:languageNames.vicommon:time.justNowcommon:time.mAgokanban:agentCountkanban: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 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 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/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 → AgentHub, Settings → Codex → AgentHub 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.tsclient/src/lib/__tests__/format.test.tsclient/src/components/__tests__/Sidebar.test.tsx
Run:
npm run test:client
Recommended test matrix
| 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 | Sidebar language toggle |
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
enandvi - 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,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.tsclient/src/components/Sidebar.tsxclient/src/lib/format.tsclient/src/i18n/__tests__/i18n.test.tsclient/src/lib/__tests__/format.test.ts