Files
retire-planner/BILINGUAL-REPORT.md
T

7.1 KiB
Raw Blame History

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
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.