@object-ui/i18n 17.6.0 → 18.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +5408 -0
  2. package/README.md +255 -13
  3. package/dist/LocalizationContext.d.ts +32 -9
  4. package/dist/LocalizationContext.js +32 -10
  5. package/dist/builtinAggregateLabels.d.ts +40 -0
  6. package/dist/builtinAggregateLabels.js +23 -0
  7. package/dist/currency.d.ts +27 -3
  8. package/dist/currency.js +28 -4
  9. package/dist/fallbackInterpolation.d.ts +79 -0
  10. package/dist/fallbackInterpolation.js +124 -0
  11. package/dist/i18n.d.ts +27 -1
  12. package/dist/i18n.js +99 -16
  13. package/dist/index.d.ts +8 -13
  14. package/dist/index.js +45 -17
  15. package/dist/locales/ar.d.ts +1504 -188
  16. package/dist/locales/ar.js +1711 -281
  17. package/dist/locales/de.d.ts +1080 -188
  18. package/dist/locales/de.js +1248 -261
  19. package/dist/locales/en.d.ts +1141 -249
  20. package/dist/locales/en.js +1714 -295
  21. package/dist/locales/es.d.ts +1186 -188
  22. package/dist/locales/es.js +1350 -255
  23. package/dist/locales/fr.d.ts +1186 -188
  24. package/dist/locales/fr.js +1351 -258
  25. package/dist/locales/index.d.ts +11940 -2033
  26. package/dist/locales/index.js +48 -9
  27. package/dist/locales/ja.d.ts +1080 -188
  28. package/dist/locales/ja.js +1240 -254
  29. package/dist/locales/ko.d.ts +1080 -188
  30. package/dist/locales/ko.js +1244 -258
  31. package/dist/locales/pt.d.ts +1186 -188
  32. package/dist/locales/pt.js +1350 -255
  33. package/dist/locales/registry.d.ts +102 -0
  34. package/dist/locales/registry.js +220 -0
  35. package/dist/locales/rtl.d.ts +24 -0
  36. package/dist/locales/rtl.js +26 -0
  37. package/dist/locales/ru.d.ts +1292 -188
  38. package/dist/locales/ru.js +1479 -272
  39. package/dist/locales/zh.d.ts +1144 -252
  40. package/dist/locales/zh.js +1245 -259
  41. package/dist/pickLocalized.d.ts +30 -0
  42. package/dist/pickLocalized.js +39 -0
  43. package/dist/provider.d.ts +101 -12
  44. package/dist/provider.js +335 -50
  45. package/dist/translateFn.d.ts +29 -0
  46. package/dist/translateFn.js +8 -0
  47. package/dist/useDisplayLocale.d.ts +11 -0
  48. package/dist/useDisplayLocale.js +11 -0
  49. package/dist/useObjectLabel.d.ts +143 -100
  50. package/dist/useObjectLabel.js +129 -82
  51. package/dist/useSafeTranslation.js +40 -42
  52. package/dist/utils/first-day-of-week.d.ts +71 -0
  53. package/dist/utils/first-day-of-week.js +61 -0
  54. package/dist/utils/formatting.d.ts +23 -1
  55. package/dist/utils/formatting.js +34 -5
  56. package/dist/utils/index.d.ts +1 -0
  57. package/dist/utils/index.js +1 -0
  58. package/dist/utils/spec-formatters.d.ts +13 -23
  59. package/dist/utils/spec-formatters.js +41 -2
  60. package/dist/utils/spec-translations.d.ts +16 -3
  61. package/dist/utils/spec-translations.js +52 -13
  62. package/package.json +12 -6
@@ -71,3 +71,33 @@ export declare function pickLocalized(value: unknown, language: string | undefin
71
71
  * this area survived unread.
72
72
  */
73
73
  export declare function setLocalized(value: unknown, language: string | undefined | null, next: string): string | Record<string, unknown>;
74
+ /**
75
+ * Remove the `language` entry from a possibly-localized value — the CLEAR arm
76
+ * of the same editor {@link setLocalized} serves, and the reason it cannot be
77
+ * spelled as `setLocalized(value, language, '')`.
78
+ *
79
+ * A single-line authoring input has three actions, not two: type, retype, and
80
+ * clear. `setLocalized` answers the first two. Answering the third with "write
81
+ * the empty string" stores `{ en: '' }` — an entry that claims "English has a
82
+ * title and it is blank", which then SHADOWS every display fallback: a viewer
83
+ * in `en` sees nothing where the map's other locales would have been offered.
84
+ * Answering it with "drop the whole value" is the objectui#9274 data loss
85
+ * itself, arriving through the clearing door instead of the typing door.
86
+ *
87
+ * So clearing removes exactly one entry — the same entry {@link setLocalized}
88
+ * would have written, chosen by the same {@link localeWriteKey} limbs, which is
89
+ * what keeps the cleared entry the one the author was actually looking at.
90
+ *
91
+ * - Plain string / `null` / `undefined` / array: there is no map, so there is
92
+ * nothing localized left — `undefined`. The caller decides how its own field
93
+ * spells empty (`undefined` for an optional key, `''` for a required one).
94
+ * - Map with the active entry present: that entry is removed and every other
95
+ * locale is carried across untouched. When it was the LAST entry the result
96
+ * is `undefined` rather than `{}`, because an empty map is not a title.
97
+ * - Map WITHOUT an entry for the active locale: returned unchanged. The box was
98
+ * showing a display fallback borrowed from another locale, and clearing a
99
+ * borrowed string must not delete the locale it was borrowed from — the same
100
+ * refusal {@link localeWriteKey} enforces on the write side, which is why
101
+ * both read the same limbs from the same file.
102
+ */
103
+ export declare function clearLocalized(value: unknown, language: string | undefined | null): Record<string, unknown> | undefined;
@@ -144,3 +144,42 @@ export function setLocalized(value, language, next) {
144
144
  const o = value;
145
145
  return { ...o, [localeWriteKey(o, language)]: next };
146
146
  }
147
+ /**
148
+ * Remove the `language` entry from a possibly-localized value — the CLEAR arm
149
+ * of the same editor {@link setLocalized} serves, and the reason it cannot be
150
+ * spelled as `setLocalized(value, language, '')`.
151
+ *
152
+ * A single-line authoring input has three actions, not two: type, retype, and
153
+ * clear. `setLocalized` answers the first two. Answering the third with "write
154
+ * the empty string" stores `{ en: '' }` — an entry that claims "English has a
155
+ * title and it is blank", which then SHADOWS every display fallback: a viewer
156
+ * in `en` sees nothing where the map's other locales would have been offered.
157
+ * Answering it with "drop the whole value" is the objectui#9274 data loss
158
+ * itself, arriving through the clearing door instead of the typing door.
159
+ *
160
+ * So clearing removes exactly one entry — the same entry {@link setLocalized}
161
+ * would have written, chosen by the same {@link localeWriteKey} limbs, which is
162
+ * what keeps the cleared entry the one the author was actually looking at.
163
+ *
164
+ * - Plain string / `null` / `undefined` / array: there is no map, so there is
165
+ * nothing localized left — `undefined`. The caller decides how its own field
166
+ * spells empty (`undefined` for an optional key, `''` for a required one).
167
+ * - Map with the active entry present: that entry is removed and every other
168
+ * locale is carried across untouched. When it was the LAST entry the result
169
+ * is `undefined` rather than `{}`, because an empty map is not a title.
170
+ * - Map WITHOUT an entry for the active locale: returned unchanged. The box was
171
+ * showing a display fallback borrowed from another locale, and clearing a
172
+ * borrowed string must not delete the locale it was borrowed from — the same
173
+ * refusal {@link localeWriteKey} enforces on the write side, which is why
174
+ * both read the same limbs from the same file.
175
+ */
176
+ export function clearLocalized(value, language) {
177
+ if (value == null || typeof value !== 'object' || Array.isArray(value))
178
+ return undefined;
179
+ const o = value;
180
+ const key = localeWriteKey(o, language);
181
+ if (!Object.prototype.hasOwnProperty.call(o, key))
182
+ return o;
183
+ const rest = Object.fromEntries(Object.entries(o).filter(([k]) => k !== key));
184
+ return Object.keys(rest).length > 0 ? rest : undefined;
185
+ }
@@ -17,6 +17,22 @@ import { type I18nConfig } from './i18n.js';
17
17
  * Exported so an app that builds its own i18next instance (and therefore owns
18
18
  * its bootstrap language — see {@link I18nProviderProps.instance}) can honour
19
19
  * the same preference, and so a sign-out flow can clear it.
20
+ *
21
+ * ⚠️ **What this slot means changed for a SIGNED-IN user (objectui#10059).**
22
+ * A signed-in user's language has one source of truth and it is not on the
23
+ * device: it is the server column `sys_user.locale`, read back through
24
+ * `GET /auth/me/localization` and written by both the profile page's language
25
+ * card and the console's globe menu. For that user this slot is a CACHE of the
26
+ * column — it still decides the first paint, because the column cannot be read
27
+ * synchronously, and it is then overwritten by whatever the column says. It
28
+ * decides on its own only BEFORE sign-in, which is the case this provider can
29
+ * see and the only one it adjudicates.
30
+ *
31
+ * This provider deliberately does not implement that rule: reading a `sys_user`
32
+ * row needs an authenticated data adapter, which is a host concern and not a
33
+ * dependency a renderer package takes. The host applies the column through
34
+ * {@link I18nContextValue.changeLanguage} like any other switch — in this
35
+ * repository, `@object-ui/app-shell`'s `useUserLocale`.
20
36
  */
21
37
  export declare const LOCALE_STORAGE_KEY = "objectui-locale";
22
38
  /**
@@ -29,28 +45,53 @@ export declare const LOCALE_STORAGE_KEY = "objectui-locale";
29
45
  */
30
46
  export declare function readStoredLanguage(): string | null;
31
47
  /**
32
- * `localStorage` key caching the TENANT's server-side locale — a *seed*, not a
33
- * choice (objectui#4035).
48
+ * `localStorage` key caching the server's resolved locale for the signed-in
49
+ * caller — a *seed*, not a choice (objectui#4035).
50
+ *
51
+ * ⚠️ **The value is per-USER; the slot is per-DEVICE (objectui#10193).**
52
+ * `GET /auth/me/localization` answers `locale` as the caller's own
53
+ * `sys_user.locale` when set, else the request's `Accept-Language`, else the
54
+ * deployment default — and answers no locale at all before sign-in. It is not
55
+ * a tenant setting, whatever earlier prose here said. The slot is deliberately
56
+ * NOT keyed by user: the boot reads it before anyone is signed in, when no
57
+ * user id exists to key by. What bounds it to one owner is
58
+ * `@object-ui/auth`'s change-of-owner purge (objectui#5664), which deletes it
59
+ * when a different user adopts the browser, and which the host answers by
60
+ * re-resolving the live language (`@object-ui/app-shell`'s
61
+ * `useSignedInUserLocale`).
62
+ *
63
+ * **What a visitor who is not signed in reads**, declared as device behaviour:
64
+ * the language the LAST signed-in owner of this browser was served, until
65
+ * someone else signs in. That is the only server answer this device has ever
66
+ * received — the endpoint gives none before sign-in — and the sign-in page is
67
+ * the one surface it paints.
34
68
  *
35
69
  * Deliberately a separate slot from {@link LOCALE_STORAGE_KEY}, and that
36
70
  * separation is the whole point rather than a tidiness preference. The two
37
71
  * values have different provenance and therefore different rights:
38
72
  *
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).
73
+ * - {@link LOCALE_STORAGE_KEY} is what the *user* picked on this device. It
74
+ * outranks everything this provider can see and must survive a change in
75
+ * what the server answers for the seed below. ⚠️ It does NOT outrank the signed-in user's own
76
+ * `sys_user.locale`, which no tier here represents — see that key's own
77
+ * docblock (objectui#10059).
78
+ * - This slot is what the server resolved for the signed-in caller, cached so
79
+ * the next boot can apply it synchronously. It outranks only the environment
80
+ * (browser language).
43
81
  *
44
82
  * Writing the server seed into the explicit-choice slot would erase that
45
83
  * 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
84
+ * deliberate user choice, so it would (a) outrank a later server-side change
85
+ * and pin the device to a stale locale forever, and (b) suppress browser
86
+ * detection for
48
87
  * a user who never expressed a preference. Only a real switch promotes a
49
88
  * language to the explicit slot — see the `languageChanged` choke point.
50
89
  */
51
90
  export declare const LOCALE_SEED_STORAGE_KEY = "objectui-locale-seed";
52
91
  /**
53
- * Read the cached tenant locale seed, or `null` when nothing is cached.
92
+ * Read the cached locale seed — the server's last resolved locale for this
93
+ * browser's last signed-in caller (see {@link LOCALE_SEED_STORAGE_KEY}) — or
94
+ * `null` when nothing is cached.
54
95
  *
55
96
  * Same defensive posture as {@link readStoredLanguage}: `localStorage` is
56
97
  * absent under SSR and *throws* in Safari private mode / partitioned iframes,
@@ -58,12 +99,13 @@ export declare const LOCALE_SEED_STORAGE_KEY = "objectui-locale-seed";
58
99
  */
59
100
  export declare function readCachedLanguageSeed(): string | null;
60
101
  /**
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).
102
+ * Cache the locale the server just resolved for the signed-in caller — the
103
+ * "revalidate" half of the stale-while-revalidate contract (objectstack#5419,
104
+ * ruling point 2).
63
105
  *
64
106
  * Call this with whatever `/auth/me/localization` actually returned for an
65
107
  * authenticated caller. A `null`/`undefined` locale CLEARS the cache rather
66
- * than leaving the old value: a tenant that unsets its locale must reach
108
+ * than leaving the old value: a server answer that drops the locale must reach
67
109
  * choice-less devices on their next boot, and "keep the last good seed forever"
68
110
  * is precisely the being-pinned-by-a-stale-seed failure the ruling forbids.
69
111
  *
@@ -72,6 +114,53 @@ export declare function readCachedLanguageSeed(): string | null;
72
114
  * cannot complete.
73
115
  */
74
116
  export declare function cacheLanguageSeed(locale: string | null | undefined): void;
117
+ /**
118
+ * Options shared by {@link resolveBootstrapLanguage} and
119
+ * {@link preloadBootstrapLocale}. Each field mirrors the {@link I18nProvider}
120
+ * prop of the same name, because the two must answer identically — a host that
121
+ * preloads one catalogue while the provider boots into another has bought a
122
+ * flash of `en` instead of avoiding one.
123
+ */
124
+ export interface BootstrapLocaleOptions {
125
+ /** The `config` you will pass to {@link I18nProvider}. */
126
+ config?: I18nConfig;
127
+ /** The `persistLanguage` you will pass to {@link I18nProvider}. Default `true`. */
128
+ persistLanguage?: boolean;
129
+ /** Whether you will pass a `loadLanguage` loader. Default `false`. */
130
+ hasLoader?: boolean;
131
+ }
132
+ /**
133
+ * The language {@link I18nProvider} will boot in, computed WITHOUT creating an
134
+ * i18next instance (objectui#7479).
135
+ *
136
+ * The full precedence chain, unchanged: explicit choice → seed →
137
+ * browser language → `defaultLanguage` → `en`.
138
+ */
139
+ export declare function resolveBootstrapLanguage(options?: BootstrapLocaleOptions): string;
140
+ /**
141
+ * Fetch the catalogue {@link I18nProvider} will boot in, BEFORE you render it.
142
+ *
143
+ * ## Why a host would await this
144
+ *
145
+ * Built-in catalogues are lazy since objectui#7479, so the provider can be in
146
+ * one of two states on its first render, and the difference is visible:
147
+ *
148
+ * - **catalogue already resident** — the first paint is in the user's
149
+ * language, exactly as it was when all ten packs shipped eagerly;
150
+ * - **catalogue still in flight** — the first paint renders through
151
+ * `fallbackLng: 'en'` and re-renders in the user's language when the fetch
152
+ * lands. Correct, never blank, and a flash of English for a `zh-CN` viewer.
153
+ *
154
+ * ⇒ any host that owns an await before `createRoot().render()` should spend it
155
+ * here; `apps/console/src/main.tsx` does, alongside the runtime-config and auth
156
+ * preflight round-trips it already waits on. A host that has no such seam gets
157
+ * the documented `en`-while-loading behaviour instead, which is why this is a
158
+ * function a host CALLS rather than a requirement the provider imposes.
159
+ *
160
+ * Never rejects: a catalogue that will not download must not take the boot
161
+ * down, and the provider retries on mount.
162
+ */
163
+ export declare function preloadBootstrapLocale(options?: BootstrapLocaleOptions): Promise<void>;
75
164
  interface I18nContextValue {
76
165
  /** Current language code */
77
166
  language: string;