Skip to content

i18n-extract's per-component pass is still region-level only, so the nested-children keys translatePage now resolves are never scaffolded into the skeleton bundle #13109

Description

@os-trump

Blocked-by: #12961

Found while implementing #12961 (branch claude/issue-12961-translate-nested-children); deliberately NOT fixed there — that card's scope is the resolver, and this half lives in another package.

The gap

packages/spec/src/system/i18n-resolver.ts documents the failure pair this creates, in PAGE_COMPONENT_COPY_KEYS's own JSDoc — verbatim:

Two hand-maintained copies of this list would drift into the classic pair of failures — the extractor offering a key the resolver ignores, or omitting one it reads — so there is one list and both sides import it.

The KEY LIST is shared, so neither side can drift on which keys. The walk is not shared, and after #12961 the two walks disagree on which components:

  • translatePage now descends into a container's declared properties.children, recursively, so pages.PAGE.components.ID.KEY resolves for a nested component id (ruled 2026-08-29, option A).
  • packages/cli/src/utils/i18n-extract.ts's per-component pass (collectExpectedEntries, the Per-component copy, addressed by the component's own id (#6080) block) still iterates regions[].components[] and stops there.

Net: the second half of the documented pair is now live — the extractor omits keys the resolver reads. A translator running os i18n extract on a page whose copy lives in nested components gets a skeleton with no entries for them, and has to know the keys to hand-write them, which the #6080 block's own comment names as "most of the reason the copy went untranslated in the first place".

The measured consumer case is exactly this shape: hotCRM's sales_home_page holds its four object-metric KPI blocks in a page:card's properties.children.

Notes for whoever picks this up

  • Same file already has a shared-traversal precedent one pass down: the object-sections pass reuses @objectstack/lint's walkPageComponents rather than growing a private copy, with a comment saying a duplicated walk "produced a dead rule once already (lint: no reference-integrity or option-key validation for app metadata #3583)". That walk reportedly descends into container nesting already, so the repair may be reuse rather than new traversal code.
  • Note the two walks would then still differ deliberately in one respect: translatePage descends properties.children ONLY, while walkPageComponents also covers slots.SLOT and properties.items[].children. Bringing the extractor to a WIDER walk than the resolver would recreate the pair's other half (offering keys the resolver ignores), so the scopes have to be matched deliberately, not just widened.
  • packages/cli/test/platform-page-i18n-parity.test.ts is the existing drift guard in this area; it compares extractor output against the shipped en bundle and does not currently notice this, since it only ever sees what the extractor emits.
  • Coverage reporting (os i18n coverage) reads the same collectExpectedEntries, so the denominator is affected too: nested copy currently counts as neither translated nor missing.

Filed unassigned, recording only — the fix is a contract-shaped choice about how wide the extractor's walk should be, not a mechanical edit.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions