Files
retire-planner/BILINGUAL-REPORT.md

90 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BILINGUAL-REPORT.md — EN/FR for https://retire.krisforbes.ca
Executed end-to-end per `BILINGUAL-PLAN.md` (Stages 0–6). Live at
https://retire.krisforbes.ca/ — every user-facing string is bilingual, with a persisted
`EN | FR` header toggle, locale-correct `Intl.NumberFormat` numbers, and `lang` carried in
share links. **The engine (`site/engine/*.js`) is untouched.**
## What shipped
| Piece | File | Notes |
|---|---|---|
| i18n scaffold | `site/i18n.js` (new, 558 lines) | `I18N` dict (EN + full FR mirror), `t(key, vars)` with dot-path lookup, fr→en→key fallback and `{name}` interpolation, `getLang/setLang/detectLang/applyI18n/wireToggle`, locale formatters `fmtMoney / fmtMoneyK / fmtInt / fmtPct / fmtMoney2`. |
| Header toggle | `site/index.html`, `site/style.css` | `EN | FR` pill (`.lang-toggle`, `#lang-en/#lang-fr`, `aria-pressed`), 44px touch targets; mobile ≤860px keeps privacy pill hidden. |
| App copy | `site/app.js` | 1:1 faithful port: all dynamic strings (14/17 steps, panel + chips + insight, chart titles/legends/annotations, verdict, gap cards, 11-col table, what-ifs, vision captions, validation errors, share/print/restart, disclaimer, privacy) via `t()`; **all geometry, ids, engine field names and control flow byte-identical to baseline.** |
| Language state | `site/app.js` + `site/i18n.js` | LS `retire-planner-v1` = `{a, step, screen, lang}` (lang merged read-modify-write); share payload `#p={a,step,lang}`; load precedence **payload lang → saved lang → `navigator.language` fr\* → en**; legacy payloads (no lang) open EN. Toggle re-renders the current screen live (`__onLangChange`). |
| FR typography | `site/i18n.js` `frNorm()` | U+2019 apostrophes, NBSP before `: ; ! ? %`, NBSP inside `« »`. |
| Numbers | formatters in `site/i18n.js` | EN `$959,835` / `$523K` / `$1.05M` / `3.5%`; FR `959 835 $` / `523 k$` / `1,05 M$` / `3,5 %` (fr-CA Intl compact emits `523 k $`, so FR compact is hand-rolled to the plan's glossary form). All chart axis/legend/annotation text and table cells go through the formatters. |
Glossary followed: REER, FERR, CELI, RPC, SV, SRG, ménage, conjoint, fractionnement du
revenu de pension, récupération de la SV, RevenuQuébec.ca, glissez →, barèmes 2026,
k$/M$ compacts. FR chart bar labels kept concise (e.g. g4 scenario names shortened) so
they fit the viewBox.
## Tests (all from workspace root, all green pre-deploy)
- `node tests/engine.test.js` → **RESULT: 36 pass, 0 fail** (engine untouched).
- `node tests/i18n.test.js` → **I18N-OK** (48 checks): deep key-set parity en↔fr, no empty
values, `{placeholder}` set equality for every key, every `t()` key used in app.js exists
in both dicts, formatter exact outputs (`$959,835`/`959 835 $`, `$523K`/`523 k$`,
`$1.05M`/`1,05 M$`, `5%`/`5 %`, `3.5%`/`3,5 %`, `−$32,962`/`−32 962 $`,
`$1,507.65`/`1 507,65 $`), FR typography (U+2019, NBSP), `applyI18n` over the real
`index.html` (FR title *Planificateur de retraite…*, h1, meta description,
`documentElement.lang`, `aria-pressed`), lang persistence merge, load precedence
(payload > saved > detect; legacy payload → en).
- `node tests/ui.smoke.js` → **SMOKE-OK** (56 checks): full begin → province → 14 steps →
results → what-if walk run **twice, EN and FR**, asserting locale strings (step titles,
validation message, panel, verdict, chart titles, legend, table header, year summary,
stamp `tax rates 2026 · CPP max $1,507.65` vs `barèmes 2026 · RPC max. 1 507,65 $`),
plus `LS.lang === 'fr'` after the FR run.
## EN regression (baseline vs deployed, live site)
`audit/bilingual-en-regression/` (fresh EN runs) vs `audit/bilingual-en-baseline/`:
- Geometry identical: panel 374×218 top 618, chips 344×158, step body, btn-next,
g1 svg 560×236, table `.tbl-wrap` clientW 326 (scrollW 518, horizontal-scroll pattern
unchanged), vision 356×250 — at 390/360/320.
- Pixel diff (thresholded, per screenshot): **0.9–4.1 %** of pixels — the differences are
the header EN|FR toggle and the reformatted numerals (`$523K`, `2 %`-style compact/trim
allowed by the plan's formatter spec). Header height 55→57 CSS px (toggle button);
full-page doc height +6 px. No element moved; content layout below the header identical.
- Desktop (≥861px): toggle is the only header addition; grid/content layout untouched
(`desktop-results-en-top` diff evidence in `audit/bilingual-fr/`).
## FR mobile audit — 320/360/390 + desktop 1280 (`audit/bilingual-fr/`, 23 PNGs + geo.json)
**98/98 programmatic checks pass** (`audit/bilingual_audit.py` → `FR-AUDIT-OK`):
- **No horizontal document overflow** at any viewport: `max(documentElement, body).scrollWidth == innerWidth` (320/360/390/1280, intro, province grid, income input, slider, sheet-open, results top/charts/table).
- **Chart text fits the viewBox**: every `<text>` in every `#g1–#g4` SVG measured with `getBBox()`; **0 violations** on every device+screen (FR labels kept concise, e.g. g4 scenarios, `L'évolution de votre épargne`).
- **Sheet/chips**: panel visible on every quiz step, chips font 11px, panel height 235px (33.5 vh at 320) and 291px (41.5 vh) open — live `--sheet-h` sync intact.
- **CTA**: quiz *Suivant →* in viewport on every step (sticky foot), intro CTA rendered, share CTA on results (full-page shots).
- **Table**: 11 headers in DOM, 9 visible at ≤560px (non-reg + GIS columns hidden by the pre-existing "Fix 5" mobile design), 13px header font, `.tbl-wrap` scrolls horizontally (clientW 256/296/326 < scrollW 549) — nothing clipped.
- FR rendering proof: RPC/REER/CELI/retraite token counts per screen (5 intro, 2 quiz steps, 8–10 results); desktop EN control shot shows 0 FR tokens.
## Deploy + off-LAN verification
- `bash scripts/deploy.sh` → **DEPLOY-OK**; served `index.html` marker `retire-planner v1` grep = 1; served `i18n.js` contains FR strings; `index.html` references `i18n.js`.
- Off-LAN (`audit/offlan_check.py` via Midhurst troublebox, result in `audit/bilingual-geo-check.json`): **200 + marker + `lang-toggle` + `i18n.js` script + FR strings** (`OFFLAN-OK`, vantage trouble@100.96.20.61).
## FR notes / accepted deltas
- Header brand uses ellipsis at 320px (*Planificateur de retraite…* with the EN
*Retire Planner* kept short) so the toggle + restart stay on one row.
- Legacy share links (payloads without `lang`) open in EN — deterministic, never garbled.
- FR verdict/stamp strings use non-breaking narrow spaces per fr-CA Intl (`1 507,65 $`);
tests normalize U+202F/U+00A0 for assertions.
- EN micro-deltas mandated by the formatter spec: compact `$523K` (was `$523k`),
trailing-zero trim on `pct2` (`2 %`-class output), `to {le}` in the verdict line
(identical at the default life expectancy).
## Files changed
`site/i18n.js` (new) · `site/app.js` · `site/index.html` · `site/style.css` ·
`tests/i18n.test.js` (new) · `tests/ui.smoke.js` · `audit/bilingual_audit.py` (new) ·
`audit/offlan_check.py` (new) · `audit/bilingual-en-baseline/` · `audit/bilingual-en-regression/` ·
`audit/bilingual-fr/` · `audit/bilingual-geo-check.json` · `BILINGUAL-PLAN.md` · `.gitignore`.
`git diff --stat site/engine/` → empty: engine untouched, 36/36.