Filed by the domain:spec dev implementing #11745 (translateFlow's region blindness). packages/cli is outside that card's declared file surface, so this is its own card, not a rider.
Measured
On origin/main, packages/cli/src/utils/i18n-extract.ts, walkScreenFlows:
function walkScreenFlows(config: any, out: ExpectedEntry[]): void {
const flows: any[] = Array.isArray(config?.flows) ? config.flows : [];
for (const flow of flows) {
…
const nodes: any[] = Array.isArray(flow.nodes) ? flow.nodes : [];
for (const node of nodes) {
if (!node || typeof node !== 'object' || node.type !== SCREEN_NODE_TYPE) continue;
Region-awareness symbols in that file:
FLOW_REGION_SLOTS 0
walkFlowNodes 0
config.body 0
try_catch 0
branches 4 ← all four are prose/unrelated (lines 690, 1237, 1435, 1831)
LIT CONTROL 'walkScreenFlows' 2 ← so the zeros are absences, not a bad path
FlowNode.config carries ADR-0031 regions — loop.config.body, parallel.config.branches[].nodes, try_catch.config.try / .catch — each holding a full nodes array, nesting arbitrarily. A type: 'screen' node inside one is a real screen: the executor pauses on it and the client receives its ScreenSpec.nodeId. So flows.NAME.screens.NODE_ID.{title,fields.*} is a live key for it, and the extractor never emits one.
Consequence: os i18n extract writes a skeleton with a hole in it, and the coverage gate has no row to demand. A translator is never shown the key, so the nested wizard step keeps rendering source-locale text — the invisible hole the flows bucket was added to close.
This is the file's own stated principle, applied to itself
walkScreenFlows's docblock already argues the case, one step short of the nesting:
A screen node whose waitForInput is false is deliberately NOT skipped: translateFlow overlays every screen node, and a walker that skipped one would re-open the extractable-but-ungated gap in miniature.
translateFlow does now overlay every screen node at every depth (#11745), so the premise that sentence rests on is true and this walker is the one skipping.
Why it is a separate card from #11745
#11745's fix moves the runtime overlay onto the shared per-flow region walk (mapFlowNodeList, packages/spec/src/conversions/walk.ts). That closes the runtime half only. The extractor is a different package, a different unit (it collects expected KEYS with a diagnostic scope, not a copy-on-write rewrite), and reaching the region table from packages/cli is its own choice — @objectstack/spec/automation exports FLOW_REGION_SLOTS_BY_TYPE, and packages/lint's walkFlowNodes is a third shape again.
Shape of the fix
Collect the node universe region-aware — either through the spec-side slot table or by whichever walker packages/cli already depends on — so the skeleton offers exactly the keys the resolver reads. The key face itself (FLOW_SCREEN_COPY_KEYS / FLOW_SCREEN_FIELD_COPY_KEYS) is already imported rather than restated and needs no change.
Note the ordering hazard the seed rules record: the title seed falls back to node.label, and the field label seed falls back to field.name via pushDerived. A nested screen must get the same treatment, not a narrower one, or the coverage gate starts demanding a translation of a string nobody authored.
Refs: #11745 · #11485 · #11287 · #7646
Filed by the
domain:specdev implementing #11745 (translateFlow's region blindness).packages/cliis outside that card's declared file surface, so this is its own card, not a rider.Measured
On
origin/main,packages/cli/src/utils/i18n-extract.ts,walkScreenFlows:Region-awareness symbols in that file:
FlowNode.configcarries ADR-0031 regions —loop.config.body,parallel.config.branches[].nodes,try_catch.config.try/.catch— each holding a fullnodesarray, nesting arbitrarily. Atype: 'screen'node inside one is a real screen: the executor pauses on it and the client receives itsScreenSpec.nodeId. Soflows.NAME.screens.NODE_ID.{title,fields.*}is a live key for it, and the extractor never emits one.Consequence:
os i18n extractwrites a skeleton with a hole in it, and the coverage gate has no row to demand. A translator is never shown the key, so the nested wizard step keeps rendering source-locale text — the invisible hole theflowsbucket was added to close.This is the file's own stated principle, applied to itself
walkScreenFlows's docblock already argues the case, one step short of the nesting:translateFlowdoes now overlay every screen node at every depth (#11745), so the premise that sentence rests on is true and this walker is the one skipping.Why it is a separate card from #11745
#11745's fix moves the runtime overlay onto the shared per-flow region walk (
mapFlowNodeList,packages/spec/src/conversions/walk.ts). That closes the runtime half only. The extractor is a different package, a different unit (it collects expected KEYS with a diagnostic scope, not a copy-on-write rewrite), and reaching the region table frompackages/cliis its own choice —@objectstack/spec/automationexportsFLOW_REGION_SLOTS_BY_TYPE, andpackages/lint'swalkFlowNodesis a third shape again.Shape of the fix
Collect the node universe region-aware — either through the spec-side slot table or by whichever walker
packages/clialready depends on — so the skeleton offers exactly the keys the resolver reads. The key face itself (FLOW_SCREEN_COPY_KEYS/FLOW_SCREEN_FIELD_COPY_KEYS) is already imported rather than restated and needs no change.Note the ordering hazard the seed rules record: the
titleseed falls back tonode.label, and the fieldlabelseed falls back tofield.nameviapushDerived. A nested screen must get the same treatment, not a narrower one, or the coverage gate starts demanding a translation of a string nobody authored.Refs: #11745 · #11485 · #11287 · #7646