Lesson 209: Cross-Engine Localization Receipt Columns on BUILD_RECEIPT (2026)
Direct answer: Studios shipping Unity RPG and Ren'Py VN SKUs from one Weblate project need two receipt schemas on one BUILD_RECEIPT row—not merged JSON. This lesson ships localization_receipt_matrix_v1.json with M1–M6 gates so Lesson 202 string_table_receipt and language_receipt_v1 stay separate but Thursday row review treats them as one promotion decision.

Why this matters now (October 2026 mixed portfolios)
October 2026 fest teams run CN/JP store capsules from Weblate while the Unity demo shows raw UI_* keys and the Ren'Py build menus stay English-only—different engines, same producer panic. Lesson 202 owns Unity string_table_receipt; after Addressables strip PRs add Lesson 219 addressables_fest_string_table_receipt; the Ren'Py evening pipeline owns language_receipt—this lesson is the portfolio milestone wiring both columns on BUILD_RECEIPT.
Beginner path (45-minute matrix pass)
| Step | Action | Success check |
|---|---|---|
| 1 | List SKUs in fest scope | Unity exe + Ren'Py build IDs |
| 2 | Pass engine-specific smokes | 202 GREEN + language_receipt GREEN |
| 3 | Verify store locale list matches both builds | Same fest_locales[] |
| 4 | Write localization_receipt_matrix_v1.json |
Both engines pass: true |
| 5 | Patch BUILD_RECEIPT template | Two columns, one row |
| 6 | Run Thursday row review | Matrix + store parity |
Time: ~45 minutes when both receipts exist; 72 minutes first mixed portfolio setup.
Developer path (gates M1–M6)
| Gate | Check | Fail when |
|---|---|---|
| M1 | string_freeze_receipt conflict-free |
Weblate merge blocked |
| M2 | Unity string_table_receipt_v1 pass |
Raw keys in installed player |
| M3 | Ren'Py language_receipt_v1 pass |
Selector missing fest locales |
| M4 | fest_locales aligned across SKUs |
Store lists CN, Unity build EN-only |
| M5 | Matrix references correct receipt paths | Wrong schema mixed into one file |
| M6 | BUILD_RECEIPT row + Thursday review | Any M1–M5 red |
Receipt schema separation (do not merge)
| Engine | Receipt file | BUILD_RECEIPT column |
|---|---|---|
| Unity | string_table_receipt_v1.json |
string_table_receipt |
| Ren'Py | language_receipt_v1.json |
language_receipt |
| Ren'Py (hash audit) | po_hash_language_receipt_v1.json |
po_hash_language (optional; see PO-hash preflight) |
| Portfolio | localization_receipt_matrix_v1.json |
localization_matrix |
Anti-pattern: Pasting Ren'Py gates.L3_selector_ui into Unity JSON—breaks CI validators.
localization_receipt_matrix_v1.json
{
"schema": "localization_receipt_matrix_v1",
"build_label": "fest-portfolio-2026-10-rc2",
"fest_locales": ["en", "ja", "zh-Hans"],
"engines": {
"unity_rpg_demo": {
"receipt_schema": "string_table_receipt_v1",
"receipt_path": "release-evidence/localization/unity/STRING_TABLE_RECEIPT.json",
"pass": true,
"boot_locale_smoke": "ja"
},
"renpy_vn_demo": {
"receipt_schema": "language_receipt_v1",
"receipt_path": "release-evidence/localization/renpy/LANGUAGE_RECEIPT.json",
"pass": true,
"store_parity": true
}
},
"gates": {
"M1_string_freeze": "pass",
"M2_unity_string_table": "pass",
"M3_renpy_language": "pass",
"M4_locale_alignment": "pass",
"M5_schema_separation": "pass",
"M6_matrix_committed": "pass"
},
"promotion_allowed": true
}
Pin under release-evidence/localization/LOCALIZATION_RECEIPT_MATRIX.json.
BUILD_RECEIPT row template (one upload, two proofs)
| Column | Unity RPG demo | Ren'Py VN demo |
|---|---|---|
string_table_receipt |
path or verified |
n/a |
language_receipt |
n/a |
path or verified |
localization_matrix |
matrix path | matrix path |
fest_locales |
ja,zh-Hans |
same array |
store_capsule_locales |
must match fest_locales |
must match |
Thursday row review adds one line: matrix promotion_allowed before fest branch merge.
Publish gate
ALTER TABLE release_publish_gate ADD COLUMN IF NOT EXISTS
localization_matrix_blocked BOOLEAN NOT NULL DEFAULT false;
CI verify_localization_receipt_matrix_v1 requires:
engines.*.pass === truefor every SKU infest_scopefest_localesidentical across engine entries- Referenced receipt files exist and validate against schema
Prerequisites
- Lesson 202 — Unity string table smoke
- Ren'Py language selector blog
- Ren'Py Weblate preflight, PO-hash language preflight, Lesson 225
po_hash_languagereceipt, Ren'Py default language +.rpycrebuild preflight - Unity String Tables empty help
- 18 Free localization tooling resource
Common mistakes
- One Weblate export, zero Ren'Py
renpy.compile_all()— PO green, game English. - Unity Editor screenshots while Ren'Py installed build is wrong locale.
- Merging receipts into a single JSON blob—breaks per-engine CI.
- Promoting Unity when only VN
language_receiptpassed.
Troubleshooting
| Symptom | Lane |
|---|---|
| Unity keys, VN OK | Lesson 202 L3–L5 |
| VN English, Unity OK | Ren'Py preflight |
| Store CN, both builds EN | M4 store parity — regen screenshots from installed builds |
| Matrix pass, promotion blocked | Check string_freeze_receipt M1 |
Mini exercise (60 minutes)
- Pass Lesson 202 smoke on Unity SKU.
- File
language_receipt_v1.jsonon Ren'Py SKU from blog pipeline. - Intentionally mismatch
fest_locales— confirm M4 fails. - Write matrix JSON with both paths.
- Add BUILD_RECEIPT row; run Thursday checklist.
Continuity
- Previous: Lesson 208 — Construct CORS hosting decision
- Next: Lesson 210 — GIF capsule marketing receipt
- Guides: Ren'Py preflight (shipped); Unity String Tables preflight (guide queue #15)
FAQ
Single-engine studio?
Skip matrix—use Lesson 202 or Ren'Py blog only. Matrix is for two receipts, one producer gate.
Same Weblate project, different components?
Map weblate_export_manifest per component; matrix rows reference each receipt path.
HTML5 Construct demo localized?
Out of scope—this lesson is Unity + Ren'Py receipt columns; itch HTML5 uses separate store copy checks.
Weblate proves PO once—your matrix proves each engine's player build before October promotion.