Skip to content

feat(spec): a repeater property panel names its columns in every locale, not only English - #19345

Merged
os-litant merged 5 commits into
mainfrom
claude/issue-17508-repeater-row-property-localisation
Sep 20, 2026
Merged

os-litant merged 5 commits into
mainfrom
claude/issue-17508-repeater-row-property-localisation

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

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 catalogsen (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 feat(spec): item-level property names for the dashboard header-action repeater #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: litdashboard:header.actions resolves with exactly ['label','actionUrl','actionType','icon'], all titled; darkagent / 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

Verification

Base origin/main 81e12e186f3 (merged in), head 1cf7f2ae8ca. Exit codes captured before any pipe throughout.

  • Gate familiesnode 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 feat(spec)!: publish the $-prefix key ban the normalized filter enforces, and make the ratchet able to see it #19335 and packages/spec/src/shared/polarity-axes.ts to fix(spec): answer an axis-silent key with both ends of the range, not the cheaper-spelled one #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

Studio renders a `type: 'repeater'` as a table whose column names come from
the form's declared row children when there are any, and from the JSON Schema
`items.properties[k].title` otherwise. `os i18n extract` only emits a
`metadataForms.<type>.fields['<path>.<prop>']` key for a DECLARED child, so an
un-enumerated repeater had no catalog channel at all: its column heads read
the English `.meta({ title })` in every locale.

Enumerate all 112 row properties of the fifteen repeaters that declared none,
each `label` equal to the item schema's own title so the extractor's English
source and the schema title stay one string. No `type` on a row child — the
row widgets stay schema-derived. `page.variables` and `object.fields.options`
already enumerated their children without labels, which let the extractor's
humanized fallback override the authored title (`variables.source` read
"Source" against the schema's "Written By"); both now carry explicit labels.

`view.columns` / `view.sort` / `view.tabs` are untitled and belong to #17507.
`object.fields.options` stays the curated four-key subset its #3786 ledger
entry declares.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
…n all four catalogs

`os i18n extract` now emits a `metadataForms.<type>.fields['<path>.<prop>']`
key for each of the 112 row properties the forms enumerate, and this lands the
`en` source plus hand-authored `zh-CN` / `ja-JP` / `es-ES` copy for every one
of them — the Studio property panel's repeater column heads read in the
author's own language instead of English in every locale.

Each term reuses the word the catalogs already use for the concept: `Label` is
the dominant 显示名称 / 表示名 / Etiqueta, `Filter` 筛选 / フィルター / Filtro,
`Widget` 组件 / ウィジェット / Widget, `Timeout (ms)` the existing
超时(毫秒)/ タイムアウト(ms)/ Tiempo de espera (ms), and `field.options.*`
mirrors its `object.fields.options.*` twin verbatim, `Color de opción`
included. A bare `ID` is qualified by its row the way `Reference ID` already
is (节点 ID / ノード ID / ID de nodo).

`page.variables.source`'s English source moves from the extractor's humanized
"Source" to the schema's authored "Written By", so its three translations are
re-authored with it; every other leaf here is new.

The pin is `repeater-row-properties.test.ts`, in the shape
`dashboard-header-children.test.ts` established for #17227 and with its last
test: a translated leaf is never a copy of its `en` source. Its population is
derived from the forms, so a repeater enumerated later is red on the day it
lands.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 130 documentable anchor(s).

56 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13d52947d81aca235133ee9619d80723a7c63348.

11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13d52947d81aca235133ee9619d80723a7c63348packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5b815bb980deee0b62bf7588af5a42bad89a3987 — the merge of head 74dcce59635a0b7d0f1f5b487a0e8dc0bd806da1 into base 13d52947d81aca235133ee9619d80723a7c63348, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5b815bb980deee0b62bf7588af5a42bad89a3987 && git checkout 5b815bb980deee0b62bf7588af5a42bad89a3987
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13d52947d81aca235133ee9619d80723a7c63348 74dcce59635a0b7d0f1f5b487a0e8dc0bd806da1 && git checkout -B drift-repro 13d52947d81aca235133ee9619d80723a7c63348 && git merge --no-ff 74dcce59635a0b7d0f1f5b487a0e8dc0bd806da1

node scripts/docs-audit/affected-docs.mjs --json 13d52947d81aca235133ee9619d80723a7c63348

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13d52947d81aca235133ee9619d80723a7c63348 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 74dcce59635a0b7d0f1f5b487a0e8dc0bd806da1

Reviewed in a detached worktree at that sha (git fetch origin refs/pull/19345/head, 2026-09-20T12:53Z). The head is the merge of 1cf7f2ae8ca (the dev's last commit) and 13d52947d81 (origin/main at the PM's 12:50Z merge); origin/main had moved one commit further, to 2277d1fcd10, by 12:53Z, so this head carries main-as-of-12:50Z, not current main. Every number below is my own reading on 74dcce59 unless it says otherwise; nothing is adopted from the PR body or the dev report, whose readings were taken on 1cf7f2ae8ca.

① Derived judgments

Accept set — unchanged, re-derived. Net diff vs 13d52947d81: 16 files — 10 packages/spec/src/**/*.form.ts, 4 catalogs, 1 pin, 1 changeset. No *.zod.ts, no .describe() or .meta() moves, no export moves. check-widening-tells --declaration no over the diff: 10 files judged against a declared surface, no tell; 6 NOT MEASURED (the four catalogs, the pin, the changeset — no declared surface covers them); exit 0.

The offered surface — this is where the line is, and it holds, with one derived side effect. Read at the pinned objectui 53ded82bf7a4 (SchemaForm.tsx): a repeater renders exactly the form's declared children as columns when any are declared, and Object.keys(items.properties) when none are (tombstones included); each cell's widget is inferWidget(spec, subSchema), which falls through to the sub-schema when the child declares no type; the column head is the declared label in the grid layout and, in the card layout, fieldSpec.label before the schema title. So enumeration REPLACES the column set, and the question is whether each declared set equals the schema's. Re-derived on the dist built from this head in the worktree (z.toJSONSchema(getMetadataTypeSchema(type), { unrepresentable: 'any', io: 'input' }), 13:05Z): 21 object-row carriers, 151 live row properties (6 [REMOVED] tombstones excluded), 126 titled, 25 untitled — all 25 on view:columns (14) / view:sort (2) / view:tabs (9). On the 18 enumerated repeaters: every declared child is a key of its item schema (0 form-only), none is a tombstone (0 retired), and every declared label equals the schema title (0 mismatches over 120 declared children). The only live keys not declared are object:fields.options.default and .visibleWhen, which is the subset ledger row in metadata-form-zod-reconciliation.test.ts. Lit control: dashboard:header.actions resolves to label / actionUrl / actionType / icon, all titled; dark: a fabricated type resolves to nothing; action.locations is a string-enum array, not an object-row carrier; view.columns is the union case the dispatch warned about (a string-array arm, a 14-property object arm, an integer arm on another view kind). Derived side effect worth naming: the 6 tombstoned item properties (5 on dashboard:widgets, 1 on flow:nodesoutputSchema) that an un-enumerated repeater used to render as columns are no longer offered. That narrows the RENDERED surface toward the accept set — retiredKey() already refused any save that filled them — and widens nothing. Clause-②: no stands as declared.

Same author input parses identically. No schema is touched; the reconciliation test walks every nested list recursively and asserts formOnly / retired / zodOnly empty, so together with the census above it is the mechanical proof, and CI's Test Core is its run of record.

Catalogs, measured from the four files at base and head: 112 new en keys, each present in zh-CN / ja-JP / es-ES (112 / 112 / 112), 336 translated leaves; exactly ONE pre-existing key changes value, in all four locales: page.fields.variables.source.label — en Source to Written By, zh-CN 来源 to 写入组件, ja-JP ソース to 書き込み元, es-ES Origen to Escrito por. Everything else is additive.

The moved string — story verified on the tree. At base page.form.ts enumerated the four variables children without labels; i18n-extract.ts line 1842 emits inlineText(field.label) ?? humanizeFieldPath(path) as the en source, so the en leaf read Source; setSchemaTitleAtPath (i18n-resolver.ts line 3187) returns { ...node, title }, so the served schema's authored Written By (page.zod.ts line 461) was overwritten. The head declares label: 'Written By', and the pin's every-row-child-declares-a-label test makes the class impossible to re-enter. The change is stated where a consumer sees it: the changeset body, which ships as CHANGELOG, names the key, the en move and all three re-authored translations. Runtime consequence checked: the *.source-hashes.generated.ts provenance tables record hashes only for extractor FILLS (source-hash.test.ts line 401 pins that), variables.source has no record in any locale, so the re-authored leaves are served rather than marked stale; check-i18n-stale-fill exit 0 on this tree.

Translation quality — 336 of 336 checked mechanically, 336 of 336 read. Mechanical: 0 echoes (no translated leaf equals its en source); against a glossary of every base leaf whose translation differs from its source (1346 en strings), 153 leaf-translations reuse the catalog's existing rendering of the same English string, 183 had no exact-string precedent, and 0 introduce a competing rendering for a string the catalog already translates (Label → 显示名称 / 表示名 / Etiqueta, Filter → 筛选 / フィルター / Filtro, Timeout (ms) → 超时(毫秒)/ タイムアウト(ms)/ Tiempo de espera (ms), Input Schema beside the existing Output Schema → 输入 Schema / 入力スキーマ / Esquema de entrada). Concept level, read against the repeaters' own existing labels: widget → 组件 / ウィジェット / widget, region → 区域 / リージョン / región, block → 分块 / ブロック / bloque, node → 节点 / ノード / nodo, edge → 连线 / エッジ / conexión all match. compareTo → 对比区间 / 比較期間 / Comparar con is right for a value shaped { kind: 'previousPeriod' | 'previousYear' }; carryOver → 沿用行值 / 値の引き継ぎ / Heredar de la fila matches "seed the value from the current row"; Values → 度量 / メジャー / Medidas is a semantic rendering of a field the schema describes as "Measure names — Y", consistent with the catalogs' own 每个度量 / 各メジャー / Cada medida help text. The two departures the PR declares are both measured true: a bare ID would be a byte-identical echo in all three locales, and Dataset is an en-echo at base in all three (report.fields.dataset.label), so 数据集 / データセット / Conjunto de datos introduces no second term. Nothing I would call wrong. Two weak spots, neither a defect: ja-JP app.areas.id = エリア ID while the areas repeater's own label is 領域 (its help text already says エリア — the catalog is split); zh-CN flow.edges.type = 连接类型 beside the repeater label 连线 (连线类型 would echo the catalog's own noun).

The pin is non-vacuous — measured, not inferred. Its population is derived from the forms (lit floor: more than 100 row properties, and dashboard:header.actions present; dark: agent / tool / hook / position / view contribute nothing), and its last test compares every translated leaf to its en source by strict equality. Ablation on this head, catalog leg, @objectstack/spec built in the worktree under the verify lock (13:04Z): baseline 8 passed; zh-CN skill:triggerConditions.operator set to its en source Operator — blob 5df9a8c to 819e861, 1 injected line, 0 removed-text lines; mutated run 1 failed, 7 passed, on exactly that test, naming zh-CN skill:triggerConditions.operator ("Operator"); restore via git checkout HEAD --, blob back to 5df9a8c, porcelain empty; restored run 8 passed (13:07Z). Shape against the precedent dashboard-header-children.test.ts: per-locale presence, en-leaf-is-the-declared-label, translated-never-a-copy — all three tests are there, plus the survey controls.

The dev's void reading and its re-take. Confirmed on this tree: label: "Operator" is present in 6 non-map dist files (grep exit 0) and label: 'Operator' in 0 (exit 1); lit label: "Written By" 6 files, dark fabricated marker 0. The single-quoted marker was never in dist, so the first --absent pre-flight passed vacuously exactly as disclosed; the re-take (6 → 0 → 6 across the mutate and restore builds, 2 failed naming the key, 8 passed restored) is the sound one. The --absent tree-limb defect is #19348 and is not routed here.

Exclusions — each real and recorded. view.form.ts is not in the diff and view is a pinned dark control (#17507); flow.nodes[].outputSchema and the 5 dashboard.widgets[] tombstones are excluded by the [REMOVED] predicate and none is declared; object.fields.options keeps 4 of 6 under the ledgered subset row. field.options now declares all 6 SelectOptionSchema keys (label, value, description, color, default, visibleWhen) — the full set, so nothing is widened there either.

PR-body arithmetic, one loose sentence: "12 leaves already existed … so 112 keys per locale are new" — it is 12 pre-existing KEYS (48 leaves: header.actions 4, page.variables 4, object.fields.options 4) plus the 2 titled-but-ledgered object.fields.options keys: 126 − 12 − 2 = 112. The counts are right; the noun is wrong.

② Semver level

@objectstack/spec 17.4.0 and @objectstack/platform-objects 17.4.0 both publish (private unset; spec ships dist and src/**/*.zod.ts, platform-objects ships dist). Changeset .changeset/17508-repeater-row-property-localisation.md: minor / minor, Clause-②: no, no ADR-0087 marker — none is owed, nothing an author can write is removed or renamed. Judged against what moved: spec gains declared row children on 15 repeaters (additive metadata on exported forms) and one authored label; platform-objects gains 448 leaves and moves one. A feature, not a fix; no may take minor; minor is the right level for both, and the body carries the one moved string. Consistent with the declaration.

③ Boundary flags

The dev raised no open_questions. Residuals for the seat, none blocking:

  1. Ledger prose drift, same class as the dev's own two notes: metadata-form-zod-reconciliation.test.ts line 121, the object.fields.options subset row's why, still says field.form.ts's options repeater "is schema-derived and so offers every SelectOptionSchema key". After this PR it is declared, not schema-derived — and still offers every key (6 of 6), so the ledger's substance holds; only the sentence is stale.
  2. Pre-existing en-echo debt on the same panels, outside this card: at base, report.fields.values, report.fields.rows, report.fields.dataset and dataset.fields.measures labels are byte-identical to en in all three locales (12 leaves). This PR now names those concepts natively one level down (度量 / データセット …), so the report and dataset panels read a translated column head under an untranslated section field. A card, not a rider.
  3. Main moved after the PM's merge: this head carries origin/main as of 12:50Z (13d52947d81); 2277d1fcd10 has landed since. The queue rebuilds on current main; no action unless it ejects.
  4. CI on this head — read at 2026-09-20T13:11:29Z via GET /commits/74dcce5963/check-runs, latest run per check name (39 runs, total_count 39, every run on this head): 35 names — 1 in_progress (Lint & Repo Gates); 4 skipped (Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)); 30 success. Nothing red at posting time; the in-progress set is the honest reading, and the PM owns convergence — this verdict is a contract reading, not a green read.

Implemented-by: claude/issue-17508-repeater-row-property-localisation
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Repeater row-property localisation: 604 catalog leaves across four locales — who authors the translated copy?

2 participants