@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.
- package/CHANGELOG.md +1929 -1
- package/README.md +41 -12
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -2
- package/dist/index.d.ts +23 -21
- package/dist/index.js +31 -21
- package/dist/locales/ar.d.ts +638 -369
- package/dist/locales/ar.js +744 -424
- package/dist/locales/de.d.ts +638 -369
- package/dist/locales/de.js +759 -446
- package/dist/locales/en.d.ts +673 -404
- package/dist/locales/en.js +899 -406
- package/dist/locales/es.d.ts +638 -369
- package/dist/locales/es.js +739 -427
- package/dist/locales/fr.d.ts +638 -369
- package/dist/locales/fr.js +738 -423
- package/dist/locales/index.d.ts +6476 -3786
- package/dist/locales/index.js +20 -20
- package/dist/locales/ja.d.ts +638 -369
- package/dist/locales/ja.js +732 -417
- package/dist/locales/ko.d.ts +638 -369
- package/dist/locales/ko.js +736 -424
- package/dist/locales/pt.d.ts +638 -369
- package/dist/locales/pt.js +737 -425
- package/dist/locales/ru.d.ts +638 -369
- package/dist/locales/ru.js +750 -424
- package/dist/locales/zh.d.ts +672 -403
- package/dist/locales/zh.js +720 -403
- package/dist/pickLocalized.d.ts +58 -0
- package/dist/pickLocalized.js +115 -7
- package/dist/provider.d.ts +84 -2
- package/dist/provider.js +305 -31
- package/dist/useDisplayLocale.d.ts +49 -0
- package/dist/useDisplayLocale.js +55 -0
- package/dist/useObjectLabel.d.ts +94 -14
- package/dist/useObjectLabel.js +134 -21
- package/dist/useSafeTranslation.js +51 -3
- package/dist/utils/index.d.ts +4 -3
- package/dist/utils/index.js +4 -3
- package/dist/utils/number-display.d.ts +32 -0
- package/dist/utils/number-display.js +32 -0
- package/dist/utils/spec-formatters.d.ts +39 -13
- package/dist/utils/spec-formatters.js +3 -3
- package/package.json +4 -3
package/dist/useObjectLabel.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
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
|
|
202
|
-
|
|
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
|
-
*
|
|
254
|
-
*
|
|
319
|
+
* Read `{ns}.apps.{appName}.navigation.{groupId}.label` out of the loaded
|
|
320
|
+
* CLIENT translation resources, falling back to `fallback`.
|
|
255
321
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
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.
|
|
495
|
-
//
|
|
496
|
-
//
|
|
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
|
-
|
|
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
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/utils/index.js
CHANGED
|
@@ -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 -
|
|
9
|
+
* @object-ui/i18n - locale formatting helpers
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 @
|
|
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
|
-
*
|
|
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 @
|
|
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
|
-
*
|
|
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
|
|
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 @
|
|
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 @
|
|
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
|
|
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
|
+
"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
|
}
|