Docs Design

Contents

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/text
    • dlg.elder:links/yes/choice
    • item.berries:name
    • ui.hud:widgets/hand/widget/text
    • rules.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 through use_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 a locale, 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.

From docs/design/localization.md in the Gamewright repository.