How do I set up Better i18n with Next.js?
@better-i18n/next integrates with next-intl: it supplies the request config, the middleware and the CDN fetching, and you keep using standard next-intl hooks in your components.
Prerequisites #
- Next.js App Router
- A project at dash.better-i18n.com — its identifier is
org/project
Step 1: Install #
npm install @better-i18n/next
# or: bun add @better-i18n/nextStep 2: Create i18n.config.ts #
At the project root, because the CLI reads the same file:
import { createI18n } from "@better-i18n/next";
export const i18n = createI18n({
projectId: "my-company/web-app",
defaultLocale: "en",
});createI18n returns an object — requestConfig, betterMiddleware, getMessages, getLocales, getManifest, proxy — so keep it whole as i18n rather than destructuring hooks out of it. The translation hooks come from next-intl, not from here.
You do not list your locales: they come from the project's manifest on the CDN, so adding a language in the dashboard does not mean editing this file.
Step 3: Wire up next-intl #
// src/i18n/request.ts
import { i18n } from "../i18n.config";
export default i18n.requestConfig;That is the whole server-side setup — no provider to mount in layout.tsx and no getMessages call to write by hand.
Step 4: Add the middleware #
// middleware.ts
import { i18n } from "./i18n.config";
export default i18n.betterMiddleware();
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)"],
};Locale detection (cookie, then browser language) is built in, so there is no detectLocale of your own to write.
If you have auth, pass a callback and keep both behaviours:
export default i18n.betterMiddleware(async (request, { locale }) => {
const isLoggedIn = !!request.cookies.get("session")?.value;
if (!isLoggedIn && request.nextUrl.pathname.includes("/dashboard")) {
return NextResponse.redirect(new URL(`/${locale}/login`, request.url));
}
// Return nothing and the i18n response is used, headers intact.
});Returning nothing from the callback is the important half: that is how the i18n response — and the locale headers on it — survives your auth check.
Step 5: Use translations #
// app/page.tsx
import { useTranslations } from "next-intl";
export default function Home() {
const t = useTranslations("common");
return <h1>{t("welcome")}</h1>;
}Standard next-intl hooks, which is also what makes better-i18n scan able to find your keys — it looks for these calls.
Revalidation (optional) #
Two windows, both Next.js ISR revalidation rather than an in-memory cache:
| Option | Production default | Dev default |
|---|---|---|
messagesRevalidateSeconds | 5 | 0 |
manifestRevalidateSeconds | 3600 | 0 |
export const i18n = createI18n({
projectId: "my-company/web-app",
defaultLocale: "en",
messagesRevalidateSeconds: 30,
});Zero in development is deliberate: an edit shows up on the next request while you work, and production still caches.
Better I18N