Framework
React
The official React 18/19 package for Verbaly. Hooks over the same reactive core, a compiler-aware <Trans> for rich text, and keys + params fully typed.
What you can accomplish:
- Translate with hooks (useT, useLocale) that re-render exactly when the locale changes
- Write rich text in place: the compiler extracts
<Trans>children like anyt`…` - Keep keys and params fully typed, with autocompletion from your own messages
- Concurrent-safe by construction: subscriptions ride on
useSyncExternalStore
Installation
Install the package with your preferred package manager.
pnpm add @verbaly/reactSetup & hooks
import { VerbalyProvider, useT, useLocale } from '@verbaly/react';
import { verbaly } from 'virtual:verbaly';
<VerbalyProvider instance={verbaly}>
<App />
</VerbalyProvider>
function Inbox() {
const t = useT();
const [locale, setLocale] = useLocale();
return <p>{t('inbox', { count: 3 })}</p>;
}- Re-renders via
useSyncExternalStore: concurrent-safe, SSR-friendly. useVerbaly()exposes the full instance when you need it.
Rich text with <Trans>
Write the source text in place, and since 0.6.0 the compiler extracts it, exactly like t`…`:
import { Trans } from '@verbaly/react';
// you write:
<Trans>Read the <a href='/terms'>terms</a> before continuing</Trans>
// the compiler extracts "Read the <a>terms</a> before continuing" and rewrites to:
<Trans id='x7Ka9q2f' components={{ a: <a href='/terms' /> }} />- Child attributes are preserved; params (
{count}) becomevalues; nesting works; unknown tags fall back to inner text. - Whitelisted tags render for real (
0.23.0):<em>,<code>and friends in a message become real elements, the same safe whitelist as the DOM interpreter.richTagsoverrides the list; an empty list restores the old flatten-to-text behavior. - Readable keys (
0.8.0): write<Trans id="inbox.title">…</Trans>with children and it extracts under that id instead of a hash. - Runtime-first still works: pass
idwithout children (or with your ownvalues/components) and the compiler leaves the element untouched. - JSX whitespace rules apply: a line break between an element and text renders no space; use the
{' '}idiom, extraction stays faithful to what React renders. - Named links (
0.11.0): thelinksprop maps tag names to hrefs:<docs>…</docs>renders as a real<a>without a custom component.componentswins overlinkson the same name;javascript:hrefs are blocked. - No provider? Pass the
instanceprop (0.23.0) and<Trans>renders straight from it, re-rendering on every locale switch.
Next.js
Server Components render already translated in the visitor's language, and Client Components hydrate with the same locale and catalog: no flash of untranslated text, no hydration mismatch.
- Every page renders in the visitor's language, chosen per request from their cookie or
Accept-Languageheader - No flash of untranslated text: the HTML the server sends and the page after hydration are identical, catalog included
- Each request gets its own instance, so simultaneous visitors never see each other's language
- Turbopack and webpack, both first-class: the
tcompiler runs as a loader on either bundler
Next.js needs the integration (@verbaly/next) alongside the React bindings:
pnpm add verbaly @verbaly/next @verbaly/reactWire it up
Wrap your config with withVerbaly. It wires live extraction in dev, the message compiler on Turbopack and webpack, and blocks next build on missing translations:
import { withVerbaly } from '@verbaly/next';
export default withVerbaly({ /* your Next config */ });
// locales live in your verbaly.config (npx verbaly init), or inline:
export default withVerbaly({}, { locales: ['en', 'es', 'pt'] });In the root layout, getVerbalyProps serializes the negotiated locale and its catalog, and VerbalyProvider seeds the client from them:
import { getRequestLocale, getVerbalyProps } from '@verbaly/next/server';
import { VerbalyProvider } from '@verbaly/next/client';
export default async function RootLayout({ children }) {
const locale = await getRequestLocale();
const props = await getVerbalyProps();
return (
<html lang={locale}>
<body>
<VerbalyProvider {...props}>{children}</VerbalyProvider>
</body>
</html>
);
}Server and Client Components
Server Components get a request-scoped t with await getT(), negotiated once per request (cookie, then Accept-Language, then your fallback):
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>;
}Switching languages
useSwitchLocale switches the client live, writes the cookie the server reads, and re-renders Server Components through router.refresh():
'use client';
import { useSwitchLocale } from '@verbaly/next/client';
export function LangPicker() {
const switchLocale = useSwitchLocale();
return <button onClick={() => switchLocale('es')}>Español</button>;
}- The cookie defaults to
verbaly-locale(same identity as the core's storage key);cookie: falsein the options disables it, for pureAccept-Language. fallbackdefaults to your source locale; negotiation understands q-values and narrows regions (es-PEmatcheses).next buildblocks on missing translations with the exact keys to fill;failOnMissing: falseopts out.- Negotiation reads request headers, so negotiated pages render dynamically. Fully static site? Prefer pre-translated output per locale with verbaly render.