ESC

Type to search the knowledge base.

Internationalization Architecture

Frontend i18n architecture — locale routing, message catalogs, formatting, RTL, and loading strategies.

intermediate4 min read
  • system-design
  • interview
  • architecture
  • i18n

Scope the problem

In scope: locale detection/routing, message catalogs, date/number/currency formatting, RTL layout, split loading, SEO hreflang.

Out of scope: human translation TMS vendor comparison deep dive.

Requirements

Type Examples
Functional switch locale; all UI strings translated
Non-functional don’t bloat main bundle with 20 locales
Quality correct pluralization, RTL, no layout overflow

Architecture

Locale resolver → load messages → I18nProvider → components use t()
                              ↘ Intl formatters (date, number)

Locale identification

Strategy Example Notes
Path prefix /fr/products best for SEO
Subdomain fr.example.com ops cost
Cookie / accept-language app after login weaker SEO

Default fallback chain: fr-CA → fr → en.

// next-like
// app/[locale]/layout.tsx

<html lang={locale} dir={dir}> set correctly (dir="rtl" for ar/he).

Message catalogs

{
  "cart.items": "{count, plural, =0 {Empty} one {# item} other {# items}}",
  "cta.buy": "Buy now"
}

Use ICU message format for plurals/selects. Never concatenate sentences in code (t('hello') + name + t('!')) — translators need full strings.

Namespaces

common.json
checkout.json
admin.json

Load namespaces per route to shrink payloads.

Loading strategy

  1. SSR embed critical messages for first page
  2. Dynamic import import(./locales/${locale}/checkout.json)
  3. Cache catalogs in memory
  4. Prefetch next locale on language switcher hover

Avoid shipping all languages in main JS.

Formatting

Always Intl with explicit locale:

new Intl.NumberFormat(locale, { style: "currency", currency }).format(cents / 100);
new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(date);

Timezone: store UTC; display in user tz preference.

RTL & layout

  • Logical CSS properties: margin-inline-start not margin-left
  • Icons that imply direction flip (chevrons)
  • Screenshots in Storybook for ar
  • Avoid hardcoded absolute positions

SEO

  • hreflang alternates for each locale URL
  • Canonical per locale
  • Translated metadata titles/descriptions
  • Don’t cloack — same content policy

Pseudo-localization (QA)

Pipe strings through expander ([!! Ĥéłłö !!]) in CI builds to catch truncation and missing t() wrappers.

State & persistence

  • Store preferred locale in cookie (SSR readable)
  • User profile override when logged in
  • Consistency between email locale and UI

Performance

  • Message catalogs compressed; tree-shake unused namespaces
  • Avoid re-creating formatters every render — memo per locale
  • Fonts: subset per script (Latin/CJK); use unicode-range

Tradeoffs

  1. One build multi-locale vs separate deploys
  2. ICU library size vs complete plural rules
  3. In-context translation tooling vs engineer velocity
  4. Machine translation for long-tail locales — quality risk

Common footguns

  • Hardcoded English in components
  • Sorting names with .sort() without localeCompare
  • Currency wrong from browser locale vs price currency
  • Date-only parsed as UTC midnight shift

Interview close

Locale in URL → async message namespaces → ICU plurals → Intl formatters → RTL logical CSS → hreflang. Bundle only active locale catalogs; pseudo-loc for QA.

Routing & middleware sketch

// conceptual middleware
const locale = pathLocale(req) ?? cookieLocale(req) ?? acceptLanguage(req) ?? "en";
if (!pathLocale(req)) redirect(`/${locale}${req.path}`);

Keep marketing and app consistent: one locale strategy across both so users don’t bounce between /fr and cookie-only English.

Translator workflow

Engineers wrap strings early; never wait for “final copy.” Export ICU catalogs to the TMS; import reviewed JSON in CI. Block release if required keys missing for tier-1 locales (en, es, fr, …). Soft-warn for long-tail locales.

Numbers in the UI beyond Intl

  • Percentages and compact notation (notation: "compact")
  • List formatting (Intl.ListFormat)
  • Relative time (“3 hours ago”) via Intl.RelativeTimeFormat

Interview close add-on

Mention bidirectional isolation in CSS and message namespaces per route as the two highest-leverage engineering moves after “don’t concatenate strings.”

Further reading