Official PAL localizations
Legend of Legaia shipped official French, German and Italian translations on PAL discs. Their PROT.DAT archives line up entry for entry with the USA disc, which is what lets legaia-patcher translate lift-official re-key an official translation onto USA coordinates and hand it to the translation pipeline as an ordinary language pack. This page covers how the PAL discs are built, how they align to the USA coordinate space, how their accented text is encoded, and how well a lifted translation fits. It carries no game text - byte values, offsets and encodings only.
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-relayoutgrows 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 relayoutlegaia_iso::relayout
legaia-patcher translate lift-official --from <PAL-FR.bin> --target <USA.bin> -o fr.yamlA 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
Region ids
| Region | Boot exe (SYSTEM.CNF) | exe t_addr | Name-table bases |
|---|---|---|---|
| USA (NTSC, reference) | SCUS_942.54 | 0x80010000 | the coordinate space itself |
| France (PAL) | SCES_019.44 | 0x80010000 | measured, pinned |
| Germany (PAL) | SCES_019.45 | 0x80010000 | measured, pinned |
| Italy (PAL) | SCES_019.46 | 0x80010000 | measured, 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 EXEheader string at+0x4Cnames 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 a0000year 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, theMOVmovies andXAstreams); - identical
PROT.DATheader (file_num=1236,header_sectors=3, 1233 usable entries), identical TOC head, the same scene-block boundaries; CDNAME.TXTbyte-identical between USA and Germany; France and Italy differ only in a few in-place label bytes, with the same#defineblock 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_dataand 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'sFUN_80054CB0multiplies 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) | France | Germany | Italy |
|---|---|---|---|
item names 0x8007436C | 0x80074C4C | 0x80075360 | 0x80075130 |
spell / magic 0x800754C8 | 0x80075DA8 | 0x800764BC | 0x8007628C |
Tactical Arts 0x80075EC4 | 0x800767A4 | 0x80076EB8 | 0x80076C88 |
accessory passive 0x8007625C | 0x80076B3C | 0x80077250 | 0x80077020 |
new-game party 0x80078C4C | 0x80079508 | 0x80079C78 | 0x80079A14 |
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:
| byte | glyph | byte | glyph | byte | glyph |
|---|---|---|---|---|---|
| 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):
- Detect the source build from
SYSTEM.CNF'sBOOTline. A measured build brings pinned bases; an unmeasured one none. - 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.
- 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>--languagestamps the pack, since a patch's language is not on the disc.--baselinenames 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.DATTOC 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.DATcan move once the directory records, path tables and PVD are fixed.
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.
Recommended path to a distributable pack
translate lift-official --from <PAL.bin> --target <USA.bin> -o <pack.yaml>- the working pack, kept local.translate fit-report --from <PAL.bin> --target <USA.bin>- the residual budget picture.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.- The accent font, so accents render instead of folding to ASCII.
translate strip --pack <pack.yaml> -o <lang>.yaml- a source-free, distributable pack.