Repository navigation
Conversation
Introduce a gettext-style i18n layer: the English source string is the
key, so `t()` falls back to the key when a translation is missing and the
app stays English-only on surfaces not yet covered. Coverage can then grow
one screen at a time instead of in one big migration.
The preference (`auto` | `en` | `zh-Hans`) follows the OS by default, the
same way the color scheme does. It persists to localStorage and fires a
change event that `useT()` subscribes to through `useSyncExternalStore`.
Tags are `zh-Hans`/`zh-Hant` rather than a bare `zh`, so a Traditional
table can be added later without re-tagging stored values.
First slice, on the General settings page:
- a Language row with an Auto / English / Simplified Chinese control
- the settings search matches localized labels (typing 语言 finds the row)
and localizes section labels and descriptions
The macOS menu bar lives outside the webview and cannot read localStorage,
so the frontend pushes the resolved tag to Rust with `set_menu_language`
on boot and on every change; the native menu and the dock menu are rebuilt
from a mirror of that decision. The Edit menu's standard items take
explicit text because Tauri's defaults are English.
A guard test scans the source for `t("…")` calls and fails when a key has
no Chinese entry, so a new call cannot silently ship English.
Making the menu builder language-aware dropped this command's #[tauri::command] attribute. Linux/Windows never noticed because the registration in lib.rs is cfg-gated to macOS, so the generated __cmd__ symbol was simply not referenced; macOS failed to compile with "cannot find __tauri_command_name_keybindings_set_overrides in menu". Restore the attribute and the macOS-only gate, matching main.
The guard test shelled out to grep, which Windows runners do not have: it found zero keys and failed with "expected 0 to be greater than 0". Walk the source tree with fs/readdirSync and match in-process so the test is platform-independent.
SeniorHan
force-pushed
the
feat/i18n-layer
branch
from
October 11, 2026 18:57
e06fe2d to
dc4ce20
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft, not for review yet — opening this so the design is visible and CI can build it.
What this is
A gettext-style i18n layer: the English source string is the key, so
t()fallsback to the key itself when a translation is missing. The app stays English on
surfaces that aren't covered yet, and coverage can grow one screen at a time
instead of in one big migration.
This is the same design as upstream PR hardbeat920#244, re-implemented cleanly on current
main(hardbeat920#244 is 68 commits behind). One deliberate difference: language tags arezh-Hans/zh-Hantrather than a barezh, so a Traditional Chinese table canbe added later without re-tagging stored values.
main's design precedent here isPR hardbeat920#244's
Lang::from_tag, which maps everyzh*to Simplified — that's the bugreported on hardbeat920#243.
Included in this PR (first slice)
src/shared/lib/i18n.ts— the table,t()with{name}placeholderinterpolation, and
resolveLanguage()(autofollows the OS, same as the colorscheme does).
src/shared/hooks/useI18n.ts—useT()/useLanguage()overuseSyncExternalStore, with astoragelistener so other windows stay in sync.labels and descriptions localize too.
The menu bar lives outside the webview and can't read localStorage, so the
frontend pushes the resolved tag to Rust via
set_menu_language; the Rust sidemirrors the decision and rebuilds. The Edit menu's standard items take explicit
text because Tauri's defaults are English.
t("…")calls and fails when a key has noChinese entry, so a new call can't silently ship English.
Verification
npm run check:web(5209 tests) +npm run check:rust(602 tests),clippy
-D warnings,cargo fmt --check, and a productionvite build.cargo check,cargo clippy -D warnings,cargo fmt --check,cargo test --lib(648 passed). This is where a dropped#[tauri::command]on a macOS-gated command surfaced — fixed in the secondcommit; Linux could not catch it because the registration is cfg-gated.
Not verified: no full
.appwas built or launched (the Mac here has onlyCommand Line Tools, not Xcode). The menu code compiles and passes clippy/tests;
the menu behavior has not been run end-to-end.
Scope is deliberately small
Only the language row itself is translated. Everything else in Settings and the
rest of the app remain English until we know whether upstream wants this at all —
see the open question on hardbeat920#243 about which of the two i18n PRs should land.