@object-ui/i18n 16.0.0 → 17.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.
@@ -181,19 +181,24 @@ export declare function useObjectLabel(): {
181
181
  sectionLabel: (objectName: string, sectionName: string, fallback: string) => string;
182
182
  /**
183
183
  * Resolve translated action label.
184
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.label`.
185
- * Falls back to `{ns}.globalActions.{actionName}.label` when objectName is omitted.
184
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.label`,
185
+ * falling back to `{ns}.globalActions.{actionName}.label` — both when
186
+ * objectName is omitted AND when the object-scoped key misses (so a
187
+ * globalAction surfaced on a record-detail action bar still resolves;
188
+ * objectui#3372).
186
189
  */
187
190
  actionLabel: (objectName: string | undefined, actionName: string, fallback: string) => string;
188
191
  /**
189
192
  * Resolve translated action confirmation prompt.
190
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`.
193
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`,
194
+ * falling back to `{ns}.globalActions.{actionName}.confirmText`.
191
195
  * Returns undefined when no translation and no fallback exist.
192
196
  */
193
197
  actionConfirm: (objectName: string | undefined, actionName: string, fallback?: string) => string | undefined;
194
198
  /**
195
199
  * Resolve translated action success message.
196
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`.
200
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`,
201
+ * falling back to `{ns}.globalActions.{actionName}.successMessage`.
197
202
  */
198
203
  actionSuccess: (objectName: string | undefined, actionName: string, fallback?: string) => string | undefined;
199
204
  /**
@@ -204,6 +209,29 @@ export declare function useObjectLabel(): {
204
209
  * metadata's literal string; undefined when nothing resolves.
205
210
  */
206
211
  actionDescription: (objectName: string | undefined, actionName: string | undefined, fallback?: string) => string | undefined;
212
+ /**
213
+ * Resolve a translated copy of an action's post-success RESULT DIALOG
214
+ * (`title` / `description` / `acknowledge` + per-field labels).
215
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.resultDialog.*`,
216
+ * falling back to `{ns}.globalActions.{actionName}.resultDialog.*` (when
217
+ * objectName is omitted OR the object-scoped key misses), then to the
218
+ * metadata's literal strings.
219
+ *
220
+ * The `fields` translation node is keyed by the LITERAL result-field path
221
+ * from the action metadata (may contain dots, e.g. `"user.email"`), so it
222
+ * is fetched whole with `returnObjects` and indexed directly — never
223
+ * resolved through a dotted i18next key.
224
+ */
225
+ actionResultDialog: <T extends {
226
+ title?: string;
227
+ description?: string;
228
+ acknowledge?: string;
229
+ fields?: Array<{
230
+ path: string;
231
+ label?: string;
232
+ [key: string]: any;
233
+ }>;
234
+ }>(objectName: string | undefined, actionName: string | undefined, spec: T | undefined) => T | undefined;
207
235
  /**
208
236
  * Resolve translated action-PARAMETER text (label / placeholder / helpText).
209
237
  * Convention: `{ns}.objects.{objectName}._actions.{actionName}.params.{paramName}.{attr}`.
@@ -224,17 +252,217 @@ export declare function useObjectLabel(): {
224
252
  * may be rendered outside an i18n context.
225
253
  */
226
254
  export declare function useSafeFieldLabel(): {
227
- fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => string;
228
- translateOptions: (_objectName: string, _fieldName: string, options: Array<{
255
+ /**
256
+ * Resolve translated object label, falling back to objectDef.label.
257
+ */
258
+ objectLabel: (objectDef: {
259
+ name: string;
260
+ label: string;
261
+ }) => string;
262
+ /**
263
+ * Resolve translated object description, falling back to objectDef.description.
264
+ */
265
+ objectDescription: (objectDef: {
266
+ name: string;
267
+ description?: string;
268
+ }) => string | undefined;
269
+ /**
270
+ * Resolve translated field label, falling back to the provided fallback string.
271
+ */
272
+ fieldLabel: (objectName: string, fieldName: string, fallback: string) => string;
273
+ /**
274
+ * Resolve a translated select option label for a given object field.
275
+ * Falls back to the provided fallback (usually the English option label).
276
+ */
277
+ fieldOptionLabel: (objectName: string, fieldName: string, optionValue: string, fallback: string) => string;
278
+ /**
279
+ * Translate all options for a given field, returning a new options array
280
+ * with translated labels. Pass the objectName and fieldName to look up
281
+ * translations; the original label is used as fallback.
282
+ */
283
+ translateOptions: (objectName: string, fieldName: string, options: Array<{
229
284
  value: string;
230
285
  label: string;
231
286
  [key: string]: any;
232
- }>) => {
233
- [key: string]: any;
287
+ }>) => Array<{
234
288
  value: string;
235
289
  label: string;
236
- }[];
237
- fieldOptionLabel: (_objectName: string, _fieldName: string, _optionValue: string, fallback: string) => string;
238
- sectionLabel: (_objectName: string, _sectionName: string, fallback: string) => string;
239
- actionLabel: (_objectName: string | undefined, _actionName: string, fallback: string) => string;
290
+ [key: string]: any;
291
+ }>;
292
+ /**
293
+ * Resolve translated app label, falling back to appDef.label.
294
+ * Looks up `{ns}.apps.{appName}.label` from loaded i18next resources.
295
+ */
296
+ appLabel: (appDef: {
297
+ name: string;
298
+ label?: string;
299
+ }) => string;
300
+ /**
301
+ * Resolve translated app description, falling back to appDef.description.
302
+ * Returns the translated value even when metadata has no description —
303
+ * translation-only descriptions (defined only in i18n bundles) are common
304
+ * in examples where the app metadata is English-only.
305
+ */
306
+ appDescription: (appDef: {
307
+ name: string;
308
+ description?: string;
309
+ }) => string | undefined;
310
+ /**
311
+ * Resolve translated label for a navigation group within an app.
312
+ * Convention: `{ns}.apps.{appName}.navigation.{groupId}.label`.
313
+ *
314
+ * Mirrors `objectLabel`/`dashboardLabel` so app metadata can keep
315
+ * English fallbacks while translation packs supply localised
316
+ * sidebar group labels (e.g. "Sales" → "销售") without explicit
317
+ * I18nLabel `{ key, defaultValue }` annotations.
318
+ */
319
+ navGroupLabel: (appName: string, groupId: string, fallback: string) => string;
320
+ /**
321
+ * Resolve translated dashboard label, falling back to dashboardDef.label.
322
+ * Convention: `{ns}.dashboards.{dashboardName}.label`.
323
+ */
324
+ dashboardLabel: (dashboardDef: {
325
+ name: string;
326
+ label?: string;
327
+ }) => string;
328
+ /**
329
+ * Resolve translated dashboard description, falling back to
330
+ * dashboardDef.description. Returns undefined when neither metadata nor
331
+ * translation provides one.
332
+ * Convention: `{ns}.dashboards.{dashboardName}.description`.
333
+ */
334
+ dashboardDescription: (dashboardDef: {
335
+ name: string;
336
+ description?: string;
337
+ }) => string | undefined;
338
+ /**
339
+ * Resolve translated dashboard header-action label.
340
+ * Convention: `{ns}.dashboards.{dashboardName}.actions.{actionKey}.label`.
341
+ * The actionKey is typically the action's `actionUrl` (e.g.
342
+ * `create_opportunity`) or its English label slugified.
343
+ */
344
+ dashboardActionLabel: (dashboardName: string, actionKey: string, fallback: string) => string;
345
+ /**
346
+ * Resolve translated widget title within a dashboard.
347
+ * Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.title`.
348
+ */
349
+ widgetTitle: (dashboardName: string, widgetId: string, fallback: string) => string;
350
+ /**
351
+ * Resolve translated widget description within a dashboard.
352
+ * Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.description`.
353
+ * Returns undefined when neither metadata nor translation provides one.
354
+ */
355
+ widgetDescription: (dashboardName: string, widgetId: string, fallback?: string) => string | undefined;
356
+ /**
357
+ * Resolve translated page label, falling back to pageDef.label.
358
+ * Convention: `{ns}.pages.{pageName}.label`.
359
+ */
360
+ pageLabel: (pageDef: {
361
+ name: string;
362
+ label?: string;
363
+ }) => string;
364
+ /**
365
+ * Resolve translated report label, falling back to reportDef.label.
366
+ * Convention: `{ns}.reports.{reportName}.label`.
367
+ */
368
+ reportLabel: (reportDef: {
369
+ name: string;
370
+ label?: string;
371
+ }) => string;
372
+ /**
373
+ * Resolve translated list-view label.
374
+ * Convention (per @objectstack/spec): `{ns}.objects.{objectName}._views.{viewName}.label`.
375
+ */
376
+ viewLabel: (objectName: string, viewName: string, fallback: string) => string;
377
+ /**
378
+ * Resolve translated list-view description.
379
+ * Convention: `{ns}.objects.{objectName}._views.{viewName}.description`.
380
+ */
381
+ viewDescription: (objectName: string, viewName: string, fallback?: string) => string | undefined;
382
+ /**
383
+ * Resolve translated list-view emptyState. Returns a {title, message}
384
+ * tuple with each field independently translated. Convention:
385
+ * `{ns}.objects.{objectName}._views.{viewName}.emptyState.title`
386
+ * `{ns}.objects.{objectName}._views.{viewName}.emptyState.message`
387
+ */
388
+ viewEmptyState: (objectName: string, viewName: string, fallback: {
389
+ title?: string;
390
+ message?: string;
391
+ icon?: string;
392
+ } | undefined) => {
393
+ title: string | undefined;
394
+ message: string | undefined;
395
+ icon?: string;
396
+ } | undefined;
397
+ /**
398
+ * Resolve translated form-section label.
399
+ * Convention: `{ns}.objects.{objectName}._sections.{sectionName}.label`.
400
+ */
401
+ sectionLabel: (objectName: string, sectionName: string, fallback: string) => string;
402
+ /**
403
+ * Resolve translated action label.
404
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.label`,
405
+ * falling back to `{ns}.globalActions.{actionName}.label` — both when
406
+ * objectName is omitted AND when the object-scoped key misses (so a
407
+ * globalAction surfaced on a record-detail action bar still resolves;
408
+ * objectui#3372).
409
+ */
410
+ actionLabel: (objectName: string | undefined, actionName: string, fallback: string) => string;
411
+ /**
412
+ * Resolve translated action confirmation prompt.
413
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`,
414
+ * falling back to `{ns}.globalActions.{actionName}.confirmText`.
415
+ * Returns undefined when no translation and no fallback exist.
416
+ */
417
+ actionConfirm: (objectName: string | undefined, actionName: string, fallback?: string) => string | undefined;
418
+ /**
419
+ * Resolve translated action success message.
420
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`,
421
+ * falling back to `{ns}.globalActions.{actionName}.successMessage`.
422
+ */
423
+ actionSuccess: (objectName: string | undefined, actionName: string, fallback?: string) => string | undefined;
424
+ /**
425
+ * Resolve translated action description (the explanatory line shown in the
426
+ * action's param dialog / sheet / drawer header).
427
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.description`.
428
+ * Falls back to `{ns}.globalActions.{actionName}.description`, then the
429
+ * metadata's literal string; undefined when nothing resolves.
430
+ */
431
+ actionDescription: (objectName: string | undefined, actionName: string | undefined, fallback?: string) => string | undefined;
432
+ /**
433
+ * Resolve a translated copy of an action's post-success RESULT DIALOG
434
+ * (`title` / `description` / `acknowledge` + per-field labels).
435
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.resultDialog.*`,
436
+ * falling back to `{ns}.globalActions.{actionName}.resultDialog.*` (when
437
+ * objectName is omitted OR the object-scoped key misses), then to the
438
+ * metadata's literal strings.
439
+ *
440
+ * The `fields` translation node is keyed by the LITERAL result-field path
441
+ * from the action metadata (may contain dots, e.g. `"user.email"`), so it
442
+ * is fetched whole with `returnObjects` and indexed directly — never
443
+ * resolved through a dotted i18next key.
444
+ */
445
+ actionResultDialog: <T extends {
446
+ title?: string;
447
+ description?: string;
448
+ acknowledge?: string;
449
+ fields?: Array<{
450
+ path: string;
451
+ label?: string;
452
+ [key: string]: any;
453
+ }>;
454
+ }>(objectName: string | undefined, actionName: string | undefined, spec: T | undefined) => T | undefined;
455
+ /**
456
+ * Resolve translated action-PARAMETER text (label / placeholder / helpText).
457
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.params.{paramName}.{attr}`.
458
+ * Falls back to the provided value (the metadata's literal string) when no
459
+ * translation exists, so untranslated params keep rendering as authored.
460
+ */
461
+ actionParamText: (objectName: string | undefined, actionName: string | undefined, paramName: string, attr: "label" | "placeholder" | "helpText", fallback?: string) => string | undefined;
462
+ /**
463
+ * Resolve a translated action-parameter SELECT OPTION label.
464
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.params.{paramName}.options.{optionValue}`.
465
+ * Falls back to the provided (English metadata) label when untranslated.
466
+ */
467
+ actionParamOptionLabel: (objectName: string | undefined, actionName: string | undefined, paramName: string, optionValue: string, fallback: string) => string;
240
468
  };
@@ -57,6 +57,11 @@ export function useObjectLabel() {
57
57
  * Returns top-level keys (outside built-in Object UI keys) that contain
58
58
  * an `objects`, `fields`, or `apps` sub-key — e.g. "crm" when resources
59
59
  * include crm.objects.* or crm.apps.*.
60
+ *
61
+ * `globalActions` is included so an app bundle whose only translated scope
62
+ * is global actions (no object/field entries) is still discovered — its
63
+ * `globalActions.<action>.*` overlays would otherwise be unreachable
64
+ * (objectui#3372).
60
65
  */
61
66
  const getAppNamespaces = () => {
62
67
  if (!i18n || typeof i18n.getResourceBundle !== 'function')
@@ -67,7 +72,8 @@ export function useObjectLabel() {
67
72
  return [];
68
73
  return Object.keys(bundle).filter((key) => !BUILTIN_KEYS.has(key) && bundle[key] && typeof bundle[key] === 'object'
69
74
  && (bundle[key].objects || bundle[key].fields || bundle[key].apps
70
- || bundle[key].dashboards || bundle[key].pages || bundle[key].reports));
75
+ || bundle[key].dashboards || bundle[key].pages || bundle[key].reports
76
+ || bundle[key].globalActions));
71
77
  };
72
78
  /**
73
79
  * Strip an ObjectStack namespace prefix (e.g. `crm__lead` → `lead`) so that
@@ -111,6 +117,30 @@ export function useObjectLabel() {
111
117
  ? [`objects.${objectName}.${tail}`, `objects.${base}.${tail}`]
112
118
  : [`objects.${objectName}.${tail}`];
113
119
  };
120
+ /**
121
+ * Build suffix candidates for an action-scoped key, mirroring the canonical
122
+ * `@objectstack/spec` resolver (`lookupActionField` in `system/i18n-resolver`).
123
+ *
124
+ * When the action is object-scoped, the object key
125
+ * (`objects.<obj>._actions.<action>.<tail>`) wins, but the global key
126
+ * (`globalActions.<action>.<tail>`) is appended as a fallback so a
127
+ * **globalAction surfaced on an object's action bar** still picks up its
128
+ * `globalActions.<action>.*` overlay when no object-scoped translation
129
+ * exists (objectui#3372). Without this fallback, a globalAction rendered on
130
+ * a record-detail action bar — where the caller passes `objectDef.name` for
131
+ * every action — misses `objects.<obj>._actions.<action>.label` and leaks the
132
+ * English metadata literal.
133
+ *
134
+ * Object-less actions (`objectName` omitted) resolve straight to the global
135
+ * namespace, as before. Object precedence is preserved: the global key is
136
+ * only consulted after every object-scoped candidate misses.
137
+ */
138
+ const actionSuffixes = (objectName, actionName, tail) => {
139
+ const globalSuffix = `globalActions.${actionName}.${tail}`;
140
+ return objectName
141
+ ? [...objectSuffixes(objectName, `_actions.${actionName}.${tail}`), globalSuffix]
142
+ : [globalSuffix];
143
+ };
114
144
  const fieldSuffixes = (objectName, fieldName) => {
115
145
  const base = stripNamespace(objectName);
116
146
  return base !== objectName
@@ -298,38 +328,32 @@ export function useObjectLabel() {
298
328
  sectionLabel: (objectName, sectionName, fallback) => resolve(objectSuffixes(objectName, `_sections.${sectionName}.label`), fallback),
299
329
  /**
300
330
  * Resolve translated action label.
301
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.label`.
302
- * Falls back to `{ns}.globalActions.{actionName}.label` when objectName is omitted.
331
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.label`,
332
+ * falling back to `{ns}.globalActions.{actionName}.label` — both when
333
+ * objectName is omitted AND when the object-scoped key misses (so a
334
+ * globalAction surfaced on a record-detail action bar still resolves;
335
+ * objectui#3372).
303
336
  */
304
- actionLabel: (objectName, actionName, fallback) => {
305
- if (objectName) {
306
- return resolve(objectSuffixes(objectName, `_actions.${actionName}.label`), fallback);
307
- }
308
- return resolve(`globalActions.${actionName}.label`, fallback);
309
- },
337
+ actionLabel: (objectName, actionName, fallback) => resolve(actionSuffixes(objectName, actionName, 'label'), fallback),
310
338
  /**
311
339
  * Resolve translated action confirmation prompt.
312
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`.
340
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`,
341
+ * falling back to `{ns}.globalActions.{actionName}.confirmText`.
313
342
  * Returns undefined when no translation and no fallback exist.
314
343
  */
315
344
  actionConfirm: (objectName, actionName, fallback) => {
316
345
  const fb = fallback ?? '';
317
- const suffixes = objectName
318
- ? objectSuffixes(objectName, `_actions.${actionName}.confirmText`)
319
- : `globalActions.${actionName}.confirmText`;
320
- const resolved = resolve(suffixes, fb);
346
+ const resolved = resolve(actionSuffixes(objectName, actionName, 'confirmText'), fb);
321
347
  return resolved || undefined;
322
348
  },
323
349
  /**
324
350
  * Resolve translated action success message.
325
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`.
351
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`,
352
+ * falling back to `{ns}.globalActions.{actionName}.successMessage`.
326
353
  */
327
354
  actionSuccess: (objectName, actionName, fallback) => {
328
355
  const fb = fallback ?? '';
329
- const suffixes = objectName
330
- ? objectSuffixes(objectName, `_actions.${actionName}.successMessage`)
331
- : `globalActions.${actionName}.successMessage`;
332
- const resolved = resolve(suffixes, fb);
356
+ const resolved = resolve(actionSuffixes(objectName, actionName, 'successMessage'), fb);
333
357
  return resolved || undefined;
334
358
  },
335
359
  /**
@@ -343,12 +367,66 @@ export function useObjectLabel() {
343
367
  const fb = fallback ?? '';
344
368
  if (!actionName)
345
369
  return fb || undefined;
346
- const suffixes = objectName
347
- ? objectSuffixes(objectName, `_actions.${actionName}.description`)
348
- : `globalActions.${actionName}.description`;
349
- const resolved = resolve(suffixes, fb);
370
+ const resolved = resolve(actionSuffixes(objectName, actionName, 'description'), fb);
350
371
  return resolved || undefined;
351
372
  },
373
+ /**
374
+ * Resolve a translated copy of an action's post-success RESULT DIALOG
375
+ * (`title` / `description` / `acknowledge` + per-field labels).
376
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.resultDialog.*`,
377
+ * falling back to `{ns}.globalActions.{actionName}.resultDialog.*` (when
378
+ * objectName is omitted OR the object-scoped key misses), then to the
379
+ * metadata's literal strings.
380
+ *
381
+ * The `fields` translation node is keyed by the LITERAL result-field path
382
+ * from the action metadata (may contain dots, e.g. `"user.email"`), so it
383
+ * is fetched whole with `returnObjects` and indexed directly — never
384
+ * resolved through a dotted i18next key.
385
+ */
386
+ actionResultDialog: (objectName, actionName, spec) => {
387
+ if (!spec || !actionName)
388
+ return spec;
389
+ const suffixesFor = (attr) => actionSuffixes(objectName, actionName, `resultDialog.${attr}`);
390
+ const textFor = (attr) => {
391
+ const resolved = resolve(suffixesFor(attr), spec[attr] ?? '');
392
+ return resolved || spec[attr];
393
+ };
394
+ const fieldsMap = (() => {
395
+ try {
396
+ for (const ns of getAppNamespaces()) {
397
+ for (const suffix of suffixesFor('fields')) {
398
+ const node = t(`${ns}.${suffix}`, {
399
+ returnObjects: true,
400
+ defaultValue: null,
401
+ [I18N_PROBE_FLAG]: true,
402
+ });
403
+ if (node && typeof node === 'object' && !Array.isArray(node)) {
404
+ return node;
405
+ }
406
+ }
407
+ }
408
+ }
409
+ catch {
410
+ // Graceful degradation when i18n provider is not available
411
+ }
412
+ return undefined;
413
+ })();
414
+ const fields = Array.isArray(spec.fields)
415
+ ? spec.fields.map((field) => {
416
+ if (!field || typeof field.path !== 'string')
417
+ return field;
418
+ const label = fieldsMap?.[field.path];
419
+ return typeof label === 'string' && label.length > 0 ? { ...field, label } : field;
420
+ })
421
+ : spec.fields;
422
+ return {
423
+ ...spec,
424
+ title: textFor('title'),
425
+ description: textFor('description'),
426
+ acknowledge: textFor('acknowledge'),
427
+ fields,
428
+ };
429
+ },
352
430
  /**
353
431
  * Resolve translated action-PARAMETER text (label / placeholder / helpText).
354
432
  * Convention: `{ns}.objects.{objectName}._actions.{actionName}.params.{paramName}.{attr}`.
@@ -359,11 +437,7 @@ export function useObjectLabel() {
359
437
  const fb = fallback ?? '';
360
438
  if (!actionName || !paramName)
361
439
  return fb || undefined;
362
- const suffix = `_actions.${actionName}.params.${paramName}.${attr}`;
363
- const suffixes = objectName
364
- ? objectSuffixes(objectName, suffix)
365
- : `globalActions.${actionName}.params.${paramName}.${attr}`;
366
- const resolved = resolve(suffixes, fb);
440
+ const resolved = resolve(actionSuffixes(objectName, actionName, `params.${paramName}.${attr}`), fb);
367
441
  return resolved || undefined;
368
442
  },
369
443
  /**
@@ -374,11 +448,7 @@ export function useObjectLabel() {
374
448
  actionParamOptionLabel: (objectName, actionName, paramName, optionValue, fallback) => {
375
449
  if (!actionName || !paramName)
376
450
  return fallback;
377
- const suffix = `_actions.${actionName}.params.${paramName}.options.${optionValue}`;
378
- const suffixes = objectName
379
- ? objectSuffixes(objectName, suffix)
380
- : `globalActions.${actionName}.params.${paramName}.options.${optionValue}`;
381
- return resolve(suffixes, fallback);
451
+ return resolve(actionSuffixes(objectName, actionName, `params.${paramName}.options.${optionValue}`), fallback);
382
452
  },
383
453
  };
384
454
  }, [t, i18n]);
@@ -402,15 +472,10 @@ const SAFE_FIELD_LABEL_FALLBACK = {
402
472
  * may be rendered outside an i18n context.
403
473
  */
404
474
  export function useSafeFieldLabel() {
405
- let resolved = null;
406
- try {
407
- resolved = useObjectLabel();
408
- }
409
- catch {
410
- resolved = null;
411
- }
412
- // useObjectLabel already returns a stable (memoized) object per language —
413
- // pass-through avoids creating a fresh wrapper here. The module-level
414
- // fallback object is reused when no provider is mounted.
415
- return resolved ?? SAFE_FIELD_LABEL_FALLBACK;
475
+ // useObjectLabel delegates to the provider-safe useObjectTranslation (react-
476
+ // i18next falls back to the global instance and never throws), so it needs no
477
+ // try/catch — wrapping the hook call would violate rules-of-hooks. It already
478
+ // returns a stable memoized object; the module-level fallback stays as a
479
+ // defensive default, reached only if it ever returns nullish.
480
+ return useObjectLabel() ?? SAFE_FIELD_LABEL_FALLBACK;
416
481
  }
@@ -1,7 +1,9 @@
1
1
  export declare function createSafeTranslation(defaults: Record<string, string>, testKey: string): () => {
2
2
  t: (key: string, options?: Record<string, unknown>) => string;
3
+ language: string;
3
4
  } | {
4
5
  t: import("i18next").TFunction<string, undefined>;
6
+ language: string;
5
7
  };
6
8
  /**
7
9
  * Per-call graceful translate hook for plugin renderers.
@@ -32,10 +32,15 @@ export function createSafeTranslation(defaults, testKey) {
32
32
  // "translations not configured" fallback.
33
33
  const result = useObjectTranslation();
34
34
  const testValue = result.t(testKey);
35
+ // `language` is surfaced so consumers that localize dates/numbers
36
+ // alongside their copy (data-table, gantt) don't have to call
37
+ // `useObjectTranslation` a second time just to read it. With no provider
38
+ // it is whatever react-i18next reports, which callers may treat as
39
+ // "follow the runtime default".
35
40
  if (testValue === testKey) {
36
- return { t: fallbackT };
41
+ return { t: fallbackT, language: result.language };
37
42
  }
38
- return { t: result.t };
43
+ return { t: result.t, language: result.language };
39
44
  };
40
45
  }
41
46
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/i18n",
3
- "version": "16.0.0",
3
+ "version": "17.1.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",
@@ -31,14 +31,14 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "i18next": "^26.3.6",
34
- "react-i18next": "^17.0.9"
34
+ "react-i18next": "^17.0.11"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": "^18.0.0 || ^19.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/react": "19.2.17",
41
- "react": "19.2.7",
41
+ "react": "19.2.8",
42
42
  "typescript": "^6.0.3",
43
43
  "vitest": "^4.1.10"
44
44
  },