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:

  1. catalogs[L][key]
  2. the fallback chain: fallbacks[L], then its fallback, and so on (cycles are guarded)
  3. catalogs[sourceLocale][key]
  4. 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.