
Table of content
Language codes look simple until a product needs more than one version of the same language. en may be sufficient for English content, but it cannot distinguish American from British spelling. zh identifies Chinese, but not whether the interface uses Simplified or Traditional characters. And a home-grown code such as english_uk may work inside one service while causing compatibility issues with other systems.
The recommended approach is to use established standards consistently. ISO 639 supplies language identifiers. BCP 47 combines language identifiers with optional script, region, variant, and extension subtags to create language tags that software can exchange reliably.
This guide focuses on implementation: how to choose tags, store them, validate them, design fallbacks, and use them across HTML, APIs, and localization repositories. If you need definitions or a complete two-letter code table, use our ISO 639 language-code reference.
ISO 639 and BCP 47 solve different problems
ISO 639 identifies languages. The current ISO 639:2023 standard consolidates the principles previously published across ISO 639-1, 639-2, 639-3, 639-4, and 639-5. Developers still encounter familiar two-letter and three-letter identifiers such as en, de, and eng in existing systems and datasets.
For most web and application interfaces, however, a language identifier alone is not always enough. Software often needs to express a language together with a script, region, or variant. This is the purpose of IETF BCP 47, the standard used for language tags on the web and across many software protocols.
Requirement | Appropriate identifier |
Identify English |
|
Distinguish British English |
|
Identify Serbian written in Latin script |
|
Identify Traditional Chinese used in Hong Kong |
|
Identify Latin American Spanish |
|
The important distinction is this:
ISO 639 code: identifies a language.
BCP 47 tag: identifies a language and, when necessary, its script, region, or other variation.
Locale: a broader application concept that may also control dates, numbers, currencies, sorting, and plural rules.
A BCP 47 tag can be used as a locale identifier in many platforms, but language and locale are not conceptually identical. A user can read English while preferring Austrian date, currency, or number formats. Model these preferences separately when your product requires that flexibility.
Understand the structure of a BCP 47 language tag
A commonly used BCP 47 structure is:
language-Script-Region-variant
Only the language subtag is normally required. Add other subtags when they make a difference to your application’s content or behavior.
de German
de-AT German as used in Austria
sr-Latn Serbian written in Latin script
sr-Cyrl-RS Serbian written in Cyrillic as used in Serbia
es-419 Spanish as used in Latin America
de-CH-1901 Swiss German using the 1901 orthography
The components follow recognizable conventions:
Language subtags are generally lowercase:
en,de,ar.Script subtags use title case:
Latn,Cyrl,Arab,Hans,Hant.Alphabetic region subtags use uppercase:
US,GB,AT.Subtags are separated with hyphens in BCP 47 tags.
Case does not change the meaning of a BCP 47 tag, but canonical formatting makes configuration files, logs, and comparisons easier to understand.
The IANA Language Subtag Registry is the authoritative registry for subtags used in BCP 47 language tags. Do not maintain an unofficial list copied into your codebase unless you also have a process for updating it.
Choose the shortest tag that preserves a meaningful distinction
More detail is not automatically better. The W3C recommends keeping tags as short as possible and adding subtags only when they distinguish content or behavior you actually support.
Use this decision process:
Start with the language. Use
frif all French-speaking users receive the same French content.Add a script when the writing system changes the content. Use
sr-Latnandsr-Cyrlif your product supports Serbian in both scripts.Add a region when you maintain a regional version. Use
pt-BRandpt-PTif your translations, terminology or formatting differ.Add a variant only when a registered variant is necessary. Avoid inventing your own suffixes.
Use private-use tags only within controlled systems. A tag such as
en-US-x-companyhas no reliable meaning outside the parties that agreed on it.
For example, ja is usually sufficient for Japanese. Adding JP provides little value unless your system intentionally distinguishes Japanese used in Japan from another supported variant. By contrast, zh-Hans and zh-Hant can be essential, as they distinguish between Simplified and Traditional writing systems.
The W3C provides a detailed language-tag selection guide for ambiguous cases.
Define one canonical format at system boundaries
Real localization stacks rarely use a single identifier format across the board. Web standards prefer pt-BR, while some frameworks, file systems, and legacy libraries use pt_BR. External translation or content platforms may support aliases of deprecated codes.
Choose one canonical representation for APIs and persistent storage, then map formats at system boundaries.
Recommended internal policy:
Canonical application format: BCP 47
Separator: hyphen
Language casing: lowercase
Script casing: title case
Region casing: uppercase
Example: zh-Hant-TW
Do not use string replacement alone to normalize arbitrary user input. Canonicalization may need to replace deprecated identifiers rather than merely adjust capitalization.
In JavaScript, Intl.getCanonicalLocales() can validate structurally well-formed tags and return their canonical form:
function canonicalizeLanguageTag(input) {
const [canonicalTag] = Intl.getCanonicalLocales(input);
return canonicalTag;
}
canonicalizeLanguageTag("EN-us"); // "en-US"
canonicalizeLanguageTag("pt-BR"); // "pt-BR"
canonicalizeLanguageTag("en_US"); // throws RangeError
If your framework requires underscores, convert only when passing the identifier into that framework. Continue using the canonical tag elsewhere.
Some i18n frameworks do not follow BCP 47 syntax exactly. For example, they may use underscores instead of hyphens or lowercase all subtags. LingoHub attempts to identify the intended language, script, and region while preserving the original filename format, allowing exported files to use the same names as the imported files.
Store language tags as identifiers, not inferred geography
Store the canonical language tag used by the localized resource. Do not derive it from a country, a flag, or a user’s IP address.
A simple translation table might look like this:
CREATE TABLE translations (
message_key VARCHAR(255) NOT NULL,
language_tag VARCHAR(35) NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (message_key, language_tag)
);
BCP 47 requires implementations to accommodate language tags of at least 35 characters. If your system supports Unicode locale extensions or private-use tags, allow additional space.
Keep other preferences separate when needed:
{
"contentLanguage": "en-GB",
"formattingLocale": "de-AT",
"timeZone": "Europe/Vienna",
"currency": "EUR"
}
This prevents assumptions such as “English content always uses US date formats” or “a German-speaking user must be located in Germany.”
Use language tags correctly in HTML
Declare the primary language of an HTML document with the lang attribute:
<html lang="en">
Mark passages in another language when they appear inside the document:
<p>
The German term <span lang="de">Übersetzungsmanagement</span>
means translation management.
</p>
This metadata can help browsers, assistive technologies, spell-checkers, and other processors handle the text correctly. The W3C’s HTML language declaration guidance recommends BCP 47 values and the shortest tag appropriate for the content.
Language tags do not replace multilingual SEO annotations. If separate URLs contain equivalent pages for different languages or regions, implement matching hreflang annotations as part of your multilingual SEO setup.
Design API language negotiation deliberately
APIs typically receive language preferences in one of three ways:
A user profile setting
A language tag in the URL, such as
/de-AT/accountThe HTTP
Accept-Languagerequest header
Treat an explicit account or URL preference as stronger than a browser hint. Accept-Language reflects browser configuration and may not match what the user wants for a specific product.
An example header can contain several weighted preferences:
Accept-Language: de-AT,de;q=0.9,en;q=0.7
Your server should compare those preferences with the languages the product actually supports. Do not assume that receiving de-AT means a de-AT translation exists.
Return the selected content language explicitly when appropriate:
Content-Language: de
Most importantly, let users override automatic selection and remember their choice.
Build a predictable fallback chain
Fallback logic prevents missing translations from becoming broken interfaces. It can also create subtle content errors if it carelessly removes meaningful subtags.
A typical regional fallback might be:
pt-BR → pt → default language
A script-sensitive fallback might be:
zh-Hant-HK → zh-Hant → configured default
Avoid blindly truncating every tag until only the primary language remains. Falling from zh-Hant to generic zh may serve the wrong writing system. Similarly, Serbian Cyrillic and Latin resources should not automatically replace one another unless your product team has approved that behavior.
Define fallback chains in configuration rather than hiding them inside application code:
{
"fallbacks": {
"pt-BR": ["pt", "en"],
"pt-PT": ["pt", "en"],
"zh-Hant-HK": ["zh-Hant", "en"],
"sr-Latn": ["en"]
}
}
Then test the configured chain for every supported locale. Your fallback policy should be a product decision, not an accidental result of string splitting.
Keep repository and localization-platform identifiers aligned
Use the same canonical identifiers across source files, localization tooling and deployment automation wherever possible.
A repository can organize resources by language tag:
locales/
├── en/
│ └── messages.json
├── de-AT/
│ └── messages.json
├── pt-BR/
│ └── messages.json
└── zh-Hant/
└── messages.json
Your pipeline should verify that:
Every directory name maps to a supported canonical tag.
Source and target tags match the localization project configuration.
The build does not create separate locales for aliases or casing differences.
Fallback resources exist before deployment.
Newly added locales are included in routing, metadata and tests.
This validation belongs in CI, especially when localization runs continuously alongside development. A continuous localization workflow helps keep repository changes, translation resources and releases synchronized.
Common implementation mistakes
Treating a country as a language
US, AT and BR are region identifiers. A country may have several languages, and a language may be used in many countries.
Using flags as language identifiers
A flag represents a country. It cannot accurately represent languages such as English, Spanish or Arabic across all their users. Display language names in their own language where possible, for example “Deutsch,” “English” and “Español.”
Inventing tags
Values such as en_UK, chinese-simple or br-portuguese may be understandable to one team but are not interoperable. Check the IANA registry and use en-GB, zh-Hans and pt-BR instead.
Adding regions everywhere
Using de-DE when the product supports only one generic German translation adds complexity without creating a useful distinction. Start with de; add a region when you genuinely maintain regional content or behavior.
Ignoring scripts
Region alone does not always identify the required writing system. Use script subtags for languages such as Serbian, Azerbaijani or Chinese when the script changes the delivered content. Script choice also affects layout and testing; our right-to-left implementation guide covers direction-sensitive interfaces.
Reusing the content tag for every locale preference
Translation language, number formatting, currency and time zone may require separate settings. One overloaded locale field often creates incorrect assumptions later.
Changing identifiers after release without migration
Language tags can appear in URLs, database keys, caches, analytics and translation memories. Treat a tag change like a schema migration: map aliases, redirect affected URLs, update integrations and preserve historical reporting.
Add language-code checks to CI
Language identifiers are configuration, so test them like configuration.
At minimum, check that:
Every supported identifier is structurally valid and canonical.
No two configured values normalize to the same tag.
Every supported locale has the required source files.
Every fallback points to an existing locale.
HTML output contains the correct
langvalue.API responses report the language actually selected.
Region and script variants use approved terminology and formatting rules.
Right-to-left locales activate the expected layout direction.
Also include pseudo-localization and missing-key tests in your release process. These catch hard-coded strings, clipped interfaces and fallback failures before translators or users find them.
A practical implementation checklist
Before adding a language or locale to your product, answer these questions:
What user-facing distinction requires this tag?
Is the value registered and valid under BCP 47?
Can we use a shorter tag without losing that distinction?
Does the language require a specific script?
Do we maintain separate regional content, or only regional formatting?
What is the approved fallback chain?
Is the tag represented consistently in APIs, storage and repositories?
Can users override automatic language selection?
Are routing, HTML metadata and multilingual SEO annotations updated?
Does CI test the new language, script direction and fallback behavior?
Document these decisions centrally. A short locale policy prevents teams from independently creating pt_BR, pt-br and br-portuguese for the same resource.
Manage language codes as part of the localization workflow
Language tags are small strings with architectural consequences. A consistent implementation improves interoperability, prevents duplicate resources and makes it easier to scale a product across scripts and regions.
Use ISO 639 when you need to identify a language. Use BCP 47 when software must exchange a language tag that may include a script, region or variant. Keep tags as short as your content allows, canonicalize them at system boundaries and define fallbacks explicitly.
When your product expands, language identifiers should move through the same controlled workflow as source strings and translations. LingoHub’s localization tools for developers connect repositories, localization resources and automated workflows so teams can keep those identifiers aligned from development to release.
Are you ready to localize your software? Start your free trial or book a demo now.
Related articles

Beyond the basics: Navigating language codes for true localization
Language codes are the backbone of true localization. Discover how ISO 639 standards and regional variants shape global user experiences in our blog article.

LingoHub support for language codes in localization
Read in our blog about multiple language codes (ISO) LingoHub supports. Learn more about the ISO codes, their types, exceptions, and legacy encoding.

What are ISO 639 language codes?
Learn what ISO 639 language codes are, how they standardize language identification, and why they are essential for localization and multilingual software.