Unity Chinese Localization Pipeline (with Self-Audit)
Turns the "extract → risk assessment → translation → write → self-audit" process into a reusable four-stage pipeline. The core is not translation itself, but first determining which strings cannot be translated — this is the only thing that will break the game.
When to Use It
Before using, confirm all three conditions. If any one is not met, do not proceed:
- Where is the text: A
Mono/IL2CPPUnity game where text is hardcoded in the managed assembly (*_Data/Managed/Assembly-CSharp.dll) orresources.assets, with noLocalizationtable,.po,.csv, or similar localization resources. If an existing localization table is available, use that directly; do not modify the assembly. - Can get the original file: You are modifying an executable. You must have an unmodified original as a baseline (from a backup directory, or re-downloaded after Steam verification). Do not make in-place modifications without a baseline.
- Chinese can be displayed: The project uses embedded dynamic fonts (TMP or Legacy
Font). Static fonts/atlas fonts require texture swapping; this pipeline only handles dynamic fonts.
IL2CPP (no Assembly-CSharp.dll, only GameAssembly.dll) is not supported; that requires Il2CppDumper + recompilation, discussed elsewhere.
Quick Start
# 0) Create project skeleton (generates l10n.json)
python <skill>/scripts/zhl10n.py init -C <your_project_dir>
# 1) Enumerate all ldstr sites (about 10s / 5000 entries)
python <skill>/scripts/zhl10n.py extract
# 2) View risk levels: which can be translated, which can only be directionally translated, and which must absolutely not be translated
python <skill>/scripts/zhl10n.py risk
# 3) Fill in trans_map.json / site_map.json, then write (does not modify originals, outputs to out/build/)
python <skill>/scripts/zhl10n.py apply
# 4) Self-audit (reads out/build artifacts by default)
python <skill>/scripts/zhl10n.py audit --fail-on P1
One-shot: zhl10n.py all (extract → apply → audit).
Install dependencies: pip install dnfile dncil UnityPy fonttools. Only audit works without UnityPy.
Responsibilities of Each Stage
| Stage | Entry Point | Input | Output | Boundary of Responsibility |
|---|---|---|---|---|
| init | zhl10n.py init | Target game path | l10n.json, empty tables | Only generates configuration, does not touch game files |
| extract | extract.py | Original DLL (+ optional assets) | out/extract/sites.jsonEach item in sites.json includes str/type/method/token/offset | Read-only. Must read the unpatched original, otherwise already localized strings will be extracted |
| risk | risk.py | sites.json | Console hierarchy statistics | Only assesses, no table modifications |
| apply | apply.py | Original DLL + translation tables + font subset | out/build/Assembly-CSharp.dllout/build/resources.assets | Writes new files; originals untouched (use --inplace to modify in place, which creates a .orig backup first). Any translation hitting a block rule is rejected and causes an error |
| audit | audit.py | Artifacts + translation tables + glossary + logs | out/audit/audit.{json,md,html}fixlist.csv | Read-only. Can run on any version of files, used for before/after comparison |
Three-layer translation tables, each with distinct responsibilities:
trans_map.json— Global table, replaces strings at all occurrences. Only include strings where "translating to Chinese will not break logic."site_map.json— Site-specific table, keyed byType::Method, only modifies theldstrat that location. Used when "the same word should be translated in menus but not in the input system."enum_map.json— Enum name table, modifies field names in the#Stringsheap. Used for display names fromToString()on enums.
Decision order: site-specific → global. If site-specific matches, global is not used.
Self-Audit
The self-audit is the focus of this pipeline. It independently reads artifacts after writing, not trusting the translation tables' own assertions.
Output
| File | Content |
|---|---|
audit.json | Full structured results: each finding includes check/severity/count/items[]/hint |
audit.md | Human-readable summary, sorted by P0→P2 |
audit.html | Collapsible single-file report |
fixlist.csv | List of items needing fixes, one per line, ready to use as a to-do list |
Checks (31 items, 6 families)
CV Completeness — Anything missing
| ID | Level | Check | Judgment Basis |
|---|---|---|---|
| CV1 | P1 | Untranslated text | Site is displayable + no translation in either table + not exempted by risk rules |
| CV1b | P2 | Text that looks like UI text but was blocked | Indicating engine constraint, not missing translation; requires site-specific translation and manual testing |
| CV2 | P1 | Empty/placeholder translation | Translation is empty or contains TODO/待译 |
| CV3 | P1 | Long sentence translation identical to original | Suspects it wasn't translated at all |
| CV4 | P2 | Short word/symbol translation identical to original | Icon words, key names kept as original are usually correct |
| CV5 | P2 | Dead entries in site-specific table | Declared but not found in original IL; method renamed or already covered by global table |
CS Consistency — Are terms aligned
| ID | Level | Check |
|---|---|---|
| CS1 | P2 | Same original translated differently in global vs. site-specific tables |
| CS2 | P1 | Mandatory glossary translation not followed |
| CS3 | P1 | Translation hits glossary forbidden words |
| CS4 | P2 | Duplicate keys (differing only by case/whitespace) |
| CS5 | P2 | Chinese punctuation style tending toward half-width |
FM Format — Will concatenation break
| ID | Level | Check | Consequence |
|---|---|---|---|
| FM1 | P1 | Placeholder count mismatch | String.Format throws exception or numbers disappear |
| FM1b | P2 | Removed English plural placeholder | Correct action, just confirm no actual parameter was accidentally deleted |
| FM2 | P2 | Line break count mismatch | Typography misalignment |
| FM3 | P1 | Unit/splicing fragment missing leading/trailing space | 1000coins concatenation |
| FM4 | P2 | Other leading/trailing space changes | Usually okay between Chinese characters |
| FM5/FM6 | P2 | Half-width punctuation / consecutive whitespace | Aesthetics |
| FM7 | P2 | Translation length expansion too large | Often due to writing explanations into labels |
RK Risk — Will it break the game
| ID | Level | Check |
|---|---|---|
| RK1 | P0 | High-risk string included in translation tables (paths, engine keys, regex, input action names…) |
| RK2 | P2 | Global table contains single identifiers, recommend moving to site-specific |
| RK3 | P1 | Translation table key contains Chinese, will cause errors on second application |
| RK4 | P2 | Translation contains residual English fragments |
IG Structure — Is the file itself still valid
| ID | Level | Check |
|---|---|---|
| IG0/IG1/IG2/IG4 | P0 | Target file missing, PE/metadata anomaly, #US unparseable, ldstr token invalid, metadata line count inconsistent with original |
| IG3 | P2 | Sites still in English after patching (expected whitelist, used for cross-checking) |
| IG5/IG6 | P1 | Font missing glyphs / font file unparseable |
RT Runtime — Did the game itself complain
| ID | Level | Check |
|---|---|---|
| RT1 | P1 | Exception stack trace found in Player.log (requires source.logs configuration) |
Trigger Methods
Three scenarios, choose accordingly:
# 1. Self-check immediately after writing (most common in dev loop)
zhl10n.py audit
# 2. As a gate: CI or pre-release, fails with non-zero exit if threshold not met
zhl10n.py audit --fail-on P0 # Fail if any blocking level exists
zhl10n.py audit --fail-on P1 # Default strictness, recommended for release
zhl10n.py audit --fail-on P2 # Clear even hints
# 3. Regression comparison: more issues than last time after this change
zhl10n.py audit --baseline
apply also automatically runs audit at the end, printing the results — the write stage will not produce a bad package unknowingly.
How to Use Audit Results
- Look at
P0first. No P0 findings allowed for release — they mean "the game might not start." - Then look at
P1.CV1is a list of missing translations,FM1is a list of guaranteed crashes,RT1is evidence that it already crashed. P2serves as a checklist:CV1btells you which are engine constraints (not your omissions),CV5/CS4are junk entries you can delete directly.fixlist.csv— fix them one by one, re-run, untilP0=0 and P1=0.
Configuration (l10n.json)
{
"project": "GameName", "version": "1.0",
"source": {
"dll": "…/Managed/Assembly-CSharp.dll", // Only for locating; extract reads backup
"backup": "…/_Backup/Assembly-CSharp.dll", // Must be unmodified original
"assets": ["…/resources.assets"],
"logs": ["…/Player.log"] // Optional, for RT1
},
"output": { "dir": "out" },
"text": { "map": "trans_map.json", "site_map": "site_map.json", "enum_map": "enum_map.json" },
"glossary": "glossary.csv",
"font": { "subset": "font/zh-subset.ttf", "source": "font/NotoSansSC-Bold.otf", "targets": {} },
"audit": { "severity_fail": "P1", "max_expansion_ratio": 2.5, "max_expansion_abs": 40 },
"risk": { "extra_patterns": [], "extra_allow": [] }
}
Two escape hatches, think of them first when getting errors:
risk.extra_patterns— Project-specific strings known not to be translatable (regex). E.g.,^Levels$risk.extra_allow— Specific strings confirmed as not needing translation. Adding them here exempts them from missing translation checks
Glossary glossary.csv: en,zh,forbidden three columns; forbidden uses | to separate synonyms. CS2/CS3 depend on it; skip these items if no glossary.
Decision Rules: Which Strings Must Absolutely Not Be Translated
One-liner: Strings used as "keys" in code will break lookups if translated.
| Category | Example | What happens if translated |
|---|---|---|
| Resource paths | "Levels", "UI/Fonts/X" | Level table empty → save restore NRE → stuck on loading screen |
| Input actions/field names | Move/Up/Button | Key bindings break |
| Save fields | legacySkillPointCredit | Save cannot be read |
| Enum names | Deposit (beyond ToString(), also Enum.Parse) | Parsing fails |
| Regex patterns | ([0-9.]+)% more coins | Hint text rewriting fails, falls back to English |
| Object/resource names | new GameObject("Display glass") | Usually doesn't crash, but pollutes resource names |
| Format strings | {0:N0} / {1:N0} | String.Format exception |
Same form, different fate: Pause is a button text in the settings menu (should be translated), but an action name in GameInput (cannot be translated).
The visual form alone can never distinguish this; only "which type and which method it appears in" can. This is what risk.py's _code_shape() does:
- Literals in object construction methods (
.ctor/Build/Initialize/Create/Ensure…) → names, do not translate - Literals in UI-drawing methods (
Draw/OnGUI/Format/get_…) → copy, translate - Literals in comparison methods (
Is/Has/Matches/Find…) → keys for matching, do not translate
After adding a new rule, must verify with a negative test: deliberately insert "Levels": "等级" into the translation table and confirm the audit reports P0.
Pitfalls with Fonts
Embedded dynamic fonts only support equal-length in-place replacement. Overwriting the entire resources.assets changes the Texture2D m_StreamData offsets, causing all textures to be misaligned.
Process: The font stage subsets according to the "actual set of Chinese characters used," converts to TTF (Unity's support for CFF/OTF is unstable), then fills into m_FontData. If the glyph count is insufficient, reduce font size or weight, never exceed original size.
Fonts like Bangers-Regular, which are only a few dozen KB, cannot accommodate Chinese — redirect the font path to another Chinese font (configured in l10n.json's font.targets), which is more reliable than forcibly embedding.
Pre-Delivery Checklist
[ ] audit --fail-on P1 passed
[ ] Actually launch the game, walk through every screen: main menu / settings / save list / in-game HUD / popups / error screens
[ ] Player.log has no new exceptions (RT1 = 0)
[ ] Backup original file location recorded in release notes (restoration depends on it)
[ ] Record artifact MD5 in version notes
Method for locating a broken modification: First, only restore the original DLL, keep everything else — if it works, the issue is definitely in the assembly patch; then only restore the DLL, keep everything else — if the issue recurs, confirm. Binary search is faster than reading code.
Reference Documents
references/flow.md— Detailed implementation and data structures for each stagereferences/audit-spec.md— Full definitions, levels, and fix instructions for all 31 checksreferences/risk-rules.md— Risk rules table and the real incidents behind themassets/l10n.example.json/assets/glossary.example.csv— Configuration templates