Internationalization Architecture
Frontend i18n architecture — locale routing, message catalogs, formatting, RTL, and loading strategies.
- 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
- SSR embed critical messages for first page
- Dynamic import
import(./locales/${locale}/checkout.json) - Cache catalogs in memory
- 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-startnotmargin-left - Icons that imply direction flip (chevrons)
- Screenshots in Storybook for
ar - Avoid hardcoded absolute positions
SEO
hreflangalternates 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
- One build multi-locale vs separate deploys
- ICU library size vs complete plural rules
- In-context translation tooling vs engineer velocity
- Machine translation for long-tail locales — quality risk
Common footguns
- Hardcoded English in components
- Sorting names with
.sort()withoutlocaleCompare - 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.”
Related on this site
- Design a Design System
- Accessibility in Large Apps
- SSR CSR Islands Architecture
- System Design Interview Framework