ai-translated-localization · git:20260624.331609b · 2026-06-24 · sha256 32357ccb5f4cc39c

ai-translated-localization git:20260624.331609bA

Immutable. This exact content is served forever at /api/v1/blob/32357ccb5f4cc39c.

---
name: ai-translated-localization
description: Default localization scope AND execution playbook for Apple-platform Apps — 7 locales (en, zh-Hant, zh-Hans, ja, ko, es, th) translated via AI agent flow using `Localizable.xcstrings`. Source = en, primary = zh-Hant. Minimum set zh-Hant + en. Invoke when (1) deciding L10n scope / catalog format / translation flow at project setup, OR (2) actually executing a translation pass to add or refresh strings.
---

# AI-Translated Localization

## When to invoke

**Scope decisions:**
- Starting a new App and deciding which locales to support.
- Choosing a string catalog format (`.strings` vs `.xcstrings`).
- Planning the translation flow (manual / agency / AI agent).
- User asks "are 7 locales too many", "how do translations enter git", "how to handle multi-locale App Store metadata".

**Execution:**
- Adding new user-facing strings (UI keys, GC titles, ASC metadata, achievement descriptions) and need to fill out their non-source locales.
- Refreshing strings whose source language changed (`extractionState: stale` entries in xcstrings).
- Auditing whether a release's xcstrings is complete (every key has every locale, no `<TRANSLATE>` placeholders shipping).

## Default decisions

### Default 7 locales

| Locale | Code | Notes |
|---|---|---|
| English | `en` | Catalog `sourceLanguage`; translation source for the fan-out |
| Traditional Chinese | `zh-Hant` | Primary language — author-written alongside `en`, never AI-translated |
| Japanese | `ja` | Largest adjacent market outside the Chinese sphere |
| Simplified Chinese | `zh-Hans` | Converted from `zh-Hant` + Mainland phrasing review |
| Spanish | `es` | World's second largest native-speaker base |
| Thai | `th` | Southeast Asia representative |
| Korean | `ko` | High-penetration Asian market |

- Locale codes are the **script-based BCP-47 forms** (`zh-Hant` / `zh-Hans`), not region
  forms (`zh-TW` / `zh-CN`) — matches the committed catalogs and your repo's L10n completeness gate.
- **Minimum set**: zh-Hant + en (every project includes at least these two).
- Per-project locale lists can be adjusted, but **English and zh-Hant are always included**.

### Translation flow

- **Handled by an AI agent**, recorded as a step in `plan.md`: "author `en` + `zh-Hant` by hand, fan out the other 5 locales from `en`, write into `Localizable.xcstrings`" (see Execution playbook Step 1).
- Covers:
  - In-app strings
  - Game Center leaderboard / achievement names (for games)
  - App Store metadata (title / description / keywords / what's new)
  - Description text inside the Privacy Manifest
- Every time strings are added / modified, run another round of the AI translation flow; the diff lands in a PR.

### Catalog format

- Use Xcode's **`Localizable.xcstrings`** (String Catalog, introduced in Xcode 15+).
- Stop using legacy `.strings` / `.stringsdict` (unless an external tool forces it).
- xcstrings is JSON-structured: PR-friendly diffs and easy for AI to manipulate.

## Rationale

- 7 locales cover most of the global market while remaining a polish scope a solo developer can sustain.
- AI translation quality for App UI strings (short, clear context) is at commercial level; long marketing copy is still recommended for human review.
- xcstrings JSON structure is naturally friendly to AI / diff / version control.
- `zh-Hant` as primary reflects the author's native-language accuracy; `en` is the
  fan-out source because translator competence is broader from English (see Field notes).

## Deviation considerations

- **Focused target market**: shrink to zh-Hant + en + one target-market locale.
- **No budget / no time**: ship zh-Hant + en first; mark others as `extractionState: stale` for later.
- **Regulated / sensitive content** (medical / financial / kids): **mandatory human review** after AI translation; add a review step to `plan.md`.
- **Special scripts / RTL** (Arabic / Hebrew): UI needs additional layout verification, not just translation.

## Execution playbook

Use when actually performing a translation pass. The flow is **source-pair seed → AI fan-out → tricky-case review → completeness check**.

### Step 1 — Seed source pair (en + zh-Hant)

- Source language for translation **must be English** (broader translator competence across all target locales than zh-Hant).
- Authors write English first; zh-Hant is the project's primary native locale and is hand-written in parallel — *not* AI-translated from English. Both are written by the author with intent; the other 5 locales fan out from `en`.
- Each new key lands in xcstrings with **at minimum** `en` + `zh-Hant` populated and `extractionState: manual`.
- Other 5 locales (`ja`, `zh-Hans`, `es`, `th`, `ko`) start either absent or with the placeholder string `<TRANSLATE>` so the fan-out pass can find them with a single grep.

### Step 2 — AI fan-out pass

For each target locale, translate each key from `en` (source-of-truth) into the target locale. The AI doing the translation should:

- **Preserve substitutions**: `%@`, `%lld`, `%1$@`, `\n`, markdown markers (`**`, `_`), and any `${...}` interpolation tokens are copied verbatim.
- **Match plural/variation forms**: if xcstrings declares plural variations for `en`, all forms must exist in the target locale (`zero` / `one` / `two` / `few` / `many` / `other`) per CLDR plural categories for that locale — don't blindly copy English's two forms.
- **Match length budget**: if source is a button label (≤ 12 chars typical), keep target short. Don't let "OK" become a 6-word phrase. UI labels lose to long translations.
- **Match register / tone**: derive from `en`'s tone. Buttons are imperative, descriptions are neutral, error messages are direct. Don't add politeness markers absent in source (see locale gotchas below).
- **Preserve product nouns**: app name, brand terms, mode names that share visual identity across locales (e.g., "Practice" → 練習 in both zh-Hant and ja for visual consistency). Maintain a small **glossary** captured per-project to enforce this.

After translation, set `extractionState: translated` (per Apple xcstrings convention) so Xcode no longer flags the key as needing attention.

### Step 3 — Tricky-case review (locale-specific gotchas)

Lessons captured from real translation passes. Apply these as a second-pass review after the bulk AI fan-out.

**Japanese (`ja`):**
- Prefer **semantic** over literal. "Pencil" (notes / candidate input) → メモ, NOT 鉛筆. The literal kanji confuses Japanese users; the semantic UI term is correct.
- Drop politeness markers (`です` / `ます`) in button labels and short UI strings — Japanese app UIs are typically declarative/imperative, not polite.
- 漢字 vs かな: prefer 漢字 for nouns, ひらがな for particles, カタカナ for loanwords. Don't romanize unless the source is romanized.

**Thai (`th`):**
- **No politeness markers** (`ครับ` / `ค่ะ`) in UI strings unless the app's voice is deliberately conversational. Calm/neutral apps drop them.
- Compound nouns: "leaderboard" → กระดานผู้นำ (no single-word equivalent). Accept the multi-word form.
- Thai has **no word boundaries** — sentence length affects line break behavior. UI strings >24 chars need spot-check rendering.

**Korean (`ko`):**
- Use **informal-formal** style (해요체) for app UI by default — neither too casual (반말) nor overly formal (합쇼체).
- 한자 should be avoided unless disambiguation is needed; pure 한글 is the modern default.
- Postpositions (조사) change based on preceding character's final consonant; AI usually handles this, but spot-check `을/를`, `이/가`, `은/는`.

**Spanish (`es`):**
- Default to **neutral Latin American Spanish** unless the project explicitly targets Spain (`es-ES`). Avoid `vosotros` forms; use `ustedes`.
- Gender agreement: nouns referring to the user (e.g., "completed") need gender-neutral phrasing if the user's gender is unknown.

**Simplified Chinese (`zh-Hans`):**
- Convert from `zh-Hant` (not from `en`) for terminology consistency; the AI still needs to substitute Mainland-preferred terms (软件 vs 軟體, 移动 vs 行動, 视频 vs 影片).
- DON'T just run `tongwen` character conversion — phrase choice differs (e.g., zh-Hant 「設定」 → zh-Hans 「设置」, not 「設定 → 设定」).

**English (`en`):**
- US English by default. UK spellings (`colour`, `centre`, `analyse`) only if explicitly targeted.
- Sentence case for buttons and UI; Title Case only for proper nouns and app section headers.

### Step 4 — Visual / glossary consistency

Beyond per-string correctness, enforce a project-level glossary so the same concept renders identically across locales and screens:

- **Mode names** (e.g., "Daily" / "Practice") — pick one term per locale and use it everywhere (HomeView card, hub header, navigation title, Settings labels).
- **Product nouns** (app name, branded features) — don't translate.
- **Difficulty labels** (Easy / Medium / Hard) — short, consistent.
- **Action verbs** (Submit / Confirm / Cancel) — match iOS native terminology in each locale (Apple has localized Human Interface Guidelines for major locales).

Maintain the glossary as either inline comments in xcstrings, or a sidecar `Localization-Glossary.md` in `docs/` if it grows.

### Step 5 — Completeness check (before PR)

Verification gates before merging a translation pass:

- `<TRANSLATE>` count across xcstrings = 0 (none shipped).
- `extractionState: stale` count = 0 (all stale entries refreshed).
- Per-key locale coverage = 100% (parse xcstrings JSON; every `localizations` dict has every declared locale).
- For plural keys: every locale has every plural form required by CLDR for that locale.
- Substitution token parity per key: `en` has N `%@` → all locales have N `%@` (or locale-specific reordering via `%1$@` / `%2$@`).
- Spot-check 3-5 keys per locale visually in the simulator with `Scheme → Run → Options → App Language`.

### L10n gate scope — and its two blind spots

Your repo's L10n completeness gate (CI-enforced) checks **per-key locale
completeness**: every key present in a catalog has all declared locales, no `<TRANSLATE>`.
Two things it does **NOT** catch — both have shipped English-fallback bugs in real projects:

1. **A required key being *absent* entirely.** The gate validates keys that
   exist; it cannot know a newly-activated capability *needs* a key that no
   catalog has. When an app adopts a shared feature (audio settings, ATT primer,
   reminders), its catalog can ship missing keys → the UI renders raw dotted keys
   or English literals, and the gate stays green. After wiring any shared-UI
   capability into a new app, **diff its catalog's key set against an app that
   already has the feature** (real-world: a new game adopted shared audio-settings
   UI without copying the required catalog keys; another adopted an ATT primer
   and the raw dotted keys appeared at runtime — both passed the gate).
2. **A key referenced from shared UI code but absent from an app's catalog.**
   Per-key completeness alone will NOT catch this — the gate only validates keys
   that already exist in a catalog, so a key referenced from a shared UI module
   that the app's own catalog never declares renders raw at runtime while the gate
   stays green. **Build a shared-code dotted-key gate:** every dotted-namespace key
   (e.g. `leave.game.close`, `att.primer.title`) referenced from a shared UI module
   (SharedUI / SettingsUI / any shared UI module) must exist in **every** app
   catalog or CI fails. Scope it to *dotted* keys to avoid app-conditional
   English-phrase false positives — English-phrase shared keys are a separate,
   harder case (no dotted namespace to anchor on; track them with a dedicated
   audit rather than this gate).

### xcstrings editing footgun

When adding keys to a `Localizable.xcstrings`, **text-splice** the new entries
into the file — do **not** round-trip the whole catalog through a Python/JSON
`load → dump`. The round-trip reformats Xcode's style (`"k" : v` space-before-
colon + Xcode key order) and produces a multi-thousand-line noise diff that
buries the real change. Splicing keeps a clean, reviewable diff (real example:
4400-line noise diff collapsed to 192 lines after switching to splice approach).

### Tooling notes

- xcstrings is JSON; parse with any JSON library. Schema: `{"sourceLanguage": "en", "strings": {<key>: {"localizations": {<locale>: {"stringUnit": {"state": "translated", "value": "..."}}}}}}`.
- Plural variations use `{"variations": {"plural": {<cldr-form>: {"stringUnit": {...}}}}}` instead of a single `stringUnit`.
- When fanning out via an LLM, send a single key + source + glossary in the prompt; don't batch hundreds at once (latency wins less than accuracy loses).
- For ASC metadata (App Store description, keywords, "what's new"), use the **same flow** but route to the ASC API endpoints rather than xcstrings. The translation principles (length budget, register, gotchas) apply identically.

## Verification checklist

- `Localizable.xcstrings` exists and each key has 7 locale entries (2 for the minimum set).
- App Store Connect metadata is complete per locale (including screenshot captions).
- Game Center / achievement display names are complete per locale.
- `PrivacyInfo.xcprivacy` description itself doesn't need to be multi-locale, but the corresponding App Store privacy policy page does.

## Related skills

- `apple-platform-targets`: xcstrings requires Xcode 15+; aligns with the deployment target's toolchain.
- `spec-phase-orchestration`: "translation" should be an explicit step in `plan.md`.

## Field notes

Real translation passes have surfaced these recurring decisions:

- **Source = `en`, primary = `zh-Hant`** — both written by author. The other 5 fan out from `en` because translator competence is broader from English than from Chinese for `th` / `ko` / `es`.
- **Glossary beats per-string correctness** — visual consistency across screens matters more than the perfect translation of any single string.
- **5 locales in one pass is the sweet spot** — fewer wastes the per-pass setup; more risks AI fatigue degradation on the last locales. 1 commit per locale keeps PR review tractable.