Frontend Engineering/
Lesson

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) │
└──────────┘ └──────────┘ └──────────┘
02

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.

03

Language reach by the numbers

Knowing which languages give you the most reach helps you prioritize which ones to support first.

LanguageApproximate internet users
English1.5 billion
Chinese1.1 billion
Hindi600 million
Spanish550 million
Arabic350 million
French300 million
Portuguese250 million
Russian250 million
German130 million
Japanese125 million
04

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.

json
// 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'
  });
Starting a new project? Set up i18n on day one. Retrofitting it into a large existing codebase means touching every file that contains UI text, a tedious and error-prone process.
05

Quick reference

TermFull nameWho does itFrequency
i18nInternationalizationDeveloperOnce per project
l10nLocalizationTranslator + developerOnce per language
localeLanguage + region code (e.g., fr-FR),Used throughout
RTLRight-to-left layoutDeveloperOnce per RTL language
javascript
// 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'