Localize a React app: complete guide with Localingos

If you have a React app and want to ship it in more than English, this guide walks you from zero to a fully localized build in about 20 minutes. We'll use react-i18next as the runtime library (it's the most mature React i18n option in 2026) and Localingos as the automated translation backend that keeps your locale files in sync as English copy evolves.

By the end you'll have: detection of the user's preferred language, persistence across sessions, interpolation with variables, plural-form correctness across 56 locales, and a CI step that automatically pulls new translations on every push.

Why automate React localization

The hardest part of React i18n isn't react-i18next — that's a one-day setup. The hard part is keeping es.json, de.json, fr.json, and every other locale file in sync as your English copy changes weekly. Teams that do this manually (CSV exports, agency emails, hand-edited JSON) burn engineering time on every release and ship missing-key errors to users.

Localingos solves the second problem: you maintain en.json, push it, and we return the other 55 locales with placeholders intact, plural forms correct per CLDR rules, and your brand glossary respected. No more "Spanish version of the app is two sprints behind."

Step 1 — Install Localingos and react-i18next

npm install react-i18next i18next i18next-browser-languagedetector
npm install -g @localingos/cli

Authenticate the CLI with your project's API key (one-time):

localingos login
# paste API key from https://www.localingos.com/dashboard

Step 2 — Wire react-i18next

Create src/i18n/index.ts:

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

import en from './en.json';
import es from './es.json';
import de from './de.json';
import fr from './fr.json';

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: en },
      es: { translation: es },
      de: { translation: de },
      fr: { translation: fr },
    },
    fallbackLng: 'en',
    interpolation: { escapeValue: false },
    detection: {
      order: ['localStorage', 'navigator'],
      caches: ['localStorage'],
    },
  });

export default i18n;

Import once in src/index.tsx:

import './i18n';

Create src/i18n/en.json — this is your source of truth:

{
  "welcome": "Welcome, {{name}}",
  "cart_one": "{{count}} item in your cart",
  "cart_other": "{{count}} items in your cart"
}

Step 3 — Configure Localingos

Run localingos init in your project root. It creates localingos.json:

{
  "projectId": "your-project-id",
  "source": {
    "path": "src/i18n/en.json",
    "locale": "en"
  },
  "targets": {
    "path": "src/i18n/{locale}.json",
    "locales": ["es", "de", "fr", "ja", "pt-BR", "zh-CN", "ko"]
  },
  "placeholders": ["{{variable}}", "${variable}"]
}

Two non-obvious things:

  • placeholders is the contract. Localingos uses this to enforce that every {{name}} in your source string is preserved exactly in every translation. If a translation comes back with {{nombre}}, the sync fails — you never ship broken React templates.
  • targets.locales controls which languages to translate. Start small (5-7); add more as you grow.

Step 4 — Sync

localingos sync

This pushes new keys from en.json, translates them across every locale in targets.locales, and writes the results back to disk. First-time sync of a 50-key file takes ~10 seconds. Subsequent syncs only translate changed keys.

Commit the resulting es.json, de.json, etc. — these files belong in git so PR reviewers see exactly what shipped.

Step 5 — Use in components

import { useTranslation } from 'react-i18next';

export const Welcome: React.FC<{ name: string; itemCount: number }> = ({ name, itemCount }) => {
  const { t } = useTranslation();
  return (
    <>
      <h1>{t('welcome', { name })}</h1>
      <p>{t('cart', { count: itemCount })}</p>
    </>
  );
};

That's it. No imports per language, no string formatting code in components. react-i18next picks the correct plural variant per the active language (Russian has 3 plural forms, Arabic has 6 — the library handles it automatically as long as you ship the variants in your locale files, which Localingos does for you).

Step 6 — Language switcher

const LANGUAGES = { en: 'English', es: 'Español', de: 'Deutsch', fr: 'Français' };

export const LanguageSwitcher: React.FC = () => {
  const { i18n } = useTranslation();
  return (
    <select value={i18n.language} onChange={e => i18n.changeLanguage(e.target.value)}>
      {Object.entries(LANGUAGES).map(([code, label]) => (
        <option key={code} value={code}>{label}</option>
      ))}
    </select>
  );
};

Because the detector is configured with caches: ['localStorage'], this choice persists across sessions — returning users land in their last picked language.

Step 7 — Automate sync in CI

Add to .github/workflows/i18n.yml:

name: i18n-sync
on:
  push:
    branches: [main]
    paths: ['src/i18n/en.json']
jobs:
  sync:
    runs-on: ubuntu-latest
    permissions: { contents: write, pull-requests: write }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm install -g @localingos/cli
      - run: localingos sync
        env:
          LOCALINGOS_API_KEY: ${{ secrets.LOCALINGOS_API_KEY }}
      - uses: peter-evans/create-pull-request@v6
        with:
          commit-message: "chore(i18n): sync translations"
          branch: i18n/auto-sync
          title: "chore(i18n): sync translations"

Every time someone merges English copy changes, a PR opens with the freshly translated locale files. Reviewer approves, merges, ships in all 7 languages. Same workflow with the same review gate as any other code change.

Production checklist

  • Lazy-load locales if you ship 10+ languages. Use i18next-http-backend to fetch JSON on demand instead of bundling everything. Cuts initial bundle weight significantly.
  • Type-check your keys. TypeScript users can wire react-i18next's CustomTypeOptions so t('missingKey') is a compile-time error. Worth its weight in gold once your key count exceeds ~200.
  • Mark do-not-translate terms. Brand names (Localingos, your product, integration partners) should never be translated. Localingos respects a glossary you maintain in the dashboard.
  • Validate placeholder integrity in CI. Add a JSON locale linter that confirms every {{var}} in source exists in every translation. Localingos does this server-side, but a local check catches mistakes before they hit the API.

Where this leaves you

Your React app now:

  • Detects the user's preferred language from browser / localStorage
  • Renders the right plural form per CLDR rules across every locale
  • Stays in sync with English copy automatically on every CI run
  • Ships in 7 locales (or 56, if you want — same workflow)

The setup is permanent — you'll never write per-language code in components again, and adding the 8th language is one line in localingos.json.

If you want to start now, the free tier covers 5,000 words across all locales with no credit card. That's enough for a typical React app's full string corpus.