You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d748ae8
Browse filesBrowse the repository at this point in the historyBrowse files
fix(spec): a page label does not fill the untitled header of a record page (#22598)
Fixes#22536
Clause-②: no
On a record page, a `page:header` with no `title` is the sanctioned
spelling for "the renderer derives the heading from the record"
(`PageHeaderProps.title`, the maintainer ruling recorded in
`component.zod.ts`). Studio's record-page seed writes exactly that
header. `translatePage` filled the absent title with the pack's
`pages.NAME.label`, so the record's name was replaced by the page's
static label. This happened in every locale whose pack carries the
label, including the `en` skeleton that `os i18n extract` writes.
Nothing warned.
## The rule (triage ruling A, comment 6092658777)
`resolvePageHeaderTitle` (`packages/spec/src/system/i18n-resolver.ts`)
no longer lets the bundle label fill an untitled root `page:header` when
all three of these hold (`headerDerivesFromRecord`):
- the page's `type === 'record'`, read as served: a document with no
`type` is not a record page;
- the page's `object` is a non-empty string;
- the header's `properties.recordChrome !== false`.
Everywhere else the label still fills an absent title, as before. That
includes the existing `#22508` control fixture, a page with no `type`
and no `object`, whose pin name now says "non-record page". An authored
title, including one that restates the page `label`, follows the
existing order unchanged. The docblock's order list carries the new
condition in its step 3.
**Where the predicate comes from.** objectui is read at the
`.objectui-sha` pin `47b1f0bb71`:
- `PageHeaderRenderer` draws the record-derived heading only when a
record context is mounted and the header keeps its record chrome
(`containers.tsx:2331`-`:2332`, `:1573`-`:1574`).
- Two hosts mount that context: the record route, for the page
`usePageAssignment` picks (`usePageAssignment.ts:139`-`:140`: `type ===
'record'` and `object` naming the route's object), and the Studio
preview (`PagePreview.tsx:186`-`:187`, `:311`).
- The one host this rule cannot see is a `type: 'modal'` action that
renders its target page inside the invoking record's context
(`useActionModal.tsx`, `RecordDetailView.tsx:3064`). Such a page keeps
today's fill. Triage accepted this gap, and its in-repo population is 0.
Docs: `content/docs/ui/translations.mdx` states the exception beside the
label-fallback sentence.
## Measured at the door
These are one-shot runs and were never committed. `GET
/api/v1/meta/:type/:name` goes through `RestServer`, using the
`meta-plural-i18n.test.ts` harness shape, on both the cached and
uncached branches. The page is the Studio-seed shape: `account_record`,
`type: 'record'`, `object: 'account'`, and a root `page:header` with
properties `{recordChrome: true}`.
| pack, locale | before (`main` d6c3791) | after (cdfc9ef) |
| --- | --- | --- |
| zh-CN pack with only `pages.account_record.label` (客户记录页), zh-CN |
header gains `title: 客户记录页` | header `{recordChrome: true}`, no title |
| `en` extract skeleton (`label: Account Record`), en | header gains
`title: Account Record` | header `{recordChrome: true}`, no title |
| no `en` pack, en | untouched | untouched |
| control: `type: 'app'` page, untitled header with `recordChrome:
false`, zh-CN | gains `title: 落地页` | gains `title: 落地页` (unchanged) |
The resolver, its tests, `component.zod.ts` and `page.zod.ts` are
byte-unchanged between d6c3791 and this branch's merge base.
## Pins (`packages/spec/src/system/i18n-resolver.test.ts`)
Each pin runs at a region root and at a slot root.
- The Studio-seed record header stays untitled with the zh-CN label-only
pack and with the `en` extract skeleton, and the page's own `label`
still translates.
- A bundle `subtitle` still applies beside the untitled header.
- The pin also goes through `translateMetadataDocument`, the function
the REST page doors call.
- One control per condition, where the label DOES fill: `type` absent,
`object` absent or `''`, and `recordChrome: false`. A further control
covers an object-bound page of another `type` (`list`, `app`, `home`,
`utility`).
- An authored title on a record page follows the existing order: a
template is kept, a title that restates the label takes the label
translation, and a pack `title` replaces it.
- The existing `#22508` control is kept as written: a header with no
title on a non-record page shows the bundle `label`.
**Ablation.** This ran at cdfc9ef with the fix committed, using `node
scripts/ablation-replace.mjs`. It turned `if (own === undefined) return
derivesFromRecord ? undefined : bundleLabel;` into `if (own ===
undefined) return bundleLabel;`. The anchor went from 1 to 0 hits, and
the blob moved from c0682db to 69817db5e283. Result: `Tests 9
failed | 349 passed (358)`. The 9 failures are exactly the record-page
pins (4 per root plus the `translateMetadataDocument` pin), and every
control stayed green. The file was restored with blob c0682db equal
to HEAD, and `git diff HEAD` is empty.
## Verification
All runs went through `scripts/pm/os-verify-lock.sh` with slot
`issue-22536`. Each result is the `VERDICT command-exit` line of its
run.
- `@objectstack/spec`:
- `i18n-resolver.test.ts` plus `translation.test.ts` at d841d26
(after the `main` merge): 505 passed.
- `pnpm test` at cdfc9ef: 638 files, 19037 passed, 1 todo.
- `pnpm typecheck` at cdfc9ef: green. The test-typecheck ledger held
at 52 files, 246 errors and 135 signatures.
- `build && check:generated` at d841d26: all 15 generated artifacts
up to date.
- The only runtime caller of the translators is `@objectstack/rest`
(`meta-item-read-gate.ts`), via `translateMetadataDocument`. Its
consumer runs, all at cdfc9ef:
- `pnpm test`: 269 files, 4979 passed, 326 skipped.
- `pnpm typecheck`: green, with 0 test-layer errors.
- `pnpm test:repo`: 191 passed, 1 skipped.
- `@objectstack/cli` unit tier, `test/platform-page-i18n-parity.test.ts`
and `test/i18n-extract.test.ts`: 51 passed. These call `translatePage`
on the shipped platform pages.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at d841d26 derived 107 commands. 106 were
run and all exited 0. `check:skill-examples` first exited 3
(PREREQUISITE NOT MET: `client-react` was unbuilt) and exited 0 after
`turbo run build --filter='@objectstack/client-react...'`. `pnpm
check:dual-build-cjs-loads`: NOT MEASURED, because the dispatch rules
out a whole-workspace build. `--ran` reconciliation: 107 derived, 106
run, 1 unrun (that one). Artifact rosters block: 48 of 51 run, all exit
0. The 3 PR-context guards (`check-closing-target-claim`,
`check-partof-closing-keyword`, `check-single-claim-paths`) are run
against this PR after it opens, and their results are reported on the
card.
## Acceptance notes
- **Seam: `PageSchema`'s parse default against `type` read as served.**
This is reported, not fixed here. `PageSchema` defaults an absent `type`
to `'record'` (`page.zod.ts:1177`).
- Stored path: closed. A page saved without `type` is stored with `type:
'record'`, and a stored row without it is served with it
(`withDeclaredPageTypeDefault`). Its 9 existing pins pass here.
- Registry path: open. Measured through the door, a page registered in
the `SchemaRegistry` without `type` and without a parse is served with
no `type`. In zh-CN its untitled header gains the label (`title:
打包无类型页`). The same page as a stored row is served `type: "record"`, and
its header stays untitled.
- No real producer was found that registers a typeless page without
parsing. `defineStack` applies the default, and the plugin pages in this
tree declare `type`. Reach is therefore not established.
- **`recordChrome` spelling.** Only `properties.recordChrome` is read.
Measured: `PageSchema` refuses a bare top-level `recordChrome` on a
component (`unrecognized_keys` at `regions.0.components.0`), so a valid
served document never carries it. Reading that spelling would be
consumer tolerance for a key the contract refuses.
- **Unchanged on purpose.** A pack's `pages.NAME.title` and `subtitle`
still apply to an untitled record-page header (step 1). Triage ruled on
the label alone.
- **Population.** `showcase_task_detail` is the one in-repo record page
with an untitled root header. No in-repo pack carries its label today,
so nothing in-repo renders differently. The plugin-approvals record page
that landed on `main` during this work is `kind: 'slotted'` with no
header slot, so the server sees no root header on it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_
Co-authored-by: Claude <noreply@anthropic.com>
A locale pack's page `label` no longer replaces the record-derived heading on a record page whose header authors no title
6
+
7
+
Clause-②: no
8
+
9
+
On a record page, a `page:header` with no `title` is the sanctioned spelling for "the renderer derives the heading from the record" (`PageHeaderProps.title`). Studio's record-page seed writes exactly that header. `translatePage` used to fill the absent title with the pack's `pages.<name>.label`, so the record's name was replaced by the page's static label. This happened in every locale whose pack carries the label, including the `en` skeleton that `os i18n extract` writes. Measured at `GET /api/v1/meta/page/:name` before this change: the header gained `title: "客户记录页"` in zh-CN, and `title: "Account Record"` in en with the extracted skeleton.
10
+
11
+
The label now does not fill an untitled root `page:header` when all three of these hold:
12
+
13
+
- the page's `type` is `'record'` as served (a document with no `type` is not a record page);
14
+
- the page's `object` is a non-empty string;
15
+
- the header keeps its record chrome (`properties.recordChrome` is not `false`).
16
+
17
+
Everywhere else the label still fills an absent header title, as before. An authored header title follows the existing order, including one that restates the page's `label`. A pack's `pages.<name>.title` and `subtitle` apply as before.
0 commit comments