Live-switching, JSON locale files, shared ClaudeDo.Localization project, English-only at launch with data-driven extensibility, installer parity. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7.7 KiB
7.7 KiB
Localization (i18n) Support — Design
Date: 2026-06-03 Status: Approved (pending spec review)
Goal
Add translation support to ClaudeDo. The user picks a language in the Settings modal and all UI text reflects it instantly (no restart). The WPF installer is localized the same way and gets its own language picker. Ship English only now, but the system is fully data-driven: adding a new language means dropping one JSON file into a folder — no code changes, no rebuild.
Decisions (from brainstorming)
- Languages: English only at launch; extensible via translation files.
- Switching: Live / instant — all bound UI text updates the moment the language changes.
- Storage: Selected language stored in
~/.todo-app/ui.config.json(the local UI config that also holdsDbPath/SignalRUrl). Purely a UI concern — does not go through the worker/SignalR settings path. - Installer: Defaults to existing config language (upgrade) → OS culture → English. Shows a language picker in the wizard, live-switches its own UI, and writes the chosen language into
ui.config.jsonso the app launches matching the installer. - Locale files: Loose
*.jsonfiles in alocales/folder next to the running exe, scanned at startup to discover available languages. - Code sharing: A shared
ClaudeDo.Localizationproject holds the loading/lookup/language-list logic, referenced byClaudeDo.Ui,ClaudeDo.App, andClaudeDo.Installer. Each UI framework keeps its own thin markup-extension binding layer (Avalonia ≠ WPF).
Architecture & Components
New shared project: ClaudeDo.Localization
LocaleStore— discovers and loads*.jsonfiles from thelocales/folder next to the running exe. Parses each file's nested JSON, flattens it into an internalDictionary<string,string>keyed by dot-path for O(1) lookup, and capturesmetadata.code/metadata.name. Exposes the list of available languages for the dropdowns.ILocalizer/Localizer— singleton holding the active language dictionary. Members:- indexer
this[string key]→ translated string (with fallback), string Get(string key, params object[] args)→string.Formatfor parameterized strings,void SetLanguage(string code)→ swaps the active dictionary and raisesPropertyChangedfor the indexer so all live bindings refresh (this is what enables instant switching),AvailableLanguages(list of{ code, name }),CurrentCode.
- indexer
- Fallback chain: requested key in active language → same key in English → the key path string itself (a missing translation is visible, never a crash).
- OS-culture resolution: helper that maps the current OS UI culture to an available locale code, falling back to English.
Per-framework binding layer (not shared)
- Avalonia: a
{loc:Tr Some.Key}markup extension that binds toLocalizer[key](Source = the singletonLocalizer, Path =[key]). Language change raises the indexerPropertyChanged, refreshing every binding. - WPF installer: an equivalent markup extension doing the same against the installer's own
Localizerinstance.
Both consume the same JSON files and the same LocaleStore/Localizer logic from the shared project.
Translation File Format
locales/en.json (and future de.json, fr.json, …) — nested, human-friendly hierarchy:
{
"metadata": { "code": "en", "name": "English" },
"settings": {
"save": "Save",
"cancel": "Cancel",
"general": { "model": "Model", "maxParallel": "Max parallel executions" }
},
"tasks": {
"addPlaceholder": "Add a task…",
"overdue": "OVERDUE"
},
"worktrees": { "autoCleanupDays": "{0} days" }
}
metadata.codeis the language id stored inui.config.jsonand matched to OS culture;metadata.nameis the dropdown label.- Lookup by dot-path key (
"settings.general.model"). On-disk file stays grouped/nested; the runtime flattens it for fast lookup. Authors edit a clean hierarchy. - Parameters:
{0},{1}placeholders resolved viaGet(key, args). - Encoding: UTF-8 — non-ASCII languages work out of the box.
Data Flow & Wiring
App config
- Add
Language(string, e.g."en") toAppSettings(ClaudeDo.Ui/AppSettings.cs) and to the installer mirrorInstallerAppSettings(ClaudeDo.Installer/Core/ConfigModels.cs). - Add a
Save()method toAppSettings(today the UI only reads it).
App startup (ClaudeDo.App/Program.cs)
AppSettings.Load()readsLanguage(missing/empty → resolve from OS culture, else"en").LocaleStorescanslocales/next to the exe;Localizeris registered as a singleton and set to the configured language.- UI renders; every
{loc:Tr ...}binding pulls from the active dictionary.
Changing language in Settings (General tab)
- New "Language" dropdown bound to
Localizer.AvailableLanguages; selection bound to current code. - On change →
Localizer.SetLanguage(code)(instant UI refresh) andAppSettings.Language = code; AppSettings.Save(). Local UI state only — not routed through worker/SignalR.
Installer (ClaudeDo.Installer)
- On launch: default language = existing
ui.config.jsonLanguageif present (upgrade), else OS culture, else English. - Wizard gets a language dropdown (same
LocaleStore, installer's own markup extension) → live-switches the installer UI. - When writing
ui.config.json, persists the chosenLanguageso the app launches matching the installer.
Build wiring
locales/*.jsoncopied to output (CopyToOutputDirectory) for both App and Installer.- Installer packages the
locales/folder so it lands beside the installed exe.
String-Extraction Scope
Mechanical but large; done screen-by-screen so each commit is reviewable, building one en.json as the single source of truth.
- 22 Avalonia
.axamlviews — replace inlineText="...",Content="...",PlaceholderText="...", and inlineComboBoxItemtext with{loc:Tr key}. - ViewModel strings — user-facing literals built in C# (e.g.
HeaderTitle,StatusPill, status text, parameterized messages) resolve via injectedILocalizer(localizer.Get(...)). Log messages and non-user-facing strings stay as-is. Live-switch note: a VM string resolved once will not refresh on language change. For VM-built user-facing text, either (a) prefer resolving in XAML via{loc:Tr}where possible, or (b) have the VM subscribe to theLocalizerchange event and re-raisePropertyChanged(or re-resolve) for its localized properties. Decide per-property during extraction. - 10 WPF installer files — same treatment with the installer's markup extension; VM-driven headings (
Heading,NextButtonText, etc.) go throughILocalizer. - Enum-ish display values (model names, permission modes, weekday names) — translate the display text while keeping the underlying value/binding intact.
Testing
ClaudeDo.Localizationunit tests: load/flatten nested JSON, dot-path lookup, fallback chain (active→en→key),{0}formatting, OS-culture resolution.LocaleStorediscovery test (folder scan → available languages).- Key-coverage test: every locale file's flattened key set matches
en.json; fails the build ifen.jsondrifts from other locale files. - Settings round-trip test:
SetLanguageupdatesLocalizerand persists toui.config.json. - Manual UI pass (user's visual review): confirm instant switching with a throwaway
de.jsonstub during dev, then remove it.
Out of Scope (YAGNI)
- Pluralization rules, RTL layout, per-string gender.
- Translating the German weekly-report body (generated content — stays as-is).
- Localizing log output and non-user-facing strings.