@verbumia/react-i18next 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -177,6 +177,23 @@ var DEFAULT_FLUSH_MS = 5e3;
177
177
  var DEFAULT_BATCH = 50;
178
178
  var DEFAULT_BUFFER = 200;
179
179
  var DEFAULT_VERSION_SLUG = "main";
180
+ var CLDR_CATEGORIES = /* @__PURE__ */ new Set([
181
+ "zero",
182
+ "one",
183
+ "two",
184
+ "few",
185
+ "many",
186
+ "other"
187
+ ]);
188
+ function isPluralForms(v) {
189
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
190
+ const keys = Object.keys(v);
191
+ if (keys.length === 0) return false;
192
+ if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;
193
+ return keys.every(
194
+ (k) => typeof v[k] === "string"
195
+ );
196
+ }
180
197
  function resolve(bundle, key) {
181
198
  if (!bundle) return void 0;
182
199
  const parts = key.split(".");
@@ -188,7 +205,22 @@ function resolve(bundle, key) {
188
205
  return void 0;
189
206
  }
190
207
  }
191
- return typeof cur === "string" ? cur : void 0;
208
+ if (typeof cur === "string") return cur;
209
+ if (isPluralForms(cur)) return cur;
210
+ return void 0;
211
+ }
212
+ function selectPluralForm(forms, count, locale) {
213
+ let category = "other";
214
+ try {
215
+ if (typeof Intl !== "undefined" && typeof Intl.PluralRules === "function") {
216
+ category = new Intl.PluralRules(locale).select(count);
217
+ }
218
+ } catch {
219
+ }
220
+ if (category in forms) return forms[category];
221
+ if ("other" in forms) return forms["other"];
222
+ const first = Object.keys(forms)[0];
223
+ return first ? forms[first] : "";
192
224
  }
193
225
  function interpolate(template, options) {
194
226
  if (!options) return template;
@@ -214,6 +246,11 @@ var VerbumiaI18n = class {
214
246
  _timer = null;
215
247
  _listeners = /* @__PURE__ */ new Set();
216
248
  _live = null;
249
+ // Stable snapshot reference for useSyncExternalStore. Returning a fresh
250
+ // object on each getSnapshot call would loop React forever — we rebuild
251
+ // it ONLY in _notify (when state actually changed) and return the cached
252
+ // reference between notifications.
253
+ _snapshot;
217
254
  constructor(config) {
218
255
  this.locale = config.defaultLocale;
219
256
  this.fallbackLng = config.fallbackLng;
@@ -237,13 +274,27 @@ var VerbumiaI18n = class {
237
274
  token: this._config.token,
238
275
  projectUuid: this._config.projectUuid
239
276
  }));
277
+ this._snapshot = this._buildSnapshot();
240
278
  }
241
279
  // ---- React subscription ----
242
280
  subscribe = (listener) => {
243
281
  this._listeners.add(listener);
244
282
  return () => this._listeners.delete(listener);
245
283
  };
284
+ /** Stable snapshot accessor for useSyncExternalStore. The returned
285
+ * object reference is identical between renders unless _notify fired. */
286
+ getSnapshot = () => this._snapshot;
287
+ _buildSnapshot() {
288
+ return {
289
+ ready: this.ready,
290
+ locale: this.locale,
291
+ setLocale: this.setLocale,
292
+ missingEvents: this.missingEvents,
293
+ flushMissing: this.flushMissing
294
+ };
295
+ }
246
296
  _notify() {
297
+ this._snapshot = this._buildSnapshot();
247
298
  for (const l of this._listeners) l();
248
299
  }
249
300
  // ---- Lifecycle ----
@@ -359,10 +410,14 @@ var VerbumiaI18n = class {
359
410
  const bareKey = namespace.bareKey;
360
411
  const ns = namespace.ns;
361
412
  const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);
362
- if (fromActive != null) return interpolate(fromActive, options);
413
+ if (fromActive != null) {
414
+ return this._render(fromActive, this.locale, options);
415
+ }
363
416
  if (this.fallbackLng && this.fallbackLng !== this.locale) {
364
417
  const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);
365
- if (fb != null) return interpolate(fb, options);
418
+ if (fb != null) {
419
+ return this._render(fb, this.fallbackLng, options);
420
+ }
366
421
  }
367
422
  if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {
368
423
  this._reportMissing({
@@ -389,6 +444,20 @@ var VerbumiaI18n = class {
389
444
  }
390
445
  };
391
446
  // ---- Internals ----
447
+ /**
448
+ * Final-stage render: pick the right plural form (when value is a CLDR
449
+ * dict and `options.count` is a number) then interpolate `{{var}}`.
450
+ */
451
+ _render(value, locale, options) {
452
+ let str;
453
+ if (typeof value === "string") {
454
+ str = value;
455
+ } else {
456
+ const count = typeof options?.count === "number" ? options.count : 0;
457
+ str = selectPluralForm(value, count, locale);
458
+ }
459
+ return interpolate(str, options);
460
+ }
392
461
  _splitNamespace(key) {
393
462
  const idx = key.indexOf(":");
394
463
  if (idx > 0) {
@@ -460,23 +529,7 @@ function useI18n() {
460
529
  }
461
530
  function useI18nSnapshot() {
462
531
  const i18n = useI18n();
463
- return (0, import_react.useSyncExternalStore)(
464
- i18n.subscribe,
465
- () => ({
466
- ready: i18n.ready,
467
- locale: i18n.locale,
468
- setLocale: i18n.setLocale,
469
- missingEvents: i18n.missingEvents,
470
- flushMissing: i18n.flushMissing
471
- }),
472
- () => ({
473
- ready: false,
474
- locale: i18n.locale,
475
- setLocale: i18n.setLocale,
476
- missingEvents: [],
477
- flushMissing: i18n.flushMissing
478
- })
479
- );
532
+ return (0, import_react.useSyncExternalStore)(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);
480
533
  }
481
534
 
482
535
  // src/hooks.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["export { VerbumiaProvider } from \"./provider\";\nexport { useTranslation } from \"./hooks\";\nexport { Trans } from \"./trans\";\nexport type {\n I18nInstance,\n Locale,\n MissingHandlerMode,\n MissingKeyEvent,\n Namespace,\n TranslationFunction,\n TranslationOptions,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nexport { defaultTransport, logTransport } from \"./transport\";\n","import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(\n i18n.subscribe,\n () => ({\n ready: i18n.ready,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: i18n.missingEvents,\n flushMissing: i18n.flushMissing,\n }),\n () => ({\n ready: false,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: [],\n flushMissing: i18n.flushMissing,\n })\n );\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.1.0\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n const ws = new WebSocket(this.cfg.url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: { id?: number; connect?: unknown; subscribe?: unknown; push?: { channel?: string; pub?: { data?: unknown } } } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { LiveClient, fetchCentrifugoToken } from \"./live\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\n/** Resolve a dotted key against a deeply-nested bundle. */\nfunction resolve(bundle: Bundle | undefined, key: string): string | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n return typeof cur === \"string\" ? cur : undefined;\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n private _config: Required<\n Pick<VerbumiaConfig, \"apiBase\" | \"cdnBase\" | \"missingHandler\">\n > & {\n token: string;\n projectUuid: string;\n namespaces: string[];\n flushIntervalMs: number;\n flushBatchSize: number;\n missingEventsBufferSize: number;\n versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: string;\n };\n\n private _transport: Transport;\n private _pending: MissingKeyEvent[] = [];\n private _seen = new Set<string>(); // dedup `${locale}/${ns}/${key}` per-flush\n private _timer: ReturnType<typeof setInterval> | null = null;\n private _listeners = new Set<Listener>();\n private _live: LiveClient | null = null;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n private _notify(): void {\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n if (this._config.liveUpdates) {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n void this._loadBundle(lang, ns, fetchImpl).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) return interpolate(fromActive, options);\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) return interpolate(fb, options);\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: typeof options?.defaultValue === \"string\" ? options.defaultValue : undefined,\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch\n ): Promise<void> {\n const url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.versionSlug}/latest/${locale}/${ns}.json`;\n try {\n const r = await fetchImpl(url, { method: \"GET\", credentials: \"omit\" });\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(`${locale}/${ns}`, data);\n } else {\n // 404 = no published bundle yet — empty bundle is fine, missing keys flow handles it.\n this._bundles.set(`${locale}/${ns}`, {});\n }\n } catch {\n this._bundles.set(`${locale}/${ns}`, {});\n } finally {\n this._attempted.add(`${locale}/${ns}`);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["export { VerbumiaProvider } from \"./provider\";\nexport { useTranslation } from \"./hooks\";\nexport { Trans } from \"./trans\";\nexport type {\n I18nInstance,\n Locale,\n MissingHandlerMode,\n MissingKeyEvent,\n Namespace,\n TranslationFunction,\n TranslationOptions,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nexport { defaultTransport, logTransport } from \"./transport\";\n","import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render.\n *\n * `getSnapshot` MUST return a stable reference between notifications,\n * otherwise React loops forever (Maximum update depth exceeded). The\n * VerbumiaI18n instance caches its snapshot internally — see\n * `_notify` / `_buildSnapshot`. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.1.0\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n const ws = new WebSocket(this.cfg.url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: { id?: number; connect?: unknown; subscribe?: unknown; push?: { channel?: string; pub?: { data?: unknown } } } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { LiveClient, fetchCentrifugoToken } from \"./live\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\ntype PluralForms = Record<string, string>;\ntype ResolvedValue = string | PluralForms;\n\n/**\n * Plural-form objects mirror the CLDR `Intl.PluralRules` categories. Treat\n * any object whose keys overlap the CLDR set AND whose values are all\n * strings as a plural object — the chunky type guard keeps stray nested\n * namespaces from being misread.\n */\nconst CLDR_CATEGORIES = new Set<string>([\n \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n]);\n\nfunction isPluralForms(v: unknown): v is PluralForms {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return false;\n const keys = Object.keys(v as object);\n if (keys.length === 0) return false;\n if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;\n return keys.every(\n (k) => typeof (v as Record<string, unknown>)[k] === \"string\",\n );\n}\n\n/** Resolve a dotted key against a deeply-nested bundle. Returns either a\n * plain string OR a CLDR plural-forms dict so the caller can pick a form. */\nfunction resolve(bundle: Bundle | undefined, key: string): ResolvedValue | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n if (typeof cur === \"string\") return cur;\n if (isPluralForms(cur)) return cur;\n return undefined;\n}\n\n/**\n * Pick the right CLDR form for `count` against the active locale's plural\n * rules. Falls back to `other` (always required by the contract) and then\n * the first available form so we never render nothing for a configured key.\n */\nfunction selectPluralForm(\n forms: PluralForms,\n count: number,\n locale: string,\n): string {\n let category: string = \"other\";\n try {\n if (typeof Intl !== \"undefined\" && typeof Intl.PluralRules === \"function\") {\n category = new Intl.PluralRules(locale).select(count);\n }\n } catch {\n // Bad locale tag — fall through to \"other\".\n }\n if (category in forms) return forms[category]!;\n if (\"other\" in forms) return forms[\"other\"]!;\n const first = Object.keys(forms)[0];\n return first ? forms[first]! : \"\";\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n private _config: Required<\n Pick<VerbumiaConfig, \"apiBase\" | \"cdnBase\" | \"missingHandler\">\n > & {\n token: string;\n projectUuid: string;\n namespaces: string[];\n flushIntervalMs: number;\n flushBatchSize: number;\n missingEventsBufferSize: number;\n versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: string;\n };\n\n private _transport: Transport;\n private _pending: MissingKeyEvent[] = [];\n private _seen = new Set<string>(); // dedup `${locale}/${ns}/${key}` per-flush\n private _timer: ReturnType<typeof setInterval> | null = null;\n private _listeners = new Set<Listener>();\n private _live: LiveClient | null = null;\n // Stable snapshot reference for useSyncExternalStore. Returning a fresh\n // object on each getSnapshot call would loop React forever — we rebuild\n // it ONLY in _notify (when state actually changed) and return the cached\n // reference between notifications.\n private _snapshot!: I18nInstance;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n this._snapshot = this._buildSnapshot();\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n /** Stable snapshot accessor for useSyncExternalStore. The returned\n * object reference is identical between renders unless _notify fired. */\n getSnapshot = (): I18nInstance => this._snapshot;\n\n private _buildSnapshot(): I18nInstance {\n return {\n ready: this.ready,\n locale: this.locale,\n setLocale: this.setLocale,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n };\n }\n\n private _notify(): void {\n this._snapshot = this._buildSnapshot();\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n if (this._config.liveUpdates) {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n void this._loadBundle(lang, ns, fetchImpl).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string; count?: number }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: typeof options?.defaultValue === \"string\" ? options.defaultValue : undefined,\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n /**\n * Final-stage render: pick the right plural form (when value is a CLDR\n * dict and `options.count` is a number) then interpolate `{{var}}`.\n */\n private _render(\n value: ResolvedValue,\n locale: Locale,\n options?: Record<string, unknown> & { count?: number },\n ): string {\n let str: string;\n if (typeof value === \"string\") {\n str = value;\n } else {\n const count = typeof options?.count === \"number\" ? options.count : 0;\n str = selectPluralForm(value, count, locale);\n }\n return interpolate(str, options);\n }\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch\n ): Promise<void> {\n const url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.versionSlug}/latest/${locale}/${ns}.json`;\n try {\n const r = await fetchImpl(url, { method: \"GET\", credentials: \"omit\" });\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(`${locale}/${ns}`, data);\n } else {\n // 404 = no published bundle yet — empty bundle is fine, missing keys flow handles it.\n this._bundles.set(`${locale}/${ns}`, {});\n }\n } catch {\n this._bundles.set(`${locale}/${ns}`, {});\n } finally {\n this._attempted.add(`${locale}/${ns}`);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime"]}
package/dist/index.js CHANGED
@@ -153,6 +153,23 @@ var DEFAULT_FLUSH_MS = 5e3;
153
153
  var DEFAULT_BATCH = 50;
154
154
  var DEFAULT_BUFFER = 200;
155
155
  var DEFAULT_VERSION_SLUG = "main";
156
+ var CLDR_CATEGORIES = /* @__PURE__ */ new Set([
157
+ "zero",
158
+ "one",
159
+ "two",
160
+ "few",
161
+ "many",
162
+ "other"
163
+ ]);
164
+ function isPluralForms(v) {
165
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
166
+ const keys = Object.keys(v);
167
+ if (keys.length === 0) return false;
168
+ if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;
169
+ return keys.every(
170
+ (k) => typeof v[k] === "string"
171
+ );
172
+ }
156
173
  function resolve(bundle, key) {
157
174
  if (!bundle) return void 0;
158
175
  const parts = key.split(".");
@@ -164,7 +181,22 @@ function resolve(bundle, key) {
164
181
  return void 0;
165
182
  }
166
183
  }
167
- return typeof cur === "string" ? cur : void 0;
184
+ if (typeof cur === "string") return cur;
185
+ if (isPluralForms(cur)) return cur;
186
+ return void 0;
187
+ }
188
+ function selectPluralForm(forms, count, locale) {
189
+ let category = "other";
190
+ try {
191
+ if (typeof Intl !== "undefined" && typeof Intl.PluralRules === "function") {
192
+ category = new Intl.PluralRules(locale).select(count);
193
+ }
194
+ } catch {
195
+ }
196
+ if (category in forms) return forms[category];
197
+ if ("other" in forms) return forms["other"];
198
+ const first = Object.keys(forms)[0];
199
+ return first ? forms[first] : "";
168
200
  }
169
201
  function interpolate(template, options) {
170
202
  if (!options) return template;
@@ -190,6 +222,11 @@ var VerbumiaI18n = class {
190
222
  _timer = null;
191
223
  _listeners = /* @__PURE__ */ new Set();
192
224
  _live = null;
225
+ // Stable snapshot reference for useSyncExternalStore. Returning a fresh
226
+ // object on each getSnapshot call would loop React forever — we rebuild
227
+ // it ONLY in _notify (when state actually changed) and return the cached
228
+ // reference between notifications.
229
+ _snapshot;
193
230
  constructor(config) {
194
231
  this.locale = config.defaultLocale;
195
232
  this.fallbackLng = config.fallbackLng;
@@ -213,13 +250,27 @@ var VerbumiaI18n = class {
213
250
  token: this._config.token,
214
251
  projectUuid: this._config.projectUuid
215
252
  }));
253
+ this._snapshot = this._buildSnapshot();
216
254
  }
217
255
  // ---- React subscription ----
218
256
  subscribe = (listener) => {
219
257
  this._listeners.add(listener);
220
258
  return () => this._listeners.delete(listener);
221
259
  };
260
+ /** Stable snapshot accessor for useSyncExternalStore. The returned
261
+ * object reference is identical between renders unless _notify fired. */
262
+ getSnapshot = () => this._snapshot;
263
+ _buildSnapshot() {
264
+ return {
265
+ ready: this.ready,
266
+ locale: this.locale,
267
+ setLocale: this.setLocale,
268
+ missingEvents: this.missingEvents,
269
+ flushMissing: this.flushMissing
270
+ };
271
+ }
222
272
  _notify() {
273
+ this._snapshot = this._buildSnapshot();
223
274
  for (const l of this._listeners) l();
224
275
  }
225
276
  // ---- Lifecycle ----
@@ -335,10 +386,14 @@ var VerbumiaI18n = class {
335
386
  const bareKey = namespace.bareKey;
336
387
  const ns = namespace.ns;
337
388
  const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);
338
- if (fromActive != null) return interpolate(fromActive, options);
389
+ if (fromActive != null) {
390
+ return this._render(fromActive, this.locale, options);
391
+ }
339
392
  if (this.fallbackLng && this.fallbackLng !== this.locale) {
340
393
  const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);
341
- if (fb != null) return interpolate(fb, options);
394
+ if (fb != null) {
395
+ return this._render(fb, this.fallbackLng, options);
396
+ }
342
397
  }
343
398
  if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {
344
399
  this._reportMissing({
@@ -365,6 +420,20 @@ var VerbumiaI18n = class {
365
420
  }
366
421
  };
367
422
  // ---- Internals ----
423
+ /**
424
+ * Final-stage render: pick the right plural form (when value is a CLDR
425
+ * dict and `options.count` is a number) then interpolate `{{var}}`.
426
+ */
427
+ _render(value, locale, options) {
428
+ let str;
429
+ if (typeof value === "string") {
430
+ str = value;
431
+ } else {
432
+ const count = typeof options?.count === "number" ? options.count : 0;
433
+ str = selectPluralForm(value, count, locale);
434
+ }
435
+ return interpolate(str, options);
436
+ }
368
437
  _splitNamespace(key) {
369
438
  const idx = key.indexOf(":");
370
439
  if (idx > 0) {
@@ -436,23 +505,7 @@ function useI18n() {
436
505
  }
437
506
  function useI18nSnapshot() {
438
507
  const i18n = useI18n();
439
- return useSyncExternalStore(
440
- i18n.subscribe,
441
- () => ({
442
- ready: i18n.ready,
443
- locale: i18n.locale,
444
- setLocale: i18n.setLocale,
445
- missingEvents: i18n.missingEvents,
446
- flushMissing: i18n.flushMissing
447
- }),
448
- () => ({
449
- ready: false,
450
- locale: i18n.locale,
451
- setLocale: i18n.setLocale,
452
- missingEvents: [],
453
- flushMissing: i18n.flushMissing
454
- })
455
- );
508
+ return useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);
456
509
  }
457
510
 
458
511
  // src/hooks.ts
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(\n i18n.subscribe,\n () => ({\n ready: i18n.ready,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: i18n.missingEvents,\n flushMissing: i18n.flushMissing,\n }),\n () => ({\n ready: false,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: [],\n flushMissing: i18n.flushMissing,\n })\n );\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.1.0\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n const ws = new WebSocket(this.cfg.url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: { id?: number; connect?: unknown; subscribe?: unknown; push?: { channel?: string; pub?: { data?: unknown } } } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { LiveClient, fetchCentrifugoToken } from \"./live\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\n/** Resolve a dotted key against a deeply-nested bundle. */\nfunction resolve(bundle: Bundle | undefined, key: string): string | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n return typeof cur === \"string\" ? cur : undefined;\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n private _config: Required<\n Pick<VerbumiaConfig, \"apiBase\" | \"cdnBase\" | \"missingHandler\">\n > & {\n token: string;\n projectUuid: string;\n namespaces: string[];\n flushIntervalMs: number;\n flushBatchSize: number;\n missingEventsBufferSize: number;\n versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: string;\n };\n\n private _transport: Transport;\n private _pending: MissingKeyEvent[] = [];\n private _seen = new Set<string>(); // dedup `${locale}/${ns}/${key}` per-flush\n private _timer: ReturnType<typeof setInterval> | null = null;\n private _listeners = new Set<Listener>();\n private _live: LiveClient | null = null;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n private _notify(): void {\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n if (this._config.liveUpdates) {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n void this._loadBundle(lang, ns, fetchImpl).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) return interpolate(fromActive, options);\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) return interpolate(fb, options);\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: typeof options?.defaultValue === \"string\" ? options.defaultValue : undefined,\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch\n ): Promise<void> {\n const url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.versionSlug}/latest/${locale}/${ns}.json`;\n try {\n const r = await fetchImpl(url, { method: \"GET\", credentials: \"omit\" });\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(`${locale}/${ns}`, data);\n } else {\n // 404 = no published bundle yet — empty bundle is fine, missing keys flow handles it.\n this._bundles.set(`${locale}/${ns}`, {});\n }\n } catch {\n this._bundles.set(`${locale}/${ns}`, {});\n } finally {\n this._attempted.add(`${locale}/${ns}`);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["useMemo","useMemo","jsx"]}
1
+ {"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render.\n *\n * `getSnapshot` MUST return a stable reference between notifications,\n * otherwise React loops forever (Maximum update depth exceeded). The\n * VerbumiaI18n instance caches its snapshot internally — see\n * `_notify` / `_buildSnapshot`. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.1.0\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n const ws = new WebSocket(this.cfg.url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: { id?: number; connect?: unknown; subscribe?: unknown; push?: { channel?: string; pub?: { data?: unknown } } } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { LiveClient, fetchCentrifugoToken } from \"./live\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\ntype PluralForms = Record<string, string>;\ntype ResolvedValue = string | PluralForms;\n\n/**\n * Plural-form objects mirror the CLDR `Intl.PluralRules` categories. Treat\n * any object whose keys overlap the CLDR set AND whose values are all\n * strings as a plural object — the chunky type guard keeps stray nested\n * namespaces from being misread.\n */\nconst CLDR_CATEGORIES = new Set<string>([\n \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n]);\n\nfunction isPluralForms(v: unknown): v is PluralForms {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return false;\n const keys = Object.keys(v as object);\n if (keys.length === 0) return false;\n if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;\n return keys.every(\n (k) => typeof (v as Record<string, unknown>)[k] === \"string\",\n );\n}\n\n/** Resolve a dotted key against a deeply-nested bundle. Returns either a\n * plain string OR a CLDR plural-forms dict so the caller can pick a form. */\nfunction resolve(bundle: Bundle | undefined, key: string): ResolvedValue | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n if (typeof cur === \"string\") return cur;\n if (isPluralForms(cur)) return cur;\n return undefined;\n}\n\n/**\n * Pick the right CLDR form for `count` against the active locale's plural\n * rules. Falls back to `other` (always required by the contract) and then\n * the first available form so we never render nothing for a configured key.\n */\nfunction selectPluralForm(\n forms: PluralForms,\n count: number,\n locale: string,\n): string {\n let category: string = \"other\";\n try {\n if (typeof Intl !== \"undefined\" && typeof Intl.PluralRules === \"function\") {\n category = new Intl.PluralRules(locale).select(count);\n }\n } catch {\n // Bad locale tag — fall through to \"other\".\n }\n if (category in forms) return forms[category]!;\n if (\"other\" in forms) return forms[\"other\"]!;\n const first = Object.keys(forms)[0];\n return first ? forms[first]! : \"\";\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n private _config: Required<\n Pick<VerbumiaConfig, \"apiBase\" | \"cdnBase\" | \"missingHandler\">\n > & {\n token: string;\n projectUuid: string;\n namespaces: string[];\n flushIntervalMs: number;\n flushBatchSize: number;\n missingEventsBufferSize: number;\n versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: string;\n };\n\n private _transport: Transport;\n private _pending: MissingKeyEvent[] = [];\n private _seen = new Set<string>(); // dedup `${locale}/${ns}/${key}` per-flush\n private _timer: ReturnType<typeof setInterval> | null = null;\n private _listeners = new Set<Listener>();\n private _live: LiveClient | null = null;\n // Stable snapshot reference for useSyncExternalStore. Returning a fresh\n // object on each getSnapshot call would loop React forever — we rebuild\n // it ONLY in _notify (when state actually changed) and return the cached\n // reference between notifications.\n private _snapshot!: I18nInstance;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n this._snapshot = this._buildSnapshot();\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n /** Stable snapshot accessor for useSyncExternalStore. The returned\n * object reference is identical between renders unless _notify fired. */\n getSnapshot = (): I18nInstance => this._snapshot;\n\n private _buildSnapshot(): I18nInstance {\n return {\n ready: this.ready,\n locale: this.locale,\n setLocale: this.setLocale,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n };\n }\n\n private _notify(): void {\n this._snapshot = this._buildSnapshot();\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n if (this._config.liveUpdates) {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n void this._loadBundle(lang, ns, fetchImpl).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string; count?: number }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: typeof options?.defaultValue === \"string\" ? options.defaultValue : undefined,\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n /**\n * Final-stage render: pick the right plural form (when value is a CLDR\n * dict and `options.count` is a number) then interpolate `{{var}}`.\n */\n private _render(\n value: ResolvedValue,\n locale: Locale,\n options?: Record<string, unknown> & { count?: number },\n ): string {\n let str: string;\n if (typeof value === \"string\") {\n str = value;\n } else {\n const count = typeof options?.count === \"number\" ? options.count : 0;\n str = selectPluralForm(value, count, locale);\n }\n return interpolate(str, options);\n }\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch\n ): Promise<void> {\n const url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.versionSlug}/latest/${locale}/${ns}.json`;\n try {\n const r = await fetchImpl(url, { method: \"GET\", credentials: \"omit\" });\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(`${locale}/${ns}`, data);\n } else {\n // 404 = no published bundle yet — empty bundle is fine, missing keys flow handles it.\n this._bundles.set(`${locale}/${ns}`, {});\n }\n } catch {\n this._bundles.set(`${locale}/${ns}`, {});\n } finally {\n this._attempted.add(`${locale}/${ns}`);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["useMemo","useMemo","jsx"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbumia/react-i18next",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "React SDK for Verbumia — translations + realtime missing-key handler.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://verbumia.ca",