Guide complet de l'internationalisation Next.js : bonnes pratiques avec next-intl

L’année dernière, j’ai repris un projet Next.js multilingue. Les réglages i18n dans la config m’ont un peu décontenancé au départ. En creusant la doc, j’ai compris qu’App Router et Pages Router n’ont pas du tout la même logique côté internationalisation. Une semaine de galère plus tard, next-intl tournait — avec pas mal de pièges au passage.
Aujourd’hui, on voit comment internationaliser Next.js, surtout avec next-intl sous App Router, proprement et sans prise de tête.
Pourquoi choisir next-intl ?
Vous vous demandez peut-être : Next.js n’a pas déjà l’i18n intégrée ? Si, à l’époque Pages Router. Avec App Router, cette fonctionnalité a été retirée.
La recommandation officielle : passer par une bibliothèque tierce. Et next-intl en est l’une des plus populaires.
Les atouts de next-intl :
- Support natif App Router — conçu pour App Router, agréable au quotidien
- Typage sûr — avec TypeScript, contrôle des clés de traduction
- Routage flexible — sous-chemins, domaines, cookies, etc.
- Fonctions avancées — pluriels, dates, nombres, texte enrichi
- Bonnes perfs — compatible Server Components, rendu statique
Comparé aux autres options, la doc next-intl est claire ; la montée en charge reste raisonnable.
Configuration de base : partir de zéro
1. Installer les dépendances
Commencez par installer next-intl :
npm install next-intl
# ou
pnpm add next-intl
# ou
yarn add next-intl
2. Créer les fichiers de traduction
À la racine du projet, créez un dossier messages (ou locales), puis un JSON par langue :
messages/
├── en.json
├── zh.json
└── ja.json
messages/zh.json :
{
"HomePage": {
"title": "欢迎来到我的网站",
"description": "这是一个支持多语言的 Next.js 应用"
},
"Navigation": {
"home": "首页",
"about": "关于",
"contact": "联系我们"
}
}
messages/en.json :
{
"HomePage": {
"title": "Welcome to My Website",
"description": "This is a multilingual Next.js application"
},
"Navigation": {
"home": "Home",
"about": "About",
"contact": "Contact Us"
}
}
La structure imbriquée n’est pas obligatoire, mais regrouper par page ou composant simplifie la maintenance.
3. Configurer i18n.ts
Créez i18n.ts (ou i18n/config.ts) pour déclarer les langues supportées :
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default
}));
Cette config indique à next-intl où charger les traductions. Le paramètre locale est extrait automatiquement de l’URL.
4. Créer le middleware
À la racine, middleware.ts gère le routage multilingue :
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// Langues supportées
locales: ['en', 'zh', 'ja'],
// Langue par défaut
defaultLocale: 'zh',
// Afficher la langue par défaut dans l'URL ?
localePrefix: 'as-needed'
});
export const config = {
// Toutes les routes sauf api, _next/static, _next/image, favicon.ico
matcher: ['/', '/(zh|en|ja)/:path*', '/((?!api|_next|_next/static|_next/image|favicon.ico).*)']
};
Options de localePrefix :
'always'— préfixe pour toutes les langues, y compris la défaut (/zh/about,/en/about)'as-needed'— pas de préfixe pour la langue par défaut (/about,/en/about)'never'— aucun préfixe (langue identifiée autrement, ex. domaine)
J’utilise souvent 'as-needed' : URLs plus propres pour les utilisateurs de la langue par défaut.
5. Réorganiser le dossier app
Étape cruciale : placer toutes les routes sous [locale] :
Avant :
app/
├── page.tsx
├── about/
│ └── page.tsx
└── layout.tsx
Après :
app/
├── [locale]/
│ ├── page.tsx
│ ├── about/
│ │ └── page.tsx
│ └── layout.tsx
└── layout.tsx (optionnel, config globale)
6. Configurer le layout racine
Dans app/[locale]/layout.tsx :
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
const locales = ['en', 'zh', 'ja'];
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
// Valider la langue
if (!locales.includes(locale)) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Avec le rendu statique, generateStaticParams indique à Next.js quelles locales pré-générer.
Utiliser les traductions dans les composants
Une fois la config en place, les traductions sont disponibles partout.
Dans un Server Component
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
Le paramètre de useTranslations est l’espace de noms (clé racine du JSON). Sans paramètre, utilisez le chemin complet : t('HomePage.title').
Dans un Client Component
Même API côté client :
'use client';
import { useTranslations } from 'next-intl';
export default function Navigation() {
const t = useTranslations('Navigation');
return (
<nav>
<a href="/">{t('home')}</a>
<a href="/about">{t('about')}</a>
<a href="/contact">{t('contact')}</a>
</nav>
);
}
Server et Client partagent la même ergonomie — un vrai plus de next-intl.
Routage multilingue
Obtenir la langue courante
import { useLocale } from 'next-intl';
export default function LanguageSwitcher() {
const locale = useLocale();
return <div>Langue actuelle : {locale}</div>;
}
Créer un sélecteur de langue
Indispensable sur tout site i18n :
'use client';
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next/navigation';
export default function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const switchLanguage = (newLocale: string) => {
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
router.push(newPath);
};
return (
<select value={locale} onChange={(e) => switchLanguage(e.target.value)}>
<option value="zh">中文</option>
<option value="en">English</option>
<option value="ja">日本語</option>
</select>
);
}
Petit piège : sur la langue par défaut (ex. zh), l’URL peut être /about sans préfixe ; en passant à l’anglais, il faut /en/about. Version corrigée :
const switchLanguage = (newLocale: string) => {
let path = pathname;
if (pathname.startsWith(`/${locale}`)) {
path = pathname.substring(locale.length + 1);
}
const newPath = newLocale === 'zh' ? path : `/${newLocale}${path}`;
router.push(newPath);
};
Utiliser le composant Link
next-intl fournit un Link qui gère les préfixes de locale :
import { Link } from '@/navigation'; // à configurer d'abord
<Link href="/about">
{t('about')}
</Link>
Configurez navigation.ts :
import { createSharedPathnamesNavigation } from 'next-intl/navigation';
export const locales = ['en', 'zh', 'ja'] as const;
export const localePrefix = 'as-needed';
export const { Link, redirect, usePathname, useRouter } =
createSharedPathnamesNavigation({ locales, localePrefix });
Link, useRouter, etc. gèrent alors automatiquement les chemins localisés.
Fonctionnalités avancées
1. Traductions paramétrées
{
"welcome": "欢迎回来,{username}!",
"items": "你有 {count} 个新消息"
}
Usage :
const t = useTranslations();
<p>{t('welcome', { username: 'John' })}</p>
<p>{t('items', { count: 5 })}</p>
2. Gestion des pluriels
messages/en.json :
{
"messages": {
"one": "You have {count} message",
"other": "You have {count} messages"
}
}
Usage :
t('messages', { count: 1 }) // "You have 1 message"
t('messages', { count: 5 }) // "You have 5 messages"
Le chinois n’a pas de pluriel grammatical :
messages/zh.json :
{
"messages": "你有 {count} 条消息"
}
3. Formatage dates et nombres
import { useFormatter } from 'next-intl';
export default function DateExample() {
const format = useFormatter();
const now = new Date();
return (
<div>
<p>{format.dateTime(now, { dateStyle: 'full' })}</p>
{/* zh : 2025年12月25日星期三 */}
{/* en : Wednesday, December 25, 2025 */}
<p>{format.number(1234567.89, { style: 'currency', currency: 'CNY' })}</p>
{/* zh : ¥1,234,567.89 */}
{/* en : CN¥1,234,567.89 */}
</div>
);
}
4. Texte enrichi
messages/zh.json :
{
"richText": "我同意<terms>服务条款</terms>和<privacy>隐私政策</privacy>"
}
Usage :
import { useTranslations } from 'next-intl';
export default function Agreement() {
const t = useTranslations();
return (
<p>
{t.rich('richText', {
terms: (chunks) => <a href="/terms">{chunks}</a>,
privacy: (chunks) => <a href="/privacy">{chunks}</a>
})}
</p>
);
}
Bonnes pratiques de gestion des traductions
Quand le projet grossit, les JSON deviennent difficiles à maintenir. Quelques astuces :
1. Découper par module
messages/
├── zh/
│ ├── common.json # Boutons, erreurs, etc.
│ ├── home.json
│ ├── about.json
│ └── auth.json
├── en/
│ ├── common.json
│ ├── home.json
│ ├── about.json
│ └── auth.json
Fusion dans i18n.ts :
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => {
const common = (await import(`./messages/${locale}/common.json`)).default;
const home = (await import(`./messages/${locale}/home.json`)).default;
const about = (await import(`./messages/${locale}/about.json`)).default;
const auth = (await import(`./messages/${locale}/auth.json`)).default;
return {
messages: {
common,
home,
about,
auth
}
};
});
2. Typage TypeScript
types/i18n.ts :
import zh from '@/messages/zh.json';
type Messages = typeof zh;
declare global {
interface IntlMessages extends Messages {}
}
tsconfig.json :
{
"compilerOptions": {
"types": ["./types/i18n"]
}
}
Clé inexistante → erreur TypeScript. Très utile au quotidien.
3. Traductions communes
messages/zh/common.json :
{
"actions": {
"save": "保存",
"cancel": "取消",
"delete": "删除",
"confirm": "确认",
"edit": "编辑"
},
"status": {
"success": "操作成功",
"error": "操作失败",
"loading": "加载中..."
}
}
const t = useTranslations('common.actions');
<button>{t('save')}</button>
4. Outils de gestion
- Tolgee — plateforme open source, édition en temps réel
- Localazy — workflows automatisés
- i18n Ally (extension VSCode) — édition inline dans l’IDE
Je m’appuie surtout sur i18n Ally : les traductions visibles pendant le code.
5. Traductions manquantes
// i18n.ts
export default getRequestConfig(async ({ locale }) => {
const messages = (await import(`./messages/${locale}.json`)).default;
const fallback = locale !== 'zh'
? (await import(`./messages/zh.json`)).default
: {};
return {
messages: {
...fallback,
...messages
}
};
});
Traduction absente → repli automatique sur le chinois.
Optimisation des performances
1. Découpage du code
export default function AdminPage() {
const t = useTranslations('admin'); // charge uniquement le namespace admin
// ...
}
2. Génération statique
// app/[locale]/about/page.tsx
export const dynamic = 'force-static';
export function generateStaticParams() {
return [
{ locale: 'zh' },
{ locale: 'en' },
{ locale: 'ja' }
];
}
3. Préchargement des traductions
import { getTranslations } from 'next-intl/server';
export default async function Home() {
const t = await getTranslations('HomePage');
return <h1>{t('title')}</h1>;
}
Problèmes courants et solutions
1. Changement de langue sur routes dynamiques
Pour /blog/[slug], conserver le slug :
const switchLanguage = (newLocale: string) => {
const segments = pathname.split('/').filter(Boolean);
if (['zh', 'en', 'ja'].includes(segments[0])) {
segments.shift();
}
if (newLocale !== 'zh' || localePrefix === 'always') {
segments.unshift(newLocale);
}
router.push('/' + segments.join('/'));
};
2. SEO
// app/[locale]/layout.tsx
export async function generateMetadata({ params: { locale } }) {
const t = await getTranslations({ locale, namespace: 'metadata' });
return {
title: t('title'),
description: t('description'),
alternates: {
canonical: `https://example.com/${locale}`,
languages: {
'zh-CN': 'https://example.com/zh',
'en-US': 'https://example.com/en',
'ja-JP': 'https://example.com/ja'
}
}
};
}
3. Détection de langue
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { NextRequest } from 'next/server';
const intlMiddleware = createMiddleware({
locales: ['en', 'zh', 'ja'],
defaultLocale: 'zh',
localeDetection: true
});
export default function middleware(request: NextRequest) {
return intlMiddleware(request);
}
next-intl lit l’en-tête Accept-Language pour la première visite.
4. Mémoriser la préférence
const switchLanguage = (newLocale: string) => {
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`;
router.push(newPath);
};
Le middleware next-intl relit ce cookie automatiquement.
Cas pratique : projet i18n complet
Structure d’un petit projet que j’ai livré :
├── app/
│ ├── [locale]/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── blog/
│ │ └── [slug]/
│ │ └── page.tsx
├── components/
│ ├── LanguageSwitcher.tsx
│ └── Navigation.tsx
├── messages/
│ ├── zh/
│ │ ├── common.json
│ │ └── blog.json
│ ├── en/
│ │ ├── common.json
│ │ └── blog.json
├── i18n.ts
├── middleware.ts
└── navigation.ts
navigation.ts :
import { createSharedPathnamesNavigation } from 'next-intl/navigation';
export const locales = ['zh', 'en'] as const;
export const localePrefix = 'as-needed';
export const { Link, redirect, usePathname, useRouter } =
createSharedPathnamesNavigation({ locales, localePrefix });
components/Navigation.tsx :
'use client';
import { Link } from '@/navigation';
import { useTranslations } from 'next-intl';
import LanguageSwitcher from './LanguageSwitcher';
export default function Navigation() {
const t = useTranslations('common.navigation');
return (
<nav className="flex items-center justify-between p-4">
<div className="flex gap-4">
<Link href="/">{t('home')}</Link>
<Link href="/blog">{t('blog')}</Link>
<Link href="/about">{t('about')}</Link>
</div>
<LanguageSwitcher />
</nav>
);
}
En production, le changement de langue était fluide, sans accroc.
Résumé
L’i18n Next.js sous App Router paraît complexe, mais avec next-intl, ça se tient :
- Config cœur : middleware + i18n.ts + dossier
[locale] - Traductions : hook
useTranslationscôté Server et Client - Routage :
LinketuseRouterfournis par next-intl - Fichiers : découpage par module + typage TypeScript
- Perfs : génération statique + chargement à la demande
Au début, middleware et routes dynamiques m’ont semblé tortueux. Après quelques projets, c’est devenu un réflexe.
Si vous préparez un site multilingue, testez next-intl : courbe d’apprentissage réelle, mais rentable sur la durée.
Ressources
J’espère que cet article vous évitera une partie de la galère sur l’i18n Next.js !
Configuration complète de l'internationalisation Next.js
De l'installation de next-intl au routage multilingue et à la gestion des fichiers de traduction
⏱️ Estimated time: 2 hr
- 1
Step 1: Installer next-intl et configurer la base
Installation :
```bash
npm install next-intl
```
Créer les fichiers de traduction :
```
messages/
zh.json
en.json
```
Configurer middleware.ts :
```ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'
export default createMiddleware(routing)
export const config = {
matcher: ['/', '/(zh|en)/:path*']
}
```
Configurer app/[locale]/layout.tsx :
```tsx
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
export default async function LocaleLayout({
children,
params: { locale }
}) {
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
```
Points clés :
• Route dynamique [locale]
• Fournir les traductions dans le layout
• Middleware pour le changement de langue - 2
Step 2: Configurer le schéma de routage multilingue
Option 1 : sous-chemins (recommandé)
• URL : /zh/about, /en/about
• Configuration simple
• SEO friendly
Option 2 : domaines
• URL : zh.example.com, en.example.com
• Plusieurs domaines à configurer
• Aspect plus professionnel
Option 3 : cookie
• Langue via cookie
• Pas de préfixe dans l'URL
• Adapté aux audiences monolingues
Config sous-chemins :
```ts
// i18n/routing.ts
export const routing = {
locales: ['zh', 'en'],
defaultLocale: 'zh'
}
```
Usage :
```tsx
import { useTranslations } from 'next-intl'
export function Page() {
const t = useTranslations('common')
return <h1>{t('title')}</h1>
}
```
Point clé : choisir le schéma adapté ; les sous-chemins suffisent pour la plupart des projets - 3
Step 3: Gérer les fichiers de traduction
Créer les fichiers :
```json
// messages/zh.json
{
"common": {
"title": "欢迎",
"description": "这是一个多语言网站"
},
"nav": {
"home": "首页",
"about": "关于"
}
}
```
Utiliser les traductions :
```tsx
import { useTranslations } from 'next-intl'
export function Page() {
const t = useTranslations('common')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
)
}
```
Typage sûr :
```ts
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`../messages/${locale}.json`)).default
}))
```
Points clés :
• Structure JSON imbriquée
• Typage TypeScript
• Extension VSCode i18n Ally
FAQ
Pourquoi App Router nécessite-t-il next-intl ?
Pages Router :
• i18n de routage intégrée
• Configuration i18n dans next.config.js
• Changement de langue automatique
App Router :
• Plus d'i18n intégrée
• Bibliothèque tierce requise
• next-intl est le choix le plus populaire
Atouts next-intl :
• Support natif App Router
• Typage sûr
• Routage flexible
• Pluriels, formatage de dates, etc.
• Excellentes performances
Conseil : sous App Router, privilégiez next-intl.
Quels schémas de routage next-intl propose-t-il ?
Option 1 : sous-chemins (recommandé)
• URL : /zh/about, /en/about
• Configuration simple
• SEO friendly
• Convient à la majorité des projets
Option 2 : domaines
• URL : zh.example.com, en.example.com
• Plusieurs domaines
• Plus professionnel
• Grands projets
Option 3 : cookie
• Langue via cookie
• Pas de préfixe URL
• Audiences monolingues
• Configuration plus complexe
Recommandation :
• Plupart des projets → sous-chemins
• Grands projets → domaines
• Besoins spécifiques → cookie
Comment configurer next-intl ?
```bash
npm install next-intl
```
Fichiers de traduction :
```
messages/
zh.json
en.json
```
middleware.ts :
```ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'
export default createMiddleware(routing)
export const config = {
matcher: ['/', '/(zh|en)/:path*']
}
```
app/[locale]/layout.tsx :
```tsx
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
export default async function LocaleLayout({
children,
params: { locale }
}) {
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
```
Points clés :
• Route [locale]
• Traductions dans le layout
• Middleware pour le changement de langue
Comment gérer les fichiers de traduction ?
```json
{
"common": {
"title": "欢迎",
"description": "这是一个多语言网站"
},
"nav": {
"home": "首页",
"about": "关于"
}
}
```
Usage :
```tsx
import { useTranslations } from 'next-intl'
export function Page() {
const t = useTranslations('common')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
)
}
```
Typage :
```ts
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`../messages/${locale}.json`)).default
}))
```
Points clés :
• JSON imbriqué
• TypeScript pour la sécurité
• Extension i18n Ally
Conseil : organiser par module, éviter un fichier unique trop gros.
Quelles fonctionnalités next-intl prend-il en charge ?
• Texte traduit (fonction t)
• Pluriels
• Formatage de dates
• Formatage de nombres
• Texte enrichi
Exemple :
```tsx
import { useTranslations, useFormatter } from 'next-intl'
export function Page() {
const t = useTranslations('common')
const format = useFormatter()
return (
<div>
<h1>{t('title')}</h1>
<p>{format.dateTime(new Date(), { dateStyle: 'long' })}</p>
<p>{format.number(1234.56, { style: 'currency', currency: 'USD' })}</p>
</div>
)
}
```
Atouts :
• Fonctions complètes
• Typage sûr
• Bonnes perfs
• Compatible Server Components
Conseil : exploitez pluriels et formatage pour une meilleure UX.
Comment implémenter le changement de langue ?
```tsx
import { Link } from '@/i18n/navigation'
<Link href="/about" locale="en">
English
</Link>
<Link href="/about" locale="zh">
中文
</Link>
```
Avec useRouter :
```tsx
'use client'
import { useRouter, usePathname } from '@/i18n/navigation'
export function LanguageSwitcher() {
const router = useRouter()
const pathname = usePathname()
const switchLanguage = (locale: string) => {
router.replace(pathname, { locale })
}
return (
<button onClick={() => switchLanguage('en')}>
English
</button>
)
}
```
Points clés :
• Link et useRouter de next-intl
• Conserver le chemin, changer la locale
• Bonne expérience utilisateur
Conseil : placez le sélecteur dans la barre de navigation ou le pied de page.
10 min de lecture · Publié le: 25 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Next.js OAuth en pratique : intégrer Google, GitHub et WeChat pas à pas
Du principe OAuth à la configuration concrète : comprendre le flux d'autorisation avec l'analogie du retrait de colis, puis implémenter Google, GitHub et WeChat avec NextAuth.js, avec un guide complet de dépannage.
Partie 14 sur 51
Suivant
Internationalisation Next.js et génération statique : pratique des sites Web multilingues SSG
Du rapport d'erreurs de construction à l'optimisation des performances, nous vous apprendrons étape par étape comment utiliser App Router pour réaliser une génération statique multilingue sans trou. Contient des exemples de code complets, une explication détaillée de la configuration de generateStaticParams et des conseils d'optimisation du temps de construction.
Partie 16 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire