@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 +21 -0
- package/README.md +244 -0
- package/dist/index.cjs +364 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +99 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
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
|
+
[](./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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|