@object-ui/i18n 4.3.0 → 4.5.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 +18 -0
- package/dist/locales/ar.d.ts +23 -0
- package/dist/locales/ar.js +23 -0
- package/dist/locales/de.d.ts +23 -0
- package/dist/locales/de.js +23 -0
- package/dist/locales/en.d.ts +45 -0
- package/dist/locales/en.js +45 -0
- package/dist/locales/es.d.ts +23 -0
- package/dist/locales/es.js +23 -0
- package/dist/locales/fr.d.ts +23 -0
- package/dist/locales/fr.js +23 -0
- package/dist/locales/index.d.ts +274 -0
- package/dist/locales/ja.d.ts +23 -0
- package/dist/locales/ja.js +23 -0
- package/dist/locales/ko.d.ts +23 -0
- package/dist/locales/ko.js +23 -0
- package/dist/locales/pt.d.ts +23 -0
- package/dist/locales/pt.js +23 -0
- package/dist/locales/ru.d.ts +23 -0
- package/dist/locales/ru.js +23 -0
- package/dist/locales/zh.d.ts +45 -0
- package/dist/locales/zh.js +45 -0
- package/dist/useObjectLabel.d.ts +7 -7
- package/dist/useObjectLabel.js +275 -257
- package/package.json +1 -1
package/dist/useObjectLabel.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module useObjectLabel
|
|
18
18
|
*/
|
|
19
|
+
import { useMemo } from 'react';
|
|
19
20
|
import { useObjectTranslation } from './provider';
|
|
20
21
|
/**
|
|
21
22
|
* Built-in Object UI top-level locale keys — not app namespaces.
|
|
@@ -45,282 +46,299 @@ const BUILTIN_KEYS = new Set([
|
|
|
45
46
|
*/
|
|
46
47
|
export function useObjectLabel() {
|
|
47
48
|
const { t, i18n } = useObjectTranslation();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
const getAppNamespaces = () => {
|
|
55
|
-
if (!i18n || typeof i18n.getResourceBundle !== 'function')
|
|
56
|
-
return [];
|
|
57
|
-
const lang = i18n.language || 'en';
|
|
58
|
-
const bundle = i18n.getResourceBundle(lang, 'translation');
|
|
59
|
-
if (!bundle)
|
|
60
|
-
return [];
|
|
61
|
-
return Object.keys(bundle).filter((key) => !BUILTIN_KEYS.has(key) && bundle[key] && typeof bundle[key] === 'object'
|
|
62
|
-
&& (bundle[key].objects || bundle[key].fields || bundle[key].apps
|
|
63
|
-
|| bundle[key].dashboards || bundle[key].pages || bundle[key].reports));
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* Strip an ObjectStack namespace prefix (e.g. `crm__lead` → `lead`) so that
|
|
67
|
-
* translations authored against short object names still resolve when the
|
|
68
|
-
* runtime presents fully-qualified names. The first `__` separates the
|
|
69
|
-
* package namespace from the base name; everything after is preserved.
|
|
70
|
-
*/
|
|
71
|
-
const stripNamespace = (name) => {
|
|
72
|
-
const idx = name.indexOf('__');
|
|
73
|
-
return idx > 0 ? name.slice(idx + 2) : name;
|
|
74
|
-
};
|
|
75
|
-
/** Try resolving a key across all discovered app namespaces. */
|
|
76
|
-
const resolve = (suffixes, fallback) => {
|
|
77
|
-
const suffixList = Array.isArray(suffixes) ? suffixes : [suffixes];
|
|
78
|
-
try {
|
|
79
|
-
const namespaces = getAppNamespaces();
|
|
80
|
-
for (const ns of namespaces) {
|
|
81
|
-
for (const suffix of suffixList) {
|
|
82
|
-
const key = `${ns}.${suffix}`;
|
|
83
|
-
const translated = t(key, { defaultValue: '' });
|
|
84
|
-
if (translated && translated !== key && translated !== '') {
|
|
85
|
-
return translated;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// Graceful degradation when i18n provider is not available
|
|
92
|
-
}
|
|
93
|
-
return fallback;
|
|
94
|
-
};
|
|
95
|
-
/** Build suffix candidates: prefer the given name, fall back to the base (unprefixed) name. */
|
|
96
|
-
const objectSuffixes = (objectName, tail) => {
|
|
97
|
-
const base = stripNamespace(objectName);
|
|
98
|
-
return base !== objectName
|
|
99
|
-
? [`objects.${objectName}.${tail}`, `objects.${base}.${tail}`]
|
|
100
|
-
: [`objects.${objectName}.${tail}`];
|
|
101
|
-
};
|
|
102
|
-
const fieldSuffixes = (objectName, fieldName) => {
|
|
103
|
-
const base = stripNamespace(objectName);
|
|
104
|
-
return base !== objectName
|
|
105
|
-
? [`fields.${objectName}.${fieldName}`, `fields.${base}.${fieldName}`]
|
|
106
|
-
: [`fields.${objectName}.${fieldName}`];
|
|
107
|
-
};
|
|
108
|
-
const optionSuffixes = (objectName, fieldName, optionValue) => {
|
|
109
|
-
const base = stripNamespace(objectName);
|
|
110
|
-
return base !== objectName
|
|
111
|
-
? [
|
|
112
|
-
`fieldOptions.${objectName}.${fieldName}.${optionValue}`,
|
|
113
|
-
`fieldOptions.${base}.${fieldName}.${optionValue}`,
|
|
114
|
-
]
|
|
115
|
-
: [`fieldOptions.${objectName}.${fieldName}.${optionValue}`];
|
|
116
|
-
};
|
|
117
|
-
/**
|
|
118
|
-
* Build suffix candidates for a dashboard-scoped key. Mirrors the
|
|
119
|
-
* object/field convention: prefer the (possibly namespaced) dashboard name,
|
|
120
|
-
* fall back to the unprefixed base name.
|
|
121
|
-
*/
|
|
122
|
-
const dashboardSuffixes = (dashboardName, tail) => {
|
|
123
|
-
const base = stripNamespace(dashboardName);
|
|
124
|
-
return base !== dashboardName
|
|
125
|
-
? [`dashboards.${dashboardName}.${tail}`, `dashboards.${base}.${tail}`]
|
|
126
|
-
: [`dashboards.${dashboardName}.${tail}`];
|
|
127
|
-
};
|
|
128
|
-
/**
|
|
129
|
-
* Build suffix candidates for a page-scoped key.
|
|
130
|
-
*/
|
|
131
|
-
const pageSuffixes = (pageName, tail) => {
|
|
132
|
-
const base = stripNamespace(pageName);
|
|
133
|
-
return base !== pageName
|
|
134
|
-
? [`pages.${pageName}.${tail}`, `pages.${base}.${tail}`]
|
|
135
|
-
: [`pages.${pageName}.${tail}`];
|
|
136
|
-
};
|
|
137
|
-
/**
|
|
138
|
-
* Build suffix candidates for a report-scoped key.
|
|
139
|
-
*/
|
|
140
|
-
const reportSuffixes = (reportName, tail) => {
|
|
141
|
-
const base = stripNamespace(reportName);
|
|
142
|
-
return base !== reportName
|
|
143
|
-
? [`reports.${reportName}.${tail}`, `reports.${base}.${tail}`]
|
|
144
|
-
: [`reports.${reportName}.${tail}`];
|
|
145
|
-
};
|
|
146
|
-
return {
|
|
147
|
-
/**
|
|
148
|
-
* Resolve translated object label, falling back to objectDef.label.
|
|
149
|
-
*/
|
|
150
|
-
objectLabel: (objectDef) => resolve(objectSuffixes(objectDef.name, 'label'), objectDef.label),
|
|
151
|
-
/**
|
|
152
|
-
* Resolve translated object description, falling back to objectDef.description.
|
|
153
|
-
*/
|
|
154
|
-
objectDescription: (objectDef) => {
|
|
155
|
-
if (!objectDef.description)
|
|
156
|
-
return undefined;
|
|
157
|
-
return resolve(objectSuffixes(objectDef.name, 'description'), objectDef.description);
|
|
158
|
-
},
|
|
159
|
-
/**
|
|
160
|
-
* Resolve translated field label, falling back to the provided fallback string.
|
|
161
|
-
*/
|
|
162
|
-
fieldLabel: (objectName, fieldName, fallback) => resolve(fieldSuffixes(objectName, fieldName), fallback),
|
|
163
|
-
/**
|
|
164
|
-
* Resolve a translated select option label for a given object field.
|
|
165
|
-
* Falls back to the provided fallback (usually the English option label).
|
|
166
|
-
*/
|
|
167
|
-
fieldOptionLabel: (objectName, fieldName, optionValue, fallback) => resolve(optionSuffixes(objectName, fieldName, optionValue), fallback),
|
|
168
|
-
/**
|
|
169
|
-
* Translate all options for a given field, returning a new options array
|
|
170
|
-
* with translated labels. Pass the objectName and fieldName to look up
|
|
171
|
-
* translations; the original label is used as fallback.
|
|
172
|
-
*/
|
|
173
|
-
translateOptions: (objectName, fieldName, options) => options.map(opt => ({
|
|
174
|
-
...opt,
|
|
175
|
-
label: resolve(optionSuffixes(objectName, fieldName, opt.value), opt.label),
|
|
176
|
-
})),
|
|
177
|
-
/**
|
|
178
|
-
* Resolve translated app label, falling back to appDef.label.
|
|
179
|
-
* Looks up `{ns}.apps.{appName}.label` from loaded i18next resources.
|
|
180
|
-
*/
|
|
181
|
-
appLabel: (appDef) => resolve(`apps.${appDef.name}.label`, appDef.label ?? appDef.name),
|
|
182
|
-
/**
|
|
183
|
-
* Resolve translated app description, falling back to appDef.description.
|
|
184
|
-
* Returns the translated value even when metadata has no description —
|
|
185
|
-
* translation-only descriptions (defined only in i18n bundles) are common
|
|
186
|
-
* in examples where the app metadata is English-only.
|
|
187
|
-
*/
|
|
188
|
-
appDescription: (appDef) => {
|
|
189
|
-
const fallback = appDef.description ?? '';
|
|
190
|
-
const resolved = resolve(`apps.${appDef.name}.description`, fallback);
|
|
191
|
-
return resolved || undefined;
|
|
192
|
-
},
|
|
193
|
-
/**
|
|
194
|
-
* Resolve translated label for a navigation group within an app.
|
|
195
|
-
* Convention: `{ns}.apps.{appName}.navigation.{groupId}.label`.
|
|
196
|
-
*
|
|
197
|
-
* Mirrors `objectLabel`/`dashboardLabel` so app metadata can keep
|
|
198
|
-
* English fallbacks while translation packs supply localised
|
|
199
|
-
* sidebar group labels (e.g. "Sales" → "销售") without explicit
|
|
200
|
-
* I18nLabel `{ key, defaultValue }` annotations.
|
|
201
|
-
*/
|
|
202
|
-
navGroupLabel: (appName, groupId, fallback) => resolve(`apps.${appName}.navigation.${groupId}.label`, fallback),
|
|
203
|
-
/**
|
|
204
|
-
* Resolve translated dashboard label, falling back to dashboardDef.label.
|
|
205
|
-
* Convention: `{ns}.dashboards.{dashboardName}.label`.
|
|
206
|
-
*/
|
|
207
|
-
dashboardLabel: (dashboardDef) => resolve(dashboardSuffixes(dashboardDef.name, 'label'), dashboardDef.label ?? dashboardDef.name),
|
|
208
|
-
/**
|
|
209
|
-
* Resolve translated dashboard description, falling back to
|
|
210
|
-
* dashboardDef.description. Returns undefined when neither metadata nor
|
|
211
|
-
* translation provides one.
|
|
212
|
-
* Convention: `{ns}.dashboards.{dashboardName}.description`.
|
|
213
|
-
*/
|
|
214
|
-
dashboardDescription: (dashboardDef) => {
|
|
215
|
-
const fallback = dashboardDef.description ?? '';
|
|
216
|
-
const resolved = resolve(dashboardSuffixes(dashboardDef.name, 'description'), fallback);
|
|
217
|
-
return resolved || undefined;
|
|
218
|
-
},
|
|
219
|
-
/**
|
|
220
|
-
* Resolve translated dashboard header-action label.
|
|
221
|
-
* Convention: `{ns}.dashboards.{dashboardName}.actions.{actionKey}.label`.
|
|
222
|
-
* The actionKey is typically the action's `actionUrl` (e.g.
|
|
223
|
-
* `create_opportunity`) or its English label slugified.
|
|
224
|
-
*/
|
|
225
|
-
dashboardActionLabel: (dashboardName, actionKey, fallback) => resolve(dashboardSuffixes(dashboardName, `actions.${actionKey}.label`), fallback),
|
|
226
|
-
/**
|
|
227
|
-
* Resolve translated widget title within a dashboard.
|
|
228
|
-
* Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.title`.
|
|
229
|
-
*/
|
|
230
|
-
widgetTitle: (dashboardName, widgetId, fallback) => resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.title`), fallback),
|
|
231
|
-
/**
|
|
232
|
-
* Resolve translated widget description within a dashboard.
|
|
233
|
-
* Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.description`.
|
|
234
|
-
* Returns undefined when neither metadata nor translation provides one.
|
|
235
|
-
*/
|
|
236
|
-
widgetDescription: (dashboardName, widgetId, fallback) => {
|
|
237
|
-
const fb = fallback ?? '';
|
|
238
|
-
const resolved = resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.description`), fb);
|
|
239
|
-
return resolved || undefined;
|
|
240
|
-
},
|
|
241
|
-
/**
|
|
242
|
-
* Resolve translated page label, falling back to pageDef.label.
|
|
243
|
-
* Convention: `{ns}.pages.{pageName}.label`.
|
|
244
|
-
*/
|
|
245
|
-
pageLabel: (pageDef) => resolve(pageSuffixes(pageDef.name, 'label'), pageDef.label ?? pageDef.name),
|
|
246
|
-
/**
|
|
247
|
-
* Resolve translated report label, falling back to reportDef.label.
|
|
248
|
-
* Convention: `{ns}.reports.{reportName}.label`.
|
|
249
|
-
*/
|
|
250
|
-
reportLabel: (reportDef) => resolve(reportSuffixes(reportDef.name, 'label'), reportDef.label ?? reportDef.name),
|
|
251
|
-
/**
|
|
252
|
-
* Resolve translated list-view label.
|
|
253
|
-
* Convention (per @objectstack/spec): `{ns}.objects.{objectName}._views.{viewName}.label`.
|
|
254
|
-
*/
|
|
255
|
-
viewLabel: (objectName, viewName, fallback) => resolve(objectSuffixes(objectName, `_views.${viewName}.label`), fallback),
|
|
49
|
+
// Memoize the entire returned object — all closures below reference `t`/`i18n`
|
|
50
|
+
// and stay valid until the language changes. Returning a fresh object on every
|
|
51
|
+
// render was busting downstream `useMemo`/`useCallback` deps in heavy consumers
|
|
52
|
+
// like ListView.filterFields, causing avoidable recomputation.
|
|
53
|
+
return useMemo(() => {
|
|
256
54
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
55
|
+
* Discover app namespace(s) from loaded i18next resources.
|
|
56
|
+
* Returns top-level keys (outside built-in Object UI keys) that contain
|
|
57
|
+
* an `objects`, `fields`, or `apps` sub-key — e.g. "crm" when resources
|
|
58
|
+
* include crm.objects.* or crm.apps.*.
|
|
259
59
|
*/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
60
|
+
const getAppNamespaces = () => {
|
|
61
|
+
if (!i18n || typeof i18n.getResourceBundle !== 'function')
|
|
62
|
+
return [];
|
|
63
|
+
const lang = i18n.language || 'en';
|
|
64
|
+
const bundle = i18n.getResourceBundle(lang, 'translation');
|
|
65
|
+
if (!bundle)
|
|
66
|
+
return [];
|
|
67
|
+
return Object.keys(bundle).filter((key) => !BUILTIN_KEYS.has(key) && bundle[key] && typeof bundle[key] === 'object'
|
|
68
|
+
&& (bundle[key].objects || bundle[key].fields || bundle[key].apps
|
|
69
|
+
|| bundle[key].dashboards || bundle[key].pages || bundle[key].reports));
|
|
70
|
+
};
|
|
265
71
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
72
|
+
* Strip an ObjectStack namespace prefix (e.g. `crm__lead` → `lead`) so that
|
|
73
|
+
* translations authored against short object names still resolve when the
|
|
74
|
+
* runtime presents fully-qualified names. The first `__` separates the
|
|
75
|
+
* package namespace from the base name; everything after is preserved.
|
|
268
76
|
*/
|
|
269
|
-
|
|
77
|
+
const stripNamespace = (name) => {
|
|
78
|
+
const idx = name.indexOf('__');
|
|
79
|
+
return idx > 0 ? name.slice(idx + 2) : name;
|
|
80
|
+
};
|
|
81
|
+
/** Try resolving a key across all discovered app namespaces. */
|
|
82
|
+
const resolve = (suffixes, fallback) => {
|
|
83
|
+
const suffixList = Array.isArray(suffixes) ? suffixes : [suffixes];
|
|
84
|
+
try {
|
|
85
|
+
const namespaces = getAppNamespaces();
|
|
86
|
+
for (const ns of namespaces) {
|
|
87
|
+
for (const suffix of suffixList) {
|
|
88
|
+
const key = `${ns}.${suffix}`;
|
|
89
|
+
const translated = t(key, { defaultValue: '' });
|
|
90
|
+
if (translated && translated !== key && translated !== '') {
|
|
91
|
+
return translated;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Graceful degradation when i18n provider is not available
|
|
98
|
+
}
|
|
99
|
+
return fallback;
|
|
100
|
+
};
|
|
101
|
+
/** Build suffix candidates: prefer the given name, fall back to the base (unprefixed) name. */
|
|
102
|
+
const objectSuffixes = (objectName, tail) => {
|
|
103
|
+
const base = stripNamespace(objectName);
|
|
104
|
+
return base !== objectName
|
|
105
|
+
? [`objects.${objectName}.${tail}`, `objects.${base}.${tail}`]
|
|
106
|
+
: [`objects.${objectName}.${tail}`];
|
|
107
|
+
};
|
|
108
|
+
const fieldSuffixes = (objectName, fieldName) => {
|
|
109
|
+
const base = stripNamespace(objectName);
|
|
110
|
+
return base !== objectName
|
|
111
|
+
? [`fields.${objectName}.${fieldName}`, `fields.${base}.${fieldName}`]
|
|
112
|
+
: [`fields.${objectName}.${fieldName}`];
|
|
113
|
+
};
|
|
114
|
+
const optionSuffixes = (objectName, fieldName, optionValue) => {
|
|
115
|
+
const base = stripNamespace(objectName);
|
|
116
|
+
return base !== objectName
|
|
117
|
+
? [
|
|
118
|
+
`fieldOptions.${objectName}.${fieldName}.${optionValue}`,
|
|
119
|
+
`fieldOptions.${base}.${fieldName}.${optionValue}`,
|
|
120
|
+
]
|
|
121
|
+
: [`fieldOptions.${objectName}.${fieldName}.${optionValue}`];
|
|
122
|
+
};
|
|
270
123
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
124
|
+
* Build suffix candidates for a dashboard-scoped key. Mirrors the
|
|
125
|
+
* object/field convention: prefer the (possibly namespaced) dashboard name,
|
|
126
|
+
* fall back to the unprefixed base name.
|
|
274
127
|
*/
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
128
|
+
const dashboardSuffixes = (dashboardName, tail) => {
|
|
129
|
+
const base = stripNamespace(dashboardName);
|
|
130
|
+
return base !== dashboardName
|
|
131
|
+
? [`dashboards.${dashboardName}.${tail}`, `dashboards.${base}.${tail}`]
|
|
132
|
+
: [`dashboards.${dashboardName}.${tail}`];
|
|
133
|
+
};
|
|
281
134
|
/**
|
|
282
|
-
*
|
|
283
|
-
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`.
|
|
284
|
-
* Returns undefined when no translation and no fallback exist.
|
|
135
|
+
* Build suffix candidates for a page-scoped key.
|
|
285
136
|
*/
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
?
|
|
290
|
-
: `
|
|
291
|
-
|
|
292
|
-
return resolved || undefined;
|
|
293
|
-
},
|
|
137
|
+
const pageSuffixes = (pageName, tail) => {
|
|
138
|
+
const base = stripNamespace(pageName);
|
|
139
|
+
return base !== pageName
|
|
140
|
+
? [`pages.${pageName}.${tail}`, `pages.${base}.${tail}`]
|
|
141
|
+
: [`pages.${pageName}.${tail}`];
|
|
142
|
+
};
|
|
294
143
|
/**
|
|
295
|
-
*
|
|
296
|
-
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`.
|
|
144
|
+
* Build suffix candidates for a report-scoped key.
|
|
297
145
|
*/
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
?
|
|
302
|
-
: `
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
146
|
+
const reportSuffixes = (reportName, tail) => {
|
|
147
|
+
const base = stripNamespace(reportName);
|
|
148
|
+
return base !== reportName
|
|
149
|
+
? [`reports.${reportName}.${tail}`, `reports.${base}.${tail}`]
|
|
150
|
+
: [`reports.${reportName}.${tail}`];
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
/**
|
|
154
|
+
* Resolve translated object label, falling back to objectDef.label.
|
|
155
|
+
*/
|
|
156
|
+
objectLabel: (objectDef) => resolve(objectSuffixes(objectDef.name, 'label'), objectDef.label),
|
|
157
|
+
/**
|
|
158
|
+
* Resolve translated object description, falling back to objectDef.description.
|
|
159
|
+
*/
|
|
160
|
+
objectDescription: (objectDef) => {
|
|
161
|
+
if (!objectDef.description)
|
|
162
|
+
return undefined;
|
|
163
|
+
return resolve(objectSuffixes(objectDef.name, 'description'), objectDef.description);
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Resolve translated field label, falling back to the provided fallback string.
|
|
167
|
+
*/
|
|
168
|
+
fieldLabel: (objectName, fieldName, fallback) => resolve(fieldSuffixes(objectName, fieldName), fallback),
|
|
169
|
+
/**
|
|
170
|
+
* Resolve a translated select option label for a given object field.
|
|
171
|
+
* Falls back to the provided fallback (usually the English option label).
|
|
172
|
+
*/
|
|
173
|
+
fieldOptionLabel: (objectName, fieldName, optionValue, fallback) => resolve(optionSuffixes(objectName, fieldName, optionValue), fallback),
|
|
174
|
+
/**
|
|
175
|
+
* Translate all options for a given field, returning a new options array
|
|
176
|
+
* with translated labels. Pass the objectName and fieldName to look up
|
|
177
|
+
* translations; the original label is used as fallback.
|
|
178
|
+
*/
|
|
179
|
+
translateOptions: (objectName, fieldName, options) => options.map(opt => ({
|
|
180
|
+
...opt,
|
|
181
|
+
label: resolve(optionSuffixes(objectName, fieldName, opt.value), opt.label),
|
|
182
|
+
})),
|
|
183
|
+
/**
|
|
184
|
+
* Resolve translated app label, falling back to appDef.label.
|
|
185
|
+
* Looks up `{ns}.apps.{appName}.label` from loaded i18next resources.
|
|
186
|
+
*/
|
|
187
|
+
appLabel: (appDef) => resolve(`apps.${appDef.name}.label`, appDef.label ?? appDef.name),
|
|
188
|
+
/**
|
|
189
|
+
* Resolve translated app description, falling back to appDef.description.
|
|
190
|
+
* Returns the translated value even when metadata has no description —
|
|
191
|
+
* translation-only descriptions (defined only in i18n bundles) are common
|
|
192
|
+
* in examples where the app metadata is English-only.
|
|
193
|
+
*/
|
|
194
|
+
appDescription: (appDef) => {
|
|
195
|
+
const fallback = appDef.description ?? '';
|
|
196
|
+
const resolved = resolve(`apps.${appDef.name}.description`, fallback);
|
|
197
|
+
return resolved || undefined;
|
|
198
|
+
},
|
|
199
|
+
/**
|
|
200
|
+
* Resolve translated label for a navigation group within an app.
|
|
201
|
+
* Convention: `{ns}.apps.{appName}.navigation.{groupId}.label`.
|
|
202
|
+
*
|
|
203
|
+
* Mirrors `objectLabel`/`dashboardLabel` so app metadata can keep
|
|
204
|
+
* English fallbacks while translation packs supply localised
|
|
205
|
+
* sidebar group labels (e.g. "Sales" → "销售") without explicit
|
|
206
|
+
* I18nLabel `{ key, defaultValue }` annotations.
|
|
207
|
+
*/
|
|
208
|
+
navGroupLabel: (appName, groupId, fallback) => resolve(`apps.${appName}.navigation.${groupId}.label`, fallback),
|
|
209
|
+
/**
|
|
210
|
+
* Resolve translated dashboard label, falling back to dashboardDef.label.
|
|
211
|
+
* Convention: `{ns}.dashboards.{dashboardName}.label`.
|
|
212
|
+
*/
|
|
213
|
+
dashboardLabel: (dashboardDef) => resolve(dashboardSuffixes(dashboardDef.name, 'label'), dashboardDef.label ?? dashboardDef.name),
|
|
214
|
+
/**
|
|
215
|
+
* Resolve translated dashboard description, falling back to
|
|
216
|
+
* dashboardDef.description. Returns undefined when neither metadata nor
|
|
217
|
+
* translation provides one.
|
|
218
|
+
* Convention: `{ns}.dashboards.{dashboardName}.description`.
|
|
219
|
+
*/
|
|
220
|
+
dashboardDescription: (dashboardDef) => {
|
|
221
|
+
const fallback = dashboardDef.description ?? '';
|
|
222
|
+
const resolved = resolve(dashboardSuffixes(dashboardDef.name, 'description'), fallback);
|
|
223
|
+
return resolved || undefined;
|
|
224
|
+
},
|
|
225
|
+
/**
|
|
226
|
+
* Resolve translated dashboard header-action label.
|
|
227
|
+
* Convention: `{ns}.dashboards.{dashboardName}.actions.{actionKey}.label`.
|
|
228
|
+
* The actionKey is typically the action's `actionUrl` (e.g.
|
|
229
|
+
* `create_opportunity`) or its English label slugified.
|
|
230
|
+
*/
|
|
231
|
+
dashboardActionLabel: (dashboardName, actionKey, fallback) => resolve(dashboardSuffixes(dashboardName, `actions.${actionKey}.label`), fallback),
|
|
232
|
+
/**
|
|
233
|
+
* Resolve translated widget title within a dashboard.
|
|
234
|
+
* Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.title`.
|
|
235
|
+
*/
|
|
236
|
+
widgetTitle: (dashboardName, widgetId, fallback) => resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.title`), fallback),
|
|
237
|
+
/**
|
|
238
|
+
* Resolve translated widget description within a dashboard.
|
|
239
|
+
* Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.description`.
|
|
240
|
+
* Returns undefined when neither metadata nor translation provides one.
|
|
241
|
+
*/
|
|
242
|
+
widgetDescription: (dashboardName, widgetId, fallback) => {
|
|
243
|
+
const fb = fallback ?? '';
|
|
244
|
+
const resolved = resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.description`), fb);
|
|
245
|
+
return resolved || undefined;
|
|
246
|
+
},
|
|
247
|
+
/**
|
|
248
|
+
* Resolve translated page label, falling back to pageDef.label.
|
|
249
|
+
* Convention: `{ns}.pages.{pageName}.label`.
|
|
250
|
+
*/
|
|
251
|
+
pageLabel: (pageDef) => resolve(pageSuffixes(pageDef.name, 'label'), pageDef.label ?? pageDef.name),
|
|
252
|
+
/**
|
|
253
|
+
* Resolve translated report label, falling back to reportDef.label.
|
|
254
|
+
* Convention: `{ns}.reports.{reportName}.label`.
|
|
255
|
+
*/
|
|
256
|
+
reportLabel: (reportDef) => resolve(reportSuffixes(reportDef.name, 'label'), reportDef.label ?? reportDef.name),
|
|
257
|
+
/**
|
|
258
|
+
* Resolve translated list-view label.
|
|
259
|
+
* Convention (per @objectstack/spec): `{ns}.objects.{objectName}._views.{viewName}.label`.
|
|
260
|
+
*/
|
|
261
|
+
viewLabel: (objectName, viewName, fallback) => resolve(objectSuffixes(objectName, `_views.${viewName}.label`), fallback),
|
|
262
|
+
/**
|
|
263
|
+
* Resolve translated list-view description.
|
|
264
|
+
* Convention: `{ns}.objects.{objectName}._views.{viewName}.description`.
|
|
265
|
+
*/
|
|
266
|
+
viewDescription: (objectName, viewName, fallback) => {
|
|
267
|
+
const fb = fallback ?? '';
|
|
268
|
+
const resolved = resolve(objectSuffixes(objectName, `_views.${viewName}.description`), fb);
|
|
269
|
+
return resolved || undefined;
|
|
270
|
+
},
|
|
271
|
+
/**
|
|
272
|
+
* Resolve translated form-section label.
|
|
273
|
+
* Convention: `{ns}.objects.{objectName}._sections.{sectionName}.label`.
|
|
274
|
+
*/
|
|
275
|
+
sectionLabel: (objectName, sectionName, fallback) => resolve(objectSuffixes(objectName, `_sections.${sectionName}.label`), fallback),
|
|
276
|
+
/**
|
|
277
|
+
* Resolve translated action label.
|
|
278
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.label`.
|
|
279
|
+
* Falls back to `{ns}.globalActions.{actionName}.label` when objectName is omitted.
|
|
280
|
+
*/
|
|
281
|
+
actionLabel: (objectName, actionName, fallback) => {
|
|
282
|
+
if (objectName) {
|
|
283
|
+
return resolve(objectSuffixes(objectName, `_actions.${actionName}.label`), fallback);
|
|
284
|
+
}
|
|
285
|
+
return resolve(`globalActions.${actionName}.label`, fallback);
|
|
286
|
+
},
|
|
287
|
+
/**
|
|
288
|
+
* Resolve translated action confirmation prompt.
|
|
289
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`.
|
|
290
|
+
* Returns undefined when no translation and no fallback exist.
|
|
291
|
+
*/
|
|
292
|
+
actionConfirm: (objectName, actionName, fallback) => {
|
|
293
|
+
const fb = fallback ?? '';
|
|
294
|
+
const suffixes = objectName
|
|
295
|
+
? objectSuffixes(objectName, `_actions.${actionName}.confirmText`)
|
|
296
|
+
: `globalActions.${actionName}.confirmText`;
|
|
297
|
+
const resolved = resolve(suffixes, fb);
|
|
298
|
+
return resolved || undefined;
|
|
299
|
+
},
|
|
300
|
+
/**
|
|
301
|
+
* Resolve translated action success message.
|
|
302
|
+
* Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`.
|
|
303
|
+
*/
|
|
304
|
+
actionSuccess: (objectName, actionName, fallback) => {
|
|
305
|
+
const fb = fallback ?? '';
|
|
306
|
+
const suffixes = objectName
|
|
307
|
+
? objectSuffixes(objectName, `_actions.${actionName}.successMessage`)
|
|
308
|
+
: `globalActions.${actionName}.successMessage`;
|
|
309
|
+
const resolved = resolve(suffixes, fb);
|
|
310
|
+
return resolved || undefined;
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}, [t, i18n]);
|
|
307
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Stable identity fallbacks for `useSafeFieldLabel` — referenced from a
|
|
317
|
+
* module-level constant so consumers using the fallback branch don't get a
|
|
318
|
+
* fresh object reference on every render (which would invalidate downstream
|
|
319
|
+
* memoization in heavy components like ListView).
|
|
320
|
+
*/
|
|
321
|
+
const SAFE_FIELD_LABEL_FALLBACK = {
|
|
322
|
+
fieldLabel: (_objectName, _fieldName, fallback) => fallback,
|
|
323
|
+
translateOptions: (_objectName, _fieldName, options) => options,
|
|
324
|
+
fieldOptionLabel: (_objectName, _fieldName, _optionValue, fallback) => fallback,
|
|
325
|
+
sectionLabel: (_objectName, _sectionName, fallback) => fallback,
|
|
326
|
+
};
|
|
308
327
|
/**
|
|
309
328
|
* Safe wrapper for useObjectLabel that falls back to identity functions
|
|
310
329
|
* when no I18nProvider is available. Suitable for plugin components that
|
|
311
330
|
* may be rendered outside an i18n context.
|
|
312
331
|
*/
|
|
313
332
|
export function useSafeFieldLabel() {
|
|
333
|
+
let resolved = null;
|
|
314
334
|
try {
|
|
315
|
-
|
|
316
|
-
return { fieldLabel, translateOptions, fieldOptionLabel, sectionLabel };
|
|
335
|
+
resolved = useObjectLabel();
|
|
317
336
|
}
|
|
318
337
|
catch {
|
|
319
|
-
|
|
320
|
-
fieldLabel: (_objectName, _fieldName, fallback) => fallback,
|
|
321
|
-
translateOptions: (_objectName, _fieldName, options) => options,
|
|
322
|
-
fieldOptionLabel: (_objectName, _fieldName, _optionValue, fallback) => fallback,
|
|
323
|
-
sectionLabel: (_objectName, _sectionName, fallback) => fallback,
|
|
324
|
-
};
|
|
338
|
+
resolved = null;
|
|
325
339
|
}
|
|
340
|
+
// useObjectLabel already returns a stable (memoized) object per language —
|
|
341
|
+
// pass-through avoids creating a fresh wrapper here. The module-level
|
|
342
|
+
// fallback object is reused when no provider is mounted.
|
|
343
|
+
return resolved ?? SAFE_FIELD_LABEL_FALLBACK;
|
|
326
344
|
}
|