ai-translated-localization — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ai-translated-localization (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Scope decisions:
.strings vs .xcstrings).Execution:
extractionState: stale entries in xcstrings).<TRANSLATE> placeholders shipping).| 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 |
zh-Hant / zh-Hans), not regionforms (zh-TW / zh-CN) — matches the committed catalogs and your repo's L10n completeness gate.
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)..strings / .stringsdict (unless an external tool forces it).zh-Hant as primary reflects the author's native-language accuracy; en is thefan-out source because translator competence is broader from English (see Field notes).
extractionState: stale for later.plan.md.Use when actually performing a translation pass. The flow is source-pair seed → AI fan-out → tricky-case review → completeness check.
en.en + zh-Hant populated and extractionState: manual.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.For each target locale, translate each key from en (source-of-truth) into the target locale. The AI doing the translation should:
%@, %lld, %1$@, \n, markdown markers (**, _), and any ${...} interpolation tokens are copied verbatim.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.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).After translation, set extractionState: translated (per Apple xcstrings convention) so Xcode no longer flags the key as needing attention.
Lessons captured from real translation passes. Apply these as a second-pass review after the bulk AI fan-out.
Japanese (`ja`):
です / ます) in button labels and short UI strings — Japanese app UIs are typically declarative/imperative, not polite.Thai (`th`):
ครับ / ค่ะ) in UI strings unless the app's voice is deliberately conversational. Calm/neutral apps drop them.Korean (`ko`):
을/를, 이/가, 은/는.Spanish (`es`):
es-ES). Avoid vosotros forms; use ustedes.Simplified Chinese (`zh-Hans`):
zh-Hant (not from en) for terminology consistency; the AI still needs to substitute Mainland-preferred terms (软件 vs 軟體, 移动 vs 行動, 视频 vs 影片).tongwen character conversion — phrase choice differs (e.g., zh-Hant 「設定」 → zh-Hans 「设置」, not 「設定 → 设定」).English (`en`):
colour, centre, analyse) only if explicitly targeted.Beyond per-string correctness, enforce a project-level glossary so the same concept renders identically across locales and screens:
Maintain the glossary as either inline comments in xcstrings, or a sidecar Localization-Glossary.md in docs/ if it grows.
Verification gates before merging a translation pass:
<TRANSLATE> count across xcstrings = 0 (none shipped).extractionState: stale count = 0 (all stale entries refreshed).localizations dict has every declared locale).en has N %@ → all locales have N %@ (or locale-specific reordering via %1$@ / %2$@).Scheme → Run → Options → App Language.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:
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).
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).
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).
{"sourceLanguage": "en", "strings": {<key>: {"localizations": {<locale>: {"stringUnit": {"state": "translated", "value": "..."}}}}}}.{"variations": {"plural": {<cldr-form>: {"stringUnit": {...}}}}} instead of a single stringUnit.Localizable.xcstrings exists and each key has 7 locale entries (2 for the minimum set).PrivacyInfo.xcprivacy description itself doesn't need to be multi-locale, but the corresponding App Store privacy policy page does.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.Real translation passes have surfaced these recurring decisions:
en because translator competence is broader from English than from Chinese for th / ko / es.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.