Files
figcraft/docs/translating.md
T
bflatastic 159de3ce43 M9: Japanese and Korean draft catalogs and CJK UI fonts
- **Japanese (`ja`) and Korean (`ko`) draft catalogs** (`crates/ui-egui/locales/ja.toml`, `ko.toml`): every UI string (1,351 keys), command label (519) and menu wording (29), machine-drafted from our English (Japanese: concise, no desu/masu endings, full-width punctuation where Japanese UI text uses it; Korean: no hamnida endings, instructions in -seyo). Placeholders identical to English, `|context` keys translated per context, plural entries give only `other`. Marked `status = "draft - needs native review"` and registered as drafts (not in `COMPLETE`), like fr/de/pt-BR.
- **Plurals:** `plural_one` returns false for ja and ko (always "other").
- **Language picker and detection:** Preferences > Language lists the two languages by their own names; System picks `ja*` / `ko*` from the OS language list; `pref.language` doc lists `ja|ko` (engine spec and docs/mcp.md).
- **CJK UI fonts** (`crates/ui-egui/src/cjk.rs`): egui's fonts and Inter have no CJK glyphs. After the existing system font scan, the desktop app works out which CJK scripts (kana, Hangul, Han) the UI shows: the UI language's, the language picker's while Preferences is open, typed/IME text, and the open file's names and comments (re-read when the tab's revision changes, not mid-drag, and never again once all three scripts have a face). For each needed script it appends one face to egui's fallback list, the UI language's script first: Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux/BSD Noto Sans CJK, Source Han Sans, Nanum, WenQuanYi; the craft-fonts build's embedded BIZ UDPGothic is tried first (free). Han without kana outside a Japanese/Korean UI reads as Chinese (Chinese faces first). English users load no CJK face. No font files are added to the repo.
- **Read once, share the bytes:** new `FontDb::find_one` reads only the one face a fallback needs instead of every file of the family (Yu Gothic UI is 4 files of ~14 MB); `theme::font_data` hands egui the font database's shared bytes instead of copying them (also used for the existing Hebrew/Arabic fallbacks). The scan's existing 64 MB per-file cap applies. Any failure degrades to the previous behaviour (tofu at worst), never a panic.
- **Web:** no system fonts there. `crates/text/build.rs` now embeds craft-fonts' Noto Sans CJK into the wasm build when `FIGCRAFT_EMBED_CJK` is set (it covers kana, Hangul and Han; +16 MB) and reruns when that variable changes. Without it, `cjk::can_show` keeps ja/ko out of the picker and out of System detection, so Japanese and Korean browser users keep the English UI instead of boxes. Nothing is fetched from the network.
- **Docs:** localization-parity (rows, CJK UI fonts section, totals, revision), translating (catalog list, CJK conventions, fonts, plural note), target-app-parity, ROADMAP, gaps (gap 5), roadmap (focus 4), mcp.md. Localization 30% -> 35% (graded low: unreviewed drafts), 40-65 h -> 32-55 h; ready for real work ~52% -> ~53% (52.9 -> 53.1 by the dimension table); full parity ~284-554 h -> ~276-544 h; beta ~162-292 h -> ~155-280 h.
2026-10-12 04:41:28 +07:00

5.3 KiB

Translating FigCraft

FigCraft's interface strings live in one TOML catalog per language in crates/ui-egui/locales/. The build bakes every catalog into the binary (desktop and web); nothing is read at run time. Preferences ▸ Language picks the language and the interface switches at once. System takes the first of the OS's preferred languages that has a catalog (the browser's language on the web): a regional tag reads its language's catalog (fr-CA → fr), and a language with only a regional catalog reads that one (pt-PT → pt-BR). pref.language {language} does the same from the CLI, MCP and the control channel.

The catalogs: English (en, the source), and Spanish (es), French (fr), German (de), Brazilian Portuguese (pt-BR), Japanese (ja) and Korean (ko), machine-drafted and waiting for native speakers' review.

The catalogs

  • en.toml lists every key. A key is the English text as the code writes it, so the code stays readable: tr("Fill"), trf("Delete {name}?", &[("name", &name)]), trn("{n} layers", count, &[]). Values equal their keys except plurals, which give both forms: "{n} layers" = { one = "{n} layer", other = "{n} layers" }.
  • es.toml and other languages translate the same keys under [ui], plus command labels by command id under [commands] (the Actions palette, the shortcuts panel and the menus) and, under [menu], the few menu items the main menu words differently from the command (the Select matching submenu's "Fill" for "Select same fill"). Command ids, the CLI, MCP and control-channel output stay English.
  • [meta] name is the language's own name (the picker shows it untranslated); [meta] status marks a draft (the Preferences window says so).

A key a catalog lacks shows its English text, so a partial catalog is safe to ship.

Typography follows each language: French puts a no-break space (\u00A0 in the TOML) before : ; ? !, inside « » and before %; German uses „ “ and formal address (Sie); Brazilian Portuguese uses Brazilian words (arquivo, salvar, tela, você), not European ones. Japanese is concise, without です/ます endings, with full-width ()「」:、。 where Japanese UI text has them; Korean labels end without 합니다 (instructions in -세요). Neither has plural forms.

CJK text needs a CJK font, which egui and Inter lack: the desktop app adds an installed one when something on screen needs it (see crates/ui-egui/src/cjk.rs). The web build has no system fonts, so Japanese and Korean appear there only in a build with craft-fonts and FIGCRAFT_EMBED_CJK=1 (Noto Sans CJK, +16 MB); otherwise they are left out of the picker. A new CJK or other non-Latin language also needs its script in cjk::language.

Adding a language

  1. Copy crates/ui-egui/locales/es.toml to <code>.toml (a BCP 47 code: fr, pt-BR, ja) and set [meta] name and status = "draft — needs native review".

  2. Translate every value; keep the keys. Keep {placeholders} exactly, "…" where English has it, brand names and keyboard glyphs. English labels of commands: figcraft-cli commands.

  3. Plurals: give one only if the language has a singular form (Japanese, Korean and Chinese give only other, and plural_one returns false for them). trn picks one for a count of 1, and also for 0 in French and Brazilian Portuguese (keep {n} in their one forms); a language with other rules needs its rule in plural_one in crates/ui-egui/src/i18n.rs.

  4. To require full coverage, add the code to COMPLETE in crates/ui-egui/src/i18n_tests.rs. Every later PR that adds a string then has to translate it into each complete language, so only Spanish is there: French, German, Brazilian Portuguese, Japanese and Korean are complete today but registered as drafts, and a string added later shows English in them until it is translated. To list what a draft lacks, add its code to COMPLETE locally and run the check.

  5. Run the check and look at the panels in the app (longer words must fit the right panel):

    cargo test -p figcraft-ui-egui i18n
    

    Dropdowns, the right panel's two-column labels and the home cards cut a label that doesn't fit with "…" and show it in full on hover; elsewhere, look for clipped or wrapped text.

The check fails on keys the code uses that en.toml lacks (it prints the lines to paste), keys no code uses, unknown command ids, placeholders that don't match English, and, for complete languages, anything untranslated. Machine-drafted catalogs stay marked as drafts until a native speaker has reviewed them.

Adding strings in code

Wrap every string a user sees or a screen reader hears: tr for plain text, trf for {named} placeholders, trn for counts, tr_noop for literals in const tables that are looked up later with tr(label). The key must be a string literal as the first argument (the check reads the source). Don't wrap ids, command ids, JSON, log messages, file extensions or document content. Then add the key to en.toml and to each complete language (Spanish); the drafts may follow later.

Labels from the engine and document crates (tool names, blend modes, easing curves…) are looked up at display with tr(x.label()); they are listed in dynamic_labels in i18n_tests.rs.