Files
lightcraft/docs/localization.md
T
goujichengandDeepSeek V3.2 22eb4d3515 LR-BEHAV-LOCALIZE: a language table, Simplified Chinese, and per-script font fallback
The localization layer was hardcoded to English and Japanese: an enum with two
variants, an `is_japanese()` gate compiled into the format macro, and one
catalog pair. Adding a language meant editing the menus, the settings row, the
checkmark logic, the format macro and the font stack — each in a different place.

A language is now one entry in `i18n::language_table!`: BCP-47 code, endonym,
ISO 15924 script and catalog. The Language menu, the settings row, the shortcut
sheet and the control channel are all built from that table, and `build.rs`
picks up `locales/<code>-formats.json` by name, so a further language is a table
entry plus two catalogs (docs/localization.md).

Simplified Chinese (`zh-hans`) ships as the second translation: `zh-hans.json`
(1019 entries, key-set and placeholder-identical to `ja.json`) and
`zh-hans-formats.json` (102 entries). Its glyphs come from craft-fonts' Noto
Sans CJK SC, whose `Hans` faces the UI now installs *before* the Japanese ones
for a Chinese reader, so shared Han characters keep the language's forms
(`fonts::cjk_fallback`); the export watermark accepts every CJK face.

`tr_format!` keeps its compile-time check. It formats the English source at the
call site — a runtime template cannot be handed to `format!` — and names those
values so a translation can place them by name (`{n}`) or position (`{_1}`).
Every translation is still checked by `format!`, so a translation with an
unknown placeholder fails the build. The Japanese catalog was rewritten from the
old `{:.0}` plural-discard trick to that by-name form; behaviour is unchanged.

Also: settings persist the BCP-47 code (never the Rust variant name),
`LIGHTCRAFT_LANGUAGE` accepts system-style tags (`zh_CN`, `ja_JP.UTF-8`),
`xtask parity` reads the second command table, and the glyph tests cover every
catalog character plus the interface symbols (`▸`/`▾` turned out to have no
glyph in any installed font and are gone from the Chinese text).

Chinese and Japanese both need `CRAFT_FONTS_DIR` as before; the web build still
embeds only the Japanese face (Noto Sans CJK SC alone would push the wasm past
Cloudflare's 25 MiB per-file cap), documented in docs/localization-zh-hans.md.

Co-Authored-By: DeepSeek V3.2 <noreply@deepseek.com>
2026-10-07 12:22:15 +08:00

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.