Next.js adapter — @forjio/locavello-next
npm i @forjio/locavello-next
The runtime for SDK-mode projects. It reads catalogs you committed
— it makes no network calls and has no dependency on the Locavello
service. ICU MessageFormat formatting, per-locale fallback chains, and
typed keys via the CLI-generated locavello.d.ts.
Entry points
| Import | Contents | Use from |
|---|---|---|
@forjio/locavello-next |
createLocavello, LocavelloProvider, useT |
Client components (the hook wiring lives behind a 'use client' boundary) |
@forjio/locavello-next/server |
createLocavello only — pure functions, no React context |
React Server Components, route handlers, anything server-side |
Client components: Provider + useT
// app/[locale]/layout.tsx
import { LocavelloProvider } from '@forjio/locavello-next';
import en from '@/messages/en.json';
import id from '@/messages/id.json';
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return (
<LocavelloProvider locale={locale} catalogs={{ en, id }} sourceLocale="en">
{children}
</LocavelloProvider>
);
}
'use client';
import { useT } from '@forjio/locavello-next';
export function Cart({ count }: { count: number }) {
const t = useT();
return <p>{t('cart.items', { count })}</p>;
}
LocavelloProvider props:
| Prop | Required | Default | Meaning |
|---|---|---|---|
locale |
yes | — | Active locale |
catalogs |
yes | — | { locale: { key: icuMessage } } — import your pulled messages/*.json |
sourceLocale |
no | 'en' |
The locale your source strings are written in |
fallbacks |
no | — | { locale: fallbackLocale }, e.g. { 'pt-BR': 'pt' } — mirror of messages/_meta.json's fallbacks |
useT() outside a provider does not crash: it warns once in the
console and returns a passthrough t that renders keys as themselves.
Server components: createLocavello
// lib/i18n.ts
import { createLocavello } from '@forjio/locavello-next/server';
import en from '@/messages/en.json';
import id from '@/messages/id.json';
export const { getT } = createLocavello({
catalogs: { en, id },
sourceLocale: 'en',
});
// app/[locale]/page.tsx (RSC)
import { getT } from '@/lib/i18n';
export default async function Page({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const t = getT((await params).locale);
return <h1>{t('Welcome back')}</h1>;
}
Create the instance once in a shared module and call getT(locale) per
request or page.
Resolution order
For t(key) under locale L:
catalogs[L][key]- the fallback chain:
fallbacks[L], then its fallback, and so on (cycles are guarded) catalogs[sourceLocale][key]- the key itself — which in source-text-as-key mode is your English copy
Empty-string catalog entries are treated as missing, so they fall
through instead of blanking UI. t() never returns empty and never
throws.
ICU formatting
Messages are ICU MessageFormat — {name} arguments, {n, number},
{count, plural, …}, {x, select, …}:
t('cart.items', { count: 2 }); // '{count, plural, one {# item} other {# items}}'
Failure behavior is fail-open at every layer:
- malformed ICU in a message → the raw message is returned (and the parse failure is remembered, not retried per render)
- a formatting error (for example a missing value) → the raw message is returned
Formatters are memoized per (locale, key). ICU apostrophe escaping
applies ('' renders as ').
Typed keys
locavello pull generates locavello.d.ts in your repo root:
// Generated by `locavello pull` — do not edit by hand.
export type LocavelloKey =
| 'Create link'
| 'cart.items';
declare module '@forjio/locavello-next' {
interface RegisteredKeys {
keys: LocavelloKey;
}
}
The declaration merge narrows every t() key parameter to your
project's real keys — autocomplete works and typos fail type-check.
Without the file, keys are plain strings and the SDK works untyped out
of the box.