A production-ready Next.js 15 starter template with Better I18N for internationalization.
Live demo: nextjs-i18n-starter.vercel.app
- Next.js 15 — App Router with Server Components
- Better I18N SDK (
@better-i18n/next@0.7.0) — Cloud-managed translations with CDN delivery - Instant locale switching — Client-side URL navigation + message fetch, no full page reload
- Dynamic language discovery — Languages auto-sync from your dashboard
- SSR translations — Pre-loaded server-side, no flash of untranslated content
- Webhook revalidation — Auto-revalidate ISR cache when translations are published
- i18n Doctor CLI — Health score, coverage audit, and CI/CD integration
- 15 locales — EN, TR, DE, ES, FR, JA, KO, AR, RU, PT, IT, NL, HI, PL, ZH
- Tailwind CSS 4 — Utility-first styling, light theme
- TypeScript — Full type safety
git clone https://github.com/better-i18n/nextjs-i18n-starter.git
cd nextjs-i18n-starter
bun install- Create a free account at dash.better-i18n.com
- Create a new project and add your languages
- Copy your project identifier (e.g.
your-org/your-project)
cp .env.example .envEdit .env:
NEXT_PUBLIC_BETTER_I18N_PROJECT=your-org/your-project
BETTER_I18N_WEBHOOK_SECRET=whsec_... # Optional: for webhook revalidationbun devOpen http://localhost:3000 — you'll be redirected to /en.
├── i18n.config.ts # Better I18N configuration (createI18n)
├── middleware.ts # Locale detection middleware (betterMiddleware)
├── src/
│ ├── i18n/
│ │ └── request.ts # next-intl request handler
│ ├── app/
│ │ ├── layout.tsx # Root layout
│ │ ├── globals.css # Tailwind imports
│ │ ├── api/i18n/revalidate/
│ │ │ └── route.ts # Webhook endpoint for ISR revalidation
│ │ └── [locale]/
│ │ ├── layout.tsx # BetterI18nProvider + getMessages()
│ │ ├── page.tsx # Home — format demos, CLI showcase
│ │ ├── about/page.tsx # About — CDN architecture, SDK APIs
│ │ ├── features/page.tsx # Features — full feature breakdown
│ │ └── demos/page.tsx # Demos — plurals, interpolation, RTL
│ └── components/
│ ├── Header.tsx # Navigation with LocaleDropdown
│ ├── Footer.tsx # Footer with locale-aware links
│ ├── CodeBlock.tsx # Syntax-highlighted code blocks
│ └── demos/
│ ├── FormatBentoDemo.tsx # Currency/date/relative time across locales
│ ├── PluralCounterDemo.tsx
│ ├── FormatShowcase.tsx
│ └── LocaleCompare.tsx
| Feature | File | API |
|---|---|---|
| Config | i18n.config.ts |
createI18n() |
| Middleware | middleware.ts |
i18n.betterMiddleware() |
| Provider | [locale]/layout.tsx |
BetterI18nProvider |
| Server messages | [locale]/layout.tsx |
i18n.getMessages(locale) |
| Server locales | [locale]/page.tsx |
i18n.getLocales() |
| Translations | Components | useTranslations("namespace") |
| Locale switching | LocaleDropdown |
useSetLocale() + router.replace() |
| Language discovery | LocaleDropdown |
useManifestLanguages() |
| Webhook revalidation | api/i18n/revalidate/route.ts |
createRevalidateHandler() |
| Request config | src/i18n/request.ts |
i18n.requestConfig |
When translations are published in the dashboard, a webhook triggers ISR cache revalidation so your production site shows fresh translations instantly.
- Go to your project in the Better I18N dashboard → Integrations → Webhooks
- Add endpoint:
https://your-domain.com/api/i18n/revalidate - Select Published event
- Copy the webhook secret to your environment:
BETTER_I18N_WEBHOOK_SECRET=whsec_your_secret_hereThe handler verifies HMAC-SHA256 signatures and calls revalidatePath("/", "layout") + revalidateTag("i18n-messages").
// app/api/i18n/revalidate/route.ts
import { createRevalidateHandler } from "@better-i18n/next/revalidate";
export const POST = createRevalidateHandler({
secret: process.env.BETTER_I18N_WEBHOOK_SECRET!,
});Run a full health check on your i18n setup — detect hardcoded strings, missing translations, and orphan keys:
npx @better-i18n/cli doctor████████████████░░░░ 82/100 A
✓ Coverage 95
✓ Quality 88
! Code 72
✓ Structure 100
Use --ci to enforce a minimum score in your CI/CD pipeline. Results can be uploaded to the dashboard with --report.
All URLs include the locale prefix:
/en— English/tr— Turkish/de/about— German about page
The middleware redirects / to /{defaultLocale} automatically.
Translations are loaded server-side in [locale]/layout.tsx:
const messages = await i18n.getMessages(locale);The LocaleDropdown uses useSetLocale() for instant locale changes with URL navigation:
const setLocale = useSetLocale();
setLocale("tr"); // Updates cookie + messages + URL (/en → /tr)Languages are fetched from the CDN manifest — add a language in the dashboard and it automatically appears in the switcher:
const { languages, isLoading } = useManifestLanguages(i18n.config);Set environment variables and run:
bun run build
bun startMIT