@object-ui/i18n 16.1.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.
@@ -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,18 +367,16 @@ 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
  },
352
373
  /**
353
374
  * Resolve a translated copy of an action's post-success RESULT DIALOG
354
375
  * (`title` / `description` / `acknowledge` + per-field labels).
355
376
  * Convention: `{ns}.objects.{objectName}._actions.{actionName}.resultDialog.*`,
356
- * falling back to `{ns}.globalActions.{actionName}.resultDialog.*` when
357
- * objectName is omitted, then to the metadata's literal strings.
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.
358
380
  *
359
381
  * The `fields` translation node is keyed by the LITERAL result-field path
360
382
  * from the action metadata (may contain dots, e.g. `"user.email"`), so it
@@ -364,12 +386,7 @@ export function useObjectLabel() {
364
386
  actionResultDialog: (objectName, actionName, spec) => {
365
387
  if (!spec || !actionName)
366
388
  return spec;
367
- const suffixesFor = (attr) => {
368
- const tail = `_actions.${actionName}.resultDialog.${attr}`;
369
- return objectName
370
- ? objectSuffixes(objectName, tail)
371
- : [`globalActions.${actionName}.resultDialog.${attr}`];
372
- };
389
+ const suffixesFor = (attr) => actionSuffixes(objectName, actionName, `resultDialog.${attr}`);
373
390
  const textFor = (attr) => {
374
391
  const resolved = resolve(suffixesFor(attr), spec[attr] ?? '');
375
392
  return resolved || spec[attr];
@@ -420,11 +437,7 @@ export function useObjectLabel() {
420
437
  const fb = fallback ?? '';
421
438
  if (!actionName || !paramName)
422
439
  return fb || undefined;
423
- const suffix = `_actions.${actionName}.params.${paramName}.${attr}`;
424
- const suffixes = objectName
425
- ? objectSuffixes(objectName, suffix)
426
- : `globalActions.${actionName}.params.${paramName}.${attr}`;
427
- const resolved = resolve(suffixes, fb);
440
+ const resolved = resolve(actionSuffixes(objectName, actionName, `params.${paramName}.${attr}`), fb);
428
441
  return resolved || undefined;
429
442
  },
430
443
  /**
@@ -435,11 +448,7 @@ export function useObjectLabel() {
435
448
  actionParamOptionLabel: (objectName, actionName, paramName, optionValue, fallback) => {
436
449
  if (!actionName || !paramName)
437
450
  return fallback;
438
- const suffix = `_actions.${actionName}.params.${paramName}.options.${optionValue}`;
439
- const suffixes = objectName
440
- ? objectSuffixes(objectName, suffix)
441
- : `globalActions.${actionName}.params.${paramName}.options.${optionValue}`;
442
- return resolve(suffixes, fallback);
451
+ return resolve(actionSuffixes(objectName, actionName, `params.${paramName}.options.${optionValue}`), fallback);
443
452
  },
444
453
  };
445
454
  }, [t, i18n]);
@@ -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.1.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.10"
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
  },