How do I set up Better i18n with Next.js?

8 min readIntermediate

The @better-i18n/next package integrates with Next.js App Router and uses next-intl under the hood. It adds CDN-powered message fetching with ISR support.

Installation

bun add @better-i18n/next next-intl

Step 1: Create the i18n configuration

Create src/i18n.ts:

import { createI18n } from '@better-i18n/next';

export const i18n = createI18n({
  project: 'acme/dashboard',
  defaultLocale: 'en',
  localePrefix: 'as-needed', // 'always' | 'as-needed' | 'never'
  manifestRevalidateSeconds: 3600, // ISR: revalidate locale list every hour
  messagesRevalidateSeconds: 30,   // ISR: revalidate messages every 30s
});

Locale prefix options

Option Default locale URL Other locale URL
as-needed /about /tr/about
always /en/about /tr/about
never /about /about (locale from cookie/header)

Step 2: Set up the request config

Create src/i18n/request.ts:

import { i18n } from '@/i18n';

export default i18n.requestConfig;

This tells next-intl how to load messages for each request. Under the hood, it fetches from the Better i18n CDN with ISR caching.

Step 3: Add the middleware

Create src/middleware.ts:

import { i18n } from '@/i18n';

export default i18n.betterMiddleware();

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

The middleware:

  • Detects the user's preferred locale from the Accept-Language header
  • Redirects to the appropriate locale prefix
  • Sets the x-better-locale header for downstream use

Step 4: Add the provider

In your root layout (src/app/[locale]/layout.tsx):

import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';

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

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

Step 5: Use translations

import { useTranslations } from 'next-intl';

export function WelcomeBanner() {
  const t = useTranslations('common');

  return (
    <div>
      <h1>{t('welcome_title')}</h1>
      <p>{t('welcome_description')}</p>
    </div>
  );
}

Environment variables

# .env.local
BETTER_I18N_PROJECT=acme/dashboard

The public key is embedded in the SDK — no API key needed for read-only CDN access.

How ISR caching works

The SDK creates two internal createI18nCore instances:

  • Manifest core — fetches available locales, revalidates every manifestRevalidateSeconds (default: 3600s)
  • Messages core — fetches translations, revalidates every messagesRevalidateSeconds (default: 30s)

This means after you publish new translations, your Next.js app picks them up within ~30 seconds without a rebuild.

Next steps