@sonenta/react-i18next 2.2.0 → 2.3.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/dist/index.d.cts CHANGED
@@ -83,6 +83,20 @@ interface LanguageMeta {
83
83
  /** CLDR plural categories present for this language. */
84
84
  plural_categories?: string[];
85
85
  }
86
+ /**
87
+ * A language PUBLISHED for the active version (task: available-languages) — a
88
+ * {@link LanguageMeta} (enriched from the catalog) plus publish info from the
89
+ * per-version CDN manifest. Returned by `i18n.availableLanguages` /
90
+ * `useAvailableLanguages()`.
91
+ */
92
+ interface AvailableLanguage extends LanguageMeta {
93
+ /** ISO timestamp this language was published for the active version, when
94
+ * the manifest provides it (optional / forward-compat). Render a
95
+ * "recently added" hint if present; ignore if absent. */
96
+ published_at?: string;
97
+ /** True when this is the project's default locale for the active version. */
98
+ is_default?: boolean;
99
+ }
86
100
  interface MissingKeyEvent {
87
101
  key: string;
88
102
  namespace: Namespace;
@@ -162,6 +176,14 @@ interface SonentaConfig {
162
176
  * you supply {@link SonentaConfig.languageCatalog} yourself.
163
177
  */
164
178
  disableLanguageCatalog?: boolean;
179
+ /**
180
+ * Skip the best-effort fetch of the per-version published-languages manifest
181
+ * (`{apiBase}/cdn/v1/{project}/{version}/latest/languages.json`, public, no
182
+ * auth, CORS-open) that powers `i18n.availableLanguages` /
183
+ * `useAvailableLanguages()`. When skipped (or on a 404), `availableLanguages`
184
+ * falls back to the configured `defaultLocale` + `fallbackLng`.
185
+ */
186
+ disableLanguageManifest?: boolean;
165
187
  /**
166
188
  * Interpolation hooks forwarded to i18next. Currently exposes `format`, the
167
189
  * value formatter i18next invokes for `{{value, format}}` placeholders — wire
@@ -434,6 +456,15 @@ interface I18nInstance {
434
456
  * parent, …), defaulting to the active locale; `undefined` if unknown.
435
457
  */
436
458
  languageMeta: (lng?: Locale) => LanguageMeta | undefined;
459
+ /**
460
+ * The languages PUBLISHED for the active version (from the per-version CDN
461
+ * manifest, enriched with catalog metadata) — drives a runtime language
462
+ * switcher that updates when a new language ships WITHOUT recompiling the
463
+ * app. Falls back to `defaultLocale` + `fallbackLng` when no manifest is
464
+ * available. Refreshed on `reload()`. Prefer the `useAvailableLanguages()`
465
+ * hook in React (reactive).
466
+ */
467
+ availableLanguages: AvailableLanguage[];
437
468
  /**
438
469
  * The underlying real `i18next` instance powering this SDK (thin-wrapper,
439
470
  * #805). Exposed for react-i18next DROP-IN: pass it to react-i18next's own
@@ -490,6 +521,25 @@ interface UseTranslationResult {
490
521
  * contribution is keyed to THIS hook instance and dropped on unmount, so
491
522
  * navigating away removes its keys automatically. */
492
523
  declare function useTranslation(defaultNamespace?: string): UseTranslationResult;
524
+ /**
525
+ * React hook — the languages PUBLISHED for the active version, enriched with
526
+ * catalog metadata (native_name / rtl / …). Drives a runtime language switcher
527
+ * that picks up a newly-published language WITHOUT recompiling the app: the
528
+ * per-version manifest is CDN-served, so the new language appears on the next
529
+ * load (or after `i18n.reload()`). Falls back to `defaultLocale` +
530
+ * `fallbackLng` when no manifest is available.
531
+ *
532
+ * ```tsx
533
+ * const langs = useAvailableLanguages();
534
+ * const { i18n } = useTranslation();
535
+ * return langs.map((l) => (
536
+ * <button key={l.code} onClick={() => i18n.setLocale(l.code)}>
537
+ * {l.native_name ?? l.code}{l.is_default ? " ★" : ""}
538
+ * </button>
539
+ * ));
540
+ * ```
541
+ */
542
+ declare function useAvailableLanguages(): AvailableLanguage[];
493
543
 
494
544
  interface TransProps {
495
545
  /** The translation key (optionally `ns:key`). */
@@ -625,4 +675,4 @@ declare class KeyRegistry {
625
675
  /** Process-wide singleton — there is exactly one on-screen registry. */
626
676
  declare const keyRegistry: KeyRegistry;
627
677
 
628
- export { A11Y_SURFACES, type A11ySurface, type AssetRef, DEFAULT_SURFACE_BREAKPOINTS, type DeclaredKey, type I18nInstance, type LanguageMeta, type Locale, type MissingHandlerMode, type MissingKeyEvent, type Namespace, type SonentaConfig, type SonentaPlugin, type SonentaPluginContext, SonentaProvider, type Surface, type SurfaceBreakpoints, Trans, type TranslationFunction, type TranslationOptions, type Transport, type SonentaConfig as VerbumiaConfig, type SonentaPlugin as VerbumiaPlugin, type SonentaPluginContext as VerbumiaPluginContext, SonentaProvider as VerbumiaProvider, defaultTransport, getI18n, getI18nSafe, keyRegistry, logTransport, surfaceForWidth, t, useTranslation };
678
+ export { A11Y_SURFACES, type A11ySurface, type A11yTranslationFunction, type AssetRef, type AvailableLanguage, DEFAULT_SURFACE_BREAKPOINTS, type DeclaredKey, type I18nInstance, type LanguageMeta, type Locale, type MissingHandlerMode, type MissingKeyEvent, type Namespace, type SonentaConfig, type SonentaPlugin, type SonentaPluginContext, SonentaProvider, type Surface, type SurfaceBreakpoints, Trans, type TranslationFunction, type TranslationOptions, type Transport, type SonentaConfig as VerbumiaConfig, type SonentaPlugin as VerbumiaPlugin, type SonentaPluginContext as VerbumiaPluginContext, SonentaProvider as VerbumiaProvider, defaultTransport, getI18n, getI18nSafe, keyRegistry, logTransport, surfaceForWidth, t, useAvailableLanguages, useTranslation };
package/dist/index.d.ts CHANGED
@@ -83,6 +83,20 @@ interface LanguageMeta {
83
83
  /** CLDR plural categories present for this language. */
84
84
  plural_categories?: string[];
85
85
  }
86
+ /**
87
+ * A language PUBLISHED for the active version (task: available-languages) — a
88
+ * {@link LanguageMeta} (enriched from the catalog) plus publish info from the
89
+ * per-version CDN manifest. Returned by `i18n.availableLanguages` /
90
+ * `useAvailableLanguages()`.
91
+ */
92
+ interface AvailableLanguage extends LanguageMeta {
93
+ /** ISO timestamp this language was published for the active version, when
94
+ * the manifest provides it (optional / forward-compat). Render a
95
+ * "recently added" hint if present; ignore if absent. */
96
+ published_at?: string;
97
+ /** True when this is the project's default locale for the active version. */
98
+ is_default?: boolean;
99
+ }
86
100
  interface MissingKeyEvent {
87
101
  key: string;
88
102
  namespace: Namespace;
@@ -162,6 +176,14 @@ interface SonentaConfig {
162
176
  * you supply {@link SonentaConfig.languageCatalog} yourself.
163
177
  */
164
178
  disableLanguageCatalog?: boolean;
179
+ /**
180
+ * Skip the best-effort fetch of the per-version published-languages manifest
181
+ * (`{apiBase}/cdn/v1/{project}/{version}/latest/languages.json`, public, no
182
+ * auth, CORS-open) that powers `i18n.availableLanguages` /
183
+ * `useAvailableLanguages()`. When skipped (or on a 404), `availableLanguages`
184
+ * falls back to the configured `defaultLocale` + `fallbackLng`.
185
+ */
186
+ disableLanguageManifest?: boolean;
165
187
  /**
166
188
  * Interpolation hooks forwarded to i18next. Currently exposes `format`, the
167
189
  * value formatter i18next invokes for `{{value, format}}` placeholders — wire
@@ -434,6 +456,15 @@ interface I18nInstance {
434
456
  * parent, …), defaulting to the active locale; `undefined` if unknown.
435
457
  */
436
458
  languageMeta: (lng?: Locale) => LanguageMeta | undefined;
459
+ /**
460
+ * The languages PUBLISHED for the active version (from the per-version CDN
461
+ * manifest, enriched with catalog metadata) — drives a runtime language
462
+ * switcher that updates when a new language ships WITHOUT recompiling the
463
+ * app. Falls back to `defaultLocale` + `fallbackLng` when no manifest is
464
+ * available. Refreshed on `reload()`. Prefer the `useAvailableLanguages()`
465
+ * hook in React (reactive).
466
+ */
467
+ availableLanguages: AvailableLanguage[];
437
468
  /**
438
469
  * The underlying real `i18next` instance powering this SDK (thin-wrapper,
439
470
  * #805). Exposed for react-i18next DROP-IN: pass it to react-i18next's own
@@ -490,6 +521,25 @@ interface UseTranslationResult {
490
521
  * contribution is keyed to THIS hook instance and dropped on unmount, so
491
522
  * navigating away removes its keys automatically. */
492
523
  declare function useTranslation(defaultNamespace?: string): UseTranslationResult;
524
+ /**
525
+ * React hook — the languages PUBLISHED for the active version, enriched with
526
+ * catalog metadata (native_name / rtl / …). Drives a runtime language switcher
527
+ * that picks up a newly-published language WITHOUT recompiling the app: the
528
+ * per-version manifest is CDN-served, so the new language appears on the next
529
+ * load (or after `i18n.reload()`). Falls back to `defaultLocale` +
530
+ * `fallbackLng` when no manifest is available.
531
+ *
532
+ * ```tsx
533
+ * const langs = useAvailableLanguages();
534
+ * const { i18n } = useTranslation();
535
+ * return langs.map((l) => (
536
+ * <button key={l.code} onClick={() => i18n.setLocale(l.code)}>
537
+ * {l.native_name ?? l.code}{l.is_default ? " ★" : ""}
538
+ * </button>
539
+ * ));
540
+ * ```
541
+ */
542
+ declare function useAvailableLanguages(): AvailableLanguage[];
493
543
 
494
544
  interface TransProps {
495
545
  /** The translation key (optionally `ns:key`). */
@@ -625,4 +675,4 @@ declare class KeyRegistry {
625
675
  /** Process-wide singleton — there is exactly one on-screen registry. */
626
676
  declare const keyRegistry: KeyRegistry;
627
677
 
628
- export { A11Y_SURFACES, type A11ySurface, type AssetRef, DEFAULT_SURFACE_BREAKPOINTS, type DeclaredKey, type I18nInstance, type LanguageMeta, type Locale, type MissingHandlerMode, type MissingKeyEvent, type Namespace, type SonentaConfig, type SonentaPlugin, type SonentaPluginContext, SonentaProvider, type Surface, type SurfaceBreakpoints, Trans, type TranslationFunction, type TranslationOptions, type Transport, type SonentaConfig as VerbumiaConfig, type SonentaPlugin as VerbumiaPlugin, type SonentaPluginContext as VerbumiaPluginContext, SonentaProvider as VerbumiaProvider, defaultTransport, getI18n, getI18nSafe, keyRegistry, logTransport, surfaceForWidth, t, useTranslation };
678
+ export { A11Y_SURFACES, type A11ySurface, type A11yTranslationFunction, type AssetRef, type AvailableLanguage, DEFAULT_SURFACE_BREAKPOINTS, type DeclaredKey, type I18nInstance, type LanguageMeta, type Locale, type MissingHandlerMode, type MissingKeyEvent, type Namespace, type SonentaConfig, type SonentaPlugin, type SonentaPluginContext, SonentaProvider, type Surface, type SurfaceBreakpoints, Trans, type TranslationFunction, type TranslationOptions, type Transport, type SonentaConfig as VerbumiaConfig, type SonentaPlugin as VerbumiaPlugin, type SonentaPluginContext as VerbumiaPluginContext, SonentaProvider as VerbumiaProvider, defaultTransport, getI18n, getI18nSafe, keyRegistry, logTransport, surfaceForWidth, t, useAvailableLanguages, useTranslation };
package/dist/index.js CHANGED
@@ -210,7 +210,7 @@ function flattenPlurals(tree, locale) {
210
210
 
211
211
  // src/transport.ts
212
212
  var SDK_LIB = "@sonenta/react-i18next";
213
- var SDK_VER = true ? "2.2.0" : "0.0.0-dev";
213
+ var SDK_VER = true ? "2.3.1" : "0.0.0-dev";
214
214
  function defaultTransport(opts) {
215
215
  return async (batch) => {
216
216
  if (!batch.length) return;
@@ -402,6 +402,14 @@ var SonentaI18n = class {
402
402
  // config.languageCatalog and/or the public GET /v1/languages in start().
403
403
  _catalog = new LanguageCatalog();
404
404
  _catalogDisabled = false;
405
+ // Published-languages manifest (available-languages feature). Per-version,
406
+ // public CDN, no auth: the SET of locales published for the active version.
407
+ // Codes are enriched from `_catalog` (#803) for native_name/rtl/etc.
408
+ _manifestDisabled = false;
409
+ _manifest = null;
410
+ // The configured default locale (the initial `this.locale`, which is mutable
411
+ // via setLocale) — used as the available-languages fallback + default marker.
412
+ _defaultLocale;
405
413
  _config;
406
414
  // Surface variant (#911). When set, each loaded (locale,ns) is composed as
407
415
  // base ⊕ sparse `{ns}.{surface}.json` overlay (overlay wins per key). We
@@ -449,6 +457,7 @@ var SonentaI18n = class {
449
457
  );
450
458
  }
451
459
  this.locale = config.defaultLocale;
460
+ this._defaultLocale = config.defaultLocale;
452
461
  this.fallbackLng = config.fallbackLng;
453
462
  this._surface = config.surface;
454
463
  for (const s of config.a11ySurfaces ?? []) this._a11ySurfaces.add(s);
@@ -551,6 +560,7 @@ var SonentaI18n = class {
551
560
  this._rebindFormat();
552
561
  this._catalogDisabled = config.disableLanguageCatalog === true;
553
562
  this._catalog.merge(config.languageCatalog);
563
+ this._manifestDisabled = config.disableLanguageManifest === true;
554
564
  const active = config.initialBundles?.[this.locale];
555
565
  if (active && this._config.namespaces.every(
556
566
  (n) => active[n] && Object.keys(active[n]).length > 0
@@ -647,6 +657,7 @@ var SonentaI18n = class {
647
657
  dir: this.dir,
648
658
  nativeName: this.nativeName,
649
659
  languageMeta: this.languageMeta,
660
+ availableLanguages: this.availableLanguages,
650
661
  i18next: this._i18next
651
662
  };
652
663
  }
@@ -708,7 +719,9 @@ var SonentaI18n = class {
708
719
  // Best-effort: align the key separator with the version's key_style (#754).
709
720
  this._loadKeyStyle(fetchImpl),
710
721
  // Best-effort: load the public language catalog for dir()/nativeName().
711
- this._loadCatalog(fetchImpl)
722
+ this._loadCatalog(fetchImpl),
723
+ // Best-effort: load the per-version published-languages manifest.
724
+ this._loadManifest(fetchImpl)
712
725
  ]);
713
726
  await this._syncLanguage();
714
727
  this.ready = true;
@@ -732,6 +745,73 @@ var SonentaI18n = class {
732
745
  }
733
746
  /** Best-effort: fetch the PUBLIC language catalog and merge it in (#803).
734
747
  * Skipped when disabled; a failure keeps any embedded catalog. */
748
+ /**
749
+ * Best-effort: fetch the per-version published-languages manifest
750
+ * (`{apiBase}/cdn/v1/{project}/{version}/latest/languages.json`, public, no
751
+ * auth, CORS-open, CDN-cached `max-age=60` + ETag) — the SET of locales
752
+ * published for the active version (compute-on-serve from cdn_releases,
753
+ * always in sync with what is published). Codes only; enriched from the
754
+ * catalog. A 404/offline keeps any prior manifest (`availableLanguages` then
755
+ * falls back to default + fallback). Same route on prod + dev (backend task
756
+ * 989+; the authed `/v1/projects/{id}/languages` is ALL project languages,
757
+ * not the published set — this manifest is the in-production set).
758
+ */
759
+ async _loadManifest(fetchImpl, opts = {}) {
760
+ if (this._manifestDisabled) return;
761
+ const url = `${this._config.apiBase.replace(/\/+$/, "")}/cdn/v1/${this._config.projectUuid}/${this._config.version}/latest/languages.json`;
762
+ const init = { method: "GET", credentials: "omit" };
763
+ if (opts.bust) init.cache = "reload";
764
+ try {
765
+ const r = await fetchImpl(url, init);
766
+ if (!r.ok) return;
767
+ const data = await r.json();
768
+ const raw = Array.isArray(data.languages) ? data.languages : [];
769
+ const languages = [];
770
+ for (const e of raw) {
771
+ if (typeof e === "string") {
772
+ languages.push({ code: e });
773
+ } else if (e && typeof e === "object" && typeof e.code === "string") {
774
+ const obj = e;
775
+ languages.push({
776
+ code: obj.code,
777
+ published_at: typeof obj.published_at === "string" ? obj.published_at : void 0
778
+ });
779
+ }
780
+ }
781
+ this._manifest = {
782
+ default_locale: typeof data.default_locale === "string" ? data.default_locale : void 0,
783
+ languages
784
+ };
785
+ } catch {
786
+ }
787
+ }
788
+ /** Languages PUBLISHED for the active version (available-languages feature):
789
+ * the manifest's codes enriched with catalog metadata, marked with the
790
+ * default locale + any `published_at`. Falls back to the configured
791
+ * `defaultLocale` + `fallbackLng` when no manifest is available. */
792
+ get availableLanguages() {
793
+ const def = this._manifest?.default_locale ?? this._defaultLocale;
794
+ let entries = this._manifest?.languages;
795
+ if (!entries || entries.length === 0) {
796
+ const seen = /* @__PURE__ */ new Set();
797
+ entries = [];
798
+ for (const c of [this._defaultLocale, ...asArray(this.fallbackLng)]) {
799
+ if (c && !seen.has(c)) {
800
+ seen.add(c);
801
+ entries.push({ code: c });
802
+ }
803
+ }
804
+ }
805
+ return entries.map(({ code, published_at }) => {
806
+ const meta = this.languageMeta(code);
807
+ return {
808
+ ...meta ?? {},
809
+ code,
810
+ ...published_at ? { published_at } : {},
811
+ is_default: code === def
812
+ };
813
+ });
814
+ }
735
815
  async _loadCatalog(fetchImpl) {
736
816
  if (this._catalogDisabled) return;
737
817
  this._catalog.merge(await loadCatalog(this._config.apiBase, fetchImpl));
@@ -822,12 +902,14 @@ var SonentaI18n = class {
822
902
  if (opts.namespace && opts.namespace !== ns) continue;
823
903
  targets.push({ locale, ns });
824
904
  }
825
- if (targets.length === 0) return;
826
- await Promise.all(
827
- targets.map(
905
+ if (targets.length === 0 && (opts.locale || opts.namespace)) return;
906
+ const manifest = this._loadManifest(fetch, { bust: true });
907
+ await Promise.all([
908
+ manifest,
909
+ ...targets.map(
828
910
  (t2) => this._loadBundle(t2.locale, t2.ns, fetch, { bust: true })
829
911
  )
830
- );
912
+ ]);
831
913
  this._notify();
832
914
  };
833
915
  /**
@@ -1418,6 +1500,9 @@ function useTranslation(defaultNamespace) {
1418
1500
  }, []);
1419
1501
  return { t: t2, i18n: snapshot };
1420
1502
  }
1503
+ function useAvailableLanguages() {
1504
+ return useI18nSnapshot().availableLanguages;
1505
+ }
1421
1506
 
1422
1507
  // src/trans.tsx
1423
1508
  import { Children, cloneElement, isValidElement } from "react";
@@ -1471,6 +1556,7 @@ export {
1471
1556
  logTransport,
1472
1557
  surfaceForWidth,
1473
1558
  t,
1559
+ useAvailableLanguages,
1474
1560
  useTranslation
1475
1561
  };
1476
1562
  //# sourceMappingURL=index.js.map