Repository navigation
User Preferences
Louis Wright edited this page Oct 14, 2026
·
1 revision
📄 Mirrors
docs/user-preferences.mdin the main repo — the repo version is canonical.
Status: planned / pre-release. This recipe works with the v0.2 APIs; the
setLocalePreferencehelper and thecreateProxy({ resolveLocale })hook ship in v1.1 (#48). See ROADMAP.
Signed-in users often have a preferred language stored in your database. next-tongue resolves it in the proxy from a signed cookie. It never calls the database in the proxy and never reads headers inside cached scopes (ADR 0002).
- Save: a server action validates the locale, stores it in your DB and sets a signed cookie.
- Login: after authentication, the login action reads the stored preference and sets the same signed cookie.
-
Resolve:
proxy.tsverifies the cookie's signature (Web Crypto, no DB call) and redirects or rewrites to that locale. Pages still get the locale fromparams, so the static shell is unaffected.
// app/[locale]/settings/actions.ts
'use server';
import { setLocalePreference } from 'next-tongue/server';
import { redirect } from 'next-tongue/navigation';
export async function saveLanguage(form: FormData) {
const id = await userId();
const locale = await setLocalePreference(form.get('locale'), { // validates with isLocale(), throws TONGUE_E_LOCALE
secret: process.env.TONGUE_PREF_SECRET!, // HMAC-SHA-256 via Web Crypto
save: (l) => db.user.update({ where: { id }, data: { locale: l } }),
});
redirect({ href: '/settings', locale });
}Before v1.1, set the cookie yourself in an action: (await cookies()).set('TONGUE_PREF', value, { httpOnly: true, sameSite: 'lax', secure: true }), where value is locale + '.' + hmac computed with crypto.subtle.
// proxy.ts
import { createProxy, verifyLocalePreference } from 'next-tongue/proxy';
export const proxy = createProxy({
// Runs before cookie/Accept-Language detection. Return a configured locale or undefined.
resolveLocale: (req) => verifyLocalePreference(req.cookies.get('TONGUE_PREF')?.value, process.env.TONGUE_PREF_SECRET!),
});- The proxy never calls your database; the signed cookie carries the preference.
- An invalid or unsigned value is ignored (normal detection applies); only configured locales are accepted.
- An explicit URL prefix still wins:
/de/...stays German even if the preference isfr. The locale switcher (#44) updates the preference.
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