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; accented and non-Latin scripts (the retail font has no glyphs for them).
The one rule
Every edit is same-size in place. Each line has a byte budget; a line over budget is reported and left English.
Sharing
A pack is safe to publish once its source: text is stripped - what remains is your text keyed by disc coordinates.
Code
crates/patcher/src/translation/; writes go through DiscPatcher (EDC/ECC re-encoded, no LBA moves).
Own a PAL disc?
Its official French / German / Italian text can be lifted onto the USA disc from your own two discs.

Workflow

your disc exportto YAML working pack(local only) fill + stats(any editor) stripsource text shipped pack(committable) importto copy holds the game script - never share coordinates + your text only
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.

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 namesscus:str:…same
artsTactical Arts namesscus: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
ui_menupause-menu / shop / status labels, battle messagesui:<prot>:0x<va>overwrite in the overlay entry at va - base_va

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) and battle (PROT 0898) overlays; see field menu. Short writes zero-fill the old span.

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. Accented Latin, Cyrillic and CJK are not encodable - the retail font has no such glyphs, so French or Italian must be written unaccented (Epee, not Épée). Lifting that limit needs a font patch, which is a separate effort.

Budgets: the same-size rule

TargetBudget
SCUS / overlay stringsthe original string's span; shorter is fine
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

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

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; NPC, cutscene and narration dialogue, picker labels, chest flavour text; overlay UI labels and battle system messages.
  • Tight: the overlay label pools - a short label can be shorter than English but rarely longer.
  • No string exists: the in-battle Attack / Arts / Magic / Item ring is icon sprites.
  • Stays English: lines the conservative segment scanner declines to export. Junk entries it does export (dev-debug strings) are harmless - leave them blank.

Official PAL translations

Sony shipped French, German and Italian discs (SCES_019.44 / .45 / .46). 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.

CommandDoes
translate lift-official --from PAL.bin --target USA.bin -o pack.yamlDetects the language, re-keys every table and line into a filled working pack. --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.

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 ui_menu labels are not lifted - the PAL overlays place their pools elsewhere. 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)
In-battle command ringAttack / Arts / Magic / Itemicon sprites; a worded localisation needs new icon art
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/patcher/src/translation/{export,import}.rsSame-size writes, source check, per-scene rollback, two-phase ordering.
Markup codectranslation/markup.rsByte-exact decode↔encode; per-character encodability errors.
Carrier gatetranslation/segments.rs (is_dialog_carrier), test translation_carrier_gate_real.rsBinary banks and real carriers separate on the retail disc.
Overlay UI poolstranslation/ui.rs, test translation_ui_menu_real.rsMenu / battle label windows at their disc coordinates.
MAN rewriterlegaia_asset::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:.

See also