Skip to main content
Technology & EngineeringI18n Services401 lines

I18next

"i18next: internationalization framework, React (react-i18next), namespaces, interpolation, plurals, backends, language detection"

Quick Summary18 lines
i18next is a standalone internationalization framework that runs in any JavaScript environment — browser, Node.js, React Native, Deno. It separates translation loading (backends), language detection, and rendering (framework bindings) into pluggable modules. Translations are organized into namespaces for code-splitting and lazy loading. The interpolation engine supports nesting, formatting, and context-based variants. Pluralization follows CLDR rules out of the box. react-i18next provides hooks and components that trigger re-renders when the language changes, with Suspense support for async translation loading.

## Key Points

- Split translations into namespaces that map to routes or features so they can be lazy-loaded.
- Use `useSuspense: true` with React Suspense boundaries for clean loading states.
- Register custom formatters for currency, dates, and relative time instead of formatting in component code.
- Provide full TypeScript type declarations via `CustomTypeOptions` for compile-time key validation.
- Use `getFixedT(lng, ns)` in server contexts where you need a specific language regardless of the global state.
- Set `escapeValue: false` in React projects since React handles XSS escaping already.
- Use the `context` feature for gendered or variant strings instead of building keys with string concatenation.
- Preload namespaces before route transitions with `i18n.loadNamespaces()` to avoid loading flashes.
- Store language preference in both cookies and localStorage so it persists across sessions and SSR.
- Use `i18n.cloneInstance()` when you need parallel requests in different languages on the server.
- Do not nest translation keys more than 2-3 levels deep — flat or shallow keys are easier to search and maintain.
- Avoid using `t()` calls with dynamically constructed keys like `t(\`error.\${code}\`)` — this breaks static analysis and type checking.
skilldb get i18n-services-skills/I18nextFull skill: 401 lines
Paste into your CLAUDE.md or agent config

i18next

Core Philosophy

i18next is a standalone internationalization framework that runs in any JavaScript environment — browser, Node.js, React Native, Deno. It separates translation loading (backends), language detection, and rendering (framework bindings) into pluggable modules. Translations are organized into namespaces for code-splitting and lazy loading. The interpolation engine supports nesting, formatting, and context-based variants. Pluralization follows CLDR rules out of the box. react-i18next provides hooks and components that trigger re-renders when the language changes, with Suspense support for async translation loading.

Setup

Core initialization

// npm install i18next react-i18next i18next-http-backend i18next-browser-languagedetector

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import HttpBackend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(HttpBackend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: "en",
    supportedLngs: ["en", "fr", "de", "ja"],
    debug: process.env.NODE_ENV === "development",

    ns: ["common", "dashboard", "auth"],
    defaultNS: "common",

    interpolation: {
      escapeValue: false, // React already escapes
    },

    backend: {
      loadPath: "/locales/{{lng}}/{{ns}}.json",
      addPath: "/locales/add/{{lng}}/{{ns}}",
    },

    detection: {
      order: ["querystring", "cookie", "localStorage", "navigator"],
      caches: ["cookie", "localStorage"],
      lookupQuerystring: "lng",
      lookupCookie: "i18next",
    },

    react: {
      useSuspense: true,
      bindI18n: "languageChanged",
      transEmptyNodeValue: "",
    },
  });

export default i18n;

Translation files structure

// public/locales/en/common.json
{
  "welcome": "Welcome, {{name}}!",
  "nav": {
    "home": "Home",
    "settings": "Settings",
    "logout": "Log out"
  },
  "itemCount": "{{count}} item",
  "itemCount_plural": "{{count}} items",
  "itemCount_zero": "No items",
  "lastSeen": "Last seen {{- date}}",
  "greeting_male": "Mr. {{name}}",
  "greeting_female": "Ms. {{name}}"
}

// public/locales/en/dashboard.json
{
  "title": "Dashboard",
  "stats": {
    "revenue": "Revenue: {{amount, currency(USD)}}",
    "users": "{{count}} active user",
    "users_plural": "{{count}} active users"
  },
  "chart": {
    "noData": "No data available for the selected period."
  }
}

Key Techniques

React hooks usage

import { useTranslation, Trans } from "react-i18next";

function DashboardPage() {
  const { t, i18n } = useTranslation("dashboard");
  const { t: tCommon } = useTranslation("common");

  const changeLanguage = (lng: string) => {
    i18n.changeLanguage(lng);
  };

  return (
    <div>
      <h1>{t("title")}</h1>
      <p>{t("stats.revenue", { amount: 12500 })}</p>
      <p>{t("stats.users", { count: 42 })}</p>
      <p>{tCommon("welcome", { name: "Alice" })}</p>

      {/* Language switcher */}
      <select
        value={i18n.language}
        onChange={(e) => changeLanguage(e.target.value)}
      >
        <option value="en">English</option>
        <option value="fr">Francais</option>
        <option value="de">Deutsch</option>
      </select>
    </div>
  );
}

Trans component for rich text

import { Trans, useTranslation } from "react-i18next";

// In translation file:
// "terms": "By signing up you agree to our <link>Terms of Service</link> and <bold>Privacy Policy</bold>."

function LegalNotice() {
  const { t } = useTranslation("auth");

  return (
    <p>
      <Trans
        i18nKey="terms"
        t={t}
        components={{
          link: <a href="/terms" />,
          bold: <strong />,
        }}
      />
    </p>
  );
}

// For lists and complex structures:
// "features": "<ul><li>Fast setup</li><li>Real-time sync</li><li>API access</li></ul>"
function FeatureList() {
  const { t } = useTranslation();
  return (
    <Trans
      i18nKey="features"
      components={{
        ul: <ul className="feature-list" />,
        li: <li className="feature-item" />,
      }}
    />
  );
}

Custom formatters

import i18n from "i18next";

// Register custom formatters after init
i18n.services.formatter?.add("currency", (value, lng, options) => {
  const currency = options?.currency ?? "USD";
  return new Intl.NumberFormat(lng, {
    style: "currency",
    currency,
  }).format(value);
});

i18n.services.formatter?.add("relativeTime", (value, lng) => {
  const rtf = new Intl.RelativeTimeFormat(lng, { numeric: "auto" });
  const diffMs = new Date(value).getTime() - Date.now();
  const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));

  if (Math.abs(diffDays) < 1) {
    const diffHours = Math.round(diffMs / (1000 * 60 * 60));
    return rtf.format(diffHours, "hour");
  }
  return rtf.format(diffDays, "day");
});

i18n.services.formatter?.add("uppercase", (value) => {
  return String(value).toUpperCase();
});

// Usage in translation files:
// "price": "Total: {{amount, currency(EUR)}}"
// "updated": "Updated {{date, relativeTime}}"
// "code": "Your code: {{code, uppercase}}"

Namespaces and lazy loading

import { useTranslation } from "react-i18next";
import { Suspense, lazy } from "react";

// Lazy-load a namespace on demand
function SettingsPage() {
  const { t, ready } = useTranslation("settings", { useSuspense: false });

  if (!ready) return <div>Loading translations...</div>;

  return <div>{t("title")}</div>;
}

// With Suspense boundary
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <SettingsPage />
    </Suspense>
  );
}

// Preload namespaces before navigation
async function navigateToSettings(router: any) {
  await i18n.loadNamespaces("settings");
  router.push("/settings");
}

// Load multiple namespaces in one component
function AdminPanel() {
  const { t } = useTranslation(["admin", "common", "dashboard"]);

  return (
    <div>
      <h1>{t("admin:title")}</h1>
      <p>{t("common:welcome", { name: "Admin" })}</p>
      <p>{t("dashboard:stats.users", { count: 100 })}</p>
    </div>
  );
}

Context and pluralization

// Translation keys with context:
// "friend": "A friend"
// "friend_male": "A boyfriend"
// "friend_female": "A girlfriend"
// "friend_plural": "{{count}} friends"
// "friend_male_plural": "{{count}} boyfriends"
// "friend_female_plural": "{{count}} girlfriends"

function FriendDisplay({ count, gender }: { count: number; gender: string }) {
  const { t } = useTranslation();

  return (
    <span>
      {t("friend", { count, context: gender })}
    </span>
  );
}

// Ordinal plurals (requires i18next-icu plugin or manual suffixes):
// "place_ordinal_one": "{{count}}st place"
// "place_ordinal_two": "{{count}}nd place"
// "place_ordinal_few": "{{count}}rd place"
// "place_ordinal_other": "{{count}}th place"

Server-side rendering with Next.js

// For Next.js Pages Router with next-i18next:
// npm install next-i18next

// next-i18next.config.js
const nextI18NextConfig = {
  i18n: {
    defaultLocale: "en",
    locales: ["en", "fr", "de"],
  },
  localePath: "./public/locales",
  reloadOnPrerender: process.env.NODE_ENV === "development",
};

export default nextI18NextConfig;

// In _app.tsx
import { appWithTranslation } from "next-i18next";
import nextI18NextConfig from "../next-i18next.config";

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default appWithTranslation(MyApp, nextI18NextConfig);

// In page components
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

export async function getStaticProps({ locale }: { locale: string }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ["common", "home"])),
    },
  };
}

TypeScript type safety

// i18next.d.ts
import "i18next";
import common from "../public/locales/en/common.json";
import dashboard from "../public/locales/en/dashboard.json";
import auth from "../public/locales/en/auth.json";

declare module "i18next" {
  interface CustomTypeOptions {
    defaultNS: "common";
    resources: {
      common: typeof common;
      dashboard: typeof dashboard;
      auth: typeof auth;
    };
  }
}

// Now t("common:nav.home") is type-checked.
// t("common:nav.typo") would produce a TypeScript error.

Backend plugins for Node.js

// Server-side with filesystem backend
// npm install i18next-fs-backend

import i18next from "i18next";
import FsBackend from "i18next-fs-backend";
import path from "path";

async function initServerI18n(): Promise<typeof i18next> {
  await i18next.use(FsBackend).init({
    lng: "en",
    fallbackLng: "en",
    ns: ["common", "emails"],
    defaultNS: "common",
    backend: {
      loadPath: path.join(__dirname, "locales/{{lng}}/{{ns}}.json"),
    },
  });
  return i18next;
}

// Use in API routes or email templates
async function sendWelcomeEmail(userLang: string, name: string) {
  const t = await i18next.cloneInstance({ lng: userLang }).loadNamespaces("emails");
  const subject = i18next.getFixedT(userLang, "emails")("welcome.subject", { name });
  const body = i18next.getFixedT(userLang, "emails")("welcome.body", { name });
  // send email with subject and body
}

Best Practices

  • Split translations into namespaces that map to routes or features so they can be lazy-loaded.
  • Use useSuspense: true with React Suspense boundaries for clean loading states.
  • Register custom formatters for currency, dates, and relative time instead of formatting in component code.
  • Provide full TypeScript type declarations via CustomTypeOptions for compile-time key validation.
  • Use getFixedT(lng, ns) in server contexts where you need a specific language regardless of the global state.
  • Set escapeValue: false in React projects since React handles XSS escaping already.
  • Use the context feature for gendered or variant strings instead of building keys with string concatenation.
  • Preload namespaces before route transitions with i18n.loadNamespaces() to avoid loading flashes.
  • Store language preference in both cookies and localStorage so it persists across sessions and SSR.
  • Use i18n.cloneInstance() when you need parallel requests in different languages on the server.

Anti-Patterns

  • Do not nest translation keys more than 2-3 levels deep — flat or shallow keys are easier to search and maintain.
  • Avoid using t() calls with dynamically constructed keys like t(\error.${code}`)` — this breaks static analysis and type checking.
  • Do not initialize i18next multiple times; call init() once and import the instance everywhere.
  • Avoid putting HTML directly in translation strings; use the Trans component with named components instead.
  • Do not load all namespaces upfront in SPAs — this defeats the purpose of namespace-based code splitting.
  • Avoid falling back to key names as user-visible text; always provide a proper fallbackLng with complete translations.
  • Do not mix plural suffix styles (_plural legacy vs CLDR _one/_other) within the same project — pick one via compatibilityJSON.
  • Avoid calling changeLanguage without awaiting it — translations may not be loaded yet when components re-render.
  • Do not store translations in component state; always read from t() so language changes propagate correctly.
  • Avoid ignoring the ready flag when useSuspense is false — rendering before translations load shows raw keys to users.

Install this skill directly: skilldb add i18n-services-skills

Get CLI access →