Languages and locale codes
Product names, descriptions, category and attribute labels and every other translated catalog text sit in a map keyed by a locale code. Which code is the right key is not something a client decides. The tenant decides it once in its market settings, the markets app resolves it, and every writer and reader follows that answer.
Write under the write key, read along the read order, and never hard-code de, de-DE or de_DE.
This applies to catalog and search data: products, categories, attributes, options, families, reference entities, asset families and association types. Interface texts in a manifest (title, label in cockpit.json or settings.json), mail templates and value lists stay keyed by the bare language (en, de) and are not affected.
Market locale and storage key are two different things
| Example | Where it comes from | |
|---|---|---|
| Market locale | de-AT | A market registers it. It is what a storefront asks for: "render this page for Austria in German". Always language-COUNTRY. |
| Storage key | de-AT or de | The key a text is written under. It depends on the tenant's locale_granularity setting. |
In a tenant that keeps text per region the two are the same code. In a tenant that keeps text per language, every German-speaking market reads one de text, and the market locale only decides the read order.
The settings
All three are tenant-wide markets settings. Changing one moves no stored text (as of 2026-10); see Changing a setting later.
locale_granularity — how translations are kept
| Value | Write key for the market locale de-AT | Right when |
|---|---|---|
regional (default) | de-AT. de-DE, de-AT and de-CH are three separate texts. | The wording actually differs by country. |
language | de. One text every German-speaking market reads. | It does not, and you would rather maintain one German. |
regional is the default of the setting. Most tenants run language, so a client that assumes a regional key writes past the text everyone else reads.
locale_fallback — what a missing translation falls back to
| Value | After the exact key holds nothing, a read tries |
|---|---|
language (default) | The bare language: de-AT → de. |
default_locale | The market's default locale: en-GB → de-DE. |
none | Nothing. The gap shows, which is the honest answer when a missing translation must be noticed. |
fallback_locale — the tenant's main locale
A full code, default de-DE. It is the default_locale fallback wherever no market answers (the tenant-wide policy below), and its write key is the tenant's main language in search.
Write key and read order
Neither setting is handed to a client as a name to interpret. GET /v1/markets/{id}/context carries a locale_policy with, per locale, the key to write under and the keys to read in order. GET /v1/markets/locale-policy answers the same for the whole tenant: the union of every market's locales, each naming the markets that registered it.
{
"granularity": "language",
"fallback": "language",
"locales": [
{ "code": "de-AT", "language": "de", "write": "de", "read": ["de-AT", "de"], "markets": ["at"] }
]
}
A read always starts at the exact code. A fallback fills a gap and never outranks a text that is stored. That is why an Austrian override under de-AT still wins in a tenant that writes de.
locale_granularity | locale_fallback | Market locale | write | read |
|---|---|---|---|---|
regional | language | de-AT | de-AT | de-AT → de |
regional | default_locale | en-GB | en-GB | en-GB → de-DE |
regional | none | de-DE | de-DE | de-DE |
language | language | de-AT | de | de-AT → de |
language | default_locale | en-GB | en | en-GB → en → de-DE |
The default_locale rows assume a market whose default locale is de-DE. In the tenant-wide answer, which stands in no market, that fallback is the tenant's fallback_locale setting.
Read the policy once per request or cache it briefly. Do not derive it from the two setting names yourself: that is how one client reads de-DE while another wrote de, and the text looks missing to the first.
From an input to a write key
An import file or an integration often carries a locale in some other spelling. The platform normalises it and maps it to the tenant's write key. The same input gives different answers in different tenants:
| Input | Tenant per region, markets de-DE and en-US | Tenant per region, markets de-DE and de-AT | Tenant per language, markets de-DE and de-AT |
|---|---|---|---|
de-DE | de-DE | de-DE | de |
de_DE, de-de | de-DE | de-DE | de |
de, deu, ger | de-DE | ambiguous: choose de-DE or de-AT | de |
de-CH | not configured | not configured | de (the language is configured) |
pl | not configured | not configured | not configured |
DE, german, de-DEU | invalid format | invalid format | invalid format |
Three-letter codes are the ISO 639-2 codes BMEcat files use (deu, ger, eng, fra, …). They map to their two-letter language.
What the platform rejects
A key the tenant cannot read is worse than an error: it is stored, and no storefront ever shows it. So a write fails instead of guessing.
| Case | Example | Answer |
|---|---|---|
| Malformed key | DE, german, de-DEU as a key in labels or a locale bucket | 422, naming the key |
| Language no market configures | pl in a tenant with only German and English markets | 422, naming the languages the tenant has |
| Ambiguous language | de in a regional tenant with de-DE and de-AT | 422, naming the codes to choose from |
| A well-formed key that is not the write key | de-DE in a tenant that keeps text per language | accepted, and reported as drift (below) |
A stored key has the shape ll or ll-CC: two lower-case letters for the language, optionally a hyphen and two upper-case letters for the country. An import does the same check before it writes anything, so a file with an unresolvable language fails as a whole with the mapping it needs, instead of loading half a catalog. A catalog import for a tenant whose markets configure no locale at all fails the same way, instead of writing under a guessed de-DE.
Imports make these checks already. Across the products API they are being rolled out (as of 2026-10): until a route enforces them, it stores what you send, so a bare de that fits no or several regions stays de. Validate against the policy on your side, as described above.
Once the products API enforces them, a spelling that only differs in form (de_DE, de-de, deu) is normalised and stored under the write key instead of refused. Only a key that cannot be read as a locale (DE, german, de-DEU) fails as invalid_locale. The other refusals answer unknown_locale and ambiguous_locale, and two spellings of one key with different values in one write answer duplicate_locale. The error's details names the allowed keys (allowed) and, per offending key, its path, the reason and the candidates (keys). Direct writes to the table bypass the API and are held to the ll / ll-CC shape only.
Drift is reported, not rejected
A key that is well formed and belongs to the tenant, but is not the write key (de-DE in a tenant that writes de), is still readable through the read order, so it is not refused. Typically it comes from a customer pipeline that hard-codes a region. The platform counts such keys per entity every night, together with products that have no name in the default language, and Cockpit shows the result on the dashboard as a data-quality item. The nightly report is being rolled out (as of 2026-10).
Importing several languages
A BMEcat file can carry texts in several languages, and catalogs often arrive as one file per language. The import profile decides how an incoming language meets the languages already stored:
| Mode | What happens | Use it when |
|---|---|---|
merge (default) | Every value in the file overwrites its attribute or label. Everything else stays, including other languages. | Regular updates. |
replace_locales | The languages contained in the file are replaced as a whole. Languages the file does not contain are untouched. | A supplier sends one complete file per language. |
replace | The stored document is replaced by the file. | A full reload where the file is the only truth. |
The mode applies to product values and to category, attribute group and attribute labels. With merge or replace_locales, two files, one in de and one in en, never erase each other, whichever arrives first. Only replace loses the language a file does not carry.
A profile can also run as a dry run: the file is read, resolved and merged, then nothing is kept, and the job reports what it would have written along with warnings, for example a locale the file spells differently from the write key. When a file language does not resolve on its own, map it in the profile's BMEcat options ("locales": {"deu": "de-AT"}).
The BMEcat locale option resolves to the tenant's write key: see Import a BMEcat catalog.
Changing a setting later
Switching locale_granularity changes the key a client writes from then on. Texts already stored stay under their old key (as of 2026-10). The read order keeps most of them readable: a tenant that moves from language to regional with locale_fallback: language still reads its old de texts through de-AT → de. With locale_fallback: none it does not, and German texts disappear from the storefront. Check the fallback before you switch.
Search
A search collection with translated fields holds one document per product and write key, not per market locale: de and en in a tenant that keeps text per language. Which products get a document in a language:
- The main language, the write key of
fallback_locale, always holds every product. A product without its own text there is filled from the field's other sources and marked untranslated. - Every other language follows the products setting
search_untranslated, per market:hide(default) publishes a product only where it has its own text in that language.fallbackpublishes every product and fills the gaps from the main language. When several markets write the same language and one saysfallback, that language usesfallback.
A tenant without market locales keeps one document per key that is stored. A change to either setting reaches the index within about ten minutes. See Markets settings.
A storefront asks for one locale with the X-Revenexx-Locale header. See Search collections.
Where to go next
- Markets — locales, the market context and the tenant-wide policy.
- Products — the locale buckets in
attribute_values. - Import and export — bulk loading with profiles.
- Search collections — querying a collection in one locale.