@object-ui/i18n 17.5.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 (58) hide show
  1. package/CHANGELOG.md +4954 -0
  2. package/README.md +234 -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 +28 -2
  10. package/dist/i18n.js +99 -17
  11. package/dist/index.d.ts +17 -23
  12. package/dist/index.js +51 -27
  13. package/dist/locales/ar.d.ts +1113 -370
  14. package/dist/locales/ar.js +1276 -428
  15. package/dist/locales/de.d.ts +785 -370
  16. package/dist/locales/de.js +896 -401
  17. package/dist/locales/en.d.ts +800 -385
  18. package/dist/locales/en.js +1261 -419
  19. package/dist/locales/es.d.ts +867 -370
  20. package/dist/locales/es.js +976 -398
  21. package/dist/locales/fr.d.ts +867 -370
  22. package/dist/locales/fr.js +976 -399
  23. package/dist/locales/index.d.ts +8682 -3761
  24. package/dist/locales/index.js +68 -29
  25. package/dist/locales/ja.d.ts +785 -370
  26. package/dist/locales/ja.js +891 -395
  27. package/dist/locales/ko.d.ts +785 -370
  28. package/dist/locales/ko.js +893 -399
  29. package/dist/locales/pt.d.ts +867 -370
  30. package/dist/locales/pt.js +975 -397
  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 +949 -370
  36. package/dist/locales/ru.js +1093 -419
  37. package/dist/locales/zh.d.ts +803 -388
  38. package/dist/locales/zh.js +899 -388
  39. package/dist/pickLocalized.d.ts +67 -0
  40. package/dist/pickLocalized.js +114 -0
  41. package/dist/provider.d.ts +102 -13
  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 +13 -2
  47. package/dist/useObjectLabel.d.ts +118 -22
  48. package/dist/useObjectLabel.js +148 -23
  49. package/dist/useSafeTranslation.js +41 -43
  50. package/dist/utils/formatting.d.ts +16 -0
  51. package/dist/utils/formatting.js +23 -3
  52. package/dist/utils/index.d.ts +4 -4
  53. package/dist/utils/index.js +4 -4
  54. package/dist/utils/spec-formatters.d.ts +13 -23
  55. package/dist/utils/spec-formatters.js +41 -2
  56. package/dist/utils/spec-translations.d.ts +16 -3
  57. package/dist/utils/spec-translations.js +52 -13
  58. package/package.json +12 -6
@@ -1,4 +1,4 @@
1
- export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from './formatting';
2
- export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, } from './spec-formatters';
3
- export { isSpecTranslationData, transformSpecTranslations, } from './spec-translations';
4
- export { formatDisplayNumber, shouldGroupDisplayNumber, } from './number-display';
1
+ export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from './formatting.js';
2
+ export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, } from './spec-formatters.js';
3
+ export { isSpecTranslationData, transformSpecTranslations, } from './spec-translations.js';
4
+ export { formatDisplayNumber, shouldGroupDisplayNumber, } from './number-display.js';
@@ -5,29 +5,6 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- /**
9
- * @object-ui/i18n - locale formatting helpers
10
- *
11
- * ## Provenance of the four shapes below (objectui#4597)
12
- *
13
- * They were authored against `@objectstack/spec/ui`, which exported
14
- * `PluralRuleSchema`, `DateFormatSchema`, `NumberFormatSchema` and
15
- * `LocaleConfigSchema` up to and including 17.0.0-rc.5. The pinned
16
- * 17.0.0-rc.6 removed all four under ADR-0049 enforce-or-remove
17
- * (objectstack#5055, maintainer ruling 2026-08-06): they carried no key on any
18
- * authorable shape and nothing ever parsed them, so the vocabulary was retired
19
- * rather than given a door. The key sets here were faithful to the retired
20
- * schemas — this is a retirement, not a drift.
21
- *
22
- * Nothing in the pinned protocol models locale formatting today, so the
23
- * interfaces below are declarations this package owns outright, not a view onto
24
- * a protocol type. The retirement note records that localisation returns as
25
- * authorable metadata through a new ADR — "the formatter that reads a
26
- * LocaleConfig first, the vocabulary second" — so re-derive these at that point
27
- * instead of assuming they still track anything upstream.
28
- *
29
- * @module spec-formatters
30
- */
31
8
  /**
32
9
  * Plural forms for a single translation key, in CLDR categories.
33
10
  *
@@ -83,6 +60,19 @@ export interface SpecDateFormat {
83
60
  /**
84
61
  * Format a date using a {@link SpecDateFormat} configuration.
85
62
  *
63
+ * A string is read through `toDisplayDate` (`@object-ui/core`), the one parse
64
+ * step behind the date faces (objectui#10866): a date-only `2026-09-01` names
65
+ * a calendar day and is rebuilt at local midnight of that day, while a value
66
+ * with a time part keeps its instant. The engine's own parse read the
67
+ * date-only form as UTC midnight, so it named August 31st west of UTC, and
68
+ * for every viewer once `timeZone` was west of UTC.
69
+ *
70
+ * `timeZone` therefore applies to an instant only. A date-only value carries
71
+ * no instant for a zone to place, and the local midnight the shared step
72
+ * built reads back as its day only in the local zone, so it is formatted
73
+ * there. A value the shared step refuses, such as a day its month does not
74
+ * have (objectui#10026), comes back as its string.
75
+ *
86
76
  * @example
87
77
  * ```ts
88
78
  * formatDateSpec(new Date(), {
@@ -5,6 +5,30 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ /**
9
+ * @object-ui/i18n - locale formatting helpers
10
+ *
11
+ * ## Provenance of the four shapes below (objectui#4597)
12
+ *
13
+ * They were authored against `@objectstack/spec/ui`, which exported
14
+ * `PluralRuleSchema`, `DateFormatSchema`, `NumberFormatSchema` and
15
+ * `LocaleConfigSchema` up to and including 17.0.0-rc.5. The pinned
16
+ * 17.0.0-rc.6 removed all four under ADR-0049 enforce-or-remove
17
+ * (objectstack#5055, maintainer ruling 2026-08-06): they carried no key on any
18
+ * authorable shape and nothing ever parsed them, so the vocabulary was retired
19
+ * rather than given a door. The key sets here were faithful to the retired
20
+ * schemas — this is a retirement, not a drift.
21
+ *
22
+ * Nothing in the pinned protocol models locale formatting today, so the
23
+ * interfaces below are declarations this package owns outright, not a view onto
24
+ * a protocol type. The retirement note records that localisation returns as
25
+ * authorable metadata through a new ADR — "the formatter that reads a
26
+ * LocaleConfig first, the vocabulary second" — so re-derive these at that point
27
+ * instead of assuming they still track anything upstream.
28
+ *
29
+ * @module spec-formatters
30
+ */
31
+ import { isRealCalendarDate, toDisplayDate } from '@object-ui/core';
8
32
  /**
9
33
  * Resolve a plural form based on count, following CLDR plural rules.
10
34
  * Uses the Intl.PluralRules API for correct locale-aware pluralization.
@@ -38,6 +62,19 @@ export function resolvePlural(rule, count, locale = 'en') {
38
62
  /**
39
63
  * Format a date using a {@link SpecDateFormat} configuration.
40
64
  *
65
+ * A string is read through `toDisplayDate` (`@object-ui/core`), the one parse
66
+ * step behind the date faces (objectui#10866): a date-only `2026-09-01` names
67
+ * a calendar day and is rebuilt at local midnight of that day, while a value
68
+ * with a time part keeps its instant. The engine's own parse read the
69
+ * date-only form as UTC midnight, so it named August 31st west of UTC, and
70
+ * for every viewer once `timeZone` was west of UTC.
71
+ *
72
+ * `timeZone` therefore applies to an instant only. A date-only value carries
73
+ * no instant for a zone to place, and the local midnight the shared step
74
+ * built reads back as its day only in the local zone, so it is formatted
75
+ * there. A value the shared step refuses, such as a day its month does not
76
+ * have (objectui#10026), comes back as its string.
77
+ *
41
78
  * @example
42
79
  * ```ts
43
80
  * formatDateSpec(new Date(), {
@@ -48,15 +85,17 @@ export function resolvePlural(rule, count, locale = 'en') {
48
85
  * ```
49
86
  */
50
87
  export function formatDateSpec(date, format, locale = 'en') {
51
- const d = date instanceof Date ? date : new Date(date);
88
+ const d = toDisplayDate(date);
52
89
  if (isNaN(d.getTime()))
53
90
  return String(date);
91
+ // The value's shape tells the two apart, the same test the shared step makes.
92
+ const dateOnly = typeof date === 'string' && isRealCalendarDate(date);
54
93
  const options = {};
55
94
  if (format.dateStyle)
56
95
  options.dateStyle = format.dateStyle;
57
96
  if (format.timeStyle)
58
97
  options.timeStyle = format.timeStyle;
59
- if (format.timeZone)
98
+ if (format.timeZone && !dateOnly)
60
99
  options.timeZone = format.timeZone;
61
100
  if (format.hour12 !== undefined)
62
101
  options.hour12 = format.hour12;
@@ -50,9 +50,22 @@ export interface SpecTranslationData {
50
50
  /**
51
51
  * Detect whether the given record uses the spec `TranslationData` format.
52
52
  *
53
- * Returns `true` when `data.objects` exists and at least one entry has a
54
- * nested `fields` object (the distinguishing trait vs. an already-flattened
55
- * namespace tree).
53
+ * Returns `true` when the payload carries at least one spec translation group
54
+ * (see {@link SPEC_TRANSLATION_GROUPS}) whose value is an object — every spec
55
+ * group is a record or an object. A bundle is recognised by ANY group it
56
+ * carries: one that translates only object labels, apps, pages, dashboards or
57
+ * flows, with no field label anywhere, is as much a spec payload as one that
58
+ * translates fields (objectui#10235 — this used to require an
59
+ * `objects.<n>.fields` entry, and such a bundle was returned untransformed and
60
+ * read by nothing).
61
+ *
62
+ * What stays `false` is the other shape the console's loader accepts: an
63
+ * i18next resource tree that is already namespaced, as a mock or local-dev
64
+ * server may return it. Its top-level keys are namespace names — a built-in
65
+ * pack namespace such as `common`, an app namespace such as `crm`, or `app`,
66
+ * the one {@link transformSpecTranslations} itself emits — and none of them
67
+ * is a spec group, so feeding the transform's output back in never wraps it
68
+ * twice. A group-named key holding a string or an array is not a group either.
56
69
  */
57
70
  export declare function isSpecTranslationData(data: Record<string, unknown> | null | undefined): data is SpecTranslationData;
58
71
  /**
@@ -23,25 +23,64 @@
23
23
  * console transform forgets to copy it, leaving lookups to fall back to the
24
24
  * untranslated source label.
25
25
  */
26
+ /**
27
+ * The top-level groups of the translation document the server serves at
28
+ * `/api/v1/i18n/translations/:locale` — the keys of the `translations` shape
29
+ * of `@objectstack/spec`'s `GetTranslationsResponseSchema`: every per-app
30
+ * `TranslationData` group plus the platform-only `settings`, because the
31
+ * served document is the merge of the app's bundle and the platform's.
32
+ *
33
+ * Written out rather than imported: `@object-ui/i18n` deliberately does not
34
+ * depend on `@objectstack/spec`. The console does, so the pin lives beside
35
+ * its loader — `apps/console/src/loadLanguage.test.tsx` walks the spec's own
36
+ * key list and requires a payload carrying any one group, alone, to take the
37
+ * transform branch. A group the spec adds and this list lacks turns that test
38
+ * red instead of dropping the group's bundles silently (objectui#10235).
39
+ */
40
+ const SPEC_TRANSLATION_GROUPS = [
41
+ 'objects',
42
+ 'apps',
43
+ 'messages',
44
+ 'globalActions',
45
+ 'dashboards',
46
+ 'datasets',
47
+ 'pages',
48
+ 'flows',
49
+ 'settings',
50
+ 'metadataForms',
51
+ 'settingsCommon',
52
+ // `@objectstack/spec` 17.6.0 serves `picklists` (objectstack `addbbf02`, the
53
+ // `picklist` metadata kind); added at that bump (objectui#11438). The
54
+ // transform below copies it through onto `app` like any other group.
55
+ 'picklists',
56
+ ];
57
+ function isPlainObject(value) {
58
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
59
+ }
26
60
  /**
27
61
  * Detect whether the given record uses the spec `TranslationData` format.
28
62
  *
29
- * Returns `true` when `data.objects` exists and at least one entry has a
30
- * nested `fields` object (the distinguishing trait vs. an already-flattened
31
- * namespace tree).
63
+ * Returns `true` when the payload carries at least one spec translation group
64
+ * (see {@link SPEC_TRANSLATION_GROUPS}) whose value is an object — every spec
65
+ * group is a record or an object. A bundle is recognised by ANY group it
66
+ * carries: one that translates only object labels, apps, pages, dashboards or
67
+ * flows, with no field label anywhere, is as much a spec payload as one that
68
+ * translates fields (objectui#10235 — this used to require an
69
+ * `objects.<n>.fields` entry, and such a bundle was returned untransformed and
70
+ * read by nothing).
71
+ *
72
+ * What stays `false` is the other shape the console's loader accepts: an
73
+ * i18next resource tree that is already namespaced, as a mock or local-dev
74
+ * server may return it. Its top-level keys are namespace names — a built-in
75
+ * pack namespace such as `common`, an app namespace such as `crm`, or `app`,
76
+ * the one {@link transformSpecTranslations} itself emits — and none of them
77
+ * is a spec group, so feeding the transform's output back in never wraps it
78
+ * twice. A group-named key holding a string or an array is not a group either.
32
79
  */
33
80
  export function isSpecTranslationData(data) {
34
- if (!data)
81
+ if (!isPlainObject(data))
35
82
  return false;
36
- const objects = data.objects;
37
- if (!objects || typeof objects !== 'object' || Array.isArray(objects))
38
- return false;
39
- for (const obj of Object.values(objects)) {
40
- if (obj && typeof obj === 'object' && !Array.isArray(obj) && 'fields' in obj) {
41
- return true;
42
- }
43
- }
44
- return false;
83
+ return SPEC_TRANSLATION_GROUPS.some((group) => isPlainObject(data[group]));
45
84
  }
46
85
  /** Top-level keys that are copied through unchanged onto the `app` namespace. */
47
86
  const PASSTHROUGH_TOP_LEVEL_KEYS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/i18n",
3
- "version": "17.5.0",
3
+ "version": "17.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",
@@ -23,6 +23,11 @@
23
23
  "import": "./dist/index.js",
24
24
  "default": "./dist/index.js"
25
25
  },
26
+ "./locales": {
27
+ "types": "./dist/locales/index.d.ts",
28
+ "import": "./dist/locales/index.js",
29
+ "default": "./dist/locales/index.js"
30
+ },
26
31
  "./locales/*": {
27
32
  "types": "./dist/locales/*.d.ts",
28
33
  "import": "./dist/locales/*.js",
@@ -30,9 +35,9 @@
30
35
  }
31
36
  },
32
37
  "dependencies": {
33
- "i18next": "^26.3.6",
38
+ "i18next": "^26.4.0",
34
39
  "react-i18next": "^17.0.11",
35
- "@object-ui/core": "17.5.0"
40
+ "@object-ui/core": "17.7.0"
36
41
  },
37
42
  "peerDependencies": {
38
43
  "react": "^18.0.0 || ^19.0.0"
@@ -41,7 +46,8 @@
41
46
  "@types/react": "19.2.18",
42
47
  "react": "19.2.8",
43
48
  "typescript": "^6.0.3",
44
- "vitest": "^4.1.10"
49
+ "vitest": "^4.1.10",
50
+ "@object-ui/test-support": "17.4.0"
45
51
  },
46
52
  "keywords": [
47
53
  "objectui",
@@ -67,8 +73,8 @@
67
73
  "LICENSE"
68
74
  ],
69
75
  "scripts": {
70
- "build": "tsc",
71
- "test": "vitest run",
76
+ "build": "tsc && node ../../scripts/check-dist-completeness.mjs",
77
+ "test": "vitest run --root ../.. packages/i18n/",
72
78
  "type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
73
79
  "lint": "eslint ."
74
80
  }