App locale mapping breaks when a team treats every language identifier as the same kind of value. The app may use zh-Hans. An Android resource folder can encode language and region another way, while a storefront may accept only a fixed code. A translation management system may use its own label. String replacements between those values can upload metadata to the wrong locale, select the wrong script, or create duplicate translation records. Use one canonical locale registry and translate its values only inside the adapters that talk to external systems. The sections below cover the registry, ambiguous input, conversions in both directions, failure handling, and release verification.
Why locale identifiers split across systems
A locale identifier is not merely a language name. It can carry a language, script, region, and other subtags. zh-Hans specifies Chinese written with the Simplified script, while pt-BR identifies Portuguese associated with Brazil. Removing those distinctions because two values begin with the same two letters can change the content a user receives.
Unicode defines the syntax, validity, and canonicalization model for language and locale identifiers. It is a suitable basis for internal identifiers, but external platforms still expose their own supported sets, API fields, and resource names.
Apple publishes a finite table of App Store localization codes for localized metadata. Android selects resources through configuration qualifiers, including language and region, as described in its guide to providing alternative resources. Google Play has a separate workflow for translated store listings. These are related surfaces, not one shared namespace.
Developers still ask whether BCP 47 is replacing older locale code conventions. A release pipeline should not depend on each engineer remembering the convention used by every API.
Use one canonical registry, not scattered conversions
Choose one internal identifier for each supported content variant. BCP 47 compatible tags are the practical choice because they can preserve language, script, and region without binding the product model to one vendor. Then record every external representation beside that canonical identifier.
A minimal registry can look like this:
locales:
- id: en-US
language: en
region: US
android: en-rUS
app_store: en-US
google_play: en-US
tms: en_US
- id: pt-BR
language: pt
region: BR
android: pt-rBR
app_store: pt-BR
google_play: pt-BR
tms: pt_BR
- id: zh-Hans
language: zh
script: Hans
android: b+zh+Hans
app_store: zh-Hans
google_play: zh-CN
tms: zh_CN
The external values above illustrate the schema. They are not a universal platform table, so populate them from the APIs and project configuration you use. Store the mappings as reviewed data instead of hiding replacements inside upload jobs.
Give each entry a stable internal id. Code, translation jobs, review state, metrics, and release manifests should refer to that value. An adapter may translate the value into a storefront code for one request, but it must not rewrite the canonical identity stored elsewhere.
Suppose a store uses a region-shaped code while the product distinguishes scripts. The adapter can record that exception without making the rest of the application use a lossy alias.
Define what deserves a separate locale
Do not add a region or script merely because a platform permits it. Create a separate canonical locale when the content, terminology, legal copy, formatting policy, support process, or storefront asset genuinely differs. Deciding which locales earn a registry entry is a separate question from mapping them, and choosing app locales from real store data covers that step.
Use these decision rules:
- Preserve a script subtag when script changes which translation is correct.
- Preserve a region when vocabulary, regulation, pricing context, or approved content differs.
- Use a language-only locale only when one reviewed translation is intentionally shared.
- Do not derive one script from a country code unless the product has an explicit, reviewed rule.
- Never collapse an unknown variant to the nearest familiar locale during an upload.
Silent approximation can make the pipeline look healthy while it moves content to the wrong destination. A rejected mapping is visible and recoverable. An upload to the wrong locale can remain unnoticed until a customer or reviewer finds it.
Put mapping at adapter boundaries
Pass a canonical locale into each integration and require either one exact external identifier or a typed error. Keep the mapper beside the adapter that owns the external contract.
For example, an App Store adapter should know Apple's accepted localization codes because Apple publishes that supported set in the App Store Connect reference. The translation service should not contain a generic toAppleLocale() helper if it otherwise has no responsibility for App Store uploads.
Use separate functions for each direction:
mapToExternal(system, canonicalLocale) -> externalLocale
mapFromExternal(system, externalLocale) -> canonicalLocale
Define both operations for every supported registry entry. When mapToExternal succeeds, the reverse lookup should return the same canonical identity. An external system may combine several canonical variants into one destination. Record that as an intentional many-to-one policy, then block automatic reverse import until the source identity can be resolved.
Do not implement this with chained calls such as replacing underscores with hyphens, lowercasing the result, and taking the first two characters. Case and separators can be normalized for lookup, but normalization cannot invent missing script or region semantics.
Separate normalization, validation, and mapping
Keep these three operations separate:
- Normalization produces a consistent spelling for comparison.
- Validation checks whether a tag is structurally valid and supported by the product.
- Mapping converts one supported product identity to an external system's accepted value.
Run them in that order. First parse untrusted input into a normalized identifier. Then require an exact registry entry or an explicitly approved alias. Only after that should an adapter request an external mapping.
Maintain aliases only for known historical inputs. A project might accept pt_BR from an older translation export and normalize it to the canonical pt-BR. Document and test that alias, and limit it to the import boundary. Store the canonical value in every new record.
Unicode's locale identifier guidance distinguishes valid identifiers and canonical forms. Use a standards-aware parser where possible rather than maintaining a homegrown regular expression as the sole validator. Product support remains a second check: a well-formed tag is not automatically a locale your app ships.
Build the mapping into the release workflow
Route every localization path through the registry. The same sequence should govern UI resources, store metadata, screenshots, release notes, and translation jobs:
- Read the canonical locale from the release manifest.
- Verify that the locale is enabled for the product and artifact type.
- Resolve the required external identifier through the owning adapter.
- Confirm that the destination system reports support for that identifier.
- Upload or export the artifact with both identities in the audit log.
- Read the destination record back when the API allows it.
- Compare the returned external code and artifact revision with the planned mapping.
Android resource packaging needs one more check. Android documents how configuration qualifiers affect alternative resource selection. Generate resource paths from the registry rather than asking feature teams to type folder names. Reject a malformed or unsupported qualifier before the application build, not after a device chooses the default resource.
Google explains how developers can add translations for Play Store listings, while Apple exposes its own localization list. Put the canonical app locale and its listing locale for each store in the release manifest. Record intentional omissions too, so a missing storefront destination is not mistaken for an incomplete configuration.
Handle missing and conflicting mappings explicitly
Use typed failure categories so the release owner knows what to fix:
invalid_locale: The input cannot be parsed as an accepted identifier.unsupported_product_locale: The identifier is valid but not in the product registry.missing_external_mapping: The product supports the locale, but this adapter has no destination.unsupported_external_locale: A mapping exists locally, but the platform no longer accepts it.ambiguous_reverse_mapping: One external code could refer to several canonical variants.mapping_drift: The platform response differs from the registry or release manifest.
Do not fall back to English during metadata upload just to complete a job. Runtime fallback can keep an interface usable. A release integration has a different job: place an approved artifact in the intended destination or stop with a precise error.
For batch jobs, fail the affected locale and preserve successful results without marking the whole batch complete. The retry should target the failed locale and artifact revision. That keeps a mapping correction from re-uploading unrelated content.
Test round trips and real destinations
Unit tests should cover every registry entry, not a sample. Generate cases from the registry so adding a locale automatically expands the test set.
Require these assertions:
- Every canonical ID is unique.
- Every external value is unique unless a reviewed many-to-one rule exists.
- Every enabled integration has a mapping for every required locale.
- Forward mapping returns the expected external value.
- Reverse mapping returns the original canonical ID.
- Unsupported and ambiguous values return the expected typed error.
- Aliases normalize to one canonical value and are never emitted as new output.
Add integration tests against staging or read-only platform endpoints. Compare Apple's published localization code list and platform responses with the registry. For Android, build the app and inspect the packaged resources for each generated qualifier. Select representative language, script, and region combinations on a test device, then confirm that Android loads the intended resource.
Include at least one script-sensitive locale, one region-sensitive locale, one language-only locale, and one unsupported locale in release tests. Also test import and export independently. A pipeline can upload correctly while a later import creates a duplicate because reverse mapping uses different logic.
Audit changes as release-impacting configuration
A locale mapping change can redirect every artifact for a language. Review the change like code, with a pull request, a named owner, test output, and a migration note when an existing canonical ID changes.
Avoid changing canonical IDs in place. Add the new ID, define an explicit migration from existing records, update external mappings, and verify references before retiring the old value. Translation history, review approvals, analytics, and release manifests may all depend on the original identity.
Log both sides of each boundary without logging customer content unnecessarily:
release=2026.07.30 artifact=store_description
canonical_locale=zh-Hans external_system=app_store
external_locale=zh-Hans mapping_revision=18 result=accepted
A mapping revision makes incident review easier. If a locale starts receiving the wrong artifact, the team can identify whether content changed or only the identifier routing changed.
Common mistakes to reject in review
Reject a mapping implementation if it does any of the following:
- Uses a two-letter language code as the universal key.
- Infers script from region without a documented product rule.
- Stores a vendor's locale code as the product identity.
- Applies string replacement without an allowlisted registry entry.
- Maps unknown input to the default locale during export.
- Tests only forward conversion.
- Lets two adapters maintain conflicting copies of the same mapping.
- Adds a locale to translation jobs without checking app resources and storefront support.
These shortcuts work for simple pairs such as en and de, which is why they survive early testing. They break when the product adds scripts, regional variants, platform-specific identifiers, or imports from older systems.
Next action
Create a registry containing every locale in the next app release. Add columns for the canonical ID, Android resource representation, App Store code, Google Play code, and translation system code. Mark missing values as errors or intentional exclusions, never blank assumptions. Generate forward and reverse tests from that registry, then run one dry release for a script-sensitive locale and one region-sensitive locale. Do not enable automated publishing until both round trips return the original canonical identity.
References
- Unicode: Language and locale identifiers supports identifier structure, validity, and canonicalization guidance.
- Apple: App Store localizations provides the accepted localization codes for App Store metadata.
- Android Developers: Providing alternative resources explains configuration qualifiers and Android resource selection.
- Google Play: Translate and localize your app documents translated store listing workflows.
- Stack Overflow: Language locale codes and BCP 47 adoption provides practitioner evidence of uncertainty around competing locale-code conventions.
