How do you localize a React app?
By FrontendPro Editorial Team Updated 9/26/2026
Answer
Localization is more than translating strings. You need a message catalogue keyed by locale, locale-aware formatting for dates, numbers and currency, plural rules, and a way to select and persist the user's locale.
The translation layer
Use a library — react-i18next, next-intl, or Lingui. They all work the same way: JSON per locale, a hook to look up keys.
// en.json { "greeting": "Hello, {name}", "items": "{count, plural, one {# item} other {# items}}" }
const t = useTranslations();
t('greeting', { name });
t('items', { count: 3 }); // "3 items" — the library applies the plural rule
Key by meaning, not by English text: cart.empty, not "Your cart is empty". When the English copy changes you want the key to survive.
Never concatenate a sentence
{t('you-have')} {count} {t('messages')} // broken in most languages
Word order differs, and so does agreement. Pass the whole sentence with placeholders and let ICU MessageFormat handle it. The same applies to embedded markup — use a rich-text placeholder rather than splitting the sentence around a <a>.
Plurals are not two cases
English has two forms; Arabic has six, Polish three with non-obvious rules, Vietnamese one. That is exactly why you delegate to Intl.PluralRules through the library instead of writing count === 1 ? a : b.
Formatting comes free from Intl
new Intl.DateTimeFormat(locale, { dateStyle: 'long' }).format(d);
new Intl.NumberFormat(locale, { style: 'currency', currency: 'VND' }).format(n);
new Intl.RelativeTimeFormat(locale).format(-3, 'day');
new Intl.Collator(locale).compare(a, b); // sorting — never plain <
No library needed, and it is correct for the separator, the currency position, and the calendar.
Routing and SEO
Put the locale in the URL — /vi/about, /en/about — not only in a cookie, so each language is a distinct, crawlable, shareable page. Then emit hreflang alternates and set <html lang>. A locale held only in client state means Google sees one page and users cannot share a link in their language.
Detect an initial locale from Accept-Language, but let an explicit choice override it and persist that in a cookie. Never redirect a user who has already chosen.
The rest of the checklist
- RTL. For Arabic and Hebrew set
dir="rtl"and use logical CSS properties —margin-inline-start, notmargin-left— so the layout mirrors without a second stylesheet. - Bundle size. Load one locale's catalogue, not all of them; split by route if the catalogue is large.
- Layout. German runs about 30% longer than English. Fixed-width buttons break.
- Pseudo-localization in development ("[Ħëľľö]") exposes hardcoded strings and truncation before a translator ever sees the app.
In this project's terms
With the Next.js App Router, next-intl plus a [locale] segment gives you server-rendered translations with no client bundle for the catalogue, and the locale is part of the route, which is what makes the SEO work.
