|$ curl https://forge-ai.dev/api/markdown?path=docs/html/i18n
$cat docs/internationalization.md
updated This week·35 min read·published

Internationalization

HTMLi18nIntermediateIntermediate
Introduction

Internationalization (i18n) is the process of designing applications that can adapt to various languages, scripts, and cultural conventions without code changes. HTML provides built-in mechanisms — the lang and dir attributes, meta charset, and elements like <time> — that form the foundation of a globally accessible web.

Proper i18n ensures content is correctly rendered by browsers, properly indexed by search engines, and accurately announced by screen readers. It also enables localization (l10n) — the translation and cultural adaptation of content for specific markets.

lang Attribute

The lang attribute declares the language of an element's content. It uses BCP 47 language tags — a language subtag (e.g., en for English, ar for Arabic) optionally followed by a region subtag (e.g., en-US, en-GB). The attribute is inherited, so setting it on <html> covers the entire document.

Language TagLanguageRegion
en-USEnglishUnited States
en-GBEnglishGreat Britain
zh-CNChinese (Simplified)China
zh-TWChinese (Traditional)Taiwan
es-MXSpanishMexico
pt-BRPortugueseBrazil
ar-SAArabicSaudi Arabia
ja-JPJapaneseJapan
fr-CAFrenchCanada
de-DEGermanGermany
lang-attribute.html
HTML
1<!-- Document-level language declaration -->
2<!DOCTYPE html>
3<html lang="en-US">
4<head>
5 <meta charset="UTF-8" />
6 <title>My Website</title>
7</head>
8<body>
9 <!-- Inline language override for a foreign phrase -->
10 <p>
11 The quick brown fox jumps over the lazy dog.
12 <span lang="fr">C'est la vie.</span>
13 The show must go on.
14 </p>
15
16 <!-- Language with region subtag -->
17 <p lang="en-GB">
18 The boot is full of petrol and the lorry is broken.
19 </p>
20
21 <!-- Chinese text with correct language -->
22 <p lang="zh-CN">
23 国际化为全球用户提供更好的体验。
24 </p>
25
26 <!-- Arabic text — note dir="rtl" is also needed -->
27 <p lang="ar" dir="rtl">
28 مرحبا بالعالم
29 </p>
30</body>
31</html>

best practice

Always set lang on the <html> element — it enables screen readers to use the correct pronunciation engine, helps search engines index content by language, and assists browser features like spell-check and translation prompts. Override it on inline elements when the language changes within a page.
dir Attribute

The dir attribute specifies the text direction of an element's content: ltr (left-to-right, default), rtl (right-to-left for Arabic, Hebrew, Persian), or auto (browser detects direction from content). Like lang, it is inherited and can be overridden on any element.

ValueDirectionTypical Scripts
ltrLeft to RightLatin, Cyrillic, Greek, CJK
rtlRight to LeftArabic, Hebrew, Persian, Urdu
autoAuto-detectUser-generated content with unknown direction
dir-attribute.html
HTML
1<!-- Document set to RTL for Arabic -->
2<html lang="ar" dir="rtl">
3<head>
4 <meta charset="UTF-8" />
5 <title>موقعي</title>
6</head>
7<body>
8 <h1>مرحبا بالعالم</h1>
9 <p>هذا هو موقعي الشخصي على الإنترنت.</p>
10
11 <!-- Mixed direction: English phrase within Arabic text -->
12 <p>
13 يمكنك زيارة
14 <span dir="ltr">example.com</span>
15 للمزيد من المعلومات.
16 </p>
17
18 <!-- Auto direction for user-generated content -->
19 <div dir="auto">
20 <!-- Browser detects direction from first strong character -->
21 مرحبا
22 </div>
23 <div dir="auto">
24 Hello there
25 </div>
26</body>
27</html>
28
29<!-- CSS logical properties respect dir -->
30<style>
31 .card {
32 margin-inline-start: 16px;
33 padding-inline: 12px;
34 border-inline-start: 2px solid #00FF41;
35 }
36</style>

Live preview showing RTL text handling alongside LTR content:

preview
🔥

pro tip

When styling RTL layouts, use CSS logical properties like margin-inline-start, padding-inline-end, and border-inline-start instead of physical properties (margin-left, padding-right). These automatically flip when dir="rtl" is set, eliminating the need for separate RTL stylesheets.
Charset & Encoding

Character encoding defines how characters are represented as bytes. UTF-8 (Unicode Transformation Format – 8-bit) is the universal standard, encoding over a million characters from every script in use today. The meta charset element declares the encoding to the browser before any text content is parsed.

charset.html
HTML
1<!-- Always use UTF-8 — the first line after <head> -->
2<!DOCTYPE html>
3<html lang="en">
4<head>
5 <meta charset="UTF-8" />
6 <title>Unicode Support: ñ, ü, 你, あ, ደማቅ</title>
7</head>
8<body>
9 <p>UTF-8 handles all scripts seamlessly:</p>
10 <ul>
11 <li>Latin: ñ, ü, à, ç, ø, ð</li>
12 <li>Greek: α, β, γ, δ, ε</li>
13 <li>Cyrillic: б, в, г, д, ж</li>
14 <li>CJK: 你好世界 (Chinese)</li>
15 <li>Japanese: こんにちは (Hiragana)</li>
16 <li>Arabic: مرحبا (Arabic)</li>
17 <li>Emoji: 🚀 🌍 🎉</li>
18 </ul>
19
20 <!-- HTML entities for reserved characters -->
21 <p>Use &amp;lt; for &lt; and &amp;gt; for &gt;.</p>
22 <p>Use &amp;amp; for &amp; itself.</p>
23 <p>Non-breaking space: 100&amp;nbsp;kg</p>
24</body>
25</html>
26
27<!-- Wrong: legacy encoding declaration -->
28<meta charset="ISO-8859-1" />
29<!-- This will mangle non-Latin characters -->

warning

The meta charset tag must appear within the first 1024 bytes of the document, ideally as the first child of <head>. If the browser encounters a different encoding declaration later, it must re-parse the document — causing a significant performance penalty. Always use UTF-8; legacy encodings like ISO-8859-1 or Shift_JIS should never be used for new projects.
Language Negotiation

Browsers send an Accept-Language HTTP header with every request, listing the user's preferred languages and their relative priority (quality values). Servers use this header to serve language-specific content — a process called content negotiation. The navigator.language JavaScript API provides read-only access to the user's preferred language on the client side.

language-negotiation.txt
TEXT
1// Accept-Language HTTP header (sent by browser)
2Accept-Language: en-US,en;q=0.9,fr;q=0.8,ar;q=0.7
3
4// Weighted preferences:
5// en-US — primary language (default weight 1.0)
6// en — fallback English (weight 0.9)
7// fr — French (weight 0.8)
8// ar — Arabic (weight 0.7)
9
10// Server-side negotiation (Node.js example)
11app.get('/', (req, res) => {
12 const lang = req.acceptsLanguages(['en', 'fr', 'ar', 'zh']);
13 // Respond with content in the best-matching language
14 res.render(`index-${lang}`);
15});
16
17// Client-side language detection
18const userLang = navigator.language; // "en-US"
19const userLangs = navigator.languages; // ["en-US", "en", "fr"]
20const isRTL = /^(ar|he|fa|ur)/.test(userLang);

info

Never rely solely on Accept-Language for language selection — users may be browsing in a language they don't read (e.g., a borrowed device, public terminal, or VPN). Always provide an explicit language switcher in the UI. Store the selected preference in a cookie, localStorage, or the user profile so it persists across sessions.
Date/Time Formatting

The <time> element marks up dates, times, or durations in a machine-readable format while displaying a human-readable version. The datetime attribute uses the ISO 8601 standard (YYYY-MM-DD, HH:MM, YYYY-MM-DDTHH:MM:SSZ). This enables calendar integration, search engine snippets, and locale-aware formatting via JavaScript.

datetime.html
HTML
1<!-- Date only -->
2<time datetime="2026-07-07">July 7, 2026</time>
3
4<!-- Time with timezone -->
5<time datetime="2026-07-07T14:30:00Z">
6 2:30 PM UTC
7</time>
8
9<!-- Time with offset -->
10<time datetime="2026-07-07T10:30:00-04:00">
11 10:30 AM Eastern
12</time>
13
14<!-- Duration -->
15<time datetime="PT2H30M">2 hours 30 minutes</time>
16
17<!-- Week -->
18<time datetime="2026-W28">Week 28 of 2026</time>
19
20<!-- Month -->
21<time datetime="2026-07">July 2026</time>
22
23<!-- Locale-aware display via JavaScript -->
24<time datetime="2026-07-07" class="locale-date">
25 July 7, 2026
26</time>
27
28<script>
29 // Convert to user's locale
30 document.querySelectorAll('.locale-date').forEach(el => {
31 const date = new Date(el.getAttribute('datetime'));
32 el.textContent = date.toLocaleDateString(
33 navigator.language,
34 { year: 'numeric', month: 'long', day: 'numeric' }
35 );
36 });
37</script>
38
39<!-- Multiple locales shown -->
40<ul>
41 <li><time datetime="2026-07-07" class="locale-full">Jul 7, 2026</time></li>
42</ul>
43<script>
44 const locales = ['en-US', 'fr-FR', 'de-DE', 'ja-JP', 'ar-SA'];
45 document.querySelectorAll('.locale-full').forEach(el => {
46 const d = new Date(el.getAttribute('datetime'));
47 el.textContent = locales
48 .map(l => `${l}: ${d.toLocaleDateString(l, { year:'numeric', month:'long', day:'numeric' })}`)
49 .join(' | ');
50 });
51</script>

Live preview showing date formatting across different locales:

preview
Number & Currency Formatting

Numbers, currencies, and percentages must be formatted according to regional conventions. JavaScript's Intl.NumberFormat API handles this natively — decimal separators, digit grouping, currency symbols, and percent notation vary dramatically across locales. HTML alone cannot localize numbers; the formatting must be done on the server or via client-side JavaScript.

number-currency.html
HTML
1<!-- Raw numeric data — no formatting in HTML -->
2<output id="price" data-value="1234567.89">$1,234,567.89</output>
3
4<!-- JavaScript locale-aware formatting -->
5<script>
6 const value = 1234567.89;
7
8 // Currency formatting per locale
9 const formats = [
10 { locale: 'en-US', style: 'currency', currency: 'USD' },
11 { locale: 'de-DE', style: 'currency', currency: 'EUR' },
12 { locale: 'ja-JP', style: 'currency', currency: 'JPY' },
13 { locale: 'ar-SA', style: 'currency', currency: 'SAR' },
14 { locale: 'en-IN', style: 'currency', currency: 'INR' },
15 ];
16
17 const rows = formats.map(f =>
18 `${f.locale}: ${new Intl.NumberFormat(
19 f.locale, f
20 ).format(value)}`
21 );
22 console.log(rows);
23 // en-US: $1,234,567.89
24 // de-DE: 1.234.567,89 €
25 // ja-JP: ¥1,234,568
26 // ar-SA: ١٬٢٣٤٬٥٦٧٫٨٩ ر.س.‏
27 // en-IN: ₹12,34,567.89
28
29 // Percentage formatting
30 const pct = new Intl.NumberFormat('en-US', {
31 style: 'percent',
32 maximumFractionDigits: 1,
33 }).format(0.856); // "85.6%"
34
35 // Compact notation
36 const compact = new Intl.NumberFormat('en-US', {
37 notation: 'compact',
38 compactDisplay: 'short',
39 }).format(1250000); // "1.3M"
40</script>
🔥

pro tip

Use Intl.NumberFormat instead of manual formatting — it correctly handles regional differences in decimal separators (dot vs comma), digit grouping (3-digit groups vs 2-digit for Indian numbering), currency symbol placement (prefix vs suffix), and Arabic/Hindu numeral systems. Never hardcode currency formatting logic.
Pluralization

Pluralization rules vary dramatically across languages. English has two forms (singular: "1 item", plural: "2 items"). Arabic has six forms (singular, dual, few, many, plural, zero). Russian and Polish have complex rules based on the last digits. JavaScript's Intl.PluralRules API determines which plural category a number belongs to for a given locale.

LanguageCategoriesExample
Englishone, other1 item, 2 items
Arabiczero, one, two, few, many, other0, 1, 2, 3-10, 11-99, 100+
Russianone, few, many, other1, 2-4, 5-20, 21+
JapaneseotherNo plural distinction
ChineseotherNo plural distinction
pluralization.html
HTML
1<!-- Pluralization with Intl.PluralRules -->
2<script>
3 function pluralize(locale, count, forms) {
4 const rules = new Intl.PluralRules(locale);
5 const category = rules.select(count);
6 return forms[category] || forms.other;
7 }
8
9 // English: two forms
10 const enForms = {
11 one: `${count} item`,
12 other: `${count} items`,
13 };
14
15 // Arabic: six forms
16 const arForms = {
17 zero: 'لا عناصر',
18 one: 'عنصر واحد',
19 two: 'عنصران',
20 few: `${count} عناصر`,
21 many: `${count} عنصراً`,
22 other: `${count} عنصر`,
23 };
24
25 // Russian: four forms
26 const ruForms = {
27 one: `${count} элемент`,
28 few: `${count} элемента`,
29 many: `${count} элементов`,
30 other: `${count} элемента`,
31 };
32
33 // Usage
34 console.log(pluralize('en', 1, enForms)); // "1 item"
35 console.log(pluralize('en', 5, enForms)); // "5 items"
36 console.log(pluralize('ar', 0, arForms)); // "لا عناصر"
37 console.log(pluralize('ar', 2, arForms)); // "عنصران"
38 console.log(pluralize('ru', 3, ruForms)); // "3 элемента"
39 console.log(pluralize('ru', 21, ruForms)); // "21 элемент"
40</script>

best practice

Never construct plural strings by appending an "s" to a noun — this only works in English. Use a localization library like ICU MessageFormat, i18next, or react-intl that handles plural rules per locale. For server-rendered content, use your backend framework's i18n system (e.g., Django i18n, Rails I18n, Next.js i18n).
Localized Strings

Localized strings are stored in message catalogs — key-value pairs where the key is a semantic identifier and the value is the translated text. These catalogs are typically organized as JSON files, one per locale. Variables, formatting, and pluralization are handled through interpolation and ICU message syntax.

localized-strings.json
JSON
1// en.json — English message catalog
2{
3 "greeting": "Hello, {name}!",
4 "welcome": "Welcome to our platform",
5 "items_count": "{count} item(s)",
6 "items_count_plural": {
7 "one": "{count} item",
8 "other": "{count} items"
9 },
10 "notification": "You have {unread, plural, one {# unread message} other {# unread messages}}",
11 "date_format": "{date, date, long}",
12 "temperature": "{value}°{unit}"
13}
14
15// fr.json — French message catalog
16{
17 "greeting": "Bonjour, {name} !",
18 "welcome": "Bienvenue sur notre plateforme",
19 "items_count_plural": {
20 "one": "{count} élément",
21 "other": "{count} éléments"
22 },
23 "notification": "Vous avez {unread, plural, one {# message non lu} other {# messages non lus}}",
24 "temperature": "{value} °{unit}"
25}
26
27// ar.json — Arabic message catalog
28{
29 "greeting": "مرحبا، {name}",
30 "welcome": "مرحباً بك في منصتنا",
31 "items_count_plural": {
32 "zero": "لا توجد عناصر",
33 "one": "عنصر واحد",
34 "two": "عنصران",
35 "few": "{count} عناصر",
36 "many": "{count} عنصراً",
37 "other": "{count} عنصر"
38 },
39 "temperature": "{value}°{unit}"
40}
localized-strings.html
HTML
1<!-- HTML template with localized strings (JS example) -->
2<!-- The actual rendering happens via a template engine or JS -->
3
4<!-- Data attributes approach for static HTML -->
5<div
6 data-i18n="greeting"
7 data-i18n-params='{"name": "Alice"}'
8>
9 Hello, Alice!
10</div>
11
12<!-- With a minimal i18n script -->
13<script>
14 const i18n = {
15 en: { greeting: 'Hello, {name}!' },
16 fr: { greeting: 'Bonjour, {name} !' },
17 ar: { greeting: 'مرحبا، {name}' },
18 };
19
20 function t(key, params = {}) {
21 const lang = document.documentElement.lang || 'en';
22 let msg = i18n[lang]?.[key] || key;
23 for (const [k, v] of Object.entries(params)) {
24 msg = msg.replace(`{${k}}`, v);
25 }
26 return msg;
27 }
28
29 // Translate all data-i18n elements
30 document.querySelectorAll('[data-i18n]').forEach(el => {
31 const key = el.dataset.i18n;
32 const params = JSON.parse(el.dataset.i18nParams || '{}');
33 el.textContent = t(key, params);
34 });
35</script>
36
37<!-- For production: use a library like i18next -->
38<script src="https://unpkg.com/i18next/i18next.min.js"></script>
39<script>
40 i18next.init({
41 lng: 'fr',
42 resources: {
43 fr: { translation: { welcome: 'Bienvenue' } }
44 }
45 });
46 document.getElementById('title').textContent =
47 i18next.t('welcome');
48</script>
hreflang

The hreflang attribute tells search engines about alternate language versions of a page. It is used in <link> elements within <head> or as an attribute on <a> links. This helps Google and other search engines serve the correct language version to users in search results.

hreflang.html
HTML
1<!-- Language alternates in <head> -->
2<head>
3 <link rel="alternate" hreflang="en" href="https://example.com/" />
4 <link rel="alternate" hreflang="fr" href="https://example.com/fr/" />
5 <link rel="alternate" hreflang="ar" href="https://example.com/ar/" />
6 <link rel="alternate" hreflang="ja" href="https://example.com/ja/" />
7 <link rel="alternate" hreflang="zh" href="https://example.com/zh/" />
8
9 <!-- x-default for language-agnostic or fallback page -->
10 <link rel="alternate" hreflang="x-default" href="https://example.com/" />
11
12 <!-- Region-specific: en-US vs en-GB -->
13 <link rel="alternate" hreflang="en-US" href="https://example.com/us/" />
14 <link rel="alternate" hreflang="en-GB" href="https://example.com/uk/" />
15</head>
16
17<!-- Inline hreflang on links -->
18<a href="/fr/" hreflang="fr" lang="fr">
19 Version française
20</a>
21<a href="/ar/" hreflang="ar" lang="ar" dir="rtl">
22 النسخة العربية
23</a>
24
25<!-- Self-referencing hreflang is mandatory -->
26<!-- Each language variant must link to all others, including itself -->
27<head>
28 <!-- On the English page: -->
29 <link rel="alternate" hreflang="en" href="https://example.com/en/" />
30 <link rel="alternate" hreflang="fr" href="https://example.com/fr/" />
31</head>
32<!-- On the French page: -->
33<head>
34 <!-- Must also link to both: -->
35 <link rel="alternate" hreflang="en" href="https://example.com/en/" />
36 <link rel="alternate" hreflang="fr" href="https://example.com/fr/" />
37</head>
38
39<!-- Alternate approach: HTML sitemap or HTTP headers -->
40<!-- Link: <https://example.com/en/>; rel="alternate"; hreflang="en" -->
41
42<!-- Language switcher with hreflang -->
43<nav aria-label="Language switcher">
44 <ul>
45 <li><a href="/en/" hreflang="en" lang="en" aria-current="page">English</a></li>
46 <li><a href="/fr/" hreflang="fr" lang="fr">Français</a></li>
47 <li><a href="/ar/" hreflang="ar" lang="ar" dir="rtl">العربية</a></li>
48 </ul>
49</nav>

warning

Every language variant must include bidirectional hreflang links to all other variants — including a self-referencing link. Missing or inconsistent hreflang annotations can confuse search engines, potentially causing all language variants to be deindexed. Use x-default for the fallback page shown to users whose language preferences don't match any specific variant.
Best Practices

Language & Direction Checklist

Always set lang on the <html> element — never omit it
Use BCP 47 tags with region subtags (en-US, en-GB) when content differs by region
Override lang on inline elements when the language changes mid-content
Always pair lang='ar', lang='he', lang='fa', lang='ur' with dir='rtl'
Use dir='auto' for user-generated content with unknown direction
Place <meta charset='UTF-8'> as the first element in <head>
Use CSS logical properties instead of physical properties for RTL/LTR switching
Provide a visible language switcher — never rely solely on Accept-Language
Use <time> with datetime attribute for all dates and times
Format numbers and currencies with Intl.NumberFormat, never with manual string concatenation
Use Intl.PluralRules or a library for pluralization — never append 's' to nouns
Include bidirectional hreflang links on every language variant
Store user language preference in a cookie, localStorage, or user profile

Related Topics

$Blueprint — Engineering Documentation·Section ID: HTML-31·Revision: 1.0