From a5d9630dd8356d7d443810201b8457b7f022af20 Mon Sep 17 00:00:00 2001 From: Marcos Paulo Date: Sat, 5 Sep 2026 21:36:16 +0000 Subject: [PATCH 1/2] docs: add task 10b, locale-paired props on the six full-guide blocks Task 15d stopped on a real blocker rather than working around it: the blocks it assembles from take plain string props, so /full-guide/ cannot render both locales without either duplicating blocks or changing a component interface, and both are outside page-migrator scope. Tasks 07-11 predate the language contract task 15c wrote, which is why none of the 19 blocks is locale-aware. This narrows the fix to the six blocks full-guide actually uses and makes the change additive, so the already-merged call sites that pass plain strings are unaffected. Records that /rules/ is already fully bilingual via client-side swapping from a content collection, and that the other five pages have no language toggle today and must not gain one. Co-Authored-By: Claude Opus 5 --- .../task-10b-bilingual-blocks.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 plans/astro-refactor/task-10b-bilingual-blocks.md diff --git a/plans/astro-refactor/task-10b-bilingual-blocks.md b/plans/astro-refactor/task-10b-bilingual-blocks.md new file mode 100644 index 0000000..2492de7 --- /dev/null +++ b/plans/astro-refactor/task-10b-bilingual-blocks.md @@ -0,0 +1,72 @@ +# Task 10b — Locale-paired props on the six full-guide blocks + +**Agent**: `component-builder` · **Model**: **Codex** **Depends on**: 10 · +**Blocks**: 15d **Worktree**: +`.agents/scripts/worktree.sh start 10b bilingual-blocks` + +## Why this task exists + +It is not in the original plan. Tasks 07–11 built the primitives and blocks +before task 15c wrote the language contract, so every prose prop on all 19 +blocks is a plain `string`. Task 15d then stopped, correctly, when it found it +could not render `/full-guide/` in both languages: the brief forbids it from +duplicating a block to get a second locale, and changing a block's interface is +component-builder work, not page-migrator work. + +`/full-guide/` is one of only two pages with a language toggle (the other is +`/rules/`, which is already fully bilingual — all 23 of its Portuguese strings +reach the built page, because `RulesInteractive` reads them from +`src/content/rules/stages.json` and swaps client-side). The five remaining pages +have no toggle today and must not gain one. + +## Scope + +Exactly six files, all in `src/components/blocks/`: + +`FleetDiagram.astro`, `HandoffTable.astro`, `PhasePanel.astro`, +`RouteTable.astro`, `SkillPackage.astro`, `WorktreeMap.astro`. + +These are the six that `src/pages/full-guide.astro` uses. Do not touch the other +13 blocks, any primitive, any island, or any page. + +## What to change + +Every **prose** prop on those six accepts `Localized` as well as `string`: + +```ts +type Localized = { en: string; pt: string }; +``` + +- Given a `string`, render exactly what it renders today. Every existing call + site keeps working untouched — this is additive. +- Given a `Localized`, render the fragment twice per + `.agents/context/content-i18n.md`: `data-language-content="en"` on one and + `"pt"` on the other, English visible, Portuguese `hidden`. + +Prose only. Do not widen `href`, `id`, `code`, numeric or enum props — several +are deliberately unlocalized, and task 05b recorded which (`trees.path`, +`trees.command`, `routes.score`, `commonSkills.source`). + +`FleetDiagram`'s `orchestrator.title` is rendered with `set:html` because it +carries inline `
`. Keep that, on both locales. + +## Do not + +- Do not change what a block renders for a `string` prop. A diff in the built + output for the existing call sites means you got it wrong. +- Do not add a language toggle to any block. `LanguageToggle` is a separate + island and the page mounts it. +- Do not edit `scripts/verify.mjs`. 42 assertions, and you are not the + verification-engineer. +- Do not touch `src/pages/full-guide.astro`. That is 15d's file; it will pass + the `Localized` values once you land. + +## Done when + +- [ ] All six blocks accept `Localized` on every prose prop and `string` still + behaves identically +- [ ] Built output byte-identical for pages that pass only strings — check + `dist/` before and after for the pages that use these blocks +- [ ] `pnpm run gate` green, 42 assertions intact +- [ ] Report lists, per block, which props became locale-aware and which you + deliberately left unlocalized and why From cac1115035c18e0efe5f7071adedb7ece2450cc7 Mon Sep 17 00:00:00 2001 From: Marcos Paulo Date: Sat, 5 Sep 2026 21:59:29 +0000 Subject: [PATCH 2/2] feat(blocks): add Localized type support to full-guide blocks Widens prose props on FleetDiagram, HandoffTable, PhasePanel, RouteTable, SkillPackage, and WorktreeMap to accept {en, pt} as well as string, and conditionally renders language spans. Non-prose props (id, code, etc) were left as strings. --- src/components/blocks/FleetDiagram.astro | 64 +++++++++++++-- src/components/blocks/HandoffTable.astro | 100 ++++++++++++++++++++--- src/components/blocks/PhasePanel.astro | 90 ++++++++++++++++++-- src/components/blocks/RouteTable.astro | 47 +++++++++-- src/components/blocks/SkillPackage.astro | 17 +++- src/components/blocks/WorktreeMap.astro | 32 +++++++- 6 files changed, 311 insertions(+), 39 deletions(-) diff --git a/src/components/blocks/FleetDiagram.astro b/src/components/blocks/FleetDiagram.astro index b8c3577..c695e9b 100644 --- a/src/components/blocks/FleetDiagram.astro +++ b/src/components/blocks/FleetDiagram.astro @@ -9,22 +9,24 @@ // hairlines between worker cards; the parent background is an off-token seam // colour marked inline with `token-gap`. +type Localized = { en: string; pt: string }; + interface Worker { /** Used as the `data-worker` hook and the key in the source. */ id: string; /** Short label rendered uppercase, e.g. "UI". */ - label: string; + label: string | Localized; /** Body copy describing the worker's remit. */ - strong: string; + strong: string | Localized; /** Path label, e.g. "agent/ui". */ code: string; } interface Props { orchestrator: { - eyebrow: string; + eyebrow: string | Localized; /** May contain inline `
`; rendered with `set:html`. */ - title: string; + title: string | Localized; code: string; }; workers: Worker[]; @@ -36,8 +38,32 @@ const { orchestrator, workers, initial = workers[0]?.id } = Astro.props;
- {orchestrator.eyebrow} -

+ + { + typeof orchestrator.eyebrow === 'string' ? ( + orchestrator.eyebrow + ) : ( + <> + <> + {orchestrator.eyebrow.en} + + + + ) + } + + { + typeof orchestrator.title === 'string' ? ( +

+ ) : ( + <> +

+

@@ -49,8 +75,30 @@ const { orchestrator, workers, initial = workers[0]?.id } = Astro.props; data-worker={worker.id} aria-pressed={worker.id === initial} > - {worker.label} - {worker.strong} + + {typeof worker.label === 'string' ? ( + worker.label + ) : ( + <> + {worker.label.en} + + + )} + + + {typeof worker.strong === 'string' ? ( + worker.strong + ) : ( + <> + {worker.strong.en} + + + )} + {worker.code} )) diff --git a/src/components/blocks/HandoffTable.astro b/src/components/blocks/HandoffTable.astro index 7dca7d1..9b96995 100644 --- a/src/components/blocks/HandoffTable.astro +++ b/src/components/blocks/HandoffTable.astro @@ -7,18 +7,20 @@ // muted body) and lives here so the next page that needs it gets the same // beat for free. +type Localized = { en: string; pt: string }; + interface Row { /** The package name, rendered as a `` (column 1). */ - package: string; + package: string | Localized; /** What the package contains (column 2). */ - contains: string; + contains: string | Localized; /** Why this matters (column 3). */ - why: string; + why: string | Localized; } interface Props { /** Column headers in render order. */ - columns: [string, string, string]; + columns: [string | Localized, string | Localized, string | Localized]; rows: Row[]; } @@ -28,18 +30,96 @@ const { columns, rows } = Astro.props; - - - + + + { rows.map((row) => ( - - - + + + )) } diff --git a/src/components/blocks/PhasePanel.astro b/src/components/blocks/PhasePanel.astro index 12fbe50..e4cbbab 100644 --- a/src/components/blocks/PhasePanel.astro +++ b/src/components/blocks/PhasePanel.astro @@ -8,18 +8,20 @@ // // Every `data-phase` value is asserted by `scripts/verify.mjs`. +type Localized = { en: string; pt: string }; + export type PhaseId = 'plan' | 'build' | 'review'; interface Phase { id: PhaseId; /** Two-letter label rendered in the tab, e.g. "PLAN". */ - label: string; + label: string | Localized; /** Numeric prefix, e.g. "01". */ number: string; - title: string; - body: string; + title: string | Localized; + body: string | Localized; /** Headline + small caption shown above the panel title. */ - meta: { deliverable: string; gate: string }; + meta: { deliverable: string | Localized; gate: string | Localized }; /** Code-line evidence shown at the bottom of the panel. */ evidence: string; } @@ -43,7 +45,17 @@ const active = phases.find((phase) => phase.id === initial) ?? phases[0]; role="tab" aria-selected={phase.id === initial} > - {phase.number} {phase.label} + {phase.number}{' '} + {typeof phase.label === 'string' ? ( + phase.label + ) : ( + <> + {phase.label.en} + + + )} )) } @@ -51,11 +63,71 @@ const active = phases.find((phase) => phase.id === initial) ?? phases[0];
- {active.meta.deliverable} - {active.meta.gate} + + { + typeof active.meta.deliverable === 'string' ? ( + active.meta.deliverable + ) : ( + <> + <> + {active.meta.deliverable.en} + + + + ) + } + + + { + typeof active.meta.gate === 'string' ? ( + active.meta.gate + ) : ( + <> + <> + {active.meta.gate.en} + + + + ) + } +
-

{active.title}

-

{active.body}

+

+ { + typeof active.title === 'string' ? ( + active.title + ) : ( + <> + <> + {active.title.en} + + + + ) + } +

+

+ { + typeof active.body === 'string' ? ( + active.body + ) : ( + <> + <> + {active.body.en} + + + + ) + } +

{active.evidence}
diff --git a/src/components/blocks/RouteTable.astro b/src/components/blocks/RouteTable.astro index c66345b..6c9d244 100644 --- a/src/components/blocks/RouteTable.astro +++ b/src/components/blocks/RouteTable.astro @@ -6,14 +6,16 @@ // renders the column header + the four rows; task 15 will hydrate the // "route detail" panel to the right. +type Localized = { en: string; pt: string }; + interface Route { id: 'plan' | 'build' | 'explore' | 'review' | string; /** Strong label, e.g. "Plan". */ - strong: string; + strong: string | Localized; /** Profile descriptor, e.g. "strong / broad". */ - profile: string; + profile: string | Localized; /** Prompt shape copy. */ - prompt: string; + prompt: string | Localized; } interface Props { @@ -37,9 +39,42 @@ const { routes, initial = routes[0]?.id ?? 'plan' } = Astro.props; data-route={route.id} aria-pressed={route.id === initial} > - {route.strong} - {route.profile} - {route.prompt} + + {typeof route.strong === 'string' ? ( + route.strong + ) : ( + <> + {route.strong.en} + + + )} + + + {typeof route.profile === 'string' ? ( + route.profile + ) : ( + <> + {route.profile.en} + + + )} + + + {typeof route.prompt === 'string' ? ( + route.prompt + ) : ( + <> + {route.prompt.en} + + + )} + )) } diff --git a/src/components/blocks/SkillPackage.astro b/src/components/blocks/SkillPackage.astro index e045b3d..6557391 100644 --- a/src/components/blocks/SkillPackage.astro +++ b/src/components/blocks/SkillPackage.astro @@ -7,12 +7,14 @@ // paired with a `
` detail panel by the parent page; this component // renders only the picker. +type Localized = { en: string; pt: string }; + interface PackageFile { id: 'skill' | 'references' | 'scripts' | 'assets' | string; /** Path rendered inside ``, e.g. "SKILL.md". */ path: string; /** Helper copy under the path. */ - small: string; + small: string | Localized; } interface Props { @@ -34,7 +36,18 @@ const { files, initial = files[0]?.id ?? 'skill' } = Astro.props; aria-selected={file.id === initial} > {file.path} - {file.small} + + {typeof file.small === 'string' ? ( + file.small + ) : ( + <> + {file.small.en} + + + )} + )) } diff --git a/src/components/blocks/WorktreeMap.astro b/src/components/blocks/WorktreeMap.astro index 4e44213..c5125a5 100644 --- a/src/components/blocks/WorktreeMap.astro +++ b/src/components/blocks/WorktreeMap.astro @@ -6,15 +6,17 @@ // the `data-tree` hook asserted by `scripts/verify.mjs`. The `root` node and // the `initial` branch are marked selected. +type Localized = { en: string; pt: string }; + interface Branch { /** The `data-tree` hook, e.g. "ui", "tests", "docs". */ id: string; /** Uppercase label, e.g. "UI AGENT". */ - label: string; + label: string | Localized; /** Strong line, e.g. "agent/ui". */ strong: string; /** Status small, e.g. "3 files · working". */ - small: string; + small: string | Localized; /** CSS modifier so each branch picks up its tone. */ tone: 'ui' | 'tests' | 'docs' | string; } @@ -50,9 +52,31 @@ const { branches, initial = 'main' } = Astro.props; aria-selected={branch.id === initial} > <> - {branch.label} + + {typeof branch.label === 'string' ? ( + branch.label + ) : ( + <> + {branch.label.en} + + + )} + {branch.strong} - {branch.small} + + {typeof branch.small === 'string' ? ( + branch.small + ) : ( + <> + {branch.small.en} + + + )} + ))
{columns[0]}{columns[1]}{columns[2]} + { + typeof columns[0] === 'string' ? ( + columns[0] + ) : ( + <> + <> + {columns[0].en} + + + + ) + } + + { + typeof columns[1] === 'string' ? ( + columns[1] + ) : ( + <> + <> + {columns[1].en} + + + + ) + } + + { + typeof columns[2] === 'string' ? ( + columns[2] + ) : ( + <> + <> + {columns[2].en} + + + + ) + } +
{row.package}{row.contains}{row.why} + {typeof row.package === 'string' ? ( + row.package + ) : ( + <> + {row.package.en} + + + )} + + {typeof row.contains === 'string' ? ( + row.contains + ) : ( + <> + {row.contains.en} + + + )} + + {typeof row.why === 'string' ? ( + row.why + ) : ( + <> + {row.why.en} + + + )} +