React i18n: The Complete Guide to Internationalization in 2025
Back to Blog
React i18n Tutorial Next.js

React i18n: The Complete Guide to Internationalization in 2025

Learn how to add multi-language support to React apps using react-i18next, react-intl, and Next.js built-in i18n. Includes best practices and translation management tips.

L
Litcode Team
React i18n: The Complete Guide to Internationalization in 2025

Adding internationalization to a React app is straightforward with the right tools. This guide covers the most popular approaches: react-i18next, react-intl (FormatJS), and Next.js’s built-in i18n—plus how to manage translations as your app scales.

Choosing Your i18n Library

react-i18next

The most popular choice. It’s flexible, well-documented, and works with any React setup.

Best for: Most React apps, especially those not using Next.js

react-intl (FormatJS)

Backed by the FormatJS project. Strong ICU message format support and good TypeScript integration.

Best for: Apps with complex pluralization/formatting needs

Next.js built-in i18n

Native routing-based internationalization for Next.js apps.

Best for: Next.js apps where you want locale-based routing

For this guide, we’ll focus on react-i18next as it’s the most widely used.

Setting Up react-i18next

Installation

npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector

Configuration

Create src/i18n/index.ts:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

i18n
  .use(HttpBackend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    supportedLngs: ['en', 'es', 'de', 'fr'],
    debug: process.env.NODE_ENV === 'development',
    
    interpolation: {
      escapeValue: false, // React already escapes
    },
    
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json',
    },
    
    detection: {
      order: ['localStorage', 'navigator'],
      caches: ['localStorage'],
    },
  });

export default i18n;

Import in your entry point (src/index.tsx or src/main.tsx):

import './i18n';

Create Translation Files

Create your locale files at public/locales/en/translation.json:

{
  "nav": {
    "home": "Home",
    "about": "About",
    "contact": "Contact"
  },
  "home": {
    "title": "Welcome to Our App",
    "subtitle": "Build something amazing",
    "cta": "Get Started"
  },
  "common": {
    "loading": "Loading...",
    "error": "Something went wrong",
    "retry": "Try Again"
  }
}

And public/locales/es/translation.json:

{
  "nav": {
    "home": "Inicio",
    "about": "Acerca de",
    "contact": "Contacto"
  },
  "home": {
    "title": "Bienvenido a Nuestra App",
    "subtitle": "Construye algo increíble",
    "cta": "Comenzar"
  },
  "common": {
    "loading": "Cargando...",
    "error": "Algo salió mal",
    "retry": "Intentar de Nuevo"
  }
}

Using Translations

The useTranslation Hook

import { useTranslation } from 'react-i18next';

function HomePage() {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('home.title')}</h1>
      <p>{t('home.subtitle')}</p>
      <button>{t('home.cta')}</button>
    </div>
  );
}

With Parameters

{
  "greeting": "Hello, {{name}}!",
  "items": "You have {{count}} items"
}
function Greeting({ name, itemCount }) {
  const { t } = useTranslation();
  
  return (
    <div>
      <p>{t('greeting', { name })}</p>
      <p>{t('items', { count: itemCount })}</p>
    </div>
  );
}

Pluralization

{
  "messages": "You have {{count}} message",
  "messages_plural": "You have {{count}} messages",
  "messages_zero": "You have no messages"
}
// Automatically picks the right form based on count
{t('messages', { count: messageCount })}

For complex pluralization (ICU format):

{
  "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}

The Trans Component

For translations with embedded JSX:

{
  "welcome": "Welcome to <strong>{{appName}}</strong>. Click <link>here</link> to continue."
}
import { Trans } from 'react-i18next';

function Welcome() {
  return (
    <Trans
      i18nKey="welcome"
      values={{ appName: 'MyApp' }}
      components={{
        strong: <strong />,
        link: <a href="/start" />
      }}
    />
  );
}

Language Switching

Create a language switcher component:

import { useTranslation } from 'react-i18next';

const languages = [
  { code: 'en', name: 'English', flag: '🇺🇸' },
  { code: 'es', name: 'Español', flag: '🇪🇸' },
  { code: 'de', name: 'Deutsch', flag: '🇩🇪' },
  { code: 'fr', name: 'Français', flag: '🇫🇷' },
];

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

  return (
    <select
      value={i18n.language}
      onChange={(e) => i18n.changeLanguage(e.target.value)}
    >
      {languages.map((lang) => (
        <option key={lang.code} value={lang.code}>
          {lang.flag} {lang.name}
        </option>
      ))}
    </select>
  );
}

Namespaces for Code Splitting

Large apps benefit from splitting translations by feature:

public/locales/en/
  ├── common.json      # Shared translations
  ├── auth.json        # Login/signup
  ├── dashboard.json   # Dashboard feature
  └── settings.json    # Settings feature

Load specific namespaces:

function Dashboard() {
  const { t } = useTranslation(['dashboard', 'common']);

  return (
    <div>
      {/* Uses dashboard namespace */}
      <h1>{t('dashboard:title')}</h1>
      
      {/* Uses common namespace */}
      <button>{t('common:save')}</button>
    </div>
  );
}

With React Suspense for loading states:

import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Dashboard />
    </Suspense>
  );
}

Next.js Integration

Next.js has built-in i18n routing. Combine it with react-i18next:

next.config.js

module.exports = {
  i18n: {
    locales: ['en', 'es', 'de', 'fr'],
    defaultLocale: 'en',
  },
};

Server-Side Translation Loading

For App Router (Next.js 13+):

// src/i18n/settings.ts
export const fallbackLng = 'en';
export const languages = ['en', 'es', 'de', 'fr'];

export function getOptions(lng = fallbackLng, ns = 'translation') {
  return {
    supportedLngs: languages,
    fallbackLng,
    lng,
    fallbackNS: 'translation',
    defaultNS: 'translation',
    ns,
  };
}
// src/i18n/server.ts
import { createInstance } from 'i18next';
import { initReactI18next } from 'react-i18next/initReactI18next';
import { getOptions } from './settings';

export async function getServerTranslation(lng: string, ns?: string) {
  const i18nInstance = createInstance();
  
  await i18nInstance
    .use(initReactI18next)
    .init({
      ...getOptions(lng, ns),
      resources: {
        [lng]: {
          [ns || 'translation']: await import(`../../public/locales/${lng}/${ns || 'translation'}.json`)
        }
      }
    });

  return {
    t: i18nInstance.t,
    i18n: i18nInstance,
  };
}

TypeScript Support

Create type-safe translations:

// src/types/i18n.d.ts
import 'react-i18next';
import translation from '../../public/locales/en/translation.json';

declare module 'react-i18next' {
  interface CustomTypeOptions {
    defaultNS: 'translation';
    resources: {
      translation: typeof translation;
    };
  }
}

Now you get autocomplete for translation keys!

Managing Translations at Scale

Once you have more than a handful of keys, managing JSON files becomes tedious. Here’s where tooling helps.

The Problem with Manual Management

  • Keys get out of sync between languages
  • Unused keys accumulate
  • New keys aren’t flagged for translation
  • No easy way to track translation progress

Using LangCtl with React

LangCtl integrates with react-i18next projects:

# Initialize
langctl init

# Scan for translation keys in your codebase
langctl scan ./src --format react-i18next

# See what needs translation
langctl status

# Push new keys to the dashboard
langctl push

# Pull translations back
langctl pull

The CLI detects:

  • t('key') calls
  • <Trans i18nKey="key"> components
  • Namespace usage

CI/CD Integration

Add to your pipeline:

# .github/workflows/i18n.yml
name: i18n Check

on: [pull_request]

jobs:
  check-translations:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Check for missing translations
        run: |
          npx langctl scan ./src
          npx langctl status --fail-missing

This fails the build if new keys haven’t been translated.

Best Practices

1. Use Meaningful Key Names

// ❌ Bad
{
  "text1": "Welcome",
  "btn": "Submit"
}

// ✅ Good
{
  "home.hero.title": "Welcome",
  "forms.submit_button": "Submit"
}

2. Keep Keys Flat-ish

Two to three levels of nesting is ideal:

{
  "feature.component.element": "Translation"
}

3. Provide Context for Translators

Use description comments in your code:

// i18n: Button shown after successful form submission
{t('forms.success_message')}

4. Handle Loading States

function App() {
  const { ready } = useTranslation();

  if (!ready) {
    return <LoadingSpinner />;
  }

  return <MainContent />;
}

5. Test with Pseudo-Localization

Add a “pseudo” locale that transforms English to catch hardcoded strings:

"Welcome" → "[Ŵéļçöɱé]"

If you see un-transformed text, it’s not going through i18n.

Common Mistakes

Hardcoding text “just for now”: It’s never just for now. Use keys from day one.

Forgetting about text expansion: German is often 30% longer than English. Design for it.

Ignoring RTL: If you might support Arabic or Hebrew, set up RTL CSS early.

Not handling missing translations: Show the key or fallback, don’t show blank strings.

Over-engineering namespaces: Start simple. Split namespaces when you actually need to.

Conclusion

React i18n is straightforward:

  1. Install react-i18next
  2. Set up translation files
  3. Use the useTranslation hook
  4. Add a language switcher
  5. Use a tool like LangCtl when you have multiple languages

The key is starting early. Adding i18n to an existing app is much harder than building with it from the start.

Have questions? Get in touch—we’ve built dozens of multilingual React apps and are happy to help.