@object-ui/i18n 4.0.10 → 4.0.11

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @object-ui/i18n
2
2
 
3
+ ## 4.0.11
4
+
5
+ ### Patch Changes
6
+
7
+ - 1909bc3: Add `transformSpecTranslations` / `isSpecTranslationData` helpers to
8
+ `@object-ui/i18n` so apps no longer need to maintain their own copy of the
9
+ `@objectstack/spec` `TranslationData` → flat namespace transform.
10
+
11
+ The new transform preserves **every** `_`-prefixed object scope by
12
+ convention (`_views`, `_actions`, `_sections`, `_notifications`, `_errors`,
13
+ `_options`, plus anything added in future spec versions), which fixes a
14
+ class of silent-failure regressions where new spec scopes were dropped
15
+ during transformation — leaving e.g. list-view labels to fall back to the
16
+ untranslated source string.
17
+
18
+ `@object-ui/console`'s `loadLanguage.ts` is rewritten to delegate to the
19
+ shared helper.
20
+
3
21
  ## 4.0.10
4
22
 
5
23
  ## 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';
@@ -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";
@@ -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',
@@ -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: "保存";
@@ -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: "保存";
@@ -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: '保存',
@@ -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';
@@ -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.10",
3
+ "version": "4.0.11",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",