agentsclimarketplace

Renpy translation

Skill Waiyakorn1879/for-the-plot/skills/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

Install
npx -y skills add Waiyakorn1879/for-the-plot --skill renpy-translation

Assembled 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_filter hook plus an English→target dictionary, generated by scripts/build_patch.py (screen/UI strings ride along in a native translate 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 translate blocks — 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. See references/native-translate-blocks.md for 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):

  1. 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, then qa_check.py --technical-only after 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).

  2. API key — fast bulk first-pass. scripts/translate_api.py --profile profile.json machine-translates everything (resumable, token-validated). Providers: deepseek (preset — just needs DEEPSEEK_API_KEY), openai-compatible (OpenAI, OpenRouter, Ollama, Azure — selected by api.base_url, no SDK needed), gemini, or anthropic. 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_tokens budget as the answer — a large prompt can come back empty. The deepseek preset disables thinking for this reason; empty replies are diagnosed with the real cause, retried once at a raised budget, and never saved.

  3. Agent (claude-cli) — no API key, but lower throughput. Same bulk pass with provider claude-cli, which spawns headless claude -p agents 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:

ScriptPurpose
init_project.pyscaffold 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.pybulk translation → progress JSON (--profile; providers: deepseek / openai-compatible / gemini / anthropic / claude-cli)
qa_check.pytechnical + declarative register checks (--profile, --technical-only, --report)
build_patch.pyprogress JSON → patch files (--profile, --translations, --strings, --out, --split-size)
translation_memory.pyTranslation Memory cache + tooling (stats / export / import / clean, all --profile)
characters.pycharacter records → persona cards + QA register rules (library, no CLI)
validation.pyshared output-validity gate + atomic writer used by the others (library, no CLI)
profile_schema.jsonJSON 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_fuzzy is a no-op).
  • Contextual variants (v1.3, schema v2). One source key holds a default_translation plus context-keyed variants, so the same English line can resolve differently per speaker. Selection is deterministic (no AI): variants are scored +100 speaker · +50 target · +25 scene; the highest wins (ties → count, then recency, then insertion order); a missing speaker or no match falls back to the default. file/line are stored as metadata but never drive selection; target/confidence/context_hash are 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.json and the say-filter dict (build_patch.py) are keyed by source text only. Per-occurrence runtime output needs the native translate blocks workflow with --no-dedupe (see references/native-translate-blocks.md).
  • The engine only ever talks to the TranslationMemory class — 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.json does not force re-translation — the TM resolves hits before batching and puts the old value straight back with no model call. Run with tm.enabled: false to force it. (clean only 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)

  1. 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-only checks all four token classes by multiset — run it before any patch build.
  2. 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.
  3. Never modify game source files. No edits to the game's .rpy/.rpyc/.rpa. The patch lives entirely in game/tl/<language>/ and is removable by deleting that folder.
  4. 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.
  5. Inner monologue markers stay. Text wrapped in (...) (or the game's own convention) keeps its wrapper in the translation.
  6. Respect the profile's keep-untranslated list (character names, proper nouns, branded terms) exactly — no transliteration unless the profile says so.
  7. Don't sanitize. Preserve the source register: crude stays crude, formal stays formal, per the game profile.
  8. Copyright: never commit or publish game scripts, extracted strings, translation dictionaries of game text, .rpa files, or commercial fonts. Tools and style guides only.

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.