Merge branch 'main' into refactor/task-15d-page-full-guide

This commit is contained in:
Marcos Paulo
2026-09-05 22:02:11 +00:00
7 changed files with 383 additions and 39 deletions
@@ -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 0711 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 `<br>`. 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
+55 -7
View File
@@ -9,22 +9,24 @@
// hairlines between worker cards; the parent background is an off-token seam // hairlines between worker cards; the parent background is an off-token seam
// colour marked inline with `token-gap`. // colour marked inline with `token-gap`.
type Localized = { en: string; pt: string };
interface Worker { interface Worker {
/** Used as the `data-worker` hook and the key in the source. */ /** Used as the `data-worker` hook and the key in the source. */
id: string; id: string;
/** Short label rendered uppercase, e.g. "UI". */ /** Short label rendered uppercase, e.g. "UI". */
label: string; label: string | Localized;
/** Body copy describing the worker's remit. */ /** Body copy describing the worker's remit. */
strong: string; strong: string | Localized;
/** Path label, e.g. "agent/ui". */ /** Path label, e.g. "agent/ui". */
code: string; code: string;
} }
interface Props { interface Props {
orchestrator: { orchestrator: {
eyebrow: string; eyebrow: string | Localized;
/** May contain inline `<br>`; rendered with `set:html`. */ /** May contain inline `<br>`; rendered with `set:html`. */
title: string; title: string | Localized;
code: string; code: string;
}; };
workers: Worker[]; workers: Worker[];
@@ -36,8 +38,32 @@ const { orchestrator, workers, initial = workers[0]?.id } = Astro.props;
<div class="fleet-grid"> <div class="fleet-grid">
<article class="captain"> <article class="captain">
<span class="captain-eyebrow">{orchestrator.eyebrow}</span> <span class="captain-eyebrow">
{
typeof orchestrator.eyebrow === 'string' ? (
orchestrator.eyebrow
) : (
<>
<>
<span data-language-content="en">{orchestrator.eyebrow.en}</span>
<span data-language-content="pt" hidden>
{orchestrator.eyebrow.pt}
</span>
</>
</>
)
}
</span>
{
typeof orchestrator.title === 'string' ? (
<h2 set:html={orchestrator.title} /> <h2 set:html={orchestrator.title} />
) : (
<>
<h2 data-language-content="en" set:html={orchestrator.title.en} />
<h2 data-language-content="pt" hidden set:html={orchestrator.title.pt} />
</>
)
}
<code>{orchestrator.code}</code> <code>{orchestrator.code}</code>
</article> </article>
<div class="arrow" aria-hidden="true"></div> <div class="arrow" aria-hidden="true"></div>
@@ -49,8 +75,30 @@ const { orchestrator, workers, initial = workers[0]?.id } = Astro.props;
data-worker={worker.id} data-worker={worker.id}
aria-pressed={worker.id === initial} aria-pressed={worker.id === initial}
> >
<span>{worker.label}</span> <span>
<strong>{worker.strong}</strong> {typeof worker.label === 'string' ? (
worker.label
) : (
<>
<span data-language-content="en">{worker.label.en}</span>
<span data-language-content="pt" hidden>
{worker.label.pt}
</span>
</>
)}
</span>
<strong>
{typeof worker.strong === 'string' ? (
worker.strong
) : (
<>
<span data-language-content="en">{worker.strong.en}</span>
<span data-language-content="pt" hidden>
{worker.strong.pt}
</span>
</>
)}
</strong>
<code>{worker.code}</code> <code>{worker.code}</code>
</button> </button>
)) ))
+90 -10
View File
@@ -7,18 +7,20 @@
// muted body) and lives here so the next page that needs it gets the same // muted body) and lives here so the next page that needs it gets the same
// beat for free. // beat for free.
type Localized = { en: string; pt: string };
interface Row { interface Row {
/** The package name, rendered as a `<th>` (column 1). */ /** The package name, rendered as a `<th>` (column 1). */
package: string; package: string | Localized;
/** What the package contains (column 2). */ /** What the package contains (column 2). */
contains: string; contains: string | Localized;
/** Why this matters (column 3). */ /** Why this matters (column 3). */
why: string; why: string | Localized;
} }
interface Props { interface Props {
/** Column headers in render order. */ /** Column headers in render order. */
columns: [string, string, string]; columns: [string | Localized, string | Localized, string | Localized];
rows: Row[]; rows: Row[];
} }
@@ -28,18 +30,96 @@ const { columns, rows } = Astro.props;
<table class="handoff-table"> <table class="handoff-table">
<thead> <thead>
<tr> <tr>
<th>{columns[0]}</th> <th>
<th>{columns[1]}</th> {
<th>{columns[2]}</th> typeof columns[0] === 'string' ? (
columns[0]
) : (
<>
<>
<span data-language-content="en">{columns[0].en}</span>
<span data-language-content="pt" hidden>
{columns[0].pt}
</span>
</>
</>
)
}
</th>
<th>
{
typeof columns[1] === 'string' ? (
columns[1]
) : (
<>
<>
<span data-language-content="en">{columns[1].en}</span>
<span data-language-content="pt" hidden>
{columns[1].pt}
</span>
</>
</>
)
}
</th>
<th>
{
typeof columns[2] === 'string' ? (
columns[2]
) : (
<>
<>
<span data-language-content="en">{columns[2].en}</span>
<span data-language-content="pt" hidden>
{columns[2].pt}
</span>
</>
</>
)
}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
rows.map((row) => ( rows.map((row) => (
<tr> <tr>
<th scope="row">{row.package}</th> <th scope="row">
<td>{row.contains}</td> {typeof row.package === 'string' ? (
<td>{row.why}</td> row.package
) : (
<>
<span data-language-content="en">{row.package.en}</span>
<span data-language-content="pt" hidden>
{row.package.pt}
</span>
</>
)}
</th>
<td>
{typeof row.contains === 'string' ? (
row.contains
) : (
<>
<span data-language-content="en">{row.contains.en}</span>
<span data-language-content="pt" hidden>
{row.contains.pt}
</span>
</>
)}
</td>
<td>
{typeof row.why === 'string' ? (
row.why
) : (
<>
<span data-language-content="en">{row.why.en}</span>
<span data-language-content="pt" hidden>
{row.why.pt}
</span>
</>
)}
</td>
</tr> </tr>
)) ))
} }
+81 -9
View File
@@ -8,18 +8,20 @@
// //
// Every `data-phase` value is asserted by `scripts/verify.mjs`. // Every `data-phase` value is asserted by `scripts/verify.mjs`.
type Localized = { en: string; pt: string };
export type PhaseId = 'plan' | 'build' | 'review'; export type PhaseId = 'plan' | 'build' | 'review';
interface Phase { interface Phase {
id: PhaseId; id: PhaseId;
/** Two-letter label rendered in the tab, e.g. "PLAN". */ /** Two-letter label rendered in the tab, e.g. "PLAN". */
label: string; label: string | Localized;
/** Numeric prefix, e.g. "01". */ /** Numeric prefix, e.g. "01". */
number: string; number: string;
title: string; title: string | Localized;
body: string; body: string | Localized;
/** Headline + small caption shown above the panel title. */ /** 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. */ /** Code-line evidence shown at the bottom of the panel. */
evidence: string; evidence: string;
} }
@@ -43,7 +45,17 @@ const active = phases.find((phase) => phase.id === initial) ?? phases[0];
role="tab" role="tab"
aria-selected={phase.id === initial} aria-selected={phase.id === initial}
> >
<b>{phase.number}</b> {phase.label} <b>{phase.number}</b>{' '}
{typeof phase.label === 'string' ? (
phase.label
) : (
<>
<span data-language-content="en">{phase.label.en}</span>
<span data-language-content="pt" hidden>
{phase.label.pt}
</span>
</>
)}
</button> </button>
)) ))
} }
@@ -51,11 +63,71 @@ const active = phases.find((phase) => phase.id === initial) ?? phases[0];
<article class="phase-panel" id="phase-panel" aria-live="polite"> <article class="phase-panel" id="phase-panel" aria-live="polite">
<div class="phase-meta"> <div class="phase-meta">
<span>{active.meta.deliverable}</span> <span>
<small>{active.meta.gate}</small> {
typeof active.meta.deliverable === 'string' ? (
active.meta.deliverable
) : (
<>
<>
<span data-language-content="en">{active.meta.deliverable.en}</span>
<span data-language-content="pt" hidden>
{active.meta.deliverable.pt}
</span>
</>
</>
)
}
</span>
<small>
{
typeof active.meta.gate === 'string' ? (
active.meta.gate
) : (
<>
<>
<span data-language-content="en">{active.meta.gate.en}</span>
<span data-language-content="pt" hidden>
{active.meta.gate.pt}
</span>
</>
</>
)
}
</small>
</div> </div>
<h3>{active.title}</h3> <h3>
<p>{active.body}</p> {
typeof active.title === 'string' ? (
active.title
) : (
<>
<>
<span data-language-content="en">{active.title.en}</span>
<span data-language-content="pt" hidden>
{active.title.pt}
</span>
</>
</>
)
}
</h3>
<p>
{
typeof active.body === 'string' ? (
active.body
) : (
<>
<>
<span data-language-content="en">{active.body.en}</span>
<span data-language-content="pt" hidden>
{active.body.pt}
</span>
</>
</>
)
}
</p>
<code>{active.evidence}</code> <code>{active.evidence}</code>
</article> </article>
+41 -6
View File
@@ -6,14 +6,16 @@
// renders the column header + the four rows; task 15 will hydrate the // renders the column header + the four rows; task 15 will hydrate the
// "route detail" panel to the right. // "route detail" panel to the right.
type Localized = { en: string; pt: string };
interface Route { interface Route {
id: 'plan' | 'build' | 'explore' | 'review' | string; id: 'plan' | 'build' | 'explore' | 'review' | string;
/** Strong label, e.g. "Plan". */ /** Strong label, e.g. "Plan". */
strong: string; strong: string | Localized;
/** Profile descriptor, e.g. "strong / broad". */ /** Profile descriptor, e.g. "strong / broad". */
profile: string; profile: string | Localized;
/** Prompt shape copy. */ /** Prompt shape copy. */
prompt: string; prompt: string | Localized;
} }
interface Props { interface Props {
@@ -37,9 +39,42 @@ const { routes, initial = routes[0]?.id ?? 'plan' } = Astro.props;
data-route={route.id} data-route={route.id}
aria-pressed={route.id === initial} aria-pressed={route.id === initial}
> >
<strong>{route.strong}</strong> <strong>
<b>{route.profile}</b> {typeof route.strong === 'string' ? (
<small>{route.prompt}</small> route.strong
) : (
<>
<span data-language-content="en">{route.strong.en}</span>
<span data-language-content="pt" hidden>
{route.strong.pt}
</span>
</>
)}
</strong>
<b>
{typeof route.profile === 'string' ? (
route.profile
) : (
<>
<span data-language-content="en">{route.profile.en}</span>
<span data-language-content="pt" hidden>
{route.profile.pt}
</span>
</>
)}
</b>
<small>
{typeof route.prompt === 'string' ? (
route.prompt
) : (
<>
<span data-language-content="en">{route.prompt.en}</span>
<span data-language-content="pt" hidden>
{route.prompt.pt}
</span>
</>
)}
</small>
</button> </button>
)) ))
} }
+15 -2
View File
@@ -7,12 +7,14 @@
// paired with a `<article>` detail panel by the parent page; this component // paired with a `<article>` detail panel by the parent page; this component
// renders only the picker. // renders only the picker.
type Localized = { en: string; pt: string };
interface PackageFile { interface PackageFile {
id: 'skill' | 'references' | 'scripts' | 'assets' | string; id: 'skill' | 'references' | 'scripts' | 'assets' | string;
/** Path rendered inside `<code>`, e.g. "SKILL.md". */ /** Path rendered inside `<code>`, e.g. "SKILL.md". */
path: string; path: string;
/** Helper copy under the path. */ /** Helper copy under the path. */
small: string; small: string | Localized;
} }
interface Props { interface Props {
@@ -34,7 +36,18 @@ const { files, initial = files[0]?.id ?? 'skill' } = Astro.props;
aria-selected={file.id === initial} aria-selected={file.id === initial}
> >
<code>{file.path}</code> <code>{file.path}</code>
<small>{file.small}</small> <small>
{typeof file.small === 'string' ? (
file.small
) : (
<>
<span data-language-content="en">{file.small.en}</span>
<span data-language-content="pt" hidden>
{file.small.pt}
</span>
</>
)}
</small>
</button> </button>
)) ))
} }
+28 -4
View File
@@ -6,15 +6,17 @@
// the `data-tree` hook asserted by `scripts/verify.mjs`. The `root` node and // the `data-tree` hook asserted by `scripts/verify.mjs`. The `root` node and
// the `initial` branch are marked selected. // the `initial` branch are marked selected.
type Localized = { en: string; pt: string };
interface Branch { interface Branch {
/** The `data-tree` hook, e.g. "ui", "tests", "docs". */ /** The `data-tree` hook, e.g. "ui", "tests", "docs". */
id: string; id: string;
/** Uppercase label, e.g. "UI AGENT". */ /** Uppercase label, e.g. "UI AGENT". */
label: string; label: string | Localized;
/** Strong line, e.g. "agent/ui". */ /** Strong line, e.g. "agent/ui". */
strong: string; strong: string;
/** Status small, e.g. "3 files · working". */ /** Status small, e.g. "3 files · working". */
small: string; small: string | Localized;
/** CSS modifier so each branch picks up its tone. */ /** CSS modifier so each branch picks up its tone. */
tone: 'ui' | 'tests' | 'docs' | string; tone: 'ui' | 'tests' | 'docs' | string;
} }
@@ -50,9 +52,31 @@ const { branches, initial = 'main' } = Astro.props;
aria-selected={branch.id === initial} aria-selected={branch.id === initial}
> >
<> <>
<span>{branch.label}</span> <span>
{typeof branch.label === 'string' ? (
branch.label
) : (
<>
<span data-language-content="en">{branch.label.en}</span>
<span data-language-content="pt" hidden>
{branch.label.pt}
</span>
</>
)}
</span>
<strong>{branch.strong}</strong> <strong>{branch.strong}</strong>
<small>{branch.small}</small> <small>
{typeof branch.small === 'string' ? (
branch.small
) : (
<>
<span data-language-content="en">{branch.small.en}</span>
<span data-language-content="pt" hidden>
{branch.small.pt}
</span>
</>
)}
</small>
</> </>
</button> </button>
)) ))