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
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
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.
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.tsdocuments the failure pair this creates, inPAGE_COMPONENT_COPY_KEYS's own JSDoc — verbatim: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:
translatePagenow descends into a container's declaredproperties.children, recursively, sopages.PAGE.components.ID.KEYresolves for a nested component id (ruled 2026-08-29, option A).packages/cli/src/utils/i18n-extract.ts's per-component pass (collectExpectedEntries, thePer-component copy, addressed by the component's own id (#6080)block) still iteratesregions[].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 extracton 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_pageholds its fourobject-metricKPI blocks in apage:card'sproperties.children.Notes for whoever picks this up
@objectstack/lint'swalkPageComponentsrather 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.translatePagedescendsproperties.childrenONLY, whilewalkPageComponentsalso coversslots.SLOTandproperties.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.tsis the existing drift guard in this area; it compares extractor output against the shippedenbundle and does not currently notice this, since it only ever sees what the extractor emits.os i18n coverage) reads the samecollectExpectedEntries, 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.