From bdd19b4d8b68fdc8024c1de669a4b00587c5e69d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 11:11:34 +0000 Subject: [PATCH 1/3] feat(spec): enumerate every repeater's row properties in the owning form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Studio renders a `type: 'repeater'` as a table whose column names come from the form's declared row children when there are any, and from the JSON Schema `items.properties[k].title` otherwise. `os i18n extract` only emits a `metadataForms..fields['.']` key for a DECLARED child, so an un-enumerated repeater had no catalog channel at all: its column heads read the English `.meta({ title })` in every locale. Enumerate all 112 row properties of the fifteen repeaters that declared none, each `label` equal to the item schema's own title so the extractor's English source and the schema title stay one string. No `type` on a row child — the row widgets stay schema-derived. `page.variables` and `object.fields.options` already enumerated their children without labels, which let the extractor's humanized fallback override the authored title (`variables.source` read "Source" against the schema's "Written By"); both now carry explicit labels. `view.columns` / `view.sort` / `view.tabs` are untitled and belong to #17507. `object.fields.options` stays the curated four-key subset its #3786 ledger entry declares. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude --- packages/spec/src/ai/skill.form.ts | 15 ++++++- packages/spec/src/automation/flow.form.ts | 46 +++++++++++++++++++- packages/spec/src/data/field.form.ts | 19 ++++++++- packages/spec/src/data/object.form.ts | 8 ++-- packages/spec/src/ui/action.form.ts | 30 ++++++++++++- packages/spec/src/ui/app.form.ts | 17 +++++++- packages/spec/src/ui/dashboard.form.ts | 52 ++++++++++++++++++++++- packages/spec/src/ui/dataset.form.ts | 39 ++++++++++++++++- packages/spec/src/ui/page.form.ts | 38 ++++++++++++++--- packages/spec/src/ui/report.form.ts | 37 +++++++++++++++- 10 files changed, 280 insertions(+), 21 deletions(-) diff --git a/packages/spec/src/ai/skill.form.ts b/packages/spec/src/ai/skill.form.ts index db86f76af6..1acdf4b80a 100644 --- a/packages/spec/src/ai/skill.form.ts +++ b/packages/spec/src/ai/skill.form.ts @@ -38,7 +38,20 @@ export const skillForm = defineForm({ fields: [ // `triggerPhrases` input removed with the key (#3896 close-out): phrases // were never matched; routing is triggerConditions + the agent allowlist. - { field: 'triggerConditions', type: 'repeater', helpText: 'Programmatic conditions (e.g., objectName == "case")' }, + { + field: 'triggerConditions', + type: 'repeater', + helpText: 'Programmatic conditions (e.g., objectName == "case")', + // 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: 'Context Field' }, + { field: 'operator', label: 'Operator' }, + { field: 'value', label: 'Value' }, + ], + }, ], }, ], diff --git a/packages/spec/src/automation/flow.form.ts b/packages/spec/src/automation/flow.form.ts index d86dffa941..82e9d32a32 100644 --- a/packages/spec/src/automation/flow.form.ts +++ b/packages/spec/src/automation/flow.form.ts @@ -35,14 +35,58 @@ export const flowForm = defineForm({ type: 'repeater', required: true, helpText: '⚠️ Consider using Flow Designer visual editor instead of JSON', + // 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: 'id', label: 'ID' }, + { field: 'type', label: 'Node Type' }, + { field: 'label', label: 'Label' }, + { field: 'config', label: 'Configuration' }, + { field: 'connectorConfig', label: 'Connector Action' }, + { field: 'position', label: 'Canvas Position' }, + { field: 'timeoutMs', label: 'Timeout (ms)' }, + { field: 'inputSchema', label: 'Input Schema' }, + { field: 'waitEventConfig', label: 'Wait Event' }, + { field: 'boundaryConfig', label: 'Boundary Event' }, + ], }, { field: 'edges', type: 'repeater', required: true, helpText: 'Connections between nodes — use Flow Designer for easier editing', + // 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: 'id', label: 'ID' }, + { field: 'source', label: 'From Node' }, + { field: 'target', label: 'To Node' }, + { field: 'condition', label: 'Condition' }, + { field: 'type', label: 'Connection Type' }, + { field: 'label', label: 'Label' }, + { field: 'isDefault', label: 'Default Path' }, + ], + }, + { + field: 'variables', + type: 'repeater', + helpText: 'Flow variables (inputs/outputs)', + // 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: 'type', label: 'Type' }, + { field: 'isInput', label: 'Input' }, + { field: 'isOutput', label: 'Output' }, + { field: 'defaultValue', label: 'Default Value' }, + ], }, - { field: 'variables', type: 'repeater', helpText: 'Flow variables (inputs/outputs)' }, ], }, { diff --git a/packages/spec/src/data/field.form.ts b/packages/spec/src/data/field.form.ts index 9c3e123dc9..beb8cb02d8 100644 --- a/packages/spec/src/data/field.form.ts +++ b/packages/spec/src/data/field.form.ts @@ -73,7 +73,24 @@ export const fieldForm = defineForm({ { field: 'precision', visibleWhen: "data.type == 'currency' || data.type == 'number'", helpText: 'Decimal places (e.g., 2 for $10.50)' }, { field: 'scale', visibleWhen: "data.type == 'number'", helpText: 'Number of decimal digits' }, // Select field options - { field: 'options', type: 'repeater', visibleWhen: "data.type == 'select' || data.type == 'multiselect'", helpText: 'Available options (label/value pairs)' }, + { + field: 'options', + type: 'repeater', + visibleWhen: "data.type == 'select' || data.type == 'multiselect'", + helpText: 'Available options (label/value pairs)', + // 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: 'label', label: 'Label' }, + { field: 'value', label: 'Value' }, + { field: 'description', label: 'Description' }, + { field: 'color', label: 'Color' }, + { field: 'default', label: 'Default' }, + { field: 'visibleWhen', label: 'Visible When' }, + ], + }, // Reference field options { field: 'reference', widget: 'ref:object', visibleWhen: "data.type == 'lookup' || data.type == 'master_detail'", helpText: 'Referenced object name' }, // Two declarations of one key, with disjoint `visibleWhen` (#11410) — diff --git a/packages/spec/src/data/object.form.ts b/packages/spec/src/data/object.form.ts index 338d795fff..1a4ceed2d9 100644 --- a/packages/spec/src/data/object.form.ts +++ b/packages/spec/src/data/object.form.ts @@ -215,10 +215,10 @@ export const objectForm = defineForm({ helpText: 'Available choices', visibleWhen: "data.type in ['select','multiselect','radio','checkboxes']", fields: [ - { field: 'label', type: 'text', required: true }, - { field: 'value', type: 'text', required: true }, - { field: 'color', type: 'color' }, - { field: 'description', type: 'text' }, + { field: 'label', label: 'Label', type: 'text', required: true }, + { field: 'value', label: 'Value', type: 'text', required: true }, + { field: 'color', label: 'Color', type: 'color' }, + { field: 'description', label: 'Description', type: 'text' }, ], }, diff --git a/packages/spec/src/ui/action.form.ts b/packages/spec/src/ui/action.form.ts index 5292f80fdf..6356c9d4df 100644 --- a/packages/spec/src/ui/action.form.ts +++ b/packages/spec/src/ui/action.form.ts @@ -51,7 +51,35 @@ export const actionForm = defineForm({ { field: 'memoryMb', type: 'number', helpText: 'Per-invocation memory cap (MB, max 256)' }, ], }, - { field: 'params', type: 'repeater', helpText: 'User input parameters (show form before executing)' }, + { + field: 'params', + type: 'repeater', + helpText: 'User input parameters (show form before executing)', + // 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: 'field', label: 'Field' }, + { field: 'objectOverride', label: 'Object Override' }, + { field: 'label', label: 'Label' }, + { field: 'type', label: 'Type' }, + { field: 'required', label: 'Required' }, + { field: 'options', label: 'Options' }, + { field: 'placeholder', label: 'Placeholder' }, + { field: 'helpText', label: 'Help Text' }, + { field: 'defaultValue', label: 'Default Value' }, + { field: 'multiple', label: 'Multiple' }, + { field: 'accept', label: 'Accepted Types' }, + { field: 'maxSize', label: 'Max Size (bytes)' }, + { field: 'reference', label: 'Reference Object' }, + { field: 'defaultFromRow', label: 'Default From Row' }, + { field: 'carryOver', label: 'Carry Over' }, + { field: 'visible', label: 'Visible When' }, + { field: 'requiresFeature', label: 'Requires Feature' }, + ], + }, { field: 'confirmText', helpText: 'Confirmation message (e.g., "Are you sure?")' }, { field: 'successMessage', helpText: 'Success message after completion' }, { field: 'refreshAfter', helpText: 'Refresh the list/page after action completes' }, diff --git a/packages/spec/src/ui/app.form.ts b/packages/spec/src/ui/app.form.ts index ebcf3bd85e..659736c8e7 100644 --- a/packages/spec/src/ui/app.form.ts +++ b/packages/spec/src/ui/app.form.ts @@ -45,7 +45,22 @@ export const appForm = defineForm({ description: 'Sidebar items and area grouping.', fields: [ { field: 'navigation', type: 'composite', helpText: 'Nav tree — recursive structure' }, - { field: 'areas', type: 'repeater', helpText: 'Group items into collapsible areas' }, + { + field: 'areas', + type: 'repeater', + helpText: 'Group items into collapsible areas', + // 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: 'id', label: 'ID' }, + { field: 'label', label: 'Label' }, + { field: 'icon', label: 'Icon' }, + { field: 'description', label: 'Description' }, + { field: 'navigation', label: 'Navigation' }, + ], + }, // `homePageId` removed: tombstoned in 17.0.0 (#4667, #4709, ADR-0049) — // an app's landing page IS its first `navigation` item by `order`, and // the root landing follows `isDefault`. Reorder the nav instead. diff --git a/packages/spec/src/ui/dashboard.form.ts b/packages/spec/src/ui/dashboard.form.ts index 4e3489c4c2..3735de9005 100644 --- a/packages/spec/src/ui/dashboard.form.ts +++ b/packages/spec/src/ui/dashboard.form.ts @@ -63,7 +63,35 @@ export const dashboardForm = defineForm({ label: 'Widgets', description: 'Cards and charts placed on the grid.', fields: [ - { field: 'widgets', type: 'repeater', required: true, helpText: 'Dashboard widgets with position and sizing' }, + { + field: 'widgets', + type: 'repeater', + required: true, + helpText: 'Dashboard widgets with position and sizing', + // 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: 'id', label: 'Widget ID' }, + { field: 'title', label: 'Title' }, + { field: 'description', label: 'Description' }, + { field: 'type', label: 'Visualization Type' }, + { field: 'chartConfig', label: 'Chart Configuration' }, + { field: 'colorVariant', label: 'Color Variant' }, + { field: 'requiresObject', label: 'Requires Object' }, + { field: 'requiresService', label: 'Requires Service' }, + { field: 'filter', label: 'Filter' }, + { field: 'compareTo', label: 'Compare To' }, + { field: 'dataset', label: 'Dataset' }, + { field: 'dimensions', label: 'Dimensions' }, + { field: 'values', label: 'Values' }, + { field: 'layout', label: 'Layout' }, + { field: 'options', label: 'Options' }, + { field: 'filterBindings', label: 'Filter Bindings' }, + { field: 'suppressWarnings', label: 'Suppress Warnings' }, + ], + }, ], }, { @@ -73,7 +101,27 @@ export const dashboardForm = defineForm({ collapsed: true, fields: [ { field: 'dateRange', type: 'composite', helpText: 'Default date range selector' }, - { field: 'globalFilters', type: 'repeater', helpText: 'Filters applied to all widgets' }, + { + field: 'globalFilters', + type: 'repeater', + helpText: 'Filters applied to all widgets', + // 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: 'field', label: 'Field' }, + { field: 'object', label: 'Object' }, + { field: 'label', label: 'Label' }, + { field: 'type', label: 'Input Type' }, + { field: 'options', label: 'Options' }, + { field: 'optionsFrom', label: 'Options From' }, + { field: 'defaultValue', label: 'Default Value' }, + { field: 'scope', label: 'Scope' }, + { field: 'targetWidgets', label: 'Target Widgets' }, + ], + }, ], }, { diff --git a/packages/spec/src/ui/dataset.form.ts b/packages/spec/src/ui/dataset.form.ts index 7ac53ab1aa..c0701a4610 100644 --- a/packages/spec/src/ui/dataset.form.ts +++ b/packages/spec/src/ui/dataset.form.ts @@ -48,7 +48,23 @@ export const datasetForm = defineForm({ label: 'Dimensions', description: 'Groupable axes. Use a base field, or `relationship.field` (e.g. account.region) for a relationship included above.', fields: [ - { field: 'dimensions', type: 'repeater', required: true, helpText: 'Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity' }, + { + field: 'dimensions', + type: 'repeater', + required: true, + helpText: 'Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity', + // 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: 'field', label: 'Field' }, + { field: 'type', label: 'Type' }, + { field: 'dateGranularity', label: 'Date Granularity' }, + ], + }, ], }, { @@ -56,7 +72,26 @@ export const datasetForm = defineForm({ label: 'Measures', description: 'Aggregatable values defined once and referenced by name. A measure is sum/avg/count/… of a field; a derived measure combines other measures (ratio/sum/difference/product). Measure-scoped filters and derived ops are edited per-row in the dataset designer.', fields: [ - { field: 'measures', type: 'repeater', required: true, helpText: 'Each: name, aggregate, field (optional for count), and display format/currency' }, + { + field: 'measures', + type: 'repeater', + required: true, + helpText: 'Each: name, aggregate, field (optional for count), and display format/currency', + // 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: 'aggregate', label: 'Aggregate' }, + { field: 'field', label: 'Field' }, + { field: 'filter', label: 'Filter' }, + { field: 'format', label: 'Format' }, + { field: 'currency', label: 'Currency' }, + { field: 'derived', label: 'Derived From' }, + ], + }, ], }, ], diff --git a/packages/spec/src/ui/page.form.ts b/packages/spec/src/ui/page.form.ts index ec4b09f098..69f160c7bb 100644 --- a/packages/spec/src/ui/page.form.ts +++ b/packages/spec/src/ui/page.form.ts @@ -62,10 +62,10 @@ export const pageForm = defineForm({ // hand. Reconciled against PageVariableSchema by // metadata-form-zod-reconciliation.test.ts (#3786). fields: [ - { field: 'name', required: true, helpText: 'Variable name — exposed to expressions as `page.`' }, - { field: 'type', helpText: 'Value type' }, - { field: 'defaultValue', helpText: 'Initial value (defaults to a type-appropriate empty value)' }, - { field: 'source', widget: 'ref:component', helpText: 'Component (by id) that writes this variable — e.g. an element:record_picker' }, + { field: 'name', label: 'Name', required: true, helpText: 'Variable name — exposed to expressions as `page.`' }, + { field: 'type', label: 'Type', helpText: 'Value type' }, + { field: 'defaultValue', label: 'Default Value', helpText: 'Initial value (defaults to a type-appropriate empty value)' }, + { field: 'source', label: 'Written By', widget: 'ref:component', helpText: 'Component (by id) that writes this variable — e.g. an element:record_picker' }, ], }, ], @@ -81,7 +81,20 @@ export const pageForm = defineForm({ // layout, slotted pages compose via `slots`, and list pages ignore regions // entirely. Marking it required made the form unsatisfiable when no region // editor was rendered (the metadata form can't yet edit a region tree). - { field: 'regions', type: 'repeater', helpText: 'Layout regions (header, main, sidebar, footer) with components' }, + { + field: 'regions', + type: 'repeater', + helpText: 'Layout regions (header, main, sidebar, footer) with components', + // 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: 'Region' }, + { field: 'width', label: 'Width' }, + { field: 'components', label: 'Components' }, + ], + }, ], }, { @@ -114,7 +127,20 @@ export const pageForm = defineForm({ // 'source'` tells the picker which object's fields to offer. { field: 'columns', widget: 'field-multi', dependsOn: 'source', helpText: 'Columns to show — defined directly on the page (blank = all object fields)' }, { field: 'filterBy', widget: 'filter-builder', dependsOn: 'source', helpText: 'Always-on base filter for the page — same visual builder as the list toolbar.' }, - { field: 'sort', type: 'repeater', dependsOn: 'source', helpText: 'Default sort order for the page, defined directly on the page.' }, + { + field: 'sort', + type: 'repeater', + dependsOn: 'source', + helpText: 'Default sort order for the page, defined directly on the page.', + // 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: 'levels', helpText: 'Hierarchy levels to display (tree-like sources)' }, // ── Appearance ── { field: 'appearance', type: 'composite', disclosure: 'popover', helpText: 'Allowed visualizations (Grid / Kanban / Calendar / …) and description visibility' }, diff --git a/packages/spec/src/ui/report.form.ts b/packages/spec/src/ui/report.form.ts index 9118d9d0d8..c4239302f4 100644 --- a/packages/spec/src/ui/report.form.ts +++ b/packages/spec/src/ui/report.form.ts @@ -45,7 +45,19 @@ export const reportForm = defineForm({ // `DatasetSelection.order` has always existed on the wire, but a report // author had no channel to reach it. Optional — a selected time // dimension is chronological by default without declaring anything. - { field: 'order', type: 'repeater', helpText: 'Sort keys, most significant first (a rows/columns dimension or a values measure). Time dimensions are chronological by default.' }, + { + field: 'order', + type: 'repeater', + helpText: 'Sort keys, most significant first (a rows/columns dimension or a values measure). Time dimensions are chronological by default.', + // 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: 'by', label: 'Order By' }, + { field: 'direction', label: 'Direction' }, + ], + }, { field: 'drilldown', helpText: 'Click an aggregated row/cell to open the underlying records' }, ], }, @@ -54,7 +66,28 @@ export const reportForm = defineForm({ description: 'Additional dataset-bound blocks stacked into a single report (joined reports only).', visibleWhen: "data.type == 'joined'", fields: [ - { field: 'blocks', type: 'repeater', helpText: 'Dataset-bound sub-reports (joined report only)' }, + { + field: 'blocks', + type: 'repeater', + helpText: 'Dataset-bound sub-reports (joined report only)', + // 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: 'description', label: 'Description' }, + { field: 'type', label: 'Block Type' }, + { field: 'chart', label: 'Chart' }, + { field: 'dataset', label: 'Dataset' }, + { field: 'rows', label: 'Rows' }, + { field: 'columns', label: 'Columns' }, + { field: 'values', label: 'Values' }, + { field: 'runtimeFilter', label: 'Runtime Filter' }, + { field: 'order', label: 'Order' }, + ], + }, ], }, { From 79ae7e0bf5a663d60435aeee6264d9b93624165d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 11:31:32 +0000 Subject: [PATCH 2/3] feat(platform-objects): name every enumerated repeater row property in all four catalogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os i18n extract` now emits a `metadataForms..fields['.']` key for each of the 112 row properties the forms enumerate, and this lands the `en` source plus hand-authored `zh-CN` / `ja-JP` / `es-ES` copy for every one of them — the Studio property panel's repeater column heads read in the author's own language instead of English in every locale. Each term reuses the word the catalogs already use for the concept: `Label` is the dominant 显示名称 / 表示名 / Etiqueta, `Filter` 筛选 / フィルター / Filtro, `Widget` 组件 / ウィジェット / Widget, `Timeout (ms)` the existing 超时(毫秒)/ タイムアウト(ms)/ Tiempo de espera (ms), and `field.options.*` mirrors its `object.fields.options.*` twin verbatim, `Color de opción` included. A bare `ID` is qualified by its row the way `Reference ID` already is (节点 ID / ノード ID / ID de nodo). `page.variables.source`'s English source moves from the extractor's humanized "Source" to the schema's authored "Written By", so its three translations are re-authored with it; every other leaf here is new. The pin is `repeater-row-properties.test.ts`, in the shape `dashboard-header-children.test.ts` established for #17227 and with its last test: a translated leaf is never a copy of its `en` source. Its population is derived from the forms, so a repeater enumerated later is red on the day it lands. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude --- .../en.metadata-forms.generated.ts | 338 +++++++++++++++++- .../es-ES.metadata-forms.generated.ts | 338 +++++++++++++++++- .../ja-JP.metadata-forms.generated.ts | 338 +++++++++++++++++- .../repeater-row-properties.test.ts | 193 ++++++++++ .../zh-CN.metadata-forms.generated.ts | 338 +++++++++++++++++- 5 files changed, 1541 insertions(+), 4 deletions(-) create mode 100644 packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts 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 c61a9c8da3..b8a2468891 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 @@ -420,6 +420,24 @@ export const enMetadataForms: NonNullable = { label: "Options", helpText: "Available options (label/value pairs)" }, + "options.label": { + label: "Label" + }, + "options.value": { + label: "Value" + }, + "options.description": { + label: "Description" + }, + "options.color": { + label: "Color" + }, + "options.default": { + label: "Default" + }, + "options.visibleWhen": { + label: "Visible When" + }, reference: { label: "Reference", helpText: "Referenced object name" @@ -817,13 +835,22 @@ export const enMetadataForms: NonNullable = { helpText: "Initial value (defaults to a type-appropriate empty value)" }, "variables.source": { - label: "Source", + label: "Written By", helpText: "Component (by id) that writes this variable — e.g. an element:record_picker" }, regions: { label: "Regions", helpText: "Layout regions (header, main, sidebar, footer) with components" }, + "regions.name": { + label: "Region" + }, + "regions.width": { + label: "Width" + }, + "regions.components": { + label: "Components" + }, interfaceConfig: { label: "Interface Config", helpText: "The page IS the view: source picks the object, columns/filterBy are defined directly here; appearance.allowedVisualizations whitelists renderers (one entry = locked); userActions toggles the toolbar." @@ -844,6 +871,12 @@ export const enMetadataForms: NonNullable = { label: "Sort", helpText: "Default sort order for the page, defined directly on the page." }, + "interfaceConfig.sort.field": { + label: "Field" + }, + "interfaceConfig.sort.order": { + label: "Direction" + }, "interfaceConfig.levels": { label: "Levels", helpText: "Hierarchy levels to display (tree-like sources)" @@ -974,6 +1007,57 @@ export const enMetadataForms: NonNullable = { label: "Widgets", helpText: "Dashboard widgets with position and sizing" }, + "widgets.id": { + label: "Widget ID" + }, + "widgets.title": { + label: "Title" + }, + "widgets.description": { + label: "Description" + }, + "widgets.type": { + label: "Visualization Type" + }, + "widgets.chartConfig": { + label: "Chart Configuration" + }, + "widgets.colorVariant": { + label: "Color Variant" + }, + "widgets.requiresObject": { + label: "Requires Object" + }, + "widgets.requiresService": { + label: "Requires Service" + }, + "widgets.filter": { + label: "Filter" + }, + "widgets.compareTo": { + label: "Compare To" + }, + "widgets.dataset": { + label: "Dataset" + }, + "widgets.dimensions": { + label: "Dimensions" + }, + "widgets.values": { + label: "Values" + }, + "widgets.layout": { + label: "Layout" + }, + "widgets.options": { + label: "Options" + }, + "widgets.filterBindings": { + label: "Filter Bindings" + }, + "widgets.suppressWarnings": { + label: "Suppress Warnings" + }, dateRange: { label: "Date Range", helpText: "Default date range selector" @@ -981,6 +1065,36 @@ export const enMetadataForms: NonNullable = { globalFilters: { label: "Global Filters", helpText: "Filters applied to all widgets" + }, + "globalFilters.name": { + label: "Name" + }, + "globalFilters.field": { + label: "Field" + }, + "globalFilters.object": { + label: "Object" + }, + "globalFilters.label": { + label: "Label" + }, + "globalFilters.type": { + label: "Input Type" + }, + "globalFilters.options": { + label: "Options" + }, + "globalFilters.optionsFrom": { + label: "Options From" + }, + "globalFilters.defaultValue": { + label: "Default Value" + }, + "globalFilters.scope": { + label: "Scope" + }, + "globalFilters.targetWidgets": { + label: "Target Widgets" } } }, @@ -1038,6 +1152,21 @@ export const enMetadataForms: NonNullable = { label: "Areas", helpText: "Group items into collapsible areas" }, + "areas.id": { + label: "ID" + }, + "areas.label": { + label: "Label" + }, + "areas.icon": { + label: "Icon" + }, + "areas.description": { + label: "Description" + }, + "areas.navigation": { + label: "Navigation" + }, defaultAgent: { label: "Default Agent", helpText: "Platform agent for the ambient assistant ('ask' by default; 'build' for authoring surfaces)" @@ -1133,6 +1262,60 @@ export const enMetadataForms: NonNullable = { label: "Params", helpText: "User input parameters (show form before executing)" }, + "params.name": { + label: "Name" + }, + "params.field": { + label: "Field" + }, + "params.objectOverride": { + label: "Object Override" + }, + "params.label": { + label: "Label" + }, + "params.type": { + label: "Type" + }, + "params.required": { + label: "Required" + }, + "params.options": { + label: "Options" + }, + "params.placeholder": { + label: "Placeholder" + }, + "params.helpText": { + label: "Help Text" + }, + "params.defaultValue": { + label: "Default Value" + }, + "params.multiple": { + label: "Multiple" + }, + "params.accept": { + label: "Accepted Types" + }, + "params.maxSize": { + label: "Max Size (bytes)" + }, + "params.reference": { + label: "Reference Object" + }, + "params.defaultFromRow": { + label: "Default From Row" + }, + "params.carryOver": { + label: "Carry Over" + }, + "params.visible": { + label: "Visible When" + }, + "params.requiresFeature": { + label: "Requires Feature" + }, confirmText: { label: "Confirm Text", helpText: "Confirmation message (e.g., \"Are you sure?\")" @@ -1234,6 +1417,12 @@ export const enMetadataForms: NonNullable = { label: "Order", helpText: "Sort keys, most significant first (a rows/columns dimension or a values measure). Time dimensions are chronological by default." }, + "order.by": { + label: "Order By" + }, + "order.direction": { + label: "Direction" + }, drilldown: { label: "Drilldown", helpText: "Click an aggregated row/cell to open the underlying records" @@ -1242,6 +1431,39 @@ export const enMetadataForms: NonNullable = { label: "Blocks", helpText: "Dataset-bound sub-reports (joined report only)" }, + "blocks.name": { + label: "Name" + }, + "blocks.label": { + label: "Label" + }, + "blocks.description": { + label: "Description" + }, + "blocks.type": { + label: "Block Type" + }, + "blocks.chart": { + label: "Chart" + }, + "blocks.dataset": { + label: "Dataset" + }, + "blocks.rows": { + label: "Rows" + }, + "blocks.columns": { + label: "Columns" + }, + "blocks.values": { + label: "Values" + }, + "blocks.runtimeFilter": { + label: "Runtime Filter" + }, + "blocks.order": { + label: "Order" + }, runtimeFilter: { label: "Runtime Filter", helpText: "Render-time scope filter, ANDed at query time" @@ -1302,9 +1524,48 @@ export const enMetadataForms: NonNullable = { label: "Dimensions", helpText: "Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity" }, + "dimensions.name": { + label: "Name" + }, + "dimensions.label": { + label: "Label" + }, + "dimensions.field": { + label: "Field" + }, + "dimensions.type": { + label: "Type" + }, + "dimensions.dateGranularity": { + label: "Date Granularity" + }, measures: { label: "Measures", helpText: "Each: name, aggregate, field (optional for count), and display format/currency" + }, + "measures.name": { + label: "Name" + }, + "measures.label": { + label: "Label" + }, + "measures.aggregate": { + label: "Aggregate" + }, + "measures.field": { + label: "Field" + }, + "measures.filter": { + label: "Filter" + }, + "measures.format": { + label: "Format" + }, + "measures.currency": { + label: "Currency" + }, + "measures.derived": { + label: "Derived From" } } }, @@ -1345,14 +1606,80 @@ export const enMetadataForms: NonNullable = { label: "Nodes", helpText: "⚠️ Consider using Flow Designer visual editor instead of JSON" }, + "nodes.id": { + label: "ID" + }, + "nodes.type": { + label: "Node Type" + }, + "nodes.label": { + label: "Label" + }, + "nodes.config": { + label: "Configuration" + }, + "nodes.connectorConfig": { + label: "Connector Action" + }, + "nodes.position": { + label: "Canvas Position" + }, + "nodes.timeoutMs": { + label: "Timeout (ms)" + }, + "nodes.inputSchema": { + label: "Input Schema" + }, + "nodes.waitEventConfig": { + label: "Wait Event" + }, + "nodes.boundaryConfig": { + label: "Boundary Event" + }, edges: { label: "Edges", helpText: "Connections between nodes — use Flow Designer for easier editing" }, + "edges.id": { + label: "ID" + }, + "edges.source": { + label: "From Node" + }, + "edges.target": { + label: "To Node" + }, + "edges.condition": { + label: "Condition" + }, + "edges.type": { + label: "Connection Type" + }, + "edges.label": { + label: "Label" + }, + "edges.isDefault": { + label: "Default Path" + }, variables: { label: "Variables", helpText: "Flow variables (inputs/outputs)" }, + "variables.name": { + label: "Name" + }, + "variables.type": { + label: "Type" + }, + "variables.isInput": { + label: "Input" + }, + "variables.isOutput": { + label: "Output" + }, + "variables.defaultValue": { + label: "Default Value" + }, status: { label: "Status", helpText: "Deployment status: draft → active → obsolete" @@ -1713,6 +2040,15 @@ export const enMetadataForms: NonNullable = { triggerConditions: { label: "Trigger Conditions", helpText: "Programmatic conditions (e.g., objectName == \"case\")" + }, + "triggerConditions.field": { + label: "Context Field" + }, + "triggerConditions.operator": { + label: "Operator" + }, + "triggerConditions.value": { + label: "Value" } } } 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 c7f2314cd0..e7fee7c087 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 @@ -420,6 +420,24 @@ export const esESMetadataForms: NonNullable = label: "Opciones", helpText: "Opciones disponibles (pares label/value)" }, + "options.label": { + label: "Etiqueta" + }, + "options.value": { + label: "Valor" + }, + "options.description": { + label: "Descripción" + }, + "options.color": { + label: "Color de opción" + }, + "options.default": { + label: "Opción predeterminada" + }, + "options.visibleWhen": { + label: "Condición de visibilidad" + }, reference: { label: "Referencia", helpText: "Nombre del objeto referenciado" @@ -817,13 +835,22 @@ export const esESMetadataForms: NonNullable = helpText: "Valor inicial (por defecto, un valor vacío según el tipo)" }, "variables.source": { - label: "Origen", + label: "Escrito por", helpText: "Componente (por id) que escribe esta variable — p. ej. un element:record_picker" }, regions: { label: "Regiones", helpText: "Regiones de diseño (header, main, sidebar, footer) con componentes" }, + "regions.name": { + label: "Región" + }, + "regions.width": { + label: "Ancho" + }, + "regions.components": { + label: "Componentes" + }, interfaceConfig: { label: "Interface Config", helpText: "The page IS the view: source picks the object, columns/filterBy are defined directly here; appearance.allowedVisualizations whitelists renderers (one entry = locked); userActions toggles the toolbar." @@ -844,6 +871,12 @@ export const esESMetadataForms: NonNullable = label: "Sort", helpText: "Default sort order for the page, defined directly on the page." }, + "interfaceConfig.sort.field": { + label: "Campo" + }, + "interfaceConfig.sort.order": { + label: "Dirección" + }, "interfaceConfig.levels": { label: "Levels", helpText: "Hierarchy levels to display (tree-like sources)" @@ -974,6 +1007,57 @@ export const esESMetadataForms: NonNullable = label: "Widgets del panel", helpText: "Widgets del panel con posición y tamaño" }, + "widgets.id": { + label: "ID del widget" + }, + "widgets.title": { + label: "Título" + }, + "widgets.description": { + label: "Descripción" + }, + "widgets.type": { + label: "Tipo de visualización" + }, + "widgets.chartConfig": { + label: "Configuración del gráfico" + }, + "widgets.colorVariant": { + label: "Variante de color" + }, + "widgets.requiresObject": { + label: "Requiere objeto" + }, + "widgets.requiresService": { + label: "Requiere servicio" + }, + "widgets.filter": { + label: "Filtro" + }, + "widgets.compareTo": { + label: "Comparar con" + }, + "widgets.dataset": { + label: "Conjunto de datos" + }, + "widgets.dimensions": { + label: "Dimensiones" + }, + "widgets.values": { + label: "Medidas" + }, + "widgets.layout": { + label: "Diseño" + }, + "widgets.options": { + label: "Opciones" + }, + "widgets.filterBindings": { + label: "Vínculos de filtro" + }, + "widgets.suppressWarnings": { + label: "Advertencias suprimidas" + }, dateRange: { label: "Rango de fechas", helpText: "Selector predeterminado de intervalo de fechas" @@ -981,6 +1065,36 @@ export const esESMetadataForms: NonNullable = globalFilters: { label: "Filtros globales", helpText: "Filtros aplicados a todos los widgets" + }, + "globalFilters.name": { + label: "Nombre" + }, + "globalFilters.field": { + label: "Campo" + }, + "globalFilters.object": { + label: "Objeto" + }, + "globalFilters.label": { + label: "Etiqueta" + }, + "globalFilters.type": { + label: "Tipo de entrada" + }, + "globalFilters.options": { + label: "Opciones" + }, + "globalFilters.optionsFrom": { + label: "Origen de las opciones" + }, + "globalFilters.defaultValue": { + label: "Valor predeterminado" + }, + "globalFilters.scope": { + label: "Ámbito" + }, + "globalFilters.targetWidgets": { + label: "Widgets de destino" } } }, @@ -1038,6 +1152,21 @@ export const esESMetadataForms: NonNullable = label: "Áreas", helpText: "Agrupa elementos en áreas plegables" }, + "areas.id": { + label: "ID de área" + }, + "areas.label": { + label: "Etiqueta" + }, + "areas.icon": { + label: "Icono" + }, + "areas.description": { + label: "Descripción" + }, + "areas.navigation": { + label: "Navegación" + }, defaultAgent: { label: "Agente predeterminado", helpText: "Agente de IA para el botón de asistente ambiental" @@ -1133,6 +1262,60 @@ export const esESMetadataForms: NonNullable = label: "Parámetros", helpText: "Parámetros de entrada de usuario (muestra el formulario antes de ejecutar)" }, + "params.name": { + label: "Nombre" + }, + "params.field": { + label: "Campo" + }, + "params.objectOverride": { + label: "Sobrescritura de objeto" + }, + "params.label": { + label: "Etiqueta" + }, + "params.type": { + label: "Tipo" + }, + "params.required": { + label: "Obligatorio" + }, + "params.options": { + label: "Opciones" + }, + "params.placeholder": { + label: "Marcador de posición" + }, + "params.helpText": { + label: "Texto de ayuda" + }, + "params.defaultValue": { + label: "Valor predeterminado" + }, + "params.multiple": { + label: "Selección múltiple" + }, + "params.accept": { + label: "Tipos aceptados" + }, + "params.maxSize": { + label: "Tamaño máximo (bytes)" + }, + "params.reference": { + label: "Objeto de referencia" + }, + "params.defaultFromRow": { + label: "Predeterminado desde la fila" + }, + "params.carryOver": { + label: "Heredar de la fila" + }, + "params.visible": { + label: "Condición de visibilidad" + }, + "params.requiresFeature": { + label: "Requiere función" + }, confirmText: { label: "Texto de confirmación", helpText: "Mensaje de confirmación (p. ej., \"Are you sure?\")" @@ -1234,6 +1417,12 @@ export const esESMetadataForms: NonNullable = label: "Orden", helpText: "Claves de ordenación, la más significativa primero (una dimensión de rows/columns o una medida de values). Las dimensiones de tiempo son cronológicas de forma predeterminada." }, + "order.by": { + label: "Ordenar por" + }, + "order.direction": { + label: "Dirección" + }, drilldown: { label: "Drilldown", helpText: "Click an aggregated row/cell to open the underlying records" @@ -1242,6 +1431,39 @@ export const esESMetadataForms: NonNullable = label: "Bloques", helpText: "Une varios objetos (solo informe joined)" }, + "blocks.name": { + label: "Nombre" + }, + "blocks.label": { + label: "Etiqueta" + }, + "blocks.description": { + label: "Descripción" + }, + "blocks.type": { + label: "Tipo de bloque" + }, + "blocks.chart": { + label: "Gráfico" + }, + "blocks.dataset": { + label: "Conjunto de datos" + }, + "blocks.rows": { + label: "Filas" + }, + "blocks.columns": { + label: "Columnas" + }, + "blocks.values": { + label: "Medidas" + }, + "blocks.runtimeFilter": { + label: "Filtro en tiempo de ejecución" + }, + "blocks.order": { + label: "Orden" + }, runtimeFilter: { label: "Runtime Filter", helpText: "Render-time scope filter, ANDed at query time" @@ -1302,9 +1524,48 @@ export const esESMetadataForms: NonNullable = label: "Dimensions", helpText: "Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity" }, + "dimensions.name": { + label: "Nombre" + }, + "dimensions.label": { + label: "Etiqueta" + }, + "dimensions.field": { + label: "Campo" + }, + "dimensions.type": { + label: "Tipo" + }, + "dimensions.dateGranularity": { + label: "Granularidad de fecha" + }, measures: { label: "Measures", helpText: "Cada medida: nombre, agregación, campo (opcional para count) y formato de visualización/moneda" + }, + "measures.name": { + label: "Nombre" + }, + "measures.label": { + label: "Etiqueta" + }, + "measures.aggregate": { + label: "Agregación" + }, + "measures.field": { + label: "Campo" + }, + "measures.filter": { + label: "Filtro" + }, + "measures.format": { + label: "Formato" + }, + "measures.currency": { + label: "Moneda" + }, + "measures.derived": { + label: "Derivada de" } } }, @@ -1345,14 +1606,80 @@ export const esESMetadataForms: NonNullable = label: "Nodos", helpText: "⚠️ Considera usar el editor visual Flow Designer en lugar de JSON" }, + "nodes.id": { + label: "ID de nodo" + }, + "nodes.type": { + label: "Tipo de nodo" + }, + "nodes.label": { + label: "Etiqueta" + }, + "nodes.config": { + label: "Configuración" + }, + "nodes.connectorConfig": { + label: "Acción del conector" + }, + "nodes.position": { + label: "Posición en el lienzo" + }, + "nodes.timeoutMs": { + label: "Tiempo de espera (ms)" + }, + "nodes.inputSchema": { + label: "Esquema de entrada" + }, + "nodes.waitEventConfig": { + label: "Evento de espera" + }, + "nodes.boundaryConfig": { + label: "Evento de límite" + }, edges: { label: "Conexiones", helpText: "Conexiones entre nodos — usa Flow Designer para editar más fácilmente" }, + "edges.id": { + label: "ID de conexión" + }, + "edges.source": { + label: "Nodo de origen" + }, + "edges.target": { + label: "Nodo de destino" + }, + "edges.condition": { + label: "Condición" + }, + "edges.type": { + label: "Tipo de conexión" + }, + "edges.label": { + label: "Etiqueta" + }, + "edges.isDefault": { + label: "Ruta predeterminada" + }, variables: { label: "Variables de flujo", helpText: "Variables de flujo (inputs/outputs)" }, + "variables.name": { + label: "Nombre" + }, + "variables.type": { + label: "Tipo" + }, + "variables.isInput": { + label: "Entrada" + }, + "variables.isOutput": { + label: "Salida" + }, + "variables.defaultValue": { + label: "Valor predeterminado" + }, status: { label: "Estado", helpText: "Estado de despliegue: draft → active → obsolete" @@ -1713,6 +2040,15 @@ export const esESMetadataForms: NonNullable = triggerConditions: { label: "Condiciones disparadoras", helpText: "Condiciones programáticas (p. ej., objectName == \"case\")" + }, + "triggerConditions.field": { + label: "Campo de contexto" + }, + "triggerConditions.operator": { + label: "Operador" + }, + "triggerConditions.value": { + label: "Valor" } } } 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 5cfe654436..6537e9fd1b 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 @@ -420,6 +420,24 @@ export const jaJPMetadataForms: NonNullable = label: "選択肢", helpText: "使用可能な選択肢(label/value ペア)" }, + "options.label": { + label: "表示名" + }, + "options.value": { + label: "値" + }, + "options.description": { + label: "説明" + }, + "options.color": { + label: "色" + }, + "options.default": { + label: "既定の選択肢" + }, + "options.visibleWhen": { + label: "表示条件" + }, reference: { label: "参照", helpText: "参照先オブジェクト名" @@ -817,13 +835,22 @@ export const jaJPMetadataForms: NonNullable = helpText: "初期値(未指定の場合は型に応じた空の値)" }, "variables.source": { - label: "ソース", + label: "書き込み元", helpText: "この変数に書き込むコンポーネント(id 指定)— 例: element:record_picker" }, regions: { label: "リージョン", helpText: "コンポーネントを含むレイアウト領域(header, main, sidebar, footer)" }, + "regions.name": { + label: "リージョン" + }, + "regions.width": { + label: "幅" + }, + "regions.components": { + label: "コンポーネント" + }, interfaceConfig: { label: "Interface Config", helpText: "The page IS the view: source picks the object, columns/filterBy are defined directly here; appearance.allowedVisualizations whitelists renderers (one entry = locked); userActions toggles the toolbar." @@ -844,6 +871,12 @@ export const jaJPMetadataForms: NonNullable = label: "Sort", helpText: "Default sort order for the page, defined directly on the page." }, + "interfaceConfig.sort.field": { + label: "フィールド" + }, + "interfaceConfig.sort.order": { + label: "並び方向" + }, "interfaceConfig.levels": { label: "Levels", helpText: "Hierarchy levels to display (tree-like sources)" @@ -974,6 +1007,57 @@ export const jaJPMetadataForms: NonNullable = label: "ウィジェット", helpText: "位置とサイズを持つダッシュボードウィジェット" }, + "widgets.id": { + label: "ウィジェット ID" + }, + "widgets.title": { + label: "タイトル" + }, + "widgets.description": { + label: "説明" + }, + "widgets.type": { + label: "可視化タイプ" + }, + "widgets.chartConfig": { + label: "チャート設定" + }, + "widgets.colorVariant": { + label: "カラーバリアント" + }, + "widgets.requiresObject": { + label: "必要なオブジェクト" + }, + "widgets.requiresService": { + label: "必要なサービス" + }, + "widgets.filter": { + label: "フィルター" + }, + "widgets.compareTo": { + label: "比較期間" + }, + "widgets.dataset": { + label: "データセット" + }, + "widgets.dimensions": { + label: "ディメンション" + }, + "widgets.values": { + label: "メジャー" + }, + "widgets.layout": { + label: "レイアウト" + }, + "widgets.options": { + label: "選択肢" + }, + "widgets.filterBindings": { + label: "フィルターの紐付け" + }, + "widgets.suppressWarnings": { + label: "抑制する警告" + }, dateRange: { label: "日付範囲", helpText: "既定の日付範囲セレクター" @@ -981,6 +1065,36 @@ export const jaJPMetadataForms: NonNullable = globalFilters: { label: "グローバルフィルター", helpText: "全ウィジェットに適用するフィルター" + }, + "globalFilters.name": { + label: "名前" + }, + "globalFilters.field": { + label: "フィールド" + }, + "globalFilters.object": { + label: "オブジェクト" + }, + "globalFilters.label": { + label: "表示名" + }, + "globalFilters.type": { + label: "入力タイプ" + }, + "globalFilters.options": { + label: "選択肢" + }, + "globalFilters.optionsFrom": { + label: "選択肢の取得元" + }, + "globalFilters.defaultValue": { + label: "既定値" + }, + "globalFilters.scope": { + label: "スコープ" + }, + "globalFilters.targetWidgets": { + label: "対象ウィジェット" } } }, @@ -1038,6 +1152,21 @@ export const jaJPMetadataForms: NonNullable = label: "領域", helpText: "項目を折りたたみ可能なエリアにグループ化" }, + "areas.id": { + label: "エリア ID" + }, + "areas.label": { + label: "表示名" + }, + "areas.icon": { + label: "アイコン" + }, + "areas.description": { + label: "説明" + }, + "areas.navigation": { + label: "ナビゲーション" + }, defaultAgent: { label: "既定エージェント", helpText: "常駐アシスタントボタン用 AI エージェント" @@ -1133,6 +1262,60 @@ export const jaJPMetadataForms: NonNullable = label: "パラメーター", helpText: "ユーザー入力パラメーター(実行前にフォームを表示)" }, + "params.name": { + label: "名前" + }, + "params.field": { + label: "フィールド" + }, + "params.objectOverride": { + label: "オブジェクトの上書き" + }, + "params.label": { + label: "表示名" + }, + "params.type": { + label: "型" + }, + "params.required": { + label: "必須" + }, + "params.options": { + label: "選択肢" + }, + "params.placeholder": { + label: "プレースホルダー" + }, + "params.helpText": { + label: "ヘルプテキスト" + }, + "params.defaultValue": { + label: "既定値" + }, + "params.multiple": { + label: "複数選択" + }, + "params.accept": { + label: "許可するタイプ" + }, + "params.maxSize": { + label: "最大サイズ(バイト)" + }, + "params.reference": { + label: "参照オブジェクト" + }, + "params.defaultFromRow": { + label: "行から既定値を取得" + }, + "params.carryOver": { + label: "値の引き継ぎ" + }, + "params.visible": { + label: "表示条件" + }, + "params.requiresFeature": { + label: "必要な機能" + }, confirmText: { label: "確認文", helpText: "確認メッセージ(例: \"Are you sure?\")" @@ -1234,6 +1417,12 @@ export const jaJPMetadataForms: NonNullable = label: "並び順", helpText: "ソートキー(優先度の高い順)。rows/columns のディメンション、または values のメジャーを指定します。時間ディメンションは既定で時系列順です。" }, + "order.by": { + label: "並び替えキー" + }, + "order.direction": { + label: "並び方向" + }, drilldown: { label: "Drilldown", helpText: "Click an aggregated row/cell to open the underlying records" @@ -1242,6 +1431,39 @@ export const jaJPMetadataForms: NonNullable = label: "ブロック", helpText: "複数オブジェクトを結合(joined レポートのみ)" }, + "blocks.name": { + label: "名前" + }, + "blocks.label": { + label: "表示名" + }, + "blocks.description": { + label: "説明" + }, + "blocks.type": { + label: "ブロックタイプ" + }, + "blocks.chart": { + label: "チャート" + }, + "blocks.dataset": { + label: "データセット" + }, + "blocks.rows": { + label: "行" + }, + "blocks.columns": { + label: "列" + }, + "blocks.values": { + label: "メジャー" + }, + "blocks.runtimeFilter": { + label: "実行時フィルター" + }, + "blocks.order": { + label: "並び順" + }, runtimeFilter: { label: "Runtime Filter", helpText: "Render-time scope filter, ANDed at query time" @@ -1302,9 +1524,48 @@ export const jaJPMetadataForms: NonNullable = label: "Dimensions", helpText: "Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity" }, + "dimensions.name": { + label: "名前" + }, + "dimensions.label": { + label: "表示名" + }, + "dimensions.field": { + label: "フィールド" + }, + "dimensions.type": { + label: "型" + }, + "dimensions.dateGranularity": { + label: "日付の粒度" + }, measures: { label: "Measures", helpText: "各メジャー:名前、集計関数、フィールド(count の場合は省略可)、表示形式/通貨" + }, + "measures.name": { + label: "名前" + }, + "measures.label": { + label: "表示名" + }, + "measures.aggregate": { + label: "集計関数" + }, + "measures.field": { + label: "フィールド" + }, + "measures.filter": { + label: "フィルター" + }, + "measures.format": { + label: "フォーマット" + }, + "measures.currency": { + label: "通貨" + }, + "measures.derived": { + label: "派生元" } } }, @@ -1345,14 +1606,80 @@ export const jaJPMetadataForms: NonNullable = label: "ノード", helpText: "⚠️ JSON ではなく Flow Designer ビジュアルエディターの利用を検討" }, + "nodes.id": { + label: "ノード ID" + }, + "nodes.type": { + label: "ノードタイプ" + }, + "nodes.label": { + label: "表示名" + }, + "nodes.config": { + label: "設定" + }, + "nodes.connectorConfig": { + label: "コネクタ操作" + }, + "nodes.position": { + label: "キャンバス位置" + }, + "nodes.timeoutMs": { + label: "タイムアウト(ms)" + }, + "nodes.inputSchema": { + label: "入力スキーマ" + }, + "nodes.waitEventConfig": { + label: "待機イベント" + }, + "nodes.boundaryConfig": { + label: "境界イベント" + }, edges: { label: "エッジ", helpText: "ノード間の接続 — 編集しやすい Flow Designer を使用" }, + "edges.id": { + label: "エッジ ID" + }, + "edges.source": { + label: "開始ノード" + }, + "edges.target": { + label: "終了ノード" + }, + "edges.condition": { + label: "条件" + }, + "edges.type": { + label: "接続タイプ" + }, + "edges.label": { + label: "表示名" + }, + "edges.isDefault": { + label: "既定パス" + }, variables: { label: "変数", helpText: "フロー変数(inputs/outputs)" }, + "variables.name": { + label: "名前" + }, + "variables.type": { + label: "型" + }, + "variables.isInput": { + label: "入力" + }, + "variables.isOutput": { + label: "出力" + }, + "variables.defaultValue": { + label: "既定値" + }, status: { label: "状態", helpText: "デプロイ状態: draft → active → obsolete" @@ -1713,6 +2040,15 @@ export const jaJPMetadataForms: NonNullable = triggerConditions: { label: "トリガー条件", helpText: "プログラム条件(例: objectName == \"case\")" + }, + "triggerConditions.field": { + label: "コンテキストフィールド" + }, + "triggerConditions.operator": { + label: "演算子" + }, + "triggerConditions.value": { + label: "値" } } } 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 new file mode 100644 index 0000000000..1d091717c5 --- /dev/null +++ b/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts @@ -0,0 +1,193 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// #17508 — THE class guard for "a repeater's column heads are English in +// every locale". +// +// Studio renders a `type: 'repeater'` as a table. Its column names come from +// the form's DECLARED row children when the form declares any, and from the +// served JSON Schema (`items.properties[k].title`) when it declares none — +// and `os i18n extract` walks a form field's declared `fields`, so only a +// DECLARED child ever gets a `metadataForms..fields['.']` +// key. #17232 (PR #17500) authored the English `.meta({ title })` on thirteen +// item schemas; #17505 and #17506 added four more carriers. None of them had +// a catalog channel: an un-enumerated repeater printed its English titles to +// a Chinese, Japanese or Spanish author, in every locale, forever. +// +// This file is the loudness for the catalog half, in the shape +// `dashboard-header-children.test.ts` established for #17227's one carrier: +// +// • every enumerated row child declares a `label` — without one the +// extractor emits `humanizeFieldPath(path)` as the English SOURCE, which +// then overlays back onto the schema and OVERRIDES the authored title +// (`page.variables.source` read "Source" against the schema's +// "Written By" until this pin was written); +// • the `en` leaf IS that declared label — one English string, not two; +// • every locale names every leaf; +// • ⛔ and a translated leaf is NEVER a copy of its `en` source, which is +// what refuses an `en`-echo catalog masquerading as a translation. +// +// 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. + +import { describe, it, expect } from 'vitest'; + +import { agentForm, skillForm, toolForm } from '@objectstack/spec/ai'; +import { flowForm } from '@objectstack/spec/automation'; +import { fieldForm, hookForm, objectForm } from '@objectstack/spec/data'; +import { positionForm } from '@objectstack/spec/identity'; +import { + actionForm, + appForm, + dashboardForm, + datasetForm, + pageForm, + reportForm, + viewForm, +} from '@objectstack/spec/ui'; + +import { enMetadataForms } from './en.metadata-forms.generated.js'; +import { zhCNMetadataForms } from './zh-CN.metadata-forms.generated.js'; +import { jaJPMetadataForms } from './ja-JP.metadata-forms.generated.js'; +import { esESMetadataForms } from './es-ES.metadata-forms.generated.js'; + +const LOCALES = [ + { name: 'en', forms: enMetadataForms as Record }, + { name: 'zh-CN', forms: zhCNMetadataForms as Record }, + { name: 'ja-JP', forms: jaJPMetadataForms as Record }, + { name: 'es-ES', forms: esESMetadataForms as Record }, +]; + +/** Every `*.form.ts` the spec package exports, by its export name. */ +const FORMS: ReadonlyArray = [ + ['actionForm', actionForm], + ['agentForm', agentForm], + ['appForm', appForm], + ['dashboardForm', dashboardForm], + ['datasetForm', datasetForm], + ['fieldForm', fieldForm], + ['flowForm', flowForm], + ['hookForm', hookForm], + ['objectForm', objectForm], + ['pageForm', pageForm], + ['positionForm', positionForm], + ['reportForm', reportForm], + ['skillForm', skillForm], + ['toolForm', toolForm], + ['viewForm', viewForm], +]; + +interface RowProperty { + /** `:.` — the test name. */ + id: string; + type: string; + /** The bundle key: `.`. */ + key: string; + /** The form's declared `label`, or `undefined` when it declares none. */ + label: string | undefined; +} + +/** Every `{ path, spec }` a form declares, composite/repeater children included. */ +function* walkFormFields(fields: any[] | undefined, prefix = ''): Generator<{ path: string; spec: any }> { + for (const f of fields ?? []) { + if (!f || typeof f !== 'object' || !f.field) continue; + const path = prefix ? `${prefix}.${f.field}` : String(f.field); + yield { path, spec: f }; + if (Array.isArray(f.fields)) yield* walkFormFields(f.fields, path); + } +} + +/** The row properties every ENUMERATED repeater declares, across every form. */ +function declaredRowProperties(): RowProperty[] { + const out: RowProperty[] = []; + for (const [, form] of FORMS) { + const f = form as any; + const type = f?.data?.schemaId as string | undefined; + if (!type) continue; + const fields = (f.sections ?? []).flatMap((s: any) => s.fields ?? []); + for (const { path, spec } of walkFormFields(fields)) { + if (spec.type !== 'repeater' || !Array.isArray(spec.fields)) continue; + for (const child of spec.fields) { + if (!child?.field) continue; + const key = `${path}.${child.field}`; + out.push({ + id: `${type}:${key}`, + type, + key, + label: typeof child.label === 'string' ? child.label : undefined, + }); + } + } + } + return out; +} + +const ROW_PROPERTIES = declaredRowProperties(); +/** The repeaters those properties belong to — used for the vacuity controls. */ +const CARRIERS = [...new Set(ROW_PROPERTIES.map((p) => `${p.type}:${p.key.replace(/\.[^.]+$/, '')}`))]; + +describe('#17508 — the enumerated-repeater survey itself (controls before verdicts)', () => { + it('walks every form the spec exports and finds row properties only where one is enumerated', () => { + // Lit — the walk really ran, over the whole form roster. + expect(FORMS.length).toBe(15); + expect(ROW_PROPERTIES.length).toBeGreaterThan(100); + expect(CARRIERS.length).toBeGreaterThan(15); + 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']) { + expect(carrying.has(type), `${type} enumerates a repeater's row properties`).toBe(true); + } + // 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', () => { + // `walkFormField` in the extractor falls back to `humanizeFieldPath(path)`, + // and `resolveMetadataFormSchemaTitles` then writes that text over the + // item schema's own `.meta({ title })`. Two English names, the worse one + // winning, in every locale. + const unlabelled = ROW_PROPERTIES.filter((p) => !p.label || p.label.length === 0).map((p) => p.id); + expect(unlabelled).toEqual([]); + }); +}); + +describe('#17508 — every enumerated row property is named in every catalog', () => { + for (const { name, forms } of LOCALES) { + it(`${name}: names every enumerated row property`, () => { + for (const { id, type, key } of ROW_PROPERTIES) { + const label = forms[type]?.fields?.[key]?.label; + expect(typeof label, `${name} ${id} is missing from the catalog`).toBe('string'); + expect(label.length, `${name} ${id} is empty`).toBeGreaterThan(0); + } + // Control — a neighbouring key known to exist, so an empty `fields` map + // cannot pass by vacuity. + expect(typeof forms.dashboard?.fields?.widgets?.label).toBe('string'); + }); + } + + it('en: each leaf IS the form\'s declared label — one English string, not two', () => { + for (const { id, type, key, label } of ROW_PROPERTIES) { + expect(enMetadataForms[type]?.fields?.[key]?.label, `en ${id}`).toBe(label); + } + }); + + it('translated locales carry their own text for every leaf, not a copy of the source', () => { + const echoes: string[] = []; + for (const { name, forms } of LOCALES) { + if (name === 'en') continue; + for (const { id, type, key } of ROW_PROPERTIES) { + const en = (enMetadataForms as any)[type]?.fields?.[key]?.label; + if (forms[type]?.fields?.[key]?.label === en) echoes.push(`${name} ${id} (${JSON.stringify(en)})`); + } + } + expect(echoes, 'these leaves still read the en source — an en-echo is not a translation').toEqual([]); + }); +}); 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 8b5914e960..5df9a8c39c 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 @@ -420,6 +420,24 @@ export const zhCNMetadataForms: NonNullable = label: "选项", helpText: "可选项(label/value 对)" }, + "options.label": { + label: "显示名称" + }, + "options.value": { + label: "值" + }, + "options.description": { + label: "描述" + }, + "options.color": { + label: "颜色" + }, + "options.default": { + label: "默认选项" + }, + "options.visibleWhen": { + label: "可见条件" + }, reference: { label: "引用对象", helpText: "被引用的对象名称" @@ -817,13 +835,22 @@ export const zhCNMetadataForms: NonNullable = helpText: "初始值(默认为对应类型的空值)" }, "variables.source": { - label: "来源", + label: "写入组件", helpText: "写入该变量的组件(按 id 指定),如 element:record_picker" }, regions: { label: "区域", helpText: "布局区域(header、main、sidebar、footer)及其组件" }, + "regions.name": { + label: "区域" + }, + "regions.width": { + label: "宽度" + }, + "regions.components": { + label: "组件" + }, interfaceConfig: { label: "Interface Config", helpText: "The page IS the view: source picks the object, columns/filterBy are defined directly here; appearance.allowedVisualizations whitelists renderers (one entry = locked); userActions toggles the toolbar." @@ -844,6 +871,12 @@ export const zhCNMetadataForms: NonNullable = label: "Sort", helpText: "Default sort order for the page, defined directly on the page." }, + "interfaceConfig.sort.field": { + label: "字段" + }, + "interfaceConfig.sort.order": { + label: "排序方向" + }, "interfaceConfig.levels": { label: "Levels", helpText: "Hierarchy levels to display (tree-like sources)" @@ -974,6 +1007,57 @@ export const zhCNMetadataForms: NonNullable = label: "组件", helpText: "包含位置和尺寸的仪表板组件" }, + "widgets.id": { + label: "组件 ID" + }, + "widgets.title": { + label: "标题" + }, + "widgets.description": { + label: "描述" + }, + "widgets.type": { + label: "可视化类型" + }, + "widgets.chartConfig": { + label: "图表配置" + }, + "widgets.colorVariant": { + label: "配色" + }, + "widgets.requiresObject": { + label: "所需对象" + }, + "widgets.requiresService": { + label: "所需服务" + }, + "widgets.filter": { + label: "筛选" + }, + "widgets.compareTo": { + label: "对比区间" + }, + "widgets.dataset": { + label: "数据集" + }, + "widgets.dimensions": { + label: "维度" + }, + "widgets.values": { + label: "度量" + }, + "widgets.layout": { + label: "布局" + }, + "widgets.options": { + label: "选项" + }, + "widgets.filterBindings": { + label: "筛选绑定" + }, + "widgets.suppressWarnings": { + label: "抑制的告警" + }, dateRange: { label: "日期范围", helpText: "默认日期范围选择器" @@ -981,6 +1065,36 @@ export const zhCNMetadataForms: NonNullable = globalFilters: { label: "全局筛选", helpText: "应用到所有组件的筛选条件" + }, + "globalFilters.name": { + label: "名称" + }, + "globalFilters.field": { + label: "字段" + }, + "globalFilters.object": { + label: "对象" + }, + "globalFilters.label": { + label: "显示名称" + }, + "globalFilters.type": { + label: "输入类型" + }, + "globalFilters.options": { + label: "选项" + }, + "globalFilters.optionsFrom": { + label: "选项来源" + }, + "globalFilters.defaultValue": { + label: "默认值" + }, + "globalFilters.scope": { + label: "范围" + }, + "globalFilters.targetWidgets": { + label: "目标组件" } } }, @@ -1038,6 +1152,21 @@ export const zhCNMetadataForms: NonNullable = label: "区域", helpText: "将菜单项组织为可折叠分组" }, + "areas.id": { + label: "区域 ID" + }, + "areas.label": { + label: "显示名称" + }, + "areas.icon": { + label: "图标" + }, + "areas.description": { + label: "描述" + }, + "areas.navigation": { + label: "导航" + }, defaultAgent: { label: "默认智能体", helpText: "环境助手使用的平台智能体(默认 'ask';创作类界面用 'build')" @@ -1133,6 +1262,60 @@ export const zhCNMetadataForms: NonNullable = label: "参数", helpText: "执行前向用户收集的输入参数" }, + "params.name": { + label: "名称" + }, + "params.field": { + label: "字段" + }, + "params.objectOverride": { + label: "对象覆盖" + }, + "params.label": { + label: "显示名称" + }, + "params.type": { + label: "类型" + }, + "params.required": { + label: "必填" + }, + "params.options": { + label: "选项" + }, + "params.placeholder": { + label: "占位文本" + }, + "params.helpText": { + label: "帮助文本" + }, + "params.defaultValue": { + label: "默认值" + }, + "params.multiple": { + label: "多选" + }, + "params.accept": { + label: "允许的类型" + }, + "params.maxSize": { + label: "最大大小(字节)" + }, + "params.reference": { + label: "引用对象" + }, + "params.defaultFromRow": { + label: "默认取自当前行" + }, + "params.carryOver": { + label: "沿用行值" + }, + "params.visible": { + label: "可见条件" + }, + "params.requiresFeature": { + label: "所需功能" + }, confirmText: { label: "确认文本", helpText: "执行前的确认提示(如 \"确定要执行吗?\")" @@ -1234,6 +1417,12 @@ export const zhCNMetadataForms: NonNullable = label: "排序", helpText: "排序键,最重要的在前(rows/columns 维度或 values 度量)。时间维度默认按时间顺序排列。" }, + "order.by": { + label: "排序依据" + }, + "order.direction": { + label: "排序方向" + }, drilldown: { label: "Drilldown", helpText: "Click an aggregated row/cell to open the underlying records" @@ -1242,6 +1431,39 @@ export const zhCNMetadataForms: NonNullable = label: "分块", helpText: "joined 报表的联合查询块" }, + "blocks.name": { + label: "名称" + }, + "blocks.label": { + label: "显示名称" + }, + "blocks.description": { + label: "描述" + }, + "blocks.type": { + label: "分块类型" + }, + "blocks.chart": { + label: "图表" + }, + "blocks.dataset": { + label: "数据集" + }, + "blocks.rows": { + label: "行" + }, + "blocks.columns": { + label: "列" + }, + "blocks.values": { + label: "度量" + }, + "blocks.runtimeFilter": { + label: "运行时筛选" + }, + "blocks.order": { + label: "排序" + }, runtimeFilter: { label: "Runtime Filter", helpText: "Render-time scope filter, ANDed at query time" @@ -1302,9 +1524,48 @@ export const zhCNMetadataForms: NonNullable = label: "Dimensions", helpText: "Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity" }, + "dimensions.name": { + label: "名称" + }, + "dimensions.label": { + label: "显示名称" + }, + "dimensions.field": { + label: "字段" + }, + "dimensions.type": { + label: "类型" + }, + "dimensions.dateGranularity": { + label: "日期粒度" + }, measures: { label: "Measures", helpText: "每个度量:名称、聚合函数、字段(count 时可省略)、显示格式/货币" + }, + "measures.name": { + label: "名称" + }, + "measures.label": { + label: "显示名称" + }, + "measures.aggregate": { + label: "聚合函数" + }, + "measures.field": { + label: "字段" + }, + "measures.filter": { + label: "筛选" + }, + "measures.format": { + label: "格式" + }, + "measures.currency": { + label: "货币" + }, + "measures.derived": { + label: "派生自" } } }, @@ -1345,14 +1606,80 @@ export const zhCNMetadataForms: NonNullable = label: "节点", helpText: "⚠️ 建议使用流程设计器,而非手写 JSON" }, + "nodes.id": { + label: "节点 ID" + }, + "nodes.type": { + label: "节点类型" + }, + "nodes.label": { + label: "显示名称" + }, + "nodes.config": { + label: "配置" + }, + "nodes.connectorConfig": { + label: "连接器操作" + }, + "nodes.position": { + label: "画布位置" + }, + "nodes.timeoutMs": { + label: "超时(毫秒)" + }, + "nodes.inputSchema": { + label: "输入 Schema" + }, + "nodes.waitEventConfig": { + label: "等待事件" + }, + "nodes.boundaryConfig": { + label: "边界事件" + }, edges: { label: "连线", helpText: "节点间的连接——建议用流程设计器编辑" }, + "edges.id": { + label: "连线 ID" + }, + "edges.source": { + label: "起始节点" + }, + "edges.target": { + label: "目标节点" + }, + "edges.condition": { + label: "条件" + }, + "edges.type": { + label: "连接类型" + }, + "edges.label": { + label: "显示名称" + }, + "edges.isDefault": { + label: "默认路径" + }, variables: { label: "变量", helpText: "流程变量(输入/输出)" }, + "variables.name": { + label: "名称" + }, + "variables.type": { + label: "类型" + }, + "variables.isInput": { + label: "输入" + }, + "variables.isOutput": { + label: "输出" + }, + "variables.defaultValue": { + label: "默认值" + }, status: { label: "状态", helpText: "部署状态:draft → active → obsolete" @@ -1713,6 +2040,15 @@ export const zhCNMetadataForms: NonNullable = triggerConditions: { label: "触发条件", helpText: "程序化条件(如 objectName == \"case\")" + }, + "triggerConditions.field": { + label: "上下文字段" + }, + "triggerConditions.operator": { + label: "运算符" + }, + "triggerConditions.value": { + label: "值" } } } From 1cf7f2ae8caf59af0cb6f5cac39ebc368d93e626 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 12:17:09 +0000 Subject: [PATCH 3/3] chore: changeset for the repeater row-property localisation Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude --- ...17508-repeater-row-property-localisation.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .changeset/17508-repeater-row-property-localisation.md diff --git a/.changeset/17508-repeater-row-property-localisation.md b/.changeset/17508-repeater-row-property-localisation.md new file mode 100644 index 0000000000..ac843f6b27 --- /dev/null +++ b/.changeset/17508-repeater-row-property-localisation.md @@ -0,0 +1,18 @@ +--- +'@objectstack/platform-objects': minor +'@objectstack/spec': minor +--- + +Studio's property-panel repeater tables name their columns in the author's own language: every repeater enumerates its row properties in the owning `*.form.ts`, and all four platform catalogs carry a translated name for each one + +Clause-②: no + +A `type: 'repeater'` renders as a table whose column heads come from the form's declared row children when it declares any, and from the served JSON Schema `items.properties[k].title` when it does not. `os i18n extract` only emits a `metadataForms..fields['.']` key for a **declared** child, so a repeater that enumerated none had no localisation channel at all — #17232 (PR #17500) authored English titles on thirteen item schemas, #17505 and #17506 on four more, and every one of those column heads reached a Chinese, Japanese or Spanish author in English. + +Both halves land together, because either alone is a half-state: 112 row properties across fifteen repeaters are now enumerated, each with a `label` equal to the item schema's own `.meta({ title })`, and the `en` / `zh-CN` / `ja-JP` / `es-ES` catalogs gain a leaf for each. Nothing in the accept set moves — the same author input parses identically before and after, and no row child declares a `type`, so the row widgets stay schema-derived. + +Terms reuse the word each catalog already uses for the concept (`Label` → 显示名称 / 表示名 / Etiqueta, `Filter` → 筛选 / フィルター / Filtro, `Timeout (ms)` → 超时(毫秒)/ タイムアウト(ms)/ Tiempo de espera (ms)), and `field.options.*` mirrors its `object.fields.options.*` twin verbatim. + +`page.variables.source` is the one existing string that moves. Its children were enumerated without labels, so the extractor emitted the humanized path `"Source"` as the English source and the bundle overlay then wrote that over the schema's authored `"Written By"`. The form now declares the label, the `en` leaf becomes `Written By`, and its three translations are re-authored with it (写入组件 / 書き込み元 / Escrito por). + +`view.columns` / `view.sort` / `view.tabs` are untitled and enumerate no children — they are #17507's, and are untouched here. `object.fields.options` stays the curated four-key subset its reconciliation-ledger entry declares.