next-intl
Excellent and mature, but reading the locale in cached/static scopes has been a long-running pain point, as have proxy.ts locale routing and dynamicParams = false SSG.
FlagshipPre-alpha · planned APINext.js 16 · MIT
Compiler-first, type-safe i18n for the Next.js 16 App Router. Works with Turbopack, keeps the locale in route params, zero dependencies beyond next and react.
npm install next-tongue # not yet publishedNot published to npm yet. No library code exists; everything here is the planned API from the README and design docs, open for feedback.
{ "inbox": "{count, plural, =0 {No messages} one {# message} other {# messages}}" }
t('title', { name })
Welcome, Ada!
t('inbox', { count })
1 message
plural category: one · typed as count: number
Illustration: the README's example messages rendered in your browser with Intl.PluralRules. next-tongue is not published yet.
01 · The problem
Next.js 16 made Cache Components and Partial Prerendering the way to build fast apps, and renamed middleware to proxy.ts. Existing i18n libraries were designed before that, and it shows.
Excellent and mature, but reading the locale in cached/static scopes has been a long-running pain point, as have proxy.ts locale routing and dynamicParams = false SSG.
No release since October 2024.
Compiles ICU at build time, but the client still receives catalogs through the provider.
Proves the compiler approach with typed, tree-shaken message functions, but its Next plugin requires --webpack.
A Next.js 16–only i18n library that compiles ICU messages ahead of time and works with Turbopack, the default bundler. Client components ship only the messages they actually call. The locale comes from route params, never from headers, so translated pages stay in the static shell under Cache Components.
02 · How it works
tongue build (or withTongue when next.config loads) parses messages with its own ICU parser, validates plurals and arguments, and writes .tongue/types.d.ts, per-key modules and a manifest.
A Turbopack/webpack loader rewrites t('inbox') in client files into a static import of #tongue/msg/home/inbox. No bundler plugin APIs.
The bundler dedupes and tree-shakes per key across routes. Translating on the server and passing strings ships no message code at all.
03 · Features planned
Own small ICU parser with a documented subset, Node built-ins only in the CLI, enforced by a CI dependency budget.
Codegen before bundling plus a per-file call-site loader; no bundler plugin APIs.
Each t() call imports a per-key module. Translate on the server and pass strings to ship no message code at all.
The locale always comes from route params (resolved by proxy.ts), never from headers() or cookies() inside cached scopes. Static shells, PPR and output: 'export' work.
Keys, namespaces and ICU arguments ({count, plural, ...} → count: number) are checked by TypeScript.
Async build-time sources in tongue.config.ts, compiled and typed like files, plus server-only formatMessage for copy that changes without a rebuild.
en-XA pseudo-localisation, tongue export/import for XLIFF 2.0 and CSV, and a per-route report of keys and bytes sent to the client.
Server components, client components, server actions, generateMetadata with hreflang, translated OG images and emails, locale-aware Link, redirect and useRouter.
tongue build, init, extract, check (CI gate for missing, unused and mismatched keys) and translate with any OpenAI-compatible endpoint.
An explicit timeZone in config (default 'UTC'). The server's default zone is never used.
Headless, keeps the same page with localized pathnames, persists the choice, works without JS.
XSS tests for rich text and database strings, strict locale validation against path traversal and header injection, and a threat model.
04 · Quick start planned API
// tongue.config.ts
import { defineConfig } from 'next-tongue/config';
export default defineConfig({
locales: ['en', 'de'],
defaultLocale: 'en',
messages: './messages/{locale}.json',
});// next.config.ts
import { withTongue } from 'next-tongue/next'; // config wrapper: runs codegen, registers the call-site loader
export default withTongue({ cacheComponents: true });// proxy.ts
import { createProxy } from 'next-tongue/proxy';
export const proxy = createProxy();
export const config = { matcher: ['/((?!api|_next|.*\\..*).*)'] };
// Note: this matcher skips every path containing a dot (e.g. /de/blog/v1.2). See Routing & proxy.// messages/en.json
{ "home": { "title": "Welcome, {name}!", "inbox": "{count, plural, =0 {No messages} one {# message} other {# messages}}" } }// app/[locale]/page.tsx
import { getT } from 'next-tongue/server';
export default async function Page({ params }: PageProps<'/[locale]'>) {
const { locale } = await params;
const t = await getT(locale, 'home');
return <h1>{t('title', { name: 'Ada' })}</h1>; // t('titel') or a missing `name` is a type error
}// app/[locale]/inbox-badge.tsx
'use client';
import { useT } from 'next-tongue/client';
export function InboxBadge({ count }: { count: number }) {
const t = useT('home');
return <span>{t('inbox', { count })}</span>;
}Full walkthrough: Getting started · API reference · CLI
05 · Configuration
| Option | Type | Default | Notes |
|---|---|---|---|
locales | readonly string[] | — (required) | Validated as BCP 47. Becomes the Locale union type. |
defaultLocale | Locale | — (required) | The source language for extract/translate. |
messages | string | './messages/{locale}.json' | Must contain {locale}; may contain {namespace}. |
fallback | Partial<Record<Locale, Locale>> | CLDR parent locales | Resolved at compile time, so there is no runtime lookup. |
timeZone | IANA zone string | 'UTC' | Passed explicitly to every Intl.DateTimeFormat; the server's default zone is never used. |
sources | Array<() => Promise<…>> | [] | Build-time database/CMS messages, merged after files, then compiled and typed. |
pseudo | object | boolean | false | Adds a pseudo-locale (default en-XA) generated from defaultLocale. |
client.split | 'none' | 'locale' | 'none' | 'locale' (opt-in, after the benchmark) splits per-key modules per locale. |
devOverlay | boolean | true | Dev-only overlay; never included in production builds. |
routing.prefix | 'always' | 'as-needed' | 'never' | 'as-needed' | 'as-needed' redirects /{defaultLocale}/* to /*. |
routing.localeDetection | boolean | true | Only the proxy reads headers/cookies. |
routing.pathnames | Record<string, …> | undefined | Typed: Link href only accepts known internal pathnames when set. |
outDir | string | '.tongue' | Git-ignore it. |
strict.* | 'error' | 'warn' | 'off' | see docs | Used by both the build and tongue check. |
Abridged. Full reference in docs/configuration.md.
No. It is pre-alpha with no published code. Follow the roadmap.
next-intl is great and mature. next-tongue explores a different core trade-off (build-time compilation that works with Turbopack, message-level client tree-shaking, params-only locale and zero dependencies) which would be a breaking redesign there. A migration guide exists; a next-intl codemod is planned after v1.0.
Paraglide pioneered compiled, tree-shaken messages and documents a Next.js 16 setup with Cache Components, but its Next plugin requires --webpack. next-tongue applies the compiler idea with a Turbopack-compatible loader and Next.js-specific pieces built in: proxy.ts routing, metadata/hreflang, tongue check.
No. It works with or without cacheComponents: true; it just never breaks it.
Yes, two ways: async build-time sources in tongue.config.ts (compiled and typed like files, need a rebuild to change), or the server-only formatMessage(locale, icu, values) for copy that changes at request time (untyped). No catalog is sent to the browser.
Yes, in v0.1: a published bundle benchmark against next-intl (with precompile) and Paraglide is the go/no-go gate. Until then there are no size or speed claims.
The latest 16.x minor, tested nightly against canary. Pages Router is not supported.
07 · Roadmap
Prove the differentiator: own ICU parser + codegen, types, getT/useT, call-site loader, Cache Components example, published bundle benchmark vs next-intl (precompile) and Paraglide — go/no-go gate
next upproxy.ts (always/as-needed), navigation, metadata, static export, server formatMessage, pseudo-localisation, Intl helpers, security hardening
CLI check/extract/init, dev overlay, LocaleSwitcher, translated validation errors, OG images and emails
Localized pathnames, domains, prefix: 'never', monorepo, build-time DB/CMS sources, XLIFF/CSV, translator context, stale detection, a11y/bidi checks, tongue report
tongue translate (BYO LLM, glossary, lockfile)
Beta: API freeze, root-params decision
Stable, semver guarantees
VS Code extension, per-user preferred language helper
next-intl codemod
Scope freeze: no new features before v0.1 ships. Acceptance criteria in ROADMAP.md.
Support this project
I build free, open-source tools that help developers ship safer websites. Every coffee helps keep them maintained, documented and free for everyone.
Buy me a coffee (opens Stripe in a new tab)Secure checkout by Stripe