Repository navigation
Database and CMS Messages
Louis Wright edited this page Oct 14, 2026
·
1 revision
📄 Mirrors
docs/database-messages.mdin the main repo — the repo version is canonical.
Status: planned / pre-release. This page documents the intended API of next-tongue. Nothing is published yet; names may change before v1.0. See ROADMAP.
next-tongue supports translations stored in a database or CMS in two ways (ADR 0013). Both need no extra dependencies, and neither ships a catalog to the browser.
Build-time sources (v0.4) |
Request-time formatMessage (v0.2) |
|
|---|---|---|
| When it runs |
tongue build / next build / next dev
|
On the server, per render |
| Typed keys and args | ✅ | ❌ (untyped string) |
| Validated (plurals, arg parity) | ✅ at build | at render (throws on invalid ICU) |
| Change without rebuild | ❌ | ✅ |
| Client components | via useT like file messages |
pass the rendered string as a prop |
// tongue.config.ts
import { defineConfig } from 'next-tongue/config';
import { fetchTranslations } from './lib/cms';
export default defineConfig({
locales: ['en', 'de'],
defaultLocale: 'en',
messages: './messages/{locale}.json',
sources: [
async () => fetchTranslations(), // → { en: { marketing: { hero: 'Hello {name}' } }, de: { ... } }
],
sourcesOnError: 'fail', // or 'use-cache' to reuse .tongue/sources.json
});- Each source returns
{ [locale]: { [namespace]: { [key]: icuString } } }. - Sources are merged after file messages, in array order; later entries win.
tongue checkreports keys defined in more than one place. - Merged messages are compiled, validated and typed like file messages.
- Use only built-in
fetchor your own DB client in the source function; next-tongue adds nothing.
- Build-time messages change only on rebuild. Trigger a redeploy from a CMS webhook (e.g. a Vercel/Netlify deploy hook) when translations are published.
- In
next dev, sources are fetched on start and ontongue build --watch-sources <seconds>; file edits still hot-reload. - Keep fast-changing copy (banners, promotions) out of
sources; useformatMessagebelow. - CI: set
sourcesOnError: 'use-cache'only if a stale snapshot is acceptable; commit nothing in.tongue/.
// app/[locale]/promo.tsx (Server Component)
import { formatMessage } from 'next-tongue/server';
import { cacheLife, cacheTag } from 'next/cache';
async function getPromo(locale: Locale) {
'use cache';
cacheTag('promo');
cacheLife('hours');
const row = await db.promo.find(locale); // { text: 'Save {pct, number, ::percent} on <b>{product}</b>' }
return formatMessage(locale, row.text, { pct: 0.2, product: 'Pro' });
}- Server-only (
import 'server-only'guard); untypedvalues; returns astring. - Compiled messages are cached in a bounded LRU (
formatMessage.configure({ cacheSize: 500 }), default 500 entries). - Pure: reads no request data, so it works inside
'use cache'. Revalidate the data withcacheTag/revalidateTagwhen editors publish. -
Rich-text safety: tags in stored messages are stripped (text kept) by default. Use
formatMessage.rich(locale, icu, values, { tags: { b: (c) => <b>{c}</b> } })to render only allow-listed tags as React nodes. Output is never raw HTML; values are always inserted as text, so stored messages cannot inject markup or attributes. - Uses the same supported ICU subset as compiled messages (ICU messages); unsupported syntax throws
TONGUE_E_ICU_SYNTAX.
Guides
- Getting Started
- Configuration
- Routing and Proxy
- Server Components
- Client Components
- Server Actions
- ICU Messages
- Type Safety
- Cache Components and PPR
- Static Export
- Metadata SEO and hreflang
- Database and CMS Messages
- Dev Tools
- Translator Workflow
- Per-user Preferred Language
- CLI
- AI Translation
- Testing
- VS Code Extension
- Migration from next intl
- Migration from next international (deferred)
Reference