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
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}
+
+ {orchestrator.eyebrow.pt}
+
+ >
+ >
+ )
+ }
+
+ {
+ typeof orchestrator.title === 'string' ? (
+
+ ) : (
+ <>
+
+
+ >
+ )
+ }
{orchestrator.code}
→
@@ -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}
+
+ {worker.label.pt}
+
+ >
+ )}
+
+
+ {typeof worker.strong === 'string' ? (
+ worker.strong
+ ) : (
+ <>
+ {worker.strong.en}
+
+ {worker.strong.pt}
+
+ >
+ )}
+
{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;
- | {columns[0]} |
- {columns[1]} |
- {columns[2]} |
+
+ {
+ typeof columns[0] === 'string' ? (
+ columns[0]
+ ) : (
+ <>
+ <>
+ {columns[0].en}
+
+ {columns[0].pt}
+
+ >
+ >
+ )
+ }
+ |
+
+ {
+ typeof columns[1] === 'string' ? (
+ columns[1]
+ ) : (
+ <>
+ <>
+ {columns[1].en}
+
+ {columns[1].pt}
+
+ >
+ >
+ )
+ }
+ |
+
+ {
+ typeof columns[2] === 'string' ? (
+ columns[2]
+ ) : (
+ <>
+ <>
+ {columns[2].en}
+
+ {columns[2].pt}
+
+ >
+ >
+ )
+ }
+ |
{
rows.map((row) => (
- | {row.package} |
- {row.contains} |
- {row.why} |
+
+ {typeof row.package === 'string' ? (
+ row.package
+ ) : (
+ <>
+ {row.package.en}
+
+ {row.package.pt}
+
+ >
+ )}
+ |
+
+ {typeof row.contains === 'string' ? (
+ row.contains
+ ) : (
+ <>
+ {row.contains.en}
+
+ {row.contains.pt}
+
+ >
+ )}
+ |
+
+ {typeof row.why === 'string' ? (
+ row.why
+ ) : (
+ <>
+ {row.why.en}
+
+ {row.why.pt}
+
+ >
+ )}
+ |
))
}
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}
+
+ {phase.label.pt}
+
+ >
+ )}
))
}
@@ -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}
+
+ {active.meta.deliverable.pt}
+
+ >
+ >
+ )
+ }
+
+
+ {
+ typeof active.meta.gate === 'string' ? (
+ active.meta.gate
+ ) : (
+ <>
+ <>
+ {active.meta.gate.en}
+
+ {active.meta.gate.pt}
+
+ >
+ >
+ )
+ }
+
- {active.title}
- {active.body}
+
+ {
+ typeof active.title === 'string' ? (
+ active.title
+ ) : (
+ <>
+ <>
+ {active.title.en}
+
+ {active.title.pt}
+
+ >
+ >
+ )
+ }
+
+
+ {
+ typeof active.body === 'string' ? (
+ active.body
+ ) : (
+ <>
+ <>
+ {active.body.en}
+
+ {active.body.pt}
+
+ >
+ >
+ )
+ }
+
{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}
+
+ {route.strong.pt}
+
+ >
+ )}
+
+
+ {typeof route.profile === 'string' ? (
+ route.profile
+ ) : (
+ <>
+ {route.profile.en}
+
+ {route.profile.pt}
+
+ >
+ )}
+
+
+ {typeof route.prompt === 'string' ? (
+ route.prompt
+ ) : (
+ <>
+ {route.prompt.en}
+
+ {route.prompt.pt}
+
+ >
+ )}
+
))
}
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}
+
+ {file.small.pt}
+
+ >
+ )}
+
))
}
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.label.pt}
+
+ >
+ )}
+
{branch.strong}
- {branch.small}
+
+ {typeof branch.small === 'string' ? (
+ branch.small
+ ) : (
+ <>
+ {branch.small.en}
+
+ {branch.small.pt}
+
+ >
+ )}
+
>
))
|