What a String Catalog is
A String Catalog lives in your Xcode project (typically Localizable.xcstrings) and is edited visually through the built-in String Catalog editor. On disk, though, it is plain UTF-8 JSON — so it reads cleanly in code review, merges in git, and can be processed by tooling without a special parser.
One .xcstrings file holds the source strings and every language's translations together. There is no separate file per locale the way en.lproj/Localizable.strings and de.lproj/Localizable.strings used to split them.
Top-level structure
Every String Catalog has three top-level keys:
{
"sourceLanguage" : "en",
"strings" : {
"Hello, %@!" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hallo, %@!"
}
}
}
}
},
"version" : "1.0"
}
sourceLanguage — the development language the keys are written in (e.g. "en").
strings — a dictionary keyed by each source string (or an explicit key you assigned). Each entry describes one localizable string.
version — the catalog format version, currently "1.0".
Inside a string entry
Each entry under strings can carry:
extractionState — where the string came from. "extracted_with_value" means Xcode found it in your source at build time; "manual" means you added it by hand; "migrated" means it came from a converted .strings file; "stale" means the string is still in the catalog but is no longer found in code.
comment — the developer comment shown to translators (the comment argument you pass to String(localized:)).
shouldTranslate — set to false to mark a string as source-only.
localizations — a dictionary keyed by language code, holding the actual translations.
Within a language, the translation is stored in a stringUnit with a value and a state. The state field is per-translation and drives the status shown in Xcode:
state |
Meaning |
new |
String exists but this language has no translation yet |
translated |
Translation present and accepted |
needs_review |
Source changed after translating; verify the value |
A "Stale" status also appears in the editor, but it is driven by the per-string extractionState above — not by a stringUnit state. Xcode marks unused keys stale rather than deleting them, so no translation is lost by accident.
Unit strings vs. "Vary by Plural"
A simple string uses a single stringUnit. When a string needs grammatical plural forms — the modern replacement for .stringsdict — the localization holds a variations object with a plural key instead. Each CLDR plural category (zero, one, two, few, many, other) gets its own stringUnit:
"%lld items in cart" : {
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : { "state" : "translated", "value" : "%lld item in cart" }
},
"other" : {
"stringUnit" : { "state" : "translated", "value" : "%lld items in cart" }
}
}
}
}
}
}
This is what "Vary by Plural" produces in the String Catalog editor. A parallel device variation exists for device-specific wording. The required plural categories differ by language — Russian needs one/few/many/other, Japanese needs only other — and each language's variations block reflects that.
How extraction works
You do not usually type strings into the catalog by hand. When you build, Xcode scans your code for String(localized:), SwiftUI Text(...), and NSLocalizedString(...) calls and writes any new keys into the source language's strings with extractionState set to "extracted_with_value". Keys that disappear from code are marked "stale" rather than deleted, so nothing is silently dropped.
.xcstrings vs. legacy .strings / .stringsdict
|
.strings / .stringsdict |
.xcstrings |
| Format |
Key/value text + plist XML |
Single JSON document |
| Files per language |
One .strings per locale |
One catalog, all locales |
| Plurals |
Separate .stringsdict |
Inline variations.plural |
| Per-string status |
None |
state per translation |
| Editing |
Text editor |
String Catalog editor |
The legacy formats still work, and Xcode can migrate a .strings file to a catalog (entries then carry extractionState "migrated"). New targets default to .xcstrings.
Keeping the catalog as your source of truth
Because the catalog tracks state per string, the reliable workflow is to leave .xcstrings in your repo as the single source of truth and never hand-edit foreign-language values. To translate, export an .xcloc bundle from Xcode (Editor > Export Localizations, or xcodebuild -exportLocalizations) — the bundle wraps XLIFF 1.2 alongside the catalog data. Run that native bundle through strings.dev, which returns the same .xcloc format with plural variations and placeholders like %@ and %lld preserved, then re-import (Editor > Import Localizations). Xcode folds the translations back into the catalog and flips each state to translated.
The free Indie tier covers one project in one language with unlimited words; adding more target languages, brand context, and the QA & analytics dashboard is Indie Plus at $20/mo, or $10/mo billed annually.