Translating games with AI: localizing strings without broken tags
How to translate thousands of game strings without breaking placeholders, tags or UI layout: batch format, checks, retries and cost.
5 min · · автоматизация · development
A neural network for translating games removes much of the manual work, but game strings are treacherous material. In the line Hello, {player}! You found %d coins. a model can easily "translate" the variable name or drop %d, and the game crashes or shows an empty gap. Below is a process that translates strings from JSON, CSV and PO files and catches breakage automatically before it reaches a build.
Why game text is harder to translate than ordinary text
Game strings have four traits that spoil automatic translation:
- Placeholders and tags.
{name},%s,%d,{{count}},<b>…</b>,[color=red],\n. They must neither be translated nor lost. - Length limits. A button sized for the word
Playwill not fit its Russian equivalentНачать игру. Russian text is usually longer than English. - No context. Is the string
Run"to sprint", "to launch" or "a run"? Without a hint the model guesses. - Consistent terminology. The item
Iron Swordmust be named the same in quests, inventory and tooltips.
All four are solved at the level of the request format, not with a "clever" prompt.
Batch format: ID, context, limit
Do not send whole files. Split strings into batches of 30–60 and pass them as a JSON list with an identifier, source text and hints:
[
{"id": "ui.play", "text": "Play", "context": "main menu button", "max_len": 12},
{"id": "npc.smith.01", "text": "Need a hand, {player}?", "context": "blacksmith, friendly tone"},
{"id": "item.sword", "text": "Iron Sword", "context": "item name"}
]You fill context and max_len yourself or export them from the engine. Even a short note such as "button" or "hero dialogue" helps the model pick the right meaning of a word. Add rules and a glossary to the system prompt:
You translate game strings from English into Russian.
Rules:
- Return a JSON list of objects {"id": ..., "text": ...} in the same order.
- Copy placeholders ({name}, %s, %d, {{x}}), tags (<b>, [color=red]) and \n unchanged.
- Respect max_len: if it does not fit, shorten the wording.
- Use the term glossary, do not invent other variants.
Glossary: Iron Sword = Железный меч; Health = ЗдоровьеKeep the glossary short and insert it in every batch: it works the same for all strings and keeps names consistent.
A script: translate a batch and verify it
A Python example with the official openai SDK (client docs: Python API). The address is https://api.zukko.pro/v1:
import json, re
from openai import OpenAI
client = OpenAI(base_url="https://api.zukko.pro/v1", api_key="fetch_your_key")
SYSTEM = open("system.txt", encoding="utf-8").read()
TOKEN_RE = re.compile(r"\{\{?\w+\}?\}|%[sd]|%\d*\.?\d*[dsf]|<[^>]+>|\[/?\w+(?:=\w+)?\]|\\n")
def tokens(s: str) -> list[str]:
return sorted(TOKEN_RE.findall(s))
def translate_batch(items: list[dict], model="deepseek-v4.1-flash") -> dict:
resp = client.chat.completions.create(
model=model,
temperature=0.2,The key function here is find_bad: it compares the set of placeholders and tags in the original and the translation. Any mismatch is a reason to translate the string again. Adjust the TOKEN_RE regular expression to your game's format: every engine has its own markup syntax.
Then a loop: translate a batch, find damaged strings, send only those again (possibly to a stronger model), and after two attempts put the remainder into a file for manual editing. If you have many requests, see the article on asynchronous processing of thousands of requests, and for general techniques with localization files see the piece on translating i18n files.
Formats: JSON, CSV, PO
The model always works with one internal structure id → text, and file formats are adapters on input and output.
- JSON. A flat or nested dictionary: walk the tree and join keys with dots. When writing the result, restore the same structure and key order so the git diff stays readable.
- CSV. Read with the standard
csvmodule andencoding="utf-8", translate the needed column and write to a new one. Do not glue lines together by hand: commas and quotes inside text break parsing. - PO. The
poliblibrary fits:po = polib.pofile("ru.po"), entries havemsgidandmsgstrfields, save withpo.save(). Pass the entry context (msgctxt) in thecontextfield.
For files from other engines, first export the strings into one of these forms with a utility for your engine, and load them back after translation. If you translate someone else's game or a mod, check the licence terms and the publisher's rules: that is a legal matter, not a technical one.
What game translation costs
Calculated for 18 October 2026. Assumptions: 20,000 strings, batches of 50, so 400 requests. Per request: a system prompt with glossary of 1,500 tokens plus 50 strings of 30 tokens = 3,000 input tokens; the Russian translation of a batch with JSON wrapping is about 2,500 output tokens. Total 400 × 3,000 = 1.2M input and 400 × 2,500 = 1.0M output. See real figures in the response usage; Russian text usually takes more tokens than English.
Formula: input × input price + output × output price.
| Model | Calculation | Total |
|---|---|---|
deepseek-v4.1-flash | 1.2 × $0.013 + 1.0 × $0.038 | $0.054 |
gpt-6.1-sol | 1.2 × $0.27 + 1.0 × $1.32 | $1.64 |
claude-sonnet-5-5 | 1.2 × $0.4 + 1.0 × $2 | $2.48 |
claude-opus-5-5 | 1.2 × $0.8 + 1.0 × $4 | $4.96 |
Add headroom for retries: if roughly every tenth string is translated twice, that adds about 10% to the total. Prices change: the current list is at GET https://api.zukko.pro/v1/models. A practical scheme: a cheap model does the main pass, a stronger one translates only what failed the checks. Pick the model on a trial sample of 100–200 strings that you read yourself: nobody can say in advance who will convey your game's style best.
Create the script's key in the dashboard and set a spending limit on it: a bug in the loop should not eat the whole balance.
FAQ
How do I avoid breaking placeholders when translating?
Tell the model in the prompt to copy them unchanged, and verify the result in code: compare the sets of placeholders and tags in the original and the translation. Send mismatched strings for another translation.
How do I handle a string length limit?
Pass max_len with each string and check the length after the response. Translate overly long strings again with a request to shorten them. Do the final check in the game itself, on real interface elements.
Should I translate with an expensive model or a cheap one?
Start with a cheap one on a trial sample and read the output. For story dialogue it is worth trying a stronger model; for interface and items a cheap one often suffices. Decide on your own sample, not on general promises.
Can I translate a whole file in one request?
For small files, yes, but batches of 30–60 strings with identifiers are more reliable: it is easier to verify, repeat failed parts and keep string order.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Translating subtitles with a neural network: SRT without broken timecodesThe main risk when translating SRT with a neural network is a broken file structure. A scheme where timecodes never reach the model, and a Python script with validation.
- Editing texts with a neural network: proofreading, style and cuttingDifferent kinds of editing need different prompts. Wording for proofreading, style and cutting, how to get edits with explanations and how not to lose the author's voice.
- A neural network for writing scripts: Bash, PowerShell and PythonA model writes scripts for routine fast, but running them blindly is risky. How to state the task, demand a safe dry-run mode and check the result before launch.
- Dify in Docker: installation, first-run setup and connecting modelsInstall Dify with Docker Compose, create the admin account and connect Claude, GPT and DeepSeek models on one key. Plus what to do when containers will not start.