Crowdin Logo - Dark Blog

React i18n with i18next: Expert Tutorial with Code Examples

19 min read
React internationalization

React i18n isn’t just a nice-to-have anymore; it’s something users expect. According to Global Multilingual CX Survey, 68% of people would switch to a brand that speaks their language. This means that about two out of three users might leave your app or website just because it doesn’t feel familiar.

Yet internationalization can feel messy. What library should you choose? What is the best way to structure JSON files? What about performance, plurals, RTL text, routing, and lazy loading?

This guide will show you how to set up i18next for React i18n from start to finish. Then, it shows how to scale your workflow using Crowdin, including GitHub/GitLab sync, QA checks, and Over-The-Air (OTA) updates, so you don’t rebuild for every copy change.

What You Will Learn:

  • Foundations: What i18n is in React, and how it prepares your code for later localization.
  • Library Choice: i18next vs. React-Intl - when to use which.
  • Hands-On: Install, configure, and ship i18next + react-i18nex with clean JSON.
  • Advanced: Interpolation, plurals, <Trans>, language detection, lazy loading, router.
  • Scale Up: Replace manual file passing with a TMS, automate with Crowdin (repo sync, translate JSON files, QA, OTA, and use AI for translation)

What is i18n in React?

Internationalization (i18n) in React means setting up your application so that it can display content in multiple languages without requiring you to rewrite the code each time you add a new language.

To set this up properly, you’ll want to:

  • Move all the text visible to the user out of components and into simple translation files (usually JSON).
  • Ensure dates, numbers, and currencies are automatically formatted to match each user’s locale.
  • Make sure your app supports plural forms, gender variations, and text direction (for example, right-to-left for Arabic).
  • Add a language switcher so people can change the app’s language whenever they want.

Once you’ve done this, your app is then ready for localization (l10n); the actual translation process can happen in parallel with your development work.

Choosing the Right React i18n Library: i18next vs. React-Intl

When you start internationalizing a React app, one of the first choices you’ll face is which i18n library to use. This decision affects flexibility, bundle size, and your overall development workflow.

Most teams narrow it down to two main options: i18next (with react-i18next) or React-Intl (FormatJS). Here’s a quick side-by-side comparison.

Criteriai18nextReact-Intl
ApproachKey-based messages in JSON and rich plugin ecosystem (detectors, backends)ICU Message Syntax
Plurals & InterpolationCLDR-based plurals, interpolation, <Trans> for rich contentICU plurals, rich ICU formatting components
React BindingsuseTranslation, <Trans>, suspense-friendly<IntlProvider>, <FormattedMessage>, hooks/API
LoadingMany backends (HTTP, chained, etc.), namespaces, and lazy loadingTypically bundles message JSON per locale
EcosystemDetectors, HTTP backends, chained fallback; massive communityMature docs and tooling; great ICU ergonomics
Best FitMost React apps needing flexibility, plugins, or gradual adoptionTeams preferring strict ICU syntax and FormatJS tooling

Why i18next Is a Standard for Most Projects

Due to its scalability, flexibility, and smooth integration with react-i18next, i18next has become a favorite among React developers. With over 6.3 million weekly downloads and 9.8K stars on GitHub, it’s definitely one of the most feature-rich libraries out there.

Why developers choose it:

  • Plugins: It’s easy to add fallback logic, HTTP backends, and language detection.
  • Namespaces: Only load translations when required and keep them arranged by feature.
  • Developer Experience: The useTranslation hook feels natural in React, while <Trans> safely handles mixed markup and variables.

Together, i18next + react-i18next combine the translation engine and React bindings for a smooth, production-ready setup.

When to Consider React-Intl (FormatJS)

React-Intl, part of the FormatJS suite, uses ICU message syntax and works best for teams already using ICU across products.

You might prefer React-Intl if:

  • Your organization has already standardized ICU message formatting.
  • You want explicit message components like <FormattedMessage> for consistency.
  • You’re contributing to a codebase that uses FormatJS tooling.

It’s a reliable, well-documented choice, but its stricter structure can feel limiting for small or fast-moving teams.

Learn how to localize JavaScript and React apps with fast and lightweight Lingui framework

For this guide, we will be implementing i18next. Let’s get started!

Step 1: Project Setup and Installation

Installing Dependencies (i18next, react-i18next)

Before translating anything, you need a working React app and the right i18n tools. Let’s create a new project from scratch using either Vite (recommended) or Create React App, and then configure i18next for translations.

Vite provides faster builds, cleaner configuration, and is widely used in modern React projects.

Terminal window
npm create vite@latest react-i18n-demo -- --template react-ts

Option 2: Create a New Project with “Create React App”

If your team still uses Create React App, the setup looks almost the same.

Terminal window
npx create-react-app react-i18n-demo --template typescript

Install i18n Dependencies

Next, install the core translation engine and its React integrations.

Terminal window
cd react-i18n-demo
npm i i18next react-i18next i18next-browser-languagedetector i18next-http-backend

This is what each package does:

  • i18next is the main translation engine that handles languages, pluralization, and formatting.
  • React-i18next: it’s a layer just for React that has hooks like useTranslation and the <Trans> component.
  • i18next-browser-languagedetector: This package automatically finds out what language the user prefers using browser, cookies or localStorage.
  • i18next-http-backend: It gets your translation JSON files straight from /public/locales/.

Create the i18n Configuration File

Now set up i18next so it knows where to load translations and how to detect the active language.

Create a new file at src/i18n.ts and add the following code:

src/i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import HttpBackend from "i18next-http-backend";
void i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: "en",
supportedLngs: ["en", "fr"],
ns: ["common", "home"],
defaultNS: "common",
backend: { loadPath: "/locales/{{lng}}/{{ns}}.json" },
detection: {
order: ["querystring", "cookie", "localStorage", "navigator"],
caches: ["localStorage", "cookie"]
},
interpolation: { escapeValue: false },
react: { useSuspense: true }
});
export default i18n;

Explanation:

  • fallbackLng sets a language that will be used if the main language is not available.
  • supportedLngs shows the languages that your app can work with.
  • ns (namespaces) lets you put translations into groups based on their features.
  • backend.loadPath tells i18next where to look for your JSON files.
  • escapeValue: false stops text from being double-escaped because React already does that.

Connect i18n to Your React App

Import the configuration once in your entry file so i18next initializes before your components load.

src/main.tsx
import "./i18n";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

Now your app is ready to use translations anywhere through useTranslation() or <Trans>.

Structuring Your JSON Translation Files

After you configure i18next in your project, it requires translation resources to function. Translations are usually stored as JSON files, with one for each language and namespace.

Inside your public directory, create a locales folder if you haven’t already.

Then add language subfolders for each supported language, such as:

Language subfolders for each supported language

Each JSON file contains key–value pairs where the key is the translation identifier, and the value is the text displayed to users. Here’s a simple example:

public/locales/en/common.json
{
"title": "Crowdin",
"nav": { "home": "Home", "pricing": "Pricing" },
"helloUser": "Hello, {{name}}!",
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}

and

public/locales/fr/common.json
{
"brand": "Crowdin",
"nav": { "home": "Accueil", "pricing": "Tarifs" },
"helloUser": "Bonjour, {{name}} !",
"items_one": "{{count}} article",
"items_other": "{{count}} articles"
}

Best Practices

  • Keep your JSON flat or only two to three levels deep. Deeply nested keys can get confusing.
  • Use consistent key names across all languages to prevent missing translations.
  • Follow pluralization rules (_one, _other, etc.). i18next automatically handles plural logic for each language.
  • Group related strings into namespaces like common, home, or dashboard to make it easier to load and navigate through the translations.

Once these files are in place, i18next will automatically load the correct language and namespace at runtime based on user preferences and configuration.

Step 2: Core Translation Techniques

You can start translating text in your React components now that i18next is set up.

This step is all about three important things that every React developer should know: how to use the translation hook, how to group strings into namespaces, and how to make a language switcher.

Using the useTranslation Hook for Basic Text

The useTranslation() hook is the easiest way to show translated text. It gives you access to the t() function, which finds translation keys and gives you the right text.

Here’s a quick example:

src/components/Header.tsx
import { useTranslation } from "react-i18next";
export function Header() {
const { t } = useTranslation();
return (
<header>
<h1>{t("brand")}</h1>
<nav>
<a href="/">{t("nav.home")}</a>
<a href="/pricing">{t("nav.pricing")}</a>
</nav>
</header>
);
}

When the language is set to English, users can see “Home” and “Pricing”. When you switch to French, the text instantly changes to “Accueil” and “Tarifs” without having to reload the page.

You can also pass variables with the t() function. For instance:

<p>{t("helloUser", { name: "John" })}</p>

Renders: Hello, John!

Or in French: Bonjour, John!

Managing Namespaces for Code Splitting and Organization

As your app gets bigger, you will probably need to translate a lot of different parts, like the dashboard, settings, and navigation.

i18next lets you group translations into namespaces instead of putting them all in one big file.

For instance: public/locales/en/common.json public/locales/en/dashboard.json public/locales/fr/common.json public/locales/fr/dashboard.json

You can then tell i18next which namespaces to load for a component:

import { useTranslation } from "react-i18next";
export function Dashboard() {
const { t } = useTranslation("dashboard");
return <h2>{t("welcomeBack", { name: "John" })}</h2>;
}

This method makes translation files smaller and allows lazy loading, so you can load only the namespaces your component needs when they are required. This makes things easier to read and faster.

Creating a Language Switcher Component

Let’s now make a simple dropdown that lets users switch languages instantly. When the language changes, i18next automatically re-renders the translated content.

src/components/LanguageSwitcher.tsx
import { useTranslation } from "react-i18next";
export function LanguageSwitcher() {
const { i18n } = useTranslation();
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
i18n.changeLanguage(event.target.value);
};
return (
<select value={i18n.language} onChange={handleChange}>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
);
}

Place this component anywhere in your layout, like in the footer or header. i18next automatically saves the user’s choice when they switch languages (thanks to the language detector plugin) and updates all the translated text on the page.

Step 3: Handle Dynamic and Complex Content

Working with Variables and Interpolation

Most apps show changing information, such as usernames, counts, or dates. i18next uses interpolation instead of string concatenation, where values replace placeholders inside translation strings at runtime. public/locales/en/common.json

public/locales/en/common.json
{
"welcomeUser": "Welcome back, {{name}}!",
"unreadMessages": "You have {{count}} unread messages."
}

Usage in component:

import { useTranslation } from "react-i18next";
export function DashboardHeader({ name, count }: { name: string; count: number }) {
const { t } = useTranslation("common");
return (
<>
<h2>{t("welcomeUser", { name })}</h2>
<p>{t("unreadMessages", { count })}</p>
</>
);
}

i18next automatically replaces the {{name}} and {{count}} placeholders. This way, you can pass any variable, including strings, numbers, and even values that are already formatted (like dates).

How to Implement Pluralization Correctly

Different languages have different rules for plurals. You may only need singular and plural forms in English, but in other languages, there can be many more.

i18next knows which forms to use for each locale because it follows the CLDR standard.

public/locales/en/common.json
{
"task_one": "{{count}} task remaining",
"task_other": "{{count}} tasks remaining"
}

and

public/locales/fr/common.json
{
"task_one": "{{count}} tâche restante",
"task_other": "{{count}} tâches restantes"
}

Usage:

<p>{t("task", { count: 1 })}</p> // → “1 task remaining”
<p>{t("task", { count: 5 })}</p> // → “5 tasks remaining”

No need to write if-else statements by hand; i18next chooses the right plural form based on the language and the count.

Translating HTML and React Components with the <Trans> Component

When you need to translate text that has links, bold text, or inline React elements in it, string concatenation or dangerouslySetInnerHTML would be messy and unsafe. That’s why i18next has the <Trans> component just for this.

public/locales/en/common.json
{
"welcomeMessage": "Welcome to <1>Crowdin</1>, <2>{{name}}</2>!"
}

Usage:

import { Trans } from "react-i18next";
export function Welcome({ name }: { name: string }) {
return (
<p>
<Trans i18nKey="welcomeMessage" values={{ name }}>
Welcome to <strong>Crowdin</strong>, <span>{name}</span>!
</Trans>
</p>
);
}

How it works:

The <Trans> component takes the numbered tags (<1>, <2>, etc.) in your translation string and replaces them with the matching React elements in your code. You can keep your text in translation files and still control how it looks in JSX this way.

Building a Production-Ready Workflow

The Bottleneck of Manual File Management

It may seem easy to manage translations in a React app at first. You just need to keep a few JSON files in your /locales folder, send them to a translator, and add the new strings when they are ready. But as your app grows and more people work on the same codebase, manual localization quickly turns into a mess.

This is what happens when teams get bigger:

  • Developers share .json files back and forth through email or chat.
  • Translators work on outdated copies while engineers add new keys.
  • Git merges lead to conflicts and missing translations.

Every little change to the UI makes developers have to re-export files, change language codes, and recheck keys. There is no central visibility, so no one knows which translations are missing, outdated, or still needed.

Another big problem is that translators often don’t have enough context to do their jobs properly. Since they usually don’t know where a string is being used in your React components or what kind of element it is, like a button, heading, or tooltip. This often leads to the wrong tone or text that doesn’t fit.

That’s exactly the kind of problem a Translation Management System (TMS) like Crowdin is built to solve.

Automate Your React i18n Workflow with a TMS

A TMS replaces your manual file management with an automated, centralized workflow. Instead of sharing JSON files via email, you connect your repository once, and Crowdin syncs everything for you.

Crowdin is a modern localization software built for continuous integration. It detects new translation keys automatically, provides translators with context-aware interfaces, and pushes completed translations back to GitHub or GitLab with zero manual steps.

Workflow automation with Crowdin

This kind of automation makes your React i18n setup truly production-ready. Whether you need to translate JSON files or manage entire front-end frameworks, Crowdin keeps your localization pipeline as automated as your deployment workflow.

You can even use AI for translation to speed up your project while still allowing human review and QA checks.

Connect Your GitHub or GitLab Repository for Continuous Localization

It only takes a few minutes to set up continuous localization with Crowdin:

  1. Use Crowdin GitHub Integration or Crowdin GitLab Integration to connect your repository.
  2. Pick branches to track, for example, main and develop.
  3. Add translation paths such as /src/locales/en/common.json.
  4. Enable two-way sync so that Crowdin can automatically get new keys and send translated files back to the repo as es.json, fr.json, and so on.

Crowdin integration UI showing branch selection

Now, when you push a change to your React project repository, Crowdin will automatically update the translation project for the translators. And once they are done, Crowdin automatically opens a pull request with the new files. You don’t have to upload files by hand or guess what to do.

How a TMS Empowers Translators with In-Context Tools

Translators often work blind. They see keys like welcome.title and button.submit, but not the page those strings belong to.

Crowdin’s In-Context Localization Tool overcomes this issue. It injects a live translation layer into your React app, which lets translators see and change text right on the UI they are translating.

In-Context Localization Tool

This method makes it less likely that there will be mistakes in tone, layout, and gender/plural use. It also makes translation reviews go faster because every change is checked against the context. The result is fewer bugs, faster releases, and a better user experience across all languages.

How to Use Crowdin to Manage and Translate JSON Files

Crowdin works with i18next JSON structures right out of the box. It can find new keys on its own and connect them to the languages in your project. Meaning that there is no need to edit by hand.

Example JSON file:

{
"welcome": {
"title": "Welcome to our app",
"description": "Start exploring your dashboard below."
},
"actions": {
"getStarted": "Get Started"
}
}

Crowdin syncs the translation dashboard when a new string is added. Translators can then use the web interface to make changes, and the system will send the new JSON files back to your repository.

Crowdin translation editor

Because Crowdin understands the React i18n ecosystem, you can also add advanced language features:

  • react i18n changes the language dynamically, so you can use i18next.changeLanguage() to switch languages right away
  • Use a React i18n language detector to find the user’s locale automatically.
  • Use React i18n get current language to get the current locale.
  • Use React i18n lazy loading to load translations only when needed to speed things up.
  • Use React Router i18n to make sure that changes to the language are reflected in the URL structure (example.com/en/dashboard).

This process makes sure that every string in your React app is always up to date, translated in context, and sent to users without you ever having to touch a JSON file again.

Advanced React i18n Techniques

Integrating i18n with React Router for Localized URLs

When using React Router, you can add the language code directly in the URL. For example, https://example.com/en/dashboard. This makes sure that each route knows what language it is in and helps with SEO.

import { BrowserRouter, Routes, Route, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
function Dashboard() {
const { lang } = useParams();
const { t, i18n } = useTranslation();
// React i18n change language dynamically
useEffect(() => {
i18n.changeLanguage(lang);
}, [lang, i18n]);
return <h1>{t("dashboard.title")}</h1>;
}
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/:lang/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}

This setup makes it so that the language changes automatically when the user goes to a different language path. You can also use the React i18n language detector plugin to automatically set the preferred locale.

Showing localized URLs /en, /fr, /es mapping to translated pages

Best Practices for Lazy Loading Translation Files

Use React i18n lazy loading with i18next-http-backend. To stop loading all the language bundles at once.

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import HttpApi from "i18next-http-backend";
i18n
.use(HttpApi)
.use(initReactI18next)
.init({
lng: "en",
fallbackLng: "en",
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json"
},
react: { useSuspense: true }
});
export default i18n;

This setup only downloads translation files when the user changes languages. Use React Suspense with it to show a fallback UI while loading.

React Native i18n

Key Differences in Setup for Mobile Apps

Using i18n in React Native is similar to using it in React for the web, but there are some important differences. Unlike web projects, there’s no /public/locales folder or static file hosting in mobile apps. Instead, translation JSON files are either:

  • Bundled directly with the app at build time, or
  • Downloaded remotely from a server or TMS like Crowdin.

Instead of using the browser settings, React Native looks at the device settings to figure out what language the user wants to use. react-native-localize and react-i18next are the two most common tools for this.

Here’s a simple example of how to set things up:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import * as RNLocalize from "react-native-localize";
import en from "./locales/en.json";
import fr from "./locales/fr.json";
const resources = {
en: { translation: en },
fr: { translation: fr }
};
i18n.use(initReactI18next).init({
compatibilityJSON: "v3",
resources,
lng: RNLocalize.getLocales()[0].languageCode, // auto-detect device language
fallbackLng: "en",
interpolation: { escapeValue: false }
});
export default i18n;

This setup makes sure that the app automatically matches the user’s system language when it starts up. If that language isn’t supported, it will fall back to English.

Using the Crowdin React Native SDK for Over-the-Air (OTA) Updates

It can take a long time for app stores to approve and publish mobile releases, which means updating translations can take a long time and cost a lot of money.

The Crowdin OTA JavaScript Client fixes this by letting you send Over-the-Air (OTA) translation updates. This means you can send new or fixed translations right away without having to resubmit your app.

To get started:

  1. Install the Crowdin OTA JavaScript Client.
  2. Initialize it with your project’s distribution hash.
  3. Use the client’s methods to fetch translation strings at runtime.
import OtaClient from "@crowdin/ota-client";
// 1. Initialize the client
const client = new OtaClient({
distributionHash: "your-distribution-hash" // Additional options like language detection or custom CDN URL can be passed here
});
// 2. Fetch translations for a specific locale (e.g., 'en')
client
.getStringsByLocale("en")
.then((translations) => {
// 'translations' is an object of key-value pairs for the requested locale
console.log("Translations successfully fetched via OTA:", translations); // You would then integrate 'translations' with your chosen i18n library (e.g., i18next, react-intl)
// Example: i18nInstance.addResourceBundle('en', 'translation', translations);
})
.catch((error) => {
console.error("Failed to fetch OTA translations:", error);
});

Localization is now always happening with OTA updates. Translators can send updates straight from Crowdin, and users will get the most recent translations the next time they open your app. There are no delays in the store or having to rebuild.

Conclusion

Most teams start by handling translation files by hand, adding JSONs, switching languages, and testing each screen by hand. At first, this method works, but as your app gets bigger and you add more languages, it becomes harder to keep up with.

That’s where automation comes in handy. You can automate repetitive tasks by linking your project to a Management System (TMS) like Crowdin. This will give you a continuous localization workflow with version control sync, in-context translation, and Over-the-Air (OTA) updates for mobile.

To put it simply, great localization isn’t just about the code. It’s also about making a system that can grow, be updated easily, and speak the language of your users as soon as they open your app.

Additional Resources

FAQs

What is i18n in React?

In React, i18n stands for “internationalization”, which means getting your app ready to work in more than one language and region. You save text in translation files (usually JSON) instead of hardcoding it into components. Then, at runtime, you use a library like i18next to show the text in the right language.

How to set up i18n in React?

To set up i18n in React:

  1. Install i18next and react-i18next.
  2. Create JSON files for translation (for example, /public/locales/en/common.json).
  3. Initialize and configure i18next in src/i18n.ts.
  4. In your app’s entry file, import the i18next config file
  5. Use the useTranslation() hook or <Trans> component to display translated text.

The i18next React documentation has a full guide on how to set things up.

How to implement i18n in React?

There are three main steps to putting it into action:

  1. Install i18next, react-i18next, and optional plugins like i18next-http-backend and i18next-browser-languagedetector.
  2. Create your translation files in the /public/locales folder
  3. In your React components, use the t() function to show strings that are specific to your language.

What is the best React i18n library?

i18next is the leading option in the industry (used with react-i18next). It supports lazy loading, plurals, interpolation, and language detection.

There are also other libraries:

  • React-Intl (FormatJS) uses ICU message syntax and integrates well with enterprise systems.
  • i18n-js is a simpler, lighter option that is often used in React Native apps.

React i18next vs i18n-js

Both libraries can help you make your React and React Native apps more localized, but they are not as flexible as each other:

  • react-i18next has advanced features like namespaces, HTTP backends, suspense integration, and compatibility with Crowdin.
  • i18n-js is a lightweight tool that works well for small or mobile projects that only need basic translation.

Choose react-i18next if you care about scalability and automation. i18n-js is a good choice for quick setups.

What is the best software for React localization?

The best React localization software is Crowdin, a full-featured Translation Management System (TMS). It automates your i18n workflow with:

  • Syncing repositories for GitHub, GitLab, or Bitbucket
  • In-context editing for translators
  • Over-the-Air (OTA) updates for React Native
  • AI-assisted translations and QA checks

Crowdin takes care of file handling automatically, making it easier for developers to scale localization.

Azeem Sarwar

Azeem Sarwar

Azeem Sarwar is a full-stack web developer with over four years of experience specializing in SvelteKit, React, and Supabase. He enjoys building modern, scalable web applications and has a strong passion for crafting clean, user-centered UI and UX designs that blend functionality with great user experience.

Share this post: