@object-ui/i18n 17.3.0 → 17.6.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/CHANGELOG.md +1929 -1
- package/README.md +41 -12
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -2
- package/dist/index.d.ts +23 -21
- package/dist/index.js +31 -21
- package/dist/locales/ar.d.ts +638 -369
- package/dist/locales/ar.js +744 -424
- package/dist/locales/de.d.ts +638 -369
- package/dist/locales/de.js +759 -446
- package/dist/locales/en.d.ts +673 -404
- package/dist/locales/en.js +899 -406
- package/dist/locales/es.d.ts +638 -369
- package/dist/locales/es.js +739 -427
- package/dist/locales/fr.d.ts +638 -369
- package/dist/locales/fr.js +738 -423
- package/dist/locales/index.d.ts +6476 -3786
- package/dist/locales/index.js +20 -20
- package/dist/locales/ja.d.ts +638 -369
- package/dist/locales/ja.js +732 -417
- package/dist/locales/ko.d.ts +638 -369
- package/dist/locales/ko.js +736 -424
- package/dist/locales/pt.d.ts +638 -369
- package/dist/locales/pt.js +737 -425
- package/dist/locales/ru.d.ts +638 -369
- package/dist/locales/ru.js +750 -424
- package/dist/locales/zh.d.ts +672 -403
- package/dist/locales/zh.js +720 -403
- package/dist/pickLocalized.d.ts +58 -0
- package/dist/pickLocalized.js +115 -7
- package/dist/provider.d.ts +84 -2
- package/dist/provider.js +305 -31
- package/dist/useDisplayLocale.d.ts +49 -0
- package/dist/useDisplayLocale.js +55 -0
- package/dist/useObjectLabel.d.ts +94 -14
- package/dist/useObjectLabel.js +134 -21
- package/dist/useSafeTranslation.js +51 -3
- package/dist/utils/index.d.ts +4 -3
- package/dist/utils/index.js +4 -3
- package/dist/utils/number-display.d.ts +32 -0
- package/dist/utils/number-display.js +32 -0
- package/dist/utils/spec-formatters.d.ts +39 -13
- package/dist/utils/spec-formatters.js +3 -3
- package/package.json +4 -3
package/dist/pickLocalized.d.ts
CHANGED
|
@@ -11,5 +11,63 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Pure — pair it with `useObjectTranslation().language` (or any current-locale
|
|
13
13
|
* source) at the call site.
|
|
14
|
+
*
|
|
15
|
+
* Every limb reads **own properties only** and accepts **only `string` values**
|
|
16
|
+
* (objectui#3907). Both guards used to hold on the regional and last-resort
|
|
17
|
+
* limbs alone, which left the other four able to resolve a locale against
|
|
18
|
+
* `Object.prototype` — `pickLocalized({ en: 'Pricing' }, 'constructor')`
|
|
19
|
+
* rendered the constructor's source text as the label — and able to
|
|
20
|
+
* short-circuit the chain with a non-string value, rendering `[object Object]`.
|
|
21
|
+
* Neither is reachable by an input the contract admits (no BCP-47 tag is an
|
|
22
|
+
* `Object.prototype` member, and `InlineLocaleMapSchema` is
|
|
23
|
+
* `z.record(<tag>, z.string())`), so applying them uniformly changes nothing
|
|
24
|
+
* for any real language tag. It makes this function agree limb for limb with
|
|
25
|
+
* the backend twin `resolveI18nLabel` (`@objectstack/spec`, objectstack#6765),
|
|
26
|
+
* whose locale can arrive from an untrusted `Accept-Language` header and which
|
|
27
|
+
* shipped with exactly these two narrowings recorded as deliberate departures.
|
|
28
|
+
* The only remaining difference between the two is how each spells a miss —
|
|
29
|
+
* `''` here for a text node, `undefined` there for a producer's `?? name` chain
|
|
30
|
+
* — pinned in `plugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts`.
|
|
31
|
+
*
|
|
32
|
+
* A guard makes its limb **miss**; it never aborts the resolution. An unusable
|
|
33
|
+
* entry falls through to the next limb exactly as an absent one does, so
|
|
34
|
+
* `pickLocalized({ en: 'Pricing' }, 'constructor')` is `'Pricing'`, not `''`.
|
|
14
35
|
*/
|
|
15
36
|
export declare function pickLocalized(value: unknown, language: string | undefined | null): string;
|
|
37
|
+
/**
|
|
38
|
+
* Write `next` back into a possibly-localized value as the `language` entry,
|
|
39
|
+
* preserving every other locale — the write-side inverse of
|
|
40
|
+
* {@link pickLocalized}.
|
|
41
|
+
*
|
|
42
|
+
* An authoring surface that shows one locale in one text input has to answer
|
|
43
|
+
* two questions, and they are not the same question: which entry to DISPLAY
|
|
44
|
+
* (`pickLocalized`) and which entry a save REPLACES. Answering the second with
|
|
45
|
+
* "the whole value" is the data-loss shape this function exists to remove —
|
|
46
|
+
* `onChange(e.target.value)` straight back over `{ en, zh-CN }` destroys every
|
|
47
|
+
* locale the author was not looking at, silently, on the first keystroke.
|
|
48
|
+
*
|
|
49
|
+
* - Plain string / `null` / `undefined` / array input: there is no map to
|
|
50
|
+
* preserve, so the result is `next` itself. A string-valued label keeps
|
|
51
|
+
* behaving exactly as it always has.
|
|
52
|
+
* - Inline locale map: the result is the same map with exactly one entry
|
|
53
|
+
* replaced (or added — see {@link localeWriteKey}). Non-string entries and
|
|
54
|
+
* unrelated locales are carried across untouched.
|
|
55
|
+
*
|
|
56
|
+
* The pairing with `pickLocalized` is the invariant that matters and it is
|
|
57
|
+
* pinned in `__tests__/setLocalized.test.ts`: for any map and any language,
|
|
58
|
+
* `pickLocalized(setLocalized(map, lang, s), lang) === s`. If the two functions
|
|
59
|
+
* ever drift apart, an edit lands in an entry the panel does not display and
|
|
60
|
+
* the "saved" string vanishes — which is why they live in one file.
|
|
61
|
+
*
|
|
62
|
+
* ⚠️ This is the minimal non-destructive write for a SINGLE-locale editor. It
|
|
63
|
+
* is not a multi-locale authoring UI and does not pretend to be: an author can
|
|
64
|
+
* only ever reach the entry for the locale they are in.
|
|
65
|
+
*
|
|
66
|
+
* Authoring every locale from one surface remains an OPEN product question, and
|
|
67
|
+
* is deliberately not deferred to a tracker here: the deferral this replaced
|
|
68
|
+
* named objectui#4163, which closed as completed on 2026-08-15 with the question
|
|
69
|
+
* still unanswered. A comment pointing at a closed card reads as though the
|
|
70
|
+
* question were settled somewhere, which is how the previous stale premise in
|
|
71
|
+
* this area survived unread.
|
|
72
|
+
*/
|
|
73
|
+
export declare function setLocalized(value: unknown, language: string | undefined | null, next: string): string | Record<string, unknown>;
|
package/dist/pickLocalized.js
CHANGED
|
@@ -11,6 +11,27 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Pure — pair it with `useObjectTranslation().language` (or any current-locale
|
|
13
13
|
* source) at the call site.
|
|
14
|
+
*
|
|
15
|
+
* Every limb reads **own properties only** and accepts **only `string` values**
|
|
16
|
+
* (objectui#3907). Both guards used to hold on the regional and last-resort
|
|
17
|
+
* limbs alone, which left the other four able to resolve a locale against
|
|
18
|
+
* `Object.prototype` — `pickLocalized({ en: 'Pricing' }, 'constructor')`
|
|
19
|
+
* rendered the constructor's source text as the label — and able to
|
|
20
|
+
* short-circuit the chain with a non-string value, rendering `[object Object]`.
|
|
21
|
+
* Neither is reachable by an input the contract admits (no BCP-47 tag is an
|
|
22
|
+
* `Object.prototype` member, and `InlineLocaleMapSchema` is
|
|
23
|
+
* `z.record(<tag>, z.string())`), so applying them uniformly changes nothing
|
|
24
|
+
* for any real language tag. It makes this function agree limb for limb with
|
|
25
|
+
* the backend twin `resolveI18nLabel` (`@objectstack/spec`, objectstack#6765),
|
|
26
|
+
* whose locale can arrive from an untrusted `Accept-Language` header and which
|
|
27
|
+
* shipped with exactly these two narrowings recorded as deliberate departures.
|
|
28
|
+
* The only remaining difference between the two is how each spells a miss —
|
|
29
|
+
* `''` here for a text node, `undefined` there for a producer's `?? name` chain
|
|
30
|
+
* — pinned in `plugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts`.
|
|
31
|
+
*
|
|
32
|
+
* A guard makes its limb **miss**; it never aborts the resolution. An unusable
|
|
33
|
+
* entry falls through to the next limb exactly as an absent one does, so
|
|
34
|
+
* `pickLocalized({ en: 'Pricing' }, 'constructor')` is `'Pricing'`, not `''`.
|
|
14
35
|
*/
|
|
15
36
|
export function pickLocalized(value, language) {
|
|
16
37
|
if (value == null)
|
|
@@ -21,18 +42,105 @@ export function pickLocalized(value, language) {
|
|
|
21
42
|
return String(value);
|
|
22
43
|
if (typeof value === 'object') {
|
|
23
44
|
const o = value;
|
|
45
|
+
/**
|
|
46
|
+
* One entry, or `undefined` when this limb does not hit — which is what
|
|
47
|
+
* lets the limbs keep chaining through `??` in the same order as before.
|
|
48
|
+
*/
|
|
49
|
+
const read = (key) => {
|
|
50
|
+
if (!Object.prototype.hasOwnProperty.call(o, key))
|
|
51
|
+
return undefined;
|
|
52
|
+
const entry = o[key];
|
|
53
|
+
return typeof entry === 'string' ? entry : undefined;
|
|
54
|
+
};
|
|
24
55
|
const lang = (language || 'en').trim();
|
|
25
56
|
const base = lang.split('-')[0];
|
|
26
57
|
// Runtime language is often a bare base code ('zh') while metadata authors
|
|
27
58
|
// write full BCP-47 tags ('zh-CN') — upgrade to any key sharing the base.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
// `Object.keys` is already own-and-enumerable, so `read` here is uniformity
|
|
60
|
+
// rather than a second guard.
|
|
61
|
+
const regional = Object.keys(o).find((k) => k.split('-')[0] === base && read(k) !== undefined);
|
|
62
|
+
const pick = read(lang) ??
|
|
63
|
+
read(base) ??
|
|
64
|
+
(regional !== undefined ? read(regional) : undefined) ??
|
|
65
|
+
read('default') ??
|
|
66
|
+
read('en') ??
|
|
34
67
|
Object.values(o).find((v) => typeof v === 'string');
|
|
35
|
-
return pick == null ? '' :
|
|
68
|
+
return pick == null ? '' : pick;
|
|
36
69
|
}
|
|
37
70
|
return String(value);
|
|
38
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The map key an edit made in `language` must be WRITTEN to — the write-side
|
|
74
|
+
* twin of {@link pickLocalized}'s read limbs.
|
|
75
|
+
*
|
|
76
|
+
* It deliberately implements only the first three limbs (exact tag, base
|
|
77
|
+
* language, region-qualified sibling) and then STOPS. `pickLocalized` continues
|
|
78
|
+
* into `default` -> `en` -> first value, and those last three limbs must never
|
|
79
|
+
* be write targets: they are DISPLAY fallbacks that hand back *another*
|
|
80
|
+
* locale's string. Following them would mean an author editing in `fr`, shown
|
|
81
|
+
* the `en` entry because the map has no French, saves and overwrites English.
|
|
82
|
+
* When no entry for the active locale exists the edit ADDS one under the active
|
|
83
|
+
* tag and every stored entry survives untouched.
|
|
84
|
+
*
|
|
85
|
+
* The first three limbs are followed exactly because they identify the entry
|
|
86
|
+
* the author was actually looking at. Writing `zh` when the map stores `zh-CN`
|
|
87
|
+
* would leave the edited-away `zh-CN` string in place — invisible in the panel
|
|
88
|
+
* that just "changed" it, still live for any consumer that asks for `zh-CN`.
|
|
89
|
+
*
|
|
90
|
+
* Own properties only, `string` values only, for the same reason every
|
|
91
|
+
* `pickLocalized` limb narrowed that way in objectui#3907: a locale tag that
|
|
92
|
+
* names an `Object.prototype` member is not an entry, so it cannot be the entry
|
|
93
|
+
* the author edited.
|
|
94
|
+
*/
|
|
95
|
+
function localeWriteKey(o, language) {
|
|
96
|
+
const lang = (language || 'en').trim() || 'en';
|
|
97
|
+
const base = lang.split('-')[0];
|
|
98
|
+
const has = (key) => Object.prototype.hasOwnProperty.call(o, key) && typeof o[key] === 'string';
|
|
99
|
+
if (has(lang))
|
|
100
|
+
return lang;
|
|
101
|
+
if (has(base))
|
|
102
|
+
return base;
|
|
103
|
+
return Object.keys(o).find((k) => k.split('-')[0] === base && has(k)) ?? lang;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Write `next` back into a possibly-localized value as the `language` entry,
|
|
107
|
+
* preserving every other locale — the write-side inverse of
|
|
108
|
+
* {@link pickLocalized}.
|
|
109
|
+
*
|
|
110
|
+
* An authoring surface that shows one locale in one text input has to answer
|
|
111
|
+
* two questions, and they are not the same question: which entry to DISPLAY
|
|
112
|
+
* (`pickLocalized`) and which entry a save REPLACES. Answering the second with
|
|
113
|
+
* "the whole value" is the data-loss shape this function exists to remove —
|
|
114
|
+
* `onChange(e.target.value)` straight back over `{ en, zh-CN }` destroys every
|
|
115
|
+
* locale the author was not looking at, silently, on the first keystroke.
|
|
116
|
+
*
|
|
117
|
+
* - Plain string / `null` / `undefined` / array input: there is no map to
|
|
118
|
+
* preserve, so the result is `next` itself. A string-valued label keeps
|
|
119
|
+
* behaving exactly as it always has.
|
|
120
|
+
* - Inline locale map: the result is the same map with exactly one entry
|
|
121
|
+
* replaced (or added — see {@link localeWriteKey}). Non-string entries and
|
|
122
|
+
* unrelated locales are carried across untouched.
|
|
123
|
+
*
|
|
124
|
+
* The pairing with `pickLocalized` is the invariant that matters and it is
|
|
125
|
+
* pinned in `__tests__/setLocalized.test.ts`: for any map and any language,
|
|
126
|
+
* `pickLocalized(setLocalized(map, lang, s), lang) === s`. If the two functions
|
|
127
|
+
* ever drift apart, an edit lands in an entry the panel does not display and
|
|
128
|
+
* the "saved" string vanishes — which is why they live in one file.
|
|
129
|
+
*
|
|
130
|
+
* ⚠️ This is the minimal non-destructive write for a SINGLE-locale editor. It
|
|
131
|
+
* is not a multi-locale authoring UI and does not pretend to be: an author can
|
|
132
|
+
* only ever reach the entry for the locale they are in.
|
|
133
|
+
*
|
|
134
|
+
* Authoring every locale from one surface remains an OPEN product question, and
|
|
135
|
+
* is deliberately not deferred to a tracker here: the deferral this replaced
|
|
136
|
+
* named objectui#4163, which closed as completed on 2026-08-15 with the question
|
|
137
|
+
* still unanswered. A comment pointing at a closed card reads as though the
|
|
138
|
+
* question were settled somewhere, which is how the previous stale premise in
|
|
139
|
+
* this area survived unread.
|
|
140
|
+
*/
|
|
141
|
+
export function setLocalized(value, language, next) {
|
|
142
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
143
|
+
return next;
|
|
144
|
+
const o = value;
|
|
145
|
+
return { ...o, [localeWriteKey(o, language)]: next };
|
|
146
|
+
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import type { i18n as I18nInstance } from 'i18next';
|
|
8
|
-
import { type I18nConfig } from './i18n';
|
|
8
|
+
import { type I18nConfig } from './i18n.js';
|
|
9
9
|
/**
|
|
10
10
|
* `localStorage` key holding the user's explicit language choice.
|
|
11
11
|
*
|
|
@@ -28,6 +28,50 @@ export declare const LOCALE_STORAGE_KEY = "objectui-locale";
|
|
|
28
28
|
* stored".
|
|
29
29
|
*/
|
|
30
30
|
export declare function readStoredLanguage(): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* `localStorage` key caching the TENANT's server-side locale — a *seed*, not a
|
|
33
|
+
* choice (objectui#4035).
|
|
34
|
+
*
|
|
35
|
+
* Deliberately a separate slot from {@link LOCALE_STORAGE_KEY}, and that
|
|
36
|
+
* separation is the whole point rather than a tidiness preference. The two
|
|
37
|
+
* values have different provenance and therefore different rights:
|
|
38
|
+
*
|
|
39
|
+
* - {@link LOCALE_STORAGE_KEY} is what the *user* picked. It outranks
|
|
40
|
+
* everything and must survive a tenant reconfiguration.
|
|
41
|
+
* - This slot is what the *admin* configured, cached so the next boot can apply
|
|
42
|
+
* it synchronously. It outranks only the environment (browser language).
|
|
43
|
+
*
|
|
44
|
+
* Writing the server seed into the explicit-choice slot would erase that
|
|
45
|
+
* difference permanently: the seed would then be indistinguishable from a
|
|
46
|
+
* deliberate user choice, so it would (a) outrank a later tenant change and pin
|
|
47
|
+
* the device to a stale locale forever, and (b) suppress browser detection for
|
|
48
|
+
* a user who never expressed a preference. Only a real switch promotes a
|
|
49
|
+
* language to the explicit slot — see the `languageChanged` choke point.
|
|
50
|
+
*/
|
|
51
|
+
export declare const LOCALE_SEED_STORAGE_KEY = "objectui-locale-seed";
|
|
52
|
+
/**
|
|
53
|
+
* Read the cached tenant locale seed, or `null` when nothing is cached.
|
|
54
|
+
*
|
|
55
|
+
* Same defensive posture as {@link readStoredLanguage}: `localStorage` is
|
|
56
|
+
* absent under SSR and *throws* in Safari private mode / partitioned iframes,
|
|
57
|
+
* and a language seed is never worth taking the app down for.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readCachedLanguageSeed(): string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Cache the tenant locale the server just answered with — the "revalidate" half
|
|
62
|
+
* of the stale-while-revalidate contract (objectstack#5419, ruling point 2).
|
|
63
|
+
*
|
|
64
|
+
* Call this with whatever `/auth/me/localization` actually returned for an
|
|
65
|
+
* authenticated caller. A `null`/`undefined` locale CLEARS the cache rather
|
|
66
|
+
* than leaving the old value: a tenant that unsets its locale must reach
|
|
67
|
+
* choice-less devices on their next boot, and "keep the last good seed forever"
|
|
68
|
+
* is precisely the being-pinned-by-a-stale-seed failure the ruling forbids.
|
|
69
|
+
*
|
|
70
|
+
* A *failed* fetch must simply not call this — the cache is then left intact,
|
|
71
|
+
* which is the correct stale-while-revalidate behaviour when revalidation
|
|
72
|
+
* cannot complete.
|
|
73
|
+
*/
|
|
74
|
+
export declare function cacheLanguageSeed(locale: string | null | undefined): void;
|
|
31
75
|
interface I18nContextValue {
|
|
32
76
|
/** Current language code */
|
|
33
77
|
language: string;
|
|
@@ -37,6 +81,19 @@ interface I18nContextValue {
|
|
|
37
81
|
direction: 'ltr' | 'rtl';
|
|
38
82
|
/** The underlying i18next instance */
|
|
39
83
|
i18n: I18nInstance;
|
|
84
|
+
/**
|
|
85
|
+
* The languages a switcher may offer (objectui#4039): the app's own locale
|
|
86
|
+
* list ∩ what this renderer can resolve. Computed here because only the
|
|
87
|
+
* provider knows both sides — the built-in packs, `config.resources` and
|
|
88
|
+
* whether a dynamic loader is wired.
|
|
89
|
+
*
|
|
90
|
+
* `null` means "still asking the app". Consumers render nothing (or a
|
|
91
|
+
* skeleton) rather than a list they will have to retract a tick later.
|
|
92
|
+
* Without {@link I18nProviderProps.loadLocales} wiring, or when the app's
|
|
93
|
+
* endpoint cannot be reached, this is the offline fallback: the built-in
|
|
94
|
+
* packs plus `config.resources`.
|
|
95
|
+
*/
|
|
96
|
+
offerableLanguages: readonly string[] | null;
|
|
40
97
|
}
|
|
41
98
|
export interface I18nProviderProps {
|
|
42
99
|
/** i18n configuration options */
|
|
@@ -61,6 +118,24 @@ export interface I18nProviderProps {
|
|
|
61
118
|
* ```
|
|
62
119
|
*/
|
|
63
120
|
loadLanguage?: (lang: string) => Promise<Record<string, unknown>>;
|
|
121
|
+
/**
|
|
122
|
+
* The app's own locale list — the codes it actually ships translations for
|
|
123
|
+
* (objectui#4039). Wired the same way as {@link I18nProviderProps.loadLanguage}:
|
|
124
|
+
* the app owns the transport, the provider owns what is done with the answer.
|
|
125
|
+
*
|
|
126
|
+
* The console reads `GET /api/v1/i18n/locales`; see
|
|
127
|
+
* `apps/console/src/loadLocales.ts`. Without this prop the switcher keeps
|
|
128
|
+
* offering the built-in packs (plus `config.resources`) exactly as before, so
|
|
129
|
+
* an app that never wires it is unaffected.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```tsx
|
|
133
|
+
* <I18nProvider loadLanguage={loadLanguage} loadLocales={loadLocales}>
|
|
134
|
+
* <App />
|
|
135
|
+
* </I18nProvider>
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
loadLocales?: () => Promise<string[]>;
|
|
64
139
|
/**
|
|
65
140
|
* Remember the active language in `localStorage` ({@link LOCALE_STORAGE_KEY})
|
|
66
141
|
* and restore it on the next mount. Default: `true`.
|
|
@@ -94,7 +169,7 @@ export interface I18nProviderProps {
|
|
|
94
169
|
* </I18nProvider>
|
|
95
170
|
* ```
|
|
96
171
|
*/
|
|
97
|
-
export declare function I18nProvider({ config, instance: externalInstance, loadLanguage, persistLanguage, children, }: I18nProviderProps): React.FunctionComponentElement<React.ProviderProps<I18nContextValue | null>>;
|
|
172
|
+
export declare function I18nProvider({ config, instance: externalInstance, loadLanguage, loadLocales, persistLanguage, children, }: I18nProviderProps): React.FunctionComponentElement<React.ProviderProps<I18nContextValue | null>>;
|
|
98
173
|
/**
|
|
99
174
|
* Hook to access Object UI i18n context
|
|
100
175
|
*
|
|
@@ -117,6 +192,13 @@ export declare function useObjectTranslation(ns?: string): {
|
|
|
117
192
|
direction: "ltr" | "rtl";
|
|
118
193
|
/** The underlying i18next instance */
|
|
119
194
|
i18n: I18nInstance;
|
|
195
|
+
/**
|
|
196
|
+
* Languages a switcher may offer (objectui#4039). `null` while the app's
|
|
197
|
+
* locale list is in flight. Outside a provider there is nobody to ask, so
|
|
198
|
+
* the built-in packs are the answer — not `null`, which would leave a
|
|
199
|
+
* standalone switcher permanently blank.
|
|
200
|
+
*/
|
|
201
|
+
offerableLanguages: readonly string[] | null;
|
|
120
202
|
};
|
|
121
203
|
/**
|
|
122
204
|
* Hook to access the i18n context directly
|