@verbumia/react-i18next 0.5.2 → 0.7.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/dist/index.cjs +105 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -2
- package/dist/index.d.ts +81 -2
- package/dist/index.js +110 -7
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
Trans: () => Trans,
|
|
24
24
|
VerbumiaProvider: () => VerbumiaProvider,
|
|
25
25
|
defaultTransport: () => defaultTransport,
|
|
26
|
+
keyRegistry: () => keyRegistry,
|
|
26
27
|
logTransport: () => logTransport,
|
|
27
28
|
useTranslation: () => useTranslation
|
|
28
29
|
});
|
|
@@ -178,6 +179,78 @@ async function fetchCentrifugoToken(endpoint, projectUuid, authToken, fetchImpl
|
|
|
178
179
|
return await r.json();
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
// src/key-registry.ts
|
|
183
|
+
var GLOBAL = "__verbumia_key_registry__";
|
|
184
|
+
var SEP = "\0";
|
|
185
|
+
function split(fullKey, defaultNamespace) {
|
|
186
|
+
const idx = fullKey.indexOf(":");
|
|
187
|
+
if (idx > 0) {
|
|
188
|
+
return { namespace: fullKey.slice(0, idx), key: fullKey.slice(idx + 1) };
|
|
189
|
+
}
|
|
190
|
+
return { namespace: defaultNamespace, key: fullKey };
|
|
191
|
+
}
|
|
192
|
+
var KeyRegistry = class {
|
|
193
|
+
// One Set per mounted hook/Trans instance (keyed by an opaque token).
|
|
194
|
+
// The on-screen set is the UNION of all live instances' latest render.
|
|
195
|
+
_instances = /* @__PURE__ */ new Map();
|
|
196
|
+
// Provider mounts that have published us onto globalThis. Ref-counted so
|
|
197
|
+
// a multi-provider tree (or fast unmount/remount in tests) never leaves
|
|
198
|
+
// a stale global or unpublishes while another provider is still live.
|
|
199
|
+
_providers = 0;
|
|
200
|
+
/** Replace an instance's contributed key set (called every render). */
|
|
201
|
+
_set(token, keys) {
|
|
202
|
+
this._instances.set(token, keys);
|
|
203
|
+
}
|
|
204
|
+
/** Drop an instance entirely (called on unmount). */
|
|
205
|
+
_delete(token) {
|
|
206
|
+
this._instances.delete(token);
|
|
207
|
+
}
|
|
208
|
+
/** Keys rendered by currently-mounted consumers. Stable insertion order. */
|
|
209
|
+
snapshot() {
|
|
210
|
+
const seen = /* @__PURE__ */ new Set();
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const set of this._instances.values()) {
|
|
213
|
+
for (const id of set) {
|
|
214
|
+
if (seen.has(id)) continue;
|
|
215
|
+
seen.add(id);
|
|
216
|
+
const c = id.indexOf(SEP);
|
|
217
|
+
out.push({ namespace: id.slice(0, c), key: id.slice(c + 1) });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
/** Escape hatch (router integrations / tests). Mount-tracking already
|
|
223
|
+
* handles navigation, so this is rarely needed. */
|
|
224
|
+
reset() {
|
|
225
|
+
this._instances.clear();
|
|
226
|
+
}
|
|
227
|
+
/** Encode a resolved key into the internal id used by `_set`. */
|
|
228
|
+
encode(fullKey, defaultNamespace) {
|
|
229
|
+
const k = split(fullKey, defaultNamespace);
|
|
230
|
+
return `${k.namespace}${SEP}${k.key}`;
|
|
231
|
+
}
|
|
232
|
+
/** Provider mounted — publish the global (idempotent, ref-counted). */
|
|
233
|
+
attach() {
|
|
234
|
+
this._providers += 1;
|
|
235
|
+
if (this._providers === 1) {
|
|
236
|
+
globalThis[GLOBAL] = {
|
|
237
|
+
snapshot: () => this.snapshot(),
|
|
238
|
+
reset: () => this.reset()
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Provider unmounted — unpublish when the last one goes away. */
|
|
243
|
+
detach() {
|
|
244
|
+
this._providers = Math.max(0, this._providers - 1);
|
|
245
|
+
if (this._providers === 0) {
|
|
246
|
+
this._instances.clear();
|
|
247
|
+
const g = globalThis;
|
|
248
|
+
if (g[GLOBAL]) delete g[GLOBAL];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
var keyRegistry = new KeyRegistry();
|
|
253
|
+
|
|
181
254
|
// src/i18n.ts
|
|
182
255
|
var DEFAULT_API_BASE = "https://api.verbumia.ca";
|
|
183
256
|
var DEFAULT_CDN_BASE = "https://cdn.verbumia.ca";
|
|
@@ -315,8 +388,14 @@ var VerbumiaI18n = class {
|
|
|
315
388
|
for (const l of this._listeners) l();
|
|
316
389
|
}
|
|
317
390
|
// ---- Lifecycle ----
|
|
391
|
+
/** Default namespace (the first configured one) — used to attribute a
|
|
392
|
+
* bare `t("key")` call when recording on-screen keys. */
|
|
393
|
+
get defaultNamespace() {
|
|
394
|
+
return this._config.namespaces[0];
|
|
395
|
+
}
|
|
318
396
|
/** Loads the configured namespaces for the active locale + fallback. */
|
|
319
397
|
async start(fetchImpl = fetch) {
|
|
398
|
+
keyRegistry.attach();
|
|
320
399
|
const targets = /* @__PURE__ */ new Set([this.locale]);
|
|
321
400
|
if (this.fallbackLng) targets.add(this.fallbackLng);
|
|
322
401
|
await Promise.all(
|
|
@@ -343,6 +422,7 @@ var VerbumiaI18n = class {
|
|
|
343
422
|
this._notify();
|
|
344
423
|
};
|
|
345
424
|
stop() {
|
|
425
|
+
keyRegistry.detach();
|
|
346
426
|
if (this._timer) {
|
|
347
427
|
clearInterval(this._timer);
|
|
348
428
|
this._timer = null;
|
|
@@ -587,10 +667,19 @@ function VerbumiaProvider({
|
|
|
587
667
|
const i18n = (0, import_react.useMemo)(() => new VerbumiaI18n(config), []);
|
|
588
668
|
(0, import_react.useEffect)(() => {
|
|
589
669
|
void i18n.start();
|
|
590
|
-
|
|
670
|
+
const teardowns = (config.plugins ?? []).map((p) => p.setup?.({ i18n, config })).filter((t) => typeof t === "function");
|
|
671
|
+
return () => {
|
|
672
|
+
teardowns.forEach((t) => t());
|
|
673
|
+
i18n.stop();
|
|
674
|
+
};
|
|
591
675
|
}, [i18n]);
|
|
592
676
|
const value = (0, import_react.useMemo)(() => ({ i18n }), [i18n]);
|
|
593
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
677
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(VerbumiaContext.Provider, { value, children: [
|
|
678
|
+
children,
|
|
679
|
+
(config.plugins ?? []).map(
|
|
680
|
+
(p) => p.render ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: p.render() }, p.name) : null
|
|
681
|
+
)
|
|
682
|
+
] });
|
|
594
683
|
}
|
|
595
684
|
function useI18n() {
|
|
596
685
|
const ctx = (0, import_react.useContext)(VerbumiaContext);
|
|
@@ -609,12 +698,25 @@ var import_react2 = require("react");
|
|
|
609
698
|
function useTranslation(defaultNamespace) {
|
|
610
699
|
const i18n = useI18n();
|
|
611
700
|
const snapshot = useI18nSnapshot();
|
|
701
|
+
const renderedRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
|
|
702
|
+
renderedRef.current = /* @__PURE__ */ new Set();
|
|
703
|
+
const tokenRef = (0, import_react2.useRef)(/* @__PURE__ */ Symbol("verbumia.t"));
|
|
612
704
|
const t = (0, import_react2.useMemo)(() => {
|
|
613
705
|
return (key, options) => {
|
|
614
706
|
const fullKey = defaultNamespace && !key.includes(":") ? `${defaultNamespace}:${key}` : key;
|
|
707
|
+
renderedRef.current.add(
|
|
708
|
+
keyRegistry.encode(fullKey, i18n.defaultNamespace)
|
|
709
|
+
);
|
|
615
710
|
return i18n.t(fullKey, options);
|
|
616
711
|
};
|
|
617
712
|
}, [i18n, defaultNamespace]);
|
|
713
|
+
(0, import_react2.useEffect)(() => {
|
|
714
|
+
keyRegistry._set(tokenRef.current, renderedRef.current);
|
|
715
|
+
});
|
|
716
|
+
(0, import_react2.useEffect)(() => {
|
|
717
|
+
const token = tokenRef.current;
|
|
718
|
+
return () => keyRegistry._delete(token);
|
|
719
|
+
}, []);
|
|
618
720
|
return { t, i18n: snapshot };
|
|
619
721
|
}
|
|
620
722
|
|
|
@@ -662,6 +764,7 @@ function splitOnComponents(text, components) {
|
|
|
662
764
|
Trans,
|
|
663
765
|
VerbumiaProvider,
|
|
664
766
|
defaultTransport,
|
|
767
|
+
keyRegistry,
|
|
665
768
|
logTransport,
|
|
666
769
|
useTranslation
|
|
667
770
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["export { VerbumiaProvider } from \"./provider\";\nexport { useTranslation } from \"./hooks\";\nexport { Trans } from \"./trans\";\nexport type {\n I18nInstance,\n Locale,\n MissingHandlerMode,\n MissingKeyEvent,\n Namespace,\n TranslationFunction,\n TranslationOptions,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nexport { defaultTransport, logTransport } from \"./transport\";\n","import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render.\n *\n * `getSnapshot` MUST return a stable reference between notifications,\n * otherwise React loops forever (Maximum update depth exceeded). The\n * VerbumiaI18n instance caches its snapshot internally — see\n * `_notify` / `_buildSnapshot`. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.5.2\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n // Centrifugo's WebSocket endpoint lives at `/connection/websocket`.\n // Be forgiving on the input — accept either a bare host\n // (`wss://centrifugo.example`) or the full path.\n let url = this.cfg.url;\n if (!url.includes(\"/connection/websocket\")) {\n url = url.replace(/\\/+$/, \"\") + \"/connection/websocket\";\n }\n const ws = new WebSocket(url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: {\n id?: number;\n connect?: { client?: string };\n subscribe?: unknown;\n push?: { channel?: string; pub?: { data?: unknown }; sub?: unknown };\n } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n // Centrifugo v2 protocol pings: server periodically sends an empty\n // object `{}`. Client MUST reply with an empty `{}` to keep the\n // connection alive — without this the server force-closes after\n // ~25-30s and we lose pushes silently.\n if (Object.keys(parsed).length === 0) {\n this._send({});\n return;\n }\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n // Connection token's `channels` claim auto-subscribes us server-side,\n // so an explicit subscribe command is unnecessary. We still send it\n // for back-compat with anonymous-channel deployments where no\n // server-side subscription was created.\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { LiveClient, fetchCentrifugoToken } from \"./live\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\ntype PluralForms = Record<string, string>;\ntype ResolvedValue = string | PluralForms;\n\n/**\n * Plural-form objects mirror the CLDR `Intl.PluralRules` categories. Treat\n * any object whose keys overlap the CLDR set AND whose values are all\n * strings as a plural object — the chunky type guard keeps stray nested\n * namespaces from being misread.\n */\nconst CLDR_CATEGORIES = new Set<string>([\n \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n]);\n\nfunction isPluralForms(v: unknown): v is PluralForms {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return false;\n const keys = Object.keys(v as object);\n if (keys.length === 0) return false;\n if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;\n return keys.every(\n (k) => typeof (v as Record<string, unknown>)[k] === \"string\",\n );\n}\n\n/** Resolve a dotted key against a deeply-nested bundle. Returns either a\n * plain string OR a CLDR plural-forms dict so the caller can pick a form. */\nfunction resolve(bundle: Bundle | undefined, key: string): ResolvedValue | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n if (typeof cur === \"string\") return cur;\n if (isPluralForms(cur)) return cur;\n return undefined;\n}\n\n/**\n * Pick the right CLDR form for `count` against the active locale's plural\n * rules. Falls back to `other` (always required by the contract) and then\n * the first available form so we never render nothing for a configured key.\n */\nfunction selectPluralForm(\n forms: PluralForms,\n count: number,\n locale: string,\n): string {\n let category: string = \"other\";\n try {\n if (typeof Intl !== \"undefined\" && typeof Intl.PluralRules === \"function\") {\n category = new Intl.PluralRules(locale).select(count);\n }\n } catch {\n // Bad locale tag — fall through to \"other\".\n }\n if (category in forms) return forms[category]!;\n if (\"other\" in forms) return forms[\"other\"]!;\n const first = Object.keys(forms)[0];\n return first ? forms[first]! : \"\";\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n // Tighter gate than `_attempted`: this set only contains (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 versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: 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 private _live: LiveClient | null = null;\n // Stable snapshot reference for useSyncExternalStore. Returning a fresh\n // object on each getSnapshot call would loop React forever — we rebuild\n // it ONLY in _notify (when state actually changed) and return the cached\n // reference between notifications.\n private _snapshot!: I18nInstance;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n env: config.env ?? \"prod\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n this._snapshot = this._buildSnapshot();\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n /** Stable snapshot accessor for useSyncExternalStore. The returned\n * object reference is identical between renders unless _notify fired. */\n getSnapshot = (): I18nInstance => this._snapshot;\n\n private _buildSnapshot(): I18nInstance {\n return {\n ready: this.ready,\n locale: this.locale,\n setLocale: this.setLocale,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n };\n }\n\n private _notify(): void {\n this._snapshot = this._buildSnapshot();\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n // Product model (ltm 341): an SDK serving a *promoted production*\n // version (`env: \"prod\"`) NEVER opens a Centrifugo WS — prod freshness\n // is the CDN `latest/` alias at max-age=60s. Realtime + the\n // `translations_published` cache-bust are a DEV-version feature only\n // (bounded connection count, no large prod realtime fleet). The\n // missing-key POST still fires in prod — it's HTTP, not WS, and is\n // gated separately. `_startLive` also no-ops when `centrifugoWsUrl`\n // is unset.\n if (this._config.liveUpdates && this._config.env === \"dev\") {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n // Live republish: the CDN `latest/` alias is mutable and may still be\n // inside its HTTP max-age window in the browser cache, so a normal\n // refetch would return the STALE bundle. Force `cache: \"reload\"` to\n // bypass the HTTP cache and pull the just-published content, then\n // re-render. (Option-c, task 580.)\n void this._loadBundle(lang, ns, fetchImpl, { bust: true }).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string; count?: number }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n // 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.locale}/${ns}`) &&\n this._hasContent.has(`${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 = `${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.versionSlug && this._config.versionSlug !== \"main\") {\n params.set(\"version_slug\", this._config.versionSlug);\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.versionSlug}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (opts.bust && hadContent) {\n // transient non-OK on a live refetch — keep prior content\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (opts.bust && hadContent) {\n // transient network error on a live refetch — keep prior content\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (per backend agreement 2026-05-14, task 575):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. The bare key itself — last resort so dashboards never render\n * a blank `source_value` column.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return bareKey;\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAOO;;;ACLP,IAAM,UAAU;AAChB,IAAM,UAAU;AAGT,SAAS,iBAAiB,MAInB;AACZ,SAAO,OAAO,UAAU;AACtB,QAAI,CAAC,MAAM,OAAQ;AACnB,UAAM,OAAO;AAAA,MACX,cAAc,KAAK;AAAA,MACnB,QAAQ,MAAM,IAAI,CAAC,OAAO;AAAA,QACxB,KAAK,EAAE;AAAA,QACP,WAAW,EAAE;AAAA,QACb,eAAe,EAAE;AAAA,QACjB,cAAc,EAAE;AAAA,QAChB,UAAU;AAAA,UACR,KAAK;AAAA,UACL,KAAK;AAAA,UACL,GAAI,OAAO,WAAW,cAClB,EAAE,KAAK,OAAO,UAAU,KAAK,IAC7B,CAAC;AAAA,UACL,GAAI,EAAE,YAAY,CAAC;AAAA,QACrB;AAAA,MACF,EAAE;AAAA,IACJ;AACA,QAAI;AACF,YAAM,MAAM,GAAG,KAAK,QAAQ,QAAQ,QAAQ,EAAE,CAAC,eAAe;AAAA,QAC5D,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,eAAe,UAAU,KAAK,KAAK;AAAA,QACrC;AAAA,QACA,MAAM,KAAK,UAAU,IAAI;AAAA;AAAA,QAEzB,WAAW;AAAA,MACb,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAGO,IAAM,eAA0B,CAAC,UAA6B;AACnE,aAAW,KAAK,OAAO;AAErB,YAAQ,KAAK,0BAA0B,CAAC;AAAA,EAC1C;AACF;;;ACvBO,IAAM,aAAN,MAAiB;AAAA,EAOtB,YAA6B,KAAuB;AAAvB;AAAA,EAAwB;AAAA,EAAxB;AAAA,EANrB,MAAwB;AAAA,EACxB,MAAM;AAAA,EACN,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,gBAAgB;AAAA;AAAA,EAKxB,MAAM,UAAyB;AAC7B,QAAI,KAAK,IAAK;AACd,QAAI,KAAK,UAAW;AACpB,SAAK,IAAI,WAAW,YAAY;AAChC,UAAM,QAAQ,KAAK,IAAI,eAAe,MAAM,KAAK,IAAI,aAAa,IAAI,KAAK,IAAI;AAI/E,QAAI,MAAM,KAAK,IAAI;AACnB,QAAI,CAAC,IAAI,SAAS,uBAAuB,GAAG;AAC1C,YAAM,IAAI,QAAQ,QAAQ,EAAE,IAAI;AAAA,IAClC;AACA,UAAM,KAAK,IAAI,UAAU,GAAG;AAC5B,SAAK,MAAM;AACX,SAAK,gBAAgB;AAErB,OAAG,SAAS,MAAM;AAEhB,WAAK,MAAM,EAAE,IAAI,EAAE,KAAK,KAAK,SAAS,EAAE,MAAM,EAAE,CAAC;AAAA,IACnD;AACA,OAAG,YAAY,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI;AAC9C,OAAG,UAAU,MAAM,KAAK,SAAS;AACjC,OAAG,UAAU,MAAM;AAAA,IAEnB;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,YAAY;AACjB,QAAI,KAAK,KAAK;AACZ,UAAI;AACF,aAAK,IAAI,MAAM;AAAA,MACjB,QAAQ;AAAA,MAER;AACA,WAAK,MAAM;AAAA,IACb;AACA,SAAK,IAAI,WAAW,cAAc;AAAA,EACpC;AAAA;AAAA,EAIQ,MAAM,KAAoB;AAChC,QAAI,CAAC,KAAK,OAAO,KAAK,IAAI,eAAe,UAAU,KAAM;AACzD,QAAI;AACF,WAAK,IAAI,KAAK,KAAK,UAAU,GAAG,CAAC;AAAA,IACnC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,SAAS,KAAoB;AACnC,QAAI;AAMJ,QAAI;AACF,eAAS,KAAK,MAAM,GAAa;AAAA,IACnC,QAAQ;AACN;AAAA,IACF;AACA,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU;AAK3C,QAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACpC,WAAK,MAAM,CAAC,CAAC;AACb;AAAA,IACF;AACA,QAAI,OAAO,WAAW,CAAC,KAAK,eAAe;AACzC,WAAK,gBAAgB;AACrB,WAAK,IAAI,WAAW,WAAW;AAC/B,WAAK,aAAa;AAKlB,WAAK,MAAM;AAAA,QACT,IAAI,EAAE,KAAK;AAAA,QACX,WAAW,EAAE,SAAS,KAAK,IAAI,QAAQ;AAAA,MACzC,CAAC;AACD;AAAA,IACF;AACA,UAAM,OAAO,OAAO;AACpB,QAAI,QAAQ,KAAK,YAAY,KAAK,IAAI,WAAW,KAAK,KAAK;AACzD,WAAK,IAAI,UAAU,KAAK,IAAI,IAAI;AAAA,IAClC;AAAA,EACF;AAAA,EAEQ,WAAiB;AACvB,SAAK,MAAM;AACX,SAAK,gBAAgB;AACrB,SAAK,IAAI,WAAW,cAAc;AAClC,QAAI,KAAK,UAAW;AACpB,UAAM,QAAQ,KAAK;AACnB,SAAK,aAAa,KAAK,IAAI,KAAK,aAAa,GAAG,GAAM;AACtD,eAAW,MAAM;AACf,UAAI,CAAC,KAAK,UAAW,MAAK,KAAK,QAAQ;AAAA,IACzC,GAAG,KAAK;AAAA,EACV;AACF;AAOA,eAAsB,qBACpB,UACA,aACA,WACA,YAA0B,OACuC;AACjE,QAAM,IAAI,MAAM,UAAU,UAAU;AAAA,IAClC,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,SAAS;AAAA,IACpC;AAAA,IACA,MAAM,KAAK,UAAU,EAAE,cAAc,YAAY,CAAC;AAAA,EACpD,CAAC;AACD,MAAI,CAAC,EAAE,IAAI;AACT,UAAM,IAAI,MAAM,6BAA6B,EAAE,MAAM,KAAK,MAAM,EAAE,KAAK,CAAC,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,EAAE,KAAK;AACvB;;;AC7JA,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AACvB,IAAM,uBAAuB;AAc7B,IAAM,kBAAkB,oBAAI,IAAY;AAAA,EACtC;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAQ;AACvC,CAAC;AAED,SAAS,cAAc,GAA8B;AACnD,MAAI,CAAC,KAAK,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,EAAG,QAAO;AAC5D,QAAM,OAAO,OAAO,KAAK,CAAW;AACpC,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI,CAAC,KAAK,KAAK,CAAC,MAAM,gBAAgB,IAAI,CAAC,CAAC,EAAG,QAAO;AACtD,SAAO,KAAK;AAAA,IACV,CAAC,MAAM,OAAQ,EAA8B,CAAC,MAAM;AAAA,EACtD;AACF;AAIA,SAAS,QAAQ,QAA4B,KAAwC;AACnF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,MAAI,MAAe;AACnB,aAAW,KAAK,OAAO;AACrB,QAAI,OAAO,OAAO,QAAQ,YAAY,KAAM,KAAiC;AAC3E,YAAO,IAAgC,CAAC;AAAA,IAC1C,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,cAAc,GAAG,EAAG,QAAO;AAC/B,SAAO;AACT;AAOA,SAAS,iBACP,OACA,OACA,QACQ;AACR,MAAI,WAAmB;AACvB,MAAI;AACF,QAAI,OAAO,SAAS,eAAe,OAAO,KAAK,gBAAgB,YAAY;AACzE,iBAAW,IAAI,KAAK,YAAY,MAAM,EAAE,OAAO,KAAK;AAAA,IACtD;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI,YAAY,MAAO,QAAO,MAAM,QAAQ;AAC5C,MAAI,WAAW,MAAO,QAAO,MAAM,OAAO;AAC1C,QAAM,QAAQ,OAAO,KAAK,KAAK,EAAE,CAAC;AAClC,SAAO,QAAQ,MAAM,KAAK,IAAK;AACjC;AAGA,SAAS,YAAY,UAAkB,SAA2C;AAChF,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,SAAS,QAAQ,kCAAkC,CAAC,IAAI,SAAS;AACtE,UAAM,IAAI,QAAQ,IAAI;AACtB,WAAO,KAAK,OAAO,KAAK,OAAO,CAAC;AAAA,EAClC,CAAC;AACH;AAIO,IAAM,eAAN,MAA2C;AAAA,EAChD,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,gBAAmC,CAAC;AAAA,EAE5B,WAAW,oBAAI,IAAoB;AAAA;AAAA,EACnC,aAAa,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ7B,cAAc,oBAAI,IAAY;AAAA,EAC9B;AAAA,EAgBA;AAAA,EACA,WAA8B,CAAC;AAAA,EAC/B,QAAQ,oBAAI,IAAY;AAAA;AAAA,EACxB,SAAgD;AAAA,EAChD,aAAa,oBAAI,IAAc;AAAA,EAC/B,QAA2B;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3B;AAAA,EAER,YAAY,QAAwB;AAClC,SAAK,SAAS,OAAO;AACrB,SAAK,cAAc,OAAO;AAC1B,SAAK,UAAU;AAAA,MACb,SAAS,OAAO,WAAW;AAAA,MAC3B,SAAS,OAAO,WAAW;AAAA,MAC3B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,YAAY,OAAO,YAAY,SAAS,OAAO,aAAa,CAAC,QAAQ;AAAA,MACrE,iBAAiB,OAAO,mBAAmB;AAAA,MAC3C,gBAAgB,OAAO,kBAAkB;AAAA,MACzC,yBACE,OAAO,2BAA2B;AAAA,MACpC,aAAa,OAAO,eAAe;AAAA,MACnC,aAAa,CAAC,CAAC,OAAO;AAAA,MACtB,yBACE,OAAO,2BACP,IAAI,OAAO,WAAW,kBAAkB,QAAQ,QAAQ,EAAE,CAAC;AAAA,MAC7D,iBAAiB,OAAO,mBAAmB;AAAA,MAC3C,KAAK,OAAO,OAAO;AAAA,IACrB;AAEA,SAAK,aACH,OAAO,cACN,KAAK,QAAQ,mBAAmB,QAC7B,eACA,iBAAiB;AAAA,MACf,SAAS,KAAK,QAAQ;AAAA,MACtB,OAAO,KAAK,QAAQ;AAAA,MACpB,aAAa,KAAK,QAAQ;AAAA,IAC5B,CAAC;AACP,SAAK,YAAY,KAAK,eAAe;AAAA,EACvC;AAAA;AAAA,EAIA,YAAY,CAAC,aAAqC;AAChD,SAAK,WAAW,IAAI,QAAQ;AAC5B,WAAO,MAAM,KAAK,WAAW,OAAO,QAAQ;AAAA,EAC9C;AAAA;AAAA;AAAA,EAIA,cAAc,MAAoB,KAAK;AAAA,EAE/B,iBAA+B;AACrC,WAAO;AAAA,MACL,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,MACb,WAAW,KAAK;AAAA,MAChB,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,UAAgB;AACtB,SAAK,YAAY,KAAK,eAAe;AACrC,eAAW,KAAK,KAAK,WAAY,GAAE;AAAA,EACrC;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,YAA0B,OAAsB;AAC1D,UAAM,UAAU,oBAAI,IAAY,CAAC,KAAK,MAAM,CAAC;AAC7C,QAAI,KAAK,YAAa,SAAQ,IAAI,KAAK,WAAW;AAClD,UAAM,QAAQ;AAAA,MACZ,CAAC,GAAG,OAAO,EAAE;AAAA,QAAQ,CAAC,QACpB,KAAK,QAAQ,WAAW,IAAI,CAAC,OAAO,KAAK,YAAY,KAAK,IAAI,SAAS,CAAC;AAAA,MAC1E;AAAA,IACF;AACA,SAAK,QAAQ;AACb,SAAK,YAAY;AASjB,QAAI,KAAK,QAAQ,eAAe,KAAK,QAAQ,QAAQ,OAAO;AAC1D,WAAK,WAAW,SAAS;AAAA,IAC3B;AACA,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,YAAY,OAAO,SAAgC;AACjD,QAAI,SAAS,KAAK,OAAQ;AAC1B,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,QAAQ;AACb,UAAM,QAAQ;AAAA,MACZ,KAAK,QAAQ,WAAW,IAAI,CAAC,OAAO,KAAK,YAAY,MAAM,EAAE,CAAC;AAAA,IAChE;AACA,SAAK,QAAQ;AACb,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,OAAa;AACX,QAAI,KAAK,QAAQ;AACf,oBAAc,KAAK,MAAM;AACzB,WAAK,SAAS;AAAA,IAChB;AACA,QAAI,KAAK,OAAO;AACd,WAAK,MAAM,QAAQ;AACnB,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,WAAW,WAA+B;AAChD,UAAM,QAAQ,KAAK,QAAQ;AAC3B,QAAI,CAAC,OAAO;AAEV,UAAI,OAAO,YAAY,aAAa;AAClC,gBAAQ;AAAA,UACN;AAAA,QACF;AAAA,MACF;AACA;AAAA,IACF;AACA,UAAM,cAAc,KAAK,QAAQ;AACjC,UAAM,gBAAgB,KAAK,QAAQ;AACnC,UAAM,WAAW,KAAK,QAAQ;AAE9B,UAAM,eAAe,YAA6B;AAChD,YAAM,EAAE,MAAM,IAAI,MAAM;AAAA,QACtB;AAAA,QAAe;AAAA,QAAa;AAAA,QAAU;AAAA,MACxC;AACA,aAAO;AAAA,IACT;AAGA,UAAM,YAAY;AAChB,UAAI;AACJ,UAAI;AACJ,UAAI;AACF,cAAM,SAAS,MAAM;AAAA,UACnB;AAAA,UAAe;AAAA,UAAa;AAAA,UAAU;AAAA,QACxC;AACA,kBAAU,OAAO;AACjB,gBAAQ,OAAO;AAAA,MACjB,SAAS,KAAK;AACZ,YAAI,OAAO,YAAY,aAAa;AAClC,kBAAQ,KAAK,mDAAmD,GAAG;AAAA,QACrE;AACA;AAAA,MACF;AACA,WAAK,QAAQ,IAAI,WAAW;AAAA,QAC1B,KAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,CAAC,SAAS,KAAK,eAAe,MAAM,SAAS;AAAA,MAC1D,CAAC;AACD,WAAK,KAAK,MAAM,QAAQ;AAAA,IAC1B,GAAG;AAAA,EACL;AAAA,EAEQ,eAAe,MAAe,WAA+B;AACnE,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,UAAM,IAAI;AACV,QAAI,EAAE,UAAU,yBAA0B;AAC1C,UAAM,OAAO,EAAE;AACf,UAAM,KAAK,EAAE;AACb,QAAI,CAAC,QAAQ,CAAC,GAAI;AAGlB,UAAM,WAAW,GAAG,IAAI,IAAI,EAAE;AAC9B,QAAI,CAAC,KAAK,WAAW,IAAI,QAAQ,EAAG;AAMpC,SAAK,KAAK,YAAY,MAAM,IAAI,WAAW,EAAE,MAAM,KAAK,CAAC,EAAE,KAAK,MAAM;AACpE,WAAK,QAAQ;AAAA,IACf,CAAC;AAAA,EACH;AAAA;AAAA,EAIA,IAAI,CAAC,KAAa,YAA0F;AAC1G,UAAM,YAAY,KAAK,gBAAgB,GAAG;AAC1C,UAAM,UAAU,UAAU;AAC1B,UAAM,KAAK,UAAU;AAErB,UAAM,aAAa,QAAQ,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,IAAI,EAAE,EAAE,GAAG,OAAO;AAC7E,QAAI,cAAc,MAAM;AACtB,aAAO,KAAK,QAAQ,YAAY,KAAK,QAAQ,OAAO;AAAA,IACtD;AAEA,QAAI,KAAK,eAAe,KAAK,gBAAgB,KAAK,QAAQ;AACxD,YAAM,KAAK,QAAQ,KAAK,SAAS,IAAI,GAAG,KAAK,WAAW,IAAI,EAAE,EAAE,GAAG,OAAO;AAC1E,UAAI,MAAM,MAAM;AACd,eAAO,KAAK,QAAQ,IAAI,KAAK,aAAa,OAAO;AAAA,MACnD;AAAA,IACF;AAQA,QACE,KAAK,SACL,KAAK,WAAW,IAAI,GAAG,KAAK,MAAM,IAAI,EAAE,EAAE,KAC1C,KAAK,YAAY,IAAI,GAAG,KAAK,MAAM,IAAI,EAAE,EAAE,GAC3C;AACA,WAAK,eAAe;AAAA,QAClB,KAAK;AAAA,QACL,WAAW;AAAA,QACX,eAAe,KAAK;AAAA,QACpB,cAAc,KAAK,gBAAgB,SAAS,IAAI,OAAO;AAAA,MACzD,CAAC;AAAA,IACH;AACA,UAAM,eAAe,SAAS;AAC9B,QAAI,OAAO,iBAAiB,UAAU;AACpC,aAAO,YAAY,cAAc,OAAO;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,eAAe,YAA2B;AACxC,QAAI,CAAC,KAAK,SAAS,OAAQ;AAC3B,UAAM,QAAQ,KAAK,SAAS,MAAM,CAAC;AACnC,SAAK,WAAW,CAAC;AACjB,QAAI,KAAK,QAAQ,mBAAmB,MAAO;AAC3C,QAAI;AACF,YAAM,KAAK,WAAW,KAAK;AAAA,IAC7B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,QACN,OACA,QACA,SACQ;AACR,QAAI;AACJ,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM;AAAA,IACR,OAAO;AACL,YAAM,QAAQ,OAAO,SAAS,UAAU,WAAW,QAAQ,QAAQ;AACnE,YAAM,iBAAiB,OAAO,OAAO,MAAM;AAAA,IAC7C;AACA,WAAO,YAAY,KAAK,OAAO;AAAA,EACjC;AAAA,EAEQ,gBAAgB,KAAiD;AAEvE,UAAM,MAAM,IAAI,QAAQ,GAAG;AAC3B,QAAI,MAAM,GAAG;AACX,aAAO,EAAE,IAAI,IAAI,MAAM,GAAG,GAAG,GAAG,SAAS,IAAI,MAAM,MAAM,CAAC,EAAE;AAAA,IAC9D;AACA,WAAO,EAAE,IAAI,KAAK,QAAQ,WAAW,CAAC,GAAI,SAAS,IAAI;AAAA,EACzD;AAAA,EAEA,MAAc,YACZ,QACA,IACA,YAA0B,OAC1B,OAA2B,CAAC,GACb;AACf,UAAM,WAAW,GAAG,MAAM,IAAI,EAAE;AAGhC,QAAI;AACJ,QAAI;AACJ,QAAI,KAAK,QAAQ,QAAQ,OAAO;AAC9B,YAAM,SAAS,IAAI,gBAAgB,EAAE,UAAU,QAAQ,WAAW,GAAG,CAAC;AACtE,UAAI,KAAK,QAAQ,eAAe,KAAK,QAAQ,gBAAgB,QAAQ;AACnE,eAAO,IAAI,gBAAgB,KAAK,QAAQ,WAAW;AAAA,MACrD;AACA,YAAM,GAAG,KAAK,QAAQ,QAAQ,QAAQ,QAAQ,EAAE,CAAC,gBAAgB,KAAK,QAAQ,WAAW,yBAAyB,OAAO,SAAS,CAAC;AACnI,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,EAAE,eAAe,UAAU,KAAK,QAAQ,KAAK,GAAG;AAAA,QACzD,aAAa;AAAA,MACf;AAAA,IACF,OAAO;AACL,YAAM,GAAG,KAAK,QAAQ,QAAQ,QAAQ,QAAQ,EAAE,CAAC,MAAM,KAAK,QAAQ,WAAW,IAAI,KAAK,QAAQ,WAAW,WAAW,MAAM,IAAI,EAAE;AAClI,aAAO,EAAE,QAAQ,OAAO,aAAa,OAAO;AAAA,IAC9C;AAIA,QAAI,KAAK,MAAM;AACb,WAAK,QAAQ;AAAA,IACf;AAIA,UAAM,aAAa,KAAK,YAAY,IAAI,QAAQ;AAChD,QAAI;AACF,YAAM,IAAI,MAAM,UAAU,KAAK,IAAI;AACnC,UAAI,EAAE,IAAI;AACR,cAAM,OAAQ,MAAM,EAAE,KAAK;AAC3B,aAAK,SAAS,IAAI,UAAU,IAAI;AAChC,YAAI,QAAQ,OAAO,SAAS,YAAY,OAAO,KAAK,IAAI,EAAE,SAAS,GAAG;AACpE,eAAK,YAAY,IAAI,QAAQ;AAAA,QAC/B,OAAO;AACL,eAAK,YAAY,OAAO,QAAQ;AAAA,QAClC;AAAA,MACF,WAAW,KAAK,QAAQ,YAAY;AAAA,MAEpC,OAAO;AAIL,aAAK,SAAS,IAAI,UAAU,CAAC,CAAC;AAC9B,aAAK,YAAY,OAAO,QAAQ;AAAA,MAClC;AAAA,IACF,QAAQ;AACN,UAAI,KAAK,QAAQ,YAAY;AAAA,MAE7B,OAAO;AACL,aAAK,SAAS,IAAI,UAAU,CAAC,CAAC;AAC9B,aAAK,YAAY,OAAO,QAAQ;AAAA,MAClC;AAAA,IACF,UAAE;AACA,WAAK,WAAW,IAAI,QAAQ;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,cAAoB;AAC1B,QAAI,KAAK,QAAQ,mBAAmB,MAAO;AAC3C,QAAI,OAAO,gBAAgB,WAAY;AACvC,SAAK,SAAS,YAAY,MAAM;AAC9B,WAAK,KAAK,aAAa;AAAA,IACzB,GAAG,KAAK,QAAQ,eAAe;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,gBACN,SACA,IACA,SACQ;AACR,QAAI,OAAO,SAAS,iBAAiB,UAAU;AAC7C,aAAO,QAAQ;AAAA,IACjB;AACA,QAAI,KAAK,eAAe,KAAK,gBAAgB,KAAK,QAAQ;AACxD,YAAM,KAAK,QAAQ,KAAK,SAAS,IAAI,GAAG,KAAK,WAAW,IAAI,EAAE,EAAE,GAAG,OAAO;AAC1E,UAAI,OAAO,OAAO,UAAU;AAC1B,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAe,OAA8B;AACnD,QAAI,KAAK,QAAQ,mBAAmB,MAAO;AAC3C,UAAM,WAAW,GAAG,MAAM,aAAa,IAAI,MAAM,SAAS,IAAI,MAAM,GAAG;AACvE,QAAI,KAAK,MAAM,IAAI,QAAQ,EAAG;AAC9B,SAAK,MAAM,IAAI,QAAQ;AAGvB,SAAK,gBAAgB,CAAC,OAAO,GAAG,KAAK,aAAa,EAAE;AAAA,MAClD;AAAA,MACA,KAAK,QAAQ;AAAA,IACf;AACA,SAAK,SAAS,KAAK,KAAK;AACxB,QAAI,KAAK,SAAS,UAAU,KAAK,QAAQ,gBAAgB;AACvD,WAAK,KAAK,aAAa;AAAA,IACzB;AACA,SAAK,QAAQ;AAAA,EACf;AACF;;;AHnfI;AApBJ,IAAM,sBAAkB,4BAA2C,IAAI;AAMhE,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,GAAG;AACL,GAA0B;AAExB,QAAM,WAAO,sBAAQ,MAAM,IAAI,aAAa,MAAM,GAAG,CAAC,CAAC;AAEvD,8BAAU,MAAM;AACd,SAAK,KAAK,MAAM;AAChB,WAAO,MAAM,KAAK,KAAK;AAAA,EACzB,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,YAAQ,sBAA8B,OAAO,EAAE,KAAK,IAAI,CAAC,IAAI,CAAC;AACpE,SACE,4CAAC,gBAAgB,UAAhB,EAAyB,OAAe,UAAS;AAEtD;AAGO,SAAS,UAAwB;AACtC,QAAM,UAAM,yBAAW,eAAe;AACtC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACA,SAAO,IAAI;AACb;AAQO,SAAS,kBAAgC;AAC9C,QAAM,OAAO,QAAQ;AACrB,aAAO,mCAAqB,KAAK,WAAW,KAAK,aAAa,KAAK,WAAW;AAChF;;;AIzDA,IAAAA,gBAAwB;AAWjB,SAAS,eAAe,kBAAiD;AAC9E,QAAM,OAAO,QAAQ;AACrB,QAAM,WAAW,gBAAgB;AACjC,QAAM,QAAI,uBAA6B,MAAM;AAC3C,WAAO,CAAC,KAAK,YAAY;AACvB,YAAM,UACJ,oBAAoB,CAAC,IAAI,SAAS,GAAG,IAAI,GAAG,gBAAgB,IAAI,GAAG,KAAK;AAC1E,aAAO,KAAK,EAAE,SAAS,OAAO;AAAA,IAChC;AAAA,EACF,GAAG,CAAC,MAAM,gBAAgB,CAAC;AAC3B,SAAO,EAAE,GAAG,MAAM,SAAS;AAC7B;;;ACtBA,IAAAC,gBAAuE;AA6BvB,IAAAC,sBAAA;AATzC,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAe;AACb,QAAM,EAAE,EAAE,IAAI,eAAe,SAAS;AACtC,QAAM,MAAM,EAAE,SAAS,EAAE,GAAI,UAAU,CAAC,GAAI,cAAc,YAAY,QAAQ,CAAC;AAC/E,MAAI,CAAC,cAAc,CAAC,WAAW,OAAQ,QAAO,6EAAG,eAAI;AACrD,SAAO,6EAAG,4BAAkB,KAAK,UAAU,GAAE;AAC/C;AAEA,SAAS,kBAAkB,MAAc,YAAsC;AAC7E,QAAM,MAAmB,CAAC;AAE1B,QAAM,KAAK;AACX,MAAI,YAAY;AAChB,MAAI;AACJ,UAAQ,IAAI,GAAG,KAAK,IAAI,OAAO,MAAM;AACnC,QAAI,EAAE,QAAQ,UAAW,KAAI,KAAK,KAAK,MAAM,WAAW,EAAE,KAAK,CAAC;AAChE,UAAM,MAAM,OAAO,EAAE,CAAC,CAAC;AACvB,UAAM,QAAQ,EAAE,CAAC;AACjB,UAAM,OAAO,WAAW,GAAG;AAC3B,YAAI,8BAAe,IAAI,GAAG;AACxB,UAAI;AAAA,YACF,4BAAa,MAAM,EAAE,KAAK,KAAK,EAAE,KAAK,GAAG,GAAG,GAAG,uBAAS,QAAQ,SAAS,EAAE,CAAC;AAAA,MAC9E;AAAA,IACF,WAAW,SAAS,QAAW;AAC7B,UAAI,KAAK,IAAI;AAAA,IACf,OAAO;AACL,UAAI,KAAK,SAAS,EAAE;AAAA,IACtB;AACA,gBAAY,GAAG;AAAA,EACjB;AACA,MAAI,YAAY,KAAK,OAAQ,KAAI,KAAK,KAAK,MAAM,SAAS,CAAC;AAC3D,SAAO;AACT;","names":["import_react","import_react","import_jsx_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/key-registry.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["export { VerbumiaProvider } from \"./provider\";\nexport { useTranslation } from \"./hooks\";\nexport { Trans } from \"./trans\";\nexport type {\n I18nInstance,\n Locale,\n MissingHandlerMode,\n MissingKeyEvent,\n Namespace,\n TranslationFunction,\n TranslationOptions,\n Transport,\n VerbumiaConfig,\n 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 type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n // 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 };\n }, [i18n]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>\n {children}\n {/* Plugin outlets: isolated sibling leaves AFTER children. Their\n internal state never propagates to the host app subtree. */}\n {(config.plugins ?? []).map((p) =>\n p.render ? <Fragment key={p.name}>{p.render()}</Fragment> : null,\n )}\n </VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render.\n *\n * `getSnapshot` MUST return a stable reference between notifications,\n * otherwise React loops forever (Maximum update depth exceeded). The\n * VerbumiaI18n instance caches its snapshot internally — see\n * `_notify` / `_buildSnapshot`. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.5.2\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n // Centrifugo's WebSocket endpoint lives at `/connection/websocket`.\n // Be forgiving on the input — accept either a bare host\n // (`wss://centrifugo.example`) or the full path.\n let url = this.cfg.url;\n if (!url.includes(\"/connection/websocket\")) {\n url = url.replace(/\\/+$/, \"\") + \"/connection/websocket\";\n }\n const ws = new WebSocket(url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: {\n id?: number;\n connect?: { client?: string };\n subscribe?: unknown;\n push?: { channel?: string; pub?: { data?: unknown }; sub?: unknown };\n } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n // Centrifugo v2 protocol pings: server periodically sends an empty\n // object `{}`. Client MUST reply with an empty `{}` to keep the\n // connection alive — without this the server force-closes after\n // ~25-30s and we lose pushes silently.\n if (Object.keys(parsed).length === 0) {\n this._send({});\n return;\n }\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n // Connection token's `channels` claim auto-subscribes us server-side,\n // so an explicit subscribe command is unnecessary. We still send it\n // for back-compat with anonymous-channel deployments where no\n // server-side subscription was created.\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","/**\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 { LiveClient, fetchCentrifugoToken } from \"./live\";\nimport { keyRegistry } from \"./key-registry\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\ntype PluralForms = Record<string, string>;\ntype ResolvedValue = string | PluralForms;\n\n/**\n * Plural-form objects mirror the CLDR `Intl.PluralRules` categories. Treat\n * any object whose keys overlap the CLDR set AND whose values are all\n * strings as a plural object — the chunky type guard keeps stray nested\n * namespaces from being misread.\n */\nconst CLDR_CATEGORIES = new Set<string>([\n \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n]);\n\nfunction isPluralForms(v: unknown): v is PluralForms {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return false;\n const keys = Object.keys(v as object);\n if (keys.length === 0) return false;\n if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;\n return keys.every(\n (k) => typeof (v as Record<string, unknown>)[k] === \"string\",\n );\n}\n\n/** Resolve a dotted key against a deeply-nested bundle. Returns either a\n * plain string OR a CLDR plural-forms dict so the caller can pick a form. */\nfunction resolve(bundle: Bundle | undefined, key: string): ResolvedValue | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n if (typeof cur === \"string\") return cur;\n if (isPluralForms(cur)) return cur;\n return undefined;\n}\n\n/**\n * Pick the right CLDR form for `count` against the active locale's plural\n * rules. Falls back to `other` (always required by the contract) and then\n * the first available form so we never render nothing for a configured key.\n */\nfunction selectPluralForm(\n forms: PluralForms,\n count: number,\n locale: string,\n): string {\n let category: string = \"other\";\n try {\n if (typeof Intl !== \"undefined\" && typeof Intl.PluralRules === \"function\") {\n category = new Intl.PluralRules(locale).select(count);\n }\n } catch {\n // Bad locale tag — fall through to \"other\".\n }\n if (category in forms) return forms[category]!;\n if (\"other\" in forms) return forms[\"other\"]!;\n const first = Object.keys(forms)[0];\n return first ? forms[first]! : \"\";\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n // Tighter gate than `_attempted`: this set only contains (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 versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: 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 private _live: LiveClient | null = null;\n // Stable snapshot reference for useSyncExternalStore. Returning a fresh\n // object on each getSnapshot call would loop React forever — we rebuild\n // it ONLY in _notify (when state actually changed) and return the cached\n // reference between notifications.\n private _snapshot!: I18nInstance;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n env: config.env ?? \"prod\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n this._snapshot = this._buildSnapshot();\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n /** Stable snapshot accessor for useSyncExternalStore. The returned\n * object reference is identical between renders unless _notify fired. */\n getSnapshot = (): I18nInstance => this._snapshot;\n\n private _buildSnapshot(): I18nInstance {\n return {\n ready: this.ready,\n locale: this.locale,\n setLocale: this.setLocale,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n };\n }\n\n private _notify(): void {\n this._snapshot = this._buildSnapshot();\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** 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 // Product model (ltm 341): an SDK serving a *promoted production*\n // version (`env: \"prod\"`) NEVER opens a Centrifugo WS — prod freshness\n // is the CDN `latest/` alias at max-age=60s. Realtime + the\n // `translations_published` cache-bust are a DEV-version feature only\n // (bounded connection count, no large prod realtime fleet). The\n // missing-key POST still fires in prod — it's HTTP, not WS, and is\n // gated separately. `_startLive` also no-ops when `centrifugoWsUrl`\n // is unset.\n if (this._config.liveUpdates && this._config.env === \"dev\") {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n keyRegistry.detach();\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n // Live republish: the CDN `latest/` alias is mutable and may still be\n // inside its HTTP max-age window in the browser cache, so a normal\n // refetch would return the STALE bundle. Force `cache: \"reload\"` to\n // bypass the HTTP cache and pull the just-published content, then\n // re-render. (Option-c, task 580.)\n void this._loadBundle(lang, ns, fetchImpl, { bust: true }).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string; count?: number }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n // 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.locale}/${ns}`) &&\n this._hasContent.has(`${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 = `${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.versionSlug && this._config.versionSlug !== \"main\") {\n params.set(\"version_slug\", this._config.versionSlug);\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.versionSlug}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (opts.bust && hadContent) {\n // transient non-OK on a live refetch — keep prior content\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (opts.bust && hadContent) {\n // transient network error on a live refetch — keep prior content\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (per backend agreement 2026-05-14, task 575):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. The bare key itself — last resort so dashboards never render\n * a blank `source_value` column.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return bareKey;\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useEffect, useMemo, useRef } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport { keyRegistry } from \"./key-registry\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call.\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 return (key, options) => {\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, options);\n };\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,14 @@ interface VerbumiaConfig {
|
|
|
33
33
|
apiBase?: string;
|
|
34
34
|
/** Override the CDN base. Defaults to `https://cdn.verbumia.ca`. */
|
|
35
35
|
cdnBase?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional plugins that hook into THIS provider's tree/registry — e.g.
|
|
38
|
+
* `@verbumia/feedback`. Plugins do NOT introduce a second React
|
|
39
|
+
* context; the provider calls `setup({ i18n, config })` once on mount
|
|
40
|
+
* and renders each plugin's `render()` as an ISOLATED sibling leaf
|
|
41
|
+
* after `children`, so enabling a plugin never re-renders the host app.
|
|
42
|
+
*/
|
|
43
|
+
plugins?: VerbumiaPlugin[];
|
|
36
44
|
/**
|
|
37
45
|
* Optional override for missing-key delivery (in-app inspector,
|
|
38
46
|
* Storybook, Cypress mocks). When set, replaces the default POST.
|
|
@@ -100,6 +108,21 @@ interface VerbumiaConfig {
|
|
|
100
108
|
*/
|
|
101
109
|
env?: "prod" | "dev";
|
|
102
110
|
}
|
|
111
|
+
/** Context handed to a plugin's `setup()` — the live i18n instance + the
|
|
112
|
+
* resolved provider config (apiBase, projectUuid, locale, …). A plugin
|
|
113
|
+
* reuses these instead of asking the customer to re-configure. */
|
|
114
|
+
interface VerbumiaPluginContext {
|
|
115
|
+
i18n: I18nInstance;
|
|
116
|
+
config: VerbumiaConfig;
|
|
117
|
+
}
|
|
118
|
+
/** A provider plugin. `setup` runs once on mount (optional teardown via
|
|
119
|
+
* the returned fn). `render` returns an isolated sibling node the
|
|
120
|
+
* provider mounts after `children` — its state never re-renders the app. */
|
|
121
|
+
interface VerbumiaPlugin {
|
|
122
|
+
name: string;
|
|
123
|
+
setup?: (ctx: VerbumiaPluginContext) => void | (() => void);
|
|
124
|
+
render?: () => ReactNode;
|
|
125
|
+
}
|
|
103
126
|
interface I18nInstance {
|
|
104
127
|
/** True once the initial namespace bundles loaded for the active locale. */
|
|
105
128
|
ready: boolean;
|
|
@@ -125,7 +148,13 @@ interface UseTranslationResult {
|
|
|
125
148
|
i18n: I18nInstance;
|
|
126
149
|
}
|
|
127
150
|
/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you
|
|
128
|
-
* drop the `ns:` prefix on every call.
|
|
151
|
+
* drop the `ns:` prefix on every call.
|
|
152
|
+
*
|
|
153
|
+
* Every key this hook resolves during a render is recorded into the
|
|
154
|
+
* on-screen key registry (so a mounted `@verbumia/feedback` widget lists
|
|
155
|
+
* only the strings rendered on the current view — spec ltm 373). The
|
|
156
|
+
* contribution is keyed to THIS hook instance and dropped on unmount, so
|
|
157
|
+
* navigating away removes its keys automatically. */
|
|
129
158
|
declare function useTranslation(defaultNamespace?: string): UseTranslationResult;
|
|
130
159
|
|
|
131
160
|
interface TransProps {
|
|
@@ -155,4 +184,54 @@ declare function defaultTransport(opts: {
|
|
|
155
184
|
/** Logs each event to console.warn — handy for dev. */
|
|
156
185
|
declare const logTransport: Transport;
|
|
157
186
|
|
|
158
|
-
|
|
187
|
+
/**
|
|
188
|
+
* On-screen key registry — the PRODUCER side of the tiny cross-package
|
|
189
|
+
* contract that `@verbumia/feedback` consumes via
|
|
190
|
+
* `globalThis.__verbumia_key_registry__` (see `@verbumia/feedback`'s
|
|
191
|
+
* `core/keys.ts`).
|
|
192
|
+
*
|
|
193
|
+
* Why this exists: the feedback widget must list only the translation
|
|
194
|
+
* strings RENDERED on the current screen (spec ltm 373) — NOT every
|
|
195
|
+
* project string. The widget can't know what's on screen; the i18n SDK
|
|
196
|
+
* does, because it resolves the keys. So the SDK tracks the keys touched
|
|
197
|
+
* by currently-MOUNTED `useTranslation`/`Trans` consumers and exposes
|
|
198
|
+
* them through a minimal global. When a component unmounts (navigation),
|
|
199
|
+
* its keys drop out automatically — so `snapshot()` is always exactly
|
|
200
|
+
* "what is on screen right now". Explicit `keys` on the feedback plugin
|
|
201
|
+
* stays a fallback for non-i18n strings; it is NOT "pass everything".
|
|
202
|
+
*
|
|
203
|
+
* The published shape is intentionally tiny so any framework port of the
|
|
204
|
+
* i18n SDK can implement the same global without depending on feedback:
|
|
205
|
+
*
|
|
206
|
+
* globalThis.__verbumia_key_registry__ = {
|
|
207
|
+
* snapshot(): { namespace: string; key: string }[];
|
|
208
|
+
* reset(): void;
|
|
209
|
+
* }
|
|
210
|
+
*/
|
|
211
|
+
interface DeclaredKey {
|
|
212
|
+
namespace: string;
|
|
213
|
+
key: string;
|
|
214
|
+
}
|
|
215
|
+
declare class KeyRegistry {
|
|
216
|
+
private _instances;
|
|
217
|
+
private _providers;
|
|
218
|
+
/** Replace an instance's contributed key set (called every render). */
|
|
219
|
+
_set(token: symbol, keys: Set<string>): void;
|
|
220
|
+
/** Drop an instance entirely (called on unmount). */
|
|
221
|
+
_delete(token: symbol): void;
|
|
222
|
+
/** Keys rendered by currently-mounted consumers. Stable insertion order. */
|
|
223
|
+
snapshot(): DeclaredKey[];
|
|
224
|
+
/** Escape hatch (router integrations / tests). Mount-tracking already
|
|
225
|
+
* handles navigation, so this is rarely needed. */
|
|
226
|
+
reset(): void;
|
|
227
|
+
/** Encode a resolved key into the internal id used by `_set`. */
|
|
228
|
+
encode(fullKey: string, defaultNamespace: string): string;
|
|
229
|
+
/** Provider mounted — publish the global (idempotent, ref-counted). */
|
|
230
|
+
attach(): void;
|
|
231
|
+
/** Provider unmounted — unpublish when the last one goes away. */
|
|
232
|
+
detach(): void;
|
|
233
|
+
}
|
|
234
|
+
/** Process-wide singleton — there is exactly one on-screen registry. */
|
|
235
|
+
declare const keyRegistry: KeyRegistry;
|
|
236
|
+
|
|
237
|
+
export { type DeclaredKey, type I18nInstance, type Locale, type MissingHandlerMode, type MissingKeyEvent, type Namespace, Trans, type TranslationFunction, type TranslationOptions, type Transport, type VerbumiaConfig, type VerbumiaPlugin, type VerbumiaPluginContext, VerbumiaProvider, defaultTransport, keyRegistry, logTransport, useTranslation };
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ interface VerbumiaConfig {
|
|
|
33
33
|
apiBase?: string;
|
|
34
34
|
/** Override the CDN base. Defaults to `https://cdn.verbumia.ca`. */
|
|
35
35
|
cdnBase?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional plugins that hook into THIS provider's tree/registry — e.g.
|
|
38
|
+
* `@verbumia/feedback`. Plugins do NOT introduce a second React
|
|
39
|
+
* context; the provider calls `setup({ i18n, config })` once on mount
|
|
40
|
+
* and renders each plugin's `render()` as an ISOLATED sibling leaf
|
|
41
|
+
* after `children`, so enabling a plugin never re-renders the host app.
|
|
42
|
+
*/
|
|
43
|
+
plugins?: VerbumiaPlugin[];
|
|
36
44
|
/**
|
|
37
45
|
* Optional override for missing-key delivery (in-app inspector,
|
|
38
46
|
* Storybook, Cypress mocks). When set, replaces the default POST.
|
|
@@ -100,6 +108,21 @@ interface VerbumiaConfig {
|
|
|
100
108
|
*/
|
|
101
109
|
env?: "prod" | "dev";
|
|
102
110
|
}
|
|
111
|
+
/** Context handed to a plugin's `setup()` — the live i18n instance + the
|
|
112
|
+
* resolved provider config (apiBase, projectUuid, locale, …). A plugin
|
|
113
|
+
* reuses these instead of asking the customer to re-configure. */
|
|
114
|
+
interface VerbumiaPluginContext {
|
|
115
|
+
i18n: I18nInstance;
|
|
116
|
+
config: VerbumiaConfig;
|
|
117
|
+
}
|
|
118
|
+
/** A provider plugin. `setup` runs once on mount (optional teardown via
|
|
119
|
+
* the returned fn). `render` returns an isolated sibling node the
|
|
120
|
+
* provider mounts after `children` — its state never re-renders the app. */
|
|
121
|
+
interface VerbumiaPlugin {
|
|
122
|
+
name: string;
|
|
123
|
+
setup?: (ctx: VerbumiaPluginContext) => void | (() => void);
|
|
124
|
+
render?: () => ReactNode;
|
|
125
|
+
}
|
|
103
126
|
interface I18nInstance {
|
|
104
127
|
/** True once the initial namespace bundles loaded for the active locale. */
|
|
105
128
|
ready: boolean;
|
|
@@ -125,7 +148,13 @@ interface UseTranslationResult {
|
|
|
125
148
|
i18n: I18nInstance;
|
|
126
149
|
}
|
|
127
150
|
/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you
|
|
128
|
-
* drop the `ns:` prefix on every call.
|
|
151
|
+
* drop the `ns:` prefix on every call.
|
|
152
|
+
*
|
|
153
|
+
* Every key this hook resolves during a render is recorded into the
|
|
154
|
+
* on-screen key registry (so a mounted `@verbumia/feedback` widget lists
|
|
155
|
+
* only the strings rendered on the current view — spec ltm 373). The
|
|
156
|
+
* contribution is keyed to THIS hook instance and dropped on unmount, so
|
|
157
|
+
* navigating away removes its keys automatically. */
|
|
129
158
|
declare function useTranslation(defaultNamespace?: string): UseTranslationResult;
|
|
130
159
|
|
|
131
160
|
interface TransProps {
|
|
@@ -155,4 +184,54 @@ declare function defaultTransport(opts: {
|
|
|
155
184
|
/** Logs each event to console.warn — handy for dev. */
|
|
156
185
|
declare const logTransport: Transport;
|
|
157
186
|
|
|
158
|
-
|
|
187
|
+
/**
|
|
188
|
+
* On-screen key registry — the PRODUCER side of the tiny cross-package
|
|
189
|
+
* contract that `@verbumia/feedback` consumes via
|
|
190
|
+
* `globalThis.__verbumia_key_registry__` (see `@verbumia/feedback`'s
|
|
191
|
+
* `core/keys.ts`).
|
|
192
|
+
*
|
|
193
|
+
* Why this exists: the feedback widget must list only the translation
|
|
194
|
+
* strings RENDERED on the current screen (spec ltm 373) — NOT every
|
|
195
|
+
* project string. The widget can't know what's on screen; the i18n SDK
|
|
196
|
+
* does, because it resolves the keys. So the SDK tracks the keys touched
|
|
197
|
+
* by currently-MOUNTED `useTranslation`/`Trans` consumers and exposes
|
|
198
|
+
* them through a minimal global. When a component unmounts (navigation),
|
|
199
|
+
* its keys drop out automatically — so `snapshot()` is always exactly
|
|
200
|
+
* "what is on screen right now". Explicit `keys` on the feedback plugin
|
|
201
|
+
* stays a fallback for non-i18n strings; it is NOT "pass everything".
|
|
202
|
+
*
|
|
203
|
+
* The published shape is intentionally tiny so any framework port of the
|
|
204
|
+
* i18n SDK can implement the same global without depending on feedback:
|
|
205
|
+
*
|
|
206
|
+
* globalThis.__verbumia_key_registry__ = {
|
|
207
|
+
* snapshot(): { namespace: string; key: string }[];
|
|
208
|
+
* reset(): void;
|
|
209
|
+
* }
|
|
210
|
+
*/
|
|
211
|
+
interface DeclaredKey {
|
|
212
|
+
namespace: string;
|
|
213
|
+
key: string;
|
|
214
|
+
}
|
|
215
|
+
declare class KeyRegistry {
|
|
216
|
+
private _instances;
|
|
217
|
+
private _providers;
|
|
218
|
+
/** Replace an instance's contributed key set (called every render). */
|
|
219
|
+
_set(token: symbol, keys: Set<string>): void;
|
|
220
|
+
/** Drop an instance entirely (called on unmount). */
|
|
221
|
+
_delete(token: symbol): void;
|
|
222
|
+
/** Keys rendered by currently-mounted consumers. Stable insertion order. */
|
|
223
|
+
snapshot(): DeclaredKey[];
|
|
224
|
+
/** Escape hatch (router integrations / tests). Mount-tracking already
|
|
225
|
+
* handles navigation, so this is rarely needed. */
|
|
226
|
+
reset(): void;
|
|
227
|
+
/** Encode a resolved key into the internal id used by `_set`. */
|
|
228
|
+
encode(fullKey: string, defaultNamespace: string): string;
|
|
229
|
+
/** Provider mounted — publish the global (idempotent, ref-counted). */
|
|
230
|
+
attach(): void;
|
|
231
|
+
/** Provider unmounted — unpublish when the last one goes away. */
|
|
232
|
+
detach(): void;
|
|
233
|
+
}
|
|
234
|
+
/** Process-wide singleton — there is exactly one on-screen registry. */
|
|
235
|
+
declare const keyRegistry: KeyRegistry;
|
|
236
|
+
|
|
237
|
+
export { type DeclaredKey, type I18nInstance, type Locale, type MissingHandlerMode, type MissingKeyEvent, type Namespace, Trans, type TranslationFunction, type TranslationOptions, type Transport, type VerbumiaConfig, type VerbumiaPlugin, type VerbumiaPluginContext, VerbumiaProvider, defaultTransport, keyRegistry, logTransport, useTranslation };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/provider.tsx
|
|
2
2
|
import {
|
|
3
3
|
createContext,
|
|
4
|
+
Fragment,
|
|
4
5
|
useContext,
|
|
5
6
|
useEffect,
|
|
6
7
|
useMemo,
|
|
@@ -154,6 +155,78 @@ async function fetchCentrifugoToken(endpoint, projectUuid, authToken, fetchImpl
|
|
|
154
155
|
return await r.json();
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// src/key-registry.ts
|
|
159
|
+
var GLOBAL = "__verbumia_key_registry__";
|
|
160
|
+
var SEP = "\0";
|
|
161
|
+
function split(fullKey, defaultNamespace) {
|
|
162
|
+
const idx = fullKey.indexOf(":");
|
|
163
|
+
if (idx > 0) {
|
|
164
|
+
return { namespace: fullKey.slice(0, idx), key: fullKey.slice(idx + 1) };
|
|
165
|
+
}
|
|
166
|
+
return { namespace: defaultNamespace, key: fullKey };
|
|
167
|
+
}
|
|
168
|
+
var KeyRegistry = class {
|
|
169
|
+
// One Set per mounted hook/Trans instance (keyed by an opaque token).
|
|
170
|
+
// The on-screen set is the UNION of all live instances' latest render.
|
|
171
|
+
_instances = /* @__PURE__ */ new Map();
|
|
172
|
+
// Provider mounts that have published us onto globalThis. Ref-counted so
|
|
173
|
+
// a multi-provider tree (or fast unmount/remount in tests) never leaves
|
|
174
|
+
// a stale global or unpublishes while another provider is still live.
|
|
175
|
+
_providers = 0;
|
|
176
|
+
/** Replace an instance's contributed key set (called every render). */
|
|
177
|
+
_set(token, keys) {
|
|
178
|
+
this._instances.set(token, keys);
|
|
179
|
+
}
|
|
180
|
+
/** Drop an instance entirely (called on unmount). */
|
|
181
|
+
_delete(token) {
|
|
182
|
+
this._instances.delete(token);
|
|
183
|
+
}
|
|
184
|
+
/** Keys rendered by currently-mounted consumers. Stable insertion order. */
|
|
185
|
+
snapshot() {
|
|
186
|
+
const seen = /* @__PURE__ */ new Set();
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const set of this._instances.values()) {
|
|
189
|
+
for (const id of set) {
|
|
190
|
+
if (seen.has(id)) continue;
|
|
191
|
+
seen.add(id);
|
|
192
|
+
const c = id.indexOf(SEP);
|
|
193
|
+
out.push({ namespace: id.slice(0, c), key: id.slice(c + 1) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
/** Escape hatch (router integrations / tests). Mount-tracking already
|
|
199
|
+
* handles navigation, so this is rarely needed. */
|
|
200
|
+
reset() {
|
|
201
|
+
this._instances.clear();
|
|
202
|
+
}
|
|
203
|
+
/** Encode a resolved key into the internal id used by `_set`. */
|
|
204
|
+
encode(fullKey, defaultNamespace) {
|
|
205
|
+
const k = split(fullKey, defaultNamespace);
|
|
206
|
+
return `${k.namespace}${SEP}${k.key}`;
|
|
207
|
+
}
|
|
208
|
+
/** Provider mounted — publish the global (idempotent, ref-counted). */
|
|
209
|
+
attach() {
|
|
210
|
+
this._providers += 1;
|
|
211
|
+
if (this._providers === 1) {
|
|
212
|
+
globalThis[GLOBAL] = {
|
|
213
|
+
snapshot: () => this.snapshot(),
|
|
214
|
+
reset: () => this.reset()
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Provider unmounted — unpublish when the last one goes away. */
|
|
219
|
+
detach() {
|
|
220
|
+
this._providers = Math.max(0, this._providers - 1);
|
|
221
|
+
if (this._providers === 0) {
|
|
222
|
+
this._instances.clear();
|
|
223
|
+
const g = globalThis;
|
|
224
|
+
if (g[GLOBAL]) delete g[GLOBAL];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var keyRegistry = new KeyRegistry();
|
|
229
|
+
|
|
157
230
|
// src/i18n.ts
|
|
158
231
|
var DEFAULT_API_BASE = "https://api.verbumia.ca";
|
|
159
232
|
var DEFAULT_CDN_BASE = "https://cdn.verbumia.ca";
|
|
@@ -291,8 +364,14 @@ var VerbumiaI18n = class {
|
|
|
291
364
|
for (const l of this._listeners) l();
|
|
292
365
|
}
|
|
293
366
|
// ---- Lifecycle ----
|
|
367
|
+
/** Default namespace (the first configured one) — used to attribute a
|
|
368
|
+
* bare `t("key")` call when recording on-screen keys. */
|
|
369
|
+
get defaultNamespace() {
|
|
370
|
+
return this._config.namespaces[0];
|
|
371
|
+
}
|
|
294
372
|
/** Loads the configured namespaces for the active locale + fallback. */
|
|
295
373
|
async start(fetchImpl = fetch) {
|
|
374
|
+
keyRegistry.attach();
|
|
296
375
|
const targets = /* @__PURE__ */ new Set([this.locale]);
|
|
297
376
|
if (this.fallbackLng) targets.add(this.fallbackLng);
|
|
298
377
|
await Promise.all(
|
|
@@ -319,6 +398,7 @@ var VerbumiaI18n = class {
|
|
|
319
398
|
this._notify();
|
|
320
399
|
};
|
|
321
400
|
stop() {
|
|
401
|
+
keyRegistry.detach();
|
|
322
402
|
if (this._timer) {
|
|
323
403
|
clearInterval(this._timer);
|
|
324
404
|
this._timer = null;
|
|
@@ -554,7 +634,7 @@ var VerbumiaI18n = class {
|
|
|
554
634
|
};
|
|
555
635
|
|
|
556
636
|
// src/provider.tsx
|
|
557
|
-
import { jsx } from "react/jsx-runtime";
|
|
637
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
558
638
|
var VerbumiaContext = createContext(null);
|
|
559
639
|
function VerbumiaProvider({
|
|
560
640
|
children,
|
|
@@ -563,10 +643,19 @@ function VerbumiaProvider({
|
|
|
563
643
|
const i18n = useMemo(() => new VerbumiaI18n(config), []);
|
|
564
644
|
useEffect(() => {
|
|
565
645
|
void i18n.start();
|
|
566
|
-
|
|
646
|
+
const teardowns = (config.plugins ?? []).map((p) => p.setup?.({ i18n, config })).filter((t) => typeof t === "function");
|
|
647
|
+
return () => {
|
|
648
|
+
teardowns.forEach((t) => t());
|
|
649
|
+
i18n.stop();
|
|
650
|
+
};
|
|
567
651
|
}, [i18n]);
|
|
568
652
|
const value = useMemo(() => ({ i18n }), [i18n]);
|
|
569
|
-
return /* @__PURE__ */
|
|
653
|
+
return /* @__PURE__ */ jsxs(VerbumiaContext.Provider, { value, children: [
|
|
654
|
+
children,
|
|
655
|
+
(config.plugins ?? []).map(
|
|
656
|
+
(p) => p.render ? /* @__PURE__ */ jsx(Fragment, { children: p.render() }, p.name) : null
|
|
657
|
+
)
|
|
658
|
+
] });
|
|
570
659
|
}
|
|
571
660
|
function useI18n() {
|
|
572
661
|
const ctx = useContext(VerbumiaContext);
|
|
@@ -581,22 +670,35 @@ function useI18nSnapshot() {
|
|
|
581
670
|
}
|
|
582
671
|
|
|
583
672
|
// src/hooks.ts
|
|
584
|
-
import { useMemo as useMemo2 } from "react";
|
|
673
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useRef } from "react";
|
|
585
674
|
function useTranslation(defaultNamespace) {
|
|
586
675
|
const i18n = useI18n();
|
|
587
676
|
const snapshot = useI18nSnapshot();
|
|
677
|
+
const renderedRef = useRef(/* @__PURE__ */ new Set());
|
|
678
|
+
renderedRef.current = /* @__PURE__ */ new Set();
|
|
679
|
+
const tokenRef = useRef(/* @__PURE__ */ Symbol("verbumia.t"));
|
|
588
680
|
const t = useMemo2(() => {
|
|
589
681
|
return (key, options) => {
|
|
590
682
|
const fullKey = defaultNamespace && !key.includes(":") ? `${defaultNamespace}:${key}` : key;
|
|
683
|
+
renderedRef.current.add(
|
|
684
|
+
keyRegistry.encode(fullKey, i18n.defaultNamespace)
|
|
685
|
+
);
|
|
591
686
|
return i18n.t(fullKey, options);
|
|
592
687
|
};
|
|
593
688
|
}, [i18n, defaultNamespace]);
|
|
689
|
+
useEffect2(() => {
|
|
690
|
+
keyRegistry._set(tokenRef.current, renderedRef.current);
|
|
691
|
+
});
|
|
692
|
+
useEffect2(() => {
|
|
693
|
+
const token = tokenRef.current;
|
|
694
|
+
return () => keyRegistry._delete(token);
|
|
695
|
+
}, []);
|
|
594
696
|
return { t, i18n: snapshot };
|
|
595
697
|
}
|
|
596
698
|
|
|
597
699
|
// src/trans.tsx
|
|
598
700
|
import { Children, cloneElement, isValidElement } from "react";
|
|
599
|
-
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
701
|
+
import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
|
|
600
702
|
function Trans({
|
|
601
703
|
i18nKey,
|
|
602
704
|
defaults,
|
|
@@ -606,8 +708,8 @@ function Trans({
|
|
|
606
708
|
}) {
|
|
607
709
|
const { t } = useTranslation(namespace);
|
|
608
710
|
const raw = t(i18nKey, { ...values ?? {}, defaultValue: defaults ?? i18nKey });
|
|
609
|
-
if (!components || !components.length) return /* @__PURE__ */ jsx2(
|
|
610
|
-
return /* @__PURE__ */ jsx2(
|
|
711
|
+
if (!components || !components.length) return /* @__PURE__ */ jsx2(Fragment2, { children: raw });
|
|
712
|
+
return /* @__PURE__ */ jsx2(Fragment2, { children: splitOnComponents(raw, components) });
|
|
611
713
|
}
|
|
612
714
|
function splitOnComponents(text, components) {
|
|
613
715
|
const out = [];
|
|
@@ -637,6 +739,7 @@ export {
|
|
|
637
739
|
Trans,
|
|
638
740
|
VerbumiaProvider,
|
|
639
741
|
defaultTransport,
|
|
742
|
+
keyRegistry,
|
|
640
743
|
logTransport,
|
|
641
744
|
useTranslation
|
|
642
745
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render.\n *\n * `getSnapshot` MUST return a stable reference between notifications,\n * otherwise React loops forever (Maximum update depth exceeded). The\n * VerbumiaI18n instance caches its snapshot internally — see\n * `_notify` / `_buildSnapshot`. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.5.2\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n // Centrifugo's WebSocket endpoint lives at `/connection/websocket`.\n // Be forgiving on the input — accept either a bare host\n // (`wss://centrifugo.example`) or the full path.\n let url = this.cfg.url;\n if (!url.includes(\"/connection/websocket\")) {\n url = url.replace(/\\/+$/, \"\") + \"/connection/websocket\";\n }\n const ws = new WebSocket(url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: {\n id?: number;\n connect?: { client?: string };\n subscribe?: unknown;\n push?: { channel?: string; pub?: { data?: unknown }; sub?: unknown };\n } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n // Centrifugo v2 protocol pings: server periodically sends an empty\n // object `{}`. Client MUST reply with an empty `{}` to keep the\n // connection alive — without this the server force-closes after\n // ~25-30s and we lose pushes silently.\n if (Object.keys(parsed).length === 0) {\n this._send({});\n return;\n }\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n // Connection token's `channels` claim auto-subscribes us server-side,\n // so an explicit subscribe command is unnecessary. We still send it\n // for back-compat with anonymous-channel deployments where no\n // server-side subscription was created.\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { LiveClient, fetchCentrifugoToken } from \"./live\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\ntype PluralForms = Record<string, string>;\ntype ResolvedValue = string | PluralForms;\n\n/**\n * Plural-form objects mirror the CLDR `Intl.PluralRules` categories. Treat\n * any object whose keys overlap the CLDR set AND whose values are all\n * strings as a plural object — the chunky type guard keeps stray nested\n * namespaces from being misread.\n */\nconst CLDR_CATEGORIES = new Set<string>([\n \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n]);\n\nfunction isPluralForms(v: unknown): v is PluralForms {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return false;\n const keys = Object.keys(v as object);\n if (keys.length === 0) return false;\n if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;\n return keys.every(\n (k) => typeof (v as Record<string, unknown>)[k] === \"string\",\n );\n}\n\n/** Resolve a dotted key against a deeply-nested bundle. Returns either a\n * plain string OR a CLDR plural-forms dict so the caller can pick a form. */\nfunction resolve(bundle: Bundle | undefined, key: string): ResolvedValue | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n if (typeof cur === \"string\") return cur;\n if (isPluralForms(cur)) return cur;\n return undefined;\n}\n\n/**\n * Pick the right CLDR form for `count` against the active locale's plural\n * rules. Falls back to `other` (always required by the contract) and then\n * the first available form so we never render nothing for a configured key.\n */\nfunction selectPluralForm(\n forms: PluralForms,\n count: number,\n locale: string,\n): string {\n let category: string = \"other\";\n try {\n if (typeof Intl !== \"undefined\" && typeof Intl.PluralRules === \"function\") {\n category = new Intl.PluralRules(locale).select(count);\n }\n } catch {\n // Bad locale tag — fall through to \"other\".\n }\n if (category in forms) return forms[category]!;\n if (\"other\" in forms) return forms[\"other\"]!;\n const first = Object.keys(forms)[0];\n return first ? forms[first]! : \"\";\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n // Tighter gate than `_attempted`: this set only contains (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 versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: 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 private _live: LiveClient | null = null;\n // Stable snapshot reference for useSyncExternalStore. Returning a fresh\n // object on each getSnapshot call would loop React forever — we rebuild\n // it ONLY in _notify (when state actually changed) and return the cached\n // reference between notifications.\n private _snapshot!: I18nInstance;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n env: config.env ?? \"prod\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n this._snapshot = this._buildSnapshot();\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n /** Stable snapshot accessor for useSyncExternalStore. The returned\n * object reference is identical between renders unless _notify fired. */\n getSnapshot = (): I18nInstance => this._snapshot;\n\n private _buildSnapshot(): I18nInstance {\n return {\n ready: this.ready,\n locale: this.locale,\n setLocale: this.setLocale,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n };\n }\n\n private _notify(): void {\n this._snapshot = this._buildSnapshot();\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n // Product model (ltm 341): an SDK serving a *promoted production*\n // version (`env: \"prod\"`) NEVER opens a Centrifugo WS — prod freshness\n // is the CDN `latest/` alias at max-age=60s. Realtime + the\n // `translations_published` cache-bust are a DEV-version feature only\n // (bounded connection count, no large prod realtime fleet). The\n // missing-key POST still fires in prod — it's HTTP, not WS, and is\n // gated separately. `_startLive` also no-ops when `centrifugoWsUrl`\n // is unset.\n if (this._config.liveUpdates && this._config.env === \"dev\") {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n // Live republish: the CDN `latest/` alias is mutable and may still be\n // inside its HTTP max-age window in the browser cache, so a normal\n // refetch would return the STALE bundle. Force `cache: \"reload\"` to\n // bypass the HTTP cache and pull the just-published content, then\n // re-render. (Option-c, task 580.)\n void this._loadBundle(lang, ns, fetchImpl, { bust: true }).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string; count?: number }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n // 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.locale}/${ns}`) &&\n this._hasContent.has(`${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 = `${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.versionSlug && this._config.versionSlug !== \"main\") {\n params.set(\"version_slug\", this._config.versionSlug);\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.versionSlug}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (opts.bust && hadContent) {\n // transient non-OK on a live refetch — keep prior content\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (opts.bust && hadContent) {\n // transient network error on a live refetch — keep prior content\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (per backend agreement 2026-05-14, task 575):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. The bare key itself — last resort so dashboards never render\n * a blank `source_value` column.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return bareKey;\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["useMemo","useMemo","jsx"]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/key-registry.ts","../src/i18n.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 type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n // 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 };\n }, [i18n]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>\n {children}\n {/* Plugin outlets: isolated sibling leaves AFTER children. Their\n internal state never propagates to the host app subtree. */}\n {(config.plugins ?? []).map((p) =>\n p.render ? <Fragment key={p.name}>{p.render()}</Fragment> : null,\n )}\n </VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render.\n *\n * `getSnapshot` MUST return a stable reference between notifications,\n * otherwise React loops forever (Maximum update depth exceeded). The\n * VerbumiaI18n instance caches its snapshot internally — see\n * `_notify` / `_buildSnapshot`. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.5.2\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n // Centrifugo's WebSocket endpoint lives at `/connection/websocket`.\n // Be forgiving on the input — accept either a bare host\n // (`wss://centrifugo.example`) or the full path.\n let url = this.cfg.url;\n if (!url.includes(\"/connection/websocket\")) {\n url = url.replace(/\\/+$/, \"\") + \"/connection/websocket\";\n }\n const ws = new WebSocket(url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: {\n id?: number;\n connect?: { client?: string };\n subscribe?: unknown;\n push?: { channel?: string; pub?: { data?: unknown }; sub?: unknown };\n } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n // Centrifugo v2 protocol pings: server periodically sends an empty\n // object `{}`. Client MUST reply with an empty `{}` to keep the\n // connection alive — without this the server force-closes after\n // ~25-30s and we lose pushes silently.\n if (Object.keys(parsed).length === 0) {\n this._send({});\n return;\n }\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n // Connection token's `channels` claim auto-subscribes us server-side,\n // so an explicit subscribe command is unnecessary. We still send it\n // for back-compat with anonymous-channel deployments where no\n // server-side subscription was created.\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","/**\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 { LiveClient, fetchCentrifugoToken } from \"./live\";\nimport { keyRegistry } from \"./key-registry\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\ntype PluralForms = Record<string, string>;\ntype ResolvedValue = string | PluralForms;\n\n/**\n * Plural-form objects mirror the CLDR `Intl.PluralRules` categories. Treat\n * any object whose keys overlap the CLDR set AND whose values are all\n * strings as a plural object — the chunky type guard keeps stray nested\n * namespaces from being misread.\n */\nconst CLDR_CATEGORIES = new Set<string>([\n \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n]);\n\nfunction isPluralForms(v: unknown): v is PluralForms {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return false;\n const keys = Object.keys(v as object);\n if (keys.length === 0) return false;\n if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;\n return keys.every(\n (k) => typeof (v as Record<string, unknown>)[k] === \"string\",\n );\n}\n\n/** Resolve a dotted key against a deeply-nested bundle. Returns either a\n * plain string OR a CLDR plural-forms dict so the caller can pick a form. */\nfunction resolve(bundle: Bundle | undefined, key: string): ResolvedValue | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n if (typeof cur === \"string\") return cur;\n if (isPluralForms(cur)) return cur;\n return undefined;\n}\n\n/**\n * Pick the right CLDR form for `count` against the active locale's plural\n * rules. Falls back to `other` (always required by the contract) and then\n * the first available form so we never render nothing for a configured key.\n */\nfunction selectPluralForm(\n forms: PluralForms,\n count: number,\n locale: string,\n): string {\n let category: string = \"other\";\n try {\n if (typeof Intl !== \"undefined\" && typeof Intl.PluralRules === \"function\") {\n category = new Intl.PluralRules(locale).select(count);\n }\n } catch {\n // Bad locale tag — fall through to \"other\".\n }\n if (category in forms) return forms[category]!;\n if (\"other\" in forms) return forms[\"other\"]!;\n const first = Object.keys(forms)[0];\n return first ? forms[first]! : \"\";\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n // Tighter gate than `_attempted`: this set only contains (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 versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: 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 private _live: LiveClient | null = null;\n // Stable snapshot reference for useSyncExternalStore. Returning a fresh\n // object on each getSnapshot call would loop React forever — we rebuild\n // it ONLY in _notify (when state actually changed) and return the cached\n // reference between notifications.\n private _snapshot!: I18nInstance;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n env: config.env ?? \"prod\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n this._snapshot = this._buildSnapshot();\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n /** Stable snapshot accessor for useSyncExternalStore. The returned\n * object reference is identical between renders unless _notify fired. */\n getSnapshot = (): I18nInstance => this._snapshot;\n\n private _buildSnapshot(): I18nInstance {\n return {\n ready: this.ready,\n locale: this.locale,\n setLocale: this.setLocale,\n missingEvents: this.missingEvents,\n flushMissing: this.flushMissing,\n };\n }\n\n private _notify(): void {\n this._snapshot = this._buildSnapshot();\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** 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 // Product model (ltm 341): an SDK serving a *promoted production*\n // version (`env: \"prod\"`) NEVER opens a Centrifugo WS — prod freshness\n // is the CDN `latest/` alias at max-age=60s. Realtime + the\n // `translations_published` cache-bust are a DEV-version feature only\n // (bounded connection count, no large prod realtime fleet). The\n // missing-key POST still fires in prod — it's HTTP, not WS, and is\n // gated separately. `_startLive` also no-ops when `centrifugoWsUrl`\n // is unset.\n if (this._config.liveUpdates && this._config.env === \"dev\") {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n keyRegistry.detach();\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n // Live republish: the CDN `latest/` alias is mutable and may still be\n // inside its HTTP max-age window in the browser cache, so a normal\n // refetch would return the STALE bundle. Force `cache: \"reload\"` to\n // bypass the HTTP cache and pull the just-published content, then\n // re-render. (Option-c, task 580.)\n void this._loadBundle(lang, ns, fetchImpl, { bust: true }).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string; count?: number }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n // 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.locale}/${ns}`) &&\n this._hasContent.has(`${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 = `${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.versionSlug && this._config.versionSlug !== \"main\") {\n params.set(\"version_slug\", this._config.versionSlug);\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.versionSlug}/latest/${locale}/${ns}.json`;\n init = { method: \"GET\", credentials: \"omit\" };\n }\n // On a live-republish refetch, bypass the browser HTTP cache so the\n // mutable `latest/` alias is re-pulled from network even within its\n // max-age window.\n if (opts.bust) {\n init.cache = \"reload\";\n }\n // A failed live refetch must NOT downgrade already-good translations to\n // keys — keep showing the last-known-good bundle. Only the initial\n // (non-bust) load may cache an empty object as the \"no bundle\" sentinel.\n const hadContent = this._hasContent.has(cacheKey);\n try {\n const r = await fetchImpl(url, init);\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(cacheKey, data);\n if (data && typeof data === \"object\" && Object.keys(data).length > 0) {\n this._hasContent.add(cacheKey);\n } else {\n this._hasContent.delete(cacheKey);\n }\n } else if (opts.bust && hadContent) {\n // transient non-OK on a live refetch — keep prior content\n } else {\n // 404 = no published bundle yet. Cache an empty object so subsequent\n // resolve()s short-circuit, but DO NOT flag as having content — the\n // gate suppresses reportMissing in this state.\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } catch {\n if (opts.bust && hadContent) {\n // transient network error on a live refetch — keep prior content\n } else {\n this._bundles.set(cacheKey, {});\n this._hasContent.delete(cacheKey);\n }\n } finally {\n this._attempted.add(cacheKey);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n /**\n * Resolve the `source_value` we send with a missing-key report.\n *\n * Fallback chain (per backend agreement 2026-05-14, task 575):\n * 1. `options.defaultValue` — explicit developer-provided string.\n * 2. The fallbackLng bundle's value for this key (typically the\n * source/canonical locale). Only used when it resolves to a\n * plain string, not a plural CLDR dict.\n * 3. The bare key itself — last resort so dashboards never render\n * a blank `source_value` column.\n */\n private _sourceValueFor(\n bareKey: string,\n ns: string,\n options?: { defaultValue?: string }\n ): string {\n if (typeof options?.defaultValue === \"string\") {\n return options.defaultValue;\n }\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (typeof fb === \"string\") {\n return fb;\n }\n }\n return bareKey;\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useEffect, useMemo, useRef } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport { keyRegistry } from \"./key-registry\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call.\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 return (key, options) => {\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, options);\n };\n }, [i18n, defaultNamespace]);\n\n // After every commit, publish this instance's latest rendered-key set.\n useEffect(() => {\n keyRegistry._set(tokenRef.current, renderedRef.current);\n });\n // Unmount only: drop this instance entirely so its keys leave the\n // on-screen snapshot when the component is gone (e.g. route change).\n useEffect(() => {\n const token = tokenRef.current;\n return () => keyRegistry._delete(token);\n }, []);\n\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["useEffect","useMemo","useMemo","useEffect","Fragment","jsx"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verbumia/react-i18next",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React SDK for Verbumia
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "React SDK for Verbumia \u2014 translations + realtime missing-key handler.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://verbumia.ca",
|
|
7
7
|
"repository": {
|
|
@@ -49,15 +49,16 @@
|
|
|
49
49
|
"typescript": "^5.5.0",
|
|
50
50
|
"vitest": "^2.1.0"
|
|
51
51
|
},
|
|
52
|
-
"publishConfig": {
|
|
53
|
-
"access": "public",
|
|
54
|
-
"registry": "https://registry.npmjs.org/",
|
|
55
|
-
"provenance": false
|
|
56
|
-
},
|
|
57
52
|
"scripts": {
|
|
58
53
|
"build": "tsup",
|
|
59
54
|
"test": "vitest run",
|
|
60
55
|
"typecheck": "tsc --noEmit",
|
|
56
|
+
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build",
|
|
61
57
|
"pack:dry-run": "pnpm pack --dry-run"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"registry": "https://registry.npmjs.org/",
|
|
62
|
+
"provenance": true
|
|
62
63
|
}
|
|
63
|
-
}
|
|
64
|
+
}
|