@verbumia/react-i18next 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Verbumia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # @verbumia/react-i18next
2
+
3
+ [![MIT licensed](https://img.shields.io/npm/l/@verbumia/react-i18next.svg)](./LICENSE)
4
+
5
+ The React SDK for [Verbumia](https://verbumia.ca). Resolve translations from
6
+ the Verbumia CDN, fall back gracefully when a key is missing, and stream those
7
+ missing keys back to your dashboard in real time so the team can fill them
8
+ without redeploying.
9
+
10
+ ```bash
11
+ npm install @verbumia/react-i18next
12
+ ```
13
+
14
+ - ✦ Zero-config CDN fetch (Bunny.net edge)
15
+ - ✦ Built-in missing-key handler with first-paint anti-spam gate
16
+ - ✦ Pluggable transport for Storybook / inspectors
17
+ - ✦ < 10 KB ESM (gzipped much smaller), tree-shakeable
18
+ - ✦ Plain `t()` + `<Trans>` semantics — drop-in for most i18next codebases
19
+
20
+ ---
21
+
22
+ ## Quickstart
23
+
24
+ ```tsx
25
+ import { VerbumiaProvider, useTranslation } from "@verbumia/react-i18next";
26
+
27
+ export function App() {
28
+ return (
29
+ <VerbumiaProvider
30
+ token={import.meta.env.VITE_VERBUMIA_TOKEN}
31
+ projectUuid={import.meta.env.VITE_VERBUMIA_PROJECT}
32
+ defaultLocale="fr"
33
+ fallbackLng="en"
34
+ namespaces={["common"]}
35
+ >
36
+ <Hello />
37
+ </VerbumiaProvider>
38
+ );
39
+ }
40
+
41
+ function Hello() {
42
+ const { t, i18n } = useTranslation("common");
43
+ if (!i18n.ready) return <span>Loading…</span>;
44
+ return <h1>{t("hello.title", { name: "Marc", defaultValue: "Hello {{name}}" })}</h1>;
45
+ }
46
+ ```
47
+
48
+ The token is the API key minted in **Org Settings → API Keys**. For the
49
+ browser SDK use a **project-scoped** key with the `missing:write` scope and
50
+ nothing else — that key only sees missing-key writes for one project, which
51
+ is the safest exposure profile.
52
+
53
+ ---
54
+
55
+ ## API surface
56
+
57
+ ### `VerbumiaProvider`
58
+
59
+ ```ts
60
+ interface VerbumiaConfig {
61
+ token: string; // vrb_live_<prefix>.<secret>
62
+ projectUuid: string;
63
+ defaultLocale: string; // BCP-47 (e.g. "fr", "fr-CA")
64
+ fallbackLng?: string; // resolved before reporting a key as missing
65
+ namespaces?: string[]; // default ['common']
66
+ apiBase?: string; // default 'https://api.verbumia.ca'
67
+ cdnBase?: string; // default 'https://cdn.verbumia.ca'
68
+ transport?: (batch: MissingKeyEvent[]) => void | Promise<void>;
69
+ missingHandler?: 'send' | 'log' | 'off'; // default 'send'
70
+ flushIntervalMs?: number; // default 5000
71
+ flushBatchSize?: number; // default 50
72
+ missingEventsBufferSize?: number; // default 200
73
+ }
74
+ ```
75
+
76
+ ### `useTranslation(defaultNamespace?)`
77
+
78
+ Returns `{ t, i18n }`.
79
+
80
+ ```ts
81
+ type TranslationFunction = (
82
+ key: string, // "ns:key.path" or "key.path"
83
+ options?: Record<string, unknown> & { defaultValue?: string }
84
+ ) => string;
85
+
86
+ interface I18nInstance {
87
+ ready: boolean;
88
+ locale: string;
89
+ setLocale(next: string): Promise<void>;
90
+ missingEvents: MissingKeyEvent[]; // newest first, capped buffer
91
+ flushMissing(): Promise<void>; // force-flush the pending batch
92
+ }
93
+ ```
94
+
95
+ ### `<Trans>`
96
+
97
+ Inline translation with JSX slots:
98
+
99
+ ```tsx
100
+ <Trans
101
+ i18nKey="cta.terms"
102
+ defaults="I accept the <0>terms</0> and <1>privacy policy</1>"
103
+ components={[<a href="/terms" />, <a href="/privacy" />]}
104
+ />
105
+ ```
106
+
107
+ The `<0>...</0>` slots are 0-indexed into `components`. The bundle string
108
+ should follow the same shape so that translators see `I accept the <0>terms</0>...`
109
+ and the SDK swaps the elements at render time.
110
+
111
+ ---
112
+
113
+ ## Missing-key flow
114
+
115
+ 1. The user navigates a page that calls `t("hello.title")`.
116
+ 2. The bundle for `(locale, namespace)` was already fetched but doesn't
117
+ contain `hello.title`. (`i18n.ready === true` and the bundle for that
118
+ tuple is in the "attempted" set — this is the **gate**.)
119
+ 3. The SDK enqueues a `MissingKeyEvent`, dedups it within the instance, and
120
+ pushes it into the `missingEvents` ring buffer.
121
+ 4. Every `flushIntervalMs` (default 5s) — or sooner if the batch hits
122
+ `flushBatchSize` (default 50) — the SDK flushes the pending batch via
123
+ the transport.
124
+
125
+ ```ts
126
+ interface MissingKeyEvent {
127
+ key: string;
128
+ namespace: string;
129
+ language_code: string;
130
+ source_value?: string;
131
+ sdk_meta?: Record<string, unknown>; // SDK adds {lib, ver, url} automatically
132
+ }
133
+ ```
134
+
135
+ ### Why the gate matters
136
+
137
+ Without the gate, every `t("…")` call between mount and bundle resolution
138
+ would report a "missing" key — which is a lie (the bundle just hadn't
139
+ arrived yet). The first-paint flood would poison your dashboard. The SDK
140
+ holds reports until both:
141
+
142
+ - `i18n.ready === true` (initial bundles loaded), AND
143
+ - the specific `(locale, namespace)` bundle was actually fetched.
144
+
145
+ You can see the gate in action with `i18n.missingEvents` — it stays empty
146
+ until the network round-trip completes.
147
+
148
+ ---
149
+
150
+ ## Custom transport
151
+
152
+ Replace the default POST with anything — Storybook mock, in-app inspector,
153
+ Cypress capture:
154
+
155
+ ```tsx
156
+ <VerbumiaProvider
157
+ {...config}
158
+ transport={(batch) => {
159
+ window.parent.postMessage({ type: "verbumia:missing", batch }, "*");
160
+ }}
161
+ >
162
+ ...
163
+ </VerbumiaProvider>
164
+ ```
165
+
166
+ The default delivery path is also exported if you need to wrap it:
167
+
168
+ ```ts
169
+ import { defaultTransport, logTransport } from "@verbumia/react-i18next";
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Recipes
175
+
176
+ ### Next.js (App Router)
177
+
178
+ Wrap the SDK in a Client Component and feed it env vars from `.env.local`:
179
+
180
+ ```tsx
181
+ // app/(verbumia)/i18n-client.tsx
182
+ "use client";
183
+ import { VerbumiaProvider } from "@verbumia/react-i18next";
184
+
185
+ export function I18nClient({ children }: { children: React.ReactNode }) {
186
+ return (
187
+ <VerbumiaProvider
188
+ token={process.env.NEXT_PUBLIC_VERBUMIA_TOKEN!}
189
+ projectUuid={process.env.NEXT_PUBLIC_VERBUMIA_PROJECT!}
190
+ defaultLocale="fr"
191
+ fallbackLng="en"
192
+ >
193
+ {children}
194
+ </VerbumiaProvider>
195
+ );
196
+ }
197
+ ```
198
+
199
+ The provider reads the bundle via the public CDN — no server-side state to
200
+ hydrate. SSR pre-renders the `defaultValue` and the client smoothly
201
+ upgrades after `i18n.ready` flips.
202
+
203
+ ### Storybook
204
+
205
+ ```ts
206
+ // .storybook/preview.tsx
207
+ import { VerbumiaProvider } from "@verbumia/react-i18next";
208
+
209
+ export const decorators = [
210
+ (Story) => (
211
+ <VerbumiaProvider
212
+ token="vrb_live_storybook.fake"
213
+ projectUuid="storybook"
214
+ defaultLocale="fr"
215
+ missingHandler="log"
216
+ transport={(batch) => action("missing-keys")(batch)}
217
+ >
218
+ <Story />
219
+ </VerbumiaProvider>
220
+ ),
221
+ ];
222
+ ```
223
+
224
+ ### Cypress
225
+
226
+ ```ts
227
+ cy.intercept("POST", "**/v1/missing", (req) => {
228
+ cy.task("captureMissing", req.body);
229
+ req.reply({ accepted: req.body.events.length, rejected: 0, items: [] });
230
+ });
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Versioning
236
+
237
+ Semver. V1.x will keep the public API stable. Internal changes (bundle
238
+ fetcher, dedup heuristics) may shift in patch releases.
239
+
240
+ Breaking changes pre-V1 are flagged in [CONTRACT.md](./CONTRACT.md).
241
+
242
+ ## License
243
+
244
+ MIT — see [LICENSE](./LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Trans: () => Trans,
24
+ VerbumiaProvider: () => VerbumiaProvider,
25
+ defaultTransport: () => defaultTransport,
26
+ logTransport: () => logTransport,
27
+ useTranslation: () => useTranslation
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/provider.tsx
32
+ var import_react = require("react");
33
+
34
+ // src/transport.ts
35
+ var SDK_LIB = "@verbumia/react-i18next";
36
+ var SDK_VER = "0.1.0";
37
+ function defaultTransport(opts) {
38
+ return async (batch) => {
39
+ if (!batch.length) return;
40
+ const body = {
41
+ project_uuid: opts.projectUuid,
42
+ events: batch.map((e) => ({
43
+ key: e.key,
44
+ namespace: e.namespace,
45
+ language_code: e.language_code,
46
+ source_value: e.source_value,
47
+ sdk_meta: {
48
+ lib: SDK_LIB,
49
+ ver: SDK_VER,
50
+ ...typeof window !== "undefined" ? { url: window.location?.href } : {},
51
+ ...e.sdk_meta ?? {}
52
+ }
53
+ }))
54
+ };
55
+ try {
56
+ await fetch(`${opts.apiBase.replace(/\/+$/, "")}/v1/missing`, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ Authorization: `ApiKey ${opts.token}`
61
+ },
62
+ body: JSON.stringify(body),
63
+ // SDKs are best-effort; never block the render path
64
+ keepalive: true
65
+ });
66
+ } catch {
67
+ }
68
+ };
69
+ }
70
+ var logTransport = (batch) => {
71
+ for (const e of batch) {
72
+ console.warn("[verbumia] missing key", e);
73
+ }
74
+ };
75
+
76
+ // src/i18n.ts
77
+ var DEFAULT_API_BASE = "https://api.verbumia.ca";
78
+ var DEFAULT_CDN_BASE = "https://cdn.verbumia.ca";
79
+ var DEFAULT_FLUSH_MS = 5e3;
80
+ var DEFAULT_BATCH = 50;
81
+ var DEFAULT_BUFFER = 200;
82
+ function resolve(bundle, key) {
83
+ if (!bundle) return void 0;
84
+ const parts = key.split(".");
85
+ let cur = bundle;
86
+ for (const p of parts) {
87
+ if (cur && typeof cur === "object" && p in cur) {
88
+ cur = cur[p];
89
+ } else {
90
+ return void 0;
91
+ }
92
+ }
93
+ return typeof cur === "string" ? cur : void 0;
94
+ }
95
+ function interpolate(template, options) {
96
+ if (!options) return template;
97
+ return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_m, name) => {
98
+ const v = options[name];
99
+ return v == null ? "" : String(v);
100
+ });
101
+ }
102
+ var VerbumiaI18n = class {
103
+ ready = false;
104
+ locale;
105
+ fallbackLng;
106
+ missingEvents = [];
107
+ _bundles = /* @__PURE__ */ new Map();
108
+ // `${locale}/${ns}` -> tree
109
+ _attempted = /* @__PURE__ */ new Set();
110
+ // `${locale}/${ns}` keys we've fetched
111
+ _config;
112
+ _transport;
113
+ _pending = [];
114
+ _seen = /* @__PURE__ */ new Set();
115
+ // dedup `${locale}/${ns}/${key}` per-flush
116
+ _timer = null;
117
+ _listeners = /* @__PURE__ */ new Set();
118
+ constructor(config) {
119
+ this.locale = config.defaultLocale;
120
+ this.fallbackLng = config.fallbackLng;
121
+ this._config = {
122
+ apiBase: config.apiBase ?? DEFAULT_API_BASE,
123
+ cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,
124
+ missingHandler: config.missingHandler ?? "send",
125
+ token: config.token,
126
+ projectUuid: config.projectUuid,
127
+ namespaces: config.namespaces?.length ? config.namespaces : ["common"],
128
+ flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,
129
+ flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,
130
+ missingEventsBufferSize: config.missingEventsBufferSize ?? DEFAULT_BUFFER
131
+ };
132
+ this._transport = config.transport ?? (this._config.missingHandler === "log" ? logTransport : defaultTransport({
133
+ apiBase: this._config.apiBase,
134
+ token: this._config.token,
135
+ projectUuid: this._config.projectUuid
136
+ }));
137
+ }
138
+ // ---- React subscription ----
139
+ subscribe = (listener) => {
140
+ this._listeners.add(listener);
141
+ return () => this._listeners.delete(listener);
142
+ };
143
+ _notify() {
144
+ for (const l of this._listeners) l();
145
+ }
146
+ // ---- Lifecycle ----
147
+ /** Loads the configured namespaces for the active locale + fallback. */
148
+ async start(fetchImpl = fetch) {
149
+ const targets = /* @__PURE__ */ new Set([this.locale]);
150
+ if (this.fallbackLng) targets.add(this.fallbackLng);
151
+ await Promise.all(
152
+ [...targets].flatMap(
153
+ (loc) => this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))
154
+ )
155
+ );
156
+ this.ready = true;
157
+ this._startTimer();
158
+ this._notify();
159
+ }
160
+ setLocale = async (next) => {
161
+ if (next === this.locale) return;
162
+ this.locale = next;
163
+ this.ready = false;
164
+ this._notify();
165
+ await Promise.all(
166
+ this._config.namespaces.map((ns) => this._loadBundle(next, ns))
167
+ );
168
+ this.ready = true;
169
+ this._notify();
170
+ };
171
+ stop() {
172
+ if (this._timer) {
173
+ clearInterval(this._timer);
174
+ this._timer = null;
175
+ }
176
+ }
177
+ // ---- Translation ----
178
+ t = (key, options) => {
179
+ const namespace = this._splitNamespace(key);
180
+ const bareKey = namespace.bareKey;
181
+ const ns = namespace.ns;
182
+ const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);
183
+ if (fromActive != null) return interpolate(fromActive, options);
184
+ if (this.fallbackLng && this.fallbackLng !== this.locale) {
185
+ const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);
186
+ if (fb != null) return interpolate(fb, options);
187
+ }
188
+ if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {
189
+ this._reportMissing({
190
+ key: bareKey,
191
+ namespace: ns,
192
+ language_code: this.locale,
193
+ source_value: typeof options?.defaultValue === "string" ? options.defaultValue : void 0
194
+ });
195
+ }
196
+ const defaultValue = options?.defaultValue;
197
+ if (typeof defaultValue === "string") {
198
+ return interpolate(defaultValue, options);
199
+ }
200
+ return key;
201
+ };
202
+ flushMissing = async () => {
203
+ if (!this._pending.length) return;
204
+ const batch = this._pending.slice(0);
205
+ this._pending = [];
206
+ if (this._config.missingHandler === "off") return;
207
+ try {
208
+ await this._transport(batch);
209
+ } catch {
210
+ }
211
+ };
212
+ // ---- Internals ----
213
+ _splitNamespace(key) {
214
+ const idx = key.indexOf(":");
215
+ if (idx > 0) {
216
+ return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };
217
+ }
218
+ return { ns: this._config.namespaces[0], bareKey: key };
219
+ }
220
+ async _loadBundle(locale, ns, fetchImpl = fetch) {
221
+ const url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/latest/${locale}/${ns}.json`;
222
+ try {
223
+ const r = await fetchImpl(url, { method: "GET", credentials: "omit" });
224
+ if (r.ok) {
225
+ const data = await r.json();
226
+ this._bundles.set(`${locale}/${ns}`, data);
227
+ } else {
228
+ this._bundles.set(`${locale}/${ns}`, {});
229
+ }
230
+ } catch {
231
+ this._bundles.set(`${locale}/${ns}`, {});
232
+ } finally {
233
+ this._attempted.add(`${locale}/${ns}`);
234
+ }
235
+ }
236
+ _startTimer() {
237
+ if (this._config.missingHandler === "off") return;
238
+ if (typeof setInterval !== "function") return;
239
+ this._timer = setInterval(() => {
240
+ void this.flushMissing();
241
+ }, this._config.flushIntervalMs);
242
+ }
243
+ _reportMissing(event) {
244
+ if (this._config.missingHandler === "off") return;
245
+ const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;
246
+ if (this._seen.has(dedupKey)) return;
247
+ this._seen.add(dedupKey);
248
+ this.missingEvents = [event, ...this.missingEvents].slice(
249
+ 0,
250
+ this._config.missingEventsBufferSize
251
+ );
252
+ this._pending.push(event);
253
+ if (this._pending.length >= this._config.flushBatchSize) {
254
+ void this.flushMissing();
255
+ }
256
+ this._notify();
257
+ }
258
+ };
259
+
260
+ // src/provider.tsx
261
+ var import_jsx_runtime = require("react/jsx-runtime");
262
+ var VerbumiaContext = (0, import_react.createContext)(null);
263
+ function VerbumiaProvider({
264
+ children,
265
+ ...config
266
+ }) {
267
+ const i18n = (0, import_react.useMemo)(() => new VerbumiaI18n(config), []);
268
+ (0, import_react.useEffect)(() => {
269
+ void i18n.start();
270
+ return () => i18n.stop();
271
+ }, [i18n]);
272
+ const value = (0, import_react.useMemo)(() => ({ i18n }), [i18n]);
273
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VerbumiaContext.Provider, { value, children });
274
+ }
275
+ function useI18n() {
276
+ const ctx = (0, import_react.useContext)(VerbumiaContext);
277
+ if (!ctx) {
278
+ throw new Error("useTranslation/Trans must be used inside <VerbumiaProvider>");
279
+ }
280
+ return ctx.i18n;
281
+ }
282
+ function useI18nSnapshot() {
283
+ const i18n = useI18n();
284
+ return (0, import_react.useSyncExternalStore)(
285
+ i18n.subscribe,
286
+ () => ({
287
+ ready: i18n.ready,
288
+ locale: i18n.locale,
289
+ setLocale: i18n.setLocale,
290
+ missingEvents: i18n.missingEvents,
291
+ flushMissing: i18n.flushMissing
292
+ }),
293
+ () => ({
294
+ ready: false,
295
+ locale: i18n.locale,
296
+ setLocale: i18n.setLocale,
297
+ missingEvents: [],
298
+ flushMissing: i18n.flushMissing
299
+ })
300
+ );
301
+ }
302
+
303
+ // src/hooks.ts
304
+ var import_react2 = require("react");
305
+ function useTranslation(defaultNamespace) {
306
+ const i18n = useI18n();
307
+ const snapshot = useI18nSnapshot();
308
+ const t = (0, import_react2.useMemo)(() => {
309
+ return (key, options) => {
310
+ const fullKey = defaultNamespace && !key.includes(":") ? `${defaultNamespace}:${key}` : key;
311
+ return i18n.t(fullKey, options);
312
+ };
313
+ }, [i18n, defaultNamespace]);
314
+ return { t, i18n: snapshot };
315
+ }
316
+
317
+ // src/trans.tsx
318
+ var import_react3 = require("react");
319
+ var import_jsx_runtime2 = require("react/jsx-runtime");
320
+ function Trans({
321
+ i18nKey,
322
+ defaults,
323
+ values,
324
+ components,
325
+ namespace
326
+ }) {
327
+ const { t } = useTranslation(namespace);
328
+ const raw = t(i18nKey, { ...values ?? {}, defaultValue: defaults ?? i18nKey });
329
+ if (!components || !components.length) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: raw });
330
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: splitOnComponents(raw, components) });
331
+ }
332
+ function splitOnComponents(text, components) {
333
+ const out = [];
334
+ const re = /<(\d+)>(.*?)<\/\1>/g;
335
+ let lastIndex = 0;
336
+ let m;
337
+ while ((m = re.exec(text)) !== null) {
338
+ if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));
339
+ const idx = Number(m[1]);
340
+ const inner = m[2];
341
+ const node = components[idx];
342
+ if ((0, import_react3.isValidElement)(node)) {
343
+ out.push(
344
+ (0, import_react3.cloneElement)(node, { key: `t-${m.index}` }, ...import_react3.Children.toArray(inner ?? ""))
345
+ );
346
+ } else if (node !== void 0) {
347
+ out.push(node);
348
+ } else {
349
+ out.push(inner ?? "");
350
+ }
351
+ lastIndex = re.lastIndex;
352
+ }
353
+ if (lastIndex < text.length) out.push(text.slice(lastIndex));
354
+ return out;
355
+ }
356
+ // Annotate the CommonJS export names for ESM import in node:
357
+ 0 && (module.exports = {
358
+ Trans,
359
+ VerbumiaProvider,
360
+ defaultTransport,
361
+ logTransport,
362
+ useTranslation
363
+ });
364
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,99 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type Locale = string;
5
+ type Namespace = string;
6
+ interface MissingKeyEvent {
7
+ key: string;
8
+ namespace: Namespace;
9
+ language_code: Locale;
10
+ source_value?: string;
11
+ sdk_meta?: Record<string, unknown>;
12
+ }
13
+ type MissingHandlerMode = "send" | "log" | "off";
14
+ type Transport = (batch: MissingKeyEvent[]) => void | Promise<void>;
15
+ interface VerbumiaConfig {
16
+ /** API key — format `vrb_live_<prefix>.<secret>` with `missing:write` scope. */
17
+ token: string;
18
+ /** Project UUID this provider is bound to. */
19
+ projectUuid: string;
20
+ /** Namespaces to preload on mount. Defaults to `['common']`. */
21
+ namespaces?: Namespace[];
22
+ /** Initial locale (BCP-47). */
23
+ defaultLocale: Locale;
24
+ /** Fallback locale used when a key is missing in `defaultLocale`. */
25
+ fallbackLng?: Locale;
26
+ /** Override the API base. Defaults to `https://api.verbumia.ca`. */
27
+ apiBase?: string;
28
+ /** Override the CDN base. Defaults to `https://cdn.verbumia.ca`. */
29
+ cdnBase?: string;
30
+ /**
31
+ * Optional override for missing-key delivery (in-app inspector,
32
+ * Storybook, Cypress mocks). When set, replaces the default POST.
33
+ */
34
+ transport?: Transport;
35
+ /** `send` (default) | `log` | `off` */
36
+ missingHandler?: MissingHandlerMode;
37
+ /** Flush cadence for the missing-key batch. Default 5_000ms. */
38
+ flushIntervalMs?: number;
39
+ /** Max events per batch before forcing a flush. Default 50. */
40
+ flushBatchSize?: number;
41
+ /** Optional ring buffer cap for `i18n.missingEvents`. Default 200. */
42
+ missingEventsBufferSize?: number;
43
+ }
44
+ interface I18nInstance {
45
+ /** True once the initial namespace bundles loaded for the active locale. */
46
+ ready: boolean;
47
+ locale: Locale;
48
+ setLocale: (l: Locale) => Promise<void>;
49
+ /** Recently captured missing-key events (most recent first). */
50
+ missingEvents: MissingKeyEvent[];
51
+ /** Force-flush the missing-key batch now. */
52
+ flushMissing: () => Promise<void>;
53
+ }
54
+ type TranslationOptions = Record<string, unknown> & {
55
+ defaultValue?: string;
56
+ };
57
+ type TranslationFunction = (key: string, options?: TranslationOptions) => string;
58
+
59
+ interface VerbumiaProviderProps extends VerbumiaConfig {
60
+ children: ReactNode;
61
+ }
62
+ declare function VerbumiaProvider({ children, ...config }: VerbumiaProviderProps): react_jsx_runtime.JSX.Element;
63
+
64
+ interface UseTranslationResult {
65
+ t: TranslationFunction;
66
+ i18n: I18nInstance;
67
+ }
68
+ /** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you
69
+ * drop the `ns:` prefix on every call. */
70
+ declare function useTranslation(defaultNamespace?: string): UseTranslationResult;
71
+
72
+ interface TransProps {
73
+ /** The translation key (optionally `ns:key`). */
74
+ i18nKey: string;
75
+ /** Default value if the key is missing — used as the fallback string. */
76
+ defaults?: string;
77
+ /** Variables interpolated into `{{var}}` placeholders. */
78
+ values?: Record<string, unknown>;
79
+ /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */
80
+ components?: ReactNode[];
81
+ /** Optional namespace shortcut. */
82
+ namespace?: string;
83
+ }
84
+ /** Bare-bones Trans component: resolves the key, interpolates values, and
85
+ * swaps `<0>...</0>` placeholders into the supplied React components.
86
+ * Keeps the surface minimal — full Trans semantics (nested keys, plural
87
+ * trees, gender) land in V1.1. */
88
+ declare function Trans({ i18nKey, defaults, values, components, namespace, }: TransProps): react_jsx_runtime.JSX.Element;
89
+
90
+ /** Default transport: POST to `${apiBase}/v1/missing` with the API key. */
91
+ declare function defaultTransport(opts: {
92
+ apiBase: string;
93
+ token: string;
94
+ projectUuid: string;
95
+ }): Transport;
96
+ /** Logs each event to console.warn — handy for dev. */
97
+ declare const logTransport: Transport;
98
+
99
+ export { type I18nInstance, type Locale, type MissingHandlerMode, type MissingKeyEvent, type Namespace, Trans, type TranslationFunction, type TranslationOptions, type Transport, type VerbumiaConfig, VerbumiaProvider, defaultTransport, logTransport, useTranslation };
@@ -0,0 +1,99 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type Locale = string;
5
+ type Namespace = string;
6
+ interface MissingKeyEvent {
7
+ key: string;
8
+ namespace: Namespace;
9
+ language_code: Locale;
10
+ source_value?: string;
11
+ sdk_meta?: Record<string, unknown>;
12
+ }
13
+ type MissingHandlerMode = "send" | "log" | "off";
14
+ type Transport = (batch: MissingKeyEvent[]) => void | Promise<void>;
15
+ interface VerbumiaConfig {
16
+ /** API key — format `vrb_live_<prefix>.<secret>` with `missing:write` scope. */
17
+ token: string;
18
+ /** Project UUID this provider is bound to. */
19
+ projectUuid: string;
20
+ /** Namespaces to preload on mount. Defaults to `['common']`. */
21
+ namespaces?: Namespace[];
22
+ /** Initial locale (BCP-47). */
23
+ defaultLocale: Locale;
24
+ /** Fallback locale used when a key is missing in `defaultLocale`. */
25
+ fallbackLng?: Locale;
26
+ /** Override the API base. Defaults to `https://api.verbumia.ca`. */
27
+ apiBase?: string;
28
+ /** Override the CDN base. Defaults to `https://cdn.verbumia.ca`. */
29
+ cdnBase?: string;
30
+ /**
31
+ * Optional override for missing-key delivery (in-app inspector,
32
+ * Storybook, Cypress mocks). When set, replaces the default POST.
33
+ */
34
+ transport?: Transport;
35
+ /** `send` (default) | `log` | `off` */
36
+ missingHandler?: MissingHandlerMode;
37
+ /** Flush cadence for the missing-key batch. Default 5_000ms. */
38
+ flushIntervalMs?: number;
39
+ /** Max events per batch before forcing a flush. Default 50. */
40
+ flushBatchSize?: number;
41
+ /** Optional ring buffer cap for `i18n.missingEvents`. Default 200. */
42
+ missingEventsBufferSize?: number;
43
+ }
44
+ interface I18nInstance {
45
+ /** True once the initial namespace bundles loaded for the active locale. */
46
+ ready: boolean;
47
+ locale: Locale;
48
+ setLocale: (l: Locale) => Promise<void>;
49
+ /** Recently captured missing-key events (most recent first). */
50
+ missingEvents: MissingKeyEvent[];
51
+ /** Force-flush the missing-key batch now. */
52
+ flushMissing: () => Promise<void>;
53
+ }
54
+ type TranslationOptions = Record<string, unknown> & {
55
+ defaultValue?: string;
56
+ };
57
+ type TranslationFunction = (key: string, options?: TranslationOptions) => string;
58
+
59
+ interface VerbumiaProviderProps extends VerbumiaConfig {
60
+ children: ReactNode;
61
+ }
62
+ declare function VerbumiaProvider({ children, ...config }: VerbumiaProviderProps): react_jsx_runtime.JSX.Element;
63
+
64
+ interface UseTranslationResult {
65
+ t: TranslationFunction;
66
+ i18n: I18nInstance;
67
+ }
68
+ /** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you
69
+ * drop the `ns:` prefix on every call. */
70
+ declare function useTranslation(defaultNamespace?: string): UseTranslationResult;
71
+
72
+ interface TransProps {
73
+ /** The translation key (optionally `ns:key`). */
74
+ i18nKey: string;
75
+ /** Default value if the key is missing — used as the fallback string. */
76
+ defaults?: string;
77
+ /** Variables interpolated into `{{var}}` placeholders. */
78
+ values?: Record<string, unknown>;
79
+ /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */
80
+ components?: ReactNode[];
81
+ /** Optional namespace shortcut. */
82
+ namespace?: string;
83
+ }
84
+ /** Bare-bones Trans component: resolves the key, interpolates values, and
85
+ * swaps `<0>...</0>` placeholders into the supplied React components.
86
+ * Keeps the surface minimal — full Trans semantics (nested keys, plural
87
+ * trees, gender) land in V1.1. */
88
+ declare function Trans({ i18nKey, defaults, values, components, namespace, }: TransProps): react_jsx_runtime.JSX.Element;
89
+
90
+ /** Default transport: POST to `${apiBase}/v1/missing` with the API key. */
91
+ declare function defaultTransport(opts: {
92
+ apiBase: string;
93
+ token: string;
94
+ projectUuid: string;
95
+ }): Transport;
96
+ /** Logs each event to console.warn — handy for dev. */
97
+ declare const logTransport: Transport;
98
+
99
+ export { type I18nInstance, type Locale, type MissingHandlerMode, type MissingKeyEvent, type Namespace, Trans, type TranslationFunction, type TranslationOptions, type Transport, type VerbumiaConfig, VerbumiaProvider, defaultTransport, logTransport, useTranslation };
package/dist/index.js ADDED
@@ -0,0 +1,339 @@
1
+ // src/provider.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useSyncExternalStore
8
+ } from "react";
9
+
10
+ // src/transport.ts
11
+ var SDK_LIB = "@verbumia/react-i18next";
12
+ var SDK_VER = "0.1.0";
13
+ function defaultTransport(opts) {
14
+ return async (batch) => {
15
+ if (!batch.length) return;
16
+ const body = {
17
+ project_uuid: opts.projectUuid,
18
+ events: batch.map((e) => ({
19
+ key: e.key,
20
+ namespace: e.namespace,
21
+ language_code: e.language_code,
22
+ source_value: e.source_value,
23
+ sdk_meta: {
24
+ lib: SDK_LIB,
25
+ ver: SDK_VER,
26
+ ...typeof window !== "undefined" ? { url: window.location?.href } : {},
27
+ ...e.sdk_meta ?? {}
28
+ }
29
+ }))
30
+ };
31
+ try {
32
+ await fetch(`${opts.apiBase.replace(/\/+$/, "")}/v1/missing`, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ Authorization: `ApiKey ${opts.token}`
37
+ },
38
+ body: JSON.stringify(body),
39
+ // SDKs are best-effort; never block the render path
40
+ keepalive: true
41
+ });
42
+ } catch {
43
+ }
44
+ };
45
+ }
46
+ var logTransport = (batch) => {
47
+ for (const e of batch) {
48
+ console.warn("[verbumia] missing key", e);
49
+ }
50
+ };
51
+
52
+ // src/i18n.ts
53
+ var DEFAULT_API_BASE = "https://api.verbumia.ca";
54
+ var DEFAULT_CDN_BASE = "https://cdn.verbumia.ca";
55
+ var DEFAULT_FLUSH_MS = 5e3;
56
+ var DEFAULT_BATCH = 50;
57
+ var DEFAULT_BUFFER = 200;
58
+ function resolve(bundle, key) {
59
+ if (!bundle) return void 0;
60
+ const parts = key.split(".");
61
+ let cur = bundle;
62
+ for (const p of parts) {
63
+ if (cur && typeof cur === "object" && p in cur) {
64
+ cur = cur[p];
65
+ } else {
66
+ return void 0;
67
+ }
68
+ }
69
+ return typeof cur === "string" ? cur : void 0;
70
+ }
71
+ function interpolate(template, options) {
72
+ if (!options) return template;
73
+ return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_m, name) => {
74
+ const v = options[name];
75
+ return v == null ? "" : String(v);
76
+ });
77
+ }
78
+ var VerbumiaI18n = class {
79
+ ready = false;
80
+ locale;
81
+ fallbackLng;
82
+ missingEvents = [];
83
+ _bundles = /* @__PURE__ */ new Map();
84
+ // `${locale}/${ns}` -> tree
85
+ _attempted = /* @__PURE__ */ new Set();
86
+ // `${locale}/${ns}` keys we've fetched
87
+ _config;
88
+ _transport;
89
+ _pending = [];
90
+ _seen = /* @__PURE__ */ new Set();
91
+ // dedup `${locale}/${ns}/${key}` per-flush
92
+ _timer = null;
93
+ _listeners = /* @__PURE__ */ new Set();
94
+ constructor(config) {
95
+ this.locale = config.defaultLocale;
96
+ this.fallbackLng = config.fallbackLng;
97
+ this._config = {
98
+ apiBase: config.apiBase ?? DEFAULT_API_BASE,
99
+ cdnBase: config.cdnBase ?? DEFAULT_CDN_BASE,
100
+ missingHandler: config.missingHandler ?? "send",
101
+ token: config.token,
102
+ projectUuid: config.projectUuid,
103
+ namespaces: config.namespaces?.length ? config.namespaces : ["common"],
104
+ flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,
105
+ flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,
106
+ missingEventsBufferSize: config.missingEventsBufferSize ?? DEFAULT_BUFFER
107
+ };
108
+ this._transport = config.transport ?? (this._config.missingHandler === "log" ? logTransport : defaultTransport({
109
+ apiBase: this._config.apiBase,
110
+ token: this._config.token,
111
+ projectUuid: this._config.projectUuid
112
+ }));
113
+ }
114
+ // ---- React subscription ----
115
+ subscribe = (listener) => {
116
+ this._listeners.add(listener);
117
+ return () => this._listeners.delete(listener);
118
+ };
119
+ _notify() {
120
+ for (const l of this._listeners) l();
121
+ }
122
+ // ---- Lifecycle ----
123
+ /** Loads the configured namespaces for the active locale + fallback. */
124
+ async start(fetchImpl = fetch) {
125
+ const targets = /* @__PURE__ */ new Set([this.locale]);
126
+ if (this.fallbackLng) targets.add(this.fallbackLng);
127
+ await Promise.all(
128
+ [...targets].flatMap(
129
+ (loc) => this._config.namespaces.map((ns) => this._loadBundle(loc, ns, fetchImpl))
130
+ )
131
+ );
132
+ this.ready = true;
133
+ this._startTimer();
134
+ this._notify();
135
+ }
136
+ setLocale = async (next) => {
137
+ if (next === this.locale) return;
138
+ this.locale = next;
139
+ this.ready = false;
140
+ this._notify();
141
+ await Promise.all(
142
+ this._config.namespaces.map((ns) => this._loadBundle(next, ns))
143
+ );
144
+ this.ready = true;
145
+ this._notify();
146
+ };
147
+ stop() {
148
+ if (this._timer) {
149
+ clearInterval(this._timer);
150
+ this._timer = null;
151
+ }
152
+ }
153
+ // ---- Translation ----
154
+ t = (key, options) => {
155
+ const namespace = this._splitNamespace(key);
156
+ const bareKey = namespace.bareKey;
157
+ const ns = namespace.ns;
158
+ const fromActive = resolve(this._bundles.get(`${this.locale}/${ns}`), bareKey);
159
+ if (fromActive != null) return interpolate(fromActive, options);
160
+ if (this.fallbackLng && this.fallbackLng !== this.locale) {
161
+ const fb = resolve(this._bundles.get(`${this.fallbackLng}/${ns}`), bareKey);
162
+ if (fb != null) return interpolate(fb, options);
163
+ }
164
+ if (this.ready && this._attempted.has(`${this.locale}/${ns}`)) {
165
+ this._reportMissing({
166
+ key: bareKey,
167
+ namespace: ns,
168
+ language_code: this.locale,
169
+ source_value: typeof options?.defaultValue === "string" ? options.defaultValue : void 0
170
+ });
171
+ }
172
+ const defaultValue = options?.defaultValue;
173
+ if (typeof defaultValue === "string") {
174
+ return interpolate(defaultValue, options);
175
+ }
176
+ return key;
177
+ };
178
+ flushMissing = async () => {
179
+ if (!this._pending.length) return;
180
+ const batch = this._pending.slice(0);
181
+ this._pending = [];
182
+ if (this._config.missingHandler === "off") return;
183
+ try {
184
+ await this._transport(batch);
185
+ } catch {
186
+ }
187
+ };
188
+ // ---- Internals ----
189
+ _splitNamespace(key) {
190
+ const idx = key.indexOf(":");
191
+ if (idx > 0) {
192
+ return { ns: key.slice(0, idx), bareKey: key.slice(idx + 1) };
193
+ }
194
+ return { ns: this._config.namespaces[0], bareKey: key };
195
+ }
196
+ async _loadBundle(locale, ns, fetchImpl = fetch) {
197
+ const url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/latest/${locale}/${ns}.json`;
198
+ try {
199
+ const r = await fetchImpl(url, { method: "GET", credentials: "omit" });
200
+ if (r.ok) {
201
+ const data = await r.json();
202
+ this._bundles.set(`${locale}/${ns}`, data);
203
+ } else {
204
+ this._bundles.set(`${locale}/${ns}`, {});
205
+ }
206
+ } catch {
207
+ this._bundles.set(`${locale}/${ns}`, {});
208
+ } finally {
209
+ this._attempted.add(`${locale}/${ns}`);
210
+ }
211
+ }
212
+ _startTimer() {
213
+ if (this._config.missingHandler === "off") return;
214
+ if (typeof setInterval !== "function") return;
215
+ this._timer = setInterval(() => {
216
+ void this.flushMissing();
217
+ }, this._config.flushIntervalMs);
218
+ }
219
+ _reportMissing(event) {
220
+ if (this._config.missingHandler === "off") return;
221
+ const dedupKey = `${event.language_code}/${event.namespace}/${event.key}`;
222
+ if (this._seen.has(dedupKey)) return;
223
+ this._seen.add(dedupKey);
224
+ this.missingEvents = [event, ...this.missingEvents].slice(
225
+ 0,
226
+ this._config.missingEventsBufferSize
227
+ );
228
+ this._pending.push(event);
229
+ if (this._pending.length >= this._config.flushBatchSize) {
230
+ void this.flushMissing();
231
+ }
232
+ this._notify();
233
+ }
234
+ };
235
+
236
+ // src/provider.tsx
237
+ import { jsx } from "react/jsx-runtime";
238
+ var VerbumiaContext = createContext(null);
239
+ function VerbumiaProvider({
240
+ children,
241
+ ...config
242
+ }) {
243
+ const i18n = useMemo(() => new VerbumiaI18n(config), []);
244
+ useEffect(() => {
245
+ void i18n.start();
246
+ return () => i18n.stop();
247
+ }, [i18n]);
248
+ const value = useMemo(() => ({ i18n }), [i18n]);
249
+ return /* @__PURE__ */ jsx(VerbumiaContext.Provider, { value, children });
250
+ }
251
+ function useI18n() {
252
+ const ctx = useContext(VerbumiaContext);
253
+ if (!ctx) {
254
+ throw new Error("useTranslation/Trans must be used inside <VerbumiaProvider>");
255
+ }
256
+ return ctx.i18n;
257
+ }
258
+ function useI18nSnapshot() {
259
+ const i18n = useI18n();
260
+ return useSyncExternalStore(
261
+ i18n.subscribe,
262
+ () => ({
263
+ ready: i18n.ready,
264
+ locale: i18n.locale,
265
+ setLocale: i18n.setLocale,
266
+ missingEvents: i18n.missingEvents,
267
+ flushMissing: i18n.flushMissing
268
+ }),
269
+ () => ({
270
+ ready: false,
271
+ locale: i18n.locale,
272
+ setLocale: i18n.setLocale,
273
+ missingEvents: [],
274
+ flushMissing: i18n.flushMissing
275
+ })
276
+ );
277
+ }
278
+
279
+ // src/hooks.ts
280
+ import { useMemo as useMemo2 } from "react";
281
+ function useTranslation(defaultNamespace) {
282
+ const i18n = useI18n();
283
+ const snapshot = useI18nSnapshot();
284
+ const t = useMemo2(() => {
285
+ return (key, options) => {
286
+ const fullKey = defaultNamespace && !key.includes(":") ? `${defaultNamespace}:${key}` : key;
287
+ return i18n.t(fullKey, options);
288
+ };
289
+ }, [i18n, defaultNamespace]);
290
+ return { t, i18n: snapshot };
291
+ }
292
+
293
+ // src/trans.tsx
294
+ import { Children, cloneElement, isValidElement } from "react";
295
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
296
+ function Trans({
297
+ i18nKey,
298
+ defaults,
299
+ values,
300
+ components,
301
+ namespace
302
+ }) {
303
+ const { t } = useTranslation(namespace);
304
+ const raw = t(i18nKey, { ...values ?? {}, defaultValue: defaults ?? i18nKey });
305
+ if (!components || !components.length) return /* @__PURE__ */ jsx2(Fragment, { children: raw });
306
+ return /* @__PURE__ */ jsx2(Fragment, { children: splitOnComponents(raw, components) });
307
+ }
308
+ function splitOnComponents(text, components) {
309
+ const out = [];
310
+ const re = /<(\d+)>(.*?)<\/\1>/g;
311
+ let lastIndex = 0;
312
+ let m;
313
+ while ((m = re.exec(text)) !== null) {
314
+ if (m.index > lastIndex) out.push(text.slice(lastIndex, m.index));
315
+ const idx = Number(m[1]);
316
+ const inner = m[2];
317
+ const node = components[idx];
318
+ if (isValidElement(node)) {
319
+ out.push(
320
+ cloneElement(node, { key: `t-${m.index}` }, ...Children.toArray(inner ?? ""))
321
+ );
322
+ } else if (node !== void 0) {
323
+ out.push(node);
324
+ } else {
325
+ out.push(inner ?? "");
326
+ }
327
+ lastIndex = re.lastIndex;
328
+ }
329
+ if (lastIndex < text.length) out.push(text.slice(lastIndex));
330
+ return out;
331
+ }
332
+ export {
333
+ Trans,
334
+ VerbumiaProvider,
335
+ defaultTransport,
336
+ logTransport,
337
+ useTranslation
338
+ };
339
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@verbumia/react-i18next",
3
+ "version": "0.1.0",
4
+ "description": "React SDK for Verbumia — translations + realtime missing-key handler.",
5
+ "license": "MIT",
6
+ "homepage": "https://verbumia.ca",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/verbumia/verbumia-react-i18next.git"
10
+ },
11
+ "keywords": ["i18n", "translations", "react", "i18next", "verbumia"],
12
+ "author": "Verbumia",
13
+ "type": "module",
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "files": ["dist", "README.md", "LICENSE"],
25
+ "sideEffects": false,
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "peerDependencies": {
30
+ "react": ">=18"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^18.3.0",
34
+ "@types/react-dom": "^18.3.0",
35
+ "happy-dom": "^15.0.0",
36
+ "react": "^18.3.0",
37
+ "react-dom": "^18.3.0",
38
+ "tsup": "^8.3.0",
39
+ "typescript": "^5.5.0",
40
+ "vitest": "^2.1.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "test": "vitest run",
45
+ "typecheck": "tsc --noEmit",
46
+ "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build",
47
+ "pack:dry-run": "pnpm pack --dry-run"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "registry": "https://registry.npmjs.org/",
52
+ "provenance": true
53
+ }
54
+ }