@verbumia/react-i18next 0.1.0 → 0.3.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 CHANGED
@@ -73,12 +73,127 @@ var logTransport = (batch) => {
73
73
  }
74
74
  };
75
75
 
76
+ // src/live.ts
77
+ var LiveClient = class {
78
+ constructor(cfg) {
79
+ this.cfg = cfg;
80
+ }
81
+ cfg;
82
+ _ws = null;
83
+ _id = 0;
84
+ _backoffMs = 1e3;
85
+ _disposed = false;
86
+ _connectAcked = false;
87
+ /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */
88
+ async connect() {
89
+ if (this._ws) return;
90
+ if (this._disposed) return;
91
+ this.cfg.onStatus?.("connecting");
92
+ const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;
93
+ const ws = new WebSocket(this.cfg.url);
94
+ this._ws = ws;
95
+ this._connectAcked = false;
96
+ ws.onopen = () => {
97
+ this._send({ id: ++this._id, connect: { token } });
98
+ };
99
+ ws.onmessage = (evt) => this._onFrame(evt.data);
100
+ ws.onclose = () => this._onClose();
101
+ ws.onerror = () => {
102
+ };
103
+ }
104
+ dispose() {
105
+ this._disposed = true;
106
+ if (this._ws) {
107
+ try {
108
+ this._ws.close();
109
+ } catch {
110
+ }
111
+ this._ws = null;
112
+ }
113
+ this.cfg.onStatus?.("disconnected");
114
+ }
115
+ // ---- internals ----
116
+ _send(msg) {
117
+ if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
118
+ try {
119
+ this._ws.send(JSON.stringify(msg));
120
+ } catch {
121
+ }
122
+ }
123
+ _onFrame(raw) {
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(raw);
127
+ } catch {
128
+ return;
129
+ }
130
+ if (!parsed || typeof parsed !== "object") return;
131
+ if (parsed.connect && !this._connectAcked) {
132
+ this._connectAcked = true;
133
+ this.cfg.onStatus?.("connected");
134
+ this._backoffMs = 1e3;
135
+ this._send({
136
+ id: ++this._id,
137
+ subscribe: { channel: this.cfg.channel }
138
+ });
139
+ return;
140
+ }
141
+ const push = parsed.push;
142
+ if (push && push.channel === this.cfg.channel && push.pub) {
143
+ this.cfg.onMessage(push.pub.data);
144
+ }
145
+ }
146
+ _onClose() {
147
+ this._ws = null;
148
+ this._connectAcked = false;
149
+ this.cfg.onStatus?.("disconnected");
150
+ if (this._disposed) return;
151
+ const delay = this._backoffMs;
152
+ this._backoffMs = Math.min(this._backoffMs * 2, 3e4);
153
+ setTimeout(() => {
154
+ if (!this._disposed) void this.connect();
155
+ }, delay);
156
+ }
157
+ };
158
+ async function fetchCentrifugoToken(endpoint, projectUuid, authToken, fetchImpl = fetch) {
159
+ const r = await fetchImpl(endpoint, {
160
+ method: "POST",
161
+ headers: {
162
+ "Content-Type": "application/json",
163
+ Authorization: `ApiKey ${authToken}`
164
+ },
165
+ body: JSON.stringify({ project_uuid: projectUuid })
166
+ });
167
+ if (!r.ok) {
168
+ throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);
169
+ }
170
+ return await r.json();
171
+ }
172
+
76
173
  // src/i18n.ts
77
174
  var DEFAULT_API_BASE = "https://api.verbumia.ca";
78
175
  var DEFAULT_CDN_BASE = "https://cdn.verbumia.ca";
79
176
  var DEFAULT_FLUSH_MS = 5e3;
80
177
  var DEFAULT_BATCH = 50;
81
178
  var DEFAULT_BUFFER = 200;
179
+ var DEFAULT_VERSION_SLUG = "main";
180
+ var CLDR_CATEGORIES = /* @__PURE__ */ new Set([
181
+ "zero",
182
+ "one",
183
+ "two",
184
+ "few",
185
+ "many",
186
+ "other"
187
+ ]);
188
+ function isPluralForms(v) {
189
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
190
+ const keys = Object.keys(v);
191
+ if (keys.length === 0) return false;
192
+ if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;
193
+ return keys.every(
194
+ (k) => typeof v[k] === "string"
195
+ );
196
+ }
82
197
  function resolve(bundle, key) {
83
198
  if (!bundle) return void 0;
84
199
  const parts = key.split(".");
@@ -90,7 +205,22 @@ function resolve(bundle, key) {
90
205
  return void 0;
91
206
  }
92
207
  }
93
- return typeof cur === "string" ? cur : void 0;
208
+ if (typeof cur === "string") return cur;
209
+ if (isPluralForms(cur)) return cur;
210
+ return void 0;
211
+ }
212
+ function selectPluralForm(forms, count, locale) {
213
+ let category = "other";
214
+ try {
215
+ if (typeof Intl !== "undefined" && typeof Intl.PluralRules === "function") {
216
+ category = new Intl.PluralRules(locale).select(count);
217
+ }
218
+ } catch {
219
+ }
220
+ if (category in forms) return forms[category];
221
+ if ("other" in forms) return forms["other"];
222
+ const first = Object.keys(forms)[0];
223
+ return first ? forms[first] : "";
94
224
  }
95
225
  function interpolate(template, options) {
96
226
  if (!options) return template;
@@ -115,6 +245,7 @@ var VerbumiaI18n = class {
115
245
  // dedup `${locale}/${ns}/${key}` per-flush
116
246
  _timer = null;
117
247
  _listeners = /* @__PURE__ */ new Set();
248
+ _live = null;
118
249
  constructor(config) {
119
250
  this.locale = config.defaultLocale;
120
251
  this.fallbackLng = config.fallbackLng;
@@ -127,7 +258,11 @@ var VerbumiaI18n = class {
127
258
  namespaces: config.namespaces?.length ? config.namespaces : ["common"],
128
259
  flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,
129
260
  flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,
130
- missingEventsBufferSize: config.missingEventsBufferSize ?? DEFAULT_BUFFER
261
+ missingEventsBufferSize: config.missingEventsBufferSize ?? DEFAULT_BUFFER,
262
+ versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,
263
+ liveUpdates: !!config.liveUpdates,
264
+ centrifugoTokenEndpoint: config.centrifugoTokenEndpoint ?? `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, "")}/v1/auth/centrifugo-token`,
265
+ centrifugoWsUrl: config.centrifugoWsUrl ?? ""
131
266
  };
132
267
  this._transport = config.transport ?? (this._config.missingHandler === "log" ? logTransport : defaultTransport({
133
268
  apiBase: this._config.apiBase,
@@ -155,6 +290,9 @@ var VerbumiaI18n = class {
155
290
  );
156
291
  this.ready = true;
157
292
  this._startTimer();
293
+ if (this._config.liveUpdates) {
294
+ this._startLive(fetchImpl);
295
+ }
158
296
  this._notify();
159
297
  }
160
298
  setLocale = async (next) => {
@@ -173,6 +311,79 @@ var VerbumiaI18n = class {
173
311
  clearInterval(this._timer);
174
312
  this._timer = null;
175
313
  }
314
+ if (this._live) {
315
+ this._live.dispose();
316
+ this._live = null;
317
+ }
318
+ }
319
+ /**
320
+ * Start the Centrifugo subscription and re-fetch the relevant bundle on
321
+ * each `translations_published` event. Best-effort: if the WS URL or
322
+ * token endpoint isn't reachable, we log silently and the SDK continues
323
+ * to serve the initial bundle.
324
+ */
325
+ _startLive(fetchImpl) {
326
+ const wsUrl = this._config.centrifugoWsUrl;
327
+ if (!wsUrl) {
328
+ if (typeof console !== "undefined") {
329
+ console.warn(
330
+ "@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription."
331
+ );
332
+ }
333
+ return;
334
+ }
335
+ const projectUuid = this._config.projectUuid;
336
+ const tokenEndpoint = this._config.centrifugoTokenEndpoint;
337
+ const apiToken = this._config.token;
338
+ const refreshToken = async () => {
339
+ const { token } = await fetchCentrifugoToken(
340
+ tokenEndpoint,
341
+ projectUuid,
342
+ apiToken,
343
+ fetchImpl
344
+ );
345
+ return token;
346
+ };
347
+ void (async () => {
348
+ let channel;
349
+ let token;
350
+ try {
351
+ const minted = await fetchCentrifugoToken(
352
+ tokenEndpoint,
353
+ projectUuid,
354
+ apiToken,
355
+ fetchImpl
356
+ );
357
+ channel = minted.channel;
358
+ token = minted.token;
359
+ } catch (err) {
360
+ if (typeof console !== "undefined") {
361
+ console.warn("@verbumia/react-i18next: live token mint failed", err);
362
+ }
363
+ return;
364
+ }
365
+ this._live = new LiveClient({
366
+ url: wsUrl,
367
+ token,
368
+ channel,
369
+ refreshToken,
370
+ onMessage: (data) => this._onLiveMessage(data, fetchImpl)
371
+ });
372
+ void this._live.connect();
373
+ })();
374
+ }
375
+ _onLiveMessage(data, fetchImpl) {
376
+ if (!data || typeof data !== "object") return;
377
+ const d = data;
378
+ if (d.event !== "translations_published") return;
379
+ const lang = d.language_code;
380
+ const ns = d.namespace_slug;
381
+ if (!lang || !ns) return;
382
+ const cacheKey = `${lang}/${ns}`;
383
+ if (!this._attempted.has(cacheKey)) return;
384
+ void this._loadBundle(lang, ns, fetchImpl).then(() => {
385
+ this._notify();
386
+ });
176
387
  }
177
388
  // ---- Translation ----
178
389
  t = (key, options) => {
@@ -180,10 +391,14 @@ var VerbumiaI18n = class {
180
391
  const bareKey = namespace.bareKey;
181
392
  const ns = namespace.ns;
182
393
  const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);
183
- if (fromActive != null) return interpolate(fromActive, options);
394
+ if (fromActive != null) {
395
+ return this._render(fromActive, this.locale, options);
396
+ }
184
397
  if (this.fallbackLng && this.fallbackLng !== this.locale) {
185
398
  const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);
186
- if (fb != null) return interpolate(fb, options);
399
+ if (fb != null) {
400
+ return this._render(fb, this.fallbackLng, options);
401
+ }
187
402
  }
188
403
  if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {
189
404
  this._reportMissing({
@@ -210,6 +425,20 @@ var VerbumiaI18n = class {
210
425
  }
211
426
  };
212
427
  // ---- Internals ----
428
+ /**
429
+ * Final-stage render: pick the right plural form (when value is a CLDR
430
+ * dict and `options.count` is a number) then interpolate `{{var}}`.
431
+ */
432
+ _render(value, locale, options) {
433
+ let str;
434
+ if (typeof value === "string") {
435
+ str = value;
436
+ } else {
437
+ const count = typeof options?.count === "number" ? options.count : 0;
438
+ str = selectPluralForm(value, count, locale);
439
+ }
440
+ return interpolate(str, options);
441
+ }
213
442
  _splitNamespace(key) {
214
443
  const idx = key.indexOf(":");
215
444
  if (idx > 0) {
@@ -218,7 +447,7 @@ var VerbumiaI18n = class {
218
447
  return { ns: this._config.namespaces[0], bareKey: key };
219
448
  }
220
449
  async _loadBundle(locale, ns, fetchImpl = fetch) {
221
- const url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/latest/${locale}/${ns}.json`;
450
+ const url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/${this._config.versionSlug}/latest/${locale}/${ns}.json`;
222
451
  try {
223
452
  const r = await fetchImpl(url, { method: "GET", credentials: "omit" });
224
453
  if (r.ok) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["export { VerbumiaProvider } from \"./provider\";\nexport { useTranslation } from \"./hooks\";\nexport { Trans } from \"./trans\";\nexport type {\n I18nInstance,\n Locale,\n MissingHandlerMode,\n MissingKeyEvent,\n Namespace,\n TranslationFunction,\n TranslationOptions,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nexport { defaultTransport, logTransport } from \"./transport\";\n","import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(\n i18n.subscribe,\n () => ({\n ready: i18n.ready,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: i18n.missingEvents,\n flushMissing: i18n.flushMissing,\n }),\n () => ({\n ready: false,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: [],\n flushMissing: i18n.flushMissing,\n })\n );\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.1.0\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\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;\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\n/** Resolve a dotted key against a deeply-nested bundle. */\nfunction resolve(bundle: Bundle | undefined, key: string): string | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n return typeof cur === \"string\" ? cur : undefined;\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n private _config: Required<\n Pick<VerbumiaConfig, \"apiBase\" | \"cdnBase\" | \"missingHandler\">\n > & {\n token: string;\n projectUuid: string;\n namespaces: string[];\n flushIntervalMs: number;\n flushBatchSize: number;\n missingEventsBufferSize: number;\n };\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\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 };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n private _notify(): void {\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n 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 }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) return interpolate(fromActive, options);\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) return interpolate(fb, options);\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: typeof options?.defaultValue === \"string\" ? options.defaultValue : undefined,\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch\n ): Promise<void> {\n const url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/latest/${locale}/${ns}.json`;\n try {\n const r = await fetchImpl(url, { method: \"GET\", credentials: \"omit\" });\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(`${locale}/${ns}`, data);\n } else {\n // 404 = no published bundle yet — empty bundle is fine, missing keys flow handles it.\n this._bundles.set(`${locale}/${ns}`, {});\n }\n } catch {\n this._bundles.set(`${locale}/${ns}`, {});\n } finally {\n this._attempted.add(`${locale}/${ns}`);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["export { VerbumiaProvider } from \"./provider\";\nexport { useTranslation } from \"./hooks\";\nexport { Trans } from \"./trans\";\nexport type {\n I18nInstance,\n Locale,\n MissingHandlerMode,\n MissingKeyEvent,\n Namespace,\n TranslationFunction,\n TranslationOptions,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nexport { defaultTransport, logTransport } from \"./transport\";\n","import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(\n i18n.subscribe,\n () => ({\n ready: i18n.ready,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: i18n.missingEvents,\n flushMissing: i18n.flushMissing,\n }),\n () => ({\n ready: false,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: [],\n flushMissing: i18n.flushMissing,\n })\n );\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.1.0\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n const ws = new WebSocket(this.cfg.url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: { id?: number; connect?: unknown; subscribe?: unknown; push?: { channel?: string; pub?: { data?: unknown } } } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { LiveClient, fetchCentrifugoToken } from \"./live\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\ntype PluralForms = Record<string, string>;\ntype ResolvedValue = string | PluralForms;\n\n/**\n * Plural-form objects mirror the CLDR `Intl.PluralRules` categories. Treat\n * any object whose keys overlap the CLDR set AND whose values are all\n * strings as a plural object — the chunky type guard keeps stray nested\n * namespaces from being misread.\n */\nconst CLDR_CATEGORIES = new Set<string>([\n \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n]);\n\nfunction isPluralForms(v: unknown): v is PluralForms {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return false;\n const keys = Object.keys(v as object);\n if (keys.length === 0) return false;\n if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;\n return keys.every(\n (k) => typeof (v as Record<string, unknown>)[k] === \"string\",\n );\n}\n\n/** Resolve a dotted key against a deeply-nested bundle. Returns either a\n * plain string OR a CLDR plural-forms dict so the caller can pick a form. */\nfunction resolve(bundle: Bundle | undefined, key: string): ResolvedValue | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n if (typeof cur === \"string\") return cur;\n if (isPluralForms(cur)) return cur;\n return undefined;\n}\n\n/**\n * Pick the right CLDR form for `count` against the active locale's plural\n * rules. Falls back to `other` (always required by the contract) and then\n * the first available form so we never render nothing for a configured key.\n */\nfunction selectPluralForm(\n forms: PluralForms,\n count: number,\n locale: string,\n): string {\n let category: string = \"other\";\n try {\n if (typeof Intl !== \"undefined\" && typeof Intl.PluralRules === \"function\") {\n category = new Intl.PluralRules(locale).select(count);\n }\n } catch {\n // Bad locale tag — fall through to \"other\".\n }\n if (category in forms) return forms[category]!;\n if (\"other\" in forms) return forms[\"other\"]!;\n const first = Object.keys(forms)[0];\n return first ? forms[first]! : \"\";\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n private _config: Required<\n Pick<VerbumiaConfig, \"apiBase\" | \"cdnBase\" | \"missingHandler\">\n > & {\n token: string;\n projectUuid: string;\n namespaces: string[];\n flushIntervalMs: number;\n flushBatchSize: number;\n missingEventsBufferSize: number;\n versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: string;\n };\n\n private _transport: Transport;\n private _pending: MissingKeyEvent[] = [];\n private _seen = new Set<string>(); // dedup `${locale}/${ns}/${key}` per-flush\n private _timer: ReturnType<typeof setInterval> | null = null;\n private _listeners = new Set<Listener>();\n private _live: LiveClient | null = null;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n private _notify(): void {\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n if (this._config.liveUpdates) {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n void this._loadBundle(lang, ns, fetchImpl).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string; count?: number }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: typeof options?.defaultValue === \"string\" ? options.defaultValue : undefined,\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n /**\n * Final-stage render: pick the right plural form (when value is a CLDR\n * dict and `options.count` is a number) then interpolate `{{var}}`.\n */\n private _render(\n value: ResolvedValue,\n locale: Locale,\n options?: Record<string, unknown> & { count?: number },\n ): string {\n let str: string;\n if (typeof value === \"string\") {\n str = value;\n } else {\n const count = typeof options?.count === \"number\" ? options.count : 0;\n str = selectPluralForm(value, count, locale);\n }\n return interpolate(str, options);\n }\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch\n ): Promise<void> {\n const url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.versionSlug}/latest/${locale}/${ns}.json`;\n try {\n const r = await fetchImpl(url, { method: \"GET\", credentials: \"omit\" });\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(`${locale}/${ns}`, data);\n } else {\n // 404 = no published bundle yet — empty bundle is fine, missing keys flow handles it.\n this._bundles.set(`${locale}/${ns}`, {});\n }\n } catch {\n this._bundles.set(`${locale}/${ns}`, {});\n } finally {\n this._attempted.add(`${locale}/${ns}`);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime"]}
package/dist/index.d.cts CHANGED
@@ -40,6 +40,32 @@ interface VerbumiaConfig {
40
40
  flushBatchSize?: number;
41
41
  /** Optional ring buffer cap for `i18n.missingEvents`. Default 200. */
42
42
  missingEventsBufferSize?: number;
43
+ /**
44
+ * Project version slug used when fetching CDN bundles. Maps to the
45
+ * `/p/{projectUuid}/{versionSlug}/latest/{lang}/{ns}.json` path layout.
46
+ * Defaults to `main`.
47
+ */
48
+ versionSlug?: string;
49
+ /**
50
+ * When true, the provider connects to Centrifugo and re-fetches a bundle
51
+ * whenever the backend publishes a new release on its
52
+ * `translations:org_<>:project_<>` channel. Off by default — opt-in for
53
+ * apps that want zero-deploy translation updates.
54
+ */
55
+ liveUpdates?: boolean;
56
+ /**
57
+ * Endpoint that mints a short-lived Centrifugo connection token for this
58
+ * project. Defaults to `${apiBase}/v1/auth/centrifugo-token`. Override
59
+ * for self-hosted deployments. The endpoint must accept POST with
60
+ * `{project_uuid}` and return `{token, channel, expires_at, ttl_seconds}`.
61
+ */
62
+ centrifugoTokenEndpoint?: string;
63
+ /**
64
+ * WebSocket URL of the Centrifugo node, e.g.
65
+ * `wss://centrifugo.verbumia.ca/connection/websocket`. Required when
66
+ * `liveUpdates: true` (the SDK can't infer it from the API base).
67
+ */
68
+ centrifugoWsUrl?: string;
43
69
  }
44
70
  interface I18nInstance {
45
71
  /** True once the initial namespace bundles loaded for the active locale. */
package/dist/index.d.ts CHANGED
@@ -40,6 +40,32 @@ interface VerbumiaConfig {
40
40
  flushBatchSize?: number;
41
41
  /** Optional ring buffer cap for `i18n.missingEvents`. Default 200. */
42
42
  missingEventsBufferSize?: number;
43
+ /**
44
+ * Project version slug used when fetching CDN bundles. Maps to the
45
+ * `/p/{projectUuid}/{versionSlug}/latest/{lang}/{ns}.json` path layout.
46
+ * Defaults to `main`.
47
+ */
48
+ versionSlug?: string;
49
+ /**
50
+ * When true, the provider connects to Centrifugo and re-fetches a bundle
51
+ * whenever the backend publishes a new release on its
52
+ * `translations:org_<>:project_<>` channel. Off by default — opt-in for
53
+ * apps that want zero-deploy translation updates.
54
+ */
55
+ liveUpdates?: boolean;
56
+ /**
57
+ * Endpoint that mints a short-lived Centrifugo connection token for this
58
+ * project. Defaults to `${apiBase}/v1/auth/centrifugo-token`. Override
59
+ * for self-hosted deployments. The endpoint must accept POST with
60
+ * `{project_uuid}` and return `{token, channel, expires_at, ttl_seconds}`.
61
+ */
62
+ centrifugoTokenEndpoint?: string;
63
+ /**
64
+ * WebSocket URL of the Centrifugo node, e.g.
65
+ * `wss://centrifugo.verbumia.ca/connection/websocket`. Required when
66
+ * `liveUpdates: true` (the SDK can't infer it from the API base).
67
+ */
68
+ centrifugoWsUrl?: string;
43
69
  }
44
70
  interface I18nInstance {
45
71
  /** True once the initial namespace bundles loaded for the active locale. */
package/dist/index.js CHANGED
@@ -49,12 +49,127 @@ var logTransport = (batch) => {
49
49
  }
50
50
  };
51
51
 
52
+ // src/live.ts
53
+ var LiveClient = class {
54
+ constructor(cfg) {
55
+ this.cfg = cfg;
56
+ }
57
+ cfg;
58
+ _ws = null;
59
+ _id = 0;
60
+ _backoffMs = 1e3;
61
+ _disposed = false;
62
+ _connectAcked = false;
63
+ /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */
64
+ async connect() {
65
+ if (this._ws) return;
66
+ if (this._disposed) return;
67
+ this.cfg.onStatus?.("connecting");
68
+ const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;
69
+ const ws = new WebSocket(this.cfg.url);
70
+ this._ws = ws;
71
+ this._connectAcked = false;
72
+ ws.onopen = () => {
73
+ this._send({ id: ++this._id, connect: { token } });
74
+ };
75
+ ws.onmessage = (evt) => this._onFrame(evt.data);
76
+ ws.onclose = () => this._onClose();
77
+ ws.onerror = () => {
78
+ };
79
+ }
80
+ dispose() {
81
+ this._disposed = true;
82
+ if (this._ws) {
83
+ try {
84
+ this._ws.close();
85
+ } catch {
86
+ }
87
+ this._ws = null;
88
+ }
89
+ this.cfg.onStatus?.("disconnected");
90
+ }
91
+ // ---- internals ----
92
+ _send(msg) {
93
+ if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
94
+ try {
95
+ this._ws.send(JSON.stringify(msg));
96
+ } catch {
97
+ }
98
+ }
99
+ _onFrame(raw) {
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(raw);
103
+ } catch {
104
+ return;
105
+ }
106
+ if (!parsed || typeof parsed !== "object") return;
107
+ if (parsed.connect && !this._connectAcked) {
108
+ this._connectAcked = true;
109
+ this.cfg.onStatus?.("connected");
110
+ this._backoffMs = 1e3;
111
+ this._send({
112
+ id: ++this._id,
113
+ subscribe: { channel: this.cfg.channel }
114
+ });
115
+ return;
116
+ }
117
+ const push = parsed.push;
118
+ if (push && push.channel === this.cfg.channel && push.pub) {
119
+ this.cfg.onMessage(push.pub.data);
120
+ }
121
+ }
122
+ _onClose() {
123
+ this._ws = null;
124
+ this._connectAcked = false;
125
+ this.cfg.onStatus?.("disconnected");
126
+ if (this._disposed) return;
127
+ const delay = this._backoffMs;
128
+ this._backoffMs = Math.min(this._backoffMs * 2, 3e4);
129
+ setTimeout(() => {
130
+ if (!this._disposed) void this.connect();
131
+ }, delay);
132
+ }
133
+ };
134
+ async function fetchCentrifugoToken(endpoint, projectUuid, authToken, fetchImpl = fetch) {
135
+ const r = await fetchImpl(endpoint, {
136
+ method: "POST",
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ Authorization: `ApiKey ${authToken}`
140
+ },
141
+ body: JSON.stringify({ project_uuid: projectUuid })
142
+ });
143
+ if (!r.ok) {
144
+ throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);
145
+ }
146
+ return await r.json();
147
+ }
148
+
52
149
  // src/i18n.ts
53
150
  var DEFAULT_API_BASE = "https://api.verbumia.ca";
54
151
  var DEFAULT_CDN_BASE = "https://cdn.verbumia.ca";
55
152
  var DEFAULT_FLUSH_MS = 5e3;
56
153
  var DEFAULT_BATCH = 50;
57
154
  var DEFAULT_BUFFER = 200;
155
+ var DEFAULT_VERSION_SLUG = "main";
156
+ var CLDR_CATEGORIES = /* @__PURE__ */ new Set([
157
+ "zero",
158
+ "one",
159
+ "two",
160
+ "few",
161
+ "many",
162
+ "other"
163
+ ]);
164
+ function isPluralForms(v) {
165
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
166
+ const keys = Object.keys(v);
167
+ if (keys.length === 0) return false;
168
+ if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;
169
+ return keys.every(
170
+ (k) => typeof v[k] === "string"
171
+ );
172
+ }
58
173
  function resolve(bundle, key) {
59
174
  if (!bundle) return void 0;
60
175
  const parts = key.split(".");
@@ -66,7 +181,22 @@ function resolve(bundle, key) {
66
181
  return void 0;
67
182
  }
68
183
  }
69
- return typeof cur === "string" ? cur : void 0;
184
+ if (typeof cur === "string") return cur;
185
+ if (isPluralForms(cur)) return cur;
186
+ return void 0;
187
+ }
188
+ function selectPluralForm(forms, count, locale) {
189
+ let category = "other";
190
+ try {
191
+ if (typeof Intl !== "undefined" && typeof Intl.PluralRules === "function") {
192
+ category = new Intl.PluralRules(locale).select(count);
193
+ }
194
+ } catch {
195
+ }
196
+ if (category in forms) return forms[category];
197
+ if ("other" in forms) return forms["other"];
198
+ const first = Object.keys(forms)[0];
199
+ return first ? forms[first] : "";
70
200
  }
71
201
  function interpolate(template, options) {
72
202
  if (!options) return template;
@@ -91,6 +221,7 @@ var VerbumiaI18n = class {
91
221
  // dedup `${locale}/${ns}/${key}` per-flush
92
222
  _timer = null;
93
223
  _listeners = /* @__PURE__ */ new Set();
224
+ _live = null;
94
225
  constructor(config) {
95
226
  this.locale = config.defaultLocale;
96
227
  this.fallbackLng = config.fallbackLng;
@@ -103,7 +234,11 @@ var VerbumiaI18n = class {
103
234
  namespaces: config.namespaces?.length ? config.namespaces : ["common"],
104
235
  flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,
105
236
  flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,
106
- missingEventsBufferSize: config.missingEventsBufferSize ?? DEFAULT_BUFFER
237
+ missingEventsBufferSize: config.missingEventsBufferSize ?? DEFAULT_BUFFER,
238
+ versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,
239
+ liveUpdates: !!config.liveUpdates,
240
+ centrifugoTokenEndpoint: config.centrifugoTokenEndpoint ?? `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, "")}/v1/auth/centrifugo-token`,
241
+ centrifugoWsUrl: config.centrifugoWsUrl ?? ""
107
242
  };
108
243
  this._transport = config.transport ?? (this._config.missingHandler === "log" ? logTransport : defaultTransport({
109
244
  apiBase: this._config.apiBase,
@@ -131,6 +266,9 @@ var VerbumiaI18n = class {
131
266
  );
132
267
  this.ready = true;
133
268
  this._startTimer();
269
+ if (this._config.liveUpdates) {
270
+ this._startLive(fetchImpl);
271
+ }
134
272
  this._notify();
135
273
  }
136
274
  setLocale = async (next) => {
@@ -149,6 +287,79 @@ var VerbumiaI18n = class {
149
287
  clearInterval(this._timer);
150
288
  this._timer = null;
151
289
  }
290
+ if (this._live) {
291
+ this._live.dispose();
292
+ this._live = null;
293
+ }
294
+ }
295
+ /**
296
+ * Start the Centrifugo subscription and re-fetch the relevant bundle on
297
+ * each `translations_published` event. Best-effort: if the WS URL or
298
+ * token endpoint isn't reachable, we log silently and the SDK continues
299
+ * to serve the initial bundle.
300
+ */
301
+ _startLive(fetchImpl) {
302
+ const wsUrl = this._config.centrifugoWsUrl;
303
+ if (!wsUrl) {
304
+ if (typeof console !== "undefined") {
305
+ console.warn(
306
+ "@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription."
307
+ );
308
+ }
309
+ return;
310
+ }
311
+ const projectUuid = this._config.projectUuid;
312
+ const tokenEndpoint = this._config.centrifugoTokenEndpoint;
313
+ const apiToken = this._config.token;
314
+ const refreshToken = async () => {
315
+ const { token } = await fetchCentrifugoToken(
316
+ tokenEndpoint,
317
+ projectUuid,
318
+ apiToken,
319
+ fetchImpl
320
+ );
321
+ return token;
322
+ };
323
+ void (async () => {
324
+ let channel;
325
+ let token;
326
+ try {
327
+ const minted = await fetchCentrifugoToken(
328
+ tokenEndpoint,
329
+ projectUuid,
330
+ apiToken,
331
+ fetchImpl
332
+ );
333
+ channel = minted.channel;
334
+ token = minted.token;
335
+ } catch (err) {
336
+ if (typeof console !== "undefined") {
337
+ console.warn("@verbumia/react-i18next: live token mint failed", err);
338
+ }
339
+ return;
340
+ }
341
+ this._live = new LiveClient({
342
+ url: wsUrl,
343
+ token,
344
+ channel,
345
+ refreshToken,
346
+ onMessage: (data) => this._onLiveMessage(data, fetchImpl)
347
+ });
348
+ void this._live.connect();
349
+ })();
350
+ }
351
+ _onLiveMessage(data, fetchImpl) {
352
+ if (!data || typeof data !== "object") return;
353
+ const d = data;
354
+ if (d.event !== "translations_published") return;
355
+ const lang = d.language_code;
356
+ const ns = d.namespace_slug;
357
+ if (!lang || !ns) return;
358
+ const cacheKey = `${lang}/${ns}`;
359
+ if (!this._attempted.has(cacheKey)) return;
360
+ void this._loadBundle(lang, ns, fetchImpl).then(() => {
361
+ this._notify();
362
+ });
152
363
  }
153
364
  // ---- Translation ----
154
365
  t = (key, options) => {
@@ -156,10 +367,14 @@ var VerbumiaI18n = class {
156
367
  const bareKey = namespace.bareKey;
157
368
  const ns = namespace.ns;
158
369
  const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);
159
- if (fromActive != null) return interpolate(fromActive, options);
370
+ if (fromActive != null) {
371
+ return this._render(fromActive, this.locale, options);
372
+ }
160
373
  if (this.fallbackLng && this.fallbackLng !== this.locale) {
161
374
  const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);
162
- if (fb != null) return interpolate(fb, options);
375
+ if (fb != null) {
376
+ return this._render(fb, this.fallbackLng, options);
377
+ }
163
378
  }
164
379
  if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {
165
380
  this._reportMissing({
@@ -186,6 +401,20 @@ var VerbumiaI18n = class {
186
401
  }
187
402
  };
188
403
  // ---- Internals ----
404
+ /**
405
+ * Final-stage render: pick the right plural form (when value is a CLDR
406
+ * dict and `options.count` is a number) then interpolate `{{var}}`.
407
+ */
408
+ _render(value, locale, options) {
409
+ let str;
410
+ if (typeof value === "string") {
411
+ str = value;
412
+ } else {
413
+ const count = typeof options?.count === "number" ? options.count : 0;
414
+ str = selectPluralForm(value, count, locale);
415
+ }
416
+ return interpolate(str, options);
417
+ }
189
418
  _splitNamespace(key) {
190
419
  const idx = key.indexOf(":");
191
420
  if (idx > 0) {
@@ -194,7 +423,7 @@ var VerbumiaI18n = class {
194
423
  return { ns: this._config.namespaces[0], bareKey: key };
195
424
  }
196
425
  async _loadBundle(locale, ns, fetchImpl = fetch) {
197
- const url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/latest/${locale}/${ns}.json`;
426
+ const url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/${this._config.versionSlug}/latest/${locale}/${ns}.json`;
198
427
  try {
199
428
  const r = await fetchImpl(url, { method: "GET", credentials: "omit" });
200
429
  if (r.ok) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(\n i18n.subscribe,\n () => ({\n ready: i18n.ready,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: i18n.missingEvents,\n flushMissing: i18n.flushMissing,\n }),\n () => ({\n ready: false,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: [],\n flushMissing: i18n.flushMissing,\n })\n );\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.1.0\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\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;\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\n/** Resolve a dotted key against a deeply-nested bundle. */\nfunction resolve(bundle: Bundle | undefined, key: string): string | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n return typeof cur === \"string\" ? cur : undefined;\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n private _config: Required<\n Pick<VerbumiaConfig, \"apiBase\" | \"cdnBase\" | \"missingHandler\">\n > & {\n token: string;\n projectUuid: string;\n namespaces: string[];\n flushIntervalMs: number;\n flushBatchSize: number;\n missingEventsBufferSize: number;\n };\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\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 };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n private _notify(): void {\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n 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 }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) return interpolate(fromActive, options);\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) return interpolate(fb, options);\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: typeof options?.defaultValue === \"string\" ? options.defaultValue : undefined,\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch\n ): Promise<void> {\n const url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/latest/${locale}/${ns}.json`;\n try {\n const r = await fetchImpl(url, { method: \"GET\", credentials: \"omit\" });\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(`${locale}/${ns}`, data);\n } else {\n // 404 = no published bundle yet — empty bundle is fine, missing keys flow handles it.\n this._bundles.set(`${locale}/${ns}`, {});\n }\n } catch {\n this._bundles.set(`${locale}/${ns}`, {});\n } finally {\n this._attempted.add(`${locale}/${ns}`);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["useMemo","useMemo","jsx"]}
1
+ {"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/live.ts","../src/i18n.ts","../src/hooks.ts","../src/trans.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport { VerbumiaI18n } from \"./i18n\";\nimport type { I18nInstance, VerbumiaConfig } from \"./types\";\n\ninterface VerbumiaContextValue {\n i18n: VerbumiaI18n;\n}\n\nconst VerbumiaContext = createContext<VerbumiaContextValue | null>(null);\n\nexport interface VerbumiaProviderProps extends VerbumiaConfig {\n children: ReactNode;\n}\n\nexport function VerbumiaProvider({\n children,\n ...config\n}: VerbumiaProviderProps) {\n // Stable instance for the lifetime of the provider mount.\n const i18n = useMemo(() => new VerbumiaI18n(config), []); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n void i18n.start();\n return () => i18n.stop();\n }, [i18n]);\n\n const value = useMemo<VerbumiaContextValue>(() => ({ i18n }), [i18n]);\n return (\n <VerbumiaContext.Provider value={value}>{children}</VerbumiaContext.Provider>\n );\n}\n\n/** Internal — used by useTranslation + Trans. */\nexport function useI18n(): VerbumiaI18n {\n const ctx = useContext(VerbumiaContext);\n if (!ctx) {\n throw new Error(\"useTranslation/Trans must be used inside <VerbumiaProvider>\");\n }\n return ctx.i18n;\n}\n\n/** Subscribes to the i18n store and returns a snapshot the React tree can render. */\nexport function useI18nSnapshot(): I18nInstance {\n const i18n = useI18n();\n return useSyncExternalStore(\n i18n.subscribe,\n () => ({\n ready: i18n.ready,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: i18n.missingEvents,\n flushMissing: i18n.flushMissing,\n }),\n () => ({\n ready: false,\n locale: i18n.locale,\n setLocale: i18n.setLocale,\n missingEvents: [],\n flushMissing: i18n.flushMissing,\n })\n );\n}\n","import type { MissingKeyEvent, Transport } from \"./types\";\n\nconst SDK_LIB = \"@verbumia/react-i18next\";\nconst SDK_VER = \"0.1.0\";\n\n/** Default transport: POST to `${apiBase}/v1/missing` with the API key. */\nexport function defaultTransport(opts: {\n apiBase: string;\n token: string;\n projectUuid: string;\n}): Transport {\n return async (batch) => {\n if (!batch.length) return;\n const body = {\n project_uuid: opts.projectUuid,\n events: batch.map((e) => ({\n key: e.key,\n namespace: e.namespace,\n language_code: e.language_code,\n source_value: e.source_value,\n sdk_meta: {\n lib: SDK_LIB,\n ver: SDK_VER,\n ...(typeof window !== \"undefined\"\n ? { url: window.location?.href }\n : {}),\n ...(e.sdk_meta ?? {}),\n },\n })),\n };\n try {\n await fetch(`${opts.apiBase.replace(/\\/+$/, \"\")}/v1/missing`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${opts.token}`,\n },\n body: JSON.stringify(body),\n // SDKs are best-effort; never block the render path\n keepalive: true,\n });\n } catch {\n // swallow — missing-key reporting must never break the host app\n }\n };\n}\n\n/** Logs each event to console.warn — handy for dev. */\nexport const logTransport: Transport = (batch: MissingKeyEvent[]) => {\n for (const e of batch) {\n // eslint-disable-next-line no-console\n console.warn(\"[verbumia] missing key\", e);\n }\n};\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Verbumia\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n const ws = new WebSocket(this.cfg.url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: { id?: number; connect?: unknown; subscribe?: unknown; push?: { channel?: string; pub?: { data?: unknown } } } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend. The\n * endpoint signature matches `POST /v1/auth/centrifugo-token` —\n * `{project_uuid}` body, `{token, channel, ...}` response.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify({ project_uuid: projectUuid }),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","import type {\n I18nInstance,\n Locale,\n MissingKeyEvent,\n Namespace,\n Transport,\n VerbumiaConfig,\n} from \"./types\";\nimport { defaultTransport, logTransport } from \"./transport\";\nimport { LiveClient, fetchCentrifugoToken } from \"./live\";\n\nconst DEFAULT_API_BASE = \"https://api.verbumia.ca\";\nconst DEFAULT_CDN_BASE = \"https://cdn.verbumia.ca\";\nconst DEFAULT_FLUSH_MS = 5_000;\nconst DEFAULT_BATCH = 50;\nconst DEFAULT_BUFFER = 200;\nconst DEFAULT_VERSION_SLUG = \"main\";\n\ntype Bundle = Record<string, unknown>;\ntype Listener = () => void;\n\ntype PluralForms = Record<string, string>;\ntype ResolvedValue = string | PluralForms;\n\n/**\n * Plural-form objects mirror the CLDR `Intl.PluralRules` categories. Treat\n * any object whose keys overlap the CLDR set AND whose values are all\n * strings as a plural object — the chunky type guard keeps stray nested\n * namespaces from being misread.\n */\nconst CLDR_CATEGORIES = new Set<string>([\n \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n]);\n\nfunction isPluralForms(v: unknown): v is PluralForms {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return false;\n const keys = Object.keys(v as object);\n if (keys.length === 0) return false;\n if (!keys.some((k) => CLDR_CATEGORIES.has(k))) return false;\n return keys.every(\n (k) => typeof (v as Record<string, unknown>)[k] === \"string\",\n );\n}\n\n/** Resolve a dotted key against a deeply-nested bundle. Returns either a\n * plain string OR a CLDR plural-forms dict so the caller can pick a form. */\nfunction resolve(bundle: Bundle | undefined, key: string): ResolvedValue | undefined {\n if (!bundle) return undefined;\n const parts = key.split(\".\");\n let cur: unknown = bundle;\n for (const p of parts) {\n if (cur && typeof cur === \"object\" && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p];\n } else {\n return undefined;\n }\n }\n if (typeof cur === \"string\") return cur;\n if (isPluralForms(cur)) return cur;\n return undefined;\n}\n\n/**\n * Pick the right CLDR form for `count` against the active locale's plural\n * rules. Falls back to `other` (always required by the contract) and then\n * the first available form so we never render nothing for a configured key.\n */\nfunction selectPluralForm(\n forms: PluralForms,\n count: number,\n locale: string,\n): string {\n let category: string = \"other\";\n try {\n if (typeof Intl !== \"undefined\" && typeof Intl.PluralRules === \"function\") {\n category = new Intl.PluralRules(locale).select(count);\n }\n } catch {\n // Bad locale tag — fall through to \"other\".\n }\n if (category in forms) return forms[category]!;\n if (\"other\" in forms) return forms[\"other\"]!;\n const first = Object.keys(forms)[0];\n return first ? forms[first]! : \"\";\n}\n\n/** Cheap interpolation: replaces `{{name}}` with `options[name]`. */\nfunction interpolate(template: string, options?: Record<string, unknown>): string {\n if (!options) return template;\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_m, name) => {\n const v = options[name];\n return v == null ? \"\" : String(v);\n });\n}\n\n/** A single ready-state + bundle store + missing-key buffer wrapped behind\n * a tiny pub-sub so React can subscribe via useSyncExternalStore. */\nexport class VerbumiaI18n implements I18nInstance {\n ready = false;\n locale: Locale;\n fallbackLng: Locale | undefined;\n missingEvents: MissingKeyEvent[] = [];\n\n private _bundles = new Map<string, Bundle>(); // `${locale}/${ns}` -> tree\n private _attempted = new Set<string>(); // `${locale}/${ns}` keys we've fetched\n private _config: Required<\n Pick<VerbumiaConfig, \"apiBase\" | \"cdnBase\" | \"missingHandler\">\n > & {\n token: string;\n projectUuid: string;\n namespaces: string[];\n flushIntervalMs: number;\n flushBatchSize: number;\n missingEventsBufferSize: number;\n versionSlug: string;\n liveUpdates: boolean;\n centrifugoTokenEndpoint: string;\n centrifugoWsUrl: string;\n };\n\n private _transport: Transport;\n private _pending: MissingKeyEvent[] = [];\n private _seen = new Set<string>(); // dedup `${locale}/${ns}/${key}` per-flush\n private _timer: ReturnType<typeof setInterval> | null = null;\n private _listeners = new Set<Listener>();\n private _live: LiveClient | null = null;\n\n constructor(config: VerbumiaConfig) {\n this.locale = config.defaultLocale;\n this.fallbackLng = config.fallbackLng;\n this._config = {\n apiBase: config.apiBase ?? DEFAULT_API_BASE,\n cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,\n missingHandler: config.missingHandler ?? \"send\",\n token: config.token,\n projectUuid: config.projectUuid,\n namespaces: config.namespaces?.length ? config.namespaces : [\"common\"],\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,\n flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,\n missingEventsBufferSize:\n config.missingEventsBufferSize ?? DEFAULT_BUFFER,\n versionSlug: config.versionSlug ?? DEFAULT_VERSION_SLUG,\n liveUpdates: !!config.liveUpdates,\n centrifugoTokenEndpoint:\n config.centrifugoTokenEndpoint ??\n `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\")}/v1/auth/centrifugo-token`,\n centrifugoWsUrl: config.centrifugoWsUrl ?? \"\",\n };\n\n this._transport =\n config.transport ??\n (this._config.missingHandler === \"log\"\n ? logTransport\n : defaultTransport({\n apiBase: this._config.apiBase,\n token: this._config.token,\n projectUuid: this._config.projectUuid,\n }));\n }\n\n // ---- React subscription ----\n\n subscribe = (listener: Listener): (() => void) => {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener) as unknown as void;\n };\n\n private _notify(): void {\n for (const l of this._listeners) l();\n }\n\n // ---- Lifecycle ----\n\n /** Loads the configured namespaces for the active locale + fallback. */\n async start(fetchImpl: typeof fetch = fetch): Promise<void> {\n const targets = new Set<string>([this.locale]);\n if (this.fallbackLng) targets.add(this.fallbackLng);\n await Promise.all(\n [...targets].flatMap((loc) =>\n this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))\n )\n );\n this.ready = true;\n this._startTimer();\n if (this._config.liveUpdates) {\n this._startLive(fetchImpl);\n }\n this._notify();\n }\n\n setLocale = async (next: Locale): Promise<void> => {\n if (next === this.locale) return;\n this.locale = next;\n this.ready = false;\n this._notify();\n await Promise.all(\n this._config.namespaces.map((ns) => this._loadBundle(next, ns))\n );\n this.ready = true;\n this._notify();\n };\n\n stop(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n if (this._live) {\n this._live.dispose();\n this._live = null;\n }\n }\n\n /**\n * Start the Centrifugo subscription and re-fetch the relevant bundle on\n * each `translations_published` event. Best-effort: if the WS URL or\n * token endpoint isn't reachable, we log silently and the SDK continues\n * to serve the initial bundle.\n */\n private _startLive(fetchImpl: typeof fetch): void {\n const wsUrl = this._config.centrifugoWsUrl;\n if (!wsUrl) {\n // No WS URL configured — emit a console warning and stay static.\n if (typeof console !== \"undefined\") {\n console.warn(\n \"@verbumia/react-i18next: liveUpdates=true but centrifugoWsUrl is empty; skipping subscription.\",\n );\n }\n return;\n }\n const projectUuid = this._config.projectUuid;\n const tokenEndpoint = this._config.centrifugoTokenEndpoint;\n const apiToken = this._config.token;\n\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n return token;\n };\n\n // Bootstrap: fetch the initial token to learn the channel name + token.\n void (async () => {\n let channel: string;\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint, projectUuid, apiToken, fetchImpl,\n );\n channel = minted.channel;\n token = minted.token;\n } catch (err) {\n if (typeof console !== \"undefined\") {\n console.warn(\"@verbumia/react-i18next: live token mint failed\", err);\n }\n return;\n }\n this._live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => this._onLiveMessage(data, fetchImpl),\n });\n void this._live.connect();\n })();\n }\n\n private _onLiveMessage(data: unknown, fetchImpl: typeof fetch): void {\n if (!data || typeof data !== \"object\") return;\n const d = data as { event?: string; language_code?: string; namespace_slug?: string };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n // Only refetch bundles we already loaded — no point pulling a (lang, ns)\n // pair the app never asked for.\n const cacheKey = `${lang}/${ns}`;\n if (!this._attempted.has(cacheKey)) return;\n void this._loadBundle(lang, ns, fetchImpl).then(() => {\n this._notify();\n });\n }\n\n // ---- Translation ----\n\n t = (key: string, options?: Record<string, unknown> & { defaultValue?: string; count?: number }): string => {\n const namespace = this._splitNamespace(key);\n const bareKey = namespace.bareKey;\n const ns = namespace.ns;\n\n const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);\n if (fromActive != null) {\n return this._render(fromActive, this.locale, options);\n }\n\n if (this.fallbackLng && this.fallbackLng !== this.locale) {\n const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);\n if (fb != null) {\n return this._render(fb, this.fallbackLng, options);\n }\n }\n\n // Missing path — only report once we've actually fetched the bundle for\n // this (locale, ns), otherwise the first paint floods the dashboard.\n if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {\n this._reportMissing({\n key: bareKey,\n namespace: ns,\n language_code: this.locale,\n source_value: typeof options?.defaultValue === \"string\" ? options.defaultValue : undefined,\n });\n }\n const defaultValue = options?.defaultValue;\n if (typeof defaultValue === \"string\") {\n return interpolate(defaultValue, options);\n }\n return key;\n };\n\n flushMissing = async (): Promise<void> => {\n if (!this._pending.length) return;\n const batch = this._pending.slice(0);\n this._pending = [];\n if (this._config.missingHandler === \"off\") return;\n try {\n await this._transport(batch);\n } catch {\n // best-effort\n }\n };\n\n // ---- Internals ----\n\n /**\n * Final-stage render: pick the right plural form (when value is a CLDR\n * dict and `options.count` is a number) then interpolate `{{var}}`.\n */\n private _render(\n value: ResolvedValue,\n locale: Locale,\n options?: Record<string, unknown> & { count?: number },\n ): string {\n let str: string;\n if (typeof value === \"string\") {\n str = value;\n } else {\n const count = typeof options?.count === \"number\" ? options.count : 0;\n str = selectPluralForm(value, count, locale);\n }\n return interpolate(str, options);\n }\n\n private _splitNamespace(key: string): { ns: Namespace; bareKey: string } {\n // i18next convention: \"ns:key\"\n const idx = key.indexOf(\":\");\n if (idx > 0) {\n return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };\n }\n return { ns: this._config.namespaces[0]!, bareKey: key };\n }\n\n private async _loadBundle(\n locale: Locale,\n ns: Namespace,\n fetchImpl: typeof fetch = fetch\n ): Promise<void> {\n const url = `${this._config.cdnBase.replace(/\\/+$/, \"\")}/p/${this._config.projectUuid}/${this._config.versionSlug}/latest/${locale}/${ns}.json`;\n try {\n const r = await fetchImpl(url, { method: \"GET\", credentials: \"omit\" });\n if (r.ok) {\n const data = (await r.json()) as Bundle;\n this._bundles.set(`${locale}/${ns}`, data);\n } else {\n // 404 = no published bundle yet — empty bundle is fine, missing keys flow handles it.\n this._bundles.set(`${locale}/${ns}`, {});\n }\n } catch {\n this._bundles.set(`${locale}/${ns}`, {});\n } finally {\n this._attempted.add(`${locale}/${ns}`);\n }\n }\n\n private _startTimer(): void {\n if (this._config.missingHandler === \"off\") return;\n if (typeof setInterval !== \"function\") return;\n this._timer = setInterval(() => {\n void this.flushMissing();\n }, this._config.flushIntervalMs);\n }\n\n private _reportMissing(event: MissingKeyEvent): void {\n if (this._config.missingHandler === \"off\") return;\n const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;\n if (this._seen.has(dedupKey)) return;\n this._seen.add(dedupKey);\n\n // Push to ring buffer (capped) for in-app inspectors.\n this.missingEvents = [event, ...this.missingEvents].slice(\n 0,\n this._config.missingEventsBufferSize\n );\n this._pending.push(event);\n if (this._pending.length >= this._config.flushBatchSize) {\n void this.flushMissing();\n }\n this._notify();\n }\n}\n","import { useMemo } from \"react\";\nimport { useI18n, useI18nSnapshot } from \"./provider\";\nimport type { I18nInstance, TranslationFunction } from \"./types\";\n\nexport interface UseTranslationResult {\n t: TranslationFunction;\n i18n: I18nInstance;\n}\n\n/** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you\n * drop the `ns:` prefix on every call. */\nexport function useTranslation(defaultNamespace?: string): UseTranslationResult {\n const i18n = useI18n();\n const snapshot = useI18nSnapshot();\n const t = useMemo<TranslationFunction>(() => {\n return (key, options) => {\n const fullKey =\n defaultNamespace && !key.includes(\":\") ? `${defaultNamespace}:${key}` : key;\n return i18n.t(fullKey, options);\n };\n }, [i18n, defaultNamespace]);\n return { t, i18n: snapshot };\n}\n","import { Children, cloneElement, isValidElement, type ReactNode } from \"react\";\nimport { useTranslation } from \"./hooks\";\n\nexport interface TransProps {\n /** The translation key (optionally `ns:key`). */\n i18nKey: string;\n /** Default value if the key is missing — used as the fallback string. */\n defaults?: string;\n /** Variables interpolated into `{{var}}` placeholders. */\n values?: Record<string, unknown>;\n /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */\n components?: ReactNode[];\n /** Optional namespace shortcut. */\n namespace?: string;\n}\n\n/** Bare-bones Trans component: resolves the key, interpolates values, and\n * swaps `<0>...</0>` placeholders into the supplied React components.\n * Keeps the surface minimal — full Trans semantics (nested keys, plural\n * trees, gender) land in V1.1. */\nexport function Trans({\n i18nKey,\n defaults,\n values,\n components,\n namespace,\n}: TransProps) {\n const { t } = useTranslation(namespace);\n const raw = t(i18nKey, { ...(values ?? {}), defaultValue: defaults ?? i18nKey });\n if (!components || !components.length) return <>{raw}</>;\n return <>{splitOnComponents(raw, components)}</>;\n}\n\nfunction splitOnComponents(text: string, components: ReactNode[]): ReactNode[] {\n const out: ReactNode[] = [];\n // Match <N>...</N> where N is a 0-based index into `components`.\n const re = /<(\\d+)>(.*?)<\\/\\1>/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));\n const idx = Number(m[1]);\n const inner = m[2];\n const node = components[idx];\n if (isValidElement(node)) {\n out.push(\n cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? \"\"))\n );\n } else if (node !== undefined) {\n out.push(node);\n } else {\n out.push(inner ?? \"\");\n }\n lastIndex = re.lastIndex;\n }\n if (lastIndex < text.length) out.push(text.slice(lastIndex));\n return out;\n}\n"],"mappings":"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","names":["useMemo","useMemo","jsx"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbumia/react-i18next",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "React SDK for Verbumia — translations + realtime missing-key handler.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://verbumia.ca",