- "photo(s){}" printed "photos(s)s"-style text: the plural suffix now follows the noun ("photo{}"),
as the Spanish catalog does; smart previews repeat it ({0}).
- Menu triangles (▸) in translations replaced by → (the shipped fonts have no ▸ glyph; test
translated_catalogs_do_not_use_missing_menu_triangles).
- "{:.0} %" -> "{:.0}%" so the Merging progress test matches every language.
- Translate the two strings main added since the branch (Camera local tone mapping).
- README, docs/localization.md and the LR-BEHAV-LOCALE parity row list French.
- Test: French plural formatting.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
6.0 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), Japanese (ja), Brazilian Portuguese (pt-br), Spanish (es), German (de), Russian (ru) and French (fr). 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,
localization-ja.md, localization-pt-br.md, localization-es.md,
localization-de.md and localization-ru.md.
Adding a language
-
Translate the two catalogs. Copy
locales/ja.jsonandlocales/ja-formats.jsontolocales/<code>.jsonandlocales/<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, andcargo test -p lightcraft-ui-egui i18n::tests -- --nocapturelists the gaps (it fails on an empty entry or mismatched placeholders). The formats catalog must carry every message (the build fails otherwise; a message copied in English, value equal to its key, is an untranslated placeholder until someone translates it), 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. -
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"); PtBr, "pt-br", "Português (Brasil)", "Latn", include_str!("../locales/pt-br.json"); De, "de", "Deutsch", "Latn", include_str!("../locales/de.json"); Ru, "ru", "Русский", "Cyrl", include_str!("../locales/ru.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.rspicks up<code>-formats.jsonby 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. -
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.
-
Run
cargo xtask ci(it includesfmt,clippy, the tests,parityand 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.rsfails the build otherwise), and atr_format!call whose message is in no catalog does not compile: add it to every*-formats.jsonfile.
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.
Release notes
Help ▸ What's New shows docs/whats-new.md, and each of its lines (a heading, or one wrapped line of a
bullet, without the - or ### ) is looked up like any message, so a catalog can translate the notes
line by line (German does). A line that is re-wrapped or edited no longer matches its translation and
shows in English until the catalog is updated; untranslated lines are reported by
display_label_gaps_are_reported, never failed.