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-Languageheader - Redirects to the appropriate locale prefix
- Sets the
x-better-localeheader 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
Was this article helpful?