Repository navigation
Commit 1a2bb9e
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.
- 1 parent be7382d commit 1a2bb9e
16 files changed
Lines changed: 1839 additions & 25 deletions
File tree
- .changeset
- packages
- platform-objects/src/apps/translations
- spec/src
- ai
- automation
- data
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
0 commit comments