lightcraft/docs/localization.md

4.2 KiB

Localization

The interface ships in the language it is written in (English) plus every language in the table in crates/ui-egui/src/i18n.rs. Today that is English, Simplified Chinese (zh-hans) and Japanese (ja). This file is the reference for adding or maintaining a language; the per-language notes are in localization-zh-hans.md and localization-ja.md.

Adding a language

  1. Translate the two catalogs. Copy locales/ja.json and locales/ja-formats.json to locales/<code>.json and locales/<code>-formats.json, then translate the values. Keys are the English source strings and must stay byte-for-byte identical — cargo test -p lightcraft-ui-egui i18n::tests fails on a missing, extra or empty entry.

  2. Add the table entry in crates/ui-egui/src/i18n.rs:

    language_table! {
        En, "en";
        ZhHans, "zh-hans", "简体中文", "Hans", include_str!("../locales/zh-hans.json");
        Ja, "ja", "日本語", "Jpan", include_str!("../locales/ja.json");
    }
    

    The fields are the BCP-47 code (also the settings-file value), the endonym shown in the Language menu, the ISO 15924 script (Latn, Jpan, Hans, Hant, Kore…), and the embedded catalog. build.rs picks up <code>-formats.json by name, so the macro and the format lookup follow automatically — nothing else in the UI needs to change. The menus, the settings row, the keyboard-shortcut sheet and the control channel all read the table.

  3. Make sure the glyphs exist. The script drives which craft-fonts faces the UI installs: a language whose script has no face in the build shows boxes (the test says so instead of failing). Fonts live in storytold/craft-fonts, never here; a missing face is added there, not worked around in this repository.

  4. Run cargo xtask ci (it includes fmt, clippy, the tests, parity and the wasm check).

How a message is found

tr("Exposure") looks the English label up in the active language's catalog and returns the source text unchanged when there is no entry; tr_format! handles text with runtime values. Both are presentation-only: command ids, file names and user-entered metadata are never translated.

tr_format! is generated by crates/ui-egui/build.rs. It formats the English source at the call site, which is what gives format! a literal to check against that call site's own arguments, and it also names those values so a translation can place them:

tr_format!("Added {n} photo{} to “{}”", if n == 1 { "" } else { "s" }, album, n = n)
  • A translation refers to a value by name exactly as the English source spells it ({n}), or by position as _1, _2, … in call-site order. Reordering is fine: "已将 {n} 张照片添加到“{_2}”".
  • The English plural suffix ("s" / "") is simply not referenced in languages without plurals.
  • Every translation is placeholder-checked at compile time: an unknown name, or a spec that does not match its value, fails the build rather than printing {} at runtime.
  • A source string with no translation falls back to the English text, values included.

Testing and looking at it

cargo test -p lightcraft-ui-egui i18n::tests                       # catalogs, keys, placeholders, commands
CRAFT_FONTS_DIR=../craft-fonts cargo test -p lightcraft-ui-egui i18n::tests   # + glyph coverage

The glyph tests are skipped without CRAFT_FONTS_DIR (there are no CJK faces to check then). To look at a language, render it headless:

LIGHTCRAFT_LANGUAGE=zh-hans lightcraft-cli snapshot --demo --script tour.jsonl -o out.png --size 1600x1000

or, with the app running (--control 7980), run the language command (engine.execute {"command": "app.language.zh-hans"}) and take a screenshot.

Language codes and settings

The settings file stores the BCP-47 code, never the Rust variant name, so a language can be renamed in code without invalidating anyone's saved preference. LIGHTCRAFT_LANGUAGE accepts what a system locale looks like (zh_Hans, zh-CN, en_US, ja_JP.UTF-8) and falls back to the base language when a region has no dedicated entry.