A one-stop-shop capsuleer manager for EVE Online pod.aaronmallen.dev
pod docs dev i18n.md
4.6 kB · 94 lines
Markdown
at main

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.rs declares #[macro_use] extern crate rust_i18n; (exposing the t! macro crate-wide) and calls rust_i18n::i18n!("assets/locales", fallback = "en"); at the crate root.
  • src/i18n.rs exposes i18n::set_locale(language: Language), which maps the Language enum to its rust-i18n locale string via Language::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() calls i18n::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 en value (the fallback = "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) #

  1. Add or extend assets/locales/<feature>.en.toml with the keys you are introducing, grouped under their namespace table.
  2. Replace text("literal") with text(t!("<feature>.<area>.<key>")) and format!(...) user-facing strings with t!("...", name => value).
  3. Leave the non-en fragments to the translation pass — author only the English baseline.

See src/app.rs notification chrome (shell.notifications.*) for the reference conversion.