best-i18n

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 as fuzzy instead 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-i18n

Pick the integration for your framework. Everything below it — the macros, the PO workflow, the URL helpers — is the same either way.

FrameworkIntegration
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 allPlain 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, fuzzy instead 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.

On this page