@verbaly/next
v0.63.0
Published
Next.js integration for Verbaly: App Router/RSC with per-request locale negotiation, Turbopack and webpack support, flash-free hydration.
Maintainers
Readme
Server Components render already translated in the visitor's language (cookie first, then Accept-Language, then your fallback) and Client Components hydrate with the same locale and the same catalog: no flash of untranslated text, no hydration mismatch. Each request gets its own instance (React cache()): no locale leaking between concurrent users.
Works with Turbopack (the Next 16 default) and webpack: the config wrapper generates the runtime module as real files and wires the t-template compiler as a loader for both.
🚀 Install
pnpm add verbaly @verbaly/next @verbaly/react⚡ Wire it up
1. The config wrapper, the whole build setup:
// next.config.ts
import { withVerbaly } from '@verbaly/next';
export default withVerbaly({/* your Next config */});Locales live in your verbaly.config (created by npx verbaly init), or inline:
export default withVerbaly({}, { locales: ['en', 'es', 'pt'] });2. The provider: root layout, Server Component:
// app/layout.tsx
import { getRequestLocale, getVerbalyProps } from '@verbaly/next/server';
import { VerbalyProvider } from '@verbaly/next/client';
import { localeDirection } from 'verbaly';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const locale = await getRequestLocale();
const props = await getVerbalyProps();
return (
<html lang={locale} dir={localeDirection(locale)}>
<body>
<VerbalyProvider {...props}>{children}</VerbalyProvider>
</body>
</html>
);
}3. Write text: Server Components use getT():
import { getT } from '@verbaly/next/server';
export default async function Page() {
const t = await getT();
return <h1>{t`Welcome back`}</h1>;
}Client Components use the React bindings (re-exported from @verbaly/next/client):
'use client';
import { useT } from '@verbaly/next/client';
export function Counter() {
const t = useT();
return <p>{t`You have new messages`}</p>;
}4. Switching languages: persists the cookie the server reads and re-renders Server Components:
'use client';
import { useSwitchLocale } from '@verbaly/next/client';
export function LocalePicker() {
const switchLocale = useSwitchLocale();
return <button onClick={() => void switchLocale('es')}>Español</button>;
}5. If your URLs carry the language (app/[locale]/…), hand Verbaly the segment. This is what
keeps the route statically rendered, because the alternative is reading request headers:
// app/[locale]/layout.tsx
import { setRequestLocale, getVerbalyProps, getAlternates } from '@verbaly/next/server';
import { locales } from 'virtual:verbaly';
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
return { alternates: getAlternates({ path: `/${locale}`, baseUrl: 'https://example.com' }) };
}
export default async function LocaleLayout({ children, params }) {
const { locale } = await params;
setRequestLocale(locale); // before any getT() in this tree
return <VerbalyProvider {...await getVerbalyProps()}>{children}</VerbalyProvider>;
}Without setRequestLocale, Verbaly negotiates from the cookie and Accept-Language, and that read
is what makes the route dynamic. With it, no header is read at all.
That's it. next dev extracts your messages live (catalogs + verbaly.d.ts stay fresh); next build blocks on missing translations.
📖 Options
Second argument of withVerbaly: every verbaly.config option, plus:
| Option | What it does |
| --------------- | ------------------------------------------------------------------------------------------------------- |
| cookie | Cookie read/written for the user's choice (default verbaly-locale); false = Accept-Language only. |
| fallback | Locale when nothing matches (defaults to the source locale). |
| failOnMissing | false opts out of the build gate. |
- Negotiation reads
headers()/cookies(), which makes a route dynamic. CallsetRequestLocalewith your[locale]segment and that read never happens, so the route prerenders. For a fully static site with no server at all,verbaly rendermirrors the output per locale. getAlternates({ path, baseUrl? })returns{ canonical, languages }forgenerateMetadata, the same hreflang set the static mirror writes. It is empty underno-prefixrouting, where one URL answers every language.- The generated
.verbaly/directory is build output (it ships its own.gitignore).
📚 Docs
Full guide: verbaly-web.vercel.app/docs/frameworks/react#next
License
MIT © Aron Soto
