Bilingual EN/FR: i18n.js dict+t(), lang state+payload, header toggle, FR copy, Intl formatters; i18n.test.js + bilingual smoke; FR mobile audit evidence + BILINGUAL-REPORT.md

This commit is contained in:
krisf committed 2026-10-09 10:06:55 -04:00
1 parent 65863c0302
commit 5f01dd7ca6
60 files changed
+2788 -241

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
# 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.