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.
On this page
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 devThis starts a development server and opens the default Vite + React page in your browser.
Commit Reference: Create React project with Vite and TypeScript
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.
Commit Reference: Build the basic UI with static English strings
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:
- Install
i18nextandreact-i18next - Create your English translation file (
en.json) - Update your component to use the
useTranslationhook
3.1 Install the i18n Libraries
From your project folder, install the libraries:
npm install i18next react-i18nextIn this example:
i18nextis the core internationalization libraryreact-i18nextadds React bindings like theuseTranslationhook
3.2 Create the English Source File for Translation (en.json)
Create the following folder structure:
src/
i18n/
locales/
en.jsonThen 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 keyi18n– 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;
Commit Reference: Create the initial English file for translation
4
Initialize i18next and Load JSON Files Automatically
Right now your component calls t('welcome'), but i18next doesn’t yet know:
- Where
en.jsonlives - 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.tsAdd 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
*.jsonfile you add tosrc/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.
5.3 Trans Component: Adding Formatting and Links
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.
- Follow the steps in the Getting Started Guide to create a new project.
- 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}}.jsonThis 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 integration | PTC opens a Pull/Merge Request with new language files |
| Manual file upload | Download the JSON file and place it in src/i18n/locales/ |
| PTC API | Use 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.
Commit Reference: PTC ReactLocalizationDemo : Automatic Translations
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.
Commit Reference: Add a language switcher to test the translation
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.
- Add the
i18next-browser-languagedetectorpackage:
npm install i18next-browser-languagedetector- 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.
- Install the HTTP backend:
npm install i18next-http-backend- 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;
- 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.jsonWith 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.