@verbumia/react-i18next 0.10.0 → 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 +25 -0
- package/dist/index.cjs +55 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +55 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,8 @@ 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 ':')
|
|
72
74
|
initialBundles?: Record<string, Record<string, object>>; // build-time snapshot (locale->ns->tree)
|
|
73
75
|
plugins?: VerbumiaPlugin[]; // e.g. @verbumia/feedback, @verbumia/realtime
|
|
74
76
|
transport?: (batch: MissingKeyEvent[]) => void | Promise<void>;
|
|
@@ -184,6 +186,29 @@ interpolation only — it does **not** select plural keys (`key_one` /
|
|
|
184
186
|
|
|
185
187
|
---
|
|
186
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
|
+
|
|
187
212
|
## Missing-key flow
|
|
188
213
|
|
|
189
214
|
1. The user navigates a page that calls `t("hello.title")`.
|
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,
|
|
@@ -326,15 +343,36 @@ var VerbumiaI18n = class {
|
|
|
326
343
|
keyRegistry.attach();
|
|
327
344
|
const targets = /* @__PURE__ */ new Set([this.locale]);
|
|
328
345
|
if (this.fallbackLng) targets.add(this.fallbackLng);
|
|
329
|
-
await Promise.all(
|
|
330
|
-
[...targets].flatMap(
|
|
346
|
+
await Promise.all([
|
|
347
|
+
...[...targets].flatMap(
|
|
331
348
|
(loc) => this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))
|
|
332
|
-
)
|
|
333
|
-
|
|
349
|
+
),
|
|
350
|
+
// Best-effort: align the key separator with the version's key_style (#754).
|
|
351
|
+
this._loadKeyStyle(fetchImpl)
|
|
352
|
+
]);
|
|
334
353
|
this.ready = true;
|
|
335
354
|
this._startTimer();
|
|
336
355
|
this._notify();
|
|
337
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
|
+
}
|
|
338
376
|
setLocale = async (next) => {
|
|
339
377
|
if (next === this.locale) return;
|
|
340
378
|
this.locale = next;
|
|
@@ -393,12 +431,12 @@ var VerbumiaI18n = class {
|
|
|
393
431
|
const namespace = this._splitNamespace(key);
|
|
394
432
|
const bareKey = namespace.bareKey;
|
|
395
433
|
const ns = namespace.ns;
|
|
396
|
-
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);
|
|
397
435
|
if (fromActive != null) {
|
|
398
436
|
return this._render(fromActive, this.locale, options);
|
|
399
437
|
}
|
|
400
438
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
401
|
-
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);
|
|
402
440
|
if (fb != null) {
|
|
403
441
|
return this._render(fb, this.fallbackLng, options);
|
|
404
442
|
}
|
|
@@ -443,9 +481,12 @@ var VerbumiaI18n = class {
|
|
|
443
481
|
return interpolate(str, options);
|
|
444
482
|
}
|
|
445
483
|
_splitNamespace(key) {
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
|
|
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
|
+
}
|
|
449
490
|
}
|
|
450
491
|
return { ns: this._config.namespaces[0], bareKey: key };
|
|
451
492
|
}
|
|
@@ -522,7 +563,7 @@ var VerbumiaI18n = class {
|
|
|
522
563
|
return options.defaultValue;
|
|
523
564
|
}
|
|
524
565
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
525
|
-
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);
|
|
526
567
|
if (typeof fb === "string") {
|
|
527
568
|
return fb;
|
|
528
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\n // Embedded build-time snapshot (#757): prime the cache synchronously so\n // the first render is instant and works OFFLINE before the first fetch,\n // then `start()` swaps in CDN/runtime data (no flash). We prime _bundles +\n // _hasContent but NOT _attempted — the missing-key gate stays closed until\n // a real fetch confirms, so keys absent from the snapshot don't generate\n // false \"missing\" reports in the meantime.\n if (config.initialBundles) {\n for (const [loc, byNs] of Object.entries(config.initialBundles)) {\n for (const [ns, tree] of Object.entries(byNs)) {\n if (!tree || typeof tree !== \"object\") continue;\n const key = this._bundleKey(loc, ns);\n this._bundles.set(key, tree as Bundle);\n if (Object.keys(tree).length > 0) this._hasContent.add(key);\n }\n }\n // ready=true only when the snapshot fully covers the active locale's\n // configured namespaces (otherwise wait for the fetch to flip it).\n const active = config.initialBundles[this.locale];\n if (\n active &&\n this._config.namespaces.every(\n (ns) => active[ns] && Object.keys(active[ns]!).length > 0,\n )\n ) {\n this.ready = true;\n }\n }\n\n this._snapshot = this._buildSnapshot();\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n /** Stable snapshot accessor for useSyncExternalStore. The returned\n * object reference is identical between renders unless _notify fired. */\n getSnapshot = (): I18nInstance => this._snapshot;\n\n private _buildSnapshot(): I18nInstance {\n return {\n ready: this.ready,\n locale: this.locale,\n language: this.locale,\n setLocale: this.setLocale,\n changeLanguage: this.changeLanguage,\n t: this.t,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n reload: this.reload,\n };\n }\n\n /** Bundle cache-key builder. Includes `version` so providers with\n * different `version` values never share cached bundles. The segments\n * (version/locale/ns) are slugs and never contain '/'. */\n private _bundleKey(locale: Locale, ns: Namespace): string {\n return `${this._config.version}/${locale}/${ns}`;\n }\n\n private _notify(): void {\n this._snapshot = this._buildSnapshot();\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Default namespace (the first configured one) — used to attribute a\n * bare `t(\"key\")` call when recording on-screen keys. */\n get defaultNamespace(): string {\n return this._config.namespaces[0]!;\n }\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n // Publish the on-screen key registry so a mounted feedback widget\n // lists only the strings rendered on the current view (spec ltm 373).\n keyRegistry.attach();\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n /** Alias of {@link setLocale} for react-i18next compatibility. */\n changeLanguage = (next: Locale): Promise<void> => this.setLocale(next);\n\n /** Alias of {@link locale} for react-i18next compatibility. */\n get language(): Locale {\n return this.locale;\n }\n\n stop(): void {\n keyRegistry.detach();\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n }\n\n /**\n * Bust-refetch already-loaded bundles and re-render once. Generic\n * replacement for the old realtime-only refetch: iterate the\n * `_attempted` cache keys (`${version}/${locale}/${ns}`), optionally\n * filtered by `opts.locale` / `opts.namespace`, and re-pull each one\n * with `{ bust: true }` so the mutable CDN `latest/` alias bypasses the\n * HTTP cache. After all settle, `_notify()` once so React re-renders.\n *\n * Used by `@verbumia/realtime` on a `translations_published` push and as\n * a manual refresh hook. If nothing matches, returns without notifying.\n */\n reload = async (\n opts: { locale?: Locale; namespace?: Namespace } = {},\n ): Promise<void> => {\n const targets: Array<{ locale: Locale; ns: Namespace }> = [];\n for (const key of this._attempted) {\n // `${version}/${locale}/${ns}` — none of the segments contain '/'.\n const parts = key.split(\"/\");\n const locale = parts[1];\n const ns = parts[2];\n if (!locale || !ns) continue;\n if (opts.locale && opts.locale !== locale) continue;\n if (opts.namespace && opts.namespace !== ns) continue;\n targets.push({ locale, ns });\n }\n if (targets.length === 0) return;\n await Promise.all(\n targets.map((t) => this._loadBundle(t.locale, t.ns, fetch, { bust: true })),\n );\n this._notify();\n };\n\n // ---- Translation ----\n\n t = (\n key: string,\n optionsOrDefault?:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | string,\n maybeOptions?: Record<string, unknown> & { defaultValue?: string; count?: number },\n ): string => {\n // react-i18next-style positional fallback: a string 2nd arg is the\n // default value. Optional 3rd arg carries interpolation/options and is\n // merged under it. `t(key, { defaultValue })` keeps working unchanged.\n const options:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | undefined =\n typeof optionsOrDefault === \"string\"\n ? { ...(maybeOptions ?? {}), defaultValue: optionsOrDefault }\n : optionsOrDefault;\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(this._bundleKey(this.locale, ns)), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n // Three-condition gate: ready + attempted + bundle had content. The\n // last clause prevents flooding when the bundle came back empty (404\n // or {}); we'd be reporting against keys we never had a chance to\n // resolve. Master 2026-05-07 P0: see `_hasContent` doc.\n if (\n this.ready &&\n this._attempted.has(this._bundleKey(this.locale, ns)) &&\n this._hasContent.has(this._bundleKey(this.locale, ns))\n ) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: this._sourceValueFor(bareKey, ns, options),\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n /**\n * Final-stage render: pick the right plural form (when value is a CLDR\n * dict and `options.count` is a number) then interpolate `{{var}}`.\n */\n private _render(\n value: ResolvedValue,\n locale: Locale,\n options?: Record<string, unknown> & { count?: number },\n ): string {\n let str: string;\n if (typeof value === \"string\") {\n str = value;\n } else {\n const count = typeof options?.count === \"number\" ? options.count : 0;\n str = selectPluralForm(value, count, locale);\n }\n return interpolate(str, options);\n }\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch,\n opts: { bust?: boolean } = {}\n ): Promise<void> {\n const cacheKey = this._bundleKey(locale, ns);\n // env routing — prod hits the CDN cache; dev hits the live runtime\n // endpoint authenticated with the API key.\n let url: string;\n let init: RequestInit;\n if (this._config.env === \"dev\") {\n const params = new URLSearchParams({ language: locale, namespace: ns });\n if (this._config.version && this._config.version !== \"main\") {\n params.set(\"version_slug\", this._config.version);\n }\n url = `${this._config.apiBase.replace(/\\/+$/, \"\")}/v1/projects/${this._config.projectUuid}/translations/runtime?${params.toString()}`;\n init = {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${this._config.token}` },\n credentials: \"omit\",\n };\n } else {\n url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.version}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (hadContent) {\n // Non-OK but we already have content (an embedded snapshot or a prior\n // successful fetch) — keep that last-known-good instead of clobbering\n // it. Covers offline/transient first loads (#757) and refetch blips.\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (hadContent) {\n // Network error but we already have content (snapshot or prior fetch)\n // — keep last-known-good (offline-first, #757).\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (Option A, task #746 — backend ingest aligned):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. Otherwise `undefined` — we DO NOT fall back to the key name (that\n * made the column unusable: a placeholder indistinguishable from a\n * real default). The key is already carried in `event.key`; an absent\n * `source_value` is the signal that there is no promotable value.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string | undefined {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return undefined;\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import type { I18nInstance } from \"./types\";\nimport type { VerbumiaI18n } from \"./i18n\";\n\n// Active instance registered by the mounted <VerbumiaProvider>. Lets code\n// OUTSIDE React (utilities, stores, non-component modules) reach the i18n\n// instance the way react-i18next exposes its default singleton.\nlet _active: VerbumiaI18n | null = null;\n\n/** @internal — VerbumiaProvider registers its instance on mount. */\nexport function _setActiveInstance(instance: VerbumiaI18n): void {\n _active = instance;\n}\n\n/** @internal — VerbumiaProvider clears its instance on unmount. */\nexport function _clearActiveInstance(instance: VerbumiaI18n): void {\n if (_active === instance) _active = null;\n}\n\n/**\n * Access the active i18n instance OUTSIDE React components — the\n * react-i18next-style standalone singleton (e.g. for `t()`/`changeLanguage()`\n * in plain modules, stores, or helpers).\n *\n * Returns the instance created by the mounted `<VerbumiaProvider>`; throws a\n * clear error if no provider is mounted yet. Assumes a single app-wide\n * provider (the common case); with multiple concurrent providers it returns\n * the most recently mounted one.\n */\nexport function getI18n(): I18nInstance {\n if (!_active) {\n throw new Error(\n \"@verbumia/react-i18next: getI18n() was called before <VerbumiaProvider> mounted (no active i18n instance).\",\n );\n }\n return _active;\n}\n","import { useEffect, useMemo, useRef } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport { keyRegistry } from \"./key-registry\";\nimport type {\n I18nInstance,\n TranslationFunction,\n TranslationOptions,\n} from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call.\n *\n * Every key this hook resolves during a render is recorded into the\n * on-screen key registry (so a mounted `@verbumia/feedback` widget lists\n * only the strings rendered on the current view — spec ltm 373). The\n * contribution is keyed to THIS hook instance and dropped on unmount, so\n * navigating away removes its keys automatically. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n\n // Keys resolved in the CURRENT render pass. The hook body runs before\n // the component's own `t()` calls, so clearing here yields a set that\n // reflects exactly this render once the component finishes.\n const renderedRef = useRef<Set<string>>(new Set());\n renderedRef.current = new Set<string>();\n // Opaque, stable token identifying this hook instance in the registry.\n const tokenRef = useRef<symbol>(Symbol(\"verbumia.t\"));\n\n const t = useMemo<TranslationFunction>(() => {\n // Forwards both call shapes — `t(key, opts)` and the react-i18next\n // positional `t(key, 'Default', opts?)` — straight to `i18n.t`, which\n // normalizes them. Registry tracking is keyed on the resolved fullKey.\n const fn = (\n key: string,\n optionsOrDefault?: TranslationOptions | string,\n maybeOptions?: TranslationOptions,\n ): string => {\n const fullKey =\n defaultNamespace && !key.includes(\":\")\n ? `${defaultNamespace}:${key}`\n : key;\n renderedRef.current.add(\n keyRegistry.encode(fullKey, i18n.defaultNamespace),\n );\n return i18n.t(fullKey, optionsOrDefault, maybeOptions);\n };\n return fn as TranslationFunction;\n }, [i18n, defaultNamespace]);\n\n // After every commit, publish this instance's latest rendered-key set.\n useEffect(() => {\n keyRegistry._set(tokenRef.current, renderedRef.current);\n });\n // Unmount only: drop this instance entirely so its keys leave the\n // on-screen snapshot when the component is gone (e.g. route change).\n useEffect(() => {\n const token = tokenRef.current;\n return () => keyRegistry._delete(token);\n }, []);\n\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime"]}
|
|
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,20 @@ 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;
|
|
36
50
|
/**
|
|
37
51
|
* Embedded build-time snapshot of translation bundles, keyed
|
|
38
52
|
* `locale -> namespace -> i18next tree` (the same shape as the CDN JSON).
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,20 @@ 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;
|
|
36
50
|
/**
|
|
37
51
|
* Embedded build-time snapshot of translation bundles, keyed
|
|
38
52
|
* `locale -> namespace -> i18next tree` (the same shape as the CDN JSON).
|
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,
|
|
@@ -301,15 +318,36 @@ var VerbumiaI18n = class {
|
|
|
301
318
|
keyRegistry.attach();
|
|
302
319
|
const targets = /* @__PURE__ */ new Set([this.locale]);
|
|
303
320
|
if (this.fallbackLng) targets.add(this.fallbackLng);
|
|
304
|
-
await Promise.all(
|
|
305
|
-
[...targets].flatMap(
|
|
321
|
+
await Promise.all([
|
|
322
|
+
...[...targets].flatMap(
|
|
306
323
|
(loc) => this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))
|
|
307
|
-
)
|
|
308
|
-
|
|
324
|
+
),
|
|
325
|
+
// Best-effort: align the key separator with the version's key_style (#754).
|
|
326
|
+
this._loadKeyStyle(fetchImpl)
|
|
327
|
+
]);
|
|
309
328
|
this.ready = true;
|
|
310
329
|
this._startTimer();
|
|
311
330
|
this._notify();
|
|
312
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
|
+
}
|
|
313
351
|
setLocale = async (next) => {
|
|
314
352
|
if (next === this.locale) return;
|
|
315
353
|
this.locale = next;
|
|
@@ -368,12 +406,12 @@ var VerbumiaI18n = class {
|
|
|
368
406
|
const namespace = this._splitNamespace(key);
|
|
369
407
|
const bareKey = namespace.bareKey;
|
|
370
408
|
const ns = namespace.ns;
|
|
371
|
-
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);
|
|
372
410
|
if (fromActive != null) {
|
|
373
411
|
return this._render(fromActive, this.locale, options);
|
|
374
412
|
}
|
|
375
413
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
376
|
-
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);
|
|
377
415
|
if (fb != null) {
|
|
378
416
|
return this._render(fb, this.fallbackLng, options);
|
|
379
417
|
}
|
|
@@ -418,9 +456,12 @@ var VerbumiaI18n = class {
|
|
|
418
456
|
return interpolate(str, options);
|
|
419
457
|
}
|
|
420
458
|
_splitNamespace(key) {
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
423
|
-
|
|
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
|
+
}
|
|
424
465
|
}
|
|
425
466
|
return { ns: this._config.namespaces[0], bareKey: key };
|
|
426
467
|
}
|
|
@@ -497,7 +538,7 @@ var VerbumiaI18n = class {
|
|
|
497
538
|
return options.defaultValue;
|
|
498
539
|
}
|
|
499
540
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
500
|
-
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);
|
|
501
542
|
if (typeof fb === "string") {
|
|
502
543
|
return fb;
|
|
503
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\n // Embedded build-time snapshot (#757): prime the cache synchronously so\n // the first render is instant and works OFFLINE before the first fetch,\n // then `start()` swaps in CDN/runtime data (no flash). We prime _bundles +\n // _hasContent but NOT _attempted — the missing-key gate stays closed until\n // a real fetch confirms, so keys absent from the snapshot don't generate\n // false \"missing\" reports in the meantime.\n if (config.initialBundles) {\n for (const [loc, byNs] of Object.entries(config.initialBundles)) {\n for (const [ns, tree] of Object.entries(byNs)) {\n if (!tree || typeof tree !== \"object\") continue;\n const key = this._bundleKey(loc, ns);\n this._bundles.set(key, tree as Bundle);\n if (Object.keys(tree).length > 0) this._hasContent.add(key);\n }\n }\n // ready=true only when the snapshot fully covers the active locale's\n // configured namespaces (otherwise wait for the fetch to flip it).\n const active = config.initialBundles[this.locale];\n if (\n active &&\n this._config.namespaces.every(\n (ns) => active[ns] && Object.keys(active[ns]!).length > 0,\n )\n ) {\n this.ready = true;\n }\n }\n\n this._snapshot = this._buildSnapshot();\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n /** Stable snapshot accessor for useSyncExternalStore. The returned\n * object reference is identical between renders unless _notify fired. */\n getSnapshot = (): I18nInstance => this._snapshot;\n\n private _buildSnapshot(): I18nInstance {\n return {\n ready: this.ready,\n locale: this.locale,\n language: this.locale,\n setLocale: this.setLocale,\n changeLanguage: this.changeLanguage,\n t: this.t,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n reload: this.reload,\n };\n }\n\n /** Bundle cache-key builder. Includes `version` so providers with\n * different `version` values never share cached bundles. The segments\n * (version/locale/ns) are slugs and never contain '/'. */\n private _bundleKey(locale: Locale, ns: Namespace): string {\n return `${this._config.version}/${locale}/${ns}`;\n }\n\n private _notify(): void {\n this._snapshot = this._buildSnapshot();\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Default namespace (the first configured one) — used to attribute a\n * bare `t(\"key\")` call when recording on-screen keys. */\n get defaultNamespace(): string {\n return this._config.namespaces[0]!;\n }\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n // Publish the on-screen key registry so a mounted feedback widget\n // lists only the strings rendered on the current view (spec ltm 373).\n keyRegistry.attach();\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n /** Alias of {@link setLocale} for react-i18next compatibility. */\n changeLanguage = (next: Locale): Promise<void> => this.setLocale(next);\n\n /** Alias of {@link locale} for react-i18next compatibility. */\n get language(): Locale {\n return this.locale;\n }\n\n stop(): void {\n keyRegistry.detach();\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n }\n\n /**\n * Bust-refetch already-loaded bundles and re-render once. Generic\n * replacement for the old realtime-only refetch: iterate the\n * `_attempted` cache keys (`${version}/${locale}/${ns}`), optionally\n * filtered by `opts.locale` / `opts.namespace`, and re-pull each one\n * with `{ bust: true }` so the mutable CDN `latest/` alias bypasses the\n * HTTP cache. After all settle, `_notify()` once so React re-renders.\n *\n * Used by `@verbumia/realtime` on a `translations_published` push and as\n * a manual refresh hook. If nothing matches, returns without notifying.\n */\n reload = async (\n opts: { locale?: Locale; namespace?: Namespace } = {},\n ): Promise<void> => {\n const targets: Array<{ locale: Locale; ns: Namespace }> = [];\n for (const key of this._attempted) {\n // `${version}/${locale}/${ns}` — none of the segments contain '/'.\n const parts = key.split(\"/\");\n const locale = parts[1];\n const ns = parts[2];\n if (!locale || !ns) continue;\n if (opts.locale && opts.locale !== locale) continue;\n if (opts.namespace && opts.namespace !== ns) continue;\n targets.push({ locale, ns });\n }\n if (targets.length === 0) return;\n await Promise.all(\n targets.map((t) => this._loadBundle(t.locale, t.ns, fetch, { bust: true })),\n );\n this._notify();\n };\n\n // ---- Translation ----\n\n t = (\n key: string,\n optionsOrDefault?:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | string,\n maybeOptions?: Record<string, unknown> & { defaultValue?: string; count?: number },\n ): string => {\n // react-i18next-style positional fallback: a string 2nd arg is the\n // default value. Optional 3rd arg carries interpolation/options and is\n // merged under it. `t(key, { defaultValue })` keeps working unchanged.\n const options:\n | (Record<string, unknown> & { defaultValue?: string; count?: number })\n | undefined =\n typeof optionsOrDefault === \"string\"\n ? { ...(maybeOptions ?? {}), defaultValue: optionsOrDefault }\n : optionsOrDefault;\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(this._bundleKey(this.locale, ns)), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n // Three-condition gate: ready + attempted + bundle had content. The\n // last clause prevents flooding when the bundle came back empty (404\n // or {}); we'd be reporting against keys we never had a chance to\n // resolve. Master 2026-05-07 P0: see `_hasContent` doc.\n if (\n this.ready &&\n this._attempted.has(this._bundleKey(this.locale, ns)) &&\n this._hasContent.has(this._bundleKey(this.locale, ns))\n ) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: this._sourceValueFor(bareKey, ns, options),\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n /**\n * Final-stage render: pick the right plural form (when value is a CLDR\n * dict and `options.count` is a number) then interpolate `{{var}}`.\n */\n private _render(\n value: ResolvedValue,\n locale: Locale,\n options?: Record<string, unknown> & { count?: number },\n ): string {\n let str: string;\n if (typeof value === \"string\") {\n str = value;\n } else {\n const count = typeof options?.count === \"number\" ? options.count : 0;\n str = selectPluralForm(value, count, locale);\n }\n return interpolate(str, options);\n }\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch,\n opts: { bust?: boolean } = {}\n ): Promise<void> {\n const cacheKey = this._bundleKey(locale, ns);\n // env routing — prod hits the CDN cache; dev hits the live runtime\n // endpoint authenticated with the API key.\n let url: string;\n let init: RequestInit;\n if (this._config.env === \"dev\") {\n const params = new URLSearchParams({ language: locale, namespace: ns });\n if (this._config.version && this._config.version !== \"main\") {\n params.set(\"version_slug\", this._config.version);\n }\n url = `${this._config.apiBase.replace(/\\/+$/, \"\")}/v1/projects/${this._config.projectUuid}/translations/runtime?${params.toString()}`;\n init = {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${this._config.token}` },\n credentials: \"omit\",\n };\n } else {\n url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.version}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (hadContent) {\n // Non-OK but we already have content (an embedded snapshot or a prior\n // successful fetch) — keep that last-known-good instead of clobbering\n // it. Covers offline/transient first loads (#757) and refetch blips.\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (hadContent) {\n // Network error but we already have content (snapshot or prior fetch)\n // — keep last-known-good (offline-first, #757).\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (Option A, task #746 — backend ingest aligned):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. Otherwise `undefined` — we DO NOT fall back to the key name (that\n * made the column unusable: a placeholder indistinguishable from a\n * real default). The key is already carried in `event.key`; an absent\n * `source_value` is the signal that there is no promotable value.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string | undefined {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return undefined;\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import type { I18nInstance } from \"./types\";\nimport type { VerbumiaI18n } from \"./i18n\";\n\n// Active instance registered by the mounted <VerbumiaProvider>. Lets code\n// OUTSIDE React (utilities, stores, non-component modules) reach the i18n\n// instance the way react-i18next exposes its default singleton.\nlet _active: VerbumiaI18n | null = null;\n\n/** @internal — VerbumiaProvider registers its instance on mount. */\nexport function _setActiveInstance(instance: VerbumiaI18n): void {\n _active = instance;\n}\n\n/** @internal — VerbumiaProvider clears its instance on unmount. */\nexport function _clearActiveInstance(instance: VerbumiaI18n): void {\n if (_active === instance) _active = null;\n}\n\n/**\n * Access the active i18n instance OUTSIDE React components — the\n * react-i18next-style standalone singleton (e.g. for `t()`/`changeLanguage()`\n * in plain modules, stores, or helpers).\n *\n * Returns the instance created by the mounted `<VerbumiaProvider>`; throws a\n * clear error if no provider is mounted yet. Assumes a single app-wide\n * provider (the common case); with multiple concurrent providers it returns\n * the most recently mounted one.\n */\nexport function getI18n(): I18nInstance {\n if (!_active) {\n throw new Error(\n \"@verbumia/react-i18next: getI18n() was called before <VerbumiaProvider> mounted (no active i18n instance).\",\n );\n }\n return _active;\n}\n","import { useEffect, useMemo, useRef } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport { keyRegistry } from \"./key-registry\";\nimport type {\n I18nInstance,\n TranslationFunction,\n TranslationOptions,\n} from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call.\n *\n * Every key this hook resolves during a render is recorded into the\n * on-screen key registry (so a mounted `@verbumia/feedback` widget lists\n * only the strings rendered on the current view — spec ltm 373). The\n * contribution is keyed to THIS hook instance and dropped on unmount, so\n * navigating away removes its keys automatically. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n\n // Keys resolved in the CURRENT render pass. The hook body runs before\n // the component's own `t()` calls, so clearing here yields a set that\n // reflects exactly this render once the component finishes.\n const renderedRef = useRef<Set<string>>(new Set());\n renderedRef.current = new Set<string>();\n // Opaque, stable token identifying this hook instance in the registry.\n const tokenRef = useRef<symbol>(Symbol(\"verbumia.t\"));\n\n const t = useMemo<TranslationFunction>(() => {\n // Forwards both call shapes — `t(key, opts)` and the react-i18next\n // positional `t(key, 'Default', opts?)` — straight to `i18n.t`, which\n // normalizes them. Registry tracking is keyed on the resolved fullKey.\n const fn = (\n key: string,\n optionsOrDefault?: TranslationOptions | string,\n maybeOptions?: TranslationOptions,\n ): string => {\n const fullKey =\n defaultNamespace && !key.includes(\":\")\n ? `${defaultNamespace}:${key}`\n : key;\n renderedRef.current.add(\n keyRegistry.encode(fullKey, i18n.defaultNamespace),\n );\n return i18n.t(fullKey, optionsOrDefault, maybeOptions);\n };\n return fn as TranslationFunction;\n }, [i18n, defaultNamespace]);\n\n // After every commit, publish this instance's latest rendered-key set.\n useEffect(() => {\n keyRegistry._set(tokenRef.current, renderedRef.current);\n });\n // Unmount only: drop this instance entirely so its keys leave the\n // on-screen snapshot when the component is gone (e.g. route change).\n useEffect(() => {\n const token = tokenRef.current;\n return () => keyRegistry._delete(token);\n }, []);\n\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["useEffect","useMemo","useMemo","useEffect","Fragment","jsx"]}
|
|
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"]}
|