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.
First workflow: translate one string
This path is for translators who want to edit text before working on a custom font. From the engine repository root, run:
The generated mod keeps its complete manifest at
mods/francais/manifest.json, its entry file at mods/francais/main.lua, and
catalogs under mods/francais/lang/; see the manifest
reference when adding fields by hand.
python3 tools/modkit.py translation francais --language "Francais" \
--base imported --pixel-font
Read the generated mods/francais/TRANSLATING.md first. The command creates
empty catalogs under mods/francais/lang/ and a separate
mods/francais-worksheet/ containing the source text to translate. Keep the
worksheet outside the mod because the package includes everything under the
mod directory.
Open one catalog, replace one empty value, and run the game with the
translation mod enabled. The --pixel-font option uses the bundled font so
this first text-only test does not require a glyph sheet. If the text does
not change, confirm that the catalog entry is non-empty, the mod is enabled
for the selected game, and the key matches the generated catalog.
After the in-game check, run these from the same repository root:
python3 tools/modkit.py validate francais --base imported
python3 tools/modkit.py lint francais
python3 tools/modkit.py pack francais -o francais-1.0.0.modpkg
validate checks that the mod loads, lint checks the distributable content,
and pack writes a package after its gates pass. Use the first reported file
and line to repair a failure, then rerun that command. Add a glyph page only
when the bundled font cannot display the characters you need.
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:
Example status: illustrative insertion; place this registration in your mod's complete main.lua beside the manifest.
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:
Example status: illustrative insertion; add this patch to the same main.lua that registers your translation.
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:
Example status: illustrative insertion; keep this override in the translation mod's main.lua.
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:
Example status: illustrative insertion; add this record patch to the complete translation mod.
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:
Example status: illustrative insertion; add this hook to the complete translation mod after the glyph setup.
mod.hooks:wrap("ui.naming.grid", function(next, 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.
Gen 2 and Gen 3
The generator writes an api 2 mod. Its manifest claims "games": ["gen1", "gen3"] when this machine has an imported Gen 3 cache, and ["gen1"]
otherwise. Gen 2 is not harvested.
- Gen 1. Everything above. The dialogue, name and status catalogs come from the Gen 1 dataset.
- Gen 3. With a FireRed, LeafGreen, Ruby, Sapphire or Emerald cache
imported (or
POKEPORT_IDENTITYpointing at the identity that holds one),lang/strings.luaalso gets every Gen 3 ROM text line, keyed by its ROM label (gText_*,sText_*,STRINGID_*,table[i]), plus Easy Chat groups and words (easyChat.group[N],easyChat.word[N]). The English is in the worksheet only. A key spelled as ROM English failsmodkit lint(MK306);--refreshmoves old English keys to labels. A refresh with no Gen 3 cache found stops rather than drop those entries. - Gen 3 font.
fontpages use the image alpha as an 8x8 glyph mask withbasebelow0x400; acharmap:row maps one character only (a multi-character sequence is ignored with a warning).--pixel-fontregisters thettfentry, which covers ASCII, Latin-1 and Latin Extended-A. Thefieldtext-box theme has no Gen 3 target. - Gen 2 (Gold, Silver, Crystal). Translate by hand from an
api2 orapi3 mod that declaresgen2:text(script text, routed to Gold's own table),rom_text(label-keyed engine prose fromsrc/core/RomText.lua), andstringsandfonton their shared Gen 1 targets.ui.naming.gridis raised on all three generations.
See also
- Art pipeline for how glyph pages sit alongside the other art
- Reference: Registries for
strings,text,font - Reference: Hooks for
ui.naming.grid
Check and recover
The first successful checkpoint is one translated catalog entry visible in the
game, followed by a package that passes validate, lint, and pack. If a
catalog or glyph change fails, restore the previous catalog or remove only the
new glyph registration, then rerun the first failing command from the engine
root.
Start here
Playing
Tutorials
- Choose a tutorial
- 01 Sprite and Text Tweak
- 02 Balance Patch
- 03 New Species
- 04 New Item and Ball
- 05 New Move
- 06 NPC and Dialogue
- 07 New Map
- 08 Quest
- 09 Custom Music
- 10 New Mechanic
- 11 Custom UI Screen
- 12 Mini Total Conversion
Creative guides
Concepts
Recipes and reference
Author and developer tools
Home · Start Playing · Glossary · Troubleshooting · Repository · Community
Game data is imported locally from a supported ROM supplied by the player. Shared mods contain original work or transform recipes, never extracted game content. Check each guide's target game before using its examples.