@verbumia/react-i18next 0.9.0 → 0.10.0

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/README.md CHANGED
@@ -69,6 +69,7 @@ interface VerbumiaConfig {
69
69
  version?: string; // version slug, default 'main' (in cache keys)
70
70
  versionSlug?: string; // @deprecated alias of `version`
71
71
  env?: 'prod' | 'dev'; // default 'prod' (drives fetch source)
72
+ initialBundles?: Record<string, Record<string, object>>; // build-time snapshot (locale->ns->tree)
72
73
  plugins?: VerbumiaPlugin[]; // e.g. @verbumia/feedback, @verbumia/realtime
73
74
  transport?: (batch: MissingKeyEvent[]) => void | Promise<void>;
74
75
  missingHandler?: 'send' | 'log' | 'off'; // default 'send'
@@ -200,11 +201,16 @@ interface MissingKeyEvent {
200
201
  key: string;
201
202
  namespace: string;
202
203
  language_code: string;
203
- source_value?: string;
204
+ source_value?: string; // explicit defaultValue or fallback value; omitted when none (never the key name)
204
205
  sdk_meta?: Record<string, unknown>; // SDK adds {lib, ver, url} automatically
205
206
  }
206
207
  ```
207
208
 
209
+ `source_value` carries the canonical default the SDK has — the `defaultValue`
210
+ you pass to `t()` (object or positional form), or the fallback-language bundle
211
+ value. When there is no default, it is **omitted** (the key name is in `key`),
212
+ so the backend never mistakes a key for a translation.
213
+
208
214
  ### Why the gate matters
209
215
 
210
216
  Without the gate, every `t("…")` call between mount and bundle resolution
@@ -276,6 +282,35 @@ each `translations_published` push. Realtime is a dev-version-only feature
276
282
 
277
283
  ---
278
284
 
285
+ ## Offline / first-paint snapshot
286
+
287
+ Native apps (and SSR/web) can render real translations on the **first paint**
288
+ and **offline** — before the first CDN fetch — by embedding a build-time
289
+ snapshot:
290
+
291
+ ```tsx
292
+ import snapshot from "./verbumia-snapshot.json"; // { locale: { namespace: tree } }
293
+
294
+ <VerbumiaProvider {...config} initialBundles={snapshot}>
295
+ <App />
296
+ </VerbumiaProvider>;
297
+ ```
298
+
299
+ `initialBundles` is keyed `locale -> namespace -> tree` (the same shape as the
300
+ CDN JSON). It is primed synchronously, so `i18n.ready` is `true` on the very
301
+ first render when the snapshot covers the active locale's namespaces. On mount
302
+ the provider fetches the CDN and swaps in fresh values with **no flash**; if
303
+ that fetch fails (offline), the snapshot stays as last-known-good. Keys absent
304
+ from the snapshot do not fire "missing" reports until a real fetch confirms.
305
+
306
+ ### Generating the snapshot
307
+
308
+ - **CLI (recommended):** `verbumia snapshot` (from `@verbumia/cli`) fetches the
309
+ current published bundles and writes the JSON module.
310
+ - **Manual:** fetch each
311
+ `https://cdn.verbumia.ca/p/<project>/<version>/latest/<locale>/<ns>.json` and
312
+ assemble them into `{ [locale]: { [namespace]: <tree> } }`, then import it.
313
+
279
314
  ## Recipes
280
315
 
281
316
  ### Next.js (App Router)
package/dist/index.cjs CHANGED
@@ -35,7 +35,7 @@ var import_react = require("react");
35
35
 
36
36
  // src/transport.ts
37
37
  var SDK_LIB = "@verbumia/react-i18next";
38
- var SDK_VER = "0.5.2";
38
+ var SDK_VER = true ? "0.10.0" : "0.0.0-dev";
39
39
  function defaultTransport(opts) {
40
40
  return async (batch) => {
41
41
  if (!batch.length) return;
@@ -45,7 +45,9 @@ function defaultTransport(opts) {
45
45
  key: e.key,
46
46
  namespace: e.namespace,
47
47
  language_code: e.language_code,
48
- source_value: e.source_value,
48
+ // Option A (#746): only send source_value when there's a real value;
49
+ // omit it otherwise (never the key name). Absent = "no default".
50
+ ...e.source_value !== void 0 ? { source_value: e.source_value } : {},
49
51
  sdk_meta: {
50
52
  lib: SDK_LIB,
51
53
  ver: SDK_VER,
@@ -264,6 +266,22 @@ var VerbumiaI18n = class {
264
266
  token: this._config.token,
265
267
  projectUuid: this._config.projectUuid
266
268
  }));
269
+ if (config.initialBundles) {
270
+ for (const [loc, byNs] of Object.entries(config.initialBundles)) {
271
+ for (const [ns, tree] of Object.entries(byNs)) {
272
+ if (!tree || typeof tree !== "object") continue;
273
+ const key = this._bundleKey(loc, ns);
274
+ this._bundles.set(key, tree);
275
+ if (Object.keys(tree).length > 0) this._hasContent.add(key);
276
+ }
277
+ }
278
+ const active = config.initialBundles[this.locale];
279
+ if (active && this._config.namespaces.every(
280
+ (ns) => active[ns] && Object.keys(active[ns]).length > 0
281
+ )) {
282
+ this.ready = true;
283
+ }
284
+ }
267
285
  this._snapshot = this._buildSnapshot();
268
286
  }
269
287
  // ---- React subscription ----
@@ -464,13 +482,13 @@ var VerbumiaI18n = class {
464
482
  } else {
465
483
  this._hasContent.delete(cacheKey);
466
484
  }
467
- } else if (opts.bust && hadContent) {
485
+ } else if (hadContent) {
468
486
  } else {
469
487
  this._bundles.set(cacheKey, {});
470
488
  this._hasContent.delete(cacheKey);
471
489
  }
472
490
  } catch {
473
- if (opts.bust && hadContent) {
491
+ if (hadContent) {
474
492
  } else {
475
493
  this._bundles.set(cacheKey, {});
476
494
  this._hasContent.delete(cacheKey);
@@ -489,13 +507,15 @@ var VerbumiaI18n = class {
489
507
  /**
490
508
  * Resolve the `source_value` we send with a missing-key report.
491
509
  *
492
- * Fallback chain (per backend agreement 2026-05-14, task 575):
510
+ * Fallback chain (Option A, task #746 — backend ingest aligned):
493
511
  * 1. `options.defaultValue` — explicit developer-provided string.
494
512
  * 2. The fallbackLng bundle's value for this key (typically the
495
513
  * source/canonical locale). Only used when it resolves to a
496
514
  * plain string, not a plural CLDR dict.
497
- * 3. The bare key itself last resort so dashboards never render
498
- * a blank `source_value` column.
515
+ * 3. Otherwise `undefined` we DO NOT fall back to the key name (that
516
+ * made the column unusable: a placeholder indistinguishable from a
517
+ * real default). The key is already carried in `event.key`; an absent
518
+ * `source_value` is the signal that there is no promotable value.
499
519
  */
500
520
  _sourceValueFor(bareKey, ns, options) {
501
521
  if (typeof options?.defaultValue === "string") {
@@ -507,7 +527,7 @@ var VerbumiaI18n = class {
507
527
  return fb;
508
528
  }
509
529
  }
510
- return bareKey;
530
+ return void 0;
511
531
  }
512
532
  _reportMissing(event) {
513
533
  if (this._config.missingHandler === "off") return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/key-registry.ts","../src/i18n.ts","../src/singleton.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["export { VerbumiaProvider } from \"./provider\";\nexport { useTranslation } from \"./hooks\";\nexport { Trans } from \"./trans\";\n/** Access the active i18n instance outside React components (react-i18next\n * drop-in: standalone singleton). Throws if no provider is mounted. */\nexport { getI18n } from \"./singleton\";\nexport type {\n I18nInstance,\n Locale,\n MissingHandlerMode,\n MissingKeyEvent,\n Namespace,\n TranslationFunction,\n TranslationOptions,\n Transport,\n VerbumiaConfig,\n VerbumiaPlugin,\n VerbumiaPluginContext,\n} from \"./types\";\nexport { defaultTransport, logTransport } from \"./transport\";\n/** Advanced: the on-screen key registry feeding `@verbumia/feedback`.\n * Mount-tracking handles navigation automatically; `reset()` is only\n * needed for non-React routing edge cases. */\nexport { keyRegistry } from \"./key-registry\";\nexport type { DeclaredKey } from \"./key-registry\";\n","import {\n createContext,\n Fragment,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport { _clearActiveInstance, _setActiveInstance } from \"./singleton\";\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 // Register as the active instance so `getI18n()` works outside React.\n _setActiveInstance(i18n);\n void i18n.start();\n // Plugins (e.g. @verbumia/feedback) hook the SAME i18n instance —\n // no second context. setup() runs once; optional teardown on unmount.\n const teardowns = (config.plugins ?? [])\n .map((p) => p.setup?.({ i18n, config }))\n .filter((t): t is () => void => typeof t === \"function\");\n return () => {\n teardowns.forEach((t) => t());\n i18n.stop();\n _clearActiveInstance(i18n);\n };\n }, [i18n]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>\n {children}\n {/* Plugin outlets: isolated sibling leaves AFTER children. Their\n internal state never propagates to the host app subtree. */}\n {(config.plugins ?? []).map((p) =>\n p.render ? <Fragment key={p.name}>{p.render()}</Fragment> : null,\n )}\n </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.5.2\";\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 * On-screen key registry — the PRODUCER side of the tiny cross-package\n * contract that `@verbumia/feedback` consumes via\n * `globalThis.__verbumia_key_registry__` (see `@verbumia/feedback`'s\n * `core/keys.ts`).\n *\n * Why this exists: the feedback widget must list only the translation\n * strings RENDERED on the current screen (spec ltm 373) — NOT every\n * project string. The widget can't know what's on screen; the i18n SDK\n * does, because it resolves the keys. So the SDK tracks the keys touched\n * by currently-MOUNTED `useTranslation`/`Trans` consumers and exposes\n * them through a minimal global. When a component unmounts (navigation),\n * its keys drop out automatically — so `snapshot()` is always exactly\n * \"what is on screen right now\". Explicit `keys` on the feedback plugin\n * stays a fallback for non-i18n strings; it is NOT \"pass everything\".\n *\n * The published shape is intentionally tiny so any framework port of the\n * i18n SDK can implement the same global without depending on feedback:\n *\n * globalThis.__verbumia_key_registry__ = {\n * snapshot(): { namespace: string; key: string }[];\n * reset(): void;\n * }\n */\n\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nconst GLOBAL = \"__verbumia_key_registry__\";\n// Internal id separator. NUL never appears in an i18next namespace or\n// key, so it round-trips even when a key itself contains ':'.\nconst SEP = \"\u0000\";\n\n/** Split an i18next-style `ns:key` (mirrors VerbumiaI18n._splitNamespace). */\nfunction split(fullKey: string, defaultNamespace: string): DeclaredKey {\n const idx = fullKey.indexOf(\":\");\n if (idx > 0) {\n return { namespace: fullKey.slice(0, idx), key: fullKey.slice(idx + 1) };\n }\n return { namespace: defaultNamespace, key: fullKey };\n}\n\nclass KeyRegistry {\n // One Set per mounted hook/Trans instance (keyed by an opaque token).\n // The on-screen set is the UNION of all live instances' latest render.\n private _instances = new Map<symbol, Set<string>>();\n // Provider mounts that have published us onto globalThis. Ref-counted so\n // a multi-provider tree (or fast unmount/remount in tests) never leaves\n // a stale global or unpublishes while another provider is still live.\n private _providers = 0;\n\n /** Replace an instance's contributed key set (called every render). */\n _set(token: symbol, keys: Set<string>): void {\n this._instances.set(token, keys);\n }\n\n /** Drop an instance entirely (called on unmount). */\n _delete(token: symbol): void {\n this._instances.delete(token);\n }\n\n /** Keys rendered by currently-mounted consumers. Stable insertion order. */\n snapshot(): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const set of this._instances.values()) {\n for (const id of set) {\n if (seen.has(id)) continue;\n seen.add(id);\n const c = id.indexOf(SEP);\n out.push({ namespace: id.slice(0, c), key: id.slice(c + 1) });\n }\n }\n return out;\n }\n\n /** Escape hatch (router integrations / tests). Mount-tracking already\n * handles navigation, so this is rarely needed. */\n reset(): void {\n this._instances.clear();\n }\n\n /** Encode a resolved key into the internal id used by `_set`. */\n encode(fullKey: string, defaultNamespace: string): string {\n const k = split(fullKey, defaultNamespace);\n return `${k.namespace}${SEP}${k.key}`;\n }\n\n /** Provider mounted — publish the global (idempotent, ref-counted). */\n attach(): void {\n this._providers += 1;\n if (this._providers === 1) {\n (globalThis as Record<string, unknown>)[GLOBAL] = {\n snapshot: () => this.snapshot(),\n reset: () => this.reset(),\n };\n }\n }\n\n /** Provider unmounted — unpublish when the last one goes away. */\n detach(): void {\n this._providers = Math.max(0, this._providers - 1);\n if (this._providers === 0) {\n this._instances.clear();\n const g = globalThis as Record<string, unknown>;\n if (g[GLOBAL]) delete g[GLOBAL];\n }\n }\n}\n\n/** Process-wide singleton — there is exactly one on-screen registry. */\nexport const keyRegistry = new KeyRegistry();\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { keyRegistry } from \"./key-registry\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.dev\";\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>(); // `${version}/${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${version}/${locale}/${ns}` keys we've fetched\n // Tighter gate than `_attempted`: this set only contains (version, locale, ns)\n // pairs whose CDN response was 200 with at least one top-level key. An\n // empty bundle (404 → {} OR 200 → {}) is treated as \"no data yet\";\n // calling t() against a key in such a bundle does NOT fire reportMissing.\n // Prevents the \"boot floods the dashboard\" failure when the project has\n // a brand-new namespace not yet published, OR when a network blip\n // produced an empty bundle.\n private _hasContent = new Set<string>();\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 version: string;\n env: \"prod\" | \"dev\";\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 // 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 // Fail-loud: realtime config moved out of core in 0.9.0. If a caller\n // still passes `liveUpdates` or ANY `centrifugo*` key, throw with a clear,\n // actionable migration message rather than silently no-op'ing (which would\n // stop realtime without anyone noticing).\n const removedRealtimeKeys = Object.keys(config).filter(\n (k) => k === \"liveUpdates\" || k.startsWith(\"centrifugo\"),\n );\n if (removedRealtimeKeys.length > 0) {\n throw new Error(\n `@verbumia/react-i18next: ${removedRealtimeKeys.join(\", \")} ${\n removedRealtimeKeys.length > 1 ? \"were\" : \"was\"\n } removed in 0.9.0 — realtime is now the @verbumia/realtime plugin. ` +\n \"Remove them and pass `plugins: [verbumiaRealtime({ wsUrl })]` to <VerbumiaProvider> instead.\",\n );\n }\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\n ? config.namespaces\n : config.defaultNS\n ? [config.defaultNS]\n : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n version: config.version ?? config.versionSlug ?? DEFAULT_VERSION_SLUG,\n env: config.env ?? \"prod\",\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 language: this.locale,\n setLocale: this.setLocale,\n changeLanguage: this.changeLanguage,\n t: this.t,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n reload: this.reload,\n };\n }\n\n /** Bundle cache-key builder. Includes `version` so providers with\n * different `version` values never share cached bundles. The segments\n * (version/locale/ns) are slugs and never contain '/'. */\n private _bundleKey(locale: Locale, ns: Namespace): string {\n return `${this._config.version}/${locale}/${ns}`;\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 /** Default namespace (the first configured one) — used to attribute a\n * bare `t(\"key\")` call when recording on-screen keys. */\n get defaultNamespace(): string {\n return this._config.namespaces[0]!;\n }\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n // Publish the on-screen key registry so a mounted feedback widget\n // lists only the strings rendered on the current view (spec ltm 373).\n keyRegistry.attach();\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 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 /** Alias of {@link setLocale} for react-i18next compatibility. */\n changeLanguage = (next: Locale): Promise<void> => this.setLocale(next);\n\n /** Alias of {@link locale} for react-i18next compatibility. */\n get language(): Locale {\n return this.locale;\n }\n\n stop(): void {\n keyRegistry.detach();\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n }\n\n /**\n * Bust-refetch already-loaded bundles and re-render once. Generic\n * replacement for the old realtime-only refetch: iterate the\n * `_attempted` cache keys (`${version}/${locale}/${ns}`), optionally\n * filtered by `opts.locale` / `opts.namespace`, and re-pull each one\n * with `{ bust: true }` so the mutable CDN `latest/` alias bypasses the\n * HTTP cache. After all settle, `_notify()` once so React re-renders.\n *\n * Used by `@verbumia/realtime` on a `translations_published` push and as\n * a manual refresh hook. If nothing matches, returns without notifying.\n */\n reload = async (\n opts: { locale?: Locale; namespace?: Namespace } = {},\n ): Promise<void> => {\n const targets: Array<{ locale: Locale; ns: Namespace }> = [];\n for (const key of this._attempted) {\n // `${version}/${locale}/${ns}` — none of the segments contain '/'.\n const parts = key.split(\"/\");\n const locale = parts[1];\n const ns = parts[2];\n if (!locale || !ns) continue;\n if (opts.locale && opts.locale !== locale) continue;\n if (opts.namespace && opts.namespace !== ns) continue;\n targets.push({ locale, ns });\n }\n if (targets.length === 0) return;\n await Promise.all(\n targets.map((t) => this._loadBundle(t.locale, t.ns, fetch, { bust: true })),\n );\n this._notify();\n };\n\n // ---- Translation ----\n\n t = (\n key: string,\n optionsOrDefault?:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | string,\n maybeOptions?: Record<string, unknown> & { defaultValue?: string; count?: number },\n ): string => {\n // react-i18next-style positional fallback: a string 2nd arg is the\n // default value. Optional 3rd arg carries interpolation/options and is\n // merged under it. `t(key, { defaultValue })` keeps working unchanged.\n const options:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | undefined =\n typeof optionsOrDefault === \"string\"\n ? { ...(maybeOptions ?? {}), defaultValue: optionsOrDefault }\n : optionsOrDefault;\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._bundleKey(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._bundleKey(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 // Three-condition gate: ready + attempted + bundle had content. The\n // last clause prevents flooding when the bundle came back empty (404\n // or {}); we'd be reporting against keys we never had a chance to\n // resolve. Master 2026-05-07 P0: see `_hasContent` doc.\n if (\n this.ready &&\n this._attempted.has(this._bundleKey(this.locale, ns)) &&\n this._hasContent.has(this._bundleKey(this.locale, ns))\n ) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: this._sourceValueFor(bareKey, ns, options),\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 opts: { bust?: boolean } = {}\n ): Promise<void> {\n const cacheKey = this._bundleKey(locale, ns);\n // env routing — prod hits the CDN cache; dev hits the live runtime\n // endpoint authenticated with the API key.\n let url: string;\n let init: RequestInit;\n if (this._config.env === \"dev\") {\n const params = new URLSearchParams({ language: locale, namespace: ns });\n if (this._config.version && this._config.version !== \"main\") {\n params.set(\"version_slug\", this._config.version);\n }\n url = `${this._config.apiBase.replace(/\\/+$/, \"\")}/v1/projects/${this._config.projectUuid}/translations/runtime?${params.toString()}`;\n init = {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${this._config.token}` },\n credentials: \"omit\",\n };\n } else {\n url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.version}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (opts.bust && hadContent) {\n // transient non-OK on a live refetch — keep prior content\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (opts.bust && hadContent) {\n // transient network error on a live refetch — keep prior content\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\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 /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (per backend agreement 2026-05-14, task 575):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. The bare key itself — last resort so dashboards never render\n * a blank `source_value` column.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return bareKey;\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 type { I18nInstance } from \"./types\";\nimport type { VerbumiaI18n } from \"./i18n\";\n\n// Active instance registered by the mounted <VerbumiaProvider>. Lets code\n// OUTSIDE React (utilities, stores, non-component modules) reach the i18n\n// instance the way react-i18next exposes its default singleton.\nlet _active: VerbumiaI18n | null = null;\n\n/** @internal — VerbumiaProvider registers its instance on mount. */\nexport function _setActiveInstance(instance: VerbumiaI18n): void {\n _active = instance;\n}\n\n/** @internal — VerbumiaProvider clears its instance on unmount. */\nexport function _clearActiveInstance(instance: VerbumiaI18n): void {\n if (_active === instance) _active = null;\n}\n\n/**\n * Access the active i18n instance OUTSIDE React components — the\n * react-i18next-style standalone singleton (e.g. for `t()`/`changeLanguage()`\n * in plain modules, stores, or helpers).\n *\n * Returns the instance created by the mounted `<VerbumiaProvider>`; throws a\n * clear error if no provider is mounted yet. Assumes a single app-wide\n * provider (the common case); with multiple concurrent providers it returns\n * the most recently mounted one.\n */\nexport function getI18n(): I18nInstance {\n if (!_active) {\n throw new Error(\n \"@verbumia/react-i18next: getI18n() was called before <VerbumiaProvider> mounted (no active i18n instance).\",\n );\n }\n return _active;\n}\n","import { useEffect, useMemo, useRef } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport { keyRegistry } from \"./key-registry\";\nimport type {\n I18nInstance,\n TranslationFunction,\n TranslationOptions,\n} 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.\n *\n * Every key this hook resolves during a render is recorded into the\n * on-screen key registry (so a mounted `@verbumia/feedback` widget lists\n * only the strings rendered on the current view — spec ltm 373). The\n * contribution is keyed to THIS hook instance and dropped on unmount, so\n * navigating away removes its keys automatically. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n\n // Keys resolved in the CURRENT render pass. The hook body runs before\n // the component's own `t()` calls, so clearing here yields a set that\n // reflects exactly this render once the component finishes.\n const renderedRef = useRef<Set<string>>(new Set());\n renderedRef.current = new Set<string>();\n // Opaque, stable token identifying this hook instance in the registry.\n const tokenRef = useRef<symbol>(Symbol(\"verbumia.t\"));\n\n const t = useMemo<TranslationFunction>(() => {\n // Forwards both call shapes — `t(key, opts)` and the react-i18next\n // positional `t(key, 'Default', opts?)` — straight to `i18n.t`, which\n // normalizes them. Registry tracking is keyed on the resolved fullKey.\n const fn = (\n key: string,\n optionsOrDefault?: TranslationOptions | string,\n maybeOptions?: TranslationOptions,\n ): string => {\n const fullKey =\n defaultNamespace && !key.includes(\":\")\n ? `${defaultNamespace}:${key}`\n : key;\n renderedRef.current.add(\n keyRegistry.encode(fullKey, i18n.defaultNamespace),\n );\n return i18n.t(fullKey, optionsOrDefault, maybeOptions);\n };\n return fn as TranslationFunction;\n }, [i18n, defaultNamespace]);\n\n // After every commit, publish this instance's latest rendered-key set.\n useEffect(() => {\n keyRegistry._set(tokenRef.current, renderedRef.current);\n });\n // Unmount only: drop this instance entirely so its keys leave the\n // on-screen snapshot when the component is gone (e.g. route change).\n useEffect(() => {\n const token = tokenRef.current;\n return () => keyRegistry._delete(token);\n }, []);\n\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/key-registry.ts","../src/i18n.ts","../src/singleton.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["export { VerbumiaProvider } from \"./provider\";\nexport { useTranslation } from \"./hooks\";\nexport { Trans } from \"./trans\";\n/** Access the active i18n instance outside React components (react-i18next\n * drop-in: standalone singleton). Throws if no provider is mounted. */\nexport { getI18n } from \"./singleton\";\nexport type {\n I18nInstance,\n Locale,\n MissingHandlerMode,\n MissingKeyEvent,\n Namespace,\n TranslationFunction,\n TranslationOptions,\n Transport,\n VerbumiaConfig,\n VerbumiaPlugin,\n VerbumiaPluginContext,\n} from \"./types\";\nexport { defaultTransport, logTransport } from \"./transport\";\n/** Advanced: the on-screen key registry feeding `@verbumia/feedback`.\n * Mount-tracking handles navigation automatically; `reset()` is only\n * needed for non-React routing edge cases. */\nexport { keyRegistry } from \"./key-registry\";\nexport type { DeclaredKey } from \"./key-registry\";\n","import {\n createContext,\n Fragment,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport { _clearActiveInstance, _setActiveInstance } from \"./singleton\";\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 // Register as the active instance so `getI18n()` works outside React.\n _setActiveInstance(i18n);\n void i18n.start();\n // Plugins (e.g. @verbumia/feedback) hook the SAME i18n instance —\n // no second context. setup() runs once; optional teardown on unmount.\n const teardowns = (config.plugins ?? [])\n .map((p) => p.setup?.({ i18n, config }))\n .filter((t): t is () => void => typeof t === \"function\");\n return () => {\n teardowns.forEach((t) => t());\n i18n.stop();\n _clearActiveInstance(i18n);\n };\n }, [i18n]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>\n {children}\n {/* Plugin outlets: isolated sibling leaves AFTER children. Their\n internal state never propagates to the host app subtree. */}\n {(config.plugins ?? []).map((p) =>\n p.render ? <Fragment key={p.name}>{p.render()}</Fragment> : null,\n )}\n </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\";\n// Replaced at build time by tsup `define` with the package.json version, so\n// `sdk_meta.ver` can never drift again. Falls back in non-bundled contexts\n// (e.g. unit tests, where the define isn't applied).\ndeclare const __SDK_VER__: string;\nconst SDK_VER =\n typeof __SDK_VER__ !== \"undefined\" ? __SDK_VER__ : \"0.0.0-dev\";\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 // Option A (#746): only send source_value when there's a real value;\n // omit it otherwise (never the key name). Absent = \"no default\".\n ...(e.source_value !== undefined\n ? { source_value: e.source_value }\n : {}),\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 * On-screen key registry — the PRODUCER side of the tiny cross-package\n * contract that `@verbumia/feedback` consumes via\n * `globalThis.__verbumia_key_registry__` (see `@verbumia/feedback`'s\n * `core/keys.ts`).\n *\n * Why this exists: the feedback widget must list only the translation\n * strings RENDERED on the current screen (spec ltm 373) — NOT every\n * project string. The widget can't know what's on screen; the i18n SDK\n * does, because it resolves the keys. So the SDK tracks the keys touched\n * by currently-MOUNTED `useTranslation`/`Trans` consumers and exposes\n * them through a minimal global. When a component unmounts (navigation),\n * its keys drop out automatically — so `snapshot()` is always exactly\n * \"what is on screen right now\". Explicit `keys` on the feedback plugin\n * stays a fallback for non-i18n strings; it is NOT \"pass everything\".\n *\n * The published shape is intentionally tiny so any framework port of the\n * i18n SDK can implement the same global without depending on feedback:\n *\n * globalThis.__verbumia_key_registry__ = {\n * snapshot(): { namespace: string; key: string }[];\n * reset(): void;\n * }\n */\n\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nconst GLOBAL = \"__verbumia_key_registry__\";\n// Internal id separator. NUL never appears in an i18next namespace or\n// key, so it round-trips even when a key itself contains ':'.\nconst SEP = \"\u0000\";\n\n/** Split an i18next-style `ns:key` (mirrors VerbumiaI18n._splitNamespace). */\nfunction split(fullKey: string, defaultNamespace: string): DeclaredKey {\n const idx = fullKey.indexOf(\":\");\n if (idx > 0) {\n return { namespace: fullKey.slice(0, idx), key: fullKey.slice(idx + 1) };\n }\n return { namespace: defaultNamespace, key: fullKey };\n}\n\nclass KeyRegistry {\n // One Set per mounted hook/Trans instance (keyed by an opaque token).\n // The on-screen set is the UNION of all live instances' latest render.\n private _instances = new Map<symbol, Set<string>>();\n // Provider mounts that have published us onto globalThis. Ref-counted so\n // a multi-provider tree (or fast unmount/remount in tests) never leaves\n // a stale global or unpublishes while another provider is still live.\n private _providers = 0;\n\n /** Replace an instance's contributed key set (called every render). */\n _set(token: symbol, keys: Set<string>): void {\n this._instances.set(token, keys);\n }\n\n /** Drop an instance entirely (called on unmount). */\n _delete(token: symbol): void {\n this._instances.delete(token);\n }\n\n /** Keys rendered by currently-mounted consumers. Stable insertion order. */\n snapshot(): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const set of this._instances.values()) {\n for (const id of set) {\n if (seen.has(id)) continue;\n seen.add(id);\n const c = id.indexOf(SEP);\n out.push({ namespace: id.slice(0, c), key: id.slice(c + 1) });\n }\n }\n return out;\n }\n\n /** Escape hatch (router integrations / tests). Mount-tracking already\n * handles navigation, so this is rarely needed. */\n reset(): void {\n this._instances.clear();\n }\n\n /** Encode a resolved key into the internal id used by `_set`. */\n encode(fullKey: string, defaultNamespace: string): string {\n const k = split(fullKey, defaultNamespace);\n return `${k.namespace}${SEP}${k.key}`;\n }\n\n /** Provider mounted — publish the global (idempotent, ref-counted). */\n attach(): void {\n this._providers += 1;\n if (this._providers === 1) {\n (globalThis as Record<string, unknown>)[GLOBAL] = {\n snapshot: () => this.snapshot(),\n reset: () => this.reset(),\n };\n }\n }\n\n /** Provider unmounted — unpublish when the last one goes away. */\n detach(): void {\n this._providers = Math.max(0, this._providers - 1);\n if (this._providers === 0) {\n this._instances.clear();\n const g = globalThis as Record<string, unknown>;\n if (g[GLOBAL]) delete g[GLOBAL];\n }\n }\n}\n\n/** Process-wide singleton — there is exactly one on-screen registry. */\nexport const keyRegistry = new KeyRegistry();\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { keyRegistry } from \"./key-registry\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.dev\";\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>(); // `${version}/${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${version}/${locale}/${ns}` keys we've fetched\n // Tighter gate than `_attempted`: this set only contains (version, locale, ns)\n // pairs whose CDN response was 200 with at least one top-level key. An\n // empty bundle (404 → {} OR 200 → {}) is treated as \"no data yet\";\n // calling t() against a key in such a bundle does NOT fire reportMissing.\n // Prevents the \"boot floods the dashboard\" failure when the project has\n // a brand-new namespace not yet published, OR when a network blip\n // produced an empty bundle.\n private _hasContent = new Set<string>();\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 version: string;\n env: \"prod\" | \"dev\";\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 // 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 // Fail-loud: realtime config moved out of core in 0.9.0. If a caller\n // still passes `liveUpdates` or ANY `centrifugo*` key, throw with a clear,\n // actionable migration message rather than silently no-op'ing (which would\n // stop realtime without anyone noticing).\n const removedRealtimeKeys = Object.keys(config).filter(\n (k) => k === \"liveUpdates\" || k.startsWith(\"centrifugo\"),\n );\n if (removedRealtimeKeys.length > 0) {\n throw new Error(\n `@verbumia/react-i18next: ${removedRealtimeKeys.join(\", \")} ${\n removedRealtimeKeys.length > 1 ? \"were\" : \"was\"\n } removed in 0.9.0 — realtime is now the @verbumia/realtime plugin. ` +\n \"Remove them and pass `plugins: [verbumiaRealtime({ wsUrl })]` to <VerbumiaProvider> instead.\",\n );\n }\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\n ? config.namespaces\n : config.defaultNS\n ? [config.defaultNS]\n : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n version: config.version ?? config.versionSlug ?? DEFAULT_VERSION_SLUG,\n env: config.env ?? \"prod\",\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 // Embedded build-time snapshot (#757): prime the cache synchronously so\n // the first render is instant and works OFFLINE before the first fetch,\n // then `start()` swaps in CDN/runtime data (no flash). We prime _bundles +\n // _hasContent but NOT _attempted — the missing-key gate stays closed until\n // a real fetch confirms, so keys absent from the snapshot don't generate\n // false \"missing\" reports in the meantime.\n if (config.initialBundles) {\n for (const [loc, byNs] of Object.entries(config.initialBundles)) {\n for (const [ns, tree] of Object.entries(byNs)) {\n if (!tree || typeof tree !== \"object\") continue;\n const key = this._bundleKey(loc, ns);\n this._bundles.set(key, tree as Bundle);\n if (Object.keys(tree).length > 0) this._hasContent.add(key);\n }\n }\n // ready=true only when the snapshot fully covers the active locale's\n // configured namespaces (otherwise wait for the fetch to flip it).\n const active = config.initialBundles[this.locale];\n if (\n active &&\n this._config.namespaces.every(\n (ns) => active[ns] && Object.keys(active[ns]!).length > 0,\n )\n ) {\n this.ready = true;\n }\n }\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 language: this.locale,\n setLocale: this.setLocale,\n changeLanguage: this.changeLanguage,\n t: this.t,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n reload: this.reload,\n };\n }\n\n /** Bundle cache-key builder. Includes `version` so providers with\n * different `version` values never share cached bundles. The segments\n * (version/locale/ns) are slugs and never contain '/'. */\n private _bundleKey(locale: Locale, ns: Namespace): string {\n return `${this._config.version}/${locale}/${ns}`;\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 /** Default namespace (the first configured one) — used to attribute a\n * bare `t(\"key\")` call when recording on-screen keys. */\n get defaultNamespace(): string {\n return this._config.namespaces[0]!;\n }\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n // Publish the on-screen key registry so a mounted feedback widget\n // lists only the strings rendered on the current view (spec ltm 373).\n keyRegistry.attach();\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 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 /** Alias of {@link setLocale} for react-i18next compatibility. */\n changeLanguage = (next: Locale): Promise<void> => this.setLocale(next);\n\n /** Alias of {@link locale} for react-i18next compatibility. */\n get language(): Locale {\n return this.locale;\n }\n\n stop(): void {\n keyRegistry.detach();\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n }\n\n /**\n * Bust-refetch already-loaded bundles and re-render once. Generic\n * replacement for the old realtime-only refetch: iterate the\n * `_attempted` cache keys (`${version}/${locale}/${ns}`), optionally\n * filtered by `opts.locale` / `opts.namespace`, and re-pull each one\n * with `{ bust: true }` so the mutable CDN `latest/` alias bypasses the\n * HTTP cache. After all settle, `_notify()` once so React re-renders.\n *\n * Used by `@verbumia/realtime` on a `translations_published` push and as\n * a manual refresh hook. If nothing matches, returns without notifying.\n */\n reload = async (\n opts: { locale?: Locale; namespace?: Namespace } = {},\n ): Promise<void> => {\n const targets: Array<{ locale: Locale; ns: Namespace }> = [];\n for (const key of this._attempted) {\n // `${version}/${locale}/${ns}` — none of the segments contain '/'.\n const parts = key.split(\"/\");\n const locale = parts[1];\n const ns = parts[2];\n if (!locale || !ns) continue;\n if (opts.locale && opts.locale !== locale) continue;\n if (opts.namespace && opts.namespace !== ns) continue;\n targets.push({ locale, ns });\n }\n if (targets.length === 0) return;\n await Promise.all(\n targets.map((t) => this._loadBundle(t.locale, t.ns, fetch, { bust: true })),\n );\n this._notify();\n };\n\n // ---- Translation ----\n\n t = (\n key: string,\n optionsOrDefault?:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | string,\n maybeOptions?: Record<string, unknown> & { defaultValue?: string; count?: number },\n ): string => {\n // react-i18next-style positional fallback: a string 2nd arg is the\n // default value. Optional 3rd arg carries interpolation/options and is\n // merged under it. `t(key, { defaultValue })` keeps working unchanged.\n const options:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | undefined =\n typeof optionsOrDefault === \"string\"\n ? { ...(maybeOptions ?? {}), defaultValue: optionsOrDefault }\n : optionsOrDefault;\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._bundleKey(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._bundleKey(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 // Three-condition gate: ready + attempted + bundle had content. The\n // last clause prevents flooding when the bundle came back empty (404\n // or {}); we'd be reporting against keys we never had a chance to\n // resolve. Master 2026-05-07 P0: see `_hasContent` doc.\n if (\n this.ready &&\n this._attempted.has(this._bundleKey(this.locale, ns)) &&\n this._hasContent.has(this._bundleKey(this.locale, ns))\n ) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: this._sourceValueFor(bareKey, ns, options),\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 opts: { bust?: boolean } = {}\n ): Promise<void> {\n const cacheKey = this._bundleKey(locale, ns);\n // env routing — prod hits the CDN cache; dev hits the live runtime\n // endpoint authenticated with the API key.\n let url: string;\n let init: RequestInit;\n if (this._config.env === \"dev\") {\n const params = new URLSearchParams({ language: locale, namespace: ns });\n if (this._config.version && this._config.version !== \"main\") {\n params.set(\"version_slug\", this._config.version);\n }\n url = `${this._config.apiBase.replace(/\\/+$/, \"\")}/v1/projects/${this._config.projectUuid}/translations/runtime?${params.toString()}`;\n init = {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${this._config.token}` },\n credentials: \"omit\",\n };\n } else {\n url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.version}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (hadContent) {\n // Non-OK but we already have content (an embedded snapshot or a prior\n // successful fetch) — keep that last-known-good instead of clobbering\n // it. Covers offline/transient first loads (#757) and refetch blips.\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (hadContent) {\n // Network error but we already have content (snapshot or prior fetch)\n // — keep last-known-good (offline-first, #757).\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\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 /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (Option A, task #746 — backend ingest aligned):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. Otherwise `undefined` — we DO NOT fall back to the key name (that\n * made the column unusable: a placeholder indistinguishable from a\n * real default). The key is already carried in `event.key`; an absent\n * `source_value` is the signal that there is no promotable value.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string | undefined {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return undefined;\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 type { I18nInstance } from \"./types\";\nimport type { VerbumiaI18n } from \"./i18n\";\n\n// Active instance registered by the mounted <VerbumiaProvider>. Lets code\n// OUTSIDE React (utilities, stores, non-component modules) reach the i18n\n// instance the way react-i18next exposes its default singleton.\nlet _active: VerbumiaI18n | null = null;\n\n/** @internal — VerbumiaProvider registers its instance on mount. */\nexport function _setActiveInstance(instance: VerbumiaI18n): void {\n _active = instance;\n}\n\n/** @internal — VerbumiaProvider clears its instance on unmount. */\nexport function _clearActiveInstance(instance: VerbumiaI18n): void {\n if (_active === instance) _active = null;\n}\n\n/**\n * Access the active i18n instance OUTSIDE React components — the\n * react-i18next-style standalone singleton (e.g. for `t()`/`changeLanguage()`\n * in plain modules, stores, or helpers).\n *\n * Returns the instance created by the mounted `<VerbumiaProvider>`; throws a\n * clear error if no provider is mounted yet. Assumes a single app-wide\n * provider (the common case); with multiple concurrent providers it returns\n * the most recently mounted one.\n */\nexport function getI18n(): I18nInstance {\n if (!_active) {\n throw new Error(\n \"@verbumia/react-i18next: getI18n() was called before <VerbumiaProvider> mounted (no active i18n instance).\",\n );\n }\n return _active;\n}\n","import { useEffect, useMemo, useRef } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport { keyRegistry } from \"./key-registry\";\nimport type {\n I18nInstance,\n TranslationFunction,\n TranslationOptions,\n} 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.\n *\n * Every key this hook resolves during a render is recorded into the\n * on-screen key registry (so a mounted `@verbumia/feedback` widget lists\n * only the strings rendered on the current view — spec ltm 373). The\n * contribution is keyed to THIS hook instance and dropped on unmount, so\n * navigating away removes its keys automatically. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n\n // Keys resolved in the CURRENT render pass. The hook body runs before\n // the component's own `t()` calls, so clearing here yields a set that\n // reflects exactly this render once the component finishes.\n const renderedRef = useRef<Set<string>>(new Set());\n renderedRef.current = new Set<string>();\n // Opaque, stable token identifying this hook instance in the registry.\n const tokenRef = useRef<symbol>(Symbol(\"verbumia.t\"));\n\n const t = useMemo<TranslationFunction>(() => {\n // Forwards both call shapes — `t(key, opts)` and the react-i18next\n // positional `t(key, 'Default', opts?)` — straight to `i18n.t`, which\n // normalizes them. Registry tracking is keyed on the resolved fullKey.\n const fn = (\n key: string,\n optionsOrDefault?: TranslationOptions | string,\n maybeOptions?: TranslationOptions,\n ): string => {\n const fullKey =\n defaultNamespace && !key.includes(\":\")\n ? `${defaultNamespace}:${key}`\n : key;\n renderedRef.current.add(\n keyRegistry.encode(fullKey, i18n.defaultNamespace),\n );\n return i18n.t(fullKey, optionsOrDefault, maybeOptions);\n };\n return fn as TranslationFunction;\n }, [i18n, defaultNamespace]);\n\n // After every commit, publish this instance's latest rendered-key set.\n useEffect(() => {\n keyRegistry._set(tokenRef.current, renderedRef.current);\n });\n // Unmount only: drop this instance entirely so its keys leave the\n // on-screen snapshot when the component is gone (e.g. route change).\n useEffect(() => {\n const token = tokenRef.current;\n return () => keyRegistry._delete(token);\n }, []);\n\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.d.cts CHANGED
@@ -8,12 +8,13 @@ interface MissingKeyEvent {
8
8
  namespace: Namespace;
9
9
  language_code: Locale;
10
10
  /**
11
- * Canonical source-language value the developer asked for. The SDK
12
- * always populates this — explicit `defaultValue` from `t()` options
13
- * first, then the fallback-language bundle value, then the key itself
14
- * so client dashboards never see an empty column.
11
+ * Canonical source-language value the developer asked for, WHEN one is
12
+ * available: the explicit `defaultValue` from `t()` first, else the
13
+ * fallback-language bundle value. OMITTED (undefined) when neither exists —
14
+ * the SDK never sends the key name as a value (the key is already in `key`).
15
+ * The backend promotes only a non-null `source_value`.
15
16
  */
16
- source_value: string;
17
+ source_value?: string;
17
18
  sdk_meta?: Record<string, unknown>;
18
19
  }
19
20
  type MissingHandlerMode = "send" | "log" | "off";
@@ -32,6 +33,16 @@ interface VerbumiaConfig {
32
33
  * first entry is the default namespace).
33
34
  */
34
35
  defaultNS?: Namespace;
36
+ /**
37
+ * Embedded build-time snapshot of translation bundles, keyed
38
+ * `locale -> namespace -> i18next tree` (the same shape as the CDN JSON).
39
+ * Primed synchronously on construction so the FIRST render is instant and
40
+ * works OFFLINE (before the first CDN/runtime fetch); the provider then
41
+ * swaps in fresh data when it arrives, with no flash. A failed initial fetch
42
+ * (offline) keeps the snapshot as last-known-good. Generate it with the
43
+ * `verbumia snapshot` CLI, or manually (fetch the CDN JSON and import it).
44
+ */
45
+ initialBundles?: Record<Locale, Record<Namespace, Record<string, unknown>>>;
35
46
  /** Initial locale (BCP-47). */
36
47
  defaultLocale: Locale;
37
48
  /** Fallback locale used when a key is missing in `defaultLocale`. */
package/dist/index.d.ts CHANGED
@@ -8,12 +8,13 @@ interface MissingKeyEvent {
8
8
  namespace: Namespace;
9
9
  language_code: Locale;
10
10
  /**
11
- * Canonical source-language value the developer asked for. The SDK
12
- * always populates this — explicit `defaultValue` from `t()` options
13
- * first, then the fallback-language bundle value, then the key itself
14
- * so client dashboards never see an empty column.
11
+ * Canonical source-language value the developer asked for, WHEN one is
12
+ * available: the explicit `defaultValue` from `t()` first, else the
13
+ * fallback-language bundle value. OMITTED (undefined) when neither exists —
14
+ * the SDK never sends the key name as a value (the key is already in `key`).
15
+ * The backend promotes only a non-null `source_value`.
15
16
  */
16
- source_value: string;
17
+ source_value?: string;
17
18
  sdk_meta?: Record<string, unknown>;
18
19
  }
19
20
  type MissingHandlerMode = "send" | "log" | "off";
@@ -32,6 +33,16 @@ interface VerbumiaConfig {
32
33
  * first entry is the default namespace).
33
34
  */
34
35
  defaultNS?: Namespace;
36
+ /**
37
+ * Embedded build-time snapshot of translation bundles, keyed
38
+ * `locale -> namespace -> i18next tree` (the same shape as the CDN JSON).
39
+ * Primed synchronously on construction so the FIRST render is instant and
40
+ * works OFFLINE (before the first CDN/runtime fetch); the provider then
41
+ * swaps in fresh data when it arrives, with no flash. A failed initial fetch
42
+ * (offline) keeps the snapshot as last-known-good. Generate it with the
43
+ * `verbumia snapshot` CLI, or manually (fetch the CDN JSON and import it).
44
+ */
45
+ initialBundles?: Record<Locale, Record<Namespace, Record<string, unknown>>>;
35
46
  /** Initial locale (BCP-47). */
36
47
  defaultLocale: Locale;
37
48
  /** Fallback locale used when a key is missing in `defaultLocale`. */
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  // src/transport.ts
12
12
  var SDK_LIB = "@verbumia/react-i18next";
13
- var SDK_VER = "0.5.2";
13
+ var SDK_VER = true ? "0.10.0" : "0.0.0-dev";
14
14
  function defaultTransport(opts) {
15
15
  return async (batch) => {
16
16
  if (!batch.length) return;
@@ -20,7 +20,9 @@ function defaultTransport(opts) {
20
20
  key: e.key,
21
21
  namespace: e.namespace,
22
22
  language_code: e.language_code,
23
- source_value: e.source_value,
23
+ // Option A (#746): only send source_value when there's a real value;
24
+ // omit it otherwise (never the key name). Absent = "no default".
25
+ ...e.source_value !== void 0 ? { source_value: e.source_value } : {},
24
26
  sdk_meta: {
25
27
  lib: SDK_LIB,
26
28
  ver: SDK_VER,
@@ -239,6 +241,22 @@ var VerbumiaI18n = class {
239
241
  token: this._config.token,
240
242
  projectUuid: this._config.projectUuid
241
243
  }));
244
+ if (config.initialBundles) {
245
+ for (const [loc, byNs] of Object.entries(config.initialBundles)) {
246
+ for (const [ns, tree] of Object.entries(byNs)) {
247
+ if (!tree || typeof tree !== "object") continue;
248
+ const key = this._bundleKey(loc, ns);
249
+ this._bundles.set(key, tree);
250
+ if (Object.keys(tree).length > 0) this._hasContent.add(key);
251
+ }
252
+ }
253
+ const active = config.initialBundles[this.locale];
254
+ if (active && this._config.namespaces.every(
255
+ (ns) => active[ns] && Object.keys(active[ns]).length > 0
256
+ )) {
257
+ this.ready = true;
258
+ }
259
+ }
242
260
  this._snapshot = this._buildSnapshot();
243
261
  }
244
262
  // ---- React subscription ----
@@ -439,13 +457,13 @@ var VerbumiaI18n = class {
439
457
  } else {
440
458
  this._hasContent.delete(cacheKey);
441
459
  }
442
- } else if (opts.bust && hadContent) {
460
+ } else if (hadContent) {
443
461
  } else {
444
462
  this._bundles.set(cacheKey, {});
445
463
  this._hasContent.delete(cacheKey);
446
464
  }
447
465
  } catch {
448
- if (opts.bust && hadContent) {
466
+ if (hadContent) {
449
467
  } else {
450
468
  this._bundles.set(cacheKey, {});
451
469
  this._hasContent.delete(cacheKey);
@@ -464,13 +482,15 @@ var VerbumiaI18n = class {
464
482
  /**
465
483
  * Resolve the `source_value` we send with a missing-key report.
466
484
  *
467
- * Fallback chain (per backend agreement 2026-05-14, task 575):
485
+ * Fallback chain (Option A, task #746 — backend ingest aligned):
468
486
  * 1. `options.defaultValue` — explicit developer-provided string.
469
487
  * 2. The fallbackLng bundle's value for this key (typically the
470
488
  * source/canonical locale). Only used when it resolves to a
471
489
  * plain string, not a plural CLDR dict.
472
- * 3. The bare key itself last resort so dashboards never render
473
- * a blank `source_value` column.
490
+ * 3. Otherwise `undefined` we DO NOT fall back to the key name (that
491
+ * made the column unusable: a placeholder indistinguishable from a
492
+ * real default). The key is already carried in `event.key`; an absent
493
+ * `source_value` is the signal that there is no promotable value.
474
494
  */
475
495
  _sourceValueFor(bareKey, ns, options) {
476
496
  if (typeof options?.defaultValue === "string") {
@@ -482,7 +502,7 @@ var VerbumiaI18n = class {
482
502
  return fb;
483
503
  }
484
504
  }
485
- return bareKey;
505
+ return void 0;
486
506
  }
487
507
  _reportMissing(event) {
488
508
  if (this._config.missingHandler === "off") return;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/key-registry.ts","../src/i18n.ts","../src/singleton.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["import {\n createContext,\n Fragment,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport { _clearActiveInstance, _setActiveInstance } from \"./singleton\";\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 // Register as the active instance so `getI18n()` works outside React.\n _setActiveInstance(i18n);\n void i18n.start();\n // Plugins (e.g. @verbumia/feedback) hook the SAME i18n instance —\n // no second context. setup() runs once; optional teardown on unmount.\n const teardowns = (config.plugins ?? [])\n .map((p) => p.setup?.({ i18n, config }))\n .filter((t): t is () => void => typeof t === \"function\");\n return () => {\n teardowns.forEach((t) => t());\n i18n.stop();\n _clearActiveInstance(i18n);\n };\n }, [i18n]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>\n {children}\n {/* Plugin outlets: isolated sibling leaves AFTER children. Their\n internal state never propagates to the host app subtree. */}\n {(config.plugins ?? []).map((p) =>\n p.render ? <Fragment key={p.name}>{p.render()}</Fragment> : null,\n )}\n </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.5.2\";\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 * On-screen key registry — the PRODUCER side of the tiny cross-package\n * contract that `@verbumia/feedback` consumes via\n * `globalThis.__verbumia_key_registry__` (see `@verbumia/feedback`'s\n * `core/keys.ts`).\n *\n * Why this exists: the feedback widget must list only the translation\n * strings RENDERED on the current screen (spec ltm 373) — NOT every\n * project string. The widget can't know what's on screen; the i18n SDK\n * does, because it resolves the keys. So the SDK tracks the keys touched\n * by currently-MOUNTED `useTranslation`/`Trans` consumers and exposes\n * them through a minimal global. When a component unmounts (navigation),\n * its keys drop out automatically — so `snapshot()` is always exactly\n * \"what is on screen right now\". Explicit `keys` on the feedback plugin\n * stays a fallback for non-i18n strings; it is NOT \"pass everything\".\n *\n * The published shape is intentionally tiny so any framework port of the\n * i18n SDK can implement the same global without depending on feedback:\n *\n * globalThis.__verbumia_key_registry__ = {\n * snapshot(): { namespace: string; key: string }[];\n * reset(): void;\n * }\n */\n\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nconst GLOBAL = \"__verbumia_key_registry__\";\n// Internal id separator. NUL never appears in an i18next namespace or\n// key, so it round-trips even when a key itself contains ':'.\nconst SEP = \"\u0000\";\n\n/** Split an i18next-style `ns:key` (mirrors VerbumiaI18n._splitNamespace). */\nfunction split(fullKey: string, defaultNamespace: string): DeclaredKey {\n const idx = fullKey.indexOf(\":\");\n if (idx > 0) {\n return { namespace: fullKey.slice(0, idx), key: fullKey.slice(idx + 1) };\n }\n return { namespace: defaultNamespace, key: fullKey };\n}\n\nclass KeyRegistry {\n // One Set per mounted hook/Trans instance (keyed by an opaque token).\n // The on-screen set is the UNION of all live instances' latest render.\n private _instances = new Map<symbol, Set<string>>();\n // Provider mounts that have published us onto globalThis. Ref-counted so\n // a multi-provider tree (or fast unmount/remount in tests) never leaves\n // a stale global or unpublishes while another provider is still live.\n private _providers = 0;\n\n /** Replace an instance's contributed key set (called every render). */\n _set(token: symbol, keys: Set<string>): void {\n this._instances.set(token, keys);\n }\n\n /** Drop an instance entirely (called on unmount). */\n _delete(token: symbol): void {\n this._instances.delete(token);\n }\n\n /** Keys rendered by currently-mounted consumers. Stable insertion order. */\n snapshot(): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const set of this._instances.values()) {\n for (const id of set) {\n if (seen.has(id)) continue;\n seen.add(id);\n const c = id.indexOf(SEP);\n out.push({ namespace: id.slice(0, c), key: id.slice(c + 1) });\n }\n }\n return out;\n }\n\n /** Escape hatch (router integrations / tests). Mount-tracking already\n * handles navigation, so this is rarely needed. */\n reset(): void {\n this._instances.clear();\n }\n\n /** Encode a resolved key into the internal id used by `_set`. */\n encode(fullKey: string, defaultNamespace: string): string {\n const k = split(fullKey, defaultNamespace);\n return `${k.namespace}${SEP}${k.key}`;\n }\n\n /** Provider mounted — publish the global (idempotent, ref-counted). */\n attach(): void {\n this._providers += 1;\n if (this._providers === 1) {\n (globalThis as Record<string, unknown>)[GLOBAL] = {\n snapshot: () => this.snapshot(),\n reset: () => this.reset(),\n };\n }\n }\n\n /** Provider unmounted — unpublish when the last one goes away. */\n detach(): void {\n this._providers = Math.max(0, this._providers - 1);\n if (this._providers === 0) {\n this._instances.clear();\n const g = globalThis as Record<string, unknown>;\n if (g[GLOBAL]) delete g[GLOBAL];\n }\n }\n}\n\n/** Process-wide singleton — there is exactly one on-screen registry. */\nexport const keyRegistry = new KeyRegistry();\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { keyRegistry } from \"./key-registry\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.dev\";\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>(); // `${version}/${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${version}/${locale}/${ns}` keys we've fetched\n // Tighter gate than `_attempted`: this set only contains (version, locale, ns)\n // pairs whose CDN response was 200 with at least one top-level key. An\n // empty bundle (404 → {} OR 200 → {}) is treated as \"no data yet\";\n // calling t() against a key in such a bundle does NOT fire reportMissing.\n // Prevents the \"boot floods the dashboard\" failure when the project has\n // a brand-new namespace not yet published, OR when a network blip\n // produced an empty bundle.\n private _hasContent = new Set<string>();\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 version: string;\n env: \"prod\" | \"dev\";\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 // 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 // Fail-loud: realtime config moved out of core in 0.9.0. If a caller\n // still passes `liveUpdates` or ANY `centrifugo*` key, throw with a clear,\n // actionable migration message rather than silently no-op'ing (which would\n // stop realtime without anyone noticing).\n const removedRealtimeKeys = Object.keys(config).filter(\n (k) => k === \"liveUpdates\" || k.startsWith(\"centrifugo\"),\n );\n if (removedRealtimeKeys.length > 0) {\n throw new Error(\n `@verbumia/react-i18next: ${removedRealtimeKeys.join(\", \")} ${\n removedRealtimeKeys.length > 1 ? \"were\" : \"was\"\n } removed in 0.9.0 — realtime is now the @verbumia/realtime plugin. ` +\n \"Remove them and pass `plugins: [verbumiaRealtime({ wsUrl })]` to <VerbumiaProvider> instead.\",\n );\n }\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\n ? config.namespaces\n : config.defaultNS\n ? [config.defaultNS]\n : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n version: config.version ?? config.versionSlug ?? DEFAULT_VERSION_SLUG,\n env: config.env ?? \"prod\",\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 language: this.locale,\n setLocale: this.setLocale,\n changeLanguage: this.changeLanguage,\n t: this.t,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n reload: this.reload,\n };\n }\n\n /** Bundle cache-key builder. Includes `version` so providers with\n * different `version` values never share cached bundles. The segments\n * (version/locale/ns) are slugs and never contain '/'. */\n private _bundleKey(locale: Locale, ns: Namespace): string {\n return `${this._config.version}/${locale}/${ns}`;\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 /** Default namespace (the first configured one) — used to attribute a\n * bare `t(\"key\")` call when recording on-screen keys. */\n get defaultNamespace(): string {\n return this._config.namespaces[0]!;\n }\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n // Publish the on-screen key registry so a mounted feedback widget\n // lists only the strings rendered on the current view (spec ltm 373).\n keyRegistry.attach();\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 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 /** Alias of {@link setLocale} for react-i18next compatibility. */\n changeLanguage = (next: Locale): Promise<void> => this.setLocale(next);\n\n /** Alias of {@link locale} for react-i18next compatibility. */\n get language(): Locale {\n return this.locale;\n }\n\n stop(): void {\n keyRegistry.detach();\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n }\n\n /**\n * Bust-refetch already-loaded bundles and re-render once. Generic\n * replacement for the old realtime-only refetch: iterate the\n * `_attempted` cache keys (`${version}/${locale}/${ns}`), optionally\n * filtered by `opts.locale` / `opts.namespace`, and re-pull each one\n * with `{ bust: true }` so the mutable CDN `latest/` alias bypasses the\n * HTTP cache. After all settle, `_notify()` once so React re-renders.\n *\n * Used by `@verbumia/realtime` on a `translations_published` push and as\n * a manual refresh hook. If nothing matches, returns without notifying.\n */\n reload = async (\n opts: { locale?: Locale; namespace?: Namespace } = {},\n ): Promise<void> => {\n const targets: Array<{ locale: Locale; ns: Namespace }> = [];\n for (const key of this._attempted) {\n // `${version}/${locale}/${ns}` — none of the segments contain '/'.\n const parts = key.split(\"/\");\n const locale = parts[1];\n const ns = parts[2];\n if (!locale || !ns) continue;\n if (opts.locale && opts.locale !== locale) continue;\n if (opts.namespace && opts.namespace !== ns) continue;\n targets.push({ locale, ns });\n }\n if (targets.length === 0) return;\n await Promise.all(\n targets.map((t) => this._loadBundle(t.locale, t.ns, fetch, { bust: true })),\n );\n this._notify();\n };\n\n // ---- Translation ----\n\n t = (\n key: string,\n optionsOrDefault?:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | string,\n maybeOptions?: Record<string, unknown> & { defaultValue?: string; count?: number },\n ): string => {\n // react-i18next-style positional fallback: a string 2nd arg is the\n // default value. Optional 3rd arg carries interpolation/options and is\n // merged under it. `t(key, { defaultValue })` keeps working unchanged.\n const options:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | undefined =\n typeof optionsOrDefault === \"string\"\n ? { ...(maybeOptions ?? {}), defaultValue: optionsOrDefault }\n : optionsOrDefault;\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._bundleKey(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._bundleKey(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 // Three-condition gate: ready + attempted + bundle had content. The\n // last clause prevents flooding when the bundle came back empty (404\n // or {}); we'd be reporting against keys we never had a chance to\n // resolve. Master 2026-05-07 P0: see `_hasContent` doc.\n if (\n this.ready &&\n this._attempted.has(this._bundleKey(this.locale, ns)) &&\n this._hasContent.has(this._bundleKey(this.locale, ns))\n ) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: this._sourceValueFor(bareKey, ns, options),\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 opts: { bust?: boolean } = {}\n ): Promise<void> {\n const cacheKey = this._bundleKey(locale, ns);\n // env routing — prod hits the CDN cache; dev hits the live runtime\n // endpoint authenticated with the API key.\n let url: string;\n let init: RequestInit;\n if (this._config.env === \"dev\") {\n const params = new URLSearchParams({ language: locale, namespace: ns });\n if (this._config.version && this._config.version !== \"main\") {\n params.set(\"version_slug\", this._config.version);\n }\n url = `${this._config.apiBase.replace(/\\/+$/, \"\")}/v1/projects/${this._config.projectUuid}/translations/runtime?${params.toString()}`;\n init = {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${this._config.token}` },\n credentials: \"omit\",\n };\n } else {\n url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.version}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (opts.bust && hadContent) {\n // transient non-OK on a live refetch — keep prior content\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (opts.bust && hadContent) {\n // transient network error on a live refetch — keep prior content\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\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 /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (per backend agreement 2026-05-14, task 575):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. The bare key itself — last resort so dashboards never render\n * a blank `source_value` column.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return bareKey;\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 type { I18nInstance } from \"./types\";\nimport type { VerbumiaI18n } from \"./i18n\";\n\n// Active instance registered by the mounted <VerbumiaProvider>. Lets code\n// OUTSIDE React (utilities, stores, non-component modules) reach the i18n\n// instance the way react-i18next exposes its default singleton.\nlet _active: VerbumiaI18n | null = null;\n\n/** @internal — VerbumiaProvider registers its instance on mount. */\nexport function _setActiveInstance(instance: VerbumiaI18n): void {\n _active = instance;\n}\n\n/** @internal — VerbumiaProvider clears its instance on unmount. */\nexport function _clearActiveInstance(instance: VerbumiaI18n): void {\n if (_active === instance) _active = null;\n}\n\n/**\n * Access the active i18n instance OUTSIDE React components — the\n * react-i18next-style standalone singleton (e.g. for `t()`/`changeLanguage()`\n * in plain modules, stores, or helpers).\n *\n * Returns the instance created by the mounted `<VerbumiaProvider>`; throws a\n * clear error if no provider is mounted yet. Assumes a single app-wide\n * provider (the common case); with multiple concurrent providers it returns\n * the most recently mounted one.\n */\nexport function getI18n(): I18nInstance {\n if (!_active) {\n throw new Error(\n \"@verbumia/react-i18next: getI18n() was called before <VerbumiaProvider> mounted (no active i18n instance).\",\n );\n }\n return _active;\n}\n","import { useEffect, useMemo, useRef } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport { keyRegistry } from \"./key-registry\";\nimport type {\n I18nInstance,\n TranslationFunction,\n TranslationOptions,\n} 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.\n *\n * Every key this hook resolves during a render is recorded into the\n * on-screen key registry (so a mounted `@verbumia/feedback` widget lists\n * only the strings rendered on the current view — spec ltm 373). The\n * contribution is keyed to THIS hook instance and dropped on unmount, so\n * navigating away removes its keys automatically. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n\n // Keys resolved in the CURRENT render pass. The hook body runs before\n // the component's own `t()` calls, so clearing here yields a set that\n // reflects exactly this render once the component finishes.\n const renderedRef = useRef<Set<string>>(new Set());\n renderedRef.current = new Set<string>();\n // Opaque, stable token identifying this hook instance in the registry.\n const tokenRef = useRef<symbol>(Symbol(\"verbumia.t\"));\n\n const t = useMemo<TranslationFunction>(() => {\n // Forwards both call shapes — `t(key, opts)` and the react-i18next\n // positional `t(key, 'Default', opts?)` — straight to `i18n.t`, which\n // normalizes them. Registry tracking is keyed on the resolved fullKey.\n const fn = (\n key: string,\n optionsOrDefault?: TranslationOptions | string,\n maybeOptions?: TranslationOptions,\n ): string => {\n const fullKey =\n defaultNamespace && !key.includes(\":\")\n ? `${defaultNamespace}:${key}`\n : key;\n renderedRef.current.add(\n keyRegistry.encode(fullKey, i18n.defaultNamespace),\n );\n return i18n.t(fullKey, optionsOrDefault, maybeOptions);\n };\n return fn as TranslationFunction;\n }, [i18n, defaultNamespace]);\n\n // After every commit, publish this instance's latest rendered-key set.\n useEffect(() => {\n keyRegistry._set(tokenRef.current, renderedRef.current);\n });\n // Unmount only: drop this instance entirely so its keys leave the\n // on-screen snapshot when the component is gone (e.g. route change).\n useEffect(() => {\n const token = tokenRef.current;\n return () => keyRegistry._delete(token);\n }, []);\n\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":["useEffect","useMemo","useMemo","useEffect","Fragment","jsx"]}
1
+ {"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/key-registry.ts","../src/i18n.ts","../src/singleton.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["import {\n createContext,\n Fragment,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport { _clearActiveInstance, _setActiveInstance } from \"./singleton\";\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 // Register as the active instance so `getI18n()` works outside React.\n _setActiveInstance(i18n);\n void i18n.start();\n // Plugins (e.g. @verbumia/feedback) hook the SAME i18n instance —\n // no second context. setup() runs once; optional teardown on unmount.\n const teardowns = (config.plugins ?? [])\n .map((p) => p.setup?.({ i18n, config }))\n .filter((t): t is () => void => typeof t === \"function\");\n return () => {\n teardowns.forEach((t) => t());\n i18n.stop();\n _clearActiveInstance(i18n);\n };\n }, [i18n]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>\n {children}\n {/* Plugin outlets: isolated sibling leaves AFTER children. Their\n internal state never propagates to the host app subtree. */}\n {(config.plugins ?? []).map((p) =>\n p.render ? <Fragment key={p.name}>{p.render()}</Fragment> : null,\n )}\n </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\";\n// Replaced at build time by tsup `define` with the package.json version, so\n// `sdk_meta.ver` can never drift again. Falls back in non-bundled contexts\n// (e.g. unit tests, where the define isn't applied).\ndeclare const __SDK_VER__: string;\nconst SDK_VER =\n typeof __SDK_VER__ !== \"undefined\" ? __SDK_VER__ : \"0.0.0-dev\";\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 // Option A (#746): only send source_value when there's a real value;\n // omit it otherwise (never the key name). Absent = \"no default\".\n ...(e.source_value !== undefined\n ? { source_value: e.source_value }\n : {}),\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 * On-screen key registry — the PRODUCER side of the tiny cross-package\n * contract that `@verbumia/feedback` consumes via\n * `globalThis.__verbumia_key_registry__` (see `@verbumia/feedback`'s\n * `core/keys.ts`).\n *\n * Why this exists: the feedback widget must list only the translation\n * strings RENDERED on the current screen (spec ltm 373) — NOT every\n * project string. The widget can't know what's on screen; the i18n SDK\n * does, because it resolves the keys. So the SDK tracks the keys touched\n * by currently-MOUNTED `useTranslation`/`Trans` consumers and exposes\n * them through a minimal global. When a component unmounts (navigation),\n * its keys drop out automatically — so `snapshot()` is always exactly\n * \"what is on screen right now\". Explicit `keys` on the feedback plugin\n * stays a fallback for non-i18n strings; it is NOT \"pass everything\".\n *\n * The published shape is intentionally tiny so any framework port of the\n * i18n SDK can implement the same global without depending on feedback:\n *\n * globalThis.__verbumia_key_registry__ = {\n * snapshot(): { namespace: string; key: string }[];\n * reset(): void;\n * }\n */\n\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nconst GLOBAL = \"__verbumia_key_registry__\";\n// Internal id separator. NUL never appears in an i18next namespace or\n// key, so it round-trips even when a key itself contains ':'.\nconst SEP = \"\u0000\";\n\n/** Split an i18next-style `ns:key` (mirrors VerbumiaI18n._splitNamespace). */\nfunction split(fullKey: string, defaultNamespace: string): DeclaredKey {\n const idx = fullKey.indexOf(\":\");\n if (idx > 0) {\n return { namespace: fullKey.slice(0, idx), key: fullKey.slice(idx + 1) };\n }\n return { namespace: defaultNamespace, key: fullKey };\n}\n\nclass KeyRegistry {\n // One Set per mounted hook/Trans instance (keyed by an opaque token).\n // The on-screen set is the UNION of all live instances' latest render.\n private _instances = new Map<symbol, Set<string>>();\n // Provider mounts that have published us onto globalThis. Ref-counted so\n // a multi-provider tree (or fast unmount/remount in tests) never leaves\n // a stale global or unpublishes while another provider is still live.\n private _providers = 0;\n\n /** Replace an instance's contributed key set (called every render). */\n _set(token: symbol, keys: Set<string>): void {\n this._instances.set(token, keys);\n }\n\n /** Drop an instance entirely (called on unmount). */\n _delete(token: symbol): void {\n this._instances.delete(token);\n }\n\n /** Keys rendered by currently-mounted consumers. Stable insertion order. */\n snapshot(): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const set of this._instances.values()) {\n for (const id of set) {\n if (seen.has(id)) continue;\n seen.add(id);\n const c = id.indexOf(SEP);\n out.push({ namespace: id.slice(0, c), key: id.slice(c + 1) });\n }\n }\n return out;\n }\n\n /** Escape hatch (router integrations / tests). Mount-tracking already\n * handles navigation, so this is rarely needed. */\n reset(): void {\n this._instances.clear();\n }\n\n /** Encode a resolved key into the internal id used by `_set`. */\n encode(fullKey: string, defaultNamespace: string): string {\n const k = split(fullKey, defaultNamespace);\n return `${k.namespace}${SEP}${k.key}`;\n }\n\n /** Provider mounted — publish the global (idempotent, ref-counted). */\n attach(): void {\n this._providers += 1;\n if (this._providers === 1) {\n (globalThis as Record<string, unknown>)[GLOBAL] = {\n snapshot: () => this.snapshot(),\n reset: () => this.reset(),\n };\n }\n }\n\n /** Provider unmounted — unpublish when the last one goes away. */\n detach(): void {\n this._providers = Math.max(0, this._providers - 1);\n if (this._providers === 0) {\n this._instances.clear();\n const g = globalThis as Record<string, unknown>;\n if (g[GLOBAL]) delete g[GLOBAL];\n }\n }\n}\n\n/** Process-wide singleton — there is exactly one on-screen registry. */\nexport const keyRegistry = new KeyRegistry();\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { keyRegistry } from \"./key-registry\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.dev\";\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>(); // `${version}/${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${version}/${locale}/${ns}` keys we've fetched\n // Tighter gate than `_attempted`: this set only contains (version, locale, ns)\n // pairs whose CDN response was 200 with at least one top-level key. An\n // empty bundle (404 → {} OR 200 → {}) is treated as \"no data yet\";\n // calling t() against a key in such a bundle does NOT fire reportMissing.\n // Prevents the \"boot floods the dashboard\" failure when the project has\n // a brand-new namespace not yet published, OR when a network blip\n // produced an empty bundle.\n private _hasContent = new Set<string>();\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 version: string;\n env: \"prod\" | \"dev\";\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 // 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 // Fail-loud: realtime config moved out of core in 0.9.0. If a caller\n // still passes `liveUpdates` or ANY `centrifugo*` key, throw with a clear,\n // actionable migration message rather than silently no-op'ing (which would\n // stop realtime without anyone noticing).\n const removedRealtimeKeys = Object.keys(config).filter(\n (k) => k === \"liveUpdates\" || k.startsWith(\"centrifugo\"),\n );\n if (removedRealtimeKeys.length > 0) {\n throw new Error(\n `@verbumia/react-i18next: ${removedRealtimeKeys.join(\", \")} ${\n removedRealtimeKeys.length > 1 ? \"were\" : \"was\"\n } removed in 0.9.0 — realtime is now the @verbumia/realtime plugin. ` +\n \"Remove them and pass `plugins: [verbumiaRealtime({ wsUrl })]` to <VerbumiaProvider> instead.\",\n );\n }\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\n ? config.namespaces\n : config.defaultNS\n ? [config.defaultNS]\n : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n version: config.version ?? config.versionSlug ?? DEFAULT_VERSION_SLUG,\n env: config.env ?? \"prod\",\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 // Embedded build-time snapshot (#757): prime the cache synchronously so\n // the first render is instant and works OFFLINE before the first fetch,\n // then `start()` swaps in CDN/runtime data (no flash). We prime _bundles +\n // _hasContent but NOT _attempted — the missing-key gate stays closed until\n // a real fetch confirms, so keys absent from the snapshot don't generate\n // false \"missing\" reports in the meantime.\n if (config.initialBundles) {\n for (const [loc, byNs] of Object.entries(config.initialBundles)) {\n for (const [ns, tree] of Object.entries(byNs)) {\n if (!tree || typeof tree !== \"object\") continue;\n const key = this._bundleKey(loc, ns);\n this._bundles.set(key, tree as Bundle);\n if (Object.keys(tree).length > 0) this._hasContent.add(key);\n }\n }\n // ready=true only when the snapshot fully covers the active locale's\n // configured namespaces (otherwise wait for the fetch to flip it).\n const active = config.initialBundles[this.locale];\n if (\n active &&\n this._config.namespaces.every(\n (ns) => active[ns] && Object.keys(active[ns]!).length > 0,\n )\n ) {\n this.ready = true;\n }\n }\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 language: this.locale,\n setLocale: this.setLocale,\n changeLanguage: this.changeLanguage,\n t: this.t,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n reload: this.reload,\n };\n }\n\n /** Bundle cache-key builder. Includes `version` so providers with\n * different `version` values never share cached bundles. The segments\n * (version/locale/ns) are slugs and never contain '/'. */\n private _bundleKey(locale: Locale, ns: Namespace): string {\n return `${this._config.version}/${locale}/${ns}`;\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 /** Default namespace (the first configured one) — used to attribute a\n * bare `t(\"key\")` call when recording on-screen keys. */\n get defaultNamespace(): string {\n return this._config.namespaces[0]!;\n }\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n // Publish the on-screen key registry so a mounted feedback widget\n // lists only the strings rendered on the current view (spec ltm 373).\n keyRegistry.attach();\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 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 /** Alias of {@link setLocale} for react-i18next compatibility. */\n changeLanguage = (next: Locale): Promise<void> => this.setLocale(next);\n\n /** Alias of {@link locale} for react-i18next compatibility. */\n get language(): Locale {\n return this.locale;\n }\n\n stop(): void {\n keyRegistry.detach();\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n }\n\n /**\n * Bust-refetch already-loaded bundles and re-render once. Generic\n * replacement for the old realtime-only refetch: iterate the\n * `_attempted` cache keys (`${version}/${locale}/${ns}`), optionally\n * filtered by `opts.locale` / `opts.namespace`, and re-pull each one\n * with `{ bust: true }` so the mutable CDN `latest/` alias bypasses the\n * HTTP cache. After all settle, `_notify()` once so React re-renders.\n *\n * Used by `@verbumia/realtime` on a `translations_published` push and as\n * a manual refresh hook. If nothing matches, returns without notifying.\n */\n reload = async (\n opts: { locale?: Locale; namespace?: Namespace } = {},\n ): Promise<void> => {\n const targets: Array<{ locale: Locale; ns: Namespace }> = [];\n for (const key of this._attempted) {\n // `${version}/${locale}/${ns}` — none of the segments contain '/'.\n const parts = key.split(\"/\");\n const locale = parts[1];\n const ns = parts[2];\n if (!locale || !ns) continue;\n if (opts.locale && opts.locale !== locale) continue;\n if (opts.namespace && opts.namespace !== ns) continue;\n targets.push({ locale, ns });\n }\n if (targets.length === 0) return;\n await Promise.all(\n targets.map((t) => this._loadBundle(t.locale, t.ns, fetch, { bust: true })),\n );\n this._notify();\n };\n\n // ---- Translation ----\n\n t = (\n key: string,\n optionsOrDefault?:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | string,\n maybeOptions?: Record<string, unknown> & { defaultValue?: string; count?: number },\n ): string => {\n // react-i18next-style positional fallback: a string 2nd arg is the\n // default value. Optional 3rd arg carries interpolation/options and is\n // merged under it. `t(key, { defaultValue })` keeps working unchanged.\n const options:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | undefined =\n typeof optionsOrDefault === \"string\"\n ? { ...(maybeOptions ?? {}), defaultValue: optionsOrDefault }\n : optionsOrDefault;\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._bundleKey(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._bundleKey(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 // Three-condition gate: ready + attempted + bundle had content. The\n // last clause prevents flooding when the bundle came back empty (404\n // or {}); we'd be reporting against keys we never had a chance to\n // resolve. Master 2026-05-07 P0: see `_hasContent` doc.\n if (\n this.ready &&\n this._attempted.has(this._bundleKey(this.locale, ns)) &&\n this._hasContent.has(this._bundleKey(this.locale, ns))\n ) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: this._sourceValueFor(bareKey, ns, options),\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 opts: { bust?: boolean } = {}\n ): Promise<void> {\n const cacheKey = this._bundleKey(locale, ns);\n // env routing — prod hits the CDN cache; dev hits the live runtime\n // endpoint authenticated with the API key.\n let url: string;\n let init: RequestInit;\n if (this._config.env === \"dev\") {\n const params = new URLSearchParams({ language: locale, namespace: ns });\n if (this._config.version && this._config.version !== \"main\") {\n params.set(\"version_slug\", this._config.version);\n }\n url = `${this._config.apiBase.replace(/\\/+$/, \"\")}/v1/projects/${this._config.projectUuid}/translations/runtime?${params.toString()}`;\n init = {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${this._config.token}` },\n credentials: \"omit\",\n };\n } else {\n url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.version}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (hadContent) {\n // Non-OK but we already have content (an embedded snapshot or a prior\n // successful fetch) — keep that last-known-good instead of clobbering\n // it. Covers offline/transient first loads (#757) and refetch blips.\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (hadContent) {\n // Network error but we already have content (snapshot or prior fetch)\n // — keep last-known-good (offline-first, #757).\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\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 /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (Option A, task #746 — backend ingest aligned):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. Otherwise `undefined` — we DO NOT fall back to the key name (that\n * made the column unusable: a placeholder indistinguishable from a\n * real default). The key is already carried in `event.key`; an absent\n * `source_value` is the signal that there is no promotable value.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string | undefined {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return undefined;\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 type { I18nInstance } from \"./types\";\nimport type { VerbumiaI18n } from \"./i18n\";\n\n// Active instance registered by the mounted <VerbumiaProvider>. Lets code\n// OUTSIDE React (utilities, stores, non-component modules) reach the i18n\n// instance the way react-i18next exposes its default singleton.\nlet _active: VerbumiaI18n | null = null;\n\n/** @internal — VerbumiaProvider registers its instance on mount. */\nexport function _setActiveInstance(instance: VerbumiaI18n): void {\n _active = instance;\n}\n\n/** @internal — VerbumiaProvider clears its instance on unmount. */\nexport function _clearActiveInstance(instance: VerbumiaI18n): void {\n if (_active === instance) _active = null;\n}\n\n/**\n * Access the active i18n instance OUTSIDE React components — the\n * react-i18next-style standalone singleton (e.g. for `t()`/`changeLanguage()`\n * in plain modules, stores, or helpers).\n *\n * Returns the instance created by the mounted `<VerbumiaProvider>`; throws a\n * clear error if no provider is mounted yet. Assumes a single app-wide\n * provider (the common case); with multiple concurrent providers it returns\n * the most recently mounted one.\n */\nexport function getI18n(): I18nInstance {\n if (!_active) {\n throw new Error(\n \"@verbumia/react-i18next: getI18n() was called before <VerbumiaProvider> mounted (no active i18n instance).\",\n );\n }\n return _active;\n}\n","import { useEffect, useMemo, useRef } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport { keyRegistry } from \"./key-registry\";\nimport type {\n I18nInstance,\n TranslationFunction,\n TranslationOptions,\n} 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.\n *\n * Every key this hook resolves during a render is recorded into the\n * on-screen key registry (so a mounted `@verbumia/feedback` widget lists\n * only the strings rendered on the current view — spec ltm 373). The\n * contribution is keyed to THIS hook instance and dropped on unmount, so\n * navigating away removes its keys automatically. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n\n // Keys resolved in the CURRENT render pass. The hook body runs before\n // the component's own `t()` calls, so clearing here yields a set that\n // reflects exactly this render once the component finishes.\n const renderedRef = useRef<Set<string>>(new Set());\n renderedRef.current = new Set<string>();\n // Opaque, stable token identifying this hook instance in the registry.\n const tokenRef = useRef<symbol>(Symbol(\"verbumia.t\"));\n\n const t = useMemo<TranslationFunction>(() => {\n // Forwards both call shapes — `t(key, opts)` and the react-i18next\n // positional `t(key, 'Default', opts?)` — straight to `i18n.t`, which\n // normalizes them. Registry tracking is keyed on the resolved fullKey.\n const fn = (\n key: string,\n optionsOrDefault?: TranslationOptions | string,\n maybeOptions?: TranslationOptions,\n ): string => {\n const fullKey =\n defaultNamespace && !key.includes(\":\")\n ? `${defaultNamespace}:${key}`\n : key;\n renderedRef.current.add(\n keyRegistry.encode(fullKey, i18n.defaultNamespace),\n );\n return i18n.t(fullKey, optionsOrDefault, maybeOptions);\n };\n return fn as TranslationFunction;\n }, [i18n, defaultNamespace]);\n\n // After every commit, publish this instance's latest rendered-key set.\n useEffect(() => {\n keyRegistry._set(tokenRef.current, renderedRef.current);\n });\n // Unmount only: drop this instance entirely so its keys leave the\n // on-screen snapshot when the component is gone (e.g. route change).\n useEffect(() => {\n const token = tokenRef.current;\n return () => keyRegistry._delete(token);\n }, []);\n\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":["useEffect","useMemo","useMemo","useEffect","Fragment","jsx"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbumia/react-i18next",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "React SDK for Verbumia — translations + realtime missing-key handler.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://verbumia.ca",