localization-ios
npx skills add https://github.com/kaakati/rails-enterprise-dev --skill localization-ios
Agent 安装分布
Skill 文档
Localization iOS â Expert Decisions
Expert decision frameworks for localization choices. Claude knows NSLocalizedString and .strings files â this skill provides judgment calls for architecture decisions and cross-language complexity.
Decision Trees
Runtime Language Switching
Do users need in-app language control?
ââ NO (respect system language)
â ââ Standard localization
â NSLocalizedString, let iOS handle it
â Simplest and recommended
â
ââ YES (business requirement)
â ââ Does app restart work?
â ââ YES â UserDefaults + AppleLanguages
â â Simpler, more reliable
â ââ NO â Full runtime switching
â Complex: Bundle swizzling or custom lookup
â
ââ Single language override only (e.g., always English)
ââ Don't localize
Bundle.main with no .lproj
The trap: Implementing runtime language switching when system language suffices. It adds complexity and can break third-party SDKs that read system locale.
String Key Architecture
How to structure your keys?
ââ Small app (< 100 strings)
â ââ Flat keys with prefixes
â "login_title", "login_email_placeholder"
â
ââ Medium app
â ââ Hierarchical dot notation
â "auth.login.title", "auth.login.email"
â
ââ Large app with teams
â ââ Feature-based files
â Auth.strings, Profile.strings, etc.
â Each team owns their strings file
â
ââ Design system / component library
ââ Component-scoped keys
"button.primary.title", "input.error.required"
Pluralization Complexity
Which languages do you support?
ââ Western languages only (en, es, fr, de)
â ââ Simple plural rules
â one, other (maybe zero)
â
ââ Slavic languages (ru, pl, uk)
â ââ Complex plural rules
â one, few, many, other
â e.g., Russian: 1 Ñайл, 2 Ñайла, 5 Ñайлов
â
ââ Arabic
â ââ Six plural forms!
â zero, one, two, few, many, other
â MUST use stringsdict
â
ââ East Asian (zh, ja, ko)
ââ No grammatical plural
But may need counters/classifiers
RTL Support Level
Do you support RTL languages?
ââ NO RTL languages planned
â ââ Still use leading/trailing
â Future-proof your layout
â
ââ Arabic only
â ââ Standard RTL support
â layoutDirection + leading/trailing
â Test thoroughly
â
ââ Arabic + Hebrew + Persian
â ââ Each has unique considerations
â Hebrew: different number handling
â Persian: different numerals (Û±Û²Û³)
â
ââ Mixed LTR/RTL content
ââ Explicit direction per component
Force LTR for code, URLs, numbers
NEVER Do
String Management
NEVER concatenate localized strings:
// â Breaks in languages with different word order
let message = NSLocalizedString("hello", comment: "") + " " + userName
// German: "Hallo" + " " + "Hans" = "Hallo Hans" â
// Japanese: "ããã«ã¡ã¯" + " " + "ç°ä¸" = "ããã«ã¡ã¯ ç°ä¸" â
// Should be "ç°ä¸ãããããã«ã¡ã¯"
// â
Use format strings
let format = NSLocalizedString("greeting.format", comment: "")
let message = String(format: format, userName)
// greeting.format = "Hello, %@!" (en)
// greeting.format = "%@ãããããã«ã¡ã¯!" (ja)
NEVER embed numbers in translation keys:
// â Doesn't handle plural rules
"items.1" = "1 item"
"items.2" = "2 items"
"items.3" = "3 items"
// What about 0? 100? Arabic's 6 forms?
// â
Use stringsdict for plurals
String.localizedStringWithFormat(
NSLocalizedString("items.count", comment: ""),
count
)
NEVER assume string length:
// â German is ~30% longer than English
.frame(width: 100) // "Settings" fits, "Einstellungen" doesn't
// â
Use flexible layouts
.frame(minWidth: 80)
// Or
.fixedSize(horizontal: true, vertical: false)
NEVER use left/right in layouts:
// â Breaks in RTL
.padding(.left, 16)
.frame(alignment: .left)
// â
Use leading/trailing
.padding(.leading, 16)
.frame(alignment: .leading)
Runtime Language
NEVER change AppleLanguages without restart:
// â Partial UI update â inconsistent state
UserDefaults.standard.set(["ar"], forKey: "AppleLanguages")
// Some views updated, others not. Third-party SDKs broken.
// â
Require restart or use custom bundle
UserDefaults.standard.set(["ar"], forKey: "AppleLanguages")
showRestartRequiredAlert() // User restarts app
NEVER forget to set locale for formatters:
// â Uses device locale, not app's selected language
let formatter = DateFormatter()
formatter.dateStyle = .medium
let date = formatter.string(from: Date()) // Wrong language!
// â
Set locale explicitly
let formatter = DateFormatter()
formatter.locale = Locale(identifier: selectedLanguage.rawValue)
formatter.dateStyle = .medium
Pluralization
NEVER use simple if/else for plurals:
// â Fails for Russian, Arabic, etc.
func itemsText(_ count: Int) -> String {
if count == 1 {
return "1 item"
} else {
return "\(count) items"
}
}
// Russian: 1 ÑоваÑ, 2 ÑоваÑа, 5 ÑоваÑов, 21 ÑоваÑ, 22 ÑоваÑа...
// This requires CLDR plural rules
// â
Use stringsdict â iOS handles rules automatically
NEVER hardcode numeral systems:
// â Arabic users may expect Arabic-Indic numerals
Text("\(count) items") // Shows "5 items" even in Arabic
// â
Use NumberFormatter with locale
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "ar")
formatter.string(from: count as NSNumber) // "Ù¥"
RTL Layouts
NEVER use fixed directional icons:
// â Arrow points wrong way in RTL
Image(systemName: "arrow.right")
// â
Use semantic icons or flip
Image(systemName: "arrow.forward") // Semantic
// Or
Image(systemName: "arrow.right")
.flipsForRightToLeftLayoutDirection(true)
NEVER force layout direction globally when it should be per-component:
// â Phone numbers, code, etc. should stay LTR
.environment(\.layoutDirection, .rightToLeft)
// â
Apply selectively
VStack {
Text(localizedContent) // Follows RTL
Text(phoneNumber)
.environment(\.layoutDirection, .leftToRight) // Always LTR
Text(codeSnippet)
.environment(\.layoutDirection, .leftToRight)
}
Essential Patterns
Type-Safe Localization with SwiftGen
// swiftgen.yml
// strings:
// inputs: Resources/en.lproj/Localizable.strings
// outputs:
// - templateName: structured-swift5
// output: Generated/Strings.swift
// Usage â compile-time safe
Text(L10n.Auth.Login.title)
Text(L10n.User.greeting(userName))
Text(L10n.Items.count(itemCount))
// Benefits:
// - Compiler catches missing keys
// - Auto-complete for strings
// - Refactoring safe
Stringsdict for Plurals
<!-- en.lproj/Localizable.stringsdict -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
<plist version="1.0">
<dict>
<key>items.count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@items@</string>
<key>items</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>No items</string>
<key>one</key>
<string>%d item</string>
<key>other</key>
<string>%d items</string>
</dict>
</dict>
</dict>
</plist>
// Usage
let text = String.localizedStringWithFormat(
NSLocalizedString("items.count", comment: ""),
count
)
// 0 â "No items"
// 1 â "1 item"
// 5 â "5 items"
RTL-Aware Layout Helpers
extension View {
/// Applies leading alignment that respects RTL
func alignLeading() -> some View {
self.frame(maxWidth: .infinity, alignment: .leading)
}
/// Force LTR for content that shouldn't flip (code, URLs, phone numbers)
func forceLTR() -> some View {
self.environment(\.layoutDirection, .leftToRight)
}
}
// ContentView
struct MessageCell: View {
let message: Message
var body: some View {
VStack(alignment: .leading) {
Text(message.content)
.alignLeading() // Respects RTL
Text(message.codeSnippet)
.font(.monospaced(.body)())
.forceLTR() // Code always LTR
Text(message.url)
.forceLTR() // URLs always LTR
}
}
}
Locale-Aware Formatting
struct LocalizedFormatters {
let locale: Locale
init(languageCode: String) {
self.locale = Locale(identifier: languageCode)
}
func formatDate(_ date: Date, style: DateFormatter.Style = .medium) -> String {
let formatter = DateFormatter()
formatter.locale = locale
formatter.dateStyle = style
return formatter.string(from: date)
}
func formatNumber(_ number: Double) -> String {
let formatter = NumberFormatter()
formatter.locale = locale
formatter.numberStyle = .decimal
return formatter.string(from: NSNumber(value: number)) ?? "\(number)"
}
func formatCurrency(_ amount: Double, code: String) -> String {
let formatter = NumberFormatter()
formatter.locale = locale
formatter.numberStyle = .currency
formatter.currencyCode = code
return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
}
}
Quick Reference
Plural Forms by Language
| Language | Forms | Example (1, 2, 5) |
|---|---|---|
| English | one, other | 1 item, 2 items, 5 items |
| French | one, other | 1 élément, 2 éléments |
| Russian | one, few, many, other | 1 Ñайл, 2 Ñайла, 5 Ñайлов |
| Arabic | zero, one, two, few, many, other | 6 forms! |
| Japanese | other only | No grammatical plural |
RTL Languages
| Language | Script Direction | Numerals |
|---|---|---|
| Arabic | RTL | Arabic-Indic (٠١٢) or Western |
| Hebrew | RTL | Western |
| Persian | RTL | Extended Arabic (Û°Û±Û²) |
| Urdu | RTL | Extended Arabic |
String Expansion Guidelines
| Source (English) | Expansion |
|---|---|
| 1-10 chars | +200-300% |
| 11-20 chars | +80-100% |
| 21-50 chars | +60-80% |
| 51-70 chars | +50-60% |
| 70+ chars | +30% |
Red Flags
| Smell | Problem | Fix |
|---|---|---|
| String concatenation | Word order varies | Format strings |
| if count == 1 else | Wrong plural rules | stringsdict |
| .padding(.left) | Breaks RTL | .padding(.leading) |
| DateFormatter without locale | Wrong language | Set locale explicitly |
| Runtime language without restart | Inconsistent UI | Require restart |
| Fixed frame widths for text | Text truncation | Flexible layouts |
| Hardcoded “1, 2, 3” | Wrong numeral system | NumberFormatter with locale |