@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +1929 -1
  2. package/README.md +41 -12
  3. package/dist/i18n.d.ts +1 -1
  4. package/dist/i18n.js +1 -2
  5. package/dist/index.d.ts +23 -21
  6. package/dist/index.js +31 -21
  7. package/dist/locales/ar.d.ts +638 -369
  8. package/dist/locales/ar.js +744 -424
  9. package/dist/locales/de.d.ts +638 -369
  10. package/dist/locales/de.js +759 -446
  11. package/dist/locales/en.d.ts +673 -404
  12. package/dist/locales/en.js +899 -406
  13. package/dist/locales/es.d.ts +638 -369
  14. package/dist/locales/es.js +739 -427
  15. package/dist/locales/fr.d.ts +638 -369
  16. package/dist/locales/fr.js +738 -423
  17. package/dist/locales/index.d.ts +6476 -3786
  18. package/dist/locales/index.js +20 -20
  19. package/dist/locales/ja.d.ts +638 -369
  20. package/dist/locales/ja.js +732 -417
  21. package/dist/locales/ko.d.ts +638 -369
  22. package/dist/locales/ko.js +736 -424
  23. package/dist/locales/pt.d.ts +638 -369
  24. package/dist/locales/pt.js +737 -425
  25. package/dist/locales/ru.d.ts +638 -369
  26. package/dist/locales/ru.js +750 -424
  27. package/dist/locales/zh.d.ts +672 -403
  28. package/dist/locales/zh.js +720 -403
  29. package/dist/pickLocalized.d.ts +58 -0
  30. package/dist/pickLocalized.js +115 -7
  31. package/dist/provider.d.ts +84 -2
  32. package/dist/provider.js +305 -31
  33. package/dist/useDisplayLocale.d.ts +49 -0
  34. package/dist/useDisplayLocale.js +55 -0
  35. package/dist/useObjectLabel.d.ts +94 -14
  36. package/dist/useObjectLabel.js +134 -21
  37. package/dist/useSafeTranslation.js +51 -3
  38. package/dist/utils/index.d.ts +4 -3
  39. package/dist/utils/index.js +4 -3
  40. package/dist/utils/number-display.d.ts +32 -0
  41. package/dist/utils/number-display.js +32 -0
  42. package/dist/utils/spec-formatters.d.ts +39 -13
  43. package/dist/utils/spec-formatters.js +3 -3
  44. package/package.json +4 -3
@@ -17,8 +17,8 @@
17
17
  * @module useObjectLabel
18
18
  */
19
19
  import { useMemo } from 'react';
20
- import { useObjectTranslation } from './provider';
21
- import { I18N_PROBE_FLAG } from './i18n';
20
+ import { useObjectTranslation } from './provider.js';
21
+ import { I18N_PROBE_FLAG } from './i18n.js';
22
22
  /**
23
23
  * Built-in Object UI top-level locale keys — not app namespaces.
24
24
  * Update this set when new top-level platform translation keys are added
@@ -30,6 +30,41 @@ const BUILTIN_KEYS = new Set([
30
30
  'list', 'kanban', 'chart', 'dashboard', 'configPanel',
31
31
  'appDesigner', 'console', 'errors', 'detail',
32
32
  ]);
33
+ /**
34
+ * Whether `i18n` is a real i18next instance rather than react-i18next's
35
+ * no-instance placeholder.
36
+ *
37
+ * With nothing to bind to, `useTranslation` warns `NO_I18NEXT_INSTANCE` and
38
+ * builds its return value out of a fresh `{}` on every render (`const
39
+ * finalI18n = i18n || {}`, which then feeds that hook's own `useMemo` deps), so
40
+ * the `i18n` we receive has a new identity each render even though nothing
41
+ * about it can have changed.
42
+ *
43
+ * `getResourceBundle` is the probe because it is the only instance member the
44
+ * resolvers below ever touch — via `getAppNamespaces`, the single gate every
45
+ * `t()` call in this module sits behind.
46
+ */
47
+ function hasUsableI18nInstance(i18n) {
48
+ return Boolean(i18n) && typeof i18n?.getResourceBundle === 'function';
49
+ }
50
+ /**
51
+ * Stand-ins pinned into the memo dependency list while no i18next instance is
52
+ * bound, so the memo in {@link useObjectLabel} actually holds on the
53
+ * no-provider path (objectui#5564). Module-level, so they carry one identity
54
+ * for the lifetime of the process instead of one per render.
55
+ *
56
+ * Substituting them is unobservable rather than merely convenient: every `t()`
57
+ * call in this module sits inside a `for (… of getAppNamespaces())` loop, and
58
+ * `getAppNamespaces()` returns `[]` under exactly {@link hasUsableI18nInstance}
59
+ * — so for as long as the substitution is in effect, the closures cannot read
60
+ * either value at all. `NO_INSTANCE_T` still mirrors react-i18next's own
61
+ * not-ready `t` (honour a string `defaultValue`, otherwise echo the key) so
62
+ * that if that reachability argument ever stops holding, behaviour does not
63
+ * change silently.
64
+ */
65
+ const NO_INSTANCE_T = ((key, options) => typeof options?.defaultValue === 'string' ? options.defaultValue : key);
66
+ /** @see NO_INSTANCE_T */
67
+ const NO_INSTANCE_I18N = Object.freeze({});
33
68
  /**
34
69
  * Hook for convention-based auto-resolution of object and field labels.
35
70
  *
@@ -46,7 +81,21 @@ const BUILTIN_KEYS = new Set([
46
81
  * ```
47
82
  */
48
83
  export function useObjectLabel() {
49
- const { t, i18n } = useObjectTranslation();
84
+ const { t: boundT, i18n: boundI18n } = useObjectTranslation();
85
+ // Pin both memo dependencies to module constants while there is no i18next
86
+ // instance to bind to. Without this the memo below never held on exactly the
87
+ // no-provider path `useSafeFieldLabel` advertises: react-i18next rebuilds its
88
+ // return value from a fresh `{}` each render, so `i18n` arrived with a new
89
+ // identity every time and re-keyed every consumer memo this hook feeds
90
+ // (objectui#5564 measured 4 distinct identities across 4 renders). See
91
+ // `hasUsableI18nInstance` for why the substitution cannot be observed.
92
+ //
93
+ // When an instance does appear these become the live values again, so a
94
+ // provider mounting after first render recomputes the object exactly once and
95
+ // resolves real translations from then on.
96
+ const bound = hasUsableI18nInstance(boundI18n);
97
+ const t = bound ? boundT : NO_INSTANCE_T;
98
+ const i18n = bound ? boundI18n : NO_INSTANCE_I18N;
50
99
  // Memoize the entire returned object — all closures below reference `t`/`i18n`
51
100
  // and stay valid until the language changes. Returning a fresh object on every
52
101
  // render was busting downstream `useMemo`/`useCallback` deps in heavy consumers
@@ -64,7 +113,7 @@ export function useObjectLabel() {
64
113
  * (objectui#3372).
65
114
  */
66
115
  const getAppNamespaces = () => {
67
- if (!i18n || typeof i18n.getResourceBundle !== 'function')
116
+ if (!hasUsableI18nInstance(i18n))
68
117
  return [];
69
118
  const lang = i18n.language || 'en';
70
119
  const bundle = i18n.getResourceBundle(lang, 'translation');
@@ -188,19 +237,36 @@ export function useObjectLabel() {
188
237
  /**
189
238
  * Build suffix candidates for a list-view scoped key.
190
239
  *
191
- * The runtime uses qualified view ids (`<objectName>.<viewName>`) in URLs
192
- * and metadata records, while translation bundles are keyed by the authored
193
- * view name under `_views`. Resolve the unqualified name first so every
194
- * surface can pass its canonical id without leaking the metadata fallback.
240
+ * The runtime uses qualified view ids (`<objectName>.<viewName>`) in URLs and
241
+ * metadata records, while translation bundles are keyed by the **bare** view
242
+ * name under `_views` — the runtime view identity's own name, stripped of the
243
+ * object prefix. That single spelling is canonical per the objectstack#5164
244
+ * ruling A (2026-08-06): the i18n extractor asks the view composer for the key
245
+ * (objectstack#6124) and `packages/lint` enforces exactly that spelling
246
+ * (objectstack#6038), so this resolver accepts exactly what those produce.
247
+ *
248
+ * Deliberately NOT a second candidate: the prefixed full name
249
+ * (`_views.<objectName>.<viewName>`). Accepting it made this client more
250
+ * lenient than the server-side resolver, which only ever reads the one key
251
+ * (objectstack#5165) — a bundle authored against the prefixed spelling showed
252
+ * translated labels in the Console while every consumer that does not run a
253
+ * second resolution pass (REST boundary, mobile, plain HTTP, SDUI) still got
254
+ * English. A prefixed key now simply misses, and the label falls back to the
255
+ * metadata default on every surface alike, so the authoring mistake is visible
256
+ * instead of half-hidden. It is caught at authoring time by `os lint`'s
257
+ * `translation-target-unknown`, not papered over here.
258
+ *
259
+ * The object-name candidates (`objects.<ns__obj>` then `objects.<obj>`) are a
260
+ * separate axis and stay: they let bundles written against short object names
261
+ * resolve when the runtime presents fully-qualified ones.
195
262
  */
196
263
  const viewSuffixes = (objectName, viewName, tail) => {
197
264
  const objectNames = [objectName, stripNamespace(objectName)];
198
265
  const matchedPrefix = objectNames
199
266
  .map((name) => `${name}.`)
200
267
  .find((prefix) => viewName.startsWith(prefix));
201
- const shortViewName = matchedPrefix ? viewName.slice(matchedPrefix.length) : viewName;
202
- const viewNames = shortViewName === viewName ? [viewName] : [shortViewName, viewName];
203
- return viewNames.flatMap((name) => objectSuffixes(objectName, `_views.${name}.${tail}`));
268
+ const bareViewName = matchedPrefix ? viewName.slice(matchedPrefix.length) : viewName;
269
+ return objectSuffixes(objectName, `_views.${bareViewName}.${tail}`);
204
270
  };
205
271
  return {
206
272
  /**
@@ -250,13 +316,27 @@ export function useObjectLabel() {
250
316
  return resolved || undefined;
251
317
  },
252
318
  /**
253
- * Resolve translated label for a navigation group within an app.
254
- * Convention: `{ns}.apps.{appName}.navigation.{groupId}.label`.
319
+ * Read `{ns}.apps.{appName}.navigation.{groupId}.label` out of the loaded
320
+ * CLIENT translation resources, falling back to `fallback`.
255
321
  *
256
- * Mirrors `objectLabel`/`dashboardLabel` so app metadata can keep
257
- * English fallbacks while translation packs supply localised
258
- * sidebar group labels (e.g. "Sales" → "销售") without explicit
259
- * I18nLabel `{ key, defaultValue }` annotations.
322
+ * ⚠️ This does NOT localize the sidebar, and adding a translation pack
323
+ * keyed this way will not change a single rendered nav label.
324
+ * **App-navigation localization is owned solely by the server-side
325
+ * `/meta` boundary**: `translateApp` in `@objectstack/spec`
326
+ * (`src/system/i18n-resolver.ts`) rewrites every navigation node's
327
+ * `label` by id, and `@objectstack/rest` applies it before the metadata
328
+ * reaches this client — so nav labels arrive already localized. One
329
+ * owner, not two. To translate a sidebar group, translate it there.
330
+ *
331
+ * History (objectui#5197): until then this docstring promised
332
+ * `"Sales" → "销售"` for sidebar groups, and `NavigationRenderer`
333
+ * accepted `resolveGroupLabel`/`resolveItemLabel` to wire it up. Those
334
+ * props could never fire — the renderer's `isCustomized` guard compared a
335
+ * node's authored label against its own `id` (`Workspace` vs
336
+ * `grp_workspace`), which never match, so the guard was true for every
337
+ * real entry. The props are gone; the promise was false, not merely
338
+ * unused. This helper is kept as a plain key reader for a consumer that
339
+ * renders navigation itself — it has no first-party caller.
260
340
  */
261
341
  navGroupLabel: (appName, groupId, fallback) => resolve(`apps.${appName}.navigation.${groupId}.label`, fallback),
262
342
  /**
@@ -297,6 +377,33 @@ export function useObjectLabel() {
297
377
  const resolved = resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.description`), fb);
298
378
  return resolved || undefined;
299
379
  },
380
+ /**
381
+ * Resolve a translated metric-widget SUB-CAPTION within a dashboard.
382
+ * Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.subCaption`.
383
+ * Returns undefined when neither metadata nor translation provides one.
384
+ *
385
+ * Deliberately its OWN key, not a second reader of `widgets.{id}.description`.
386
+ * The KPI card renders two authored strings from two different fields — the
387
+ * shared card header's `widget.description`, and the sub-caption under the
388
+ * value, which is authored as `widget.options.description` — and the
389
+ * objectstack#5428 item-4 ruling (2026-08-06) settled that they get two
390
+ * keys, not one: 「两个作者字段两个 key」. Collapsing them would make one
391
+ * translation entry silently retarget the other field on any widget type
392
+ * that renders both at once (`kpi`, `gauge`, `bullet` — every metric-family
393
+ * type except the self-contained `metric`).
394
+ *
395
+ * `subCaption` is the member objectstack#8056 added to the widget
396
+ * translation node for exactly this, shipped in `@objectstack/spec@17.0.0`.
397
+ * The server-side resolver reads the SAME key and overlays it onto
398
+ * `options.description` (`translateDashboard`), so a document served
399
+ * through `/meta` and a document translated here land on the same string —
400
+ * this is the client half of one convention, not a second dialect.
401
+ */
402
+ widgetSubCaption: (dashboardName, widgetId, fallback) => {
403
+ const fb = fallback ?? '';
404
+ const resolved = resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.subCaption`), fb);
405
+ return resolved || undefined;
406
+ },
300
407
  /**
301
408
  * Resolve translated page label, falling back to pageDef.label.
302
409
  * Convention: `{ns}.pages.{pageName}.label`.
@@ -309,7 +416,10 @@ export function useObjectLabel() {
309
416
  reportLabel: (reportDef) => resolve(reportSuffixes(reportDef.name, 'label'), reportDef.label ?? reportDef.name),
310
417
  /**
311
418
  * Resolve translated list-view label.
312
- * Convention (per @objectstack/spec): `{ns}.objects.{objectName}._views.{viewName}.label`.
419
+ * Convention (per @objectstack/spec): `{ns}.objects.{objectName}._views.{viewName}.label`,
420
+ * where `{viewName}` is the **bare** view name — pass either the bare name or
421
+ * the qualified runtime id, the object prefix is stripped either way. A key
422
+ * that spells the prefix out does not resolve (see `viewSuffixes`).
313
423
  */
314
424
  viewLabel: (objectName, viewName, fallback) => resolve(viewSuffixes(objectName, viewName, 'label'), fallback),
315
425
  /**
@@ -491,8 +601,11 @@ const SAFE_FIELD_LABEL_FALLBACK = {
491
601
  export function useSafeFieldLabel() {
492
602
  // useObjectLabel delegates to the provider-safe useObjectTranslation (react-
493
603
  // i18next falls back to the global instance and never throws), so it needs no
494
- // try/catch — wrapping the hook call would violate rules-of-hooks. It already
495
- // returns a stable memoized object; the module-level fallback stays as a
496
- // defensive default, reached only if it ever returns nullish.
604
+ // try/catch — wrapping the hook call would violate rules-of-hooks. Its memo
605
+ // now holds with or without an i18next instance, so the object below is
606
+ // stable on both paths — it was NOT before objectui#5564, which measured a
607
+ // fresh object every render outside a provider, i.e. the opposite of what
608
+ // this wrapper advertises. The module-level fallback stays as a defensive
609
+ // default, reached only if it ever returns nullish.
497
610
  return useObjectLabel() ?? SAFE_FIELD_LABEL_FALLBACK;
498
611
  }
@@ -3,20 +3,68 @@
3
3
  *
4
4
  * When no I18nProvider is available (e.g., in tests or standalone usage),
5
5
  * this hook falls back to the provided default translations instead of
6
- * returning raw i18n keys.
6
+ * returning raw i18n keys — and, for a key the `defaults` map does not carry,
7
+ * to the call site's own inline `t(key, { defaultValue })` (objectui#3865).
8
+ * The lookup order is i18next's, so a provider-less host and a provider-mounted
9
+ * one render the same string: `defaults[key]` (the pack value's stand-in here)
10
+ * -> `defaultValue` -> the key.
7
11
  *
8
12
  * @param defaults - Fallback English translations keyed by i18n key
9
13
  * @param testKey - A key to test if i18n is properly configured (must be in defaults)
10
14
  */
11
- import { useObjectTranslation } from './provider';
15
+ import { useObjectTranslation } from './provider.js';
16
+ /**
17
+ * The i18next option that names the string to use when the lookup misses.
18
+ *
19
+ * It is a LOOKUP CONTROL, not interpolation data (objectui#3865): it chooses
20
+ * which string is rendered, so it must never also be spliced into a
21
+ * `{{defaultValue}}` hole in the string it chose — or in any other.
22
+ */
23
+ const DEFAULT_VALUE_OPTION = 'defaultValue';
12
24
  export function createSafeTranslation(defaults, testKey) {
13
25
  // Factory-level fallback: one stable reference per defaults map, so
14
26
  // downstream useMemo/useCallback deps don't invalidate every render in
15
27
  // the no-translations case.
16
28
  const fallbackT = (key, options) => {
17
- let value = defaults[key] || key;
29
+ // Lookup order: defaults table -> inline `defaultValue` -> the key itself
30
+ // (objectui#3865). The table is the pack value's stand-in on this path, so
31
+ // it takes the pack's position in i18next's own order — verified against a
32
+ // real i18next 26.3.6 instance configured the way `createI18n` configures
33
+ // it: `t('anchor', { defaultValue: 'INLINE' })` is the pack's `'Anchor
34
+ // value'`, `t('missing', { defaultValue: 'INLINE' })` is `'INLINE'`, and
35
+ // `t('missing')` is `'missing'`.
36
+ //
37
+ // Before this, the inline default was DEAD on both paths for every
38
+ // component behind this factory: the provider path never reaches it
39
+ // (the pack value always wins), and this path did not read it at all —
40
+ // it rendered the raw key and then treated `defaultValue` as one more
41
+ // interpolation variable. A census over all 26 `createSafeTranslation`
42
+ // hooks measured 27 keys whose table lacks them, 21 of those carrying an
43
+ // inline default that used to be dropped (objectui#3865; the fact is
44
+ // recorded on objectui#3810, whose option C rests on it).
45
+ //
46
+ // Non-strings are ignored rather than coerced — this function returns a
47
+ // `string`, and i18next itself declines a `null` default. `||` (not `??`)
48
+ // keeps the whole chain consistent with the pre-existing `defaults[key] ||`
49
+ // step: an empty string at any position falls through to the next.
50
+ const inlineDefault = options?.[DEFAULT_VALUE_OPTION];
51
+ let value = defaults[key] || (typeof inlineDefault === 'string' ? inlineDefault : '') || key;
18
52
  if (options) {
19
53
  for (const [k, v] of Object.entries(options)) {
54
+ // Reserved: see DEFAULT_VALUE_OPTION. Skipped whatever its type, so an
55
+ // ignored non-string default cannot re-enter through this loop.
56
+ //
57
+ // This is the one deliberate divergence from i18next on this path, and
58
+ // it is unreachable with today's strings: i18next passes the whole
59
+ // options object to its interpolator, so it WOULD render
60
+ // `'Fallback: {{defaultValue}}'` as `'Fallback: INLINE'` (measured on
61
+ // 26.3.6) — but no value in this repo spells that hole (zero hits for
62
+ // `{{defaultValue` across packages/apps/examples), and splicing a
63
+ // fallback string into a hole named after itself has no sensible
64
+ // reading. The alternative — letting a call site's fallback text leak
65
+ // into an unrelated table value — is the worse of the two.
66
+ if (k === DEFAULT_VALUE_OPTION)
67
+ continue;
20
68
  // `split(needle).join(value)` — deliberately not `replace`, and not
21
69
  // `replaceAll` either (objectui#3418). This path must agree with
22
70
  // i18next, which serves the *provider* path; any divergence is a
@@ -1,3 +1,4 @@
1
- export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from './formatting';
2
- export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, type SpecPluralRule, type SpecDateFormat, type SpecNumberFormat, type SpecLocaleConfig, } from './spec-formatters';
3
- export { isSpecTranslationData, transformSpecTranslations, type SpecTranslationData, } from './spec-translations';
1
+ export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from './formatting.js';
2
+ export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, type SpecPluralRule, type SpecDateFormat, type SpecNumberFormat, type SpecLocaleConfig, } from './spec-formatters.js';
3
+ export { isSpecTranslationData, transformSpecTranslations, type SpecTranslationData, } from './spec-translations.js';
4
+ export { formatDisplayNumber, shouldGroupDisplayNumber, type DisplayNumberFormatOptions, } from './number-display.js';
@@ -1,3 +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';
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';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * RE-EXPORT ONLY — the implementation moved to `@object-ui/core`
10
+ * (`utils/number-display.ts`) in objectui#4576. Read it there; nothing is
11
+ * redeclared here, so there is no second copy to drift.
12
+ *
13
+ * Why it moved: `@object-ui/core`'s `dataset-format.ts` needs this exact
14
+ * policy, and it could not import it while it lived here — `core` is the
15
+ * React-free engine and this package depends on `i18next`/`react-i18next` and
16
+ * peer-depends on React. So `formatMeasure` kept a parallel `Intl`
17
+ * implementation, and the two drifted apart in the one place `Intl` and a
18
+ * hand-built string disagree: a German session read `1.234,5 %` from a list
19
+ * cell beside `1.234,5%` from a dashboard measure (objectui#4576). The function
20
+ * is pure, so the boundary was never a property of the code — only of where the
21
+ * code sat. Moving it DOWN removed the obstacle; this package gained a
22
+ * dependency on `@object-ui/core` (no cycle: `core` imports nothing from here).
23
+ *
24
+ * This file survives as a re-export rather than being deleted so that BOTH
25
+ * historical import paths keep working byte-compatibly — the package entry
26
+ * (`@object-ui/i18n`, used by `fields`, `components` and `plugin-dashboard`)
27
+ * and the relative `../utils/number-display` this package's own suite uses.
28
+ * `number-display.reexport-identity.test.ts` pins that the two paths resolve to
29
+ * the SAME function object and the same type, so a future edit cannot quietly
30
+ * reintroduce a copy.
31
+ */
32
+ export { formatDisplayNumber, shouldGroupDisplayNumber, type DisplayNumberFormatOptions, } from '@object-ui/core';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * RE-EXPORT ONLY — the implementation moved to `@object-ui/core`
10
+ * (`utils/number-display.ts`) in objectui#4576. Read it there; nothing is
11
+ * redeclared here, so there is no second copy to drift.
12
+ *
13
+ * Why it moved: `@object-ui/core`'s `dataset-format.ts` needs this exact
14
+ * policy, and it could not import it while it lived here — `core` is the
15
+ * React-free engine and this package depends on `i18next`/`react-i18next` and
16
+ * peer-depends on React. So `formatMeasure` kept a parallel `Intl`
17
+ * implementation, and the two drifted apart in the one place `Intl` and a
18
+ * hand-built string disagree: a German session read `1.234,5 %` from a list
19
+ * cell beside `1.234,5%` from a dashboard measure (objectui#4576). The function
20
+ * is pure, so the boundary was never a property of the code — only of where the
21
+ * code sat. Moving it DOWN removed the obstacle; this package gained a
22
+ * dependency on `@object-ui/core` (no cycle: `core` imports nothing from here).
23
+ *
24
+ * This file survives as a re-export rather than being deleted so that BOTH
25
+ * historical import paths keep working byte-compatibly — the package entry
26
+ * (`@object-ui/i18n`, used by `fields`, `components` and `plugin-dashboard`)
27
+ * and the relative `../utils/number-display` this package's own suite uses.
28
+ * `number-display.reexport-identity.test.ts` pins that the two paths resolve to
29
+ * the SAME function object and the same type, so a future edit cannot quietly
30
+ * reintroduce a copy.
31
+ */
32
+ export { formatDisplayNumber, shouldGroupDisplayNumber, } from '@object-ui/core';
@@ -6,18 +6,33 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * @object-ui/i18n - Spec-aligned i18n utilities
9
+ * @object-ui/i18n - locale formatting helpers
10
10
  *
11
- * Runtime consumers for @objectstack/spec v2.0.7 i18n types:
12
- * - PluralRuleSchema → resolvePlural()
13
- * - DateFormatSchema → formatDateSpec()
14
- * - NumberFormatSchema → formatNumberSpec()
15
- * - LocaleConfigSchema → applyLocaleConfig()
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.
16
28
  *
17
29
  * @module spec-formatters
18
30
  */
19
31
  /**
20
- * Spec-aligned PluralRule (mirrors @objectstack/spec PluralRuleSchema).
32
+ * Plural forms for a single translation key, in CLDR categories.
33
+ *
34
+ * Local shape — authored against the protocol's `PluralRuleSchema`, retired in
35
+ * 17.0.0-rc.6 (see the module doc), so there is nothing upstream to derive from.
21
36
  */
22
37
  export interface SpecPluralRule {
23
38
  /** Translation key */
@@ -54,7 +69,10 @@ export interface SpecPluralRule {
54
69
  */
55
70
  export declare function resolvePlural(rule: SpecPluralRule, count: number, locale?: string): string;
56
71
  /**
57
- * Spec-aligned DateFormat (mirrors @objectstack/spec DateFormatSchema).
72
+ * Date/time formatting options passed through to `Intl.DateTimeFormat`.
73
+ *
74
+ * Local shape — authored against the protocol's `DateFormatSchema`, retired in
75
+ * 17.0.0-rc.6 (see the module doc), so there is nothing upstream to derive from.
58
76
  */
59
77
  export interface SpecDateFormat {
60
78
  dateStyle?: 'full' | 'long' | 'medium' | 'short';
@@ -63,7 +81,7 @@ export interface SpecDateFormat {
63
81
  hour12?: boolean;
64
82
  }
65
83
  /**
66
- * Format a date using @objectstack/spec DateFormatSchema configuration.
84
+ * Format a date using a {@link SpecDateFormat} configuration.
67
85
  *
68
86
  * @example
69
87
  * ```ts
@@ -76,7 +94,11 @@ export interface SpecDateFormat {
76
94
  */
77
95
  export declare function formatDateSpec(date: Date | string | number, format: SpecDateFormat, locale?: string): string;
78
96
  /**
79
- * Spec-aligned NumberFormat (mirrors @objectstack/spec NumberFormatSchema).
97
+ * Number formatting options passed through to `Intl.NumberFormat`.
98
+ *
99
+ * Local shape — authored against the protocol's `NumberFormatSchema`, retired
100
+ * in 17.0.0-rc.6 (see the module doc), so there is nothing upstream to derive
101
+ * from.
80
102
  */
81
103
  export interface SpecNumberFormat {
82
104
  style?: 'currency' | 'percent' | 'decimal' | 'unit';
@@ -87,7 +109,7 @@ export interface SpecNumberFormat {
87
109
  useGrouping?: boolean;
88
110
  }
89
111
  /**
90
- * Format a number using @objectstack/spec NumberFormatSchema configuration.
112
+ * Format a number using a {@link SpecNumberFormat} configuration.
91
113
  *
92
114
  * @example
93
115
  * ```ts
@@ -100,7 +122,11 @@ export interface SpecNumberFormat {
100
122
  */
101
123
  export declare function formatNumberSpec(value: number, format: SpecNumberFormat, locale?: string): string;
102
124
  /**
103
- * Spec-aligned LocaleConfig (mirrors @objectstack/spec LocaleConfigSchema).
125
+ * One locale's formatting defaults, resolved by `applyLocaleConfig()`.
126
+ *
127
+ * Local shape — authored against the protocol's `LocaleConfigSchema`, retired
128
+ * in 17.0.0-rc.6 (see the module doc), so there is nothing upstream to derive
129
+ * from.
104
130
  */
105
131
  export interface SpecLocaleConfig {
106
132
  /** BCP 47 language code (e.g., 'en-US', 'zh-CN') */
@@ -115,7 +141,7 @@ export interface SpecLocaleConfig {
115
141
  dateFormat?: SpecDateFormat;
116
142
  }
117
143
  /**
118
- * Apply a LocaleConfigSchema to configure i18n formatting defaults.
144
+ * Apply a {@link SpecLocaleConfig} to configure i18n formatting defaults.
119
145
  * Returns resolved formatting functions bound to the locale config.
120
146
  *
121
147
  * @example
@@ -36,7 +36,7 @@ export function resolvePlural(rule, count, locale = 'en') {
36
36
  return template.replace(/\{count\}/g, String(count));
37
37
  }
38
38
  /**
39
- * Format a date using @objectstack/spec DateFormatSchema configuration.
39
+ * Format a date using a {@link SpecDateFormat} configuration.
40
40
  *
41
41
  * @example
42
42
  * ```ts
@@ -63,7 +63,7 @@ export function formatDateSpec(date, format, locale = 'en') {
63
63
  return new Intl.DateTimeFormat(locale, options).format(d);
64
64
  }
65
65
  /**
66
- * Format a number using @objectstack/spec NumberFormatSchema configuration.
66
+ * Format a number using a {@link SpecNumberFormat} configuration.
67
67
  *
68
68
  * @example
69
69
  * ```ts
@@ -91,7 +91,7 @@ export function formatNumberSpec(value, format, locale = 'en') {
91
91
  return new Intl.NumberFormat(locale, options).format(value);
92
92
  }
93
93
  /**
94
- * Apply a LocaleConfigSchema to configure i18n formatting defaults.
94
+ * Apply a {@link SpecLocaleConfig} to configure i18n formatting defaults.
95
95
  * Returns resolved formatting functions bound to the locale config.
96
96
  *
97
97
  * @example
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/i18n",
3
- "version": "17.3.0",
3
+ "version": "17.6.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",
@@ -31,7 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "i18next": "^26.3.6",
34
- "react-i18next": "^17.0.11"
34
+ "react-i18next": "^17.0.11",
35
+ "@object-ui/core": "17.6.0"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "react": "^18.0.0 || ^19.0.0"
@@ -68,7 +69,7 @@
68
69
  "scripts": {
69
70
  "build": "tsc",
70
71
  "test": "vitest run",
71
- "type-check": "tsc --noEmit",
72
+ "type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
72
73
  "lint": "eslint ."
73
74
  }
74
75
  }