ISO 639-1 Cheatsheet - Language Code Reference
Localization breaks silently when the language you detect doesn't match the one you ship. Grouped by region, each two-letter code pairs its English and native name so you pick without confusing zh/zh-CN or pt/pt-BR. Use it to set the lang attribute, build Accept-Language negotiation, and store a canonical locale key. After reading you assign locale codes that browsers and backends agree on.
Typical Use Case
For developers building multilingual sites, locale routing and language negotiation. ISO 639-1 defines the two-letter language codes (zh, ja, en), which in practice combine with region subtags into BCP 47 tags such as zh-CN, en-US and pt-BR. This sheet covers 39 common languages plus their regional variants, along with four canonical Accept-Language header forms and four HTML lang attribute forms. The failure mode here is not an exception but silent degradation: a wrong lang attribute makes screen readers apply the wrong pronunciation rules to the whole page.
Asian Languages 12
zhzh-CNzh-TWzh-HKjakovithidmshibnEuropean Languages 22
enen-USen-GBfrdeesitptpt-BRruplnlsvdanofieltrcshuroukMiddle East & African Languages 5
arhefaswafOther Languages 2
laeoHTTP Accept-Language Example 4
Accept-Language: zh-CN,zh;q=0.9Accept-Language: en-US,en;q=0.9Accept-Language: ja,en;q=0.8Accept-Language: *HTML lang 4
<html lang="zh-CN"><html lang="zh-TW"><html lang="en"><html lang="ja">Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
bare language code (zh / ja / en) | declare only the language, with no region preference | zh :: Chinese |
language + region (zh-CN / en-GB) | distinguish writing systems and word choice within one language | zh-CN :: Simplified Chinese (Mainland China) |
<html lang=""> | declare the page language, driving screen reader pronunciation and translate prompts | <html lang="zh-CN"> |
Accept-Language header | let the client express preference; q values rank them and the server picks a variant | Accept-Language: zh-CN,zh;q=0.9 |
wildcard * | accept anything, which belongs last as a fallback rather than first | Accept-Language: * |
Common pitfalls
SymptomA screen reader pronounces the Chinese page with pinyin or American English.
CauseThe lang attribute says zh rather than zh-CN, so the engine guesses a pronunciation rule.
FixWrite the exact tag that matches the content: zh-CN for Simplified, zh-TW or zh-HK for Traditional.
SymptomEnglish users in every region end up with the same spelling.
CauseThe locale key is just en and never splits en-US from en-GB, which differ in dates, spelling and word choice.
FixStore full tags in routes (en-GB / en-US) and fall back to bare en only for language-only matching.
SymptomThe Accept-Language header has q values, yet the server ignores the second preference.
CauseA q value must follow its language segment immediately, in the fixed form language;q=weight with weight between 0 and 1.
FixOrder by preference and write it as zh-CN,zh;q=0.9,en;q=0.8.
SymptomTraditional Chinese users get Simplified copy (or the reverse) with no visible error.
Causezh-HK, zh-TW and zh-CN are three separate tags, so a bundle keyed only on zh cannot tell them apart.
FixMaintain zh-CN and zh-TW bundles separately, and only map zh-HK to zh-TW when the difference genuinely does not matter.
Troubleshooting
1Check the language preference the request actually carries
curl -sI https://example.com | grep -i accept-languageNo output means the request omitted the header, so the server can only fall back to a default.
2Verify the language tag the page declares
curl -s https://example.com | grep -o "<html lang=[^>]*>"It should match the actual copy; a Traditional page needs lang="zh-TW" or lang="zh-HK".
Command Examples
Resolving language tags to readable names
node -e "
const d = new Intl.DisplayNames(['en'], { type: 'language' });
console.log(['zh-CN', 'ja', 'pt-BR', 'en-GB', 'ar'].map(c => c + '=' + d.of(c)).join(' '));
"Intl.DisplayNames reads the CLDR database, which confirms whether a tag is standard-recognized; an invalid tag throws RangeError, so it doubles as a validator.
Output
zh-CN=Chinese (China) ja=Japanese pt-BR=Brazilian Portuguese en-GB=British English ar=Arabic
Splitting language and region, then expanding likely subtags
node -e "
for (const t of ['zh', 'zh-CN', 'zh-TW', 'en', 'pt-BR']) {
const l = new Intl.Locale(t);
console.log(t, 'language=' + l.language, 'region=' + (l.region || '-'), 'max=' + l.maximize().toString());
}
"maximize() turns zh into zh-Hans-CN, which is direct evidence that bare zh is presumed simplified. zh-TW expands to zh-Hant-TW, pinning script and region together.
Output
zh language=zh region=- max=zh-Hans-CN zh-CN language=zh region=CN max=zh-Hans-CN zh-TW language=zh region=TW max=zh-Hant-TW en language=en region=- max=en-Latn-US pt-BR language=pt region=BR max=pt-Latn-BR
Sorting Accept-Language into a fallback order by q
node -e "
const h = 'zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7';
const parsed = h.split(',').map(s => { const p = s.trim().split(';q='); return { tag: p[0], q: p[1] ? +p[1] : 1 }; });
console.log(parsed.sort((a, b) => b.q - a.q).map(x => x.tag + '(' + x.q + ')').join(' > '));
"Treat a missing q as 1, then sort descending to get the fallback chain; the server simply picks the first tag it has a translation for.
Output
zh-CN(1) > zh(0.9) > en(0.8) > ja(0.7)
The same number formats differently per language
node -e "
console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.5));
console.log(new Intl.NumberFormat('ja-JP').format(1234567.891));
"German uses dots for thousands and a comma for decimals, Japanese the reverse. The region tag alone reshapes the output.
Output
1.234,50 € 1,234,567.891
Common Pitfalls
- A wrong lang attribute never errors, but screen readers speak the wrong language and translation prompts plus search indexing decisions follow suit.
- Never put the * wildcard first in Accept-Language: it matches immediately and swallows every specific preference after it, making negotiation meaningless.
- Traditional-Chinese users receiving simplified copy — from writing zh-TW as zh or omitting the tag entirely — sees a page that looks perfectly fine.
- Sharing one English string across all regions concentrates en-US versus en-GB spelling mismatches onto a handful of pages; automatic conversion will not cover them.
- Join the two-letter code and the region with a hyphen (zh-CN). An underscore or slash makes the tag invalid and takes the q ordering down with it.
Tips
- ISO 639-1 is two-letter codes; ISO 639-2 is three-letter (more detailed).
- Language tags can carry a region suffix, e.g. zh-CN (Simplified), zh-TW (Traditional).
- The Accept-Language request header negotiates content language; the server returns the matching version.
- The HTML lang attribute helps search engines and screen readers detect language.
- i18n libraries (i18next, vue-i18n) use these codes as locale identifiers.
FAQ
Which of zh, zh-CN, zh-TW and zh-HK should I use?
Choose by content, not by audience. Use zh-CN for simplified copy, zh-TW or zh-HK for traditional, and plain zh when you only declare a language without caring about region. The catch is that bare zh carries no script subtag, so BCP 47 likely-subtag rules resolve it to Hans — meaning zh is not the neutral "Chinese" anyone assumes, and a traditional-Chinese user served zh gets simplified text. zh-TW and zh-HK share the same script and differ only in regional conventions such as currency and date format.
Does the difference between en and en-US actually matter?
It depends on whether your content varies by region. Spelling (colour vs color), date format, currency symbol and quoting conventions all follow the region, so en-US or en-GB is required there. For region-neutral technical content, plain en suffices and saves a maintenance point. In practice most multilingual sites default to en-US, accepting that a few British phrasings need manual adjustment.
How exactly are q values in Accept-Language ordered?
q runs from 0 to 1, larger means more preferred, and an omitted q defaults to 1; ties fall back to header order. The syntax must be the tag followed by a semicolon and then q, as in zh-CN;q=0.9. Any other arrangement, such as putting q ahead of the tag or separating it with a comma, is ignored by parsers and silently drops the second preference. Once sorted descending the order is your fallback chain: zh-CN;q=0.9,zh;q=0.8 prefers an exact match and degrades to the bare language tag.
What breaks when the HTML lang attribute is wrong?
Nothing throws — it just fails silently. Screen readers pick pronunciation rules from lang and read Chinese with an English voice when it is wrong, browsers decide whether to offer translation based on it, search engines use it to decide whether alternate versions are needed, and spellcheck plus word breaking follow it too. It is the one declaration that produces entirely wrong behaviour without any error, so keep it centralized in the layout rather than scattered per page.
Can a language code be part of a URL route?
Yes, and it is the most common use of this table, but mind case and separators. Keep route segments lowercase (/pt-br, /zh-cn) because URL case sensitivity varies by server, and an uppercase form can 404 behind Nginx or a CDN. Normalize to a canonical form before comparing instead of matching the raw request string. Also reserve a default branch without a language prefix, or search engines cannot reach the root path.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us