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
- 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 byLocalizedText::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. - 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, usemanifold_l10n::text("engine.launch.retry")ormanifold_l10n::format(&localized_text).Literal(..)is for names and symbols. Mark nodes that are intentionally untranslated withtext_options: (untranslated: true), and developer-only documents with a// ui-validate: untranslatedline. - Layout is logical. Use
TopStart,CenterEnd, … anchors andstart/endinsets. Rows, grids and anchors mirror in a right-to-left UI unless the node setsmirror: Never(a compass, a media control). - 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, andmanifold_ui_chrome::text(crates/manifold-ui-chrome/src/text.rs) haslabelandpaintfor a text role. - Elsewhere, use the shaped widgets in
manifold-egui-text(crates/manifold-egui-text/src/lib.rs):ShapedLabel,button,checkboxandcombo_box, or theShapedUiextension (ui.s_label(..),ui.s_button(..), …) for egui-style code. - Use
ShapedTextEditfor text input (with IME).
- Screens in the Manifold look use the chrome kit
(
- 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
| Piece | Source | Tests |
|---|---|---|
| Wire and ABI types, keys, locale tags, sanitization of names and chat | manifold-l10n-types (crates/manifold-l10n-types/) | tests/ there |
| Shaping, bidi, line breaking, fallback, glyph atlas, editor, font manifest and coverage | manifold-text (crates/manifold-text/) | tests/corpus.rs (golden images), tests/locale_coverage.rs |
| Catalogs, fallback, ICU4X formatting, pseudo-locales, catalog lints, pack validation | manifold-l10n (crates/manifold-l10n/) | tests/l10n.rs |
| egui bridge: text system, shaped widgets, text field and IME, UI direction | manifold-egui-text (crates/manifold-egui-text/) | manifold-ui-runtime/tests/rtl_text_render.rs |
BUI contract: TextValue::Localized, TextOptions, logical anchors, Mirror, DropdownAppend | widget_tree.rs (crates/manifold-ui-protocol/src/widget_tree.rs) | manifold-ui-protocol/tests/localization_serde.rs |
| Session: gathering catalogs, the locale setting, system language | l10n_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 fonts | native (crates/manifold-host/src/font_fetch.rs), browser (crates/manifold-host-web/src/font_fetch.rs) | locale_coverage.rs checks requests |
| Lints | ui-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:
- a translation pack the player installed for the language;
- the mod’s catalog for the language, then its parent languages (
fr-CA→fr); - the mod’s
default_localecatalog.
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-XAaccents and lengthens every message, so hardcoded text and tight layouts stand out.ar-XBshows 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-validatechecks 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(inci-smoke) ratchets string literals passed to egui text calls in Rust againstui-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_labelcaptions 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:MMshape 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.