Naar inhoud gaan
Next.js i18n

Next.js i18n met App Router

Server Components, ISR en edge-geoptimaliseerde vertalingen voor Next.js-applicaties.

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

Installatie

Voeg @better-i18n/next en next-intl toe aan je project.

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

Voeg middleware toe voor locale-detectie

De middleware leest de Accept-Language-header en het URL-voorvoegsel om de locale van de gebruiker te detecteren en dienovereenkomstig door te sturen.

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).*)'] };

Laad berichten in een Server Component

Gebruik getMessages() in je root layout om vertalingen server-side op te halen en door te geven aan 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>
  );
}

Gebruik vertalingen in Client Components

Roep useTranslations() aan in elke Client Component. Berichten zijn al gehydrateerd vanaf de server — geen extra fetch nodig.

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

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

Routing

Edge Runtime en detectie van lokale instellingen

Voer locale-detectie en het laden van berichten uit aan de rand voor responstijden van minder dan 50 ms wereldwijd.

Middleware-configuratie

Voeg locale-detectie en routering toe aan je Next.js-app met één middleware-bestand.

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).*)'],
};

Edge-compatibel berichten laden

Cache vertalingen op de edge met een lichtgewicht in-memory TTL-cache voor directe reacties.

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;
}

Edge API-route met i18n

Retourneer vertaalde API-reacties vanuit edge-functies met een minimale cold start.

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 & Internationalisering

Combineer incrementele statische regeneratie met i18n voor snelle, altijd actuele meertalige pagina's.

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

Snelle start

Voeg i18n toe aan je Next.js-app met slechts een paar regels 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 met generateStaticParams

Pre-render pagina's voor elke locale tijdens de build, en vernieuw ze vervolgens periodiek met 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>;
}

On-demand revalidatie

Activeer ISR-revalidatie wanneer vertalingen worden bijgewerkt — haak in op de publish-webhook van 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

Geavanceerde patronen

Geneste lay-outs, parallelle routes en serveracties met typeveilige vertalingen.

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>
  );
}

Parallelle routes met i18n

Laad vertalingen onafhankelijk in parallelle route-slots voor modulaire, locale-bewuste layouts.

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 met vertaling

Retourneer vertaalde validatiefouten en succesberichten vanuit 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>
  );
}

Problemen met i18n oplossen

Los problemen op met betrekking tot hydratatie, ontbrekende locale fallbacks en verschillen in datum-/getalnotatie.

// 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>;
}

Locale-fallbackketen

Definieer fallbackketens zodat regionale varianten zoals pt-BR terugvallen op pt en vervolgens op 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
  );
}

Consistente datumopmaak

Voorkom server/client-datumverschillen door timeZone expliciet in te stellen op 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

Functies

Ondersteuning voor App Router en Pages Router
Middleware voor automatische locale-detectie
Ondersteuning voor React Server Components
Statische generatie met generateStaticParams
Incrementele statische regeneratie
Typeveilige vertalingen
Edge CDN-distributie
SEO-geoptimaliseerd met hreflang
Locale-gebaseerde routing
Integrations

Werkt met populaire Next.js i18n-bibliotheken

Better I18N vervangt je favoriete i18n-bibliotheek niet — het is de vertaalmanagementlaag die ze nog krachtiger maakt.

Better I18N + next-intl

De populairste Next.js i18n-bibliotheek met App Router-ondersteuning, type-veilige berichten en ICU-syntax.Better I18N synchroniseert vertalingen direct naar next-intl JSON-formaat. Beheer in ons dashboard, deploy direct via CDN.

Better I18N + next-i18next

Beproefde i18n-bibliotheek voor Next.js gebaseerd op i18next. Ideaal voor Pages Router en migratie naar App Router.Exporteer naar i18next-compatibel JSON met namespaces. Better I18N beheert de vertaalworkflow, next-i18next de runtime.

Better I18N + Lingui

Lichtgewicht, macro-gebaseerde i18n-bibliotheek met uitstekende DX en automatische berichtextractie.Extraheer berichten met Lingui CLI, beheer vertalingen in Better I18N, synchroniseer automatisch via GitHub-integratie.

Bekijk andere framework-handleidingen

Begin met bouwen met Next.js i18n

Gratis pakket beschikbaar. Geen creditcard vereist.