diff --git a/.changeset/dataset-panel-en-echoes-decided.md b/.changeset/dataset-panel-en-echoes-decided.md new file mode 100644 index 00000000000..a7d0d9f1e0c --- /dev/null +++ b/.changeset/dataset-panel-en-echoes-decided.md @@ -0,0 +1,37 @@ +--- +"@objectstack/platform-objects": patch +--- + +fix(platform-objects): decide the `dataset` panel en-echoes per leaf, and derive the panel pin (#19403) + +The `dataset` metadata form — the ADR-0021 analytics semantic-layer editor — shipped its English +source in `zh-CN`, `ja-JP` and `es-ES` on 24 string leaves: the type display pair, all four section +headings, and both string leaves of its seven non-repeater fields. An author working in one of those +locales read English on the whole panel while the thirteen repeater row properties inside it, and the +`report` editor next to it, were translated. + +Each leaf was decided on its own evidence, not translated wholesale: the verdicts, their per-leaf +reasons and the `en` source each was judged against are recorded in +`dataset-panel-echo-decisions.test.ts`, which also derives the panel population from the `en` catalog +so a key added to this form is caught rather than missed. + +Two groups of tokens stay **English**, on an authored precedent rather than by habit: + +- the strict-enum values the measures section names — `sum/avg/count/…` (`AggregationFunction`) and + `ratio/sum/difference/product` (`DerivedMeasureOp`), both `z.enum` inside a `strictObject`. Rendering + them as words would tell an author in their own language to write a token the schema refuses. The + precedent is `report.fields.type.helpText`, which keeps `tabular/summary/matrix/joined` verbatim in + all three locales; +- the machine tokens an author types — `lookup` / `master_detail`, `relationship.field`, the worked + example `account.region`, and the `FROM` / `ON` SQL keywords the prose names. The precedent is + `object.fields.fields.reference.helpText`, which keeps `tree` and `lookup` verbatim inside otherwise + translated prose. + +Catalog values only. No key is added, renamed or removed in any bundle (24 insertions / 24 deletions +per translated bundle, a pure value replacement), no schema or export moves, and the three +`*.source-hashes.generated.ts` provenance tables lose exactly the 24 rows per locale that recorded +these leaves as unauthored extractor fills. + +Why `patch` rather than `skip-changeset`: `@objectstack/platform-objects` is not private and ships +`files: ["dist", …]`, and `src/metadata-translations/index.ts` imports all three translated bundles, so +the new leaves are published — measured on the built output rather than assumed. diff --git a/packages/platform-objects/src/apps/translations/dataset-panel-echo-decisions.test.ts b/packages/platform-objects/src/apps/translations/dataset-panel-echo-decisions.test.ts new file mode 100644 index 00000000000..10befb9fe1b --- /dev/null +++ b/packages/platform-objects/src/apps/translations/dataset-panel-echo-decisions.test.ts @@ -0,0 +1,698 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// #19403 round 3 — the DECISION LEDGER for the DATASET panel en-echoes. +// +// An en-echo is not automatically a defect: a leaf that reads its English +// source may be an unauthored extractor fill, or it may be the right rendering +// for that locale. The two are byte-identical, so the distinction cannot be +// recovered from the catalog later — it has to be RECORDED when someone looks. +// This file is that record for ONE panel family, in the shape #19355 landed in +// `report-dataset-panel-echo-decisions.test.ts`, round 1 of this card extended +// in `object-field-editor-panel-echo-decisions.test.ts` and round 2 extended +// again in `page-interface-panel-echo-decisions.test.ts`: one row per string +// leaf, each carrying its verdict per locale, the reason it was reached, and +// the `en` source it was judged against. +// +// ## The family +// +// The whole `dataset` metadata form — the ADR-0021 analytics semantic-layer +// editor. Its type display pair, its four section headings, and both string +// leaves of its seven non-repeater fields. +// +// ⇒ decided here: 12 label keys · 24 string leaves · 3 locales = 72 decisions. +// +// Both string leaves of every key are decided, `helpText` included: a panel +// whose field name is Chinese and whose tooltip is English is the same defect +// half fixed. +// +// ## The instrument, with its controls +// +// Census: flatten every `*.metadata-forms.generated.ts` bundle, compare each +// `.label` leaf against `en`. On base 2951c0f8f, before this change: +// +// en `.label` leaves 538 +// POSITIVE CONTROL — labels genuinely translated 488 (zh-CN) · 472 · 472 +// labels echoing `en` 50 (zh-CN) · 66 · 66 +// label keys echoing in ALL THREE locales 50 ⇒ 150 leaves +// +// After: 38 keys / 114 leaves, control 500 · 484 · 484 — echoes down 12 and +// the control up 12, same population, same run. A parser matching too broadly +// cannot produce that agreement. +// +// ## Three controls this panel supplies that the earlier rounds could not +// +// 1. THE AUTHORED TWIN INSIDE THE SAME KEY. `dataset.fields.measures` was +// already authored in all three locales (度量 / メジャー / Medidas, with its +// helpText) while `dataset.sections.measures` — the heading directly over it +// — read English, and `dataset.fields.dimensions` read English while its own +// five row properties were authored. The identical string, on the same +// panel, rendered one way and echoed the other: an echo cannot be the +// deliberate rendering when the same word is rendered beside it. +// +// 2. THE SIBLING EDITOR. `datasetForm`'s own docblock says it "Mirrors +// reportForm — the sibling analytics editor", and `report` is authored in +// all three locales for the same identity fields, the same section shape and +// the same semantic-layer vocabulary. So most of this family has a recorded +// answer one panel away, taken by a translator looking at the same words. +// +// 3. THE REPEATER BLIND SPOT, MEASURED FROM BOTH SIDES. `dataset` is one of the +// types `repeater-row-properties.test.ts` already derives — 13 row +// properties, from the two enumerated repeaters on this panel — and every +// one of those 13 was ALREADY translated, while NONE of the 24 leaves +// decided here is inside that derivation. Round 1 measured the `record` +// blind spot and round 2 the `composite` one; this is a third shape: the +// repeater is covered and its own two string leaves are not, because the +// pin walks a repeater's CHILDREN and never the repeater itself. Both +// halves are asserted below. +// +// Per-leaf control: this package's provenance table +// (`.source-hashes.generated.ts`) holds an entry exactly while a leaf +// is still a byte copy of the source revision. All 24 leaves decided here +// carried one in all three locales — 24 rows under `metadataForms.dataset` per +// locale, and exactly 24 — while all 15 authored neighbours in the same +// `dataset` block (measures.label, measures.helpText and the thirteen repeater +// row properties) carried none in any of them. `pnpm i18n:extract` dropped the +// 24 rows per locale for the leaves this round translated, so the table now +// records them as authored — asserted below. +// +// ⛔ Do not add a row here to make a red go away. A row is a decision someone +// took about one leaf; the `echo` verdict needs its own per-locale reason +// precisely so that recording "the English is right here" costs a sentence. + +import { describe, it, expect } from 'vitest'; + +import { datasetForm } 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'; +import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js'; +import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js'; +import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js'; + +const TRANSLATED_LOCALES: ReadonlyArray]> = [ + ['zh-CN', zhCNMetadataForms as Record], + ['ja-JP', jaJPMetadataForms as Record], + ['es-ES', esESMetadataForms as Record], +]; + +const PROVENANCE: ReadonlyArray>]> = [ + ['zh-CN', zhCNGeneratedSourceHashes], + ['ja-JP', jaJPGeneratedSourceHashes], + ['es-ES', esESGeneratedSourceHashes], +]; + +/** `translate` — the echo was an unauthored fill. `echo` — the English IS the rendering. */ +type Verdict = 'translate' | 'echo'; + +interface Decision { + /** + * `type` for the metadata type's own display pair, `fields` for a form + * field, `sections` for a section heading. + */ + scope: 'type' | 'fields' | 'sections'; + /** The bundle key under `dataset.` (`dataset` itself for `type`). */ + key: string; + /** Which string leaf of that entry this row decides. */ + prop: 'label' | 'helpText' | 'description'; + /** + * The `en` source the verdict was taken against. Held equal to the live + * bundle, so a reworded source reds this file instead of leaving a decision + * standing over text nobody judged. + */ + en: string; + /** The verdict per translated locale. */ + verdict: Readonly>; + /** Why — recorded for `translate` as much as for `echo`. */ + reason: string; + /** Required for every `echo` verdict: why the English is right in THAT locale. */ + departures?: Readonly>; +} + +const ALL_TRANSLATE: Readonly> = { + 'zh-CN': 'translate', + 'ja-JP': 'translate', + 'es-ES': 'translate', +}; + +const DECISIONS: readonly Decision[] = [ + { + scope: 'type', + key: 'dataset', + prop: 'label', + en: 'Dataset', + verdict: ALL_TRANSLATE, + reason: + 'Three authored precedents answer this exact word identically — report.fields.dataset.label, report.fields.blocks.dataset.label and dashboard.fields.widgets.dataset.label all read 数据集 / データセット / Conjunto de datos. The metadata type own display name was the one leaf left in English while every panel that REFERS to a dataset already names it in the author language; being the type name is not a reason to be the exception.', + }, + { + scope: 'type', + key: 'dataset', + prop: 'description', + en: 'Analytics semantic layer — dimensions & measures', + verdict: ALL_TRANSLATE, + reason: + 'Prose over three terms this catalog has already answered, in a sentence about this very object: report.fields.dataset.helpText renders `semantic layer` as 语义层 / セマンティックレイヤー / capa semántica and `measures/dimensions` as 度量/维度, メジャー/ディメンション, medidas/dimensiones. The vocabulary was not invented here, it was taken from the sibling editor that consumes this type.', + }, + { + scope: 'sections', + key: 'basics', + prop: 'label', + en: 'Basics', + verdict: ALL_TRANSLATE, + reason: + 'Eleven authored answers for this exact word in this catalog (object, field, view, page, dashboard, app, action, report, flow, tool, skill), every one of them 基础信息 / 基本 / Aspectos básicos — and `dataset` was the twelfth and the only echo. A translator who judged the English right here would have had to judge it wrong eleven times.', + }, + { + scope: 'sections', + key: 'basics', + prop: 'description', + en: 'Dataset identity.', + verdict: ALL_TRANSLATE, + reason: + 'report.sections.basics.description — the sibling analytics editor this form docblock says it mirrors — answers the same sentence shape: "Identity and report type." is 标识与报表类型 / ID とレポートタイプ。/ Identidad y tipo de informe. Rendered the same way, and the trailing stop follows this catalog measured style: of the 67 genuinely-translated section descriptions, zh-CN drops it in 56 while ja-JP and es-ES keep it in all 67.', + }, + { + scope: 'sections', + key: 'source', + prop: 'label', + en: 'Source', + verdict: ALL_TRANSLATE, + reason: + 'DECIDED AGAINST its precedents, per locale. This catalog answers the bare word `Source` in three directions: hook.fields.body.source is 源码 / ソース / Código fuente (source CODE), sys_metadata.fields.source is 来源 / ソース / Origen (provenance), and page.fields.interfaceConfig.source is 数据来源 / ソース / Origen de datos (a data binding, decided one round earlier on this same card). This section holds the base object, the joins and the intrinsic scope, so it is the third sense and neither of the first two: 源码 is plainly wrong, and a bare 来源 / Origen reads as provenance on a panel that records none. zh-CN and es-ES therefore depart from two of the three precedents; ja-JP keeps ソース, which all three precedents already use and which carries the data-source sense unambiguously in Japanese. Per locale, not per string.', + }, + { + scope: 'sections', + key: 'source', + prop: 'description', + en: 'The base object, the relationships to join, and the dataset’s intrinsic scope. Joins are derived from the object graph — pick relationship (lookup / master_detail) names, never write an ON clause.', + verdict: ALL_TRANSLATE, + reason: + 'Prose around machine tokens. `lookup` and `master_detail` are FieldType enum values an author types, and this catalog keeps exactly those tokens English inside translated prose: object.fields.fields.reference.helpText keeps `tree` and `lookup` verbatim in all three locales, field.fields.multiple.helpText keeps `select/lookup`. `ON` is the SQL keyword the sentence tells the author NOT to write, so rendering it as a word would destroy the instruction. Everything else is translated.', + }, + { + scope: 'sections', + key: 'dimensions', + prop: 'label', + en: 'Dimensions', + verdict: ALL_TRANSLATE, + reason: + 'dashboard.fields.widgets.dimensions.label is the authored answer for this exact word — 维度 / ディメンション / Dimensiones — and it names the same semantic-layer concept, because a dashboard widget dimensions ARE a dataset dimensions. The in-family evidence agrees: every row property under dataset.fields.dimensions was already authored while this heading over them was not.', + }, + { + scope: 'sections', + key: 'dimensions', + prop: 'description', + en: 'Groupable axes. Use a base field, or `relationship.field` (e.g. account.region) for a relationship included above.', + verdict: ALL_TRANSLATE, + reason: + '`relationship.field` is the literal shape an author types into a dimension field, and `account.region` is its worked example; both stay English on the authored precedent of view.fields.appearance.helpText, which keeps `allowedVisualizations` verbatim in all three locales inside otherwise translated prose. A key an author types is not a word.', + }, + { + scope: 'sections', + key: 'measures', + prop: 'label', + en: 'Measures', + verdict: ALL_TRANSLATE, + reason: + 'The strongest control this family supplies. dataset.fields.measures.label — the field this very section wraps, one entry down the same panel — was ALREADY authored 度量 / メジャー / Medidas while the section heading over it read English. Same word, same panel, one authored and one filled: an echo cannot be the deliberate rendering when the identical string is rendered otherwise beside it.', + }, + { + scope: 'sections', + key: 'measures', + prop: 'description', + en: '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.', + verdict: ALL_TRANSLATE, + reason: + 'Prose naming two STRICT ENUMS legal values, checked against the schema BEFORE a word was translated. `sum/avg/count/…` are AggregationFunction (packages/spec/src/data/query.zod.ts) and `ratio/sum/difference/product` are DerivedMeasureOp (packages/spec/src/ui/dataset.zod.ts), both z.enum inside a strictObject: rendering them as words would tell an author in their own language to write a token the schema refuses — the inverse defect round 2 of this card found and filed. They stay English, on this catalog own authored precedent for the shape: report.fields.type.helpText keeps `tabular/summary/matrix/joined` verbatim in all three locales. The surrounding prose is translated.', + }, + { + scope: 'fields', + key: 'name', + prop: 'label', + en: 'Name', + verdict: ALL_TRANSLATE, + reason: + 'Twenty authored answers for this exact word in this catalog, all 名称 / 名前 / Nombre — two of them INSIDE this family (dataset.fields.dimensions.name.label and dataset.fields.measures.name.label, the repeater row properties the #17508 pin already covers). The panel own row headers were translated while the field above them was not.', + }, + { + scope: 'fields', + key: 'name', + prop: 'helpText', + en: 'snake_case unique identifier', + verdict: ALL_TRANSLATE, + reason: + 'An exact-string authored precedent, byte for byte: dashboard.fields.name.helpText and report.fields.name.helpText carry the same English source and read snake_case 唯一标识符 / snake_case の一意識別子 / Identificador único snake_case. `snake_case` itself stays, as it does in both precedents — it names a spelling convention, not a word.', + }, + { + scope: 'fields', + key: 'label', + prop: 'label', + en: 'Label', + verdict: ALL_TRANSLATE, + reason: + 'Seventeen authored answers, whose dominant reading for a `label` field meaning the display name is 显示名称 / 表示名 / Etiqueta — including the two in-family row properties dataset.fields.dimensions.label.label and dataset.fields.measures.label.label. The one divergent precedent, dashboard.fields.header.actions.label.label (标签 / ラベル), is a button caption rather than a display name; this leaf is the display name, and its own helpText says so.', + }, + { + scope: 'fields', + key: 'label', + prop: 'helpText', + en: 'Display name', + verdict: ALL_TRANSLATE, + reason: + 'Exact-string authored precedent dashboard.fields.label.helpText: 显示名 / 表示名 / Nombre mostrado. Checked rather than assumed that ja-JP renders this leaf and its `label` sibling with the same 表示名 — the authored precedent pair does exactly that, so the repetition IS the answer a translator gave and not a copy-paste here.', + }, + { + scope: 'fields', + key: 'description', + prop: 'label', + en: 'Description', + verdict: ALL_TRANSLATE, + reason: + 'Twenty authored answers for this exact word, all 描述 / 説明 / Descripción, across object, field, hook, view, page, dashboard, app, report, flow, email_template, position, tool and skill. Nothing distinguishes a dataset description from any of them.', + }, + { + scope: 'fields', + key: 'description', + prop: 'helpText', + en: 'What this dataset measures', + verdict: ALL_TRANSLATE, + reason: + 'The sentence shape is authored twice here — flow.fields.description.helpText "What this flow does" is 此流程做什么 / このフローの処理内容 / Qué hace este flujo, and skill has 此技能的作用 / このスキルの処理内容 / Qué hace esta skill. Only the verb changes, and "measures" is the dataset vocabulary already fixed by the type description row above.', + }, + { + scope: 'fields', + key: 'object', + prop: 'label', + en: 'Object', + verdict: ALL_TRANSLATE, + reason: + 'Five authored answers for this exact word — object.label, hook.fields.object.label, page.fields.object.label, dashboard.fields.globalFilters.object.label and object.fields.fields.summaryOperations.object.label — all 对象 / オブジェクト / Objeto. field.fields.summaryOperations.object.label is a sixth occurrence and is itself an undecided echo belonging to the `field` family a later round takes: recorded here, deliberately NOT counted as evidence.', + }, + { + scope: 'fields', + key: 'object', + prop: 'helpText', + en: 'Base object — the FROM', + verdict: ALL_TRANSLATE, + reason: + '`FROM` is the SQL clause this sentence equates the field to — a keyword, not a word, in the same vocabulary as the `ON` clause the section description tells the author not to write. Kept English on that reasoning; the rest rendered.', + }, + { + scope: 'fields', + key: 'include', + prop: 'label', + en: 'Include', + verdict: ALL_TRANSLATE, + reason: + 'THE ONE LEAF IN THIS FAMILY WITH NO SAME-STRING PRECEDENT anywhere in either catalog, so it is decided from its own helpText rather than from the corpus. `Include` alone answers "include what?" with nothing, on a panel whose neighbouring labels (Object, Filter, Dimensions, Measures) all name their content — so the rendering names it, in this catalog own authored words for the concept: 关联 (report.sections.joined_blocks.label is 关联对象), 関係 (field.fields.summaryOperations.helpText renders parent-child relationships as 親子関係) and relación (the same leaf es-ES reads relaciones padre-hijo). Recorded as an EXPANSION rather than a literal rendering, because it is one.', + }, + { + scope: 'fields', + key: 'include', + prop: 'helpText', + en: 'Relationship (lookup / master_detail) field names to join — enables `relationship.field` dimensions/measures (e.g. include "account" → group by account.region)', + verdict: ALL_TRANSLATE, + reason: + 'The same token discipline as the two section descriptions: `lookup / master_detail`, `relationship.field`, the literal example value "account" and the path `account.region` all stay English because an author types them. `include` inside the example is the spec key and not the English verb, so it stays too — a distinction this leaf would lose if the label verb and the key were rendered together.', + }, + { + scope: 'fields', + key: 'filter', + prop: 'label', + en: 'Filter', + verdict: ALL_TRANSLATE, + reason: + 'Three authored answers — view.fields.filter.label, dashboard.fields.widgets.filter.label, and dataset.fields.measures.filter.label INSIDE this family — all 筛选 / フィルター / Filtro. The measure-scoped filter one entry down was authored while the dataset-scoped filter above it was not.', + }, + { + scope: 'fields', + key: 'filter', + prop: 'helpText', + en: 'Intrinsic scope filter (e.g. exclude soft-deleted records), ANDed into every query', + verdict: ALL_TRANSLATE, + reason: + '`AND` is the boolean operator this sentence describes the filter being composed with, and this catalog spells it as an operator in English elsewhere (report.fields.runtimeFilter.helpText "ANDed at query time", field.fields.relatedListFilter.helpText "AND-composed"). Kept; the rest rendered, with "soft-deleted records" taking each locale own term for a logical delete.', + }, + { + scope: 'fields', + key: 'dimensions', + prop: 'label', + en: 'Dimensions', + verdict: ALL_TRANSLATE, + reason: + 'The same authored answer as the section heading above it (dashboard.fields.widgets.dimensions.label), with the in-family evidence read from the other side: this field own five row properties were authored in all three locales while the field label was not — and the pin that covers those row properties cannot see this leaf, which the coverage assertions below measure rather than assert.', + }, + { + scope: 'fields', + key: 'dimensions', + prop: 'helpText', + en: 'Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity', + verdict: ALL_TRANSLATE, + reason: + 'The in-family sentence template, authored: dataset.fields.measures.helpText — the sibling repeater on the same panel — reads "Each: name, aggregate, field (optional for count), and display format/currency" and is rendered 每个度量:… / 各メジャー:… / Cada medida: …, keeping `count` as the enum token. This leaf is the same "Each: …" construction for dimensions and is rendered in the same shape, down to the full-width colon and enumeration comma in zh-CN and ja-JP.', + }, +]; + +/** The live leaf a decision points at. */ +function leafOf(bundle: Record, d: Decision): unknown { + const dataset = bundle.dataset; + return d.scope === 'type' ? dataset?.[d.prop] : dataset?.[d.scope]?.[d.key]?.[d.prop]; +} + +/** The provenance-table key for a decided leaf. */ +function provenanceKey(d: Decision): string { + return d.scope === 'type' + ? `metadataForms.dataset.${d.prop}` + : `metadataForms.dataset.${d.scope}.${d.key}.${d.prop}`; +} + +/** The dotted id a decision is reported under. */ +function idOf(d: Decision): string { + return d.scope === 'type' ? `dataset.${d.prop}` : `dataset.${d.scope}.${d.key}.${d.prop}`; +} + +/** + * Every `echo` verdict that carries no per-locale departure reason. Exported + * as a predicate rather than inlined so the suite can prove it FIRES — with + * every verdict in this round `translate`, asserting it over DECISIONS alone + * would evaluate nothing. + */ +function undeclaredEchoes(rows: readonly Decision[]): string[] { + const bad: string[] = []; + for (const d of rows) { + for (const [locale, verdict] of Object.entries(d.verdict)) { + if (verdict !== 'echo') continue; + if ((d.departures?.[locale] ?? '').length <= 40) bad.push(`${locale} ${idOf(d)}`); + } + } + return bad; +} + +interface PanelLeaf { + scope: 'type' | 'fields' | 'sections'; + key: string; + prop: string; + en: string; +} + +/** Every string leaf of the `dataset` panel, DERIVED from the `en` catalog. */ +function panelLeaves(): PanelLeaf[] { + const out: PanelLeaf[] = []; + const dataset = (enMetadataForms as Record).dataset ?? {}; + for (const [prop, value] of Object.entries(dataset)) { + if (typeof value === 'string') out.push({ scope: 'type', key: 'dataset', prop, en: value }); + } + for (const scope of ['fields', 'sections'] as const) { + for (const [key, entry] of Object.entries(dataset[scope] ?? {})) { + if (!entry || typeof entry !== 'object') continue; + for (const [prop, value] of Object.entries(entry as Record)) { + if (typeof value === 'string') out.push({ scope, key, prop, en: value }); + } + } + } + return out; +} + +const PANEL_LEAVES = panelLeaves(); + +function liveLeaf(bundle: Record, l: PanelLeaf): unknown { + const dataset = bundle.dataset; + return l.scope === 'type' ? dataset?.[l.prop] : dataset?.[l.scope]?.[l.key]?.[l.prop]; +} + +/** + * The row properties `repeater-row-properties.test.ts` derives for `dataset`, + * re-derived here by the same rule (a `type: 'repeater'` field that enumerates + * `fields`), so the two halves of its blind spot can be measured rather than + * remembered. + */ +function datasetRowProperties(): string[] { + const out: string[] = []; + const walk = (fields: any[] | undefined, prefix = ''): void => { + for (const f of fields ?? []) { + if (!f || typeof f !== 'object' || !f.field) continue; + const path = prefix ? `${prefix}.${f.field}` : String(f.field); + if (f.type === 'repeater' && Array.isArray(f.fields)) { + for (const child of f.fields) if (child?.field) out.push(`${path}.${child.field}`); + } + if (Array.isArray(f.fields)) walk(f.fields, path); + } + }; + walk(((datasetForm as any).sections ?? []).flatMap((s: any) => s.fields ?? [])); + return out; +} + +const ROW_PROPERTIES = datasetRowProperties(); + +describe('#19403 round 3 — the ledger itself (controls before verdicts)', () => { + it('decides every echoing string leaf of the 12 keys this round takes, and nothing else', () => { + // Lit — the ledger is the size it claims: 12 label keys, 24 leaves, three + // locales, 72 decisions. + expect(DECISIONS.length).toBe(24); + expect(new Set(DECISIONS.map((d) => `${d.scope}.${d.key}`)).size).toBe(12); + expect(DECISIONS.flatMap((d) => Object.keys(d.verdict)).length).toBe(72); + for (const d of DECISIONS) { + if (d.scope === 'type') { + expect(d.key, 'a type-scoped row is the dataset display pair').toBe('dataset'); + } + expect(Object.keys(d.verdict).sort(), `${idOf(d)} names every translated locale`).toEqual([ + 'es-ES', + 'ja-JP', + 'zh-CN', + ]); + } + // Every decided key is decided on BOTH of its string leaves — a panel + // whose field name is translated and whose tooltip is not is the same + // defect half fixed. + for (const id of new Set(DECISIONS.map((d) => `${d.scope}.${d.key}`))) { + expect( + DECISIONS.filter((d) => `${d.scope}.${d.key}` === id) + .map((d) => d.prop) + .sort() + .join('+'), + `${id} is decided on one leaf only`, + ).toMatch(/^(description\+label|helpText\+label)$/); + } + }); + + it('every row is pinned to the live `en` source it was decided against', () => { + for (const d of DECISIONS) { + expect( + leafOf(enMetadataForms as Record, d), + `en ${idOf(d)} moved — re-judge the decision, do not refresh this row`, + ).toBe(d.en); + } + }); + + it('the echo predicate can say "echo" — fed the `en` catalog, it flags every row', () => { + // Dark. `translated !== en` is the whole verdict test below; run it against + // the source catalog itself and it must fail for all 24 rows, or a green + // verdict run means nothing. + const flagged = DECISIONS.filter((d) => leafOf(enMetadataForms as Record, d) === d.en); + expect(flagged.length).toBe(DECISIONS.length); + }); + + it('every decision records a reason', () => { + for (const d of DECISIONS) { + expect(d.reason.length, `${idOf(d)} records no reason`).toBeGreaterThan(40); + } + }); + + it('refuses an `echo` verdict that carries no per-locale reason — proved on a synthetic row', () => { + // Every verdict in this round is `translate`, so the rule has nothing to + // evaluate over DECISIONS. Assert it there AND prove the predicate fires, + // or the requirement is a phantom check that deleting would leave green. + expect(undeclaredEchoes(DECISIONS), 'a declared echo here carries no reason').toEqual([]); + + const synthetic: Decision = { + scope: 'sections', + key: 'source', + prop: 'label', + en: 'Source', + verdict: { 'zh-CN': 'echo', 'ja-JP': 'translate', 'es-ES': 'translate' }, + reason: 'A synthetic row that exists only to prove this file can refuse an undeclared echo.', + }; + expect(undeclaredEchoes([synthetic])).toEqual(['zh-CN dataset.sections.source.label']); + expect( + undeclaredEchoes([ + { + ...synthetic, + departures: { + 'zh-CN': 'A departure reason long enough to satisfy the rule, standing in for a real judgement.', + }, + }, + ]), + ).toEqual([]); + }); +}); + +describe('#19403 round 3 — the catalogs hold what the ledger decided', () => { + for (const [locale, forms] of TRANSLATED_LOCALES) { + it(`${locale}: every decided leaf matches its verdict`, () => { + for (const d of DECISIONS) { + const id = `${locale} ${idOf(d)}`; + const value = leafOf(forms, d); + expect(typeof value, `${id} is missing from the catalog`).toBe('string'); + expect((value as string).length, `${id} is empty`).toBeGreaterThan(0); + if (d.verdict[locale] === 'translate') { + expect(value, `${id} reads its en source again — the decision was that this echo is a fill`).not.toBe(d.en); + } else { + expect(value, `${id} is a declared echo and must stay the en source`).toBe(d.en); + } + } + }); + } +}); + +describe('#19403 round 3 — the provenance table agrees these leaves are now authored', () => { + // A second, independent witness to the same fact, from a table nobody edits + // by hand. An entry exists exactly while a leaf is still a byte copy of the + // source revision, so re-filling a decided leaf and re-running the extract + // brings its row back and reds this block — even in the locale where no + // second locale exists to disagree with it. + for (const [locale, table] of PROVENANCE) { + it(`${locale}: no decided leaf is still recorded as an extractor fill`, () => { + // Lit — the table really loaded, so "no entry" cannot pass by the import + // having come back empty. + expect(Object.keys(table).length, `${locale} provenance table is empty`).toBeGreaterThan(100); + const stillFilled = DECISIONS.filter((d) => table[provenanceKey(d)] !== undefined).map(idOf); + expect(stillFilled, 'these leaves are still byte copies of their source revision').toEqual([]); + }); + + it(`${locale}: the provenance lookup can say "still a fill"`, () => { + // Dark. The verdict above is a run of `undefined`s, which is also what a + // misspelt key shape returns. So take a key the table DOES hold, prove it + // has the very shape `provenanceKey` composes, and prove the same lookup + // finds it. Deliberately matched by pattern rather than pinned to a + // family, so a later round of this card cannot red this control by + // fixing one. + const sample = Object.keys(table).find((k) => /^metadataForms\.[^.]+\.(fields|sections)\..+\.[^.]+$/.test(k)); + expect(sample, `${locale} provenance table records no metadata-form leaf`).toBeTruthy(); + const parts = /^metadataForms\.([^.]+)\.(fields|sections)\.(.+)\.([^.]+)$/.exec(sample!); + expect(parts, 'the table key does not decompose').toBeTruthy(); + const [, type, scope, key, prop] = parts!; + expect(`metadataForms.${type}.${scope}.${key}.${prop}`, 'composing the key back must reproduce it').toBe(sample); + expect(table[sample!], 'the lookup this file performs finds a key the table holds').toBeTruthy(); + // And the composer under test builds exactly those two templates. + expect(provenanceKey({ ...DECISIONS[0], scope: 'fields', key: 'k', prop: 'label' })).toBe( + 'metadataForms.dataset.fields.k.label', + ); + expect(provenanceKey({ ...DECISIONS[0], scope: 'type', key: 'dataset', prop: 'label' })).toBe( + 'metadataForms.dataset.label', + ); + }); + } +}); + +describe('#19403 round 3 — the panel population, DERIVED (the pin no census had)', () => { + it('the derivation reaches this panel, and only this panel', () => { + // Lit — floors, not exact counts, so a key added to the dataset form is + // caught by the echo rule below rather than by a number nobody can + // interpret. Measured on base 2951c0f8f: 24 entries, 39 string leaves (the + // 24 decided here plus the 15 already-authored neighbours). + expect(PANEL_LEAVES.length).toBeGreaterThan(35); + expect(new Set(PANEL_LEAVES.map((l) => `${l.scope}.${l.key}`)).size).toBeGreaterThan(20); + for (const key of ['name', 'include', 'dimensions', 'measures', 'dimensions.dateGranularity']) { + expect( + PANEL_LEAVES.some((l) => l.scope === 'fields' && l.key === key), + `${key} is on the dataset panel`, + ).toBe(true); + } + for (const key of ['basics', 'source', 'dimensions', 'measures']) { + expect(PANEL_LEAVES.some((l) => l.scope === 'sections' && l.key === key)).toBe(true); + } + expect(PANEL_LEAVES.some((l) => l.scope === 'type' && l.prop === 'label')).toBe(true); + expect(PANEL_LEAVES.some((l) => l.scope === 'type' && l.prop === 'description')).toBe(true); + // Dark — the derivation is the `dataset` entry and nothing else. These keys + // exist in the catalog on OTHER types, so a walk that read the whole bundle + // would pick them up. + for (const key of ['interfaceConfig', 'drilldown', 'runtimeFilter', 'retryPolicy', 'validations']) { + expect( + PANEL_LEAVES.some((l) => l.scope === 'fields' && l.key === key), + `${key} belongs to another type and is not a dataset leaf`, + ).toBe(false); + } + for (const key of ['dataset_binding', 'joined_blocks', 'interface']) { + expect(PANEL_LEAVES.some((l) => l.scope === 'sections' && l.key === key)).toBe(false); + } + expect(PANEL_LEAVES.every((l) => ['label', 'helpText', 'description'].includes(l.prop))).toBe(true); + }); + + it('no leaf on this panel reads its `en` source unless the ledger decided it is an echo', () => { + const undecided: string[] = []; + for (const [locale, forms] of TRANSLATED_LOCALES) { + for (const leaf of PANEL_LEAVES) { + if (liveLeaf(forms, leaf) !== leaf.en) continue; + const decided = DECISIONS.find( + (d) => d.scope === leaf.scope && d.key === leaf.key && d.prop === leaf.prop, + ); + if (decided?.verdict[locale] === 'echo') continue; + const id = leaf.scope === 'type' ? `dataset.${leaf.prop}` : `dataset.${leaf.scope}.${leaf.key}.${leaf.prop}`; + undecided.push(`${locale} ${id} (${JSON.stringify(leaf.en)})`); + } + } + expect( + undecided, + 'these leaves read their en source and no row in this ledger says that is right — decide them, do not refresh anything', + ).toEqual([]); + }); + + it('the derived predicate can fire — fed the `en` catalog it flags every leaf on the panel', () => { + // Dark. Same walk, with `en` standing in for a translated catalog: every + // leaf must come back flagged, or the green above means only that the walk + // found nothing. + const flagged = PANEL_LEAVES.filter( + (l) => liveLeaf(enMetadataForms as Record, l) === l.en, + ); + expect(flagged.length).toBe(PANEL_LEAVES.length); + }); +}); + +describe('#19403 round 3 — the repeater blind spot, measured from BOTH sides', () => { + it('what the existing pin CAN see on this panel was already translated — the covered slice is its own control', () => { + // `repeater-row-properties.test.ts` derives a row property for every child + // of a `type: 'repeater'` field. On this form that is dimensions.* (5) and + // measures.* (8) — 13 leaves, and every one of them carried its own text in + // all three locales before this round. Everything the existing pin could + // see was fine. + expect(ROW_PROPERTIES.length).toBeGreaterThanOrEqual(13); + expect(ROW_PROPERTIES).toContain('dimensions.dateGranularity'); + expect(ROW_PROPERTIES).toContain('measures.derived'); + for (const [locale, forms] of TRANSLATED_LOCALES) { + for (const key of ROW_PROPERTIES) { + const en = (enMetadataForms as Record).dataset?.fields?.[key]?.label; + expect(typeof en, `en dataset.fields.${key}.label`).toBe('string'); + expect( + forms.dataset?.fields?.[key]?.label, + `${locale} dataset.fields.${key}.label was translated before this round and must stay so`, + ).not.toBe(en); + } + } + }); + + it('what it CANNOT see is every leaf this round decided — including the repeaters own two', () => { + // The blind spot, a third shape after round 1 (`record`) and round 2 + // (`composite`): here the repeater IS covered and its own string leaves are + // not, because the derivation walks a repeater CHILDREN and never the + // repeater itself. `dataset.fields.dimensions.label` and `.helpText` sit + // directly above thirteen covered row properties and no pin saw them. + const covered = new Set(ROW_PROPERTIES); + const decidedInside = DECISIONS.filter((d) => d.scope === 'fields' && covered.has(d.key)).map(idOf); + expect(decidedInside, 'no leaf decided here is one the existing pin already derives').toEqual([]); + for (const key of ['dimensions', 'measures']) { + expect(covered.has(key), `the repeater ${key} is not itself a row property`).toBe(false); + } + expect( + DECISIONS.some((d) => d.scope === 'fields' && d.key === 'dimensions' && d.prop === 'label'), + 'the dimensions repeater own label is decided here', + ).toBe(true); + // And the section headings and the type display pair are outside it + // entirely — the derivation only ever yields `fields` keys. + expect( + DECISIONS.filter((d) => d.scope !== 'fields').length, + 'ten of this round twelve keys are not even the shape that pin walks', + ).toBeGreaterThan(9); + }); +}); 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 a93c8c049f0..b28b05b2baa 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 @@ -1475,54 +1475,54 @@ export const esESMetadataForms: NonNullable = } }, dataset: { - label: "Dataset", - description: "Analytics semantic layer — dimensions & measures", + label: "Conjunto de datos", + description: "Capa semántica de analítica — dimensiones y medidas", sections: { basics: { - label: "Basics", - description: "Dataset identity." + label: "Aspectos básicos", + description: "Identidad del conjunto de datos." }, source: { - label: "Source", - description: "The base object, the relationships to join, and the dataset’s intrinsic scope. Joins are derived from the object graph — pick relationship (lookup / master_detail) names, never write an ON clause." + label: "Origen de datos", + description: "El objeto base, las relaciones a unir y el ámbito intrínseco del conjunto de datos. Las uniones se derivan del grafo de objetos: elige nombres de relación (lookup / master_detail), nunca escribas una cláusula ON." }, dimensions: { - label: "Dimensions", - description: "Groupable axes. Use a base field, or `relationship.field` (e.g. account.region) for a relationship included above." + label: "Dimensiones", + description: "Ejes agrupables. Usa un campo del objeto base o, para una relación incluida arriba, `relationship.field` (p. ej. account.region)." }, measures: { - 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." + label: "Medidas", + description: "Valores agregables definidos una vez y referenciados por nombre. Una medida es sum/avg/count/… de un campo; una medida derivada combina otras medidas (ratio/sum/difference/product). Los filtros por medida y las operaciones derivadas se editan por fila en el diseñador de conjuntos de datos." } }, fields: { name: { - label: "Name", - helpText: "snake_case unique identifier" + label: "Nombre", + helpText: "Identificador único snake_case" }, label: { - label: "Label", - helpText: "Display name" + label: "Etiqueta", + helpText: "Nombre mostrado" }, description: { - label: "Description", - helpText: "What this dataset measures" + label: "Descripción", + helpText: "Qué mide este conjunto de datos" }, object: { - label: "Object", - helpText: "Base object — the FROM" + label: "Objeto", + helpText: "Objeto base: el FROM" }, include: { - label: "Include", - helpText: "Relationship (lookup / master_detail) field names to join — enables `relationship.field` dimensions/measures (e.g. include \"account\" → group by account.region)" + label: "Relaciones a incluir", + helpText: "Nombres de campo de relación (lookup / master_detail) a unir — habilita dimensiones/medidas `relationship.field` (p. ej. incluir \"account\" → agrupar por account.region)" }, filter: { - label: "Filter", - helpText: "Intrinsic scope filter (e.g. exclude soft-deleted records), ANDed into every query" + label: "Filtro", + helpText: "Filtro de ámbito intrínseco (p. ej. excluir registros eliminados de forma lógica), combinado con AND en cada consulta" }, dimensions: { - label: "Dimensions", - helpText: "Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity" + label: "Dimensiones", + helpText: "Cada dimensión: nombre (referenciado por las presentaciones), campo, tipo y, para fechas, una granularidad de agrupación predeterminada" }, "dimensions.name": { label: "Nombre" diff --git a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts index 2c69fd2cef5..2c5e07082d4 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts @@ -36,30 +36,6 @@ export const esESGeneratedSourceHashes: Readonly> = { "metadataForms.book.label": "b48c719759fca087", "metadataForms.capability.description": "1130a8a487c35f3d", "metadataForms.capability.label": "cb58b72c5cc77632", - "metadataForms.dataset.description": "ce0d0a4d536bbc1e", - "metadataForms.dataset.fields.description.helpText": "f82c5070e1f78508", - "metadataForms.dataset.fields.description.label": "779a4342c8eb6707", - "metadataForms.dataset.fields.dimensions.helpText": "a4df7393a6bd88c9", - "metadataForms.dataset.fields.dimensions.label": "d75e6dd9d87e93b4", - "metadataForms.dataset.fields.filter.helpText": "8d996f9121810664", - "metadataForms.dataset.fields.filter.label": "802895ec427fc6af", - "metadataForms.dataset.fields.include.helpText": "e8f9933ae4f99e77", - "metadataForms.dataset.fields.include.label": "45b27393fb9f3b2a", - "metadataForms.dataset.fields.label.helpText": "f0cbad8e334db1d3", - "metadataForms.dataset.fields.label.label": "c680364257b11569", - "metadataForms.dataset.fields.name.helpText": "8c3cae6dba12b42a", - "metadataForms.dataset.fields.name.label": "5274417ff1ccaf01", - "metadataForms.dataset.fields.object.helpText": "09a85579054a7197", - "metadataForms.dataset.fields.object.label": "d0c49032d95ebb36", - "metadataForms.dataset.label": "19647dcb84ce82d3", - "metadataForms.dataset.sections.basics.description": "844b82a322f8edd6", - "metadataForms.dataset.sections.basics.label": "55562f900da68c5d", - "metadataForms.dataset.sections.dimensions.description": "da19c8faa0cb1023", - "metadataForms.dataset.sections.dimensions.label": "d75e6dd9d87e93b4", - "metadataForms.dataset.sections.measures.description": "8186d1c0fe6a86f4", - "metadataForms.dataset.sections.measures.label": "395dac46abcf1943", - "metadataForms.dataset.sections.source.description": "ee9be5509026b678", - "metadataForms.dataset.sections.source.label": "8fe786e8e29c8cec", "metadataForms.doc.description": "f233af60238b0879", "metadataForms.doc.label": "3f35cf5088b999ad", "metadataForms.email_template.fields.variables.helpText": "27197774c2cda633", 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 e7bffc93556..7da349e0146 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 @@ -1475,54 +1475,54 @@ export const jaJPMetadataForms: NonNullable = } }, dataset: { - label: "Dataset", - description: "Analytics semantic layer — dimensions & measures", + label: "データセット", + description: "分析セマンティックレイヤー — ディメンションとメジャー", sections: { basics: { - label: "Basics", - description: "Dataset identity." + label: "基本", + description: "データセットの識別情報。" }, source: { - label: "Source", - description: "The base object, the relationships to join, and the dataset’s intrinsic scope. Joins are derived from the object graph — pick relationship (lookup / master_detail) names, never write an ON clause." + label: "ソース", + description: "ベースオブジェクト、結合する関係、そしてデータセット固有のスコープ。結合はオブジェクトグラフから導出されます — 関係(lookup / master_detail)の名前を選択し、ON 句は記述しません。" }, dimensions: { - label: "Dimensions", - description: "Groupable axes. Use a base field, or `relationship.field` (e.g. account.region) for a relationship included above." + label: "ディメンション", + description: "グループ化できる軸。ベースオブジェクトのフィールド、または上で含めた関係に対しては `relationship.field`(例: account.region)を使用します。" }, measures: { - 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." + label: "メジャー", + description: "一度定義すれば名前で参照できる集計可能な値。メジャーはフィールドの sum/avg/count/… であり、派生メジャーは他のメジャーを組み合わせます(ratio/sum/difference/product)。メジャー単位のフィルターと派生演算はデータセットデザイナーで行ごとに編集します。" } }, fields: { name: { - label: "Name", - helpText: "snake_case unique identifier" + label: "名前", + helpText: "snake_case の一意識別子" }, label: { - label: "Label", - helpText: "Display name" + label: "表示名", + helpText: "表示名" }, description: { - label: "Description", - helpText: "What this dataset measures" + label: "説明", + helpText: "このデータセットが測定する内容" }, object: { - label: "Object", - helpText: "Base object — the FROM" + label: "オブジェクト", + helpText: "ベースオブジェクト — FROM に相当" }, include: { - label: "Include", - helpText: "Relationship (lookup / master_detail) field names to join — enables `relationship.field` dimensions/measures (e.g. include \"account\" → group by account.region)" + label: "含める関係", + helpText: "結合する関係(lookup / master_detail)のフィールド名 — `relationship.field` のディメンション/メジャーを有効にします(例: include \"account\" → account.region でグループ化)" }, filter: { - label: "Filter", - helpText: "Intrinsic scope filter (e.g. exclude soft-deleted records), ANDed into every query" + label: "フィルター", + helpText: "固有スコープのフィルター(例: 論理削除済みレコードを除外)。すべてのクエリに AND で結合されます" }, dimensions: { - label: "Dimensions", - helpText: "Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity" + label: "ディメンション", + helpText: "各ディメンション:名前(プレゼンテーションから参照)、フィールド、型、日付の場合は既定のバケット粒度" }, "dimensions.name": { label: "名前" diff --git a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts index 7d9ece71414..d9c2d702a29 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts @@ -36,30 +36,6 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "metadataForms.book.label": "b48c719759fca087", "metadataForms.capability.description": "1130a8a487c35f3d", "metadataForms.capability.label": "cb58b72c5cc77632", - "metadataForms.dataset.description": "ce0d0a4d536bbc1e", - "metadataForms.dataset.fields.description.helpText": "f82c5070e1f78508", - "metadataForms.dataset.fields.description.label": "779a4342c8eb6707", - "metadataForms.dataset.fields.dimensions.helpText": "a4df7393a6bd88c9", - "metadataForms.dataset.fields.dimensions.label": "d75e6dd9d87e93b4", - "metadataForms.dataset.fields.filter.helpText": "8d996f9121810664", - "metadataForms.dataset.fields.filter.label": "802895ec427fc6af", - "metadataForms.dataset.fields.include.helpText": "e8f9933ae4f99e77", - "metadataForms.dataset.fields.include.label": "45b27393fb9f3b2a", - "metadataForms.dataset.fields.label.helpText": "f0cbad8e334db1d3", - "metadataForms.dataset.fields.label.label": "c680364257b11569", - "metadataForms.dataset.fields.name.helpText": "8c3cae6dba12b42a", - "metadataForms.dataset.fields.name.label": "5274417ff1ccaf01", - "metadataForms.dataset.fields.object.helpText": "09a85579054a7197", - "metadataForms.dataset.fields.object.label": "d0c49032d95ebb36", - "metadataForms.dataset.label": "19647dcb84ce82d3", - "metadataForms.dataset.sections.basics.description": "844b82a322f8edd6", - "metadataForms.dataset.sections.basics.label": "55562f900da68c5d", - "metadataForms.dataset.sections.dimensions.description": "da19c8faa0cb1023", - "metadataForms.dataset.sections.dimensions.label": "d75e6dd9d87e93b4", - "metadataForms.dataset.sections.measures.description": "8186d1c0fe6a86f4", - "metadataForms.dataset.sections.measures.label": "395dac46abcf1943", - "metadataForms.dataset.sections.source.description": "ee9be5509026b678", - "metadataForms.dataset.sections.source.label": "8fe786e8e29c8cec", "metadataForms.doc.description": "f233af60238b0879", "metadataForms.doc.label": "3f35cf5088b999ad", "metadataForms.email_template.fields.variables.helpText": "27197774c2cda633", 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 71cc7c4c47c..6cadd2c0231 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 @@ -1475,54 +1475,54 @@ export const zhCNMetadataForms: NonNullable = } }, dataset: { - label: "Dataset", - description: "Analytics semantic layer — dimensions & measures", + label: "数据集", + description: "分析语义层 — 维度与度量", sections: { basics: { - label: "Basics", - description: "Dataset identity." + label: "基础信息", + description: "数据集标识" }, source: { - label: "Source", - description: "The base object, the relationships to join, and the dataset’s intrinsic scope. Joins are derived from the object graph — pick relationship (lookup / master_detail) names, never write an ON clause." + label: "数据来源", + description: "基础对象、要关联的关系,以及数据集的固有范围。关联由对象关系图推导 — 选择关系(lookup / master_detail)字段名,不要手写 ON 子句" }, dimensions: { - label: "Dimensions", - description: "Groupable axes. Use a base field, or `relationship.field` (e.g. account.region) for a relationship included above." + label: "维度", + description: "可分组的轴。使用基础对象的字段,或对上方已包含的关系使用 `relationship.field`(如 account.region)" }, measures: { - 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." + label: "度量", + description: "定义一次即可按名称引用的可聚合值。度量是某个字段的 sum/avg/count/…;派生度量由其他度量组合而成(ratio/sum/difference/product)。度量级筛选与派生运算在数据集设计器中逐行编辑" } }, fields: { name: { - label: "Name", - helpText: "snake_case unique identifier" + label: "名称", + helpText: "snake_case 唯一标识符" }, label: { - label: "Label", - helpText: "Display name" + label: "显示名称", + helpText: "显示名" }, description: { - label: "Description", - helpText: "What this dataset measures" + label: "描述", + helpText: "此数据集衡量什么" }, object: { - label: "Object", - helpText: "Base object — the FROM" + label: "对象", + helpText: "基础对象 — 即 FROM" }, include: { - label: "Include", - helpText: "Relationship (lookup / master_detail) field names to join — enables `relationship.field` dimensions/measures (e.g. include \"account\" → group by account.region)" + label: "包含关联", + helpText: "要关联的关系(lookup / master_detail)字段名 — 用于启用 `relationship.field` 维度/度量(例如 include \"account\" → 按 account.region 分组)" }, filter: { - label: "Filter", - helpText: "Intrinsic scope filter (e.g. exclude soft-deleted records), ANDed into every query" + label: "筛选", + helpText: "固有范围筛选(例如排除软删除记录),以 AND 方式并入每个查询" }, dimensions: { - label: "Dimensions", - helpText: "Each: name (referenced by presentations), field, type, and — for dates — a default bucketing granularity" + label: "维度", + helpText: "每个维度:名称(供展示层引用)、字段、类型,以及日期类型的默认分桶粒度" }, "dimensions.name": { label: "名称" diff --git a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts index 4746249617f..d279b182827 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts @@ -36,30 +36,6 @@ export const zhCNGeneratedSourceHashes: Readonly> = { "metadataForms.book.label": "b48c719759fca087", "metadataForms.capability.description": "1130a8a487c35f3d", "metadataForms.capability.label": "cb58b72c5cc77632", - "metadataForms.dataset.description": "ce0d0a4d536bbc1e", - "metadataForms.dataset.fields.description.helpText": "f82c5070e1f78508", - "metadataForms.dataset.fields.description.label": "779a4342c8eb6707", - "metadataForms.dataset.fields.dimensions.helpText": "a4df7393a6bd88c9", - "metadataForms.dataset.fields.dimensions.label": "d75e6dd9d87e93b4", - "metadataForms.dataset.fields.filter.helpText": "8d996f9121810664", - "metadataForms.dataset.fields.filter.label": "802895ec427fc6af", - "metadataForms.dataset.fields.include.helpText": "e8f9933ae4f99e77", - "metadataForms.dataset.fields.include.label": "45b27393fb9f3b2a", - "metadataForms.dataset.fields.label.helpText": "f0cbad8e334db1d3", - "metadataForms.dataset.fields.label.label": "c680364257b11569", - "metadataForms.dataset.fields.name.helpText": "8c3cae6dba12b42a", - "metadataForms.dataset.fields.name.label": "5274417ff1ccaf01", - "metadataForms.dataset.fields.object.helpText": "09a85579054a7197", - "metadataForms.dataset.fields.object.label": "d0c49032d95ebb36", - "metadataForms.dataset.label": "19647dcb84ce82d3", - "metadataForms.dataset.sections.basics.description": "844b82a322f8edd6", - "metadataForms.dataset.sections.basics.label": "55562f900da68c5d", - "metadataForms.dataset.sections.dimensions.description": "da19c8faa0cb1023", - "metadataForms.dataset.sections.dimensions.label": "d75e6dd9d87e93b4", - "metadataForms.dataset.sections.measures.description": "8186d1c0fe6a86f4", - "metadataForms.dataset.sections.measures.label": "395dac46abcf1943", - "metadataForms.dataset.sections.source.description": "ee9be5509026b678", - "metadataForms.dataset.sections.source.label": "8fe786e8e29c8cec", "metadataForms.doc.description": "f233af60238b0879", "metadataForms.doc.label": "3f35cf5088b999ad", "metadataForms.field.fields.placeholder.helpText": "07cecac0844860be",