Internationalization (i18n)

Internationalization (i18n — "i", 18 letters, "n") is designing and building software so it can be adapted to different languages, regions, and cultural conventions without engineering changes. Localization (l10n) is the act of actually adapting it — translating text, formatting dates and currencies, adjusting layouts. i18n is the architecture; l10n is filling it in.

Getting this right is the difference between "we can add Spanish next quarter by handing translators a file" and "adding a language means rewriting the app." It's far more than translating strings: plural rules differ by language, dates and numbers format differently by region, some scripts run right-to-left, and text length varies wildly (German is famously long). Teams that bolt i18n on late pay dearly; teams that design for it from the start expand globally cheaply.

TL;DR

Quick Example

Externalized strings with a library, plus locale-aware formatting:

The key discipline: code references keys ("greeting"), never English text. Translation is then a data problem (edit JSON), not a code problem.

Core Concepts

i18n vs Localization vs Globalization

The crucial insight: i18n is a developer job done once; l10n is a content job done per language. Good i18n means adding a language costs a translation file, not a sprint.

Externalizing Strings

The foundational practice: no user-facing text in code. Every label, message, and error lives in translation resource files keyed by ID, loaded per the user's locale. This lets translators (who aren't engineers) work in their tools, enables translation-management platforms, and means the code path is identical across languages. A single hardcoded "Submit" in a component is an i18n bug — it can't be translated without a code change and redeploy.

Plurals and Grammar (Harder Than It Looks)

English has two plural forms (1 item / 2 items); many languages have more (Arabic has six; Russian and Polish have three with complex rules). Naive concatenation breaks:

ICU MessageFormat is the standard solution — a syntax for expressing plurals, gender, and selection that translators fill in per language's rules (shown in the example above). Every serious i18n library supports it. Also beware: word order, gendered nouns, and interpolation position all vary — never assemble sentences by concatenating translated fragments ("Deleted " + count + " files" puts the count in the wrong place in many languages). Translate whole messages with placeholders.

Formatting: Use Intl, Not Hand-Rolling

Dates, numbers, currencies, times, and relative times ("3 days ago") format differently everywhere — decimal separators, digit grouping, date order, calendar systems, currency symbol placement. JavaScript's built-in Intl API handles all of it correctly for hundreds of locales:

Hand-writing ${day}/${month}/${year} guarantees bugs (US wants month/day, most of the world wants day/month). Reach for Intl (or a library wrapping it) always.

Right-to-Left (RTL) and Layout

Arabic, Hebrew, Persian, and Urdu read right-to-left, which mirrors the entire layout — navigation, alignment, icons, progress direction. The modern approach is CSS logical properties (margin-inline-start instead of margin-left, text-align: start) plus dir="rtl" on the document — the browser mirrors automatically. Hardcoded left/right breaks RTL. Also budget for text expansion: translated text can be 30–50% longer (German) or shorter (Chinese) — designs with fixed-width buttons and no wrapping break under real translations.

The Ecosystem

You rarely build i18n from scratch — libraries handle loading, interpolation, plurals, and formatting:

Beyond libraries, translation management systems (Crowdin, Lokalise, Phrase, Weblate) are where translation actually happens at scale — they sync with your resource files, give translators context and screenshots, handle review workflows, and push updates back. For a serious multi-language product, the TMS is as important as the library.

Common Mistakes

Retrofitting i18n Late

The single most expensive mistake: launching with hardcoded English everywhere, then trying to internationalize a mature codebase. Every string, every concatenated sentence, every hardcoded date format is a change. Designing for i18n from the start (externalized strings, Intl formatting, logical CSS) costs little; retrofitting costs months.

Concatenating Translated Fragments

t("deleted") + " " + count + " " + t("files") assumes English word order and grammar and breaks in most languages. Translate complete messages with placeholders (t("deletedFiles", { count })) so translators can reorder and adjust grammar per language.

Hand-Rolling Date/Number Formatting

${d.getMonth()+1}/${d.getDate()}/${d.getFullYear()} is US-only and wrong for most of the world. Use Intl.DateTimeFormat/Intl.NumberFormat — they're built in, free, and correct for every locale.

Assuming Text Length and LTR

Designs that only fit short English text, or hardcode left/right, break when German doubles the length or Arabic mirrors the layout. Design flexible layouts (wrapping, min-width), use CSS logical properties, and test with a long language and an RTL language early.

Flags for Languages

Using country flags to select languages is a classic UX error — languages aren't countries (which flag for English? Spanish? Arabic is 20+ countries). Use language names in their own script ("Deutsch", "日本語"), not flags.

FAQ

What's the difference between i18n and localization?

i18n is the one-time engineering work of making the app capable of supporting multiple locales — externalized strings, flexible layout, locale-aware formatting. l10n is the ongoing work of adapting it to each specific locale — translation, regional formats, cultural adjustments. Developers do i18n; translators/localizers do l10n. Good i18n makes l10n cheap.

Do I need i18n if I'm only launching in English?

Design for it, even if you don't localize yet — externalize strings and use Intl formatting from the start. It costs little upfront and saves enormous pain if you ever expand. The mistake isn't "not translating yet"; it's architecting so that translating later requires a rewrite. Note: even English-only apps benefit from proper number/date formatting for en-GB vs en-US.

How do plurals work across languages?

Languages have between one and six plural categories (English: one/other; Arabic: six; Russian: three with numeric rules). You can't express this with if (count === 1). ICU MessageFormat lets translators specify the correct forms per language, and libraries pick the right one based on the count and locale. Always use it for anything involving quantities.

What is the Intl API?

A built-in JavaScript API (Intl.NumberFormat, Intl.DateTimeFormat, Intl.RelativeTimeFormat, Intl.PluralRules, Intl.ListFormat, Intl.Collator for sorting) that handles locale-aware formatting for hundreds of locales — no library or data download needed. It's the correct foundation for all formatting; libraries mostly wrap it with ergonomics.

How do I handle right-to-left languages?

Set dir="rtl" on the document for RTL locales and build layouts with CSS logical properties (inline-start/inline-end instead of left/right) so the browser mirrors automatically. Test with Arabic or Hebrew early — RTL bugs (misaligned layouts, wrong-direction icons) are pervasive if you design LTR-only.

Related Topics

References