Skip to content

Commit 59d91d1

Browse files
committed
docs(spec): drop the stale "the id wins" sentence and add the changeset
The visitor's lead comment still described the id route as beating the page-name route for a header that carries an id -- the sentence the gate below it now contradicts. Rewritten to say what the code does. Changeset: `minor` on `@objectstack/spec`, stating the behaviour change and the FROM -> TO mapping for a bundle that used the components key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
1 parent cc8968f commit 59d91d1

2 files changed

Lines changed: 47 additions & 8 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`translatePage` now reads a region-level `page:header` by **page name only**. The id route
6+
(`pages.<page>.components.<headerId>.*`) is no longer read for that component, even when it carries an `id`.
7+
8+
**Behaviour change, stated plainly:** a bundle that overrode a region-level header's title through
9+
`pages.<page>.components.<headerId>.title` now falls back to `pages.<page>.title` (which itself falls back to
10+
`pages.<page>.label`). The components key still parses — nothing is removed from `TranslationBundleSchema` — it is
11+
simply no longer the address for this one component.
12+
13+
```
14+
FROM pages.<page>.components.<headerId>.title # region-level page:header — no longer read
15+
TO pages.<page>.title # …and .subtitle for the subtitle
16+
```
17+
18+
Fix in one line: move the string from the components entry up to the page's own `title` key, and delete the
19+
components entry for that header id. `os i18n extract` has always offered exactly the `TO` key, so a bundle
20+
generated or checked by the CLI already writes it.
21+
22+
**Blast radius, as measured on the card (inherited, not re-measured here):** HotCRM found **zero** such overrides —
23+
all five of its region-level headers carry ids and none writes the components key.
24+
25+
**Why.** Both sides were deliberate and they disagreed. The extractor skips a region-level `page:header` on purpose
26+
(its copy is offered under `pages.<page>.title` / `.subtitle`, and emitting it twice would put one string under two
27+
keys); the resolver read the id route on purpose (the more specific route wins). Together they produced the exact
28+
failure `walkAddressedPageComponents` was extracted to prevent — the resolver reading an id the extractor omits — so
29+
the key an author reached for won silently while the key the tooling reported as translated lost. The maintainer
30+
ruled (2026-09-06, decision batch #58, verbatim 「同意」) that the page-name route is canonical: one component, one
31+
address. `title` and `subtitle` now follow the same rule, closing the asymmetry where `title` had two addresses and
32+
`subtitle` — never in `PAGE_COMPONENT_COPY_KEYS` — had one.
33+
34+
Unchanged: a `page:header` **nested** inside a container is reached by the id route only, as it always has been; and
35+
a region-level header's id still claims its bundle entry and still blocks a nested namesake, which is what the
36+
extractor does too.
37+
38+
Not `major`: nothing an author can write is removed or renamed. `pages.<page>.components.<id>` remains a declared,
39+
parsing, resolving address for every other component — including a nested `page:header` — so there is no key to
40+
tombstone and no ADR-0087 conversion to register. Recorded here so the choice is checkable rather than assumed.

packages/spec/src/system/i18n-resolver.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,14 +1745,13 @@ export function translatePage<T extends PageLike>(
17451745
// `children` after the visitor returns, so the overlay never contends with
17461746
// the descent for a key (`children` is not a copy key).
17471747
const regions = walkAddressedPageComponents(doc, (component, { nested, id, addressed }) => {
1748-
// Per-component copy (#6080) — addressed by the component's own id, so it
1749-
// is strictly more specific than the page-name route below and is applied
1750-
// first. A `page:header` that DOES carry an id can therefore be translated
1751-
// either way, and the id wins. `addressed` carries the ruled collision
1752-
// arbitration (#12961), so a looked-up entry is this component's alone;
1753-
// within one call `lookupPageComponentCopy` is a pure function of the id
1754-
// (bundle, page name and options are fixed), so the walk's claim-on-first-
1755-
// sighting selects the same component a claim-on-resolved-lookup would.
1748+
// Per-component copy (#6080) — addressed by the component's own id, and
1749+
// applied before the page-name route below. `addressed` carries the ruled
1750+
// collision arbitration (#12961), so a looked-up entry is this component's
1751+
// alone; within one call `lookupPageComponentCopy` is a pure function of
1752+
// the id (bundle, page name and options are fixed), so the walk's
1753+
// claim-on-first-sighting selects the same component a
1754+
// claim-on-resolved-lookup would.
17561755
//
17571756
// The one component this route does NOT serve is a REGION-LEVEL
17581757
// `page:header`: its copy is addressed by page name below, and reading

0 commit comments

Comments
 (0)