Claude Code for i18next: React Internationalization — Claude Skills 360 Blog
Blog / Frontend / Claude Code for i18next: React Internationalization
Frontend

Claude Code for i18next: React Internationalization

Published: June 23, 2027
Read time: 6 min read
By: Claude Skills 360

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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free