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,166 @@
|
||||
# FEATURE: FULL BILINGUAL (EN/FR) — retire.krisforbes.ca
|
||||
|
||||
**Plan v1 — written 2026-10-09 by Hermes (GLM). Execution: DSH (local 27B), all stages, zero user interaction.**
|
||||
User request (verbatim): *"I also want this application to be available in english and french fully bilingual."*
|
||||
This plan is stage A of a two-stage feature (stage B = public service pension module, see PS-PENSION-PLAN.md, runs after this one).
|
||||
|
||||
## Goal
|
||||
|
||||
Make **every user-facing string** in the app available in English and French, with a visible
|
||||
language toggle, persisted per visitor, included in shared links, with locale-correct number
|
||||
formatting. The engine (`site/engine/*.js`) is presentation-neutral and must NOT change.
|
||||
English behavior must remain functionally identical to today (mobile pass round 1+2 state is the baseline).
|
||||
|
||||
## Current state (verified 2026-10-09, after mobile rounds 1+2)
|
||||
|
||||
- Live: https://retire.krisforbes.ca (Unraid `retireapp` nginx :8103, webroot `/mnt/better/appdata/retireapp`, Caddy + CF grey-cloud). Deploy: `bash scripts/deploy.sh` → `DEPLOY-OK`.
|
||||
- Repo: `gitea.krisforbes.ca/krisf/retire-planner`, main @ `65863c0`. Tests: engine 36/36, UI smoke OK.
|
||||
- Files: `site/index.html`, `site/app.js` (questionnaire STEPS, panel, charts G0–G3, results, table, vision card), `site/style.css`, `site/engine/{engine,canada}.js`, `site/vision/manifest.json` + images, `tests/{engine.test.js,ui.smoke.js}`, `scripts/deploy.sh`.
|
||||
- localStorage key `retire-planner-v1` holds `{a: <answers>, step, screen}`. Share links use `#p=<payload>` encoding the answers.
|
||||
- Header (global, sticky): logo, privacy pill, spacer, `Start over` / `Print` / `Copy link` ghost buttons.
|
||||
- 14 questionnaire steps (single) / 17 (couple), conditional; persistent "Your plan so far" sheet with 4 stat chips + insight line; results with verdict, gap cards, vision card, 4 SVG charts, year-by-year table (11 cols), what-if scenarios, disclaimers, privacy box, footer.
|
||||
- Vision card: `CAPTIONS` object in app.js (14 slugs incl. `hero`) + `vision/manifest.json`.
|
||||
|
||||
## Architecture (implement exactly this shape)
|
||||
|
||||
1. **New file `site/i18n.js`** (dependency-free, loads before `app.js`):
|
||||
- `var I18N = { en: {…}, fr: {…} }` — nested objects, dot-path keys (e.g. `panel.title`, `q.step1.title`).
|
||||
- `function t(key, vars)` — resolves dot path in current language; falls back to `en`, then to the key itself. Interpolates `{name}` placeholders from `vars`.
|
||||
- `function getLang()` / `function setLang(l)` — `'en' | 'fr'`; persists to state; calls `applyI18n()` + re-renders current screen.
|
||||
- `function detectLang()` — `navigator.language` starts with `fr` → `'fr'`, else `'en'`. Used only when no saved state exists.
|
||||
- Locale-aware formatters (use `Intl.NumberFormat`):
|
||||
- `fmtMoney(n)` → en-CA `$959,835` / fr-CA `959 835 $` (0 decimals, CAD)
|
||||
- `fmtMoneyK(n)` → en-CA `$523K`, `$1.05M` / fr-CA `523 k$`, `1,05 M$` (compact, ≤3 significant digits)
|
||||
- `fmtInt(n)` → `959,835` / `959 835`
|
||||
- `fmtPct(x)` → `5%` / `5 %` (fr: space before %)
|
||||
- All chart axis/legend/annotation text and table cells must go through these (today they hand-roll `$523k`).
|
||||
- `function applyI18n()` — walks `[data-i18n="key"]` (sets textContent), `[data-i18n-html="key"]` (innerHTML, for strings with markup), `[data-i18n-attr="placeholder:key,aria-label:key2"]`; sets `document.documentElement.lang` and `document.title` per language.
|
||||
2. **Language state**: add `lang` to the persisted state object (`{a, step, screen, lang}`). On load: payload lang > saved state lang > `detectLang()`. **Share payload**: include `lang` in the `#p=` payload; old payloads without it → `en` (backward compatible).
|
||||
3. **Header toggle**: a compact two-button pill group `EN | FR` in the header (all screens), active button styled, `aria-pressed`. Placed after the logo / before the spacer. On mobile it must fit the single-row header (round-2 layout: ≤52px, hide pill ≤700px, hide header Print ≤860px).
|
||||
4. **index.html**: add `data-i18n` attributes to every static text node (intro, privacy box, footer, disclaimers, panel header label, button labels, section headings, table headers are dynamic — see app.js). Add `<script src="i18n.js"></script>` before `app.js`. Keep `<!-- retire-planner v1 -->` as first line in `<head>`.
|
||||
5. **app.js**: replace every user-facing literal with `t()` / formatters:
|
||||
- `STEPS` definitions: title/sub/options/hints/placeholders → keys.
|
||||
- Province names → keys (`prov.yt` … `prov.nu`).
|
||||
- Panel: chips labels/values, insight line, "Your plan so far".
|
||||
- Insights (all templates), gap cards, verdict text, what-if scenario names + descriptions.
|
||||
- Chart titles/subtitles/axis labels/legends/annotations (G0–G3), incl. the `retire` marker, `runs out ≈`, `shortfall from ~`, G4 bar names + values.
|
||||
- Year table column headers; the collapsed `<summary>` label.
|
||||
- Vision card: `CAPTIONS` becomes `{ slug: {en, fr} }`; `renderVision` picks by current lang.
|
||||
- Validation/error messages ("Enter a value between {min} and {max}").
|
||||
- Buttons: Next/Back/Start over/Print/Copy link/Edit answers/Start/Continue/Yes/No/optional tags.
|
||||
- Share success/failure strings; print header if any.
|
||||
6. **style.css**: minor additions only if needed for the toggle + FR (e.g., `.lang-toggle` styles; allow `hyphens:auto` on FR paragraphs/cards if overflow appears — see risk section). No redesign.
|
||||
|
||||
## Canadian French glossary (use these; official terminology where one exists)
|
||||
|
||||
| EN | FR |
|
||||
|---|---|
|
||||
| Retire Planner (title) | Planificateur de retraite |
|
||||
| Your plan so far | Votre plan jusqu'ici |
|
||||
| Savings at {age} (chip) | Épargne à {age} ans |
|
||||
| Monthly need (chip) | Besoin mensuel |
|
||||
| Shortfall (chip) | Manque à gagner |
|
||||
| Surplus (chip) | Excédent |
|
||||
| RRSP | REER (régime enregistré d'épargne-retraite) |
|
||||
| TFSA | CELI (compte d'épargne libre d'impôt) |
|
||||
| RRIF | FERR (fonds enregistré de revenu de retraite) |
|
||||
| CPP | RPC (Régime de pensions du Canada) |
|
||||
| OAS | SV (Sécurité de la vieillesse) |
|
||||
| GIS | SRG (Supplément de revenu garanti) |
|
||||
| YMPE | MGA (maximum des gains admissibles) |
|
||||
| Age amount (credit) | Crédit pour âge |
|
||||
| Pension income splitting | Fractionnement du revenu de pension |
|
||||
| Pension income amount ($2,000) | Montant pour revenu de pension |
|
||||
| Province names | Colombie-Britannique, Alberta, Saskatchewan, Manitoba, Ontario, Québec, Nouveau-Brunswick, Nouvelle-Écosse, Île-du-Prince-Édouard, Terre-Neuve-et-Labrador, Yukon, Territoires du Nord-Ouest, Nunavut |
|
||||
| Single | Célibataire |
|
||||
| Couple / common-law | Couple / conjoint de fait |
|
||||
| Current age | Âge actuel |
|
||||
| Annual income | Revenu annuel |
|
||||
| Household | Ménage |
|
||||
| Retirement age | Âge de la retraite |
|
||||
| Spending goal | Objectif de dépenses |
|
||||
| Monthly contribution | Cotisation mensuelle |
|
||||
| Employer match | Contrepartie de l'employeur |
|
||||
| Real return | Rendement réel |
|
||||
| Inflation | Inflation |
|
||||
| Life expectancy | Espérance de vie |
|
||||
| Pension / rental / other income | Revenu de pension / locatif / autre |
|
||||
| RRSP balance | Solde du REER |
|
||||
| TFSA balance | Solde du CELI |
|
||||
| Non-registered | Non enregistré |
|
||||
| CPP start age | Âge de début de la RPC |
|
||||
| OAS start age | Âge de début de la SV |
|
||||
| Next / Back | Suivant / Précédent |
|
||||
| Start over | Recommencer |
|
||||
| Print / save PDF | Imprimer / enregistrer en PDF |
|
||||
| Copy link | Copier le lien |
|
||||
| Edit answers | Modifier les réponses |
|
||||
| Start / Continue | Commencer / Continuer |
|
||||
| Optional | Facultatif |
|
||||
| Your savings journey (chart) | L'évolution de votre épargne |
|
||||
| Retirement income by source (chart) | Revenu de retraite par source |
|
||||
| Cash vs. planned spending (chart) | Encaisse vs dépenses prévues |
|
||||
| Four futures (chart) | Quatre scénarios |
|
||||
| Current plan (scenario) | Plan actuel |
|
||||
| +$200/mo to RRSP (scenario) | +200 $/mois au REER |
|
||||
| Retire at 63 (scenario) | Retraite à 63 ans |
|
||||
| Spend $5k more/yr (scenario) | Dépenser 5 000 $ de plus par an |
|
||||
| Year-by-year detail | Détail année par année |
|
||||
| Try a what-if | Essayer un scénario |
|
||||
| What-if scenarios | Scénarios « et si » |
|
||||
| Age (table col) | Âge |
|
||||
| Withdraw (col) | Retrait |
|
||||
| Total (col) | Total |
|
||||
| Non-reg (col) | Non enreg. |
|
||||
| Tax + clawback (col) | Impôt + récup. |
|
||||
| Unfunded (col) | Non financé |
|
||||
| Estimates only — not financial advice | Estimations seulement — il ne s'agit pas de conseils financiers |
|
||||
| 100% client-side — answers never leave your browser | 100 % côté client — vos réponses ne quittent jamais votre navigateur |
|
||||
| Interests: dock/travel/garden/family/volunteering/learning/snowbird/hobbies/cottage/city/pets/grandkids/health/legacy | quai / voyages / jardinage / famille / bénévolat / apprentissage / snowbird (courant au Québec) / passe-temps / chalet / ville / animaux / petits-enfants / santé / héritage |
|
||||
| Sources | Sources |
|
||||
| (public service pension terms — stage B will need these too) | pension de la fonction publique, service ouvrant droit à pension, pension viagère, prestation de raccordement, allocation annuelle, pension différée, pension non réduite, réduction pour retraite anticipée, conjoint survivant, indexée à l'IPC, salaire moyen des cinq années consécutives les mieux payées, classification TI, groupe 1 / groupe 2, MMGP (moyenne des maximums des gains admissibles) |
|
||||
|
||||
Number formatting rules (fr-CA): thousands = narrow no-break space, decimal = comma,
|
||||
percent has a space before `%`, currency symbol trails (`959 835 $`, `523 k$`).
|
||||
In SVG text the same formatters apply (`1,05 M$` etc.).
|
||||
|
||||
## Known risk: French is ~20–30% longer than English
|
||||
|
||||
Mitigate and verify:
|
||||
- Chart SVG labels: keep FR labels concise; after implementing, assert programmatically that
|
||||
every `<text>` in each chart SVG has `getBBox().width` within the 640-wide viewBox (allow the
|
||||
horizontally-scrollable `.chartbox` on mobile, but nothing may overflow the viewBox itself).
|
||||
- Mobile: re-run the mobile audit in FR at 320/360/390 (script: adapt
|
||||
`/home/ubuntu/.hermes/cache/scratch/mobile_audit.py` to set `lang:'fr'` in localStorage before load).
|
||||
The `--sheet-h` live-sync from mobile round 2 handles panel height automatically. Check:
|
||||
no horizontal document overflow, chips readable, table scrolls, sticky CTA works, buttons fit.
|
||||
- Desktop FR screenshot at 1280px.
|
||||
- If specific FR strings overflow, shorten the FR wording (never the EN) or allow wrapping —
|
||||
fix in CSS, not by truncating text.
|
||||
|
||||
## Stages (execute in order; zero questions; every stage pre-approved)
|
||||
|
||||
0. **Preflight**: confirm working tree clean at `65863c0`; run `node tests/engine.test.js` (36/36) and `node tests/ui.smoke.js` (SMOKE-OK); capture EN baseline screenshots at 390/320 (reuse `mobile_audit.py` → copy to `audit/bilingual-en-baseline/`).
|
||||
1. **i18n scaffold**: create `site/i18n.js` (dictionary with **all** keys for both languages — build it by walking index.html + app.js and extracting every string), formatters, `t()`, lang state, `applyI18n()`, share-payload lang, header toggle in index.html + style.css, script tag.
|
||||
2. **Wrap static HTML**: `data-i18n` attributes across index.html.
|
||||
3. **Wrap app.js**: all dynamic strings via `t()`/formatters (steps, panel, insights, charts, table, vision captions, scenarios, errors, share/print strings). Vision `CAPTIONS` → `{slug:{en,fr}}`.
|
||||
4. **Tests**: new `tests/i18n.test.js` — (a) deep key-set parity en↔fr; (b) no empty values; (c) `{placeholder}` sets match across languages for every key; (d) formatter exact-output checks (`$959,835` / `959 835 $` / `$1.05M` / `1,05 M$` / `5%` / `5 %`); (e) FR render assertions: with lang=fr, DOM shows French for ~15 critical strings (title, panel label, a step title, a chip label, results heading, a chart title, a table header, the privacy line, the disclaimer). Extend `tests/ui.smoke.js` to run its full assertion set in **both** languages (parameterize expected strings by lang; keep output ending `SMOKE-OK`). Engine tests untouched (36/36).
|
||||
5. **Deploy + verify**: `bash scripts/deploy.sh` → `DEPLOY-OK`; origin grep marker = 1. EN regression: fresh EN screenshots at 390/320 — functionally identical to baseline (spot-check via vision). FR: mobile audit variant at 320/360/390 + desktop 1280; programmatic checks: no horizontal overflow (`document.documentElement.scrollWidth <= innerWidth + 1`), chart text fits viewBox (getBBox assertion), sheet/chips/CTA checks pass. Off-LAN paramiko check (Midhurst troublebox `trouble@100.96.20.61:2222`, password candidates in `~/projects/*.py` lines containing `b1tvvt`): site 200 + marker. Save FR screenshots to `audit/bilingual-fr/`.
|
||||
6. **Report + commit + push**: `BILINGUAL-REPORT.md` (what changed, glossary notes, test evidence, FR screenshots, known FR layout notes) + `audit/` evidence; commit + push to `krisf/retire-planner` main.
|
||||
|
||||
## Hard constraints
|
||||
|
||||
- **Do not modify** `site/engine/engine.js` or `site/engine/canada.js` (presentation-only change).
|
||||
- Keep `<!-- retire-planner v1 -->` first line in `<head>`; keep the privacy statement's meaning intact in both languages.
|
||||
- No frameworks, no build step, no CDN. `deploy.sh` rsyncs `site/` — `i18n.js` ships automatically.
|
||||
- Desktop (≥861px) EN layout unchanged; FR may wrap slightly longer text (acceptable if nothing overflows horizontally).
|
||||
- If a stage fails twice, stop, keep the last good deploy live, and report the blocker.
|
||||
|
||||
## Definition of done
|
||||
|
||||
- Toggle EN|FR in header on every screen; persists across reload; survives in share links; `navigator.language=fr*` defaults to FR for new visitors.
|
||||
- Every visible string renders in the selected language (EN and FR verified by screenshots + tests).
|
||||
- `tests/i18n.test.js` green (parity/placeholders/formatters/FR render), `tests/ui.smoke.js` green in both languages, engine 36/36 untouched.
|
||||
- FR mobile verified at 320/360/390 + desktop; no horizontal overflow; charts fit viewBox.
|
||||
- Deployed, off-LAN 200, committed + pushed with `BILINGUAL-REPORT.md` + audit evidence.
|
||||
- **Your FINAL output line must be exactly: `BILINGUAL_DONE`**
|
||||
Reference in new issue
Block a user