Skip to content

feat(i18n): localization foundation and language picker - #57

Merged
noahbclarkson merged 2 commits into
mainfrom
feat/i18n-foundation
Oct 19, 2026
Merged

noahbclarkson merged 2 commits into
mainfrom
feat/i18n-foundation

Conversation

@noahbclarkson

@noahbclarkson noahbclarkson commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Foundation for #55: the rgitui_i18n crate and the language setting. #89's Simplified Chinese translations are being ported onto this in a follow-up PR (stacked on this branch), and git operation messages in another after that.

Rebased onto main (0.5.0) and reworked after reviewing it against #89. The original commit is kept as the first of two; the second carries the review fixes.

What works

  • Settings → Appearance → Language. Lists Match system language plus every locale found, each written in its own script, with a count of strings it still leaves in English.
  • Switching applies immediately, with no restart.
  • New installs and existing settings.json files follow the OS language. A missing language key defaults to system.
  • Simplified Chinese ships as the first translation. Only the Appearance panel is converted here.

Design

English is Rust, translations are JSON. crates/rgitui_i18n/src/en.rs is the source of truth, one key => message line per entry. Translations live in assets/locales/*.json, embedded at build time. User files in <config>/rgitui/locales/ are layered over a shipped locale with the same id, so a translator can fix a few strings against a release build.

Keys are checked at compile time. t!, tp! and key! take literal keys and expand to const assertions against the English table:

t!("settings.nav.genral")
// error: evaluation panicked: unknown translation key "settings.nav.genral": add it to crates/rgitui_i18n/src/en.rs
t!("settings.language.system_detail", lang = id)
// error: the arguments to t!("settings.language.system_detail") do not match the placeholders in its English message

This keeps #89's main advantage (rustc catches key typos) without putting translations in Rust. Lookup is a binary search. An unsorted or duplicated en.rs fails the build and names the first key out of place. key! checks a key stored as data, for menus and filter lists that are translated later.

set_language(cx, …) refreshes every window and replaces an ActiveLanguage GPUI global. Anything translated in render updates on its own. Views that keep text outside render (input placeholders) observe ActiveLanguage, and only that, instead of every settings change.

Plurals: tp!("key", n) picks .one / .other under the locale's rule and fills {count} itself.

Changes since the original commit

  • Traditional Chinese no longer gets Simplified. zh-TW, zh-HK, zh-MO and zh-Hant matched zh-CN on the primary subtag. Matching now requires the same script, stated or implied. POSIX tags (zh_CN.UTF-8) are accepted.
  • A complete zh-CN no longer reports "1 string not translated". Plural forms a locale's rule never selects aren't counted.
  • No per-render work for the picker. The system language and the missing counts are computed once at init.
  • Loader:
    • User files must be regular files under 1 MiB, so a FIFO named *.json can't hang startup.
    • Files are read in path order.
    • Keys English doesn't define are logged.
    • An unknown plural_rule is an error instead of silently becoming one_other.
  • Tests:
    • Shipped locales must use exactly the English placeholders (it used to be one-way), and only the plural forms their rule selects.
    • A test reports English keys no code uses.
  • CJK fonts. The UI font gains Simplified Chinese fallback families when the UI is Simplified Chinese, and only then, so Japanese or Traditional Chinese text in an English UI keeps the platform's glyph choice.
  • Settings view. It reads the selection from rgitui_i18n instead of keeping a second copy. "Detected:" shows the matched language's own name, or says the system language has no translation yet.
  • zh-CN. Applied @aiysya's wording fix: {count} 条尚未翻译.
  • Housekeeping. Crate version 0.5.0; CHANGELOG entry under [Unreleased].

Verified

  • cargo fmt --check: clean.
  • cargo clippy --workspace --all-targets -D warnings: clean.
  • cargo test -p rgitui_i18n -p rgitui_settings -p rgitui_workspace: 50 + 31 + 771 passed.
  • Confirmed by hand that each kind of mistake fails to compile: unknown key, missing arguments, misnamed argument, unknown plural stem, and an unsorted table.

Known gaps

  • The settings window doesn't use the workspace's UI font, before or after this PR. Language names in other scripts there rely on the platform's font fallback.
  • Relative times ("3d ago") are still built by hand in English. That's covered in the port PR.

🤖 Generated with Claude Code

@aiysya

aiysya commented Aug 18, 2026

Copy link
Copy Markdown

@noahbclarkson I'm a native Simplified Chinese speaker — happy to review, done.

The translation is in good shape: terminology is consistent (Appearance = 外观,
Light/Dark = 浅色/深色), the renderings are natural rather than literal
(auto-detect → 自动跟随系统, Color Theme → 配色主题), and the full-width
punctuation is used consistently. I found it very usable as-is.

One suggestion, purely idiomatic polish (not a bug): settings.language.missing.other
"{count} 条文本尚未翻译" — the collocation 条文本 reads a bit stiff. I'd write
"{count} 条尚未翻译" instead.

Two things worth flagging from your own "Known gaps" section, since they hit
Chinese users first:

  • The bundled fonts have no CJK glyphs. On Windows this falls back to the system
    font and is usually fine, but it's untested on the other platforms — I'd
    prioritize the per-locale default ui_font follow-up.
  • The zh-CN file is other_only, which is correct for Chinese — no action needed
    there, just confirming.

noahbclarkson and others added 2 commits October 15, 2026 21:17
Adds `rgitui_i18n` and wires it into settings, so a language can be chosen
and applied without a restart. This is the infrastructure only — the
Appearance settings panel is converted as the working example; the other
~1500 user-facing strings are still English literals.

English is the source of truth in `src/en.rs` rather than a JSON asset, for
the same reason the built-in themes are Rust: it is always present and
cannot drift, so any missing translation has something to fall back to.
Translations are JSON under `assets/locales/`, embedded at build time, with
user overrides from the config directory so a translator can iterate against
a release build.

The catalogue is a process-wide RwLock rather than a GPUI global, because
error strings are built on background threads and need translating too. The
tradeoff is that GPUI cannot know a language change dirties the frame, so
`set_language` must be followed by `cx.refresh_windows()`.

`t!` and `tp!` return `SharedString`, which every `rgitui_ui` builder already
accepts — converting a call site is a one-line change.

Locale files are validated by tests: unknown keys fail (they would silently
never render), placeholders must survive translation, and plural forms must
be declared in pairs. Missing keys are deliberately allowed so partial
translations are worth shipping.

Simplified Chinese ships as the first translation and needs native review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the loader

Keys: t!, tp! and the new key! take literal keys and evaluate const
assertions against the English table, so a key missing from en.rs, or named
arguments that do not match the English message's placeholders, fail the
build at the call site instead of rendering the raw key. tp! fills {count}
itself. en.rs is one `key => message` line per entry, and an unsorted or
duplicated table fails the build naming the first key out of place, which is
what lets the lookup be a binary search. A test reports English keys that no
code uses.

Language changes: set_language takes the App, refreshes every window and
replaces an ActiveLanguage global, so callers no longer have to remember
refresh_windows, and views that keep translated text outside render can
observe it. The settings view reads the selection from rgitui_i18n instead of
keeping its own copy.

Matching: Traditional Chinese systems (zh-TW, zh-HK, zh-MO, zh-Hant) matched
the shipped zh-CN on the primary subtag alone and got Simplified characters;
matching now requires the same script, stated or implied. POSIX tags such as
zh_CN.UTF-8 are accepted. The system language and per-locale missing counts
are read once at init rather than on every render, and the count no longer
includes plural forms a locale's rule never selects, so a complete zh-CN
stops reporting one missing string.

Loader: user locale files must be regular files under 1 MiB, are read in path
order, and are layered over a shipped locale with the same id instead of
replacing it; keys English does not define are logged. An unknown plural_rule
is an error rather than silently becoming one_other. Shipped locales must use
exactly the English placeholders and only the plural forms their rule selects.

The UI font gains Simplified Chinese fallback families when the UI is
Simplified Chinese, and only then, so Japanese and Traditional Chinese text in
an English UI keeps the platform's glyph choice. The cached font is keyed on
the locale as well as the family.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@noahbclarkson

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T08:38:34.106983Z 4bc9034 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 4bc90344d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@noahbclarkson
noahbclarkson merged commit 989b92f into main Oct 19, 2026
4 checks passed
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