@object-ui/i18n 17.6.0 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5408 -0
- package/README.md +255 -13
- package/dist/LocalizationContext.d.ts +32 -9
- package/dist/LocalizationContext.js +32 -10
- package/dist/builtinAggregateLabels.d.ts +40 -0
- package/dist/builtinAggregateLabels.js +23 -0
- package/dist/currency.d.ts +27 -3
- package/dist/currency.js +28 -4
- package/dist/fallbackInterpolation.d.ts +79 -0
- package/dist/fallbackInterpolation.js +124 -0
- package/dist/i18n.d.ts +27 -1
- package/dist/i18n.js +99 -16
- package/dist/index.d.ts +8 -13
- package/dist/index.js +45 -17
- package/dist/locales/ar.d.ts +1504 -188
- package/dist/locales/ar.js +1711 -281
- package/dist/locales/de.d.ts +1080 -188
- package/dist/locales/de.js +1248 -261
- package/dist/locales/en.d.ts +1141 -249
- package/dist/locales/en.js +1714 -295
- package/dist/locales/es.d.ts +1186 -188
- package/dist/locales/es.js +1350 -255
- package/dist/locales/fr.d.ts +1186 -188
- package/dist/locales/fr.js +1351 -258
- package/dist/locales/index.d.ts +11940 -2033
- package/dist/locales/index.js +48 -9
- package/dist/locales/ja.d.ts +1080 -188
- package/dist/locales/ja.js +1240 -254
- package/dist/locales/ko.d.ts +1080 -188
- package/dist/locales/ko.js +1244 -258
- package/dist/locales/pt.d.ts +1186 -188
- package/dist/locales/pt.js +1350 -255
- package/dist/locales/registry.d.ts +102 -0
- package/dist/locales/registry.js +220 -0
- package/dist/locales/rtl.d.ts +24 -0
- package/dist/locales/rtl.js +26 -0
- package/dist/locales/ru.d.ts +1292 -188
- package/dist/locales/ru.js +1479 -272
- package/dist/locales/zh.d.ts +1144 -252
- package/dist/locales/zh.js +1245 -259
- package/dist/pickLocalized.d.ts +30 -0
- package/dist/pickLocalized.js +39 -0
- package/dist/provider.d.ts +101 -12
- package/dist/provider.js +335 -50
- package/dist/translateFn.d.ts +29 -0
- package/dist/translateFn.js +8 -0
- package/dist/useDisplayLocale.d.ts +11 -0
- package/dist/useDisplayLocale.js +11 -0
- package/dist/useObjectLabel.d.ts +143 -100
- package/dist/useObjectLabel.js +129 -82
- package/dist/useSafeTranslation.js +40 -42
- package/dist/utils/first-day-of-week.d.ts +71 -0
- package/dist/utils/first-day-of-week.js +61 -0
- package/dist/utils/formatting.d.ts +23 -1
- package/dist/utils/formatting.js +34 -5
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/spec-formatters.d.ts +13 -23
- package/dist/utils/spec-formatters.js +41 -2
- package/dist/utils/spec-translations.d.ts +16 -3
- package/dist/utils/spec-translations.js +52 -13
- package/package.json +12 -6
package/dist/useObjectLabel.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* | What | Auto-generated key | Fallback |
|
|
12
12
|
* |--------------------|-------------------------------------------------|-----------------------|
|
|
13
13
|
* | Object label | {ns}.objects.{objectName}.label | objectDef.label |
|
|
14
|
+
* | Object plural | {ns}.objects.{objectName}.pluralLabel | objectDef.pluralLabel, then the object label |
|
|
14
15
|
* | Object description | {ns}.objects.{objectName}.description | objectDef.description |
|
|
15
16
|
* | Field label | {ns}.fields.{objectName}.{fieldName} | field.label |
|
|
16
17
|
*
|
|
@@ -108,9 +109,10 @@ export function useObjectLabel() {
|
|
|
108
109
|
* include crm.objects.* or crm.apps.*.
|
|
109
110
|
*
|
|
110
111
|
* `globalActions` is included so an app bundle whose only translated scope
|
|
111
|
-
* is global actions (no object/field entries) is still discovered —
|
|
112
|
-
* `globalActions.<action>.*`
|
|
113
|
-
* (objectui#3372
|
|
112
|
+
* is global actions (no object/field entries) is still discovered — the
|
|
113
|
+
* `globalActions.<action>.*` copy of its object-less actions would otherwise
|
|
114
|
+
* be unreachable (objectui#3372; object-less only since objectui#11439, see
|
|
115
|
+
* `actionSuffixes`).
|
|
114
116
|
*/
|
|
115
117
|
const getAppNamespaces = () => {
|
|
116
118
|
if (!hasUsableI18nInstance(i18n))
|
|
@@ -168,28 +170,35 @@ export function useObjectLabel() {
|
|
|
168
170
|
};
|
|
169
171
|
/**
|
|
170
172
|
* Build suffix candidates for an action-scoped key, mirroring the canonical
|
|
171
|
-
* `@objectstack/spec` resolver (`
|
|
173
|
+
* `@objectstack/spec` resolver (`actionTranslationNode` behind
|
|
174
|
+
* `lookupActionField`, in `system/i18n-resolver`): ONE node per action, chosen
|
|
175
|
+
* by its key object.
|
|
172
176
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* `globalActions.<action>.*` overlay when no object-scoped translation
|
|
178
|
-
* exists (objectui#3372). Without this fallback, a globalAction rendered on
|
|
179
|
-
* a record-detail action bar — where the caller passes `objectDef.name` for
|
|
180
|
-
* every action — misses `objects.<obj>._actions.<action>.label` and leaks the
|
|
181
|
-
* English metadata literal.
|
|
177
|
+
* `objectName` is the action's KEY object — the object the action belongs
|
|
178
|
+
* to: its declared `objectName`, else the host object that embeds it, which
|
|
179
|
+
* is what the spec's `translateObject` stamps on an embedded action. Callers
|
|
180
|
+
* pass that object (`useActionTextLocalizer` derives it from the action).
|
|
182
181
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
182
|
+
* - A key object reads `objects.<obj>._actions.<action>.<tail>` and nothing
|
|
183
|
+
* else.
|
|
184
|
+
* - No key object reads `globalActions.<action>.<tail>` and nothing else.
|
|
185
|
+
*
|
|
186
|
+
* There is deliberately NO object-then-global chain. objectui#3372 added one
|
|
187
|
+
* so `globalActions` copy reached actions drawn on an object's bar; the
|
|
188
|
+
* spec's `TranslationDataSchema.globalActions` is for actions "not bound to a
|
|
189
|
+
* specific object via `objectName`", `@objectstack/spec` 17.7.0 never reads it
|
|
190
|
+
* for a bound action, and objectui#11439 (triage's amended ruling) retired the
|
|
191
|
+
* chain so this client and the server resolve one bundle entry the same way.
|
|
192
|
+
* A bound action's copy filed under `globalActions` now misses on every
|
|
193
|
+
* surface alike and the authored text shows — the misfiling is what
|
|
194
|
+
* `os validate` already refuses, and it is fixed in the bundle, not here.
|
|
195
|
+
*
|
|
196
|
+
* The two object-name candidates (`objects.<ns__obj>` then `objects.<obj>`)
|
|
197
|
+
* are a separate axis and stay — see `stripNamespace`.
|
|
186
198
|
*/
|
|
187
|
-
const actionSuffixes = (objectName, actionName, tail) =>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
? [...objectSuffixes(objectName, `_actions.${actionName}.${tail}`), globalSuffix]
|
|
191
|
-
: [globalSuffix];
|
|
192
|
-
};
|
|
199
|
+
const actionSuffixes = (objectName, actionName, tail) => objectName
|
|
200
|
+
? objectSuffixes(objectName, `_actions.${actionName}.${tail}`)
|
|
201
|
+
: [`globalActions.${actionName}.${tail}`];
|
|
193
202
|
const fieldSuffixes = (objectName, fieldName) => {
|
|
194
203
|
const base = stripNamespace(objectName);
|
|
195
204
|
return base !== objectName
|
|
@@ -242,8 +251,8 @@ export function useObjectLabel() {
|
|
|
242
251
|
* name under `_views` — the runtime view identity's own name, stripped of the
|
|
243
252
|
* object prefix. That single spelling is canonical per the objectstack#5164
|
|
244
253
|
* ruling A (2026-08-06): the i18n extractor asks the view composer for the key
|
|
245
|
-
* (objectstack
|
|
246
|
-
* (objectstack
|
|
254
|
+
* (objectstack `b3c1f3cd5`) and `packages/lint` enforces exactly that spelling
|
|
255
|
+
* (objectstack `7618ee814`), so this resolver accepts exactly what those produce.
|
|
247
256
|
*
|
|
248
257
|
* Deliberately NOT a second candidate: the prefixed full name
|
|
249
258
|
* (`_views.<objectName>.<viewName>`). Accepting it made this client more
|
|
@@ -268,11 +277,37 @@ export function useObjectLabel() {
|
|
|
268
277
|
const bareViewName = matchedPrefix ? viewName.slice(matchedPrefix.length) : viewName;
|
|
269
278
|
return objectSuffixes(objectName, `_views.${bareViewName}.${tail}`);
|
|
270
279
|
};
|
|
280
|
+
/** The singular object label; `objectPluralLabel` falls back to it. */
|
|
281
|
+
const objectLabel = (objectDef) => resolve(objectSuffixes(objectDef.name, 'label'), objectDef.label);
|
|
271
282
|
return {
|
|
272
283
|
/**
|
|
273
284
|
* Resolve translated object label, falling back to objectDef.label.
|
|
274
285
|
*/
|
|
275
|
-
objectLabel
|
|
286
|
+
objectLabel,
|
|
287
|
+
/**
|
|
288
|
+
* Resolve the object's PLURAL label — what a list of its records is called:
|
|
289
|
+
* an object list page's title and the breadcrumb segment that links to that
|
|
290
|
+
* list (objectui#11696). Record-scoped surfaces (the record page, a record
|
|
291
|
+
* drawer, "New …") keep {@link objectLabel}.
|
|
292
|
+
*
|
|
293
|
+
* Order: the translated plural (`{ns}.objects.{objectName}.pluralLabel`),
|
|
294
|
+
* else the declared `objectDef.pluralLabel`, else the singular as
|
|
295
|
+
* `objectLabel` resolves it — `pluralLabel` is optional in the spec, and an
|
|
296
|
+
* object that declares none is called by its label everywhere.
|
|
297
|
+
*
|
|
298
|
+
* The first two rungs are the client half of `translateObject` in
|
|
299
|
+
* `@objectstack/spec` (`system/i18n-resolver`), which serves `pluralLabel`
|
|
300
|
+
* as the catalog entry, else the authored value. There is deliberately NO
|
|
301
|
+
* rung that prefers a translated SINGULAR over a declared plural the bundle
|
|
302
|
+
* leaves untranslated: the server has none either, so every consumer of
|
|
303
|
+
* `/meta` shows the authored plural in that case, and this client showing
|
|
304
|
+
* something else would be a second dialect (see `viewSuffixes`). The gap is
|
|
305
|
+
* the bundle's — `os i18n extract` writes `pluralLabel` wherever the object
|
|
306
|
+
* declares one — and is fixed there.
|
|
307
|
+
*/
|
|
308
|
+
objectPluralLabel: (objectDef) => resolve(objectSuffixes(objectDef.name, 'pluralLabel'), '')
|
|
309
|
+
|| objectDef.pluralLabel
|
|
310
|
+
|| objectLabel(objectDef),
|
|
276
311
|
/**
|
|
277
312
|
* Resolve translated object description, falling back to objectDef.description.
|
|
278
313
|
*/
|
|
@@ -328,15 +363,17 @@ export function useObjectLabel() {
|
|
|
328
363
|
* reaches this client — so nav labels arrive already localized. One
|
|
329
364
|
* owner, not two. To translate a sidebar group, translate it there.
|
|
330
365
|
*
|
|
331
|
-
* History (
|
|
366
|
+
* History (`9c60144b5`): until then this docstring promised
|
|
332
367
|
* `"Sales" → "销售"` for sidebar groups, and `NavigationRenderer`
|
|
333
368
|
* accepted `resolveGroupLabel`/`resolveItemLabel` to wire it up. Those
|
|
334
369
|
* props could never fire — the renderer's `isCustomized` guard compared a
|
|
335
370
|
* node's authored label against its own `id` (`Workspace` vs
|
|
336
371
|
* `grp_workspace`), which never match, so the guard was true for every
|
|
337
|
-
* real entry. The props are gone
|
|
338
|
-
*
|
|
339
|
-
*
|
|
372
|
+
* real entry. The props are gone, and so is the guard itself
|
|
373
|
+
* (objectui#11201: the renderer no longer matches a label's text against
|
|
374
|
+
* any name); the promise was false, not merely unused. This helper is kept
|
|
375
|
+
* as a plain key reader for a consumer that renders navigation itself — it
|
|
376
|
+
* has no first-party caller.
|
|
340
377
|
*/
|
|
341
378
|
navGroupLabel: (appName, groupId, fallback) => resolve(`apps.${appName}.navigation.${groupId}.label`, fallback),
|
|
342
379
|
/**
|
|
@@ -371,39 +408,17 @@ export function useObjectLabel() {
|
|
|
371
408
|
* Resolve translated widget description within a dashboard.
|
|
372
409
|
* Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.description`.
|
|
373
410
|
* Returns undefined when neither metadata nor translation provides one.
|
|
411
|
+
*
|
|
412
|
+
* This is a widget's ONE translated description. The sibling sub-caption
|
|
413
|
+
* key, `widgets.{widgetId}.subCaption`, is retired with its resolver
|
|
414
|
+
* (objectui#11389, ruling C): `@objectstack/spec` 17.7.0 refuses it by
|
|
415
|
+
* name in a translation bundle, and nothing here reads it.
|
|
374
416
|
*/
|
|
375
417
|
widgetDescription: (dashboardName, widgetId, fallback) => {
|
|
376
418
|
const fb = fallback ?? '';
|
|
377
419
|
const resolved = resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.description`), fb);
|
|
378
420
|
return resolved || undefined;
|
|
379
421
|
},
|
|
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
|
-
},
|
|
407
422
|
/**
|
|
408
423
|
* Resolve translated page label, falling back to pageDef.label.
|
|
409
424
|
* Convention: `{ns}.pages.{pageName}.label`.
|
|
@@ -422,15 +437,27 @@ export function useObjectLabel() {
|
|
|
422
437
|
* that spells the prefix out does not resolve (see `viewSuffixes`).
|
|
423
438
|
*/
|
|
424
439
|
viewLabel: (objectName, viewName, fallback) => resolve(viewSuffixes(objectName, viewName, 'label'), fallback),
|
|
425
|
-
|
|
426
|
-
*
|
|
427
|
-
*
|
|
440
|
+
/*
|
|
441
|
+
* There is deliberately NO `viewDescription` member here, and no
|
|
442
|
+
* `{ns}.objects.{objectName}._views.{viewName}.description` convention for
|
|
443
|
+
* it to resolve (objectui#7219, maintainer ruling 2026-09-02, option B).
|
|
444
|
+
*
|
|
445
|
+
* A list view's description has exactly ONE channel: the `I18nLabel` value
|
|
446
|
+
* authored on the view entry -- a string or an inline locale map -- which
|
|
447
|
+
* `ObjectView` relays and the render site resolves with `pickLocalized`
|
|
448
|
+
* (objectui#7199). The catalog key used to be declared and resolved right
|
|
449
|
+
* here, between its two wired-up siblings, but had zero callers and zero
|
|
450
|
+
* in-repo bundle usage: a bundle entry written under it reached no screen.
|
|
451
|
+
*
|
|
452
|
+
* Wiring it in instead was weighed and NOT taken -- two vocabularies for
|
|
453
|
+
* one concept plus a precedence rule is the ambiguity, not the fix. Leaving
|
|
454
|
+
* it declared and unfulfilled was not taken either; the standing rule is
|
|
455
|
+
* remove, not phase out, when a surface has measured zero use.
|
|
456
|
+
*
|
|
457
|
+
* `viewSuffixes` is NOT retired with it: `viewLabel` above and
|
|
458
|
+
* `viewEmptyState` below share that helper and keep resolving. Only the
|
|
459
|
+
* `'description'` tail passed to it is gone.
|
|
428
460
|
*/
|
|
429
|
-
viewDescription: (objectName, viewName, fallback) => {
|
|
430
|
-
const fb = fallback ?? '';
|
|
431
|
-
const resolved = resolve(viewSuffixes(objectName, viewName, 'description'), fb);
|
|
432
|
-
return resolved || undefined;
|
|
433
|
-
},
|
|
434
461
|
/**
|
|
435
462
|
* Resolve translated list-view emptyState. Returns a {title, message}
|
|
436
463
|
* tuple with each field independently translated. Convention:
|
|
@@ -455,17 +482,18 @@ export function useObjectLabel() {
|
|
|
455
482
|
sectionLabel: (objectName, sectionName, fallback) => resolve(objectSuffixes(objectName, `_sections.${sectionName}.label`), fallback),
|
|
456
483
|
/**
|
|
457
484
|
* Resolve translated action label.
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
* objectName
|
|
461
|
-
*
|
|
462
|
-
* objectui#
|
|
485
|
+
* `objectName` is the action's KEY object (its declared `objectName`, else
|
|
486
|
+
* the host that embeds it). Convention:
|
|
487
|
+
* `{ns}.objects.{objectName}._actions.{actionName}.label`; with no key
|
|
488
|
+
* object, `{ns}.globalActions.{actionName}.label`. Never both: a bound
|
|
489
|
+
* action does not read `globalActions` (objectui#11439, see
|
|
490
|
+
* `actionSuffixes`). Every action resolver below follows the same rule.
|
|
463
491
|
*/
|
|
464
492
|
actionLabel: (objectName, actionName, fallback) => resolve(actionSuffixes(objectName, actionName, 'label'), fallback),
|
|
465
493
|
/**
|
|
466
494
|
* Resolve translated action confirmation prompt.
|
|
467
|
-
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText
|
|
468
|
-
*
|
|
495
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`;
|
|
496
|
+
* with no key object, `{ns}.globalActions.{actionName}.confirmText`.
|
|
469
497
|
* Returns undefined when no translation and no fallback exist.
|
|
470
498
|
*/
|
|
471
499
|
actionConfirm: (objectName, actionName, fallback) => {
|
|
@@ -475,20 +503,38 @@ export function useObjectLabel() {
|
|
|
475
503
|
},
|
|
476
504
|
/**
|
|
477
505
|
* Resolve translated action success message.
|
|
478
|
-
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage
|
|
479
|
-
*
|
|
506
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`;
|
|
507
|
+
* with no key object, `{ns}.globalActions.{actionName}.successMessage`.
|
|
480
508
|
*/
|
|
481
509
|
actionSuccess: (objectName, actionName, fallback) => {
|
|
482
510
|
const fb = fallback ?? '';
|
|
483
511
|
const resolved = resolve(actionSuffixes(objectName, actionName, 'successMessage'), fb);
|
|
484
512
|
return resolved || undefined;
|
|
485
513
|
},
|
|
514
|
+
/**
|
|
515
|
+
* Resolve the translated success copy for ONE handler outcome — an entry of
|
|
516
|
+
* the action's `outcomeMessages` (`@objectstack/spec` 17.6.0, objectui#11344).
|
|
517
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.outcomeMessages.{outcome}`;
|
|
518
|
+
* with no key object, `{ns}.globalActions.{actionName}.outcomeMessages.{outcome}`
|
|
519
|
+
* — the addresses the spec's translation bundle declares for the key.
|
|
520
|
+
* Returns undefined when no translation and no fallback exist.
|
|
521
|
+
*
|
|
522
|
+
* Like `actionSuccess`, the answer is a TEMPLATE: a `${result.*}` token in a
|
|
523
|
+
* bundle entry passes through untouched (i18next interpolates its own
|
|
524
|
+
* `{{…}}` form, never `${…}`), and the action runner fills it in from the
|
|
525
|
+
* handler's answer once the action has run.
|
|
526
|
+
*/
|
|
527
|
+
actionOutcome: (objectName, actionName, outcome, fallback) => {
|
|
528
|
+
const fb = fallback ?? '';
|
|
529
|
+
const resolved = resolve(actionSuffixes(objectName, actionName, `outcomeMessages.${outcome}`), fb);
|
|
530
|
+
return resolved || undefined;
|
|
531
|
+
},
|
|
486
532
|
/**
|
|
487
533
|
* Resolve translated action description (the explanatory line shown in the
|
|
488
534
|
* action's param dialog / sheet / drawer header).
|
|
489
|
-
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.description
|
|
490
|
-
*
|
|
491
|
-
* metadata's literal string; undefined when nothing resolves.
|
|
535
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.description`;
|
|
536
|
+
* with no key object, `{ns}.globalActions.{actionName}.description`. Falls
|
|
537
|
+
* back to the metadata's literal string; undefined when nothing resolves.
|
|
492
538
|
*/
|
|
493
539
|
actionDescription: (objectName, actionName, fallback) => {
|
|
494
540
|
const fb = fallback ?? '';
|
|
@@ -500,10 +546,9 @@ export function useObjectLabel() {
|
|
|
500
546
|
/**
|
|
501
547
|
* Resolve a translated copy of an action's post-success RESULT DIALOG
|
|
502
548
|
* (`title` / `description` / `acknowledge` + per-field labels).
|
|
503
|
-
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.resultDialog
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
* metadata's literal strings.
|
|
549
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.resultDialog.*`;
|
|
550
|
+
* with no key object, `{ns}.globalActions.{actionName}.resultDialog.*`.
|
|
551
|
+
* Falls back to the metadata's literal strings.
|
|
507
552
|
*
|
|
508
553
|
* The `fields` translation node is keyed by the LITERAL result-field path
|
|
509
554
|
* from the action metadata (may contain dots, e.g. `"user.email"`), so it
|
|
@@ -556,7 +601,8 @@ export function useObjectLabel() {
|
|
|
556
601
|
},
|
|
557
602
|
/**
|
|
558
603
|
* Resolve translated action-PARAMETER text (label / placeholder / helpText).
|
|
559
|
-
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.params.{paramName}.{attr}
|
|
604
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.params.{paramName}.{attr}`;
|
|
605
|
+
* with no key object, `{ns}.globalActions.{actionName}.params.{paramName}.{attr}`.
|
|
560
606
|
* Falls back to the provided value (the metadata's literal string) when no
|
|
561
607
|
* translation exists, so untranslated params keep rendering as authored.
|
|
562
608
|
*/
|
|
@@ -569,7 +615,8 @@ export function useObjectLabel() {
|
|
|
569
615
|
},
|
|
570
616
|
/**
|
|
571
617
|
* Resolve a translated action-parameter SELECT OPTION label.
|
|
572
|
-
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.params.{paramName}.options.{optionValue}
|
|
618
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.params.{paramName}.options.{optionValue}`;
|
|
619
|
+
* with no key object, the same path under `{ns}.globalActions.{actionName}`.
|
|
573
620
|
* Falls back to the provided (English metadata) label when untranslated.
|
|
574
621
|
*/
|
|
575
622
|
actionParamOptionLabel: (objectName, actionName, paramName, optionValue, fallback) => {
|
|
@@ -6,21 +6,38 @@
|
|
|
6
6
|
* returning raw i18n keys — and, for a key the `defaults` map does not carry,
|
|
7
7
|
* to the call site's own inline `t(key, { defaultValue })` (objectui#3865).
|
|
8
8
|
* The lookup order is i18next's, so a provider-less host and a provider-mounted
|
|
9
|
-
* one render the same string: `defaults[
|
|
9
|
+
* one render the same string: `defaults[key_<category>]` for a numeric `count`
|
|
10
|
+
* (objectui#11445), then `defaults[key]` (the pack value's stand-in here)
|
|
10
11
|
* -> `defaultValue` -> the key.
|
|
11
12
|
*
|
|
12
13
|
* @param defaults - Fallback English translations keyed by i18n key
|
|
13
14
|
* @param testKey - A key to test if i18n is properly configured (must be in defaults)
|
|
14
15
|
*/
|
|
15
16
|
import { useObjectTranslation } from './provider.js';
|
|
17
|
+
import { DEFAULT_VALUE_OPTION, interpolateFallback } from './fallbackInterpolation.js';
|
|
16
18
|
/**
|
|
17
|
-
* The
|
|
19
|
+
* The plural rules the defaults tables are written in. Every table is English
|
|
20
|
+
* (the `en` pack's stand-in), so English selects the slot.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULTS_PLURAL_RULES = new Intl.PluralRules('en');
|
|
23
|
+
/**
|
|
24
|
+
* The `defaults` row a count family answers from, the way i18next walks a pack
|
|
25
|
+
* (objectui#11445): `key_<category>` first, then the base `key`.
|
|
18
26
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
27
|
+
* i18next plural-selects only for a NUMERIC `count` (`needsPluralHandling` in
|
|
28
|
+
* i18next 26 is `count !== undefined && !isString(count)`), so a string count
|
|
29
|
+
* reads the base row on both paths. `_zero` is not looked up: no English row
|
|
30
|
+
* spells one, because `en` selects no `zero` category.
|
|
22
31
|
*/
|
|
23
|
-
|
|
32
|
+
function defaultsRowFor(defaults, key, options) {
|
|
33
|
+
const count = options?.count;
|
|
34
|
+
if (typeof count === 'number' && Number.isFinite(count)) {
|
|
35
|
+
const slot = defaults[`${key}_${DEFAULTS_PLURAL_RULES.select(count)}`];
|
|
36
|
+
if (slot)
|
|
37
|
+
return slot;
|
|
38
|
+
}
|
|
39
|
+
return defaults[key];
|
|
40
|
+
}
|
|
24
41
|
export function createSafeTranslation(defaults, testKey) {
|
|
25
42
|
// Factory-level fallback: one stable reference per defaults map, so
|
|
26
43
|
// downstream useMemo/useCallback deps don't invalidate every render in
|
|
@@ -47,43 +64,24 @@ export function createSafeTranslation(defaults, testKey) {
|
|
|
47
64
|
// `string`, and i18next itself declines a `null` default. `||` (not `??`)
|
|
48
65
|
// keeps the whole chain consistent with the pre-existing `defaults[key] ||`
|
|
49
66
|
// step: an empty string at any position falls through to the next.
|
|
67
|
+
//
|
|
68
|
+
// A count family's suffixed rows are reached here too (objectui#11445):
|
|
69
|
+
// `t('detail.replyCount', { count: 3 })` reads `detail.replyCount_other`
|
|
70
|
+
// before the base row, exactly as i18next reads the `en` pack. Before that
|
|
71
|
+
// card a family's `_one` / `_other` rows were unreachable on this path, so
|
|
72
|
+
// the code-selected `xxxCountOne` pairs were the only way a provider-less
|
|
73
|
+
// host rendered "1 reply" — the card retired those pairs.
|
|
50
74
|
const inlineDefault = options?.[DEFAULT_VALUE_OPTION];
|
|
51
|
-
let value = defaults
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
|
68
|
-
// `split(needle).join(value)` — deliberately not `replace`, and not
|
|
69
|
-
// `replaceAll` either (objectui#3418). This path must agree with
|
|
70
|
-
// i18next, which serves the *provider* path; any divergence is a
|
|
71
|
-
// silent fork that only shows up on provider-less hosts, where we are
|
|
72
|
-
// least likely to see it:
|
|
73
|
-
// 1. `replace` with a string needle substitutes only the FIRST
|
|
74
|
-
// occurrence. i18next substitutes every one, so a sentence that
|
|
75
|
-
// repeats a placeholder ("Selected {{count}} of {{count}} items"
|
|
76
|
-
// — natural in many locales, and often required by RTL /
|
|
77
|
-
// agglutinative word order) leaked literal braces to users.
|
|
78
|
-
// 2. `replace` AND `replaceAll` both interpret `$&`, `` $` ``, `$'`
|
|
79
|
-
// and `$$` in the *replacement* string. i18next does not. Values
|
|
80
|
-
// here are runtime data — record labels, search terms — so this
|
|
81
|
-
// one is reachable today, unlike (1).
|
|
82
|
-
// split/join is literal on both sides, which is exactly i18next's
|
|
83
|
-
// behaviour, and needs no regex escaping of the placeholder name.
|
|
84
|
-
value = value.split(`{{${k}}}`).join(String(v));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
75
|
+
let value = defaultsRowFor(defaults, key, options) ||
|
|
76
|
+
(typeof inlineDefault === 'string' ? inlineDefault : '') ||
|
|
77
|
+
key;
|
|
78
|
+
// The interpolation itself lives in `fallbackInterpolation.ts` — ONE
|
|
79
|
+
// function, shared with `useObjectTranslation`'s not-ready path
|
|
80
|
+
// (objectui#6219). Both provider-less renderers therefore fill exactly
|
|
81
|
+
// the `{{name}}` spelling objectui#3512 holds the copy to, and the
|
|
82
|
+
// reserved-name rule for `defaultValue` (objectui#3865) is stated once.
|
|
83
|
+
// The behaviour pinned by `useSafeTranslation.test.tsx` is unchanged.
|
|
84
|
+
value = interpolateFallback(value, options);
|
|
87
85
|
return value;
|
|
88
86
|
};
|
|
89
87
|
return function useSafeTranslation() {
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
* The first day of the week for a BCP-47 locale tag (objectui#11675).
|
|
10
|
+
*
|
|
11
|
+
* The first day of the week follows the user's locale, as dates, times and
|
|
12
|
+
* numbers do (ruling B on objectui#11675: there is no separate week-start
|
|
13
|
+
* setting). So a week grid, a weekday header row and a "this week" bound all
|
|
14
|
+
* read it from the tag they already format with, and never from a hard-coded
|
|
15
|
+
* Sunday or Monday: `en-US` starts on Sunday, `en-GB` and `zh-CN` on Monday.
|
|
16
|
+
*
|
|
17
|
+
* ## The engine's own week info comes first
|
|
18
|
+
*
|
|
19
|
+
* `Intl.Locale` carries CLDR's week data for the tag's region, including a
|
|
20
|
+
* `-u-fw-` keyword on the tag. It has two spellings: the `getWeekInfo()` method
|
|
21
|
+
* of the current Intl Locale Info proposal, and the `weekInfo` accessor engines
|
|
22
|
+
* shipped first. Both are read, the method first. Measured while writing this:
|
|
23
|
+
* Chromium 141 has both, Node 22 (the test and CI runtime) has the accessor
|
|
24
|
+
* only. Both count ISO weekdays, 1 (Monday) to 7 (Sunday).
|
|
25
|
+
*
|
|
26
|
+
* ## Where the engine has none, CLDR's table answers
|
|
27
|
+
*
|
|
28
|
+
* An engine with neither spelling gets {@link NON_MONDAY_REGIONS}: CLDR's
|
|
29
|
+
* `weekData` `firstDay` rows, read by region. The region is the tag's own, or
|
|
30
|
+
* the one CLDR's likely subtags give it (`Intl.Locale.prototype.maximize`), so
|
|
31
|
+
* `en` reads `US` and `zh` reads `CN`. A region the table does not list, and a
|
|
32
|
+
* tag with no region even after `maximize()`, reads Monday, CLDR's world
|
|
33
|
+
* default (`001`). This path reads the region only: a `-u-fw-` or `-u-rg-`
|
|
34
|
+
* keyword is the engine path's alone.
|
|
35
|
+
*
|
|
36
|
+
* The table transcribes ONE CLDR release, the one {@link WEEK_DATA_CLDR} names:
|
|
37
|
+
* the newest release the repository's CI runtime ships. Engines carry their own
|
|
38
|
+
* week data, and it differs: CLDR moves a region now and then, and a browser
|
|
39
|
+
* may carry data of its own. That is why the engine's answer wins wherever
|
|
40
|
+
* there is one. What re-derives the table against the running engine is
|
|
41
|
+
* `__tests__/firstDayOfWeek-11675.test.ts`, region by region. On a runtime
|
|
42
|
+
* whose CLDR release is not the table's, it reads that release's recorded
|
|
43
|
+
* differences from the table instead of reporting them as drift.
|
|
44
|
+
*
|
|
45
|
+
* ## A tag `Intl` refuses is refused here too
|
|
46
|
+
*
|
|
47
|
+
* A malformed tag throws `Intl.Locale`'s `RangeError`, as `Intl.DateTimeFormat`
|
|
48
|
+
* and `toLocaleDateString` do for the same tag. Every caller formats its dates
|
|
49
|
+
* with the tag it hands this function, so a tag that reached here and is
|
|
50
|
+
* malformed fails the caller's own formatting the same way. ⛔ No Sunday or
|
|
51
|
+
* Monday is guessed for it.
|
|
52
|
+
*/
|
|
53
|
+
/** A weekday as `Date.prototype.getDay` numbers it: 0 is Sunday, 6 is Saturday. */
|
|
54
|
+
export type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
55
|
+
/**
|
|
56
|
+
* The CLDR release {@link NON_MONDAY_REGIONS} transcribes, as the major part of
|
|
57
|
+
* `process.versions.cldr`. Change it only together with the table, from a
|
|
58
|
+
* runtime that ships that release.
|
|
59
|
+
*/
|
|
60
|
+
export declare const WEEK_DATA_CLDR = "48";
|
|
61
|
+
/**
|
|
62
|
+
* The first day of the week in `locale`, numbered as `Date.prototype.getDay`
|
|
63
|
+
* numbers weekdays (0 is Sunday), which is also what react-day-picker's
|
|
64
|
+
* `weekStartsOn` takes. Read the module header for where the answer comes from.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* firstDayOfWeek('en-US'); // 0, Sunday
|
|
68
|
+
* firstDayOfWeek('en-GB'); // 1, Monday
|
|
69
|
+
* firstDayOfWeek('zh-CN'); // 1, Monday
|
|
70
|
+
*/
|
|
71
|
+
export declare function firstDayOfWeek(locale: string): WeekdayIndex;
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
/** ISO weekday 1 (Monday) to 7 (Sunday), as a {@link WeekdayIndex}. */
|
|
9
|
+
const BY_ISO_WEEKDAY = [1, 2, 3, 4, 5, 6, 0];
|
|
10
|
+
function fromIsoWeekday(day) {
|
|
11
|
+
return Number.isInteger(day) && day >= 1 && day <= 7 ? BY_ISO_WEEKDAY[day - 1] : undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The CLDR release {@link NON_MONDAY_REGIONS} transcribes, as the major part of
|
|
15
|
+
* `process.versions.cldr`. Change it only together with the table, from a
|
|
16
|
+
* runtime that ships that release.
|
|
17
|
+
*/
|
|
18
|
+
export const WEEK_DATA_CLDR = '48';
|
|
19
|
+
/**
|
|
20
|
+
* CLDR's `weekData` `firstDay` rows other than Monday, by region, as of the
|
|
21
|
+
* release {@link WEEK_DATA_CLDR} names. Regions are the canonical codes
|
|
22
|
+
* `Intl.Locale` reports (`BU` reads `MM`), so the deprecated aliases CLDR also
|
|
23
|
+
* lists are left out.
|
|
24
|
+
*/
|
|
25
|
+
const NON_MONDAY_REGIONS = [
|
|
26
|
+
[
|
|
27
|
+
0,
|
|
28
|
+
'AG AS BD BR BS BT BW BZ CA CO DM DO ET GT GU HK HN ID IL IN IS JM JP KE KH KR LA MH MM MO MT MX ' +
|
|
29
|
+
'MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW',
|
|
30
|
+
],
|
|
31
|
+
[5, 'MV'],
|
|
32
|
+
[6, 'AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY'],
|
|
33
|
+
];
|
|
34
|
+
/** CLDR's world default (`001`): the week starts on Monday. */
|
|
35
|
+
const WORLD_FIRST_DAY = 1;
|
|
36
|
+
const FIRST_DAY_BY_REGION = new Map(NON_MONDAY_REGIONS.flatMap(([day, regions]) => regions.split(' ').map((region) => [region, day])));
|
|
37
|
+
function engineFirstDay(locale) {
|
|
38
|
+
const faces = locale;
|
|
39
|
+
if (typeof faces.getWeekInfo === 'function')
|
|
40
|
+
return fromIsoWeekday(faces.getWeekInfo().firstDay);
|
|
41
|
+
const info = faces.weekInfo;
|
|
42
|
+
return info ? fromIsoWeekday(info.firstDay) : undefined;
|
|
43
|
+
}
|
|
44
|
+
function tableFirstDay(locale) {
|
|
45
|
+
const region = locale.region ?? locale.maximize().region;
|
|
46
|
+
return (region !== undefined ? FIRST_DAY_BY_REGION.get(region) : undefined) ?? WORLD_FIRST_DAY;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The first day of the week in `locale`, numbered as `Date.prototype.getDay`
|
|
50
|
+
* numbers weekdays (0 is Sunday), which is also what react-day-picker's
|
|
51
|
+
* `weekStartsOn` takes. Read the module header for where the answer comes from.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* firstDayOfWeek('en-US'); // 0, Sunday
|
|
55
|
+
* firstDayOfWeek('en-GB'); // 1, Monday
|
|
56
|
+
* firstDayOfWeek('zh-CN'); // 1, Monday
|
|
57
|
+
*/
|
|
58
|
+
export function firstDayOfWeek(locale) {
|
|
59
|
+
const parsed = new Intl.Locale(locale);
|
|
60
|
+
return engineFirstDay(parsed) ?? tableFirstDay(parsed);
|
|
61
|
+
}
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* @object-ui/i18n - Date and currency formatting utilities
|
|
3
3
|
*
|
|
4
4
|
* Uses the native Intl API for locale-aware formatting.
|
|
5
|
+
*
|
|
6
|
+
* Every date helper below reads a string through `toDisplayDate`
|
|
7
|
+
* (`@object-ui/core`), the one parse step behind the date faces
|
|
8
|
+
* (objectui#10110, objectui#10183). The engine's own parse reads a date-only
|
|
9
|
+
* `2026-09-01` as UTC midnight, so each helper named August 31st for every
|
|
10
|
+
* viewer west of UTC (objectui#10866). The shared step tells the two shapes
|
|
11
|
+
* apart by the value: a date-only string is rebuilt at local midnight of the
|
|
12
|
+
* day it names, and a value with a time part keeps its instant. It also
|
|
13
|
+
* refuses a day its month does not have (`2026-02-30`, objectui#10026), which
|
|
14
|
+
* each helper then renders as the raw string, its face for any unparsable
|
|
15
|
+
* value.
|
|
5
16
|
*/
|
|
6
17
|
export interface DateFormatOptions {
|
|
7
18
|
locale?: string;
|
|
@@ -33,8 +44,19 @@ export declare function formatDate(date: Date | string | number, options?: DateF
|
|
|
33
44
|
export declare function formatDateTime(date: Date | string | number, options?: DateFormatOptions): string;
|
|
34
45
|
/**
|
|
35
46
|
* Format a relative time (e.g., "2 days ago", "in 3 hours")
|
|
47
|
+
*
|
|
48
|
+
* A date-only value counts from now to the START of the day it names (local
|
|
49
|
+
* midnight), the same distance in every zone at the same wall-clock time.
|
|
50
|
+
* An unparsable value comes back as its string, as it does from
|
|
51
|
+
* {@link formatDate}; `Intl.RelativeTimeFormat` would throw on it.
|
|
52
|
+
*
|
|
53
|
+
* `options` are `Intl.RelativeTimeFormat`'s own `style` and `numeric`, passed
|
|
54
|
+
* through unchanged; omitted, the result is what it always was (`long`,
|
|
55
|
+
* `auto`: "yesterday", "2 days ago"). A surface with little room asks for a
|
|
56
|
+
* compact form, e.g. `{ style: 'short', numeric: 'always' }` ("1 day ago",
|
|
57
|
+
* "3 hr. ago").
|
|
36
58
|
*/
|
|
37
|
-
export declare function formatRelativeTime(date: Date | string | number, locale?: string): string;
|
|
59
|
+
export declare function formatRelativeTime(date: Date | string | number, locale?: string, options?: Pick<Intl.RelativeTimeFormatOptions, 'style' | 'numeric'>): string;
|
|
38
60
|
/**
|
|
39
61
|
* Format a currency value according to locale conventions. When `currency`
|
|
40
62
|
* is omitted, falls back to a plain locale-formatted number (no symbol)
|