What is i18n?i18n stands for internationalization. It's a process of designing and developing applications, websites, and other software in a way that they can be adapted to various languages, regions, and cultural conventions *without requiring engineering changes*.
Think of it as preparing your software for localization. i18n focuses on the technical architecture that *enables* localization. It involves: - Abstracting text and other locale-specific data: This means separating text, dates, numbers, currencies, etc., from the core code.
- Using character encoding that supports multiple languages: UTF-8 is the most common.
- Designing flexible layouts that can accommodate different text lengths:Some languages are more verbose than others.
- Handling date, time, and number formats correctly for different locales:This includes things like decimal separators, date order, and currency symbols.
- Supporting right-to-left languages, like Arabic or Hebrew.
- Considering cultural sensitivities: Colors, symbols, and images may have different meanings in different cultures.

In essence, i18n sets the stage for making your software usable in a global context. Structuring Multi-Language Contextual Metadata for DeliveryWhen delivering contextual metadata in multiple languages, you need a robust structure that allows you to easily manage and deliver the correct language version based on the user's locale. Here's how you can approach it: 1. Key-Value Pair Storage with Locale Identifiers: - Principle: The fundamental approach is to store each piece of contextual metadata (e.g., a description, a title, a label) as a key-value pair. The key is a unique identifier, and the value is a dictionary (or similar structure) containing the translations for different locales.
- Example (JSON):
- Explanation:
"product_description" and "product_title" are keys representing the data.- Each key's value is an object (dictionary).
- Within that object, the keys are locale identifiers (e.g.,
"en-US" for English (United States), "fr-FR" for French (France)). - The values associated with the locale identifiers are the translated strings.
2. Locale Identifiers (Language Tags): - Importance: Using standard language tags (following the BCP 47 specification) is *crucial*. This ensures consistent and accurate language identification.
- Structure: A language tag typically consists of:
language: (Required) A two- or three-letter ISO 639 language code (e.g., en, fr, de, es).script: (Optional) An ISO 15924 script code (e.g., Latn for Latin, Cyrl for Cyrillic).region: (Optional) An ISO 3166-1 alpha-2 country code (e.g., US, FR, DE, ES).variants: (Optional) Further specifies dialectal variations.
- Examples:
en-US: English (United States)fr-CA: French (Canada)zh-Hans: Chinese (Simplified)zh-Hant: Chinese (Traditional)
3. Data Storage Options: - JSON Files: Suitable for smaller datasets and simple configurations. The example above is JSON. Easy to read and edit manually.
- Property Files: Similar to JSON but uses a different syntax (key=value). Common in Java-based applications.
- Databases: Ideal for larger, more complex datasets. You can create tables with columns for the key, locale, and translated value.
- Content Management Systems (CMS): Many CMS platforms have built-in i18n and localization features that simplify the process of managing and delivering multilingual content.
- Translation Management Systems (TMS): Specialized tools for managing the translation workflow, storing translations, and ensuring consistency.
- Resource Bundles: In languages like Java, resource bundles provide a way to group locale-specific resources (text, images, etc.) into separate files.
How do we build the i18n structure?File Structure /i18n/
en.json
es.json
ko.json
index.html
app.js
Example of JSON dictionaries: en.json {
"app.title": "Hello, world!",
"greeting.named": "Hi, {name}!",
"inbox.count": "{count} message |||| {count} messages",
"date.today": "Today is {date}",
"errors.missing": "Missing translation: {key}"
}
ko.json {
"app.title": "안녕하세요!",
"greeting.named": "{name}님, 안녕하세요!",
"inbox.count": "{count}개 메시지",
"date.today": "오늘은 {date}",
"errors.missing": "번역이 없습니다: {key}"
}
Minimal HTML: index.html <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>i18n JSON Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<label>
Language:
<select id="lang">
<option value="en" selected>English</option>
<option value="es">Español</option>
<option value="ko">한국어</option>
</select>
</label>
<h1 data-i18n="app.title"></h1>
<p data-i18n="greeting.named" data-params='{"name":"Chun"}'></p>
<p>
<span data-i18n="inbox.count" data-params='{"count":1}'></span><br />
<span data-i18n="inbox.count" data-params='{"count":5}'></span>
</p>
<p data-i18n="date.today"></p>
<script src="app.js"></script>
</body>
</html>
Loader + translator: app.js const I18N = (() => {
const cache = new Map();
let current = "en";
let fallback = "en";
async function load(lang) {
if (!cache.has(lang)) {
const res = await fetch(`/i18n/${lang}.json`);
if (!res.ok) throw new Error(`Failed to load ${lang}`);
cache.set(lang, await res.json());
}
return cache.get(lang);
}
function interpolate(str, params = {}) {
return str.replace(/\{(\w+)\}/g, (_, k) => (params[k] ?? `{${k}}`));
}
// Very simple pluralization: split forms with "||||"
function pluralize(template, count) {
if (!template.includes("||||")) return template;
const [one, many] = template.split("||||").map(s => s.trim());
return count === 1 ? one : many;
}
function lookup(dict, key) {
return key.split(".").reduce((o, k) => (o ? o[k] : undefined), dict);
}
async function t(key, params = {}) {
const dict = await load(current);
let val = lookup(dict, key);
if (val == null) {
// try fallback
if (current !== fallback) {
const fb = await load(fallback);
val = lookup(fb, key);
}
}
if (val == null) {
const fb = await load(fallback);
val = lookup(fb, "errors.missing") || "Missing translation: {key}";
return interpolate(val, { ...params, key });
}
// handle pluralization if count provided
if (typeof params.count === "number") {
val = pluralize(val, params.count);
}
return interpolate(val, params);
}
async function setLanguage(lang) {
current = lang;
await load(lang);
await renderAll();
}
async function renderAll() {
const nodes = document.querySelectorAll("[data-i18n]");
for (const el of nodes) {
const key = el.getAttribute("data-i18n");
const params = JSON.parse(el.getAttribute("data-params") || "{}");
// example: dynamic date param
if (key === "date.today") {
params.date = new Date().toLocaleDateString(current);
}
el.textContent = await t(key, params);
}
}
return { t, setLanguage, renderAll, setFallback: f => (fallback = f) };
})();
window.addEventListener("DOMContentLoaded", async () => {
I18N.setFallback?.("en");
await I18N.setLanguage("en");
document.getElementById("lang").addEventListener("change", async e => {
await I18N.setLanguage(e.target.value);
});
});
Notes & tips- Pluralization: This demo uses a very simple
"one||||many" split. For production, consider proper CLDR rules (e.g., via Intl.PluralRules). - Interpolation:
{name} and {count} are replaced from data-params. You can pass any variables you need. - Fallbacks: If a key is missing in the active language, it tries the fallback (
en) and shows a missing string if still not found. - Server paths: Adjust
/i18n/${lang}.json if your files are served from a different path.
If you want to consolidate all the translation in a single JSON.Below example will help you to have better understanding: {
"en": {
"app": {
"title": "Hello, world!"
},
"greeting": {
"named": "Hi, {name}!"
},
"inbox": {
"count": "{count} message |||| {count} messages"
},
"date": {
"today": "Today is {date}"
},
"errors": {
"missing": "Missing translation: {key}"
}
},
"es": {
"app": {
"title": "¡Hola, mundo!"
},
"greeting": {
"named": "¡Hola, {name}!"
},
"inbox": {
"count": "{count} mensaje |||| {count} mensajes"
},
"date": {
"today": "Hoy es {date}"
},
"errors": {
"missing": "Falta traducción: {key}"
}
},
"ko": {
"app": {
"title": "안녕하세요!"
},
"greeting": {
"named": "{name}님, 안녕하세요!"
},
"inbox": {
"count": "{count}개 메시지"
},
"date": {
"today": "오늘은 {date}"
},
"errors": {
"missing": "번역이 없습니다: {key}"
}
}
}
When you use an element, you can use it like below: lang = "ko"
print( translation[ lang ][ "app" ][ "title" ] )
You can also flip the structure for better management, like below: {
"app.title": {
"en": "Hello, world!",
"es": "¡Hola, mundo!",
"ko": "안녕하세요!"
},
"greeting.named": {
"en": "Hi, {name}!",
"es": "¡Hola, {name}!",
"ko": "{name}님, 안녕하세요!"
},
"inbox.count": {
"en": "{count} message |||| {count} messages",
"es": "{count} mensaje |||| {count} mensajes",
"ko": "{count}개 메시지"
},
"date.today": {
"en": "Today is {date}",
"es": "Hoy es {date}",
"ko": "오늘은 {date}"
},
"errors.missing": {
"en": "Missing translation: {key}",
"es": "Falta traducción: {key}",
"ko": "번역이 없습니다: {key}"
}
}
Then you will be able to use the element like below: lang = "ko"
print( translation[ "app.title" ][ lang ]
|