@object-ui/i18n 17.6.0 → 17.7.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 (56) hide show
  1. package/CHANGELOG.md +4320 -0
  2. package/README.md +192 -13
  3. package/dist/builtinAggregateLabels.d.ts +40 -0
  4. package/dist/builtinAggregateLabels.js +23 -0
  5. package/dist/currency.d.ts +27 -3
  6. package/dist/currency.js +28 -4
  7. package/dist/fallbackInterpolation.d.ts +79 -0
  8. package/dist/fallbackInterpolation.js +124 -0
  9. package/dist/i18n.d.ts +27 -1
  10. package/dist/i18n.js +99 -16
  11. package/dist/index.d.ts +7 -13
  12. package/dist/index.js +41 -17
  13. package/dist/locales/ar.d.ts +946 -113
  14. package/dist/locales/ar.js +1095 -161
  15. package/dist/locales/de.d.ts +618 -113
  16. package/dist/locales/de.js +715 -134
  17. package/dist/locales/en.d.ts +632 -127
  18. package/dist/locales/en.js +1044 -162
  19. package/dist/locales/es.d.ts +700 -113
  20. package/dist/locales/es.js +794 -130
  21. package/dist/locales/fr.d.ts +700 -113
  22. package/dist/locales/fr.js +795 -132
  23. package/dist/locales/index.d.ts +6990 -1169
  24. package/dist/locales/index.js +48 -9
  25. package/dist/locales/ja.d.ts +618 -113
  26. package/dist/locales/ja.js +708 -128
  27. package/dist/locales/ko.d.ts +618 -113
  28. package/dist/locales/ko.js +712 -132
  29. package/dist/locales/pt.d.ts +700 -113
  30. package/dist/locales/pt.js +794 -130
  31. package/dist/locales/registry.d.ts +102 -0
  32. package/dist/locales/registry.js +220 -0
  33. package/dist/locales/rtl.d.ts +24 -0
  34. package/dist/locales/rtl.js +26 -0
  35. package/dist/locales/ru.d.ts +782 -113
  36. package/dist/locales/ru.js +910 -152
  37. package/dist/locales/zh.d.ts +635 -130
  38. package/dist/locales/zh.js +725 -131
  39. package/dist/pickLocalized.d.ts +30 -0
  40. package/dist/pickLocalized.js +39 -0
  41. package/dist/provider.d.ts +101 -12
  42. package/dist/provider.js +335 -50
  43. package/dist/translateFn.d.ts +29 -0
  44. package/dist/translateFn.js +8 -0
  45. package/dist/useDisplayLocale.d.ts +11 -0
  46. package/dist/useDisplayLocale.js +11 -0
  47. package/dist/useObjectLabel.d.ts +40 -18
  48. package/dist/useObjectLabel.js +46 -14
  49. package/dist/useSafeTranslation.js +40 -42
  50. package/dist/utils/formatting.d.ts +16 -0
  51. package/dist/utils/formatting.js +23 -3
  52. package/dist/utils/spec-formatters.d.ts +13 -23
  53. package/dist/utils/spec-formatters.js +41 -2
  54. package/dist/utils/spec-translations.d.ts +16 -3
  55. package/dist/utils/spec-translations.js +52 -13
  56. package/package.json +12 -6
package/dist/i18n.d.ts CHANGED
@@ -10,7 +10,15 @@ export interface I18nConfig {
10
10
  defaultLanguage?: string;
11
11
  /** Fallback language (default: 'en') */
12
12
  fallbackLanguage?: string;
13
- /** Additional translation resources to merge with built-in locales */
13
+ /**
14
+ * Additional translation resources, **deep-merged** over the built-in packs.
15
+ *
16
+ * The packs' top-level keys are the namespace groups (`common`, `calendar`,
17
+ * `list`, ...), so supplying a partial group merges into it and leaves the
18
+ * group's other keys in place:
19
+ * `{ en: { calendar: { today: 'Heute' } } }` overrides `calendar.today` only.
20
+ * Arrays are replaced wholesale, never concatenated (objectui#7572).
21
+ */
14
22
  resources?: Record<string, Record<string, unknown>>;
15
23
  /** Whether to detect browser language automatically (default: true) */
16
24
  detectBrowserLanguage?: boolean;
@@ -39,6 +47,24 @@ export interface I18nConfig {
39
47
  * API — shared between `i18n.ts` and `useObjectLabel.ts` to avoid drift.
40
48
  */
41
49
  export declare const I18N_PROBE_FLAG = "__ouiLabelProbe";
50
+ /**
51
+ * The language {@link createI18n} will boot in for this config — `defaultLanguage`,
52
+ * overridden by a browser language this renderer can produce.
53
+ *
54
+ * Exported because the answer is needed BEFORE an instance exists
55
+ * (objectui#7479): with the catalogues lazy, a host that wants its first render
56
+ * to already be in the right language has to know which catalogue to await, and
57
+ * the only alternative — create an instance, read `.language`, throw it away —
58
+ * would boot i18next in the wrong locale to find out what the right one is.
59
+ * `I18nProvider` and {@link preloadBootstrapLocale} answer with THIS function,
60
+ * so the host and the provider cannot disagree about which catalogue is active.
61
+ *
62
+ * ⚠️ Browser detection is asked of {@link isBuiltInLanguage} rather than of the
63
+ * merged resource map: a catalogue that has not been fetched yet is still a
64
+ * language this renderer can produce, and asking the map would have made
65
+ * detection depend on load order.
66
+ */
67
+ export declare function pickInitialLanguage(config?: I18nConfig): string;
42
68
  /**
43
69
  * Create and initialize an i18next instance with Object UI defaults
44
70
  */
package/dist/i18n.js CHANGED
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import i18next from 'i18next';
7
7
  import { initReactI18next } from 'react-i18next';
8
- import { builtInLocales, isRTL } from './locales/index.js';
8
+ import { BUILT_IN_LANGUAGE_CODES, getLoadedBuiltInLocales, isBuiltInLanguage, } from './locales/registry.js';
9
+ import { isRTL } from './locales/rtl.js';
9
10
  /**
10
11
  * Internal `t()` option flag set by `useObjectLabel` on its convention-key
11
12
  * probes. The missing-key handler skips any lookup carrying this flag, so the
@@ -37,19 +38,108 @@ function createMissingKeyHandler() {
37
38
  console.warn(`[object-ui i18n] Missing translation for "${key}" (language "${lng}") — falling back to ${fb}.`);
38
39
  };
39
40
  }
41
+ /**
42
+ * A plain object — the only shape the resource merge recurses into. Arrays and
43
+ * `null` are excluded on purpose; see {@link deepMergeTranslations}.
44
+ */
45
+ function isPlainObject(value) {
46
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
47
+ }
48
+ /**
49
+ * Deep-merge caller-supplied translations over a built-in language pack.
50
+ *
51
+ * A language pack's top-level keys ARE the namespace groups (`common`,
52
+ * `calendar`, `list`, ...), each a nested object, so the one-level merge this
53
+ * replaced made `resources: { en: { calendar: { today: 'Heute' } } }` **replace**
54
+ * the whole `calendar` group: `month` / `week` / `day` / `allDay` / `newEvent` /
55
+ * `moreEvents` / `unscheduled` left the instance, `t('calendar.allDay')` returned
56
+ * the bare key, and `calendar.allDay` reached the DOM as literal text
57
+ * (objectui#7572). Packs nest up to four levels below the group
58
+ * (`console.ai.empty.build.title`), so the merge recurses rather than adding one
59
+ * fixed extra level.
60
+ *
61
+ * ARRAYS ARE REPLACED, NOT CONCATENATED — a deliberate choice, not a library
62
+ * default. Measured: no built-in pack carries an array value today (every leaf
63
+ * in all ten packs is a string), so nothing observable rides on this; the rule
64
+ * decides what a future array means. An author who writes an array is naming the
65
+ * whole list, so replacement is the only rule that lets them shorten or reorder
66
+ * one, and the only one that is idempotent when the merge runs again. This is
67
+ * narrower than i18next's own `deepExtend` (which the provider's async
68
+ * `addResourceBundle` path uses): that recurses into arrays index-wise and would
69
+ * leave a longer base array's tail behind — the same silent-hybrid shape this
70
+ * issue is about.
71
+ *
72
+ * `__proto__` is skipped. Recursive assignment, unlike the object spread it
73
+ * replaces, would otherwise reach the prototype setter; i18next's `deepExtend`
74
+ * guards the same key.
75
+ */
76
+ function deepMergeTranslations(base, override) {
77
+ const merged = { ...base };
78
+ for (const key of Object.keys(override)) {
79
+ if (key === '__proto__')
80
+ continue;
81
+ const incoming = override[key];
82
+ const existing = merged[key];
83
+ merged[key] =
84
+ isPlainObject(existing) && isPlainObject(incoming)
85
+ ? deepMergeTranslations(existing, incoming)
86
+ : incoming;
87
+ }
88
+ return merged;
89
+ }
90
+ /**
91
+ * The language {@link createI18n} will boot in for this config — `defaultLanguage`,
92
+ * overridden by a browser language this renderer can produce.
93
+ *
94
+ * Exported because the answer is needed BEFORE an instance exists
95
+ * (objectui#7479): with the catalogues lazy, a host that wants its first render
96
+ * to already be in the right language has to know which catalogue to await, and
97
+ * the only alternative — create an instance, read `.language`, throw it away —
98
+ * would boot i18next in the wrong locale to find out what the right one is.
99
+ * `I18nProvider` and {@link preloadBootstrapLocale} answer with THIS function,
100
+ * so the host and the provider cannot disagree about which catalogue is active.
101
+ *
102
+ * ⚠️ Browser detection is asked of {@link isBuiltInLanguage} rather than of the
103
+ * merged resource map: a catalogue that has not been fetched yet is still a
104
+ * language this renderer can produce, and asking the map would have made
105
+ * detection depend on load order.
106
+ */
107
+ export function pickInitialLanguage(config = {}) {
108
+ const { defaultLanguage = 'en', resources = {}, detectBrowserLanguage = true } = config;
109
+ if (!detectBrowserLanguage || typeof navigator === 'undefined')
110
+ return defaultLanguage;
111
+ const browserLang = navigator.language?.split('-')[0];
112
+ if (!browserLang)
113
+ return defaultLanguage;
114
+ const known = isBuiltInLanguage(browserLang) ||
115
+ Object.prototype.hasOwnProperty.call(resources, browserLang);
116
+ return known ? browserLang : defaultLanguage;
117
+ }
40
118
  /**
41
119
  * Create and initialize an i18next instance with Object UI defaults
42
120
  */
43
121
  export function createI18n(config = {}) {
44
- const { defaultLanguage = 'en', fallbackLanguage = 'en', resources = {}, detectBrowserLanguage = true, interpolation, warnMissingKeys = isDevEnv(), } = config;
45
- // Merge built-in locales with user-provided resources
122
+ const { fallbackLanguage = 'en', resources = {}, interpolation, warnMissingKeys = isDevEnv(), } = config;
123
+ // `defaultLanguage` and `detectBrowserLanguage` are read by
124
+ // {@link pickInitialLanguage}, which owns that precedence for this function
125
+ // AND for the hosts that must know the answer before an instance exists.
126
+ // Merge the catalogues that are RESIDENT with user-provided resources.
127
+ //
128
+ // ⛔ Every built-in code gets a bundle here even when its catalogue has not
129
+ // been fetched yet (objectui#7479). The empty bundle is load-bearing, not a
130
+ // placeholder: it keeps `instance.options.resources` — which
131
+ // {@link getAvailableLanguages}, the browser detection below and i18next's
132
+ // own `hasResourceBundle` all read — enumerating every language this package
133
+ // can produce, instead of shrinking to whatever happened to be loaded at
134
+ // instance-creation time. Until the catalogue lands, `fallbackLng` renders
135
+ // those keys through `en`; `I18nProvider` then deep-merges the real
136
+ // catalogue in WITHOUT overwriting anything `resources` put there, so the
137
+ // precedence below survives the async arrival.
46
138
  const mergedResources = {};
47
- for (const [lang, translations] of Object.entries(builtInLocales)) {
139
+ const residentCatalogues = getLoadedBuiltInLocales();
140
+ for (const lang of BUILT_IN_LANGUAGE_CODES) {
48
141
  mergedResources[lang] = {
49
- translation: {
50
- ...translations,
51
- ...(resources[lang] || {}),
52
- },
142
+ translation: deepMergeTranslations((residentCatalogues[lang] ?? {}), resources[lang] || {}),
53
143
  };
54
144
  }
55
145
  // Add any additional languages from resources not in built-in locales
@@ -58,14 +148,7 @@ export function createI18n(config = {}) {
58
148
  mergedResources[lang] = { translation: translations };
59
149
  }
60
150
  }
61
- // Detect browser language if enabled
62
- let lng = defaultLanguage;
63
- if (detectBrowserLanguage && typeof navigator !== 'undefined') {
64
- const browserLang = navigator.language?.split('-')[0];
65
- if (browserLang && mergedResources[browserLang]) {
66
- lng = browserLang;
67
- }
68
- }
151
+ const lng = pickInitialLanguage(config);
69
152
  const instance = i18next.createInstance();
70
153
  // IMPORTANT: disable react-i18next Suspense. Otherwise `useTranslation()`
71
154
  // calls during bootstrap throw a Suspense promise (the i18n instance is
package/dist/index.d.ts CHANGED
@@ -30,25 +30,19 @@
30
30
  *
31
31
  * @packageDocumentation
32
32
  */
33
- export { createI18n, getDirection, getAvailableLanguages, type I18nConfig, type TranslationKeys } from './i18n.js';
34
- export { I18nProvider, useObjectTranslation, useI18nContext, LOCALE_STORAGE_KEY, readStoredLanguage, LOCALE_SEED_STORAGE_KEY, readCachedLanguageSeed, cacheLanguageSeed, type I18nProviderProps, } from './provider.js';
33
+ export { createI18n, getDirection, getAvailableLanguages, pickInitialLanguage, type I18nConfig, type TranslationKeys, } from './i18n.js';
34
+ export { I18nProvider, useObjectTranslation, useI18nContext, LOCALE_STORAGE_KEY, readStoredLanguage, LOCALE_SEED_STORAGE_KEY, readCachedLanguageSeed, cacheLanguageSeed, resolveBootstrapLanguage, preloadBootstrapLocale, type BootstrapLocaleOptions, type I18nProviderProps, } from './provider.js';
35
35
  export { createSafeTranslation, useSafeTranslate } from './useSafeTranslation.js';
36
+ export { builtinAggregateLabels, type SafeTranslate } from './builtinAggregateLabels.js';
37
+ export type { TranslateFn } from './translateFn.js';
36
38
  export { useObjectLabel, useSafeFieldLabel } from './useObjectLabel.js';
37
- export { builtInLocales, isRTL, RTL_LANGUAGES } from './locales/index.js';
39
+ export { isRTL, RTL_LANGUAGES } from './locales/rtl.js';
40
+ export { BUILT_IN_LANGUAGE_CODES, DEFAULT_BUILT_IN_LANGUAGE, getLoadedBuiltInLocales, isBuiltInLanguage, isBuiltInLocaleLoaded, loadBuiltInLocale, type LocaleCatalogue, } from './locales/registry.js';
38
41
  export { default as en } from './locales/en.js';
39
- export { default as zh } from './locales/zh.js';
40
- export { default as ja } from './locales/ja.js';
41
- export { default as ko } from './locales/ko.js';
42
- export { default as de } from './locales/de.js';
43
- export { default as fr } from './locales/fr.js';
44
- export { default as es } from './locales/es.js';
45
- export { default as pt } from './locales/pt.js';
46
- export { default as ru } from './locales/ru.js';
47
- export { default as ar } from './locales/ar.js';
48
42
  export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from './utils/index.js';
49
43
  export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, type SpecPluralRule, type SpecDateFormat, type SpecNumberFormat, type SpecLocaleConfig, } from './utils/index.js';
50
44
  export { isSpecTranslationData, transformSpecTranslations, type SpecTranslationData, } from './utils/index.js';
51
- export { pickLocalized, setLocalized } from './pickLocalized.js';
45
+ export { pickLocalized, setLocalized, clearLocalized } from './pickLocalized.js';
52
46
  export { LocalizationProvider, useLocalization, type LocalizationValue } from './LocalizationContext.js';
53
47
  export { resolveFieldCurrency } from './currency.js';
54
48
  export { useDisplayLocale } from './useDisplayLocale.js';
package/dist/index.js CHANGED
@@ -31,33 +31,57 @@
31
31
  * @packageDocumentation
32
32
  */
33
33
  // Core i18n setup
34
- export { createI18n, getDirection, getAvailableLanguages } from './i18n.js';
34
+ export { createI18n, getDirection, getAvailableLanguages, pickInitialLanguage, } from './i18n.js';
35
35
  // React integration
36
36
  export { I18nProvider, useObjectTranslation, useI18nContext,
37
37
  // Language-preference persistence: the key the provider writes, and the
38
38
  // reader apps that bring their own i18next instance need to honour it.
39
39
  LOCALE_STORAGE_KEY, readStoredLanguage,
40
- // Tenant locale seed (objectui#4035) — a SEPARATE slot from the explicit
41
- // choice above, and never a substitute for it. An app that fetches its
42
- // tenant's server-side locale caches it here; the provider applies it at
40
+ // Locale seed (objectui#4035) — a SEPARATE slot from the explicit
41
+ // choice above, and never a substitute for it. An app that fetches the
42
+ // server's resolved locale for the last signed-in caller caches it here; the provider applies it at
43
43
  // bootstrap only when the user has chosen nothing themselves.
44
- LOCALE_SEED_STORAGE_KEY, readCachedLanguageSeed, cacheLanguageSeed, } from './provider.js';
44
+ LOCALE_SEED_STORAGE_KEY, readCachedLanguageSeed, cacheLanguageSeed,
45
+ // Resolve the boot language, and fetch its catalogue, BEFORE first render
46
+ // (objectui#7479). A host with an await before `createRoot().render()` spends
47
+ // it here and paints in the user's language; a host without one gets the
48
+ // documented `en`-while-loading render instead.
49
+ resolveBootstrapLanguage, preloadBootstrapLocale, } from './provider.js';
45
50
  // Safe translation hook factory
46
51
  export { createSafeTranslation, useSafeTranslate } from './useSafeTranslation.js';
52
+ // objectui#7258 — locale labels for the server's built-in default measures,
53
+ // handed to core's `buildChartSeries` as `builtinAggregateLabels`.
54
+ export { builtinAggregateLabels } from './builtinAggregateLabels.js';
47
55
  // Convention-based object/field label i18n
48
56
  export { useObjectLabel, useSafeFieldLabel } from './useObjectLabel.js';
49
- // Locale packs
50
- export { builtInLocales, isRTL, RTL_LANGUAGES } from './locales/index.js';
57
+ // Locale packs — ⛔ ONE catalogue is statically re-exported here, not ten
58
+ // (objectui#7479).
59
+ //
60
+ // This entry used to re-export all ten packs and `builtInLocales`, which made
61
+ // every one of them statically reachable from any import of this package. The
62
+ // console measured the result: ~450 KB gzipped of translation data in the EAGER
63
+ // closure, ten catalogues fetched before first paint for a viewer who reads
64
+ // one, and every new key paid for by every visitor in every language.
65
+ //
66
+ // ⛔ Do not restore a static re-export of a second catalogue here, and do not
67
+ // re-export `builtInLocales` from this entry. Both put all ten back:
68
+ // `builtInLocales` names every pack in one object literal, so nothing can shake
69
+ // it. `scripts/check-eager-locale-catalogues.mjs` fails the build if a
70
+ // non-active catalogue reappears in the console's eager closure, and
71
+ // `src/__tests__/entry-locale-shape-7479.test.ts` fails on the SOURCE shape so
72
+ // the reason is legible without a console build.
73
+ //
74
+ // What replaced them:
75
+ // - `en` stays — it is `fallbackLng`, the source of `TranslationKeys`, and
76
+ // the synchronous dictionary the app-shell splash renders from before i18n
77
+ // is usable. See `./locales/registry.js` for the full argument.
78
+ // - `loadBuiltInLocale(code)` fetches any of the ten on demand.
79
+ // - `BUILT_IN_LANGUAGE_CODES` enumerates them at zero payload.
80
+ // - `@object-ui/i18n/locales` is the explicit, published door to all ten for
81
+ // a caller that genuinely wants them all resident (the parity suites do).
82
+ export { isRTL, RTL_LANGUAGES } from './locales/rtl.js';
83
+ export { BUILT_IN_LANGUAGE_CODES, DEFAULT_BUILT_IN_LANGUAGE, getLoadedBuiltInLocales, isBuiltInLanguage, isBuiltInLocaleLoaded, loadBuiltInLocale, } from './locales/registry.js';
51
84
  export { default as en } from './locales/en.js';
52
- export { default as zh } from './locales/zh.js';
53
- export { default as ja } from './locales/ja.js';
54
- export { default as ko } from './locales/ko.js';
55
- export { default as de } from './locales/de.js';
56
- export { default as fr } from './locales/fr.js';
57
- export { default as es } from './locales/es.js';
58
- export { default as pt } from './locales/pt.js';
59
- export { default as ru } from './locales/ru.js';
60
- export { default as ar } from './locales/ar.js';
61
85
  // Formatting utilities
62
86
  export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from './utils/index.js';
63
87
  // Spec-aligned formatters (v2.0.7)
@@ -66,7 +90,7 @@ export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, } f
66
90
  // `useObjectLabel`). Exposed so apps don't have to copy/paste — see
67
91
  // `apps/console/src/loadLanguage.ts` for the canonical consumer.
68
92
  export { isSpecTranslationData, transformSpecTranslations, } from './utils/index.js';
69
- export { pickLocalized, setLocalized } from './pickLocalized.js';
93
+ export { pickLocalized, setLocalized, clearLocalized } from './pickLocalized.js';
70
94
  export { LocalizationProvider, useLocalization } from './LocalizationContext.js';
71
95
  export { resolveFieldCurrency } from './currency.js';
72
96
  // The one number-display formatter (objectui#4033) and the locale it formats