Vous avez un site Next.js qui cartonne en France et vous voulez l'ouvrir à l'international ? Ou vous démarrez un projet qui doit dès le départ servir français, anglais et espagnol ? L'internationalisation (i18n) est l'un des chantiers les plus sous-estimés en frontend : entre le routing par locale, les traductions, les formats de dates et de devises, le SEO multilingue et les Server Components, il y a vite de quoi se perdre.
Dans cet article, je vous montre comment mettre en place une i18n robuste avec Next.js 16 (App Router) et next-intl, la lib qui s'intègre proprement avec les Server Components, le streaming et le routing dynamique.
Pourquoi next-intl plutôt qu'autre chose ?
Le paysage des libs i18n pour React est saturé : react-i18next, lingui, formatjs, next-translate… Pourquoi je recommande next-intl sur Next.js 16 ?
- Compatible Server Components nativement : pas besoin de tout passer en
"use client"pour traduire un texte. C'est devenu critique avec l'App Router. - Routing par locale intégré : middleware prêt à l'emploi pour gérer
/fr,/en,/esavec détection auto via le headerAccept-Language. - Types TypeScript stricts : vos clés de traduction sont auto-complétées et vérifiées au build. Plus de typo silencieuse en prod.
- Formatage standard : dates, nombres, devises et pluriels via l'API
Intldu navigateur, sans dépendance lourde.
L'alternative sérieuse est next-translate, mais elle est moins activement maintenue et son support des Server Components est plus bancal.
Installation et configuration de base
Je pars du principe que vous avez un projet Next.js 16 avec TypeScript et l'App Router. Si ce n'est pas le cas, vous pouvez consulter mon article sur les Server Actions Next.js 15 qui présente le setup de base.
Installation :
npm install next-intl
Créez un dossier messages/ à la racine pour stocker vos traductions :
messages/
├── fr.json
├── en.json
└── es.json
messages/fr.json :
{
"Home": {
"title": "Bienvenue sur mon site",
"description": "Développeur web freelance basé en France",
"cta": "Discutons de votre projet"
},
"Common": {
"loading": "Chargement…",
"error": "Une erreur est survenue"
}
}
messages/en.json :
{
"Home": {
"title": "Welcome to my website",
"description": "Freelance web developer based in France",
"cta": "Let's talk about your project"
},
"Common": {
"loading": "Loading…",
"error": "An error occurred"
}
}
Structure du projet avec routing par locale
next-intl recommande de placer toutes les pages dans un segment dynamique [locale]. Cela permet d'avoir des URLs comme /fr/blog/mon-article et /en/blog/my-article.
src/
├── app/
│ └── [locale]/
│ ├── layout.tsx
│ ├── page.tsx
│ └── blog/
│ └── [slug]/
│ └── page.tsx
├── i18n/
│ ├── routing.ts
│ └── request.ts
├── middleware.ts
└── messages/
├── fr.json
└── en.json
1. Définir les locales et le routing
src/i18n/routing.ts :
import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";
export const routing = defineRouting({
locales: ["fr", "en", "es"] as const,
defaultLocale: "fr",
localePrefix: "as-needed", // /fr est optionnel pour la locale par défaut
});
export type Locale = (typeof routing.locales)[number];
// Wrappers typés pour la navigation
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);
L'option localePrefix: "as-needed" est importante côté SEO : elle évite d'avoir /fr/blog ET /blog qui pointent vers le même contenu (contenu dupliqué = pénalité Google).
2. Configurer le middleware
src/middleware.ts :
import createMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";
export default createMiddleware(routing);
export const config = {
// Match toutes les routes sauf les fichiers statiques et l'API
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
Ce middleware fait deux choses : il détecte la locale du visiteur (cookie → header Accept-Language → defaultLocale) et il rewrite les URLs pour toujours tomber sur la bonne page. Si vous avez d'autres règles dans votre middleware (auth, redirections), il faudra composer comme je l'explique dans mon article sur le middleware Next.js App Router.
3. Charger les messages côté serveur
src/i18n/request.ts :
import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});
Et dans next.config.ts :
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
const nextConfig: NextConfig = {
// votre config
};
export default withNextIntl(nextConfig);
Utiliser les traductions dans vos pages
Layout racine localisé
src/app/[locale]/layout.tsx :
import { NextIntlClientProvider, hasLocale } from "next-intl";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
L'attribut lang={locale} sur <html> est crucial : Google et les lecteurs d'écran s'en servent.
Traduire dans un Server Component
import { getTranslations } from "next-intl/server";
export default async function HomePage() {
const t = await getTranslations("Home");
return (
<main className="mx-auto max-w-3xl px-6 py-12">
<h1 className="text-4xl font-bold">{t("title")}</h1>
<p className="mt-4 text-lg text-gray-600">{t("description")}</p>
<a
href="#contact"
className="mt-8 inline-block rounded-md bg-blue-600 px-6 py-3 text-white"
>
{t("cta")}
</a>
</main>
);
}
Traduire dans un Client Component
"use client";
import { useTranslations } from "next-intl";
export function ContactForm() {
const t = useTranslations("Common");
return (
<form>
<button type="submit" disabled>
{t("loading")}
</button>
</form>
);
}
L'API est volontairement quasi-identique entre serveur et client. Vous changez juste l'import.
Typer les clés de traduction (le truc qui change la vie)
Sans typage, une faute de frappe dans une clé (t("titel") au lieu de t("title")) casse silencieusement votre UI — le texte affiché devient juste la clé brute. Avec next-intl, vous pouvez faire du type-safe i18n en quelques lignes.
global.d.ts à la racine :
import { routing } from "@/i18n/routing";
import messages from "@/../messages/fr.json";
declare module "next-intl" {
interface AppConfig {
Locale: (typeof routing.locales)[number];
Messages: typeof messages;
}
}
Résultat : votre IDE auto-complète les clés disponibles, et le build casse si vous référencez une clé inexistante. Plus aucune surprise en prod. Si vous découvrez TypeScript à fond, j'ai écrit un guide sur les Generics avancés qui complète très bien cette approche.
Formater dates, nombres et pluriels proprement
C'est souvent là où les sites se plantent : un prix affiché en 1234.56 € au lieu de 1 234,56 € en français, ou une date au format US sur un site français.
next-intl expose des helpers basés sur l'API Intl standard :
import { useFormatter, useTranslations } from "next-intl";
export function ProductCard({ price, releasedAt }: Props) {
const format = useFormatter();
const t = useTranslations("Product");
return (
<article>
<p>
{format.number(price, {
style: "currency",
currency: "EUR",
})}
</p>
<p>
{t("releasedOn", {
date: format.dateTime(releasedAt, {
dateStyle: "long",
}),
})}
</p>
</article>
);
}
En français : 1 234,56 € et 8 mai 2026. En anglais : €1,234.56 et May 8, 2026. Aucune logique métier à écrire, l'API navigateur s'en occupe.
Pluriels et messages riches
Le format ICU MessageFormat permet de gérer pluriels et interpolations sans gymnastique :
messages/fr.json :
{
"Cart": {
"items": "{count, plural, =0 {Aucun article} one {# article} other {# articles}}"
}
}
const t = useTranslations("Cart");
t("items", { count: 0 }); // "Aucun article"
t("items", { count: 1 }); // "1 article"
t("items", { count: 5 }); // "5 articles"
SEO multilingue : les balises qui font la différence
Avoir trois versions de votre site ne sert à rien si Google ne les indexe pas correctement. Trois choses à mettre en place :
1. Balises hreflang
Elles indiquent à Google les versions linguistiques d'une page. À ajouter dans le <head> via la metadata API :
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Home" });
return {
title: t("title"),
description: t("description"),
alternates: {
canonical: `/${locale}`,
languages: {
fr: "/fr",
en: "/en",
es: "/es",
"x-default": "/fr",
},
},
};
}
La valeur x-default est utilisée par Google quand il ne sait pas quelle langue servir.
2. Sitemap multilingue
Votre sitemap.ts doit lister chaque URL pour chaque locale :
import { MetadataRoute } from "next";
import { routing } from "@/i18n/routing";
const SITE_URL = "https://votresite.com";
export default function sitemap(): MetadataRoute.Sitemap {
const pages = ["", "/blog", "/contact"];
return pages.flatMap((page) =>
routing.locales.map((locale) => ({
url: `${SITE_URL}/${locale}${page}`,
lastModified: new Date(),
alternates: {
languages: Object.fromEntries(
routing.locales.map((l) => [l, `${SITE_URL}/${l}${page}`]),
),
},
})),
);
}
3. Slugs traduits (SEO avancé)
Pour le top-niveau du SEO, traduisez aussi vos slugs : /fr/contact → /en/contact, mais surtout /fr/blog/mon-article → /en/blog/my-article. next-intl supporte ça via pathnames dans la config de routing :
export const routing = defineRouting({
locales: ["fr", "en"] as const,
defaultLocale: "fr",
pathnames: {
"/": "/",
"/contact": {
fr: "/contact",
en: "/contact",
},
"/blog": {
fr: "/articles",
en: "/blog",
},
},
});
C'est un peu plus de boulot à maintenir, mais l'impact SEO est réel sur des marchés concurrentiels.
Erreurs classiques à éviter
Quelques pièges que je vois régulièrement chez mes clients :
- Oublier
hreflang: Google indexe alors les trois versions comme du contenu dupliqué. Trafic divisé par trois. - Charger toutes les locales côté client : si votre fichier
en.jsonfait 200 Ko, le visiteur français les télécharge pour rien. next-intl charge la locale active uniquement, mais vérifiez bien que vous n'avez pas un import statique malencontreux. - Mélanger contenu localisé et non localisé : si une partie de votre site est dans
/apiou/dashboardsans préfixe locale, exclude-la bien dans lematcherdu middleware. - Traduire à l'arrache via Google Translate sans relecture : pour le SEO et l'image de marque, c'est rédhibitoire. Faites relire par un natif, ou budgétez un traducteur pro.
Conclusion
Internationaliser un site Next.js correctement, ce n'est pas juste afficher du texte dans une autre langue : c'est repenser le routing, le SEO, le formatage, et la maintenance des traductions. next-intl rend ce chantier nettement moins douloureux grâce à son intégration profonde avec les Server Components et au typage strict des messages.
Le trio defineRouting + middleware + getTranslations/useTranslations couvre 90 % des cas d'usage. Pour les 10 % restants (pluriels complexes, slugs traduits, formatage métier), l'API ICU et les helpers useFormatter font le job sans ajouter de dépendance.
Si vous lancez un projet qui vise plusieurs marchés, mettez l'i18n en place dès le début. Rajouter une seconde langue après coup sur un site qui a 80 pages, c'est un chantier de plusieurs jours. La faire dès le début, c'est une heure de setup.
Vous avez un projet de site multilingue (vitrine, portfolio, e-commerce ou SaaS) et vous voulez le faire tourner sur une stack moderne et performante ? N'hésitez pas à me contacter pour en discuter — je vous accompagne du cadrage à la mise en production. Vous pouvez aussi m'écrire directement à contact@alexis-mouchon.fr.