Localization in React.js using react-i18next
Frontend DevelopmentUpdated: 12 min read

By Akshay Singh

Share this article:

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 hreflang tags 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:

  1. i18next — the core i18n framework. Language-agnostic — works with React, Vue, Node.js, or plain JavaScript.
  2. react-i18next — the React binding. Provides hooks (useTranslation), components (<Trans />), and HOCs for React components.
  3. 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:

PackagePurpose
i18nextCore i18n framework
react-i18nextReact bindings (hooks, components)
i18next-http-backendLoads translation JSON files via HTTP
i18next-browser-languagedetectorDetects 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 semantichero.title is better than text_1
  • Use consistent naming — don't mix home.title and title.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:

  1. i18n.changeLanguage() loads the new translation file
  2. The language preference is saved to localStorage
  3. 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, _many suffixes (based on Unicode CLDR rules) instead of the old _plural suffix. 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)
  • hreflang tags 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-label on 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 consistentauth.loginButton not button_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

reactlocalisationi18nreact-i18nextfrontend
TheDailyDevsTheDailyDevs
TheDailyDevs is a developer-first blog and knowledge hub created by passionate engineers to share real-world development tips, deep-dive tutorials, industry insights, and hands-on solutions to everyday coding challenges. Whether you're building apps, exploring new frameworks, or leveling up your dev game, you'll find practical, no-fluff content here, updated daily.