Client-Side Tooling · 8 of 8

Internationalization (i18n)

Designing software so it can ship in any language, locale, currency, and writing direction without code changes. The hardest part isn't translation — it's everything around it: dates, numbers, plurals, sort order, layout direction.

i18nl10nICU MessageFormatIntl APIRTLUnicode
← Back to Client Side
Quick Facts

At a Glance

Basic Concepts

  • i18n = internationalization (18 letters between i and n): preparing software to support any locale.
  • l10n = localization: adapting it to a specific locale (translating, formatting, regional rules).
  • Locale = language + region tag, e.g. en-US, pt-BR, zh-Hant-TW. Drives all formatting decisions.
  • Translation key — UI code references a stable ID (checkout.placeOrder); a catalog maps it to text per locale.
  • RTL (right-to-left) — Arabic, Hebrew, Persian, Urdu. Mirrors layout, not just text.
Why It Matters

The Business Case

  • Less than 20% of the world speaks English. Most prefer to buy in their own language even when they understand English.
  • Adding a single major locale (German, Japanese, Spanish) often unlocks a market larger than the original.
  • Bolting i18n on later is one of the most expensive frontend retrofits — every hardcoded string, every new Date().toLocaleString(), every fixed-width layout.
  • Some markets require a localized product to launch (Quebec's Bill 96, China's regulations, EU consumer protection).
The Layers

What Actually Has to Be Localized

Strings

UI labels, microcopy, errors, emails. The visible part — but only ~30% of the work.

Numbers & Currency

1,234.56 (en-US) vs 1.234,56 (de-DE) vs 1 234,56 (fr-FR). Currency symbol position varies.

Dates & Times

MM/DD/YYYY vs DD/MM/YYYY vs YYYY-MM-DD. 12h vs 24h. Different calendars (Gregorian, Hijri, Japanese era).

Plurals

English has 2 forms (1, other). Russian has 4 (1, few, many, other). Arabic has 6.

Sort & Search

Locale-aware collation. ä sorts after z in Swedish, with a in German.

Layout Direction

RTL languages flip the entire UI — navigation, icons, progress bars, animations.

Names & Addresses

Family-name-first (Japan, Hungary). Postal-code formats. State/province vs prefecture vs canton.

Units & Imagery

Metric vs imperial. Culturally appropriate icons and stock photos. Avoid hand gestures and color taboos.

Mechanics

How Modern i18n Works

The Translation Pipeline
  1. Extract: tooling pulls every t('checkout.placeOrder') call into a source catalog (usually JSON or PO).
  2. Translate: catalogs sync to a TMS (Lokalise, Phrase, Crowdin, Transifex) where translators or LLMs work.
  3. Sync back: translated catalogs return to the repo, often via webhook or CI.
  4. Bundle: the build emits one chunk per locale; the runtime loads the active one.
  5. QA: pseudo-localization (réspéçt mode) and longest-string tests catch overflow.
ICU MessageFormat — The Lingua Franca

A pattern language Unicode standardized for handling plurals, gender, and selects without if-statements in code.

{count, plural,
  =0 {No messages}
  one {# new message}
  other {# new messages}
}

Translators fill in each plural category for their language without the developer caring how many categories that is.

The Intl API — Built into Every Browser

Modern JavaScript exposes ECMAScript Internationalization APIs natively. Use them before reaching for a library.

  • Intl.NumberFormat — currencies, units, scientific.
  • Intl.DateTimeFormat — dates, times, calendars.
  • Intl.RelativeTimeFormat — "3 days ago", "in 5 minutes".
  • Intl.PluralRules — plural categories per locale.
  • Intl.Collator — locale-aware sort & compare.
  • Intl.ListFormat — "A, B, and C" / "A, B et C".
  • Intl.Segmenter — word/sentence/grapheme boundaries (essential for CJK).
RTL — More Than Mirroring Text
  • Set <html dir="rtl"> — the browser flips inline direction automatically.
  • Use logical CSS properties (margin-inline-start not margin-left; padding-block not padding-top).
  • Mirror directional icons (back arrows, progress bars) — but not media controls or numbers.
  • Bidi text: a Hebrew sentence containing an English brand name needs Unicode bidi marks (&lrm;, &rlm;) to render correctly.
  • Test layouts with a long German string and Arabic on the same screen.
Routing & URLs
  • Path prefix (/en/about, /de/about) — most common; SEO-friendly.
  • Subdomain (de.example.com) — clean separation; harder for SEO consolidation.
  • ccTLD (.de, .fr) — strongest local SEO signal; expensive to manage.
  • Add hreflang tags so Google serves the right locale per region.
  • Detect locale from URL first, then user setting, then Accept-Language header — never solely from IP geolocation.
Translation Memory & Glossaries

A TMS keeps a translation memory (every prior translation) and a glossary (brand terms, product names that must stay untranslated). New strings are pre-filled from memory; reviewers confirm. This is what keeps cost predictable as the catalog grows.

LLM-Assisted Translation

Modern TMSes route strings through GPT/Claude with the glossary, brand voice, and surrounding context as the system prompt. Output goes to a human reviewer for high-visibility surfaces (legal, marketing) and ships unchecked for low-stakes microcopy. Cost per word has dropped roughly 10× since 2023.

Tooling

Libraries & Platforms

ToolLayerSweet spot
FormatJS / react-intlLibraryICU MessageFormat in React; the most rigorous option.
i18nextLibraryFramework-agnostic; huge plugin ecosystem; the JS default.
LinguiJSLibraryMacro-based extraction; great DX with TypeScript.
Vue i18nLibraryOfficial solution for the Vue ecosystem.
next-intl / next-i18nextLibraryNext.js App Router-aware i18n.
Angular i18n / @angular/localizeBuilt-inAngular's own pipeline, AOT-friendly.
Lokalise / Phrase / CrowdinTMSHosted translation management, integrations with Git/CI.
Transifex / SmartlingTMSEnterprise-grade workflows, big translator marketplaces.
Globalize, date-fns/locale, LuxonFormattingFor projects targeting older browsers without full Intl.
Picking

Choosing an Approach

Just-in-case (1 language today)

Use Intl.* APIs from day one. Extract strings into a catalog file even if there's only one. The retrofit cost is huge.

2–5 languages

i18next or react-intl + a TMS like Lokalise. JSON catalogs in the repo, sync via CI.

10+ languages, large surface

FormatJS for ICU rigor, dedicated TMS, dedicated localization PM, pseudo-loc in CI, LLM pre-translation.

RTL markets

Logical CSS properties everywhere, mirrored design system, RTL visual regression tests.

Pitfalls

Common Anti-Patterns

  • String concatenation: "You have " + n + " items" — impossible to translate cleanly. Use a single message with a placeholder.
  • Sentence-fragment translation — translators need full sentences with context, not "Save" floating in space.
  • Hardcoded pluralsn === 1 ? 'item' : 'items' breaks in Russian, Arabic, Polish.
  • Fixed-width buttons — German is ~30% longer than English; layout must flex.
  • Date strings parsed by hand instead of Intl.DateTimeFormat.
  • IP-based locale forcing — a German tourist in Spain doesn't want Spanish.
  • Time zones treated as locale — they're independent. Asia/Tokyo is not the same as ja-JP.
  • Unicode unaware string ops"👨‍👩‍👧".length === 8; use Intl.Segmenter.
Continue

Other Client-Side Tooling