Localizing Mobile Apps: The Complete Guide for iOS and Android
String extraction, plural handling, layout issues, RTL support, and app store listing translation for iOS and Android apps.
Mobile app localization has more moving parts than web localization. You're dealing with platform-specific string formats, system-level locale handling, layout constraints on small screens, and app store listings that need to rank in local search. Here's the practical walkthrough.
iOS: String Catalogs (the modern way)
As of Xcode 15, Apple recommends String Catalogs (.xcstrings files) over the legacy .strings and .stringsdict formats. String Catalogs are JSON files that store all translations in one place with built-in plural support.
A basic workflow:
String(localized:) in your Swift code:Text(String(localized: "welcome_message"))
// With interpolation
Text(String(localized: "items_count \(count)", defaultValue: "\(count) items in your cart"))
Product > Export Localizations, which generates .xcloc bundles (essentially XLIFF files with metadata).Plural handling in String Catalogs:
String Catalogs handle CLDR plural rules natively. For a string like "You have X items," you define variants:
// Xcode String Catalog editor or raw JSON:
"items_count %lld" : {
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : { "stringUnit" : { "value" : "%lld item" } },
"other" : { "stringUnit" : { "value" : "%lld items" } }
}
}
},
"pl" : {
"variations" : {
"plural" : {
"one" : { "stringUnit" : { "value" : "%lld element" } },
"few" : { "stringUnit" : { "value" : "%lld elementy" } },
"many" : { "stringUnit" : { "value" : "%lld elementów" } },
"other" : { "stringUnit" : { "value" : "%lld elementów" } }
}
}
}
}
}
Polish needs four plural forms. If you only provide one and other, numbers like 2, 3, 4, 22, 23, 24 display incorrectly.
Android: resources and string arrays
Android uses XML resource files in locale-specific directories:
res/
values/
strings.xml # Default (English)
values-ja/
strings.xml # Japanese
values-de/
strings.xml # German
values-ar/
strings.xml # Arabic
Basic strings:
<!-- res/values/strings.xml -->
<resources>
<string name="welcome_message">Welcome back, %1$s!</string>
<string name="settings">Settings</string>
</resources>
Plurals in Android:
<plurals name="items_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
val text = resources.getQuantityString(R.plurals.items_count, count, count)
Android supports zero, one, two, few, many, other — but only if you define them. For Arabic, you need all six:
<!-- res/values-ar/strings.xml -->
<plurals name="items_count">
<item quantity="zero">لا عناصر</item>
<item quantity="one">عنصر واحد</item>
<item quantity="two">عنصران</item>
<item quantity="few">%d عناصر</item>
<item quantity="many">%d عنصرًا</item>
<item quantity="other">%d عنصر</item>
</plurals>
Layout issues on small screens
Text expansion is the top layout problem. German text is typically 30% longer than English. Finnish and Russian can be even longer. On a mobile screen where space is already tight, this breaks layouts.
Buttons and labels. Never set fixed widths. Use wrap_content (Android) or intrinsic content size (iOS). But also set maximum widths with ellipsis truncation as a safety net:
<!-- Android -->
<Button
android:layout_width="wrap_content"
android:maxWidth="200dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/submit_button" />
// iOS
Text(String(localized: "submit_button"))
.lineLimit(1)
.truncationMode(.tail)
Dynamic Type / font scaling. Both iOS and Android support system-level font size adjustments. A user with "Extra Large" text size will blow out any layout that assumes default font sizes. Test your translated layouts at the largest system font size.
Tab bars and navigation. English tab labels like "Home," "Search," "Profile" are short. German equivalents ("Startseite," "Suche," "Profil") are longer. Japanese equivalents might be shorter. Test your navigation at both extremes.
RTL support
Arabic, Hebrew, Farsi, and Urdu require right-to-left layout.
Android: Add android:supportsRtl="true" to your manifest and replace Left/Right with Start/End in all layout parameters:
<!-- Bad -->
<TextView android:layout_marginLeft="16dp" />
<!-- Good -->
<TextView android:layout_marginStart="16dp" />
iOS: Use leading/trailing constraints instead of left/right:
// Bad
view.leftAnchor.constraint(equalTo: parent.leftAnchor)
// Good
view.leadingAnchor.constraint(equalTo: parent.leadingAnchor)
SwiftUI handles this automatically for most layouts. UIKit requires manual attention.
Testing RTL without Arabic translations: Both platforms let you force RTL layout in developer settings. On iOS, add the -AppleTextDirection YES -NSForceRightToLeftWritingDirection YES launch arguments in your scheme. On Android, enable "Force RTL layout direction" in Developer Options.
String extraction and automation
Manually managing translation files for a large app is error-prone. Automate extraction and import:
For Android, you can write a script that parses strings.xml, sends strings to a translation API, and writes back the translated XML:
import xml.etree.ElementTree as ET
def extract_android_strings(filepath):
tree = ET.parse(filepath)
root = tree.getroot()
strings = {}
for elem in root.findall('string'):
strings[elem.get('name')] = elem.text
return strings
def write_android_strings(strings, filepath):
root = ET.Element('resources')
for name, value in strings.items():
elem = ET.SubElement(root, 'string', name=name)
elem.text = value
tree = ET.ElementTree(root)
tree.write(filepath, encoding='utf-8', xml_declaration=True)
For iOS String Catalogs, the .xcstrings file is JSON, so parsing and writing is straightforward:
import json
def extract_ios_strings(filepath):
with open(filepath, 'r') as f:
catalog = json.load(f)
strings = {}
for key, entry in catalog.get('strings', {}).items():
source = entry.get('localizations', {}).get('en', {})
if 'stringUnit' in source:
strings[key] = source['stringUnit']['value']
return strings
You can pipe extracted strings through auto18n or any translation API, then write the translations back into the platform-specific format. This fits into CI — run on every release to catch untranslated strings.
App Store listing translation
Your app's name, subtitle, description, keywords, and screenshots text all need localization. This is marketing content, not UI strings — the quality bar is higher.
App Store (iOS): Supports 40 locale-specific listings. Key fields:
- App name (30 characters)
- Subtitle (30 characters)
- Description (4,000 characters)
- Keywords (100 characters, comma-separated)
- What's New (4,000 characters)
- App name (30 characters in some locales, 50 in others)
- Short description (80 characters)
- Full description (4,000 characters)
Fastlane integration. Fastlane's deliver (iOS) and supply (Android) tools support locale-specific metadata directories:
fastlane/metadata/
en-US/
name.txt
subtitle.txt
description.txt
keywords.txt
ja/
name.txt
subtitle.txt
description.txt
keywords.txt
Automate this: translate metadata files, commit to the repo, and deploy via Fastlane in CI.
Testing checklist
Before submitting a localized app:
- [ ] All strings display correctly (no truncation, no overflow)
- [ ] Plural forms work for numbers 0, 1, 2, 5, 11, 21, 100 (covers most plural rule edge cases)
- [ ] Date/time formatting respects locale
- [ ] Number formatting (decimal separators, thousands separators)
- [ ] Currency display (symbol position, decimal places)
- [ ] RTL layout renders correctly (if applicable)
- [ ] Keyboard input works for the locale (IME support for CJK)
- [ ] Deep links and notification text are translated
- [ ] Error messages from the API are translated
- [ ] App Store screenshots show localized UI
- [ ] App name and description read naturally in each language
- [ ] Accessibility labels are translated (VoiceOver / TalkBack)