@sonenta/react-i18next 2.0.1 → 2.2.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 +48 -5
- package/dist/index.cjs +182 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -4
- package/dist/index.d.ts +92 -4
- package/dist/index.js +181 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](./LICENSE)
|
|
4
4
|
|
|
5
|
-
The React SDK for [Sonenta](https://
|
|
5
|
+
The React SDK for [Sonenta](https://sonenta.com). Resolve translations from
|
|
6
6
|
the Sonenta CDN, fall back gracefully when a key is missing, and stream those
|
|
7
7
|
missing keys back to your dashboard in real time so the team can fill them
|
|
8
8
|
without redeploying.
|
|
@@ -94,8 +94,8 @@ interface SonentaConfig {
|
|
|
94
94
|
fallbackLng?: string | string[]; // fallback locale(s); variants also fall back to their base (fr-CA → fr)
|
|
95
95
|
namespaces?: string[]; // default ['common']
|
|
96
96
|
defaultNS?: string; // alias: default namespace for single-ns apps
|
|
97
|
-
apiBase?: string; // default 'https://api.
|
|
98
|
-
cdnBase?: string; // default 'https://cdn.
|
|
97
|
+
apiBase?: string; // default 'https://api.sonenta.com'
|
|
98
|
+
cdnBase?: string; // default 'https://cdn.sonenta.com'
|
|
99
99
|
languageCatalog?: LanguageMeta[]; // embed the language catalog (offline/SSR/RN); powers dir()/nativeName()
|
|
100
100
|
disableLanguageCatalog?: boolean; // skip the public GET /v1/languages fetch
|
|
101
101
|
version?: string; // version slug, default 'main' (in cache keys)
|
|
@@ -412,7 +412,7 @@ import { sonentaRealtime } from "@sonenta/realtime/react";
|
|
|
412
412
|
{...config}
|
|
413
413
|
env="dev"
|
|
414
414
|
plugins={[
|
|
415
|
-
sonentaRealtime({ wsUrl: "wss://rt.
|
|
415
|
+
sonentaRealtime({ wsUrl: "wss://rt.sonenta.dev/connection/websocket" }),
|
|
416
416
|
]}
|
|
417
417
|
>
|
|
418
418
|
<App />
|
|
@@ -455,7 +455,7 @@ from the snapshot do not fire "missing" reports until a real fetch confirms.
|
|
|
455
455
|
- **CLI (recommended):** `verbumia snapshot` (from `@verbumia/cli`) fetches the
|
|
456
456
|
current published bundles and writes the JSON module.
|
|
457
457
|
- **Manual:** fetch each
|
|
458
|
-
`https://cdn.
|
|
458
|
+
`https://cdn.sonenta.com/p/<project>/<version>/latest/<locale>/<ns>.json` and
|
|
459
459
|
assemble them into `{ [locale]: { [namespace]: <tree> } }`, then import it.
|
|
460
460
|
|
|
461
461
|
## Surface variants
|
|
@@ -497,6 +497,49 @@ Plurals work the same in overlays (single key + CLDR plural forms); an overlay
|
|
|
497
497
|
plural set fully replaces the base key's. Surface overlays are served from the
|
|
498
498
|
CDN; `env: "dev"` is base-only for now.
|
|
499
499
|
|
|
500
|
+
## Accessibility surfaces
|
|
501
|
+
|
|
502
|
+
A11y variants attach SEMANTIC accessibility text to a key — `aria_label`,
|
|
503
|
+
`alt_text`, `screen_reader`, `plain_language` — delivered through the same
|
|
504
|
+
sparse-overlay engine as device surfaces, but applied **orthogonally** to the
|
|
505
|
+
visible text (an element has both its visible label AND an accessible name).
|
|
506
|
+
Opt in per surface; they load alongside the base bundles.
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
<SonentaProvider {...config} a11ySurfaces={["aria_label", "alt_text"]}>
|
|
510
|
+
<App />
|
|
511
|
+
</SonentaProvider>
|
|
512
|
+
|
|
513
|
+
function SaveButton() {
|
|
514
|
+
const { t } = useTranslation("common");
|
|
515
|
+
return (
|
|
516
|
+
<button aria-label={t.aria("save")}>{t("save")}</button>
|
|
517
|
+
);
|
|
518
|
+
// t("save") → visible text ("Save")
|
|
519
|
+
// t.aria("save") → aria_label overlay, or the visible text if no override
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function Hero() {
|
|
523
|
+
const { i18n } = useTranslation();
|
|
524
|
+
return <img src={i18n.a11yAsset("hero")?.ref} alt={i18n.alt("hero")} />;
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
- `t.aria(key)` / `t.alt(key)` — overlay value, **falling back to the visible
|
|
529
|
+
text** when no a11y override exists. Also on the instance: `i18n.aria()` /
|
|
530
|
+
`i18n.alt()`.
|
|
531
|
+
- `t.a11y(key, surface)` / `i18n.a11y(key, surface)` — the raw resolver:
|
|
532
|
+
returns `undefined` when there's no override (use for `screen_reader` /
|
|
533
|
+
`plain_language`, which should be **omitted** rather than fall back).
|
|
534
|
+
- `i18n.a11yAsset(key)` — the `alt_text` overlay's localized-image `$asset`.
|
|
535
|
+
- **Plain language (cognitive) toggle:** include `plain_language` (or pass
|
|
536
|
+
`plainLanguage`) and call `i18n.setPlainLanguage(true)` — `t()` then returns
|
|
537
|
+
the `plain_language` overlay for keys that have one (else the base text).
|
|
538
|
+
|
|
539
|
+
Resolution is locale-outer / surface-inner (same chain as `t()`): `(fr-CA,
|
|
540
|
+
aria_label) > (fr-CA, base) > (fr, aria_label) > (fr, base)`. Overlays are
|
|
541
|
+
CDN-only (`env: "dev"` is base-only).
|
|
542
|
+
|
|
500
543
|
## Recipes
|
|
501
544
|
|
|
502
545
|
### Next.js (App Router)
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
A11Y_SURFACES: () => A11Y_SURFACES,
|
|
23
24
|
DEFAULT_SURFACE_BREAKPOINTS: () => DEFAULT_SURFACE_BREAKPOINTS,
|
|
24
25
|
SonentaProvider: () => SonentaProvider,
|
|
25
26
|
Trans: () => Trans,
|
|
@@ -236,7 +237,7 @@ function flattenPlurals(tree, locale) {
|
|
|
236
237
|
|
|
237
238
|
// src/transport.ts
|
|
238
239
|
var SDK_LIB = "@sonenta/react-i18next";
|
|
239
|
-
var SDK_VER = true ? "2.0
|
|
240
|
+
var SDK_VER = true ? "2.2.0" : "0.0.0-dev";
|
|
240
241
|
function defaultTransport(opts) {
|
|
241
242
|
return async (batch) => {
|
|
242
243
|
if (!batch.length) return;
|
|
@@ -390,8 +391,8 @@ var KeyRegistry = class {
|
|
|
390
391
|
var keyRegistry = new KeyRegistry();
|
|
391
392
|
|
|
392
393
|
// src/i18n.ts
|
|
393
|
-
var DEFAULT_API_BASE = "https://api.
|
|
394
|
-
var DEFAULT_CDN_BASE = "https://cdn.
|
|
394
|
+
var DEFAULT_API_BASE = "https://api.sonenta.com";
|
|
395
|
+
var DEFAULT_CDN_BASE = "https://cdn.sonenta.com";
|
|
395
396
|
var DEFAULT_FLUSH_MS = 5e3;
|
|
396
397
|
var DEFAULT_BATCH = 50;
|
|
397
398
|
var DEFAULT_BUFFER = 200;
|
|
@@ -442,6 +443,14 @@ var SonentaI18n = class {
|
|
|
442
443
|
// Resolved asset refs (#911 minimal v1), keyed `${locale}/${ns}/${keyPath}`
|
|
443
444
|
// for the CURRENT composition (base assets, then overlay assets override).
|
|
444
445
|
_assets = /* @__PURE__ */ new Map();
|
|
446
|
+
// A11y surfaces (#989 / task 994). Each configured a11y surface S is loaded
|
|
447
|
+
// as its OWN i18next namespace `${ns}__${S}` (sparse `{ns}.{S}.json`), so the
|
|
448
|
+
// accessors resolve through i18next's native locale-fallback + nested-key
|
|
449
|
+
// logic without touching the visible-text bundle. `_a11yAssets` carries the
|
|
450
|
+
// `alt_text` `$asset` refs, keyed `${locale}/${ns}/${keyPath}#${surface}`.
|
|
451
|
+
_a11ySurfaces = /* @__PURE__ */ new Set();
|
|
452
|
+
_plainLanguage = false;
|
|
453
|
+
_a11yAssets = /* @__PURE__ */ new Map();
|
|
445
454
|
_missing;
|
|
446
455
|
_listeners = /* @__PURE__ */ new Set();
|
|
447
456
|
// Stable snapshot reference for useSyncExternalStore. Rebuilt ONLY in _notify
|
|
@@ -469,6 +478,9 @@ var SonentaI18n = class {
|
|
|
469
478
|
this.locale = config.defaultLocale;
|
|
470
479
|
this.fallbackLng = config.fallbackLng;
|
|
471
480
|
this._surface = config.surface;
|
|
481
|
+
for (const s of config.a11ySurfaces ?? []) this._a11ySurfaces.add(s);
|
|
482
|
+
this._plainLanguage = config.plainLanguage === true;
|
|
483
|
+
if (this._plainLanguage) this._a11ySurfaces.add("plain_language");
|
|
472
484
|
let keySeparator = ".";
|
|
473
485
|
if (config.keySeparator !== void 0) {
|
|
474
486
|
keySeparator = config.keySeparator;
|
|
@@ -653,6 +665,12 @@ var SonentaI18n = class {
|
|
|
653
665
|
surface: this._surface,
|
|
654
666
|
setSurface: this.setSurface,
|
|
655
667
|
asset: this.asset,
|
|
668
|
+
aria: this.aria,
|
|
669
|
+
alt: this.alt,
|
|
670
|
+
a11y: this.a11y,
|
|
671
|
+
a11yAsset: this.a11yAsset,
|
|
672
|
+
plainLanguage: this._plainLanguage,
|
|
673
|
+
setPlainLanguage: this.setPlainLanguage,
|
|
656
674
|
dir: this.dir,
|
|
657
675
|
nativeName: this.nativeName,
|
|
658
676
|
languageMeta: this.languageMeta,
|
|
@@ -894,9 +912,96 @@ var SonentaI18n = class {
|
|
|
894
912
|
}
|
|
895
913
|
return void 0;
|
|
896
914
|
};
|
|
915
|
+
// ---- A11y (#989 / task 994) ----
|
|
916
|
+
/** Split a possibly-`ns:key` string into `{ ns, bareKey }`, honoring the
|
|
917
|
+
* configured nsSeparator (mirrors {@link asset}). */
|
|
918
|
+
_splitKey(key, namespace) {
|
|
919
|
+
let ns = namespace ?? this.defaultNamespace;
|
|
920
|
+
let bareKey = key;
|
|
921
|
+
if (typeof this._nsSeparator === "string" && this._nsSeparator) {
|
|
922
|
+
const idx = key.indexOf(this._nsSeparator);
|
|
923
|
+
if (idx > 0) {
|
|
924
|
+
ns = key.slice(0, idx);
|
|
925
|
+
bareKey = key.slice(idx + this._nsSeparator.length);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return { ns, bareKey };
|
|
929
|
+
}
|
|
930
|
+
/** Resolve an a11y `surface` override for `key`, or `undefined` when none
|
|
931
|
+
* exists (the namespace is sparse). Goes through i18next so the locale
|
|
932
|
+
* fallback chain + nested keys + plurals apply. */
|
|
933
|
+
a11y = (key, surface, namespace) => {
|
|
934
|
+
if (!this._a11ySurfaces.has(surface)) return void 0;
|
|
935
|
+
const { ns, bareKey } = this._splitKey(key, namespace);
|
|
936
|
+
const a11yNs = this._a11yNs(ns, surface);
|
|
937
|
+
if (!this._i18next.exists(bareKey, { ns: a11yNs })) return void 0;
|
|
938
|
+
return this._i18next.t(bareKey, { ns: a11yNs });
|
|
939
|
+
};
|
|
940
|
+
/** Accessible name for `key` (`aria_label` overlay) — falls back to the
|
|
941
|
+
* visible `t(key)` text when no override exists. Spread as `aria-label`. */
|
|
942
|
+
aria = (key, namespace) => {
|
|
943
|
+
const v = this.a11y(key, "aria_label", namespace);
|
|
944
|
+
if (v !== void 0) return v;
|
|
945
|
+
const { ns, bareKey } = this._splitKey(key, namespace);
|
|
946
|
+
return this._i18next.t(bareKey, { ns });
|
|
947
|
+
};
|
|
948
|
+
/** Image alt text for `key` (`alt_text` overlay) — falls back to the visible
|
|
949
|
+
* `t(key)` text. Pair with {@link a11yAsset} for a localized image. */
|
|
950
|
+
alt = (key, namespace) => {
|
|
951
|
+
const v = this.a11y(key, "alt_text", namespace);
|
|
952
|
+
if (v !== void 0) return v;
|
|
953
|
+
const { ns, bareKey } = this._splitKey(key, namespace);
|
|
954
|
+
return this._i18next.t(bareKey, { ns });
|
|
955
|
+
};
|
|
956
|
+
/** Localized-image `$asset` ref from a key's `alt_text` overlay, or
|
|
957
|
+
* `undefined`. Walks the locale fallback chain like `t()` would. */
|
|
958
|
+
a11yAsset = (key, namespace) => {
|
|
959
|
+
const { ns, bareKey } = this._splitKey(key, namespace);
|
|
960
|
+
for (const loc of this._resolutionChain(this.locale)) {
|
|
961
|
+
const ref = this._a11yAssets.get(`${loc}/${ns}/${bareKey}#alt_text`);
|
|
962
|
+
if (ref) return ref;
|
|
963
|
+
}
|
|
964
|
+
return void 0;
|
|
965
|
+
};
|
|
966
|
+
get plainLanguage() {
|
|
967
|
+
return this._plainLanguage;
|
|
968
|
+
}
|
|
969
|
+
/** Toggle simplified-language ("plain language" / FALC) mode (#989). When
|
|
970
|
+
* on, `t()` returns the `plain_language` overlay value for keys that have
|
|
971
|
+
* one. Loads the `plain_language` overlays on first enable if they were not
|
|
972
|
+
* configured, then re-renders. No-op when unchanged. */
|
|
973
|
+
setPlainLanguage = async (on) => {
|
|
974
|
+
if (on === this._plainLanguage) return;
|
|
975
|
+
this._plainLanguage = on;
|
|
976
|
+
if (on && !this._a11ySurfaces.has("plain_language")) {
|
|
977
|
+
this._a11ySurfaces.add("plain_language");
|
|
978
|
+
const targets = [];
|
|
979
|
+
for (const k of this._attempted) {
|
|
980
|
+
const parts = k.split("/");
|
|
981
|
+
const locale = parts[1];
|
|
982
|
+
const ns = parts[2];
|
|
983
|
+
if (locale && ns) targets.push({ locale, ns });
|
|
984
|
+
}
|
|
985
|
+
await Promise.all(
|
|
986
|
+
targets.map((t2) => this._loadA11yOverlays(t2.locale, t2.ns))
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
this._notify();
|
|
990
|
+
this._signalLoaded();
|
|
991
|
+
};
|
|
897
992
|
// ---- Translation ----
|
|
898
993
|
t = (key, optionsOrDefault, maybeOptions) => {
|
|
899
994
|
const options = typeof optionsOrDefault === "string" ? { ...maybeOptions ?? {}, defaultValue: optionsOrDefault } : optionsOrDefault;
|
|
995
|
+
if (this._plainLanguage && this._a11ySurfaces.has("plain_language")) {
|
|
996
|
+
const { ns, bareKey } = this._splitKey(key);
|
|
997
|
+
const plainNs = this._a11yNs(ns, "plain_language");
|
|
998
|
+
if (this._i18next.exists(bareKey, { ...options ?? {}, ns: plainNs })) {
|
|
999
|
+
return this._i18next.t(bareKey, {
|
|
1000
|
+
...options ?? {},
|
|
1001
|
+
ns: plainNs
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
900
1005
|
const literal = this._probeLiteral(key);
|
|
901
1006
|
if (literal !== void 0) {
|
|
902
1007
|
const interpolator = this._i18next.services?.interpolator;
|
|
@@ -1047,6 +1152,67 @@ var SonentaI18n = class {
|
|
|
1047
1152
|
this._attempted.add(cacheKey);
|
|
1048
1153
|
}
|
|
1049
1154
|
await this._composeBundle(locale, ns, fetchImpl, opts.bust);
|
|
1155
|
+
await this._loadA11yOverlays(locale, ns, fetchImpl, opts.bust);
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Load every configured a11y surface overlay (#989 / task 994) for
|
|
1159
|
+
* (locale, ns) into a DEDICATED i18next namespace `${ns}__${surface}`, so
|
|
1160
|
+
* `aria()` / `alt()` / `a11y()` resolve them through i18next (locale
|
|
1161
|
+
* fallback + nested keys + plurals) WITHOUT polluting the visible-text
|
|
1162
|
+
* bundle. Sparse + best-effort: an absent overlay registers `{}` and the
|
|
1163
|
+
* accessor reports "no override".
|
|
1164
|
+
*/
|
|
1165
|
+
async _loadA11yOverlays(locale, ns, fetchImpl = fetch, bust = false) {
|
|
1166
|
+
if (this._a11ySurfaces.size === 0) return;
|
|
1167
|
+
await Promise.all(
|
|
1168
|
+
[...this._a11ySurfaces].map(async (surface) => {
|
|
1169
|
+
const overlay = await this._loadOverlay(
|
|
1170
|
+
locale,
|
|
1171
|
+
ns,
|
|
1172
|
+
surface,
|
|
1173
|
+
fetchImpl,
|
|
1174
|
+
bust
|
|
1175
|
+
);
|
|
1176
|
+
const tree = this._unwrapA11y(overlay, locale, ns, surface);
|
|
1177
|
+
this._i18next.addResourceBundle(
|
|
1178
|
+
locale,
|
|
1179
|
+
this._a11yNs(ns, surface),
|
|
1180
|
+
flattenPlurals(tree, locale),
|
|
1181
|
+
false,
|
|
1182
|
+
true
|
|
1183
|
+
);
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
/** i18next namespace that backs an a11y surface overlay for `ns`. */
|
|
1188
|
+
_a11yNs(ns, surface) {
|
|
1189
|
+
return `${ns}__${surface}`;
|
|
1190
|
+
}
|
|
1191
|
+
/** Like {@link _unwrapAssets} but records `$asset` refs into `_a11yAssets`
|
|
1192
|
+
* (keyed with the `#${surface}` suffix) instead of the visible-text
|
|
1193
|
+
* `_assets`, so `alt_text` images don't collide with device-surface assets. */
|
|
1194
|
+
_unwrapA11y(tree, locale, ns, surface) {
|
|
1195
|
+
const sep = typeof this._i18next.options.keySeparator === "string" ? this._i18next.options.keySeparator : ".";
|
|
1196
|
+
const walk = (node, path) => {
|
|
1197
|
+
if (!node || typeof node !== "object") return node;
|
|
1198
|
+
const obj = node;
|
|
1199
|
+
if (Object.prototype.hasOwnProperty.call(obj, "$value")) {
|
|
1200
|
+
const a = obj.$asset;
|
|
1201
|
+
if (a && typeof a.kind === "string" && typeof a.ref === "string") {
|
|
1202
|
+
this._a11yAssets.set(
|
|
1203
|
+
`${locale}/${ns}/${path.join(sep)}#${surface}`,
|
|
1204
|
+
{ kind: a.kind, ref: a.ref }
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
return obj.$value;
|
|
1208
|
+
}
|
|
1209
|
+
const out = {};
|
|
1210
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1211
|
+
out[k] = walk(v, [...path, k]);
|
|
1212
|
+
}
|
|
1213
|
+
return out;
|
|
1214
|
+
};
|
|
1215
|
+
return walk(tree, []);
|
|
1050
1216
|
}
|
|
1051
1217
|
/**
|
|
1052
1218
|
* Compose the i18next bundle for (locale, ns) as base ⊕ surface overlay
|
|
@@ -1174,6 +1340,12 @@ function t(key, optionsOrDefault, maybeOptions) {
|
|
|
1174
1340
|
}
|
|
1175
1341
|
|
|
1176
1342
|
// src/surface.ts
|
|
1343
|
+
var A11Y_SURFACES = [
|
|
1344
|
+
"aria_label",
|
|
1345
|
+
"alt_text",
|
|
1346
|
+
"screen_reader",
|
|
1347
|
+
"plain_language"
|
|
1348
|
+
];
|
|
1177
1349
|
var DEFAULT_SURFACE_BREAKPOINTS = {
|
|
1178
1350
|
mobile: 640,
|
|
1179
1351
|
tablet: 1024
|
|
@@ -1257,7 +1429,12 @@ function useTranslation(defaultNamespace) {
|
|
|
1257
1429
|
);
|
|
1258
1430
|
return i18n.t(fullKey, optionsOrDefault, maybeOptions);
|
|
1259
1431
|
};
|
|
1260
|
-
|
|
1432
|
+
const withNs = (key) => defaultNamespace && !key.includes(":") ? `${defaultNamespace}:${key}` : key;
|
|
1433
|
+
const aug = fn;
|
|
1434
|
+
aug.aria = (key, namespace) => i18n.aria(withNs(key), namespace);
|
|
1435
|
+
aug.alt = (key, namespace) => i18n.alt(withNs(key), namespace);
|
|
1436
|
+
aug.a11y = (key, surface, namespace) => i18n.a11y(withNs(key), surface, namespace);
|
|
1437
|
+
return aug;
|
|
1261
1438
|
}, [i18n, defaultNamespace]);
|
|
1262
1439
|
(0, import_react2.useEffect)(() => {
|
|
1263
1440
|
keyRegistry._set(tokenRef.current, renderedRef.current);
|
|
@@ -1310,6 +1487,7 @@ function splitOnComponents(text, components) {
|
|
|
1310
1487
|
}
|
|
1311
1488
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1312
1489
|
0 && (module.exports = {
|
|
1490
|
+
A11Y_SURFACES,
|
|
1313
1491
|
DEFAULT_SURFACE_BREAKPOINTS,
|
|
1314
1492
|
SonentaProvider,
|
|
1315
1493
|
Trans,
|