@sonenta/react-i18next 2.0.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.
@@ -0,0 +1,540 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { i18n } from 'i18next';
4
+
5
+ /**
6
+ * Surface variants (#911) — a second resolution dimension layered ON TOP of
7
+ * the locale fallback chain. A "surface" (`desktop` / `mobile` / `tablet`) is
8
+ * an additive overlay: the base bundle applies to every surface, and a sparse
9
+ * surface overlay overrides individual keys for that surface only.
10
+ *
11
+ * This module is the framework-neutral surface helpers; the engine
12
+ * (`SonentaI18n`) owns the overlay loading/compose, and the provider wires
13
+ * the reactive viewport listener (web) — see `provider.tsx`.
14
+ */
15
+ type Surface = "desktop" | "mobile" | "tablet";
16
+ /** Min-width (px) thresholds that map a viewport width to a surface. A width
17
+ * `< mobile` → `mobile`; `< tablet` → `tablet`; otherwise `desktop`. Mirrors
18
+ * the common mobile-first breakpoint ladder. */
19
+ interface SurfaceBreakpoints {
20
+ /** Upper bound (exclusive) of the `mobile` surface, in px. Default 640. */
21
+ mobile: number;
22
+ /** Upper bound (exclusive) of the `tablet` surface, in px. Default 1024. */
23
+ tablet: number;
24
+ }
25
+ declare const DEFAULT_SURFACE_BREAKPOINTS: SurfaceBreakpoints;
26
+ /**
27
+ * Map a viewport width (px) to a {@link Surface} using `breakpoints`
28
+ * (defaults to {@link DEFAULT_SURFACE_BREAKPOINTS}). Framework-neutral and
29
+ * pure — React Native callers can feed it `useWindowDimensions().width` and
30
+ * pass the result to `i18n.setSurface(...)`; the web provider uses it
31
+ * internally against `window.innerWidth`.
32
+ */
33
+ declare function surfaceForWidth(width: number, breakpoints?: SurfaceBreakpoints): Surface;
34
+
35
+ type Locale = string;
36
+ type Namespace = string;
37
+ /** An asset variant ref carried by an overlay key (#911 minimal v1):
38
+ * `{ "$value": <str|pluralDict>, "$asset": { kind, ref } }`. The translated
39
+ * `$value` resolves through `t()` as usual; `$asset` is exposed via
40
+ * `i18n.asset(key, ns?)`. */
41
+ interface AssetRef {
42
+ kind: string;
43
+ ref: string;
44
+ }
45
+ /**
46
+ * A language-catalog entry — the subset of the public `GET /v1/languages`
47
+ * catalog the SDK uses to expose text direction and endonyms. Regional
48
+ * variants (`is_variant`) inherit `rtl`/`script` from their `parent_code`.
49
+ */
50
+ interface LanguageMeta {
51
+ /** BCP-47 code, e.g. `fr`, `fr-CA`, `zh-Hant`. */
52
+ code: Locale;
53
+ /** Endonym (name in its own language), e.g. `français (Canada)`. */
54
+ native_name?: string;
55
+ /** English name, e.g. `French (Canada)`. */
56
+ english_name?: string;
57
+ /** Right-to-left script. */
58
+ rtl?: boolean;
59
+ /** ISO 15924 script code, e.g. `Latn`, `Arab`, `Hans`. */
60
+ script?: string;
61
+ /** True for a regional/script variant (e.g. `fr-CA`, `zh-Hant`). */
62
+ is_variant?: boolean;
63
+ /** Base language of a variant, e.g. `fr-CA` → `fr`. */
64
+ parent_code?: Locale | null;
65
+ /** CLDR plural categories present for this language. */
66
+ plural_categories?: string[];
67
+ }
68
+ interface MissingKeyEvent {
69
+ key: string;
70
+ namespace: Namespace;
71
+ language_code: Locale;
72
+ /**
73
+ * Canonical source-language value the developer asked for, WHEN one is
74
+ * available: the explicit `defaultValue` from `t()` first, else the
75
+ * fallback-language bundle value. OMITTED (undefined) when neither exists —
76
+ * the SDK never sends the key name as a value (the key is already in `key`).
77
+ * The backend promotes only a non-null `source_value`.
78
+ */
79
+ source_value?: string;
80
+ sdk_meta?: Record<string, unknown>;
81
+ }
82
+ type MissingHandlerMode = "send" | "log" | "off";
83
+ type Transport = (batch: MissingKeyEvent[]) => void | Promise<void>;
84
+ interface SonentaConfig {
85
+ /** API key — format `vrb_live_<prefix>.<secret>` with `missing:write` scope. */
86
+ token: string;
87
+ /** Project UUID this provider is bound to. */
88
+ projectUuid: string;
89
+ /** Namespaces to preload on mount. Defaults to `['common']`. */
90
+ namespaces?: Namespace[];
91
+ /**
92
+ * react-i18next-style alias for the default namespace. Convenience for
93
+ * single-namespace apps: when `namespaces` is omitted, the SDK loads
94
+ * `[defaultNS]`. Ignored when `namespaces` is provided (use that — its
95
+ * first entry is the default namespace).
96
+ */
97
+ defaultNS?: Namespace;
98
+ /**
99
+ * How keys map to the bundle structure. `false` = **flat** (keys are looked
100
+ * up literally, so dotted keys like `"App Version 6.3.8"` work); a string =
101
+ * **nested**, split on that separator (default `"."`). Explicit value here is
102
+ * an override; when omitted, the SDK auto-detects the project's
103
+ * `key_style` / `key_separator` from the version metadata (#754).
104
+ */
105
+ keySeparator?: string | false;
106
+ /**
107
+ * Separator between namespace and key in `t("ns:key")`. Default `":"`.
108
+ * Set `false` (or `""`) to disable namespace parsing so keys may contain
109
+ * `":"`. i18next-parity companion to `keySeparator`.
110
+ */
111
+ nsSeparator?: string | false;
112
+ /**
113
+ * Embedded build-time snapshot of translation bundles, keyed
114
+ * `locale -> namespace -> i18next tree` (the same shape as the CDN JSON).
115
+ * Primed synchronously on construction so the FIRST render is instant and
116
+ * works OFFLINE (before the first CDN/runtime fetch); the provider then
117
+ * swaps in fresh data when it arrives, with no flash. A failed initial fetch
118
+ * (offline) keeps the snapshot as last-known-good. Generate it with the
119
+ * `verbumia snapshot` CLI, or manually (fetch the CDN JSON and import it).
120
+ */
121
+ initialBundles?: Record<Locale, Record<Namespace, Record<string, unknown>>>;
122
+ /** Initial locale (BCP-47). */
123
+ defaultLocale: Locale;
124
+ /**
125
+ * Fallback locale(s) used when a key is missing in the active locale.
126
+ * Accepts a single locale or an ordered chain. Regional variants also fall
127
+ * back to their base language automatically (e.g. `fr-CA → fr`), so the full
128
+ * lookup order for an active `fr-CA` is `fr-CA → fr → fallbackLng…` — the
129
+ * `fr-CA → fr → source` chain (native i18next semantics). The CDN already
130
+ * serves variants as merged bundles, so this is defense-in-depth.
131
+ */
132
+ fallbackLng?: Locale | Locale[];
133
+ /**
134
+ * Embedded language catalog (offline/SSR/React Native), same items as the
135
+ * public `GET /v1/languages`. Primed synchronously so `dir()` / `nativeName()`
136
+ * work before — or entirely without — the network fetch; a successful fetch
137
+ * augments it. Pairs well with {@link SonentaConfig.initialBundles}.
138
+ */
139
+ languageCatalog?: LanguageMeta[];
140
+ /**
141
+ * Skip the best-effort fetch of the public language catalog
142
+ * (`GET {apiBase}/v1/languages`, no auth, CDN-cached) that powers `dir()` /
143
+ * `nativeName()`. Set when you don't need direction/endonym metadata, or
144
+ * you supply {@link SonentaConfig.languageCatalog} yourself.
145
+ */
146
+ disableLanguageCatalog?: boolean;
147
+ /**
148
+ * Interpolation hooks forwarded to i18next. Currently exposes `format`, the
149
+ * value formatter i18next invokes for `{{value, format}}` placeholders — wire
150
+ * your own date/number formatter here (the SDK does NOT bundle one).
151
+ *
152
+ * Example (date-fns):
153
+ * ```ts
154
+ * import { format as formatDate } from "date-fns";
155
+ * interpolation: {
156
+ * format: (v, f) =>
157
+ * f === "long" && v instanceof Date ? formatDate(v, "PPPP") : String(v),
158
+ * }
159
+ * ```
160
+ * i18next always keeps `escapeValue: false` (React escapes for us); only
161
+ * `format` is configurable here.
162
+ */
163
+ interpolation?: {
164
+ /**
165
+ * Called by i18next for each `{{value, format}}` placeholder. `value` is the
166
+ * interpolation variable, `format` the token after the comma (e.g. `"long"`,
167
+ * `"number"`), `lng` the active language, `options` the full `t()` options.
168
+ * Return the rendered string.
169
+ */
170
+ format?: (value: unknown, format: string | undefined, lng: string | undefined, options: Record<string, unknown>) => string;
171
+ };
172
+ /** Override the API base. Defaults to `https://api.verbumia.dev`. */
173
+ apiBase?: string;
174
+ /** Override the CDN base. Defaults to `https://cdn.verbumia.ca`. */
175
+ cdnBase?: string;
176
+ /**
177
+ * Optional plugins that hook into THIS provider's tree/registry — e.g.
178
+ * `@sonenta/feedback`. Plugins do NOT introduce a second React
179
+ * context; the provider calls `setup({ i18n, config })` once on mount
180
+ * and renders each plugin's `render()` as an ISOLATED sibling leaf
181
+ * after `children`, so enabling a plugin never re-renders the host app.
182
+ */
183
+ plugins?: SonentaPlugin[];
184
+ /**
185
+ * Optional override for missing-key delivery (in-app inspector,
186
+ * Storybook, Cypress mocks). When set, replaces the default POST.
187
+ */
188
+ transport?: Transport;
189
+ /** `send` (default) | `log` | `off` */
190
+ missingHandler?: MissingHandlerMode;
191
+ /** Flush cadence for the missing-key batch. Default 5_000ms. */
192
+ flushIntervalMs?: number;
193
+ /** Max events per batch before forcing a flush. Default 50. */
194
+ flushBatchSize?: number;
195
+ /** Optional ring buffer cap for `i18n.missingEvents`. Default 200. */
196
+ missingEventsBufferSize?: number;
197
+ /**
198
+ * Project version slug used when fetching bundles (BCP-style slug, e.g.
199
+ * `main`, `v2`). Maps to the
200
+ * `/p/{projectUuid}/{version}/latest/{lang}/{ns}.json` CDN path layout.
201
+ * Defaults to `main`. Included in the SDK's bundle cache keys, so two
202
+ * providers configured with different `version` values keep separate
203
+ * bundle caches.
204
+ */
205
+ version?: string;
206
+ /**
207
+ * @deprecated Use {@link SonentaConfig.version} instead. Kept as a
208
+ * back-compat alias of `version`; if both are set, `version` wins.
209
+ */
210
+ versionSlug?: string;
211
+ /**
212
+ * Surface variant (#911) — a second resolution dimension layered over the
213
+ * locale chain: `t()` returns the surface-specific value when an overlay
214
+ * provides one, else the base. Set the INITIAL surface here; it also
215
+ * becomes reactive when {@link SonentaConfig.surfaceBreakpoints} is set
216
+ * (the provider listens to the viewport on web). Omit to disable surface
217
+ * resolution entirely (base bundles only — fully back-compatible).
218
+ */
219
+ surface?: Surface;
220
+ /**
221
+ * Enable reactive surface detection from the viewport width (web): the
222
+ * provider maps `window.innerWidth` → surface via these min-width
223
+ * thresholds and calls `setSurface` on resize. Pass `true` for the default
224
+ * ladder (mobile <640 <tablet <1024 ≤desktop) or custom thresholds. On
225
+ * React Native (no `window`), set the initial {@link SonentaConfig.surface}
226
+ * and drive changes via `i18n.setSurface(surfaceForWidth(width))` from your
227
+ * own `useWindowDimensions`. Ignored when `surface` is unset.
228
+ */
229
+ surfaceBreakpoints?: SurfaceBreakpoints | boolean;
230
+ /**
231
+ * Deployment environment. Drives where the SDK fetches translations from:
232
+ *
233
+ * Maps to the customer's version model: a *promoted production*
234
+ * version is `"prod"`; any non-promoted (working) version is `"dev"`.
235
+ *
236
+ * - "prod" (default): fetch from `cdnBase/p/<project>/<version>/latest/...`.
237
+ * Cheap + cache-friendly. Freshness is the CDN `latest/` alias at
238
+ * `max-age=60s` — a republish is picked up within ~a minute.
239
+ * - "dev": fetch from `apiBase/v1/projects/<id>/translations/runtime`.
240
+ * Live data, no CDN delay; the `token` (an API key with
241
+ * env_type="dev" + populated ip/origin allowlist) is sent as
242
+ * `Authorization: ApiKey ...`.
243
+ *
244
+ * Browser callers: `Origin` is automatically sent and validated against
245
+ * the key's origin_allowlist. Node clients (SSR, scripts) don't send
246
+ * Origin and must rely on `ip_allowlist`.
247
+ */
248
+ env?: "prod" | "dev";
249
+ }
250
+ /** Context handed to a plugin's `setup()` — the live i18n instance + the
251
+ * resolved provider config (apiBase, projectUuid, locale, …). A plugin
252
+ * reuses these instead of asking the customer to re-configure. */
253
+ interface SonentaPluginContext {
254
+ i18n: I18nInstance;
255
+ config: SonentaConfig;
256
+ /**
257
+ * #806 — subscribe to RUNTIME language changes (alternative B in the
258
+ * SeedSower diagnosis: a stable, public plugin-context hook that does
259
+ * not couple plugins to the private `_i18next` field). Fires every
260
+ * time the active language changes via `setLocale()` /
261
+ * `changeLanguage()` / the drop-in `i18n.i18next.changeLanguage()`
262
+ * path. The handler receives the NEW language (BCP-47). The returned
263
+ * function unsubscribes — plugins MUST call it from their `setup()`
264
+ * teardown, otherwise the subscription survives the host's provider
265
+ * unmount.
266
+ *
267
+ * ADDITIVE: existing plugins that ignore the field keep working; the
268
+ * provider supplies it on every mount from 1.0.5 onwards. The shape
269
+ * mirrors a standard React-style `subscribe → unsubscribe` so other
270
+ * framework ports of the SDK (Vue, Svelte) can implement the same
271
+ * contract.
272
+ */
273
+ onLanguageChange(cb: (lng: Locale) => void): () => void;
274
+ }
275
+ /** A provider plugin. `setup` runs once on mount (optional teardown via
276
+ * the returned fn). `render` returns an isolated sibling node the
277
+ * provider mounts after `children` — its state never re-renders the app. */
278
+ interface SonentaPlugin {
279
+ name: string;
280
+ setup?: (ctx: SonentaPluginContext) => void | (() => void);
281
+ render?: () => ReactNode;
282
+ }
283
+ interface I18nInstance {
284
+ /** True once the initial namespace bundles loaded for the active locale. */
285
+ ready: boolean;
286
+ locale: Locale;
287
+ /** Alias of `locale` for react-i18next compatibility. */
288
+ language: Locale;
289
+ setLocale: (l: Locale) => Promise<void>;
290
+ /**
291
+ * Alias of `setLocale` for react-i18next compatibility. Resolves once the
292
+ * new locale's namespace bundles have loaded.
293
+ */
294
+ changeLanguage: (l: Locale) => Promise<void>;
295
+ /**
296
+ * Translate a key. Exposed here mainly for out-of-React use via
297
+ * `getI18n()`; inside components prefer the `t` returned by
298
+ * `useTranslation()` (it also feeds the on-screen key registry).
299
+ */
300
+ t: TranslationFunction;
301
+ /** Recently captured missing-key events (most recent first). */
302
+ missingEvents: MissingKeyEvent[];
303
+ /** Force-flush the missing-key batch now. */
304
+ flushMissing: () => Promise<void>;
305
+ /**
306
+ * Bust-refetch already-loaded bundles and re-render. Without `opts`, all
307
+ * loaded `(locale, ns)` bundles are refreshed; pass `locale`/`namespace`
308
+ * to narrow. Used by `@sonenta/realtime` on a `translations_published`
309
+ * push and for manual refresh.
310
+ */
311
+ reload: (opts?: {
312
+ locale?: Locale;
313
+ namespace?: Namespace;
314
+ }) => Promise<void>;
315
+ /**
316
+ * Force every `useTranslation` consumer to re-render WITHOUT refetching —
317
+ * re-resolves `t()` against the CURRENT resources (both this SDK's hooks and
318
+ * react-i18next-native consumers on the exposed instance). For plugins that
319
+ * mutate i18next resources directly — e.g. `@sonenta/in-context` applies a
320
+ * live edit via `i18next.addResource` — and need the snapshot to repaint in
321
+ * place. Unlike {@link reload}, does NO fetch, so the override is not
322
+ * clobbered. Additive in 1.0.6.
323
+ */
324
+ refresh: () => void;
325
+ /**
326
+ * Active surface variant (#911), or `undefined` when surface resolution is
327
+ * off. `t()` resolves surface-overlay values on top of the base bundle.
328
+ */
329
+ surface: Surface | undefined;
330
+ /**
331
+ * Switch the active surface and recompose the loaded bundles (base ⊕ the
332
+ * new surface's sparse overlay), then re-render. Pass `undefined` to drop
333
+ * back to base-only. Loads each loaded namespace's `{ns}.{surface}.json`
334
+ * overlay on demand. The provider calls this from its viewport listener
335
+ * when `surfaceBreakpoints` is set; call it yourself on React Native.
336
+ */
337
+ setSurface: (surface: Surface | undefined) => Promise<void>;
338
+ /**
339
+ * The asset variant ref for a key under the ACTIVE locale+surface (#911
340
+ * minimal v1), or `undefined` when the resolved key carries no `$asset`.
341
+ * `t(key)` still returns the key's `$value`; this exposes the companion
342
+ * asset (e.g. an icon/image ref) for the host to render.
343
+ */
344
+ asset: (key: string, namespace?: Namespace) => AssetRef | undefined;
345
+ /**
346
+ * Text direction of a locale (defaults to the active locale) — i18next
347
+ * parity. Sourced from the language catalog's `rtl` (variant → base
348
+ * inheritance); falls back to a built-in RTL-language list when the catalog
349
+ * is not loaded. Apply to `<html dir>` or a container's `dir` attribute.
350
+ */
351
+ dir: (lng?: Locale) => "ltr" | "rtl";
352
+ /**
353
+ * Endonym (native name) of a locale from the catalog — the fallback for
354
+ * runtimes WITHOUT `Intl.DisplayNames` (React Native/Hermes, SSR). Returns
355
+ * `undefined` when the catalog has no entry. For UI-localized names prefer
356
+ * `Intl.DisplayNames(uiLocale, { type: "language" }).of(code)`; use this
357
+ * when that API is unavailable.
358
+ */
359
+ nativeName: (lng?: Locale) => string | undefined;
360
+ /**
361
+ * Full language-catalog entry for a locale (script, plural categories,
362
+ * parent, …), defaulting to the active locale; `undefined` if unknown.
363
+ */
364
+ languageMeta: (lng?: Locale) => LanguageMeta | undefined;
365
+ /**
366
+ * The underlying real `i18next` instance powering this SDK (thin-wrapper,
367
+ * #805). Exposed for react-i18next DROP-IN: pass it to react-i18next's own
368
+ * `<Trans i18n={…}>` or `useTranslation(ns, { i18n })` for rich JSX, or call
369
+ * any i18next API directly. Present on instances created by this SDK.
370
+ */
371
+ i18next?: i18n;
372
+ }
373
+ type TranslationOptions = Record<string, unknown> & {
374
+ defaultValue?: string;
375
+ };
376
+ interface TranslationFunction {
377
+ /**
378
+ * react-i18next-style positional fallback: `t('key', 'Default text')`,
379
+ * optionally with interpolation/options as a 3rd argument:
380
+ * `t('key', 'Hi {{name}}', { name })`.
381
+ */
382
+ (key: string, defaultValue: string, options?: TranslationOptions): string;
383
+ /** Native form: `t('key', { defaultValue, ...interpolation })`. */
384
+ (key: string, options?: TranslationOptions): string;
385
+ }
386
+
387
+ interface SonentaProviderProps extends SonentaConfig {
388
+ children: ReactNode;
389
+ }
390
+ declare function SonentaProvider({ children, ...config }: SonentaProviderProps): react_jsx_runtime.JSX.Element;
391
+
392
+ interface UseTranslationResult {
393
+ t: TranslationFunction;
394
+ i18n: I18nInstance;
395
+ }
396
+ /** React hook — returns `{ t, i18n }`. Optional `defaultNamespace` lets you
397
+ * drop the `ns:` prefix on every call.
398
+ *
399
+ * Every key this hook resolves during a render is recorded into the
400
+ * on-screen key registry (so a mounted `@sonenta/feedback` widget lists
401
+ * only the strings rendered on the current view — spec ltm 373). The
402
+ * contribution is keyed to THIS hook instance and dropped on unmount, so
403
+ * navigating away removes its keys automatically. */
404
+ declare function useTranslation(defaultNamespace?: string): UseTranslationResult;
405
+
406
+ interface TransProps {
407
+ /** The translation key (optionally `ns:key`). */
408
+ i18nKey: string;
409
+ /** Default value if the key is missing — used as the fallback string. */
410
+ defaults?: string;
411
+ /** Variables interpolated into `{{var}}` placeholders. */
412
+ values?: Record<string, unknown>;
413
+ /** JSX components mapped by 0-based numeric index — `<0>bold</0>` etc. */
414
+ components?: ReactNode[];
415
+ /** Optional namespace shortcut. */
416
+ namespace?: string;
417
+ }
418
+ /** Bare-bones Trans component: resolves the key, interpolates values, and
419
+ * swaps `<0>...</0>` placeholders into the supplied React components.
420
+ * Keeps the surface minimal — full Trans semantics (nested keys, plural
421
+ * trees, gender) land in V1.1. */
422
+ declare function Trans({ i18nKey, defaults, values, components, namespace, }: TransProps): react_jsx_runtime.JSX.Element;
423
+
424
+ /**
425
+ * Access the active i18n instance OUTSIDE React components — the
426
+ * react-i18next-style standalone singleton (e.g. for `t()`/`changeLanguage()`
427
+ * in plain modules, stores, or helpers).
428
+ *
429
+ * Returns the instance created by the mounted `<SonentaProvider>`; throws a
430
+ * clear error if no provider is mounted yet. Assumes a single app-wide
431
+ * provider (the common case); with multiple concurrent providers it returns
432
+ * the most recently mounted one.
433
+ */
434
+ declare function getI18n(): I18nInstance;
435
+ /**
436
+ * SAFE variant of {@link getI18n} (#805): returns the active i18n instance, or
437
+ * `null` when no provider is mounted yet — it does NOT throw. Use at module
438
+ * load / in plain helpers where a provider may not be mounted.
439
+ */
440
+ declare function getI18nSafe(): I18nInstance | null;
441
+ /**
442
+ * SAFE out-of-React translate (#805). Resolves against the active i18n instance
443
+ * when a provider is mounted; otherwise returns `options.defaultValue` (when a
444
+ * string) or the `key` — it NEVER throws. This makes module-load-time / helper
445
+ * `t()` calls safe before mount (the throwing {@link getI18n} would break them).
446
+ * Accepts both shapes: `t('k', { defaultValue })` and `t('k', 'Default', opts?)`.
447
+ */
448
+ declare function t(key: string, optionsOrDefault?: (Record<string, unknown> & {
449
+ defaultValue?: string;
450
+ count?: number;
451
+ }) | string, maybeOptions?: Record<string, unknown> & {
452
+ defaultValue?: string;
453
+ count?: number;
454
+ }): string;
455
+
456
+ /** Default transport: POST to `${apiBase}/v1/missing` with the API key. */
457
+ declare function defaultTransport(opts: {
458
+ apiBase: string;
459
+ token: string;
460
+ projectUuid: string;
461
+ }): Transport;
462
+ /** Logs each event to console.warn — handy for dev. */
463
+ declare const logTransport: Transport;
464
+
465
+ /**
466
+ * On-screen key registry — the PRODUCER side of the tiny cross-package
467
+ * contract that `@sonenta/feedback` consumes via
468
+ * `globalThis.__verbumia_key_registry__` (see `@sonenta/feedback`'s
469
+ * `core/keys.ts`).
470
+ *
471
+ * Why this exists: the feedback widget must list only the translation
472
+ * strings RENDERED on the current screen (spec ltm 373) — NOT every
473
+ * project string. The widget can't know what's on screen; the i18n SDK
474
+ * does, because it resolves the keys. The registry exposes a minimal
475
+ * global with TWO producer paths feeding it:
476
+ *
477
+ * 1. HOOK-LEVEL — our own `useTranslation` / `Trans` push their
478
+ * per-render key set via `_set(token, set)`. Mount-tracking handles
479
+ * navigation: when a component unmounts, its keys drop out of the
480
+ * union automatically.
481
+ * 2. INSTANCE-LEVEL — SonentaI18n wraps `i18next.t` so EVERY resolved
482
+ * key (including those resolved through react-i18next's NATIVE
483
+ * `useTranslation` / `<Trans>` bound to the exposed i18next, or
484
+ * through a direct `i18n.t()` call) feeds `_track(token, id)`. This
485
+ * makes the registry "instance-level producer", per #806 SeedSower
486
+ * diagnosis: a 1.0.2 thin-wrapper drop-in lets host apps keep their
487
+ * `from 'react-i18next'` imports, so the hook-level producer alone
488
+ * misses 100% of those keys. Without this, the widget shows "no
489
+ * strings on this view" even when the view is full of text.
490
+ *
491
+ * `snapshot()` is the UNION of both, deduped. The instance-level
492
+ * contribution accumulates for the i18n instance's lifetime (no per-
493
+ * component unmount signal), which is by-design: a stale superset is
494
+ * strictly better than a false empty.
495
+ *
496
+ * The published shape is intentionally tiny so any framework port of the
497
+ * i18n SDK can implement the same global without depending on feedback:
498
+ *
499
+ * globalThis.__verbumia_key_registry__ = {
500
+ * snapshot(): { namespace: string; key: string }[];
501
+ * isPopulated(): boolean;
502
+ * reset(): void;
503
+ * }
504
+ */
505
+ interface DeclaredKey {
506
+ namespace: string;
507
+ key: string;
508
+ }
509
+ declare class KeyRegistry {
510
+ private _instances;
511
+ private _providers;
512
+ /** Replace an instance's contributed key set (per-render hook producer). */
513
+ _set(token: symbol, keys: Set<string>): void;
514
+ /** Append ONE id to a token's set, lazy-creating it. Used by the
515
+ * instance-level i18n.t wrap, which accumulates over the i18n
516
+ * instance's lifetime (no per-render reset semantics). Safe to call
517
+ * before `attach()` — the global publishes whenever a provider mounts. */
518
+ _track(token: symbol, id: string): void;
519
+ /** Drop an instance entirely (called on hook unmount or i18n stop). */
520
+ _delete(token: symbol): void;
521
+ /** Keys rendered by currently-mounted consumers. Stable insertion order. */
522
+ snapshot(): DeclaredKey[];
523
+ /** True when ANY producer has contributed ≥1 key. Cheap O(producers)
524
+ * check exposed for DEV-time integration asserts ("did my
525
+ * useTranslation imports end up wired to @sonenta/react-i18next?"). */
526
+ isPopulated(): boolean;
527
+ /** Escape hatch (router integrations / tests). Mount-tracking already
528
+ * handles navigation, so this is rarely needed. */
529
+ reset(): void;
530
+ /** Encode a resolved key into the internal id used by `_set`. */
531
+ encode(fullKey: string, defaultNamespace: string): string;
532
+ /** Provider mounted — publish the global (idempotent, ref-counted). */
533
+ attach(): void;
534
+ /** Provider unmounted — unpublish when the last one goes away. */
535
+ detach(): void;
536
+ }
537
+ /** Process-wide singleton — there is exactly one on-screen registry. */
538
+ declare const keyRegistry: KeyRegistry;
539
+
540
+ export { 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 };