All posts

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:

  • Use 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"))

  • Xcode automatically extracts these into the String Catalog when you build. The catalog shows every string, its translation status per language, and any plural variants.
  • Export for translation via 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)
    Google Play: Supports locale-specific listings with:
    • App name (30 characters in some locales, 50 in others)
    • Short description (80 characters)
    • Full description (4,000 characters)
    Keyword strategy. Don't literally translate your English keywords. Research what users in each market actually search for. The Japanese term for "to-do list" might have less search volume than the English loanword "ToDo リスト." Use ASO (App Store Optimization) tools per locale.

    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)
    Mobile localization is one of those areas where getting the basics right covers 80% of users' experience, and the remaining 20% is a long tail of locale-specific edge cases. Start with the high-impact items (string extraction, plurals, layout flexibility), ship, gather feedback from users in each market, and iterate.