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:
1 parent
1b194b6f92
commit
ea953ec23c
60 files changed
+2788
-241
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user