next-tongue
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.
[!NOTE] Beta 0.9 (API freeze). Install with
npm install next-tongue. Public surfaces are frozen per API freeze. Feedback welcome in issues and the ADRs.
Why
Next.js 16 made Cache Components and Partial Prerendering (PPR) the way to build fast apps, and renamed middleware to proxy.ts. Existing i18n libraries were designed before that, and it shows:
- next-intl (~7M downloads/week) is excellent and mature, but reading the locale in cached/static scopes has been a long-running pain point (amannn/next-intl#1493, #2412), as have
proxy.tslocale routing (#1362) anddynamicParams = falseSSG (#2060). - next-international has had no release since October 2024.
- next-intl 4.8+ has an experimental
messages.precompileoption that compiles ICU at build time (~9 KB less client JS, per its blog), but the client still receives catalogs through the provider. - Paraglide JS 2 proves the compiler approach (tree-shaken, typed message functions) and documents a Next.js 16 setup with a
[locale]root layout, an explicit locale and Cache Components (guide). Its Next plugin requires--webpack.
Positioning
next-tongue is 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. It is in beta (0.9). Size and speed claims ship with reproducible benchmarks against next-intl (with precompile) and Paraglide.
We make no claims about runtime speed or "only the active locale on the client" until the v0.1 benchmark is published. See the critical review.
Design principles
- Build time over run time. If it can be parsed, validated, typed or tree-shaken at build time, it is. (ADR 0001)
- The locale is a route param. Only
proxy.tstouches headers and cookies, so every translated scope can be cached and prerendered. (ADR 0002) - Standards, not lock-in. ICU MessageFormat, plain JSON files, any OpenAI-compatible model, MIT license.
- Next.js-native, nothing else. Next.js 16+ App Router only, latest minor supported, with a nightly canary run. (ADR 0006, ADR 0011)
- Zero dependencies. Nothing beyond the
nextandreactpeers, at runtime, build time or in the CLI. (ADR 0012)
Non-goals (v1): Pages Router, Next.js ≤ 15, client-side runtime catalogs, a hosted translation service.
Features (planned)
- Zero dependencies beyond
nextandreact— own small ICU parser with a documented subset, Node built-ins only in the CLI, enforced by a CI dependency budget (ADR 0012). - Compiled messages that work with Turbopack — codegen before bundling plus a per-file call-site loader; no bundler plugin APIs (ADR 0001).
- Client components ship only the messages they call — each
t()call imports a per-key module. The recommended pattern is to translate on the server and pass strings, which ships no message code at all (ADR 0009). - Params-only locale for Cache Components — the locale always comes from route
params(resolved byproxy.ts), never fromheaders()/cookies()inside cached scopes. Static shells, PPR andoutput: 'export'work. - Database and CMS messages — async build-time
sourcesintongue.config.ts(compiled and typed like files), plus server-onlyformatMessage(locale, icu, values)for copy that changes without a rebuild (Database messages, ADR 0013). - Translator-friendly tooling —
en-XApseudo-localisation,tongue export/importfor XLIFF 2.0 and CSV, and a per-route report of keys and bytes sent to the client (Dev tools, Translator workflow). - Accessible headless
LocaleSwitcher— keeps the same page (localized pathnames), persists the choice, works without JS. - Typed
Intlhelpers — lists, relative time, units and display names (Intl.ListFormat,RelativeTimeFormat,DisplayNames, unit formats). - Translator context and stale detection —
$metacomments, screenshot links and max-length limits fed into translate/export;tongue checkflags translations whose source changed (hashes intongue.lock). - Per-user preferred language — a documented recipe now; a helper with a signed cookie and a
createProxy({ resolveLocale })hook in v1.1, with no DB call in the proxy (User preferences). - Security-hardened — XSS tests for rich text and database strings, strict locale validation against path traversal and header injection, and a threat model.
- VS Code extension (v1.1) — inline previews, missing-key diagnostics, go-to-message, hover with all locales (VS Code).
- Dev overlay — a dev-only panel with the current locale, missing keys and the messages sent to the client; click a string to jump to its key. Stripped from production.
- Fully typed — keys, namespaces and ICU arguments (
{count, plural, ...}→count: number) are checked by TypeScript. - One API across the App Router — server components, client components, server actions,
generateMetadatawith hreflang, translated OG images and emails, locale-awareLink/redirect/useRouter, and typed translated validation errors (translateIssues, schema-agnostic). - CLI (Node built-ins only) —
tongue build,tongue init,tongue extract,tongue check(CI gate for missing / unused / mismatched keys, pluslang/dirand bidi checks) andtongue translate(bring your own LLM key via any OpenAI-compatible endpoint, with glossary and lockfile). - Hydration-safe dates — an explicit
timeZonein config (default'UTC'). The server's default zone is never used, and per-user zones are scoped to dynamic boundaries (ADR 0008). - Fast proxy at any locale count — locale matching tables are precomputed at build time; the target is < 1 ms p50 with 150 locales.
- Monorepo-ready — shared message packages in pnpm workspaces, with portable generated types.
- Migration from next-intl — a guide now; a codemod after v1.0.
Quick start (planned API)
npm install next-tongue
npx tongue init
// 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 — locales + defaultLocale required
import { createProxy } from 'next-tongue/proxy';
export const proxy = createProxy({
locales: ['en', 'de'],
defaultLocale: 'en',
prefix: 'as-needed',
});
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]/layout.tsx (excerpt)
import { localeParams } from 'next-tongue/server';
export const generateStaticParams = localeParams; // keeps the shell static under cacheComponents
// 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>;
}
See Getting started for the full walkthrough.
Comparison
Honest, feature-level comparison as of October 2026. next-tongue's column is planned, not shipped. No performance numbers are claimed; benchmarks will be published with reproducible scripts once code exists.
| next-tongue (planned) | next-intl | Paraglide JS 2 | gt-next | next-international | Lingui | |
|---|---|---|---|---|---|---|
| Status | beta 0.9 | mature, very active | stable, active | active, company-backed | unmaintained since Oct 2024 | mature, active |
| Approach | build-time compiler (codegen + per-file loader) | runtime catalogs; experimental messages.precompile compiles ICU at build time | build-time compiler | runtime + AI translation service; experimental compiler | runtime catalogs | macros + compiled catalogs |
| Works with Turbopack (its compiler) | goal | yes (loader) | Next plugin requires --webpack | auto-injection is webpack-only (docs) | n/a | SWC plugin, pinned to Next's swc_core |
| Next.js App Router routing helpers | yes (proxy.ts, Link, redirect) | yes | generic middleware + localizeUrl | yes | yes | no (bring your own) |
| Cache Components / params-only locale | yes (params-only locale) | supported with caveats, via root params (see #1493) | documented on Next 16.4 with Cache Components | unverified | no | root-params example |
| Client gets only the messages it calls | yes, per key (all locales of used keys by default) | no — provider passes catalogs (narrow with pick); precompile removes the parser, not unused messages | yes, per message (all locales by default) | no | partial (per-locale import) | per catalog |
| Typed keys | yes | yes | yes (functions) | no | yes | n/a (source strings / macro IDs) |
| Typed ICU arguments | yes | opt-in (v4, via generated declarations) | yes | no | params typed, no ICU | n/a (args come from JS template literals) |
| Message format | ICU MessageFormat (documented subset) | ICU MessageFormat | inlang message format (plugins incl. ICU) | ICU / JSX | custom interpolation | ICU (PO / JSON) |
| Extraction / CI check CLI | yes (extract, check) | experimental useExtracted (v4.5+); no built-in CI check | via inlang tooling | yes (CLI) | no | yes (lingui extract) |
| AI translation | BYO key CLI | no (separate products) | via inlang ecosystem | core product (service) | no | no |
| Migration from next-intl | guide; codemod after v1.0 | — | no | no | — | no |
Uses next/root-params for implicit locale | not by default (ADR 0007); explicit locale arg | required for Cache Components support (#1493) | no (explicit locale) | unverified | no | yes, in example/docs on Next 16.3+ (lingui#2645) |
Sources: each project's public docs and issue tracker, checked October 2026; see the critical review for links. Corrections welcome — open an issue if any cell is wrong.
Documentation
Also available as a GitHub wiki (same content as docs/).
Roadmap
| Milestone | Theme |
|---|---|
| v0.1 | 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 |
| v0.2 | proxy.ts (always/as-needed), navigation, metadata, static export, server formatMessage, pseudo-localisation, Intl helpers, security hardening |
| v0.3 | CLI check/extract/init, dev overlay, LocaleSwitcher, translated validation errors, OG images and emails |
| v0.4 | Localized pathnames, domains, prefix: 'never', monorepo, build-time DB/CMS sources, XLIFF/CSV, translator context, stale detection, a11y/bidi checks, tongue report |
| v0.5 | tongue translate (BYO LLM, glossary, lockfile) |
| v0.9 | Beta: API freeze, root-params decision (#24) |
| v1.0 | Stable, semver / deprecation / support policy |
| v1.1 | VS Code extension, per-user preferred language helper |
| post-1.0 | next-intl codemod (next-international codemod dropped) |
Full detail with acceptance criteria: ROADMAP.md · Milestones · Project board.
Support policy
From v1.0: Semantic Versioning, documented deprecations before removal, and Next.js support as in ADR 0011 — latest Next 16.x + React ≥ 19.2 (App Router). Details: Versioning.
Contributing
Design feedback is the most valuable contribution right now. Read CONTRIBUTING.md, pick an issue labelled good first issue or comment on an ADR. Please follow the Code of Conduct; report vulnerabilities per SECURITY.md.
Support
If this project is useful to you, you can support its development with a donation: Buy me a coffee. It helps keep these tools maintained, documented and free for everyone.
License
MIT © Louis Wright (Louis Wright)