Lesson 289: Ren'Py Localization String Freeze PO Import Receipt on BUILD_RECEIPT (2026)
Direct answer: Before August–September 2026 October Next Fest runway treats “we froze strings” as GREEN, promote localization_freeze_receipt_v1.json with L1–L6—pin locale_manifest_v1.json, close Weblate writes, walk every required game/tl/ path, prove Preferences ↔ Steam language parity, smoke an installed build, and fail-close localization_freeze_ok on BUILD_RECEIPT so verbal freeze cannot leave selectors stuck on English. Pair Guide #49, freeze evening, and Help #54 for post-import recovery.

Why this matters now (August–September freeze before October upload)
October Next Fest store language claims ship while Weblate stays writable and Preferences still boot English. Lesson 288 closed the February→March Whisper arc; 289 opens the August–October ops spine for the RPG track—localization freeze is the first BUILD_RECEIPT latch before dual-SKU (290) and soft-launch (293).
Guide #49 is the ninety-second Ren'Py PO / tl/ gate; this lesson is the course-project BUILD_RECEIPT milestone + verify script.
Beginner path (55-minute freeze latch)
| Step | Action | Success check |
|---|---|---|
| 1 | File locale_manifest_v1.json |
Required locales listed with tl_path |
| 2 | Pin Weblate freeze commit | writes_closed: true |
| 3 | Walk every required game/tl/ dir |
No MISSING lines |
| 4 | Fill Preferences ↔ Steam parity table | All fest locales Y |
| 5 | Installed smoke EN + 2 locales | Screenshots in smoke/ |
| 6 | File freeze receipt + run verify | Exit 0 |
| 7 | Latch BUILD_RECEIPT localization_freeze_ok |
Boolean after verify |
Time: ~55 minutes first freeze week; ~20 minutes when manifest already pinned.
Prerequisites: Ren'Py project with game/tl/; Weblate (or TMS) export access; Steam Supported languages draft; jq + Bash/Git Bash; Guide #49 bookmarked.
Developer path (gates L1–L6)
| Gate | Check | Fail when |
|---|---|---|
| L1 | Locale manifest pinned | Verbal “we support JP” without tl_path |
| L2 | TMS / Weblate writes closed | Writers still open after “freeze” Slack |
| L3 | Required tl_path dirs exist |
Empty or missing locale folders |
| L4 | Preferences ↔ store parity | Steam lists language Ren'Py cannot select |
| L5 | Installed player smoke | Editor-only Language() click |
| L6 | Receipt + verify | localization_freeze_ok without verify |
Cross-cutting: Do not set po_import_reopen_allowed: true without a numbered exception. Do not treat Lesson 225 PO-hash / language wiring as freeze proof.
Cousin receipt crosswalk
| Field | Lesson 225 PO-hash / language | Freeze evening blog | This lesson (289) |
|---|---|---|---|
| Schema | PO-hash / language selector | localization_freeze_receipt_v1 (ritual) |
localization_freeze_receipt_v1 |
| Window | Hash + selector probe | One-evening L1–L6 | BUILD_RECEIPT latch |
| Pass field | language / hash ok | evening localization_freeze_ok |
localization_freeze_ok |
Reference cousins in cousin_receipts—do not flatten schemas.
L1–L3 — Manifest + Weblate pin + tl walk (course project)
MAN="release-evidence/localization/october-next-fest-2026/locale_manifest_v1.json"
jq -e '.locales | map(select(.in_game_required==true)) | length >= 1' "$MAN"
# After Weblate freeze commit:
jq -n --arg c "$(git -C weblate-mirror rev-parse HEAD)" \
'{writes_closed:true, weblate_freeze_commit:$c}' \
> release-evidence/localization/october-next-fest-2026/weblate_freeze_pin.json
jq -r '.locales[] | select(.in_game_required==true) | .tl_path' "$MAN" \
| while IFS= read -r p; do
test -d "$p" || { echo "MISSING tl: $p"; exit 3; }
done
L4 — Preferences freeze latch (store parity)
| Steam Supported language | Ren'Py Preferences / Language() id |
Pass? |
|---|---|---|
| English | english |
Y/N |
| Japanese | japanese |
Y/N |
| Simplified Chinese | schinese / project id |
Y/N |
Add a one-line Preferences freeze note: no new Language() buttons without L1 manifest bump.
L5 — Installed smoke (not editor-only)
Build distribution → install → boot English + two fest locales → capture main-menu screenshots into release-evidence/localization/october-next-fest-2026/smoke/. Editor-only Language() does not pass L5.
L6 — Receipt + verify + BUILD_RECEIPT
{
"schema": "localization_freeze_receipt_v1",
"freeze_label": "october-next-fest-2026",
"locked_utc": "2026-09-12T18:00:00Z",
"locale_manifest_path": "release-evidence/localization/october-next-fest-2026/locale_manifest_v1.json",
"locale_manifest_sha256": "REPLACE",
"weblate_freeze_commit": "abc123def",
"writes_closed": true,
"language_map_path": "release-evidence/localization/language_map_v1.json",
"store_parity_path": "release-evidence/localization/october-next-fest-2026/store_language_parity.md",
"smoke_dir": "release-evidence/localization/october-next-fest-2026/smoke",
"cousin_receipts": {
"language_map_lesson_225": "release-evidence/localization/PO_HASH_LANGUAGE_RECEIPT.json"
},
"gates": {
"L1_manifest": "pass",
"L2_tms_pin": "pass",
"L3_tl_paths": "pass",
"L4_store_parity": "pass",
"L5_installed_smoke": "pass",
"L6_receipt": "pass"
},
"localization_freeze_ok": true,
"po_import_reopen_allowed": false,
"notes": "Post-freeze PO requires exception list + new freeze label"
}
Pin at release-evidence/localization/october-next-fest-2026/LOCALIZATION_FREEZE_RECEIPT.json.
./scripts/verify_localization_freeze.sh \
release-evidence/localization/october-next-fest-2026/LOCALIZATION_FREEZE_RECEIPT.json
Minimum verify sketch:
#!/usr/bin/env bash
set -euo pipefail
REC="${1:-release-evidence/localization/october-next-fest-2026/LOCALIZATION_FREEZE_RECEIPT.json}"
jq -e '.schema == "localization_freeze_receipt_v1"' "$REC"
jq -e '.localization_freeze_ok == true' "$REC"
jq -e '.writes_closed == true' "$REC"
MAN="$(jq -r '.locale_manifest_path' "$REC")"
test -f "$MAN"
while IFS= read -r p; do
test -d "$p" || exit 3
done < <(jq -r '.locales[] | select(.in_game_required==true) | .tl_path' "$MAN")
echo "localization_freeze verify: OK"
BUILD_RECEIPT latch:
{
"localization_freeze_ok": true,
"localization_freeze_receipt": "release-evidence/localization/october-next-fest-2026/LOCALIZATION_FREEZE_RECEIPT.json",
"po_import_reopen_allowed": false
}
Relationship to Guide / evening / Help / language map
| Artifact | Role |
|---|---|
| Guide #49 | Ninety-second Ren'Py freeze gate |
| Freeze evening blog | Full L1–L6 ritual depth |
| This lesson | BUILD_RECEIPT milestone |
| Lesson 225 | PO-hash / language cousin |
| Help #54 | Selector stuck English recovery |
| Resource #52 forward | Weblate freeze tool bookmarks |
Order: language map GREEN (225) → Guide #49 L1–L6 → this lesson latch → dual-SKU Lesson 290. If Preferences stay English after unauthorized reopen, run Help #54 before flipping OK again.
Common mistakes
- Slack “freeze” while Weblate writers still open.
- Flipping
localization_freeze_okwithouttl_pathwalk. - Editor-only Language() as L5.
- Steam Supported languages listing locales Ren'Py cannot select.
- Treating Lesson 225 map GREEN as freeze proof.
- Reopening PO import without exception list (
po_import_reopen_allowed).
Troubleshooting
| Symptom | Fix |
|---|---|
| Selector stuck English after PO | Help #54 — tl_path walk, cache rebuild, Preferences probe |
MISSING tl_path |
Re-export PO into correct game/tl/<locale>/; bump manifest |
| Weblate still writable | Re-run L2 pin; do not latch L6 |
| Merge-conflict export block | Weblate merge help / freeze evening conflict branch |
| Capstone later needs freeze child | Lesson 300 will require 289 (or waiver) |
Arc continuity (289–294 high spine)
| Lesson | Milestone |
|---|---|
| 289 (this) | Ren'Py localization string freeze |
| 290 (queued) | Godot itch/Steam dual-depot dress rehearsal |
| 291 (queued) | Unity April Addressables warm catalog |
| 292 (queued) | Photoshop Summer Sale capsule sticker |
| 293 (queued) | Construct soft-launch wishlist window |
| 294 (queued) | GameMaker IARC age-rating packet |
Previous: Lesson 288 — OBS post-fest Whisper clips_index handoff
Next: Lesson 290 — Godot itch/Steam dual-depot dress rehearsal (queued)
FAQ
Does 289 replace the freeze evening blog?
No—evening owns full ritual depth; 289 latches BUILD_RECEIPT for the RPG course project.
Can we reopen Weblate after L6?
Only with po_import_reopen_allowed + numbered exception and a new freeze label—then re-run L1–L6.
Is Help #54 required first?
Only when selector already stuck after a bad import—prevention is Guide #49 + this lesson.
Is Lesson 225 enough?
No—language map wiring ≠ writes-closed freeze + installed smoke.
Key takeaways
- Verbal freeze is not
localization_freeze_ok. - L1–L3 pin manifest, Weblate close, and
tl_pathwalk. - L4–L5 require store parity + installed smoke.
- Guide #49 is the ninety-second gate; 289 is the BUILD_RECEIPT milestone.
- Help #54 recovers stuck-English after unauthorized reopen.
- Next spine lesson is dual-SKU 290.
Related GamineAI reads
- Ren'Py localization freeze preflight (Guide #49)
- Your first localization string freeze evening
- Freeze PO import selector stuck English (Help #54)
- 5-day localization freeze gate challenge
- Weblate PO language map Preferences (Guide)
- Official: Ren'Py Translation · Weblate
October store language claims without a freeze receipt are soft-launch debt—close Weblate, walk game/tl/, smoke installed Preferences, file localization_freeze_receipt_v1.json, then latch localization_freeze_ok only when L1–L6 are honestly green.