Internationalization & Localization
Translate UI, emails, and errors across locales with correct pluralization, RTL support, locale-aware formatting, and a translation pipeline that scales past a few hand-edited JSON files.
Options
Translation Pipeline*
Message Format*
Locale Detection & Persistence*
Layout & Formatting Support
Decision Points
Ship multi-language UI at launch or English-only until demand?
If yes
Wire i18n primitives (message catalogs, `Intl` helpers, locale router) from day one — retrofitting later is a multi-week project. Even if you launch with only English in the catalog, the infrastructure should exist.
If no
English-only is a legitimate v1 choice, but avoid hardcoding strings in components — at minimum route them through a `t()` helper so swapping to a catalog is mechanical.
Use ICU MessageFormat for plurals/genders, or simple key/value?
If yes
Adopt ICU — FormatJS or i18next-icu. Plural categories differ wildly across languages (Arabic has 6, English has 2); rolling your own DSL leads to broken sentences in every non-English locale.
If no
Plain key/value works for English and a couple of Latin languages, but you'll hit the wall the moment you add Polish or Russian. Accept you'll migrate later.
Translate server-rendered errors and emails, or UI strings only?
If yes
Share the same catalog between client and server. API errors should return message keys (not pre-formatted strings) so the receiving client can localize them — or accept `Accept-Language` on the API.
If no
UI-only localization leaves emails, PDFs, and exports in English — acceptable early but becomes a visible gap once non-English users onboard in volume.
Bundle translations at build time or fetch per locale on demand?
If yes
Bundle if <20 locales and bundle size isn't a concern. Simpler, no runtime fetch failure mode, translations version with the code.
If no
Fetch on demand past 20+ locales or when the total catalog exceeds several hundred KB. Cache in the CDN and preload the user's preferred locale.
Community translations (Crowdin, Weblate) or paid LSP only?
If yes
Community + TMS is how open-source projects cover 30+ locales cheaply. Accept variable quality and add a review step before strings ship to prod.
If no
Paid LSPs (language service providers) give consistent quality and SLAs — right for regulated industries or customer-facing marketing copy.
Per-user locale override or auto-detect from browser/IP?
If yes
Store locale on the user account and honor it on every request. Never override an explicit choice — users who picked French don't want to see Spanish because they're on a business trip.
If no
Auto-detect from Accept-Language for signed-out traffic is fine; switch to stored preference the moment they log in.
Support right-to-left (RTL) layouts for Arabic/Hebrew?
If yes
Adopt logical CSS properties (`margin-inline-start`, `padding-inline-end`) and test with `dir="rtl"` on every screen. Mirror directional icons (arrows, chevrons); leave semantic ones (play, pause) alone.
If no
Skip RTL only if you genuinely will not ship to RTL markets. Retrofitting a large React app for RTL is a multi-sprint engagement.
Locale-aware number, date, and currency formatting everywhere?
If yes
Wrap every number, date, and currency display in `Intl.NumberFormat` / `Intl.DateTimeFormat` — not string concatenation. Lint for raw `toLocaleString()` without a locale argument.
If no
Inconsistent formatting is a credibility killer in finance, analytics, and commerce UIs. Don't skip this even if you ship one locale.
Translate user-generated content (on-the-fly MT) or leave as-is?
If yes
Wire a managed MT provider (Google, DeepL, Amazon Translate) with a 'Translate' button on posts/comments — machine-translate on click, cache per (content_id, target_locale). Never auto-translate silently.
If no
Leaving UGC in the author's language is the common choice; clearly label the content's language so TTS and screen readers behave correctly.
Store timezone per user, or infer per request?
If yes
Store IANA timezone (`America/New_York`, not an offset) on the user. Emails, digests, and scheduled notifications need a stable zone; per-request inference breaks batch jobs.
If no
Inferring from the browser per request works for stateless rendering but produces wrong times in emails and cron-driven features.
Fallback locale chain (e.g., pt-BR → pt → en) explicit or automatic?
If yes
Define the chain explicitly per locale. `pt-BR` should fall back to `pt`, then `en` — never to `es` because it's 'closer.' Most i18n libraries support this; configure it rather than relying on defaults.
If no
Automatic fallback (just strip region tags) is fine early but produces weird results (e.g., `zh-TW` falling through to `zh-CN`). Explicit chains avoid surprises.
Localize outbound email, SMS, and push content?
If yes
Send each user their stored locale in the outbound pipeline — template engine picks the right variant. Subject lines, button copy, and legal footers all need translation; don't ship English-only transactional mail to French users.
If no
English-only transactional mail is a frequent gap that produces support tickets. Prioritize this before the UI is 100% localized — users see email before the app.
Per-locale landing URLs for SEO (hreflang) or query param?
If yes
Use path-based (`/fr/`) or subdomain-based (`fr.example.com`) URLs with correct `hreflang` and `canonical` tags. Generate per-locale sitemaps. Required to rank in non-English markets.
If no
Query-string locales (`?lang=fr`) do not get indexed as separate pages. Acceptable only for authenticated apps without public marketing pages.
Per-region pricing and currency or one global price?
If yes
Store prices per currency in a price book (not computed from a single USD number); show the user's currency by default with an explicit override. Handle payment-provider currency rules — Stripe requires per-currency Price objects.
If no
Single-currency pricing (usually USD) is simpler but loses conversion in price-sensitive markets. Accept it's a deliberate tradeoff, not an oversight.
Fail CI on missing translation keys or ship with fallbacks?
If yes
Fail CI on missing keys in supported locales — prevents English strings leaking into French builds. Provide an escape hatch for "just-added" keys with a short grace period if needed.
If no
Fallback-to-source is kinder to release velocity but means untranslated strings silently ship. Pair with a daily report of coverage percentages per locale.
Tradeoffs
Every string change is a deploy; translator workflow is pull requests, which excludes non-engineers
Extra network request on language switch; first paint on a secondary locale is slower
Per-seat translator licensing plus ongoing sync/CI integration — real recurring cost, but the only way to scale past a few locales
Correct plurals and gender in every locale — trust and polish go up
Design, QA, and screenshot-test matrix double — every screen now has an LTR and RTL variant
Implementation Examples
Idiomatic i18n for Next.js App Router with ICU MessageFormat, locale routing, and server-component support.
Mature framework-agnostic i18n library with plugins for ICU, backend loading, and framework bindings.
React and plain-JS ICU MessageFormat tooling — `react-intl`, `@formatjs/intl`, and Babel/SWC extract plugins.
Translation management system with community and paid translator workflows, CI integration, and in-context editing.
Managed TMS with a strong API, branch-aware workflows, and figma/screenshot context for translators.