Introduction
Compile-time i18n — no keys, no runtime, no catalog to load
Compile-time i18n. Write the source text inline; the compiler inlines every translation at the call site, so there is no runtime catalog, no lookup, and nothing to load. A per-locale build collapses to plain string literals.
import { useI18n } from 'best-i18n/react/macro'
function About() {
const t = useI18n()
return <h1>{t`A small starter with room to grow.`}</h1>
}compiles (default build) to
const t = useLocale() // re-renders on locale change
return (
<h1>
{t === 'zh'
? `一个小而可长的起始模板。`
: `A small starter with room to grow.`}
</h1>
)and with staticLocale: 'zh' (per-locale build) to
return <h1>{`一个小而可长的起始模板。`}</h1>Why
- No keys. The source text is the message; ids are content hashes managed for you in PO files.
- No runtime. Messages compile to ternaries (single build) or literals (per-locale build). Unused messages tree-shake with the code that used them.
- PO workflow.
messages.pot+<locale>.po— the format translators, TMSes and LLMs already understand. Rewording a message carries its translation over asfuzzyinstead of losing it; removed messages become#~obsolete entries, never deleted. - SSR-safe. The server locale lives in AsyncLocalStorage per request; if the runtime cannot provide isolation it throws instead of silently sharing state between requests.
Setup
pnpm add best-i18nPick the integration for your framework. Everything below it — the macros, the PO workflow, the URL helpers — is the same either way.
| Framework | Integration |
|---|---|
| Vite, and anything on it (TanStack Start, React Router, SvelteKit, Astro) | best-i18n/vite |
| Next.js (App Router, Turbopack or webpack) | best-i18n/next |
| Rolldown used directly, and tools built on it (tsdown, ...) | best-i18n/rolldown |
| No framework at all | Plain JavaScript |
rolldown-vite keeps the Vite plugin API, so it takes best-i18n/vite
unchanged; best-i18n/rolldown is for Rolldown without Vite around it.
Where the ideas come from
best-i18n did not invent its best ideas, it inherited them:
- GNU gettext — the PO workflow this
package speaks: source text as the message,
fuzzyinstead of data loss,#~instead of deletion. Decades of translator tooling work because these conventions are respected. - Lingui — the macro shape and the
<0>...</0>placeholder convention for markup in messages, adopted here for the same reason it exists there: a translator should never see a JSX attribute. - Paraglide JS — the proof that compile-time i18n with per-locale tree-shaking is viable, and the bar for what a locale-strategy API can look like.
- next-intl — the reference for what a complete Next.js App Router integration covers; its playground twin in this repo is what keeps the size claims honest.
- gettext-parser — the PO codec
underneath
i18n-extract.