Translate React Apps with Human-Quality AI 

Learn how to translate a React app using react-i18next and JSON files in this step-by-step React internationalization guide.

This guide uses a small React + Vite project and prepares it for localization, including:

  • Loading translation JSON files
  • Using the t() function for translations
  • Handling dynamic text (interpolation, plurals, rich text with <Trans />)
  • Translating JSON files with AI

You can follow along with the commit examples or apply the same steps to your existing React app.

What You Need to Translate a React App

To follow this tutorial, you’ll need:

  • Node.js and npm
  • Basic React knowledge
  • A code editor such as VS Code

Of course, you’ll also need a translation tool. We’ll use PTC (Private Translation Cloud) to create translated JSON files. You can sign up for a free trial now or wait until we reach the translation step.

Set Up React Internationalization (with Commit Examples)

1

Set Up Your React App for Translation

If you already have a React application, skip to Step 2.

To create a new React + TypeScript app, open your terminal, navigate to the folder where you keep your projects, and run:

npm create vite@latest react-localization-demo -- --template react-ts
cd react-localization-demo
npm install
npm run dev

This starts a development server and opens the default Vite + React page in your browser.

2

Add User-Facing Text to Your React App

Before you add react-i18next, you need to prepare the source language text you want to translate. Open src/App.tsx and add your user-facing strings:

function App() {
  return (
    <div>
      <h1>Welcome</h1>
      <p>This is a localization demo.</p>
      <button onClick={() => alert('Click me')}>Click me</button>
    </div>
  );
}

For the purpose of our demo, we’ve kept our UI simple with a heading, description, and button.

3

Set Up react-i18next in Your React App

Now that your React app shows some basic English text, it’s time to connect react-i18next so those strings can come from a translation file instead of being hard-coded.

This step has three parts:

  1. Install i18next and react-i18next
  2. Create your English translation file (en.json)
  3. Update your component to use the useTranslation hook

3.1 Install the i18n Libraries

From your project folder, install the libraries:

npm install i18next react-i18next

In this example:

  • i18next is the core internationalization library
  • react-i18next adds React bindings like the useTranslation hook

3.2 Create the English Source File for Translation (en.json)

Create the following folder structure:

src/
  i18n/
    locales/
      en.json

Then add your English strings to src/i18n/locales/en.json:

{
  "welcome": "Welcome",
  "description": "This is a localization demo.",
  "clickMe": "Click me"
}

Each key (welcome, description, clickMe) maps to the text you want to display in the UI.

3.3 Update App.tsx to Use useTranslation

Replace your hard-coded English text with calls to the t() translation function.

The useTranslation() hook gives your component access to:

  • t() – looks up a string by key
  • i18n – lets you change languages programmatically

Update src/App.tsx:

import { useTranslation } from 'react-i18next';

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

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <p>{t('description')}</p>
      <button onClick={() => alert(t('clickMe'))}>{t('clickMe')}</button>
    </div>
  );
}

export default App;

4

Initialize i18next and Load JSON Files Automatically

Right now your component calls t('welcome'), but i18next doesn’t yet know:

  • Where en.json lives
  • Which languages exist
  • What the default language should be

In this step, you’ll initialize i18next and tell it to load all JSON files from your locales folder automatically using Vite’s import.meta.glob.

4.1 Create the i18n Configuration File

Create a new file at:

src/i18n/index.ts

Add the following setup:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

type TranslationResources = string | { [k: string]: TranslationResources } | TranslationResources[];

const modules = import.meta.glob<{default: Record<string, TranslationResources>}>('./locales/*.json', { eager: true })

const resources: Record<string, { translation: Record<string, TranslationResources> }> = {};

for (const path in modules) {
  const lang = path.match(/\.\/locales\/(.*)\.json$/)?.[1];
  if (lang) {
    resources[lang] = { translation: modules[path].default };
  }
}

i18n
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: 'en',
    interpolation: { escapeValue: false },
  });

export default i18n;

This setup is useful because:

  • Every *.json file you add to src/i18n/locales/ becomes available automatically.
  • When PTC creates new language files (for example ar.json), they’ll be auto-loaded by this configuration — no imports or code changes needed.

Commit Reference: Set up i18n configuration

4.2 Import i18n in Your App Entry Point

Before React renders your app, i18next needs a chance to initialize. To make that happen, you must import your i18n configuration in the entry file.
If you skip this step, t() will just return translation keys instead of translated text.

Open src/main.tsx and add the highlighted import:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import './i18n'  // ← Add this import!

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

5

Handle Plurals, Interpolation, and Rich Text

So far, you’ve only used static strings like “Welcome” and “Click me”. But real apps need dynamic content:

  • Including links or formatting inside translated text
  • Greeting users by name
  • Showing counts (“1 message” vs “3 messages”)

5.1 Interpolation: Adding Variables to Translations

Instead of hardcoding values that change at runtime, you can interpolate them into your translations.

To do this, include a variable inside {{ }} in your JSON, then pass the actual value when calling t().

Update src/i18n/locales/en.json:

{
  "welcome": "Welcome",
  "userGreeting": "Welcome back, {{firstName}}!",
  "description": "This is a localization demo.",
  "clickMe": "Click me"
}

Now update your component to use the interpolated greeting:

import { useTranslation } from 'react-i18next';

function App() {
  const { t } = useTranslation();
  const user = { firstName: 'Sarah' };

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <p>{t('userGreeting', { firstName: user.firstName })}</p>
      <p>{t('description')}</p>
      <button onClick={() => alert(t('clickMe'))}>{t('clickMe')}</button>
    </div>
  );
}

export default App;

The second argument to t() is an object with the values you want to substitute. You can have as many variables as you need—just make sure each one appears in your translations.

5.2 Add Plural Forms

Some text changes based on a number. For example, “1 new message” vs “3 new messages”. i18next handles this automatically when you provide plural forms.

Update src/i18n/locales/en.json:

{
  "welcome": "Welcome",
  "userGreeting": "Welcome back, {{firstName}}!",
  "description": "This is a localization demo.",
  "clickMe": "Click me",
  "newMessages_one": "You have {{count}} new message.",
  "newMessages_other": "You have {{count}} new messages."
}

For pluralization to work, the interpolated variable must be named count. i18next uses this value to choose the correct plural form for each language.

Now use it in your component:

import { useTranslation } from 'react-i18next';

function App() {
  const { t } = useTranslation();
  const user = { firstName: 'Sarah' };
  const [messageCount, setMessageCount] = useState<number>(1);

  const handleIncrement = () => {
    setMessageCount(messageCount + 1);
  };

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <p>{t('userGreeting', { firstName: user.firstName })}</p>
      <p>{t('newMessages', { count: messageCount })}</p>
      <p>{t('description')}</p>
      <button onClick={handleIncrement}>{t('clickMe')}</button>
    </div>
  );
}

export default App;

i18next will automatically choose newMessages (singular) when count is 1, and newMessages_other (plural) for any other number.

Sometimes you need formatting (like bold text) or interactive elements (like links) inside a translated string. The Trans component lets you include JSX (JavaScript XML) elements within your translations.

First, import the Trans component:

import { useTranslation, Trans } from 'react-i18next';

Add a new key to src/i18n/locales/en.json:

{
  "welcome": "Welcome",
  "userGreeting": "Welcome back, {{firstName}}!",
  "newMessages_one": "You have {{count}} new message.",
  "newMessages_other": "You have {{count}} new messages.",
  "description": "This is a localization demo.",
  "termsText": "I agree to the <1>Terms of Service</1> and <3>Privacy Policy</3>.",
  "clickMe": "Click me"
}

Notice the <1> and <3> tags. These are placeholders that map to child components by index (starting from 0).

Now use the Trans component in your JSX:

import { useTranslation, Trans } from 'react-i18next';

function App() {
  const { t } = useTranslation();
  const user = { firstName: "Sarah" };
  const [messageCount, setMessageCount] = useState<number>(1);

  const handleIncrement = () => {
    setMessageCount(messageCount + 1);
  };

  return (
    <div>
      <h1>{t("welcome")}</h1>
      <p>{t("userGreeting", { firstName: user.firstName })}</p>
      <p>{t("newMessages", { count: messageCount })}</p>
      <p>{t("description")}</p>

      <p>
        <Trans i18nKey="termsText">
          I agree to the <a href="/terms">Terms of Service</a> and 
          <a href="/privacy">Privacy Policy</a>.
        </Trans>
      </p>

      <button onClick={handleIncrement}>{t("clickMe")}</button>
    </div>
  );
}

export default App;

Translate Your React App

At this point, your app is fully internationalization-ready. Now it’s time to translate your en.json file.

There are different ways to do this, but we’ll focus on using PTC to generate translated JSON files automatically. It allows manual file uploads, Git integration, or using the API

For our demo project, we’ll integrate PTC with our GitHub repository.

Step 1

Set Up A Project in PTC to Translate the JSON File

To connect your project to PTC, sign up for a free trial.

  1. Follow the steps in the Getting Started Guide to create a new project.
  2. In the setup wizard, select your source file (in our case, src/i18n/locales/en.json) and choose your output path:
src/i18n/locales/{{lang}}.json

This pattern creates one file per language (e.g., fr.json, de.json) using the same folder structure your app already expects.

Step 2

Get Translated JSON Files and Add Them to Your Project

When your translations are ready, PTC will notify you. Your next steps depend on how you’re using PTC:

If you use…What happens next
Git integrationPTC opens a Pull/Merge Request with new language files
Manual file uploadDownload the JSON file and place it in src/i18n/locales/
PTC APIUse the file retrieval endpoint

For example, if you translate into French and use Git integration, PTC will return fr.json and place the file in src/i18n/locales/fr.json.

Step 3

Test Your Translated React App with a Language Switcher

Now that you have at least one translation file, let’s add a simple language switcher to verify everything works.

Update src/App.tsx to add language switcher buttons:

import { useTranslation } from 'react-i18next';

function App() {
  const { t, i18n } = useTranslation();

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <p>{t('userGreeting', { firstName: user.firstName })}</p>
      <p>{t('newMessages', { count: messageCount })}</p>
      <p>{t('description')}</p>
      
      <p>
        <Trans i18nKey="termsText">
          I agree to the <a href="/terms">Terms of Service</a> and <a href="/privacy">Privacy Policy</a>.
        </Trans>
      </p>
      <button onClick={() => alert(t('clickMe'))}>{t('clickMe')}</button>

      <button onClick={() => i18n.changeLanguage('en')}>EN</button>
      <button onClick={() => i18n.changeLanguage('fr')}>FR</button>
    </div>
  );
}

Run your app (npm run dev) and click the EN / FR buttons. When you run the app in French, the layout should look exactly the same as in English — only the language of the text changes. 

Optimize Translation Loading and Language Detection

Now that your translations are working, you can optimize i18next to make your app faster and more user-friendly, especially if you support multiple languages.

Auto-Detecting User Language

By default, your app starts in English and requires users to switch languages manually. With automatic language detection, i18next checks the browser, URL, or saved preferences and loads the correct language instantly.

  1. Add the i18next-browser-languagedetector package:
npm install i18next-browser-languagedetector
  1.  Update your src/i18n/index.ts:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

type TranslationResources = string | { [k: string]: TranslationResources } | TranslationResources[];

const modules = import.meta.glob<{default: Record<string, TranslationResources>}>('./locales/*.json', { eager: true })

const resources: Record<string, { translation: Record<string, TranslationResources> }> = {};

for (const path in modules) {
  const lang = path.match(/\.\/locales\/(.*)\.json$/)?.[1];
  if (lang) {
    resources[lang] = { translation: modules[path].default };
  }
}

i18n
  .use(LanguageDetector) // detect browser/localStorage/etc.
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: "en",
    supportedLngs: ["en", "fr", "de", "ar"], // adjust to your languages
    interpolation: { escapeValue: false },
    detection: {
      order: ["querystring", "localStorage", "cookie", "navigator"],
      caches: ["localStorage", "cookie"],
    }
  });

export default i18n;

With this in place, users will automatically see the app in their preferred language when available. They can still switch manually if needed.

Lazy-Loading Translations

Right now, i18next loads every translation file at startup. That’s fine for one or two languages, but if you support many languages, your bundle size can grow quickly.

Lazy loading solves this by downloading only the language files the user needs.

  1. Install the HTTP backend:
npm install i18next-http-backend
  1. Update your src/i18n/index.ts:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(Backend) // load JSON over HTTP
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: "en",
    supportedLngs: ["en", "fr"],
    interpolation: { escapeValue: false },
    backend: {
      loadPath: "/locales/{{lng}}/{{ns}}.json"
    }
  });

export default i18n;
  1. After enabling the backend, i18next will load translation files over HTTP instead of bundling them. For this to work, your translations need to be served from a public path that the browser can request:
public/
  locales/
    en/
      translation.json
    fr/
      translation.json

With this structure, each language lives in its own folder, mirroring the {{lng}} part of your loadPath. So, for example, when a user switches to French, i18next will fetch:

/locales/fr/translation.json

Translate Your React App with PTC

Your app is ready for localization with i18next. Now, let PTC take care of the translation work for you.
Start your free trial and translate up to 2,500 words into 2 languages, then activate Pay-As-You-Go to pay only for what you translate.

Translate JSON files in React apps with AI

Get context-aware translations in minutes

Upload files, or automate via API or Git integration


1

Start a free trial

Translate 2500 words for free.

2

Add quick project details

Give context about your app and users.

3

Get translations

Download a ZIP, or merge in your repo.

Scroll to Top