Files
solvecraft/docs/localization.md
T
Sloptimum 783ee91404 docs: a new label needs Arabic and Hebrew too (#3457) (#3458)
Arabic and Hebrew are complete catalogs since #3768, so
every_complete_catalog_carries_every_english_message fails a pull request
that adds an English message without them. The localization guide's list of
languages a new label needs now says so.

Co-authored-by: rootarchitect <7781866+rootarchitect@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:00:02 +00:00

160 lines
11 KiB
Markdown

# UI localization
SolidCraft embeds its translations and works offline. English, Arabic, Czech, Danish, German, English (UK), Spanish, French, Galician, Hebrew, Italian,
Japanese, Korean, Malay, Norwegian Bokmål, Dutch, Polish, Brazilian Portuguese, Russian, Turkish, Ukrainian and Chinese are currently registered in `crates/ui-egui/src/i18n.rs`. Adding a language requires an
embedded catalog and one registration there; the preferences selector and validation use the same
registry.
## Messages
`assets/locales/en.json` defines stable message IDs and English fallback text.
Keep IDs when changing wording. Other catalogs use the same IDs; missing messages fall back
to English. Use `app.tr("ui.preferences")` for static messages and
`Locale::format` for full sentences with named placeholders. Translate the entire sentence,
including punctuation, so a translator can reorder its arguments. Placeholder values are
inserted literally, even if a document name contains braces.
The English-label adapter (`app.tr_label`) supports existing engine and UI tables. It maps
display text to a message ID, then uses the same catalog lookup. New UI messages should use
stable IDs directly. The adapter does not change command IDs, model names, or scenario handles.
When a table's English wording changes, update the corresponding English catalog entry too.
Generated browser labels use named templates while their underlying model names stay intact.
Catalogs are parsed once and looked up through ordered maps. This is offline localization,
not a claim of zero runtime overhead. The catalog tests check registered locale IDs,
translation keys, and placeholder consistency. Run `cargo xtask ci` before submitting changes.
## Terms
The [glossary](book/src/reference/glossary.md) gives one English term for each idea, and each
entry has a note for translators: what the term must not be confused with. Its "Words with more
than one meaning" table lists the English words that need a separate catalogue entry for each
meaning (Construction, Flat, Round, Normal, Gap, Clearance).
## English (UK)
`en-GB.json` holds only the English messages whose spelling differs in UK English (Centre,
Customise, Organisation, Licence, colours, …); every other message falls back to English. When an
English message with a US spelling is added, add its UK spelling there too.
## Coverage
A message a locale never received falls back to English silently, so a gap shows up only as a row
that looks untranslated. Two guards catch that in the build:
- `every_complete_catalog_carries_every_english_message` — adding a key to `en.json`
without giving it to a complete catalog fails the tests. English is the source catalog,
Arabic, Hebrew, Japanese and Chinese are delivered complete; Danish, German, Spanish, Galician, Italian, Korean, Malay, Norwegian Bokmål, Polish, Brazilian Portuguese, Russian, French, Ukrainian and
Turkish are partial — their gaps fall back to English and are reported by `cargo xtask i18n` without
failing the build. A new label needs en, ar, he, ja, zh, de and ko; Russian, French, Ukrainian and Malay may
follow later.
- `every_toolbar_tab_and_panel_is_in_the_english_catalog` — tabs and panels reach the UI as bare
source strings, resolved only by the English-label adapter, so a heading missing from `en.json`
can never translate.
Command labels are the same case on a larger scale: every label in the engine's registry now has
an entry under `cmd.<command id>`, and `every_command_label_is_in_the_english_catalog` fails if a
command is added without one.
The context menu and the browser are the same case again, and they carry the most labels of all:
`every_menu_label_is_in_the_english_catalog` opens every menu the app can reach, flips the state
each conditional label branches on (origin shown, bodies locked, bodies hidden) so both branches
are seen, and fails on any label the catalog has never seen. Unit symbols are left out of the
sweep: `mm` is `mm` in every language, so they are not prose to translate. The sweep also asserts
it keeps reaching a broad set of labels, so it cannot pass by finding nothing.
`cargo xtask i18n` prints the same picture across every locale — which messages each one is
missing and which command labels English cannot translate. `--check` turns it into a non-zero exit
for CI. `--add-missing` writes each uncovered label into `en.json` under `cmd.<command id>`, with
the English label as its text, so the label adapter can find it and only the translations are left
to do. One key per distinct label: two commands that share a label (`Trim`, `Extend`) must not both
claim it, or the English-text lookup would silently pick one.
## Rendering and coverage
The Italian catalog uses original translations of SolidCraft's English messages, not text
extracted from commercial CAD software. Its core terminology is Schizzo (Sketch),
Estrusione (Extrude), Raccordo (Fillet), Smusso (Chamfer), Corpo (Body), Componente
(Component) and Vincolo (Constraint). Draft geometry is Sformo, while a report draft
is Bozza; Counterbore is Lamatura and Countersink is Svasatura. Established technical
terms such as Loft, Sweep, Spline and Offset remain unchanged. Command IDs, shortcuts,
file formats, units and user-provided names are not translated.
The Norwegian Bokmål catalog (`nb`, shown as Norsk bokmål) uses Skisse (Sketch),
Kropp (Body), Funksjon (timeline Feature), Binding (Constraint), Avrunding (Fillet)
and Fas (Chamfer). Ground/Unground are Fest posisjon/Frigjør posisjon, Draft is
Slippvinkel, Flat Pattern is Utbretting, and Counterbore/Countersink are Sylindrisk
forsenkning/Konisk forsenkning. Capture Position is Registrer posisjon, while
sketch snapping is Punktfangst. Loft, Sweep, Spline and Offset remain technical
terms. Command IDs, shortcuts, file formats, units and user-provided names stay
unchanged.
One immutable `Locale` type handles all translations. Table widgets without an app reference
use a render scope that restores the previous locale when it exits, including nested renders.
Standalone translated panels enter the same scope. Measure rendered text to size custom
widgets; UTF-8 byte lengths do not describe their displayed width.
The desktop host tries system CJK fonts across Windows, macOS and Linux, the Flatpak host font
folders (`/run/host/fonts`, `/run/host/user-fonts`), the folders listed in `SOLIDCRAFT_FONT_DIRS`
and an optional `CRAFT_FONTS_DIR` font pack, and skips files that cannot be parsed. Chinese uses its own fallback
chain after the general CJK font: many Japanese CJK fonts (Hiragino Gothic, Meiryo, Yu Gothic)
match the CJK font search but lack simplified characters, so a second fallback found by the
Chinese font-name list (`PingFang`, `Microsoft YaHei`, `Noto Sans CJK SC`, …) covers them per
glyph. No fonts are redistributed in native desktop distribution packages; native builds rely
entirely on existing operating system fonts or craft-fonts.
In the browser, `solidcraft-web` bundles a self-hosted Japanese font (`BIZUDPGothic-Regular.ttf`, licensed
under SIL OFL 1.1) within the web distribution artifacts only. The font is loaded asynchronously from the same
origin (`./fonts/BIZUDPGothic-Regular.ttf`) on-demand when the interface language is a CJK one, or, with
any other interface language, the first time Japanese text (kana, kanji or full-width forms in a design name,
file name, body name or typed text) is drawn, without contacting any external CDNs or third-party servers.
A session that never shows such text downloads no font. If font loading fails with a CJK interface, the web
interface shows English for that session only to prevent missing glyphs (tofu); the saved language
preference is kept, so the next visit tries again. The bundled font specifically targets Japanese; it also
renders the kanji Chinese shares with Japanese, while Korean (Hangul) and simplified-only Chinese characters
are not covered, so a self-hosted SC/Korean font bundle remains follow-up work for full web coverage.
## Right-to-left text
egui 0.36 shapes each run of one font with harfrust, so a run of Hebrew letters is drawn right to
left, but it has no bidirectional layout: the runs of a line (words, spaces, numbers, punctuation)
are placed left to right in the order they are stored. A Hebrew sentence stored in reading order
would show its words in the wrong order and its punctuation on the wrong side. `crate::bidi` runs
the Unicode Bidirectional Algorithm (the `unicode-bidi` crate, UAX #9) over every message of a
catalog that has right-to-left text, once, when the catalog is first used, and `Locale::text`
returns the order egui needs: the runs of the line in display order, brackets inside
right-to-left text mirrored, and the letters of each Hebrew run left in reading order for the
shaper to reverse. Each line is its own paragraph and takes its direction from its first strong
letter, so English fallback text is unchanged. `hebrew_messages_land_on_screen_in_display_order`
lays messages out with a real Hebrew font and checks where the glyphs land.
Arabic takes the same path. Its letters join, which the shaper does from the font's own tables, so
each Arabic word is one run: the Arabic font comes after the Latin interface font, which keeps the
spaces, digits and Latin text, and it is the first fallback for an Arabic UI (Segoe UI, Tahoma or
Arial on Windows; SF Arabic, Geeza Pro, Damascus or Tahoma on macOS; Noto Sans or Naskh Arabic,
KACST One or DejaVu Sans on Linux; `NotoSansArabic-Regular.ttf` in `CRAFT_FONTS_DIR`). Write Arabic
messages with Western digits, which keep their left-to-right order inside the sentence.
`arabic_messages_land_on_screen_in_display_order` checks the word order on screen.
Placeholders are kept whole where they stand, so `format` substitutes values into the
display-order text and a value keeps the order it has: a number, a file name, a label that is
already in display order. Translators write normal Hebrew in reading order; they never add
directional marks. When a prefix letter joins a Latin word or a placeholder, use the maqaf
(`ב־{name}`, `ל־STEP`), which is a Hebrew letter to the algorithm, not an ASCII hyphen.
This relies on Hebrew letters and the spaces and punctuation between them coming from different
fonts (the bundled IBM Plex Sans has no Hebrew), which is how the default fonts are set up; an
interface font that covers both Hebrew and Latin puts a whole line in one run and can reverse it.
What this does not do yet: the layout is not mirrored (the browser stays on the left, labels stay
left-aligned), and a message egui wraps onto several lines keeps the order of the whole string, so
its lines read bottom to top. Names people type in Hebrew are shown as stored. The desktop host
appends a system Hebrew font (Arial Hebrew or SF Hebrew on macOS, Segoe UI or Arial on Windows,
Noto Sans Hebrew or DejaVu Sans on Linux); the web build has no Hebrew font yet, so a Hebrew
interface shows empty boxes there.
The current messages support named substitution, not locale-specific plural selection,
number/date formatting, right-to-left layout mirroring, or shaping for joining scripts such as Arabic. Add those capabilities when
introducing messages or languages that need them; use an established message formatter for
plural rules instead of adding language branches to panels. The catalogs cover the UI surfaces
introduced by this PR, not every engine error or every newer command dialog.