@verbumia/react-i18next 0.9.1 → 0.11.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 +55 -0
- package/dist/index.cjs +73 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +73 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,9 @@ 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
|
+
keySeparator?: string | false; // false = flat (literal keys); else split (default '.'); auto-detected from the version when omitted
|
|
73
|
+
nsSeparator?: string | false; // 'ns:key' separator (default ':'); false to disable (keys may contain ':')
|
|
74
|
+
initialBundles?: Record<string, Record<string, object>>; // build-time snapshot (locale->ns->tree)
|
|
72
75
|
plugins?: VerbumiaPlugin[]; // e.g. @verbumia/feedback, @verbumia/realtime
|
|
73
76
|
transport?: (batch: MissingKeyEvent[]) => void | Promise<void>;
|
|
74
77
|
missingHandler?: 'send' | 'log' | 'off'; // default 'send'
|
|
@@ -183,6 +186,29 @@ interpolation only — it does **not** select plural keys (`key_one` /
|
|
|
183
186
|
|
|
184
187
|
---
|
|
185
188
|
|
|
189
|
+
## Flat vs nested keys
|
|
190
|
+
|
|
191
|
+
By default keys are **nested** and split on `.` — `t("hero.title")` reads
|
|
192
|
+
`{ hero: { title } }`. If your project stores **flat** keys (literal keys that
|
|
193
|
+
may contain dots, e.g. `"App Version 6.3.8"`), set `keySeparator={false}` so
|
|
194
|
+
keys are looked up verbatim:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
<VerbumiaProvider {...config} keySeparator={false}>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `keySeparator={false}` — flat (literal keys; dotted keys work, never split).
|
|
201
|
+
- `keySeparator="."` (default) or any string — nested, split on it.
|
|
202
|
+
- **Omitted** — the SDK auto-detects the project's `key_style` / `key_separator`
|
|
203
|
+
from the version metadata on mount (best-effort; needs an API key with
|
|
204
|
+
`project:read`, otherwise it falls back to nested `"."`). Set `keySeparator`
|
|
205
|
+
explicitly to skip that lookup and guarantee the style.
|
|
206
|
+
|
|
207
|
+
Resolution is **literal-first**: an exact `bundle[key]` always wins, so a dotted
|
|
208
|
+
key resolves even in nested mode without config (the nested split is the
|
|
209
|
+
fallback). The namespace separator is configurable too — `nsSeparator` (default
|
|
210
|
+
`":"`; `false` disables `"ns:key"` parsing so keys may contain `":"`).
|
|
211
|
+
|
|
186
212
|
## Missing-key flow
|
|
187
213
|
|
|
188
214
|
1. The user navigates a page that calls `t("hello.title")`.
|
|
@@ -281,6 +307,35 @@ each `translations_published` push. Realtime is a dev-version-only feature
|
|
|
281
307
|
|
|
282
308
|
---
|
|
283
309
|
|
|
310
|
+
## Offline / first-paint snapshot
|
|
311
|
+
|
|
312
|
+
Native apps (and SSR/web) can render real translations on the **first paint**
|
|
313
|
+
and **offline** — before the first CDN fetch — by embedding a build-time
|
|
314
|
+
snapshot:
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
import snapshot from "./verbumia-snapshot.json"; // { locale: { namespace: tree } }
|
|
318
|
+
|
|
319
|
+
<VerbumiaProvider {...config} initialBundles={snapshot}>
|
|
320
|
+
<App />
|
|
321
|
+
</VerbumiaProvider>;
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`initialBundles` is keyed `locale -> namespace -> tree` (the same shape as the
|
|
325
|
+
CDN JSON). It is primed synchronously, so `i18n.ready` is `true` on the very
|
|
326
|
+
first render when the snapshot covers the active locale's namespaces. On mount
|
|
327
|
+
the provider fetches the CDN and swaps in fresh values with **no flash**; if
|
|
328
|
+
that fetch fails (offline), the snapshot stays as last-known-good. Keys absent
|
|
329
|
+
from the snapshot do not fire "missing" reports until a real fetch confirms.
|
|
330
|
+
|
|
331
|
+
### Generating the snapshot
|
|
332
|
+
|
|
333
|
+
- **CLI (recommended):** `verbumia snapshot` (from `@verbumia/cli`) fetches the
|
|
334
|
+
current published bundles and writes the JSON module.
|
|
335
|
+
- **Manual:** fetch each
|
|
336
|
+
`https://cdn.verbumia.ca/p/<project>/<version>/latest/<locale>/<ns>.json` and
|
|
337
|
+
assemble them into `{ [locale]: { [namespace]: <tree> } }`, then import it.
|
|
338
|
+
|
|
284
339
|
## Recipes
|
|
285
340
|
|
|
286
341
|
### 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 = true ? "0.
|
|
38
|
+
var SDK_VER = true ? "0.11.0" : "0.0.0-dev";
|
|
39
39
|
function defaultTransport(opts) {
|
|
40
40
|
return async (batch) => {
|
|
41
41
|
if (!batch.length) return;
|
|
@@ -173,11 +173,14 @@ function isPluralForms(v) {
|
|
|
173
173
|
(k) => typeof v[k] === "string"
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
|
-
function resolve(bundle, key) {
|
|
176
|
+
function resolve(bundle, key, sep) {
|
|
177
177
|
if (!bundle) return void 0;
|
|
178
|
-
const
|
|
178
|
+
const literal = bundle[key];
|
|
179
|
+
if (typeof literal === "string") return literal;
|
|
180
|
+
if (isPluralForms(literal)) return literal;
|
|
181
|
+
if (sep === false || sep === "") return void 0;
|
|
179
182
|
let cur = bundle;
|
|
180
|
-
for (const p of
|
|
183
|
+
for (const p of key.split(sep)) {
|
|
181
184
|
if (cur && typeof cur === "object" && p in cur) {
|
|
182
185
|
cur = cur[p];
|
|
183
186
|
} else {
|
|
@@ -237,6 +240,15 @@ var VerbumiaI18n = class {
|
|
|
237
240
|
// it ONLY in _notify (when state actually changed) and return the cached
|
|
238
241
|
// reference between notifications.
|
|
239
242
|
_snapshot;
|
|
243
|
+
// Effective key separator (#754): false = flat (keys looked up literally, so
|
|
244
|
+
// dotted keys like "App Version 6.3.8" work), a string = nested (split on it).
|
|
245
|
+
// Set from config.keySeparator (explicit dev override, wins) or auto-detected
|
|
246
|
+
// from the version's key_style/key_separator on start(). Defaults to ".".
|
|
247
|
+
_keySeparator = ".";
|
|
248
|
+
_keySeparatorExplicit = false;
|
|
249
|
+
// Namespace separator (#754): false = no ns prefix parsing (the whole key is
|
|
250
|
+
// the bare key in the default namespace; allows keys containing ':'). Default ':'.
|
|
251
|
+
_nsSeparator = ":";
|
|
240
252
|
constructor(config) {
|
|
241
253
|
const removedRealtimeKeys = Object.keys(config).filter(
|
|
242
254
|
(k) => k === "liveUpdates" || k.startsWith("centrifugo")
|
|
@@ -248,6 +260,11 @@ var VerbumiaI18n = class {
|
|
|
248
260
|
}
|
|
249
261
|
this.locale = config.defaultLocale;
|
|
250
262
|
this.fallbackLng = config.fallbackLng;
|
|
263
|
+
if (config.keySeparator !== void 0) {
|
|
264
|
+
this._keySeparator = config.keySeparator;
|
|
265
|
+
this._keySeparatorExplicit = true;
|
|
266
|
+
}
|
|
267
|
+
if (config.nsSeparator !== void 0) this._nsSeparator = config.nsSeparator;
|
|
251
268
|
this._config = {
|
|
252
269
|
apiBase: config.apiBase ?? DEFAULT_API_BASE,
|
|
253
270
|
cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,
|
|
@@ -266,6 +283,22 @@ var VerbumiaI18n = class {
|
|
|
266
283
|
token: this._config.token,
|
|
267
284
|
projectUuid: this._config.projectUuid
|
|
268
285
|
}));
|
|
286
|
+
if (config.initialBundles) {
|
|
287
|
+
for (const [loc, byNs] of Object.entries(config.initialBundles)) {
|
|
288
|
+
for (const [ns, tree] of Object.entries(byNs)) {
|
|
289
|
+
if (!tree || typeof tree !== "object") continue;
|
|
290
|
+
const key = this._bundleKey(loc, ns);
|
|
291
|
+
this._bundles.set(key, tree);
|
|
292
|
+
if (Object.keys(tree).length > 0) this._hasContent.add(key);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const active = config.initialBundles[this.locale];
|
|
296
|
+
if (active && this._config.namespaces.every(
|
|
297
|
+
(ns) => active[ns] && Object.keys(active[ns]).length > 0
|
|
298
|
+
)) {
|
|
299
|
+
this.ready = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
269
302
|
this._snapshot = this._buildSnapshot();
|
|
270
303
|
}
|
|
271
304
|
// ---- React subscription ----
|
|
@@ -310,15 +343,36 @@ var VerbumiaI18n = class {
|
|
|
310
343
|
keyRegistry.attach();
|
|
311
344
|
const targets = /* @__PURE__ */ new Set([this.locale]);
|
|
312
345
|
if (this.fallbackLng) targets.add(this.fallbackLng);
|
|
313
|
-
await Promise.all(
|
|
314
|
-
[...targets].flatMap(
|
|
346
|
+
await Promise.all([
|
|
347
|
+
...[...targets].flatMap(
|
|
315
348
|
(loc) => this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))
|
|
316
|
-
)
|
|
317
|
-
|
|
349
|
+
),
|
|
350
|
+
// Best-effort: align the key separator with the version's key_style (#754).
|
|
351
|
+
this._loadKeyStyle(fetchImpl)
|
|
352
|
+
]);
|
|
318
353
|
this.ready = true;
|
|
319
354
|
this._startTimer();
|
|
320
355
|
this._notify();
|
|
321
356
|
}
|
|
357
|
+
/** Best-effort: read the version's `key_style` / `key_separator` (#754) so the
|
|
358
|
+
* SDK resolves keys the way the project's bundles are shaped. Skipped when
|
|
359
|
+
* the dev set `keySeparator` explicitly; on 403 (key lacks project:read),
|
|
360
|
+
* 404, or offline we keep the current separator. */
|
|
361
|
+
async _loadKeyStyle(fetchImpl) {
|
|
362
|
+
if (this._keySeparatorExplicit) return;
|
|
363
|
+
try {
|
|
364
|
+
const url = `${this._config.apiBase.replace(/\/+$/, "")}/v1/projects/${this._config.projectUuid}/versions/${encodeURIComponent(this._config.version)}`;
|
|
365
|
+
const r = await fetchImpl(url, {
|
|
366
|
+
method: "GET",
|
|
367
|
+
headers: { Authorization: `ApiKey ${this._config.token}` },
|
|
368
|
+
credentials: "omit"
|
|
369
|
+
});
|
|
370
|
+
if (!r.ok) return;
|
|
371
|
+
const meta = await r.json();
|
|
372
|
+
this._keySeparator = meta.key_style === "flat" ? false : meta.key_separator || ".";
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
322
376
|
setLocale = async (next) => {
|
|
323
377
|
if (next === this.locale) return;
|
|
324
378
|
this.locale = next;
|
|
@@ -377,12 +431,12 @@ var VerbumiaI18n = class {
|
|
|
377
431
|
const namespace = this._splitNamespace(key);
|
|
378
432
|
const bareKey = namespace.bareKey;
|
|
379
433
|
const ns = namespace.ns;
|
|
380
|
-
const fromActive = resolve(this._bundles.get(this._bundleKey(this.locale, ns)), bareKey);
|
|
434
|
+
const fromActive = resolve(this._bundles.get(this._bundleKey(this.locale, ns)), bareKey, this._keySeparator);
|
|
381
435
|
if (fromActive != null) {
|
|
382
436
|
return this._render(fromActive, this.locale, options);
|
|
383
437
|
}
|
|
384
438
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
385
|
-
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);
|
|
439
|
+
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey, this._keySeparator);
|
|
386
440
|
if (fb != null) {
|
|
387
441
|
return this._render(fb, this.fallbackLng, options);
|
|
388
442
|
}
|
|
@@ -427,9 +481,12 @@ var VerbumiaI18n = class {
|
|
|
427
481
|
return interpolate(str, options);
|
|
428
482
|
}
|
|
429
483
|
_splitNamespace(key) {
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
|
|
484
|
+
const sep = this._nsSeparator;
|
|
485
|
+
if (sep !== false && sep !== "") {
|
|
486
|
+
const idx = key.indexOf(sep);
|
|
487
|
+
if (idx > 0) {
|
|
488
|
+
return { ns: key.slice(0, idx), bareKey: key.slice(idx + sep.length) };
|
|
489
|
+
}
|
|
433
490
|
}
|
|
434
491
|
return { ns: this._config.namespaces[0], bareKey: key };
|
|
435
492
|
}
|
|
@@ -466,13 +523,13 @@ var VerbumiaI18n = class {
|
|
|
466
523
|
} else {
|
|
467
524
|
this._hasContent.delete(cacheKey);
|
|
468
525
|
}
|
|
469
|
-
} else if (
|
|
526
|
+
} else if (hadContent) {
|
|
470
527
|
} else {
|
|
471
528
|
this._bundles.set(cacheKey, {});
|
|
472
529
|
this._hasContent.delete(cacheKey);
|
|
473
530
|
}
|
|
474
531
|
} catch {
|
|
475
|
-
if (
|
|
532
|
+
if (hadContent) {
|
|
476
533
|
} else {
|
|
477
534
|
this._bundles.set(cacheKey, {});
|
|
478
535
|
this._hasContent.delete(cacheKey);
|
|
@@ -506,7 +563,7 @@ var VerbumiaI18n = class {
|
|
|
506
563
|
return options.defaultValue;
|
|
507
564
|
}
|
|
508
565
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
509
|
-
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);
|
|
566
|
+
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey, this._keySeparator);
|
|
510
567
|
if (typeof fb === "string") {
|
|
511
568
|
return fb;
|
|
512
569
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -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\";\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 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 (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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAQO;;;ACNP,IAAM,UAAU;AAKhB,IAAM,UACJ,OAAqC,UAAc;AAG9C,SAAS,iBAAiB,MAInB;AACZ,SAAO,OAAO,UAAU;AACtB,QAAI,CAAC,MAAM,OAAQ;AACnB,UAAM,OAAO;AAAA,MACX,cAAc,KAAK;AAAA,MACnB,QAAQ,MAAM,IAAI,CAAC,OAAO;AAAA,QACxB,KAAK,EAAE;AAAA,QACP,WAAW,EAAE;AAAA,QACb,eAAe,EAAE;AAAA;AAAA;AAAA,QAGjB,GAAI,EAAE,iBAAiB,SACnB,EAAE,cAAc,EAAE,aAAa,IAC/B,CAAC;AAAA,QACL,UAAU;AAAA,UACR,KAAK;AAAA,UACL,KAAK;AAAA,UACL,GAAI,OAAO,WAAW,cAClB,EAAE,KAAK,OAAO,UAAU,KAAK,IAC7B,CAAC;AAAA,UACL,GAAI,EAAE,YAAY,CAAC;AAAA,QACrB;AAAA,MACF,EAAE;AAAA,IACJ;AACA,QAAI;AACF,YAAM,MAAM,GAAG,KAAK,QAAQ,QAAQ,QAAQ,EAAE,CAAC,eAAe;AAAA,QAC5D,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,eAAe,UAAU,KAAK,KAAK;AAAA,QACrC;AAAA,QACA,MAAM,KAAK,UAAU,IAAI;AAAA;AAAA,QAEzB,WAAW;AAAA,MACb,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAGO,IAAM,eAA0B,CAAC,UAA6B;AACnE,aAAW,KAAK,OAAO;AAErB,YAAQ,KAAK,0BAA0B,CAAC;AAAA,EAC1C;AACF;;;AChCA,IAAM,SAAS;AAGf,IAAM,MAAM;AAGZ,SAAS,MAAM,SAAiB,kBAAuC;AACrE,QAAM,MAAM,QAAQ,QAAQ,GAAG;AAC/B,MAAI,MAAM,GAAG;AACX,WAAO,EAAE,WAAW,QAAQ,MAAM,GAAG,GAAG,GAAG,KAAK,QAAQ,MAAM,MAAM,CAAC,EAAE;AAAA,EACzE;AACA,SAAO,EAAE,WAAW,kBAAkB,KAAK,QAAQ;AACrD;AAEA,IAAM,cAAN,MAAkB;AAAA;AAAA;AAAA,EAGR,aAAa,oBAAI,IAAyB;AAAA;AAAA;AAAA;AAAA,EAI1C,aAAa;AAAA;AAAA,EAGrB,KAAK,OAAe,MAAyB;AAC3C,SAAK,WAAW,IAAI,OAAO,IAAI;AAAA,EACjC;AAAA;AAAA,EAGA,QAAQ,OAAqB;AAC3B,SAAK,WAAW,OAAO,KAAK;AAAA,EAC9B;AAAA;AAAA,EAGA,WAA0B;AACxB,UAAM,OAAO,oBAAI,IAAY;AAC7B,UAAM,MAAqB,CAAC;AAC5B,eAAW,OAAO,KAAK,WAAW,OAAO,GAAG;AAC1C,iBAAW,MAAM,KAAK;AACpB,YAAI,KAAK,IAAI,EAAE,EAAG;AAClB,aAAK,IAAI,EAAE;AACX,cAAM,IAAI,GAAG,QAAQ,GAAG;AACxB,YAAI,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,CAAC,GAAG,KAAK,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC;AAAA,MAC9D;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA,EAIA,QAAc;AACZ,SAAK,WAAW,MAAM;AAAA,EACxB;AAAA;AAAA,EAGA,OAAO,SAAiB,kBAAkC;AACxD,UAAM,IAAI,MAAM,SAAS,gBAAgB;AACzC,WAAO,GAAG,EAAE,SAAS,GAAG,GAAG,GAAG,EAAE,GAAG;AAAA,EACrC;AAAA;AAAA,EAGA,SAAe;AACb,SAAK,cAAc;AACnB,QAAI,KAAK,eAAe,GAAG;AACzB,MAAC,WAAuC,MAAM,IAAI;AAAA,QAChD,UAAU,MAAM,KAAK,SAAS;AAAA,QAC9B,OAAO,MAAM,KAAK,MAAM;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,SAAe;AACb,SAAK,aAAa,KAAK,IAAI,GAAG,KAAK,aAAa,CAAC;AACjD,QAAI,KAAK,eAAe,GAAG;AACzB,WAAK,WAAW,MAAM;AACtB,YAAM,IAAI;AACV,UAAI,EAAE,MAAM,EAAG,QAAO,EAAE,MAAM;AAAA,IAChC;AAAA,EACF;AACF;AAGO,IAAM,cAAc,IAAI,YAAY;;;ACtG3C,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AACvB,IAAM,uBAAuB;AAc7B,IAAM,kBAAkB,oBAAI,IAAY;AAAA,EACtC;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAQ;AACvC,CAAC;AAED,SAAS,cAAc,GAA8B;AACnD,MAAI,CAAC,KAAK,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,EAAG,QAAO;AAC5D,QAAM,OAAO,OAAO,KAAK,CAAW;AACpC,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI,CAAC,KAAK,KAAK,CAAC,MAAM,gBAAgB,IAAI,CAAC,CAAC,EAAG,QAAO;AACtD,SAAO,KAAK;AAAA,IACV,CAAC,MAAM,OAAQ,EAA8B,CAAC,MAAM;AAAA,EACtD;AACF;AAIA,SAAS,QAAQ,QAA4B,KAAwC;AACnF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,MAAI,MAAe;AACnB,aAAW,KAAK,OAAO;AACrB,QAAI,OAAO,OAAO,QAAQ,YAAY,KAAM,KAAiC;AAC3E,YAAO,IAAgC,CAAC;AAAA,IAC1C,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,cAAc,GAAG,EAAG,QAAO;AAC/B,SAAO;AACT;AAOA,SAAS,iBACP,OACA,OACA,QACQ;AACR,MAAI,WAAmB;AACvB,MAAI;AACF,QAAI,OAAO,SAAS,eAAe,OAAO,KAAK,gBAAgB,YAAY;AACzE,iBAAW,IAAI,KAAK,YAAY,MAAM,EAAE,OAAO,KAAK;AAAA,IACtD;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI,YAAY,MAAO,QAAO,MAAM,QAAQ;AAC5C,MAAI,WAAW,MAAO,QAAO,MAAM,OAAO;AAC1C,QAAM,QAAQ,OAAO,KAAK,KAAK,EAAE,CAAC;AAClC,SAAO,QAAQ,MAAM,KAAK,IAAK;AACjC;AAGA,SAAS,YAAY,UAAkB,SAA2C;AAChF,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,SAAS,QAAQ,kCAAkC,CAAC,IAAI,SAAS;AACtE,UAAM,IAAI,QAAQ,IAAI;AACtB,WAAO,KAAK,OAAO,KAAK,OAAO,CAAC;AAAA,EAClC,CAAC;AACH;AAIO,IAAM,eAAN,MAA2C;AAAA,EAChD,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,gBAAmC,CAAC;AAAA,EAE5B,WAAW,oBAAI,IAAoB;AAAA;AAAA,EACnC,aAAa,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ7B,cAAc,oBAAI,IAAY;AAAA,EAC9B;AAAA,EAaA;AAAA,EACA,WAA8B,CAAC;AAAA,EAC/B,QAAQ,oBAAI,IAAY;AAAA;AAAA,EACxB,SAAgD;AAAA,EAChD,aAAa,oBAAI,IAAc;AAAA;AAAA;AAAA;AAAA;AAAA,EAK/B;AAAA,EAER,YAAY,QAAwB;AAKlC,UAAM,sBAAsB,OAAO,KAAK,MAAM,EAAE;AAAA,MAC9C,CAAC,MAAM,MAAM,iBAAiB,EAAE,WAAW,YAAY;AAAA,IACzD;AACA,QAAI,oBAAoB,SAAS,GAAG;AAClC,YAAM,IAAI;AAAA,QACR,4BAA4B,oBAAoB,KAAK,IAAI,CAAC,IACxD,oBAAoB,SAAS,IAAI,SAAS,KAC5C;AAAA,MAEF;AAAA,IACF;AACA,SAAK,SAAS,OAAO;AACrB,SAAK,cAAc,OAAO;AAC1B,SAAK,UAAU;AAAA,MACb,SAAS,OAAO,WAAW;AAAA,MAC3B,SAAS,OAAO,WAAW;AAAA,MAC3B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,YAAY,OAAO,YAAY,SAC3B,OAAO,aACP,OAAO,YACL,CAAC,OAAO,SAAS,IACjB,CAAC,QAAQ;AAAA,MACf,iBAAiB,OAAO,mBAAmB;AAAA,MAC3C,gBAAgB,OAAO,kBAAkB;AAAA,MACzC,yBACE,OAAO,2BAA2B;AAAA,MACpC,SAAS,OAAO,WAAW,OAAO,eAAe;AAAA,MACjD,KAAK,OAAO,OAAO;AAAA,IACrB;AAEA,SAAK,aACH,OAAO,cACN,KAAK,QAAQ,mBAAmB,QAC7B,eACA,iBAAiB;AAAA,MACf,SAAS,KAAK,QAAQ;AAAA,MACtB,OAAO,KAAK,QAAQ;AAAA,MACpB,aAAa,KAAK,QAAQ;AAAA,IAC5B,CAAC;AACP,SAAK,YAAY,KAAK,eAAe;AAAA,EACvC;AAAA;AAAA,EAIA,YAAY,CAAC,aAAqC;AAChD,SAAK,WAAW,IAAI,QAAQ;AAC5B,WAAO,MAAM,KAAK,WAAW,OAAO,QAAQ;AAAA,EAC9C;AAAA;AAAA;AAAA,EAIA,cAAc,MAAoB,KAAK;AAAA,EAE/B,iBAA+B;AACrC,WAAO;AAAA,MACL,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,WAAW,KAAK;AAAA,MAChB,gBAAgB,KAAK;AAAA,MACrB,GAAG,KAAK;AAAA,MACR,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,MACnB,QAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,WAAW,QAAgB,IAAuB;AACxD,WAAO,GAAG,KAAK,QAAQ,OAAO,IAAI,MAAM,IAAI,EAAE;AAAA,EAChD;AAAA,EAEQ,UAAgB;AACtB,SAAK,YAAY,KAAK,eAAe;AACrC,eAAW,KAAK,KAAK,WAAY,GAAE;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,mBAA2B;AAC7B,WAAO,KAAK,QAAQ,WAAW,CAAC;AAAA,EAClC;AAAA;AAAA,EAGA,MAAM,MAAM,YAA0B,OAAsB;AAG1D,gBAAY,OAAO;AACnB,UAAM,UAAU,oBAAI,IAAY,CAAC,KAAK,MAAM,CAAC;AAC7C,QAAI,KAAK,YAAa,SAAQ,IAAI,KAAK,WAAW;AAClD,UAAM,QAAQ;AAAA,MACZ,CAAC,GAAG,OAAO,EAAE;AAAA,QAAQ,CAAC,QACpB,KAAK,QAAQ,WAAW,IAAI,CAAC,OAAO,KAAK,YAAY,KAAK,IAAI,SAAS,CAAC;AAAA,MAC1E;AAAA,IACF;AACA,SAAK,QAAQ;AACb,SAAK,YAAY;AACjB,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,YAAY,OAAO,SAAgC;AACjD,QAAI,SAAS,KAAK,OAAQ;AAC1B,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,QAAQ;AACb,UAAM,QAAQ;AAAA,MACZ,KAAK,QAAQ,WAAW,IAAI,CAAC,OAAO,KAAK,YAAY,MAAM,EAAE,CAAC;AAAA,IAChE;AACA,SAAK,QAAQ;AACb,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,iBAAiB,CAAC,SAAgC,KAAK,UAAU,IAAI;AAAA;AAAA,EAGrE,IAAI,WAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,OAAa;AACX,gBAAY,OAAO;AACnB,QAAI,KAAK,QAAQ;AACf,oBAAc,KAAK,MAAM;AACzB,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,SAAS,OACP,OAAmD,CAAC,MAClC;AAClB,UAAM,UAAoD,CAAC;AAC3D,eAAW,OAAO,KAAK,YAAY;AAEjC,YAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,YAAM,SAAS,MAAM,CAAC;AACtB,YAAM,KAAK,MAAM,CAAC;AAClB,UAAI,CAAC,UAAU,CAAC,GAAI;AACpB,UAAI,KAAK,UAAU,KAAK,WAAW,OAAQ;AAC3C,UAAI,KAAK,aAAa,KAAK,cAAc,GAAI;AAC7C,cAAQ,KAAK,EAAE,QAAQ,GAAG,CAAC;AAAA,IAC7B;AACA,QAAI,QAAQ,WAAW,EAAG;AAC1B,UAAM,QAAQ;AAAA,MACZ,QAAQ,IAAI,CAAC,MAAM,KAAK,YAAY,EAAE,QAAQ,EAAE,IAAI,OAAO,EAAE,MAAM,KAAK,CAAC,CAAC;AAAA,IAC5E;AACA,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAIA,IAAI,CACF,KACA,kBAGA,iBACW;AAIX,UAAM,UAGJ,OAAO,qBAAqB,WACxB,EAAE,GAAI,gBAAgB,CAAC,GAAI,cAAc,iBAAiB,IAC1D;AACN,UAAM,YAAY,KAAK,gBAAgB,GAAG;AAC1C,UAAM,UAAU,UAAU;AAC1B,UAAM,KAAK,UAAU;AAErB,UAAM,aAAa,QAAQ,KAAK,SAAS,IAAI,KAAK,WAAW,KAAK,QAAQ,EAAE,CAAC,GAAG,OAAO;AACvF,QAAI,cAAc,MAAM;AACtB,aAAO,KAAK,QAAQ,YAAY,KAAK,QAAQ,OAAO;AAAA,IACtD;AAEA,QAAI,KAAK,eAAe,KAAK,gBAAgB,KAAK,QAAQ;AACxD,YAAM,KAAK,QAAQ,KAAK,SAAS,IAAI,KAAK,WAAW,KAAK,aAAa,EAAE,CAAC,GAAG,OAAO;AACpF,UAAI,MAAM,MAAM;AACd,eAAO,KAAK,QAAQ,IAAI,KAAK,aAAa,OAAO;AAAA,MACnD;AAAA,IACF;AAQA,QACE,KAAK,SACL,KAAK,WAAW,IAAI,KAAK,WAAW,KAAK,QAAQ,EAAE,CAAC,KACpD,KAAK,YAAY,IAAI,KAAK,WAAW,KAAK,QAAQ,EAAE,CAAC,GACrD;AACA,WAAK,eAAe;AAAA,QAClB,KAAK;AAAA,QACL,WAAW;AAAA,QACX,eAAe,KAAK;AAAA,QACpB,cAAc,KAAK,gBAAgB,SAAS,IAAI,OAAO;AAAA,MACzD,CAAC;AAAA,IACH;AACA,UAAM,eAAe,SAAS;AAC9B,QAAI,OAAO,iBAAiB,UAAU;AACpC,aAAO,YAAY,cAAc,OAAO;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,eAAe,YAA2B;AACxC,QAAI,CAAC,KAAK,SAAS,OAAQ;AAC3B,UAAM,QAAQ,KAAK,SAAS,MAAM,CAAC;AACnC,SAAK,WAAW,CAAC;AACjB,QAAI,KAAK,QAAQ,mBAAmB,MAAO;AAC3C,QAAI;AACF,YAAM,KAAK,WAAW,KAAK;AAAA,IAC7B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,QACN,OACA,QACA,SACQ;AACR,QAAI;AACJ,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM;AAAA,IACR,OAAO;AACL,YAAM,QAAQ,OAAO,SAAS,UAAU,WAAW,QAAQ,QAAQ;AACnE,YAAM,iBAAiB,OAAO,OAAO,MAAM;AAAA,IAC7C;AACA,WAAO,YAAY,KAAK,OAAO;AAAA,EACjC;AAAA,EAEQ,gBAAgB,KAAiD;AAEvE,UAAM,MAAM,IAAI,QAAQ,GAAG;AAC3B,QAAI,MAAM,GAAG;AACX,aAAO,EAAE,IAAI,IAAI,MAAM,GAAG,GAAG,GAAG,SAAS,IAAI,MAAM,MAAM,CAAC,EAAE;AAAA,IAC9D;AACA,WAAO,EAAE,IAAI,KAAK,QAAQ,WAAW,CAAC,GAAI,SAAS,IAAI;AAAA,EACzD;AAAA,EAEA,MAAc,YACZ,QACA,IACA,YAA0B,OAC1B,OAA2B,CAAC,GACb;AACf,UAAM,WAAW,KAAK,WAAW,QAAQ,EAAE;AAG3C,QAAI;AACJ,QAAI;AACJ,QAAI,KAAK,QAAQ,QAAQ,OAAO;AAC9B,YAAM,SAAS,IAAI,gBAAgB,EAAE,UAAU,QAAQ,WAAW,GAAG,CAAC;AACtE,UAAI,KAAK,QAAQ,WAAW,KAAK,QAAQ,YAAY,QAAQ;AAC3D,eAAO,IAAI,gBAAgB,KAAK,QAAQ,OAAO;AAAA,MACjD;AACA,YAAM,GAAG,KAAK,QAAQ,QAAQ,QAAQ,QAAQ,EAAE,CAAC,gBAAgB,KAAK,QAAQ,WAAW,yBAAyB,OAAO,SAAS,CAAC;AACnI,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,EAAE,eAAe,UAAU,KAAK,QAAQ,KAAK,GAAG;AAAA,QACzD,aAAa;AAAA,MACf;AAAA,IACF,OAAO;AACL,YAAM,GAAG,KAAK,QAAQ,QAAQ,QAAQ,QAAQ,EAAE,CAAC,MAAM,KAAK,QAAQ,WAAW,IAAI,KAAK,QAAQ,OAAO,WAAW,MAAM,IAAI,EAAE;AAC9H,aAAO,EAAE,QAAQ,OAAO,aAAa,OAAO;AAAA,IAC9C;AAIA,QAAI,KAAK,MAAM;AACb,WAAK,QAAQ;AAAA,IACf;AAIA,UAAM,aAAa,KAAK,YAAY,IAAI,QAAQ;AAChD,QAAI;AACF,YAAM,IAAI,MAAM,UAAU,KAAK,IAAI;AACnC,UAAI,EAAE,IAAI;AACR,cAAM,OAAQ,MAAM,EAAE,KAAK;AAC3B,aAAK,SAAS,IAAI,UAAU,IAAI;AAChC,YAAI,QAAQ,OAAO,SAAS,YAAY,OAAO,KAAK,IAAI,EAAE,SAAS,GAAG;AACpE,eAAK,YAAY,IAAI,QAAQ;AAAA,QAC/B,OAAO;AACL,eAAK,YAAY,OAAO,QAAQ;AAAA,QAClC;AAAA,MACF,WAAW,KAAK,QAAQ,YAAY;AAAA,MAEpC,OAAO;AAIL,aAAK,SAAS,IAAI,UAAU,CAAC,CAAC;AAC9B,aAAK,YAAY,OAAO,QAAQ;AAAA,MAClC;AAAA,IACF,QAAQ;AACN,UAAI,KAAK,QAAQ,YAAY;AAAA,MAE7B,OAAO;AACL,aAAK,SAAS,IAAI,UAAU,CAAC,CAAC;AAC9B,aAAK,YAAY,OAAO,QAAQ;AAAA,MAClC;AAAA,IACF,UAAE;AACA,WAAK,WAAW,IAAI,QAAQ;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,cAAoB;AAC1B,QAAI,KAAK,QAAQ,mBAAmB,MAAO;AAC3C,QAAI,OAAO,gBAAgB,WAAY;AACvC,SAAK,SAAS,YAAY,MAAM;AAC9B,WAAK,KAAK,aAAa;AAAA,IACzB,GAAG,KAAK,QAAQ,eAAe;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeQ,gBACN,SACA,IACA,SACoB;AACpB,QAAI,OAAO,SAAS,iBAAiB,UAAU;AAC7C,aAAO,QAAQ;AAAA,IACjB;AACA,QAAI,KAAK,eAAe,KAAK,gBAAgB,KAAK,QAAQ;AACxD,YAAM,KAAK,QAAQ,KAAK,SAAS,IAAI,KAAK,WAAW,KAAK,aAAa,EAAE,CAAC,GAAG,OAAO;AACpF,UAAI,OAAO,OAAO,UAAU;AAC1B,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAe,OAA8B;AACnD,QAAI,KAAK,QAAQ,mBAAmB,MAAO;AAC3C,UAAM,WAAW,GAAG,MAAM,aAAa,IAAI,MAAM,SAAS,IAAI,MAAM,GAAG;AACvE,QAAI,KAAK,MAAM,IAAI,QAAQ,EAAG;AAC9B,SAAK,MAAM,IAAI,QAAQ;AAGvB,SAAK,gBAAgB,CAAC,OAAO,GAAG,KAAK,aAAa,EAAE;AAAA,MAClD;AAAA,MACA,KAAK,QAAQ;AAAA,IACf;AACA,SAAK,SAAS,KAAK,KAAK;AACxB,QAAI,KAAK,SAAS,UAAU,KAAK,QAAQ,gBAAgB;AACvD,WAAK,KAAK,aAAa;AAAA,IACzB;AACA,SAAK,QAAQ;AAAA,EACf;AACF;;;AC7gBA,IAAI,UAA+B;AAG5B,SAAS,mBAAmB,UAA8B;AAC/D,YAAU;AACZ;AAGO,SAAS,qBAAqB,UAA8B;AACjE,MAAI,YAAY,SAAU,WAAU;AACtC;AAYO,SAAS,UAAwB;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;AJaI;AA/BJ,IAAM,sBAAkB,4BAA2C,IAAI;AAMhE,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,GAAG;AACL,GAA0B;AAExB,QAAM,WAAO,sBAAQ,MAAM,IAAI,aAAa,MAAM,GAAG,CAAC,CAAC;AAEvD,8BAAU,MAAM;AAEd,uBAAmB,IAAI;AACvB,SAAK,KAAK,MAAM;AAGhB,UAAM,aAAa,OAAO,WAAW,CAAC,GACnC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,CAAC,EACtC,OAAO,CAAC,MAAuB,OAAO,MAAM,UAAU;AACzD,WAAO,MAAM;AACX,gBAAU,QAAQ,CAAC,MAAM,EAAE,CAAC;AAC5B,WAAK,KAAK;AACV,2BAAqB,IAAI;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,YAAQ,sBAA8B,OAAO,EAAE,KAAK,IAAI,CAAC,IAAI,CAAC;AACpE,SACE,6CAAC,gBAAgB,UAAhB,EAAyB,OACvB;AAAA;AAAA,KAGC,OAAO,WAAW,CAAC,GAAG;AAAA,MAAI,CAAC,MAC3B,EAAE,SAAS,4CAAC,yBAAuB,YAAE,OAAO,KAAlB,EAAE,IAAkB,IAAc;AAAA,IAC9D;AAAA,KACF;AAEJ;AAGO,SAAS,UAAwB;AACtC,QAAM,UAAM,yBAAW,eAAe;AACtC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACA,SAAO,IAAI;AACb;AAQO,SAAS,kBAAgC;AAC9C,QAAM,OAAO,QAAQ;AACrB,aAAO,mCAAqB,KAAK,WAAW,KAAK,aAAa,KAAK,WAAW;AAChF;;;AK7EA,IAAAA,gBAA2C;AAsBpC,SAAS,eAAe,kBAAiD;AAC9E,QAAM,OAAO,QAAQ;AACrB,QAAM,WAAW,gBAAgB;AAKjC,QAAM,kBAAc,sBAAoB,oBAAI,IAAI,CAAC;AACjD,cAAY,UAAU,oBAAI,IAAY;AAEtC,QAAM,eAAW,sBAAe,uBAAO,YAAY,CAAC;AAEpD,QAAM,QAAI,uBAA6B,MAAM;AAI3C,UAAM,KAAK,CACT,KACA,kBACA,iBACW;AACX,YAAM,UACJ,oBAAoB,CAAC,IAAI,SAAS,GAAG,IACjC,GAAG,gBAAgB,IAAI,GAAG,KAC1B;AACN,kBAAY,QAAQ;AAAA,QAClB,YAAY,OAAO,SAAS,KAAK,gBAAgB;AAAA,MACnD;AACA,aAAO,KAAK,EAAE,SAAS,kBAAkB,YAAY;AAAA,IACvD;AACA,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,gBAAgB,CAAC;AAG3B,+BAAU,MAAM;AACd,gBAAY,KAAK,SAAS,SAAS,YAAY,OAAO;AAAA,EACxD,CAAC;AAGD,+BAAU,MAAM;AACd,UAAM,QAAQ,SAAS;AACvB,WAAO,MAAM,YAAY,QAAQ,KAAK;AAAA,EACxC,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,GAAG,MAAM,SAAS;AAC7B;;;ACnEA,IAAAC,gBAAuE;AA6BvB,IAAAC,sBAAA;AATzC,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAe;AACb,QAAM,EAAE,EAAE,IAAI,eAAe,SAAS;AACtC,QAAM,MAAM,EAAE,SAAS,EAAE,GAAI,UAAU,CAAC,GAAI,cAAc,YAAY,QAAQ,CAAC;AAC/E,MAAI,CAAC,cAAc,CAAC,WAAW,OAAQ,QAAO,6EAAG,eAAI;AACrD,SAAO,6EAAG,4BAAkB,KAAK,UAAU,GAAE;AAC/C;AAEA,SAAS,kBAAkB,MAAc,YAAsC;AAC7E,QAAM,MAAmB,CAAC;AAE1B,QAAM,KAAK;AACX,MAAI,YAAY;AAChB,MAAI;AACJ,UAAQ,IAAI,GAAG,KAAK,IAAI,OAAO,MAAM;AACnC,QAAI,EAAE,QAAQ,UAAW,KAAI,KAAK,KAAK,MAAM,WAAW,EAAE,KAAK,CAAC;AAChE,UAAM,MAAM,OAAO,EAAE,CAAC,CAAC;AACvB,UAAM,QAAQ,EAAE,CAAC;AACjB,UAAM,OAAO,WAAW,GAAG;AAC3B,YAAI,8BAAe,IAAI,GAAG;AACxB,UAAI;AAAA,YACF,4BAAa,MAAM,EAAE,KAAK,KAAK,EAAE,KAAK,GAAG,GAAG,GAAG,uBAAS,QAAQ,SAAS,EAAE,CAAC;AAAA,MAC9E;AAAA,IACF,WAAW,SAAS,QAAW;AAC7B,UAAI,KAAK,IAAI;AAAA,IACf,OAAO;AACL,UAAI,KAAK,SAAS,EAAE;AAAA,IACtB;AACA,gBAAY,GAAG;AAAA,EACjB;AACA,MAAI,YAAY,KAAK,OAAQ,KAAI,KAAK,KAAK,MAAM,SAAS,CAAC;AAC3D,SAAO;AACT;","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 key against a bundle.\n * LITERAL-FIRST: an exact `bundle[key]` always wins — so dotted keys like\n * \"App Version 6.3.8\" resolve even in nested mode and without config. If the\n * literal misses AND `sep` is a string (nested), the key is split on `sep` and\n * the tree is walked. With `sep === false` (flat) only the literal is tried.\n * Returns a plain string OR a CLDR plural-forms dict so the caller can pick. */\nfunction resolve(\n bundle: Bundle | undefined,\n key: string,\n sep: string | false,\n): ResolvedValue | undefined {\n if (!bundle) return undefined;\n // literal-first\n const literal = (bundle as Record<string, unknown>)[key];\n if (typeof literal === \"string\") return literal;\n if (isPluralForms(literal)) return literal;\n // flat: stop at the literal lookup; nested: split + walk the tree.\n if (sep === false || sep === \"\") return undefined;\n let cur: unknown = bundle;\n for (const p of key.split(sep)) {\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 // Effective key separator (#754): false = flat (keys looked up literally, so\n // dotted keys like \"App Version 6.3.8\" work), a string = nested (split on it).\n // Set from config.keySeparator (explicit dev override, wins) or auto-detected\n // from the version's key_style/key_separator on start(). Defaults to \".\".\n private _keySeparator: string | false = \".\";\n private _keySeparatorExplicit = false;\n // Namespace separator (#754): false = no ns prefix parsing (the whole key is\n // the bare key in the default namespace; allows keys containing ':'). Default ':'.\n private _nsSeparator: string | false = \":\";\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 if (config.keySeparator !== undefined) {\n // Explicit override wins and skips the version-metadata lookup.\n this._keySeparator = config.keySeparator;\n this._keySeparatorExplicit = true;\n }\n if (config.nsSeparator !== undefined) this._nsSeparator = config.nsSeparator;\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 // Best-effort: align the key separator with the version's key_style (#754).\n this._loadKeyStyle(fetchImpl),\n ]);\n this.ready = true;\n this._startTimer();\n this._notify();\n }\n\n /** Best-effort: read the version's `key_style` / `key_separator` (#754) so the\n * SDK resolves keys the way the project's bundles are shaped. Skipped when\n * the dev set `keySeparator` explicitly; on 403 (key lacks project:read),\n * 404, or offline we keep the current separator. */\n private async _loadKeyStyle(fetchImpl: typeof fetch): Promise<void> {\n if (this._keySeparatorExplicit) return;\n try {\n const url = `${this._config.apiBase.replace(/\\/+$/, \"\")}/v1/projects/${this._config.projectUuid}/versions/${encodeURIComponent(this._config.version)}`;\n const r = await fetchImpl(url, {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${this._config.token}` },\n credentials: \"omit\",\n });\n if (!r.ok) return;\n const meta = (await r.json()) as {\n key_style?: string;\n key_separator?: string;\n };\n this._keySeparator =\n meta.key_style === \"flat\" ? false : meta.key_separator || \".\";\n } catch {\n // best-effort — keep the default / last-known separator\n }\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, this._keySeparator);\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, this._keySeparator);\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\" — honoring the configured nsSeparator.\n // `false`/\"\" disables parsing so keys may contain ':'.\n const sep = this._nsSeparator;\n if (sep !== false && sep !== \"\") {\n const idx = key.indexOf(sep);\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + sep.length) };\n }\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, this._keySeparator);\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
|
@@ -33,6 +33,30 @@ interface VerbumiaConfig {
|
|
|
33
33
|
* first entry is the default namespace).
|
|
34
34
|
*/
|
|
35
35
|
defaultNS?: Namespace;
|
|
36
|
+
/**
|
|
37
|
+
* How keys map to the bundle structure. `false` = **flat** (keys are looked
|
|
38
|
+
* up literally, so dotted keys like `"App Version 6.3.8"` work); a string =
|
|
39
|
+
* **nested**, split on that separator (default `"."`). Explicit value here is
|
|
40
|
+
* an override; when omitted, the SDK auto-detects the project's
|
|
41
|
+
* `key_style` / `key_separator` from the version metadata (#754).
|
|
42
|
+
*/
|
|
43
|
+
keySeparator?: string | false;
|
|
44
|
+
/**
|
|
45
|
+
* Separator between namespace and key in `t("ns:key")`. Default `":"`.
|
|
46
|
+
* Set `false` (or `""`) to disable namespace parsing so keys may contain
|
|
47
|
+
* `":"`. i18next-parity companion to `keySeparator`.
|
|
48
|
+
*/
|
|
49
|
+
nsSeparator?: string | false;
|
|
50
|
+
/**
|
|
51
|
+
* Embedded build-time snapshot of translation bundles, keyed
|
|
52
|
+
* `locale -> namespace -> i18next tree` (the same shape as the CDN JSON).
|
|
53
|
+
* Primed synchronously on construction so the FIRST render is instant and
|
|
54
|
+
* works OFFLINE (before the first CDN/runtime fetch); the provider then
|
|
55
|
+
* swaps in fresh data when it arrives, with no flash. A failed initial fetch
|
|
56
|
+
* (offline) keeps the snapshot as last-known-good. Generate it with the
|
|
57
|
+
* `verbumia snapshot` CLI, or manually (fetch the CDN JSON and import it).
|
|
58
|
+
*/
|
|
59
|
+
initialBundles?: Record<Locale, Record<Namespace, Record<string, unknown>>>;
|
|
36
60
|
/** Initial locale (BCP-47). */
|
|
37
61
|
defaultLocale: Locale;
|
|
38
62
|
/** Fallback locale used when a key is missing in `defaultLocale`. */
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,30 @@ interface VerbumiaConfig {
|
|
|
33
33
|
* first entry is the default namespace).
|
|
34
34
|
*/
|
|
35
35
|
defaultNS?: Namespace;
|
|
36
|
+
/**
|
|
37
|
+
* How keys map to the bundle structure. `false` = **flat** (keys are looked
|
|
38
|
+
* up literally, so dotted keys like `"App Version 6.3.8"` work); a string =
|
|
39
|
+
* **nested**, split on that separator (default `"."`). Explicit value here is
|
|
40
|
+
* an override; when omitted, the SDK auto-detects the project's
|
|
41
|
+
* `key_style` / `key_separator` from the version metadata (#754).
|
|
42
|
+
*/
|
|
43
|
+
keySeparator?: string | false;
|
|
44
|
+
/**
|
|
45
|
+
* Separator between namespace and key in `t("ns:key")`. Default `":"`.
|
|
46
|
+
* Set `false` (or `""`) to disable namespace parsing so keys may contain
|
|
47
|
+
* `":"`. i18next-parity companion to `keySeparator`.
|
|
48
|
+
*/
|
|
49
|
+
nsSeparator?: string | false;
|
|
50
|
+
/**
|
|
51
|
+
* Embedded build-time snapshot of translation bundles, keyed
|
|
52
|
+
* `locale -> namespace -> i18next tree` (the same shape as the CDN JSON).
|
|
53
|
+
* Primed synchronously on construction so the FIRST render is instant and
|
|
54
|
+
* works OFFLINE (before the first CDN/runtime fetch); the provider then
|
|
55
|
+
* swaps in fresh data when it arrives, with no flash. A failed initial fetch
|
|
56
|
+
* (offline) keeps the snapshot as last-known-good. Generate it with the
|
|
57
|
+
* `verbumia snapshot` CLI, or manually (fetch the CDN JSON and import it).
|
|
58
|
+
*/
|
|
59
|
+
initialBundles?: Record<Locale, Record<Namespace, Record<string, unknown>>>;
|
|
36
60
|
/** Initial locale (BCP-47). */
|
|
37
61
|
defaultLocale: Locale;
|
|
38
62
|
/** 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 = true ? "0.
|
|
13
|
+
var SDK_VER = true ? "0.11.0" : "0.0.0-dev";
|
|
14
14
|
function defaultTransport(opts) {
|
|
15
15
|
return async (batch) => {
|
|
16
16
|
if (!batch.length) return;
|
|
@@ -148,11 +148,14 @@ function isPluralForms(v) {
|
|
|
148
148
|
(k) => typeof v[k] === "string"
|
|
149
149
|
);
|
|
150
150
|
}
|
|
151
|
-
function resolve(bundle, key) {
|
|
151
|
+
function resolve(bundle, key, sep) {
|
|
152
152
|
if (!bundle) return void 0;
|
|
153
|
-
const
|
|
153
|
+
const literal = bundle[key];
|
|
154
|
+
if (typeof literal === "string") return literal;
|
|
155
|
+
if (isPluralForms(literal)) return literal;
|
|
156
|
+
if (sep === false || sep === "") return void 0;
|
|
154
157
|
let cur = bundle;
|
|
155
|
-
for (const p of
|
|
158
|
+
for (const p of key.split(sep)) {
|
|
156
159
|
if (cur && typeof cur === "object" && p in cur) {
|
|
157
160
|
cur = cur[p];
|
|
158
161
|
} else {
|
|
@@ -212,6 +215,15 @@ var VerbumiaI18n = class {
|
|
|
212
215
|
// it ONLY in _notify (when state actually changed) and return the cached
|
|
213
216
|
// reference between notifications.
|
|
214
217
|
_snapshot;
|
|
218
|
+
// Effective key separator (#754): false = flat (keys looked up literally, so
|
|
219
|
+
// dotted keys like "App Version 6.3.8" work), a string = nested (split on it).
|
|
220
|
+
// Set from config.keySeparator (explicit dev override, wins) or auto-detected
|
|
221
|
+
// from the version's key_style/key_separator on start(). Defaults to ".".
|
|
222
|
+
_keySeparator = ".";
|
|
223
|
+
_keySeparatorExplicit = false;
|
|
224
|
+
// Namespace separator (#754): false = no ns prefix parsing (the whole key is
|
|
225
|
+
// the bare key in the default namespace; allows keys containing ':'). Default ':'.
|
|
226
|
+
_nsSeparator = ":";
|
|
215
227
|
constructor(config) {
|
|
216
228
|
const removedRealtimeKeys = Object.keys(config).filter(
|
|
217
229
|
(k) => k === "liveUpdates" || k.startsWith("centrifugo")
|
|
@@ -223,6 +235,11 @@ var VerbumiaI18n = class {
|
|
|
223
235
|
}
|
|
224
236
|
this.locale = config.defaultLocale;
|
|
225
237
|
this.fallbackLng = config.fallbackLng;
|
|
238
|
+
if (config.keySeparator !== void 0) {
|
|
239
|
+
this._keySeparator = config.keySeparator;
|
|
240
|
+
this._keySeparatorExplicit = true;
|
|
241
|
+
}
|
|
242
|
+
if (config.nsSeparator !== void 0) this._nsSeparator = config.nsSeparator;
|
|
226
243
|
this._config = {
|
|
227
244
|
apiBase: config.apiBase ?? DEFAULT_API_BASE,
|
|
228
245
|
cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,
|
|
@@ -241,6 +258,22 @@ var VerbumiaI18n = class {
|
|
|
241
258
|
token: this._config.token,
|
|
242
259
|
projectUuid: this._config.projectUuid
|
|
243
260
|
}));
|
|
261
|
+
if (config.initialBundles) {
|
|
262
|
+
for (const [loc, byNs] of Object.entries(config.initialBundles)) {
|
|
263
|
+
for (const [ns, tree] of Object.entries(byNs)) {
|
|
264
|
+
if (!tree || typeof tree !== "object") continue;
|
|
265
|
+
const key = this._bundleKey(loc, ns);
|
|
266
|
+
this._bundles.set(key, tree);
|
|
267
|
+
if (Object.keys(tree).length > 0) this._hasContent.add(key);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const active = config.initialBundles[this.locale];
|
|
271
|
+
if (active && this._config.namespaces.every(
|
|
272
|
+
(ns) => active[ns] && Object.keys(active[ns]).length > 0
|
|
273
|
+
)) {
|
|
274
|
+
this.ready = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
244
277
|
this._snapshot = this._buildSnapshot();
|
|
245
278
|
}
|
|
246
279
|
// ---- React subscription ----
|
|
@@ -285,15 +318,36 @@ var VerbumiaI18n = class {
|
|
|
285
318
|
keyRegistry.attach();
|
|
286
319
|
const targets = /* @__PURE__ */ new Set([this.locale]);
|
|
287
320
|
if (this.fallbackLng) targets.add(this.fallbackLng);
|
|
288
|
-
await Promise.all(
|
|
289
|
-
[...targets].flatMap(
|
|
321
|
+
await Promise.all([
|
|
322
|
+
...[...targets].flatMap(
|
|
290
323
|
(loc) => this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))
|
|
291
|
-
)
|
|
292
|
-
|
|
324
|
+
),
|
|
325
|
+
// Best-effort: align the key separator with the version's key_style (#754).
|
|
326
|
+
this._loadKeyStyle(fetchImpl)
|
|
327
|
+
]);
|
|
293
328
|
this.ready = true;
|
|
294
329
|
this._startTimer();
|
|
295
330
|
this._notify();
|
|
296
331
|
}
|
|
332
|
+
/** Best-effort: read the version's `key_style` / `key_separator` (#754) so the
|
|
333
|
+
* SDK resolves keys the way the project's bundles are shaped. Skipped when
|
|
334
|
+
* the dev set `keySeparator` explicitly; on 403 (key lacks project:read),
|
|
335
|
+
* 404, or offline we keep the current separator. */
|
|
336
|
+
async _loadKeyStyle(fetchImpl) {
|
|
337
|
+
if (this._keySeparatorExplicit) return;
|
|
338
|
+
try {
|
|
339
|
+
const url = `${this._config.apiBase.replace(/\/+$/, "")}/v1/projects/${this._config.projectUuid}/versions/${encodeURIComponent(this._config.version)}`;
|
|
340
|
+
const r = await fetchImpl(url, {
|
|
341
|
+
method: "GET",
|
|
342
|
+
headers: { Authorization: `ApiKey ${this._config.token}` },
|
|
343
|
+
credentials: "omit"
|
|
344
|
+
});
|
|
345
|
+
if (!r.ok) return;
|
|
346
|
+
const meta = await r.json();
|
|
347
|
+
this._keySeparator = meta.key_style === "flat" ? false : meta.key_separator || ".";
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
}
|
|
297
351
|
setLocale = async (next) => {
|
|
298
352
|
if (next === this.locale) return;
|
|
299
353
|
this.locale = next;
|
|
@@ -352,12 +406,12 @@ var VerbumiaI18n = class {
|
|
|
352
406
|
const namespace = this._splitNamespace(key);
|
|
353
407
|
const bareKey = namespace.bareKey;
|
|
354
408
|
const ns = namespace.ns;
|
|
355
|
-
const fromActive = resolve(this._bundles.get(this._bundleKey(this.locale, ns)), bareKey);
|
|
409
|
+
const fromActive = resolve(this._bundles.get(this._bundleKey(this.locale, ns)), bareKey, this._keySeparator);
|
|
356
410
|
if (fromActive != null) {
|
|
357
411
|
return this._render(fromActive, this.locale, options);
|
|
358
412
|
}
|
|
359
413
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
360
|
-
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);
|
|
414
|
+
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey, this._keySeparator);
|
|
361
415
|
if (fb != null) {
|
|
362
416
|
return this._render(fb, this.fallbackLng, options);
|
|
363
417
|
}
|
|
@@ -402,9 +456,12 @@ var VerbumiaI18n = class {
|
|
|
402
456
|
return interpolate(str, options);
|
|
403
457
|
}
|
|
404
458
|
_splitNamespace(key) {
|
|
405
|
-
const
|
|
406
|
-
if (
|
|
407
|
-
|
|
459
|
+
const sep = this._nsSeparator;
|
|
460
|
+
if (sep !== false && sep !== "") {
|
|
461
|
+
const idx = key.indexOf(sep);
|
|
462
|
+
if (idx > 0) {
|
|
463
|
+
return { ns: key.slice(0, idx), bareKey: key.slice(idx + sep.length) };
|
|
464
|
+
}
|
|
408
465
|
}
|
|
409
466
|
return { ns: this._config.namespaces[0], bareKey: key };
|
|
410
467
|
}
|
|
@@ -441,13 +498,13 @@ var VerbumiaI18n = class {
|
|
|
441
498
|
} else {
|
|
442
499
|
this._hasContent.delete(cacheKey);
|
|
443
500
|
}
|
|
444
|
-
} else if (
|
|
501
|
+
} else if (hadContent) {
|
|
445
502
|
} else {
|
|
446
503
|
this._bundles.set(cacheKey, {});
|
|
447
504
|
this._hasContent.delete(cacheKey);
|
|
448
505
|
}
|
|
449
506
|
} catch {
|
|
450
|
-
if (
|
|
507
|
+
if (hadContent) {
|
|
451
508
|
} else {
|
|
452
509
|
this._bundles.set(cacheKey, {});
|
|
453
510
|
this._hasContent.delete(cacheKey);
|
|
@@ -481,7 +538,7 @@ var VerbumiaI18n = class {
|
|
|
481
538
|
return options.defaultValue;
|
|
482
539
|
}
|
|
483
540
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
484
|
-
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);
|
|
541
|
+
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey, this._keySeparator);
|
|
485
542
|
if (typeof fb === "string") {
|
|
486
543
|
return fb;
|
|
487
544
|
}
|
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\";\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 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 (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"]}
|
|
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 key against a bundle.\n * LITERAL-FIRST: an exact `bundle[key]` always wins — so dotted keys like\n * \"App Version 6.3.8\" resolve even in nested mode and without config. If the\n * literal misses AND `sep` is a string (nested), the key is split on `sep` and\n * the tree is walked. With `sep === false` (flat) only the literal is tried.\n * Returns a plain string OR a CLDR plural-forms dict so the caller can pick. */\nfunction resolve(\n bundle: Bundle | undefined,\n key: string,\n sep: string | false,\n): ResolvedValue | undefined {\n if (!bundle) return undefined;\n // literal-first\n const literal = (bundle as Record<string, unknown>)[key];\n if (typeof literal === \"string\") return literal;\n if (isPluralForms(literal)) return literal;\n // flat: stop at the literal lookup; nested: split + walk the tree.\n if (sep === false || sep === \"\") return undefined;\n let cur: unknown = bundle;\n for (const p of key.split(sep)) {\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 // Effective key separator (#754): false = flat (keys looked up literally, so\n // dotted keys like \"App Version 6.3.8\" work), a string = nested (split on it).\n // Set from config.keySeparator (explicit dev override, wins) or auto-detected\n // from the version's key_style/key_separator on start(). Defaults to \".\".\n private _keySeparator: string | false = \".\";\n private _keySeparatorExplicit = false;\n // Namespace separator (#754): false = no ns prefix parsing (the whole key is\n // the bare key in the default namespace; allows keys containing ':'). Default ':'.\n private _nsSeparator: string | false = \":\";\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 if (config.keySeparator !== undefined) {\n // Explicit override wins and skips the version-metadata lookup.\n this._keySeparator = config.keySeparator;\n this._keySeparatorExplicit = true;\n }\n if (config.nsSeparator !== undefined) this._nsSeparator = config.nsSeparator;\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 // Best-effort: align the key separator with the version's key_style (#754).\n this._loadKeyStyle(fetchImpl),\n ]);\n this.ready = true;\n this._startTimer();\n this._notify();\n }\n\n /** Best-effort: read the version's `key_style` / `key_separator` (#754) so the\n * SDK resolves keys the way the project's bundles are shaped. Skipped when\n * the dev set `keySeparator` explicitly; on 403 (key lacks project:read),\n * 404, or offline we keep the current separator. */\n private async _loadKeyStyle(fetchImpl: typeof fetch): Promise<void> {\n if (this._keySeparatorExplicit) return;\n try {\n const url = `${this._config.apiBase.replace(/\\/+$/, \"\")}/v1/projects/${this._config.projectUuid}/versions/${encodeURIComponent(this._config.version)}`;\n const r = await fetchImpl(url, {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${this._config.token}` },\n credentials: \"omit\",\n });\n if (!r.ok) return;\n const meta = (await r.json()) as {\n key_style?: string;\n key_separator?: string;\n };\n this._keySeparator =\n meta.key_style === \"flat\" ? false : meta.key_separator || \".\";\n } catch {\n // best-effort — keep the default / last-known separator\n }\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, this._keySeparator);\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, this._keySeparator);\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\" — honoring the configured nsSeparator.\n // `false`/\"\" disables parsing so keys may contain ':'.\n const sep = this._nsSeparator;\n if (sep !== false && sep !== \"\") {\n const idx = key.indexOf(sep);\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + sep.length) };\n }\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, this._keySeparator);\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"]}
|