Skip to content

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

Open
noahbclarkson wants to merge 1 commit into
mainfrom
feat/i18n-foundation
Open

feat(i18n): localization foundation and language picker#57
noahbclarkson wants to merge 1 commit into
mainfrom
feat/i18n-foundation

Conversation

@noahbclarkson

Copy link
Copy Markdown
Owner

Foundation for #55. This is steps 1–2 of the plan on that issue: the i18n crate
and the language setting. It deliberately does not convert the rest of the
UI — that is the follow-up work, and it is mechanical once this lands.

What works now

  • Settings → Appearance → Language. Lists "Match system language" plus every
    locale found, each labelled in its own script, with a count of how many strings
    it still leaves untranslated.
  • Switching applies immediately. No restart.
  • New installs follow the OS language, and so do existing settings.json
    files, which have no language key and default to system.
  • The Appearance panel is translated as the working example. Everything else
    is still English literals.
  • Simplified Chinese ships as the first translation.

Design

English is Rust, translations are JSON. crates/rgitui_i18n/src/en.rs is the
source of truth — same reasoning as rgitui_theme::builtin_themes: it is always
present and cannot drift from an asset file, so every missing translation has
something to fall back to. Translations live in assets/locales/*.json, embedded
at build time, with user overrides from <config>/rgitui/locales/ so a
translator can iterate against a release build without rebuilding.

t! returns SharedString. Every rgitui_ui builder already takes
impl Into<SharedString>, so converting a call site is a one-line change with no
API churn:

Label::new("Color Theme")                        // before
Label::new(t!("settings.appearance.theme.label"))   // after

The catalogue is a process-wide RwLock, not a GPUI global. Error messages
are built on background threads and will need translating too, and a GPUI global
is unreachable from there. The cost is that GPUI cannot know a language change
dirties the frame, so set_language must be followed by cx.refresh_windows();
this is documented at both ends and is the only manual step callers have.

Plural support is in from the starttp!("key", count) selects a
.one/.other form under the locale's declared rule (one_other,
zero_one_other, other_only). Retrofitting this later would have meant
revisiting every count-bearing string a second time.

What the tests enforce

cargo test -p rgitui_i18n (35 tests) validates every file in assets/locales/:

  • Unknown keys fail the build. A key not in en.rs is a typo or a leftover
    from a deleted message, and would silently never render.
  • Placeholders must survive translation. If English has {count}, the
    translation must too. Surrounding text may be reordered freely.
  • Plural forms must be declared in pairs in English.
  • Missing keys are allowed on purpose. They fall back to English, so a
    partial translation is worth shipping — that is the whole contribution model.
  • The English table must stay sorted and duplicate-free, so translation diffs
    stay readable.

Verified

cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, and
cargo test --workspace all pass. Booted the app and confirmed the bundled
locale loads and that system and an explicit zh-CN both resolve as intended.

I did not visually confirm the Chinese UI renders — see below.

Known gaps, for follow-ups

  • CJK glyphs are not bundled. assets/fonts/ has IBM Plex Sans, JetBrains
    Mono and Lilex; Chinese depends on system font fallback. Untested on all three
    platforms, and may need a per-locale default ui_font.
  • The zh-CN translation needs a native speaker's review. I wrote it; it is
    plausible UI vocabulary, not verified. @aiysya, if you are up for it, this is a
    small file and your read on it would be worth a lot.
  • Errors should not be translated at the source. The ~50 anyhow! sites in
    rgitui_git run on background threads and feed 95 show_toast calls. The
    right move is typed error variants translated at the toast boundary, not t!
    scattered through the git layer. Separate PR.
  • Relative time is still hardcoded Englishformat_relative_time in
    rgitui_graph/src/lib.rs builds "{}h ago" by hand.
  • Longer languages will clip. German and Russian run 30–40% longer than
    English; the fixed author_column_width / date_column_width defaults and the
    sidebar buttons have not been checked against that.

Closes nothing yet — #55 stays open until the UI is actually converted.

🤖 Generated with Claude Code

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>
@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.

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