BCP 47 language tags: what ja-JP, zh-Hans, and pt-BR actually mean
"What language tag should I use" looks like a trivial question until a codebase has three different answers scattered across it — ja_JP in one config, ja-jp in another, JA in a third. BCP 47 (the IETF specification, built on the ISO 639, ISO 15924, and ISO 3166 registries) is the actual standard behind every language tag you will ever need, and most of the confusion comes from not knowing which subtag is doing which job.
The subtags and what each one means
A BCP 47 tag is a sequence of subtags joined by hyphens, ordered by a fixed grammar. The three that come up constantly in game localization are:
- Primary language subtag — a two- or three-letter code from ISO 639, e.g. ja (Japanese), en (English), zh (Chinese). Always required, always first.
- Script subtag — a four-letter code from ISO 15924, e.g. Hans (Simplified Han) or Hant (Traditional Han). Present only when a language is written in more than one script and the script needs to be distinguished.
- Region subtag — a two-letter code from ISO 3166-1 (or a three-digit UN M49 code for broader regions), e.g. JP, US, BR. Marks a regional variant, not the language itself.
Reading real examples
ja is just "Japanese" — sufficient on its own, since Japanese has no meaningfully distinct regional variants in the way this matters for content. ja-JP adds a region subtag that is largely redundant for the same reason; you will see it in the wild, but it rarely carries information ja does not already carry.
zh-Hans and zh-Hant are the tags that matter: Chinese is one language subtag with two distinct scripts in active use, and the script subtag is what actually determines which character set your text needs to be in. pt-BR is the opposite case — Portuguese written in one script everywhere, but Brazilian and European Portuguese differ enough in vocabulary and spelling that the region subtag carries real information. es-419 uses a UN M49 region code (419 = Latin America and the Caribbean) rather than a single country, for content aimed at the region as a whole rather than one specific country's dialect.
Why script outranks region for Chinese
The instinct to write zh-CN and zh-TW instead of zh-Hans and zh-Hant is understandable — those are the regions most associated with each script — but it conflates two different axes. Region tags describe geography; script tags describe which character set the text is actually written in. A Simplified Chinese release read by users in Singapore or Malaysia is still zh-Hans; tagging by region instead of script would either mismatch those users or force you to invent a region tag for every place Simplified Chinese is read.
The general rule this generalizes to: tag for the actual distinguishing property of your content — script, not incidental geography — and add a region subtag only when the region itself changes the text (as with pt-BR).
Matching and fallback
Tags are matched by progressively stripping subtags from the end until something matches: a request for ja-JP that finds no exact match falls back to ja before falling back to a default locale. This is why the primary language subtag should never be omitted and why an overly specific tag with no corresponding broader fallback in your resource files can silently produce no match at all.
// naive fallback resolution
function resolve(tag, available) {
const parts = tag.split("-");
while (parts.length > 0) {
const candidate = parts.join("-");
if (available.includes(candidate)) return candidate;
parts.pop();
}
return null; // caller falls back to a hardcoded default
}Common mistakes
A short list of things that reliably cause matching bugs downstream:
- Inventing tags instead of using the registries — jp for Japan's language is wrong; the language is ja, JP is the region
- Using an underscore instead of a hyphen (ja_JP) — valid in some older locale identifier conventions, but not a BCP 47 tag, and libraries that parse strictly will reject or mishandle it
- Confusing a language with a locale — a locale is broader, potentially including formatting conventions beyond just the language tag, and the two should not be used interchangeably in code that only needs one
- Adding a region subtag out of habit rather than necessity — every unnecessary subtag is another way for matching to fail silently