---
name: bcp47-locale-and-fallback
source: https://app.decimal.ai/s/bcp47-locale-and-fallback@1/SKILL.md
source_sha256: 14b3b99cda53
---

# BCP-47 Locale Identifiers and Fallback

Two jobs that framework tutorials assume you already know: **name a locale correctly**, and
**decide which translation to serve when the exact one is missing**. The base model knows the
form of a language tag but does not default to the right one — it invents subtags
(`zh-Simplified`, `en-UK`), guesses the script/region axis, and when a translation is missing it
grabs *any* file that shares the language — which can hand a Hong Kong reader Simplified text or
show a raw string key. This skill forces well-formed tags and a deterministic fallback.

## Tag structure

A tag is `language`, then an optional `Script`, then an optional `REGION`, in that order, joined
by hyphens. Casing is conventional (case-insensitive to parsers, but write it the canonical way):

| Subtag | Length / form | Case | Examples |
|---|---|---|---|
| language | 2–3 letters | lower | `zh`, `sr`, `pt`, `en`, `fr` |
| Script | exactly 4 letters | Title | `Hans`, `Hant`, `Latn`, `Cyrl` |
| REGION | 2 letters, or 3 digits (UN M.49) | UPPER | `CN`, `HK`, `BR`, `GB`, `419` |

So `zh-Hant-HK` = Chinese, Traditional script, Hong Kong region.

**Script and region are different axes — do not confuse them.** `zh-Hans` says *which writing
system* (Simplified Han); `zh-CN` says *which country* (mainland China, Simplified by convention).
Pick the script subtag when the writing system is the thing that differs; pick the region when a
country's conventions differ. When both matter, use both (`zh-Hant-HK`).

## Canonical pairs (memorize these)

- **Chinese script:** `zh-Hans` (Simplified) vs `zh-Hant` (Traditional). Regions: `zh-CN`, `zh-SG`
  are Simplified-writing; `zh-TW`, `zh-HK`, `zh-MO` are Traditional-writing.
- **Serbian script:** `sr-Latn` (Latin) vs `sr-Cyrl` (Cyrillic). Bare `sr` is ambiguous — mark the
  script.
- **Portuguese region:** `pt-BR` (Brazil) vs `pt-PT` (Portugal).
- **Latin-American Spanish:** `es-419` — the region is the M.49 numeric code `419`, not a country.
- **English:** UK is `en-GB`, not `en-UK`. US is `en-US`.

## Never invent subtags

Emit only real registered subtags. Wrong, and what it should be:

- `zh-Simplified`, `zh-simplified`, `zh-CHS` → `zh-Hans`
- `zh-Traditional`, `zh-CHT` → `zh-Hant`
- `pt-Brazil` → `pt-BR`  ·  `en-UK` → `en-GB`  ·  `es-LatinAmerica` → `es-419`
- Do not glue subtags (`zh-HantHK`); separate them: `zh-Hant-HK`.

## Fallback: which translation to serve

Given a requested locale and the set of locales you actually have, resolve by **prefix
truncation** (the RFC 4647 "lookup" rule):

1. Try an exact match.
2. Drop the **right-most** subtag and try again. Repeat: `zh-Hant-HK` → `zh-Hant` → `zh`.
3. Take the first available tag in that chain.
4. If the chain runs out (down to the bare language) with no match, serve the **ultimate default**
   (usually `en`, or whatever your app declares).

Three rules that make this correct rather than naive:

- **Fall back by prefix, not by "same language."** The danger is grabbing *any* file that shares
  the language. `zh-Hant` and `zh-Hans` share the language `zh` but are **mutually unreadable** —
  Traditional and Simplified. Truncation never turns `zh-Hant-*` into `zh-Hans`, because `zh-Hans`
  is not a prefix of `zh-Hant-HK`. Do not add sideways matching that would.
- **Do not sideways-match regions either.** Requested `pt-BR` with only `pt-PT` and `en` available
  resolves to **`en`**, not `pt-PT` — `pt-PT` is not in the chain `pt-BR → pt → default`.
- **Never surface the raw key or tag to the user.** If nothing resolves, the user sees a real
  string in the default language, never the literal message key or the tag `zh-Hant-HK`.

### Worked chain

Requested `zh-Hant-HK`; available `{zh-Hant, zh-Hans, en}`:

```
zh-Hant-HK   → miss
zh-Hant      → HIT  ✅  serve this
```

Never `zh-Hans` (would be Simplified text for a Traditional reader). If instead only
`{zh-Hans, en}` were available, the chain `zh-Hant-HK → zh-Hant → zh` all miss, so serve `en` —
**not** `zh-Hans`.

## When to activate

Use when choosing the identifier for a resource file, translation column, or language switcher, or
when deciding which existing translation to show a user whose exact locale you don't have.

Do NOT use for: formatting dates/numbers/currency to a locale (that is a formatting skill), plural
category selection, or translating the actual prose.

For the extended language → script/region catalog, region-code gotchas, and the full lookup steps,
see `references/bcp47-catalog.md`.
