The simplest way to build internationalization in Next.js β fully typed, file-based, zero runtime.
Stop managing translation files. Stop jumping between locales.
Just colocate translations with your features and import a single t.
Instead of splitting translations across files per language:
/locales/en.json
/locales/pt.json
/locales/es.json
You write translations next to the feature they belong to, once:
// app/homepage/t.ts
export default {
title: { en: "Hello {name}", pt: "OlΓ‘ {name}" },
hero: {
subtitle: { en: "Welcome", pt: "Bem-vindo" },
},
}Thatβs it.
You think in features, not files. You think in meaning, not locales.
From those t.ts files, better-intl generates a fully typed translation tree:
import { t } from "@/i18n/generated"
t.homepage.title({ name: "Ada" }) // "OlΓ‘ Ada"
t.homepage.hero.subtitle // "Bem-vindo"Just TypeScript + generated code.
better-intl does one thing:
It transforms your
t.tsfiles into a single typed translation object grouped by feature path.
Pipeline:
t.ts files (feature-based)
β
scan project structure
β
transpose locales into keys
β
generate typed module
β
you import a single `t`
The file system becomes your i18n structure.
bun add better-intlbunx --bun better-intl initinit writes intl.config.ts, runs the first
generate. It is idempotent β existing files are left untouched. The two steps
below (the Next plugin and the layout setLocale) are the wiring init can't do
for you β both are required.
// next.config.ts
import { withInternationalization } from "better-intl/next"
export default withInternationalization({
reactStrictMode: true,
})Thatβs it. No webpack plugins. No runtime setup.
init already wrote a working intl.config.ts. Tweak it as you grow β add
locales, fallback chains, change the scan root:
// intl.config.ts
import type { I18nUserConfig } from "better-intl"
export default {
root: "./src",
out: "./src/i18n/generated.ts",
defaultLocale: "en",
locales: ["en", "pt", "es"],
onMissing: "warn",
fallback: {
es: ["pt"],
},
storage: {
type: "cookie",
key: "locale",
},
} satisfies I18nUserConfigThe generated module already exports everything bound to your translations
and config β there's no lib/i18n wiring to write. Import t straight from it:
import { t, setLocale, updateLocale } from "@/i18n/generated"t is the active locale's slice (sync on both client and server), setLocale()
fills the per-request locale once in your root layout, and updateLocale(locale)
persists a new preference to the cookie. They are produced by createI18n, which
the generator calls for you.
Resolve the locale once per request and stamp it on <html lang>. setLocale()
fills the server store and returns the locale β the client reads that lang
first, so its first render matches the server HTML and there's no locale
flash.
// app/layout.tsx
import { setLocale } from "@/i18n/generated"
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const locale = await setLocale()
return (
<html lang={locale}>
<body>{children}</body>
</html>
)
}// src/components/header/t.ts
export default {
greeting: { en: "Hello {name}", pt: "OlΓ‘ {name}" },
nav: {
home: { en: "Home", pt: "InΓcio" },
},
}import { t } from "@/i18n/generated"
export function Header() {
return (
<header>
<h1>{t.components.header.greeting({ name: "Ada" })}</h1>
<a>{t.components.header.nav.home}</a>
</header>
)
}No hooks. No async. No context.
better-intl resolves locale in this order:
- user stored preference (cookie)
- browser / Accept-Language
- fallback chain
- defaultLocale
Region-aware matching is supported:
pt-BR β pt
If a translation is missing:
locale β fallback β defaultLocale
Control behavior:
| mode | behavior |
|---|---|
| error | fail build |
| warn | log + fallback (default) |
| silent | fallback silently |
Traditional i18n forces you to think like this:
βWhich file contains this string?β
better-intl flips it:
βWhich feature contains this meaning?β
| Problem | better-intl |
|---|---|
| runtime translation lookup | β removed |
| string keys | β removed |
| provider/hook boilerplate | β removed |
| missing translations at runtime | β impossible |
| feature-based organization | β built-in |
| full type inference | β native |
- Feature-based colocation (
t.ts) - Build-time transformation
- Fully typed translation tree
- Zero runtime dependency
- Synchronous usage everywhere
- Token-aware typing (
{ name }) - Next.js-first design
| option | default | description |
|---|---|---|
| root | ./app |
scan root |
| out | generated.ts |
output file |
| defaultLocale | en |
fallback |
| locales | inferred | supported |
| onMissing | warn |
missing handling |
| fallback | {} |
fallback map |
| storage | cookie | persistence |
better-intl is built on one belief:
Localization should follow your codebase structure β not fight it.