Aller au contenu
Next.js i18n

Next.js i18n avec App Router

Server Components, ISR et traductions optimisées pour l’edge pour les applications Next.js.

1middleware
middleware.ts
2getRequestConfig
i18n/request.ts
3getMessages()
app/[locale]/page.tsx
4useTranslations()
components/Hero.tsx
cdn.better-i18n.com/your-org/your-project/{locale}/translations.jsonmax-age=60

Setup

Set up in 4 steps

Installation

Ajoutez @better-i18n/next et next-intl à votre projet.

terminal
npm install @better-i18n/next next-intl

Ajoutez le middleware de détection de locale

Le middleware lit l'en-tête Accept-Language et le préfixe de l'URL pour détecter la locale de l'utilisateur et rediriger en conséquence.

middleware.ts
import { createBetterI18nMiddleware } from '@better-i18n/next';

export default createBetterI18nMiddleware({
  project: 'your-org/your-project',
  defaultLocale: 'en',
  localePrefix: 'always',
});

export const config = { matcher: ['/((?!api|_next).*)'] };

Chargez les messages dans un Server Component

Utilisez getMessages() dans votre layout racine pour récupérer les traductions côté serveur et les transmettre à BetterI18nProvider.

app/[locale]/layout.tsx
// app/[locale]/layout.tsx
import { BetterI18nProvider } from '@better-i18n/next/client';
import { getMessages } from '@better-i18n/next/server';

const config = { project: 'your-org/your-project', defaultLocale: 'en' };

export default async function RootLayout({ children, params }) {
  const { locale } = await params;
  const messages = await getMessages(config, locale);

  return (
    <html lang={locale}>
      <body>
        <BetterI18nProvider locale={locale} messages={messages} config={config}>
          {children}
        </BetterI18nProvider>
      </body>
    </html>
  );
}

Utilisez les traductions dans les Client Components

Appelez useTranslations() dans n'importe quel Client Component. Les messages sont déjà hydratés depuis le serveur — aucune requête supplémentaire.

components/HeroSection.tsx
'use client';
import { useTranslations } from 'next-intl';

export function HeroSection() {
  const t = useTranslations('home');
  return <h1>{t('title')}</h1>;
}

Routing

Détection de l'environnement d'exécution Edge et des paramètres régionaux

Exécutez la détection des paramètres régionaux et le chargement des messages en périphérie pour obtenir des temps de réponse inférieurs à 50 ms dans le monde entier.

Configuration du middleware

Ajoutez la détection de locale et le routage à votre application Next.js avec un seul fichier middleware.

middleware.ts
// middleware.ts — locale detection
import { createBetterI18nMiddleware } from '@better-i18n/next'

export default createBetterI18nMiddleware({
  project: 'your-org/your-project',
  defaultLocale: 'en',
  localePrefix: 'always',
})

export const config = { matcher: ['/((?!api|_next).*)'] }
middleware.ts
// middleware.ts — Edge-based locale detection
import { NextRequest, NextResponse } from 'next/server';

const SUPPORTED_LOCALES = ['en', 'de', 'fr', 'ja', 'es'] as const;
const DEFAULT_LOCALE = 'en';

function getPreferredLocale(request: NextRequest): string {
  // 1. Check URL prefix
  const pathname = request.nextUrl.pathname;
  const urlLocale = SUPPORTED_LOCALES.find(
    (l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`
  );
  if (urlLocale) return urlLocale;

  // 2. Check cookie
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as any)) {
    return cookieLocale;
  }

  // 3. Parse Accept-Language header
  const acceptLang = request.headers.get('accept-language') ?? '';
  const preferred = acceptLang
    .split(',')
    .map((part) => part.split(';')[0].trim().substring(0, 2))
    .find((code) => SUPPORTED_LOCALES.includes(code as any));

  return preferred ?? DEFAULT_LOCALE;
}

export function middleware(request: NextRequest) {
  const locale = getPreferredLocale(request);
  const { pathname } = request.nextUrl;

  const hasLocale = SUPPORTED_LOCALES.some(
    (l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`
  );

  if (!hasLocale) {
    return NextResponse.redirect(
      new URL(`/${locale}${pathname}`, request.url)
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

Chargement des messages compatible avec Edge

Mettez en cache les traductions à l'edge grâce à un cache en mémoire léger basé sur un TTL, pour des réponses instantanées.

lib/edge-messages.ts
// lib/edge-messages.ts — Edge-compatible message loading
const messageCache = new Map<string, { data: Record<string, string>; ts: number }>();
const TTL = 60_000; // 1 minute cache at edge

export async function getEdgeMessages(
  locale: string,
  namespace: string
): Promise<Record<string, string>> {
  const cacheKey = `${locale}:${namespace}`;
  const cached = messageCache.get(cacheKey);

  if (cached && Date.now() - cached.ts < TTL) {
    return cached.data;
  }

  const response = await fetch(
    `https://cdn.better-i18n.com/your-org/your-project/${locale}/${namespace}.json`,
    { next: { revalidate: 60 } }
  );

  const data = await response.json();
  messageCache.set(cacheKey, { data, ts: Date.now() });
  return data;
}

Route API Edge avec i18n

Renvoyez des réponses API traduites depuis des fonctions edge avec un cold start minimal.

app/api/translate/route.ts
// app/api/translate/route.ts — Edge API route with i18n
import { getEdgeMessages } from '@/lib/edge-messages';

export const runtime = 'edge';

export async function GET(request: Request) {
  const url = new URL(request.url);
  const locale = url.searchParams.get('locale') ?? 'en';
  const key = url.searchParams.get('key') ?? '';

  const messages = await getEdgeMessages(locale, 'api-responses');
  const translated = messages[key] ?? key;

  return Response.json({ text: translated, locale });
}

Rendering

ISR et internationalisation

Combinez la régénération statique incrémentielle avec i18n pour obtenir des pages multilingues rapides et toujours à jour.

PublishR2 writeCDN purgeRevalidatePage served
revalidate = 3600app/[locale]/layout.tsx~60 min
revalidate = 1800app/[locale]/[slug]/page.tsx~30 min
revalidatePath()app/api/revalidate/route.ts< 1 min

Démarrage rapide

Ajoutez l’i18n à votre app Next.js en quelques lignes de code.

app/[locale]/page.tsx
// app/[locale]/page.tsx
import { getTranslations } from 'next-intl/server';

export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'home' });

  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </main>
  );
}
app/[locale]/layout.tsx
// app/[locale]/layout.tsx — ISR with i18n
import { getMessages } from '@better-i18n/next/server';
import { BetterI18nProvider } from '@better-i18n/next/client';

export const revalidate = 3600; // Revalidate every hour

const config = { project: 'your-org/your-project', defaultLocale: 'en' };

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const messages = await getMessages(config, locale);

  return (
    <BetterI18nProvider locale={locale} messages={messages} config={config}>
      {children}
    </BetterI18nProvider>
  );
}

ISR avec generateStaticParams

Pré-rendez les pages pour chaque locale au moment du build, puis actualisez-les périodiquement grâce à l'ISR.

app/[locale]/[slug]/page.tsx
// app/[locale]/[slug]/page.tsx — Generate static pages per locale
import { getMessages } from '@better-i18n/next/server';

const config = { project: 'your-org/your-project', defaultLocale: 'en' };

export async function generateStaticParams() {
  const locales = ['en', 'de', 'fr', 'ja'];
  const slugs = await fetchAllSlugs();
  return locales.flatMap((locale) =>
    slugs.map((slug) => ({ locale, slug }))
  );
}

export const revalidate = 1800; // ISR: refresh every 30 min

export default async function Page({
  params,
}: {
  params: Promise<{ locale: string; slug: string }>;
}) {
  const { locale, slug } = await params;
  const messages = await getMessages(config, locale, { namespaces: ['blog'] });
  return <article><h1>{messages.blog[slug + '.title']}</h1></article>;
}

Revalidation à la demande

Déclenchez la revalidation ISR lorsque les traductions sont mises à jour — connectez-vous au webhook de publication de Better I18N.

app/api/revalidate/route.ts
// app/api/i18n/revalidate/route.ts — On-demand ISR for translation updates
import { createRevalidateHandler } from '@better-i18n/next/revalidate';

// Called by the Better i18n publish webhook — verifies the HMAC signature,
// then revalidates the paths/tags below.
export const POST = createRevalidateHandler({
  secret: process.env.BETTER_I18N_WEBHOOK_SECRET!,
  revalidatePaths: ['/'],
  revalidateTags: ['i18n-messages'],
});

Advanced

Modèles avancés

Dispositions imbriquées, itinéraires parallèles et actions serveur avec traductions sécurisées par type.

app/[locale]/dashboard/layout.tsx
// app/[locale]/dashboard/layout.tsx — Nested layout with namespace
import { getMessages } from '@better-i18n/next/server';
import { BetterI18nProvider } from '@better-i18n/next/client';
import { DashboardNav } from '@/components/DashboardNav';

const config = { project: 'your-org/your-project', defaultLocale: 'en' };

export default async function DashboardLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  // Load the dashboard-specific namespace alongside common messages
  const messages = await getMessages(config, locale, {
    namespaces: ['common', 'dashboard'],
  });

  return (
    <BetterI18nProvider locale={locale} messages={messages} config={config}>
      <DashboardNav />
      <main>{children}</main>
    </BetterI18nProvider>
  );
}

Routes parallèles avec i18n

Chargez les traductions de manière indépendante dans les emplacements de routes parallèles pour des layouts modulaires et sensibles à la locale.

app/[locale]/@analytics/page.tsx
// app/[locale]/@analytics/page.tsx — Parallel route with i18n
import { getTranslations } from 'next-intl/server';

export default async function AnalyticsSlot({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'analytics' });

  return (
    <section aria-label={t('title')}>
      <h2>{t('title')}</h2>
      <p>{t('description')}</p>
    </section>
  );
}

// app/[locale]/layout.tsx — Consuming parallel routes
export default function Layout({
  children,
  analytics,
  notifications,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  notifications: React.ReactNode;
}) {
  return (
    <div>
      <main>{children}</main>
      <aside>{analytics}</aside>
      <aside>{notifications}</aside>
    </div>
  );
}

Server Actions avec traduction

Renvoyez des erreurs de validation traduites et des messages de succès depuis les server actions.

app/[locale]/contact/actions.ts
// app/[locale]/contact/actions.ts — Server action with i18n
'use server';
import { getTranslations } from 'next-intl/server';
import { headers } from 'next/headers';

export async function submitContactForm(formData: FormData) {
  const headersList = await headers();
  // Set by createBetterI18nMiddleware — see the routing section above
  const locale = headersList.get('x-locale') ?? 'en';
  const t = await getTranslations({ locale, namespace: 'contact' });

  const email = formData.get('email') as string;
  const message = formData.get('message') as string;

  if (!email || !message) {
    return { error: t('validation.required') };
  }

  try {
    await sendEmail({ email, message, locale });
    return { success: t('form.success') };
  } catch {
    return { error: t('form.error') };
  }
}

// app/[locale]/contact/page.tsx — Using the server action
'use client';
import { useTranslations } from 'next-intl';
import { submitContactForm } from './actions';

export default function ContactPage() {
  const t = useTranslations('contact');

  return (
    <form action={submitContactForm}>
      <label>{t('form.email')}</label>
      <input name="email" type="email" required />
      <label>{t('form.message')}</label>
      <textarea name="message" required />
      <button type="submit">{t('form.submit')}</button>
    </form>
  );
}

Dépannage des problèmes courants liés à l'internationalisation (i18n)

Corrigez les incompatibilités d'hydratation, les remplacements de paramètres régionaux manquants et les différences de formatage des dates/nombres.

// Fix: Hydration mismatch with date/number formatting
// Problem: Server renders "1,000" but client renders "1.000"
// Solution: BetterI18nProvider already passes an explicit timeZone down to
// NextIntlClientProvider, so server and client share the same formatting locale.

// app/[locale]/layout.tsx
import { getFormatter } from 'next-intl/server';

export default async function Layout({ children, params }: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  // Pre-format on server with the explicit locale
  const format = await getFormatter({ locale });

  return (
    <html lang={locale} suppressHydrationWarning>
      <body>{children}</body>
    </html>
  );
}

// components/Price.tsx — Client component
'use client';
import { useFormatter } from 'next-intl';

export function Price({ amount }: { amount: number }) {
  const format = useFormatter();
  // useFormatter automatically uses the locale/timeZone from BetterI18nProvider
  // ensuring server and client render the same output
  return <span>{format.number(amount, { style: 'currency', currency: 'USD' })}</span>;
}

Chaîne de repli de locale

Définissez des chaînes de repli afin que les variantes régionales comme pt-BR se rabattent sur pt, puis sur en.

lib/i18n-config.ts
// lib/i18n-config.ts — Locale fallback chain
const FALLBACK_CHAIN: Record<string, string[]> = {
  'pt-BR': ['pt', 'en'],
  'zh-TW': ['zh-CN', 'en'],
  'en-GB': ['en'],
  'de-AT': ['de', 'en'],
};

export function resolveMessages(
  locale: string,
  allMessages: Record<string, Record<string, string>>
): Record<string, string> {
  const chain = FALLBACK_CHAIN[locale] ?? ['en'];
  const primary = allMessages[locale] ?? {};

  // Merge fallback messages (primary overrides fallbacks)
  return chain.reduceRight(
    (merged, fallbackLocale) => ({
      ...merged,
      ...(allMessages[fallbackLocale] ?? {}),
    }),
    primary
  );
}

Formatage de date cohérent

Évitez les décalages de date entre le serveur et le client en définissant explicitement timeZone sur UTC.

components/LocalizedDate.tsx
// components/LocalizedDate.tsx — Consistent date formatting
'use client';
import { useFormatter, useLocale } from 'next-intl';

export function LocalizedDate({ date }: { date: Date | string }) {
  const format = useFormatter();
  const locale = useLocale();
  const dateObj = typeof date === 'string' ? new Date(date) : date;

  return (
    <time dateTime={dateObj.toISOString()}>
      {format.dateTime(dateObj, {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        // Explicitly set timeZone to avoid server/client mismatch
        timeZone: 'UTC',
      })}
    </time>
  );
}

Capabilities

Fonctionnalités

Support App Router et Pages Router
Middleware pour la détection automatique de la locale
Support React Server Components
Génération statique avec generateStaticParams
Régénération statique incrémentale (ISR)
Traductions type-safe
Diffusion via edge CDN
Optimisé SEO avec hreflang
Routage basé sur la locale
Integrations

Compatible avec les bibliothèques i18n populaires de Next.js

Better I18N ne remplace pas votre bibliothèque i18n préférée — c'est la couche de gestion des traductions qui la rend encore plus puissante.

Better I18N + next-intl

La bibliothèque i18n Next.js la plus populaire avec support App Router, messages typés et syntaxe ICU.Better I18N synchronise les traductions directement au format JSON next-intl. Gérez depuis notre tableau de bord, déployez instantanément via CDN.

Better I18N + next-i18next

Bibliothèque i18n éprouvée pour Next.js basée sur i18next. Idéale pour Pages Router et la migration vers App Router.Export au format JSON i18next avec namespaces. Better I18N gère le workflow de traduction, next-i18next gère l'exécution.

Better I18N + Lingui

Bibliothèque i18n légère basée sur les macros avec une excellente DX et extraction automatique des messages.Extrayez les messages avec Lingui CLI, gérez les traductions dans Better I18N, synchronisez automatiquement via l'intégration GitHub.

Explorer d'autres guides de frameworks

Commencez à construire avec Next.js i18n

Offre gratuite disponible. Aucune carte bancaire requise.