@verbumia/react-i18next 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -5
- package/dist/index.cjs +89 -199
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -35
- package/dist/index.d.ts +66 -35
- package/dist/index.js +88 -199
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,8 +63,13 @@ interface VerbumiaConfig {
|
|
|
63
63
|
defaultLocale: string; // BCP-47 (e.g. "fr", "fr-CA")
|
|
64
64
|
fallbackLng?: string; // resolved before reporting a key as missing
|
|
65
65
|
namespaces?: string[]; // default ['common']
|
|
66
|
-
|
|
66
|
+
defaultNS?: string; // alias: default namespace for single-ns apps
|
|
67
|
+
apiBase?: string; // default 'https://api.verbumia.dev'
|
|
67
68
|
cdnBase?: string; // default 'https://cdn.verbumia.ca'
|
|
69
|
+
version?: string; // version slug, default 'main' (in cache keys)
|
|
70
|
+
versionSlug?: string; // @deprecated alias of `version`
|
|
71
|
+
env?: 'prod' | 'dev'; // default 'prod' (drives fetch source)
|
|
72
|
+
plugins?: VerbumiaPlugin[]; // e.g. @verbumia/feedback, @verbumia/realtime
|
|
68
73
|
transport?: (batch: MissingKeyEvent[]) => void | Promise<void>;
|
|
69
74
|
missingHandler?: 'send' | 'log' | 'off'; // default 'send'
|
|
70
75
|
flushIntervalMs?: number; // default 5000
|
|
@@ -73,25 +78,58 @@ interface VerbumiaConfig {
|
|
|
73
78
|
}
|
|
74
79
|
```
|
|
75
80
|
|
|
81
|
+
`version` selects which published version's bundles to load
|
|
82
|
+
(`/p/<project>/<version>/latest/...`); it defaults to `'main'` and is part
|
|
83
|
+
of the SDK's bundle cache keys, so two providers with different `version`
|
|
84
|
+
values never share cached bundles. `versionSlug` is a **deprecated** alias
|
|
85
|
+
of `version` (if both are set, `version` wins).
|
|
86
|
+
|
|
87
|
+
> **Removed in 0.9.0:** the `liveUpdates` / `centrifugoWsUrl` /
|
|
88
|
+
> `centrifugoTokenEndpoint` config keys. Realtime updates now live in the
|
|
89
|
+
> separate [`@verbumia/realtime`](../realtime) plugin (see
|
|
90
|
+
> [Realtime updates](#realtime-updates)). Passing any of those keys throws
|
|
91
|
+
> a clear migration error.
|
|
92
|
+
```
|
|
93
|
+
|
|
76
94
|
### `useTranslation(defaultNamespace?)`
|
|
77
95
|
|
|
78
96
|
Returns `{ t, i18n }`.
|
|
79
97
|
|
|
80
98
|
```ts
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
// Two call shapes — the native object form AND the react-i18next-style
|
|
100
|
+
// positional fallback (a string 2nd arg is the default value):
|
|
101
|
+
type TranslationFunction = {
|
|
102
|
+
(key: string, defaultValue: string, options?: Record<string, unknown>): string;
|
|
103
|
+
(key: string, options?: Record<string, unknown> & { defaultValue?: string }): string;
|
|
104
|
+
};
|
|
85
105
|
|
|
86
106
|
interface I18nInstance {
|
|
87
107
|
ready: boolean;
|
|
88
108
|
locale: string;
|
|
109
|
+
language: string; // alias of `locale`
|
|
89
110
|
setLocale(next: string): Promise<void>;
|
|
111
|
+
changeLanguage(next: string): Promise<void>; // alias of `setLocale`
|
|
112
|
+
t: TranslationFunction; // for out-of-React use via getI18n()
|
|
90
113
|
missingEvents: MissingKeyEvent[]; // newest first, capped buffer
|
|
91
114
|
flushMissing(): Promise<void>; // force-flush the pending batch
|
|
115
|
+
reload(opts?: { locale?: string; namespace?: string }): Promise<void>;
|
|
92
116
|
}
|
|
93
117
|
```
|
|
94
118
|
|
|
119
|
+
### `i18n.reload(opts?)`
|
|
120
|
+
|
|
121
|
+
Bust-refetches already-loaded bundles (bypassing the browser HTTP cache)
|
|
122
|
+
and re-renders. Without `opts` it refreshes every loaded `(locale, ns)`
|
|
123
|
+
bundle; pass `{ locale }` and/or `{ namespace }` to narrow. Returns once
|
|
124
|
+
all refetches settle. Useful for a manual "refresh translations" button,
|
|
125
|
+
and it's what [`@verbumia/realtime`](#realtime-updates) calls on a
|
|
126
|
+
`translations_published` push.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
await getI18n().reload(); // refresh all
|
|
130
|
+
await getI18n().reload({ locale: "fr", namespace: "common" });
|
|
131
|
+
```
|
|
132
|
+
|
|
95
133
|
### `<Trans>`
|
|
96
134
|
|
|
97
135
|
Inline translation with JSX slots:
|
|
@@ -110,6 +148,41 @@ and the SDK swaps the elements at render time.
|
|
|
110
148
|
|
|
111
149
|
---
|
|
112
150
|
|
|
151
|
+
## Migrating from react-i18next
|
|
152
|
+
|
|
153
|
+
`@verbumia/react-i18next` is built to be a near drop-in for react-i18next, so
|
|
154
|
+
existing codebases migrate with minimal changes:
|
|
155
|
+
|
|
156
|
+
- **Positional default value** — `t('key', 'Default text')` works (so does
|
|
157
|
+
`t('key', 'Hi {{name}}', { name })`), alongside the native
|
|
158
|
+
`t('key', { defaultValue })`. No codemod needed for inline fallbacks.
|
|
159
|
+
- **`changeLanguage` / `language`** — `i18n.changeLanguage('en')` (alias of
|
|
160
|
+
`setLocale`) and the `i18n.language` getter (alias of `locale`) are available.
|
|
161
|
+
- **Out-of-React access** — `getI18n()` returns the active instance for use in
|
|
162
|
+
plain modules, stores, or helpers (the react-i18next standalone-singleton
|
|
163
|
+
pattern):
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { getI18n } from "@verbumia/react-i18next";
|
|
167
|
+
// anywhere after <VerbumiaProvider> has mounted:
|
|
168
|
+
const label = getI18n().t("nav.home", "Home");
|
|
169
|
+
await getI18n().changeLanguage("en");
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`getI18n()` throws a clear error if no provider is mounted yet, and assumes a
|
|
173
|
+
single app-wide provider.
|
|
174
|
+
- **Default namespace** — the default is `['common']` (not react-i18next's
|
|
175
|
+
`'translation'`). Migrants pass `namespaces={['translation']}`, or the
|
|
176
|
+
`defaultNS="translation"` alias for single-namespace apps.
|
|
177
|
+
|
|
178
|
+
### Not yet supported (planned for V1.1)
|
|
179
|
+
|
|
180
|
+
Plurals and context are **not** resolved yet: `t('key', { count })` performs
|
|
181
|
+
interpolation only — it does **not** select plural keys (`key_one` /
|
|
182
|
+
`key_other`) or context keys (`key_male`). Handle these manually until V1.1.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
113
186
|
## Missing-key flow
|
|
114
187
|
|
|
115
188
|
1. The user navigates a page that calls `t("hello.title")`.
|
|
@@ -171,6 +244,38 @@ import { defaultTransport, logTransport } from "@verbumia/react-i18next";
|
|
|
171
244
|
|
|
172
245
|
---
|
|
173
246
|
|
|
247
|
+
## Realtime updates
|
|
248
|
+
|
|
249
|
+
Zero-deploy translation updates (subscribe to the project's Centrifugo
|
|
250
|
+
`translations:` channel and bust-refetch on publish) live in the separate
|
|
251
|
+
[`@verbumia/realtime`](../realtime) package — added as a **plugin** of this
|
|
252
|
+
provider, not configured here:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
import { VerbumiaProvider } from "@verbumia/react-i18next";
|
|
256
|
+
import { verbumiaRealtime } from "@verbumia/realtime/react";
|
|
257
|
+
|
|
258
|
+
<VerbumiaProvider
|
|
259
|
+
{...config}
|
|
260
|
+
env="dev"
|
|
261
|
+
plugins={[
|
|
262
|
+
verbumiaRealtime({ wsUrl: "wss://centrifugo.verbumia.ca/connection/websocket" }),
|
|
263
|
+
]}
|
|
264
|
+
>
|
|
265
|
+
<App />
|
|
266
|
+
</VerbumiaProvider>;
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Under the hood the plugin calls [`i18n.reload(...)`](#i18nreloadopts) on
|
|
270
|
+
each `translations_published` push. Realtime is a dev-version-only feature
|
|
271
|
+
(it only subscribes when `env: "dev"`).
|
|
272
|
+
|
|
273
|
+
> The `liveUpdates` / `centrifugoWsUrl` / `centrifugoTokenEndpoint` config
|
|
274
|
+
> keys were **removed in 0.9.0**. Install `@verbumia/realtime` and use the
|
|
275
|
+
> plugin instead.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
174
279
|
## Recipes
|
|
175
280
|
|
|
176
281
|
### Next.js (App Router)
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
Trans: () => Trans,
|
|
24
24
|
VerbumiaProvider: () => VerbumiaProvider,
|
|
25
25
|
defaultTransport: () => defaultTransport,
|
|
26
|
+
getI18n: () => getI18n,
|
|
26
27
|
keyRegistry: () => keyRegistry,
|
|
27
28
|
logTransport: () => logTransport,
|
|
28
29
|
useTranslation: () => useTranslation
|
|
@@ -74,111 +75,6 @@ var logTransport = (batch) => {
|
|
|
74
75
|
}
|
|
75
76
|
};
|
|
76
77
|
|
|
77
|
-
// src/live.ts
|
|
78
|
-
var LiveClient = class {
|
|
79
|
-
constructor(cfg) {
|
|
80
|
-
this.cfg = cfg;
|
|
81
|
-
}
|
|
82
|
-
cfg;
|
|
83
|
-
_ws = null;
|
|
84
|
-
_id = 0;
|
|
85
|
-
_backoffMs = 1e3;
|
|
86
|
-
_disposed = false;
|
|
87
|
-
_connectAcked = false;
|
|
88
|
-
/** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */
|
|
89
|
-
async connect() {
|
|
90
|
-
if (this._ws) return;
|
|
91
|
-
if (this._disposed) return;
|
|
92
|
-
this.cfg.onStatus?.("connecting");
|
|
93
|
-
const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;
|
|
94
|
-
let url = this.cfg.url;
|
|
95
|
-
if (!url.includes("/connection/websocket")) {
|
|
96
|
-
url = url.replace(/\/+$/, "") + "/connection/websocket";
|
|
97
|
-
}
|
|
98
|
-
const ws = new WebSocket(url);
|
|
99
|
-
this._ws = ws;
|
|
100
|
-
this._connectAcked = false;
|
|
101
|
-
ws.onopen = () => {
|
|
102
|
-
this._send({ id: ++this._id, connect: { token } });
|
|
103
|
-
};
|
|
104
|
-
ws.onmessage = (evt) => this._onFrame(evt.data);
|
|
105
|
-
ws.onclose = () => this._onClose();
|
|
106
|
-
ws.onerror = () => {
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
dispose() {
|
|
110
|
-
this._disposed = true;
|
|
111
|
-
if (this._ws) {
|
|
112
|
-
try {
|
|
113
|
-
this._ws.close();
|
|
114
|
-
} catch {
|
|
115
|
-
}
|
|
116
|
-
this._ws = null;
|
|
117
|
-
}
|
|
118
|
-
this.cfg.onStatus?.("disconnected");
|
|
119
|
-
}
|
|
120
|
-
// ---- internals ----
|
|
121
|
-
_send(msg) {
|
|
122
|
-
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
123
|
-
try {
|
|
124
|
-
this._ws.send(JSON.stringify(msg));
|
|
125
|
-
} catch {
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
_onFrame(raw) {
|
|
129
|
-
let parsed;
|
|
130
|
-
try {
|
|
131
|
-
parsed = JSON.parse(raw);
|
|
132
|
-
} catch {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (!parsed || typeof parsed !== "object") return;
|
|
136
|
-
if (Object.keys(parsed).length === 0) {
|
|
137
|
-
this._send({});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (parsed.connect && !this._connectAcked) {
|
|
141
|
-
this._connectAcked = true;
|
|
142
|
-
this.cfg.onStatus?.("connected");
|
|
143
|
-
this._backoffMs = 1e3;
|
|
144
|
-
this._send({
|
|
145
|
-
id: ++this._id,
|
|
146
|
-
subscribe: { channel: this.cfg.channel }
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const push = parsed.push;
|
|
151
|
-
if (push && push.channel === this.cfg.channel && push.pub) {
|
|
152
|
-
this.cfg.onMessage(push.pub.data);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
_onClose() {
|
|
156
|
-
this._ws = null;
|
|
157
|
-
this._connectAcked = false;
|
|
158
|
-
this.cfg.onStatus?.("disconnected");
|
|
159
|
-
if (this._disposed) return;
|
|
160
|
-
const delay = this._backoffMs;
|
|
161
|
-
this._backoffMs = Math.min(this._backoffMs * 2, 3e4);
|
|
162
|
-
setTimeout(() => {
|
|
163
|
-
if (!this._disposed) void this.connect();
|
|
164
|
-
}, delay);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
async function fetchCentrifugoToken(endpoint, projectUuid, authToken, fetchImpl = fetch) {
|
|
168
|
-
const r = await fetchImpl(endpoint, {
|
|
169
|
-
method: "POST",
|
|
170
|
-
headers: {
|
|
171
|
-
"Content-Type": "application/json",
|
|
172
|
-
Authorization: `ApiKey ${authToken}`
|
|
173
|
-
},
|
|
174
|
-
body: JSON.stringify({ project_uuid: projectUuid })
|
|
175
|
-
});
|
|
176
|
-
if (!r.ok) {
|
|
177
|
-
throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);
|
|
178
|
-
}
|
|
179
|
-
return await r.json();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
78
|
// src/key-registry.ts
|
|
183
79
|
var GLOBAL = "__verbumia_key_registry__";
|
|
184
80
|
var SEP = "\0";
|
|
@@ -316,10 +212,10 @@ var VerbumiaI18n = class {
|
|
|
316
212
|
fallbackLng;
|
|
317
213
|
missingEvents = [];
|
|
318
214
|
_bundles = /* @__PURE__ */ new Map();
|
|
319
|
-
// `${locale}/${ns}` -> tree
|
|
215
|
+
// `${version}/${locale}/${ns}` -> tree
|
|
320
216
|
_attempted = /* @__PURE__ */ new Set();
|
|
321
|
-
// `${locale}/${ns}` keys we've fetched
|
|
322
|
-
// Tighter gate than `_attempted`: this set only contains (locale, ns)
|
|
217
|
+
// `${version}/${locale}/${ns}` keys we've fetched
|
|
218
|
+
// Tighter gate than `_attempted`: this set only contains (version, locale, ns)
|
|
323
219
|
// pairs whose CDN response was 200 with at least one top-level key. An
|
|
324
220
|
// empty bundle (404 → {} OR 200 → {}) is treated as "no data yet";
|
|
325
221
|
// calling t() against a key in such a bundle does NOT fire reportMissing.
|
|
@@ -334,13 +230,20 @@ var VerbumiaI18n = class {
|
|
|
334
230
|
// dedup `${locale}/${ns}/${key}` per-flush
|
|
335
231
|
_timer = null;
|
|
336
232
|
_listeners = /* @__PURE__ */ new Set();
|
|
337
|
-
_live = null;
|
|
338
233
|
// Stable snapshot reference for useSyncExternalStore. Returning a fresh
|
|
339
234
|
// object on each getSnapshot call would loop React forever — we rebuild
|
|
340
235
|
// it ONLY in _notify (when state actually changed) and return the cached
|
|
341
236
|
// reference between notifications.
|
|
342
237
|
_snapshot;
|
|
343
238
|
constructor(config) {
|
|
239
|
+
const removedRealtimeKeys = Object.keys(config).filter(
|
|
240
|
+
(k) => k === "liveUpdates" || k.startsWith("centrifugo")
|
|
241
|
+
);
|
|
242
|
+
if (removedRealtimeKeys.length > 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`@verbumia/react-i18next: ${removedRealtimeKeys.join(", ")} ${removedRealtimeKeys.length > 1 ? "were" : "was"} removed in 0.9.0 \u2014 realtime is now the @verbumia/realtime plugin. Remove them and pass \`plugins: [verbumiaRealtime({ wsUrl })]\` to <VerbumiaProvider> instead.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
344
247
|
this.locale = config.defaultLocale;
|
|
345
248
|
this.fallbackLng = config.fallbackLng;
|
|
346
249
|
this._config = {
|
|
@@ -349,14 +252,11 @@ var VerbumiaI18n = class {
|
|
|
349
252
|
missingHandler: config.missingHandler ?? "send",
|
|
350
253
|
token: config.token,
|
|
351
254
|
projectUuid: config.projectUuid,
|
|
352
|
-
namespaces: config.namespaces?.length ? config.namespaces : ["common"],
|
|
255
|
+
namespaces: config.namespaces?.length ? config.namespaces : config.defaultNS ? [config.defaultNS] : ["common"],
|
|
353
256
|
flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,
|
|
354
257
|
flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,
|
|
355
258
|
missingEventsBufferSize: config.missingEventsBufferSize ?? DEFAULT_BUFFER,
|
|
356
|
-
|
|
357
|
-
liveUpdates: !!config.liveUpdates,
|
|
358
|
-
centrifugoTokenEndpoint: config.centrifugoTokenEndpoint ?? `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, "")}/v1/auth/centrifugo-token`,
|
|
359
|
-
centrifugoWsUrl: config.centrifugoWsUrl ?? "",
|
|
259
|
+
version: config.version ?? config.versionSlug ?? DEFAULT_VERSION_SLUG,
|
|
360
260
|
env: config.env ?? "prod"
|
|
361
261
|
};
|
|
362
262
|
this._transport = config.transport ?? (this._config.missingHandler === "log" ? logTransport : defaultTransport({
|
|
@@ -378,11 +278,21 @@ var VerbumiaI18n = class {
|
|
|
378
278
|
return {
|
|
379
279
|
ready: this.ready,
|
|
380
280
|
locale: this.locale,
|
|
281
|
+
language: this.locale,
|
|
381
282
|
setLocale: this.setLocale,
|
|
283
|
+
changeLanguage: this.changeLanguage,
|
|
284
|
+
t: this.t,
|
|
382
285
|
missingEvents: this.missingEvents,
|
|
383
|
-
flushMissing: this.flushMissing
|
|
286
|
+
flushMissing: this.flushMissing,
|
|
287
|
+
reload: this.reload
|
|
384
288
|
};
|
|
385
289
|
}
|
|
290
|
+
/** Bundle cache-key builder. Includes `version` so providers with
|
|
291
|
+
* different `version` values never share cached bundles. The segments
|
|
292
|
+
* (version/locale/ns) are slugs and never contain '/'. */
|
|
293
|
+
_bundleKey(locale, ns) {
|
|
294
|
+
return `${this._config.version}/${locale}/${ns}`;
|
|
295
|
+
}
|
|
386
296
|
_notify() {
|
|
387
297
|
this._snapshot = this._buildSnapshot();
|
|
388
298
|
for (const l of this._listeners) l();
|
|
@@ -405,9 +315,6 @@ var VerbumiaI18n = class {
|
|
|
405
315
|
);
|
|
406
316
|
this.ready = true;
|
|
407
317
|
this._startTimer();
|
|
408
|
-
if (this._config.liveUpdates && this._config.env === "dev") {
|
|
409
|
-
this._startLive(fetchImpl);
|
|
410
|
-
}
|
|
411
318
|
this._notify();
|
|
412
319
|
}
|
|
413
320
|
setLocale = async (next) => {
|
|
@@ -421,102 +328,64 @@ var VerbumiaI18n = class {
|
|
|
421
328
|
this.ready = true;
|
|
422
329
|
this._notify();
|
|
423
330
|
};
|
|
331
|
+
/** Alias of {@link setLocale} for react-i18next compatibility. */
|
|
332
|
+
changeLanguage = (next) => this.setLocale(next);
|
|
333
|
+
/** Alias of {@link locale} for react-i18next compatibility. */
|
|
334
|
+
get language() {
|
|
335
|
+
return this.locale;
|
|
336
|
+
}
|
|
424
337
|
stop() {
|
|
425
338
|
keyRegistry.detach();
|
|
426
339
|
if (this._timer) {
|
|
427
340
|
clearInterval(this._timer);
|
|
428
341
|
this._timer = null;
|
|
429
342
|
}
|
|
430
|
-
if (this._live) {
|
|
431
|
-
this._live.dispose();
|
|
432
|
-
this._live = null;
|
|
433
|
-
}
|
|
434
343
|
}
|
|
435
344
|
/**
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
345
|
+
* Bust-refetch already-loaded bundles and re-render once. Generic
|
|
346
|
+
* replacement for the old realtime-only refetch: iterate the
|
|
347
|
+
* `_attempted` cache keys (`${version}/${locale}/${ns}`), optionally
|
|
348
|
+
* filtered by `opts.locale` / `opts.namespace`, and re-pull each one
|
|
349
|
+
* with `{ bust: true }` so the mutable CDN `latest/` alias bypasses the
|
|
350
|
+
* HTTP cache. After all settle, `_notify()` once so React re-renders.
|
|
351
|
+
*
|
|
352
|
+
* Used by `@verbumia/realtime` on a `translations_published` push and as
|
|
353
|
+
* a manual refresh hook. If nothing matches, returns without notifying.
|
|
440
354
|
*/
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
355
|
+
reload = async (opts = {}) => {
|
|
356
|
+
const targets = [];
|
|
357
|
+
for (const key of this._attempted) {
|
|
358
|
+
const parts = key.split("/");
|
|
359
|
+
const locale = parts[1];
|
|
360
|
+
const ns = parts[2];
|
|
361
|
+
if (!locale || !ns) continue;
|
|
362
|
+
if (opts.locale && opts.locale !== locale) continue;
|
|
363
|
+
if (opts.namespace && opts.namespace !== ns) continue;
|
|
364
|
+
targets.push({ locale, ns });
|
|
450
365
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
projectUuid,
|
|
458
|
-
apiToken,
|
|
459
|
-
fetchImpl
|
|
460
|
-
);
|
|
461
|
-
return token;
|
|
462
|
-
};
|
|
463
|
-
void (async () => {
|
|
464
|
-
let channel;
|
|
465
|
-
let token;
|
|
466
|
-
try {
|
|
467
|
-
const minted = await fetchCentrifugoToken(
|
|
468
|
-
tokenEndpoint,
|
|
469
|
-
projectUuid,
|
|
470
|
-
apiToken,
|
|
471
|
-
fetchImpl
|
|
472
|
-
);
|
|
473
|
-
channel = minted.channel;
|
|
474
|
-
token = minted.token;
|
|
475
|
-
} catch (err) {
|
|
476
|
-
if (typeof console !== "undefined") {
|
|
477
|
-
console.warn("@verbumia/react-i18next: live token mint failed", err);
|
|
478
|
-
}
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
this._live = new LiveClient({
|
|
482
|
-
url: wsUrl,
|
|
483
|
-
token,
|
|
484
|
-
channel,
|
|
485
|
-
refreshToken,
|
|
486
|
-
onMessage: (data) => this._onLiveMessage(data, fetchImpl)
|
|
487
|
-
});
|
|
488
|
-
void this._live.connect();
|
|
489
|
-
})();
|
|
490
|
-
}
|
|
491
|
-
_onLiveMessage(data, fetchImpl) {
|
|
492
|
-
if (!data || typeof data !== "object") return;
|
|
493
|
-
const d = data;
|
|
494
|
-
if (d.event !== "translations_published") return;
|
|
495
|
-
const lang = d.language_code;
|
|
496
|
-
const ns = d.namespace_slug;
|
|
497
|
-
if (!lang || !ns) return;
|
|
498
|
-
const cacheKey = `${lang}/${ns}`;
|
|
499
|
-
if (!this._attempted.has(cacheKey)) return;
|
|
500
|
-
void this._loadBundle(lang, ns, fetchImpl, { bust: true }).then(() => {
|
|
501
|
-
this._notify();
|
|
502
|
-
});
|
|
503
|
-
}
|
|
366
|
+
if (targets.length === 0) return;
|
|
367
|
+
await Promise.all(
|
|
368
|
+
targets.map((t) => this._loadBundle(t.locale, t.ns, fetch, { bust: true }))
|
|
369
|
+
);
|
|
370
|
+
this._notify();
|
|
371
|
+
};
|
|
504
372
|
// ---- Translation ----
|
|
505
|
-
t = (key,
|
|
373
|
+
t = (key, optionsOrDefault, maybeOptions) => {
|
|
374
|
+
const options = typeof optionsOrDefault === "string" ? { ...maybeOptions ?? {}, defaultValue: optionsOrDefault } : optionsOrDefault;
|
|
506
375
|
const namespace = this._splitNamespace(key);
|
|
507
376
|
const bareKey = namespace.bareKey;
|
|
508
377
|
const ns = namespace.ns;
|
|
509
|
-
const fromActive = resolve(this._bundles.get(
|
|
378
|
+
const fromActive = resolve(this._bundles.get(this._bundleKey(this.locale, ns)), bareKey);
|
|
510
379
|
if (fromActive != null) {
|
|
511
380
|
return this._render(fromActive, this.locale, options);
|
|
512
381
|
}
|
|
513
382
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
514
|
-
const fb = resolve(this._bundles.get(
|
|
383
|
+
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);
|
|
515
384
|
if (fb != null) {
|
|
516
385
|
return this._render(fb, this.fallbackLng, options);
|
|
517
386
|
}
|
|
518
387
|
}
|
|
519
|
-
if (this.ready && this._attempted.has(
|
|
388
|
+
if (this.ready && this._attempted.has(this._bundleKey(this.locale, ns)) && this._hasContent.has(this._bundleKey(this.locale, ns))) {
|
|
520
389
|
this._reportMissing({
|
|
521
390
|
key: bareKey,
|
|
522
391
|
namespace: ns,
|
|
@@ -563,13 +432,13 @@ var VerbumiaI18n = class {
|
|
|
563
432
|
return { ns: this._config.namespaces[0], bareKey: key };
|
|
564
433
|
}
|
|
565
434
|
async _loadBundle(locale, ns, fetchImpl = fetch, opts = {}) {
|
|
566
|
-
const cacheKey =
|
|
435
|
+
const cacheKey = this._bundleKey(locale, ns);
|
|
567
436
|
let url;
|
|
568
437
|
let init;
|
|
569
438
|
if (this._config.env === "dev") {
|
|
570
439
|
const params = new URLSearchParams({ language: locale, namespace: ns });
|
|
571
|
-
if (this._config.
|
|
572
|
-
params.set("version_slug", this._config.
|
|
440
|
+
if (this._config.version && this._config.version !== "main") {
|
|
441
|
+
params.set("version_slug", this._config.version);
|
|
573
442
|
}
|
|
574
443
|
url = `${this._config.apiBase.replace(/\/+$/, "")}/v1/projects/${this._config.projectUuid}/translations/runtime?${params.toString()}`;
|
|
575
444
|
init = {
|
|
@@ -578,7 +447,7 @@ var VerbumiaI18n = class {
|
|
|
578
447
|
credentials: "omit"
|
|
579
448
|
};
|
|
580
449
|
} else {
|
|
581
|
-
url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/${this._config.
|
|
450
|
+
url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/${this._config.version}/latest/${locale}/${ns}.json`;
|
|
582
451
|
init = { method: "GET", credentials: "omit" };
|
|
583
452
|
}
|
|
584
453
|
if (opts.bust) {
|
|
@@ -633,7 +502,7 @@ var VerbumiaI18n = class {
|
|
|
633
502
|
return options.defaultValue;
|
|
634
503
|
}
|
|
635
504
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
636
|
-
const fb = resolve(this._bundles.get(
|
|
505
|
+
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);
|
|
637
506
|
if (typeof fb === "string") {
|
|
638
507
|
return fb;
|
|
639
508
|
}
|
|
@@ -657,6 +526,23 @@ var VerbumiaI18n = class {
|
|
|
657
526
|
}
|
|
658
527
|
};
|
|
659
528
|
|
|
529
|
+
// src/singleton.ts
|
|
530
|
+
var _active = null;
|
|
531
|
+
function _setActiveInstance(instance) {
|
|
532
|
+
_active = instance;
|
|
533
|
+
}
|
|
534
|
+
function _clearActiveInstance(instance) {
|
|
535
|
+
if (_active === instance) _active = null;
|
|
536
|
+
}
|
|
537
|
+
function getI18n() {
|
|
538
|
+
if (!_active) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
"@verbumia/react-i18next: getI18n() was called before <VerbumiaProvider> mounted (no active i18n instance)."
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
return _active;
|
|
544
|
+
}
|
|
545
|
+
|
|
660
546
|
// src/provider.tsx
|
|
661
547
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
662
548
|
var VerbumiaContext = (0, import_react.createContext)(null);
|
|
@@ -666,11 +552,13 @@ function VerbumiaProvider({
|
|
|
666
552
|
}) {
|
|
667
553
|
const i18n = (0, import_react.useMemo)(() => new VerbumiaI18n(config), []);
|
|
668
554
|
(0, import_react.useEffect)(() => {
|
|
555
|
+
_setActiveInstance(i18n);
|
|
669
556
|
void i18n.start();
|
|
670
557
|
const teardowns = (config.plugins ?? []).map((p) => p.setup?.({ i18n, config })).filter((t) => typeof t === "function");
|
|
671
558
|
return () => {
|
|
672
559
|
teardowns.forEach((t) => t());
|
|
673
560
|
i18n.stop();
|
|
561
|
+
_clearActiveInstance(i18n);
|
|
674
562
|
};
|
|
675
563
|
}, [i18n]);
|
|
676
564
|
const value = (0, import_react.useMemo)(() => ({ i18n }), [i18n]);
|
|
@@ -702,13 +590,14 @@ function useTranslation(defaultNamespace) {
|
|
|
702
590
|
renderedRef.current = /* @__PURE__ */ new Set();
|
|
703
591
|
const tokenRef = (0, import_react2.useRef)(/* @__PURE__ */ Symbol("verbumia.t"));
|
|
704
592
|
const t = (0, import_react2.useMemo)(() => {
|
|
705
|
-
|
|
593
|
+
const fn = (key, optionsOrDefault, maybeOptions) => {
|
|
706
594
|
const fullKey = defaultNamespace && !key.includes(":") ? `${defaultNamespace}:${key}` : key;
|
|
707
595
|
renderedRef.current.add(
|
|
708
596
|
keyRegistry.encode(fullKey, i18n.defaultNamespace)
|
|
709
597
|
);
|
|
710
|
-
return i18n.t(fullKey,
|
|
598
|
+
return i18n.t(fullKey, optionsOrDefault, maybeOptions);
|
|
711
599
|
};
|
|
600
|
+
return fn;
|
|
712
601
|
}, [i18n, defaultNamespace]);
|
|
713
602
|
(0, import_react2.useEffect)(() => {
|
|
714
603
|
keyRegistry._set(tokenRef.current, renderedRef.current);
|
|
@@ -764,6 +653,7 @@ function splitOnComponents(text, components) {
|
|
|
764
653
|
Trans,
|
|
765
654
|
VerbumiaProvider,
|
|
766
655
|
defaultTransport,
|
|
656
|
+
getI18n,
|
|
767
657
|
keyRegistry,
|
|
768
658
|
logTransport,
|
|
769
659
|
useTranslation
|