Most apps start out in one language, usually English, because that's what the team speaks. But only about 25% of internet users are native English speakers. If you want the other 75% to feel at home in your product, you need to build with multiple languages in mind from the start. That's exactly what i18nWhat is i18n?Short for internationalization (18 letters between i and n) - structuring your code so it can support multiple languages and regions. is about.
What i18nWhat is i18n?Short for internationalization (18 letters between i and n) - structuring your code so it can support multiple languages and regions. and l10nWhat is l10n?Short for localization (10 letters between l and n) - the work of translating content and adapting formats for a specific language or region. actually mean
The terms look intimidating, but the concepts are simple. Think of i18n as building a house with empty picture frames on the walls. L10n is hanging the actual paintings, different paintings for each room (language). The frame structure only needs to be built once.
Internationalization (i18n) means setting up your code so it can swap text, formats, and layouts depending on the user's language. You do this once per project. Localization (l10n) means filling in the actual translated content for each language you want to support. You repeat l10n for every new language you add.
┌─────────────────────────────────────────┐
│ Internationalization │
│ (code structure, done once) │
└──────────────────┬──────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ English │ │ French │ │ Spanish │
│ (l10n) │ │ (l10n) │ │ (l10n) │
└──────────┘ └──────────┘ └──────────┘The three pillars of i18nWhat is i18n?Short for internationalization (18 letters between i and n) - structuring your code so it can support multiple languages and regions.
When you internationalize an app, there are three broad areas to handle. Miss any one of them and you'll end up with a partially localized experience that feels patchy to users.
Text and strings
The most obvious part: your UI copy needs to come from translation files rather than being hardcoded. This includes handling plurals correctly (different languages have very different plural rules), and interpolating dynamic values like usernames or counts into translated strings.
Formatting
Numbers, dates, and currencies look different depending on where you are. 1,234.56 in English is 1.234,56 in German. 12/20/2024 in the US is 20/12/2024 in France. These need to format automatically based on the user's localeWhat is locale?A code combining language and region (like fr-FR for French in France) that determines how dates, numbers, and currencies are formatted., not just their language.
Layout
Some languages flow right-to-left (Arabic, Hebrew, Persian). German text is often 30% longer than its English equivalent. Japanese uses completely different typographic conventions. Your layout needs to flex gracefully under these conditions.
Language reach by the numbers
Knowing which languages give you the most reach helps you prioritize which ones to support first.
| Language | Approximate internet users |
|---|---|
| English | 1.5 billion |
| Chinese | 1.1 billion |
| Hindi | 600 million |
| Spanish | 550 million |
| Arabic | 350 million |
| French | 300 million |
| Portuguese | 250 million |
| Russian | 250 million |
| German | 130 million |
| Japanese | 125 million |
Getting started: the three-step pattern
Every i18nWhat is i18n?Short for internationalization (18 letters between i and n) - structuring your code so it can support multiple languages and regions. implementation follows the same basic pattern regardless of which library you use. Understanding this pattern means you can pick up any i18n tool quickly.
Step 1: extract your text
The first move is getting all hardcoded strings out of your components and replacing them with calls to a translation function, usually named t.
// Before: text is locked in the component
function Header() {
return <h1>Welcome</h1>;
}
// After: text comes from a translation key
function Header() {
const { t } = useTranslation();
return <h1>{t('welcome')}</h1>;
}Step 2: create translation files
Each language gets its own file mapping keys to translated strings. The keys stay the same across all languages, only the values change.
// locales/en.json
{
"welcome": "Welcome",
"hello": "Hello, {{name}}!"
}
// locales/fr.json
{
"welcome": "Bienvenue",
"hello": "Bonjour, {{name}} !"
}Step 3: wire up the i18n library
Your chosen library reads the translation files and gives your components access to the t function. Here is what that initialization looks like with i18next, the most popular choice for JavaScript apps.
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n
.use(initReactI18next)
.init({
resources: {
en: { translation: enTranslations },
fr: { translation: frTranslations }
},
lng: 'en',
fallbackLng: 'en'
});Quick reference
| Term | Full name | Who does it | Frequency |
|---|---|---|---|
| i18n | Internationalization | Developer | Once per project |
| l10n | Localization | Translator + developer | Once per language |
| locale | Language + region code (e.g., fr-FR) | , | Used throughout |
| RTL | Right-to-left layout | Developer | Once per RTL language |
// Basic i18n structure without framework
// translations.js
const translations = {
en: {
welcome: 'Welcome',
hello: 'Hello, {{name}}!',
items_zero: 'No items',
items_one: 'One item',
items_other: '{{count}} items',
buttons: {
save: 'Save',
cancel: 'Cancel'
}
},
fr: {
welcome: 'Bienvenue',
hello: 'Bonjour, {{name}} !',
items_zero: 'Aucun article',
items_one: 'Un article',
items_other: '{{count}} articles',
buttons: {
save: 'Enregistrer',
cancel: 'Annuler'
}
},
es: {
welcome: 'Bienvenido',
hello: '¡Hola, {{name}}!',
items_zero: 'Ningún artículo',
items_one: 'Un artículo',
items_other: '{{count}} artículos',
buttons: {
save: 'Guardar',
cancel: 'Cancelar'
}
}
};
// i18n.js, Minimal system
class I18n {
constructor() {
this.locale = 'en';
this.translations = translations;
}
setLocale(locale) {
this.locale = locale;
document.documentElement.lang = locale;
}
t(key, options = {}) {
const keys = key.split('.');
let value = this.translations[this.locale];
for (const k of keys) {
value = value?.[k];
if (!value) break;
}
if (!value) {
console.warn(`Missing translation: ${key}`);
return key;
}
// Interpolation {{variable}}
return value.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return options[varName] !== undefined ? options[varName] : match;
});
}
// Simple pluralization
tp(key, count, options = {}) {
const pluralKey = `${key}_${this.getPluralForm(count)}`;
return this.t(pluralKey, { ...options, count });
}
getPluralForm(count) {
// Simplified rules
if (count === 0) return 'zero';
if (count === 1) return 'one';
return 'other';
}
}
const i18n = new I18n();
export default i18n;
// Usage
i18n.setLocale('fr');
console.log(i18n.t('welcome')); // 'Bienvenue'
console.log(i18n.t('hello', { name: 'Marie' })); // 'Bonjour, Marie !'
console.log(i18n.tp('items', 0)); // 'Aucun article'
console.log(i18n.tp('items', 5)); // '5 articles'