i18next with react-i18next is the most widely used i18n solution for React — i18n.use(initReactI18next).init({ resources: { en, fr }, lng: "en", fallbackLng: "en", interpolation: { escapeValue: false } }) initializes. const { t } = useTranslation("common") returns the translation function. t("greet", { name: "Alice" }) interpolates variables. t("item", { count: n }) handles plurals. <Trans i18nKey="html_message" values={{ name }}> renders JSX translations. i18next-browser-languagedetector picks language from navigator, cookie, or query param. Namespaces: useTranslation(["common", "forms"]) loads lazily. i18next-http-backend fetches JSON from /public/locales/{lng}/{ns}.json. i18n.changeLanguage("fr") switches at runtime. TypeScript: declare module "i18next" with DefaultResources for key inference. i18n.services.formatter?.add("relativeTime", ...) adds custom formatters. Claude Code generates i18next setup, typed translations, pluralization, and namespace splits.
CLAUDE.md for i18next
## i18next Stack
- Version: i18next >= 24, react-i18next >= 15
- Init: i18n.use(LanguageDetector).use(HttpBackend).use(initReactI18next).init({ backend: { loadPath: "/locales/{{lng}}/{{ns}}.json" }, fallbackLng: "en", ns: ["common"], defaultNS: "common", interpolation: { escapeValue: false } })
- Hook: const { t, i18n } = useTranslation("common")
- Plural: t("key", { count: n }) — JSON needs key, key_one, key_other per language
- Switch: i18n.changeLanguage("fr")
- Type-safe: declare module "i18next" { interface CustomTypeOptions { defaultNS: "common"; resources: { common: typeof enCommon } } }
i18n Configuration
// lib/i18n/config.ts — i18next initialization with backend and detector
import i18n from "i18next"
import { initReactI18next } from "react-i18next"
import LanguageDetector from "i18next-browser-languagedetector"
import HttpBackend from "i18next-http-backend"
// Supported locales
export const LOCALES = ["en", "fr", "de", "es", "ja", "ar"] as const
export type Locale = (typeof LOCALES)[number]
export const LOCALE_NAMES: Record<Locale, string> = {
en: "English",
fr: "Français",
de: "Deutsch",
es: "Español",
ja: "日本語",
ar: "العربية",
}
export const RTL_LOCALES = new Set<Locale>(["ar"])
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
lng: undefined, // Let the detector handle it
fallbackLng: "en",
supportedLngs: LOCALES,
ns: ["common", "auth", "errors", "dashboard"],
defaultNS: "common",
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
requestOptions: { cache: "no-cache" },
},
detection: {
order: ["querystring", "cookie", "localStorage", "navigator"],
lookupQuerystring: "lng",
lookupCookie: "i18n",
lookupLocalStorage: "i18nLang",
caches: ["localStorage", "cookie"],
},
interpolation: {
escapeValue: false, // React already handles XSS
},
react: {
useSuspense: true,
},
})
export default i18n
TypeScript Types
// lib/i18n/types.ts — type-safe translation keys
// Import translations to derive types
import type enCommon from "public/locales/en/common.json"
import type enAuth from "public/locales/en/auth.json"
import type enErrors from "public/locales/en/errors.json"
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "common"
resources: {
common: typeof enCommon
auth: typeof enAuth
errors: typeof enErrors
}
}
}
Language Switcher Component
// components/i18n/LanguageSwitcher.tsx — locale selector with direction support
"use client"
import { useTranslation } from "react-i18next"
import { useEffect } from "react"
import { LOCALES, LOCALE_NAMES, RTL_LOCALES, type Locale } from "@/lib/i18n/config"
export function LanguageSwitcher() {
const { i18n } = useTranslation()
const currentLocale = i18n.language as Locale
// Apply RTL direction to the document
useEffect(() => {
const dir = RTL_LOCALES.has(currentLocale) ? "rtl" : "ltr"
document.documentElement.dir = dir
document.documentElement.lang = currentLocale
}, [currentLocale])
const handleChange = (locale: Locale) => {
i18n.changeLanguage(locale)
}
return (
<div className="relative">
<select
value={currentLocale}
onChange={(e) => handleChange(e.target.value as Locale)}
className="appearance-none bg-transparent border rounded-lg pl-3 pr-8 py-1.5 text-sm cursor-pointer focus:outline-none focus:ring-2 focus:ring-ring"
>
{LOCALES.map((locale) => (
<option key={locale} value={locale}>
{LOCALE_NAMES[locale]}
</option>
))}
</select>
<span className="absolute right-2.5 top-1/2 -translate-y-1/2 text-xs pointer-events-none text-muted-foreground">
▾
</span>
</div>
)
}
Translated Form Component
// components/i18n/ContactForm.tsx — form with i18n validation and placeholders
"use client"
import { useTranslation, Trans } from "react-i18next"
import { useState } from "react"
import { z } from "zod"
export function ContactForm() {
const { t } = useTranslation("common")
const [submitted, setSubmitted] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({})
const schema = z.object({
name: z.string().min(2, t("validation.name_min", { min: 2 })),
email: z.string().email(t("validation.email_invalid")),
message: z.string().min(10, t("validation.message_min", { min: 10 })),
})
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const fd = new FormData(e.currentTarget)
const data = Object.fromEntries(fd)
const result = schema.safeParse(data)
if (!result.success) {
const fieldErrors: Record<string, string> = {}
result.error.errors.forEach((err) => {
if (err.path[0]) fieldErrors[String(err.path[0])] = err.message
})
setErrors(fieldErrors)
return
}
setErrors({})
await fetch("/api/contact", { method: "POST", body: JSON.stringify(result.data), headers: { "Content-Type": "application/json" } })
setSubmitted(true)
}
if (submitted) {
return (
<div className="p-6 rounded-xl bg-green-50 dark:bg-green-950/30 text-green-700 dark:text-green-400 text-sm">
<Trans i18nKey="contact.success_message" components={{ strong: <strong /> }} />
</div>
)
}
const fields = [
{ name: "name", label: t("contact.name"), placeholder: t("contact.name_placeholder"), type: "text" },
{ name: "email", label: t("contact.email"), placeholder: t("contact.email_placeholder"), type: "email" },
]
return (
<form onSubmit={handleSubmit} className="space-y-4">
<h2 className="text-lg font-semibold">{t("contact.title")}</h2>
{fields.map(({ name, label, placeholder, type }) => (
<div key={name} className="space-y-1">
<label className="text-sm font-medium">{label}</label>
<input
name={name}
type={type}
placeholder={placeholder}
className={`w-full px-3 py-2 rounded-lg border text-sm bg-background ${
errors[name] ? "border-destructive" : "border-input"
}`}
/>
{errors[name] && <p className="text-xs text-destructive">{errors[name]}</p>}
</div>
))}
<div className="space-y-1">
<label className="text-sm font-medium">{t("contact.message")}</label>
<textarea
name="message"
rows={4}
placeholder={t("contact.message_placeholder")}
className={`w-full px-3 py-2 rounded-lg border text-sm bg-background resize-none ${
errors.message ? "border-destructive" : "border-input"
}`}
/>
{errors.message && <p className="text-xs text-destructive">{errors.message}</p>}
</div>
<button
type="submit"
className="w-full py-2.5 rounded-xl bg-primary text-primary-foreground font-medium text-sm"
>
{t("contact.submit")}
</button>
</form>
)
}
Sample Translation File
// public/locales/en/common.json
{
"greet": "Hello, {{name}}!",
"item": "{{count}} item",
"item_other": "{{count}} items",
"validation": {
"name_min": "Name must be at least {{min}} characters",
"email_invalid": "Please enter a valid email address",
"message_min": "Message must be at least {{min}} characters"
},
"contact": {
"title": "Get in touch",
"name": "Full Name",
"name_placeholder": "John Doe",
"email": "Email Address",
"email_placeholder": "[email protected]",
"message": "Message",
"message_placeholder": "How can we help you?",
"submit": "Send Message",
"success_message": "Thank you! We'll get back to you <strong>within 24 hours</strong>."
}
}
For the next-intl alternative when building a Next.js App Router application and needing server component support, locale-based routing with next-intl/navigation, server-side translation loading without client hydration, and a simpler Next.js-first API — next-intl is purpose-built for the App Router model while i18next works across any React framework, see the next-intl guide. For the react-i18n (Format.js/react-intl) alternative when ICU message format with built-in plural, select, and date/number formatting primitives out of the box, strong CLDR data support, and enterprise-grade formatting are required — Format.js uses the industry-standard ICU syntax while i18next has a simpler key-value model, see the react-intl guide. The Claude Skills 360 bundle includes i18next skill sets covering backend loading, pluralization, and type-safe keys. Start with the free tier to try internationalization generation.