Skip to content

Latest commit

 

History

History
268 lines (225 loc) · 12.3 KB

text.md

File metadata and controls

268 lines (225 loc) · 12.3 KB

The game's text

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).

The entries

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"
  • msgctxt is 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.
  • msgid is the English text, the template's. English has no .po.
  • msgstr is 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, and make text-update keeps them up to date in the .po files.

The keys:

  • ui.<group>.<name>: a UI string. The ui.* entries are in text id order: the first is text id 0, and the id is GetUiText's argument (GetUiText(0x1b) is ui.menu.load_game). A new one goes at the end of them. The level names are ui.level.<level>, the level's constant in levels.json (LEVEL_JUNGLE_JAM is ui.level.jungle_jam), and a level's name_text is its entry's text id: tools/text.py stops if it isn't the level's own ui.level.<level> entry (which catches an entry inserted in the middle, and a level pointing at another level's name) and if a ui.level.* entry is no level's name. There are UI_TEXT_COUNT of them (levels.py's constants/levels.h), and the build stops if the template has another number.
  • cutscene<NN>.page<P>.<I>: string I of page P of cutscene NN, the page that shows with slide P of 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 is games/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 text

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.

Translating

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 #, fuzzy line and its #| lines;
  • a key the language lacks or one the template doesn't have (msgcmp against the template, both ways): run make text-update;
  • a credits.* entry whose msgstr isn't the English msgid, 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).

Changing the template

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 new level's name

A level's name is data in the six languages (#966):

  1. The template. Add the entry after the last ui.* one (before the first cutscene* entry) of games/xs/text/crashxs.pot, keyed ui.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.

  2. The languages. make text-update brings it into the five .po files untranslated. Fill in each msgstr (the English again where the game keeps it). make text-check fails until all five have it: an untranslated entry fails msgfmt --check --statistics's count, a missing one msgcmp.

  3. The level. Set its name_text in levels.json to the text id.

  4. Run make. UI_TEXT_COUNT grows to one past the highest name_text, the gUiText* 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

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}: logo n (0-4, gCreditsLogos) on a line of its own;
  • {"blank": n}: n empty 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.

How the tables are built

  • msgfmt --check compiles each .po into build/crashbandicootxs/text/<lang>.mo (an untranslated or fuzzy entry isn't in the .mo).
  • tools/text.py cpp reads the template (the keys, their order and the English) and each language's .mo with Python's gettext module (pgettext, the key being the context), and writes build/crashbandicootxs/text/<table>.inc, which engine/src/*/data/<table>.cpp includes. It stops on a key a .mo lacks.
  • The tables store each distinct string once, at its first use, in the order docs/data.md, "Text tables", gives, as the ROM does.

Extracting the text again

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.