feat(i18n): localize backend (main-process) strings, default Chinese#256
feat(i18n): localize backend (main-process) strings, default Chinese#256craigamcw wants to merge 1 commit into
Conversation
The renderer is fully localized, but strings produced by the main process —
model/network error messages, the "no usable credentials" prompt, the
startup-failure dialog, and the default config-set name — were hardcoded in
Chinese and reached the UI untranslated.
This adds a small main-process i18n layer so those strings follow the user's
language while keeping Chinese as the default and ultimate fallback (the
product is Chinese-first):
- src/main/i18n/catalog.ts — backend message catalog for all 12 shipped
languages (zh is the source; the other 11 are translations).
- src/main/i18n/index.ts — mt(key, params) translator with {{placeholder}}
interpolation, locale normalization (es-ES -> es, nb/nn -> no), and
zh -> key fallback. setBackendLanguage() switches the active language.
- The renderer mirrors its react-i18next language into config.uiLanguage; the
main process seeds setBackendLanguage() from the persisted value on launch
and updates it whenever the config is saved.
- Routed the hardcoded strings in agent-runner-message-end.ts, index.ts and
config-store.ts through mt().
Behavior is unchanged by default: with no language synced, mt() returns the
original Chinese text. A key-parity + behavior unit test is included.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Review mode: initial
Findings
-
[Minor] Module-level
mt()call in config-store.ts produces a static string at import time, preventing live language changes from retroactively affecting the default config set name. This is intentional per the PR description ("Persisted config-set names are data"), but consider documenting it more explicitly near thedefaultConfigSetdefinition to avoid future confusion that the name is frozen at module load time. (src/main/config/config-store.ts:223) -
[Minor] The
normalizeBackendLanguagefunction does not trim whitespace from the input. If a locale tag with surrounding whitespace is passed (e.g.," en "), it getssplit-corrected, but leading whitespace would cause the first split element to be" en"which would not match any catalog key, falling through to the default. This is defensible since input comes from react-i18next (which trims), but adding alang = lang.trim()before the fallback check would harden the function against unexpected input. (src/main/i18n/index.ts:22)
Suggested fix:+ if (!lang) return DEFAULT_BACKEND_LANGUAGE; + lang = lang.trim();
Questions
- None.
Summary
Review mode: initial
Well-structured PR with proper test coverage, no regressions, no new security concerns. The i18n layer is cleanly separated, the language sync path from renderer through config to backend translator is correct, and the fallback behavior (Chinese default) preserves existing behavior. The static nature of defaultConfigSet.name is intentional and documented. No blockers or major issues found.
Testing
src/tests/i18n/backend-i18n.test.ts covers catalog key parity, placeholder presence, mt() interpolation, locale normalization, and fallback logic. Additional tests for the renderer-to-main language sync path (e.g., simulated renderer language change IPC) and for the error message interpolation with edge-case error texts (e.g., error containing {{error}} literal) would improve confidence, but the current coverage is adequate for an initial implementation.
Open Cowork Bot
What
The renderer is fully localized, but a handful of strings are produced by the main process and reached the UI hardcoded in Chinese, untranslated:
agent-runner-message-end.ts— timeout, empty result, 400/401/429/5xx, network interrupted, and the retry/check-config hints),index.ts),index.ts),默认方案) and the numbered fallback name (config-store.ts).This adds a small main-process i18n layer so those strings follow the user's chosen language, while keeping Chinese as the default and ultimate fallback (the product is Chinese-first).
How
src/main/i18n/catalog.ts— backend message catalog for all 12 shipped languages.zhis the source; the other 11 (en, es, fr, de, it, uk, pl, sv, no, nl, ro) are translations. Keys cover only the backend strings above.src/main/i18n/index.ts—mt(key, params)translator:{{placeholder}}interpolation, locale normalization (es-ES → es,zh-CN → zh,nb/nn → no), andzh → keyfallback.setBackendLanguage()switches the active language.config.uiLanguage; the main process seedssetBackendLanguage()from the persisted value on launch and updates it whenever the config is saved.uiLanguageis threaded throughnormalizeConfig/updateso it round-trips.mt().Behavior / compatibility
mt()returns the original Chinese text — existing Chinese users see exactly what they see today.uiLanguageis not part of the agent runtime signature), so switching is side-effect-free for in-flight work.默认方案keeps its stored name; the localization applies to the name chosen at creation time. (Live display-translation of system set names could be a small renderer follow-up if desired.)Tests
src/tests/i18n/backend-i18n.test.ts— asserts catalog key-parity across all 12 languages, placeholder/underscore preservation,mt()interpolation, locale normalization, and the zh/key fallbacks.tsc --noEmitis clean.Notes
This completes the localization story started in #253 (renderer European locales) by covering the main-process strings, but it is self-contained — it ships its own catalog and does not depend on #253.
🤖 Generated with Claude Code