Limitations
The edges of the design, stated plainly
- Messages must be statically visible — no dynamic message construction.
- On Next.js, a Client Component has to take its locale from
useI18n(). Plaintand a<Trans>with nouseI18n()above it readgetLocale(), and client components render in a module graph where nothing has bound the request's locale — so they would fall back to the base one, on the server only, and React patches the difference at hydration without a word. The loader refuses to compile that, naming the file, the line and the message. Server Components are the other way round: they cannot call a hook, and do not need to. - Plurals are gettext plurals (
plural(count, one, other)), not ICU: there is noselect/gender construct yet, and no number/date formatting — reach forIntlwithgetLocale()for those. from/hookFrommatch import specifiers as written in the source, so if you re-export the macros, list your module path in the plugin options.- On Next.js,
best-i18n/next/serverreads the locale out of Next's internal render storage, becausethas to resolve synchronously whileparamsandheaders()are async. That is a private API, so a Next.js major version can break it. The field it reads,rootParams, arrived in 15.2, which is the peer floor. - In a Client Component prefer
useI18n. Plaintthere reads the ambient client locale, whichLocaleProvidermirrors on the client — but only from the nearest provider, and without re-rendering on a locale change the wayuseI18ndoes. best-i18n/serverneedsAsyncLocalStorage. Node and Bun ship it; on Cloudflare Workers it exists only behind thenodejs_compat(ornodejs_als) compatibility flag, and without the flag the import throws with an error saying exactly that, instead of silently sharing one locale between requests.- workerd does not implement
AsyncLocalStorage.enterWith(). best-i18n only calls it inbest-i18n/next/server— a route handler prerendered outside a React render — which by definition runs on Node, where it is fully supported (Bun too). On Workers the locale is bound withwithRequestLocalefrombest-i18n/server, which uses.run()and is unaffected. (enterWithbinds the remainder of the current synchronous frame, which is why Node's docs preferrun— here that frame is exactly the handler invocation, which is the intent.)