Files
lightcraft/docs/localization.md
ca0c569863 LR-BEHAV-LOCALIZE: Traditional Chinese (Taiwan) UI, stock preset and date localisation
Port of PR #198 onto the language table:
- Traditional Chinese (Taiwan) catalogs as a table entry: code zh-hant, name
  繁體中文(台灣), script Hant; zh-TW / zh-HK / zh-Hant tags reach it; command
  app.language.traditionalChinese
- display-only helpers: builtin_label (stock preset/profile names), history_label
  (generated history steps, user preset names verbatim), source_label (library
  headings, album names verbatim), date_group_label / display_time
- menu, dialog and panel strings (profiles, presets, curves, crop, versions,
  history, grid, date sidebar, About links) go through the catalogs
- a language command and ui.set apply the language at once; a native-menu test
  checks that every language switch rebuilds the menu titles

Adapted while porting (Claude):
- date headings and capture times are localised for every language from its
  *-formats.json date patterns, not only for zh-TW; ja and zh-hans gain the date
  patterns, weekday names and "Unknown Date", and zh-hans the new menu messages
  (ja's come from the PR)
- plain catalogs may cover different messages (English fallback); the test
  reports the gaps instead of failing. The test requiring every engine command
  label to be translated is dropped
- preset groups stay grouped by name: a user preset in a stock-named group is
  not split into a separate group (the group header is translated when it holds
  stock presets); the PR's split was a side effect of labelling
- no Traditional Chinese face in craft-fonts yet: Hant prefers the Simplified
  Chinese face (which carries the Traditional characters) over the Japanese ones
- zh-hant translations for the messages main added since the PR branched

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 06:54:26 -04:00

4.9 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), Traditional Chinese (zh-hant, Taiwan) and Japanese (ja). This file is the reference for adding or maintaining a language; the per-language notes are in localization-zh-hans.md, localization-zh-hant.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. The plain catalog may grow at its own pace: a message it lacks shows in English, and cargo test -p lightcraft-ui-egui i18n::tests -- --nocapture lists the gaps (it fails on an empty entry or mismatched placeholders). The formats catalog must carry every message (the build fails otherwise), including the date patterns ({year}, {month} {year}, {weekday}, {day} {month} {year}…) that date headings and capture times use in every language but English; weekday names go in the plain catalog.

  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");
        ZhHant, "zh-hant", "繁體中文(台灣)", "Hant", include_str!("../locales/zh-hant.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: one arm per English message, which formats the active language's translation with format! (English is the fallback arm). Every catalog's format string is therefore checked by Rust against the call site's own arguments:

tr_format!("Added {n} photo{} to “{}”", if n == 1 { "" } else { "s" }, album, n = n)
  • A translation is an ordinary Rust format string over the same arguments. Refer to named values by name ({n}) and to positional ones by index when the word order changes ({0}, {1}): "已将 {n} 张照片添加到“{1}”{0:.0}". Keep the source's spec ({:.1}, {d:+}, {:.0}%).
  • format! rejects an unused argument, so a value a language does not need — the English plural suffix ("s" / "") — is consumed as {:.0} (a string at precision 0 prints nothing).
  • Every formats catalog carries the same messages (build.rs fails the build otherwise), and a tr_format! call whose message is in no catalog does not compile: add it to every *-formats.json file.

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.simplifiedChinese"}) 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.