Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Text and localization

Every piece of player-facing text is shaped by one engine, and every sentence a player reads comes from a message catalog in their language. The engine handles right-to-left scripts, mixed-direction text, complex shaping, languages without spaces between words and language-specific glyph forms, so a community can add a language as data, without engine changes.

Design and rationale: text and localization design (docs/superpowers/specs/2026-09-27-text-and-localization-design.md). This page is the current contract.

Rules

  1. Send keys, not sentences. Server, mod and engine messages cross the wire, the WIT boundary and saves as LocalizedText { key, args } (manifold-l10n-types (crates/manifold-l10n-types/src/lib.rs)). At the WIT boundary (manifold:core/text) nested messages travel in a side table, converted by LocalizedText::to_flat/from_flat; see the mod contract. The client resolves them in the player’s language. LocalizedText::plain(..) is for text that is not a sentence to translate: admin console output, player-written text, diagnostics.
  2. Player-facing UI text is a message key. In RON UI documents, use Localized(key: "mymod.shop.buy", args: {"price": Bind("Shop.price")}). In Rust, use manifold_l10n::text("engine.launch.retry") or manifold_l10n::format(&localized_text). Literal(..) is for names and symbols. Mark nodes that are intentionally untranslated with text_options: (untranslated: true), and developer-only documents with a // ui-validate: untranslated line.
  3. Layout is logical. Use TopStart, CenterEnd, … anchors and start/end insets. Rows, grids and anchors mirror in a right-to-left UI unless the node sets mirror: Never (a compass, a media control).
  4. All text is drawn through manifold-text. Never hand a string to egui’s text rendering. Egui has no bidi or shaping. Instead:
    • Screens in the Manifold look use the chrome kit (manifold-ui-chrome (crates/manifold-ui-chrome/src/widgets.rs)). Its widgets draw shaped text, and manifold_ui_chrome::text (crates/manifold-ui-chrome/src/text.rs) has label and paint for a text role.
    • Elsewhere, use the shaped widgets in manifold-egui-text (crates/manifold-egui-text/src/lib.rs): ShapedLabel, button, checkbox and combo_box, or the ShapedUi extension (ui.s_label(..), ui.s_button(..), …) for egui-style code.
    • Use ShapedTextEdit for text input (with IME).
  5. Saves store identity. Inventory items persist by namespaced id and re-resolve on load. Display names are catalog lookups (<mod>.block.<id>.name).

Where things live

PieceSourceTests
Wire and ABI types, keys, locale tags, sanitization of names and chatmanifold-l10n-types (crates/manifold-l10n-types/)tests/ there
Shaping, bidi, line breaking, fallback, glyph atlas, editor, font manifest and coveragemanifold-text (crates/manifold-text/)tests/corpus.rs (golden images), tests/locale_coverage.rs
Catalogs, fallback, ICU4X formatting, pseudo-locales, catalog lints, pack validationmanifold-l10n (crates/manifold-l10n/)tests/l10n.rs
egui bridge: text system, shaped widgets, text field and IME, UI directionmanifold-egui-text (crates/manifold-egui-text/)manifold-ui-runtime/tests/rtl_text_render.rs
BUI contract: TextValue::Localized, TextOptions, logical anchors, Mirror, DropdownAppendwidget_tree.rs (crates/manifold-ui-protocol/src/widget_tree.rs)manifold-ui-protocol/tests/localization_serde.rs
Session: gathering catalogs, the locale setting, system languagel10n_session.rs (crates/manifold-client-state/src/l10n_session.rs), native (crates/manifold-host/src/l10n.rs), browser (crates/manifold-host-web/src/l10n.rs)client-state unit tests
On-demand fontsnative (crates/manifold-host/src/font_fetch.rs), browser (crates/manifold-host-web/src/font_fetch.rs)locale_coverage.rs checks requests
Lintsui-validate (crates/manifold-ui-validate/), check-ui-literals.py (scripts/check-ui-literals.py)manifold-ui-validate/tests/

Catalogs

A mod ships Fluent files under locales/<bcp47>/*.ftl. It declares the language it is written in with default_locale in mod.toml (default en). The key <mod-id>.<a>.<b> is the message a-b in that mod’s catalog. The engine’s own messages are engine.* (engine.ftl (crates/manifold-l10n/locales/en/engine.ftl)). Give every message a # comment for translators; ui-validate warns when one is missing.

Lookup order for a key:

  1. a translation pack the player installed for the language;
  2. the mod’s catalog for the language, then its parent languages (fr-CA → fr);
  3. the mod’s default_locale catalog.

A missing key draws as ⟦key⟧ in debug builds and as the default-language text in release builds. Arguments are isolated (FSI…PDI) so a name in one script can’t reorder the sentence around it. Numbers, plurals, durations and currency are formatted for the locale with ICU4X.

Pseudo-locales ship with the engine and are listed in Settings → Language:

  • en-XA accents and lengthens every message, so hardcoded text and tight layouts stand out.
  • ar-XB shows every message right to left and mirrors the UI.

Translation packs

A translation pack is a data-only mod (kind = "translation-pack"). Its manifest has a [translation] section giving locale, targets (mod ids and optional version ranges), credits and contact. Its files sit under translations/<target-mod-id>/*.ftl. A pack can translate any mod. Build one with cargo mod build, and validate it against the mods it targets:

cargo run -p manifold-ui-validate --bin ui-validate -- \
    --translation-pack path/to/pack --source game/flagship-game

The validator checks:

  • errors for message ids the target lacks, variables that differ from the source, bidi controls outside isolates, and characters no engine font draws;
  • warnings for plural categories the language needs, and for the share of messages still untranslated.

Players install packs in <config>/translations/, as a directory or a .mod. There is no hosted registry: translators coordinate on the Manifold Discord, and a mod author can adopt a pack by merging its files into the mod’s locales/.

Fonts

Fonts come only from the engine’s manifest (faces.rs (crates/manifold-text/src/faces.rs)), the look, or mods, never from the system, so native and browser lay out identically. The look’s Source Sans 3 and Source Code Pro lead the UI and monospace stacks (TextEngine::set_look_faces). Noto covers every script they lack; every Noto face is pinned to an upstream commit and checked against its SHA-256:

  • Every build: Latin, Cyrillic, Greek, monospace and symbols.
  • Native builds: Arabic, Hebrew, Indic, Southeast Asian, Armenian, Georgian, Ethiopic and Thaana. The browser fetches these when text first needs them.
  • Every build fetches: Chinese, Japanese, Korean (Han glyph forms follow the text’s language) and Tibetan.

A fetched face is cached (<cache>/fonts/ natively, OPFS in the browser). Until it arrives, its characters draw as boxes. Mirror the fonts with MANIFOLD_FONT_BASE_URL natively or window.MANIFOLD_FONT_BASE_URL in the browser; the file names stay the same.

Enforcement

  • ui-validate checks RON UI documents:

    • warns on hardcoded text and on physical anchors or unequal left/right margins without a mirror decision;
    • fails on message keys missing from the catalogs;
    • lints catalogs.

    The asset gate test holds the flagship UI to no hardcoded text.

  • scripts/check-ui-literals.py (in ci-smoke) ratchets string literals passed to egui text calls in Rust against ui-literals-baseline.json (scripts/ui-literals-baseline.json). Only developer tools remain in the baseline.

  • scripts/test-text-wasm.sh (in the WASM CI job) runs the text, catalog and sanitization tests compiled to WebAssembly. The golden images must match native exactly.

Limits

  • Tool window titles and a few ComboBox::from_label captions in developer tools are still egui text (English chrome).
  • egui’s own sliders and combo-box buttons keep their left-to-right insides in a right-to-left UI; their placement and text mirror.
  • Admin console command output is plain English (LocalizedText::plain). The dispatcher’s framing (unknown command, usage, permissions) is localized.
  • Browser players can’t install translation packs yet.
  • The browser’s HTML launcher and boot panels, shown before the game loads, are English. Delivered mods’ catalogs load on both hosts.
  • Timestamps use a fixed YYYY-MM-DD HH:MM shape with locale digits, not CLDR date patterns.
  • No emoji face ships yet, so emoji draw as boxes. Urdu uses naskh (Noto Sans Arabic), not Nastaliq.
  • The Asset Lab’s asset-edit refusal reason is a plain string.