Internationalization (i18n) #
Pod localizes its own UI chrome with rust-i18n. Translation strings live in
TOML files under assets/locales/. The framework is wired in src/main.rs (the i18n! macro) and the
active locale is set once at startup from the configured language.
How it is wired #
src/main.rsdeclares#[macro_use] extern crate rust_i18n;(exposing thet!macro crate-wide) and callsrust_i18n::i18n!("assets/locales", fallback = "en");at the crate root.src/i18n.rsexposesi18n::set_locale(language: Language), which maps theLanguageenum to its rust-i18n locale string viaLanguage::esi_code()(the enum codes —en,de,en-us,es,fr,ja,ko,ru,zh— are exactly the locale strings rust-i18n expects).app::boot()callsi18n::set_locale(accessibility.language())once at startup, alongside the existing high-contrast and scale application. A language change applies at the next restart (see ADR-0041); nothing swaps the locale live mid-session.
Locale file layout — one file per locale #
Each locale is a single file, assets/locales/<locale>.toml, holding every feature's strings for that locale. rust-i18n
globs assets/locales/**/*.{toml,yml,yaml} and reads the locale from the filename stem, so assets/locales/en.toml is
the en locale, assets/locales/fr.toml is fr, and so on:
assets/locales/
en.toml # English baseline (the fallback locale)
en-us.toml # exact copy of en, US spelling
de.toml # German
fr.toml # French
es.toml ru.toml ja.toml ko.toml zh.toml
Within a file, group keys under their feature/area namespace as TOML tables. The distinct namespaces keep features
from colliding in the one merged key map per locale (wallet.*, shell.*, common.*, and so on).
assets/locales/en.toml is the authored baseline. en-us is an exact copy of en with US spelling; the other seven
are machine-translated. The locale-parity check (mise run lint:i18n) enforces that every locale carries the same key
set with the same %{...} placeholders.
Key-namespace convention #
Keys are dot-separated and namespaced by their origin:
| Pattern | Use for | Example |
|---|---|---|
<feature>.<area>.<key> |
feature-module UI strings | wallet.journal.title |
config.<enum>.<variant> |
user-facing labels on domain enums | config.feature.wallet |
common.<key> |
shared, cross-feature strings | common.cancel |
In a TOML fragment, group keys under their namespace as a table so the file stays a small, append-only surface:
[shell.notifications]
mark_all_read = "Mark all read"
title = "Notifications"
<feature> is the module name (wallet, assets, calendar, industry, mail, roster, skills, settings,
shell, splash, ...). <area> groups within a feature (journal, transactions, ...). Keep variants and keys
alphabetical, matching the project's ordering convention.
Interpolation convention #
Use rust-i18n named placeholders — %{name} in the locale value, and name => value (or name = value) when
calling t!. Never build user-facing strings with format!; interpolate through t! so word order can change per
language.
# assets/locales/shell.en.toml
[shell.notifications]
footer_unread = "%{count} unread"
// returns a Cow<str>; call .into_owned() / .to_string() when an owned String is needed
let label = t!("shell.notifications.footer_unread", count => total);
Missing-key behavior #
- A key missing in the active locale resolves to the
envalue (thefallback = "en"chain). - A key absent from every locale resolves to the key string itself and never panics.
Both are covered by unit tests in src/i18n.rs.
Extracting strings (the per-feature task) #
- Add or extend
assets/locales/<feature>.en.tomlwith the keys you are introducing, grouped under their namespace table. - Replace
text("literal")withtext(t!("<feature>.<area>.<key>"))andformat!(...)user-facing strings witht!("...", name => value). - Leave the non-
enfragments to the translation pass — author only the English baseline.
See src/app.rs notification chrome (shell.notifications.*) for the reference conversion.