Xcode String Catalogs, translated by AI with your team
FlutterLocalisation now speaks native iOS. A project can ship a Flutter app, a Swift app, or both, and for the Swift side the file format is the one Xcode 15 introduced: the String Catalog, Localizable.xcstrings. Import the catalog Xcode made, translate every language in one click, review with your team, and pull the catalog back in Xcode's own format. This post walks through the whole loop on a real app and says plainly what is and is not supported.
Why String Catalogs
Before Xcode 15, an iOS app carried one Localizable.strings per language plus a .stringsdict for plurals. Xcode 15 folded all of that into a single JSON file per table. One Localizable.xcstrings holds every key, its comment, and for each language the value and a review state (translated, needs_review, new, stale). Plurals are "variations" inside the same entry.
That single file is good news for tooling. It is JSON, it is deterministic (Xcode sorts keys and uses a fixed layout), and the build step xcstringstool compile turns it into the .strings and .stringsdict files the runtime still expects. If your project still has .strings files, Xcode migrates them in one click: select the file, then Edit, Convert, To String Catalog.
What shipped
A project now has a platforms setting: Flutter, iOS, or both. With iOS on:
- The dashboard imports a
.xcstringsfile. No language picker is needed, the catalog names its own. - Every language is rendered into one
Localizable.xcstrings, plus anInfoPlist.xcstringswhen the project holds Info.plist keys such asNSCameraUsageDescriptionorCFBundleDisplayName. Those keys are recognised by name. - The export matches Xcode's own serialisation: sorted keys,
:spacing, empty objects on three lines. Xcode opens the file, compiles it and leaves it alone, so a build never rewrites it and Git never shows a spurious diff. - The Git sync pushes the catalog like it pushes ARB files, into the folder you point it at.
- The CLI (
flutter_localisation_cli1.10 and later) hasfl import file.xcstrings,fl pullandfl export, and a config keyxcstrings_dir. The MCP server'simport_arbandpull_arbstools take catalogs too.
The first real client is Gush, a native SwiftUI app with 75 keys in 23 languages. Its catalog lives in Gush/Resources, and after a fl pull the app builds with xcodebuild without touching the file.
The loop, end to end
1. Create the project and import the catalog
In the dashboard, create a project, pick iOS (or Flutter + iOS) as the platform, and import Localizable.xcstrings. Keys, comments, every language and every review state come in as they are. Or seed it from the terminal:
dart pub global activate flutter_localisation_cli
fl login --token flk_live_…
fl import Gush/Resources/Localizable.xcstrings
The CLI needs a Dart SDK on the machine, which is the one thing a Swift developer may not have yet. brew install dart covers it.
2. Translate and review
Pick the languages you want and hit translate. The AI keeps %@, %lld and numbered placeholders in place and produces plural forms per language. A translator you invite sees only their language and the review state Xcode will see.
3. Pull the catalog into Xcode
Put a flutterlocalisation.json next to your .xcodeproj:
{
"project": "Gush",
"xcstrings_dir": "Gush/Resources"
}
Then:
fl pull
It writes Gush/Resources/Localizable.xcstrings (and InfoPlist.xcstrings when there are Info.plist keys) byte for byte as the backend serves them. Build, and String(localized: "welcome_title") resolves in every language.
If the repo is linked to GitHub, GitLab or Bitbucket, you can skip the pull: the sync commits the catalog for you, and a change you make in Xcode and push is imported back.
What maps to what
The dashboard keeps strings in the ICU shape Flutter uses, so a project can feed both platforms from one set of keys. For the catalog, the converter maps:
| In the dashboard | In the String Catalog |
|---|---|
Text placeholder {name} |
%@, numbered %1$@ when a string has several |
| Integer placeholder | %lld |
| Decimal placeholder | %lf |
Plural with one, few, many, other |
Plural variations per CLDR category |
=0 arm |
Only kept where the language has a zero category |
| Info.plist keys | InfoPlist.xcstrings |
Importing goes the other way: %@ becomes a named argument, %lld an integer one, and the original specifier is kept so an export reproduces it exactly.
A plural arm a language lacks falls back to other and is flagged needs_review, so nothing ships silently wrong.
What is refused on purpose
Three String Catalog features have no clean ICU equivalent, and two ICU features have no catalog equivalent. Rather than guess, the import and export refuse them with a message that names the key:
- ICU select (gender switches). Catalogs have no select.
- Substitutions, Xcode's way to put several plural arguments in one string.
- Device variations (one text for iPhone, another for iPad).
- In a project that also ships Flutter, keys that are not valid identifiers, since Dart needs them as method names. An iOS-only project can use sentence keys.
If one of those matters to you, write to support and tell us which. The list is short on purpose and it can grow.
What does not change for Flutter users
Nothing. ARB export, type-safe codegen, live over-the-air updates and the rest work as before. Over-the-air updates stay a Flutter package feature: a native Swift app reads its strings from the compiled catalog, so a copy change ships with the next build.
Try it on your own catalog
Not ready to create an account? Drop your file into the free String Catalog viewer. It runs in the browser, shows coverage per language, lists what is missing or needs review, and diffs two versions of a catalog. Nothing is uploaded.
When you are ready, create a free iOS project and import the same file. The free plan covers 500 keys, which is most indie apps. The full product page for iOS is at /ios.