Skip to content

Translating a game

Everything a game says to a player (a label, a message, a refusal, a line of commentary) is kept in its catalogue, apart from the code, once per language. This page is how the catalogue is stored, how to add a language or translate lines, and how to check the work.

Who does what:

  • A translator edits text files. No programming is needed, only a text editor or a translation tool such as Poedit or Weblate.
  • A programmer declares what lines exist and what blanks they have, and runs the checks below (they are commands at a terminal; a translator can run them too once the game is checked out).

How a game's words are stored

The catalogue is a folder of .po files, the format of gettext, one file per language: lusine's own words are in lusine/resources/catalogue/ (en.po and fr.po: numbers, SI prefixes, units, currency names), and a game adds its own in its resources/catalogue/. A game's entries add to lusine's, and replace one of the same key.

Each entry is a key, the source language's text and the translation:

msgctxt "number.score"
msgid "{a}–{b}"
msgstr "{a} à {b}"
  • The key (msgctxt) names the line, such as number.score or unit.metre. The code asks for a line by its key, never by its English words, so changing the English never breaks a translation's link to it.
  • The source text (msgid) is the line in the game's source language, for the translator to read. The translation (msgstr) is what you write.
  • Slots are the blanks in braces, {a} and {b} here: the game fills them in when the line is said, with a name, a number or another line. Keep every slot the source uses, and move it wherever your language puts it. Never translate the name inside the braces.
  • A shared phrase, {@score}, stands for another entry, phrase.score, translated once and used by many lines. Use it or write your own words, as your language reads best.
  • Variants are keys numbered .1, .2, ...: several wordings of one line, so it does not repeat. Your language may have more or fewer variants than the source.
  • Plurals have one form for each plural category your language has, in the order its header gives:
#. plural: {n}
msgctxt "unit.metre"
msgid "{n} metre"
msgid_plural "{n} metres"
msgstr[0] "{n} mètre"
msgstr[1] "{n} mètres"

Each form is a whole sentence. The #. plural: line names the slot that chooses the form.

Domains are the values a slot can take: the players, a count from 1 to 25, a distance put into a few phrases ("just wide", "a hand's width"). The programmer declares each line's slots and their domains in the game's code. A slot with a known, finite domain lets the line be spoken by the voice (below); a slot without one leaves the line as text only.

Adding a language

  1. Copy the source language's file to a new one named for the language's code: resources/catalogue/en.po to resources/catalogue/de.po, for example.
  2. Edit the header, the first entry, whose msgid is empty. It needs:
  3. Language: the language's code (de);
  4. Plural-Forms: gettext's rule for the language's plurals, such as nplurals=2; plural=(n != 1); for German or nplurals=2; plural=(n > 1); for French. Translation tools fill it in for you;
  5. X-Native-Name: the language's name as its speakers write it (Deutsch);
  6. X-Source-Language: the source language (en), and X-Fallback: what happens to a line you have not translated: default shows the source language's line, refuse makes the game's build refuse the language until every line is there.

Always give both Plural-Forms and the native name. lusine knows the rule for some languages, but a language whose plurals it cannot work out is refused, and so is a Plural-Forms that disagrees with the language's real plural categories. 3. Replace each msgstr with your translation. An entry marked fuzzy by a tool counts as not yet translated.

Translating lines

Edit the language's .po file in the game's repository, directly or in Poedit or Weblate; there is no export or import step. Write each line as a person of that language would say it, not word for word: a line the source has and your language lacks is fine, and so is the reverse. lusine's catalogue-authoring skill, which Claude Code reads from your game's checkout, has more on writing lines that sound like a person.

Checking the work

From the game's folder:

python lusine/tools/lusine/catalogue.py validate lusine/resources/catalogue resources/catalogue

It prints catalogue: valid, or every problem by key and language: a key missing where the fallback does not allow it, a slot the line uses that was never declared (or a declared one the source line leaves out), a plural form your language needs and the entry lacks.

python lusine/tools/lusine/catalogue.py worklist resources/catalogue

prints the worklist: a table of every key with its text in each language, and how many entries are missing. For a game with commentary, the programmer can also make the translator's fuller worklist, a web page with every line, when it is said, the values each blank takes and examples filled in, sorted by how often each line is heard (lusine's worklist tool).

In the game itself: lusine's engine does not read the catalogue yet. A panel's line shows its key where its text will be, so for now a translation is checked with the two commands above, not by playing.

Voices

A line can also be spoken. lusine's voice tooling renders every sentence a line can make, whole, for every combination of its slots' values, in each language, with a computer voice whose licence allows the game to be sold. The list of rendered sentences, the voice manifest, is kept beside the .po files, so a changed line shows up as a sentence to render again; the audio itself is built, not kept. Because every combination is rendered, a line with several wide slots can mean thousands of recordings: the worklist shows the count per line, and grouping values into a few phrases keeps it small. The game's build does not yet run this step; tools/lusine/voices.py runs it from a small module of the game's inputs (its commentary, the .po folder, its voices.json, where the manifests go, and the wording floors):

python lusine/tools/lusine/voices.py voices --game tools/voices_game.py
python lusine/tools/lusine/voices.py check-voices --game tools/voices_game.py
python lusine/tools/lusine/voices.py worklist --game tools/voices_game.py --target fr

voices renders every sentence and writes the manifests, check-voices lists every sentence whose manifest no longer matches the .po, and worklist writes the translator's page for a language.