React i18n Internationalization Tutorial: From Zero to Production
Learn React i18n with i18next: setup, plurals, formatting, RTL, lazy loading, testing, and best practices for production-ready localization.
Image used for representation purposes only.
Overview
Internationalization (i18n) makes your React app readable, searchable, and usable by people in different languages, regions, and writing directions. In this tutorial you’ll build a robust, production‑grade i18n setup using React with i18next and react‑i18next. You’ll learn how to structure translations, switch languages at runtime, format dates and numbers, support RTL scripts, split translations per route for performance, test localized UI, and avoid common pitfalls.
What we’ll cover:
- Library choices and when to use each
- Step‑by‑step setup with Vite + React + TypeScript
- Translation files, namespaces, and lazy loading
- Plurals, interpolation, rich text, and gender/context
- Language switching, persistence, and RTL support
- Locale‑aware formatting with Intl APIs
- Testing and continuous localization practices
- Notes for SSR and Next.js
Choosing an i18n approach for React
Several mature options exist:
-
react‑i18next (with i18next)
- Pros: battle‑tested, dynamic runtime loading, powerful plural/context rules, broad ecosystem (detectors, backends, ICU plugin).
- Best for: SPAs that need runtime language switching and fine‑grained control.
-
FormatJS / react‑intl
- Pros: strong ICU MessageFormat support out of the box; great for complex message formatting.
- Best for: teams standardizing on ICU syntax and message extraction workflows.
-
LinguiJS
- Pros: dev‑friendly tooling and extraction; ICU‑style messages; minimal runtime.
- Best for: projects that want tree‑shakable catalogs and tight compile‑time integration.
This tutorial uses react‑i18next because it offers ergonomic React hooks, flexible loading strategies, and a rich plugin ecosystem.
Project setup
We’ll start from a fresh Vite + React + TypeScript project.
# 1) Create the project
npm create vite@latest my-i18n-app -- --template react-ts
cd my-i18n-app
# 2) Install i18n dependencies
npm install i18next react-i18next i18next-browser-languagedetector i18next-http-backend
# (optional) ICU message syntax
# npm install i18next-icu
# 3) Start dev server
npm run dev
Recommended folders:
src/
i18n/
index.ts # i18next init
components/
pages/
public/
locales/
en/
common.json
home.json
es/
common.json
home.json
Create your first translations
Add minimal catalogs under public/locales:
public/locales/en/common.json
{
"app_title": "Acme Store",
"language": "Language",
"cart_items_one": "You have {{count}} item in your cart",
"cart_items_other": "You have {{count}} items in your cart",
"welcome_user": "Welcome, {{name}}!",
"rich_example": "Click <link>here</link> to learn more"
}
public/locales/es/common.json
{
"app_title": "Tienda Acme",
"language": "Idioma",
"cart_items_one": "Tienes {{count}} artículo en tu carrito",
"cart_items_other": "Tienes {{count}} artículos en tu carrito",
"welcome_user": "¡Bienvenido, {{name}}!",
"rich_example": "Haz clic <link>aquí</link> para saber más"
}
Tip: Use explicit plural forms (…_one, …_other) so i18next can select the right variant based on count.
Initialize i18next
Create src/i18n/index.ts and configure runtime loading from public/locales using the HTTP backend and browser language detection.
src/i18n/index.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", "es"],
ns: ["common", "home"],
defaultNS: "common",
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json"
},
detection: {
order: ["querystring", "cookie", "localStorage", "navigator"],
caches: ["localStorage", "cookie"]
},
interpolation: {
escapeValue: false // React escapes by default
},
react: {
useSuspense: true
}
});
export default i18n;
Then import the initializer once at app startup so translations are ready before render.
src/main.tsx
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./i18n"; // initialize i18n
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Suspense fallback={<div>Loading…</div>}>
<App />
</Suspense>
</React.StrictMode>
);
Translating components with hooks and Trans
Use the useTranslation hook for simple strings and interpolation.
src/components/Header.tsx
import { useTranslation } from "react-i18next";
export function Header() {
const { t } = useTranslation();
return (
<header>
<h1>{t("app_title")}</h1>
</header>
);
}
Pluralization and interpolation:
function CartInfo({ count }: { count: number }) {
const { t } = useTranslation();
return <p>{t("cart_items_other", { count })}</p>; // i18next picks _one/_other automatically
}
Rich text using the Trans component (for markup like links or emphasis embedded in translations):
import { Trans, useTranslation } from "react-i18next";
function LearnMore() {
const { t } = useTranslation();
return (
<p>
<Trans i18nKey="rich_example" t={t}>
Click <a href="/learn">here</a> to learn more
</Trans>
</p>
);
}
Building a language switcher with persistence and RTL support
Create a switcher that updates i18n, persists choice (via detector cache), and toggles the document’s lang and dir attributes.
src/components/LanguageSwitcher.tsx
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
const RTL_LANGS = new Set(["ar", "he", "fa", "ur"]); // extend as needed
export function LanguageSwitcher() {
const { i18n, t } = useTranslation();
useEffect(() => {
const lng = i18n.resolvedLanguage || i18n.language || "en";
document.documentElement.lang = lng;
document.documentElement.dir = RTL_LANGS.has(lng) ? "rtl" : "ltr";
}, [i18n.language, i18n.resolvedLanguage]);
return (
<label>
{t("language")}:{" "}
<select
value={i18n.resolvedLanguage}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
<option value="en">English</option>
<option value="es">Español</option>
</select>
</label>
);
}
Accessibility note:
- Always keep updated for screen readers and tools.
- Switch text alignment and layout mirroring when dir=“rtl” by using logical CSS properties (inline‑start/inline‑end) or an RTL‑aware design system.
Formatting dates, numbers, and currency
Use the built‑in Intl APIs so formatting matches the active locale:
export function formatCurrency(value: number, locale: string, currency = "USD") {
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(value);
}
export function formatDate(d: Date, locale: string) {
return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "2-digit"
}).format(d);
}
Integrate with translations via interpolation:
import { useTranslation } from "react-i18next";
import { formatCurrency } from "../utils/format";
function Price({ amount }: { amount: number }) {
const { t, i18n } = useTranslation();
return <p>{t("price_label", { price: formatCurrency(amount, i18n.language) })}</p>;
}
Optional: If you prefer ICU messages (e.g., complex plurals/select), add i18next‑icu and place ICU strings in your JSON catalogs.
Namespaces and route‑level code splitting
Large apps benefit from splitting translations per feature/route. Load only what’s needed using namespaces and React Suspense.
Example: A Home page that uses the “home” namespace only when that route is active.
src/pages/Home.tsx
import { useTranslation } from "react-i18next";
export function Home() {
const { t } = useTranslation(["home", "common"]);
return <h2>{t("home:headline", "Welcome to our homepage")}</h2>;
}
public/locales/en/home.json
{ "headline": "Discover our newest collections" }
public/locales/es/home.json
{ "headline": "Descubre nuestras colecciones más recientes" }
With the HTTP backend and Suspense enabled, react‑i18next will fetch /locales/{lng}/home.json on demand the first time Home renders.
Context (gender) and advanced plural rules
Sometimes strings vary by gender or context. i18next supports contextual variants using the “.context” pattern.
Catalog:
{
"invite": "{{name}} invited you",
"invite_male": "{{name}} lo invitó",
"invite_female": "{{name}} la invitó"
}
Usage:
const { t } = useTranslation();
const gender: "male" | "female" = "female";
return <p>{t("invite", { context: gender, name: "Ana" })}</p>;
For complex language rules, consider ICU messages via i18next‑icu.
Testing localized UI
Write tests that assert rendered text for a given locale. Initialize i18n with in‑memory resources to keep tests fast.
src/test/i18n-test.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
export async function setupI18nForTests() {
await i18n.use(initReactI18next).init({
lng: "en",
fallbackLng: "en",
resources: {
en: { common: { app_title: "Acme Store" } }
},
defaultNS: "common",
interpolation: { escapeValue: false }
});
return i18n;
}
Example component test:
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Header } from "../components/Header";
import { setupI18nForTests } from "./i18n-test";
beforeAll(async () => {
await setupI18nForTests();
});
test("renders localized title", () => {
render(<Header />);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Acme Store");
});
Continuous localization workflow
- Use descriptive, stable keys. Prefer semantic keys (“checkout.place_order”) over raw English sentences as keys.
- Validate catalogs in CI: ensure all required keys exist for each supported language.
- Extract keys: i18next‑scanner or custom scripts can scan source for t("…") calls to build/update catalogs.
- Collaborate with translators via a TMS (translation management system). Many platforms integrate with i18next JSON.
- Lock down JSON shape with TypeScript: declare a Resource type and augment react‑i18next to type‑check t().
Type‑safe t() example:
// src/i18n/types.d.ts
import "react-i18next";
import type common from "../../public/locales/en/common.json";
import type home from "../../public/locales/en/home.json";
declare module "react-i18next" {
interface CustomTypeOptions {
defaultNS: "common";
resources: {
common: typeof common;
home: typeof home;
};
}
}
Now t(“app_title”) will be type‑checked, and invalid keys will raise compile‑time errors.
Performance tips
- Load only the namespaces needed on each route; keep “common” small.
- Enable HTTP caching for /locales/* assets and use content hashing when deploying.
- Avoid dynamic keys built from user input (they prevent static analysis and cache hits).
- Debounce language switches if triggered by rapid UI interactions.
Accessibility and UX considerations
- Keep messages concise and culturally neutral; avoid idioms that don’t translate well.
- Provide clear language names in their native form (“Deutsch”, “Français”).
- Mirror layout for RTL locales and test keyboard navigation thoroughly.
- Don’t use flags to represent languages; prefer language names.
SSR and routing notes (Next.js)
- For Next.js, you can use next‑i18next or wire i18next directly with server components.
- Preload required namespaces during SSR so content is hydrated with the correct language without flashes.
- Use locale‑prefixed routes (e.g., “/es/products”) and sync Next.js router locale with i18n.changeLanguage.
Common pitfalls and how to avoid them
- Concatenating translated fragments: prefer full sentences with interpolation instead of string building.
- Missing pluralization: always provide plural forms where applicable.
- Hard‑coded dates/currency: always format via Intl with the active locale.
- Forgetting html[lang] and dir: update them on language change for proper accessibility and layout.
- Oversized catalogs: split by namespace and lazy‑load per route.
Putting it all together
With i18next and react‑i18next you can deliver a polished, multilingual React application:
- Localize copy with t() and Trans
- Handle plurals, interpolation, and context/gender
- Switch languages instantly and persist the choice
- Format dates, numbers, and currency via Intl
- Support RTL scripts and accessible semantics
- Scale with namespaces, lazy loading, and type‑safe keys
- Test confidently and keep catalogs healthy in CI
From the first key to production deployment, the patterns in this tutorial will help you internationalize your React app without sacrificing performance or developer experience. Happy shipping—worldwide!
Related Posts
React Hooks Best Practices: Patterns for Clean, Correct, and Performant Components
A practical guide to React Hooks best practices: state, effects, memoization, refs, custom hooks, concurrency, testing, and pitfalls—with examples.
Mastering React Virtualized Lists: Performance Patterns and Tuning Guide
Build fast, memory‑efficient React virtualized lists with tuning tips for overscan, variable heights, a11y, and profiling—plus practical code examples.
React useMemo and useCallback: A Practical Optimization Guide
Practical guide to React’s useMemo and useCallback: when to use them, pitfalls to avoid, patterns, and profiling tips for faster apps.