Repository navigation
Commit 9529989
fix(platform-objects): decide the page Interface panel en-echoes per leaf, and derive the panel pin (#19428)
Part of #19403
Clause-②: no
## What this is
Round 2 of this card. One panel family, decided **leaf by leaf**. The
card's deliverable is a per-leaf judgement with its reason recorded, not
30 translations; every leaf below was decided on its own evidence, and
the verdicts with their reasons live in a ledger the suite reads
(`page-interface-panel-echo-decisions.test.ts`), not only in this body.
**The family: the `page` Interface panel** —
`page.fields['interfaceConfig*']` plus the `page.sections.interface`
heading it sits under. This is where a list page is authored: which
object it reads, which columns it shows, how end users filter it, what
the toolbar offers. It is the largest family the round-1 table left (15
of the 65 keys).
Not `Fixes` — this is one family of the card's population, and the card
outlives this round. **Seven families remain** (50 label keys), tabled
at the foot of this body.
## The reading, re-taken on this base — the numbers are NOT inherited
Census instrument: flatten every `*.metadata-forms.generated.ts` bundle
and compare `.label` leaves against the `en` bundle. Population fixed
here: the `.label` leaves of the metadata-form catalogs (the card's
population, 538 leaves), **plus the sibling `helpText` leaf of every key
this round decides, plus the `description` leaf of the section** — a
panel whose field name is Chinese and whose tooltip is English is the
same defect half fixed.
Measured on worktree base `576d5df66`:
| reading | result |
|:--|:--|
| `en` string leaves in the metadata-form catalogs | 893 |
| of which `.label` leaves | 538 |
| POSITIVE CONTROL — labels genuinely translated | **473** (`zh-CN`) ·
**457** (`ja-JP`) · **457** (`es-ES`) |
| labels echoing the `en` source, per locale | 65 · 81 · 81 |
| label keys echoing in ALL THREE locales | **65** ⇒ 195 leaves |
The control is what makes the echo count mean something: 473 translated
against 65 echoing is a discrimination, not a parser that matches
everything.
**This is 65 / 195, the same reading round 1's LANDED comment recorded
on the trunk** — this round inherited no drift, and that is itself a
measurement rather than an assumption: four commits landed on
`origin/main` between my base and this push (`50bfe137c`, `7ffddfa49`,
`f9e16d856`, `488f4f546`) and `git diff 576d5df..origin/main --
packages/platform-objects/src/apps/translations/` is **empty**. The
card's own banner exists because a sibling PR moved this population
under a previous round; it did not happen here, and the way to know is
to look.
**After this change**: 50 keys / 150 leaves echo in all three locales;
control **488** · **472** · **472** — echoes down 15 and the control up
15, same population, same run.
## The verdicts — 15 keys × 2 string leaves × 3 locales = 90 decisions
Every one is `translate`, and **not one of them is "translate because it
is English"**. The echo direction was tested per leaf by asking what
this catalog's own authored corpus answers for that term.
- **The authored twin.** `interfaceConfig` inlines the view's own
surface onto the page — the form's own helpText says "The page IS the
view" — and `view.fields.<same key>` is authored in all three locales
for most of the family: `columns` (列 / 列 / Columnas), `sort` (排序 / 並び替え
/ Orden), `appearance` (外观 / 外観 / Apariencia), `userFilters` (用户筛选器 /
ユーザーフィルター / Filtros de usuario), `userActions` (用户操作 / ユーザーアクション /
Acciones de usuario), `addRecord` (添加记录 / レコードを追加 / Agregar registro),
`showRecordCount` (显示记录数 / レコード数を表示 / Mostrar recuento de registros). A
translator who judged the English right here would have had to judge it
wrong one panel away.
- **Decided AGAINST both of its same-string precedents** —
`interfaceConfig.source.label`. This catalog answers the bare word
`Source` twice, in opposite directions: `hook.fields.body.source` is 源码
/ ソース / Código fuente (source CODE) and `sys_metadata.fields.source` is
来源 / ソース / Origen (provenance). This leaf is neither — its own helpText
says "Object this page reads from". So `zh-CN` and `es-ES` depart from
**both** (数据来源 / Origen de datos) while `ja-JP` keeps ソース, the word both
precedents already use and the one that carries the data-source sense
unambiguously in Japanese. Per locale, not per string.
- **Decided AGAINST a same-string precedent, the other way** —
`interfaceConfig.userFilters.helpText`.
`view.fields.userFilters.helpText` translates its `dropdown / tabs /
toggle` (下拉 / 标签页 / 开关), but those are the stored `element` VALUES. The
`None / Tabs / Dropdown` here are the visible option labels of the
`filter-mode` widget, which objectui renders and this catalog holds no
key for — translating them would assert a rendering this package cannot
deliver. Kept **English**, the way this same panel's authored
`page.fields.type.helpText` keeps the quoted option labels "List /
Interface". (The precedent it departs from turns out to be a defect in
its own right — see Acceptance notes.)
- **Machine tokens and product terms kept English inside translated
prose** — the five literal key names in `interfaceConfig.helpText`
(`source`, `columns`, `filterBy`, `appearance.allowedVisualizations`,
`userActions`), on the authored precedent of
`view.fields.appearance.helpText`, which keeps `allowedVisualizations`
verbatim in all three locales; the renderer tokens `Grid / Kanban /
Calendar / …`, on this same panel's authored
`page.fields.type.helpText`, which keeps `grid / kanban / calendar / …`;
and `Interface` and `Airtable`, on `page.fields.type.helpText` and
`view.sections.end_user_controls.description` respectively.
- **A distinction the source draws, preserved** —
`interfaceConfig.levels.helpText` says "tree-like sources".
`object.fields.fields.reference.helpText` is authored on both sides of
that line in all three locales: it keeps the machine token (`tree 字段` /
`tree フィールド` / `un campo tree`) and translates the shape in the same
sentence (树 / ツリー / árbol). This one is the shape, so it is translated.
- **Panel-internal control** for the prose leaves: each carried an entry
in this package's per-locale `source-hashes.generated.ts` provenance
table in all three locales, while all 43 authored neighbours on the same
`page` panel carried none in any of them — which is what an unauthored
extractor fill looks like here.
Per-leaf reasons for all 30 leaves are in the ledger; this list is the
summary, not the record.
## The instrument, extended not reinvented
`page-interface-panel-echo-decisions.test.ts` is the #19355 shape as
round 1 left it: one row per leaf, verdict per locale, reason, and the
`en` source pinned EQUAL to the live bundle so rewording the English
reds the row instead of leaving a decision standing over text nobody
judged. Both of round 1's deliberate additions are carried over:
1. **The echo-reason rule is a predicate with a dark control.** Every
verdict here is `translate`, so asserting the rule over the ledger alone
evaluates nothing — the suite feeds it a synthetic `echo` row, asserts
it is refused, then asserts it is accepted once a departure reason is
added.
2. **The panel population is DERIVED from the `en` catalog**, not
hand-listed, so the pin cannot drift into a maintained list.
Three things this panel let the instrument gain that the earlier rounds
could not:
- **An authored control INSIDE the family.** `interfaceConfig.sort` is
`type: 'repeater'`, so its two row properties
(`interfaceConfig.sort.field` / `.order`) are the one slice of this
panel that `repeater-row-properties.test.ts` already derives — and they
are the **only two** leaves under `interfaceConfig` that were already
translated (字段 / 排序方向, フィールド / 並び方向, Campo / Dirección). Everything the
existing pin could see was fine; everything it could not see was an
echo. That is the card's 「0 of the keys sit inside it」 measured on this
panel, with the covered slice as its own control, and the suite asserts
both halves.
- **A SECOND, independent witness: the provenance table.** The ledger
now also asserts that no decided leaf is still recorded as an extractor
fill in any of the three `source-hashes.generated.ts` companions. It
fires on a different trigger than the catalog assertion (a re-fill
followed by an extract, rather than the re-fill itself) and it is
per-locale, so it is not blind in the case `check:i18n-stale-fill`'s
cross-locale witness is blind. It carries its own dark control: the
lookup is proved to find a key the table does hold, matched by pattern
rather than pinned to a family, so a later round of this card cannot red
it by fixing one.
- **The population covers the section heading too.**
`page.sections.interface` is a `.label` + `.description` pair that no
`.label`-only census counts in full; the derived walk takes both.
## Regeneration — the silent-loss hazard on this path
Translated-locale values are hand-written; the provenance tables are
not. Regenerated with **`pnpm i18n:extract`** (the command this
package's extract config documents), never by hand, and **no key was
added or removed** — so the `merge=os-regen` gap on
`*.metadata-forms.generated.ts` (`git check-attr merge` reads
`unspecified` on these paths) has nothing to lose even in principle.
Measured rather than asserted: the bundle diff is **30 insertions / 30
deletions** in each of the three translated bundles — a pure value
replacement — and the key lines of all four bundles are byte-identical
lists before and after (1465 in each, same order). The extract's own
diff is **0 insertions / 30 deletions** per provenance table: the 30
rows per locale that now record these leaves as authored. The `en`
bundle and all four `*.objects.generated.ts` bundles are untouched.
`pnpm check:i18n` green is the proof that the hand-written values are
byte-identical to what a fresh extract writes, across all nine bundle
sets.
## Reverse verification — the new pin can fail, in three independent
places
Committed first, then ablated through `scripts/ablation-replace.mjs`
(anchor must hit; on-disk counts and blob hashes are the tool's own
verdict). No rebuild is needed for these: the ledger imports the bundles
by relative path inside its own package, so nothing here resolves
through a package `exports` to `dist/` — this package's one vitest alias
is for `@objectstack/lint` and touches none of it.
**Ablation A — one decided leaf reverted to its English source in
`zh-CN`:**
- anchor `label: "数据来源",` x1 → x0 · replacement `label: "Source",` x2 →
x3 · blob `71cc7c4c47c0` → `8a8147b989ed`
- suite under the mutation: **2 failed | 16 passed** — the enumerated
verdict (`zh-CN page.fields.interfaceConfig.source.label reads its en
source again`) AND the derived panel pin, independently
- restored: blob back to `71cc7c4c47c0` == HEAD, `git diff HEAD` empty
**Ablation B — one provenance row re-added for a decided leaf** (a row
swapped in `zh-CN.source-hashes.generated.ts`, so the table claims the
leaf is still a byte copy of its source):
- anchor x1 → x0 · replacement x0 → x1 · blob `4746249617fb` →
`5a5f08eea895`
- suite under the mutation: **1 failed | 17 passed** — and it is a
**different** assertion than A's two (`zh-CN: no decided leaf is still
recorded as an extractor fill`), so the second witness is not a
restatement of the first
- restored: blob back to `4746249617fb` == HEAD, `git diff HEAD` empty
Suite restored and re-run: **18 passed**, working tree clean.
## Verification
- `pnpm --filter @objectstack/platform-objects test` — 45 files, **628
passed** (610 before; +18 from the new ledger)
- `pnpm --filter @objectstack/platform-objects typecheck` — green; its
`tsconfig.test.json` program reaches the new file (`--listFiles` names
it) and it contributes no errors
- `pnpm check:i18n` — **green**, 9 packages in sync. Its first run
exited **3** (PREREQUISITE NOT MET), which is not a reading; cleared by
building the closure the gate itself names, then re-run green
- `pnpm lint` — repo-wide, `eslint . --no-inline-config`, **exit 0**, at
`9adb9f774` (the head of this branch; no commit has landed since that
run)
- Gate families derived with `scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack` and reconciled with `--ran` carrying every
exit code: **60 derived · 59 run green · 0 unrun · 1 NOT MEASURED** —
`pnpm check:dual-build-cjs-loads` exits 3 (PREREQUISITE NOT MET: it
reads built output for every package in the repo). That prerequisite is
a whole-repo build, which is CI's `Build Core`, and this diff adds no
package and changes no export.
## Changeset, measured
`@objectstack/platform-objects` declares no `private` and ships `files:
["dist", …]`, and `src/metadata-translations/index.ts` imports all three
translated bundles. Probed on the built output: three new leaves
(`Origen de datos`, and 数据来源 / 階層レベル in their esbuild-escaped form) are
present in `dist/metadata-translations/index.js`, `dist/index.js` and
`dist/plugin.js`, with three pre-existing translated leaves as positive
controls and two negative controls (an invented string, and the raw
un-escaped CJK). ⇒ the diff publishes, `skip-changeset` would be wrong,
and a **`patch`** changeset is included.
One line for whoever probes this dist next, refining the lesson round 1
recorded. Round 1 wrote that "tsup emits non-ASCII as `\uXXXX` escapes",
which is right but not enough to write a working probe: esbuild emits
**upper-case** hex, and uses `\xNN` for U+0080–U+00FF. My first probe
spelled the escape in lower case and came back **0 hits including its
own positive control** — the same failure round 1 warned about, caught
the same way. The raw-CJK negative control above is in the probe
precisely so the next reader can see that the zero means "escaped", not
"absent".
## The seven families that remain (50 label keys, all-three-locale
echoes, this base)
| family | keys |
|:--|--:|
| `dataset.fields.*` + `dataset.sections.*` + `dataset.label` | 12 |
| `object.fields.enable.*` (+ `validations`) — the object capabilities
panel | 9 |
| `field.fields.*` — the standalone field panel (several keys have a
precedent from round 1) | 9 |
| `action.fields.body.*` + `action.fields.ai` | 6 |
| metadata-type display names with no panel: `seed` · `mapping` · `api`
· `doc` · `book` · `capability` | 6 |
| `hook.fields.retryPolicy.*` + `hook.fields.body.memoryMb` +
`hook.fields.timeoutMs` | 5 |
| `report.fields.drilldown` + `report.fields.runtimeFilter` +
`report.sections.dataset_binding` | 3 |
The `page` type is now absent from the echo census **and** from the
`metadataForms.page.*` half of all three provenance tables — 0 rows
remain there, where 30 per locale stood on this base.
## Acceptance notes
To file (class (c), a metadata-authoring trap — dedupe words: `helpText
translates enum values` · `userFilters element dropdown tabs toggle` ·
`translated locale names illegal value` · `metadata form tooltip enum
spelling` · `strict enum rejects translated token`):
- `view.fields.userFilters.helpText` names the legal values of
`UserFiltersSchema.element` — a `z.enum(['dropdown', 'tabs', 'toggle'])`
inside a `strictObject` — and all three translated locales render those
three tokens as words: 控件样式(下拉 / 标签页 / 开关), 要素スタイル(ドロップダウン / タブ / トグル),
estilo de elemento (desplegable / pestañas / interruptor). An author
working in one of those locales is told the element styles are 下拉 /
ドロップダウン / desplegable; writing that is refused by the enum, and the
tooltip is the only place the panel names them. This is the inverse of
this card's defect and it is what made this round keep `None / Tabs /
Dropdown` English. Seam: `spec:UserFiltersSchema.element` → renderer:
objectui `plugin-list/src/UserFilters.tsx`. Not fixed here: the
bounded-fix exemption needs the finding to be the same defect class as
the card, and this is the opposite one.
Noted, not filed:
- `repeater-row-properties.test.ts` filters on `spec.type ===
'repeater'`. Round 1 measured the `record` blind spot; this round
measures the `composite` one: `page.fields.interfaceConfig` is declared
`{ field: 'interfaceConfig', type: 'composite' }` with 15 children, and
that derivation therefore yielded exactly the two
`interfaceConfig.sort.*` row properties and none of the 30 leaves
decided here. Closed for THIS panel by the derived pin this PR adds; the
same blind spot remains for any other `composite` or `record` child.
Successor: the remaining-family rounds, each of which will meet it.
- `report.sections.*` (label and description) — the population the card
records as having no home. Still uncounted by any `.label` census and
unseen by any pin. Successor: the `report` family row above, if the seat
dispatches it. Note the same shape now has one fewer instance:
`page.sections.interface` was exactly such a pair and is decided here,
so the class is reachable, not structural.
- `page.fields.interfaceConfig.appearance.helpText` writes the renderer
tokens capitalised (`Grid / Kanban / Calendar`) while
`page.fields.type.helpText` on the same panel writes them lower-case
(`grid / kanban / calendar`) and `VisualizationTypeSchema`'s values are
lower-case. Checked because a capitalisation split looked like the same
trap as the finding above: it is softer — the sentence reads as display
names of the renderers rather than as the literal whitelist entries, and
the neighbouring helpText spells the values correctly one row up.
Recorded as a boundary of that class, not a second instance. Successor:
none.
- The eight bundle sets other than this one still do not opt into the
provenance table (`--source-hashes`), so their generated leaves stay
legacy-trusted and no per-leaf control like this round's exists for
them. Documented in the extract config as deliberate; not this card's.
承接者:无.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c27e160 commit 9529989
8 files changed
Lines changed: 778 additions & 180 deletions
File tree
- .changeset
- packages/platform-objects/src/apps/translations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
Lines changed: 30 additions & 30 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
781 | 781 | | |
782 | 782 | | |
783 | 783 | | |
784 | | - | |
785 | | - | |
| 784 | + | |
| 785 | + | |
786 | 786 | | |
787 | 787 | | |
788 | 788 | | |
| |||
852 | 852 | | |
853 | 853 | | |
854 | 854 | | |
855 | | - | |
856 | | - | |
| 855 | + | |
| 856 | + | |
857 | 857 | | |
858 | 858 | | |
859 | | - | |
860 | | - | |
| 859 | + | |
| 860 | + | |
861 | 861 | | |
862 | 862 | | |
863 | | - | |
864 | | - | |
| 863 | + | |
| 864 | + | |
865 | 865 | | |
866 | 866 | | |
867 | | - | |
868 | | - | |
| 867 | + | |
| 868 | + | |
869 | 869 | | |
870 | 870 | | |
871 | | - | |
872 | | - | |
| 871 | + | |
| 872 | + | |
873 | 873 | | |
874 | 874 | | |
875 | 875 | | |
| |||
878 | 878 | | |
879 | 879 | | |
880 | 880 | | |
881 | | - | |
882 | | - | |
| 881 | + | |
| 882 | + | |
883 | 883 | | |
884 | 884 | | |
885 | | - | |
886 | | - | |
| 885 | + | |
| 886 | + | |
887 | 887 | | |
888 | 888 | | |
889 | | - | |
890 | | - | |
| 889 | + | |
| 890 | + | |
891 | 891 | | |
892 | 892 | | |
893 | | - | |
894 | | - | |
| 893 | + | |
| 894 | + | |
895 | 895 | | |
896 | 896 | | |
897 | | - | |
898 | | - | |
| 897 | + | |
| 898 | + | |
899 | 899 | | |
900 | 900 | | |
901 | | - | |
902 | | - | |
| 901 | + | |
| 902 | + | |
903 | 903 | | |
904 | 904 | | |
905 | | - | |
906 | | - | |
| 905 | + | |
| 906 | + | |
907 | 907 | | |
908 | 908 | | |
909 | | - | |
910 | | - | |
| 909 | + | |
| 910 | + | |
911 | 911 | | |
912 | 912 | | |
913 | | - | |
914 | | - | |
| 913 | + | |
| 914 | + | |
915 | 915 | | |
916 | 916 | | |
917 | 917 | | |
| |||
Lines changed: 0 additions & 30 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | 139 | | |
170 | 140 | | |
171 | 141 | | |
| |||
Lines changed: 30 additions & 30 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
781 | 781 | | |
782 | 782 | | |
783 | 783 | | |
784 | | - | |
785 | | - | |
| 784 | + | |
| 785 | + | |
786 | 786 | | |
787 | 787 | | |
788 | 788 | | |
| |||
852 | 852 | | |
853 | 853 | | |
854 | 854 | | |
855 | | - | |
856 | | - | |
| 855 | + | |
| 856 | + | |
857 | 857 | | |
858 | 858 | | |
859 | | - | |
860 | | - | |
| 859 | + | |
| 860 | + | |
861 | 861 | | |
862 | 862 | | |
863 | | - | |
864 | | - | |
| 863 | + | |
| 864 | + | |
865 | 865 | | |
866 | 866 | | |
867 | | - | |
868 | | - | |
| 867 | + | |
| 868 | + | |
869 | 869 | | |
870 | 870 | | |
871 | | - | |
872 | | - | |
| 871 | + | |
| 872 | + | |
873 | 873 | | |
874 | 874 | | |
875 | 875 | | |
| |||
878 | 878 | | |
879 | 879 | | |
880 | 880 | | |
881 | | - | |
882 | | - | |
| 881 | + | |
| 882 | + | |
883 | 883 | | |
884 | 884 | | |
885 | | - | |
886 | | - | |
| 885 | + | |
| 886 | + | |
887 | 887 | | |
888 | 888 | | |
889 | | - | |
890 | | - | |
| 889 | + | |
| 890 | + | |
891 | 891 | | |
892 | 892 | | |
893 | | - | |
894 | | - | |
| 893 | + | |
| 894 | + | |
895 | 895 | | |
896 | 896 | | |
897 | | - | |
898 | | - | |
| 897 | + | |
| 898 | + | |
899 | 899 | | |
900 | 900 | | |
901 | | - | |
902 | | - | |
| 901 | + | |
| 902 | + | |
903 | 903 | | |
904 | 904 | | |
905 | | - | |
906 | | - | |
| 905 | + | |
| 906 | + | |
907 | 907 | | |
908 | 908 | | |
909 | | - | |
910 | | - | |
| 909 | + | |
| 910 | + | |
911 | 911 | | |
912 | 912 | | |
913 | | - | |
914 | | - | |
| 913 | + | |
| 914 | + | |
915 | 915 | | |
916 | 916 | | |
917 | 917 | | |
| |||
Lines changed: 0 additions & 30 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | 139 | | |
170 | 140 | | |
171 | 141 | | |
| |||
0 commit comments