@verbumia/react-i18next 0.8.0 → 0.9.1
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 +70 -1
- package/dist/index.cjs +63 -201
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -38
- package/dist/index.d.ts +29 -38
- package/dist/index.js +63 -201
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,10 @@ interface VerbumiaConfig {
|
|
|
66
66
|
defaultNS?: string; // alias: default namespace for single-ns apps
|
|
67
67
|
apiBase?: string; // default 'https://api.verbumia.dev'
|
|
68
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
|
|
69
73
|
transport?: (batch: MissingKeyEvent[]) => void | Promise<void>;
|
|
70
74
|
missingHandler?: 'send' | 'log' | 'off'; // default 'send'
|
|
71
75
|
flushIntervalMs?: number; // default 5000
|
|
@@ -74,6 +78,19 @@ interface VerbumiaConfig {
|
|
|
74
78
|
}
|
|
75
79
|
```
|
|
76
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
|
+
|
|
77
94
|
### `useTranslation(defaultNamespace?)`
|
|
78
95
|
|
|
79
96
|
Returns `{ t, i18n }`.
|
|
@@ -95,9 +112,24 @@ interface I18nInstance {
|
|
|
95
112
|
t: TranslationFunction; // for out-of-React use via getI18n()
|
|
96
113
|
missingEvents: MissingKeyEvent[]; // newest first, capped buffer
|
|
97
114
|
flushMissing(): Promise<void>; // force-flush the pending batch
|
|
115
|
+
reload(opts?: { locale?: string; namespace?: string }): Promise<void>;
|
|
98
116
|
}
|
|
99
117
|
```
|
|
100
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
|
+
|
|
101
133
|
### `<Trans>`
|
|
102
134
|
|
|
103
135
|
Inline translation with JSX slots:
|
|
@@ -168,11 +200,16 @@ interface MissingKeyEvent {
|
|
|
168
200
|
key: string;
|
|
169
201
|
namespace: string;
|
|
170
202
|
language_code: string;
|
|
171
|
-
source_value?: string;
|
|
203
|
+
source_value?: string; // explicit defaultValue or fallback value; omitted when none (never the key name)
|
|
172
204
|
sdk_meta?: Record<string, unknown>; // SDK adds {lib, ver, url} automatically
|
|
173
205
|
}
|
|
174
206
|
```
|
|
175
207
|
|
|
208
|
+
`source_value` carries the canonical default the SDK has — the `defaultValue`
|
|
209
|
+
you pass to `t()` (object or positional form), or the fallback-language bundle
|
|
210
|
+
value. When there is no default, it is **omitted** (the key name is in `key`),
|
|
211
|
+
so the backend never mistakes a key for a translation.
|
|
212
|
+
|
|
176
213
|
### Why the gate matters
|
|
177
214
|
|
|
178
215
|
Without the gate, every `t("…")` call between mount and bundle resolution
|
|
@@ -212,6 +249,38 @@ import { defaultTransport, logTransport } from "@verbumia/react-i18next";
|
|
|
212
249
|
|
|
213
250
|
---
|
|
214
251
|
|
|
252
|
+
## Realtime updates
|
|
253
|
+
|
|
254
|
+
Zero-deploy translation updates (subscribe to the project's Centrifugo
|
|
255
|
+
`translations:` channel and bust-refetch on publish) live in the separate
|
|
256
|
+
[`@verbumia/realtime`](../realtime) package — added as a **plugin** of this
|
|
257
|
+
provider, not configured here:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { VerbumiaProvider } from "@verbumia/react-i18next";
|
|
261
|
+
import { verbumiaRealtime } from "@verbumia/realtime/react";
|
|
262
|
+
|
|
263
|
+
<VerbumiaProvider
|
|
264
|
+
{...config}
|
|
265
|
+
env="dev"
|
|
266
|
+
plugins={[
|
|
267
|
+
verbumiaRealtime({ wsUrl: "wss://centrifugo.verbumia.ca/connection/websocket" }),
|
|
268
|
+
]}
|
|
269
|
+
>
|
|
270
|
+
<App />
|
|
271
|
+
</VerbumiaProvider>;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Under the hood the plugin calls [`i18n.reload(...)`](#i18nreloadopts) on
|
|
275
|
+
each `translations_published` push. Realtime is a dev-version-only feature
|
|
276
|
+
(it only subscribes when `env: "dev"`).
|
|
277
|
+
|
|
278
|
+
> The `liveUpdates` / `centrifugoWsUrl` / `centrifugoTokenEndpoint` config
|
|
279
|
+
> keys were **removed in 0.9.0**. Install `@verbumia/realtime` and use the
|
|
280
|
+
> plugin instead.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
215
284
|
## Recipes
|
|
216
285
|
|
|
217
286
|
### Next.js (App Router)
|
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,7 @@ var import_react = require("react");
|
|
|
35
35
|
|
|
36
36
|
// src/transport.ts
|
|
37
37
|
var SDK_LIB = "@verbumia/react-i18next";
|
|
38
|
-
var SDK_VER = "0.
|
|
38
|
+
var SDK_VER = true ? "0.9.1" : "0.0.0-dev";
|
|
39
39
|
function defaultTransport(opts) {
|
|
40
40
|
return async (batch) => {
|
|
41
41
|
if (!batch.length) return;
|
|
@@ -45,7 +45,9 @@ function defaultTransport(opts) {
|
|
|
45
45
|
key: e.key,
|
|
46
46
|
namespace: e.namespace,
|
|
47
47
|
language_code: e.language_code,
|
|
48
|
-
|
|
48
|
+
// Option A (#746): only send source_value when there's a real value;
|
|
49
|
+
// omit it otherwise (never the key name). Absent = "no default".
|
|
50
|
+
...e.source_value !== void 0 ? { source_value: e.source_value } : {},
|
|
49
51
|
sdk_meta: {
|
|
50
52
|
lib: SDK_LIB,
|
|
51
53
|
ver: SDK_VER,
|
|
@@ -75,111 +77,6 @@ var logTransport = (batch) => {
|
|
|
75
77
|
}
|
|
76
78
|
};
|
|
77
79
|
|
|
78
|
-
// src/live.ts
|
|
79
|
-
var LiveClient = class {
|
|
80
|
-
constructor(cfg) {
|
|
81
|
-
this.cfg = cfg;
|
|
82
|
-
}
|
|
83
|
-
cfg;
|
|
84
|
-
_ws = null;
|
|
85
|
-
_id = 0;
|
|
86
|
-
_backoffMs = 1e3;
|
|
87
|
-
_disposed = false;
|
|
88
|
-
_connectAcked = false;
|
|
89
|
-
/** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */
|
|
90
|
-
async connect() {
|
|
91
|
-
if (this._ws) return;
|
|
92
|
-
if (this._disposed) return;
|
|
93
|
-
this.cfg.onStatus?.("connecting");
|
|
94
|
-
const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;
|
|
95
|
-
let url = this.cfg.url;
|
|
96
|
-
if (!url.includes("/connection/websocket")) {
|
|
97
|
-
url = url.replace(/\/+$/, "") + "/connection/websocket";
|
|
98
|
-
}
|
|
99
|
-
const ws = new WebSocket(url);
|
|
100
|
-
this._ws = ws;
|
|
101
|
-
this._connectAcked = false;
|
|
102
|
-
ws.onopen = () => {
|
|
103
|
-
this._send({ id: ++this._id, connect: { token } });
|
|
104
|
-
};
|
|
105
|
-
ws.onmessage = (evt) => this._onFrame(evt.data);
|
|
106
|
-
ws.onclose = () => this._onClose();
|
|
107
|
-
ws.onerror = () => {
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
dispose() {
|
|
111
|
-
this._disposed = true;
|
|
112
|
-
if (this._ws) {
|
|
113
|
-
try {
|
|
114
|
-
this._ws.close();
|
|
115
|
-
} catch {
|
|
116
|
-
}
|
|
117
|
-
this._ws = null;
|
|
118
|
-
}
|
|
119
|
-
this.cfg.onStatus?.("disconnected");
|
|
120
|
-
}
|
|
121
|
-
// ---- internals ----
|
|
122
|
-
_send(msg) {
|
|
123
|
-
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
124
|
-
try {
|
|
125
|
-
this._ws.send(JSON.stringify(msg));
|
|
126
|
-
} catch {
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
_onFrame(raw) {
|
|
130
|
-
let parsed;
|
|
131
|
-
try {
|
|
132
|
-
parsed = JSON.parse(raw);
|
|
133
|
-
} catch {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (!parsed || typeof parsed !== "object") return;
|
|
137
|
-
if (Object.keys(parsed).length === 0) {
|
|
138
|
-
this._send({});
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (parsed.connect && !this._connectAcked) {
|
|
142
|
-
this._connectAcked = true;
|
|
143
|
-
this.cfg.onStatus?.("connected");
|
|
144
|
-
this._backoffMs = 1e3;
|
|
145
|
-
this._send({
|
|
146
|
-
id: ++this._id,
|
|
147
|
-
subscribe: { channel: this.cfg.channel }
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const push = parsed.push;
|
|
152
|
-
if (push && push.channel === this.cfg.channel && push.pub) {
|
|
153
|
-
this.cfg.onMessage(push.pub.data);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
_onClose() {
|
|
157
|
-
this._ws = null;
|
|
158
|
-
this._connectAcked = false;
|
|
159
|
-
this.cfg.onStatus?.("disconnected");
|
|
160
|
-
if (this._disposed) return;
|
|
161
|
-
const delay = this._backoffMs;
|
|
162
|
-
this._backoffMs = Math.min(this._backoffMs * 2, 3e4);
|
|
163
|
-
setTimeout(() => {
|
|
164
|
-
if (!this._disposed) void this.connect();
|
|
165
|
-
}, delay);
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
async function fetchCentrifugoToken(endpoint, projectUuid, authToken, fetchImpl = fetch) {
|
|
169
|
-
const r = await fetchImpl(endpoint, {
|
|
170
|
-
method: "POST",
|
|
171
|
-
headers: {
|
|
172
|
-
"Content-Type": "application/json",
|
|
173
|
-
Authorization: `ApiKey ${authToken}`
|
|
174
|
-
},
|
|
175
|
-
body: JSON.stringify({ project_uuid: projectUuid })
|
|
176
|
-
});
|
|
177
|
-
if (!r.ok) {
|
|
178
|
-
throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);
|
|
179
|
-
}
|
|
180
|
-
return await r.json();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
80
|
// src/key-registry.ts
|
|
184
81
|
var GLOBAL = "__verbumia_key_registry__";
|
|
185
82
|
var SEP = "\0";
|
|
@@ -317,10 +214,10 @@ var VerbumiaI18n = class {
|
|
|
317
214
|
fallbackLng;
|
|
318
215
|
missingEvents = [];
|
|
319
216
|
_bundles = /* @__PURE__ */ new Map();
|
|
320
|
-
// `${locale}/${ns}` -> tree
|
|
217
|
+
// `${version}/${locale}/${ns}` -> tree
|
|
321
218
|
_attempted = /* @__PURE__ */ new Set();
|
|
322
|
-
// `${locale}/${ns}` keys we've fetched
|
|
323
|
-
// Tighter gate than `_attempted`: this set only contains (locale, ns)
|
|
219
|
+
// `${version}/${locale}/${ns}` keys we've fetched
|
|
220
|
+
// Tighter gate than `_attempted`: this set only contains (version, locale, ns)
|
|
324
221
|
// pairs whose CDN response was 200 with at least one top-level key. An
|
|
325
222
|
// empty bundle (404 → {} OR 200 → {}) is treated as "no data yet";
|
|
326
223
|
// calling t() against a key in such a bundle does NOT fire reportMissing.
|
|
@@ -335,13 +232,20 @@ var VerbumiaI18n = class {
|
|
|
335
232
|
// dedup `${locale}/${ns}/${key}` per-flush
|
|
336
233
|
_timer = null;
|
|
337
234
|
_listeners = /* @__PURE__ */ new Set();
|
|
338
|
-
_live = null;
|
|
339
235
|
// Stable snapshot reference for useSyncExternalStore. Returning a fresh
|
|
340
236
|
// object on each getSnapshot call would loop React forever — we rebuild
|
|
341
237
|
// it ONLY in _notify (when state actually changed) and return the cached
|
|
342
238
|
// reference between notifications.
|
|
343
239
|
_snapshot;
|
|
344
240
|
constructor(config) {
|
|
241
|
+
const removedRealtimeKeys = Object.keys(config).filter(
|
|
242
|
+
(k) => k === "liveUpdates" || k.startsWith("centrifugo")
|
|
243
|
+
);
|
|
244
|
+
if (removedRealtimeKeys.length > 0) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`@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.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
345
249
|
this.locale = config.defaultLocale;
|
|
346
250
|
this.fallbackLng = config.fallbackLng;
|
|
347
251
|
this._config = {
|
|
@@ -354,10 +258,7 @@ var VerbumiaI18n = class {
|
|
|
354
258
|
flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_MS,
|
|
355
259
|
flushBatchSize: config.flushBatchSize ?? DEFAULT_BATCH,
|
|
356
260
|
missingEventsBufferSize: config.missingEventsBufferSize ?? DEFAULT_BUFFER,
|
|
357
|
-
|
|
358
|
-
liveUpdates: !!config.liveUpdates,
|
|
359
|
-
centrifugoTokenEndpoint: config.centrifugoTokenEndpoint ?? `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, "")}/v1/auth/centrifugo-token`,
|
|
360
|
-
centrifugoWsUrl: config.centrifugoWsUrl ?? "",
|
|
261
|
+
version: config.version ?? config.versionSlug ?? DEFAULT_VERSION_SLUG,
|
|
361
262
|
env: config.env ?? "prod"
|
|
362
263
|
};
|
|
363
264
|
this._transport = config.transport ?? (this._config.missingHandler === "log" ? logTransport : defaultTransport({
|
|
@@ -384,9 +285,16 @@ var VerbumiaI18n = class {
|
|
|
384
285
|
changeLanguage: this.changeLanguage,
|
|
385
286
|
t: this.t,
|
|
386
287
|
missingEvents: this.missingEvents,
|
|
387
|
-
flushMissing: this.flushMissing
|
|
288
|
+
flushMissing: this.flushMissing,
|
|
289
|
+
reload: this.reload
|
|
388
290
|
};
|
|
389
291
|
}
|
|
292
|
+
/** Bundle cache-key builder. Includes `version` so providers with
|
|
293
|
+
* different `version` values never share cached bundles. The segments
|
|
294
|
+
* (version/locale/ns) are slugs and never contain '/'. */
|
|
295
|
+
_bundleKey(locale, ns) {
|
|
296
|
+
return `${this._config.version}/${locale}/${ns}`;
|
|
297
|
+
}
|
|
390
298
|
_notify() {
|
|
391
299
|
this._snapshot = this._buildSnapshot();
|
|
392
300
|
for (const l of this._listeners) l();
|
|
@@ -409,9 +317,6 @@ var VerbumiaI18n = class {
|
|
|
409
317
|
);
|
|
410
318
|
this.ready = true;
|
|
411
319
|
this._startTimer();
|
|
412
|
-
if (this._config.liveUpdates && this._config.env === "dev") {
|
|
413
|
-
this._startLive(fetchImpl);
|
|
414
|
-
}
|
|
415
320
|
this._notify();
|
|
416
321
|
}
|
|
417
322
|
setLocale = async (next) => {
|
|
@@ -437,97 +342,52 @@ var VerbumiaI18n = class {
|
|
|
437
342
|
clearInterval(this._timer);
|
|
438
343
|
this._timer = null;
|
|
439
344
|
}
|
|
440
|
-
if (this._live) {
|
|
441
|
-
this._live.dispose();
|
|
442
|
-
this._live = null;
|
|
443
|
-
}
|
|
444
345
|
}
|
|
445
346
|
/**
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
347
|
+
* Bust-refetch already-loaded bundles and re-render once. Generic
|
|
348
|
+
* replacement for the old realtime-only refetch: iterate the
|
|
349
|
+
* `_attempted` cache keys (`${version}/${locale}/${ns}`), optionally
|
|
350
|
+
* filtered by `opts.locale` / `opts.namespace`, and re-pull each one
|
|
351
|
+
* with `{ bust: true }` so the mutable CDN `latest/` alias bypasses the
|
|
352
|
+
* HTTP cache. After all settle, `_notify()` once so React re-renders.
|
|
353
|
+
*
|
|
354
|
+
* Used by `@verbumia/realtime` on a `translations_published` push and as
|
|
355
|
+
* a manual refresh hook. If nothing matches, returns without notifying.
|
|
450
356
|
*/
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
357
|
+
reload = async (opts = {}) => {
|
|
358
|
+
const targets = [];
|
|
359
|
+
for (const key of this._attempted) {
|
|
360
|
+
const parts = key.split("/");
|
|
361
|
+
const locale = parts[1];
|
|
362
|
+
const ns = parts[2];
|
|
363
|
+
if (!locale || !ns) continue;
|
|
364
|
+
if (opts.locale && opts.locale !== locale) continue;
|
|
365
|
+
if (opts.namespace && opts.namespace !== ns) continue;
|
|
366
|
+
targets.push({ locale, ns });
|
|
460
367
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
projectUuid,
|
|
468
|
-
apiToken,
|
|
469
|
-
fetchImpl
|
|
470
|
-
);
|
|
471
|
-
return token;
|
|
472
|
-
};
|
|
473
|
-
void (async () => {
|
|
474
|
-
let channel;
|
|
475
|
-
let token;
|
|
476
|
-
try {
|
|
477
|
-
const minted = await fetchCentrifugoToken(
|
|
478
|
-
tokenEndpoint,
|
|
479
|
-
projectUuid,
|
|
480
|
-
apiToken,
|
|
481
|
-
fetchImpl
|
|
482
|
-
);
|
|
483
|
-
channel = minted.channel;
|
|
484
|
-
token = minted.token;
|
|
485
|
-
} catch (err) {
|
|
486
|
-
if (typeof console !== "undefined") {
|
|
487
|
-
console.warn("@verbumia/react-i18next: live token mint failed", err);
|
|
488
|
-
}
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
this._live = new LiveClient({
|
|
492
|
-
url: wsUrl,
|
|
493
|
-
token,
|
|
494
|
-
channel,
|
|
495
|
-
refreshToken,
|
|
496
|
-
onMessage: (data) => this._onLiveMessage(data, fetchImpl)
|
|
497
|
-
});
|
|
498
|
-
void this._live.connect();
|
|
499
|
-
})();
|
|
500
|
-
}
|
|
501
|
-
_onLiveMessage(data, fetchImpl) {
|
|
502
|
-
if (!data || typeof data !== "object") return;
|
|
503
|
-
const d = data;
|
|
504
|
-
if (d.event !== "translations_published") return;
|
|
505
|
-
const lang = d.language_code;
|
|
506
|
-
const ns = d.namespace_slug;
|
|
507
|
-
if (!lang || !ns) return;
|
|
508
|
-
const cacheKey = `${lang}/${ns}`;
|
|
509
|
-
if (!this._attempted.has(cacheKey)) return;
|
|
510
|
-
void this._loadBundle(lang, ns, fetchImpl, { bust: true }).then(() => {
|
|
511
|
-
this._notify();
|
|
512
|
-
});
|
|
513
|
-
}
|
|
368
|
+
if (targets.length === 0) return;
|
|
369
|
+
await Promise.all(
|
|
370
|
+
targets.map((t) => this._loadBundle(t.locale, t.ns, fetch, { bust: true }))
|
|
371
|
+
);
|
|
372
|
+
this._notify();
|
|
373
|
+
};
|
|
514
374
|
// ---- Translation ----
|
|
515
375
|
t = (key, optionsOrDefault, maybeOptions) => {
|
|
516
376
|
const options = typeof optionsOrDefault === "string" ? { ...maybeOptions ?? {}, defaultValue: optionsOrDefault } : optionsOrDefault;
|
|
517
377
|
const namespace = this._splitNamespace(key);
|
|
518
378
|
const bareKey = namespace.bareKey;
|
|
519
379
|
const ns = namespace.ns;
|
|
520
|
-
const fromActive = resolve(this._bundles.get(
|
|
380
|
+
const fromActive = resolve(this._bundles.get(this._bundleKey(this.locale, ns)), bareKey);
|
|
521
381
|
if (fromActive != null) {
|
|
522
382
|
return this._render(fromActive, this.locale, options);
|
|
523
383
|
}
|
|
524
384
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
525
|
-
const fb = resolve(this._bundles.get(
|
|
385
|
+
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);
|
|
526
386
|
if (fb != null) {
|
|
527
387
|
return this._render(fb, this.fallbackLng, options);
|
|
528
388
|
}
|
|
529
389
|
}
|
|
530
|
-
if (this.ready && this._attempted.has(
|
|
390
|
+
if (this.ready && this._attempted.has(this._bundleKey(this.locale, ns)) && this._hasContent.has(this._bundleKey(this.locale, ns))) {
|
|
531
391
|
this._reportMissing({
|
|
532
392
|
key: bareKey,
|
|
533
393
|
namespace: ns,
|
|
@@ -574,13 +434,13 @@ var VerbumiaI18n = class {
|
|
|
574
434
|
return { ns: this._config.namespaces[0], bareKey: key };
|
|
575
435
|
}
|
|
576
436
|
async _loadBundle(locale, ns, fetchImpl = fetch, opts = {}) {
|
|
577
|
-
const cacheKey =
|
|
437
|
+
const cacheKey = this._bundleKey(locale, ns);
|
|
578
438
|
let url;
|
|
579
439
|
let init;
|
|
580
440
|
if (this._config.env === "dev") {
|
|
581
441
|
const params = new URLSearchParams({ language: locale, namespace: ns });
|
|
582
|
-
if (this._config.
|
|
583
|
-
params.set("version_slug", this._config.
|
|
442
|
+
if (this._config.version && this._config.version !== "main") {
|
|
443
|
+
params.set("version_slug", this._config.version);
|
|
584
444
|
}
|
|
585
445
|
url = `${this._config.apiBase.replace(/\/+$/, "")}/v1/projects/${this._config.projectUuid}/translations/runtime?${params.toString()}`;
|
|
586
446
|
init = {
|
|
@@ -589,7 +449,7 @@ var VerbumiaI18n = class {
|
|
|
589
449
|
credentials: "omit"
|
|
590
450
|
};
|
|
591
451
|
} else {
|
|
592
|
-
url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/${this._config.
|
|
452
|
+
url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/${this._config.version}/latest/${locale}/${ns}.json`;
|
|
593
453
|
init = { method: "GET", credentials: "omit" };
|
|
594
454
|
}
|
|
595
455
|
if (opts.bust) {
|
|
@@ -631,25 +491,27 @@ var VerbumiaI18n = class {
|
|
|
631
491
|
/**
|
|
632
492
|
* Resolve the `source_value` we send with a missing-key report.
|
|
633
493
|
*
|
|
634
|
-
* Fallback chain (
|
|
494
|
+
* Fallback chain (Option A, task #746 — backend ingest aligned):
|
|
635
495
|
* 1. `options.defaultValue` — explicit developer-provided string.
|
|
636
496
|
* 2. The fallbackLng bundle's value for this key (typically the
|
|
637
497
|
* source/canonical locale). Only used when it resolves to a
|
|
638
498
|
* plain string, not a plural CLDR dict.
|
|
639
|
-
* 3.
|
|
640
|
-
* a
|
|
499
|
+
* 3. Otherwise `undefined` — we DO NOT fall back to the key name (that
|
|
500
|
+
* made the column unusable: a placeholder indistinguishable from a
|
|
501
|
+
* real default). The key is already carried in `event.key`; an absent
|
|
502
|
+
* `source_value` is the signal that there is no promotable value.
|
|
641
503
|
*/
|
|
642
504
|
_sourceValueFor(bareKey, ns, options) {
|
|
643
505
|
if (typeof options?.defaultValue === "string") {
|
|
644
506
|
return options.defaultValue;
|
|
645
507
|
}
|
|
646
508
|
if (this.fallbackLng && this.fallbackLng !== this.locale) {
|
|
647
|
-
const fb = resolve(this._bundles.get(
|
|
509
|
+
const fb = resolve(this._bundles.get(this._bundleKey(this.fallbackLng, ns)), bareKey);
|
|
648
510
|
if (typeof fb === "string") {
|
|
649
511
|
return fb;
|
|
650
512
|
}
|
|
651
513
|
}
|
|
652
|
-
return
|
|
514
|
+
return void 0;
|
|
653
515
|
}
|
|
654
516
|
_reportMissing(event) {
|
|
655
517
|
if (this._config.missingHandler === "off") return;
|