Skip to content

Commit 1a2bb9e

Browse files
os-litantclaude
andauthored
feat(spec): a repeater property panel names its columns in every locale, not only English (#19345)
Fixes #17508 Clause-②: no — enumerating a repeater's row children in a `*.form.ts` and adding catalog leaves adds no key to a published payload and moves no accept set; the same author input parses identically before and after. Verified against the diff: no schema file is touched, no `.describe()` or `.meta()` moves, and no row child declares a `type` (so no new form input is offered for anything). ## What this lands 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.TYPE.fields['PATH.PROP']` key. #17232 (PR #17500) authored the English `.meta({ title })` on thirteen item schemas and #17505 / #17506 on four more, but none of them had a catalog channel: every one of those column heads reached a Chinese, Japanese or Spanish author in English. Both halves land together, as the ruling on this card requires: 1. **The form-child enumeration** — 112 row properties across fifteen repeaters, each `label` equal to the item schema's own `.meta({ title })` so the extractor's English source and the schema title stay one string. No `type` on a row child: the row widgets stay schema-derived. 2. **All four catalogs** — `en` (generated from those labels) plus hand-authored `zh-CN` / `ja-JP` / `es-ES` for every leaf. 3. **The pin**, `packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts`, in the shape `dashboard-header-children.test.ts` established for #17227, **including its last test**: a translated leaf is never a copy of its `en` source. ## The re-derived population The card's figures are a 2026-09-10 reading at `76ddab772f`. Re-derived on this branch's base through the platform's own predicate (`z.toJSONSchema(getMetadataTypeSchema(type), { unrepresentable: 'any', io: 'input' })`, the same instrument `packages/spec/src/kernel/repeater-item-titles.test.ts` uses), at `origin/main` = `e3b3cdd2df3`, 2026-09-20T11:12Z: | | card, 2026-09-10 | measured, 2026-09-20 | |---|---|---| | catalog leaves for full localisation | 604 | **604** (151 row properties x 4) | | leaves belonging to titled properties | 348 | **504** (126 x 4) | | row properties titled | 87 | **126** | | carriers fully titled | 14 | **18** | The total is unchanged; the titled share moved, exactly as the three sibling cards landed since would predict — #17505 paid `dashboard:widgets` (17) and `dashboard:globalFilters` (10), #17506 paid `field:options` and `object:fields.options` (6 each, one `SelectOptionSchema`). 87 + 27 + 12 = 126, and 14 + 4 = 18. Of the 504, **12 leaves already existed** (`dashboard:header.actions` from #17227, and four each on `object:fields.options` and `page:variables`), so **112 keys per locale are new** — 448 new leaves, 336 of them hand-authored translations. Controls on the census, as required: **lit** — `dashboard:header.actions` resolves with exactly `['label','actionUrl','actionType','icon']`, all titled; **dark** — `agent` / `tool` / `hook` / `position` declare no repeater at all and contribute nothing, and a fabricated carrier id resolves to nothing. Both are asserted in the pin, not only measured by hand. ## Translation quality, and how it was checked per locale A glossary was built mechanically from the pre-change catalogs: every `metadataForms.*` and `objects.*` leaf whose translated value differs from its `en` source, indexed by the English string — 1342 distinct English strings with at least one translation. Every new label was then looked up in it, and the catalog's **dominant** existing rendering reused where one existed: - `Label` → 显示名称 / 表示名 / Etiqueta (18 / 19 / 20 existing uses, and the exact values its `object.fields.options.label` twin already carries) - `Name` → 名称 / 名前 / Nombre (30 uses each) · `Type` → 类型 / 型 / Tipo (8) · `Description` → 描述 / 説明 / Descripción (18) - `Filter` → 筛选 / フィルター / Filtro · `Scope` → 范围 / スコープ / Ámbito · `Options` → 选项 / 選択肢 / Opciones - `Timeout (ms)` → the existing 超时(毫秒) / タイムアウト(ms) / Tiempo de espera (ms) · `Input Schema` follows the existing `Output Schema` → 输入 Schema / 入力スキーマ / Esquema de entrada - `field.options.*` mirrors its `object.fields.options.*` twin verbatim, `Color de opción` included — the same `SelectOptionSchema`, so the same words - domain nouns come from the carriers' own already-translated `helpText`: widget 组件 / ウィジェット / widget, dimension 维度 / ディメンション / dimensión, measure 度量 / メジャー / medida, node 节点 / ノード / nodo, edge 连线 / エッジ / conexión, region 区域 / リージョン / región Two deliberate departures, both stated rather than hidden: - **A bare `ID` is qualified by its row**, following the catalog's own `Reference ID` → 引用 ID / 参照 ID / ID de referencia: 节点 ID / ノード ID / ID de nodo. An unqualified `ID` would be byte-identical to its `en` source in all three locales, which the pin's last test refuses — and rightly, since a column head reading `ID` in a Chinese panel is the untranslated state this card exists to end. - **`Dataset` is translated** (数据集 / データセット / Conjunto de datos) although the catalogs carry it untranslated elsewhere. That existing `Dataset` is an `en`-echo — untranslated debt, not a chosen loanword — so this introduces a term for a concept the catalogs do not yet name rather than a second term for one they do. ## The one existing string that moves `page.variables`' children were enumerated (by #3786's round) **without labels**. The extractor therefore emitted `humanizeFieldPath(path)` as the English source, and `resolveMetadataFormSchemaTitles` wrote that text back over the item schema's authored title: the panel's column head read **`Source`** in every locale while `PageVariableSchema.source` declares `.meta({ title: 'Written By' })`. Two English names for one column, the unauthored one winning. The form now declares the label, so `en` becomes `Written By` and the three translations are re-authored with it (写入组件 / 書き込み元 / Escrito por). That is the **only** pre-existing leaf this PR changes; everything else is additive. The pin's new every-row-child-declares-a-label test is what makes the class impossible to re-enter. ## Fenced out, and seen - `view.columns` / `view.sort` / `view.tabs` — **untouched**. The census surfaces all three (14 + 2 + 9 = 25 untitled row properties), and all three enumerate no children, so they contribute nothing to this diff. Their titling is #17507's, which is open and unclaimed. The pin asserts `view` is outside its population, so #17507 will have to add the leaves with the titles — by design. - `retiredKey()` tombstones — **not titled**. The census excludes any row property whose description opens `[REMOVED] `, and `flow.nodes[].outputSchema` is the pinned dark control for that exclusion. - `object.fields.options` keeps its curated four-key subset (`default` and the per-option `visibleWhen` CEL predicate are **not** added as inputs) — that subset is a declared entry in `metadata-form-zod-reconciliation.test.ts`'s ledger, and adding them would widen the offered authoring surface against a recorded decision. ## Verification Base `origin/main` `81e12e186f3` (merged in), head `1cf7f2ae8ca`. Exit codes captured before any pipe throughout. - **Gate families** — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **83** commands on this tree; all 83 run with exit codes recorded and reconciled: `83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN` — "a DERIVED zero — all 83 recorded an exit code and none of them is 3". Two first returned `PREREQUISITE NOT MET` (exit **3**, not a finding): `check:i18n` and `check:dual-build-cjs-loads` both needed built output. Both re-run green after a full build — `check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys)`. - **Tests** — `@objectstack/spec`: 501 files / 14655 tests passed, `typecheck` clean. `@objectstack/platform-objects`: 41 files / 583 tests passed, `typecheck` clean. - **Repo-wide lint** — not narrowed: `eslint . --no-inline-config --format json` over eslint's own declared population of **6926** files at `1cf7f2ae8ca`, **0 errors, 0 warnings**, exit 0. - **Ablation — the pin can fail, twice, on both of its load-bearing halves.** Both legs rebuilt and verified against the artifact the suite actually resolves (`@objectstack/spec` is unaliased in `packages/platform-objects/vitest.config.ts`, so the forms come from `dist/`). - *Catalog leg* (no build needed — the catalogs are source to this suite): set `zh-CN` `report.blocks.chart` back to its `en` source. Result: `1 failed | 7 passed`, on `translated locales carry their own text for every leaf, not a copy of the source`. Restored; `git status --porcelain` empty. - *Form leg*: dropped `label: 'Operator'` from `skill.form.ts`'s `triggerConditions` child. Marker `label: "Operator"` in `packages/spec/dist`: **6** files at baseline → **0** after the mutate build → **6** after the restore build. Mutated run `2 failed | 6 passed`, naming `skill:triggerConditions.operator` on both `every enumerated row child declares a label` and `en: each leaf IS the form's declared label`. Restored run `8 passed`, `ablation-dist-preflight` green on presence **and** on a clean tree, source blob back to its `HEAD` hash byte-for-byte. - ⚠️ **The first form-leg attempt was a void reading and is reported as such.** Its marker was spelled `label: 'Operator'` (single quotes, as the source has it) while `tsup` emits `label: "Operator"` into `dist` — so the `--absent` pre-flight passed **vacuously**, on a marker that had never been in `dist` at all. The run above is the re-take with the corrected anchor; the numbers quoted are that run's. - **Serial constraints re-taken first-hand** at 2026-09-20T12:17Z, not inherited: the changed-file page of all **35** open PRs, **281** file rows. **No open PR holds any `*.form.ts`, and none holds the translation catalogs.** Firing control: the same map resolves `packages/spec/src/data/filter.zod.ts` to #19335 and `packages/spec/src/shared/polarity-axes.ts` to #19318, so it does see `packages/spec/src` holders. Dark control: `packages/spec/src/zzz-no-such.zod.ts` resolves to nothing. ## Acceptance notes Seen while working here, left alone — none is in this card's scope and none is filed: - `scripts/ablation-dist-preflight.mjs`'s `--absent` mode conflates two independent questions in one exit code. Its own header says `--absent` "is two things at once: the mode for a DELETE ablation, and the restore leg of a PLANT one", but its tree limb hard-codes the restore-leg reading — it prints `restore leg: ... 1 path still differs from HEAD` and exits **1** whenever the working tree is dirty. A DELETE ablation's *mutate* leg is necessarily dirty (the mutation is the dirt), so that shape can never pass its own pre-flight, and the failure text instructs the author to restore the very mutation being measured. Reproduced here: a driver that trusted the exit code aborted the ablation at exit 93 with the dist assertion already printed green above it. This is a reproducible defect in an instrument with a named repro, so it is proposed for a card in the report rather than fixed here. - `packages/spec/src/kernel/repeater-item-titles.test.ts`'s header states the bundle overlay "only ever REPLACES a `title` that is already there". `setSchemaTitleAtPath` in `packages/spec/src/system/i18n-resolver.ts` returns `{ ...node, title }` unconditionally, so it also *creates* one. The claim is inert for that file's verdicts (an untitled property with no catalog leaf is untouched either way), so this is a prose inaccuracy, not a defect. - `object.form.ts`'s `fields.options` comment reads "the offered inputs are exactly `SelectOptionSchema`'s authorable keys, minus `visibleWhen`". `default` is also not offered, so the sentence under-counts by one. The behaviour is correct and ledgered as a `subset` in `metadata-form-zod-reconciliation.test.ts`; only the comment's arithmetic is stale. --- _Generated by [Claude Code](https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent be7382d commit 1a2bb9e

16 files changed

Lines changed: 1839 additions & 25 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
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
7+
8+
Clause-②: no
9+
10+
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.<type>.fields['<path>.<prop>']` 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.
11+
12+
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.
13+
14+
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.
15+
16+
`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).
17+
18+
`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.

0 commit comments

Comments
 (0)