Repository navigation
Commit 0e00d36
fix(spec): a locale bundle's page label no longer replaces a page:header title that says something else (#22534)
Fixes #22508
Clause-②: no
## What changes
`translatePage` (`packages/spec/src/system/i18n-resolver.ts`) used to
resolve a root-level `page:header` title as the bundle's
`pages.NAME.title`, else the bundle's `pages.NAME.label`, and write it
over whatever the header authored. A locale pack that translated the
page label and left out the title therefore replaced a record page's
dynamic `'{name}'` heading with the page's static name in that locale,
with nothing warning at build, lint or serve.
The header title now resolves through ONE function,
`resolvePageHeaderTitle`, called from the one visitor both roots pass
through (a region's entry and a `slots.SLOT` entry both arrive with
`nested: false`), so the two roots cannot disagree:
1. the bundle's `pages.NAME.title`, across the locale chain;
2. the header's AUTHORED title, left as written (a template, a plain
string, or an inline locale map);
3. the bundle's `pages.NAME.label`, only where the header authors no
title, or where its title restates the page's own authored `label`.
The declared contract is restated with that limit in the same PR: the
`pages` JSDoc and the `pages.NAME.title` describe in
`translation.zod.ts` (and the regenerated
`content/docs/references/system/translation.mdx`), the `translatePage`
docblock, the hand-written `content/docs/ui/translations.mdx` callout,
and the `pages` row of `packages/spec/liveness/translation.json`.
## One deviation from the triage text, measured, and why
Triage `6085431021` reads: "The bundle's page `label` stands in only
where the authored header has no title of its own." Step 3 above adds
one case to that: a header title that restates the page's own `label`.
The triage expected nothing to rely on the label replacing an authored
title. The measurement says something does:
- `os i18n extract` (`packages/cli/src/utils/i18n-extract.ts`, the
page-header pass) deliberately offers NO `pages.NAME.title` key when the
header title restates the page label: "`title` duplicating `label` is
the common case and resolves via the label fallback, only emit it when
the two genuinely differ" (`sameAuthored`). Every skeleton it has
written relies on the label reaching such a header.
- In this repo, three shipped pages are that shape:
`marketplace_installed`, `cloud_connection_settings`
(`@objectstack/cloud-connection`) and `connect_agent`
(`@objectstack/mcp`). Each authors a header title equal to its page
label, and `@objectstack/platform-objects`' bundles carry `label` +
`subtitle` and no `title` for them in all four locales (en, zh-CN,
ja-JP, es-ES).
- Applied literally, the triage sentence turns those three Setup headers
back to English in zh-CN / ja-JP / es-ES. The ablation below measures
it: `packages/cli/test/platform-page-i18n-parity.test.ts` ("rewrites the
page:header copy end-to-end for a non-English locale") goes red for all
three.
Step 3's second case keeps the triage's own principle ("A bundle
supplies a translation of an attribute; it does not swap one attribute
for another over authored text"): when the authored title IS the page
label's text, the label's translation is a translation of that very
text. It also keeps the resolver and the extractor a matched pair: the
extractor omits `title` exactly when the resolver will read the label
for it. The comparison (`sameAuthoredText`) mirrors the extractor's
`sameAuthored` (strings by text, inline maps as sets of non-empty
entries). The seat may rule otherwise; the alternative is in the
report's `open_questions`.
What still changes for an author (named in the changeset): a header
whose title says something else than the page label, in a locale whose
pack carries `label` but no `title`, now shows the authored title. In
this repo that population is zero (H3 below); the reporting app (hotcrm)
already writes `title` in every pack.
## Measurements (dispatch hypotheses)
- **H1, holds.** `translatePage` is reached only through
`translateMetadataDocument('page', …)` (the
`METADATA_DOCUMENT_TRANSLATORS` table). Its only runtime callers are
`translateMetaList` and `translateMetaDocument` in
`packages/rest/src/meta-item-read-gate.ts`, which serve `GET
/api/v1/meta/page` and `GET /api/v1/meta/page/NAME`. The console reads
those doors: objectui's `useObjectLabel` resolves only
`pages.NAME.label` client-side (the page's nav label) and nothing reads
`pages.NAME.title` there. One fix covers both reach points the card
names.
- **H2, holds.** The authored title reaches `translatePage` as written
in `properties.title` (the REST door serves the stored or packaged
document; `PageComponentSchema` is strict, so `properties` is the only
place a title can be authored). Pinned at both roots: a template
(`'{name}'`), a plain string, an inline locale map, absent (`{}`, `''`),
and a title restating the label (string and map forms).
- **H3, does not hold as stated.** In-repo bundles with a `pages` group:
`@objectstack/platform-objects` (4 locales, 6 page entries each) and
`examples/app-showcase` (zh-CN, 4 entries, `components` only). Pages
whose bundle carries `label` and no `title` while a root-level header
authors a title: **3** (the three Setup pages above), all of them
title-restates-label, all kept translated by step 3. Pages of the defect
shape (header title differs from the label, pack has `label` and no
`title`): **0**. The `sys_*_detail` pages carry a label-only entry but
no `page:header`. app-crm, app-todo and app-multi-package carry no page
translations.
Clause-② measured: no accept set or public surface widens. No schema,
key, accepted value, default or export changes; the helpers are
module-private. What changes is what a translated page RENDERS, which is
resolver behaviour. The one schema-adjacent change is the
`pages.NAME.title` describe string.
## Tests
All readings below are at head `194157f5e1` (the last commit; no commit
followed any of them). Wall-clock figures are shared-box seconds.
- `pnpm --filter @objectstack/spec exec vitest run --project local`: 631
files, 18842 passed, 1 todo. `--project repo`: 54 files, 915 passed.
`src/system/i18n-resolver.test.ts` alone: 327 passed (the new `#22508`
block is 13 cases: six per root plus the REST-door case).
- `pnpm --filter @objectstack/spec typecheck` (tsc, scripts typecheck,
`check:test-typecheck`): exit 0.
- `@objectstack/cli` unit layer, the two files that feed `translatePage`
and the extractor: `test/platform-page-i18n-parity.test.ts`,
`test/i18n-extract.test.ts`, 51 passed (CLI resolves `@objectstack/spec`
through `dist`, built at this head).
- `@objectstack/rest` (the one runtime caller): `pnpm --filter
@objectstack/rest exec vitest run`: 273 files, 5166 passed, 327 skipped
(rest resolves `@objectstack/spec` through `dist`, rebuilt clean after
the ablation).
- Fixture triage (one fixture): `regionHeaderWithId()` in the batch #58
block omitted `PageSchema`'s required `label` while its header restated
`homePage()`'s label. Disposition: add the declaration (`label: 'Sales
Home'`), so the fixture is the shape it was cut from. No other fixture
in the rule's consumer radius changed (platform-objects, cli, lint, rest
fixtures read; only the CLI parity test exercises the header title, and
it stays green).
- eslint, narrowed and proven: `eslint --no-inline-config --format json`
on the three changed TS files reports 3 files, 0 errors, 0 warnings.
Population: `eslint.config.mjs` lints
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, so the `.md`, `.mdx` and `.json`
files in this diff are outside it. Invariance: the config never enables
type-aware linting (no `parserOptions.project`, no typed rules, stated
in the config itself), so this diff cannot move a verdict on an
untouched file.
### Ablations (one-shot, both legs restored and proven)
- Leg 1, the old fallback restored (`resolvePageHeaderTitle` answering
bundle title, else bundle label, whatever the header authors), through
`scripts/ablation-replace.mjs` (anchor hit 1, blob changed):
`i18n-resolver.test.ts` 7 failed / 320 passed. The 7 are exactly the
predicted ones: template, plain string and inline map at each root, plus
the REST-door case. The controls (pack with `title` replaces; no
authored title shows the label; title restating the label) stayed green.
Restored: blob equals HEAD, `git diff HEAD` empty.
- Leg 2, the triage sentence applied literally (the restates-the-label
disjunct removed), spec rebuilt, `ablation-dist-preflight` found the
marker in `dist`: spec 6 failed / 321 passed (the two restates-label
pins, the original `connect_agent` pins and the batch #58 pins), and
`platform-page-i18n-parity.test.ts` "rewrites the page:header copy
end-to-end for a non-English locale" went red at its first page,
`marketplace_installed` (`titleChanged: false`: the zh-CN header stayed
"Installed Apps"). Restore leg: source blob equals HEAD, spec rebuilt,
preflight `--absent`: marker absent from all 232 built files, tree
clean.
### Gates
`node scripts/pm/dispatch-gates.mjs --commands` derived 111 families
from this diff (the 85 the dispatch named plus 26 the docs and changeset
paths add). All 111 ran and recorded an exit code; `--ran` reconciles
111 derived, 111 run, 0 NOT MEASURED. 110 exit 0. One exits 1: `pnpm
check:platform-checklist`, on
`docs/qa/platform-checklist/areas/attachments-storage.json`, whose
anchor `attachment-access-hooks.ts#canEdit` lost its declaration in
`ce3d0ad419` (#22513). That is red on `main` at this branch's base and
at `origin/main` `ee8751d41e` too; this diff touches neither file. Seven
gates first refused with PREREQUISITE NOT MET (exit 3, unbuilt `lint` /
`client-react`); they were re-run after the build and pass, as were the
dist-reading gates after the ablation's restore rebuild.
## Acceptance notes
- **Ruled control, recorded rather than changed:** a root-level
`page:header` with NO authored title still receives the bundle's page
`label` (the triage's third pin). On a record page that is not neutral:
`PageHeaderProps.title` documents "omit to let the renderer derive the
heading from the record", and objectui's `PageHeaderRenderer` draws
`properties.title` when one is present, so a pack that translates the
page label replaces a record-derived heading with the static label, the
same failure as this card in another shape. In this repo the population
is zero: no root-level `page:header` without a title sits on a page with
a translated label (the `sys_*_detail` pages carry no header). A
Studio-seeded record page stores its synthesized header without a title,
so it can be reached outside this repo; measured only by reading, not at
a door. Raised as an open question in the report, not changed here.
- **Locale chain, unchanged:** step 1 walks the request's whole locale
chain (the locale, then its declared `fallbackChain`) before step 2. So
a request whose own pack has only `label`, while a pack on its fallback
chain has `title`, shows that fallback title over the authored one. That
is still the bundle's title, which the triage put first.
- **Pre-existing red on main:** `pnpm check:platform-checklist` (see
Gates). Its carrier is whoever re-anchors the `attachments-storage`
checklist item after `ce3d0ad419`; not this PR's file surface.
- Generated output: `content/docs/references/system/translation.mdx` was
regenerated by `gen:docs` from the new describe (`check:generated` named
exactly that one artifact stale). No other generated artifact moved;
`authorable-surface.base.json` untouched.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7731d70 commit 0e00d36
7 files changed
Lines changed: 208 additions & 18 deletions
File tree
- .changeset
- content/docs
- references/system
- ui
- packages/spec
- liveness
- src/system
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
| 252 | + | |
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
| |||
448 | 448 | | |
449 | 449 | | |
450 | 450 | | |
451 | | - | |
| 451 | + | |
452 | 452 | | |
453 | 453 | | |
454 | 454 | | |
| |||
608 | 608 | | |
609 | 609 | | |
610 | 610 | | |
611 | | - | |
| 611 | + | |
612 | 612 | | |
613 | 613 | | |
614 | 614 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
129 | | - | |
130 | | - | |
131 | | - | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
132 | 136 | | |
133 | 137 | | |
134 | 138 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
112 | | - | |
113 | | - | |
| 112 | + | |
| 113 | + | |
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1724 | 1724 | | |
1725 | 1725 | | |
1726 | 1726 | | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
1727 | 1730 | | |
1728 | 1731 | | |
| 1732 | + | |
1729 | 1733 | | |
1730 | 1734 | | |
1731 | 1735 | | |
| |||
2722 | 2726 | | |
2723 | 2727 | | |
2724 | 2728 | | |
| 2729 | + | |
| 2730 | + | |
| 2731 | + | |
| 2732 | + | |
| 2733 | + | |
| 2734 | + | |
| 2735 | + | |
| 2736 | + | |
| 2737 | + | |
| 2738 | + | |
| 2739 | + | |
| 2740 | + | |
| 2741 | + | |
| 2742 | + | |
| 2743 | + | |
| 2744 | + | |
| 2745 | + | |
| 2746 | + | |
| 2747 | + | |
| 2748 | + | |
| 2749 | + | |
| 2750 | + | |
| 2751 | + | |
| 2752 | + | |
| 2753 | + | |
| 2754 | + | |
| 2755 | + | |
| 2756 | + | |
| 2757 | + | |
| 2758 | + | |
| 2759 | + | |
| 2760 | + | |
| 2761 | + | |
| 2762 | + | |
| 2763 | + | |
| 2764 | + | |
| 2765 | + | |
| 2766 | + | |
| 2767 | + | |
| 2768 | + | |
| 2769 | + | |
| 2770 | + | |
| 2771 | + | |
| 2772 | + | |
| 2773 | + | |
| 2774 | + | |
| 2775 | + | |
| 2776 | + | |
| 2777 | + | |
| 2778 | + | |
| 2779 | + | |
| 2780 | + | |
| 2781 | + | |
| 2782 | + | |
| 2783 | + | |
| 2784 | + | |
| 2785 | + | |
| 2786 | + | |
| 2787 | + | |
| 2788 | + | |
| 2789 | + | |
| 2790 | + | |
| 2791 | + | |
| 2792 | + | |
| 2793 | + | |
| 2794 | + | |
| 2795 | + | |
| 2796 | + | |
| 2797 | + | |
| 2798 | + | |
| 2799 | + | |
| 2800 | + | |
| 2801 | + | |
| 2802 | + | |
| 2803 | + | |
| 2804 | + | |
| 2805 | + | |
| 2806 | + | |
| 2807 | + | |
| 2808 | + | |
| 2809 | + | |
| 2810 | + | |
| 2811 | + | |
| 2812 | + | |
| 2813 | + | |
| 2814 | + | |
| 2815 | + | |
2725 | 2816 | | |
2726 | 2817 | | |
2727 | 2818 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1842 | 1842 | | |
1843 | 1843 | | |
1844 | 1844 | | |
| 1845 | + | |
| 1846 | + | |
| 1847 | + | |
| 1848 | + | |
| 1849 | + | |
| 1850 | + | |
| 1851 | + | |
| 1852 | + | |
| 1853 | + | |
| 1854 | + | |
| 1855 | + | |
| 1856 | + | |
| 1857 | + | |
| 1858 | + | |
| 1859 | + | |
| 1860 | + | |
| 1861 | + | |
| 1862 | + | |
| 1863 | + | |
| 1864 | + | |
| 1865 | + | |
| 1866 | + | |
| 1867 | + | |
| 1868 | + | |
| 1869 | + | |
| 1870 | + | |
| 1871 | + | |
| 1872 | + | |
| 1873 | + | |
| 1874 | + | |
| 1875 | + | |
| 1876 | + | |
| 1877 | + | |
| 1878 | + | |
| 1879 | + | |
| 1880 | + | |
| 1881 | + | |
| 1882 | + | |
| 1883 | + | |
| 1884 | + | |
| 1885 | + | |
| 1886 | + | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
| 1890 | + | |
| 1891 | + | |
| 1892 | + | |
| 1893 | + | |
| 1894 | + | |
| 1895 | + | |
| 1896 | + | |
| 1897 | + | |
| 1898 | + | |
| 1899 | + | |
| 1900 | + | |
| 1901 | + | |
| 1902 | + | |
| 1903 | + | |
| 1904 | + | |
| 1905 | + | |
1845 | 1906 | | |
1846 | 1907 | | |
1847 | 1908 | | |
| |||
2197 | 2258 | | |
2198 | 2259 | | |
2199 | 2260 | | |
2200 | | - | |
2201 | | - | |
2202 | | - | |
| 2261 | + | |
| 2262 | + | |
| 2263 | + | |
| 2264 | + | |
| 2265 | + | |
| 2266 | + | |
| 2267 | + | |
2203 | 2268 | | |
2204 | 2269 | | |
2205 | 2270 | | |
| |||
2283 | 2348 | | |
2284 | 2349 | | |
2285 | 2350 | | |
2286 | | - | |
| 2351 | + | |
| 2352 | + | |
2287 | 2353 | | |
2288 | | - | |
2289 | | - | |
| 2354 | + | |
| 2355 | + | |
| 2356 | + | |
2290 | 2357 | | |
2291 | 2358 | | |
2292 | 2359 | | |
| |||
2349 | 2416 | | |
2350 | 2417 | | |
2351 | 2418 | | |
| 2419 | + | |
| 2420 | + | |
| 2421 | + | |
| 2422 | + | |
| 2423 | + | |
2352 | 2424 | | |
2353 | 2425 | | |
2354 | 2426 | | |
| |||
0 commit comments