Skip to content

Guide Translations

bryanthaboi edited this page Jul 27, 2026 · 1 revision

Translating the game

A translation is an ordinary content mod. It needs no engine changes and no special permissions: it registers a glyph page, a charmap, and a pile of strings, exactly like any other mod registers a species or a song.

Start by generating one:

python3 tools/modkit.py translation francais --language "Francais" --base imported

That writes mods/francais/ with every translatable string in the game as an empty catalog, plus mods/francais-worksheet/ holding the English to translate from. Read the generated TRANSLATING.md first; this page is the background it assumes.

The two kinds of string

The game's text comes from two places, and they are reached differently.

Where it lives Registry Key
Script text Data.text, extracted from the ROM text the pokered label, _PalletTownText1
Engine text Literals in src/, wrapped in Strings(...) strings the English source string

Script text is the dialogue: signs, NPCs, dex entries. Engine text is everything the engine says on its own behalf: But, it failed!, SAVE, TEXT SPEED, the link-play screens.

The split exists because only one of the two is ROM-derived. That matters for what you are allowed to ship (see below).

Glyphs

Text is drawn from pages: an image of 8x8 cells plus a charmap mapping byte sequences to glyph codes. The vanilla pages sit at $60 and $80. Anything from 0x100 up is free, so a new alphabet is added rather than swapping one out:

mod.content.font:register("kana", {
  image = "assets/font/kana.png",
  base = 0x100,
  glyphsPerRow = 16,
  -- advance = 8,   -- only if your glyphs are not 8px wide
})
mod.content.font:register("charmap:a", { seq = "", code = 0x100 })

Codes run left to right, top to bottom from base. Sequences are matched longest first, so a multi-byte character and a multi-character ligature both work and neither shadows the other: "ch" can be one glyph even though "c" is also mapped, and that is the same mechanism the vanilla font already uses for <PK>, <MN> and 'd.

advance is per page. A proportional font needs one page per width.

Line length is glyphs, not bytes

The dialogue box holds 18 glyphs. A 3-byte character costs one column, and the engine will never cut a character in half, so a kana line wraps at 18 and not at 6.

Your own \n breaks are honoured exactly as written. Break lines where they read best rather than where English happened to fit.

To change how much fits, restyle the box:

mod.content.field:patch("theme", { textBox = { maxCols = 16 } })

The budget is measured in pixels behind the scenes (maxCols * 8), so a page with a non-default advance re-measures rather than overflowing.

Format directives

Some engine sources carry %s or %d:

mod.content.strings:override("Wild %s\nappeared!", "Un %s\nsauvage apparait !")

Keep every directive, in a matching count. Word order is yours - the engine substitutes in the order the directives appear, so if your language wants the name last, write the sentence with the %s last.

A translation whose directive count does not match the source is refused at runtime: the English is drawn instead and the mismatch is logged once. It will not crash a battle, but it will not show your text either.

Names

Species, move, item and trainer-class names are fields on their records, so they are patched rather than overridden:

mod.content.pokemon:patch("BULBASAUR", { name = "BULBIZARRE" })

Status labels (PSN, BRN) sit in the battle HUD and have very little room; the vanilla ones are three glyphs.

Name entry

The naming screen's letter grid is a hook, so a translation can replace the alphabet without replacing the screen:

mod.hooks:on("ui.naming.grid", function(base, ctx)
  return ctx.lower and MY_LOWER or MY_UPPER
end)

Each cell is whatever sequence your charmap maps, so a multi-byte character is one cell and counts as one character toward the name length. The row holding a single "lower case" / "UPPER CASE" cell is the case switch, and the cell spelled "ED" is the confirm.

What you may ship

Extracted script text and the vanilla names are ROM content. Your translation of them is your own work and ships freely; the English does not. This is why the generator writes the English to a -worksheet/ directory beside the mod rather than inside it: modkit pack zips everything under the mod directory, so a worksheet kept inside would land in your release no matter what a .gitignore said.

lang/strings.lua is the exception. Those sources are the engine's own Lua, not ROM output, so the key is allowed to be the English.

modkit lint enforces this. Run it before you publish:

python3 tools/modkit.py lint francais
python3 tools/modkit.py pack francais

Shipping an unfinished translation

Every catalog starts empty and the generated main.lua skips empty values, so an untranslated string falls through to English. A half-finished translation is always playable. Ship early.

When the engine adds new strings, re-harvest:

python3 tools/modkit.py translation francais --refresh

--refresh keeps every translation you have written, adds the new keys, and parks anything whose key no longer exists in an ORPHANED comment block at the end of the catalog rather than deleting your work.

See also

Clone this wiki locally