@asteby/metacore-runtime-react 18.19.0 → 18.20.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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6ec7baf: CollectionCell is now locale-aware: jsonb/array popover headers and the item
8
+ count noun render in the org's language. Resolution per key: host `t(rawKey)`
9
+ override → built-in es/en dictionary of common data/commerce keys (product_id,
10
+ quantity, price, total, name, sku, …) → snake→Title prettify fallback. Count
11
+ noun localizes (es: ítem/ítems). Locale + translator are threaded from the
12
+ dynamic columns factory; defaults to English when absent.
13
+
3
14
  ## 18.19.0
4
15
 
5
16
  ### Minor Changes
@@ -1,6 +1,19 @@
1
1
  import * as React from 'react';
2
- /** snake_case / dotted / kebab key Title Case (`product_id` → "Product ID"). */
3
- export declare function prettifyKey(key: string): string;
2
+ /** Host i18n translator (react-i18next `t`), as threaded into the columns factory. */
3
+ export type Translate = (key: string, options?: any) => string;
4
+ /**
5
+ * Localized key label for a popover column header. Resolution order:
6
+ * (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
7
+ * (b) the built-in generic es/en data/commerce dictionary;
8
+ * (c) snake_case → Title Case prettify (`product_id` → "Product ID").
9
+ * `locale` defaults to 'en' when absent.
10
+ */
11
+ export declare function prettifyKey(key: string, locale?: string, t?: Translate): string;
12
+ /**
13
+ * Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
14
+ * count plural via `defaultValue`); otherwise the built-in es/en noun map.
15
+ */
16
+ export declare function countLabel(count: number, locale?: string, t?: Translate): string;
4
17
  /**
5
18
  * Render a single scalar (or near-scalar) value for compact display.
6
19
  * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
@@ -13,10 +26,14 @@ export interface CollectionCellProps {
13
26
  value: unknown;
14
27
  /** Max items previewed inline for scalar arrays. */
15
28
  maxInline?: number;
29
+ /** Org/UI language tag (e.g. `es`, `en-US`). Defaults to `'en'`. */
30
+ locale?: string;
31
+ /** Host i18n translator; takes precedence over the built-in dictionary. */
32
+ t?: Translate;
16
33
  }
17
34
  /**
18
35
  * Generic renderer for jsonb / array / object cell values. Brand-neutral,
19
- * compact, dark-mode friendly. Never throws on unexpected shapes.
36
+ * compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
20
37
  */
21
- export declare function CollectionCell({ value, maxInline }: CollectionCellProps): React.JSX.Element;
38
+ export declare function CollectionCell({ value, maxInline, locale, t, }: CollectionCellProps): React.JSX.Element;
22
39
  //# sourceMappingURL=collection-cell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYnD;AAyID,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,SAAa,EAAE,EAAE,mBAAmB,qBA6E3E"}
1
+ {"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,sFAAsF;AACtF,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,CAAA;AAiE9D;;;;;;GAMG;AACH,wBAAgB,WAAW,CACvB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,EACf,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAWR;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAcR;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYnD;AAyJD,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2EAA2E;IAC3E,CAAC,CAAC,EAAE,SAAS,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAa,EACb,MAAM,EACN,CAAC,GACJ,EAAE,mBAAmB,qBAgFrB"}
@@ -3,11 +3,106 @@ import { List } from 'lucide-react';
3
3
  import { Badge, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@asteby/metacore-ui';
4
4
  import { humanizeToken } from './dynamic-columns-helpers';
5
5
  const UUID_LIKE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
- /** snake_case / dotted / kebab key Title Case (`product_id` → "Product ID"). */
7
- export function prettifyKey(key) {
6
+ /** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
7
+ function baseLang(locale) {
8
+ return (locale || 'en').toLowerCase().split('-')[0];
9
+ }
10
+ // Built-in generic data/commerce vocabulary. Localizes the common jsonb keys
11
+ // (line-item rows, config blobs) to the org language out of the box. This is
12
+ // intentionally GENERIC — no domain-specific narrative — and a host i18n bundle
13
+ // can override any key via the `t` resolver (which takes precedence). Keys not
14
+ // found here fall back to snake→Title prettify, so unknown shapes still read.
15
+ const KEY_DICTIONARY = {
16
+ es: {
17
+ product_id: 'Producto',
18
+ product: 'Producto',
19
+ quantity: 'Cantidad',
20
+ qty: 'Cantidad',
21
+ unit_cost: 'Costo unitario',
22
+ cost: 'Costo',
23
+ price: 'Precio',
24
+ total: 'Total',
25
+ subtotal: 'Subtotal',
26
+ amount: 'Importe',
27
+ name: 'Nombre',
28
+ sku: 'SKU',
29
+ code: 'Código',
30
+ date: 'Fecha',
31
+ notes: 'Notas',
32
+ reason: 'Motivo',
33
+ delta: 'Variación',
34
+ warehouse: 'Almacén',
35
+ description: 'Descripción',
36
+ id: 'ID',
37
+ },
38
+ en: {
39
+ product_id: 'Product',
40
+ product: 'Product',
41
+ quantity: 'Quantity',
42
+ qty: 'Quantity',
43
+ unit_cost: 'Unit Cost',
44
+ cost: 'Cost',
45
+ price: 'Price',
46
+ total: 'Total',
47
+ subtotal: 'Subtotal',
48
+ amount: 'Amount',
49
+ name: 'Name',
50
+ sku: 'SKU',
51
+ code: 'Code',
52
+ date: 'Date',
53
+ notes: 'Notes',
54
+ reason: 'Reason',
55
+ delta: 'Delta',
56
+ warehouse: 'Warehouse',
57
+ description: 'Description',
58
+ id: 'ID',
59
+ },
60
+ };
61
+ /** Localized count noun for the array-of-objects badge (`1 ítem` / `2 ítems`). */
62
+ const ITEM_NOUN = {
63
+ es: { one: 'ítem', other: 'ítems' },
64
+ en: { one: 'item', other: 'items' },
65
+ };
66
+ /**
67
+ * Localized key label for a popover column header. Resolution order:
68
+ * (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
69
+ * (b) the built-in generic es/en data/commerce dictionary;
70
+ * (c) snake_case → Title Case prettify (`product_id` → "Product ID").
71
+ * `locale` defaults to 'en' when absent.
72
+ */
73
+ export function prettifyKey(key, locale, t) {
74
+ if (t) {
75
+ const translated = t(key);
76
+ if (translated && translated !== key)
77
+ return translated;
78
+ }
79
+ const lang = baseLang(locale);
80
+ const dict = KEY_DICTIONARY[lang] ?? KEY_DICTIONARY.en;
81
+ const hit = dict[key.toLowerCase()];
82
+ if (hit)
83
+ return hit;
8
84
  const pretty = humanizeToken(key);
9
85
  return pretty || key;
10
86
  }
87
+ /**
88
+ * Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
89
+ * count plural via `defaultValue`); otherwise the built-in es/en noun map.
90
+ */
91
+ export function countLabel(count, locale, t) {
92
+ const lang = baseLang(locale);
93
+ const noun = ITEM_NOUN[lang] ?? ITEM_NOUN.en;
94
+ const fallback = `${count} ${count === 1 ? noun.one : noun.other}`;
95
+ if (t) {
96
+ const translated = t('runtime.collectionCell.itemCount', {
97
+ count,
98
+ defaultValue: fallback,
99
+ });
100
+ if (translated && translated !== 'runtime.collectionCell.itemCount') {
101
+ return translated;
102
+ }
103
+ }
104
+ return fallback;
105
+ }
11
106
  /**
12
107
  * Render a single scalar (or near-scalar) value for compact display.
13
108
  * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
@@ -69,18 +164,18 @@ function unionKeys(rows) {
69
164
  return seen;
70
165
  }
71
166
  const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0';
72
- function MiniTable({ rows }) {
167
+ function MiniTable({ rows, locale, t, }) {
73
168
  const keys = unionKeys(rows);
74
169
  if (keys.length === 0) {
75
170
  return _jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "-" });
76
171
  }
77
- return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: keys.map((key) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: prettifyKey(key) }, key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: keys.map((key) => (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: formatScalar(row[key]) }, key))) }, i))) })] }));
172
+ return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: keys.map((key) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: prettifyKey(key, locale, t) }, key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: keys.map((key) => (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: formatScalar(row[key]) }, key))) }, i))) })] }));
78
173
  }
79
174
  function ScalarList({ values }) {
80
175
  return (_jsx("ul", { className: "p-3 space-y-1", children: values.map((v, i) => (_jsx("li", { className: "text-xs text-foreground", children: formatScalar(v) }, i))) }));
81
176
  }
82
- function PairList({ entries }) {
83
- return (_jsx("ul", { className: "p-3 space-y-1", children: entries.map(([key, v]) => (_jsxs("li", { className: "text-xs", children: [_jsxs("span", { className: "text-muted-foreground", children: [prettifyKey(key), ":"] }), ' ', _jsx("span", { className: "text-foreground", children: formatScalar(v) })] }, key))) }));
177
+ function PairList({ entries, locale, t, }) {
178
+ return (_jsx("ul", { className: "p-3 space-y-1", children: entries.map(([key, v]) => (_jsxs("li", { className: "text-xs", children: [_jsxs("span", { className: "text-muted-foreground", children: [prettifyKey(key, locale, t), ":"] }), ' ', _jsx("span", { className: "text-foreground", children: formatScalar(v) })] }, key))) }));
84
179
  }
85
180
  /** Compact badge trigger that opens a popover panel. */
86
181
  function PopoverShell({ label, title, children, icon = true, }) {
@@ -88,9 +183,9 @@ function PopoverShell({ label, title, children, icon = true, }) {
88
183
  }
89
184
  /**
90
185
  * Generic renderer for jsonb / array / object cell values. Brand-neutral,
91
- * compact, dark-mode friendly. Never throws on unexpected shapes.
186
+ * compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
92
187
  */
93
- export function CollectionCell({ value, maxInline = 3 }) {
188
+ export function CollectionCell({ value, maxInline = 3, locale, t, }) {
94
189
  const parsed = parseValue(value);
95
190
  // Empty-ish → muted dash.
96
191
  if (parsed === null ||
@@ -111,13 +206,13 @@ export function CollectionCell({ value, maxInline = 3 }) {
111
206
  if (allObjects) {
112
207
  const rows = parsed;
113
208
  const count = rows.length;
114
- const label = count === 1 ? '1 ítem' : `${count} ítems`;
209
+ const label = countLabel(count, locale, t);
115
210
  const title = rows
116
211
  .map((row) => Object.entries(row)
117
- .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
212
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
118
213
  .join(', '))
119
214
  .join(' | ');
120
- return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows }) }));
215
+ return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows, locale: locale, t: t }) }));
121
216
  }
122
217
  // Array of scalars (or mixed): preview first N joined, "+N" overflow.
123
218
  const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ');
@@ -130,12 +225,12 @@ export function CollectionCell({ value, maxInline = 3 }) {
130
225
  const entries = Object.entries(parsed);
131
226
  const inline = entries
132
227
  .slice(0, maxInline)
133
- .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
228
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
134
229
  .join(', ');
135
230
  const overflow = entries.length - maxInline;
136
231
  const label = overflow > 0 ? `${inline} +${overflow}` : inline;
137
232
  const title = entries
138
- .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
233
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
139
234
  .join(', ');
140
- return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(PairList, { entries: entries }) }));
235
+ return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(PairList, { entries: entries, locale: locale, t: t }) }));
141
236
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAiC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAunBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAiC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA6nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -736,7 +736,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
736
736
  }
737
737
  default: {
738
738
  if (typeof value === 'object' && value !== null) {
739
- return _jsx(CollectionCell, { value: value });
739
+ return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t }));
740
740
  }
741
741
  if (col.key === 'description' ||
742
742
  col.key === 'features' ||
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export * from './dynamic-icon';
22
22
  export type { ColumnFilterConfig, FilterOption as DynamicColumnFilterOption, GetDynamicColumns, DynamicIconComponent, } from './dynamic-columns-shim';
23
23
  export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, type DynamicColumnsHelpers, } from './dynamic-columns';
24
24
  export { humanizeToken } from './dynamic-columns-helpers';
25
- export { CollectionCell, formatScalar, prettifyKey, type CollectionCellProps, } from './collection-cell';
25
+ export { CollectionCell, formatScalar, prettifyKey, countLabel, type CollectionCellProps, type Translate as CollectionCellTranslate, } from './collection-cell';
26
26
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
27
27
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
28
28
  export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,kBAAkB,EAClB,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,eAAe,GACvB,MAAM,wBAAwB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,WAAW,EAChB,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,OAAO,EACH,mBAAmB,EACnB,MAAM,EACN,oBAAoB,EACpB,OAAO,EACP,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,KAAK,KAAK,EACV,KAAK,wBAAwB,GAChC,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,OAAO,EACZ,KAAK,SAAS,GACjB,MAAM,uBAAuB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA;AACtC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,GACtC,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC9B,MAAM,yBAAyB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,cAAc,EACd,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EACH,cAAc,EACd,YAAY,EACZ,WAAW,EACX,UAAU,EACV,KAAK,mBAAmB,EACxB,KAAK,SAAS,IAAI,uBAAuB,GAC5C,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzE,YAAY,EAAE,wBAAwB,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC5G,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,YAAY,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACH,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACH,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,GACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,0BAA0B,GAClC,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACzB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACH,aAAa,EACb,KAAK,kBAAkB,GAC1B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACH,gBAAgB,EAChB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,aAAa,EACb,eAAe,GAClB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACR,UAAU,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,GACvB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACH,UAAU,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,cAAc,EACd,KAAK,iBAAiB,GACzB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,cAAc,EACd,cAAc,EACd,SAAS,EACT,UAAU,EACV,KAAK,mBAAmB,GAC3B,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,UAAU,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,KAAK,eAAe,GACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,aAAa,EACb,YAAY,EACZ,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA"}
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederation
26
26
  export * from './dynamic-icon';
27
27
  export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, } from './dynamic-columns';
28
28
  export { humanizeToken } from './dynamic-columns-helpers';
29
- export { CollectionCell, formatScalar, prettifyKey, } from './collection-cell';
29
+ export { CollectionCell, formatScalar, prettifyKey, countLabel, } from './collection-cell';
30
30
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
31
31
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
32
32
  export { CreateRecordDialog } from './dialogs/create-record-dialog';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.19.0",
3
+ "version": "18.20.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -6,6 +6,8 @@
6
6
  // - plain object → inline key: value pairs
7
7
  // - null / empty → "-"
8
8
  // - JSON-string value is defensively parsed
9
+ // - locale-aware: count noun + header keys render in the org language; the
10
+ // host `t` overrides; unknown keys fall back to snake→Title prettify.
9
11
  import { afterEach, describe, expect, it } from 'vitest'
10
12
  import { cleanup, render, screen } from '@testing-library/react'
11
13
 
@@ -14,6 +16,7 @@ afterEach(cleanup)
14
16
 
15
17
  import {
16
18
  CollectionCell,
19
+ countLabel,
17
20
  formatScalar,
18
21
  prettifyKey,
19
22
  } from '../collection-cell'
@@ -41,14 +44,50 @@ describe('formatScalar', () => {
41
44
  })
42
45
 
43
46
  describe('prettifyKey', () => {
44
- it('snake_case Title Case with acronyms', () => {
45
- expect(prettifyKey('product_id')).toBe('Product ID')
46
- expect(prettifyKey('quantity')).toBe('Quantity')
47
+ it('localizes common data/commerce keys to Spanish', () => {
48
+ expect(prettifyKey('product_id', 'es')).toBe('Producto')
49
+ expect(prettifyKey('quantity', 'es')).toBe('Cantidad')
50
+ })
51
+
52
+ it('localizes common keys to English (default locale)', () => {
53
+ expect(prettifyKey('product_id')).toBe('Product')
54
+ expect(prettifyKey('product_id', 'en')).toBe('Product')
55
+ expect(prettifyKey('quantity', 'en')).toBe('Quantity')
56
+ })
57
+
58
+ it('accepts a regional tag and normalizes to base language', () => {
59
+ expect(prettifyKey('quantity', 'es-MX')).toBe('Cantidad')
60
+ })
61
+
62
+ it('prefers a host `t` translation over the built-in dictionary', () => {
63
+ const t = (k: string) => (k === 'quantity' ? 'Piezas' : k)
64
+ expect(prettifyKey('quantity', 'es', t)).toBe('Piezas')
65
+ })
66
+
67
+ it('falls back to snake→Title prettify for unknown keys', () => {
68
+ expect(prettifyKey('shelf_position', 'es')).toBe('Shelf Position')
69
+ expect(prettifyKey('warehouse_bin', 'en')).toBe('Warehouse Bin')
70
+ })
71
+ })
72
+
73
+ describe('countLabel', () => {
74
+ it('pluralizes the count noun per locale', () => {
75
+ expect(countLabel(1, 'es')).toBe('1 ítem')
76
+ expect(countLabel(2, 'es')).toBe('2 ítems')
77
+ expect(countLabel(1, 'en')).toBe('1 item')
78
+ expect(countLabel(3, 'en')).toBe('3 items')
79
+ expect(countLabel(1)).toBe('1 item') // default → en
80
+ })
81
+
82
+ it('prefers a host `t` count plural', () => {
83
+ const t = (_k: string, o?: any) =>
84
+ o?.count === 1 ? '1 renglón' : `${o?.count} renglones`
85
+ expect(countLabel(2, 'es', t)).toBe('2 renglones')
47
86
  })
48
87
  })
49
88
 
50
89
  describe('CollectionCell', () => {
51
- it('renders a count badge for an array of objects', () => {
90
+ it('renders an English count badge by default for an array of objects', () => {
52
91
  render(
53
92
  <CollectionCell
54
93
  value={[
@@ -57,32 +96,55 @@ describe('CollectionCell', () => {
57
96
  ]}
58
97
  />
59
98
  )
60
- // Count badge (plural).
61
- expect(screen.getByText('2 ítems')).toBeTruthy()
62
- // The trigger's title carries the formatted rows for the no-JS fallback.
63
- const badge = screen.getByText('2 ítems').closest('[title]')
99
+ // Default locale → English plural.
100
+ expect(screen.getByText('2 items')).toBeTruthy()
101
+ // The trigger's title carries the localized rows for the no-JS fallback.
102
+ const badge = screen.getByText('2 items').closest('[title]')
64
103
  expect(badge).toBeTruthy()
65
104
  const title = badge!.getAttribute('title') ?? ''
66
105
  expect(title).toContain('Quantity: 2')
67
106
  expect(title).toContain('Quantity: 5')
68
- expect(title).toContain('Product ID')
107
+ expect(title).toContain('Product:') // product_id → "Product" (en)
69
108
  })
70
109
 
71
- it('renders singular label for a single-item array', () => {
72
- render(<CollectionCell value={[{ sku: 'A1' }]} />)
110
+ it('renders Spanish count + headers when locale is es', () => {
111
+ render(
112
+ <CollectionCell
113
+ locale="es"
114
+ value={[
115
+ { product_id: 'abc', quantity: 2 },
116
+ { product_id: 'def', quantity: 5 },
117
+ ]}
118
+ />
119
+ )
120
+ expect(screen.getByText('2 ítems')).toBeTruthy()
121
+ const title =
122
+ screen.getByText('2 ítems').closest('[title]')!.getAttribute('title') ??
123
+ ''
124
+ expect(title).toContain('Producto:')
125
+ expect(title).toContain('Cantidad: 2')
126
+ })
127
+
128
+ it('renders the singular Spanish noun for a single-item array', () => {
129
+ render(<CollectionCell locale="es" value={[{ sku: 'A1' }]} />)
73
130
  expect(screen.getByText('1 ítem')).toBeTruthy()
74
131
  })
75
132
 
133
+ it('renders the singular English noun for a single-item array', () => {
134
+ render(<CollectionCell value={[{ sku: 'A1' }]} />)
135
+ expect(screen.getByText('1 item')).toBeTruthy()
136
+ })
137
+
76
138
  it('previews the first scalars with overflow for a scalar array', () => {
77
139
  render(<CollectionCell value={['a', 'b', 'c', 'd', 'e']} />)
78
140
  expect(screen.getByText('a, b, c +2')).toBeTruthy()
79
141
  })
80
142
 
81
- it('renders inline key: value pairs for a plain object', () => {
82
- render(<CollectionCell value={{ width: 10, height: 20 }} />)
83
- expect(
84
- screen.getByText(/Width: 10, Height: 20/)
85
- ).toBeTruthy()
143
+ it('renders inline key: value pairs (localized) for a plain object', () => {
144
+ render(
145
+ <CollectionCell locale="es" value={{ price: 10, quantity: 20 }} />
146
+ )
147
+ expect(screen.getByText(/Precio: 10, Cantidad: 20/)).toBeTruthy()
86
148
  })
87
149
 
88
150
  it('renders a dash for null / empty values', () => {
@@ -98,12 +160,13 @@ describe('CollectionCell', () => {
98
160
  it('parses a JSON-string value into a collection', () => {
99
161
  render(
100
162
  <CollectionCell
163
+ locale="es"
101
164
  value={'[{"product_id":"abc","quantity":1}]'}
102
165
  />
103
166
  )
104
167
  expect(screen.getByText('1 ítem')).toBeTruthy()
105
168
  const badge = screen.getByText('1 ítem').closest('[title]')
106
- expect(badge!.getAttribute('title')).toContain('Quantity: 1')
169
+ expect(badge!.getAttribute('title')).toContain('Cantidad: 1')
107
170
  })
108
171
 
109
172
  it('truncates an unparseable string instead of crashing', () => {
@@ -25,12 +25,120 @@ import { humanizeToken } from './dynamic-columns-helpers'
25
25
  const UUID_LIKE_RE =
26
26
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
27
27
 
28
- /** snake_case / dotted / kebab key Title Case (`product_id` → "Product ID"). */
29
- export function prettifyKey(key: string): string {
28
+ /** Host i18n translator (react-i18next `t`), as threaded into the columns factory. */
29
+ export type Translate = (key: string, options?: any) => string
30
+
31
+ /** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
32
+ function baseLang(locale?: string): string {
33
+ return (locale || 'en').toLowerCase().split('-')[0]
34
+ }
35
+
36
+ // Built-in generic data/commerce vocabulary. Localizes the common jsonb keys
37
+ // (line-item rows, config blobs) to the org language out of the box. This is
38
+ // intentionally GENERIC — no domain-specific narrative — and a host i18n bundle
39
+ // can override any key via the `t` resolver (which takes precedence). Keys not
40
+ // found here fall back to snake→Title prettify, so unknown shapes still read.
41
+ const KEY_DICTIONARY: Record<string, Record<string, string>> = {
42
+ es: {
43
+ product_id: 'Producto',
44
+ product: 'Producto',
45
+ quantity: 'Cantidad',
46
+ qty: 'Cantidad',
47
+ unit_cost: 'Costo unitario',
48
+ cost: 'Costo',
49
+ price: 'Precio',
50
+ total: 'Total',
51
+ subtotal: 'Subtotal',
52
+ amount: 'Importe',
53
+ name: 'Nombre',
54
+ sku: 'SKU',
55
+ code: 'Código',
56
+ date: 'Fecha',
57
+ notes: 'Notas',
58
+ reason: 'Motivo',
59
+ delta: 'Variación',
60
+ warehouse: 'Almacén',
61
+ description: 'Descripción',
62
+ id: 'ID',
63
+ },
64
+ en: {
65
+ product_id: 'Product',
66
+ product: 'Product',
67
+ quantity: 'Quantity',
68
+ qty: 'Quantity',
69
+ unit_cost: 'Unit Cost',
70
+ cost: 'Cost',
71
+ price: 'Price',
72
+ total: 'Total',
73
+ subtotal: 'Subtotal',
74
+ amount: 'Amount',
75
+ name: 'Name',
76
+ sku: 'SKU',
77
+ code: 'Code',
78
+ date: 'Date',
79
+ notes: 'Notes',
80
+ reason: 'Reason',
81
+ delta: 'Delta',
82
+ warehouse: 'Warehouse',
83
+ description: 'Description',
84
+ id: 'ID',
85
+ },
86
+ }
87
+
88
+ /** Localized count noun for the array-of-objects badge (`1 ítem` / `2 ítems`). */
89
+ const ITEM_NOUN: Record<string, { one: string; other: string }> = {
90
+ es: { one: 'ítem', other: 'ítems' },
91
+ en: { one: 'item', other: 'items' },
92
+ }
93
+
94
+ /**
95
+ * Localized key label for a popover column header. Resolution order:
96
+ * (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
97
+ * (b) the built-in generic es/en data/commerce dictionary;
98
+ * (c) snake_case → Title Case prettify (`product_id` → "Product ID").
99
+ * `locale` defaults to 'en' when absent.
100
+ */
101
+ export function prettifyKey(
102
+ key: string,
103
+ locale?: string,
104
+ t?: Translate,
105
+ ): string {
106
+ if (t) {
107
+ const translated = t(key)
108
+ if (translated && translated !== key) return translated
109
+ }
110
+ const lang = baseLang(locale)
111
+ const dict = KEY_DICTIONARY[lang] ?? KEY_DICTIONARY.en
112
+ const hit = dict[key.toLowerCase()]
113
+ if (hit) return hit
30
114
  const pretty = humanizeToken(key)
31
115
  return pretty || key
32
116
  }
33
117
 
118
+ /**
119
+ * Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
120
+ * count plural via `defaultValue`); otherwise the built-in es/en noun map.
121
+ */
122
+ export function countLabel(
123
+ count: number,
124
+ locale?: string,
125
+ t?: Translate,
126
+ ): string {
127
+ const lang = baseLang(locale)
128
+ const noun = ITEM_NOUN[lang] ?? ITEM_NOUN.en
129
+ const fallback = `${count} ${count === 1 ? noun.one : noun.other}`
130
+ if (t) {
131
+ const translated = t('runtime.collectionCell.itemCount', {
132
+ count,
133
+ defaultValue: fallback,
134
+ })
135
+ if (translated && translated !== 'runtime.collectionCell.itemCount') {
136
+ return translated
137
+ }
138
+ }
139
+ return fallback
140
+ }
141
+
34
142
  /**
35
143
  * Render a single scalar (or near-scalar) value for compact display.
36
144
  * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
@@ -92,7 +200,15 @@ function unionKeys(rows: Record<string, unknown>[]): string[] {
92
200
 
93
201
  const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0'
94
202
 
95
- function MiniTable({ rows }: { rows: Record<string, unknown>[] }) {
203
+ function MiniTable({
204
+ rows,
205
+ locale,
206
+ t,
207
+ }: {
208
+ rows: Record<string, unknown>[]
209
+ locale?: string
210
+ t?: Translate
211
+ }) {
96
212
  const keys = unionKeys(rows)
97
213
  if (keys.length === 0) {
98
214
  return <div className="p-3 text-xs text-muted-foreground">-</div>
@@ -103,7 +219,7 @@ function MiniTable({ rows }: { rows: Record<string, unknown>[] }) {
103
219
  <TableRow>
104
220
  {keys.map((key) => (
105
221
  <TableHead key={key} className="text-xs whitespace-nowrap">
106
- {prettifyKey(key)}
222
+ {prettifyKey(key, locale, t)}
107
223
  </TableHead>
108
224
  ))}
109
225
  </TableRow>
@@ -138,13 +254,21 @@ function ScalarList({ values }: { values: unknown[] }) {
138
254
  )
139
255
  }
140
256
 
141
- function PairList({ entries }: { entries: [string, unknown][] }) {
257
+ function PairList({
258
+ entries,
259
+ locale,
260
+ t,
261
+ }: {
262
+ entries: [string, unknown][]
263
+ locale?: string
264
+ t?: Translate
265
+ }) {
142
266
  return (
143
267
  <ul className="p-3 space-y-1">
144
268
  {entries.map(([key, v]) => (
145
269
  <li key={key} className="text-xs">
146
270
  <span className="text-muted-foreground">
147
- {prettifyKey(key)}:
271
+ {prettifyKey(key, locale, t)}:
148
272
  </span>{' '}
149
273
  <span className="text-foreground">{formatScalar(v)}</span>
150
274
  </li>
@@ -191,13 +315,22 @@ export interface CollectionCellProps {
191
315
  value: unknown
192
316
  /** Max items previewed inline for scalar arrays. */
193
317
  maxInline?: number
318
+ /** Org/UI language tag (e.g. `es`, `en-US`). Defaults to `'en'`. */
319
+ locale?: string
320
+ /** Host i18n translator; takes precedence over the built-in dictionary. */
321
+ t?: Translate
194
322
  }
195
323
 
196
324
  /**
197
325
  * Generic renderer for jsonb / array / object cell values. Brand-neutral,
198
- * compact, dark-mode friendly. Never throws on unexpected shapes.
326
+ * compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
199
327
  */
200
- export function CollectionCell({ value, maxInline = 3 }: CollectionCellProps) {
328
+ export function CollectionCell({
329
+ value,
330
+ maxInline = 3,
331
+ locale,
332
+ t,
333
+ }: CollectionCellProps) {
201
334
  const parsed = parseValue(value)
202
335
 
203
336
  // Empty-ish → muted dash.
@@ -230,17 +363,20 @@ export function CollectionCell({ value, maxInline = 3 }: CollectionCellProps) {
230
363
  if (allObjects) {
231
364
  const rows = parsed as Record<string, unknown>[]
232
365
  const count = rows.length
233
- const label = count === 1 ? '1 ítem' : `${count} ítems`
366
+ const label = countLabel(count, locale, t)
234
367
  const title = rows
235
368
  .map((row) =>
236
369
  Object.entries(row)
237
- .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
370
+ .map(
371
+ ([k, v]) =>
372
+ `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`
373
+ )
238
374
  .join(', ')
239
375
  )
240
376
  .join(' | ')
241
377
  return (
242
378
  <PopoverShell label={label} title={title}>
243
- <MiniTable rows={rows} />
379
+ <MiniTable rows={rows} locale={locale} t={t} />
244
380
  </PopoverShell>
245
381
  )
246
382
  }
@@ -262,16 +398,16 @@ export function CollectionCell({ value, maxInline = 3 }: CollectionCellProps) {
262
398
  const entries = Object.entries(parsed)
263
399
  const inline = entries
264
400
  .slice(0, maxInline)
265
- .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
401
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
266
402
  .join(', ')
267
403
  const overflow = entries.length - maxInline
268
404
  const label = overflow > 0 ? `${inline} +${overflow}` : inline
269
405
  const title = entries
270
- .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
406
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
271
407
  .join(', ')
272
408
  return (
273
409
  <PopoverShell label={label} title={title} icon={false}>
274
- <PairList entries={entries} />
410
+ <PairList entries={entries} locale={locale} t={t} />
275
411
  </PopoverShell>
276
412
  )
277
413
  }
@@ -1174,7 +1174,13 @@ export function makeDefaultGetDynamicColumns(
1174
1174
 
1175
1175
  default: {
1176
1176
  if (typeof value === 'object' && value !== null) {
1177
- return <CollectionCell value={value} />
1177
+ return (
1178
+ <CollectionCell
1179
+ value={value}
1180
+ locale={currentLanguage}
1181
+ t={t}
1182
+ />
1183
+ )
1178
1184
  }
1179
1185
  if (
1180
1186
  col.key === 'description' ||
package/src/index.ts CHANGED
@@ -105,7 +105,9 @@ export {
105
105
  CollectionCell,
106
106
  formatScalar,
107
107
  prettifyKey,
108
+ countLabel,
108
109
  type CollectionCellProps,
110
+ type Translate as CollectionCellTranslate,
109
111
  } from './collection-cell'
110
112
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid'
111
113
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record'