Your App's Locale Codes Aren't Fastlane's Locale Codes
Published 10 September 2026
Quick answer: In-app localisation and App Store Connect metadata use different locale identifiers.
Localizable.xcstringsusesde,fr,es,ar;fastlane/metadata/needsde-DE,fr-FR,es-ES,ar-SA. A barede/directory makesdeliverabort, and it aborts after uploading the locales it did understand.
You add German to an app. In the String Catalog it's de. So you create fastlane/metadata/de/, fill in the description and keywords, run the metadata lane, and it fails.
The reason is that these are two unrelated systems that happen to both call their identifiers "locales". Xcode uses language codes. App Store Connect uses its own list, and several entries are region-qualified.
The mapping that catches people
| Language | In-app (xcstrings) |
Metadata (fastlane/metadata/) |
|---|---|---|
| German | de |
de-DE |
| French | fr |
fr-FR |
| Spanish | es |
es-ES |
| Arabic | ar |
ar-SA |
| Portuguese (Brazil) | pt-BR |
pt-BR |
| Japanese | ja |
ja |
| Korean | ko |
ko |
| Chinese (Simplified) | zh-Hans |
zh-Hans |
The inconsistency is the trap. Japanese and Korean are bare, German and French are not, and there's no principle to derive it from — you either know or you find out during a release.
Also note es-ES is Spain specifically; Latin American Spanish is es-MX, a separate storefront locale. And en-US and en-GB are distinct, which matters if your copy uses "color" or "colour".
Why the failure is annoying
deliver validates locale directories as it goes rather than up front. So an unsupported directory aborts the lane after it has already uploaded the locales it understood.
You're left in a partial state: some locales updated, some not, and a lane you have to re-run. Nothing is broken, but "did that upload or not?" is a question you now have to answer by looking at App Store Connect.
Check before you run
A three-line guard beats reading the error:
VALID="ar-SA ca cs da de-DE el en-AU en-CA en-GB en-US es-ES es-MX fi fr-CA fr-FR he hi hr hu id it ja ko ms nl-NL no pl pt-BR pt-PT ro ru sk sv th tr uk vi zh-Hans zh-Hant"
for d in fastlane/metadata/*/; do
loc=$(basename "$d")
case " $VALID " in
*" $loc "*) ;;
*) echo "unsupported metadata locale: $loc" ;;
esac
done
Skip the non-locale directories your setup uses — review_information, trade_representative_contact_information and similar live alongside the locales and aren't locales.
While you're checking, check the lengths
The other thing that fails an upload late is a field one character over its limit. The limits worth guarding:
- App name — 30 characters
- Subtitle — 30
- Keywords — 100, commas included
- Promotional text — 170
- What's New — 4,000
One over and the upload is rejected, per locale, after other locales have gone up.
A detail that costs a confusing minute: wc -m counts the trailing newline. A 30-character name in a file reports 31. Strip it before comparing, or you'll "fix" a field that was already fine:
printf '%s' "$(cat fastlane/metadata/en-GB/name.txt)" | wc -m
The general shape
Both of these are the same class of problem: a validation that happens remotely, per-item, partway through a batch. The fix is the same too — validate locally before you start, because the local check is instant and the remote one costs you a partial upload and a rerun.
Worth putting in a preflight lane so it runs on every release rather than when someone remembers.
Related posts
- xcstringstool sync: The String Catalog Repair Tool Nobody Mentions How to add missing keys, drop orphans, and rewrite a mangled Localizable.xcstrings back into Xcode's canonical format — from the command line, without losing translations.
- SwiftData Enum Predicates: Store a Queryable Raw Value SwiftData can persist Codable enums, but current #Predicate expressions cannot compare enum cases. Store a raw value when you need filtering or sorting.
- CloudKit Rejects @Attribute(.unique) — Model Around It SwiftData's unique constraint is incompatible with CloudKit sync. Why there's no workaround, and how to enforce uniqueness without it.