Implement i18n in Next.js with routing, translations, locale detection, and multi-language support.
[locale] folder to organize them by language.app/
[locale]/
layout.tsx
page.tsx
about/
page.tsx/en, /es, /en/about, /es/about, etc.params is an async Promise that must be awaited.export default async function Layout({
children,
params
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return <html lang={locale}>{children}</html>;
}locales/
en.json
es.json
fr.json{
"common": {
"welcome": "Welcome",
"goodbye": "Goodbye"
},
"home": {
"title": "Home Page",
"description": "Welcome to our site"
}
}import translations from "@/locales/en.json";
const message = translations.common.welcome;next-intl for complex needs.npm install next-intlimport { getRequestConfig } from "next-intl/server";
export default getRequestConfig(async ({ requestLocale }) => {
const locale = await requestLocale;
return {
locale,
messages: (await import(`./messages/${locale}.json`)).default
};
});useTranslations hook to access translations in components."use client";
import { useTranslations } from "next-intl";
export default function HomePage() {
const t = useTranslations("home");
return <h1>{t("title")}</h1>;
}middleware.ts to redirect users to the right locale automatically.// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const acceptLang = request.headers.get("accept-language") ?? "en";
const locale = acceptLang.split(",")[0].split("-")[0];
const supported = ["en", "es", "fr"];
const chosen = supported.includes(locale) ? locale : "en";
if (!pathname.startsWith(`/${chosen}`)) {
return NextResponse.redirect(new URL(`/${chosen}${pathname}`, request.url));
}
return NextResponse.next();
}const cookieLocale = request.cookies.get("locale")?.value;
const chosen = (cookieLocale && supported.includes(cookieLocale))
? cookieLocale
: detectedLocale;"use client";
import { useRouter } from "next/navigation";
export function LocaleSwitcher({ current }: { current: string }) {
const router = useRouter();
const switchLocale = (locale: string) => {
document.cookie = `locale=${locale};path=/`;
router.push(`/${locale}`);
};
return (
<select value={current} onChange={(e) => switchLocale(e.target.value)}>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
);
}useLocale() from next-intl to read the active locale in client components.lang on the HTML element per locale.<html lang={locale}>export async function generateMetadata({ params }) {
const alternates = {
languages: {
en: `https://example.com/en/page`,
es: `https://example.com/es/page`,
fr: `https://example.com/fr/page`
}
};
return { alternates };
}Create a [locale] dynamic segment at the root of your app directory (e.g., app/[locale]/page.tsx) and configure supported locales in middleware.ts to redirect users and validate locale params. This gives you clean URLs like /en/about and /fr/about automatically.
Use next-intl when you need pluralization rules, number/date formatting, or ICU message syntax, since the built-in Next.js i18n only handles routing. next-intl also provides React hooks like useTranslations that make accessing nested keys and interpolating variables much cleaner.
By default, Next.js detects locale from the Accept-Language request header and redirects accordingly. You can override this in middleware.ts by reading cookies, session data, or custom headers before the redirect, giving you full control over the detection priority.
Keep one JSON file per locale per namespace under a messages or locales directory (e.g., messages/en.json, messages/es.json) and split by feature or page to avoid loading all strings upfront. With next-intl, pass the relevant namespace to unstable_setRequestLocale and useTranslations('namespace') to scope your keys.
Export a generateMetadata function in each page and return an alternates.languages object mapping each locale to its full URL (e.g., { en: 'https://example.com/en/page', es: 'https://example.com/es/page' }). Next.js renders these as tags in the
automatically.