Internationalization is one of those topics where senior engineers are expected to know more than the basics. By 2026 the tooling has matured: ICU MessageFormat, FormatJS, native CSS logical properties, and reliable RTL support. This guide is the working state of frontend i18n for production apps.
The four pillars of i18n
- Strings: translate text with proper plurals, gender, dates
- Layout: handle right-to-left languages
- Formatting: dates, numbers, currencies in user locale
- Routing: URL strategy for locale-aware sites
The string story: ICU MessageFormat
ICU is the standard for messages with parameters, plurals, and selects:
{count, plural,
=0 {No items}
one {# item}
other {# items}
}
Library: react-intl / FormatJS. Lingui is a competitor. Both convert ICU strings to React-renderable output.
Translation workflow
- Engineers write strings in source language (English)
- Strings extracted to a JSON or PO file
- Sent to translators (Lokalise, Crowdin, Phrase, in-house)
- Translated files synced back to repo
- Build bundles per-locale or loads on demand
RTL handling — the 2026 way
- CSS logical properties: use
margin-inline-startinstead ofmargin-left - RTL is a single CSS class away with logical properties
- Browser flips reading direction; you do not write LTR/RTL CSS twice
- Icons that need flipping (e.g., back arrow): scoped flip with a class
- Text alignment respects
text-align: startautomatically
What still needs work for RTL
- Hard-coded LTR layouts in some components — audit
- Numbers in flow with text — usually stay LTR even in RTL strings
- Mixed direction (English brand name in Arabic sentence) — handled by the browser given proper bidi
- Tables and complex layouts — test with real RTL content
Date and number formatting
- Use
Intl.DateTimeFormatandIntl.NumberFormatfrom the platform - Pass user locale; outputs locale-correct format
- For dates with relative formatting (“3 hours ago”), use date-fns or dayjs with locale plugins
- Currency:
Intl.NumberFormat(locale, {style: "currency", currency: "USD"})
The “no English in code” rule
Senior teams enforce: no string literals in JSX/TSX:
// Bad
<button>Save</button>
// Good
<button>{t("settings.save")}</button>
Lint rules can enforce this. Review reviewers should catch it.
Pluralization beyond English
- Russian: 3 forms (1, 2-4, 5+)
- Arabic: 6 forms
- Polish: complex
- ICU MessageFormat handles this declaratively per locale
Gender and contextual translation
- Some languages require gendered noun forms for the user being addressed
- ICU select solves this if you know the user’s gender preference
- Most apps default to gender-neutral; allow user preference where supported
Locale routing strategies
Path-based
/en/about,/fr/about,/ar/about- Cleanest URL; favored by Next.js, Astro, SvelteKit
- Can be SEO-friendly
Subdomain-based
en.example.com,fr.example.com- Used by Wikipedia and a few others
- Cookie scoping concerns; SSL cert per subdomain
Domain-based
example.comfor English,example.frfor France- Stronger geo-SEO signal
- Operational complexity
Query parameter
example.com/about?locale=fr- Worst SEO; bad for caching
- Avoid for user-facing apps
Locale detection
- 1st: explicit user choice (cookie / preference)
- 2nd: URL path
- 3rd:
Accept-Languageheader - 4th: IP-based geographic guess
- Allow override; persist user preference
SEO for international
hreflangtags pointing to alternate-locale versions- Sitemaps per locale
- Canonical tags
- Server-side rendering for the language-appropriate content
Bundle size considerations
- Per-locale bundles loaded on demand
- Or single bundle with all locales (fine for small string count)
- Server-side translation for SSR; client gets only the active locale
Long strings vs short strings
- German tends to be ~30% longer than English
- Buttons can break layout
- Test with longest-language proxies (German, Russian)
- Avoid fixed widths for translatable text
The currency landmines
- Decimal vs comma separators
- Currency-symbol position (€100 vs 100 EUR)
- Negative numbers (“-100” vs “(100)” in accounting)
- Always use
Intl.NumberFormat
Time zones
- Display dates in user time zone
- Server stores in UTC; client converts
- “Tomorrow at 3 PM” requires knowing the user’s zone
- For meetings: show in invitee’s zone, not host’s
What separates senior from staff
Senior candidates know ICU and Intl APIs. Staff candidates discuss the locale routing strategy and SEO. Principal candidates address the translation pipeline (extraction, review, ship), the bundle-size strategy, and the cross-team coordination of i18n vocabulary.
Frequently Asked Questions
react-intl or Lingui?
Both are mature. Lingui has slightly better DX with macros. react-intl (FormatJS) has the largest ecosystem. Pick by team familiarity.
Do I really need a translation management platform?
For 1–2 locales, JSON files in the repo work. For 5+, use a TMP (Lokalise, Crowdin, Phrase) — workflow scales beyond manual.
What about machine translation?
Useful for first drafts; human review needed for production. LLMs (Claude, GPT-4-class) produce better translations than older neural-MT for many languages.