feat(i18n): add Simplified Chinese UI localization - #152
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…tionary Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The update service and native menu are macOS-only concepts on upstream main, but the guards themselves shipped from the Windows base. Remove them so this branch is i18n-only: platform flows match upstream verbatim, and the darwin branches were no-ops anyway.
…t invoke The share menu items now call useI18n(), so plain function invocation (no render context) and unwrapped trees crash. Render the items through I18nProvider like the parent wiring test does; keep bun's process-wide mock.module registration away from @llm-space/ui/lib/i18n so later test files still resolve the real module.
|
I verified this PR at Blocking findings
Additional issues
Verification performed
|
…ng surfaces Addresses the PR deer-flow#152 review: restore the persisted UI language in the bun process before the native menu is read (menu and error copy no longer come back in the OS locale across restarts), and give Electrobun's fixed-English role items explicit labels so the zh menu is fully localized. - bun: read llm-space-app-lang from the local-storage mirror at startup; restoreAppLocale() + preselectMenuLanguage() set the locale without rebuilding the menu (B1) - menu: label every role item (app/edit/window submenus) from the i18n tree (B2) - playground: toasts and web viewer chrome now resolve through the shared messages tree via a new getMessages() helper for non-React consumers (B3) - provider.test: capture and restore the fake test DOM so the i18n suite no longer leaks a global Event into other suites (B4) - languageChanged RPC message replaced by a setLanguage command through the command layer (minor b); I18nProvider context value memoized (minor a); plural one/other branch tests (minor c); locale-registry and zh-* -> zh-CN documentation (minor d/e) Co-Authored-By: Claude <noreply@anthropic.com>
|
Thanks for the thorough review — all 4 blockers and the 5 minor items are addressed in commit Blockers
Minors
Verification: |
Two
|
thread-share-button.test.tsx installed installReactTestDom() at module scope but never ran restore(), so the fake global Event could leak into a later suite that dispatched real events on the same worker thread (CI hit this as the message-scroll TypeError on PR deer-flow#152; main is immune because its version of the file installs no DOM). provider.test.tsx mirrored window.localStorage onto globalThis without restoring it — same leak class. Capture the previous value and put it back (or delete the mirror) in afterAll, matching the restore discipline of the other installers. Co-Authored-By: Claude <noreply@anthropic.com>
…I ellipsis in en copy
The setLanguage tests in commands.test.ts failed on zh-locale machines:
the menu module seeds _menuLang from the OS locale at load time, so a
zh_CN machine starts at 'zh' and setMenuLanguage('zh') early-returned,
leaving the rebuild counter at 0. Reset both pieces of module state
(setAppLocale('en') + preselectMenuLanguage('en') + counter) in
beforeEach, and make the second test drive both transitions itself
instead of relying on the first test's rebuild count.
Also revert 12 en menu/command labels that had drifted from upstream's
ASCII '...' to typographic ellipsis ('Check for Updates...', 'Settings...',
'New from Examples...', etc.) — the en copy stays byte-identical to
main. Strings where upstream genuinely uses a typographic ellipsis
(Loading…, Getting version…, Codex…) are untouched.
Co-Authored-By: Claude <noreply@anthropic.com>
|
Both issues are fixed in commit 1. zh-locale-dependent The tests now reset both pieces of module state in beforeEach(() => {
setAppLocale("en");
preselectMenuLanguage("en");
MENU_REBUILD_COUNT.count = 0;
});so they no longer depend on what the OS locale seeded into Verification note: I'm on Windows, where 2. ASCII Reverted the 12 menu/command labels that had drifted from upstream's ASCII ellipsis: |
…oned the locale registry
`timeago.js/lib/lang/zh_CN` is a compiled-CJS deep path; under Vite's
CJS interop its default export resolves to the `{ __esModule, default }`
exports object instead of the formatter function. Re-registering it
overwrote the working ESM `zh_CN` that timeago.js's own barrel had
already installed, so any Chinese-locale relative-time render
(`format(ts, "zh_CN")`) threw "TypeError: localeFunc is not a function".
With no error boundary above it, opening a thread with run history —
e.g. restoring a Deep Research tab — unmounted the entire React tree,
leaving a blank white/black window.
The barrel import every call site already does registers both mapped
locales (`en_US`, `zh_CN`), so drop our re-registration entirely and
pin the contract with a test covering both ids.
Co-Authored-By: Claude <noreply@anthropic.com>
|
Found and fixed a serious bug in this PR's code: it could blank the entire app window when opening a thread that has run history while the UI language is Chinese (3918b11). Root cause
import zh_CN from "timeago.js/lib/lang/zh_CN";
register("zh_CN", zh_CN);That deep path is a compiled-CJS module ( From then on, every Chinese-locale relative-time render threw: With no error boundary above Reproduction was deterministic: restore a Deep Research tab (any thread with run history) in a Chinese-locale session → white screen within seconds of window open. FixDrop our re-registration entirely — importing VerificationBuilt app (no HMR), Chinese session, scratch data home with a tab restoring into the deep-research thread:
Note for reviewers: bun's CJS interop unwraps |
Summary
Adds a lightweight i18n layer to the desktop app with Simplified Chinese as the first non-English locale. No new dependencies — the dictionary is pure-data TypeScript, and the runtime is a React context + hook.
What changes
packages/ui/src/lib/i18n): message dictionary (en + zh) with plural/param templates,I18nProvider+useI18nhook, language preference persisted to settings.endictionary restores provider-detected copy verbatim — every string rendered with the English locale is byte-identical to today's hardcoded text.I18nProvider; plural-template tests.Review fixes
llm-space-app-langfrom the local-storage mirror before the native menu is read, so menus and error copy no longer come back in the OS locale after a restart.thread-viewer,not-found) resolve through the shared messages tree; a newgetMessages()helper serves non-React consumers (stores, imperative toasts).Eventafter the suite.languageChangedRPC message replaced by asetLanguagecommand routed through the command layer.zh-*fallback documentation.Notes
LANGUAGES(picker +Langunion), locale detection (isZhLocale/resolveInitialLang),langToTimeago, the native menu's locale mapping, andisAppLang(startup restore) — see theLANGUAGESdoc inpackages/ui/src/lib/i18n/messages.ts.zh-*variant (zh, zh-CN, zh-TW, zh-Hans, …) resolves to the singlezhlocale, whose text is Simplified Chinese — including zh-TW, until a Traditional-Chinese locale ships.