Renpy translation
Translate a Ren'Py visual novel game into another language. Use when the user wants to translate, localize, or build a translation/language patch for a Ren'Py game (.rpa, .rpyc, .rpy files), extract dialog strings from a visual novel, decompile Ren'Py archives with unrpa/unrpyc, set up character speech registers or a translation glossary for a game, or QA and deploy a fan translation patch.From its SKILL.md
npx -y skills add Waiyakorn1879/for-the-plot --skill renpy-translationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
13.4 KB, as published. Nobody here has run it
Ren'Py Game Translation
Full workflow for translating a Ren'Py visual novel into any target language. Work in a dedicated project folder next to the game install, never inside it. The game itself is only ever read from — the finished patch is the single thing copied in, at game/tl/<language>/.
Pipeline
0. Init scaffold the project folder → scripts/init_project.py
1. Decompile .rpa archives → .rpy sources → references/decompiling.md
2. Extract .rpy sources → strings.json → references/extraction.md
3. Profile build profile.json + style guide → references/game-profile.md
4. Translate strings → translations.json → references/translating.md
5. QA technical + register checks → references/qa.md
6. Patch translations → game/tl/<lang>/ files → references/patching.md
7. Deploy copy patch into game, launch, verify → references/patching.md
Start a new project with python init_project.py --game <path> --language <id>: it creates the working folder beside the game with a wired-up profile.json, a style-guide skeleton, qa_rules.json, a .gitignore that keeps game text out of version control, and TRANSLATION.md — the project's own always-loaded instruction file, with harness stubs (CLAUDE.md/AGENTS.md/GEMINI.md) pointing at it. A translation runs for hundreds of batches across many sessions; rules that live only in chat history get re-broken within ten batches.
Read the reference file for a phase when you reach it — don't preload everything. For GUI text (screens, menus) extract with --screens; for text built dynamically in Python (phone/chat/feed subsystems) see references/custom-subsystems.md and scope it with the user early.
Beyond Ren'Py, beyond Claude Code
Ren'Py is this skill's primary purpose and the only shipped engine adapter — but only extract_strings.py, build_patch.py, and four reference docs actually know what Ren'Py is. Everything else (profile system, progress store, TM, QA, token parity) is engine-neutral. references/engine-seam.md states the two file contracts at that boundary, so another engine means writing two programs and changing nothing else.
Likewise, none of the tooling requires Claude Code. PLAYBOOK.md is the same workflow written for any assistant, and AGENTS.md at the repo root routes other agentic tools to it. When a user asks about running this outside Claude Code, point them at PLAYBOOK.md.
First decision: patch mechanism
Two ways to deliver a translation; choose before extracting:
- Runtime filter (default) — a
config.say_menu_text_filterhook plus an English→target dictionary, generated byscripts/build_patch.py(screen/UI strings ride along in a nativetranslate strings:file). Survives game updates, coexists with mods (SanchoMod etc.), needs no Ren'Py SDK. Limitation: exact-match lookup, so one English string gets one translation everywhere it appears. - Native
translateblocks — Ren'Py's official mechanism. Per-occurrence context (the same English line can translate differently in different scenes), but requires the matching Ren'Py SDK and breaks on game updates. Seereferences/native-translate-blocks.mdfor when it's worth it.
Game profile and character records
Every game/language pair gets a profile: a profile.json (machine config: characters, glossary, fonts, paths, validation) plus a translation-guide.md (style guide: registers, formality, slang policy, few-shot examples). All pipeline scripts take --profile profile.json.
profile.json → speakers is the character dictionary. Beyond name/gender/role, a record can declare register, self_pronoun/address_pronoun, forbidden, must_use, speech_notes, examples, a monologue override for (...) inner thoughts, and a to map of per-relationship overrides. That last one matters most in register-rich languages, where the pronoun is a property of the pair, not the person — the same protagonist uses one pronoun with a close friend and another with an authority figure.
Each record feeds both ends: translate_api.py renders it as a persona card in the prompt, and qa_check.py turns the same forbidden list into a category-2 rule. A voice the prompt asks for and the gate doesn't check is worse than either alone. Every field is optional — a v1.3 profile with only name/gender/role produces exactly the old behavior.
Relationships are declared, not auto-detected. Nothing tries to infer who a line is addressed to: an addressee resolver that guesses wrong is wrong silently and everywhere. The declared table goes in the prompt alongside the scene, and the translator applies it.
A character who speaks under more than one code ("???" before introduction) gets alias_of on the extra code — one record, shared QA rules, no duplicate cast entries.
- Spec and the interview process for building one:
references/game-profile.md - Complete worked example (Being a DIK → Thai, 35+ characters):
examples/badik-thai/
When starting a new game, copy the example's structure, not its content.
Translation method — always ask first
Before starting the Translate phase, always ask the user which method to use. Never assume or silently default. Present these three options (recommend in-session):
-
In-session — recommended (best quality and throughput). Translate directly in this conversation, in batches of ~40–60 with speaker labels, following the game's
translation-guide.md; results go into the progress JSON, thenqa_check.py --technical-onlyafter each session. No API key, and the style guide is loaded once per session rather than re-sent per batch — so it gets more strings done per unit of quota than the agent method. Protocol:references/translating.md(Path A). -
API key — fast bulk first-pass.
scripts/translate_api.py --profile profile.jsonmachine-translates everything (resumable, token-validated). Providers:deepseek(preset — just needsDEEPSEEK_API_KEY),openai-compatible(OpenAI, OpenRouter, Ollama, Azure — selected byapi.base_url, no SDK needed),gemini, oranthropic. A bulk pass is a draft: always follow with an in-session review driven by the QA report. Details:references/translating.md(Path B).With a reasoning model, note that thinking tokens are drawn from the same
max_tokensbudget as the answer — a large prompt can come back empty. Thedeepseekpreset disables thinking for this reason; empty replies are diagnosed with the real cause, retried once at a raised budget, and never saved. -
Agent (claude-cli) — no API key, but lower throughput. Same bulk pass with provider
claude-cli, which spawns headlessclaude -pagents on the user's existing Claude Code subscription. Works without a key, but each spawned agent re-pays its own context overhead, so it finishes fewer strings per unit of quota than in-session. Offer it mainly when no API key is available and the user prefers not to translate in-session. Details:references/translating.md(Path B).
Confirm the choice, then set api.provider in profile.json accordingly for methods 2–3.
Scripts
In Claude Code they live in ${CLAUDE_PLUGIN_ROOT}/skills/renpy-translation/scripts/; anywhere else, in the scripts/ folder beside this file. Python 3.8+, cross-platform, UTF-8, stdlib-only:
| Script | Purpose |
|---|---|
init_project.py | scaffold a project folder (--game, --language, --name, --dir, --force) |
extract_strings.py | .rpy sources → strings.json (--src, --out, --no-dedupe, --screens for GUI/_() strings) |
translate_api.py | bulk translation → progress JSON (--profile; providers: deepseek / openai-compatible / gemini / anthropic / claude-cli) |
qa_check.py | technical + declarative register checks (--profile, --technical-only, --report) |
build_patch.py | progress JSON → patch files (--profile, --translations, --strings, --out, --split-size) |
translation_memory.py | Translation Memory cache + tooling (stats / export / import / clean, all --profile) |
characters.py | character records → persona cards + QA register rules (library, no CLI) |
validation.py | shared output-validity gate + atomic writer used by the others (library, no CLI) |
profile_schema.json | JSON Schema documenting profile.json |
Translation Memory (TM)
A durable source→translation cache at .ftp/translation_memory.json (project-local by default; set profile.json → tm.path to a shared file to reuse translations across games). translate_api.py consults it before every LLM call and writes new translations back, so repeated lines — within a game, and on later game updates — never hit the model twice. It seeds itself from any existing translations.json on first run, and prints a savings summary (TM Hits / Context Hits / Default Hits / LLM Calls / Savings %) at the end of a bulk pass.
- Exact + normalized matching (whitespace/CRLF-insensitive, but case- and token-sensitive). Every non-exact hit is re-validated for token preservation against the new source. Fuzzy matching is a v2 feature (
lookup_fuzzyis a no-op). - Contextual variants (v1.3, schema v2). One source key holds a
default_translationplus context-keyedvariants, so the same English line can resolve differently per speaker. Selection is deterministic (no AI): variants are scored+100speaker ·+50target ·+25scene; the highest wins (ties → count, then recency, then insertion order); a missing speaker or no match falls back to the default.file/lineare stored as metadata but never drive selection;target/confidence/context_hashare reserved for future use. v1 TM files migrate transparently on load (old translation →default_translation). - Where context variants help — and the boundary. Variants give the translator zero-cost, speaker-consistent retrieval (re-runs, game updates, shared cross-game TMs). They do not, on their own, make the runtime patch emit different renderings of one line:
translations.jsonand the say-filter dict (build_patch.py) are keyed by source text only. Per-occurrence runtime output needs the nativetranslateblocks workflow with--no-dedupe(seereferences/native-translate-blocks.md). - The engine only ever talks to the
TranslationMemoryclass — it never opens the JSON directly. - Commands (conceptually
ftp tm <action>; run as plain scripts here):python translation_memory.py stats --profile profile.json·… export --profile profile.json --out tm.csv·… import --profile profile.json --in tm.csv·… clean --profile profile.json - The re-translation trap. Deleting keys from
translations.jsondoes not force re-translation — the TM resolves hits before batching and puts the old value straight back with no model call. Run withtm.enabled: falseto force it. (cleanonly drops empty/duplicate entries.) Entries that echo their source or miss the target script are rejected on lookup automatically, so a TM poisoned by an older run self-heals. - Copyright: the TM contains game text — treat it like the progress JSON; never commit or publish it.
Hard rules (always apply)
- Token preservation is non-negotiable. Every
[variable],{tag},\n/\"escape, and%%in the English must appear in the translation, identical and in the same count. A missing[mc_name]or{/i}crashes or corrupts rendering; a duplicated one is just as broken.qa_check.py --technical-onlychecks all four token classes by multiset — run it before any patch build. - A translation identical to its source, or containing none of the target script, is not a translation. It is refused before it can reach the progress store or the Translation Memory, and reported as category 1 by QA. Sources whose only translatable content is a kept proper noun, a variable, a tag, or punctuation are exempt automatically; for anything else that genuinely should stay identical, use
validation.allow_identical. - Never modify game source files. No edits to the game's
.rpy/.rpyc/.rpa. The patch lives entirely ingame/tl/<language>/and is removable by deleting that folder. - Never translate code-like strings: label names, image/audio names, transform names, screen ids. The extractor filters most of these; when in doubt, skip and flag.
- Inner monologue markers stay. Text wrapped in
(...)(or the game's own convention) keeps its wrapper in the translation. - Respect the profile's keep-untranslated list (character names, proper nouns, branded terms) exactly — no transliteration unless the profile says so.
- Don't sanitize. Preserve the source register: crude stays crude, formal stays formal, per the game profile.
- Copyright: never commit or publish game scripts, extracted strings, translation dictionaries of game text,
.rpafiles, or commercial fonts. Tools and style guides only.