At a glance

What it does
Turns your own PAL disc's official script into a filled language pack keyed for your own USA disc
Why it works
The containers are 1:1: a USA PROT coordinate names the same logical asset on every PAL disc
Lift
legaia-patcher translate lift-official --from <PAL.bin> --target <USA.bin> -o fr.yaml
Measure
translate diff-disc --input <USA.bin> --other <PAL.bin> (alignment counts), translate fit-report (budgets)
Accents
One high byte per letter on a CP437-aligned layout; drawn on NTSC by the accent font, or folded to ASCII
Fit
Same-size in place for part of the corpus; --allow-relayout grows scene MANs by whole sectors and imports the rest byte-faithfully
In the browser
The ROM patcher runs the same lift in the tab; neither disc is uploaded
Code
legaia_translate::lift, legaia_translate::diff; disc-level relayout legaia_iso::relayout
legaia-patcher translate lift-official --from <PAL-FR.bin> --target <USA.bin> -o fr.yaml

A lifted pack is Sony text

The working pack carries both the USA source text and the official translation. Keep it local; translate strip removes the source side before anything is shared.

The lift, end to end

PAL disc (yours)build from SYSTEM.CNF PROT 1:1 alignmentsame 1233-entry index space USA disc (yours)the coordinate space pairing, per kind of text Name tableslocate base by meta bytes, id for id DialogMAN walk (record, lead); scan fallback String poolsanchors, in-order alignment, references Accent byteskeep (accent font) or fold to ASCII Re-key onto USA coordinatesfilled working pack fit-reportper-string and per-MAN budgets Import in placesame-size; over-budget lines reported --allow-relayoutMANs grow by whole sectors
Every stage reads the two discs you supply. The pairing differs by kind of text because each kind is laid out differently: tables carry ids, dialog follows the script's program order, and string pools repack by length.

Region ids

RegionBoot exe (SYSTEM.CNF)exe t_addrName-table bases
USA (NTSC, reference)SCUS_942.540x80010000the coordinate space itself
France (PAL)SCES_019.440x80010000measured, pinned
Germany (PAL)SCES_019.450x80010000measured, pinned
Italy (PAL)SCES_019.460x80010000measured, pinned
Spain (PAL)SCES_019.47-located at run time
Europe, English (PAL)SCES_017.52-located at run time

All measured builds are PS-X EXE. The PAL executables are 2-4 KB larger (extra code), so pc0 and the data segment shift up relative to USA. Unpinned builds are accepted and their tables found by search, and a fan-patched disc of any of these builds - the USA disc included - lifts the same way. The JP discs (SCPS_*) are refused: their text is not the Latin codec.

Mastering-metadata quirks

  • The PAL executables carry the North America region mark. The PS-X EXE header string at +0x4C names the North America area on all three PAL discs. The system-area licence text (sectors 0..15) is the authority - it names Europe, and it is what the console validates. Region identification must not trust the exe header string.
  • France's PVD creation date has a zeroed year (0000). A parser treats a 0000 year as absent, not as data.

Structural parity with USA

The PAL discs are 1:1 with USA at the container level:

  • identical ISO 9660 file tree (SYSTEM.CNF, the boot exe, PROT.DAT, DMY.DAT, CDNAME.TXT, the MOV movies and XA streams);
  • identical PROT.DAT header (file_num=1236, header_sectors=3, 1233 usable entries), identical TOC head, the same scene-block boundaries;
  • CDNAME.TXT byte-identical between USA and Germany; France and Italy differ only in a few in-place label bytes, with the same #define block structure;
  • about 95% of PROT entries have the USA size. Nearly every difference is exactly +1 sector - a scene whose localized MAN recompresses slightly larger - with a few larger deltas in the audio, battle_data and overlay carriers. No entry is added, removed or reordered.

Consequence

A USA PROT coordinate names the same logical asset on every PAL disc: entry i (scene block, MAN, overlay) is the same thing on all four. Absolute LBAs are not a parity signal - the sector growth of the differing entries shifts every later LBA even though the index space is identical.

Disc size delta

The PAL discs are about 34 MB larger than USA, almost entirely in XA streamed audio (PAL 50 Hz re-timing). The movies and DMY.DAT are byte-identical, PROT.DAT grows about 150 KB and the executable about 4 KB. The localization did not restructure or re-author assets.

What the executables do not share

Container parity says nothing about code. The boot executable differs, and two of the differences change play:

  • No battle-load stat boost. SCUS_942.54's FUN_80054CB0 multiplies ATK / UDF / LDF / INT as it installs each enemy (two profiles keyed on the boss switch). Every PAL executable copies the record in untouched - see battle.
  • Larger victory spoils. The PAL spoils routine has neither the second gold halving nor the 3/4 EXP cut of the USA build: half the record gold and the whole record EXP reach the party - see battle formulas.

Both are code-only: the monster archive (PROT 0867) carries the same stats and rewards on all four discs. The JP original behaves like PAL on both counts, so both are USA additions rather than PAL removals. Anything that fingerprints a disc by its executable bytes (scus-pokes, the static-overlay map, the port-catalog denominators) is USA-only by construction.

Name-table alignment

The five executable name tables exist in each SCES executable at shifted, language-specific addresses. The pointer region shifts by roughly a constant per language with small local drift, so each table is located - its language-independent stats and meta columns fingerprinted against USA - rather than shift-computed.

Table (USA VA)FranceGermanyItaly
item names 0x8007436C0x80074C4C0x800753600x80075130
spell / magic 0x800754C80x80075DA80x800764BC0x8007628C
Tactical Arts 0x80075EC40x800767A40x80076EB80x80076C88
accessory passive 0x8007625C0x80076B3C0x800772500x80077020
new-game party 0x80078C4C0x800795080x80079C780x80079A14

Record layout (stride, count, field order) is unchanged, and each record carries the address of its own string, so once the base is located id N maps to the localized name for id N. The string pool itself repacks per language, so only the pointer tables are followed - never a constant string offset.

Dialog-corpus alignment

The 0x1F-segment dialog corpus (scene-bundle MANs plus raw event-script carriers) is walked by PROT entry index - the same index space on every disc. Segment byte offsets never match, because a localized line has a different length and the MAN repacks. Line order is the script's, not the text's, so lines pair by position: the Nth qualifying segment of entry i on USA is the Nth on the PAL disc.

Measured with diff-disc, corpus totals match within about 1% and about 99% of lines are order-pairable per entry. The rest needs reconciliation: scanner-marginal short runs, coincidental hits, the occasional localizer line split or merge. A strict "whole entry must match exactly" metric reads far lower and is only a lower bound.

Accented-text encoding

The PAL discs keep the USA markup framing: 0x1F segment lead, 0x00 terminator, the same two-byte opcodes (0xC1..0xC5 substitution, 0xCE spacing, 0xCF colour). Only the glyph atlas is extended above 0x7E.

Accented Latin is a single high byte on a CP437-aligned layout for the lowercase accents and the capitals CP437 carries:

byteglyphbyteglyphbyteglyph
0x80Ç0x8Aè0x94ö
0x81ü0x8Bï0x95ò
0x82é0x8Cî0x96û
0x83â0x8Dì0x97ù
0x84ä0x8EÄ0x99Ö
0x85à0x90É0x9AÜ
0x87ç0x93ô0xE1ß

The accented capitals CP437 lacks sit at their CP850 positions: Â 0xB6, È 0xD4, Î 0xD7, Ì 0xDE, Ô 0xE2. Two of these bytes draw a placeholder box on the PAL disc itself - its font page has no Î at 0xD7 and no ° at 0xF8, and no PAL executable remaps either - so retail French shows a box for every Î and retail Italian one for every ° (dialog font). The French executable also folds oe / OE to its ligature cells at draw time; the script itself spells both letters.

No accent byte falls in the 0xC0..0xCF two-byte-opcode window, so glyph space and control space stay disjoint. German needs 7 cells, French about 14, Italian about 10; the union is about 40.

Font-patch scope for NTSC

The dialog font is one 4bpp TIM at PROT.DAT offset 0x7F40 (uploaded to VRAM (896, 0)) plus the 256-byte width table at 0x80073F1C; the PAL discs carry theirs at the same offset. The USA page already holds some accented glyphs in the high cells but with zero widths, so they overprint the next letter. Rendering PAL text on NTSC therefore needs no structural change - only glyphs in the layout's cells and a width for each. The accent font does exactly that at patch time, from your own disc; a pack asks for it with accents: font. It is not the 0xCE escape table at 0x80074050 and not the menu atlas at PROT.DAT 0x11218.

Lifting an official translation

translate lift-official re-keys the official text onto the USA coordinate space the importer patches (legaia_translate::lift):

  1. Detect the source build from SYSTEM.CNF's BOOT line. A measured build brings pinned bases; an unmeasured one none.
  2. Locate each name-table base. A pinned base is accepted when it validates against the USA-populated id set (the same ids resolve to name-shaped strings), else a window around it is searched. Pointer validity alone does not pick a base - the item table's records read as the accessory table's four bytes in - so candidates are ranked by how many meta bytes (stats, ids, scope) equal the USA table's. The unpinned search lands on every pinned base of the measured discs. The party template is found by the same fingerprint.
  3. Re-key. Name tables id for id; party names by fixed field; dialog structurally: each MAN is walked record by record with the field-VM disassembler, and a line's coordinate is (record ordinal, ordinal among the record's text leads) - the same on every build, because the script is one program with different strings. The scan-ordinal pairing is only the fallback for a line the walk does not place.

Why the walk, not the scan

A scan gates segments on prose quality. Where one build reduces a line to a bare item token and the gate drops it on that side only, every later scan ordinal names the previous line - a shift the pair counts never show. The lift report counts lines placed structural, by scan ordinal and shifted. A walked MAN lead skips the gate entirely.

Narration crawls are the one place the program itself differs: the page count is part of the script, and the PAL builds reflow a block into more pages with blank pages between paragraphs. A crawl page is keyed (record, block, page) apart from the record's other lines; an equal-length block pairs page for page, a reflowed one pairs its text pages in order when both builds carry the same number, and anything else pairs nothing.

The output is a filled working pack (source = USA text, translation = official text, USA byte budgets). Across French, German and Italian, every pooled table locates at full validity with no unmapped string, and the dialog corpus pairs at 98.5-99.8% per PROT entry.

Accent folding

By default the pack keeps the accent bytes verbatim (as {82}-style escapes the codec round-trips) and is stamped accents: font, so import writes the accent font and they draw - 0xD7 and 0xF8 included, which the PAL disc they came from draws as boxes. --fold-accents (the default in the browser) instead folds accent cells onto ASCII: é to e, ß to ss, Ü to U, via legaia_font::latin. Folding is one byte for one except ß, æ / Æ and œ / Œ, which can push a line over budget. Non-accent high cells are left alone, and both counts - folded and left raw - are reported.

How the string pools pair

Menu, system and place-name pools are lifted by their own pairing. A pool is pinned by a USA-coordinate window, and the same pool on the other build sits near the same offset but not at it. Both windows are cut into NUL-delimited chunks; chunks byte-identical and unique on both sides are anchors that partition the lists. Inside a partition chunks pair by ordinal when the counts match, otherwise by an in-order alignment that lets the source carry extra chunks anywhere and requires each pair to agree in shape. A head or tail alignment is the wrong model: one extra label variant in the middle of a pool slides every later label by one.

Two more rules keep a pool's tail from sliding. A fragment the NUL scan cuts at a name token's 0x00 argument (the @{c1} head of @{c1:00} will equip) is not a string: it never anchors and pairs only with a fragment of the same side. And skipping source chunks is cheap only past the pool's end, where the widened window runs on; a skip before or between pairs costs more. The French, German and Italian menu overlays are laid out differently from USA's, so the menu pool pairs by these rules alone; every label and confirm prompt the port reads pairs on all three. The two notice templates in the overlay's data segment pair on none and are filled by hand.

A pool string is also paired through the code and data that reference it (translation::refpair). A localized build lays its pools out by length - short strings in the $gp small-data pool, longer ones in read-only data - so a translation can sit in a different pool from the USA string while the record pointing at it is the same record. Each reference's surrounding words, with every address masked out, form a signature, and a USA reference pairs with the closest source reference. An unpaired label stays vanilla. Monster names pair record for record.

Lifting a fan translation

A fan translation shipped as a binary patch (xdelta, PPF) cannot become a pack by reading the patch - a patch is bytes, not text. The disc it produces can:

legaia-patcher translate lift-official --from <patched.bin> --baseline <retail.bin> --target <USA.bin> --language pt-BR --fold-accents -o <pack.yaml>
  • --language stamps the pack, since a patch's language is not on the disc.
  • --baseline names the retail disc the patch was built on and blanks every line that disc also carries, so lines the patch left untranslated stay vanilla on import instead of lifting the underlying build's text. A line with no prose of its own (control tokens and punctuation only) is kept even when the baseline carries it.
  • The baseline compare runs on raw bytes before the fold, so a line the patch only re-accented survives as the translator's work. Trailing pad spaces are dropped; a cell outside the fold tables stays a raw {xx} escape and is reported.
  • A translation that moved PROT entries or re-laid the ISO still lifts as long as the patched disc's TOC is consistent: the walk is by entry index, with sizes from that disc's own TOC.

The Spanish disc, measured this way, locates every table at full pointer validity and pairs its dialog in the same band as the measured three.

Fit rate against the USA target

translate fit-report --from <PAL.bin> --target <USA.bin> measures fit under two budgets, counts only:

Per string
A line fits when its encoded bytes are within its own USA segment span: about half the MAN dialog lines, 36-60% of name-table entries (Italian lowest)
Per MAN
A whole scene MAN fits when all its lines, grown to full length, relocate, validate and recompress within the MAN's on-disc footprint at the same LBA (MAN relocation)

The constraint is sector alignment, not string length

USA scene-bundle entries are sector-aligned with zero compressed slack, so growing any line overflows and whole-MAN growth in place fits only a small fraction. The residual is the large scene MANs, each needing a sub-sector amount of extra room - every residual deficit is under one 2048-byte sector. That is exactly the +1-sector-per-entry growth the PAL discs applied at mastering.

Full-ISO relayout

translate import --allow-relayout grows each sector-crossing scene MAN by whole sectors so its full-length dialog imports byte-faithfully - the operation the official discs did at mastering. Two structural facts make it safe:

  • The PROT.DAT TOC stores PROT-relative LBAs, so growing an interior entry shifts only later entries' internal start LBAs.
  • No file is located by a hardcoded absolute LBA: every file is found by ISO 9660 directory lookup, so files after PROT.DAT can move once the directory records, path tables and PVD are fixed.
Scene MAN growsceil(deficit / 2048) sectors PROT.DAT rebuiltlater entries' relative LBAs + G ISO 9660 refsevery LBA > PROT start gains G Path tables, dir records, PVDsize + G*2048, volume + G Sectors after PROT.DAT moveheader rewritten; ECC only if data changed
The whole reference graph reduces to one rule, so the relayout is a mechanical cascade rather than a search. The PROT entry index space is preserved, so every index-keyed randomizer edit still resolves afterwards.

The disc layer is legaia_iso::relayout::grow_prot_dat (generic ISO 9660 + ECMA-130), driven by DiscPatcher::grow_prot_entries. Above it the importer inserts the sectors after the MAN, shifts the later sub-assets and bumps their descriptor offsets. What the rewriter still refuses is a handful of keyed lines whose 0x1F … 0x00 framing is a coincidence inside an instruction's operands; the importer skips them with a diagnostic.

The raw corpus has no residual: every raw line lives in the uncompressed MAN leading one of the ten streaming dungeon scenes, which the importer rewrites with the same relocator. Growth lands in the entry's trailing sector slack when it fits (a same-size write) and otherwise in the relayout, bounded by the loader's 0x62C00-byte asset arena with 64 KiB of headroom.

In the browser

The ROM patcher offers the lift as a second language path: pick the official translation from your own PAL disc, supply it beside your USA disc, and lift_official_pack(usa, pal, fold_accents) runs in the tab. Neither disc is uploaded; the lifted pack lives in page memory and feeds straight into the patch with the same coverage report every pack gets. Folding is on by default there, and the relayout is the page's give translated dialog more room checkbox. Without it, roughly a third of the dialog corpus lands in place; the rest is reported, never dropped quietly.

  1. translate lift-official --from <PAL.bin> --target <USA.bin> -o <pack.yaml> - the working pack, kept local.
  2. translate fit-report --from <PAL.bin> --target <USA.bin> - the residual budget picture.
  3. translate import --input <USA.bin> --pack <pack.yaml> --allow-relayout --output <patched.bin> - byte-faithful dialog for every scene MAN. No --patch: a relayout grows the image, so it is not a same-size PPF overlay.
  4. The accent font, so accents render instead of folding to ASCII.
  5. translate strip --pack <pack.yaml> -o <lang>.yaml - a source-free, distributable pack.

See also