Frontend Internationalization: i18n, RTL, and Locale-Aware UI

Internationalization (i18n) is one of those frontend areas where the surface looks simple (“just translate the strings”) but the depth surprises engineers. Pluralization, RTL layouts, locale-aware formatting, dynamic content — interviews probe whether you understand the gotchas.

The basics

i18n consists of:

  • Translation: strings in different languages
  • Locale-aware formatting: dates, numbers, currency, lists
  • Layout direction: LTR (English, French) vs RTL (Arabic, Hebrew)
  • Pluralization rules: cat / cats vs Polish 5 forms
  • Time zones

i18n libraries

  • react-intl (FormatJS): mature, ICU MessageFormat support, broad ecosystem
  • react-i18next / i18next: simpler API, large community
  • Lingui: compile-time optimization, smaller bundles
  • Next-intl: built for Next.js App Router

For most projects: react-i18next or Lingui in 2026.

ICU MessageFormat

The standard for parameterized + pluralized translations:

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

Handles plural forms across languages without bespoke logic.

Locale-aware formatting

Use Intl APIs (built into browsers):

  • Intl.NumberFormat for numbers and currency
  • Intl.DateTimeFormat for dates
  • Intl.ListFormat for lists (“apple, banana, and cherry”)
  • Intl.RelativeTimeFormat for “3 days ago” style

Don’t hardcode formats. ${date.toLocaleDateString()} is locale-aware automatically.

RTL support

For Arabic, Hebrew, Persian, Urdu, the layout flows right-to-left:

  • Set dir="rtl" on the html element
  • Use logical CSS properties (margin-inline-start, padding-block-end) instead of physical
  • Mirror icons that have direction (arrows, back buttons)
  • Numbers can stay LTR even in RTL text

Bonus: many bidi (mixed direction) edge cases. Use the Unicode Bidi Algorithm via the browser; do not try to override.

Translation workflow

Common tooling:

  • Source format: ICU MessageFormat strings in source code
  • Extraction: automated tooling pulls strings into a translation file (JSON, YAML, PO)
  • TMS: Translation Management System like Lokalise, Phrase, or Crowdin
  • Re-import: translated strings imported back into the codebase
  • Validation: CI catches missing translations, mismatched placeholders

String IDs vs natural-language keys

Two patterns:

  • String IDs: “checkout.button.confirm” — explicit, but disconnected from actual text
  • Natural language as key: “Confirm checkout” — reads naturally, but renaming sources of strings means re-translating

Modern tools (Lingui, Next-intl) often use natural language as the key with hashing for stability.

Variable interpolation

Strings often have parameters:

"Welcome, {name}!"

Translate the template, not the result. The translator decides the position of the placeholder; word order varies between languages.

Pluralization gotchas

Languages have wildly different plural systems:

  • English: 2 forms (1 vs everything else)
  • French: 2 forms but with different boundaries (0 and 1 are singular)
  • Arabic: 6 forms based on count
  • Polish: 4 forms with complex rules

ICU MessageFormat handles this. Don’t hand-roll plural logic with conditionals.

Loading translations

  • Inline (bundle all languages): simple, but bundle bloats
  • Code-split per locale: load only the user’s language
  • Server-side rendering with the right locale baked in (Next.js, Remix do this)

Time zones

Always store dates in UTC + IANA time zone identifier (“America/New_York”). Render in the user’s local time zone unless the event is explicitly in another zone (e.g., a flight from JFK).

Use date-fns-tz, Luxon, or date-fns with built-in time zone support.

Common mistakes

  • Concatenating translated fragments (“You have ” + count + ” messages”) — breaks word order
  • Hardcoded date formats (not locale-aware)
  • Physical CSS properties (margin-left, padding-right) — break in RTL
  • Translating the wrong parts (button text translated, status messages forgotten)
  • No pseudo-localization in development to catch hardcoded strings

Pseudo-localization

A debugging mode where strings are converted to look “translated” but are still readable: “Account [Áççôûñţ]”. Catches hardcoded strings before they ship.

Frequently Asked Questions

How important is i18n for a US-only app?

Less critical, but still useful. Date and number formatting respect user locale, accessibility scales internationally, and “growing into i18n later” is harder than starting with it.

Should I use Google Translate to ship quickly?

For production, no. Mistakes are common, idioms break, and quality affects brand. Use professional translation for shipped copy.

How do I test RTL?

Set dir="rtl" in DevTools. Spot-check icons, layout, and any custom CSS. Add to E2E tests if RTL is a supported locale.

Scroll to Top