@verbumia/react-i18next
v0.9.0
Published
React SDK for Verbumia — translations + realtime missing-key handler.
Downloads
1,810
Maintainers
Readme
@verbumia/react-i18next
The React SDK for Verbumia. Resolve translations from the Verbumia CDN, fall back gracefully when a key is missing, and stream those missing keys back to your dashboard in real time so the team can fill them without redeploying.
npm install @verbumia/react-i18next- ✦ Zero-config CDN fetch (Bunny.net edge)
- ✦ Built-in missing-key handler with first-paint anti-spam gate
- ✦ Pluggable transport for Storybook / inspectors
- ✦ < 10 KB ESM (gzipped much smaller), tree-shakeable
- ✦ Plain
t()+<Trans>semantics — drop-in for most i18next codebases
Quickstart
import { VerbumiaProvider, useTranslation } from "@verbumia/react-i18next";
export function App() {
return (
<VerbumiaProvider
token={import.meta.env.VITE_VERBUMIA_TOKEN}
projectUuid={import.meta.env.VITE_VERBUMIA_PROJECT}
defaultLocale="fr"
fallbackLng="en"
namespaces={["common"]}
>
<Hello />
</VerbumiaProvider>
);
}
function Hello() {
const { t, i18n } = useTranslation("common");
if (!i18n.ready) return <span>Loading…</span>;
return <h1>{t("hello.title", { name: "Marc", defaultValue: "Hello {{name}}" })}</h1>;
}The token is the API key minted in Org Settings → API Keys. For the
browser SDK use a project-scoped key with the missing:write scope and
nothing else — that key only sees missing-key writes for one project, which
is the safest exposure profile.
API surface
VerbumiaProvider
interface VerbumiaConfig {
token: string; // vrb_live_<prefix>.<secret>
projectUuid: string;
defaultLocale: string; // BCP-47 (e.g. "fr", "fr-CA")
fallbackLng?: string; // resolved before reporting a key as missing
namespaces?: string[]; // default ['common']
defaultNS?: string; // alias: default namespace for single-ns apps
apiBase?: string; // default 'https://api.verbumia.dev'
cdnBase?: string; // default 'https://cdn.verbumia.ca'
version?: string; // version slug, default 'main' (in cache keys)
versionSlug?: string; // @deprecated alias of `version`
env?: 'prod' | 'dev'; // default 'prod' (drives fetch source)
plugins?: VerbumiaPlugin[]; // e.g. @verbumia/feedback, @verbumia/realtime
transport?: (batch: MissingKeyEvent[]) => void | Promise<void>;
missingHandler?: 'send' | 'log' | 'off'; // default 'send'
flushIntervalMs?: number; // default 5000
flushBatchSize?: number; // default 50
missingEventsBufferSize?: number; // default 200
}version selects which published version's bundles to load
(/p/<project>/<version>/latest/...); it defaults to 'main' and is part
of the SDK's bundle cache keys, so two providers with different version
values never share cached bundles. versionSlug is a deprecated alias
of version (if both are set, version wins).
Removed in 0.9.0: the
liveUpdates/centrifugoWsUrl/centrifugoTokenEndpointconfig keys. Realtime updates now live in the separate@verbumia/realtimeplugin (see Realtime updates). Passing any of those keys throws a clear migration error.
### `useTranslation(defaultNamespace?)`
Returns `{ t, i18n }`.
```ts
// Two call shapes — the native object form AND the react-i18next-style
// positional fallback (a string 2nd arg is the default value):
type TranslationFunction = {
(key: string, defaultValue: string, options?: Record<string, unknown>): string;
(key: string, options?: Record<string, unknown> & { defaultValue?: string }): string;
};
interface I18nInstance {
ready: boolean;
locale: string;
language: string; // alias of `locale`
setLocale(next: string): Promise<void>;
changeLanguage(next: string): Promise<void>; // alias of `setLocale`
t: TranslationFunction; // for out-of-React use via getI18n()
missingEvents: MissingKeyEvent[]; // newest first, capped buffer
flushMissing(): Promise<void>; // force-flush the pending batch
reload(opts?: { locale?: string; namespace?: string }): Promise<void>;
}i18n.reload(opts?)
Bust-refetches already-loaded bundles (bypassing the browser HTTP cache)
and re-renders. Without opts it refreshes every loaded (locale, ns)
bundle; pass { locale } and/or { namespace } to narrow. Returns once
all refetches settle. Useful for a manual "refresh translations" button,
and it's what @verbumia/realtime calls on a
translations_published push.
await getI18n().reload(); // refresh all
await getI18n().reload({ locale: "fr", namespace: "common" });<Trans>
Inline translation with JSX slots:
<Trans
i18nKey="cta.terms"
defaults="I accept the <0>terms</0> and <1>privacy policy</1>"
components={[<a href="/terms" />, <a href="/privacy" />]}
/>The <0>...</0> slots are 0-indexed into components. The bundle string
should follow the same shape so that translators see I accept the <0>terms</0>...
and the SDK swaps the elements at render time.
Migrating from react-i18next
@verbumia/react-i18next is built to be a near drop-in for react-i18next, so
existing codebases migrate with minimal changes:
Positional default value —
t('key', 'Default text')works (so doest('key', 'Hi {{name}}', { name })), alongside the nativet('key', { defaultValue }). No codemod needed for inline fallbacks.changeLanguage/language—i18n.changeLanguage('en')(alias ofsetLocale) and thei18n.languagegetter (alias oflocale) are available.Out-of-React access —
getI18n()returns the active instance for use in plain modules, stores, or helpers (the react-i18next standalone-singleton pattern):import { getI18n } from "@verbumia/react-i18next"; // anywhere after <VerbumiaProvider> has mounted: const label = getI18n().t("nav.home", "Home"); await getI18n().changeLanguage("en");getI18n()throws a clear error if no provider is mounted yet, and assumes a single app-wide provider.Default namespace — the default is
['common'](not react-i18next's'translation'). Migrants passnamespaces={['translation']}, or thedefaultNS="translation"alias for single-namespace apps.
Not yet supported (planned for V1.1)
Plurals and context are not resolved yet: t('key', { count }) performs
interpolation only — it does not select plural keys (key_one /
key_other) or context keys (key_male). Handle these manually until V1.1.
Missing-key flow
- The user navigates a page that calls
t("hello.title"). - The bundle for
(locale, namespace)was already fetched but doesn't containhello.title. (i18n.ready === trueand the bundle for that tuple is in the "attempted" set — this is the gate.) - The SDK enqueues a
MissingKeyEvent, dedups it within the instance, and pushes it into themissingEventsring buffer. - Every
flushIntervalMs(default 5s) — or sooner if the batch hitsflushBatchSize(default 50) — the SDK flushes the pending batch via the transport.
interface MissingKeyEvent {
key: string;
namespace: string;
language_code: string;
source_value?: string;
sdk_meta?: Record<string, unknown>; // SDK adds {lib, ver, url} automatically
}Why the gate matters
Without the gate, every t("…") call between mount and bundle resolution
would report a "missing" key — which is a lie (the bundle just hadn't
arrived yet). The first-paint flood would poison your dashboard. The SDK
holds reports until both:
i18n.ready === true(initial bundles loaded), AND- the specific
(locale, namespace)bundle was actually fetched.
You can see the gate in action with i18n.missingEvents — it stays empty
until the network round-trip completes.
Custom transport
Replace the default POST with anything — Storybook mock, in-app inspector, Cypress capture:
<VerbumiaProvider
{...config}
transport={(batch) => {
window.parent.postMessage({ type: "verbumia:missing", batch }, "*");
}}
>
...
</VerbumiaProvider>The default delivery path is also exported if you need to wrap it:
import { defaultTransport, logTransport } from "@verbumia/react-i18next";Realtime updates
Zero-deploy translation updates (subscribe to the project's Centrifugo
translations: channel and bust-refetch on publish) live in the separate
@verbumia/realtime package — added as a plugin of this
provider, not configured here:
import { VerbumiaProvider } from "@verbumia/react-i18next";
import { verbumiaRealtime } from "@verbumia/realtime/react";
<VerbumiaProvider
{...config}
env="dev"
plugins={[
verbumiaRealtime({ wsUrl: "wss://centrifugo.verbumia.ca/connection/websocket" }),
]}
>
<App />
</VerbumiaProvider>;Under the hood the plugin calls i18n.reload(...) on
each translations_published push. Realtime is a dev-version-only feature
(it only subscribes when env: "dev").
The
liveUpdates/centrifugoWsUrl/centrifugoTokenEndpointconfig keys were removed in 0.9.0. Install@verbumia/realtimeand use the plugin instead.
Recipes
Next.js (App Router)
Wrap the SDK in a Client Component and feed it env vars from .env.local:
// app/(verbumia)/i18n-client.tsx
"use client";
import { VerbumiaProvider } from "@verbumia/react-i18next";
export function I18nClient({ children }: { children: React.ReactNode }) {
return (
<VerbumiaProvider
token={process.env.NEXT_PUBLIC_VERBUMIA_TOKEN!}
projectUuid={process.env.NEXT_PUBLIC_VERBUMIA_PROJECT!}
defaultLocale="fr"
fallbackLng="en"
>
{children}
</VerbumiaProvider>
);
}The provider reads the bundle via the public CDN — no server-side state to
hydrate. SSR pre-renders the defaultValue and the client smoothly
upgrades after i18n.ready flips.
Storybook
// .storybook/preview.tsx
import { VerbumiaProvider } from "@verbumia/react-i18next";
export const decorators = [
(Story) => (
<VerbumiaProvider
token="vrb_live_storybook.fake"
projectUuid="storybook"
defaultLocale="fr"
missingHandler="log"
transport={(batch) => action("missing-keys")(batch)}
>
<Story />
</VerbumiaProvider>
),
];Cypress
cy.intercept("POST", "**/v1/missing", (req) => {
cy.task("captureMissing", req.body);
req.reply({ accepted: req.body.events.length, rejected: 0, items: [] });
});Versioning
Semver. V1.x will keep the public API stable. Internal changes (bundle fetcher, dedup heuristics) may shift in patch releases.
Breaking changes pre-V1 are flagged in CONTRACT.md.
License
MIT — see LICENSE.
