The game's text is in games/xs/text/ as gettext files (#1070): the menus, the
level names and the other UI strings (GetUiText's text ids), and the
cutscenes' pages, in the six languages of the language select (English,
French, German, Spanish, Italian, Dutch), and the credits, which every
language shows in English (see "The credits"). The build generates the ROM's three string
tables and the credits from them (docs/data.md, "Text tables").
| File | What |
|---|---|
games/xs/text/crashxs.pot |
the template: every string's key, its English text and the translator comments |
games/xs/text/fr.po, de.po, es.po, it.po, nl.po |
a language's text, one entry per template entry |
tools/text.py |
the generator, the checks and the extractor |
text.mk |
the build rules and the text-check/text-update targets |
The gettext tools (msgfmt, msgcmp, msgmerge) are part of the build: the
devshell has them, and on Debian/Ubuntu they are the gettext package
(INSTALL.md).
An entry is one string:
#. Text id 27 (0x1B). Title screen menu item, and the save menu's first option
#. and the title of its screen (large font).
#. Widest shipped text: 92 px in the small font, 147 px in the large font; the
#. screen is 240 px wide.
msgctxt "ui.menu.load_game"
msgid "load game"
msgstr "charger partie"msgctxtis the key. The English text can be the same for two keys (and a language's text the same for several), so the key, not the English, says which string it is.msgidis the English text, the template's. English has no.po.msgstris the language's text. Where the game keeps the English in a language (most level names, a few labels, two cutscene pages), it is the English text again: an entry is never left empty.- The
#.comments say where the game shows the string and, for the UI text, how wide the shipped texts are, in pixels of the font (the screen is 240 pixels wide). The game draws some UI strings in the large font too: the level names (the level select's title), the save menu's titles and the powers' names (the power dialog's title). The comments come from the template, andmake text-updatekeeps them up to date in the.pofiles.
The keys:
ui.<group>.<name>: a UI string. Theui.*entries are in text id order: the first is text id 0, and the id is GetUiText's argument (GetUiText(0x1b)isui.menu.load_game). A new one goes at the end of them. The level names areui.level.<level>, the level's constant in levels.json (LEVEL_JUNGLE_JAMisui.level.jungle_jam), and a level'sname_textis its entry's text id: tools/text.py stops if it isn't the level's ownui.level.<level>entry (which catches an entry inserted in the middle, and a level pointing at another level's name) and if aui.level.*entry is no level's name. There areUI_TEXT_COUNTof them (levels.py'sconstants/levels.h), and the build stops if the template has another number.cutscene<NN>.page<P>.<I>: stringIof pagePof cutsceneNN, the page that shows with slidePof the cutscene. A page's strings show one after the other. A page used twice (cutscene 10's page 2 is cutscene 9's page 1) has one key. Which slide shows a page isgames/xs/data/cutscenes.json's (a slide's"text": "cutscene<NN>.page<P>", data.md, "The cutscene data"), and the page has as many strings as the template has entries for it.
The game draws the text with its two HUD fonts, whose characters are
gSmallFontChars and gLargeFontChars (engine/src/game/ui/data/hud_font_tables.cpp,
the glyphs are games/xs/graphics/fonts/): the 26 lower case letters, the digits,
- + ! : , ? . ' % and < > (the small font only), the accented letters
ä à á â ë è é ê ï ì í î ö ò ó ô ü ù ú û ñ ß ç ¿ ¡ ´ œ, and A, B,
L, R, which draw the GBA's buttons (the small font only). So the text
is lower case. The files are UTF-8, and tools/text.py writes the ROM's
bytes in Latin-1 (œ is 0x9C, Windows-1252's code for it, where the
font has it). A character the font lacks stops the build: the game would
draw it as an a. A string the game also draws in the large font
(tools/text.py's LARGE_FONT_KEYS and the level names) must be in both
fonts.
The cutscene text is word-wrapped into a 228x30 pixel box (three lines of the small font) under the picture, and a longer string goes on after a key press, so it has no length limit. A UI string isn't wrapped (the power dialog's help text is), so it has to fit where the game draws it: the comment's widest shipped text is a safe width.
Edit the language's .po file (a PO editor such as Poedit or Lokalize,
or any text editor; keep it UTF-8), then:
make text-check # the checks below
make # builds the ROM with the new text
make rebuilds the tables from the .po files; only make compare
checks the ROM against the original, so changed text builds a working
ROM that make compare rejects.
make text-check (CI runs it) fails on:
- an untranslated or fuzzy entry (
msgfmt --check --statistics): a fuzzy entry is one whose English changed since it was translated (see below). Check the translation, then delete its#, fuzzyline and its#|lines; - a key the language lacks or one the template doesn't have (
msgcmpagainst the template, both ways): runmake text-update; - a
credits.*entry whosemsgstrisn't the Englishmsgid, or that is missing or fuzzy (see "The credits"); - a character the font lacks, a character Latin-1 lacks, a level name that isn't at its text id (tools/text.py's own checks, the same as the build's).
To change the English text or add an entry, edit games/xs/text/crashxs.pot
(the English, the key, the comments), then
make text-update
which merges the template into every .po file (msgmerge --update --previous): a new key comes in untranslated, a changed English text
marks its entry fuzzy, with the old English as #| msgid, and a removed
key becomes an obsolete #~ entry. make text-check fails until each
language has been seen to. Where the game should keep the English, copy
it into msgstr.
A new UI string is a new ui.* entry after the last one, and its text
id is its position (for a level's name, set the level's name_text in
levels.json to it, see "A new level's name" below).
A new cutscene string is an entry after the last of its page's
(cutscene01.page0.1 after cutscene01.page0.0): the page shows it
after the others, with no other edit, since the page's string count is
its number of entries. The entries' order in the template is the order
the strings are stored in, and a page's entries are together and
numbered from 0 (tools/text.py stops otherwise). A new page is new
entries (cutscene11.page0.0, ...) that a slide of
games/xs/data/cutscenes.json shows (data.md, "A new
cutscene"); a page no slide shows, or a slide
showing a page the template lacks, stops the build.
A level's name is data in the six languages (#966):
-
The template. Add the entry after the last
ui.*one (before the firstcutscene*entry) ofgames/xs/text/crashxs.pot, keyedui.level.<level>after the level's constant (LEVEL_LAB:ui.level.lab), with its comment:#. Text id 70 (0x46). A level's name (LEVEL_LAB): the level select's title #. (large font) and the pause menu's info page. msgctxt "ui.level.lab" msgid "the lab" msgstr ""
Its text id is its position: 70 for the first one after the ROM's 70 strings.
-
The languages.
make text-updatebrings it into the five.pofiles untranslated. Fill in eachmsgstr(the English again where the game keeps it).make text-checkfails until all five have it: an untranslated entry failsmsgfmt --check --statistics's count, a missing onemsgcmp. -
The level. Set its
name_textin levels.json to the text id. -
Run
make.UI_TEXT_COUNTgrows to one past the highestname_text, thegUiText*tables get the name in each language and the level select and the pause menu show it.
The name is drawn in both fonts, so each language's text must be in
both (the font check above). Appending moves no other text id: the
code's GetUiText(0x1b) and the other menu ids (26-69) keep their
strings. Only an entry inserted before them would shift them, and the
level-name check stops the build then.
Tested in scratch with a 26th level, LEVEL_LAB ("the lab", "le labo",
"das labor", "el laboratorio", "il laboratorio", "het lab", text id 70).
make text-check passes with the six names and fails with German's
msgstr emptied or its entry removed. Plain make builds it
(UI_TEXT_COUNT 71, gUiText*70 in each table), and a second make
does nothing. In mGBA, the level's pause menu shows its name in English,
French and German.
The credits' text is the template's credits.* entries (#1092), after
the cutscenes'. The game has one credits text, gCreditsText, and every
language shows it in English (Credits::Init reads it, not a
per-language table, so a translation would need a code change and
another ROM). An entry is a heading or a block of lines (msgid with
\n between the lines, and an empty line where a block has a gap),
keyed credits.<heading> and credits.<heading>.names. Each .po has
the entries too, as copies of the English, with a comment that says so:
#. The credits: lines, in the small font.
#. The credits are shown in English in every language (Credits::Init reads one
#. text, gCreditsText): msgstr is the English text, unchanged (tools/text.py
#. checks).
msgctxt "credits.executive_producers.names"
msgid ""
"karthik bala\n"
"tobi saulnier"
msgstr ""
"karthik bala\n"
"tobi saulnier"The build reads only the template's English. make text-check fails if
a language's copy isn't the English, unchanged, or is missing or fuzzy,
so a translation can't go in by mistake and look as if the game used it.
games/xs/data/credits.json gives the order the credits show them in, with the
fonts and the logos:
{"large": "credits.executive_producers"},
{"small": "credits.executive_producers.names"},
{"blank": 2},
...
{"picture": 1},{"large": key},{"small": key}: the font switch, then the entry's lines (the stream's 3 or 2,CREDITS_OP_LARGE/SMALL);{"text": key}: the entry's lines in the font so far;{"picture": n}: logon(0-4,gCreditsLogos) on a line of its own;{"blank": n}:nempty lines.
tools/text.py cpp credits_text writes the stream from them, a line per
string (credits_text.cpp #includes it). To change a name or add one,
edit the entry in games/xs/text/crashxs.pot; to add a heading, add its entries
to the template and its items to the list. Then run make text-update
and copy the new English into each .po's msgstr (for a changed
entry, also delete its #, fuzzy and #| lines). The build, and make text-check, stop on an item
whose key the template lacks, an entry no item uses, an empty entry, a
logo past the fifth, and a character the font the item is drawn in lacks
(a heading's must be in the large font). tools/text.py extract writes
the credits.* entries from the ROM too, keyed by games/xs/data/credits.json,
into the template and the .po files, and extract --check compares
them.
msgfmt --checkcompiles each.pointobuild/crashbandicootxs/text/<lang>.mo(an untranslated or fuzzy entry isn't in the.mo).tools/text.py cppreads the template (the keys, their order and the English) and each language's.mowith Python's gettext module (pgettext, the key being the context), and writesbuild/crashbandicootxs/text/<table>.inc, whichengine/src/*/data/<table>.cppincludes. It stops on a key a.molacks.- The tables store each distinct string once, at its first use, in the order docs/data.md, "Text tables", gives, as the ROM does.
tools/text.py extract [baserom.gba] writes games/xs/text/ from the ROM's
tables, with the keys and the comments; this is how the files were made.
tools/text.py extract --check writes them into a temporary directory
and compares them with games/xs/text/ byte for byte: it passes as long as
games/xs/text/ is the ROM's text.