Skip to main content

The Complete Guide to iOS App Localization

From Localizable.strings to App Store metadata: localize your iOS app with Xcode, SwiftUI, Fastlane, and automated AI translation.

1

Enable Localization in Xcode

Open your Xcode project settings, go to Info > Localizations, and add the languages you want to support. Xcode creates .lproj directories for each language automatically.

Xcode Project Settings
// In Xcode:
// 1. Select your project in the navigator
// 2. Go to Info tab > Localizations
// 3. Click + to add languages (e.g., German, Japanese)
// 4. Select which files to localize
//
// Xcode creates .lproj directories automatically:
// en.lproj/Localizable.strings
// de.lproj/Localizable.strings
// ja.lproj/Localizable.strings
Base localization separates your UI from its strings. When you add a language, Xcode offers to create localized versions of your storyboards, XIBs, and string files.
2

Create Localizable.strings

The standard iOS localization file uses key-value pairs separated by equals signs, with each line ending in a semicolon. Place it in your Base.lproj folder for the source language.

Base.lproj/Localizable.strings
// Base.lproj/Localizable.strings

"welcome_title" = "Welcome to MyApp";
"login_button" = "Sign In";
"settings_label" = "Settings";
"greeting" = "Hello, %@!";    // %@ = string placeholder
"item_count" = "%d items";     // %d = integer placeholder
Missing semicolons cause silent failures — the file loads without error but translations are empty. Also ensure the file is added to your target's Copy Bundle Resources build phase, or it won't be included in the app bundle.
Common .strings Mistakes
// ❌ Common mistakes in .strings files:

// Missing semicolon — file loads but translations are empty
"welcome_title" = "Welcome"

// Unescaped quotes — causes parse error
"message" = "Click "here" to continue";

// ✅ Correct versions:
"welcome_title" = "Welcome";
"message" = "Click \"here\" to continue";
3

Migrate to String Catalogs (Xcode 15+)

String Catalogs (.xcstrings) are Apple's modern replacement for .strings files. They offer a visual editor in Xcode, automatic string extraction from your SwiftUI views, and built-in plural support.

Localizable.xcstrings
// Xcode 15+ String Catalog (Localizable.xcstrings)
// Xcode automatically extracts strings from your code
// and manages translations in a visual editor.

// In SwiftUI, strings are automatically localizable:
Text("Welcome to MyApp")
Text("Hello, \(userName)!")

// Mark strings explicitly:
let title = String(localized: "welcome_title")
String Catalogs store ALL languages in a single .xcstrings JSON file. In teams, this means frequent Git merge conflicts when multiple people add strings. Consider one catalog per module for large projects.
4

Use Localized Strings in SwiftUI and UIKit

SwiftUI's Text view auto-localizes string literals. UIKit uses NSLocalizedString. For iOS 16+, the modern String(localized:comment:) API provides a cleaner syntax with built-in compiler support.

ContentView.swift
import SwiftUI

struct ContentView: View {
    let userName: String

    var body: some View {
        VStack {
            // ✅ SwiftUI auto-localizes string literals
            Text("welcome_title")

            // ⚠️ This does NOT localize (String interpolation)
            // Text("Hello, \(userName)")

            // ✅ Use String(localized:) for dynamic strings
            Text(String(localized: "greeting \(userName)"))

            // ✅ UIKit style (works everywhere)
            let title = NSLocalizedString(
                "settings_label",
                comment: "Settings screen title"
            )

            // ✅ Modern API (iOS 16+)
            let modern = String(
                localized: "welcome_title",
                comment: "Main screen title"
            )
        }
    }
}
Text("Hello \(name)") silently fails to localize when name is a plain String variable. SwiftUI's string interpolation creates a LocalizedStringKey, but only specific types (Int, Double, etc.) are interpolated correctly. For String variables, use String(localized:) to build the localized string first.
5

Handle Plurals

iOS uses .stringsdict files for plural rules, supporting all CLDR plural categories: zero, one, two, few, many, other. String Catalogs handle plurals with a visual editor in Xcode — much simpler than writing stringsdict XML by hand.

Localizable.stringsdict
<!-- Localizable.stringsdict -->
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
    <key>items_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@count@</string>
        <key>count</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 in Swift:
String(format: NSLocalizedString("items_count", comment: ""),
       itemCount)
Languages like Arabic have 6 plural forms, Russian has 3, and Japanese has 1. Always define all CLDR categories your target languages need. String Catalogs make this easier with their visual plural editor.
6

Localize App Store Metadata with Fastlane

Use Fastlane's deliver tool to keep App Store metadata — app name, subtitle, description, keywords, release notes — version-controlled in your repository as plain text files organized by locale.

Terminal
# Install Fastlane
$ gem install fastlane

# Initialize deliver for App Store metadata
$ fastlane deliver init

# Directory structure created:
# fastlane/metadata/
# ├── en-US/
# │   ├── name.txt            # App name (30 chars)
# │   ├── subtitle.txt        # Subtitle (30 chars)
# │   ├── description.txt     # Full description
# │   ├── keywords.txt        # Search keywords (100 chars)
# │   ├── release_notes.txt   # What's New
# │   └── promotional_text.txt
# ├── de-DE/
# │   └── ...
# └── ja/
#     └── ...

# Push metadata to App Store Connect:
$ fastlane deliver
Cross-localization: the US App Store indexes both English and Spanish keywords. Localizing your metadata to Spanish captures searches from Hispanic US users without targeting a separate market.
7

Test Your Localization

Test localized content without changing your device language. Use Xcode scheme overrides to run the app in any language, SwiftUI previews with locale environment, and XCUITest with launch arguments for automated testing.

Testing Localization
// 1. Xcode Scheme Override:
// Edit Scheme > Run > Options > App Language > Choose language

// 2. SwiftUI Preview with locale:
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environment(\.locale, Locale(identifier: "de"))

        ContentView()
            .environment(\.locale, Locale(identifier: "ja"))

        ContentView()
            .environment(\.locale, Locale(identifier: "ar"))
    }
}

// 3. XCUITest with language override:
let app = XCUIApplication()
app.launchArguments += ["-AppleLanguages", "(de)"]
app.launchArguments += ["-AppleLocale", "de_DE"]
app.launch()
Test with German (strings expand ~30% compared to English) and Japanese (strings shrink ~50%) to catch layout issues early. Use Xcode's pseudolocalization to stress-test layouts without real translations.

Automate Translation Quality

Catch missing keys, broken placeholders, and plural issues before they ship with i18n-validate. Test your UI with pseudo-translations using i18n-pseudo before real translations arrive.
8

Automate Translations

Translate your .strings, .xcstrings, and Fastlane metadata files using AI. Automate translation of both in-app strings and App Store metadata for a fully localized presence.

Terminal
# Translate .strings files
> Translate Base.lproj/Localizable.strings
  to Japanese, German, and Spanish

# Translate App Store metadata too
> Translate fastlane/metadata/en-US/
  to de-DE, ja, es-MX

✓ 6 files translated in 3.2s
A localized App Store presence increases downloads 30%+ in non-English markets. Translate your metadata alongside your app strings — it's the highest-ROI localization you can do.
+

Bonus: Smart Locale Fallback with LocaleChain

By default, iOS falls back to your development language when a user's exact locale isn't available. A pt-BR user with only pt-PT translations sees English instead of Portuguese. LocaleChain fixes this with configurable fallback chains.

LocaleChain is an open-source Swift package. View on GitHub

Package Dependencies
// Swift Package Manager
// File > Add Package Dependencies >
// https://github.com/i18n-agent/ios-localechain.git
MyApp.swift
import LocaleChain

// In your App init or AppDelegate:
LocaleChain.configure()  // Activates all default chains

// pt-BR user with only pt-PT translations?
// → Shows Portuguese instead of falling back to English

// Custom overrides for your specific locales:
LocaleChain.configure(
    overrides: ["es-MX": ["es-419", "es"]]
)

Common Pitfalls

.strings File Syntax Errors

Missing semicolons, unescaped quotes, or incorrect encoding cause silent failures. The file loads but translations appear empty. Always validate .strings files before committing.

SwiftUI Text Interpolation Not Localizing

Text("Hello \(stringVar)") doesn't localize as expected. Use String(localized:) for computed strings, or ensure interpolated variables are the correct type for LocalizedStringKey.StringInterpolation.

Widgets/Extensions Show Raw Keys

App extensions have separate bundles. Ensure your .strings or .xcstrings files are added to the extension target's Copy Bundle Resources phase, not just the main app target.

Missing Translations Show Keys in Production

When a key has no translation for the user's language, iOS shows the key itself. Use a fallback language strategy and test all supported locales before release.

Recommended File Structure

Project Structure
MyApp/
├── MyApp.xcodeproj
├── MyApp/
│   ├── Base.lproj/
│   │   ├── Localizable.strings       # Source strings
│   │   └── Localizable.stringsdict   # Plural rules
│   ├── en.lproj/
│   │   └── Localizable.strings
│   ├── de.lproj/
│   │   └── Localizable.strings
│   ├── ja.lproj/
│   │   └── Localizable.strings
│   ├── Localizable.xcstrings          # OR String Catalog
│   └── Info.plist
├── MyAppTests/
├── fastlane/
│   ├── Fastfile
│   └── metadata/
│       ├── en-US/
│       │   ├── name.txt
│       │   ├── description.txt
│       │   └── keywords.txt
│       ├── de-DE/
│       └── ja/
└── Package.swift

Try i18n Agent Now

Drop your translation file here

JSON, YAML, PO, XML, CSV, Markdown, Properties

or click to browse

Target languages

No signup requiredInstant estimate

Locale Fallback with ios-localechain

When a translation key is missing in a regional locale like de-AT, iOS jumps straight to the development language instead of checking the parent locale de first.

Terminal
// Swift Package Manager
// https://github.com/i18n-agent/ios-localechain
Configuration
import LocaleChain

LocaleChain.configure(overrides: [
    "de": ["en-GB", "en"],
    "pt-BR": ["pt", "en"],
    "zh-Hant-HK": ["zh-Hant", "zh", "en"],
])

See our Locale Fallback Guide for the full list of supported frameworks and 75 built-in chains. Learn more →

iOS Localization FAQ