Guides 17 min read
iOS localization and internationalization (i18n): from Localizable.strings to String Catalogs
A practical guide to iOS localization and internationalization: i18n vs l10n, migrating Localizable.strings and .stringsdict to String Catalogs (.xcstrings), the Swift i18n APIs, plurals, permission strings, formatters, right-to-left layouts, pseudolanguages, XLIFF export, localization tools and App Store metadata.
Contents19 sections
iOS localization (l10n) adapts an app’s text, formats and App Store page to each language and region; iOS internationalization (i18n) is the engineering that makes this possible, by keeping strings out of the code, formatting with the user’s locale and building layouts that grow and mirror. In practice, you internationalize once in Swift, keep every user-facing string in Xcode String Catalogs (Localizable.xcstrings, which replaces Localizable.strings and .stringsdict, and InfoPlist.xcstrings), let Xcode extract them, test with pseudolanguages, then translate with machine translation for speed and human review where it matters. Finish with the App Store product page, which is localized separately in App Store Connect. This is how Develak ships its own apps in up to 44 languages.
Key takeaways
- Internationalization is done once in the code; localization is repeated for every language.
- String Catalogs (
.xcstrings, Xcode 15 and later) replace.stringsand.stringsdict: one file per table, with plurals and device variations built in. Xcode migrates legacy files table by table. - Never build sentences by concatenation or format dates and numbers by hand.
- Localize
InfoPlist.xcstringstoo: permission prompts and the app name are part of the first impression. - Test with pseudolanguages before paying for a single translation.
- Localize the App Store product page (name, subtitle, keywords, description, screenshots) for each market.
- Machine translation, LLMs included, is a good first pass; have people review the strings that sell, charge or warn.
Why localize an iOS app?
The App Store is available in 175 countries and regions, and people search, read reviews and decide in their own language. A localized app with a localized product page competes for local keywords and feels trustworthy in markets where most English-only apps don’t bother. Users can also choose a language per app in iOS Settings, so each language you add is used by people who explicitly want it.
Once the process exists, each extra language costs far less than the first. Our most recent apps, such as the seizure diary Seizly and the lap timer ApexLap, ship in 44 languages, including Arabic, Hebrew and Urdu (right-to-left) and Indian languages such as Hindi, Bengali, Tamil and Telugu; the pill reminder Pillio ships in 33. That is only sustainable because the steps below are routine.
Internationalization (i18n) vs localization (l10n) on iOS
The numerals count the letters between the first and last ones. Apple’s guide to supporting multiple languages puts them in order: you first internationalize by preparing code and assets for other languages and regions, then localize by translating strings and varying assets. Concretely, an internationalized Swift or SwiftUI app passes this checklist:
- No hard-coded strings: every user-facing text goes through
Text,String(localized:)orLocalizedStringResource, with a comment when the meaning is ambiguous. - Locale-aware formatters: dates, numbers, currencies and units use
FormatStyle, never fixed patterns or symbols. - Layouts that expand: SwiftUI stacks or Auto Layout constraints without fixed widths, multi-line labels and Dynamic Type, so longer German or Finnish text fits.
- Right-to-left ready: leading and trailing instead of left and right, and directional images that mirror (see below).
- Grammar in the catalog: plurals as catalog variations, never
count == 1tests in code. - Resources in the bundle: localizable images, colors and sounds in asset catalogs, strings looked up in the right bundle (
Bundle.modulein packages), and no text baked into images.
String Catalogs in five minutes
A String Catalog is a JSON file with the .xcstrings extension, introduced in Xcode 15, that stores every string of a table and all its translations. Add one named Localizable to your target and build: Xcode extracts strings from SwiftUI views, String(localized:), LocalizedStringResource and NSLocalizedString. Each entry shows a state (New, Needs Review, Translated, Stale) and a comment for translators. Existing .strings and .stringsdict files can be migrated, as described in the next section.
// Extracted automatically into Localizable.xcstrings
Text("Take your medication")
// A comment gives translators the context they need
Text("Open", comment: "Button: opens the selected bottle's details")
// Outside SwiftUI views
let title = String(localized: "Cellar", comment: "Tab title for the list of bottles")Comments are the cheapest quality improvement in localization: “Open” can be a verb or an adjective, and “Archive” a noun or an action. Xcode 26 can also draft translator comments with an on-device model and generate type-safe Swift symbols for strings you add manually to the catalog. In Swift packages, set defaultLocalization in Package.swift and look strings up in Bundle.module; with Xcode 26, the #bundle macro resolves the right bundle for you in an app, a framework or a package.
Localizable.strings vs String Catalogs (.xcstrings): migrate step by step
The legacy format: .strings and .stringsdict
Before Xcode 15, each language had its own Localizable.strings file in an .lproj folder (en.lproj, fr.lproj…), a list of key-value pairs maintained by hand or generated with genstrings (our Localizable.strings guide details the syntax, the lookup and the errors this format produces). Plurals needed a second file per language, Localizable.stringsdict, a property list with one entry per plural category:
/* fr.lproj/Localizable.strings */
/* Button: opens the selected bottle's details */
"Open" = "Ouvrir";
"Welcome back, %@!" = "Bon retour, %@ !";
/* en.lproj/Localizable.stringsdict (excerpt) */
<key>%lld reminders</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@reminders@</string>
<key>reminders</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>one</key>
<string>%lld reminder</string>
<key>other</key>
<string>%lld reminders</string>
</dict>
</dict>Why String Catalogs replace them
Apple now calls String Catalogs the recommended way to localize plurals in Xcode 15 and later, and announced in its Discover String Catalogs session that the format will supersede .strings and .stringsdict. At build time, catalogs compile to .strings and .stringsdict files, so adopting them does not raise your minimum deployment target.
Localizable.strings + .stringsdict | String Catalog (.xcstrings) | |
|---|---|---|
| Files | One or two per language, in each .lproj folder | One per table, all languages inside |
| Keeping up with the code | Manual, or genstrings | Automatic on every build |
| Plurals | Separate property list, edited by hand | Vary by Plural in the same entry, with each language’s categories |
| Device-specific text | Not in the plural format | Vary by Device |
| Translation status | None | New, Translated, Needs Review, Stale, with progress per language |
| Format | Text and XML property list | JSON, readable in diffs |
| Requirements | Any Xcode version | Xcode 15 or later to build; deployment target unchanged |
Migration checklist
- Commit or branch first, so the migration is one reviewable diff.
- Check that the Use Compiler to Extract Swift Strings build setting is enabled, so Swift strings are extracted.
- In the Project navigator, Control-click
Localizable.stringsand choose Migrate to String Catalog (Xcode 15 or later). The migration assistant lists the.stringsand.stringsdictfiles it can migrate, by target; pick the tables you want. - Let Xcode build: in Apple’s demo, the translations and the
.stringsdictplurals end up in the catalog, plurals as variations of the matching key, while missing translations show up in each language’s progress. - Review keys marked Stale or Needs Review before deleting anything.
- Migrate the other tables, such as
InfoPlist.strings, at your own pace: catalogs coexist with legacy files. Leave no.stringsor.stringsdictfile with the same table name behind, including inBase.lproj. - Run the app in a pseudolanguage and in a language with complex plurals, such as Russian or Arabic.
Legacy code keeps working: the table name does not change, so NSLocalizedString and String(localized:table:) calls resolve as before, and Objective-C strings are extracted without extra settings. Swift packages need defaultLocalization in the manifest before you add a catalog.
Swift i18n basics: String(localized:), LocalizedStringResource and formatters
Internationalization (i18n) is the code side of the job: making every user-facing value localizable, so that localization (l10n) is only a matter of adding translations. In Swift, a handful of APIs cover almost every case, and choosing the right one decides whether a string reaches the catalog at all.
| API | Use it for | When the lookup happens | Available from |
|---|---|---|---|
Text("…"), Button("…") (LocalizedStringKey) | String literals in SwiftUI views | When the view renders | iOS 13 |
String(localized:) | Text built outside views: alerts, notifications, view models | Immediately, in the current locale | iOS 15 |
String(localized:defaultValue:) | An explicit key when the English word is ambiguous (“Call” as a noun or a verb) | Immediately | iOS 15 |
LocalizedStringResource | Values resolved later or in another process: App Intents, model properties | Deferred until you create a String or a Text | iOS 16 |
FormatStyle and .formatted() | Dates, numbers, currencies, percentages, measurements, lists | When formatting, with the user’s locale | iOS 15 |
// SwiftUI: a string literal is a LocalizedStringKey, looked up automatically
Text("Settings")
// A String variable is shown as-is, without any lookup
let label = "Settings"
Text(label) // not localized
Text(LocalizedStringKey(label)) // looked up; add the key to the catalog yourself
Text(verbatim: "Pillio") // never localized
// Outside views: resolved immediately, interpolation becomes a placeholder
let toast = String(localized: "Welcome back, \(name)!", comment: "Toast on launch")
// Explicit key for an ambiguous source word
let action = String(localized: "call.verb", defaultValue: "Call", comment: "Button: phone the pharmacy")
// Deferred: the system resolves it later, possibly with other language settings
struct LogDoseIntent: AppIntent {
static let title: LocalizedStringResource = "Log a dose"
func perform() async throws -> some IntentResult { .result() }
}
// Formatting follows the user's region
Text(Date.now, format: .dateTime.day().month(.wide))
Text(0.25, format: .percent)The most common Swift i18n bug is the second example: passing a String variable to Text silently bypasses localization, as Apple’s LocalizedStringKey documentation explains. Keep literals at the call site, or store LocalizedStringResource values in your models instead of plain strings. Apple describes LocalizedStringResource as a reference to a localizable string whose lookup you defer; App Intents rely on it so that Siri and Shortcuts can resolve text with their own language settings. To know which language the app is actually running in, read Bundle.main.preferredLocalizations rather than guessing from the device language.
Plurals and device variations
Write the sentence once with interpolation, then choose Vary by Plural on that key in the catalog. Xcode shows exactly the plural categories each language needs:
// Catalog key: "%lld reminders" → Vary by Plural
Text("\(count) reminders")| Languages | Plural categories | What goes wrong with “count == 1” |
|---|---|---|
| English, German | one, other | Nothing, which is why the bug stays hidden |
| Japanese, Chinese, Korean, Thai, Vietnamese | other | A needless singular form appears |
| Russian, Ukrainian, Polish | one, few, many, other | “5 reminders” takes the wrong ending |
| Arabic | zero, one, two, few, many, other | Four of six forms are wrong |
A string with two numbers (“%lld photos in %lld albums”) can vary on each one independently. Vary by Device handles wording that depends on the hardware, such as “Tap to start” on iPhone and “Click to start” on Mac.
InfoPlist.xcstrings: permission prompts and the app name
Add a second String Catalog named InfoPlist to the target and Xcode fills it with the localizable keys of Info.plist: the Home Screen name (CFBundleDisplayName) and every purpose string, from NSCameraUsageDescription and NSMicrophoneUsageDescription to NSHealthShareUsageDescription. A permission prompt is the moment you ask for trust, and an English prompt in a French app looks unfinished at exactly that moment. App Review also reads these strings under guideline 5.1.1, so their translations should stay precise.
Two more places are easy to forget: App Shortcuts phrases live in AppShortcuts.xcstrings, and widget extensions need their strings available in the extension target.
Dates, numbers and units: let FormatStyle do it
Date order, 12- or 24-hour clocks, decimal separators, currency placement, the first day of the week and the measurement system all change with the user’s region. Never hard-code "MM/dd/yyyy" or a currency symbol:
// Weekday and time, in the user's language and clock format
Text(dose.scheduledAt, format: .dateTime.weekday(.wide).hour().minute())
// Currency with the right symbol and separators
Text(price, format: .currency(code: "EUR"))
// Kilometers or miles, depending on the user's region
Text(distance, format: .measurement(width: .abbreviated, usage: .road))
// "Pillio, Seizly, and ApexLap" or "Pillio, Seizly et ApexLap"
Text(appNames.formatted(.list(type: .and)))Sort user-visible lists with localizedStandardCompare and search with localizedStandardContains, so accents and case behave as people expect.
Right-to-left languages: Arabic, Hebrew, Urdu
SwiftUI mirrors layouts automatically when you use leading and trailing instead of left and right. Directional SF Symbols flip on their own; for your own directional images, opt in explicitly. Do not mirror what isn’t directional: logos, photos, checkmarks. Check the result in a preview:
Image("swipe-hint")
.flipsForRightToLeftLayoutDirection(true)
#Preview("Arabic, right-to-left") {
ContentView()
.environment(\.locale, Locale(identifier: "ar"))
.environment(\.layoutDirection, .rightToLeft)
}Test before you translate: pseudolanguages
In the scheme editor (Run › Options › App Language), Xcode offers pseudolanguages that reveal localization bugs with no translation at all. Turn on “Show non-localized strings” in the same panel to spot hard-coded text.
| Pseudolanguage | What it catches |
|---|---|
| Double-Length | Truncated labels and clipped buttons, as with long German or Finnish strings |
| Accented | Clipped diacritics and tight line heights; hard-coded strings stay unaccented |
| Bounded String | Concatenated or truncated strings, since you see where each one starts and ends |
| Right-to-Left | Layouts that don’t mirror, with readable text |
| Right-to-Left with Right-to-Left Strings | The full right-to-left experience, text included |
UI tests can launch the app in any language, which is also how localized App Store screenshots can be automated:
let app = XCUIApplication()
app.launchArguments += ["-AppleLanguages", "(de)", "-AppleLocale", "de_DE"]
app.launch()Localize the App Store product page too
The binary and the product page are localized separately. The “Languages” line of your listing comes from the app bundle; everything else is set per localization in App Store Connect, which accepts metadata in 50 languages and locales:
- Name and subtitle (30 characters each), promotional text, description and What’s New.
- Keywords (100 bytes per localization, so accented letters and non-Latin scripts use up the allowance faster), researched in each language rather than translated: a French user looking for a pill reminder may type “pilulier”, a word no translation of “pill reminder” produces.
- Screenshots and app previews with translated captions; without them, the primary language’s are shown. Arabic and Hebrew screenshots should show the mirrored interface. Our App Store screenshot sizes guide lists the dimensions each localized set needs.
- In-app purchase display names, descriptions and subscription group names.
Localized keywords and screenshots are core App Store Optimization work, which is why our ASO service treats each market separately.
Human, machine or both?
Large language models now translate interface strings well when they get context: the translator comment, the surrounding screen, a glossary of product terms. They still stumble on register (formal or informal “you” in French, German or Spanish), gender agreement, length, and placeholders that must survive untouched. Our rule: machine translation for the first pass on everything, then human review for onboarding, the paywall and subscription terms, permission prompts, App Store metadata and anything medical, legal or safety-related. Brand names such as Pillio or Seizly are marked “Don’t translate”, and TestFlight builds go to native speakers before release.
Localization tools for iOS
- Xcode String Catalog editor: built in, with states, plural and device variations and, in Xcode 26, generated comments.
- Export and import (XLIFF): Product › Export Localizations creates an Xcode Localization Catalog (
.xcloc) per language, containing an industry-standard XLIFF file for translators or translation tools; Product › Import Localizations brings it back.xcodebuild -exportLocalizationsand-importLocalizationsdo the same in CI. genstrings: the legacy command-line tool that scansNSLocalizedStringcalls to generate aLocalizable.stringsfile, as described in Apple’s archived internationalization guide. String Catalogs make it unnecessary.- fastlane: open-source automation; deliver uploads localized metadata and screenshots to App Store Connect, and snapshot captures localized screenshots with UI tests.
- Xlocalize: our Mac app, which opens the
.xcstringsfile directly and translates it with AI, with no export or import step (details below). - Translation platforms and services: most accept XLIFF and many read
.xcstrings; check that plural variations and placeholders survive a round trip, and see when a service is worth it.
Our tool: Xlocalize for .xcstrings files
Localizing a catalogue of more than 30 apps pushed us to build our own Mac app. Xlocalize opens a .xcstrings file, lets you pick target languages and translates with OpenAI, Anthropic Claude or Google Gemini, using your own API key stored in the macOS Keychain, with no account required. It preserves plurals, device variations and substitutions, sends per-key comments as context, tracks each key as new, translated or “don’t translate”, and flags placeholder mismatches, untranslated copies and missing translations before they reach a build. It runs on macOS 14 or later and is free to download, with optional in-app purchases. The Xlocalize case study explains why we built it.
A localization workflow checklist
- Internationalize the code: no hard-coded strings,
FormatStyleeverywhere, leading and trailing layouts, flexible sizes and Dynamic Type. - Create
Localizable,InfoPlistand, if needed,AppShortcutscatalogs, then build to extract. - Add comments to ambiguous strings and mark brand names “Don’t translate”.
- Set up plural and device variations.
- Test with pseudolanguages and right-to-left.
- Translate: machine first pass, human review of critical strings.
- Check placeholders and truncation in every language on the smallest supported iPhone.
- Localize App Store metadata, keywords, screenshots and in-app purchase names.
- Ship to native speakers through TestFlight.
- Every release, translate new and stale strings before you submit.
Common pitfalls
- Concatenation.
"Hello " + namebreaks word order; use one localized string with interpolation. - One key, two meanings. Reusing “Open” for a verb and an adjective forces a bad translation in half the languages.
- Argument order. Translators may need to reorder values; positional specifiers such as
%1$@and%2$lldmake that possible. - Text inside images, which no String Catalog can translate.
- Fixed widths and single-line labels that collapse in German.
- Accessibility labels. VoiceOver reads
.accessibilityLabeltext aloud; it is extracted like any other string, so check that it is translated rather than left in English. - Server-side text: push notifications and e-mails need localization too, for example with
loc-keyin the notification payload. - Stale strings left untranslated after a redesign.
Do it yourself or use an iOS localization service?
Everything above can be done in-house with Xcode and a translation tool, and for two or three languages it usually should be. An outside iOS localization service starts to pay off when the number of languages grows faster than your ability to review them, when the app contains medical, legal or subscription wording that must not be approximated, or when each market needs its own keyword research rather than translated keywords. Check that a provider works directly in your .xcstrings files, tests the running app in each language rather than a spreadsheet, and treats the App Store listing as part of the job.
If you would rather hand the whole process to a team that does it every week, our app localization service covers String Catalog setup, translation and adaptation, layout and right-to-left checks, linguistic review on device and the localized App Store listing.
FAQ
What is the difference between localization and internationalization in iOS?
Internationalization (i18n) prepares the code once: no hard-coded strings, locale-aware formatters, layouts that expand and mirror, resources in the bundle. Localization (l10n) then adapts the app to each language and region: translations, plural forms, localized assets and the App Store product page. Without internationalization, localizing means rewriting code for every language.
What is an .xcstrings file?
An .xcstrings file is an Xcode String Catalog: a JSON file, introduced in Xcode 15, that stores every localizable string of a table with all its translations, plural forms, device variations, comments and translation states. It replaces the older .strings and .stringsdict files.
Should I still use Localizable.strings files?
Not for new work. Existing Localizable.strings and .stringsdict files keep working, and Xcode can migrate them to a String Catalog. Catalogs are a build-time format: Xcode compiles them into the classic .strings and .stringsdict resources inside the app, so you need Xcode 15 or later to build, but the app’s deployment target is not affected.
How do I migrate from Localizable.strings to a String Catalog?
In Xcode 15 or later, Control-click Localizable.strings in the Project navigator and choose Migrate to String Catalog, select the tables to convert, then build. Translations and .stringsdict plurals move into Localizable.xcstrings, and your code does not change because the table name stays the same. Our migration checklist covers the checks before and after.
What is the difference between String(localized:) and LocalizedStringResource?
String(localized:) (iOS 15 and later) looks the translation up immediately and returns a plain String in the current locale. LocalizedStringResource (iOS 16 and later) only stores the key, table, bundle and locale, and is resolved later, when you create a String or a Text from it. Use the first for text you display right away and the second for values that travel: model properties, App Intents, or anything resolved in another process.
How many languages should an iOS app start with?
Start with the languages of the markets you already see in App Store Connect analytics, plus the ones your competitors neglect. Once the code is internationalized and the workflow is in place, adding a language is mostly translation and review, which is why studios like ours can ship apps in 40 or more languages.
Can I translate my iOS app with ChatGPT, Claude or Gemini?
Yes, as a first pass. Give the model context (translator comments, a glossary, the screen the string appears on), use a tool that edits the .xcstrings file without breaking plurals or placeholders, such as Xlocalize, and have native speakers review onboarding, paywall, permission prompts and App Store metadata.
How do I localize my app’s name?
There are two names. The one under the icon on the Home Screen is CFBundleDisplayName, translated in InfoPlist.xcstrings. The one on the App Store is set per localization in App Store Connect, with a limit of 30 characters. Many apps keep the brand identical everywhere and localize only the descriptive part, such as the subtitle.
Building an iOS app?
Native Swift and SwiftUI, from the first sketch to the App Store and beyond.
Start a projectContents