@object-ui/i18n 17.2.0 → 17.5.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +1817 -1
  2. package/README.md +26 -13
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +14 -1
  5. package/dist/locales/ar.d.ts +550 -107
  6. package/dist/locales/ar.js +646 -156
  7. package/dist/locales/de.d.ts +550 -107
  8. package/dist/locales/de.js +661 -178
  9. package/dist/locales/en.d.ts +586 -143
  10. package/dist/locales/en.js +851 -143
  11. package/dist/locales/es.d.ts +550 -107
  12. package/dist/locales/es.js +647 -160
  13. package/dist/locales/fr.d.ts +550 -107
  14. package/dist/locales/fr.js +640 -155
  15. package/dist/locales/index.d.ts +5587 -1157
  16. package/dist/locales/ja.d.ts +550 -107
  17. package/dist/locales/ja.js +632 -149
  18. package/dist/locales/ko.d.ts +550 -107
  19. package/dist/locales/ko.js +638 -156
  20. package/dist/locales/pt.d.ts +550 -107
  21. package/dist/locales/pt.js +639 -157
  22. package/dist/locales/ru.d.ts +550 -107
  23. package/dist/locales/ru.js +650 -156
  24. package/dist/locales/zh.d.ts +588 -145
  25. package/dist/locales/zh.js +629 -145
  26. package/dist/pickLocalized.d.ts +21 -0
  27. package/dist/pickLocalized.js +40 -7
  28. package/dist/provider.d.ts +125 -1
  29. package/dist/provider.js +380 -4
  30. package/dist/useDisplayLocale.d.ts +49 -0
  31. package/dist/useDisplayLocale.js +55 -0
  32. package/dist/useObjectLabel.d.ts +8 -2
  33. package/dist/useObjectLabel.js +28 -8
  34. package/dist/useSafeTranslation.js +67 -3
  35. package/dist/utils/index.d.ts +1 -0
  36. package/dist/utils/index.js +1 -0
  37. package/dist/utils/number-display.d.ts +32 -0
  38. package/dist/utils/number-display.js +32 -0
  39. package/dist/utils/spec-formatters.d.ts +39 -13
  40. package/dist/utils/spec-formatters.js +3 -3
  41. package/package.json +5 -4
@@ -11,5 +11,26 @@
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;
@@ -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,30 @@ 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
- const regional = Object.keys(o).find((k) => k.split('-')[0] === base && typeof o[k] === 'string');
29
- const pick = o[lang] ??
30
- o[base] ??
31
- (regional !== undefined ? o[regional] : undefined) ??
32
- o.default ??
33
- o.en ??
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 ? '' : String(pick);
68
+ return pick == null ? '' : pick;
36
69
  }
37
70
  return String(value);
38
71
  }
@@ -6,6 +6,72 @@
6
6
  import React from 'react';
7
7
  import type { i18n as I18nInstance } from 'i18next';
8
8
  import { type I18nConfig } from './i18n';
9
+ /**
10
+ * `localStorage` key holding the user's explicit language choice.
11
+ *
12
+ * Follows the console's existing preference-key convention
13
+ * (`objectui-favorites`, `objectui-recent-items`), and sits beside the theme
14
+ * preference the same avatar menu writes — a language switch is a preference,
15
+ * not ephemeral UI state, so it belongs in storage (AGENTS.md §5 #8).
16
+ *
17
+ * Exported so an app that builds its own i18next instance (and therefore owns
18
+ * its bootstrap language — see {@link I18nProviderProps.instance}) can honour
19
+ * the same preference, and so a sign-out flow can clear it.
20
+ */
21
+ export declare const LOCALE_STORAGE_KEY = "objectui-locale";
22
+ /**
23
+ * Read the persisted language choice, or `null` when nothing is stored.
24
+ *
25
+ * Defensive by design: `localStorage` does not exist under SSR and *throws* on
26
+ * access in Safari private mode / partitioned iframes. A language preference is
27
+ * never worth taking the app down for, so every failure degrades to "nothing
28
+ * stored".
29
+ */
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;
9
75
  interface I18nContextValue {
10
76
  /** Current language code */
11
77
  language: string;
@@ -15,6 +81,19 @@ interface I18nContextValue {
15
81
  direction: 'ltr' | 'rtl';
16
82
  /** The underlying i18next instance */
17
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;
18
97
  }
19
98
  export interface I18nProviderProps {
20
99
  /** i18n configuration options */
@@ -39,6 +118,44 @@ export interface I18nProviderProps {
39
118
  * ```
40
119
  */
41
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[]>;
139
+ /**
140
+ * Remember the active language in `localStorage` ({@link LOCALE_STORAGE_KEY})
141
+ * and restore it on the next mount. Default: `true`.
142
+ *
143
+ * Every language change on the instance is persisted — whether it came from
144
+ * this provider's `changeLanguage`, a switcher calling
145
+ * `i18n.changeLanguage()` directly, or app code — because the promise the UI
146
+ * makes ("this is my language now") must not depend on which API the caller
147
+ * reached for.
148
+ *
149
+ * The *restore* half only applies to an instance this provider creates: when
150
+ * you pass your own {@link I18nProviderProps.instance}, its bootstrap
151
+ * language is yours to choose (call {@link readStoredLanguage} if you want
152
+ * the same preference honoured).
153
+ *
154
+ * Set `false` for surfaces that must stay on a fixed language regardless of
155
+ * what the user picked elsewhere on the origin — previews, demos, screenshot
156
+ * harnesses.
157
+ */
158
+ persistLanguage?: boolean;
42
159
  /** Children to render */
43
160
  children: React.ReactNode;
44
161
  }
@@ -52,7 +169,7 @@ export interface I18nProviderProps {
52
169
  * </I18nProvider>
53
170
  * ```
54
171
  */
55
- export declare function I18nProvider({ config, instance: externalInstance, loadLanguage, 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>>;
56
173
  /**
57
174
  * Hook to access Object UI i18n context
58
175
  *
@@ -75,6 +192,13 @@ export declare function useObjectTranslation(ns?: string): {
75
192
  direction: "ltr" | "rtl";
76
193
  /** The underlying i18next instance */
77
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;
78
202
  };
79
203
  /**
80
204
  * Hook to access the i18n context directly