@sonenta/react-i18next 2.1.0 → 2.3.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 +78 -0
- package/dist/index.cjs +270 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +140 -2
- package/dist/index.d.ts +140 -2
- package/dist/index.js +268 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -497,6 +497,84 @@ 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
|
+
|
|
543
|
+
## Available languages (runtime switcher)
|
|
544
|
+
|
|
545
|
+
List the languages **published for the active version** at runtime — so adding
|
|
546
|
+
a language in Sonenta makes it appear in your switcher **without recompiling
|
|
547
|
+
the app**. The set comes from a per-version CDN manifest (public, cacheable,
|
|
548
|
+
no auth); each code is enriched from the language catalog (`native_name`,
|
|
549
|
+
`rtl`, …).
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
import { useTranslation, useAvailableLanguages } from "@sonenta/react-i18next";
|
|
553
|
+
|
|
554
|
+
function LanguageSwitcher() {
|
|
555
|
+
const { i18n } = useTranslation();
|
|
556
|
+
const languages = useAvailableLanguages(); // [{ code, native_name, rtl, is_default, published_at? }]
|
|
557
|
+
return (
|
|
558
|
+
<select value={i18n.language} onChange={(e) => i18n.setLocale(e.target.value)}>
|
|
559
|
+
{languages.map((l) => (
|
|
560
|
+
<option key={l.code} value={l.code}>
|
|
561
|
+
{l.native_name ?? l.code}
|
|
562
|
+
</option>
|
|
563
|
+
))}
|
|
564
|
+
</select>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
- A newly-published language appears on the **next load** (the manifest is
|
|
570
|
+
CDN-served), or immediately after `i18n.reload()` — no rebuild, no redeploy
|
|
571
|
+
of your app.
|
|
572
|
+
- `is_default` marks the project's default locale; `published_at` (when the
|
|
573
|
+
manifest provides it) lets you flag recently-added languages.
|
|
574
|
+
- Falls back to `defaultLocale` + `fallbackLng` when no manifest is available
|
|
575
|
+
(or `env: "dev"`, which is CDN-only). Opt out with `disableLanguageManifest`.
|
|
576
|
+
- Also imperative: `i18n.availableLanguages`.
|
|
577
|
+
|
|
500
578
|
## Recipes
|
|
501
579
|
|
|
502
580
|
### 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,
|
|
@@ -31,6 +32,7 @@ __export(index_exports, {
|
|
|
31
32
|
logTransport: () => logTransport,
|
|
32
33
|
surfaceForWidth: () => surfaceForWidth,
|
|
33
34
|
t: () => t,
|
|
35
|
+
useAvailableLanguages: () => useAvailableLanguages,
|
|
34
36
|
useTranslation: () => useTranslation
|
|
35
37
|
});
|
|
36
38
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -236,7 +238,7 @@ function flattenPlurals(tree, locale) {
|
|
|
236
238
|
|
|
237
239
|
// src/transport.ts
|
|
238
240
|
var SDK_LIB = "@sonenta/react-i18next";
|
|
239
|
-
var SDK_VER = true ? "2.
|
|
241
|
+
var SDK_VER = true ? "2.3.0" : "0.0.0-dev";
|
|
240
242
|
function defaultTransport(opts) {
|
|
241
243
|
return async (batch) => {
|
|
242
244
|
if (!batch.length) return;
|
|
@@ -428,6 +430,14 @@ var SonentaI18n = class {
|
|
|
428
430
|
// config.languageCatalog and/or the public GET /v1/languages in start().
|
|
429
431
|
_catalog = new LanguageCatalog();
|
|
430
432
|
_catalogDisabled = false;
|
|
433
|
+
// Published-languages manifest (available-languages feature). Per-version,
|
|
434
|
+
// public CDN, no auth: the SET of locales published for the active version.
|
|
435
|
+
// Codes are enriched from `_catalog` (#803) for native_name/rtl/etc.
|
|
436
|
+
_manifestDisabled = false;
|
|
437
|
+
_manifest = null;
|
|
438
|
+
// The configured default locale (the initial `this.locale`, which is mutable
|
|
439
|
+
// via setLocale) — used as the available-languages fallback + default marker.
|
|
440
|
+
_defaultLocale;
|
|
431
441
|
_config;
|
|
432
442
|
// Surface variant (#911). When set, each loaded (locale,ns) is composed as
|
|
433
443
|
// base ⊕ sparse `{ns}.{surface}.json` overlay (overlay wins per key). We
|
|
@@ -442,6 +452,14 @@ var SonentaI18n = class {
|
|
|
442
452
|
// Resolved asset refs (#911 minimal v1), keyed `${locale}/${ns}/${keyPath}`
|
|
443
453
|
// for the CURRENT composition (base assets, then overlay assets override).
|
|
444
454
|
_assets = /* @__PURE__ */ new Map();
|
|
455
|
+
// A11y surfaces (#989 / task 994). Each configured a11y surface S is loaded
|
|
456
|
+
// as its OWN i18next namespace `${ns}__${S}` (sparse `{ns}.{S}.json`), so the
|
|
457
|
+
// accessors resolve through i18next's native locale-fallback + nested-key
|
|
458
|
+
// logic without touching the visible-text bundle. `_a11yAssets` carries the
|
|
459
|
+
// `alt_text` `$asset` refs, keyed `${locale}/${ns}/${keyPath}#${surface}`.
|
|
460
|
+
_a11ySurfaces = /* @__PURE__ */ new Set();
|
|
461
|
+
_plainLanguage = false;
|
|
462
|
+
_a11yAssets = /* @__PURE__ */ new Map();
|
|
445
463
|
_missing;
|
|
446
464
|
_listeners = /* @__PURE__ */ new Set();
|
|
447
465
|
// Stable snapshot reference for useSyncExternalStore. Rebuilt ONLY in _notify
|
|
@@ -467,8 +485,12 @@ var SonentaI18n = class {
|
|
|
467
485
|
);
|
|
468
486
|
}
|
|
469
487
|
this.locale = config.defaultLocale;
|
|
488
|
+
this._defaultLocale = config.defaultLocale;
|
|
470
489
|
this.fallbackLng = config.fallbackLng;
|
|
471
490
|
this._surface = config.surface;
|
|
491
|
+
for (const s of config.a11ySurfaces ?? []) this._a11ySurfaces.add(s);
|
|
492
|
+
this._plainLanguage = config.plainLanguage === true;
|
|
493
|
+
if (this._plainLanguage) this._a11ySurfaces.add("plain_language");
|
|
472
494
|
let keySeparator = ".";
|
|
473
495
|
if (config.keySeparator !== void 0) {
|
|
474
496
|
keySeparator = config.keySeparator;
|
|
@@ -566,6 +588,7 @@ var SonentaI18n = class {
|
|
|
566
588
|
this._rebindFormat();
|
|
567
589
|
this._catalogDisabled = config.disableLanguageCatalog === true;
|
|
568
590
|
this._catalog.merge(config.languageCatalog);
|
|
591
|
+
this._manifestDisabled = config.disableLanguageManifest === true;
|
|
569
592
|
const active = config.initialBundles?.[this.locale];
|
|
570
593
|
if (active && this._config.namespaces.every(
|
|
571
594
|
(n) => active[n] && Object.keys(active[n]).length > 0
|
|
@@ -653,9 +676,16 @@ var SonentaI18n = class {
|
|
|
653
676
|
surface: this._surface,
|
|
654
677
|
setSurface: this.setSurface,
|
|
655
678
|
asset: this.asset,
|
|
679
|
+
aria: this.aria,
|
|
680
|
+
alt: this.alt,
|
|
681
|
+
a11y: this.a11y,
|
|
682
|
+
a11yAsset: this.a11yAsset,
|
|
683
|
+
plainLanguage: this._plainLanguage,
|
|
684
|
+
setPlainLanguage: this.setPlainLanguage,
|
|
656
685
|
dir: this.dir,
|
|
657
686
|
nativeName: this.nativeName,
|
|
658
687
|
languageMeta: this.languageMeta,
|
|
688
|
+
availableLanguages: this.availableLanguages,
|
|
659
689
|
i18next: this._i18next
|
|
660
690
|
};
|
|
661
691
|
}
|
|
@@ -717,7 +747,9 @@ var SonentaI18n = class {
|
|
|
717
747
|
// Best-effort: align the key separator with the version's key_style (#754).
|
|
718
748
|
this._loadKeyStyle(fetchImpl),
|
|
719
749
|
// Best-effort: load the public language catalog for dir()/nativeName().
|
|
720
|
-
this._loadCatalog(fetchImpl)
|
|
750
|
+
this._loadCatalog(fetchImpl),
|
|
751
|
+
// Best-effort: load the per-version published-languages manifest.
|
|
752
|
+
this._loadManifest(fetchImpl)
|
|
721
753
|
]);
|
|
722
754
|
await this._syncLanguage();
|
|
723
755
|
this.ready = true;
|
|
@@ -741,6 +773,71 @@ var SonentaI18n = class {
|
|
|
741
773
|
}
|
|
742
774
|
/** Best-effort: fetch the PUBLIC language catalog and merge it in (#803).
|
|
743
775
|
* Skipped when disabled; a failure keeps any embedded catalog. */
|
|
776
|
+
/**
|
|
777
|
+
* Best-effort: fetch the per-version published-languages manifest
|
|
778
|
+
* (`{cdnBase}/p/{project}/{version}/latest/languages.json`, public, no auth,
|
|
779
|
+
* CDN-cached) — the SET of locales published for the active version. Codes
|
|
780
|
+
* only; enriched from the catalog. A 404/offline keeps any prior manifest
|
|
781
|
+
* (`availableLanguages` then falls back to default + fallback). CDN-only:
|
|
782
|
+
* `env: "dev"` skips it (the dev runtime has no manifest yet).
|
|
783
|
+
*/
|
|
784
|
+
async _loadManifest(fetchImpl, opts = {}) {
|
|
785
|
+
if (this._manifestDisabled) return;
|
|
786
|
+
if (this._config.env === "dev") return;
|
|
787
|
+
const url = `${this._config.cdnBase.replace(/\/+$/, "")}/p/${this._config.projectUuid}/${this._config.version}/latest/languages.json`;
|
|
788
|
+
const init = { method: "GET", credentials: "omit" };
|
|
789
|
+
if (opts.bust) init.cache = "reload";
|
|
790
|
+
try {
|
|
791
|
+
const r = await fetchImpl(url, init);
|
|
792
|
+
if (!r.ok) return;
|
|
793
|
+
const data = await r.json();
|
|
794
|
+
const raw = Array.isArray(data.languages) ? data.languages : [];
|
|
795
|
+
const languages = [];
|
|
796
|
+
for (const e of raw) {
|
|
797
|
+
if (typeof e === "string") {
|
|
798
|
+
languages.push({ code: e });
|
|
799
|
+
} else if (e && typeof e === "object" && typeof e.code === "string") {
|
|
800
|
+
const obj = e;
|
|
801
|
+
languages.push({
|
|
802
|
+
code: obj.code,
|
|
803
|
+
published_at: typeof obj.published_at === "string" ? obj.published_at : void 0
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
this._manifest = {
|
|
808
|
+
default_locale: typeof data.default_locale === "string" ? data.default_locale : void 0,
|
|
809
|
+
languages
|
|
810
|
+
};
|
|
811
|
+
} catch {
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/** Languages PUBLISHED for the active version (available-languages feature):
|
|
815
|
+
* the manifest's codes enriched with catalog metadata, marked with the
|
|
816
|
+
* default locale + any `published_at`. Falls back to the configured
|
|
817
|
+
* `defaultLocale` + `fallbackLng` when no manifest is available. */
|
|
818
|
+
get availableLanguages() {
|
|
819
|
+
const def = this._manifest?.default_locale ?? this._defaultLocale;
|
|
820
|
+
let entries = this._manifest?.languages;
|
|
821
|
+
if (!entries || entries.length === 0) {
|
|
822
|
+
const seen = /* @__PURE__ */ new Set();
|
|
823
|
+
entries = [];
|
|
824
|
+
for (const c of [this._defaultLocale, ...asArray(this.fallbackLng)]) {
|
|
825
|
+
if (c && !seen.has(c)) {
|
|
826
|
+
seen.add(c);
|
|
827
|
+
entries.push({ code: c });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return entries.map(({ code, published_at }) => {
|
|
832
|
+
const meta = this.languageMeta(code);
|
|
833
|
+
return {
|
|
834
|
+
...meta ?? {},
|
|
835
|
+
code,
|
|
836
|
+
...published_at ? { published_at } : {},
|
|
837
|
+
is_default: code === def
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
744
841
|
async _loadCatalog(fetchImpl) {
|
|
745
842
|
if (this._catalogDisabled) return;
|
|
746
843
|
this._catalog.merge(await loadCatalog(this._config.apiBase, fetchImpl));
|
|
@@ -831,12 +928,14 @@ var SonentaI18n = class {
|
|
|
831
928
|
if (opts.namespace && opts.namespace !== ns) continue;
|
|
832
929
|
targets.push({ locale, ns });
|
|
833
930
|
}
|
|
834
|
-
if (targets.length === 0) return;
|
|
835
|
-
|
|
836
|
-
|
|
931
|
+
if (targets.length === 0 && (opts.locale || opts.namespace)) return;
|
|
932
|
+
const manifest = this._loadManifest(fetch, { bust: true });
|
|
933
|
+
await Promise.all([
|
|
934
|
+
manifest,
|
|
935
|
+
...targets.map(
|
|
837
936
|
(t2) => this._loadBundle(t2.locale, t2.ns, fetch, { bust: true })
|
|
838
937
|
)
|
|
839
|
-
);
|
|
938
|
+
]);
|
|
840
939
|
this._notify();
|
|
841
940
|
};
|
|
842
941
|
/**
|
|
@@ -894,9 +993,96 @@ var SonentaI18n = class {
|
|
|
894
993
|
}
|
|
895
994
|
return void 0;
|
|
896
995
|
};
|
|
996
|
+
// ---- A11y (#989 / task 994) ----
|
|
997
|
+
/** Split a possibly-`ns:key` string into `{ ns, bareKey }`, honoring the
|
|
998
|
+
* configured nsSeparator (mirrors {@link asset}). */
|
|
999
|
+
_splitKey(key, namespace) {
|
|
1000
|
+
let ns = namespace ?? this.defaultNamespace;
|
|
1001
|
+
let bareKey = key;
|
|
1002
|
+
if (typeof this._nsSeparator === "string" && this._nsSeparator) {
|
|
1003
|
+
const idx = key.indexOf(this._nsSeparator);
|
|
1004
|
+
if (idx > 0) {
|
|
1005
|
+
ns = key.slice(0, idx);
|
|
1006
|
+
bareKey = key.slice(idx + this._nsSeparator.length);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return { ns, bareKey };
|
|
1010
|
+
}
|
|
1011
|
+
/** Resolve an a11y `surface` override for `key`, or `undefined` when none
|
|
1012
|
+
* exists (the namespace is sparse). Goes through i18next so the locale
|
|
1013
|
+
* fallback chain + nested keys + plurals apply. */
|
|
1014
|
+
a11y = (key, surface, namespace) => {
|
|
1015
|
+
if (!this._a11ySurfaces.has(surface)) return void 0;
|
|
1016
|
+
const { ns, bareKey } = this._splitKey(key, namespace);
|
|
1017
|
+
const a11yNs = this._a11yNs(ns, surface);
|
|
1018
|
+
if (!this._i18next.exists(bareKey, { ns: a11yNs })) return void 0;
|
|
1019
|
+
return this._i18next.t(bareKey, { ns: a11yNs });
|
|
1020
|
+
};
|
|
1021
|
+
/** Accessible name for `key` (`aria_label` overlay) — falls back to the
|
|
1022
|
+
* visible `t(key)` text when no override exists. Spread as `aria-label`. */
|
|
1023
|
+
aria = (key, namespace) => {
|
|
1024
|
+
const v = this.a11y(key, "aria_label", namespace);
|
|
1025
|
+
if (v !== void 0) return v;
|
|
1026
|
+
const { ns, bareKey } = this._splitKey(key, namespace);
|
|
1027
|
+
return this._i18next.t(bareKey, { ns });
|
|
1028
|
+
};
|
|
1029
|
+
/** Image alt text for `key` (`alt_text` overlay) — falls back to the visible
|
|
1030
|
+
* `t(key)` text. Pair with {@link a11yAsset} for a localized image. */
|
|
1031
|
+
alt = (key, namespace) => {
|
|
1032
|
+
const v = this.a11y(key, "alt_text", namespace);
|
|
1033
|
+
if (v !== void 0) return v;
|
|
1034
|
+
const { ns, bareKey } = this._splitKey(key, namespace);
|
|
1035
|
+
return this._i18next.t(bareKey, { ns });
|
|
1036
|
+
};
|
|
1037
|
+
/** Localized-image `$asset` ref from a key's `alt_text` overlay, or
|
|
1038
|
+
* `undefined`. Walks the locale fallback chain like `t()` would. */
|
|
1039
|
+
a11yAsset = (key, namespace) => {
|
|
1040
|
+
const { ns, bareKey } = this._splitKey(key, namespace);
|
|
1041
|
+
for (const loc of this._resolutionChain(this.locale)) {
|
|
1042
|
+
const ref = this._a11yAssets.get(`${loc}/${ns}/${bareKey}#alt_text`);
|
|
1043
|
+
if (ref) return ref;
|
|
1044
|
+
}
|
|
1045
|
+
return void 0;
|
|
1046
|
+
};
|
|
1047
|
+
get plainLanguage() {
|
|
1048
|
+
return this._plainLanguage;
|
|
1049
|
+
}
|
|
1050
|
+
/** Toggle simplified-language ("plain language" / FALC) mode (#989). When
|
|
1051
|
+
* on, `t()` returns the `plain_language` overlay value for keys that have
|
|
1052
|
+
* one. Loads the `plain_language` overlays on first enable if they were not
|
|
1053
|
+
* configured, then re-renders. No-op when unchanged. */
|
|
1054
|
+
setPlainLanguage = async (on) => {
|
|
1055
|
+
if (on === this._plainLanguage) return;
|
|
1056
|
+
this._plainLanguage = on;
|
|
1057
|
+
if (on && !this._a11ySurfaces.has("plain_language")) {
|
|
1058
|
+
this._a11ySurfaces.add("plain_language");
|
|
1059
|
+
const targets = [];
|
|
1060
|
+
for (const k of this._attempted) {
|
|
1061
|
+
const parts = k.split("/");
|
|
1062
|
+
const locale = parts[1];
|
|
1063
|
+
const ns = parts[2];
|
|
1064
|
+
if (locale && ns) targets.push({ locale, ns });
|
|
1065
|
+
}
|
|
1066
|
+
await Promise.all(
|
|
1067
|
+
targets.map((t2) => this._loadA11yOverlays(t2.locale, t2.ns))
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
this._notify();
|
|
1071
|
+
this._signalLoaded();
|
|
1072
|
+
};
|
|
897
1073
|
// ---- Translation ----
|
|
898
1074
|
t = (key, optionsOrDefault, maybeOptions) => {
|
|
899
1075
|
const options = typeof optionsOrDefault === "string" ? { ...maybeOptions ?? {}, defaultValue: optionsOrDefault } : optionsOrDefault;
|
|
1076
|
+
if (this._plainLanguage && this._a11ySurfaces.has("plain_language")) {
|
|
1077
|
+
const { ns, bareKey } = this._splitKey(key);
|
|
1078
|
+
const plainNs = this._a11yNs(ns, "plain_language");
|
|
1079
|
+
if (this._i18next.exists(bareKey, { ...options ?? {}, ns: plainNs })) {
|
|
1080
|
+
return this._i18next.t(bareKey, {
|
|
1081
|
+
...options ?? {},
|
|
1082
|
+
ns: plainNs
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
900
1086
|
const literal = this._probeLiteral(key);
|
|
901
1087
|
if (literal !== void 0) {
|
|
902
1088
|
const interpolator = this._i18next.services?.interpolator;
|
|
@@ -1047,6 +1233,67 @@ var SonentaI18n = class {
|
|
|
1047
1233
|
this._attempted.add(cacheKey);
|
|
1048
1234
|
}
|
|
1049
1235
|
await this._composeBundle(locale, ns, fetchImpl, opts.bust);
|
|
1236
|
+
await this._loadA11yOverlays(locale, ns, fetchImpl, opts.bust);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Load every configured a11y surface overlay (#989 / task 994) for
|
|
1240
|
+
* (locale, ns) into a DEDICATED i18next namespace `${ns}__${surface}`, so
|
|
1241
|
+
* `aria()` / `alt()` / `a11y()` resolve them through i18next (locale
|
|
1242
|
+
* fallback + nested keys + plurals) WITHOUT polluting the visible-text
|
|
1243
|
+
* bundle. Sparse + best-effort: an absent overlay registers `{}` and the
|
|
1244
|
+
* accessor reports "no override".
|
|
1245
|
+
*/
|
|
1246
|
+
async _loadA11yOverlays(locale, ns, fetchImpl = fetch, bust = false) {
|
|
1247
|
+
if (this._a11ySurfaces.size === 0) return;
|
|
1248
|
+
await Promise.all(
|
|
1249
|
+
[...this._a11ySurfaces].map(async (surface) => {
|
|
1250
|
+
const overlay = await this._loadOverlay(
|
|
1251
|
+
locale,
|
|
1252
|
+
ns,
|
|
1253
|
+
surface,
|
|
1254
|
+
fetchImpl,
|
|
1255
|
+
bust
|
|
1256
|
+
);
|
|
1257
|
+
const tree = this._unwrapA11y(overlay, locale, ns, surface);
|
|
1258
|
+
this._i18next.addResourceBundle(
|
|
1259
|
+
locale,
|
|
1260
|
+
this._a11yNs(ns, surface),
|
|
1261
|
+
flattenPlurals(tree, locale),
|
|
1262
|
+
false,
|
|
1263
|
+
true
|
|
1264
|
+
);
|
|
1265
|
+
})
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
/** i18next namespace that backs an a11y surface overlay for `ns`. */
|
|
1269
|
+
_a11yNs(ns, surface) {
|
|
1270
|
+
return `${ns}__${surface}`;
|
|
1271
|
+
}
|
|
1272
|
+
/** Like {@link _unwrapAssets} but records `$asset` refs into `_a11yAssets`
|
|
1273
|
+
* (keyed with the `#${surface}` suffix) instead of the visible-text
|
|
1274
|
+
* `_assets`, so `alt_text` images don't collide with device-surface assets. */
|
|
1275
|
+
_unwrapA11y(tree, locale, ns, surface) {
|
|
1276
|
+
const sep = typeof this._i18next.options.keySeparator === "string" ? this._i18next.options.keySeparator : ".";
|
|
1277
|
+
const walk = (node, path) => {
|
|
1278
|
+
if (!node || typeof node !== "object") return node;
|
|
1279
|
+
const obj = node;
|
|
1280
|
+
if (Object.prototype.hasOwnProperty.call(obj, "$value")) {
|
|
1281
|
+
const a = obj.$asset;
|
|
1282
|
+
if (a && typeof a.kind === "string" && typeof a.ref === "string") {
|
|
1283
|
+
this._a11yAssets.set(
|
|
1284
|
+
`${locale}/${ns}/${path.join(sep)}#${surface}`,
|
|
1285
|
+
{ kind: a.kind, ref: a.ref }
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
return obj.$value;
|
|
1289
|
+
}
|
|
1290
|
+
const out = {};
|
|
1291
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1292
|
+
out[k] = walk(v, [...path, k]);
|
|
1293
|
+
}
|
|
1294
|
+
return out;
|
|
1295
|
+
};
|
|
1296
|
+
return walk(tree, []);
|
|
1050
1297
|
}
|
|
1051
1298
|
/**
|
|
1052
1299
|
* Compose the i18next bundle for (locale, ns) as base ⊕ surface overlay
|
|
@@ -1174,6 +1421,12 @@ function t(key, optionsOrDefault, maybeOptions) {
|
|
|
1174
1421
|
}
|
|
1175
1422
|
|
|
1176
1423
|
// src/surface.ts
|
|
1424
|
+
var A11Y_SURFACES = [
|
|
1425
|
+
"aria_label",
|
|
1426
|
+
"alt_text",
|
|
1427
|
+
"screen_reader",
|
|
1428
|
+
"plain_language"
|
|
1429
|
+
];
|
|
1177
1430
|
var DEFAULT_SURFACE_BREAKPOINTS = {
|
|
1178
1431
|
mobile: 640,
|
|
1179
1432
|
tablet: 1024
|
|
@@ -1257,7 +1510,12 @@ function useTranslation(defaultNamespace) {
|
|
|
1257
1510
|
);
|
|
1258
1511
|
return i18n.t(fullKey, optionsOrDefault, maybeOptions);
|
|
1259
1512
|
};
|
|
1260
|
-
|
|
1513
|
+
const withNs = (key) => defaultNamespace && !key.includes(":") ? `${defaultNamespace}:${key}` : key;
|
|
1514
|
+
const aug = fn;
|
|
1515
|
+
aug.aria = (key, namespace) => i18n.aria(withNs(key), namespace);
|
|
1516
|
+
aug.alt = (key, namespace) => i18n.alt(withNs(key), namespace);
|
|
1517
|
+
aug.a11y = (key, surface, namespace) => i18n.a11y(withNs(key), surface, namespace);
|
|
1518
|
+
return aug;
|
|
1261
1519
|
}, [i18n, defaultNamespace]);
|
|
1262
1520
|
(0, import_react2.useEffect)(() => {
|
|
1263
1521
|
keyRegistry._set(tokenRef.current, renderedRef.current);
|
|
@@ -1268,6 +1526,9 @@ function useTranslation(defaultNamespace) {
|
|
|
1268
1526
|
}, []);
|
|
1269
1527
|
return { t: t2, i18n: snapshot };
|
|
1270
1528
|
}
|
|
1529
|
+
function useAvailableLanguages() {
|
|
1530
|
+
return useI18nSnapshot().availableLanguages;
|
|
1531
|
+
}
|
|
1271
1532
|
|
|
1272
1533
|
// src/trans.tsx
|
|
1273
1534
|
var import_react3 = require("react");
|
|
@@ -1310,6 +1571,7 @@ function splitOnComponents(text, components) {
|
|
|
1310
1571
|
}
|
|
1311
1572
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1312
1573
|
0 && (module.exports = {
|
|
1574
|
+
A11Y_SURFACES,
|
|
1313
1575
|
DEFAULT_SURFACE_BREAKPOINTS,
|
|
1314
1576
|
SonentaProvider,
|
|
1315
1577
|
Trans,
|
|
@@ -1321,6 +1583,7 @@ function splitOnComponents(text, components) {
|
|
|
1321
1583
|
logTransport,
|
|
1322
1584
|
surfaceForWidth,
|
|
1323
1585
|
t,
|
|
1586
|
+
useAvailableLanguages,
|
|
1324
1587
|
useTranslation
|
|
1325
1588
|
});
|
|
1326
1589
|
//# sourceMappingURL=index.cjs.map
|