Internationalization and Localization
1. Language Declaration and BCP 47 Tags
| Attribute/Tag | Syntax | Description | Example |
|---|---|---|---|
| lang (HTML element) | <html lang="en"> |
Primary language of the entire document | <html lang="en-US"> - English (United States) |
| lang (on any element) | <span lang="fr">Bonjour</span> |
Override language for specific content section | Multi-language pages, foreign quotes |
| hreflang (links) | <link rel="alternate" hreflang="es"> |
Language of linked resource | Alternate language versions for SEO |
| BCP 47 Language Tag | language-Script-REGION-variant |
Standard format for language identification | zh-Hans-CN - Chinese, Simplified, China |
| Primary Language | 2-3 letter code (ISO 639) | Base language identifier | en, fr, de, ja, ar |
| Script Subtag | 4-letter code (ISO 15924) | Writing system specification | Hans (Simplified), Hant (Traditional), Latn (Latin) |
| Region Subtag | 2-letter or 3-digit code | Country/region specification | US, GB, CN, BR |
Example: Language declaration with BCP 47 tags
<!DOCTYPE html>
<!-- Primary language: English (United States) -->
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>Multilingual Website</title>
<!-- Alternate language versions for SEO -->
<link rel="alternate" hreflang="en-US" href="https://example.com/en-us/">
<link rel="alternate" hreflang="en-GB" href="https://example.com/en-gb/">
<link rel="alternate" hreflang="fr-FR" href="https://example.com/fr/">
<link rel="alternate" hreflang="de-DE" href="https://example.com/de/">
<link rel="alternate" hreflang="es-ES" href="https://example.com/es/">
<link rel="alternate" hreflang="ja-JP" href="https://example.com/ja/">
<link rel="alternate" hreflang="zh-Hans-CN" href="https://example.com/zh-cn/">
<link rel="alternate" hreflang="zh-Hant-TW" href="https://example.com/zh-tw/">
<link rel="alternate" hreflang="ar-SA" href="https://example.com/ar/">
<link rel="alternate" hreflang="x-default" href="https://example.com/">
</head>
<body>
<p>Welcome to our website.</p>
<!-- Foreign language quote -->
<blockquote lang="fr-FR">
<p>La vie est belle.</p>
</blockquote>
<!-- Mixed language content -->
<p>The Japanese word <span lang="ja">こんにちは</span> means "hello".</p>
<!-- Arabic text section -->
<section lang="ar-SA" dir="rtl">
<h2>مرحبا</h2>
<p>هذا نص باللغة العربية</p>
</section>
</body>
</html>
| Common Language Codes | BCP 47 Tag | Description | Script/Region Notes |
|---|---|---|---|
| English (US) | en-US | American English | Default English variant |
| English (UK) | en-GB | British English | Different spelling (colour vs color) |
| Spanish (Spain) | es-ES | Castilian Spanish | Differs from Latin American Spanish |
| Spanish (Mexico) | es-MX | Mexican Spanish | Different vocabulary and idioms |
| Chinese (Simplified) | zh-Hans-CN | Mainland China | Hans = Simplified characters |
| Chinese (Traditional) | zh-Hant-TW | Taiwan | Hant = Traditional characters |
| Arabic (Saudi Arabia) | ar-SA | Saudi Arabic | RTL text direction required |
| Portuguese (Brazil) | pt-BR | Brazilian Portuguese | Distinct from European Portuguese |
| French (Canada) | fr-CA | Canadian French | Different from France French |
SEO Tip: Use
hreflang="x-default" to specify the default version when no language
matches. Always include bidirectional links (page A links to B, page B links to A) for proper SEO. Google
requires consistency across all language versions.
2. Text Direction (LTR, RTL) and BiDi Support
| Attribute | Value | Description | Languages |
|---|---|---|---|
| dir="ltr" | Left-to-Right | Default text direction for most languages | English, French, Spanish, German, Chinese, Japanese, Korean |
| dir="rtl" | Right-to-Left | Text flows from right to left | Arabic, Hebrew, Persian, Urdu |
| dir="auto" | Automatic direction | Browser determines direction from first strong character | User-generated content, mixed-direction text |
| BiDi Isolation | <bdi> element |
Isolate text with different directionality | Usernames, product names in RTL contexts |
| BiDi Override | <bdo dir="rtl"> element |
Force specific text direction | Override browser's direction algorithm |
| CSS direction | direction: rtl; |
CSS property for text direction | Styling RTL layouts |
| Logical Properties | margin-inline-start |
Direction-agnostic CSS properties | Adapts to LTR/RTL automatically |
Example: RTL and BiDi text handling
<!-- RTL document (Arabic) -->
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>موقع عربي</title>
<style>
/* Direction-agnostic CSS using logical properties */
.card {
margin-inline-start: 1rem; /* Left in LTR, Right in RTL */
margin-inline-end: 1rem; /* Right in LTR, Left in RTL */
padding-inline: 2rem; /* Horizontal padding */
border-inline-start: 2px solid blue; /* Left border in LTR */
}
</style>
</head>
<body>
<h1>مرحبا بكم</h1>
<p>هذا نص يُكتب من اليمين إلى اليسار</p>
<!-- Embedding LTR text (English) in RTL context -->
<p>
المؤلف هو <bdi>John Smith</bdi> من أمريكا
</p>
<!-- User-generated content with auto direction -->
<div class="comment" dir="auto">
<!-- Browser detects direction from first strong character -->
<p>This will be LTR</p>
</div>
<div class="comment" dir="auto">
<p>هذا سيكون RTL</p>
</div>
</body>
</html>
<!-- LTR document with embedded RTL content -->
<html lang="en" dir="ltr">
<body>
<h1>Learning Arabic</h1>
<!-- RTL section in LTR document -->
<section lang="ar" dir="rtl">
<h2>الأرقام العربية</h2>
<p>١، ٢، ٣، ٤، ٥</p>
</section>
<!-- Bidirectional text with isolation -->
<p>User <bdi>أحمد</bdi> has 15 messages.</p>
<!-- Force direction override (rare use case) -->
<p>Display in reverse: <bdo dir="rtl">ABCDEF</bdo> becomes FEDCBA</p>
</body>
</html>
| CSS Logical Property | Physical Equivalent (LTR) | Physical Equivalent (RTL) | Use Case |
|---|---|---|---|
| margin-inline-start | margin-left |
margin-right |
Start margin (direction-aware) |
| margin-inline-end | margin-right |
margin-left |
End margin (direction-aware) |
| padding-inline-start | padding-left |
padding-right |
Start padding |
| border-inline-start | border-left |
border-right |
Start border |
| inset-inline-start | left |
right |
Positioning (absolute/fixed) |
| text-align: start | text-align: left |
text-align: right |
Direction-aware alignment |
| text-align: end | text-align: right |
text-align: left |
Opposite side alignment |
RTL Common Pitfalls: Avoid using physical properties (left/right) in CSS for international
sites. Use logical properties (inline-start/inline-end) instead. Icons and arrows may need to be flipped in RTL.
Test with actual RTL content, not just reversing English text.
3. Character Encoding and Unicode Support
| Encoding | Syntax | Description | Coverage |
|---|---|---|---|
| UTF-8 | <meta charset="UTF-8"> |
Universal character encoding (recommended) | All languages, emojis, symbols |
| UTF-16 | <meta charset="UTF-16"> |
16-bit Unicode encoding | Less efficient than UTF-8 for web |
| ISO-8859-1 (Latin-1) | <meta charset="ISO-8859-1"> |
Western European languages only | Legacy - use UTF-8 |
| HTML Entities (Decimal) | € |
Numeric character reference (€) | Any Unicode character by code point |
| HTML Entities (Hex) | € |
Hexadecimal character reference (€) | Same as decimal, hex format |
| Named Entities | € © |
Named character references | Limited set of common characters |
| Unicode Escape (CSS/JS) | \20AC (CSS), \u20AC (JS) |
Unicode escape sequences | In stylesheets and scripts |
Example: Character encoding and special characters
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ALWAYS declare UTF-8 encoding first -->
<meta charset="UTF-8">
<title>Unicode Characters</title>
</head>
<body>
<!-- Direct UTF-8 characters (recommended with UTF-8) -->
<p>Currency: € £ ¥ ₹ ₽</p>
<p>Symbols: © ® ™ § ¶ † ‡</p>
<p>Math: × ÷ ± ≠ ≈ ∞ √ ∑</p>
<p>Arrows: ← → ↑ ↓ ↔ ⇒ ⇔</p>
<p>Emojis: 😀 😂 ❤️ 👍 🌟 🎉</p>
<!-- HTML entities (decimal) -->
<p>Euro: € (€)</p>
<p>Copyright: © (©)</p>
<p>Heart: ❤ (❤)</p>
<!-- HTML entities (hexadecimal) -->
<p>Euro: € (€)</p>
<p>Em dash: — (—)</p>
<p>Bullet: • (•)</p>
<!-- Named entities -->
<p>€ © ® ™</p>
<p>< > & " '</p>
<p> — – …</p>
<!-- Diacritics and accents -->
<p>French: café, naïve, résumé</p>
<p>German: Grüße, Äpfel, Größe</p>
<p>Spanish: ñ, á, é, í, ó, ú</p>
<!-- Non-Latin scripts -->
<p lang="el">Greek: Ελληνικά</p>
<p lang="ru">Russian: Русский</p>
<p lang="ar" dir="rtl">Arabic: العربية</p>
<p lang="he" dir="rtl">Hebrew: עברית</p>
<p lang="hi">Hindi: हिन्दी</p>
<p lang="th">Thai: ไทย</p>
<p lang="ja">Japanese: 日本語</p>
<p lang="ko">Korean: 한국어</p>
<p lang="zh">Chinese: 中文</p>
</body>
</html>
| Common HTML Entities | Named Entity | Decimal | Hex | Character |
|---|---|---|---|---|
| Non-breaking space | |   |
  |
|
| Em dash | — | — |
— |
— |
| En dash | – | – |
– |
– |
| Ellipsis | … | … |
… |
… |
| Left quote | “ | “ |
“ |
" |
| Right quote | ” | ” |
” |
" |
| Apostrophe | ’ | ’ |
’ |
' |
| Bullet | • | • |
• |
• |
Best Practice: Always use UTF-8 encoding. Direct Unicode characters are preferred over entities
when using UTF-8. Use entities only for HTML-reserved characters (<, >, &, ") or when forced to
use legacy encodings.
4. Date, Time, and Number Formatting
| Element/Attribute | Syntax | Description | Use Case |
|---|---|---|---|
| <time> element | <time datetime="2025-12-22">Dec 22, 2025</time> |
Machine-readable date/time with human-readable display | Events, publication dates, timestamps |
| datetime attribute | datetime="2025-12-22T14:30:00Z" |
ISO 8601 format for dates and times | Search engines, screen readers, scripts |
| Intl.DateTimeFormat (JS) | new Intl.DateTimeFormat('en-US') |
JavaScript API for locale-specific formatting | Dynamic date display based on user locale |
| Intl.NumberFormat (JS) | new Intl.NumberFormat('de-DE') |
Format numbers according to locale | Prices, quantities, percentages |
| Intl.RelativeTimeFormat (JS) | new Intl.RelativeTimeFormat('en') |
Format relative time ("2 days ago") | Social media timestamps, activity feeds |
| input type="date" | <input type="date"> |
Native date picker with locale formatting | Forms with date selection |
| input type="time" | <input type="time"> |
Native time picker | Appointment scheduling, time entry |
Example: Date, time, and number formatting
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Localized Formatting</title>
</head>
<body>
<!-- Time element with various formats -->
<p>Published: <time datetime="2025-12-22T14:30:00Z">December 22, 2025 at 2:30 PM</time></p>
<p>Event date: <time datetime="2025-12-31">New Year's Eve 2025</time></p>
<p>Duration: <time datetime="PT2H30M">2 hours 30 minutes</time></p>
<!-- Form with date/time inputs -->
<form>
<label>Appointment Date: <input type="date" name="date"></label>
<label>Appointment Time: <input type="time" name="time"></label>
<label>Birth Month: <input type="month" name="birth-month"></label>
<label>Week: <input type="week" name="week"></label>
</form>
<!-- JavaScript locale-aware formatting -->
<script>
const date = new Date('2025-12-22T14:30:00Z');
// Date formatting for different locales
const usDate = new Intl.DateTimeFormat('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
}).format(date); // December 22, 2025
const ukDate = new Intl.DateTimeFormat('en-GB', {
year: 'numeric', month: 'long', day: 'numeric'
}).format(date); // 22 December 2025
const frDate = new Intl.DateTimeFormat('fr-FR', {
year: 'numeric', month: 'long', day: 'numeric'
}).format(date); // 22 décembre 2025
const jpDate = new Intl.DateTimeFormat('ja-JP', {
year: 'numeric', month: 'long', day: 'numeric'
}).format(date); // 2025年12月22日
// Number formatting
const price = 1234567.89;
const usPrice = new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD'
}).format(price); // $1,234,567.89
const euPrice = new Intl.NumberFormat('de-DE', {
style: 'currency', currency: 'EUR'
}).format(price); // 1.234.567,89 €
const jpPrice = new Intl.NumberFormat('ja-JP', {
style: 'currency', currency: 'JPY'
}).format(price); // ¥1,234,568 (no decimals)
// Relative time formatting
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(2, 'day'); // "in 2 days"
rtf.format(-3, 'month'); // "3 months ago"
// Percentage formatting
const percent = 0.456;
const usPercent = new Intl.NumberFormat('en-US', {
style: 'percent', minimumFractionDigits: 1
}).format(percent); // 45.6%
// Unit formatting
const distance = 1234;
const usDistance = new Intl.NumberFormat('en-US', {
style: 'unit', unit: 'kilometer'
}).format(distance); // 1,234 km
</script>
<!-- Display formatted values -->
<div id="date-display"></div>
<div id="number-display"></div>
</body>
</html>
| Locale | Date Format | Time Format | Number Format | Currency Symbol |
|---|---|---|---|---|
| en-US | MM/DD/YYYY | 12-hour (AM/PM) | 1,234.56 | $ (before) |
| en-GB | DD/MM/YYYY | 24-hour | 1,234.56 | £ (before) |
| de-DE | DD.MM.YYYY | 24-hour | 1.234,56 | € (after) |
| fr-FR | DD/MM/YYYY | 24-hour | 1 234,56 | € (after, space) |
| ja-JP | YYYY/MM/DD | 24-hour | 1,234.56 | ¥ (before, no decimals) |
| zh-CN | YYYY-MM-DD | 24-hour | 1,234.56 | ¥ (before) |
| ar-SA | DD/MM/YYYY | 12-hour (AM/PM) | 1,234.56 | ر.س (after) |
5. Font Selection for Multiple Languages
| Technique | Implementation | Description | Coverage |
|---|---|---|---|
| System Font Stack | font-family: system-ui, -apple-system |
Use system default fonts for best language coverage | All platforms, all scripts |
| Unicode Range (@font-face) | unicode-range: U+0600-06FF; |
Load specific font for character range (Arabic) | Optimize loading for specific scripts |
| Google Fonts (Noto Family) | Noto Sans, Noto Serif |
Comprehensive multi-script font family | 800+ languages, all scripts |
| Fallback Fonts | sans-serif, serif, monospace |
Generic font families as final fallback | Guaranteed rendering (browser default) |
| Language-Specific Fonts | :lang() CSS selector | Apply different fonts based on element language | Optimized typography per language |
| Variable Fonts | font-variation-settings | Single font file with multiple styles | Reduces HTTP requests, bandwidth |
Example: Multi-language font configuration
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Multi-Language Fonts</title>
<style>
/* System font stack for broad language support */
body {
font-family:
system-ui, /* Modern system default */
-apple-system, /* macOS/iOS */
BlinkMacSystemFont, /* Chrome on macOS */
'Segoe UI', /* Windows */
Roboto, /* Android, Chrome OS */
'Noto Sans', /* Cross-platform Unicode */
'Helvetica Neue', /* Legacy macOS */
Arial, /* Fallback */
sans-serif, /* Generic fallback */
'Apple Color Emoji', /* Emoji support */
'Segoe UI Emoji',
'Noto Color Emoji';
}
/* Arabic-specific font */
:lang(ar) {
font-family: 'Noto Naskh Arabic', 'Arabic Typesetting', 'Simplified Arabic', serif;
}
/* Chinese fonts (Simplified) */
:lang(zh-Hans),
:lang(zh-CN) {
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
/* Chinese fonts (Traditional) */
:lang(zh-Hant),
:lang(zh-TW) {
font-family: 'Noto Sans TC', 'PingFang TC', 'Microsoft JhengHei', sans-serif;
}
/* Japanese fonts */
:lang(ja) {
font-family: 'Noto Sans JP', 'Hiragino Sans', 'Yu Gothic', 'Meiryo', sans-serif;
}
/* Korean fonts */
:lang(ko) {
font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
}
/* Hebrew fonts */
:lang(he) {
font-family: 'Noto Sans Hebrew', 'Arial Hebrew', 'David', serif;
}
/* Devanagari (Hindi, Sanskrit) */
:lang(hi),
:lang(sa) {
font-family: 'Noto Sans Devanagari', 'Mangal', 'Nirmala UI', sans-serif;
}
/* Thai fonts */
:lang(th) {
font-family: 'Noto Sans Thai', 'Leelawadee UI', 'Thonburi', sans-serif;
}
/* @font-face with unicode-range for optimized loading */
@font-face {
font-family: 'WebFont';
src: url('latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F;
}
@font-face {
font-family: 'WebFont';
src: url('arabic.woff2') format('woff2');
unicode-range: U+0600-06FF, U+FE70-FEFF; /* Arabic block */
}
@font-face {
font-family: 'WebFont';
src: url('chinese.woff2') format('woff2');
unicode-range: U+4E00-9FFF; /* CJK Unified Ideographs */
}
</style>
<!-- Google Fonts with multiple scripts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&family=Noto+Sans+Arabic:wght@400;700&family=Noto+Sans+JP:wght@400;700&family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
<h1>Multi-Language Typography</h1>
<p lang="en">English: The quick brown fox jumps over the lazy dog.</p>
<p lang="ar" dir="rtl">Arabic: الثعلب البني السريع يقفز فوق الكلب الكسول</p>
<p lang="zh-Hans">Chinese (Simplified): 快速的棕色狐狸跳过懒狗</p>
<p lang="zh-Hant">Chinese (Traditional): 快速的棕色狐狸跳過懶狗</p>
<p lang="ja">Japanese: 速い茶色のキツネが怠惰な犬を飛び越える</p>
<p lang="ko">Korean: 빠른 갈색 여우가 게으른 개를 뛰어넘습니다</p>
<p lang="hi">Hindi: तेज भूरी लोमड़ी आलसी कुत्ते के ऊपर कूदती है</p>
<p lang="th">Thai: สุนัขจิ้งจอกสีน้ำตาลที่รวดเร็วกระโดดข้ามสุนัขที่ขี้เกียจ</p>
<p lang="he" dir="rtl">Hebrew: השועל החום המהיר קופץ מעל הכלב העצלן</p>
</body>
</html>
| Script/Language | Recommended Fonts | Unicode Range | Special Considerations |
|---|---|---|---|
| Latin | Roboto, Open Sans, Inter, system-ui | U+0000-00FF | Most web fonts support Latin |
| Arabic | Noto Naskh Arabic, Amiri, Cairo | U+0600-06FF | Requires contextual shaping, RTL support |
| Chinese (Simplified) | Noto Sans SC, Source Han Sans SC | U+4E00-9FFF | Large font files (~5-10MB), use subsetting |
| Japanese | Noto Sans JP, Source Han Sans JP | U+3040-309F (Hiragana), U+30A0-30FF (Katakana) | Includes Kanji (U+4E00-9FFF) |
| Korean | Noto Sans KR, Spoqa Han Sans | U+AC00-D7AF (Hangul) | 11,172 syllable blocks |
| Devanagari (Hindi) | Noto Sans Devanagari, Poppins | U+0900-097F | Complex ligatures, combining marks |
| Thai | Noto Sans Thai, Sukhumvit | U+0E00-0E7F | Tone marks, complex rendering |
| Emoji | Noto Color Emoji, Apple Color Emoji | U+1F300-1F9FF | Color fonts (SVG, CBDT, COLR) |
Performance Tip: Use
font-display: swap to prevent invisible text during font
loading. For CJK fonts (Chinese, Japanese, Korean), subset fonts to reduce file size from 10MB+ to under 1MB by
including only commonly used characters.
6. Content Localization Strategies
| Strategy | Implementation | Description | Best For |
|---|---|---|---|
| Separate HTML Files | /en/page.html, /fr/page.html |
Different HTML file per language | Static sites, complete translations |
| URL Path Strategy | example.com/en/, example.com/fr/ |
Language in URL path | SEO-friendly, clear separation |
| Subdomain Strategy | en.example.com, fr.example.com |
Language as subdomain | Large sites, regional content |
| Query Parameter | example.com?lang=fr |
Language via query string | Poor SEO - avoid |
| Cookie/LocalStorage | Store user language preference | Remember choice, load appropriate content | User preference persistence |
| Accept-Language Header | Server detects browser language | Auto-redirect based on browser settings | Initial language detection |
| JavaScript i18n Libraries | i18next, FormatJS, Globalize | Client-side translation and formatting | SPAs, dynamic content |
| Server-Side Rendering | Template engines with translation | Generate localized HTML on server | SEO, initial load performance |
Example: Complete localization implementation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Localized Website</title>
<!-- hreflang for all language versions -->
<link rel="alternate" hreflang="en" href="https://example.com/en/">
<link rel="alternate" hreflang="fr" href="https://example.com/fr/">
<link rel="alternate" hreflang="de" href="https://example.com/de/">
<link rel="alternate" hreflang="ja" href="https://example.com/ja/">
<link rel="alternate" hreflang="x-default" href="https://example.com/">
</head>
<body>
<!-- Language selector -->
<nav aria-label="Language selection">
<select id="language-selector" onchange="changeLanguage(this.value)">
<option value="en" selected>English</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
<option value="ja">日本語</option>
<option value="zh">中文</option>
<option value="ar">العربية</option>
</select>
</nav>
<!-- Content with data attributes for translation -->
<h1 data-i18n="welcome">Welcome</h1>
<p data-i18n="description">This is a localized website.</p>
<!-- Dynamic content with placeholders -->
<p data-i18n="greeting" data-i18n-options='{"name":"John"}'>
Hello, John!
</p>
<!-- Date/time with locale formatting -->
<p>
<time datetime="2025-12-22T14:30:00Z" id="event-time"></time>
</p>
<!-- Price with currency formatting -->
<p class="price" data-amount="1234.56" data-currency="USD"></p>
<script>
// Translation data (normally loaded from JSON files)
const translations = {
en: {
welcome: 'Welcome',
description: 'This is a localized website.',
greeting: 'Hello, {{name}}!'
},
fr: {
welcome: 'Bienvenue',
description: 'Ceci est un site web localisé.',
greeting: 'Bonjour, {{name}}!'
},
de: {
welcome: 'Willkommen',
description: 'Dies ist eine lokalisierte Website.',
greeting: 'Hallo, {{name}}!'
},
ja: {
welcome: 'ようこそ',
description: 'これはローカライズされたウェブサイトです。',
greeting: 'こんにちは、{{name}}さん!'
}
};
// Detect user's language
function detectLanguage() {
// 1. Check localStorage
const stored = localStorage.getItem('preferredLanguage');
if (stored) return stored;
// 2. Check browser language
const browserLang = navigator.language.split('-')[0];
if (translations[browserLang]) return browserLang;
// 3. Default to English
return 'en';
}
// Change language
function changeLanguage(lang) {
// Save preference
localStorage.setItem('preferredLanguage', lang);
// Update HTML lang attribute
document.documentElement.lang = lang;
// Update text direction for RTL languages
if (lang === 'ar' || lang === 'he') {
document.documentElement.dir = 'rtl';
} else {
document.documentElement.dir = 'ltr';
}
// Translate all elements
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
let text = translations[lang][key];
// Handle placeholders
const options = el.getAttribute('data-i18n-options');
if (options) {
const vars = JSON.parse(options);
Object.keys(vars).forEach(k => {
text = text.replace(`{{${k}}}`, vars[k]);
});
}
el.textContent = text;
});
// Format date/time
const eventTime = document.getElementById('event-time');
const date = new Date(eventTime.getAttribute('datetime'));
eventTime.textContent = new Intl.DateTimeFormat(lang, {
year: 'numeric', month: 'long', day: 'numeric',
hour: 'numeric', minute: 'numeric'
}).format(date);
// Format price
document.querySelectorAll('.price').forEach(el => {
const amount = parseFloat(el.getAttribute('data-amount'));
const currency = el.getAttribute('data-currency');
el.textContent = new Intl.NumberFormat(lang, {
style: 'currency', currency: currency
}).format(amount);
});
}
// Initialize on page load
const currentLang = detectLanguage();
document.getElementById('language-selector').value = currentLang;
changeLanguage(currentLang);
</script>
</body>
</html>
Localization Best Practices:
- Store translations in separate JSON files, not in code
- Use ICU MessageFormat for complex plurals and gender
- Never concatenate translated strings
- Leave space for text expansion (German +35%, French +20%)
- Translate alt text, title attributes, error messages
- Consider cultural differences (colors, images, gestures)
Common Pitfalls:
- Hard-coding text in HTML/JS instead of using translation keys
- Forgetting to translate meta tags, Open Graph data
- Using flags to represent languages (flag ≠ language)
- Not accounting for text direction (RTL/LTR)
- Ignoring date/number/currency formatting differences
- Assuming all users speak English as fallback
Resources: Use Google Translate API, DeepL, or professional translators for quality. Test with
tools like BrowserStack for international browsers. Consider Crowdin, Phrase, or Lokalise for translation
management. Follow W3C Internationalization guidelines (w3.org/International).