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 through DiscPatcher (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

  1. Your discThe USA image you own.
  2. ExportEvery catalogued line to YAML.
  3. Working packHolds the game script beside your text - local only, never share.
  4. Fill + statsAny text editor; stats dry-runs against the disc.
  5. StripDrops the source: text.
  6. Shipped packDisc coordinates + your text only: committable.
  7. ImportOnto a copy of the disc (plus a PPF).

Only the first and last steps need a disc; everything in between is text editing.

StepCommandDoes
1translate export --input DISC.bin -o legaia_en.yamlDumps the disc's own text into a working pack (once).
2translate init --lang fr --from legaia_en.yaml --contributor you -o legaia_fr.yamlSkeleton for your language; --resume site/lang/fr.yaml seeds it from a published pack.
3translate merge --base legaia_fr.yaml --pack part1.yaml --pack part2.yaml -o legaia_fr.yamlFill the translation: fields with any editor; init --chunk N splits the work, merge recombines it.
4translate stats --pack legaia_fr.yaml [--input DISC.bin]Coverage, encodability and budgets; --input dry-runs against a real disc.
5translate strip --pack legaia_fr.yaml -o site/lang/fr.yamlDrops the source text - the result is the shareable pack.
6translate import --input DISC.bin --pack legaia_fr.yaml --output out.bin --patch out.ppfApplies 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 carries source:.
  • Never commit working packs - they carry the game's script. /translations/ and legaia_*.yaml are 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 controlCLI equivalent
Export a starter pack from my disctranslate export
Export a working copy of this pack (a pack chosen; English beside each line)translate init --resume
Check pack against my disctranslate stats --input
Download skipped lines (.csv) - columns key, reason, messagethe stats / import skip list (--verbose)
Give translated dialog more roomtranslate import --allow-relayout
Make a shareable pack (for an imported pack)translate strip
Translation from another disc I owntranslate 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.

SubcommandFlagsDoes
export--input, -oevery cataloged string into a working pack with empty translation: fields
coverage--input, --json, --verbosethe 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, -oan 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), -omerges filled entries onto the base by key; -o may be the base
stats--pack, --input, --verbosecoverage + encodability + budgets; with --input a full in-memory dry run
space--input, --pack, --section, --allow-relayout, --scene, --json, --verboseroom 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, --notesthe shareable shape: filled entries only, no source: / context:
import--input, --pack, --output, --patch, --allow-relayout, --accents strict|fold|font, --verboseapplies 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, --baselineanother Latin disc's text as a USA-keyed working pack (below)
diff-disc--input, --othertext-free alignment counts between two discs
fit-report--from, --targettext-free per-string vs per-MAN fit

Two pack shapes

Shapesource:HoldsLives
workingyesthe disc's own text, for the translator to readyour machine only
distributablenoonly the new text, keyed by disc coordinatesshareable / 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
SectionContentsKeyPatch mechanism
itemsitem namesscus:str:0x<va>overwrite the string in SCUS_942.54 in place, re-terminate
item_typesshared item "type" stringsscus:str:…same
spellsspell / magic names and their info-window descriptionsscus:str:…same
artsTactical Arts names and arts-menu descriptionsscus:str:…same
accessory_passivesGoods-menu passive names + descriptionsscus:str:…same
party_namesnew-game roster namesscus:party:<n>fixed 10-byte field, 9-byte budget
scene_dialogNPC / event dialogue in scene MANsman:<prot>:0x<off>edit inside the decompressed MAN, recompress into the original footprint
inline_textdialogue / narration in raw carriersraw:<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_menupause-menu / shop / status labels, battle messages and help, Seru-magic effect lines, record screen, name entry, memory-card messagesui:<prot>:0x<va>overwrite in the overlay entry at va - base_va
system_textexecutable strings outside the name tables: battle command chips (Attack / Item / Run / Begin), level-up lines, empty-list messages, steal resultsscus:str:0x<va>same as the name tables
place_namesworld-map quick-travel place namesscus:cell:0x<va>fixed 32-byte cell
monster_namesenemy 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 context lists 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_menu pools 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 $N variant. 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.

MarkupMeaning
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 / strictencode error, reported per character with its fold and cellunchanged
foldwritten as eunchanged
fontwritten as the byte 0x82the 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

TargetBudget
Menu / battle / system labelsthe 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 namefixed fields: 9 and 31 bytes
Item / spell / art / accessory namein 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 namein place, its record's 7 / 11 / 15-byte slot; a longer one grows the record, up to 15 bytes (the longest retail name)
Dialogue lineits 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 scenethe 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.

CommandDoes
translate lift-official --from OTHER.bin --target USA.bin -o pack.yamlDetects 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-discAlignment counts between two discs, text-free.
translate fit-reportPer-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
TextureBaked textNotes
Title wordmark (PROT 0888)logo, PRESS START BUTTON, copyright bandsbands are sub-rects of one 256×256 TIM; copyright bands must stay
Title menu NEW GAME / CONTINUEdrawn from the glyph atlasreal text in a region the pipeline does not address by coordinate
Save / load UI (PROT 0899)SLOT n pill, Load panelsmall 4bpp TIMs; a same-footprint swap is feasible (save screen)
Boot / publisher logos (PROT 0895)brand marksdo not alter - trademark art
Opening prologue captiona baked caption under the crawlthe 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

ComponentWhereWhat it proves
Exporter / importercrates/translate/src/{export,import}.rsSame-size writes, source check, per-scene rollback, two-phase ordering.
Markup codecmarkup.rsByte-exact decode↔encode; per-character encodability errors.
Carrier gatesegments.rs (is_dialog_carrier), test translation_carrier_gate_real.rsBinary banks and real carriers separate on the retail disc.
Overlay UI poolsui.rs, test translation_ui_menu_real.rsMenu / battle label windows at their disc coordinates.
MAN rewriterlegaia_scene_formats::man_edit (apply_text_edits, text_edits_preserve_scripts)A grown line leaves the scene script identical when re-walked.
PAL lift + fittranslation/{lift,diff,fit}.rs, tests translate_lift_official_real.rs, translate_relayout_import_real.rs1:1 alignment; the fit numbers above.
Shipped-pack hygienetest translation_shipped_packs.rsNo tracked pack carries source:; with a disc, no translation is its own English line with the words reversed.

See also