@object-ui/i18n 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +2 -0
  4. package/dist/locales/ar.d.ts +314 -0
  5. package/dist/locales/ar.js +314 -0
  6. package/dist/locales/de.d.ts +314 -0
  7. package/dist/locales/de.js +314 -0
  8. package/dist/locales/en.d.ts +372 -0
  9. package/dist/locales/en.js +372 -0
  10. package/dist/locales/es.d.ts +314 -0
  11. package/dist/locales/es.js +314 -0
  12. package/dist/locales/fr.d.ts +314 -0
  13. package/dist/locales/fr.js +314 -0
  14. package/dist/locales/index.d.ts +3256 -0
  15. package/dist/locales/ja.d.ts +314 -0
  16. package/dist/locales/ja.js +314 -0
  17. package/dist/locales/ko.d.ts +314 -0
  18. package/dist/locales/ko.js +314 -0
  19. package/dist/locales/pt.d.ts +314 -0
  20. package/dist/locales/pt.js +314 -0
  21. package/dist/locales/ru.d.ts +314 -0
  22. package/dist/locales/ru.js +314 -0
  23. package/dist/locales/zh.d.ts +372 -0
  24. package/dist/locales/zh.js +372 -0
  25. package/dist/provider.js +34 -4
  26. package/dist/useObjectLabel.d.ts +61 -0
  27. package/dist/useObjectLabel.js +114 -0
  28. package/package.json +3 -3
  29. package/src/__tests__/useObjectLabel.test.tsx +131 -0
  30. package/src/index.ts +3 -0
  31. package/src/locales/ar.ts +314 -0
  32. package/src/locales/de.ts +314 -0
  33. package/src/locales/en.ts +372 -0
  34. package/src/locales/es.ts +314 -0
  35. package/src/locales/fr.ts +314 -0
  36. package/src/locales/ja.ts +314 -0
  37. package/src/locales/ko.ts +314 -0
  38. package/src/locales/pt.ts +314 -0
  39. package/src/locales/ru.ts +314 -0
  40. package/src/locales/zh.ts +372 -0
  41. package/src/provider.tsx +33 -4
  42. package/src/useObjectLabel.ts +122 -0
  43. package/tsconfig.tsbuildinfo +1 -1
package/src/locales/zh.ts CHANGED
@@ -69,6 +69,33 @@ const zh = {
69
69
  selectAllRows: '选择所有行',
70
70
  expandRow: '展开行',
71
71
  collapseRow: '折叠行',
72
+ hideColumn: '隐藏列',
73
+ freezeColumn: '冻结列',
74
+ unfreezeColumn: '取消冻结列',
75
+ pageInfo: '第 {{current}} 页,共 {{total}} 页',
76
+ totalRecords: '共 {{count}} 条',
77
+ noResults: '未找到结果',
78
+ noResultsHint: '请尝试调整筛选条件或搜索关键词。',
79
+ cancelAll: '全部取消',
80
+ saveAll: '全部保存 ({{count}})',
81
+ addRecord: '添加记录',
82
+ open: '打开',
83
+ search: '搜索...',
84
+ modified: '{{count}} 行已修改',
85
+ selected: '已选择 {{count}} 项',
86
+ },
87
+ grid: {
88
+ actions: '操作',
89
+ edit: '编辑',
90
+ delete: '删除',
91
+ export: '导出',
92
+ exportAs: '导出为 {{format}}',
93
+ loading: '加载数据中...',
94
+ errorLoading: '数据加载失败',
95
+ pullToRefresh: '下拉刷新',
96
+ refreshing: '刷新中…',
97
+ openRecord: '打开记录',
98
+ rowHeight: '行高: {{mode}}',
72
99
  },
73
100
  calendar: {
74
101
  today: '今天',
@@ -79,6 +106,38 @@ const zh = {
79
106
  allDay: '全天',
80
107
  noEvents: '暂无事件',
81
108
  newEvent: '新建事件',
109
+ moreEvents: '+{{count}} 更多',
110
+ },
111
+ list: {
112
+ recordCount: '{{count}} 条记录',
113
+ recordCountOne: '{{count}} 条记录',
114
+ addRecord: '添加记录',
115
+ tabs: '标签页',
116
+ allRecords: '全部记录',
117
+ search: '搜索',
118
+ filter: '筛选',
119
+ filterRecords: '筛选记录',
120
+ sort: '排序',
121
+ sortRecords: '排序记录',
122
+ group: '分组',
123
+ groupBy: '分组依据',
124
+ export: '导出',
125
+ exportAs: '导出为 {{format}}',
126
+ color: '颜色',
127
+ rowColor: '行颜色',
128
+ colorByField: '按字段着色',
129
+ clear: '清除',
130
+ none: '无',
131
+ hideFields: '隐藏字段',
132
+ noItems: '未找到项目',
133
+ noItemsMessage: '没有可显示的记录。请尝试调整筛选条件或添加新数据。',
134
+ showAll: '显示全部',
135
+ pullToRefresh: '下拉刷新',
136
+ refreshing: '刷新中…',
137
+ share: '分享',
138
+ print: '打印',
139
+ hideFieldsTitle: '隐藏字段',
140
+ dataLimitReached: '显示前 {{limit}} 条记录。可能有更多数据。',
82
141
  },
83
142
  kanban: {
84
143
  addCard: '添加卡片',
@@ -87,6 +146,41 @@ const zh = {
87
146
  deleteCard: '删除卡片',
88
147
  deleteColumn: '删除列',
89
148
  },
149
+ detail: {
150
+ back: '返回',
151
+ edit: '编辑',
152
+ editInline: '内联编辑',
153
+ save: '保存',
154
+ saveChanges: '保存更改',
155
+ editFieldsInline: '内联编辑字段',
156
+ share: '分享',
157
+ duplicate: '复制',
158
+ export: '导出',
159
+ viewHistory: '查看历史',
160
+ delete: '删除',
161
+ moreActions: '更多操作',
162
+ addToFavorites: '添加到收藏',
163
+ removeFromFavorites: '从收藏中移除',
164
+ previousRecord: '上一条记录',
165
+ nextRecord: '下一条记录',
166
+ recordOf: '第 {{current}} 条,共 {{total}} 条',
167
+ recordNotFound: '未找到记录',
168
+ recordNotFoundDescription: '您查找的记录不存在或已被删除。',
169
+ goBack: '返回',
170
+ details: '详情',
171
+ related: '相关',
172
+ relatedRecords: '{{count}} 条记录',
173
+ relatedRecordOne: '{{count}} 条记录',
174
+ noRelatedRecords: '暂无相关记录',
175
+ loading: '加载中...',
176
+ copyToClipboard: '复制到剪贴板',
177
+ copied: '已复制!',
178
+ deleteConfirmation: '确定要删除此记录吗?',
179
+ editRecord: '编辑记录',
180
+ viewAll: '查看全部',
181
+ new: '新建',
182
+ emptyValue: '—',
183
+ },
90
184
  chart: {
91
185
  noData: '暂无图表数据',
92
186
  loading: '图表加载中...',
@@ -98,9 +192,162 @@ const zh = {
98
192
  saveLayout: '保存布局',
99
193
  resetLayout: '重置布局',
100
194
  },
195
+ configPanel: {
196
+ save: '保存',
197
+ discard: '丢弃',
198
+ close: '关闭',
199
+ layout: '布局',
200
+ columns: '列数',
201
+ gap: '间距',
202
+ rowHeight: '行高',
203
+ data: '数据',
204
+ refreshInterval: '刷新间隔',
205
+ appearance: '外观',
206
+ title: '标题',
207
+ showDescription: '显示描述',
208
+ theme: '主题',
209
+ configuration: '配置',
210
+ general: '基本',
211
+ advanced: '高级',
212
+ },
213
+ appDesigner: {
214
+ createApp: '创建应用',
215
+ editApp: '编辑应用',
216
+ basicInfo: '基本信息',
217
+ objects: '业务对象',
218
+ navigation: '导航',
219
+ branding: '品牌',
220
+ appName: '应用名称',
221
+ appTitle: '标题',
222
+ appDescription: '描述',
223
+ appIcon: '图标',
224
+ template: '模板',
225
+ layout: '布局',
226
+ layoutSidebar: '侧边栏',
227
+ layoutHeader: '顶部导航',
228
+ layoutEmpty: '空白',
229
+ selectObjects: '选择对象',
230
+ searchObjects: '搜索对象…',
231
+ selectAll: '全选',
232
+ deselectAll: '取消全选',
233
+ navBuilder: '导航构建器',
234
+ addGroup: '添加分组',
235
+ addUrl: '添加链接',
236
+ addSeparator: '添加分隔线',
237
+ noNavItems: '暂无导航项。',
238
+ logoUrl: 'Logo 链接',
239
+ primaryColor: '主色调',
240
+ faviconUrl: '网站图标链接',
241
+ preview: '预览',
242
+ complete: '完成',
243
+ snakeCaseHint: '必须使用 snake_case 格式(如 my_app)',
244
+ modeEdit: '编辑',
245
+ modePreview: '预览',
246
+ modeCode: '代码',
247
+ addWidget: '添加组件',
248
+ widgetProperties: '组件属性',
249
+ dataSource: '数据源',
250
+ valueField: '数值字段',
251
+ aggregate: '聚合',
252
+ colorVariant: '颜色',
253
+ addComponent: '添加组件',
254
+ componentProperties: '组件属性',
255
+ viewType: '视图类型',
256
+ fields: '字段',
257
+ toolbar: '工具栏',
258
+ showSearch: '显示搜索',
259
+ showFilters: '显示筛选',
260
+ showSort: '显示排序',
261
+ appearance: '外观',
262
+ rowHeight: '行高',
263
+ stripedRows: '斑马纹',
264
+ bordered: '边框',
265
+ livePreview: '实时预览',
266
+ stepBasicDesc: '名称、标题和布局',
267
+ stepObjectsDesc: '选择业务对象',
268
+ stepNavigationDesc: '构建导航树',
269
+ stepBrandingDesc: 'Logo、颜色和图标',
270
+ noObjectsFound: '未找到对象。',
271
+ noNavItemsHint: '暂无导航项。请在上一步选择对象或手动添加项目。',
272
+ separator: '分隔线',
273
+ separatorLabel: '— 分隔线 —',
274
+ newGroup: '新建分组',
275
+ newLink: '新建链接',
276
+ saveDraft: '保存草稿',
277
+ cancelConfirmTitle: '放弃更改?',
278
+ cancelConfirmMessage: '您有未保存的更改。确定要取消吗?',
279
+ confirmDiscard: '放弃',
280
+ keepEditing: '继续编辑',
281
+ navNoItems: '暂无导航项。点击上方按钮添加项目。',
282
+ navNoPreviewItems: '无项目',
283
+ navLivePreview: '实时预览',
284
+ navCollapseGroup: '折叠分组',
285
+ navExpandGroup: '展开分组',
286
+ navAddChild: '添加子项',
287
+ navMoveUp: '上移',
288
+ navMoveDown: '下移',
289
+ navRemove: '移除',
290
+ navObjectPage: '对象页面',
291
+ navDashboard: '仪表盘',
292
+ navPage: '页面',
293
+ navReport: '报表',
294
+ navGroup: '分组',
295
+ navUrl: '链接',
296
+ navSeparator: '分隔线',
297
+ navTypeObject: '对象',
298
+ navTypeDashboard: '仪表盘',
299
+ navTypePage: '页面',
300
+ navTypeReport: '报表',
301
+ navTypeUrl: '链接',
302
+ navTypeGroup: '分组',
303
+ navTypeSeparator: '分隔线',
304
+ navTypeAction: '操作',
305
+ navEditIcon: '编辑图标',
306
+ navToggleVisible: '切换可见性',
307
+ navHidden: '已隐藏',
308
+ navExportSchema: '导出 JSON',
309
+ navImportSchema: '导入 JSON',
310
+ navExportSuccess: '导航结构已导出',
311
+ navImportSuccess: '导航结构已导入',
312
+ navImportError: '无效的导航 JSON',
313
+ navIconPlaceholder: '图标名称(如 Users)',
314
+ dashboardEditor: '仪表盘编辑器',
315
+ noWidgets: '暂无小组件。点击上方按钮添加。',
316
+ widgetLayoutSize: '布局尺寸',
317
+ widgetWidth: '宽度',
318
+ widgetHeight: '高度',
319
+ dashboardPreview: '仪表盘预览',
320
+ noWidgetsPreview: '暂无可预览的小组件',
321
+ pageCanvasEditor: '页面画布编辑器',
322
+ emptyPage: '空白页面。点击上方按钮添加组件。',
323
+ pagePreview: '页面预览',
324
+ noComponentsPreview: '暂无可预览的组件',
325
+ modePage: '页面',
326
+ modeDashboard: '仪表盘',
327
+ undo: '撤销',
328
+ redo: '重做',
329
+ brandingEditor: '品牌编辑器',
330
+ brandingExport: '导出 JSON',
331
+ brandingImport: '导入 JSON',
332
+ brandingPreview: '预览',
333
+ brandingSampleButton: '示例按钮',
334
+ brandingSampleText: '这是您的品牌主题效果预览。',
335
+ colorPalette: '颜色面板',
336
+ fontFamily: '字体',
337
+ fontDefault: '默认(系统)',
338
+ modeLight: '浅色',
339
+ modeDark: '深色',
340
+ mobilePreview: '移动端预览',
341
+ },
101
342
  console: {
102
343
  title: 'ObjectStack 控制台',
103
344
  initializing: '正在初始化应用程序...',
345
+ breadcrumb: {
346
+ dashboards: '仪表盘',
347
+ pages: '页面',
348
+ reports: '报告',
349
+ system: '系统',
350
+ },
104
351
  loadingSteps: {
105
352
  connecting: '正在连接数据源',
106
353
  loadingConfig: '正在加载配置',
@@ -140,6 +387,7 @@ const zh = {
140
387
  systemTheme: '系统主题',
141
388
  actions: '操作',
142
389
  openFullSearch: '打开完整搜索页面',
390
+ createApp: '创建新应用',
143
391
  },
144
392
  errors: {
145
393
  somethingWentWrong: '出错了',
@@ -166,6 +414,130 @@ const zh = {
166
414
  addView: '添加视图',
167
415
  designTools: '设计工具',
168
416
  new: '新建',
417
+ configureView: '配置视图',
418
+ closePanel: '关闭面板',
419
+ page: '页面',
420
+ pageConfigHint: '工具栏、导航与视图外壳设置',
421
+ listConfigHint: '此列表类型的数据渲染、列与外观',
422
+ general: '常规',
423
+ generalHint: '视图标题、描述和类型',
424
+ toolbar: '工具栏',
425
+ toolbarHint: '搜索、筛选、排序、分组和密度切换',
426
+ toolbarEnabledCount: '{{count}} of {{total}} enabled',
427
+ navigationSection: '导航',
428
+ navigationHint: '行点击行为和详情视图设置',
429
+ records: '记录',
430
+ recordsHint: '选择模式、添加记录和内联编辑',
431
+ exportPrint: '导出与打印',
432
+ exportPrintHint: '导出格式、打印和记录数显示',
433
+ showAllFields: '全部显示',
434
+ hideAllFields: '全部隐藏',
435
+ searchFields: '搜索字段...',
436
+ title: '标题',
437
+ description: '描述',
438
+ data: '数据',
439
+ source: '数据源',
440
+ columns: '列',
441
+ filterBy: '筛选条件',
442
+ sortBy: '排序条件',
443
+ appearance: '外观',
444
+ showDescription: '显示描述',
445
+ viewType: '视图类型',
446
+ userFilters: '用户筛选器',
447
+ enableSearch: '启用搜索',
448
+ enableFilter: '启用筛选',
449
+ enableSort: '启用排序',
450
+ enableHideFields: '启用隐藏字段',
451
+ enableGroup: '启用分组',
452
+ enableColor: '启用颜色',
453
+ enableDensity: '启用密度',
454
+ userActions: '用户操作',
455
+ addRecordViaForm: '通过表单添加记录',
456
+ advanced: '高级',
457
+ allowExport: '允许导出',
458
+ recordCount: '{{count}} 条记录',
459
+ noDescription: '无描述',
460
+ none: '无',
461
+ columnsConfigured: '{{count}} 列',
462
+ save: '保存',
463
+ discard: '丢弃',
464
+ createView: '创建视图',
465
+ newView: '新视图',
466
+ typeOptions: '类型选项',
467
+ groupByField: '分组字段',
468
+ startDateField: '开始日期字段',
469
+ titleField: '标题字段',
470
+ latitudeField: '纬度字段',
471
+ longitudeField: '经度字段',
472
+ imageField: '图片字段',
473
+ dateField: '日期字段',
474
+ selectField: '选择字段...',
475
+ gridOptionsHint: '网格视图使用上方配置的列。',
476
+ groupBy: '分组依据',
477
+ prefixField: '前缀字段',
478
+ fields: '字段',
479
+ fieldsVisible: '{{count}} 个可见',
480
+ sortsCount: '{{count}} 个排序',
481
+ filtersCount: '{{count}} 个筛选',
482
+ endDateField: '结束日期字段',
483
+ color: '颜色',
484
+ fieldTextColor: '字段文字颜色',
485
+ rowHeight: '行高',
486
+ wrapHeaders: '自动换行标题',
487
+ showFieldDescriptions: '显示字段描述',
488
+ collapseAllByDefault: '默认全部折叠',
489
+ striped: '斑马纹行',
490
+ bordered: '边框单元格',
491
+ inlineEdit: '内联编辑记录',
492
+ addDeleteRecordsInline: '内联添加/删除记录',
493
+ clickIntoRecordDetails: '点击进入记录详情',
494
+ navigationMode: '导航模式',
495
+ navigationWidth: '导航宽度',
496
+ navigationWidthHint: '适用于抽屉、弹窗和分屏导航模式',
497
+ openNewTab: '在新标签页打开',
498
+ openNewTabHint: '适用于页面和新窗口导航模式',
499
+ selectionMode: '选择模式',
500
+ selectionNone: '无',
501
+ selectionSingle: '单选',
502
+ selectionMultiple: '多选',
503
+ pageSize: '每页记录数',
504
+ pageSizeOptions: '每页选项',
505
+ exportFormats: '导出格式',
506
+ exportMaxRecords: '最大记录数',
507
+ exportIncludeHeaders: '包含表头',
508
+ exportFileNamePrefix: '文件名前缀',
509
+ searchableFields: '可搜索字段',
510
+ filterableFields: '可筛选字段',
511
+ resizableColumns: '可调整列宽',
512
+ densityCompact: '紧凑',
513
+ densityComfortable: '舒适',
514
+ densitySpacious: '宽松',
515
+ densityMode: '密度模式',
516
+ hiddenFields: '隐藏字段',
517
+ rowActions: '行操作',
518
+ bulkActions: '批量操作',
519
+ sharing: '共享',
520
+ sharingEnabled: '启用共享',
521
+ sharingVisibility: '可见性',
522
+ addRecordEnabled: '启用添加记录',
523
+ addRecordPosition: '位置',
524
+ addRecordMode: '模式',
525
+ addRecordFormView: '表单视图',
526
+ conditionalFormatting: '条件格式',
527
+ addRule: '添加规则',
528
+ quickFilters: '快速筛选',
529
+ addQuickFilter: '添加快速筛选',
530
+ showRecordCount: '显示记录计数',
531
+ allowPrinting: '允许打印',
532
+ virtualScroll: '虚拟滚动',
533
+ emptyStateTitle: '空状态标题',
534
+ emptyStateMessage: '空状态消息',
535
+ emptyStateIcon: '空状态图标',
536
+ ariaLabel: 'ARIA 标签',
537
+ ariaDescribedBy: 'ARIA 描述',
538
+ ariaLive: 'ARIA 实时区域',
539
+ accessibility: '无障碍',
540
+ viewTabs: '视图标签',
169
541
  },
170
542
  localeSwitcher: {
171
543
  label: '语言',
package/src/provider.tsx CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides I18nProvider and useObjectTranslation hook for React components.
5
5
  */
6
- import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
6
+ import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
7
7
  import { I18nextProvider, useTranslation } from 'react-i18next';
8
8
  import type { i18n as I18nInstance } from 'i18next';
9
9
  import { createI18n, getDirection, type I18nConfig } from './i18n';
@@ -67,6 +67,10 @@ export function I18nProvider({ config, instance: externalInstance, loadLanguage,
67
67
  const [language, setLanguage] = useState(i18nInstance.language || 'en');
68
68
  const direction = getDirection(language);
69
69
 
70
+ // Track which languages have had app-specific translations loaded
71
+ // (separate from built-in locales that ship with the library)
72
+ const loadedAppLangs = useRef<Set<string>>(new Set());
73
+
70
74
  useEffect(() => {
71
75
  const handleLanguageChanged = (lng: string) => {
72
76
  setLanguage(lng);
@@ -83,14 +87,39 @@ export function I18nProvider({ config, instance: externalInstance, loadLanguage,
83
87
  };
84
88
  }, [i18nInstance]);
85
89
 
90
+ // Load app-specific translations for the initial language on mount
91
+ useEffect(() => {
92
+ if (!loadLanguage) return;
93
+ const currentLang = i18nInstance.language || 'en';
94
+ if (loadedAppLangs.current.has(currentLang)) return;
95
+ loadedAppLangs.current.add(currentLang);
96
+ loadLanguage(currentLang).then((resources) => {
97
+ if (resources && Object.keys(resources).length > 0) {
98
+ i18nInstance.addResourceBundle(currentLang, 'translation', resources, true, true);
99
+ // Force re-render so components pick up newly loaded translations
100
+ setLanguage(currentLang);
101
+ }
102
+ }).catch((err) => {
103
+ // Allow retry on failure by removing from loaded set
104
+ loadedAppLangs.current.delete(currentLang);
105
+ console.warn(`[i18n] Failed to load app translations for '${currentLang}':`, err);
106
+ });
107
+ }, [i18nInstance, loadLanguage]);
108
+
86
109
  const contextValue = useMemo<I18nContextValue>(
87
110
  () => ({
88
111
  language,
89
112
  changeLanguage: async (lang: string) => {
90
113
  // Dynamic language pack loading (v2.0.7)
91
- if (loadLanguage && !i18nInstance.hasResourceBundle(lang, 'translation')) {
92
- const resources = await loadLanguage(lang);
93
- i18nInstance.addResourceBundle(lang, 'translation', resources, true, true);
114
+ if (loadLanguage && !loadedAppLangs.current.has(lang)) {
115
+ loadedAppLangs.current.add(lang);
116
+ try {
117
+ const resources = await loadLanguage(lang);
118
+ i18nInstance.addResourceBundle(lang, 'translation', resources, true, true);
119
+ } catch (err) {
120
+ loadedAppLangs.current.delete(lang);
121
+ console.warn(`[i18n] Failed to load app translations for '${lang}':`, err);
122
+ }
94
123
  }
95
124
  await i18nInstance.changeLanguage(lang);
96
125
  },
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @object-ui/i18n - Convention-based Object & Field Label i18n
3
+ *
4
+ * Provides automatic translation resolution for object metadata labels
5
+ * using Salesforce-style convention-based key generation.
6
+ *
7
+ * The app namespace (e.g. "crm") is discovered dynamically from loaded
8
+ * i18next resources — no hardcoded app names in platform code.
9
+ *
10
+ * Convention Rules:
11
+ * | What | Auto-generated key | Fallback |
12
+ * |--------------------|-------------------------------------------------|-----------------------|
13
+ * | Object label | {ns}.objects.{objectName}.label | objectDef.label |
14
+ * | Object description | {ns}.objects.{objectName}.description | objectDef.description |
15
+ * | Field label | {ns}.fields.{objectName}.{fieldName} | field.label |
16
+ *
17
+ * @module useObjectLabel
18
+ */
19
+
20
+ import { useObjectTranslation } from './provider';
21
+
22
+ /**
23
+ * Built-in Object UI top-level locale keys — not app namespaces.
24
+ * Update this set when new top-level platform translation keys are added
25
+ * to `packages/i18n/src/locales/en.ts` to prevent them from being treated
26
+ * as app namespaces during dynamic namespace discovery.
27
+ */
28
+ const BUILTIN_KEYS = new Set([
29
+ 'common', 'validation', 'form', 'table', 'grid', 'calendar',
30
+ 'list', 'kanban', 'chart', 'dashboard', 'configPanel',
31
+ 'appDesigner', 'console', 'errors', 'detail',
32
+ ]);
33
+
34
+ /**
35
+ * Hook for convention-based auto-resolution of object and field labels.
36
+ *
37
+ * Automatically constructs i18n keys from object/field names and looks up
38
+ * translations, falling back to the plain-string label when no translation exists.
39
+ *
40
+ * The app namespace is discovered dynamically from loaded i18next resources
41
+ * by finding top-level keys that contain an `objects` sub-key.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * const { objectLabel, fieldLabel } = useObjectLabel();
46
+ * <h1>{objectLabel(objectDef)}</h1>
47
+ * ```
48
+ */
49
+ export function useObjectLabel() {
50
+ const { t, i18n } = useObjectTranslation();
51
+
52
+ /**
53
+ * Discover app namespace(s) from loaded i18next resources.
54
+ * Returns top-level keys (outside built-in Object UI keys) that contain
55
+ * an `objects` or `fields` sub-key — e.g. "crm" when resources include crm.objects.*.
56
+ */
57
+ const getAppNamespaces = (): string[] => {
58
+ if (!i18n || typeof i18n.getResourceBundle !== 'function') return [];
59
+ const lang = i18n.language || 'en';
60
+ const bundle = i18n.getResourceBundle(lang, 'translation') as Record<string, any> | undefined;
61
+ if (!bundle) return [];
62
+ return Object.keys(bundle).filter(
63
+ (key) => !BUILTIN_KEYS.has(key) && bundle[key] && typeof bundle[key] === 'object'
64
+ && (bundle[key].objects || bundle[key].fields),
65
+ );
66
+ };
67
+
68
+ /** Try resolving a key across all discovered app namespaces. */
69
+ const resolve = (suffix: string, fallback: string): string => {
70
+ try {
71
+ const namespaces = getAppNamespaces();
72
+ for (const ns of namespaces) {
73
+ const key = `${ns}.${suffix}`;
74
+ const translated = t(key, { defaultValue: '' });
75
+ if (translated && translated !== key && translated !== '') {
76
+ return translated;
77
+ }
78
+ }
79
+ } catch {
80
+ // Graceful degradation when i18n provider is not available
81
+ }
82
+ return fallback;
83
+ };
84
+
85
+ return {
86
+ /**
87
+ * Resolve translated object label, falling back to objectDef.label.
88
+ */
89
+ objectLabel: (objectDef: { name: string; label: string }) =>
90
+ resolve(`objects.${objectDef.name}.label`, objectDef.label),
91
+
92
+ /**
93
+ * Resolve translated object description, falling back to objectDef.description.
94
+ */
95
+ objectDescription: (objectDef: { name: string; description?: string }) => {
96
+ if (!objectDef.description) return undefined;
97
+ return resolve(`objects.${objectDef.name}.description`, objectDef.description);
98
+ },
99
+
100
+ /**
101
+ * Resolve translated field label, falling back to the provided fallback string.
102
+ */
103
+ fieldLabel: (objectName: string, fieldName: string, fallback: string) =>
104
+ resolve(`fields.${objectName}.${fieldName}`, fallback),
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Safe wrapper for useObjectLabel that falls back to identity functions
110
+ * when no I18nProvider is available. Suitable for plugin components that
111
+ * may be rendered outside an i18n context.
112
+ */
113
+ export function useSafeFieldLabel() {
114
+ try {
115
+ const { fieldLabel } = useObjectLabel();
116
+ return { fieldLabel };
117
+ } catch {
118
+ return {
119
+ fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
120
+ };
121
+ }
122
+ }