By Akshay Singh
How to Add Localization to a React App with react-i18next
If you've ever built a React app and wondered "how will users from other countries use this?" — localization is the answer. Internationalization (i18n) ensures your app feels native to users in any region, whether they speak English, Hindi, Spanish, Arabic, or Japanese.
In this guide, I'll walk you through adding localization to a React application using react-i18next — a library I've found to be the most reliable, well-documented, and production-ready i18n solution for React.
By the end, you'll have a working multi-language React app with translation files, a language switcher, dynamic content interpolation, pluralization, and namespace-based code splitting.
Why Localize Your React App?
- Reach more users — localized apps can serve users in dozens of countries and languages without building separate apps.
- Better user experience — people engage more when an interface is in their own language. Bounce rates drop, session duration increases.
- Inclusivity — handle cultural formatting differences like dates, numbers, currency, and text direction correctly.
- SEO benefits — localized pages with proper
hreflangtags can rank in regional Google results. - Business growth — if you're building a SaaS, localization is often the difference between domestic-only and international revenue.
When I first added Hindi translations to a React project, I noticed how much smoother the user experience became. Users stayed longer just because the UI felt familiar.
How react-i18next Works
Before jumping into code, let's understand the architecture:
- i18next — the core i18n framework. Language-agnostic — works with React, Vue, Node.js, or plain JavaScript.
- react-i18next — the React binding. Provides hooks (
useTranslation), components (<Trans />), and HOCs for React components. - Plugins — extend i18next with backend loading (fetching translations from files/API), language detection (browser, cookie, URL), and caching.
The flow is:
User opens app
→ i18next detects browser language (or uses saved preference)
→ Loads the matching translation file (en/translation.json, hi/translation.json)
→ Components use t("key") to display the translated string
→ User switches language → i18next reloads translations → UI re-renders
Prerequisites
- A working React project (Vite, Create React App, or custom setup)
- Node.js and npm/yarn installed
- Basic knowledge of React hooks
Step 1 — Install Dependencies
npm install i18next react-i18next i18next-http-backend i18next-browser-languagedetector
Here's what each package does:
| Package | Purpose |
|---|---|
i18next | Core i18n framework |
react-i18next | React bindings (hooks, components) |
i18next-http-backend | Loads translation JSON files via HTTP |
i18next-browser-languagedetector | Detects user's language from browser settings, cookies, URL, or localStorage |
Step 2 — Create Translation Files
Create a folder structure inside public/ for your translation files:
public/
locales/
en/
translation.json
dashboard.json # optional: namespace for dashboard
hi/
translation.json
dashboard.json
es/
translation.json
Each language has its own folder with one or more namespace files. The default namespace is translation.
English (public/locales/en/translation.json):
{
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact",
"language": "Language"
},
"hero": {
"title": "Build faster with React",
"subtitle": "Ship international-ready apps in minutes",
"cta": "Get Started"
},
"footer": {
"copyright": "© {{year}} My Company. All rights reserved.",
"madeWith": "Made with ❤️ by {{author}}"
},
"common": {
"loading": "Loading...",
"error": "Something went wrong",
"retry": "Try again",
"save": "Save",
"cancel": "Cancel"
}
}
Hindi (public/locales/hi/translation.json):
{
"nav": {
"home": "घर",
"about": "हमारे बारे में",
"contact": "संपर्क",
"language": "भाषा"
},
"hero": {
"title": "React के साथ तेज़ बनाएं",
"subtitle": "कुछ ही मिनटों में अंतरराष्ट्रीय-तैयार ऐप्स लॉन्च करें",
"cta": "शुरू करें"
},
"footer": {
"copyright": "© {{year}} मेरी कंपनी। सर्वाधिकार सुरक्षित।",
"madeWith": "{{author}} द्वारा ❤️ से बनाया गया"
},
"common": {
"loading": "लोड हो रहा है...",
"error": "कुछ गड़बड़ हो गई",
"retry": "पुनः प्रयास करें",
"save": "सहेजें",
"cancel": "रद्द करें"
}
}
Tips for structuring translation keys:
- Group by feature or page section, not by component name
- Use nested objects for related keys (
nav.home,nav.about) - Keep keys semantic —
hero.titleis better thantext_1 - Use consistent naming — don't mix
home.titleandtitle.home
Step 3 — Initialize i18next
Create src/i18n.js:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import HttpBackend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
i18n
.use(HttpBackend) // Load translations via HTTP
.use(LanguageDetector) // Detect user's language
.use(initReactI18next) // Bind to React
.init({
// Fallback language when a translation is missing
fallbackLng: "en",
// Languages your app supports
supportedLngs: ["en", "hi", "es"],
// Don't escape values — React already handles XSS
interpolation: {
escapeValue: false,
},
// Where to load translation files from
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
// Language detection order
detection: {
order: ["localStorage", "navigator", "htmlTag"],
caches: ["localStorage"], // Save preference to localStorage
},
// Use Suspense for loading translations
react: {
useSuspense: true,
},
});
export default i18n;
Import this file in your entry point before rendering the app:
// src/main.jsx
import "./i18n"; // Initialize i18next BEFORE anything else
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.Suspense fallback={<div>Loading translations...</div>}>
<App />
</React.Suspense>
);
The Suspense boundary shows a fallback while translations load from the JSON files. Once loaded, your app renders with the correct language.
Step 4 — Use Translations in Components
The useTranslation hook is the primary way to access translations:
import { useTranslation } from "react-i18next";
function Header() {
const { t } = useTranslation();
return (
<nav>
<a href="/">{t("nav.home")}</a>
<a href="/about">{t("nav.about")}</a>
<a href="/contact">{t("nav.contact")}</a>
</nav>
);
}
Dynamic values with interpolation
Pass variables into translations using double curly braces {{variable}}:
function Footer() {
const { t } = useTranslation();
const year = new Date().getFullYear();
return (
<footer>
<p>{t("footer.copyright", { year })}</p>
<p>{t("footer.madeWith", { author: "Akshay" })}</p>
</footer>
);
}
Output (English): © 2025 My Company. All rights reserved.
Output (Hindi): © 2025 मेरी कंपनी। सर्वाधिकार सुरक्षित।
Step 5 — Language Switcher
Build a component that lets users change the language:
import { useTranslation } from "react-i18next";
const languages = [
{ code: "en", label: "English", flag: "🇬🇧" },
{ code: "hi", label: "हिन्दी", flag: "🇮🇳" },
{ code: "es", label: "Español", flag: "🇪🇸" },
];
function LanguageSwitcher() {
const { i18n, t } = useTranslation();
const handleChange = (langCode) => {
i18n.changeLanguage(langCode);
// The preference is automatically saved to localStorage
// (configured in the detection options)
};
return (
<div>
<span>{t("nav.language")}: </span>
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => handleChange(lang.code)}
style={{
fontWeight: i18n.language === lang.code ? "bold" : "normal",
marginLeft: "8px",
}}
aria-label={`Switch to ${lang.label}`}
>
{lang.flag} {lang.label}
</button>
))}
</div>
);
}
When the user selects a language:
i18n.changeLanguage()loads the new translation file- The language preference is saved to
localStorage - All components using
useTranslation()automatically re-render with the new translations
Step 6 — Advanced Features
Namespaces (Code Splitting Translations)
For large apps, loading all translations upfront is wasteful. Split them into namespaces — load only what the current page needs:
public/locales/en/
translation.json # Common/shared translations
dashboard.json # Dashboard-specific translations
settings.json # Settings-specific translations
Use a specific namespace in a component:
function DashboardPage() {
const { t } = useTranslation("dashboard");
return <h1>{t("welcome")}</h1>; // Reads from dashboard.json
}
Use multiple namespaces in one component:
function DashboardPage() {
const { t } = useTranslation(["dashboard", "common"]);
return (
<div>
<h1>{t("dashboard:welcome")}</h1>
<button>{t("common:save")}</button>
</div>
);
}
Pluralization
Different languages have different pluralization rules. i18next handles this correctly using the count parameter:
{
"notification_one": "You have {{count}} notification",
"notification_other": "You have {{count}} notifications"
}
<p>{t("notification", { count: 1 })}</p> // "You have 1 notification"
<p>{t("notification", { count: 5 })}</p> // "You have 5 notifications"
<p>{t("notification", { count: 0 })}</p> // "You have 0 notifications"
Note: i18next v21+ uses
_one,_other,_few,_manysuffixes (based on Unicode CLDR rules) instead of the old_pluralsuffix. This correctly handles languages like Arabic and Polish that have complex pluralization rules.
RTL (Right-to-Left) Support
For languages like Arabic, Hebrew, and Urdu, you need to set the text direction:
import { useTranslation } from "react-i18next";
import { useEffect } from "react";
function App() {
const { i18n } = useTranslation();
useEffect(() => {
const rtlLanguages = ["ar", "he", "ur"];
const dir = rtlLanguages.includes(i18n.language) ? "rtl" : "ltr";
document.documentElement.dir = dir;
document.documentElement.lang = i18n.language;
}, [i18n.language]);
return <div>{/* Your app content */}</div>;
}
In your CSS, use logical properties instead of left/right:
/* Instead of: margin-left: 16px; */
margin-inline-start: 16px;
/* Instead of: padding-right: 8px; */
padding-inline-end: 8px;
/* Instead of: text-align: left; */
text-align: start;
These CSS logical properties automatically flip for RTL languages.
The <Trans /> Component
For translations that contain HTML markup or React components, use the <Trans /> component:
{
"welcome": "Hello <bold>{{name}}</bold>, welcome to <link>our platform</link>!"
}
import { Trans } from "react-i18next";
function Welcome({ name }) {
return (
<Trans
i18nKey="welcome"
values={{ name }}
components={{
bold: <strong />,
link: <a href="/about" />,
}}
/>
);
}
Output: Hello Akshay, welcome to our platform!
SEO and Accessibility
For server-rendered apps (Next.js)
If you're using Next.js, consider next-intl or next-i18next which handle:
- Localized URLs (
/en/about,/hi/about) hreflangtags for each language version- Per-locale metadata (title, description)
- Server-side translation loading
For client-rendered apps (Vite/CRA)
- Set the
<html lang="...">attribute dynamically when the language changes - Translate your page
<title>and meta description - Provide fallback text for missing translations to avoid blank UI
Accessibility
- Use
aria-labelon the language switcher buttons - Announce language changes to screen readers using ARIA live regions
- Ensure translated content maintains proper heading hierarchy
Testing
Unit tests
Mock the useTranslation hook in your tests:
// __mocks__/react-i18next.js
export const useTranslation = () => ({
t: (key) => key, // Returns the key as the translation
i18n: {
changeLanguage: jest.fn(),
language: "en",
},
});
Integration tests
Test that language switching actually works:
import { render, screen, fireEvent } from "@testing-library/react";
import { I18nextProvider } from "react-i18next";
import i18n from "../i18n"; // Your actual i18n config
test("switches language correctly", async () => {
render(
<I18nextProvider i18n={i18n}>
<LanguageSwitcher />
</I18nextProvider>
);
fireEvent.click(screen.getByText("हिन्दी"));
// Assert that Hindi translations are now displayed
});
Manual verification checklist
- All visible text is translated (no hardcoded strings)
- Dynamic values (names, numbers, dates) render correctly in each language
- Pluralization works for 0, 1, and multiple items
- RTL layout renders correctly for Arabic/Hebrew
- Language preference persists across page reloads
- Fallback language works when a translation key is missing
Final Tips
- Start small — begin with 2 languages and expand. Adding more languages later is easy once the infrastructure is in place.
- Use a translation management tool like Crowdin, Locize, or Phrase if you work with translators. They provide visual context, version control, and collaborative workflows.
- Keep translation keys semantic and consistent —
auth.loginButtonnotbutton_3. - Don't translate developer-facing content — error codes, console logs, and API responses don't need localization.
- Test with long translations — German and Finnish translations are often 30-40% longer than English. Make sure your UI doesn't break.
Localizing React apps isn't complicated once the foundation is set up correctly. It opens your app to a global audience and makes your product feel polished and professional.
Keep Learning
- New to React? Start with our React vs Next.js comparison to understand which framework fits your project.
- Need state management alongside i18n? Learn how to integrate Redux Toolkit with React.
- Building with Next.js? Check our guide on Next.js 14.3+ App Router best practices.
- Ready to deploy your localized app? Follow our VPS hosting guide with Apache.
