Games in more than one language
The October audit's Wave 4 item "Localization and the game's own accessibility options", part one: languages (docs/design/audit-2026-10.md). Before it, a game spoke one language, and kit.ui's face drew ASCII only, so a translated word with an accent showed as "?".
The shape
- A game is written in one language, its own: the GameManifest's
language(en when it names none). - A Locale document carries the game in another:
{kind: "Locale", id: "locale.fr", name: "Français", language: "fr", strings: [{key, text}], phrases: [{source, text}]}. - strings give each text by its key. A key names one text field by what it is, never by where it falls
in a list when an id exists: the document's id, ":", then the path to the field, its steps
joined by "/". An array of things with ids is stepped by the id, and any other array by its index:
dlg.elder:lines/hello/textdlg.elder:links/yes/choiceitem.berries:nameui.hud:widgets/hand/widget/textrules.adventure:rules/found/do/0/text(actions have no ids)game.vale:menu/words/resume(a menu word the game left to the engine may be given too).
- phrases cover text a game works out as it plays rather than writes in a field: a HUD line a
Screen's bind chooses (
'Find the relic.' if var.quest == 1 else ''), or the verb a key strip shows throughuse_verb(a usable's talk, an Item's eat). Each gives a source text, as the bind gives it, and its translation, and kit.ui shows the translation wherever a bound text equals the source. Expressions are compiled at bake and keyed by their source, so they are never rewritten. - A text the Locale leaves out shows in the game's own language.
In the player
The documents are overlaid once, when a game boots in a language (GwLocale.content_in): the
running game reads them as if they had been written so, and every document no key names is shared,
not copied. Text is presentation only, so the language changes no state, save, replay or hash.
Larkmead's look tour ends on the same state hash in English and in French, and a test switches a
game mid-play and compares. The one place a text's length set the simulation's pace, a Timeline's
say step that gives no reading time, has that time written in at bake from the game's own line,
so a translation never moves when the Timeline goes on.
- A person picks the language on the game's Options page, where the Language row lists the
game's languages by their own names (Français, Deutsch). It is kept with their options. The first
time, the player picks their device's language when the game has it (
OS.get_locale, "fr_CA" → fr). Changing the language boots the running game again in that language and takes up its state (GwSession.relocalize): the same tick, the same hash, with the menu still open on its page. - A tool run speaks the game's own language, unless given
--locale CODE: the player, the engine CLI's sim and shot, and the core's sim, shot, tour and play. A Playtest can name alocale, and then L2 plays it and L3 draws its frames in that language, so a translation is checked to fit and read, on a desktop and on a phone, as the game's own words are.
The face
kit.ui's 5x7 face is data now (kits/ui/face.json), read by the engine to draw and by the core to
check. Beside its 126 drawn glyphs (ASCII, ß, æ, ø, œ, ł, €, £, °, ¡, ¿, « », typeset quotes), it
composes 109 accented Latin letters from a base letter and a mark (GwUiGlyphs).
- Over a lower-case letter, the mark sits in the two rows above its x-height; i and j lose their dots first.
- Over a capital, the letter gives up one of its repeated rows and the mark takes the row above it. So the face carries one more row than it draws, the room over a capital, and existing text draws pixel for pixel as before.
- Cedilla and ogonek go under the letter.
This covers French, German, Spanish, Portuguese, Italian, Dutch, the Nordic languages, Polish, Hungarian and Turkish, and Czech but for ď and ť.
Checks (core/src/game/locale.ts)
| Code | Level | When |
|---|---|---|
locale.key |
error | a key names no text of the game (with the nearest that does) |
locale.placeholder |
error | a translation drops or adds a {placeholder} |
locale.glyph |
error | a translation uses a character the face can't draw |
locale.language |
error | a Locale in the game's own language, or two in one language |
locale.duplicate |
error | a key or a phrase translated twice in one Locale |
locale.phrase |
error | a phrase no bind gives |
locale.missing |
warning | texts the Locale leaves out, counted, the first few named |
text.glyph |
warning | the game's own text uses a character the face can't draw |
playtest.locale |
error | a Playtest plays in a language the game doesn't speak |
gamewright strings [--locale CODE] <pack> lists every text and phrase with its key and a Locale's
translation. A translator, or the Director, works from that.
Not yet
- Cyrillic, Greek and CJK: the face has no room for them, so such a game needs a second face.
- Right-to-left scripts, plural rules, and words that agree with a number.
- An Item's use verb (a Slug the events carry), which is translated as a phrase only where a bind shows it.
- The few words engine views still draw themselves: kit.platformer's "paused", kit.grid's "solved in", the touch Pause and Back, and key names such as ESC.
- Voices.