Your First Ren'Py Language Selector After Weblate PO Import in One Evening - 2026 Beginner Pipeline
Your Weblate dashboard shows 100% for Chinese, Japanese, and Korean. Steam capsule screenshots look localized. You launch the fest demo, open Preferences → Language, and only English appears—or switching languages does nothing until restart crashes.
October 2026 visual-novel fest traffic expects store copy and in-game menus to match. Weblate proves PO files; Ren'Py proves translate blocks + UI wiring + clean builds. This Tutorials & Beginner-First pipeline runs one evening: import PO, regenerate translations, wire the language screen, boot-smoke three locales, file language_receipt_v1.json.
Non-repetition note: 7-day Ren'Py freeze challenge owns label and asset freeze—not selector wiring. Steam 300-char CJK help is store text—not in-game menus. Planned help: Ren'Py language selector stuck English.
Pair with Weblate merge-conflict help, 14-free Weblate PO tools, localization tooling resource, and store-page QA checklist.
Who this is for and what you get
| Audience | You will be able to… |
|---|---|
| First-time Ren'Py localizer | Wire a language menu after Weblate export |
| Producer | Require language_receipt_v1.json before fest promotion |
| Writer | Know when PO import requires rebuild vs hotfix |
Time: one evening (3–4 hours first locale set; 60 minutes when repeating for one new language).
Prerequisites: Ren'Py 8.x project with tl/ or generate translations enabled, Weblate PO export access, Windows build target for Steam demo.
Why this matters now (July–October 2026)
- Weblate + fest capsules — Teams pass string freeze on store strings but ship English-only executables.
- Refund optics — Players quote localized store tags then call the demo “English scam.”
- Ren'Py
.rpyccache — Stale bytecode hides newtranslateblocks until clean build. - Freeze week pairing — Ren'Py freeze challenge Gate 7 should include language smoke rows.
- Metadata diff gap — Wednesday store metadata diff cannot see Preferences screen text.
Direct answer: Export PO from Weblate → import into Ren'Py tl/ → renpy.compile_all() or launcher regenerate → add Language screen entries → delete cache/ → build → smoke en + two fest locales → language_receipt_v1.json.
Evening overview (five blocks)
| Block | Minutes | Output |
|---|---|---|
| 1 — Weblate export proof | 30 | weblate_export_manifest.json |
| 2 — PO placement + generate | 45 | tl/zh_CN/, tl/ja/, tl/ko/ populated |
| 3 — Language screen (L1–L3) | 45 | Preferences lists all shipped locales |
| 4 — Clean build + L4–L5 | 60 | Standalone exe per smoke matrix |
| 5 — Receipt + store parity | 20 | language_receipt_v1.json |
Mental model — three localization layers
| Layer | Tool | Player sees |
|---|---|---|
| Store | Weblate + Steamworks | Capsule, description |
| Engine strings | translate blocks in tl/ |
Dialogue + UI |
| Selector | screen language / Preferences |
Pickable languages |
Passing layer 1 alone creates the “localized store, English game” bug this pipeline fixes.
Block 1 — Weblate export proof
Before touching Ren'Py, prove PO left Weblate cleanly (pairs merge-conflict help):
{
"export_utc": "2026-05-25T18:00:00Z",
"components": [
{ "name": "game/tl/zh_CN", "po": "zh_CN.po", "fuzzy_count": 0, "translated_percent": 100 },
{ "name": "game/tl/ja", "po": "ja.po", "fuzzy_count": 0, "translated_percent": 100 },
{ "name": "game/tl/ko", "po": "ko.po", "fuzzy_count": 0, "translated_percent": 100 }
],
"string_freeze_receipt": "release-evidence/localization/string_freeze_receipt_v1.json"
}
Beginner rule: Any fuzzy_count greater than 0 on fest locales → stop and resolve in Weblate, not in Ren'Py.
Gates L1–L5 (language pass)
| Gate | Name | Pass criterion |
|---|---|---|
| L1 | PO on disk | Each fest locale has tl/<lang>/ with non-empty .po |
| L2 | Translate blocks | Launcher Generate Translations or CLI compile sees new strings |
| L3 | Selector UI | Preferences lists en, zh_CN, ja, ko (your set) |
| L4 | Boot smoke | Each locale reaches main menu without traceback |
| L5 | Switch smoke | Change language in Preferences → UI updates without restart crash |
L3–L5 block promotion when RED.
Block 2 — PO placement and generate
Folder layout (Ren'Py 8.x convention)
game/
tl/
None/ # optional source hooks
zh_CN/
common.rpy
screens.rpy
script.rpy
ja/
...
ko/
...
Copy Weblate-exported PO into matching folders—or configure Weblate Ren'Py component paths to push directly to game/tl/<lang>/.
Generate translations (launcher)
- Open Ren'Py Launcher → project.
- Generate Translations → select languages → Generate.
- Confirm new
translate chinese strings:(etc.) blocks appear intl/zh_CN/*.rpy.
CLI-oriented teams:
# illustrative: use your project's documented renpy.sh path
renpy.sh . compile
Outbound: Ren'Py translation documentation.
Common beginner mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| PO in wrong folder | 0% strings update | Match Weblate component path to tl/ |
Edit only .po, never regenerate |
English in game | Run Generate Translations |
Old translate IDs |
Mixed old/new lines | Delete obsolete tl files per Ren'Py doc |
Hardcoded language "english" in script |
Selector ignored | Remove forced language in start label |
Block 3 — Language screen wiring (L3)
Minimal Preferences pattern in screens.rpy (adapt to your UI skin):
screen preferences():
tag menu
vbox:
textbutton _("Return") action Return()
text _("Language")
textbutton "English" action Language(None)
textbutton "简体中文" action Language("chinese")
textbutton "日本語" action Language("japanese")
textbutton "한국어" action Language("korean")
Critical: Language("chinese") must match Ren'Py language name in project settings—not always ISO code. Check Launcher → Translate → List languages for exact identifiers.
Map Weblate locale codes to Ren'Py names in language_map_v1.json:
{
"weblate_to_renpy": {
"zh_CN": "chinese",
"ja": "japanese",
"ko": "korean"
}
}
Store beside receipt—prevents “Weblate says zh_CN, Ren'Py expects chinese” drift.
config.default_language trap
If options.rpy contains:
define config.default_language = "english"
that is fine for first boot—but do not hardcode renpy.change_language("english") on every start label after the player picks another language.
Block 4 — Clean build and boot smoke (L4–L5)
Delete stale cache
Before distribution build:
rm -rf game/cache/
# Windows: delete game\cache\ folder in Explorer
Stale .rpyc is the #1 “Weblate 100% but game English” support cause.
Smoke matrix (standalone exe)
| Run | Start language | Action | Pass |
|---|---|---|---|
| A | English | Reach main menu | No traceback |
| B | 简体中文 | Preferences → 简体中文 → main menu | UI Chinese |
| C | 日本語 | Switch mid-menu | No crash |
| D | 한국어 | Save slot → quit → relaunch Korean | Save loads |
Log in renpy_language_smoke.md:
# Ren'Py language smoke — 2026-05-25
build_id: nextfest-vn-rc3
| locale | L4 | L5 | notes |
| english | pass | pass | |
| chinese | pass | pass | font fallback OK |
| japanese | pass | pass | |
| korean | pass | YELLOW | one untranslated preferences line — fixed |
Pair with Wednesday demo smoke on promotion day—smoke gameplay after language smoke passes.
language_receipt_v1.json
{
"schema": "language_receipt_v1",
"build_id": "nextfest-vn-rc3",
"weblate_export_manifest": "release-evidence/localization/weblate_export_manifest.json",
"language_map": "release-evidence/localization/language_map_v1.json",
"shipped_locales": ["english", "chinese", "japanese", "korean"],
"gates": {
"L1_po_on_disk": "pass",
"L2_translate_blocks": "pass",
"L3_selector_ui": "pass",
"L4_boot_smoke": "pass",
"L5_switch_smoke": "pass"
},
"store_parity": {
"steam_supported_languages_claims": ["schinese", "japanese", "koreana"],
"in_game_selector_matches": true
},
"promotion_allowed": true,
"paired_renpy_freeze": "release-evidence/renpy/freeze-week/renpy_freeze_receipt_v1.json"
}
Attach to BUILD_RECEIPT notes and Thursday row review when string_table_receipt rows exist for Unity siblings—VN teams still file language_receipt for Ren'Py.
Store parity check (15 minutes)
| Store claim | In-game proof |
|---|---|
| Steam Supported languages includes Chinese | Selector shows Chinese; dialogue Chinese |
| Screenshots CN | At least one screenshot from chinese playthrough |
| Short description JP | Preferences Japanese + sample line JP |
Use store-page QA localization resource—checklist row in-game language selector matches Supported languages.
Fonts and CJK (beginner)
| Issue | Fix |
|---|---|
| tofu squares | Add CJK font in gui.rpy / style.default.font |
| overflow | Test 1280×720; shorten UI strings in Weblate |
| vertical JP | Optional preferences — document if unsupported |
Ship font license in release-evidence/localization/fonts/README.md.
Weblate ↔ Ren'Py workflow (ongoing)
| Event | Owner | Action |
|---|---|---|
| String freeze | Producer | Lock Weblate per help |
| Export PO | Localization lead | Manifest JSON |
| Import | Engineer | Blocks 2–4 tonight |
| Writer typo | Writer | Fix in Weblate → re-export → regenerate |
| Fest promotion | Producer | language_receipt.promotion_allowed |
Do not email PO files without updating weblate_export_manifest.json version.
Engine version pin
Log in receipt:
"renpy_version": "8.3.2",
"build_classification": "development"
Mismatch with freeze challenge build_classification_freeze.md → restart freeze Gate 2.
Troubleshooting table
| Symptom | Check |
|---|---|
| Only English in list | L3 screen; config.languages |
| Language flips back on restart | start label forcing english |
| Some lines English | Missing translate for those strings |
| Crash on switch | Font missing glyphs |
| Weblate 100%, game 0% | L2 not run; cache not cleared |
| Steam CN, game EN | This whole pipeline skipped |
PowerShell clean cache (Windows)
Remove-Item -Recurse -Force "game\cache" -ErrorAction SilentlyContinue
Rebuild distribution before Steam upload.
Integration with fest ops week
| Day | Task |
|---|---|
| Mon | Weblate freeze |
| Tue | This pipeline (evening) |
| Wed | Metadata diff |
| Wed PM | Demo smoke |
| Thu | Row review |
Outbound references
Related GamineAI reads
- 7-day Ren'Py freeze challenge
- Weblate merge-conflict help
- Steam 300-char CJK help
- 14-free Weblate PO export tools
- 18-free localization tooling
- Store-page QA localization
- Save-slot label case study (branch discipline cousin)
Key takeaways
- Weblate 100% does not imply Ren'Py shipped translations—run Generate Translations and clean cache.
- Wire Preferences language buttons with correct Ren'Py language names (
chinese, not onlyzh_CN). - Gates L1–L5 block fest promotion when selector or smoke fails.
- File
language_receipt_v1.jsonwith store parity booleans. - Pair with Ren'Py freeze challenge and weekly metadata + demo smoke.
- Store capsules and in-game selector must match Supported languages.
- One evening first time; ~one hour per new locale after map exists.
- Planned help article covers failure-first fixes; this URL is the first success path.
FAQ
Do I need Weblate?
No—but you need PO files from somewhere consistent. Manual PO works if paths match tl/.
Can I use Ren'Py native dialogue translator without Weblate?
Yes for small demos; fest teams outgrow spreadsheet handoff—Weblate returns in July stacks.
Does this change save compatibility?
Changing translation only usually preserves saves; changing script labels after freeze does not—see freeze challenge.
What about voice lines?
VO is separate; this pipeline is text UI + dialogue. Subtitles may still English if VO not localized.
Mac build too?
Smoke Windows first for Steam fest; add macOS matrix row to receipt when depot ships.
Where does Blender-Fab ORM backlog fit?
Unrelated art pipeline—finish language receipt before ORM proof sphere challenge week (now published).
Worked example (three-person VN studio)
Context: nextfest-vn-rc3 — Weblate green for zh_CN, ja, ko; Steam Supported Languages updated; demo still English.
| Step | Finding | Fix |
|---|---|---|
| L1 | tl/ko/ missing screens.rpy |
Re-export Korean component |
| L2 | Generate Translations not run after PO drop | Launcher regenerate |
| L3 | Selector showed English only | Added Language("chinese") buttons |
| Cache | Old .rpyc |
Deleted game/cache/ |
| L5 | Korean crash on switch | Missing Noto Sans KR in gui.rpy |
Outcome: language_receipt_v1.json promotion_allowed: true Thursday; metadata diff GREEN Wednesday—no more “localized store” refund tags.
Scan for untranslated dialogue (developer)
# illustrative ripgrep — strings still using _() without translate
rg "^\s+\"[^\"]+\"$" game/ --glob "*.rpy" | head
Pair with Ren'Py Lint in launcher—untranslated dialogue warnings before build. Log warning count in renpy_language_smoke.md.
Poedit lane (no Weblate yet)
| Tool | When |
|---|---|
| Poedit | Solo dev, fewer than 3 locales |
| Weblate | Contractors + freeze windows |
Same tl/ folders—export PO from Poedit, drop files, run Block 2 identically. Receipt still uses language_receipt_v1.json; set weblate_export_manifest to null and po_source: poedit in notes.
renpy.known_languages() debug (L3 aid)
Add temporary debug screen during development:
screen language_debug():
text "[renpy.known_languages()]"
Screenshot output into smoke log—proves engine sees languages even if Preferences UI broken.
Remove debug screen before Steam upload—fails dev console opinion spirit on fest builds.
Narration vs UI strings
| String type | File usual location | Weblate component |
|---|---|---|
| Dialogue | script.rpy / chapters |
game/script |
| Menus | screens.rpy |
game/screens |
| System | common.rpy |
game/common |
Split components in Weblate prevent translator merge conflicts on unrelated files—mirrors merge-conflict help.
Steam depot + language smoke order
- This pipeline on internal folder build.
- Upload to playtest branch.
- Demo smoke on chinese boot path once.
- Promote fest branch.
Skipping step 1 on playtest branch wastes player trust.
Receipt validation one-liner
jq -e '.gates.L3_selector_ui == "pass" and .store_parity.in_game_selector_matches == true' language_receipt_v1.json
Add to validate-packet when VN in Q3 diligence packet.
Writer rules during fest week (paste in Discord)
- No new language codes mid-week without producer sign-off
- Typos: fix in Weblate → re-export → engineer reruns Blocks 2–4
- No renaming dialogue labels (freeze challenge)
- Screenshot requests: specify locale (CN/JP/KR)
Compare Unity localization empty tables
Unity teams hit String Tables empty after strip—different engine, same store vs build lesson. VN producers on mixed-engine portfolios should read both helps—receipt types differ (string_table_receipt vs language_receipt).
Accessibility note
Language toggle should remain reachable without obscure gamepad combo—Deck glyph tutorial culture applies if VN ships Deck controls; Ren'Py _() strings in selector must fit button width at 1280×800.
Evidence folder (copy tree)
release-evidence/localization/
weblate_export_manifest.json
language_map_v1.json
language_receipt_v1.json
renpy_language_smoke.md
fonts/README.md
screenshots/
menu_chinese_1280x720.png
menu_japanese_1280x720.png
Friday Block 5 archives screenshots older than eight weeks—keep latest three locales only.
Minute-by-minute evening (first locale set)
| Min | Task |
|---|---|
| 0–30 | Export PO + manifest JSON |
| 30–75 | Copy tl/, Generate Translations |
| 75–120 | Wire Preferences + language_map_v1.json |
| 120–180 | Delete cache, build, smoke matrix A–D |
| 180–200 | language_receipt_v1.json + store parity row |
Second language add is mostly smoke matrix row + Weblate export—under 60 minutes.
False positives that waste evenings
| Report | Usually means |
|---|---|
| “Translation broken” | Cache not cleared |
| “Weblate wrong” | PO never imported to tl/ |
| “Steam lied” | Supported languages checkbox ahead of build |
| “Crash on Korean” | Font/glyph, not PO |
Teach facilitators to ask for language_receipt before debating translator quality.
Forward link for Help-Create
When Ren'Py language selector help publishes, it should link here first for success path—help owns traceback fixes; blog owns first evening order of operations.
Checklist card (printable)
[ ] Weblate export manifest — fuzzy 0 on fest locales
[ ] tl/ folders populated
[ ] Generate Translations run
[ ] Language screen lists every shipped locale
[ ] game/cache deleted
[ ] Standalone smoke A–D GREEN
[ ] language_receipt_v1.json promotion_allowed
[ ] Store Supported languages matches selector
Tape inside localization tooling resource bookmark folder—physical reminders beat Slack pins during crunch week.
Producer sign-off line: “I will not promote the fest branch until language_receipt_v1.json shows promotion_allowed: true and screenshots exist for every Supported language we claim on Steam.”