Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
If you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
Translate your Next.js 15 using next-i18next website using Intlayer | Internationalization (i18n)
Who this guide is for
- Junior: Follow the exact steps and copy the code blocks. You’ll get a working multilingual app.
- Mid-level: Use the checklists and best-practice callouts to avoid common pitfalls.
- Senior: Skim the high-level structure, SEO, and automation sections; you’ll find sensible defaults and extension points.
What you’ll build
- App Router project with localized routes (e.g.,
/,/fr/...) - i18n config with locales, default locale, RTL support
- Server-side i18n initialization and a client provider
- Namespaced translations loaded on-demand
- SEO with
hreflang, localizedsitemap,robots - Middleware for locale routing
- Intlayer integration to automate translation workflows (tests, AI fill, JSON sync)
Note: next-i18next is built on top of i18next. This guide uses the i18next primitives compatible with next-i18next in the App Router, while keeping the architecture simple and production-ready. For a broader comparison, see next-i18next vs next-intl vs Intlayer.
1) Project structure
Install the next-i18next dependencies:
Copy the code to the clipboard
npm install next-i18next i18next react-i18next i18next-resources-to-backendStart with a clear structure. Keep messages split by locale and namespace.
Copy the code to the clipboard
.├── i18n.config.ts└── src ├── locales │ ├── en │ │ ├── common.json │ │ └── about.json │ └── fr │ ├── common.json │ └── about.json ├── app │ ├── i18n │ │ └── server.ts │ └── [locale] │ ├── layout.tsx │ └── about.tsx └── components ├── I18nProvider.tsx ├── ClientComponent.tsx └── ServerComponent.tsxChecklist (mid/senior):
- Keep one JSON per namespace per locale
- Do not over-centralize messages; use small, page/feature-scoped namespaces
- Avoid importing all locales at once; load only what you need
2) Install dependencies
Copy the code to the clipboard
pnpm add i18next react-i18next i18next-resources-to-backendIf you plan to use next-i18next APIs or config interop, also:
Copy the code to the clipboard
pnpm add next-i18next3) Core i18n config
Define locales, default locale, RTL, and helpers for localized paths/URLs.
Copy the code to the clipboard
export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export const rtlLocales = ["ar", "he", "fa", "ur"] as const;export const isRtl = (locale: string) => (rtlLocales as readonly string[]).includes(locale);export function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";export function abs(locale: string, path: string) { return ORIGIN + localizedPath(locale, path);}Senior note: If you use next-i18next.config.js, keep it aligned with i18n.config.ts to avoid drift.
4) Server-side i18n initialization
Initialize i18next on the server with a dynamic backend that imports only the required locale/namespace JSON.
Copy the code to the clipboard
import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";// Load JSON resources from src/locales/<locale>/<namespace>.jsonconst backend = resourcesToBackend( (locale: string, namespace: string) => import(`../../locales/${locale}/${namespace}.json`));export async function initI18next( locale: string, namespaces: string[] = ["common"]) { const i18n = createInstance(); await i18n .use(initReactI18next) .use(backend) .init({ lng: locale, fallbackLng: defaultLocale, ns: namespaces, defaultNS: "common", interpolation: { escapeValue: false }, react: { useSuspense: false }, }); return i18n;}Mid note: Keep the namespace list short per page to limit payload. Avoid global “catch-all” bundles.
5) Client provider for React components
Wrap client components with a provider that mirrors the server config and loads only the requested namespaces.
Copy the code to the clipboard
"use client";import * as React from "react";import { I18nextProvider } from "react-i18next";import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";const backend = resourcesToBackend( (locale: string, namespace: string) => import(`../../locales/${locale}/${namespace}.json`));type Props = { locale: string; namespaces?: string[]; resources?: Record<string, any>; // { ns: bundle } children: React.ReactNode;};export default function I18nProvider({ locale, namespaces = ["common"], resources, children,}: Props) { const [i18n] = React.useState(() => { const i = createInstance(); i.use(initReactI18next) .use(backend) .init({ lng: locale, fallbackLng: defaultLocale, ns: namespaces, resources: resources ? { [locale]: resources } : undefined, defaultNS: "common", interpolation: { escapeValue: false }, react: { useSuspense: false }, }); return i; }); return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;}Junior tip: You don’t need to pass all messages to the client. Start with the page’s namespaces only.
6) Localized layout and routes
Set language and direction, and pre-generate routes per locale to favor static rendering.
Copy the code to the clipboard
import type { ReactNode } from "react";import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";export const dynamicParams = false;export function generateStaticParams() { return locales.map((locale) => ({ locale }));}export default function LocaleLayout({ children, params,}: { children: ReactNode; params: { locale: string };}) { const locale: Locale = (locales as readonly string[]).includes(params.locale) ? params.locale : defaultLocale; const dir = isRtl(locale) ? "rtl" : "ltr"; return ( <html lang={locale} dir={dir}> <body>{children}</body> </html> );}7) Example page with server + client usage
Copy the code to the clipboard
import I18nProvider from "@/components/I18nProvider";import { initI18next } from "@/app/i18n/server";import type { Locale } from "@/i18n.config";import ClientComponent from "@/components/ClientComponent";import ServerComponent from "@/components/ServerComponent";// Force static rendering for the pageexport const dynamic = "force-static";export default async function AboutPage({ params: { locale },}: { params: { locale: Locale };}) { const namespaces = ["common", "about"] as const; const i18n = await initI18next(locale, [...namespaces]); const tAbout = i18n.getFixedT(locale, "about"); return ( <I18nProvider locale={locale} namespaces={[...namespaces]}> <main> <h1>{tAbout("title")}</h1> <ClientComponent /> <ServerComponent t={tAbout} locale={locale} count={0} /> </main> </I18nProvider> );}Translations (one JSON per namespace under src/locales/...):
Copy the code to the clipboard
{ "title": "About", "description": "About page description", "counter": { "label": "Counter", "increment": "Increment" }}Copy the code to the clipboard
{ "title": "À propos", "description": "Description de la page À propos", "counter": { "label": "Compteur", "increment": "Incrémenter" }}Client component (loads only the required namespace):
Copy the code to the clipboard
"use client";import React, { useState } from "react";import { useTranslation } from "react-i18next";const ClientComponent = () => { const { t, i18n } = useTranslation("about"); const [count, setCount] = useState(0); const numberFormat = new Intl.NumberFormat(i18n.language); return ( <div> <p>{numberFormat.format(count)}</p> <button aria-label={t("counter.label")} onClick={() => setCount((c) => c + 1)} > {t("counter.increment")} </button> </div> );};export default ClientComponent;Ensure the page/provider includes only the namespaces you need (e.g.,
about). If you use React < 19, memoize heavy formatters likeIntl.NumberFormat.
Synchronous server component embedded under a client boundary:
Copy the code to the clipboard
type ServerComponentProps = { t: (key: string) => string; locale: string; count: number;};const ServerComponent = ({ t, locale, count }: ServerComponentProps) => { const formatted = new Intl.NumberFormat(locale).format(count); return ( <div> <p>{formatted}</p> <button aria-label={t("counter.label")}>{t("counter.increment")}</button> </div> );};export default ServerComponent;8) SEO: Metadata, Hreflang, Sitemap, Robots
Translating content is a means to improve reach. Wire up multilingual SEO thoroughly.
Best practices:
- Set
langanddirat the root - Add
alternates.languagesfor each locale (+x-default) - List translated URLs in
sitemap.xmland usehreflang - Exclude localized private areas (e.g.,
/fr/admin) inrobots.txt
Copy the code to the clipboard
import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; // Import the correct JSON bundle from src/locales const messages = (await import("@/locales/" + locale + "/about.json")) .default; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, "/about")]) ); return { title: messages.title, description: messages.description, alternates: { canonical: localizedPath(locale, "/about"), languages: { ...languages, "x-default": "/about" }, }, };}export default async function AboutPage() { return <h1>About</h1>;}Copy the code to the clipboard
import type { MetadataRoute } from "next";import { locales, defaultLocale, abs } from "@/i18n.config";export default function sitemap(): MetadataRoute.Sitemap { const languages = Object.fromEntries( locales.map((locale) => [locale, abs(locale, "/about")]) ); return [ { url: abs(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", priority: 0.7, alternates: { languages }, }, ];}Copy the code to the clipboard
import type { MetadataRoute } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";const ORIGIN = "https://example.com";const expandAllLocales = (path: string) => [ localizedPath(defaultLocale, path), ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => localizedPath(locale, path)),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...expandAllLocales("/dashboard"), ...expandAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: ORIGIN, sitemap: ORIGIN + "/sitemap.xml", };}9) Middleware for locale routing
Detect locale and redirect to a localized route if missing.
Copy the code to the clipboard
import { NextResponse, type NextRequest } from "next/server";import { defaultLocale, locales } from "@/i18n.config";const PUBLIC_FILE = /\.[^/]+$/; // exclude files with extensionsexport function middleware(request: NextRequest) { const { pathname } = request.nextUrl; if ( pathname.startsWith("/_next") || pathname.startsWith("/api") || pathname.startsWith("/static") || PUBLIC_FILE.test(pathname) ) { return; } const hasLocale = locales.some( (locale) => pathname === "/" + locale || pathname.startsWith("/" + locale + "/") ); if (!hasLocale) { const locale = defaultLocale; const url = request.nextUrl.clone(); url.pathname = "/" + locale + (pathname === "/" ? "" : pathname); return NextResponse.redirect(url); }}export const config = { matcher: [ // Match all paths except the ones starting with these and files with an extension "/((?!api|_next|static|.*\\..*).*)", ],};10) Performance and DX best practices
- Set html
langanddir: Done insrc/app/[locale]/layout.tsx. - Split messages by namespace: Keep bundles small (
common.json,about.json, etc.). - Minimize client payload: On pages, pass only required namespaces to the provider.
- Prefer static pages: Use
export const dynamic = 'force-static'andgenerateStaticParamsper locale. - Sync server components: Pass precomputed strings/formatting instead of async calls at render time.
- Memoize heavy operations: Especially in client code for older React versions.
- Cache and headers: Prefer static or
revalidateover dynamic rendering when feasible.
11) Testing and CI
- Add unit tests for components using
tto ensure keys exist. - Validate that each namespace has the same keys across locales.
- Surface missing keys during CI before deploy.
Intlayer will automate much of this (see next section).
12) Add Intlayer on top (automation)
Intlayer helps you keep JSON translations in sync, test for missing keys, and fill with AI when desired.
Install the intlayer dependencies:
Copy the code to the clipboard
npm install intlayer @intlayer/sync-json-plugin --save-devnpx intlayer initCopy the code to the clipboard
import { type IntlayerConfig, Locales } from "intlayer";import { locales, defaultLocale } from "@/i18n";import { syncJSON } from "@intlayer/sync-json";export const locales = [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH];const config: IntlayerConfig = { internationalization: { locales, defaultLocale, }, ai: { apiKey: process.env.OPENAI_API_KEY, }, plugins: [ syncJSON({ source: ({ locale }) => `./locales/${locale}.json`, }), ],};export default config;Add package scripts:
Copy the code to the clipboard
{ "scripts": { "i18n:fill": "intlayer fill", "i18n:test": "intlayer test" }}Common flows:
pnpm i18n:testin CI to fail builds on missing keyspnpm i18n:filllocally to propose AI translations for newly added keys
You can provide CLI arguments; see the Intlayer CLI docs.
13) Troubleshooting
- Keys not found: Ensure the page/provider lists the correct namespaces and the JSON file exists under
src/locales/<locale>/<namespace>.json. - Wrong language/flash of English: Double-check locale detection in
middleware.tsand providerlng. - RTL layout issues: Verify
diris derived fromisRtl(locale)and that your CSS respects[dir="rtl"]. - SEO alternates missing: Confirm
alternates.languagesincludes all locales andx-default. - Bundles too large: Split namespaces further and avoid importing entire
localestrees on the client.
14) What’s next
- Add more locales and namespaces as features grow
- Localize error pages, emails, and API-driven content
- Extend Intlayer workflows to auto-open PRs for translation updates
If you prefer a starter, try the template: https://github.com/aymericzip/intlayer-next-i18next-template.