Language Tags — BCP 47 Language Tags and Locale Codes Explained
In this tutorial, you will learn about Language Tags. We cover key concepts, practical examples, and best practices to help you master this topic.
BCP 47 language tags uniquely identify languages, regions, scripts, and variants for internationalized applications using standardized codes.
What You'll Learn
By the end of this tutorial, you'll understand the structure of BCP 47 language tags, how to use subtags for language, region, script, and variant, and how to parse and validate locale codes in JavaScript.
Why It Matters
Incorrect language tags break locale detection, date formatting, and translation loading. Using "en" when you need "en-US" vs "en-GB" changes date formats, spelling, and currency symbols. Understanding BCP 47 ensures your application correctly identifies and serves the right locale.
Real-World Use
A streaming platform uses BCP 47 tags to select subtitle tracks. The user requests "es-MX" (Mexican Spanish). The platform falls back to "es" (general Spanish) if Mexican-specific subtitles aren't available, then to "en" if no Spanish exists. Without proper tag Parsing, the fallback chain fails and the user gets no subtitles.
Language Tag Structure
graph LR
A["en-US"] --> B[Primary language
en = English]
A --> C[Region
US = United States]
D["zh-Hans-CN"] --> E[Primary language
zh = Chinese]
D --> F[Script
Hans = Simplified]
D --> G[Region
CN = China]
H["sr-Latn-RS"] --> I[Primary language
sr = Serbian]
H --> J[Script
Latn = Latin script]
H --> K[Region
RS = Serbia]
L["es-419"] --> M[Primary language
es = Spanish]
L --> N[Region
419 = Latin America]
style A fill:#4a90d9,color:#fff
style D fill:#27ae60,color:#fff
style H fill:#f39c12,color:#fff
style L fill:#e74c3c,color:#fff
Parsing Language Tags
// i18n/language-tags.js — BCP 47 tag parsing
class LanguageTag {
constructor(tag) {
this.tag = tag;
this.language = null;
this.script = null;
this.region = null;
this.variant = null;
this.extensions = [];
this.parse(tag);
}
parse(tag) {
const parts = tag.split('-');
// First part: primary language (ISO 639-1 or 639-3)
if (parts.length > 0) {
this.language = parts[0].toLowerCase();
}
// Second part: could be script (ISO 15924) or region (ISO 3166-1)
if (parts.length > 1) {
// Script codes are 4 characters, title case (e.g., "Latn")
if (parts[1].length === 4) {
this.script = parts[1].charAt(0).toUpperCase() + parts[1].slice(1).toLowerCase();
}
// Region codes are 2 letters (ISO 3166-1) or 3 digits (UN M.49)
else if (parts[1].length === 2 || /^\d{3}$/.test(parts[1])) {
this.region = parts[1].toUpperCase();
}
}
// Third part: region (if second was script)
if (parts.length > 2) {
const third = parts[2];
if (third.length === 2 || /^\d{3}$/.test(third)) {
this.region = third.toUpperCase();
}
}
// Remaining parts: variants or extensions
for (let i = 3; i < parts.length; i++) {
if (parts[i].length >= 5 && /^[a-z]/.test(parts[i])) {
this.variant = parts[i];
} else if (parts[i].length === 1) {
this.extensions.push(parts[i]);
}
}
}
toString() {
let result = this.language;
if (this.script) result += `-${this.script}`;
if (this.region) result += `-${this.region}`;
if (this.variant) result += `-${this.variant}`;
return result;
}
// Check if this tag matches another, considering fallbacks
matches(other, options = {}) {
const otherTag = typeof other === 'string' ? new LanguageTag(other) : other;
// Exact match
if (this.tag === otherTag.tag) return true;
// Language + region match (e.g., en-US matches en-US)
if (this.language === otherTag.language &&
this.region && otherTag.region &&
this.region === otherTag.region) return true;
// Language match with fallback (e.g., en-US matches en)
if (options.fallback && this.language === otherTag.language) return true;
return false;
}
// Get the parent tag (for fallback chains)
parent() {
if (this.region) {
return new LanguageTag(this.language + (this.script ? `-${this.script}` : ''));
}
if (this.script) {
return new LanguageTag(this.language);
}
return null;
}
// Validate the tag structure
static validate(tag) {
const pattern = /^([a-z]{2,3})(-[A-Z][a-z]{3})?(-[A-Z]{2}|\-\d{3})?(-[a-z]{5,})?(-[a-z])?$/i;
return pattern.test(tag);
}
}
// Usage examples
const tags = [
new LanguageTag('en-US'),
new LanguageTag('zh-Hans-CN'),
new LanguageTag('sr-Latn-RS'),
new LanguageTag('es-419'),
new LanguageTag('en'),
];
tags.forEach(t => {
console.log(`${t.tag}: lang=${t.language}, script=${t.script || 'none'}, region=${t.region || 'none'}`);
});
// Fallback chain example
const userTag = new LanguageTag('es-MX');
console.log('Fallback chain:');
let current = userTag;
while (current) {
console.log(` ${current.tag}`);
current = current.parent();
}
Locale Resolution with Fallbacks
// i18n/locale-resolver.js — Find best locale match
class LocaleResolver {
constructor(supportedLocales = []) {
this.locales = supportedLocales.map(l => new LanguageTag(l));
}
// Find best match for user locale
resolve(userLocale) {
const userTag = new LanguageTag(userLocale);
// 1. Exact match
const exact = this.locales.find(l => l.tag === userTag.tag);
if (exact) return exact.tag;
// 2. Language + region match
const regionMatch = this.locales.find(l =>
l.language === userTag.language &&
l.region === userTag.region
);
if (regionMatch) return regionMatch.tag;
// 3. Language match (any region)
const langMatch = this.locales.find(l =>
l.language === userTag.language
);
if (langMatch) return langMatch.tag;
// 4. Script match (for Chinese, Arabic, etc.)
if (userTag.script) {
const scriptMatch = this.locales.find(l =>
l.script === userTag.script
);
if (scriptMatch) return scriptMatch.tag;
}
// 5. Default to first supported
return this.locales[0]?.tag || 'en-US';
}
// Build Accept-Language style ranking
prioritize(locales) {
return locales
.map(locale => ({ tag: locale, tagObj: new LanguageTag(locale) }))
.map(item => ({
...item,
priority: this.calculatePriority(item.tagObj)
}))
.sort((a, b) => b.priority - a.priority)
.map(item => item.tag);
}
calculatePriority(tag) {
let score = 0;
if (this.locales.find(l => l.tag === tag.tag)) score += 100;
if (this.locales.find(l => l.language === tag.language)) score += 50;
if (this.locales.find(l => l.script === tag.script)) score += 25;
return score;
}
}
const resolver = new LocaleResolver(['en-US', 'en-GB', 'es-ES', 'fr-FR', 'zh-Hans-CN']);
console.log('User es-MX resolves to:', resolver.resolve('es-MX'));
console.log('User en resolves to:', resolver.resolve('en'));
console.log('User zh-Hant-TW resolves to:', resolver.resolve('zh-Hant-TW'));
Common Mistakes
- Using only language codes without regions. "en" is ambiguous — it could be US English (en-US), British English (en-GB), or Australian English (en-AU). Use region subtags when format differences matter.
- Case sensitivity in tags. BCP 47 specifies lowercase for language, title case for script, uppercase for region. "EN-US" and "en-us" both work in most browsers, but stick to the standard for consistency.
- Assuming all speakers of a language use the same region. Spanish has es-ES (European), es-MX (Mexican), es-AR (Argentinian). The language is the same, but date formats and vocabulary differ.
- Ignoring script subtags for Chinese. zh-Hans (Simplified, used in mainland China) and zh-Hant (Traditional, used in Taiwan/Hong Kong) are different scripts. Using zh-CN assumes Simplified, but zh-SG might be Simplified or Traditional.
- Not handling deprecated tags. Some language tags are deprecated (e.g., "iw" for Hebrew, "in" for Indonesian). Use the current codes ("he" and "id") and map old codes to new ones.
Practice Questions
- What is the structure of a BCP 47 language tag?
- How does the fallback chain work when a specific locale isn't available?
- What is the difference between zh-Hans-CN and zh-Hant-TW?
- Why should you use region-specific tags like en-GB instead of just en?
- How do you validate a language tag in JavaScript?
Challenge: Build a locale resolver that accepts a user's preferred locales (from Accept-Language header or navigator.languages) and a list of supported locales. Implement a fallback chain that tries exact match, language+region, language-only, and finally a default. Display the resolved locale and the fallback path.
FAQ
Mini Project
Build a language tag explorer: create a web page that accepts a BCP 47 language tag as input, parses and displays its components (language, script, region, variant), shows the fallback chain, lists all supported locales that match, and validates the tag structure. Include presets for common tags (en-US, zh-Hant-TW, sr-Latn-RS, es-419).
What's Next
You've mastered language tags. Next, learn how to implement Locale Detection to automatically determine the user's preferred locale from browser and server signals.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro