Skip to content

feat(i18n): localize the whole app, with a complete zh-CN translation - #330

Merged
simion merged 4 commits into
simion:mainfrom
lymanzhao:feat/i18n
Sep 27, 2026
Merged

simion merged 4 commits into
simion:mainfrom
lymanzhao:feat/i18n

Conversation

@lymanzhao

@lymanzhao lymanzhao commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

{"body":"This adds i18next + react-i18next and wires every user-visible string in the webview app, shipping a complete Simplified Chinese (zh-CN) locale alongside English.\n\nEnglish output is byte-identical to before, so existing behavior, styling and e2e assertions are unchanged.\n\n### How it is wired\n\n- String-rendering components resolve text through useTranslation, with Trans for embedded markup.\n- Outside React (toasts, OS notifications, confirm dialogs, tab titles), strings resolve via i18n.t at event time.\n- Helpers that render for a component take the caller's t as a parameter, so there is no second i18n instance.\n- Eight namespaces keep the locale files navigable: common, sidebar, settings, dialogs, task, panels, chrome, backend.\n- A new uiLanguage preference (system / English / 中文) switches live, with no reload.\n- Locales are bundled TS modules: no network fetch, no Suspense boundary, no change to the CSP.\n\n### Guardrails\n\n- src/locales/parity.test.ts pins en and zh-CN to identical key and placeholder sets, so a new English string without a Chinese counterpart fails the suite instead of silently reaching Chinese users as English.\n- docs/i18n.md documents the wiring rules (hooks vs i18n.t, Trans, plurals, zh-CN conventions) for future strings.\n- A settings e2e spec covers the language switch.\n- Data-testids, raw backend error passthroughs and brand names are deliberately left untranslated.\n\n### Testing\n\nnpm test (2460 tests across 167 files) and tsc -b are green on this branch, which sits on top of v1.9.1.\nManually exercised in the running app by the author: switched the language between system/English/中文 in Settings and walked the sidebar, task view, settings pages and dialogs in both languages, confirming the zh-CN surface renders completely with no raw keys.\n\n🤖 Generated with Claude Code"}

@simion

simion commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Hey man, thank for the contribution.
Released windows support recently so you’ll need to do a rebase before merging 😁

lymanzhao and others added 4 commits September 27, 2026 13:43
Wire every user-visible string to i18next across all components, stores,
lib and hooks, and ship a full zh-CN locale. English output is byte
identical to before, so existing e2e assertions hold.

- src/lib/i18n.ts: react-i18next with bundled TS locale modules (no
  network, no Suspense), uiLanguage pref (system/en/zh-CN), live switch
- eight namespaces (common/sidebar/settings/dialogs/task/panels/chrome/
  backend); parity test pins en/zh-CN key + placeholder sets identical
- components: useTranslation per string-rendering component; Trans for
  embedded markup; t added to memo deps only where recompute is safe
- outside React (toasts, notifications, confirm dialogs, tab titles):
  i18n.t at event time; helpers that render for a component take t
- data-testids, raw backend error passthroughs and brand names unchanged

See docs/i18n.md for the wiring rules and conventions.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Follow-up to the whole-app localization for everything the newer
upstream surfaces added: task groups and their menus, the project
task filter, the delegated-work marks and legend, the checkout
picker, the machine- and project-level YOLO defaults, and the env
hints reworked for a second <code> element. Also carries the Clear
filter button in the sidebar's empty filter state.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The active language defaults to "system", which follows
navigator.language, and Node >= 21 exposes a real navigator with the
machine's locale. On a zh-CN machine every user-visible string renders
as Chinese and assertions matching on English catalog text fail, which
reads like an app bug but is pure environment. The suites are written
against the English catalog (and CI is an en machine), so pin it here;
src/locales/parity.test.ts covers the other language's keys.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Two v1.8.3..v1.9.1 additions carried hardcoded English:

- SpawnedFromMark's title and aria-label ("Started by ...").
- The Windows caption buttons in WindowControls (Minimize / Maximize /
  Restore / Close), which ship aria-labels and tooltips.

Both wire through the usual namespaces, with en byte-identical to the
upstream strings.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@lymanzhao

Copy link
Copy Markdown
Contributor Author

Thanks for the heads-up! Rebased on v1.9.1, including the Windows port and the spawn links work.

Two small additions came out of the rebase:

  • The v1.9 surfaces that carried hardcoded English are localized too: the spawn-link mark's title/aria-label, and the Windows caption buttons in the new title bar.
  • The test suite now pins the UI language to English. On a zh-CN machine navigator.language made every catalog string resolve as Chinese, so the English-matching assertions failed in a way that read like app bugs but was pure environment. parity.test.ts still covers the other language's keys.

npm test (2460 tests) and tsc -b are green.
English output stays byte-identical to upstream, so the e2e specs written against upstream's strings behave the same here.

@simion simion left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wired correctly across the app and the full suite stays green (2407/2407) with no init race. FileFinderDialog is still entirely untranslated and a handful of strings drifted slightly from the original English, worth a follow-up pass whenever convenient.

Thanks for the contribution!

@simion
simion merged commit 2283fd2 into simion:main Sep 27, 2026
simion added a commit that referenced this pull request Sep 27, 2026
`useTranslation("panels")` binds `t`, but <Trans> does not inherit it:
with no `t={t}` and no `ns=`, it looks the key up in defaultNS
("common"), misses, and renders the key itself. Thirteen call sites did
that, so the PR card told people "pr.cliMissingBody", and the clone
dialog dropped its whole destination line, since the `<code>` element
lives in the translation and a missed key takes it (and its testid)
with it. Four e2e cases caught two of the thirteen; the other nine were
shipping silently.

The guard is static rather than a render test, so it covers the call
sites no spec opens: every <Trans> key must resolve in the namespace
that element will actually use, plural suffixes included.

Refs #330

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBZ9SY1zTBpPW33QAekZC1
simion added a commit that referenced this pull request Sep 27, 2026
The static guard reads every <Trans> call site; this reads what the app
paints. `rawI18nKeysOnScreen` hands each dotted token on screen to
i18next's own `exists` across every namespace, so "package.json" is not
a key, a real one cannot hide behind a regex nobody updated, and the
check needs no list of expected sentences to stay true.

Covers the main window, every settings page, the new-task and
new-project dialogs (clone mode included, where a missed key took the
whole destination line with it) and a zh-CN pass.

The first case is the control: it plants "pr.cliMissingBody" on screen
and requires the detector to name it. Every other assertion here is "we
found nothing", which is also what a detector that stopped seeing
anything reports.

Needs the live i18next instance on `window.__termic`, e2e build only.

Refs #330

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBZ9SY1zTBpPW33QAekZC1
@simion

simion commented Sep 28, 2026

Copy link
Copy Markdown
Owner

This is live in v1.10.0, released today, and it is the release's headline: Termic now speaks Simplified Chinese, with the picker under Settings, General, Language.
Thanks for the contribution!

simion pushed a commit that referenced this pull request Oct 10, 2026
The Shortcuts settings page and the ⌘/ cheat sheet drew their row labels and
hints from English strings in lib/shortcuts.ts, so a Chinese UI showed a
Chinese page whose every row was English. The page's own chrome (subtitle,
Reset all, the conflict note) had locale entries; the rows under it did not.

The copy moves into the locale files (settings:shortcuts.*) and
lib/shortcutCopy.ts becomes the id -> key map plus the accessors both
surfaces read. One copy is the only way the settings list and the sheet
cannot disagree about what a command is called, and a new ShortcutId is a
compile error until it has a key there.

The registry keeps structure only (ids, groups, bindings, glyphs) and
DOUBLE_SHIFT_MODES / CTRL_TAB_MODES become id lists. English text is
unchanged, word for word, so the specs that assert on it stay valid.

Keys built from a table are invisible to usedKeys.test.ts (literals only) and
to parity.test.ts (en against zh-CN only: a row missing from BOTH passes it),
so shortcutCopy.test.ts pins them, and i18n.e2e.ts gains a case that opens
the page in Chinese and reads the rows back.

Follow-up to #330.

Co-authored-by: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants