A complete implementation of internationalization (i18n) in Next.js 16 using i18next and react-i18next with the App Router.
- ✅ Full i18next integration with Next.js App Router
- ✅ TypeScript support with strongly typed translation keys
- ✅ Server and Client components with proper SSR and hydration
- ✅ SEO optimized with proper metadata, sitemap, and robots.txt
- ✅ Locale detection with automatic redirection via middleware
- ✅ Locale switcher with styled UI component
- ✅ Namespace-based translations for better organization
- ✅ Dynamic locale routing with static generation
- English (en) - Default
- French (fr)
.
├── i18n.config.ts # i18n configuration
└── src
├── locales
│ ├── en
│ │ ├── common.json # Common translations
│ │ └── about.json # About page translations
│ └── fr
│ ├── common.json
│ └── about.json
├── app
│ ├── i18n
│ │ └── server.ts # Server-side i18n initialization
│ ├── actions
│ │ └── get-current-locale.ts # Server actions for locale
│ └── [locale]
│ ├── layout.tsx # Root layout with locale handling
│ ├── page.tsx # Home page
│ └── about
│ ├── layout.tsx # About page metadata
│ └── page.tsx # About page
├── components
│ ├── I18nProvider.tsx # Client-side i18n provider
│ ├── ClientComponent.tsx # Example client component
│ ├── ServerComponent.tsx # Example server component
│ ├── LocaleSwitcher.tsx # Language switcher UI
│ └── LocalizedLink.tsx # Locale-aware Link component
├── middleware.ts # Locale detection middleware
├── i18n.namespaces.ts # Translation namespaces
└── types
└── i18next.d.ts # TypeScript augmentation
npm installnpm run devOpen http://localhost:3000 to see the app. You'll be automatically redirected to your locale (e.g., /en or /fr based on your browser language).
npm run build
npm startAll locale configuration is centralized in i18n.config.ts:
export const locales = ["en", "fr"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "en";Translations are organized by namespace in src/i18n.namespaces.ts:
export const namespaces = ["common", "about"] as const;
export type Namespace = (typeof namespaces)[number];Translation keys are strongly typed via TypeScript declaration merging in src/types/i18next.d.ts:
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "common";
resources: {
common: typeof import("@/locales/en/common.json");
about: typeof import("@/locales/en/about.json");
};
}
}This provides autocomplete and type safety for all translation keys.
Pages initialize i18next on the server to load translations before rendering:
const i18n = await initI18next(locale, namespaces);
const t = i18n.getFixedT(locale, "common");The I18nProvider receives pre-loaded translations from the server to prevent FOUC (Flash of Untranslated Content):
<I18nProvider locale={locale} namespaces={namespaces} resources={resources}>
{children}
</I18nProvider>The middleware automatically detects the user's preferred locale and redirects:
- Checks for
NEXT_LOCALEcookie (user preference) - Falls back to
Accept-Languageheader (browser language) - Redirects to locale-prefixed URL (e.g.,
/→/en)
- Metadata: Each page generates localized metadata with hreflang tags
- Sitemap: Includes all locale versions of each page
- Robots.txt: Properly excludes protected routes for all locales
"use client";
import { useTranslation } from "react-i18next";
export default function MyComponent() {
const { t, i18n } = useTranslation("common");
return (
<div>
<h1>{t("welcome")}</h1>
<p>Current locale: {i18n.language}</p>
</div>
);
}import { initI18next } from "@/app/i18n/server";
export default async function MyPage({ params: { locale } }) {
const i18n = await initI18next(locale, ["common"]);
const t = i18n.getFixedT(locale, "common");
return <h1>{t("welcome")}</h1>;
}import LocalizedLink from "@/components/LocalizedLink";
export default function Nav() {
return (
<nav>
<LocalizedLink href="/">Home</LocalizedLink>
<LocalizedLink href="/about">About</LocalizedLink>
</nav>
);
}-
Add the locale to
i18n.config.ts:export const locales = ["en", "fr", "es"] as const;
-
Create translation files:
src/locales/es/common.json src/locales/es/about.json -
Update
LocaleSwitcher.tsxwith the new locale label:const localeLabels: Record<Locale, string> = { en: "English", fr: "Français", es: "Español", };
-
Rebuild the app:
npm run build
-
Create the page file:
src/app/[locale]/new-page/page.tsx -
Create translation files:
src/locales/en/new-page.json src/locales/fr/new-page.json -
Add namespace to
src/i18n.namespaces.ts:export const namespaces = ["common", "about", "new-page"] as const;
-
Update TypeScript types in
src/types/i18next.d.ts:interface CustomTypeOptions { resources: { common: typeof import("@/locales/en/common.json"); about: typeof import("@/locales/en/about.json"); "new-page": typeof import("@/locales/en/new-page.json"); }; }
-
Add to sitemap in
src/app/sitemap.ts:const pages = ["/", "/about", "/new-page"];
- Use namespaces to organize translations by feature/page
- Load only required namespaces per page to reduce bundle size
- Pre-load translations on the server to prevent FOUC
- Use TypeScript for type-safe translation keys
- Set HTML lang and dir attributes for accessibility and SEO
- Generate static pages when possible for better performance
- Keep server components synchronous by passing pre-translated strings
MIT