Skip to main content

The Complete Guide to React Internationalization

From zero to multilingual: set up i18n in your React app, then automate translations with AI.

1

Install Packages

You need three packages: react-i18next (the React bindings), i18next (the core library), and optionally i18next-browser-languagedetector for automatic locale detection.

react-i18next provides React hooks and components. i18next is the core engine handling translation loading, interpolation, and pluralization. The language detector plugin reads the browser's language preference automatically.
Terminal
npm install react-i18next i18next i18next-browser-languagedetector i18next-http-backend
2

Configure the i18n Instance

Create an i18n configuration file that initializes i18next with your default language, translation resources, and plugin chain. This file must be imported at your app's entry point before any component renders.

src/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)  // Must come before .init()
  .init({
    fallbackLng: 'en',
    debug: process.env.NODE_ENV === 'development',
    interpolation: {
      escapeValue: false,  // React already escapes
    },
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json',
    },
  });

export default i18n;
"You will need to pass in an i18next instance by using initReactI18next" — this error means you forgot to call i18n.use(initReactI18next) before i18n.init(). The .use() call must come before .init().
3

Wrap Your App with I18nextProvider

Import your i18n config file at the app root and wrap your component tree with I18nextProvider. Without this, useTranslation() returns raw keys instead of translated text.

src/main.tsx
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n';  // Import your config
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Suspense fallback={<div>Loading...</div>}>
      <I18nextProvider i18n={i18n}>
        <App />
      </I18nextProvider>
    </Suspense>
  </React.StrictMode>
);
If translations show raw keys like "welcome" instead of "Welcome to our app", the most common cause is a missing I18nextProvider or not importing the i18n config file.
4

Create Translation Files

Create one JSON file per language. Use nested keys to organize strings by feature or page. Keep your source language (usually English) as the single source of truth.

public/locales/en/translation.json
// public/locales/en/translation.json
{
  "nav": {
    "home": "Home",
    "about": "About",
    "settings": "Settings"
  },
  "greeting": "Hello, {{name}}!",
  "cart": {
    "itemCount_one": "{{count}} item",
    "itemCount_other": "{{count}} items"
  }
}

// public/locales/de/translation.json
{
  "nav": {
    "home": "Startseite",
    "about": "Über uns",
    "settings": "Einstellungen"
  },
  "greeting": "Hallo, {{name}}!",
  "cart": {
    "itemCount_one": "{{count}} Artikel",
    "itemCount_other": "{{count}} Artikel"
  }
}
Name keys by what they describe, not where they appear: "cart.itemCount" is better than "homepageCartLabel". Keys should survive UI redesigns.
5

Use Translations in Components

Call useTranslation() in any component to get the t() function. Use it for simple strings, interpolated variables, and JSX-embedded translations with the Trans component.

Greeting.tsx
import { useTranslation } from 'react-i18next';

function Greeting({ userName }: { userName: string }) {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('greeting', { name: userName })}</h1>
      <nav>
        <a href="/">{t('nav.home')}</a>
        <a href="/about">{t('nav.about')}</a>
      </nav>
    </div>
  );
}
Trans component for JSX
import { Trans, useTranslation } from 'react-i18next';

// For JSX inside translations:
// "terms": "By signing up, you agree to our <link>Terms</link>."

function SignUp() {
  const { t } = useTranslation();
  return (
    <Trans i18nKey="terms" components={{
      link: <a href="/terms" className="underline" />
    }} />
  );
}
Dynamic keys like t(`error.$'{code}'`) work at runtime but cannot be statically extracted by tools like i18next-scanner. If you use extraction tools, list dynamic keys explicitly or use a comment hint.
6

Handle Plurals and Variables

i18next handles plurals using CLDR rules — not just singular/plural. Arabic has 6 forms (zero, one, two, few, many, other). Japanese has 1 (other). Define all required forms in your translation files and i18next selects the correct one automatically.

Plural forms by language
// English: 2 forms (one, other)
{
  "itemCount_one": "{{count}} item",
  "itemCount_other": "{{count}} items"
}

// Arabic: 6 forms (zero, one, two, few, many, other)
{
  "itemCount_zero": "لا عناصر",
  "itemCount_one": "عنصر واحد",
  "itemCount_two": "عنصران",
  "itemCount_few": "{{count}} عناصر",
  "itemCount_many": "{{count}} عنصرًا",
  "itemCount_other": "{{count}} عنصر"
}

// Japanese: 1 form (other)
{
  "itemCount_other": "{{count}}個のアイテム"
}
Never hardcode count === 1 for singular detection. Languages like French treat 0 as singular. Russian, Arabic, and Polish have forms English doesn't have. Let i18next handle the plural rules.
7

Add Language Switching and Detection

Build a language selector that calls i18n.changeLanguage(). Combine it with the browser language detector to auto-detect the user's preferred language on first visit, then persist their explicit choice.

LanguageSwitcher.tsx
import { useTranslation } from 'react-i18next';

const LANGUAGES = [
  { code: 'en', label: 'English' },
  { code: 'de', label: 'Deutsch' },
  { code: 'ja', label: '日本語' },
  { code: 'es', label: 'Español' },
];

function LanguageSwitcher() {
  const { i18n } = useTranslation();

  return (
    <select
      value={i18n.language}
      onChange={(e) => i18n.changeLanguage(e.target.value)}
    >
      {LANGUAGES.map(({ code, label }) => (
        <option key={code} value={code}>{label}</option>
      ))}
    </select>
  );
}
If you use SSR (Next.js, Remix), the server may detect a different language than the client (server has no browser preferences). This causes a hydration mismatch. Fix: pass the detected locale from the server to the client as a prop or cookie, so both render the same language.
8

Automate Translations

With your i18n setup complete, translate your locale files using AI. In your IDE, ask your AI assistant to translate your source file, or use the i18n Agent CLI in your CI/CD pipeline.

Terminal
# In your IDE, ask your AI assistant:
> Translate public/locales/en/translation.json to German, Japanese, and Spanish

✓ de/translation.json created (1.2s)
✓ ja/translation.json created (1.5s)
✓ es/translation.json created (1.1s)

# Or use the CLI in CI/CD:
npx i18n-agent translate public/locales/en/translation.json --lang de,ja,es
Translate incrementally — when you add new keys to your source file, translate just the diff rather than regenerating all files. This preserves any human-reviewed translations.

Automate Translation Quality

Catch missing keys and broken placeholders before they ship with i18n-validate. Test your UI with fake translations using i18n-pseudo before real translations arrive.

Common Pitfalls

Translations Show Raw Keys

Causes: I18nextProvider missing, i18n config not imported at app root, namespace not loaded, or translations still loading asynchronously. Check the browser console with debug: true for clues.

Suspense Error Without Fallback

"A component suspended while responding to synchronous input" — add a '&lt;Suspense&gt;' boundary around your app, or set useSuspense: false in the i18next init config.

SSR Hydration Mismatch

Server renders in one locale, client hydrates in another. Ensure both use the same locale source — pass it as a prop from the server, don't rely on browser detection alone.

No Autocomplete for Translation Keys

Augment the i18next module with your resource type: declare module 'i18next' '{ interface CustomTypeOptions { resources: typeof resources } }'. This gives you type-safe t() calls with autocomplete.

Recommended File Structure

Project Structure
my-react-app/
├── public/
│   └── locales/
│       ├── en/
│       │   ├── translation.json    # Default namespace
│       │   ├── common.json         # Shared strings
│       │   └── dashboard.json      # Feature namespace
│       ├── de/
│       │   ├── translation.json
│       │   ├── common.json
│       │   └── dashboard.json
│       └── ja/
│           └── ...
├── src/
│   ├── i18n.ts                     # i18n configuration
│   ├── main.tsx                    # App entry with Provider
│   ├── App.tsx
│   └── components/
│       └── LanguageSwitcher.tsx
└── package.json

Try i18n Agent Now

Drop your translation file here

JSON, YAML, PO, XML, CSV, Markdown, Properties

or click to browse

Target languages

No signup requiredInstant estimate

Frequently Asked Questions