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. 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: "值" } } } 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' }, + ], + }, ], }, {