Quiz

How do you localize React applications?

Topics
ReactInternationalization

TL;DR

To localize a React application, you typically use a library like react-i18next or react-intl. First, you set up your translation files for different languages. Then, you configure the localization library in your React app. Finally, you use the provided hooks or components to display localized text in your components.

// Example using react-i18next
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
return <p>{t('welcome_message')}</p>;
};

Setting up localization in React

Localization combines message lookup with locale selection, plural rules, formatting, layout direction, and server/client consistency.

Choosing a localization library

There are several libraries available for localizing React applications, including react-i18next and react-intl. The examples below use react-i18next.

Installing the library

First, install the necessary packages:

npm install i18next react-i18next

Setting up translation files

Create JSON files for each language you want to support. For example, create en.json and fr.json in a locales directory:

locales/en.json:

{
"welcome_message": "Welcome to our application!"
}

locales/fr.json:

{
"welcome_message": "Bienvenue dans notre application!"
}

Configuring the localization library

Set up i18next and react-i18next in your application. Create an i18n.js file for the configuration:

// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './locales/en.json';
import fr from './locales/fr.json';
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
fr: { translation: fr },
},
lng: 'en', // default language
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React is safe from XSS by default — it escapes string children. Only `dangerouslySetInnerHTML` bypasses that, so don't pass translated HTML through it without sanitization.
},
});
export default i18n;

Integrating with your React application

Because initReactI18next already registers the i18n instance with React, you no longer need to wrap your tree in I18nextProvider (only use it when you want to scope a different instance to a subtree). Just import the config once at your entry and render with createRoot:

// index.js
import { createRoot } from 'react-dom/client';
import App from './App';
import './i18n'; // side-effect import: initializes i18next
const root = createRoot(document.getElementById('root'));
root.render(<App />);

Using translations in components

Use the useTranslation hook to access the t function for translating text:

// MyComponent.js
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
return <p>{t('welcome_message')}</p>;
};
export default MyComponent;

Switching languages

To switch languages, use the i18n.changeLanguage method:

// LanguageSwitcher.js
import { useTranslation } from 'react-i18next';
const LanguageSwitcher = () => {
const { i18n } = useTranslation();
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<div>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('fr')}>Français</button>
</div>
);
};
export default LanguageSwitcher;

Interpolation and variables

Most translations need to inject runtime values. Pass an options object as the second argument to t:

locales/en.json:

{
"greeting": "Hello, {{name}}!"
}
const { t } = useTranslation();
return <p>{t('greeting', { name: user.name })}</p>;

For rich text with embedded React elements (e.g. links inside a sentence), use the Trans component so translators can reorder children without breaking JSX:

import { Trans } from 'react-i18next';
<Trans i18nKey="terms">
By continuing you accept our <a href="/terms">terms</a>.
</Trans>;

Pluralization

i18next picks the correct plural form based on the active language's CLDR rules. Define keys with the _one, _other, _zero, _few, _many suffixes and pass count:

{
"items_zero": "No items",
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}
t('items', { count: cart.length });

Namespaces

Splitting translations into namespaces (one JSON file per feature) keeps bundles small and avoids key collisions. Load only the namespaces a component needs:

const { t } = useTranslation(['checkout', 'common']);
t('checkout:placeOrder');
t('common:cancel');

Lazy-loading translations

Shipping every locale to every user wastes bandwidth. Use i18next-http-backend to fetch translation files on demand and i18next-browser-languagedetector to pick the user's language automatically:

import i18n from 'i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
react: { useSuspense: true },
});

With Suspense mode enabled in the same initialization call, components can render a fallback while a translation file loads:

import { Suspense } from 'react';
<Suspense fallback={<Spinner />}>
<App />
</Suspense>;

Right-to-left (RTL) layout

For languages like Arabic or Hebrew, set the document direction when the language changes and let CSS logical properties (margin-inline-start, padding-inline-end, etc.) handle the rest:

useEffect(() => {
document.documentElement.dir = i18n.dir(); // 'ltr' or 'rtl'
document.documentElement.lang = i18n.language;
}, [i18n.language]);

Date, number, and currency formatting

Don't hand-format numbers or dates. Use the built-in Intl APIs, which respect the locale's conventions for separators, ordering, and currency symbols:

new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(1234.5); // "1.234,50 €"
const date = new Date('2026-04-23T00:00:00Z');
new Intl.DateTimeFormat('ja-JP', {
dateStyle: 'long',
timeZone: 'UTC',
}).format(date);
// "2026年4月23日"
new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-1, 'day');
// "yesterday"

i18next can call these via its built-in formatter: t('updated', { date, formatParams: { date: { dateStyle: 'long' } } }).

SSR and Next.js considerations

Server rendering adds request isolation and serialization constraints to the localization setup:

  • For Next.js App Router, use next-intl or i18next with the i18next/react-i18next SSR setup. Initialize i18next per request so the server doesn't leak one user's language to another.
  • Send the active language in the initial HTML (<html lang="...">) and serialize the loaded resources so the client can hydrate without a flash of untranslated content.
  • Server Components can translate after the request-specific i18n instance is initialized. Pass translated strings or other serializable data to Client Components; a t function cannot cross the Server-to-Client Component boundary because functions are not serializable.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

A React checkout page must support English, Arabic, and Japanese. Outline the component and data boundaries needed beyond replacing visible strings.