@object-ui/i18n 16.1.0 → 17.2.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
@@ -155,6 +185,23 @@ export function useObjectLabel() {
155
185
  ? [`reports.${reportName}.${tail}`, `reports.${base}.${tail}`]
156
186
  : [`reports.${reportName}.${tail}`];
157
187
  };
188
+ /**
189
+ * Build suffix candidates for a list-view scoped key.
190
+ *
191
+ * The runtime uses qualified view ids (`<objectName>.<viewName>`) in URLs
192
+ * and metadata records, while translation bundles are keyed by the authored
193
+ * view name under `_views`. Resolve the unqualified name first so every
194
+ * surface can pass its canonical id without leaking the metadata fallback.
195
+ */
196
+ const viewSuffixes = (objectName, viewName, tail) => {
197
+ const objectNames = [objectName, stripNamespace(objectName)];
198
+ const matchedPrefix = objectNames
199
+ .map((name) => `${name}.`)
200
+ .find((prefix) => viewName.startsWith(prefix));
201
+ const shortViewName = matchedPrefix ? viewName.slice(matchedPrefix.length) : viewName;
202
+ const viewNames = shortViewName === viewName ? [viewName] : [shortViewName, viewName];
203
+ return viewNames.flatMap((name) => objectSuffixes(objectName, `_views.${name}.${tail}`));
204
+ };
158
205
  return {
159
206
  /**
160
207
  * Resolve translated object label, falling back to objectDef.label.
@@ -264,14 +311,14 @@ export function useObjectLabel() {
264
311
  * Resolve translated list-view label.
265
312
  * Convention (per @objectstack/spec): `{ns}.objects.{objectName}._views.{viewName}.label`.
266
313
  */
267
- viewLabel: (objectName, viewName, fallback) => resolve(objectSuffixes(objectName, `_views.${viewName}.label`), fallback),
314
+ viewLabel: (objectName, viewName, fallback) => resolve(viewSuffixes(objectName, viewName, 'label'), fallback),
268
315
  /**
269
316
  * Resolve translated list-view description.
270
317
  * Convention: `{ns}.objects.{objectName}._views.{viewName}.description`.
271
318
  */
272
319
  viewDescription: (objectName, viewName, fallback) => {
273
320
  const fb = fallback ?? '';
274
- const resolved = resolve(objectSuffixes(objectName, `_views.${viewName}.description`), fb);
321
+ const resolved = resolve(viewSuffixes(objectName, viewName, 'description'), fb);
275
322
  return resolved || undefined;
276
323
  },
277
324
  /**
@@ -284,10 +331,10 @@ export function useObjectLabel() {
284
331
  if (!fallback)
285
332
  return undefined;
286
333
  const title = fallback.title
287
- ? resolve(objectSuffixes(objectName, `_views.${viewName}.emptyState.title`), fallback.title)
334
+ ? resolve(viewSuffixes(objectName, viewName, 'emptyState.title'), fallback.title)
288
335
  : fallback.title;
289
336
  const message = fallback.message
290
- ? resolve(objectSuffixes(objectName, `_views.${viewName}.emptyState.message`), fallback.message)
337
+ ? resolve(viewSuffixes(objectName, viewName, 'emptyState.message'), fallback.message)
291
338
  : fallback.message;
292
339
  return { ...fallback, title, message };
293
340
  },
@@ -298,38 +345,32 @@ export function useObjectLabel() {
298
345
  sectionLabel: (objectName, sectionName, fallback) => resolve(objectSuffixes(objectName, `_sections.${sectionName}.label`), fallback),
299
346
  /**
300
347
  * Resolve translated action label.
301
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.label`.
302
- * Falls back to `{ns}.globalActions.{actionName}.label` when objectName is omitted.
348
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.label`,
349
+ * falling back to `{ns}.globalActions.{actionName}.label` — both when
350
+ * objectName is omitted AND when the object-scoped key misses (so a
351
+ * globalAction surfaced on a record-detail action bar still resolves;
352
+ * objectui#3372).
303
353
  */
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
- },
354
+ actionLabel: (objectName, actionName, fallback) => resolve(actionSuffixes(objectName, actionName, 'label'), fallback),
310
355
  /**
311
356
  * Resolve translated action confirmation prompt.
312
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`.
357
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.confirmText`,
358
+ * falling back to `{ns}.globalActions.{actionName}.confirmText`.
313
359
  * Returns undefined when no translation and no fallback exist.
314
360
  */
315
361
  actionConfirm: (objectName, actionName, fallback) => {
316
362
  const fb = fallback ?? '';
317
- const suffixes = objectName
318
- ? objectSuffixes(objectName, `_actions.${actionName}.confirmText`)
319
- : `globalActions.${actionName}.confirmText`;
320
- const resolved = resolve(suffixes, fb);
363
+ const resolved = resolve(actionSuffixes(objectName, actionName, 'confirmText'), fb);
321
364
  return resolved || undefined;
322
365
  },
323
366
  /**
324
367
  * Resolve translated action success message.
325
- * Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`.
368
+ * Convention: `{ns}.objects.{objectName}._actions.{actionName}.successMessage`,
369
+ * falling back to `{ns}.globalActions.{actionName}.successMessage`.
326
370
  */
327
371
  actionSuccess: (objectName, actionName, fallback) => {
328
372
  const fb = fallback ?? '';
329
- const suffixes = objectName
330
- ? objectSuffixes(objectName, `_actions.${actionName}.successMessage`)
331
- : `globalActions.${actionName}.successMessage`;
332
- const resolved = resolve(suffixes, fb);
373
+ const resolved = resolve(actionSuffixes(objectName, actionName, 'successMessage'), fb);
333
374
  return resolved || undefined;
334
375
  },
335
376
  /**
@@ -343,18 +384,16 @@ export function useObjectLabel() {
343
384
  const fb = fallback ?? '';
344
385
  if (!actionName)
345
386
  return fb || undefined;
346
- const suffixes = objectName
347
- ? objectSuffixes(objectName, `_actions.${actionName}.description`)
348
- : `globalActions.${actionName}.description`;
349
- const resolved = resolve(suffixes, fb);
387
+ const resolved = resolve(actionSuffixes(objectName, actionName, 'description'), fb);
350
388
  return resolved || undefined;
351
389
  },
352
390
  /**
353
391
  * Resolve a translated copy of an action's post-success RESULT DIALOG
354
392
  * (`title` / `description` / `acknowledge` + per-field labels).
355
393
  * 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.
394
+ * falling back to `{ns}.globalActions.{actionName}.resultDialog.*` (when
395
+ * objectName is omitted OR the object-scoped key misses), then to the
396
+ * metadata's literal strings.
358
397
  *
359
398
  * The `fields` translation node is keyed by the LITERAL result-field path
360
399
  * from the action metadata (may contain dots, e.g. `"user.email"`), so it
@@ -364,12 +403,7 @@ export function useObjectLabel() {
364
403
  actionResultDialog: (objectName, actionName, spec) => {
365
404
  if (!spec || !actionName)
366
405
  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
- };
406
+ const suffixesFor = (attr) => actionSuffixes(objectName, actionName, `resultDialog.${attr}`);
373
407
  const textFor = (attr) => {
374
408
  const resolved = resolve(suffixesFor(attr), spec[attr] ?? '');
375
409
  return resolved || spec[attr];
@@ -420,11 +454,7 @@ export function useObjectLabel() {
420
454
  const fb = fallback ?? '';
421
455
  if (!actionName || !paramName)
422
456
  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);
457
+ const resolved = resolve(actionSuffixes(objectName, actionName, `params.${paramName}.${attr}`), fb);
428
458
  return resolved || undefined;
429
459
  },
430
460
  /**
@@ -435,11 +465,7 @@ export function useObjectLabel() {
435
465
  actionParamOptionLabel: (objectName, actionName, paramName, optionValue, fallback) => {
436
466
  if (!actionName || !paramName)
437
467
  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);
468
+ return resolve(actionSuffixes(objectName, actionName, `params.${paramName}.options.${optionValue}`), fallback);
443
469
  },
444
470
  };
445
471
  }, [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.2.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
  },