Internationalisation Next.js et génération statique : pratique des sites Web multilingues SSG

La première fois que j’ai tenté la génération statique multilingue dans un projet Next.js App Router, j’ai vraiment marché sur tous les pièges. Vous avez peut-être vécu la même chose : la config semble correcte dans la doc, puis le build plante ; ou le build passe enfin, mais il faut quinze minutes pour générer toutes les pages…
Voici quelques scènes de cette semaine — est-ce que ça vous rappelle quelque chose ?
Avez-vous rencontré ces problèmes ?
Scénario 1 : erreurs au build
Je me souviens avoir lancé npm run build avec enthousiasme, et le terminal m’a renvoyé une erreur :
Error: Page "/en/about" is missing `generateStaticParams()`
so it cannot be used with `output: "export"`.
À ce moment-là, j’étais complètement confus et j’ai pensé : « Qu’est-ce qui se passe ? J’ai clairement configuré « i18n » dans « next.config.js » ! Plus tard, j’ai découvert que les méthodes d’internationalisation d’App Router et de Pages Router sont complètement différentes et que la configuration précédente ne fonctionnait pas du tout.
Scénario 2 : Le temps de construction est trop long
Une autre fois, mon projet prenait en charge 6 langues, avec environ 50 pages par langue. Le résultat se construit en une seule fois :
✓ Generating static pages (152/152) - 15m 32s
15 minutes ! Vous avez bien lu. Chaque fois que je change quelque chose de petit, je dois attendre très longtemps et l’expérience de développement est tout simplement gâchée. Je pensais : « Si cela est mis dans un environnement de production, le CI/CD n’aura pas à attendre une éternité ?
Scénario 3 : La mise à jour de la traduction ne prend pas effet
La chose la plus exaspérante est la suivante : j’ai mis à jour le fichier de traduction « zh-CN.json », reconstruit et redéployé, et l’ancienne traduction était toujours affichée sur le site Web ! Le cache du navigateur doit être vidé pour voir le nouveau contenu. C’est un désastre dans un environnement de production. Tout ce que les utilisateurs voient est du contenu expiré.
Quelle est la racine du problème ?
Plus tard, j’ai passé beaucoup de temps à faire des recherches pour comprendre l’essence de ces problèmes :
-
App Router ne prend plus en charge la configuration i18n de Pages Router - C’est le plus gros piège. Si vous configurez le champ « i18n » dans « next.config.js », App Router ne le reconnaîtra pas du tout.
-
Conflit entre l’exportation statique et le rendu dynamique - Lorsque vous définissez
output: 'export', Next.js exige que toutes les pages soient déterminées au moment de la construction. Si vous utilisez des API dynamiques telles quecookies()etheaders(), une erreur sera signalée. -
Mécanisme de mise en cache des fichiers de traduction - Next.js mettra en cache les fichiers JSON importés. La traduction a été mise à jour pendant le développement, mais le cache n’a pas expiré, le contenu le plus récent ne peut donc pas être vu.
Si vous avez également rencontré ces problèmes, alors cet article est fait pour vous. Ensuite, je vais vous apprendre étape par étape comment implémenter correctement la génération statique multilingue de Next.js App Router et éviter ces pièges.
Comprendre le nouveau paradigme i18n d’App Router
Avant de commencer à écrire du code, je pense qu’il est nécessaire de comprendre les idées d’internationalisation d’App Router. C’est vraiment différent de Pages Router.
Pages Router vs App Router : Deux solutions très différentes
J’ai fait un tableau comparatif pour que vous puissiez sentir intuitivement la différence :
| Caractéristiques | Routeur de pages | Routeur d’applications |
|---|---|---|
| Méthode de configuration | Champ i18n de next.config.js | middleware + routage dynamique [lang] |
| Structure de routage | Générer automatiquement les préfixes /en/, /zh/ | Créez manuellement app/[lang]/page.tsx |
| Génération statique | Utilisez getStaticPaths | Utilisez generateStaticParams |
| Chargement de la traduction | Fonction serverSideTranslations | Les composants côté serveur « importent » directement JSON |
Avez-vous vu ça ? Presque tout a changé. Quand j’ai rencontré cela pour la première fois, j’avais vraiment l’impression “J’ai appris le faux Next.js”.
Qu’est-ce que generateStaticParams exactement ?
C’est l’un des concepts fondamentaux d’App Router. En termes simples, son rôle est d’indiquer à Next.js : “Pour quels paramètres dois-je générer une page statique”.
Par exemple:
// app/[lang]/layout.tsx
export async function generateStaticParams() {
// Renvoie tous les paramètres de langue qui doivent être pré-rendus
return [
{ lang: 'en' },
{ lang: 'zh' },
{ lang: 'ja' }
]
}
Next.js exécutera cette fonction lors de la construction, obtiendra la liste de paramètres renvoyée, puis générera un fichier HTML statique pour chaque combinaison de paramètres. Le résultat final est :
out/
├── en/
│ └── index.html
├── zh/
│ └── index.html
└── ja/
└── index.html
Point clé : Cette fonction doit être définie dans layout.tsx ou page.tsx, et le nom de la fonction doit correspondre exactement (pas getStaticParams, ni generateParams, doit être generateStaticParams).
Comment charger les fichiers de traduction ?
À l’ère de Pages Router, nous utilisions la fonction serverSideTranslations de la bibliothèque next-i18next. Mais dans App Router, vous pouvez importer des fichiers de traduction directement dans le composant serveur :
// Les composants côté serveur peuvent le faire directement
import enTranslations from '@/i18n/locales/en/common.json'
import zhTranslations from '@/i18n/locales/zh-CN/common.json'
const translations = {
'en': enTranslations,
'zh-CN': zhTranslations,
}
export default function Page({ params }: { params: { lang: string } }) {
const t = translations[params.lang]
return <h1>{t.title}</h1>
}
Mais il y a un problème avec cela : toutes les traductions de langue seront regroupées dans des lots, ce qui entraînera une augmentation de la taille du fichier. Ainsi, dans les projets réels, nous écrivons généralement une fonction de chargement :
// i18n/utils.ts
export async function loadTranslations(locale: string, namespaces: string[]) {
const translations: Record<string, any> = {}
for (const ns of namespaces) {
try {
const translation = await import(`@/i18n/locales/${locale}/${ns}.json`)
translations[ns] = translation.default
} catch (error) {
console.warn(`Translation file not found: ${locale}/${ns}`)
translations[ns] = {}
}
}
return translations
}
Cela permet un chargement à la demande, en chargeant uniquement les espaces de noms de traduction requis pour la page actuelle.
Combat pratique : créez un projet SSG multilingue à partir de zéro
Bon, j’en ai fini avec la théorie, commençons maintenant à coder. Je vous accompagnerai du début à la fin pour créer un site statique multilingue complet.
Étape 1 : Concevoir la structure du projet
Tout d’abord, nous devons établir une structure de répertoires claire. C’est la structure que j’utilise dans des projets réels, et elle fonctionne bien dans mon propre test :
app/
├── [lang]/ # Routage dynamique du langage (noyau)
│ ├── layout.tsx # Disposition racine, y compris generateStaticParams
│ ├── page.tsx # Page d'accueil
│ ├── about/
│ │ └── page.tsx # À propos de la page
│ └── blog/
│ ├── page.tsx # Liste des blogs
│ └── [slug]/
│ └── page.tsx # Détails du blog (routage dynamique imbriqué)
├── i18n/
│ ├── locales/ # Répertoire des fichiers de traduction
│ │ ├── en/
│ │ │ ├── common.json # Traduction publique
│ │ │ ├── home.json # Traduction de la page d'accueil
│ │ │ └── blog.json # Traduction du blog
│ │ ├── zh-CN/
│ │ │ ├── common.json
│ │ │ ├── home.json
│ │ │ └── blog.json
│ │ └── ja/
│ │ ├── common.json
│ │ ├── home.json
│ │ └── blog.json
│ ├── fichier de configuration config.ts # i18n
│ └── utils.ts # Fonction de l'outil de traduction
└── middleware.ts # Détection et redirection de langue
**Pourquoi est-il conçu comme ça ? **
- Dossier
[lang]: c’est le cœur du routage dynamique. Next.js transmettra les paramètres de langue dans l’URL aux composants de la page. - Divisez les traductions par espace de noms : Pour éviter un fichier de traduction trop volumineux, séparez-le par fonction de page et chargez-le à la demande.
config.tsConfiguration centralisée : toutes les configurations liées à la langue sont placées ici pour une maintenance facile.
Étape 2 : Configurer les fichiers principaux du i18n
Tout d’abord, écrivons le fichier de configuration, qui constitue la base de l’ensemble du système :
// i18n/config.ts
export const i18nConfig = {
// Liste des langues prises en charge
locales: ['en', 'zh-CN', 'ja'],
// Langue par défaut
defaultLocale: 'en',
// Politique de préfixe de chemin
// 'toujours' : toutes les langues sont préfixées par /en/, /zh-CN/
// 'au besoin' : langue par défaut sans préfixe, autres langues avec préfixe
localePrefix: 'always',
// [Important] Pré-rendez uniquement la langue principale (optimisez le temps de construction)
localesToPrerender: process.env.NODE_ENV === 'production'
? ['en', 'zh-CN'] // L'environnement de production pré-rend uniquement l'anglais et le chinois
: ['en'], // L'environnement de développement restitue uniquement la langue par défaut
} as const
// Exporter des types à utiliser par la vérification de type TypeScript
export type Locale = (typeof i18nConfig)['locales'][number]
// Espace de noms de traduction (pour le fractionnement du code)
export const namespaces = ['common', 'home', 'about', 'blog'] as const
export type Namespace = (typeof namespaces)[number]
Explication des points clés :
as const: il s’agit de la manière d’écrire TypeScript, garantissant que le type est un type littéral précis, et non une largestring[].localesToPrerender: C’est très important ! Si vous prenez en charge 10 langues mais ne pré-rendez que les 2 langues principales, les temps de construction peuvent être réduits de 80 %. D’autres langages peuvent être générés via ISR (Incremental Static Regeneration) ou à la demande.- Espace de noms : divisez le fichier de traduction en plusieurs JSON pour éviter de télécharger un énorme fichier de traduction pour la première fois.
Étape 3 : Implémenter l’outil de chargement de traduction
Il s’agit d’un chargeur de traduction simple mais fonctionnel :
// i18n/utils.ts
import type { Locale, Namespace } from './config'
// Mise en cache des fichiers de traduction (pour éviter les lectures répétées)
const translationsCache = new Map<string, any>()
/**
* Chargez le fichier de traduction dans la langue spécifiée
*
* Code de langue locale @param, tel que « en », « zh-CN »
* @param tableau d'espaces de noms de traduction, tel que ['common', 'home']
* @returns objet de traduction { commun : {...}, home : {...} }
*/
export async function loadTranslations(
locale: Locale,
namespaces: Namespace[]
) {
const translations: Record<string, any> = {}
for (const namespace of namespaces) {
const cacheKey = `${locale}-${namespace}`
// Vérifiez le cache pour éviter des chargements répétés
if (!translationsCache.has(cacheKey)) {
try {
// Importer dynamiquement des fichiers de traduction
const translation = await import(
`@/i18n/locales/${locale}/${namespace}.json`
)
translationsCache.set(cacheKey, translation.default)
} catch (error) {
console.warn(`⚠️ Translation file not found: ${locale}/${namespace}.json`)
translationsCache.set(cacheKey, {})
}
}
translations[namespace] = translationsCache.get(cacheKey)
}
return translations
}
/**
* Créer des fonctions de traduction de type sécurisé
*
* Utilisation :
* const t = createTranslator(translations)
* t('common.nav.home')
* t('home.welcome', { name: 'John' }) // Prise en charge de la substitution de variable
*/
export function createTranslator(translations: any) {
return (key: string, params?: Record<string, string>) => {
const keys = key.split('.')
let value = translations
// Accéder aux propriétés imbriquées couche par couche
for (const k of keys) {
value = value?.[k]
}
// Renvoie la clé elle-même lorsqu'aucune traduction n'est trouvée (facilite le débogage)
if (!value) {
console.warn(`⚠️ Translation missing: ${key}`)
return key
}
// Prise en charge de la substitution de variable : remplacez {{name}} par la valeur réelle
if (params) {
return Object.entries(params).reduce(
(str, [key, val]) => str.replace(`{{${key}}}`, val),
value
)
}
return value
}
}
Points forts de cet outil :
- Mécanisme de mise en cache : mise en cache après le premier chargement pour éviter la lecture répétée des fichiers.
- Gestion des erreurs : il ne plantera pas lorsque le fichier de traduction est introuvable, il avertira simplement et renverra un objet vide.
- Substitution de variable : prend en charge l’utilisation de l’espace réservé
{{nom de la variable}}dans la traduction. - Type-friendly : une vérification des clés de traduction sécurisées peut être réalisée avec TypeScript.
Étape 4 : Créer la disposition racine (la plus critique)
Il s’agit du fichier central de l’ensemble du système multilingue :
// app/[lang]/layout.tsx
import { i18nConfig } from '@/i18n/config'
import { loadTranslations } from '@/i18n/utils'
import type { Locale } from '@/i18n/config'
/**
* [Core] Générer des paramètres statiques pour toutes les langues
*
* Cette fonction est exécutée pendant la construction et Next.js générera la page statique correspondante en fonction de la valeur de retour.
*
*REMARQUE IMPORTANTE :
* 1. Le nom de la fonction doit être generateStaticParams (ne peut pas être mal orthographié)
* 2. Doit être défini dans layout.tsx ou page.tsx
* 3. Le nom du paramètre renvoyé doit correspondre au nom du dossier de routage ([lang] → lang)
*/
export async function generateStaticParams() {
console.log(`🌍 Generating static params for ${i18nConfig.localesToPrerender.length} locales...`)
return i18nConfig.localesToPrerender.map((locale) => ({
lang : locale, // ⚠️ Remarque : doit être "lang" et non "locale"
}))
}
/**
*Composant de disposition racine
*
* Ce composant enveloppera toutes les pages et sera utilisé pour définir les configurations globales
*/
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode
params: { lang: string }
}) {
// Charger les traductions publiques (navigation, pied de page, etc.)
const translations = await loadTranslations(params.lang as Locale, ['common'])
return (
<html
lang={params.lang}
// Si vous êtes en arabe, définissez la disposition de droite à gauche
dir={params.lang === 'ar' ? 'rtl' : 'ltr'}
>
<head>
{/* Les balises méta globales peuvent être ajoutées ici */}
</head>
<body>
{/* Les composants globaux tels que la barre de navigation et le pied de page peuvent être placés ici */}
{children}
</body>
</html>
)
}
/**
* Générer des métadonnées (SEO)
*
* Cette fonction permet de générer les <title>, <meta> et autres balises de la page
*/
export async function generateMetadata({ params }: { params: { lang: string } }) {
return {
// Définir des balises méta liées à la langue
alternates: {
canonical: `https://example.com/${params.lang}`,
languages: {
'en': 'https://example.com/en',
'zh-CN': 'https://example.com/zh-CN',
'ja': 'https://example.com/ja',
},
},
// Balise Open Graph (pour le partage sur les réseaux sociaux)
openGraph: {
locale: params.lang,
alternateLocale: i18nConfig.locales.filter(l => l !== params.lang),
},
}
}
Voici quelques pièges faciles :
⚠️ Pit 1 : les noms des paramètres doivent correspondre
// ❌ Erreur : le nom du paramètre est locale, mais le dossier de route est [lang]
export async function generateStaticParams() {
return [{ locale: 'en' }] // Cela signalera une erreur
}
// ✅ Correct : le nom du paramètre est cohérent avec le nom du dossier
export async function generateStaticParams() {
return [{ lang: 'en' }] // doit être lang
}
⚠️ Pit 2 : Impossible d’utiliser l’API dynamique
// ❌ Erreur : Utilisation de cookies dans des pages générées statiquement
export default async function Layout({ children }) {
const locale = cookies().get('NEXT_LOCALE') // Cela entraînera l'échec de la construction
return <html lang={locale}>{children}</html>
}
// ✅ Correct : utiliser les paramètres d'itinéraire
export default async function Layout({ children, params }) {
return <html lang={params.lang}>{children}</html>
}
Étape 5 : Créer un fichier de traduction
La structure du fichier de traduction est également importante. Voici le format que je recommande :
// i18n/locales/zh-CN/common.json
{
"nav": {
"home": "首页",
"about": "关于",
"blog": "博客",
"contact": "联系"
},
"footer": {
"copyright": "© {{year}} 版权所有",
"privacy": "隐私政策",
"terms": "服务条款"
},
"actions": {
"readMore": "阅读更多",
"backToTop": "返回顶部",
"share": "分享",
"edit": "编辑"
},
"messages": {
"loading": "加载中...",
"error": "出错了",
"success": "操作成功",
"noResults": "没有找到结果"
}
}
// i18n/locales/zh-CN/blog.json
{
"title": "博客文章",
"publishedAt": "发布于",
"author": "作者",
"tags": "标签",
"relatedPosts": "相关文章",
"readingTime": "阅读时间:{{minutes}} 分钟",
"shareOn": "分享到 {{platform}}"
}
Bonnes pratiques pour la traduction de documents :
- Structure hiérarchique : utilisez des objets imbriqués pour organiser les traductions, ne placez pas toutes les clés au niveau supérieur.
- Espace réservé pour la variable : utilisez le format
{{nom de la variable}}pour faciliter le traitement unifié. - Gardez les noms de clés cohérents : les fichiers de traduction pour toutes les langues doivent avoir la même structure de clés.
- Ajouter des commentaires : ajoutez des commentaires à côté des traductions complexes pour expliquer les scénarios d’utilisation.
Étape 6 : Traiter le routage dynamique imbriqué
Si votre projet comporte un blog ou une page de détails sur le produit, vous devez gérer le routage dynamique imbriqué. C’est l’un des plus gros pièges que j’ai jamais rencontré.
// app/[lang]/blog/[slug]/page.tsx
import { i18nConfig } from '@/i18n/config'
import { loadTranslations, createTranslator } from '@/i18n/utils'
import type { Locale } from '@/i18n/config'
// Supposons que vous disposez de ces fonctions auxiliaires (vous devez les implémenter vous-même dans des projets réels)
async function getBlogSlugs(): Promise<string[]> {
// Obtenez le slug pour tous les articles de blog à partir du système de fichiers ou du CMS
return ['getting-started', 'advanced-tips', 'performance-guide']
}
async function getBlogPost(slug: string, locale: Locale) {
// Obtenez le contenu des articles de blog dans une langue spécifique
// ...
}
/**
* [Clé] generateStaticParams du routage imbriqué
*
* Nécessité de générer toutes les combinaisons de langue × article
* Par exemple : fr/getting-started, zh-CN/getting-started, fr/advanced-tips...
*/
export async function generateStaticParams() {
const startTime = Date.now()
console.log('📝 Generating blog post params...')
// Obtenez des slugs pour tous les articles (une seule demande requise)
const slugs = await getBlogSlugs()
// Utilisez flatMap pour générer des combinaisons de toutes les langues et articles
const params = i18nConfig.localesToPrerender.flatMap((locale) =>
slugs.map((slug) => ({
lang: locale,
slug: slug,
}))
)
const duration = Date.now() - startTime
console.log(`✅ Generated ${params.length} blog post params in ${duration}ms`)
return params
}
/**
* Composant de page d'article de blog
*/
export default async function BlogPost({
params,
}: {
params: { lang: string; slug: string }
}) {
// Charger la traduction et le contenu de l'article
const [translations, post] = await Promise.all([
loadTranslations(params.lang as Locale, ['common', 'blog']),
getBlogPost(params.slug, params.lang as Locale),
])
const t = createTranslator(translations)
return (
<article className="prose">
<h1>{post.title}</h1>
<p className="text-gray-600">
{t('blog.publishedAt')}: {new Date(post.date).toLocaleDateString(params.lang)}
</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Points clés pour l’optimisation des performances :
Il y a une erreur facile à commettre ici. Vous souhaiterez peut-être obtenir les données séparément pour chaque langue :
// ❌ Mauvaise pratique : demandes multiples, construction lente
export async function generateStaticParams() {
const results = []
for (const locale of i18nConfig.localesToPrerender) {
// Demander la base de données ou le CMS une fois par langue est trop lent !
const slugs = await getBlogSlugs(locale)
results.push(...slugs.map(slug => ({ lang: locale, slug })))
}
return results
}
L’approche correcte consiste à demander les données une seule fois, puis à utiliser flatMap pour générer la combinaison :
// ✅ Approche correcte : une requête, génération rapide
export async function generateStaticParams() {
// Ne demander des données qu’une seule fois
const slugs = await getBlogSlugs()
// Utilisez flatMap pour générer toutes les combinaisons langue × article
return i18nConfig.localesToPrerender.flatMap((locale) =>
slugs.map((slug) => ({ lang: locale, slug }))
)
}
Dans mon projet, cette optimisation a réduit le temps de construction de 18 minutes à 6 minutes, l’effet est très évident !
Étape 7 : Implémenter la détection du langage Middleware
Le rôle du Middleware est de détecter automatiquement la préférence linguistique de l’utilisateur et de le rediriger vers la version linguistique correspondante.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { i18nConfig } from './i18n/config'
/**
* Intergiciel middleware
*
* Cette fonction sera exécutée avant chaque requête et sert à :
* 1. Détecter la préférence linguistique de l'utilisateur
* 2. Redirection vers le chemin de langue correspondant
*/
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Vérifiez si le chemin contient déjà le préfixe de langue
const pathnameHasLocale = i18nConfig.locales.some(
(locale) =>
pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
// S'il existe déjà un préfixe de langue, il sera autorisé directement.
if (pathnameHasLocale) return
// Obtenez la langue préférée de l'utilisateur
const locale = getLocale(request) ?? i18nConfig.defaultLocale
// Redirection vers le chemin avec le préfixe de langue
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}
/**
* Fonction de détection de langue
*
*Priorité:
* 1. Préférence de langue enregistrée dans le cookie
* 2. En-tête de demande Accept-Language
* 3. Renvoie null, utilise la langue par défaut
*/
function getLocale(request: NextRequest): string | null {
// Priorité 1 : Vérifier les cookies
const localeCookie = request.cookies.get('NEXT_LOCALE')?.value
if (localeCookie && i18nConfig.locales.includes(localeCookie as any)) {
return localeCookie
}
// Priorité 2 : Vérifier l'en-tête de la requête Accept-Language
const acceptLanguage = request.headers.get('accept-language')
if (acceptLanguage) {
// Format de langue accepté : zh-CN,zh;q=0.9,en;q=0.8
const preferred = acceptLanguage.split(',')[0].split('-')[0]
const match = i18nConfig.locales.find(locale =>
locale.toLowerCase().startsWith(preferred.toLowerCase())
)
if (match) return match
}
// Aucune langue correspondante trouvée, renvoie null
return null
}
/**
*Configuration du middleware
*
* Matcher définit les chemins nécessaires pour exécuter le middleware
*/
export const config = {
// Correspond à tous les chemins sauf :
// - Routes API commençant par /api
// - /_next/static fichiers statiques
// - /_suivant/image image
// - /favicon.ico et autres ressources statiques
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
Scénarios d’utilisation du middleware :
En supposant que l’utilisateur accède directement à « https://example.com/blog », le middleware :
- Vérifiez s’il existe une préférence de langue enregistrée dans le cookie (par exemple, l’utilisateur a sélectionné le chinois la dernière fois)
- Sinon, vérifiez l’en-tête « Accept-Language » du navigateur (le navigateur enverra automatiquement la langue du système de l’utilisateur)
- En fonction des résultats de détection, redirigez vers « https://example.com/zh-CN/blog » ou « https://example.com/en/blog »
De cette manière, la détection automatique de la langue est obtenue et l’expérience utilisateur est meilleure.
Étape 8 : Créer le composant de changement de langue
Enfin, nous avons besoin d’un sélecteur de langue qui permette à l’utilisateur de changer de langue manuellement :
// components/LanguageSwitcher.tsx
'use client'
import { usePathname, useRouter } from 'next/navigation'
import { i18nConfig } from '@/i18n/config'
import type { Locale } from '@/i18n/config'
// Mappage du nom d’affichage de la langue
const localeNames: Record<Locale, string> = {
'en': 'English',
'zh-CN' : 'Chinois simplifié',
'ja' : 'japonais',
}
export function LanguageSwitcher({ currentLocale }: { currentLocale: Locale }) {
const pathname = usePathname()
const router = useRouter()
const handleLocaleChange = (newLocale: Locale) => {
// Enregistrer la préférence de langue dans le cookie
document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`
// Remplacer le préfixe de langue dans le chemin
// Par exemple : /zh-CN/blog → /en/blog
const newPathname = pathname.replace(`/${currentLocale}`, `/${newLocale}`)
// Accéder à la nouvelle version linguistique
router.push(newPathname)
}
return (
<div className="relative">
<select
value={currentLocale}
onChange={(e) => handleLocaleChange(e.target.value as Locale)}
className="px-4 py-2 border rounded-lg"
>
{i18nConfig.locales.map((locale) => (
<option key={locale} value={locale}>
{localeNames[locale]}
</option>
))}
</select>
</div>
)
}
Utilisation dans la barre de navigation :
// components/Navigation.tsx
import { LanguageSwitcher } from './LanguageSwitcher'
export function Navigation({ lang }: { lang: string }) {
return (
<nav className="flex items-center justify-between p-4">
<div className="flex gap-4">
<a href={`/${lang}/`}>Page d'accueil</a>
<a href={`/${lang}/about`}>À propos</a>
<a href={`/${lang}/blog`}>Blog</a>
</div>
<LanguageSwitcher currentLocale={lang} />
</nav>
)
}
Optimisation des performances : faites voler la vitesse de construction
Les fonctions de base sont désormais implémentées, mais si votre site Web prend en charge plusieurs langues, le temps de construction peut être très long. Permettez-moi de partager quelques conseils pratiques d’optimisation.
Optimisation 1 : Pré-rendu sélectif
C’est l’optimisation la plus efficace. Si vous prenez en charge 10 langues, mais que le trafic réel est concentré dans 2 à 3 langues principales, pré-affichez uniquement les langues principales :
// i18n/config.ts
export const i18nConfig = {
// Toutes les langues prises en charge
locales: ['en', 'zh-CN', 'ja', 'ko', 'de', 'fr', 'es', 'pt'],
defaultLocale: 'en',
// [Clé] Pré-afficher uniquement la langue principale
localesToPrerender: process.env.NODE_ENV === 'production'
? ['en', 'zh-CN'] // Environnement de production : pré-rendu uniquement en anglais et en chinois
: ['fr'], // Environnement de développement : afficher uniquement le langage par défaut (accélère le développement)
}
Comparaison des effets :
| Configuration | Temps de construction | Descriptif |
|---|---|---|
| Pré-rendu en 8 langues | ~24 minutes | Génération de pages statiques pour toutes les langues |
| Pré-rendu en 2 langues | ~6 minutes | Généré lors de la première visite dans d’autres langues |
| Ne restitue qu’une seule langue | ~3 minutes | Environnement de développement recommandé |
Économisé 75 % du temps de construction !
Optimisation 2 : Utiliser la régénération statique incrémentielle (ISR)
Pour les langues moins importantes ou les pages peu fréquemment visitées, l’ISR peut être utilisé pour générer à la demande :
// app/[lang]/blog/[slug]/page.tsx
// Activer ISR et ré-authentifier après 1 heure
export const revalidate = 3600
export async function generateStaticParams() {
const slugs = await getBlogSlugs()
// Pré-afficher uniquement les articles populaires dans les principales langues
const topSlugs = slugs.slice(0, 10) // Pré-rendu uniquement les 10 premiers articles
return i18nConfig.localesToPrerender.flatMap((locale) =>
topSlugs.map((slug) => ({ lang: locale, slug }))
)
}
// [Important] Autoriser la génération dynamique de pages non pré-rendues
export const dynamicParams = true
Après avoir configuré comme ceci :
- Seulement 2 langues × 10 articles = 20 pages sont générées lors de la construction
- Lorsqu’un utilisateur accède à une page qui n’est pas pré-rendue, Next.js sera généré et mis en cache en temps réel.
- Cache et mise à jour automatique après 1 heure
Optimisation 3 : Obtenir des données en parallèle
Dans generateStaticParams, si vous avez besoin d’obtenir plusieurs types de données, vous devez les traiter en parallèle :
// ❌ Erreur : acquisition en série (lente)
export async function generateStaticParams() {
const posts = wait getBlogPosts() // attends 2 secondes
const catégories = attendre getCategories() // attendre 1 seconde
// 3 secondes au total
}
// ✅ Correct : acquisition parallèle (rapide)
export async function generateStaticParams() {
const [posts, categories] = await Promise.all([
getBlogPosts(), // s'exécute simultanément
getCategories(), // s'exécute simultanément
])
// 2 secondes au total (prenez la plus longue)
}
Dans mon projet, cette optimisation a réduit le temps d’acquisition des données de 40 %.
Optimisation 4 : Résoudre le problème du cache de traduction
Le plus ennuyeux lors du développement est que le fichier de traduction est mis à jour, mais la page ne s’actualise pas. En effet, Next.js met en cache les fichiers JSON importés.
Solution : Désactivez la mise en cache dans l’environnement de développement
// i18n/utils.ts
import fs from 'fs/promises'
import path from 'path'
const isDev = process.env.NODE_ENV === 'development'
export async function loadTranslations(
locale: Locale,
namespaces: Namespace[]
) {
// Environnement de développement : relisez le fichier à chaque fois
if (isDev) {
const translations: Record<string, any> = {}
for (const ns of namespaces) {
const filePath = path.join(
process.cwd(),
'i18n',
'locales',
locale,
`${ns}.json`
)
try {
const content = await fs.readFile(filePath, 'utf-8')
translations[ns] = JSON.parse(content)
} catch (error) {
console.warn(`Translation file not found: ${filePath}`)
translations[ns] = {}
}
}
return translations
}
// Environnement de production : utilisation du cache
return loadTranslationsWithCache(locale, namespaces)
}
De cette façon, vous pouvez voir le contenu le plus récent en actualisant la page après la mise à jour du fichier de traduction pendant le développement.
FAQ Guide de dépannage
Dans le développement réel, vous pouvez également rencontrer d’autres problèmes. J’ai compilé ici certaines des solutions les plus courantes, ainsi que mes solutions.
Problème 1 : Erreur “generateStaticParams not found” lors de la construction
message d’erreur :
Error: Page "/en/about" is missing `generateStaticParams()`
so it cannot be used with `output: "export"`.
Étapes de dépannage :
- ✅ Vérifiez si
generateStaticParamsest défini danslayout.tsxoupage.tsx - ✅ Assurez-vous que le nom de la fonction est correctement orthographié (pas
getStaticParams, pasgenerateParams) - ✅ Confirmez que la fonction est exportée correctement (doit être « exporter la fonction asynchrone »)
- ✅ Vérifiez si le nom du paramètre correspond au nom du dossier de routage
// ❌ Exemple d'erreur
export async function getStaticParams() { // Nom de fonction incorrect
return [{ locale: 'en' }] // Le nom du paramètre est également erroné
}
// ✅ Exemple correct
export async function generateStaticParams() {
return [{ lang: 'en' }] // Le nom du paramètre doit correspondre à [lang]
}
Question 2 : Rendu dynamique détecté
message d’erreur :
Error: Route /[lang]/about couldn't be rendered statically
because it used `headers` or `cookies`.
Cause : Les API dynamiques (headers(), cookies(), searchParams) sont utilisées dans les pages générées statiquement.
Solution:
// ❌ Erreur : Utilisation de cookies dans les composants côté serveur
export default async function Page() {
const locale = cookies().get('NEXT_LOCALE') // Déclencher le rendu dynamique
return <div>...</div>
}
// ✅ Option 1 : Processus en middleware
// middleware.ts
export function middleware(request: NextRequest) {
const locale = request.cookies.get('NEXT_LOCALE')
// Logique de traitement...
}
// ✅ Option 2 : Utiliser les composants clients
'use client'
export function LanguageSwitcher() {
const [locale, setLocale] = useState(() => {
// Lecture des cookies côté client
return getCookie('NEXT_LOCALE')
})
// ...
}
Question 3 : Fichier de traduction introuvable
message d’erreur :
Error: Cannot find module './locales/en/common.json'
Liste de contrôle:
- ✅ Vérifiez si le chemin du fichier est correct (notez la casse, Linux est sensible à la casse)
- ✅ Confirmez que la syntaxe du fichier JSON est correcte (peut être vérifiée avec des outils en ligne)
- ✅ Vérifiez la configuration de l’alias de chemin de
tsconfig.json:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
- ✅ Confirmez que les fichiers de traduction sont correctement inclus dans le build :
// next.config.js
module.exports = {
// Assurez-vous que le fichier JSON est inclus
webpack: (config) => {
config.module.rules.push({
test: /\.json$/,
type: 'json',
})
return config
},
}
Problème 4 : Les paramètres de routage sont perdus après le changement de langue
Phénomènes : après être passé de /zh-CN/blog/my-post à l’anglais, il passe à /en/ au lieu de /en/blog/my-post.
Cause : Le sélecteur de langue n’a pas conservé correctement les paramètres de routage.
Solution:
// ❌ Erreur : chemin codé en dur
<Link href="/about">About</Link>
// ✅ Option 1 : Épisser manuellement les paramètres de langue
<Link href={`/${params.lang}/about`}>About</Link>
// ✅ Solution 2 : Encapsuler un composant smart Link
// components/LocalizedLink.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export function LocalizedLink({
href,
children,
...props
}: {
href: string
children: React.ReactNode
[key: string]: any
}) {
const pathname = usePathname()
// Extraire la langue du chemin actuel
const locale = pathname.split('/')[1]
// Ajouter automatiquement un préfixe de langue
const localizedHref = `/${locale}${href}`
return (
<Link href={localizedHref} {...props}>
{children}
</Link>
)
}
Problème 5 : Balises SEO manquantes ou incorrectes
Problème : Les balises SEO (hreflang, canonique) des pages multilingues sont mal configurées, ce qui affecte l’inclusion dans les moteurs de recherche.
Solution : Configurez correctement dans generateMetadata de chaque page :
// app/[lang]/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: { lang: string; slug: string }
}) {
const baseUrl = 'https://example.com'
return {
// Titre et description de la page
title: 'My Blog Post',
description: 'This is a blog post',
// URL canonique (lien canonique)
alternates: {
canonical: `${baseUrl}/${params.lang}/blog/${params.slug}`,
// balise hreflang (informer les moteurs de recherche des autres versions linguistiques)
languages: {
'en': `${baseUrl}/en/blog/${params.slug}`,
'zh-CN': `${baseUrl}/zh-CN/blog/${params.slug}`,
'ja': `${baseUrl}/ja/blog/${params.slug}`,
'x-default' : `${baseUrl}/en/blog/${params.slug}`, // Langue par défaut
},
},
// Balise Open Graph (pour le partage sur les réseaux sociaux)
openGraph: {
title: 'My Blog Post',
description: 'This is a blog post',
url: `${baseUrl}/${params.lang}/blog/${params.slug}`,
locale: params.lang,
alternateLocale: i18nConfig.locales.filter(l => l !== params.lang),
},
}
}
## Résumé des meilleures pratiques
Après tant de pratique, j’ai dressé une liste de bonnes pratiques qui peuvent être mises en œuvre directement.
Liste d’initialisation du projet
Avant de commencer le développement, assurez-vous de terminer ces configurations :
- Déterminer la liste des langues prises en charge et la langue par défaut
- Créer une structure de répertoires
app/[lang] - Configurer
i18n/config.tset le répertoire des fichiers de traduction - Implémenter la détection du langage
middleware.ts - Ajoutez
generateStaticParamsà la disposition racine - Configurez
next.config.js(si une exportation statique est requise, définissezoutput : 'export')
Suggestions d’étape de développement
- L’environnement de développement pré-rend uniquement la langue par défaut (
localesToPrerender: ['en'])
-[ ] Utilisez TypeScript pour garantir la sécurité du type des clés de traduction - Divisez l’espace de noms de traduction selon les modules fonctionnels (commun, home, blog…)
- Désactivez la mise en cache des traductions dans l’environnement de développement (utilisez
fs.readFilepour lire en temps réel) - Ajouter un journal d’avertissement pour les traductions manquantes (pour faciliter la découverte des problèmes)
Liste de contrôle du déploiement en production
- Pré-rendu sélectif des principales langues (optimise les temps de construction)
- Configurer la politique ISR (langue secondaire générée à la demande, définir
revalidate) - Utiliser la récupération de données parallèle (
Promise.all) - Configurer les balises hreflang et canoniques correctes
- Définir la stratégie de mise en cache CDN (envisager des chemins multilingues)
- Surveiller le nombre de visites et le temps de construction de chaque version linguistique
next.config.js Exemple de configuration complet
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Exportation statique (si nécessaire)
output: 'export',
// Configuration de l'optimisation des images
images: {
non optimisé : vrai, // requis pour l'exportation statique
},
// variables d'environnement
env: {
BUILD_TIME: new Date().toISOString(),
},
// ID de build personnalisé (pour l'invalidation du cache)
generateBuildId: async () => {
return `build-${Date.now()}`
},
// Configuration du pack Web
webpack: (config, { isServer }) => {
// Assurez-vous que les fichiers JSON sont traités correctement
config.module.rules.push({
test: /\.json$/,
type: 'json',
})
return config
},
}
module.exports = nextConfig
Outils et bibliothèques recommandés
Si vous ne souhaitez pas l’implémenter à partir de zéro, pensez à utiliser ces bibliothèques prêtes à l’emploi :
| Outils/Bibliothèque | Utilisation | Indice de recommandations | Descriptif |
|---|---|---|---|
| suivant-intl | Solution complète i18n | ⭐⭐⭐⭐⭐ | Recommandation officielle, les fonctions les plus complètes, prend en charge App Router |
| prochain international | Bibliothèque i18n légère | ⭐⭐⭐⭐ | Léger, concis et sécurisé |
| @formatjs/intl | Formatage international | ⭐⭐⭐⭐ | Date de traitement, nombre, devise et autres formats |
| typesafe-i18n | Traduction type-safe | ⭐⭐⭐⭐ | Générer automatiquement des définitions de types |
| i18suivant | Ancienne bibliothèque i18n | ⭐⭐⭐ | Puissant mais doit être adapté à App Router |
Je recommande personnellement next-intl, spécialement conçu pour Next.js App Router. Il fonctionne immédiatement et ne nécessite pas beaucoup de configuration de votre part. Mais si vous souhaitez comprendre en profondeur les principes d’implémentation d’i18n, ou si vous avez besoin d’un haut degré de personnalisation, alors l’implémentation manuelle (comme cet article) est également un bon choix.
Résumer
Pour rappel, nous avons implémenté une solution complète de génération statique multilingue Next.js App Router, comprenant :
-
Fonctions principales
- Structure multilingue basée sur le routage dynamique
[lang] - Utilisez
generateStaticParamspour générer des pages statiques - Traduction du système de fichiers par espace de noms
- Détection et redirection automatiques de la langue par middleware
- Structure multilingue basée sur le routage dynamique
-
Optimisation des performances
- Pré-rendu sélectif des principales langues (réduit le temps de construction de 75%)
- Utiliser ISR pour générer des langues secondaires à la demande
- Acquisition de données en parallèle
- Désactiver la mise en cache dans l’environnement de développement
-
Résolution de problèmes
- Erreur de configuration
generateStaticParams - L’API dynamique provoque un échec de construction
- Problème de mise en cache du fichier de traduction
- Itinéraire de changement de langue perdu
- Paramétrage des balises SEO
- Erreur de configuration
Principaux points à retenir :
- L’i18n d’App Router doit être implémenté manuellement et ne peut pas être configuré à l’aide de Pages Router.
generateStaticParamsdoit être défini dans la mise en page ou la page, et les noms des paramètres doivent correspondre- Les pages générées statiquement ne peuvent pas utiliser d’API dynamiques telles que
cookies()etheaders() - Utilisez le pré-rendu sélectif et l’ISR de manière appropriée pour éviter de longs temps de construction
Si vous utilisez également Next.js App Router pour créer un site Web multilingue, j’espère que cet article pourra vous aider à éviter certains pièges. En fait, l’internationalisation en elle-même n’est pas compliquée. La clé est de comprendre le mécanisme de construction de Next.js puis de le configurer selon ses règles.
Enfin, si vous pensez que l’implémentation manuelle est trop compliquée, n’oubliez pas d’essayer la bibliothèque next-intl, qui peut vous éviter bien des ennuis.
FAQ
Pourquoi le build échoue-t-il avec « missing generateStaticParams » ?
Solution :
• Définissez generateStaticParams dans le layout ou la page
• Renvoyez toutes les combinaisons de langue
• Les noms de paramètres doivent correspondre à la structure de la route
Exemple :
export async function generateStaticParams() {
return locales.map(locale => ({ locale }))
}
Comment réduire le temps de build d'un site multilingue ?
• Utilisez le pré-rendu sélectif (uniquement les pages importantes)
• Utilisez l'ISR pour le contenu fréquemment mis à jour
• Mettez en cache les traductions
• Parallélisez le processus de build
• Réduisez le nombre de pages par langue
Exemple : pré-rendez la page d'accueil dans toutes les langues et utilisez l'ISR pour les articles de blog.
Pourquoi les mises à jour de traduction ne prennent-elles pas effet ?
• Cache de build non vidé
• Cache du navigateur
• Cache du CDN
• Cache de la génération statique
Solutions :
• Videz le cache de build
• Utilisez le cache busting
• Mettez en place l'ISR avec revalidation
• Vérifiez les réglages de cache du CDN
Comment utiliser generateStaticParams pour des routes multilingues ?
Exemple :
export async function generateStaticParams() {
const locales = ['en', 'zh']
const posts = await getPosts()
return locales.flatMap(locale =>
posts.map(post => ({ locale, slug: post.slug }))
)
}
Cela génère toutes les combinaisons : /en/post-1, /zh/post-1, etc.
Puis-je utiliser des API dynamiques dans la génération statique ?
• cookies()
• headers()
• Les API dynamiques
Contournements :
• Utilisez des Server Components pour les données dynamiques
• Utilisez l'ISR plutôt que le SSG
• Déplacez la logique dynamique vers des Client Components
Pour l'i18n, utilisez un middleware pour détecter la langue, pas les cookies dans les pages statiques.
Comment implémenter le pré-rendu sélectif ?
1) Ne générez statiquement que les pages importantes
2) Utilisez l'ISR pour les autres
3) Utilisez le rendu dynamique pour les pages propres à chaque utilisateur
Exemple :
• Page d'accueil : SSG (toutes les langues)
• Articles de blog : ISR (revalidate: 3600)
• Tableau de bord utilisateur : rendu dynamique
Ainsi vous réduisez le temps de build tout en gardant les performances.
Quelle est la différence entre SSG et ISR pour l'i18n ?
• Génère toutes les pages au moment du build
• Performances maximales
• Mais nécessite un rebuild pour les mises à jour
ISR (régénération statique incrémentale) :
• Pré-génère, mais peut revalider
• Bon équilibre entre performances et fraîcheur
• Mieux pour le contenu fréquemment mis à jour
Pour les sites multilingues, utilisez le SSG pour le contenu stable et l'ISR pour le dynamique.
26 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
Guide complet de l'internationalisation Next.js : bonnes pratiques avec next-intl
Plongée dans l'i18n avec Next.js App Router : configuration complète de next-intl, routage multilingue, gestion des fichiers de traduction et exemples de code prêts pour la production.
Partie 15 sur 51
Suivant
SEO multilingue Next.js : guide complet pour un indexage correct de chaque langue
Plus de 60 % des sites multilingues ont des erreurs de configuration SEO. Ce guide détaille hreflang, les sitemaps multilingues et le choix de la stratégie d’URL pour éviter les pièges et faire ranker chaque version linguistique.
Partie 17 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire