ios-localization — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-localization (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.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.
Production-grade localization skill for iOS codebases. AI coding assistants systematically produce broken localization code across 30+ identifiable patterns — from missing Slavic plural categories to hardcoded left/right constraints that shatter RTL layouts. This skill intercepts those patterns and enforces correct localization from the start.
Covers String Catalogs (.xcstrings), CLDR pluralization, SwiftUI/UIKit APIs, date/number/currency formatting, RTL layout, accessibility localization, and enterprise patterns (modular apps, white-label, Swift Packages).
AI assistants generate localization code trained predominantly on English-only codebases. The result: only one/other plurals (breaking Russian, Ukrainian, Polish, Arabic), hardcoded English strings, String(format:) without positional specifiers, dateFormat strings that break in non-Gregorian calendars, and left/right constraints that break for 400M+ Arabic/Hebrew users. These bugs are silent in English testing and only surface in production.
Is the project SwiftUI or UIKit?
├── SwiftUI → String literals in Text/Button/Toggle auto-localize
│ ├── Need to prevent localization? → Text(verbatim: "v2.0")
│ ├── Dynamic key from variable? → Text(LocalizedStringKey(key))
│ ├── In a Swift Package? → Text("key", bundle: .module)
│ └── Passing localized content across APIs? → Use LocalizedStringResource
└── UIKit → Use String(localized:) for iOS 15+, NSLocalizedString for older
├── In a Swift Package? → String(localized: "key", bundle: .module)
└── Passing to another module? → Use LocalizedStringResourceIs the project using Xcode 15+ and targeting iOS 16+?
├── YES → Use String Catalogs (.xcstrings)
│ ├── Need pluralization without visible number? → .stringsdict (exception)
│ └── .xcstrings too large for direct editing? → Use scripts/xcstrings_tool.py
└── NO → Use .strings + .stringsdict
└── Migrating? → Xcode can convert: right-click .strings → Migrate to String CatalogLanguage family?
├── English, Spanish, Portuguese, Italian, French, German → one + other (2)
├── Russian, Ukrainian → one + few + many + other (4)
├── Polish → one + few + many + other (4, BUT different rules from Russian!)
├── Arabic → zero + one + two + few + many + other (6)
├── Japanese, Chinese, Korean, Turkish, Thai → other only (1)
└── Unsure → Check CLDR at unicode.org/cldr/charts/latest/supplemental/language_plural_rules.htmlIs this date from a server/API (parsing)?
├── YES → Fixed format with en_US_POSIX locale. NEVER user-facing styles
│ └── DateFormatter + locale = Locale(identifier: "en_US_POSIX") + dateFormat
└── NO → Displaying to user?
├── YES → NEVER use custom dateFormat strings
│ ├── iOS 15+ → Date.formatted(.dateTime.month().day().year())
│ ├── Older → DateFormatter with .dateStyle/.timeStyle
│ └── Custom pattern needed → setLocalizedDateFormatFromTemplate()
└── NO → Internal logic → Use Date components, not formatted stringsDoes the issue produce wrong output for non-English users?
├── YES → Is it silently wrong (no crash, just wrong text/layout)?
│ ├── YES → 🔴 CRITICAL (missing plurals, wrong date format, concatenated strings)
│ └── NO → 🟡 HIGH (crash on nil forced unwrap from missing key)
└── NO → Does it create localization debt (harder to translate later)?
├── YES → 🟡 HIGH (hardcoded strings, missing comments, String not LocalizedStringKey)
└── NO → 🟢 MEDIUM (non-optimal API, missing verbatim:)dateFormat for user-facing dates, currency interpolation without formatter.String parameter where LocalizedStringKey needed, missing bundle: .module in packages, NSLocalizedString with interpolation.NSLocalizedString on iOS 15+ (should be String(localized:)), missing verbatim: on non-localizable text, same key for different contexts.For all 30 patterns with code pairs, read references/ai-failure-patterns.md| # | AI Failure | Severity |
|---|---|---|
| F1 | Hardcoded user-facing strings without localization | 🟡 |
| F2 | English text as localization key (ambiguous) | 🟡 |
| F3 | Empty translator comments | 🟡 |
| F4 | Concatenated string fragments instead of format strings | 🔴 |
| F5 | Non-positional format specifiers (%@ not %1$@) | 🔴 |
| F6 | Only one/other plural categories (breaks Slavic/Arabic) | 🔴 |
| F7 | count == 1 ? singular : plural instead of stringsdict | 🔴 |
| F8 | Polish plural rules copied from Russian | 🔴 |
| F9 | String variable passed to Text() (skips localization) | 🟡 |
| F10 | Missing verbatim: on non-localizable strings | 🟢 |
| F11 | Missing bundle: .module in Swift Packages | 🟡 |
| F12 | NSLocalizedString with string interpolation | 🟡 |
| F13 | Custom dateFormat for user-facing dates | 🔴 |
| F14 | Missing en_US_POSIX locale for API date parsing | 🔴 |
| F15 | YYYY instead of yyyy in date format | 🔴 |
| F16 | Currency interpolation without NumberFormatter | 🔴 |
| F17 | Left/right constraints instead of leading/trailing | 🔴 |
| F18 | .left text alignment instead of .natural | 🔴 |
| F19 | Fixed-width containers on localized text | 🟡 |
| F20 | Hardcoded English accessibility labels | 🟡 |
String Catalog files are large single-JSON files (often 10K+ lines). AI assistants cannot reliably edit them directly. Use the bundled scripts:
# Validate .xcstrings for missing plurals, empty translations, stale entries
python3 scripts/xcstrings_tool.py validate path/to/Localizable.xcstrings
# Add a new key with comment
python3 scripts/xcstrings_tool.py add-key path/to/Localizable.xcstrings \
--key "greeting_format" \
--comment "Greeting with user name, e.g. 'Hello, Maria!'" \
--value "Hello, %@!"
# Audit plural completeness for a specific language
python3 scripts/xcstrings_tool.py audit-plurals path/to/Localizable.xcstrings --lang ru
# Export translation status report
python3 scripts/xcstrings_tool.py status path/to/Localizable.xcstringsFallback if scripts fail: Read the .xcstrings as JSON, find the key under strings.<key>.localizations.<lang>, and edit the stringUnit.value field. Always validate JSON after editing. Never attempt to rewrite the entire file — make surgical edits to specific keys only.
When: Creating a new SwiftUI or UIKit screen with user-facing text.
references/ai-failure-patterns.md — internalize patterns to avoidLocalizedStringKey for all user-facing text in SwiftUIText(verbatim:) for non-localizable content (versions, URLs, identifiers)references/pluralization.md.natural text alignmentDate.formatted() or .dateStyle/.timeStyle for displayed datesWhen: User asks "review localization", "check i18n", or any variant.
String(format: without positional specifierscount == 1 ? or count > 1 ? — should use stringsdict.left/.right in constraints and text alignmentdateFormat = on user-facing DateFormatters (missing en_US_POSIX check)"\(price)" or string-interpolated currency valuespython3 scripts/xcstrings_tool.py audit-pluralsbundle: .moduleWhen: Adding a new language or auditing plural completeness.
python3 scripts/xcstrings_tool.py audit-plurals path/to/file.xcstrings --lang <code>references/pluralization.md for the language's required categories[0, 1, 2, 3, 4, 5, 10, 11, 12, 14, 20, 21, 22, 25, 100, 101, 111, 1.5]When: User is displaying or parsing dates, numbers, or currencies.
references/formatting.md for the specific formatting rulesen_US_POSIX localedateFormat — use system stylesNumberFormatter or .formatted(.currency(code:))### [SEVERITY] [Short title]
**File:** `path/to/file.swift:42`
**Rule:** [Rule number from the 30 rules]
**Issue:** [1-2 sentence description]
**Impact:** [Which languages/locales are affected]
**Fix:**// ❌ Current [broken code]
// ✅ Corrected [localized replacement]
<critical_rules>
Whether generating new code or reviewing existing code, ALWAYS enforce these rules:
String(localized:) (UIKit) or auto-localization via Text() literal (SwiftUI)."settings.account.delete" not "Delete Account".comment: ""."Hello, " + name prevents word reordering. Use a single format string with placeholders."%1$@ invited %2$@" not "%@ invited %@" — translators need to reorder arguments.one/few/many/other. Polish needs the same but with different rules. Arabic needs all six. Check CLDR for every supported language.count == 1 ? singular : plural. This breaks every language with more than two plural forms. Always use .stringsdict or String Catalog plural variations.Text(verbatim:) for non-localizable strings. Version numbers, URLs, identifiers, debug text, format codes — none of these should enter String Catalogs.bundle: .module to Text(), String(localized:), and NSLocalizedString.locale = Locale(identifier: "en_US_POSIX"). Without it, Buddhist calendar users get year 2568, 12-hour time users break HH:mm.dateFormat for user-facing dates. Use .dateStyle/.timeStyle, setLocalizedDateFormatFromTemplate(), or Date.formatted().yyyy not YYYY, dd not DD in date format strings. YYYY = week-of-year year (wrong near Jan 1). DD = day of year (1-366).NumberFormatter with .currency style or .formatted(.currency(code:))..natural, never .left. .natural auto-adapts to RTL.accessibilityLabel("Close") in English is broken for every other language. Use String(localized:) or localized Text().scripts/xcstrings_tool.py to validate, add entries, and audit programmatically.[0, 1, 2, 3, 4, 5, 10, 11, 12, 14, 20, 21, 22, 25, 100, 101, 111, 1.5]. This set hits every boundary where Russian/Polish/Arabic categories change (e.g., Russian flips one→few at 2, few→many at 5, then repeats at 11; Polish has different rules at 22; the 1.5 catches fraction plurals). Do NOT improvise the set..xcstrings when the same key is extracted from multiple source locations with different comments — this creates noisy diffs on every build. Fix by extracting the call site to a single helper function or literal, or by overriding the comment in one canonical place..strings file) or use a different table name. Mixing results in undefined load-order behavior..xcstrings plural variations — use a separate .stringsdict file for those specific keys.String(format: NSLocalizedString("greeting", comment: ""), name) requires %1$@ in the translation, not %@. This is the #1 UIKit localization regression — translators CANNOT reorder %@ placeholders.imageFlippedForRightToLeftLayoutDirection() (UIKit) or flipsForRightToLeftLayoutDirection(true) (SwiftUI).Text("Hello, \(name)") infers DefaultStringInterpolation and produces a non-localized String, not a LocalizedStringKey. To force localization, use Text("greeting_\(name)", tableName: nil) with a key that's extracted, or Text(String(localized: "greeting_\(name)")). Simply interpolating into Text(...) loses localization.bundle: .module bug appears to work in English because the key matches the fallback value OR because only Bundle.main is populated with translations. Always verify by running the app in a non-English language — English-only testing hides this bug completely.0.75 must read as "soixante-quinze pour cent" in French, which requires passing the value to .formatted(.percent) not "\(value * 100)%".</critical_rules>
Split catalogs by feature or module to reduce merge pressure on large teams: Settings.xcstrings, Checkout.xcstrings, Auth.xcstrings instead of one monolithic Localizable.xcstrings. Cross-feature duplication is cheap (bytes) — the merge-conflict savings are large.
Hand-sorting `.xcstrings` JSON as a primary fix is wrong. Xcode rewrites the file on every build, so sort ordering is not a durable fix. The real fix is one canonical comment per key (rule 19 above) and catalog splitting.
Add these to your localization QA pipeline:
.leading/.trailing constraints flip correctly AND that text mirrors, without needing to add Arabic translations first.<fallback_strategies>
If .xcstrings file is too large to read/edit directly: Use scripts/xcstrings_tool.py for programmatic operations. If the script fails, read the file as JSON, navigate to the specific key path (strings.<key>.localizations.<lang>.stringUnit.value), make a surgical edit, and validate JSON after. Never attempt to rewrite the full file.
If unsure which CLDR plural categories a language needs: Check references/pluralization.md for common languages. For others, query unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html. When in doubt, include all six categories — extra categories are harmless, missing ones produce wrong output.
If String Catalog extraction misses a key: Verify the string is a literal in a localization-aware API (Text(), String(localized:), NSLocalizedString). Variables passed as String type won't be extracted. Wrap in LocalizedStringKey(variable) or String(localized: .init(stringLiteral: variable)).
If merge conflicts in .xcstrings: Both sides likely added different keys to the same JSON object. Resolution: accept either side, then manually re-add the other side's keys. Or split strings across multiple catalogs to reduce conflict frequency.
If date parsing fails silently in testing but works locally: Check if the test device/simulator uses a non-Gregorian calendar or 12-hour time. Set locale = Locale(identifier: "en_US_POSIX") on the DateFormatter used for parsing. </fallback_strategies>
Before finalizing generated or reviewed code, verify ALL:
[ ] No hardcoded user-facing strings — all use localization APIs
[ ] Localization keys are semantic (dot-notation), not English text
[ ] Every localized string has a meaningful translator comment
[ ] No string concatenation — all use format strings with placeholders
[ ] All format specifiers are positional (%1$@, %2$@)
[ ] Plural forms cover all CLDR-required categories for each supported language
[ ] No count == 1 ternaries — all use stringsdict or String Catalog plurals
[ ] Non-localizable strings use Text(verbatim:) in SwiftUI
[ ] Swift Package strings pass bundle: .module
[ ] API date parsing uses en_US_POSIX locale
[ ] User-facing dates use system styles (not custom dateFormat)
[ ] Date formats use yyyy (not YYYY) and dd (not DD)
[ ] Currency values use NumberFormatter or .formatted(.currency(code:))
[ ] All constraints use leading/trailing (not left/right)
[ ] Text alignment uses .natural (not .left)
[ ] All accessibility labels and hints are localized
[ ] .xcstrings validated with scripts/xcstrings_tool.py| Finding type | Companion skill | Apply when |
|---|---|---|
| Accessibility labels need localization | skills/ios-accessibility/SKILL.md | VoiceOver labels and hints must be localized |
| SwiftUI architecture for localized ViewModels | skills/swiftui-mvvm/SKILL.md | Managing localized state in ViewModels |
| Testing localization | skills/ios-testing/SKILL.md | XCTest with pseudolanguages, locale-specific tests |
| Security of localized content | skills/ios-security/SKILL.md | Format string vulnerabilities in localized strings |
| Reference | When to Read |
|---|---|
references/rules.md | Do's and Don'ts quick reference: all 30 rules ranked by severity |
references/ai-failure-patterns.md | Every code generation/review — all failure patterns with ❌/✅ code pairs |
references/string-catalogs.md | Working with .xcstrings — format, pitfalls, migration, Xcode 26 features |
references/pluralization.md | Plural rules — CLDR categories per language, Russian vs Polish differences, test sets |
references/swiftui-localization.md | SwiftUI-specific — LocalizedStringKey, verbatim, interpolation, packages |
references/formatting.md | Date, number, currency formatting — parsing vs display, locale traps |
references/rtl-layout.md | RTL layout — leading/trailing, semantic content attribute, exceptions |
references/enterprise-patterns.md | Modular apps, white-label, accessibility localization, bundle management |
references/testing.md | Testing — pseudolanguages, NSShowNonLocalizedStrings, automated checks |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.