From 55669aa46c7a1e0c987c920bed26f0520592466d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 11:15:00 +0000 Subject: [PATCH 1/4] feat(spec): the view columns, sort and tabs repeaters enumerate their row children Each child's label is the item schema's own `.meta({ title })`, so the i18n extractor emits a catalog key per column. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- packages/spec/src/ui/view.form.ts | 62 +++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/spec/src/ui/view.form.ts b/packages/spec/src/ui/view.form.ts index 8fac36f4890..7dd048b82a0 100644 --- a/packages/spec/src/ui/view.form.ts +++ b/packages/spec/src/ui/view.form.ts @@ -37,9 +37,46 @@ export const viewForm = defineForm({ label: 'Columns & filters', description: 'What rows show and how users filter them.', fields: [ - { field: 'columns', type: 'repeater', required: true, helpText: 'Columns to display (field names from selected object)' }, + { + field: 'columns', + type: 'repeater', + required: true, + helpText: 'Columns to display (field names from selected object)', + // Row-property names (#17508): every authorable row property, `label` + // equal to the item schema's `.meta({ title })`, so `os i18n extract` + // emits a catalog key per column and the panel keeps its schema-derived + // widgets (no `type` here). + fields: [ + { field: 'field', label: 'Field' }, + { field: 'label', label: 'Label' }, + { field: 'width', label: 'Width (px)' }, + { field: 'align', label: 'Alignment' }, + { field: 'hidden', label: 'Hidden' }, + { field: 'sortable', label: 'Sortable' }, + { field: 'resizable', label: 'Resizable' }, + { field: 'wrap', label: 'Wrap Text' }, + { field: 'type', label: 'Renderer Type' }, + { field: 'pinned', label: 'Pinned' }, + { field: 'summary', label: 'Summary' }, + { field: 'prefix', label: 'Prefix' }, + { field: 'link', label: 'Primary Link' }, + { field: 'action', label: 'Click Action' }, + ], + }, { field: 'filter', widget: 'filter-builder', dependsOn: 'data.object', helpText: 'Filter conditions — same visual builder as the list toolbar, with field-type-aware operators and value inputs' }, - { field: 'sort', type: 'repeater', helpText: 'Default sort order' }, + { + field: 'sort', + type: 'repeater', + helpText: 'Default sort order', + // Row-property names (#17508): every authorable row property, `label` + // equal to the item schema's `.meta({ title })`, so `os i18n extract` + // emits a catalog key per column and the panel keeps its schema-derived + // widgets (no `type` here). + fields: [ + { field: 'field', label: 'Field' }, + { field: 'order', label: 'Direction' }, + ], + }, { field: 'searchableFields', widget: 'string-tags', helpText: 'Field names available for quick search' }, { field: 'filterableFields', widget: 'string-tags', helpText: 'Field names available for filtering' }, ], @@ -129,7 +166,26 @@ export const viewForm = defineForm({ collapsed: true, fields: [ { field: 'userFilters', type: 'composite', helpText: 'Quick-filter bar: element style (dropdown / tabs / toggle) + exposed fields or tab presets' }, - { field: 'tabs', type: 'repeater', helpText: 'In-view filter tabs — each tab applies its own filter rules' }, + { + field: 'tabs', + type: 'repeater', + helpText: 'In-view filter tabs — each tab applies its own filter rules', + // Row-property names (#17508): every authorable row property, `label` + // equal to the item schema's `.meta({ title })`, so `os i18n extract` + // emits a catalog key per column and the panel keeps its schema-derived + // widgets (no `type` here). + fields: [ + { field: 'name', label: 'Name' }, + { field: 'label', label: 'Label' }, + { field: 'icon', label: 'Icon' }, + { field: 'view', label: 'List View' }, + { field: 'filter', label: 'Filter' }, + { field: 'order', label: 'Display Order' }, + { field: 'pinned', label: 'Pinned' }, + { field: 'isDefault', label: 'Default Tab' }, + { field: 'visible', label: 'Visible' }, + ], + }, { field: 'appearance', type: 'composite', helpText: 'allowedVisualizations: which renderers users may switch between' }, { field: 'userActions', type: 'composite', helpText: 'Toolbar toggles: sort / search / filter / row height' }, { field: 'addRecord', type: 'composite' }, From 7d0dfd8c788bbdfbd5639668204eae3e8466edf3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 11:25:12 +0000 Subject: [PATCH 2/4] feat(platform-objects): name the view columns, sort and tabs row properties in all four catalogs The extractor emits the new keys for en; the zh-CN, ja-JP and es-ES leaves are hand-authored, reusing each catalog's existing rendering where one exists. The repeater-row-properties pin now counts `view` inside its population, so its en-echo case covers these leaves. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .../en.metadata-forms.generated.ts | 75 +++++++++++++++++++ .../es-ES.metadata-forms.generated.ts | 75 +++++++++++++++++++ .../ja-JP.metadata-forms.generated.ts | 75 +++++++++++++++++++ .../repeater-row-properties.test.ts | 15 ++-- .../zh-CN.metadata-forms.generated.ts | 75 +++++++++++++++++++ 5 files changed, 309 insertions(+), 6 deletions(-) diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index f05b4d46c90..d8245effd9f 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -790,6 +790,48 @@ export const enMetadataForms: NonNullable = { label: "Columns", helpText: "Columns to display (field names from selected object)" }, + "columns.field": { + label: "Field" + }, + "columns.label": { + label: "Label" + }, + "columns.width": { + label: "Width (px)" + }, + "columns.align": { + label: "Alignment" + }, + "columns.hidden": { + label: "Hidden" + }, + "columns.sortable": { + label: "Sortable" + }, + "columns.resizable": { + label: "Resizable" + }, + "columns.wrap": { + label: "Wrap Text" + }, + "columns.type": { + label: "Renderer Type" + }, + "columns.pinned": { + label: "Pinned" + }, + "columns.summary": { + label: "Summary" + }, + "columns.prefix": { + label: "Prefix" + }, + "columns.link": { + label: "Primary Link" + }, + "columns.action": { + label: "Click Action" + }, filter: { label: "Filter", helpText: "Filter conditions — same visual builder as the list toolbar, with field-type-aware operators and value inputs" @@ -798,6 +840,12 @@ export const enMetadataForms: NonNullable = { label: "Sort", helpText: "Default sort order" }, + "sort.field": { + label: "Field" + }, + "sort.order": { + label: "Direction" + }, searchableFields: { label: "Searchable Fields", helpText: "Field names available for quick search" @@ -847,6 +895,33 @@ export const enMetadataForms: NonNullable = { label: "Tabs", helpText: "In-view filter tabs — each tab applies its own filter rules" }, + "tabs.name": { + label: "Name" + }, + "tabs.label": { + label: "Label" + }, + "tabs.icon": { + label: "Icon" + }, + "tabs.view": { + label: "List View" + }, + "tabs.filter": { + label: "Filter" + }, + "tabs.order": { + label: "Display Order" + }, + "tabs.pinned": { + label: "Pinned" + }, + "tabs.isDefault": { + label: "Default Tab" + }, + "tabs.visible": { + label: "Visible" + }, appearance: { label: "Appearance", helpText: "allowedVisualizations: which renderers users may switch between" diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index 391b1e77ecc..03ececb5625 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -790,6 +790,48 @@ export const esESMetadataForms: NonNullable = label: "Columnas", helpText: "Columnas que mostrar (nombres de campo del objeto seleccionado)" }, + "columns.field": { + label: "Campo" + }, + "columns.label": { + label: "Etiqueta" + }, + "columns.width": { + label: "Ancho (px)" + }, + "columns.align": { + label: "Alineación" + }, + "columns.hidden": { + label: "Oculta" + }, + "columns.sortable": { + label: "Ordenable" + }, + "columns.resizable": { + label: "Redimensionable" + }, + "columns.wrap": { + label: "Ajustar texto" + }, + "columns.type": { + label: "Tipo de renderizador" + }, + "columns.pinned": { + label: "Fijada" + }, + "columns.summary": { + label: "Resumen" + }, + "columns.prefix": { + label: "Prefijo" + }, + "columns.link": { + label: "Enlace principal" + }, + "columns.action": { + label: "Acción al hacer clic" + }, filter: { label: "Filtro", helpText: "Condiciones de filtro — el mismo constructor visual que la barra de herramientas de la lista, con operadores y entradas de valor según el tipo de campo" @@ -798,6 +840,12 @@ export const esESMetadataForms: NonNullable = label: "Orden", helpText: "Orden predeterminado" }, + "sort.field": { + label: "Campo" + }, + "sort.order": { + label: "Dirección" + }, searchableFields: { label: "Campos buscables", helpText: "Nombres de campo disponibles para búsqueda rápida" @@ -847,6 +895,33 @@ export const esESMetadataForms: NonNullable = label: "Pestañas", helpText: "Pestañas de filtro en la vista: cada pestaña aplica sus propias reglas de filtro" }, + "tabs.name": { + label: "Nombre" + }, + "tabs.label": { + label: "Etiqueta" + }, + "tabs.icon": { + label: "Icono" + }, + "tabs.view": { + label: "Vista de lista" + }, + "tabs.filter": { + label: "Filtro" + }, + "tabs.order": { + label: "Orden de visualización" + }, + "tabs.pinned": { + label: "Fijada" + }, + "tabs.isDefault": { + label: "Pestaña predeterminada" + }, + "tabs.visible": { + label: "Visibilidad" + }, appearance: { label: "Apariencia", helpText: "allowedVisualizations: qué renderizadores pueden alternar los usuarios" diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index 32fa0f39c11..b0fe0305a02 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -790,6 +790,48 @@ export const jaJPMetadataForms: NonNullable = label: "列", helpText: "表示する列(選択オブジェクトのフィールド名)" }, + "columns.field": { + label: "フィールド" + }, + "columns.label": { + label: "表示名" + }, + "columns.width": { + label: "幅(ピクセル)" + }, + "columns.align": { + label: "配置" + }, + "columns.hidden": { + label: "非表示" + }, + "columns.sortable": { + label: "並び替え可能" + }, + "columns.resizable": { + label: "サイズ変更可" + }, + "columns.wrap": { + label: "テキストの折り返し" + }, + "columns.type": { + label: "レンダラータイプ" + }, + "columns.pinned": { + label: "固定" + }, + "columns.summary": { + label: "集計" + }, + "columns.prefix": { + label: "プレフィックス" + }, + "columns.link": { + label: "主リンク" + }, + "columns.action": { + label: "クリック時のアクション" + }, filter: { label: "フィルター", helpText: "フィルター条件 — リストツールバーと同じビジュアルビルダー。演算子と値入力はフィールドタイプに応じて変わります" @@ -798,6 +840,12 @@ export const jaJPMetadataForms: NonNullable = label: "並び替え", helpText: "既定の並び順" }, + "sort.field": { + label: "フィールド" + }, + "sort.order": { + label: "並び方向" + }, searchableFields: { label: "検索対象フィールド", helpText: "クイック検索で使用可能なフィールド名" @@ -847,6 +895,33 @@ export const jaJPMetadataForms: NonNullable = label: "タブ", helpText: "ビュー内フィルタータブ——各タブが独自のフィルタールールを適用" }, + "tabs.name": { + label: "名前" + }, + "tabs.label": { + label: "表示名" + }, + "tabs.icon": { + label: "アイコン" + }, + "tabs.view": { + label: "リストビュー" + }, + "tabs.filter": { + label: "フィルター" + }, + "tabs.order": { + label: "表示順" + }, + "tabs.pinned": { + label: "固定" + }, + "tabs.isDefault": { + label: "既定のタブ" + }, + "tabs.visible": { + label: "表示" + }, appearance: { label: "外観", helpText: "allowedVisualizations:ユーザーが切り替え可能なレンダラー" diff --git a/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts b/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts index 1d091717c59..6f03cde6c60 100644 --- a/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts +++ b/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts @@ -28,8 +28,8 @@ // // The population is DERIVED from the forms, not listed here, so a repeater // enumerated tomorrow is red on the day it lands rather than a month later. -// `view.columns` / `view.sort` / `view.tabs` enumerate no children and are -// therefore outside it — their titles are #17507's. +// `view.columns` / `view.sort` / `view.tabs` joined it with #19955: #17507 +// titled their item schemas, and `view.form.ts` now enumerates their children. import { describe, it, expect } from 'vitest'; @@ -136,17 +136,20 @@ describe('#17508 — the enumerated-repeater survey itself (controls before verd expect(CARRIERS).toContain('dashboard:header.actions'); const carrying = new Set(ROW_PROPERTIES.map((p) => p.type)); - for (const type of ['action', 'app', 'dashboard', 'dataset', 'field', 'flow', 'page', 'report', 'skill']) { + for (const type of ['action', 'app', 'dashboard', 'dataset', 'field', 'flow', 'page', 'report', 'skill', 'view']) { expect(carrying.has(type), `${type} enumerates a repeater's row properties`).toBe(true); } + // Lit — the three `view` repeaters are inside the population (#19955). + // `view.columns` is the union case: a string-array arm beside the + // object-array arm whose properties these are. + for (const carrier of ['view:columns', 'view:sort', 'view:tabs']) { + expect(CARRIERS, `${carrier} enumerates its row properties`).toContain(carrier); + } // Dark — forms that enumerate none contribute none. A walk that matched // everything, or nothing, cannot pass both halves. for (const type of ['agent', 'tool', 'hook', 'position']) { expect(carrying.has(type), `${type} enumerates no repeater row properties`).toBe(false); } - // Dark — `view.columns` / `view.sort` / `view.tabs` enumerate no children - // (their titles are #17507's), so `view` is outside this population. - expect(carrying.has('view'), 'view enumerates no repeater row properties yet').toBe(false); }); it('every enumerated row child declares a `label` — an omitted one silently becomes the English source', () => { diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index 789f6f28b69..fbbd8440702 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -790,6 +790,48 @@ export const zhCNMetadataForms: NonNullable = label: "列", helpText: "要展示的列(来自所选对象的字段名)" }, + "columns.field": { + label: "字段" + }, + "columns.label": { + label: "显示名称" + }, + "columns.width": { + label: "宽度(像素)" + }, + "columns.align": { + label: "对齐方式" + }, + "columns.hidden": { + label: "隐藏" + }, + "columns.sortable": { + label: "可排序" + }, + "columns.resizable": { + label: "可调整大小" + }, + "columns.wrap": { + label: "自动换行" + }, + "columns.type": { + label: "渲染器类型" + }, + "columns.pinned": { + label: "固定" + }, + "columns.summary": { + label: "汇总" + }, + "columns.prefix": { + label: "前缀" + }, + "columns.link": { + label: "主链接" + }, + "columns.action": { + label: "点击操作" + }, filter: { label: "筛选", helpText: "筛选条件——与列表工具栏相同的可视化构建器,运算符与取值输入随字段类型变化" @@ -798,6 +840,12 @@ export const zhCNMetadataForms: NonNullable = label: "排序", helpText: "默认排序方式" }, + "sort.field": { + label: "字段" + }, + "sort.order": { + label: "排序方向" + }, searchableFields: { label: "可搜索字段", helpText: "可用于快速搜索的字段名" @@ -847,6 +895,33 @@ export const zhCNMetadataForms: NonNullable = label: "标签页", helpText: "视图内筛选标签页——每个标签页应用各自的筛选规则" }, + "tabs.name": { + label: "名称" + }, + "tabs.label": { + label: "显示名称" + }, + "tabs.icon": { + label: "图标" + }, + "tabs.view": { + label: "列表视图" + }, + "tabs.filter": { + label: "筛选" + }, + "tabs.order": { + label: "显示顺序" + }, + "tabs.pinned": { + label: "固定" + }, + "tabs.isDefault": { + label: "默认标签页" + }, + "tabs.visible": { + label: "可见" + }, appearance: { label: "外观", helpText: "allowedVisualizations:允许用户切换的渲染器" From eddaba6c2801842b0036791454e4f75d909f78be Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 11:26:51 +0000 Subject: [PATCH 3/4] chore(changeset): patch for the view repeater row-property catalog keys Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .changeset/19955-view-repeater-row-i18n.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/19955-view-repeater-row-i18n.md diff --git a/.changeset/19955-view-repeater-row-i18n.md b/.changeset/19955-view-repeater-row-i18n.md new file mode 100644 index 00000000000..9877a5ef4aa --- /dev/null +++ b/.changeset/19955-view-repeater-row-i18n.md @@ -0,0 +1,12 @@ +--- +'@objectstack/spec': patch +'@objectstack/platform-objects': patch +--- + +Studio's view property panel names the columns of its Columns, Sort and Tabs tables in the author's language, not only in English + +Clause-②: no + +`view.form.ts` now enumerates the row properties of the `columns`, `sort` and `tabs` repeaters, each with a `label` equal to its item schema's own `.meta({ title })`, so `os i18n extract` emits a `metadataForms.view.fields` key for each row property. The `en`, `zh-CN`, `ja-JP` and `es-ES` platform catalogs carry those keys. The translated catalogs reuse the word they already use for the same concept where they have one (`Label` → 显示名称 / 表示名 / Etiqueta, `Direction` → 排序方向 / 並び方向 / Dirección). + +The row children declare no `type`, so each row input's widget is still derived from the schema. The view schema itself is unchanged. From b05a88136d1e1d0e0d34b7da5c0c45be220cc910 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 12:09:22 +0000 Subject: [PATCH 4/4] test(platform-objects): the translated-label positive control counts the 25 view repeater row labels Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .../object-lifecycle-panel-echo-decisions.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts b/packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts index 193f30de939..eb380076ec0 100644 --- a/packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts +++ b/packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts @@ -1111,8 +1111,10 @@ describe('#19403 round 10 — the verdicts, on the live bundles', () => { // control moves by exactly the number of rows that landed, in every // locale, which is the reading a per-locale count is for. 584 since // #19814: the view form's new `pagination` section, its label authored - // in all three locales. - expect(translated.length, `${locale} positive control`).toBe(584); + // in all three locales. 609 since #19955: the 25 row labels of the view + // form's `columns` / `sort` / `tabs` repeaters, authored in all three + // locales. + expect(translated.length, `${locale} positive control`).toBe(609); } // ⭐ DARK — the blindness, executable. On a synthetic two-locale catalog the // all-three predicate returns 0 while the per-locale one returns 1, so the