I18next
"i18next: internationalization framework, React (react-i18next), namespaces, interpolation, plurals, backends, language detection"
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 linesi18next
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: truewith 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
CustomTypeOptionsfor compile-time key validation. - Use
getFixedT(lng, ns)in server contexts where you need a specific language regardless of the global state. - Set
escapeValue: falsein React projects since React handles XSS escaping already. - Use the
contextfeature 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 liket(\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
Transcomponent 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
fallbackLngwith complete translations. - Do not mix plural suffix styles (
_plurallegacy vs CLDR_one/_other) within the same project — pick one viacompatibilityJSON. - Avoid calling
changeLanguagewithout 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
readyflag whenuseSuspenseis false — rendering before translations load shows raw keys to users.
Install this skill directly: skilldb add i18n-services-skills
Related Skills
Crowdin
"Crowdin: translation management, OTA content delivery, API, CLI, GitHub integration, in-context editing, glossaries"
Lokalise
"Lokalise: translation management, API, SDKs, OTA updates, branching, screenshots, plural forms, CLI"
Next Intl
"next-intl: Next.js internationalization, message catalogs, routing, middleware, server components, formatters, pluralization"
Phrase
"Phrase (formerly PhraseApp): translation management, API, CLI, OTA, in-context editor, branching, webhooks, glossaries"
Pontoon
"Mozilla Pontoon: open-source translation management, Fluent/FTL support, in-place editing, community localization, VCS sync"
Transifex
"Transifex: translation management, Native SDK, OTA delivery, API v3, CLI, GitHub/GitLab integration, ICU plurals"