Přejít na obsah
Next.js i18n

Next.js i18n s aplikací App Router

Komponenty serveru, ISR a překlady optimalizované pro aplikace Next.js.

Set up in 4 steps

1

Install

Add @better-i18n/use-intl, use-intl, and the Next.js adapter to your project.

terminal
npm install @better-i18n/use-intl use-intl
2

Add middleware for locale detection

The middleware reads the Accept-Language header and URL prefix to detect the user's locale and redirect accordingly.

middleware.ts
import { betterI18nMiddleware } from '@better-i18n/next';
export const middleware = betterI18nMiddleware;
export const config = { matcher: ['/((?!api|_next).*)'] };
3

Load messages in a Server Component

Use getMessages() in your root layout to fetch translations server-side and pass them to BetterI18nProvider.

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

export default async function RootLayout({ children, params }) {
  const messages = await getMessages({
    projectId: 'your-org/your-project',
    locale: params.locale,
  });
  return (
    <html lang={params.locale}>
      <body>
        <BetterI18nProvider messages={messages} locale={params.locale} projectId="your-org/your-project">
          {children}
        </BetterI18nProvider>
      </body>
    </html>
  );
}
4

Use translations in Client Components

Call useTranslations() in any Client Component. Messages are already hydrated from the server — no extra fetch.

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

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

Funkce

Podpora směrovače aplikací a stránek
Middleware pro automatickou detekci lokalizace
Podpora komponent serveru React
Statické generování pomocí generateStaticParams
Inkrementální statická regenerace
Typově bezpečné překlady
Dodávka Edge CDN
SEO optimalizované pomocí hreflang
Směrování na základě místní polohy

Middleware Setup

Add locale detection and routing to your Next.js app with a single middleware file.

// middleware.ts — locale detection
import { betterI18nMiddleware } from '@better-i18n/next'
export const middleware = betterI18nMiddleware
export const config = { matcher: ['/((?!api|_next).*)'] }

Rychlý start

Přidejte i18n do své Next.js aplikace jen s několika řádky kódu.

// app/[locale]/page.tsx
import { getTranslations } from '@better-i18n/next';

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

  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </main>
  );
}

ISR a internacionalizace

Kombinujte inkrementální statickou regeneraci s i18n pro rychlé a vždy aktuální vícejazyčné stránky.

// app/[locale]/layout.tsx — ISR with i18n
import { getMessages } from '@better-i18n/use-intl/server';
import { BetterI18nProvider } from '@better-i18n/use-intl';

export const revalidate = 3600; // Revalidate every hour

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  const messages = await getMessages({
    projectId: 'your-org/your-project',
    locale: params.locale,
  });

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

ISR with generateStaticParams

Pre-render pages for every locale at build time, then refresh with ISR on a schedule.

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

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: { locale: string; slug: string };
}) {
  const t = await getMessages({
    projectId: 'your-org/your-project',
    locale: params.locale,
    namespace: 'blog',
  });
  return <article><h1>{t[params.slug + '.title']}</h1></article>;
}

On-Demand Revalidation

Trigger ISR revalidation when translations are updated — hook into the Better I18N publish webhook.

// app/api/revalidate/route.ts — On-demand ISR for translation updates
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { locale, path, secret } = await request.json();

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
  }

  // Revalidate the specific locale path
  revalidatePath(`/${locale}${path}`);
  return NextResponse.json({ revalidated: true, locale, path });
}

Edge Runtime a detekce národního prostředí

Spusťte detekci locale a načítání zpráv na okraji s odezvou pod 50 ms po celém světě.

// 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-Compatible Message Loading

Cache translations at the edge with a lightweight in-memory TTL cache for instant responses.

// 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 with i18n

Return translated API responses from edge functions with minimal cold start.

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

Řešení běžných problémů s i18n

Opravte nesrovnalosti v hydrataci, chybějící záložní lokální nastavení a rozdíly ve formátování datumu/čísel.

// Fix: Hydration mismatch with date/number formatting
// Problem: Server renders "1,000" but client renders "1.000"
// Solution: Ensure the same locale is used on both server and client

// app/[locale]/layout.tsx
import { getFormatter } from '@better-i18n/use-intl/server';

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

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

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

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

Locale Fallback Chain

Define fallback chains so regional variants like pt-BR fall back to pt, then en.

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

Consistent Date Formatting

Avoid server/client date mismatches by explicitly setting timeZone to UTC.

// components/LocalizedDate.tsx — Consistent date formatting
'use client';
import { useFormatter, useLocale } from '@better-i18n/use-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>
  );
}

Pokročilé vzory

Vnořené rozvržení, paralelní trasy a akce serveru s typově bezpečnými překlady.

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

export default async function DashboardLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  // Load dashboard-specific namespace alongside common messages
  const [commonMessages, dashMessages] = await Promise.all([
    getMessages({ projectId: 'your-org/your-project', locale: params.locale, namespace: 'common' }),
    getMessages({ projectId: 'your-org/your-project', locale: params.locale, namespace: 'dashboard' }),
  ]);

  const messages = { ...commonMessages, ...dashMessages };

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

Parallel Routes with i18n

Load translations independently in parallel route slots for modular, locale-aware layouts.

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

export default async function AnalyticsSlot({
  params,
}: {
  params: { locale: string };
}) {
  const t = await getTranslations(params.locale, '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 with Translation

Return translated validation errors and success messages from server actions.

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

export async function submitContactForm(formData: FormData) {
  const headersList = headers();
  const locale = headersList.get('x-locale') ?? 'en';
  const t = await getTranslations(locale, '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 '@better-i18n/use-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>
  );
}

Funguje s populárními i18n knihovnami pro Next.js

Better I18N nenahrazuje vaši oblíbenou i18n knihovnu — je to vrstva správy překladů, která je činí ještě výkonnějšími.

Better I18N + next-intl

Nejpopulárnější i18n knihovna pro Next.js s podporou App Router, typově bezpečnými zprávami a ICU syntaxí.

Better I18N synchronizuje překlady přímo do JSON formátu next-intl. Spravujte v dashboardu, nasaďte okamžitě přes CDN.

Better I18N + next-i18next

Osvědčená i18n knihovna pro Next.js založená na i18next. Ideální pro Pages Router a migraci na App Router.

Export do i18next kompatibilního JSON s namespace. Better I18N řídí překladový workflow, next-i18next řídí runtime.

Better I18N + Lingui

Lehká, na makrech založená i18n knihovna s vynikajícím DX a automatickou extrakcí zpráv.

Extrahujte zprávy pomocí Lingui CLI, spravujte překlady v Better I18N, synchronizujte automaticky přes GitHub integraci.

Začněte tvořit s Next.js i18n

K dispozici je bezplatná úroveň. Není vyžadována kreditní karta.