@object-ui/i18n 4.0.10 → 4.0.12
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 +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/locales/en.d.ts +14 -0
- package/dist/locales/en.js +14 -0
- package/dist/locales/index.d.ts +28 -0
- package/dist/locales/zh.d.ts +14 -0
- package/dist/locales/zh.js +14 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/spec-translations.d.ts +80 -0
- package/dist/utils/spec-translations.js +141 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @object-ui/i18n
|
|
2
2
|
|
|
3
|
+
## 4.0.12
|
|
4
|
+
|
|
5
|
+
## 4.0.11
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- 1909bc3: Add `transformSpecTranslations` / `isSpecTranslationData` helpers to
|
|
10
|
+
`@object-ui/i18n` so apps no longer need to maintain their own copy of the
|
|
11
|
+
`@objectstack/spec` `TranslationData` → flat namespace transform.
|
|
12
|
+
|
|
13
|
+
The new transform preserves **every** `_`-prefixed object scope by
|
|
14
|
+
convention (`_views`, `_actions`, `_sections`, `_notifications`, `_errors`,
|
|
15
|
+
`_options`, plus anything added in future spec versions), which fixes a
|
|
16
|
+
class of silent-failure regressions where new spec scopes were dropped
|
|
17
|
+
during transformation — leaving e.g. list-view labels to fall back to the
|
|
18
|
+
untranslated source string.
|
|
19
|
+
|
|
20
|
+
`@object-ui/console`'s `loadLanguage.ts` is rewritten to delegate to the
|
|
21
|
+
shared helper.
|
|
22
|
+
|
|
3
23
|
## 4.0.10
|
|
4
24
|
|
|
5
25
|
## 4.0.9
|
package/dist/index.d.ts
CHANGED
|
@@ -47,3 +47,4 @@ export { default as ru } from './locales/ru';
|
|
|
47
47
|
export { default as ar } from './locales/ar';
|
|
48
48
|
export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from './utils/index';
|
|
49
49
|
export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, type SpecPluralRule, type SpecDateFormat, type SpecNumberFormat, type SpecLocaleConfig, } from './utils/index';
|
|
50
|
+
export { isSpecTranslationData, transformSpecTranslations, type SpecTranslationData, } from './utils/index';
|
package/dist/index.js
CHANGED
|
@@ -54,3 +54,7 @@ export { default as ar } from './locales/ar';
|
|
|
54
54
|
export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from './utils/index';
|
|
55
55
|
// Spec-aligned formatters (v2.0.7)
|
|
56
56
|
export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, } from './utils/index';
|
|
57
|
+
// Spec TranslationData → flat namespace transform (consumed by
|
|
58
|
+
// `useObjectLabel`). Exposed so apps don't have to copy/paste — see
|
|
59
|
+
// `apps/console/src/loadLanguage.ts` for the canonical consumer.
|
|
60
|
+
export { isSpecTranslationData, transformSpecTranslations, } from './utils/index';
|
package/dist/locales/en.d.ts
CHANGED
|
@@ -213,6 +213,8 @@ declare const en: {
|
|
|
213
213
|
readonly moveCard: "Move card";
|
|
214
214
|
readonly deleteCard: "Delete card";
|
|
215
215
|
readonly deleteColumn: "Delete column";
|
|
216
|
+
readonly noCards: "No cards";
|
|
217
|
+
readonly cardTitlePlaceholder: "Enter card title...";
|
|
216
218
|
};
|
|
217
219
|
readonly timeline: {
|
|
218
220
|
readonly bucket: {
|
|
@@ -250,6 +252,8 @@ declare const en: {
|
|
|
250
252
|
readonly dragToReorder: "Drag to reorder";
|
|
251
253
|
readonly defaultView: "Default view";
|
|
252
254
|
readonly tabActionsFor: "View actions for {{name}}";
|
|
255
|
+
readonly readonlyAriaLabel: "Read-only view";
|
|
256
|
+
readonly readonlyTooltip: "System view — duplicate to customize.";
|
|
253
257
|
};
|
|
254
258
|
readonly detail: {
|
|
255
259
|
readonly back: "Back";
|
|
@@ -444,6 +448,16 @@ declare const en: {
|
|
|
444
448
|
readonly saveLayout: "Save layout";
|
|
445
449
|
readonly resetLayout: "Reset layout";
|
|
446
450
|
readonly total: "Total";
|
|
451
|
+
readonly noDataAvailable: "No data available";
|
|
452
|
+
readonly noDataSourceFor: "No data source available for";
|
|
453
|
+
readonly trend: {
|
|
454
|
+
readonly vsLastQuarter: "vs last quarter";
|
|
455
|
+
readonly vsLastMonth: "vs last month";
|
|
456
|
+
readonly vsLastWeek: "vs last week";
|
|
457
|
+
readonly vsLastYear: "vs last year";
|
|
458
|
+
readonly vsYesterday: "vs yesterday";
|
|
459
|
+
readonly vsPreviousPeriod: "vs previous period";
|
|
460
|
+
};
|
|
447
461
|
};
|
|
448
462
|
readonly configPanel: {
|
|
449
463
|
readonly save: "Save";
|
package/dist/locales/en.js
CHANGED
|
@@ -213,6 +213,8 @@ const en = {
|
|
|
213
213
|
moveCard: 'Move card',
|
|
214
214
|
deleteCard: 'Delete card',
|
|
215
215
|
deleteColumn: 'Delete column',
|
|
216
|
+
noCards: 'No cards',
|
|
217
|
+
cardTitlePlaceholder: 'Enter card title...',
|
|
216
218
|
},
|
|
217
219
|
timeline: {
|
|
218
220
|
bucket: {
|
|
@@ -250,6 +252,8 @@ const en = {
|
|
|
250
252
|
dragToReorder: 'Drag to reorder',
|
|
251
253
|
defaultView: 'Default view',
|
|
252
254
|
tabActionsFor: 'View actions for {{name}}',
|
|
255
|
+
readonlyAriaLabel: 'Read-only view',
|
|
256
|
+
readonlyTooltip: 'System view — duplicate to customize.',
|
|
253
257
|
},
|
|
254
258
|
detail: {
|
|
255
259
|
back: 'Back',
|
|
@@ -454,6 +458,16 @@ const en = {
|
|
|
454
458
|
saveLayout: 'Save layout',
|
|
455
459
|
resetLayout: 'Reset layout',
|
|
456
460
|
total: 'Total',
|
|
461
|
+
noDataAvailable: 'No data available',
|
|
462
|
+
noDataSourceFor: 'No data source available for',
|
|
463
|
+
trend: {
|
|
464
|
+
vsLastQuarter: 'vs last quarter',
|
|
465
|
+
vsLastMonth: 'vs last month',
|
|
466
|
+
vsLastWeek: 'vs last week',
|
|
467
|
+
vsLastYear: 'vs last year',
|
|
468
|
+
vsYesterday: 'vs yesterday',
|
|
469
|
+
vsPreviousPeriod: 'vs previous period',
|
|
470
|
+
},
|
|
457
471
|
},
|
|
458
472
|
configPanel: {
|
|
459
473
|
save: 'Save',
|
package/dist/locales/index.d.ts
CHANGED
|
@@ -225,6 +225,8 @@ export declare const builtInLocales: {
|
|
|
225
225
|
readonly moveCard: "Move card";
|
|
226
226
|
readonly deleteCard: "Delete card";
|
|
227
227
|
readonly deleteColumn: "Delete column";
|
|
228
|
+
readonly noCards: "No cards";
|
|
229
|
+
readonly cardTitlePlaceholder: "Enter card title...";
|
|
228
230
|
};
|
|
229
231
|
readonly timeline: {
|
|
230
232
|
readonly bucket: {
|
|
@@ -262,6 +264,8 @@ export declare const builtInLocales: {
|
|
|
262
264
|
readonly dragToReorder: "Drag to reorder";
|
|
263
265
|
readonly defaultView: "Default view";
|
|
264
266
|
readonly tabActionsFor: "View actions for {{name}}";
|
|
267
|
+
readonly readonlyAriaLabel: "Read-only view";
|
|
268
|
+
readonly readonlyTooltip: "System view — duplicate to customize.";
|
|
265
269
|
};
|
|
266
270
|
readonly detail: {
|
|
267
271
|
readonly back: "Back";
|
|
@@ -456,6 +460,16 @@ export declare const builtInLocales: {
|
|
|
456
460
|
readonly saveLayout: "Save layout";
|
|
457
461
|
readonly resetLayout: "Reset layout";
|
|
458
462
|
readonly total: "Total";
|
|
463
|
+
readonly noDataAvailable: "No data available";
|
|
464
|
+
readonly noDataSourceFor: "No data source available for";
|
|
465
|
+
readonly trend: {
|
|
466
|
+
readonly vsLastQuarter: "vs last quarter";
|
|
467
|
+
readonly vsLastMonth: "vs last month";
|
|
468
|
+
readonly vsLastWeek: "vs last week";
|
|
469
|
+
readonly vsLastYear: "vs last year";
|
|
470
|
+
readonly vsYesterday: "vs yesterday";
|
|
471
|
+
readonly vsPreviousPeriod: "vs previous period";
|
|
472
|
+
};
|
|
459
473
|
};
|
|
460
474
|
readonly configPanel: {
|
|
461
475
|
readonly save: "Save";
|
|
@@ -1419,6 +1433,8 @@ export declare const builtInLocales: {
|
|
|
1419
1433
|
readonly moveCard: "移动卡片";
|
|
1420
1434
|
readonly deleteCard: "删除卡片";
|
|
1421
1435
|
readonly deleteColumn: "删除列";
|
|
1436
|
+
readonly noCards: "暂无卡片";
|
|
1437
|
+
readonly cardTitlePlaceholder: "输入卡片标题...";
|
|
1422
1438
|
};
|
|
1423
1439
|
readonly timeline: {
|
|
1424
1440
|
readonly bucket: {
|
|
@@ -1456,6 +1472,8 @@ export declare const builtInLocales: {
|
|
|
1456
1472
|
readonly dragToReorder: "拖动排序";
|
|
1457
1473
|
readonly defaultView: "默认视图";
|
|
1458
1474
|
readonly tabActionsFor: "{{name}} 的视图操作";
|
|
1475
|
+
readonly readonlyAriaLabel: "只读视图";
|
|
1476
|
+
readonly readonlyTooltip: "系统视图 — 复制后可自定义。";
|
|
1459
1477
|
};
|
|
1460
1478
|
readonly detail: {
|
|
1461
1479
|
readonly back: "返回";
|
|
@@ -1650,6 +1668,16 @@ export declare const builtInLocales: {
|
|
|
1650
1668
|
readonly saveLayout: "保存布局";
|
|
1651
1669
|
readonly resetLayout: "重置布局";
|
|
1652
1670
|
readonly total: "总计";
|
|
1671
|
+
readonly noDataAvailable: "暂无数据";
|
|
1672
|
+
readonly noDataSourceFor: "没有可用的数据源:";
|
|
1673
|
+
readonly trend: {
|
|
1674
|
+
readonly vsLastQuarter: "较上季度";
|
|
1675
|
+
readonly vsLastMonth: "较上月";
|
|
1676
|
+
readonly vsLastWeek: "较上周";
|
|
1677
|
+
readonly vsLastYear: "较去年";
|
|
1678
|
+
readonly vsYesterday: "较昨日";
|
|
1679
|
+
readonly vsPreviousPeriod: "较上期";
|
|
1680
|
+
};
|
|
1653
1681
|
};
|
|
1654
1682
|
readonly configPanel: {
|
|
1655
1683
|
readonly save: "保存";
|
package/dist/locales/zh.d.ts
CHANGED
|
@@ -213,6 +213,8 @@ declare const zh: {
|
|
|
213
213
|
readonly moveCard: "移动卡片";
|
|
214
214
|
readonly deleteCard: "删除卡片";
|
|
215
215
|
readonly deleteColumn: "删除列";
|
|
216
|
+
readonly noCards: "暂无卡片";
|
|
217
|
+
readonly cardTitlePlaceholder: "输入卡片标题...";
|
|
216
218
|
};
|
|
217
219
|
readonly timeline: {
|
|
218
220
|
readonly bucket: {
|
|
@@ -250,6 +252,8 @@ declare const zh: {
|
|
|
250
252
|
readonly dragToReorder: "拖动排序";
|
|
251
253
|
readonly defaultView: "默认视图";
|
|
252
254
|
readonly tabActionsFor: "{{name}} 的视图操作";
|
|
255
|
+
readonly readonlyAriaLabel: "只读视图";
|
|
256
|
+
readonly readonlyTooltip: "系统视图 — 复制后可自定义。";
|
|
253
257
|
};
|
|
254
258
|
readonly detail: {
|
|
255
259
|
readonly back: "返回";
|
|
@@ -444,6 +448,16 @@ declare const zh: {
|
|
|
444
448
|
readonly saveLayout: "保存布局";
|
|
445
449
|
readonly resetLayout: "重置布局";
|
|
446
450
|
readonly total: "总计";
|
|
451
|
+
readonly noDataAvailable: "暂无数据";
|
|
452
|
+
readonly noDataSourceFor: "没有可用的数据源:";
|
|
453
|
+
readonly trend: {
|
|
454
|
+
readonly vsLastQuarter: "较上季度";
|
|
455
|
+
readonly vsLastMonth: "较上月";
|
|
456
|
+
readonly vsLastWeek: "较上周";
|
|
457
|
+
readonly vsLastYear: "较去年";
|
|
458
|
+
readonly vsYesterday: "较昨日";
|
|
459
|
+
readonly vsPreviousPeriod: "较上期";
|
|
460
|
+
};
|
|
447
461
|
};
|
|
448
462
|
readonly configPanel: {
|
|
449
463
|
readonly save: "保存";
|
package/dist/locales/zh.js
CHANGED
|
@@ -213,6 +213,8 @@ const zh = {
|
|
|
213
213
|
moveCard: '移动卡片',
|
|
214
214
|
deleteCard: '删除卡片',
|
|
215
215
|
deleteColumn: '删除列',
|
|
216
|
+
noCards: '暂无卡片',
|
|
217
|
+
cardTitlePlaceholder: '输入卡片标题...',
|
|
216
218
|
},
|
|
217
219
|
timeline: {
|
|
218
220
|
bucket: {
|
|
@@ -250,6 +252,8 @@ const zh = {
|
|
|
250
252
|
dragToReorder: '拖动排序',
|
|
251
253
|
defaultView: '默认视图',
|
|
252
254
|
tabActionsFor: '{{name}} 的视图操作',
|
|
255
|
+
readonlyAriaLabel: '只读视图',
|
|
256
|
+
readonlyTooltip: '系统视图 — 复制后可自定义。',
|
|
253
257
|
},
|
|
254
258
|
detail: {
|
|
255
259
|
back: '返回',
|
|
@@ -454,6 +458,16 @@ const zh = {
|
|
|
454
458
|
saveLayout: '保存布局',
|
|
455
459
|
resetLayout: '重置布局',
|
|
456
460
|
total: '总计',
|
|
461
|
+
noDataAvailable: '暂无数据',
|
|
462
|
+
noDataSourceFor: '没有可用的数据源:',
|
|
463
|
+
trend: {
|
|
464
|
+
vsLastQuarter: '较上季度',
|
|
465
|
+
vsLastMonth: '较上月',
|
|
466
|
+
vsLastWeek: '较上周',
|
|
467
|
+
vsLastYear: '较去年',
|
|
468
|
+
vsYesterday: '较昨日',
|
|
469
|
+
vsPreviousPeriod: '较上期',
|
|
470
|
+
},
|
|
457
471
|
},
|
|
458
472
|
configPanel: {
|
|
459
473
|
save: '保存',
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from './formatting';
|
|
2
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';
|
package/dist/utils/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from './formatting';
|
|
2
2
|
export { resolvePlural, formatDateSpec, formatNumberSpec, applyLocaleConfig, } from './spec-formatters';
|
|
3
|
+
export { isSpecTranslationData, transformSpecTranslations, } from './spec-translations';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapters between `@objectstack/spec` `TranslationData` (the wire format
|
|
3
|
+
* served by `/api/v1/i18n/translations/:locale`) and the flat namespace
|
|
4
|
+
* tree that `@object-ui/i18n`'s `useObjectLabel` hook reads.
|
|
5
|
+
*
|
|
6
|
+
* The spec format nests every per-object scope (`fields`, `_views`,
|
|
7
|
+
* `_actions`, …) under `objects.<n>.*`. `useObjectLabel` resolves keys
|
|
8
|
+
* in two different shapes depending on the scope:
|
|
9
|
+
*
|
|
10
|
+
* - **Flattened scopes** — `fields` becomes a top-level `fields.<obj>.<fld>`
|
|
11
|
+
* string map (and field options become `fieldOptions.<obj>.<fld>.<value>`)
|
|
12
|
+
* so per-field label lookup is cheap.
|
|
13
|
+
* - **Nested scopes** — every `_`-prefixed key (`_views`, `_actions`,
|
|
14
|
+
* `_sections`, `_notifications`, `_errors`, `_options`, …) is preserved
|
|
15
|
+
* verbatim under `objects.<n>._xxx`, matching the resolver
|
|
16
|
+
* conventions documented in `useObjectLabel` (e.g.
|
|
17
|
+
* `viewLabel` reads `objects.<n>._views.<view>.label`).
|
|
18
|
+
*
|
|
19
|
+
* The "preserve every `_`-prefixed key" rule is intentional: it future-proofs
|
|
20
|
+
* the transform so newly-introduced spec scopes do **not** need a code change
|
|
21
|
+
* here to flow through to the UI. This prevents a class of silent-failure
|
|
22
|
+
* regressions where a new scope (e.g. `_views`) is added to the spec but the
|
|
23
|
+
* console transform forgets to copy it, leaving lookups to fall back to the
|
|
24
|
+
* untranslated source label.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Minimal shape of a `@objectstack/spec` `TranslationData` payload. We only
|
|
28
|
+
* type the fields this transformer reads; everything else is forwarded
|
|
29
|
+
* through opaquely.
|
|
30
|
+
*/
|
|
31
|
+
export interface SpecTranslationData {
|
|
32
|
+
objects?: Record<string, {
|
|
33
|
+
label?: string;
|
|
34
|
+
pluralLabel?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
fields?: Record<string, {
|
|
37
|
+
label?: string;
|
|
38
|
+
options?: Record<string, string>;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
apps?: unknown;
|
|
44
|
+
messages?: unknown;
|
|
45
|
+
validationMessages?: unknown;
|
|
46
|
+
dashboards?: unknown;
|
|
47
|
+
globalActions?: unknown;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Detect whether the given record uses the spec `TranslationData` format.
|
|
52
|
+
*
|
|
53
|
+
* Returns `true` when `data.objects` exists and at least one entry has a
|
|
54
|
+
* nested `fields` object (the distinguishing trait vs. an already-flattened
|
|
55
|
+
* namespace tree).
|
|
56
|
+
*/
|
|
57
|
+
export declare function isSpecTranslationData(data: Record<string, unknown> | null | undefined): data is SpecTranslationData;
|
|
58
|
+
/**
|
|
59
|
+
* Transform a `@objectstack/spec` `TranslationData` payload into the
|
|
60
|
+
* namespaced tree expected by `useObjectLabel`.
|
|
61
|
+
*
|
|
62
|
+
* The output is wrapped under an `app` namespace key so the hook's
|
|
63
|
+
* `getAppNamespaces()` discovery (which keys off the presence of `objects`
|
|
64
|
+
* and `fields`) picks it up automatically.
|
|
65
|
+
*
|
|
66
|
+
* Behaviour:
|
|
67
|
+
* - `objects.<n>.label / pluralLabel / description` — copied verbatim.
|
|
68
|
+
* - `objects.<n>._*` — every underscore-prefixed key (any depth) is
|
|
69
|
+
* preserved as-is. This is the rule that prevents future spec scopes
|
|
70
|
+
* (`_views`, `_actions`, `_sections`, `_notifications`, `_errors`,
|
|
71
|
+
* `_options`, anything new) from silently disappearing.
|
|
72
|
+
* - `objects.<n>.fields.<f>.label` — flattened to `fields.<n>.<f>`.
|
|
73
|
+
* - `objects.<n>.fields.<f>.options` — flattened to
|
|
74
|
+
* `fieldOptions.<n>.<f>.<value>`.
|
|
75
|
+
* - `apps / messages / validationMessages / dashboards / globalActions`
|
|
76
|
+
* — copied verbatim to the `app` namespace root.
|
|
77
|
+
* - Any other top-level key on the source payload is also copied through,
|
|
78
|
+
* so new top-level spec namespaces flow through without a code change.
|
|
79
|
+
*/
|
|
80
|
+
export declare function transformSpecTranslations(data: SpecTranslationData): Record<string, unknown>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapters between `@objectstack/spec` `TranslationData` (the wire format
|
|
3
|
+
* served by `/api/v1/i18n/translations/:locale`) and the flat namespace
|
|
4
|
+
* tree that `@object-ui/i18n`'s `useObjectLabel` hook reads.
|
|
5
|
+
*
|
|
6
|
+
* The spec format nests every per-object scope (`fields`, `_views`,
|
|
7
|
+
* `_actions`, …) under `objects.<n>.*`. `useObjectLabel` resolves keys
|
|
8
|
+
* in two different shapes depending on the scope:
|
|
9
|
+
*
|
|
10
|
+
* - **Flattened scopes** — `fields` becomes a top-level `fields.<obj>.<fld>`
|
|
11
|
+
* string map (and field options become `fieldOptions.<obj>.<fld>.<value>`)
|
|
12
|
+
* so per-field label lookup is cheap.
|
|
13
|
+
* - **Nested scopes** — every `_`-prefixed key (`_views`, `_actions`,
|
|
14
|
+
* `_sections`, `_notifications`, `_errors`, `_options`, …) is preserved
|
|
15
|
+
* verbatim under `objects.<n>._xxx`, matching the resolver
|
|
16
|
+
* conventions documented in `useObjectLabel` (e.g.
|
|
17
|
+
* `viewLabel` reads `objects.<n>._views.<view>.label`).
|
|
18
|
+
*
|
|
19
|
+
* The "preserve every `_`-prefixed key" rule is intentional: it future-proofs
|
|
20
|
+
* the transform so newly-introduced spec scopes do **not** need a code change
|
|
21
|
+
* here to flow through to the UI. This prevents a class of silent-failure
|
|
22
|
+
* regressions where a new scope (e.g. `_views`) is added to the spec but the
|
|
23
|
+
* console transform forgets to copy it, leaving lookups to fall back to the
|
|
24
|
+
* untranslated source label.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Detect whether the given record uses the spec `TranslationData` format.
|
|
28
|
+
*
|
|
29
|
+
* Returns `true` when `data.objects` exists and at least one entry has a
|
|
30
|
+
* nested `fields` object (the distinguishing trait vs. an already-flattened
|
|
31
|
+
* namespace tree).
|
|
32
|
+
*/
|
|
33
|
+
export function isSpecTranslationData(data) {
|
|
34
|
+
if (!data)
|
|
35
|
+
return false;
|
|
36
|
+
const objects = data.objects;
|
|
37
|
+
if (!objects || typeof objects !== 'object' || Array.isArray(objects))
|
|
38
|
+
return false;
|
|
39
|
+
for (const obj of Object.values(objects)) {
|
|
40
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj) && 'fields' in obj) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/** Top-level keys that are copied through unchanged onto the `app` namespace. */
|
|
47
|
+
const PASSTHROUGH_TOP_LEVEL_KEYS = [
|
|
48
|
+
'apps',
|
|
49
|
+
'messages',
|
|
50
|
+
'validationMessages',
|
|
51
|
+
'dashboards',
|
|
52
|
+
'globalActions',
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Transform a `@objectstack/spec` `TranslationData` payload into the
|
|
56
|
+
* namespaced tree expected by `useObjectLabel`.
|
|
57
|
+
*
|
|
58
|
+
* The output is wrapped under an `app` namespace key so the hook's
|
|
59
|
+
* `getAppNamespaces()` discovery (which keys off the presence of `objects`
|
|
60
|
+
* and `fields`) picks it up automatically.
|
|
61
|
+
*
|
|
62
|
+
* Behaviour:
|
|
63
|
+
* - `objects.<n>.label / pluralLabel / description` — copied verbatim.
|
|
64
|
+
* - `objects.<n>._*` — every underscore-prefixed key (any depth) is
|
|
65
|
+
* preserved as-is. This is the rule that prevents future spec scopes
|
|
66
|
+
* (`_views`, `_actions`, `_sections`, `_notifications`, `_errors`,
|
|
67
|
+
* `_options`, anything new) from silently disappearing.
|
|
68
|
+
* - `objects.<n>.fields.<f>.label` — flattened to `fields.<n>.<f>`.
|
|
69
|
+
* - `objects.<n>.fields.<f>.options` — flattened to
|
|
70
|
+
* `fieldOptions.<n>.<f>.<value>`.
|
|
71
|
+
* - `apps / messages / validationMessages / dashboards / globalActions`
|
|
72
|
+
* — copied verbatim to the `app` namespace root.
|
|
73
|
+
* - Any other top-level key on the source payload is also copied through,
|
|
74
|
+
* so new top-level spec namespaces flow through without a code change.
|
|
75
|
+
*/
|
|
76
|
+
export function transformSpecTranslations(data) {
|
|
77
|
+
const objects = {};
|
|
78
|
+
const fields = {};
|
|
79
|
+
const fieldOptions = {};
|
|
80
|
+
const srcObjects = data.objects;
|
|
81
|
+
if (srcObjects) {
|
|
82
|
+
for (const [objName, objData] of Object.entries(srcObjects)) {
|
|
83
|
+
if (!objData || typeof objData !== 'object')
|
|
84
|
+
continue;
|
|
85
|
+
const obj = {};
|
|
86
|
+
if (objData.label)
|
|
87
|
+
obj.label = objData.label;
|
|
88
|
+
if (objData.pluralLabel)
|
|
89
|
+
obj.pluralLabel = objData.pluralLabel;
|
|
90
|
+
if (objData.description)
|
|
91
|
+
obj.description = objData.description;
|
|
92
|
+
// Preserve EVERY `_`-prefixed nested scope (e.g. `_views`, `_actions`,
|
|
93
|
+
// `_sections`, `_notifications`, `_errors`, `_options`, …). Doing this
|
|
94
|
+
// by-convention (rather than a whitelist) means new spec scopes flow
|
|
95
|
+
// through without a code change here.
|
|
96
|
+
for (const [k, v] of Object.entries(objData)) {
|
|
97
|
+
if (k.startsWith('_'))
|
|
98
|
+
obj[k] = v;
|
|
99
|
+
}
|
|
100
|
+
objects[objName] = obj;
|
|
101
|
+
// Flatten fields: objects.<n>.fields.<f>.label → fields.<n>.<f>
|
|
102
|
+
if (objData.fields && typeof objData.fields === 'object') {
|
|
103
|
+
const fieldEntries = Object.entries(objData.fields);
|
|
104
|
+
for (const [fieldName, fieldData] of fieldEntries) {
|
|
105
|
+
if (fieldData?.label) {
|
|
106
|
+
if (!fields[objName])
|
|
107
|
+
fields[objName] = {};
|
|
108
|
+
fields[objName][fieldName] = fieldData.label;
|
|
109
|
+
}
|
|
110
|
+
if (fieldData?.options &&
|
|
111
|
+
typeof fieldData.options === 'object' &&
|
|
112
|
+
Object.keys(fieldData.options).length > 0) {
|
|
113
|
+
if (!fieldOptions[objName])
|
|
114
|
+
fieldOptions[objName] = {};
|
|
115
|
+
fieldOptions[objName][fieldName] = fieldData.options;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const appNs = {};
|
|
122
|
+
if (Object.keys(objects).length > 0)
|
|
123
|
+
appNs.objects = objects;
|
|
124
|
+
if (Object.keys(fields).length > 0)
|
|
125
|
+
appNs.fields = fields;
|
|
126
|
+
if (Object.keys(fieldOptions).length > 0)
|
|
127
|
+
appNs.fieldOptions = fieldOptions;
|
|
128
|
+
for (const key of PASSTHROUGH_TOP_LEVEL_KEYS) {
|
|
129
|
+
if (data[key] !== undefined)
|
|
130
|
+
appNs[key] = data[key];
|
|
131
|
+
}
|
|
132
|
+
// Forward any future top-level namespace we don't explicitly know about,
|
|
133
|
+
// skipping the ones we've already consumed or flattened.
|
|
134
|
+
const consumedTopLevel = new Set(['objects', ...PASSTHROUGH_TOP_LEVEL_KEYS]);
|
|
135
|
+
for (const [k, v] of Object.entries(data)) {
|
|
136
|
+
if (consumedTopLevel.has(k))
|
|
137
|
+
continue;
|
|
138
|
+
appNs[k] = v;
|
|
139
|
+
}
|
|
140
|
+
return { app: appNs };
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/i18n",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"i18next": "^26.
|
|
33
|
+
"i18next": "^26.2.0",
|
|
34
34
|
"react-i18next": "^17.0.7"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|