Repository navigation
Server Actions
📄 Mirrors
docs/server-actions.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.
Server actions have no params. Pass the locale explicitly — either bound or as a form field — and validate it (ADR 0002, #26).
// app/[locale]/contact/actions.ts
'use server';
import { getT, isLocale } from 'next-tongue/server';
export async function sendMessage(locale: string, prev: State, form: FormData): Promise<State> {
if (!isLocale(locale)) throw new Error('Invalid locale');
const t = await getT(locale, 'contact');
const email = form.get('email');
if (typeof email !== 'string' || !email.includes('@')) {
return { error: t('errors.invalidEmail') };
}
// ...
return { ok: t('sent') };
}// app/[locale]/contact/form.tsx
'use client';
import { useActionState } from 'react';
import { useLocale } from 'next-tongue/client';
import { sendMessage } from './actions';
export function ContactForm() {
const locale = useLocale();
const [state, action] = useActionState(sendMessage.bind(null, locale), {});
return <form action={action}>{/* ... */}</form>;
}Bound arguments are visible to the client and can be tampered with — always validate with
isLocale(). The locale only selects a language; never use it for authorization.
import { redirect } from 'next-tongue/navigation';
redirect({ href: '/thanks', locale });<form action={sendMessage}><input type="hidden" name="locale" value={locale} />…</form>Read it with form.get('locale') and validate with isLocale(). withLocale(action) (v0.2) wraps an action whose first argument is a bound locale, validates it and throws TONGUE_E_LOCALE otherwise.
translateIssues turns validation issues into typed, translated messages. It is schema-agnostic: it accepts any array shaped like Standard Schema issues ({ message: string; path?: ReadonlyArray<PropertyKey | { key: PropertyKey }> }), so it works with any validator that emits that shape. next-tongue has no validator dependency.
'use server';
import { getT, translateIssues, isLocale } from 'next-tongue/server';
export async function signup(locale: string, prev: State, form: FormData): Promise<State> {
if (!isLocale(locale)) throw new Error('Invalid locale');
const result = await schema['~standard'].validate(Object.fromEntries(form)); // any Standard Schema validator
if (result.issues) {
const t = await getT(locale, 'validation');
return { errors: translateIssues(t, result.issues) }; // { email: ['Enter a valid email'], ... }
}
// ...
}- An issue's
messageis treated as a key in the namespace (typed:translateIssuesacceptsT<'validation'>, and unknown keys hitfallback). -
paramsper issue ({ message: 'tooShort', params: { min: 8 } }) become ICU args. - Options:
{ fallback?: Key; formatPath?: (path) => string }. Result:Record<string, string[]>, with root issues under''. - Works the same in route handlers and client components (
useT('validation')+translateIssuesfromnext-tongue/client).
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