Translation / language packs
Legend of Legaia shipped in English, French, German, Italian and Japanese. A language pack lets a community translate the game's text - item and spell names, menus, dialogue - into any Latin-script language and play it on the retail disc. The pack is a plain YAML file you edit in any text editor; by default applying it never changes the size of anything on the disc, and an opt-in relayout gives scene dialog extra room when a translation runs long. This page is the reference; new translators should start with the step-by-step guide, translating the game.
At a glance
- What you get
- A translated game on your own disc, applied in the browser patcher or with
legaia-patcher translate. Composes with every randomizer option. - Covered
- Name tables (items, spells, arts, accessory passives, party names), the whole NPC / cutscene / narration dialogue corpus, pause-menu / shop / battle system labels.
- Not covered
- Text baked into textures; non-Latin scripts. Accented Latin draws with the accent font (
accents: font). - The one rule
- Every edit is same-size in place unless you opt into the relayout. Each line has a byte budget; a line over budget is reported and left English - except a name, which moves (or, for a monster, grows its record) when there is room.
- Sharing
- A pack is safe to publish once its
source:text is stripped - what remains is your text keyed by disc coordinates. - Code
crates/translate/src/; writes go throughDiscPatcher(EDC/ECC re-encoded, no LBA moves).- Own another disc?
- An official European disc's text, or a fan-translated disc's, can be lifted onto the USA disc from your own two discs.
Workflow
- Your discThe USA image you own.
- ExportEvery catalogued line to YAML.
- Working packHolds the game script beside your text - local only, never share.
- Fill + statsAny text editor;
statsdry-runs against the disc. - StripDrops the
source:text. - Shipped packDisc coordinates + your text only: committable.
- ImportOnto a copy of the disc (plus a PPF).
Only the first and last steps need a disc; everything in between is text editing.
| Step | Command | Does |
|---|---|---|
| 1 | translate export --input DISC.bin -o legaia_en.yaml | Dumps the disc's own text into a working pack (once). |
| 2 | translate init --lang fr --from legaia_en.yaml --contributor you -o legaia_fr.yaml | Skeleton for your language; --resume site/lang/fr.yaml seeds it from a published pack. |
| 3 | translate merge --base legaia_fr.yaml --pack part1.yaml --pack part2.yaml -o legaia_fr.yaml | Fill the translation: fields with any editor; init --chunk N splits the work, merge recombines it. |
| 4 | translate stats --pack legaia_fr.yaml [--input DISC.bin] | Coverage, encodability and budgets; --input dry-runs against a real disc. |
| 5 | translate strip --pack legaia_fr.yaml -o site/lang/fr.yaml | Drops the source text - the result is the shareable pack. |
| 6 | translate import --input DISC.bin --pack legaia_fr.yaml --output out.bin --patch out.ppf | Applies to a scratch copy and/or emits a PPF patch. |
Every disc argument takes a .bin or a .cue. A partially filled pack is always playable: empty entries stay byte-identical. Import is idempotent and incremental. stats and import summarise skipped entries per reason; --verbose lists every key.
- Commit distributable packs (
site/lang/*.yaml) - new authored text plus a coordinate table. A disc-free test fails the build if a tracked pack still carriessource:. - Never commit working packs - they carry the game's script.
/translations/andlegaia_*.yamlare gitignored.
The in-browser ROM patcher does the same from a language dropdown (default None): shipped packs, your own pack, a starter export, or a PAL lift - validated against your disc, with a per-section coverage report. Nothing is uploaded.
| Page control | CLI equivalent |
|---|---|
| Export a starter pack from my disc | translate export |
| Export a working copy of this pack (a pack chosen; English beside each line) | translate init --resume |
| Check pack against my disc | translate stats --input |
Download skipped lines (.csv) - columns key, reason, message | the stats / import skip list (--verbose) |
| Give translated dialog more room | translate import --allow-relayout |
| Make a shareable pack (for an imported pack) | translate strip |
| Translation from another disc I own | translate lift-official |
The same loop as a walkthrough, with what each skip message means: the translating guide.
CLI reference
Every subcommand lives under legaia-patcher translate; --help on any of them prints the full flag descriptions. Every disc argument takes a .bin or a .cue.
| Subcommand | Flags | Does |
|---|---|---|
export | --input, -o | every cataloged string into a working pack with empty translation: fields |
coverage | --input, --json, --verbose | the export measured against the script walk: walked lines, exported lines and each missing line's reason, per scene; counts and offsets only, no text |
init | --lang, --from or --input, --contributor (repeatable), --resume, --chunk N, -o | an empty per-language skeleton; --resume pre-fills from a working or shareable pack by key; --chunk also writes <stem>.001.yaml, ... |
merge | --base, --pack (repeatable, in order), -o | merges filled entries onto the base by key; -o may be the base |
stats | --pack, --input, --verbose | coverage + encodability + budgets; with --input a full in-memory dry run |
space | --input, --pack, --section, --allow-relayout, --scene, --json, --verbose | room report: each string's room, English use and, with --pack, the pack's use and what import does with each line (a dry run) |
strip | --pack, -o, --notes | the shareable shape: filled entries only, no source: / context: |
import | --input, --pack, --output, --patch, --allow-relayout, --accents strict|fold|font, --verbose | applies a pack to a copy; --accents overrides the pack's accents: header; --allow-relayout needs --output and refuses --patch |
lift-official | --from, --target, -o, --fold-accents, --language, --baseline | another Latin disc's text as a USA-keyed working pack (below) |
diff-disc | --input, --other | text-free alignment counts between two discs |
fit-report | --from, --target | text-free per-string vs per-MAN fit |
Two pack shapes
| Shape | source: | Holds | Lives |
|---|---|---|---|
| working | yes | the disc's own text, for the translator to read | your machine only |
| distributable | no | only the new text, keyed by disc coordinates | shareable / committable |
strip drops every source: and context: field and every unfilled entry. The key is a disc coordinate, not text, so a distributable pack carries none of the original script. Its budget is only a hint: import re-measures every target on the disc it patches and rejects anything that does not fit. Same-size is enforced from the disc, never from the pack.
What a pack contains
format: 'legaia-text-pack-v1'
language: 'fr'
game: 'Legend of Legaia (USA) SCUS-94254'
contributors: ['...']
sections:
items: # one list per section, fixed order
- key: 'scus:str:0x80012260' # disc coordinate
context: 'item 0x79' # human hint, not machine-read
source: 'Healing Berry' # US text (working pack only)
translation: '' # fill me
budget: 13 # max encoded bytes
| Section | Contents | Key | Patch mechanism |
|---|---|---|---|
items | item names | scus:str:0x<va> | overwrite the string in SCUS_942.54 in place, re-terminate |
item_types | shared item "type" strings | scus:str:… | same |
spells | spell / magic names and their info-window descriptions | scus:str:… | same |
arts | Tactical Arts names and arts-menu descriptions | scus:str:… | same |
accessory_passives | Goods-menu passive names + descriptions | scus:str:… | same |
party_names | new-game roster names | scus:party:<n> | fixed 10-byte field, 9-byte budget |
scene_dialog | NPC / event dialogue in scene MANs | man:<prot>:0x<off> | edit inside the decompressed MAN, recompress into the original footprint |
inline_text | dialogue / narration in raw carriers | raw:<prot>:0x<off> | space-padded same-size overwrite in the PROT entry; in the streaming dungeon scenes a longer line grows the uncompressed MAN chunk instead (within the entry's sector slack, or by whole sectors with --allow-relayout) |
ui_menu | pause-menu / shop / status labels, battle messages and help, Seru-magic effect lines, record screen, name entry, memory-card messages | ui:<prot>:0x<va> | overwrite in the overlay entry at va - base_va |
system_text | executable strings outside the name tables: battle command chips (Attack / Item / Run / Begin), level-up lines, empty-list messages, steal results | scus:str:0x<va> | same as the name tables |
place_names | world-map quick-travel place names | scus:cell:0x<va> | fixed 32-byte cell |
monster_names | enemy names (battle plaque and battle lines) | mon:<id> | rewrite inside the monster's record (PROT 867), re-pack its fixed slot |
Dialogue sections are line-granular: the pager packs up to three consecutive segments into one box (MES), so consecutive pack entries are consecutive rows on screen. Translate them as a group, each row inside its own budget.
Details: shared strings, alignment padding, duplicates
- Strings pointer-shared by several table slots export once; the
contextlists the referencing ids, and interior pointers clamp the budget. - The SCUS name pools are 4-byte aligned, so each budget also claims the 0..3 zero bytes after the terminator (verified zero, never past another string) - about 1.5 extra bytes on average, which is what lets the tightest tables translate at all.
- Duplicate PROT TOC entries over the same disc bytes are deduplicated by LBA.
ui_menupools are pinned by VA windows in the menu (PROT 0899), battle (PROT 0898) and field (PROT 0897) overlays; see field menu. Short writes zero-fill the old span. A window that mixes labels with debug formats reads each string token-aware (a{c1:00}name token is not a terminator) and keeps only UI-shaped text.- The port draws the game's own strings for the pause menu (labels, Options rows, shop strings, the Throw Out / Door of Light / Incense confirms, Yes / No, the spell level-up and art-learned notices), the battle screen (command chips, banners, steal captions, the victory report and the loss line) and the name-entry screen, so a translation of them shows in retail and the port alike. Its readers follow a moved label through the code that forms its address and keep accent-cell bytes. The two notice templates are patched by their renderers: the spell sentence must open with its
{c3:..}token, and the art line must keep its{c1:..}and{c5:..}tokens; import refuses a translation that breaks either. - A monster name keeps its markup: a leading
{5e:xx}element badge and a trailing$Nvariant. Its in-place budget is the record's own room up to the next data the record addresses; a longer name grows the record, never above the longest retail name (15 bytes).
Text markup + encoding
Game text is plain ASCII plus a few control tokens (dialog font). Keep every token where the source has it.
| Markup | Meaning |
|---|---|
A-Z a-z 0-9 … | printable ASCII maps to itself |
| | in-game newline |
{c1:00} | character-name substitution |
{c2:79} {c3:..} {c5:..} | item / magic / art name substitution |
{cf:0n} {ce:..} | colour change; spacing / icon escape |
{xx} | bare byte ({01} item-icon prefix); literal braces are {7b} / {7d} |
Encoding reports per-character errors. Smart quotes, dashes, ellipsis and NBSP fold automatically. The retail font draws plain letters only; the pack's accents: header decides what happens to a typed accent on import:
accents: | Typed é | The disc |
|---|---|---|
empty / strict | encode error, reported per character with its fold and cell | unchanged |
fold | written as e | unchanged |
font | written as the byte 0x82 | the accent font is written once: accented glyphs built from the disc's own letters, with a width each |
One table, legaia_font::latin, holds the byte layout (CP437 / CP850, the bytes the PAL discs use) and the folds; the CLI, the ROM patcher and the workbench all read it. translate import --accents overrides the header; stats and space --pack list every character that will not draw as typed, with its key and index. Cyrillic, Greek and CJK have no glyph and no fold. Reference: pack-format.md § Accents.
Budgets: the same-size rule
| Target | Budget |
|---|---|
| Menu / battle / system labels | the original string's span plus its alignment padding; a longer label moves, and every instruction or pointer that reaches it is rewritten - into bytes other labels in the same image free, the menu overlay's reserved translation bytes, or (system text) the name tables' free runs; a label nothing references directly stays, and longer is reported |
| Party name / place name | fixed fields: 9 and 31 bytes |
| Item / spell / art / accessory name | in place, its 4-byte-aligned slot - so 0..3 spare bytes depending on the English length; a longer name moves into bytes other, shorter names give up, and its table pointers follow (still same-size, PPF-safe) |
| Monster name | in place, its record's 7 / 11 / 15-byte slot; a longer one grows the record, up to 15 bytes (the longest retail name) |
| Dialogue line | its own span, space-padded when shorter; an overflowing line triggers the MAN rewriter, which grows the line and relocates everything after it (MAN relocation) |
| Whole scene | the rewritten MAN must recompress into its original LZS footprint - retail scenes have zero compressed slack |
A name that outgrows its slot is moved automatically - no option to set. The pause menu, battle and every other screen read names through the executable's pointer tables, and those are the only references to them, so the text can live anywhere in the name pools. A name moves only when the other translations leave a free run long enough for it; otherwise it stays English and is reported (no free run) - shorten it or its neighbours in the same tables. Byte room is not screen room: a list still has its columns, and a very long item name runs into the quantity.
When a scene still overflows, import rolls back its longest lines one at a time with a per-key diagnostic - shorten those and re-run. The CLI's import --allow-relayout - the patcher page's Give translated dialog more room checkbox - instead grows the scene by whole sectors (an --output image only, never a PPF). Each target is also checked against the pack's source; a mismatch skips the entry rather than writing blind.
To see the room: legaia-patcher translate space --input DISC.bin [--pack PACK.yaml] reports every name region, monster record, label pool and scene with what uses it, and the translation workbench shows the same numbers in the browser while you edit, with each line's width on screen drawn in the game's font.
Details: the dialogue-carrier gate on raw: writes
The 0x1F <text> 0x00 dialogue framing is short enough to occur by coincidence in binary banks - music, sample banks, mesh packs, monster archives, animation slots. A same-size write into one passes every budget check and freezes the game. Both export and import therefore accept a raw: target only when its PROT entry carries at least MIN_CARRIER_PROSE multi-word segments; real carriers and binary banks separate by a wide margin. scus:, man: and ui: targets are structurally addressed and unaffected.
Details: ordering next to the randomizer
Combined with a randomizer run, a pack is applied in two phases. Dialogue (man: / raw:) goes first, because the door, location-rename and starting-bag passes relocate MAN records, and text applied earlier rides along. Names (scus: / ui:) go last, because the equipment-bonus-drop pass classifies gear by matching English item names; nothing in the randomizer relocates a SCUS or overlay string. The randomizer otherwise reads ids, never text.
Coverage + limits
- Covered: every SCUS name table and the spell / arts descriptions; NPC, cutscene and narration dialogue, picker labels, chest flavour text; overlay UI labels, battle system messages and the battle command chips; enemy names.
- Tight: the overlay label pools and the command chips - a short label can be shorter than English but rarely longer.
- Stays English: lines the conservative segment scanner declines to export. Junk entries it does export (dev-debug strings) are harmless - leave them blank.
Translations from another disc
Sony shipped French, German and Italian discs (SCES_019.44 / .45 / .46), plus Spanish (.47) and an English European release (SCES_017.52). They are structurally 1:1 with the USA disc, so their text lifts onto USA coordinates id-for-id and line-for-line - from your own two discs, in the browser or on the CLI. The official text is never bundled with this site. The three measured discs have their name tables pinned; the others are found by search at run time.
Fan translations too. A community translation published as a binary patch (xdelta, PPF) becomes an editable pack the same way: apply the patch to the disc it was made for, then lift from the result with --language naming the language (the disc does not say). A patched USA disc works as well as a patched PAL one. Lines the patch left untouched lift as the underlying disc's text; Japanese discs are not readable this way.
| Command | Does |
|---|---|
translate lift-official --from OTHER.bin --target USA.bin -o pack.yaml | Detects the build, re-keys every table and line into a filled working pack. --language pt-BR stamps a fan patch's language; --baseline RETAIL.bin blanks every line the patch left as the retail disc had it, so only the translator's own text remains. --fold-accents (browser default) maps é→e, ß→ss. |
translate diff-disc | Alignment counts between two discs, text-free. |
translate fit-report | Per-string vs per-MAN fit, text-free. |
Not all of it fits, because of sector alignment: USA scenes are packed with zero slack, so a longer line overflows. Roughly a third of the dialogue lands in place; the rest is reported, not dropped quietly. import --allow-relayout closes the residual, on the CLI or with the patcher page's relayout checkbox.
Details: fit numbers and the recommended path
The PAL discs gave most scene entries one extra sector at mastering. In place, name tables land 36-60% and dialogue about half per string. Overlay and executable labels are lifted by the code and data that point at them (the PAL builds lay their string pools out differently); an opening crawl the PAL build reflowed into more pages pairs its text pages in order. Recommended path: lift → fit-report → import with relayout → font patch → strip to site/lang/<lang>.yaml. Full layout, accent map and fit numbers: pal-localizations.md.
Text baked into textures
Some UI text is pixels in a texture, not a string. Replacing one means new art at the exact same TIM footprint so the same-size write applies; the ROM patcher's texture replacement makes that possible. The blocker is art, not the pipeline.
Details: the text-bearing textures and their footprints
| Texture | Baked text | Notes |
|---|---|---|
| Title wordmark (PROT 0888) | logo, PRESS START BUTTON, copyright bands | bands are sub-rects of one 256×256 TIM; copyright bands must stay |
Title menu NEW GAME / CONTINUE | drawn from the glyph atlas | real text in a region the pipeline does not address by coordinate |
| Save / load UI (PROT 0899) | SLOT n pill, Load panel | small 4bpp TIMs; a same-footprint swap is feasible (save screen) |
| Boot / publisher logos (PROT 0895) | brand marks | do not alter - trademark art |
| Opening prologue caption | a baked caption under the crawl | the crawl narration itself is dialogue and is covered |
AI example packs
Machine-translated packs are starting points only. Prefer community packs, treat any AI-filled pack as a draft to correct, and credit editors in contributors.
How we know
| Component | Where | What it proves |
|---|---|---|
| Exporter / importer | crates/translate/src/{export,import}.rs | Same-size writes, source check, per-scene rollback, two-phase ordering. |
| Markup codec | markup.rs | Byte-exact decode↔encode; per-character encodability errors. |
| Carrier gate | segments.rs (is_dialog_carrier), test translation_carrier_gate_real.rs | Binary banks and real carriers separate on the retail disc. |
| Overlay UI pools | ui.rs, test translation_ui_menu_real.rs | Menu / battle label windows at their disc coordinates. |
| MAN rewriter | legaia_scene_formats::man_edit (apply_text_edits, text_edits_preserve_scripts) | A grown line leaves the scene script identical when re-walked. |
| PAL lift + fit | translation/{lift,diff,fit}.rs, tests translate_lift_official_real.rs, translate_relayout_import_real.rs | 1:1 alignment; the fit numbers above. |
| Shipped-pack hygiene | test translation_shipped_packs.rs | No tracked pack carries source:; with a disc, no translation is its own English line with the words reversed. |