Files
retire-planner/site/i18n.js
T

705 lines
40 KiB
JavaScript
Raw 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.
/* Retire Planner i18n — dependency-free, loads before app.js.
EN/FR dictionary (dot-path keys), t() with {placeholder} interpolation, lang state
(payload > saved state > navigator), Intl formatters, applyI18n(), header EN|FR toggle.
Exposes globals: I18N, t, getLang, setLang, detectLang, applyI18n,
fmtMoney, fmtMoneyK, fmtMoney2, fmtInt, fmtPct. */
(function () {
'use strict';
var LS_KEY = 'retire-planner-v1';
var MINUS = '\u2212'; /* − */
var NB = '\u00a0'; /* no-break space (French punctuation) */
var NNB = '\u202f'; /* narrow no-break space (French units, matches Intl fr-CA) */
/* ---------- dictionary ---------- */
var EN = {
doc: {
title: 'Retire Planner — Canadian retirement, modelled properly',
desc: 'A Canadian retirement calculator with real CPP/OAS/GIS/RRIF mechanics and live 2026 tax rates. 100% client-side — answers never leave your browser.'
},
brand: 'Retire Planner',
hdr: {
pill: '🔒 100% client-side — answers never leave your browser',
pillTitle: 'No server, no tracking, no accounts',
startOver: 'Start over'
},
intro: {
h1: 'Will you actually run out of money?',
lede: 'Most Canadian retirement calculators stop at “your RRSP grows at 6%”. This one models <b>real CPP/OAS/GIS/RRIF mechanics</b>, provincial taxes fetched live, and the tax cliff most planners won\'t show you.',
tick1: 'CPP early/late (−0.6%/mo before 65, +0.7% after), OAS deferral, clawback & GIS',
tick2: 'Live 2026 federal + all 13 province/territory tax tables',
tick3: 'RRIF minimums at 71, pension splitting, tax-smart drawdown',
tick4: 'Couples: joint taxes, split pensions, household GIS',
privacy: '🔒 100% client-side — answers never leave your browser. No account, no tracking, nothing is sent anywhere.',
cta: 'Build my retirement plan →',
fine: '~2 minutes. Works on one screen at a time; you can change anything later.'
},
quiz: {
back: '← Back',
next: 'Next →',
seePlan: 'See my plan →',
stepCount: '{i} / {n}'
},
panel: {
title: 'Your plan so far',
savingsAt: 'Savings at retirement',
cppOas67: 'CPP + OAS at 67',
lastsTo: 'Money lasts to',
runsOut: 'runs out ~{age}',
lifetimeTax: 'Lifetime tax'
},
unit: { yr: '/yr', mo: '/mo' },
fields: { unit: '($)' },
i: {
depletion: '⚠️ On this path you run out of money around age <b>{age}</b>. Keep answering — the plan ends with three concrete ways to close the gap.',
cppEarly: 'CPP at {age} is permanently reduced — deferring to 70 is a +42% inflation-proof raise. Move the slider later and watch the panel.',
oasLate: 'OAS at {age}: +36% for life, plus a bigger GIS window in between. Good trade for most people.',
qc: 'ℹ️ Québec: QPP is <b>not</b> modelled — add your QPP estimate as “Pension $/yr”. QPP typically replaces ~35% of income.',
cliff: 'You’ll hit the RRIF-minimum tax cliff at 71. TFSA room now softens it later — check the gap cards.',
couple: 'With pension splitting + household GIS, your joint taxes drop — the panel shows the joint total.',
psCover: 'ℹ️ Your public service pension covers about <b>{pct}%</b> of your retirement spending goal.',
live: 'Live numbers — change any answer and this panel moves.'
},
prov: {
ON: 'Ontario', BC: 'British Columbia', AB: 'Alberta', SK: 'Saskatchewan',
MB: 'Manitoba', QC: 'Quebec', NS: 'Nova Scotia', NB: 'New Brunswick',
NL: 'Newfoundland & Labrador', PE: 'PEI', YT: 'Yukon', NT: 'Northwest Territories', NU: 'Nunavut'
},
int: {
rockies: '🏔 Mountains', maritimes: '🌊 Ocean', toronto: '🏙 Big city',
dock: '🛶 Lake / dock', muskoka: '🌲 Cottage country', prairies: '🌾 Open sky',
winter: '⛷ Snow / ski', workshop: '🪵 Workshop', garden: '🌺 Garden',
travel: '✈️ Travel', quebec: '🇫🇷 Québec'
},
q: {
province: { title: 'Where do you live?', hint: 'Taxes, and your retirement view.' },
age: { title: 'Your age', hint: 'Years.' },
household: {
title: 'Household', hint: 'Couple means joint taxes, pension splitting and household GIS.',
single: 'Single', couple: 'Couple'
},
p_age: { title: 'Partner’s age' },
p_income: { title: 'Partner’s annual employment income', hint: '$/yr before tax. 0 if not working.' },
p_savings: {
title: 'Partner’s savings', hint: 'Current balances, today’s dollars.',
rrsp: 'RRSP / RRIF', tfsa: 'TFSA', nonreg: 'Non-registered'
},
income: { title: 'Your annual employment income', hint: '$/yr. Used to estimate your CPP history.' },
rrsp: { title: 'RRSP / RRIF balance' },
savings2: {
title: 'TFSA and non-registered savings',
tfsa: 'TFSA', nonreg: 'Non-registered (cash, stocks, mutual funds)'
},
contrib: {
title: 'Monthly savings into RRSP', hint: 'Your contribution + any employer match.',
you: 'You contribute $/mo', match: 'Employer match $/mo'
},
retire: { title: 'Retirement age', hint: 'When you stop earning employment income. Early retirement (FIRE) supported — plan must self-fund the CPP/OAS gap years.', years: '{v} years' },
spend: { title: 'Planned annual spending in retirement', hint: 'Today’s dollars, after tax. Be honest — this is the number that decides everything.' },
cppstart: {
title: 'Start CPP at…', hint: 'Age {age}: {adj}',
m60: '−36% for life', m65: 'Full rate', m70: '+42% for life', mCustom: 'custom adjustment',
os60: '−36% for life', os65: 'full rate', os70: '+42% for life'
},
oasstart: {
title: 'Start OAS at…', hint: 'Deferring to 70 is +36% for life, plus a bigger GIS window.',
os65: 'on eligibility', os70: '+36% for life'
},
extra: {
title: 'Other income', hint: 'DB pension, rental, royalties… leave 0 if none.',
pension: 'Pension $/yr', rental: 'Rental $/yr', other: 'Other $/yr',
psNote: 'Covered by your public service pension module: {v} — see your plan.',
psNotePending: 'Answer the pension module questions — this is computed from them.'
},
ps: {
title: 'Do you have a federal public service pension?',
hint: 'Optional — for federal employees under the PSSA. We’ll compute it from PSPC rules and add it to your plan.',
yes: 'Yes — I am (or was) a federal public servant', yesD: 'Public Service Pension Plan (PSSA)',
no: 'No / not sure', noD: 'Skip the pension module'
},
ps_join: {
title: 'What year did you join the federal public service?',
hint: 'This sets your group: on/before Dec 31, 2012 = Group 1 (annual allowance from 50); on/after Jan 1, 2013 = Group 2 (from 55).'
},
ps_service: {
title: 'Years of pensionable service',
hint: 'Recognized by PSPC (the calculation caps it at 35 years).',
auto: 'Calculated: joined {join} at age {joinAge}, retiring at {ret} → {years} years of service. Adjust only if you bought past service.',
override: 'Using your value ({years} years). The calculation from your join year gives {auto} years.',
useCalc: 'Use calculated'
},
ps_salary: {
title: 'Average salary — your 5 consecutive highest-paid years',
hint: 'The annual average of your five consecutive years of highest paid service, in today’s dollars.',
hintIt: 'Your IT table shows {lo}–{hi} a year (TBS–PSAC, effective {asof}) — pick your step next, or type your own.'
},
ps_occ: {
title: 'Are you in the IT (Information Technology) group?',
hint: 'The pension formula is the same for every occupational group — we just show official TBS–PSAC pay tables to help with the salary.',
it: 'Yes — IT group', itD: 'IT-01 to IT-05, TBS–PSAC rates',
other: 'No — another group', otherD: 'I’ll type my average salary'
},
ps_it: {
title: 'Which IT level are you at?',
hint: 'Official annual rates, effective {asof} — you can override with your own salary.',
stepTitle: 'Which step of {level}?',
stepHint: 'Range {lo}–{hi} · rates effective {asof}. Your step sets the 5-year average (override it on the salary step).',
stepMin: 'first step', stepTop: 'top step',
skip: 'Not my pay scale', skipD: 'I’ll type my average salary'
},
assumptions: {
title: 'Assumptions', hint: 'Real (above-inflation) returns. Defaults match a balanced portfolio.',
pre: 'Return before retirement', post: 'Return in retirement', inf: 'Inflation',
life: 'Plan to age', yrs: '{v} yrs'
},
vision: { title: 'What are you retiring toward?', hint: 'Pick a few. We’ll paint your plan with it.' }
},
v: {
number: 'Enter a number between {min} and {max}.',
province: 'Pick a province to continue.'
},
chart: {
age: 'age', swipe: 'swipe →',
runsOut: 'runs out ≈ {age}', retire: 'retire'
},
g1: {
h: 'Will it last? Projected savings',
cap: 'Total portfolio (RRSP + TFSA + non-registered), in today\'s dollars. Shaded: money running out.',
legend: 'Total savings (today’s $)'
},
g2: {
h: 'Where retirement income comes from',
cap: 'CPP, OAS, GIS and what you must withdraw from savings — stacked, after-tax view.',
empty: 'Retirement after your plan horizon.',
cpp: 'CPP', oas: 'OAS (net)', gis: 'GIS', savings: 'From savings', psPension: 'Public service pension'
},
ps: {
card: {
title: 'Your federal public service pension',
group: 'Group {n} (rules from your join year)',
service: 'Pensionable service used: {y} years (max 35)',
avg: '5-year average salary (highest paid)',
ampe: 'AMPE (proxy = YMPE {v}, {y})',
lifetime: 'Lifetime annual pension',
bridge: 'Bridge benefit (until 65)',
reduction: 'Early reduction (annual allowance): {pct} — {f}',
none: 'None — you qualify for an unreduced pension',
fF1: 'Formula 1', fF2: 'Formula 2', fMin: 'lower of F1 and F2',
atRet: 'Your pension at retirement (from {age})',
from65: 'From age 65 (bridge ends)',
lt2: 'Under 2 years of service: return of contributions only — no annuity.',
deferred: 'You retire before 55 (Group 2): modelled as a deferred annuity — unreduced pension from age 65. Alternative: an annual allowance from age 55 would be permanently reduced by 50%.',
under50: 'Retiring before 50 (Group 1): the annual allowance is payable from your 50th birthday, with Formula 1 computed at age 50 — a 50% reduction.',
under55: 'Payable from 55 with Formula 1 computed at age 55 — a 50% reduction.',
bridgeEnds: 'The bridge benefit ends the month you turn 65 (earlier only with CPP/QPP disability).',
nSurvivor: 'Survivor: 50% of your pre-65 pension before any reduction, plus child allowances to 18 (25 if in school).',
nIndex: 'Indexation: the pension is increased every January 1 by CPI.',
nPartTime: 'Part-time service is pro-rated for assigned hours — not modelled; full-time assumed.',
nSplit: 'The bridge benefit is a term annuity — excluded from pension income splitting (conservative). The lifetime pension is splittable at any age.',
sources: 'Sources:',
srcPspc: 'PSPC formula & reduction rules ({d})',
srcGlance: 'TBS pension at a glance',
srcGloss: 'TBS glossary (AMPE)',
srcCra: 'CRA pension splitting',
srcYmpe: 'CRA YMPE {v} ({y})',
srcIt: 'TBS–PSAC IT agreement rates ({d})',
dataAsOf: 'benefit constants as of {d}'
}
},
g3: {
h: 'Spending vs. what you\'ll actually have',
cap: 'Annual cash in (income after tax & clawback, plus withdrawals) vs. planned spending.',
shortfall: 'shortfall from ~{age}',
cash: 'Cash you’ll have (after tax)', need: 'Planned spending'
},
g4: {
h: 'Four futures, one number',
cap: 'Sustainable annual spending that lasts to <span id="lifeexp-lbl">{age}</span> under each scenario.',
calc: 'Calculating scenarios…',
computed: 'Computed from the current answers.',
best: 'Biggest lever here: “{name}” → {money}/yr, forever.',
s0: 'Current plan', s1: '+$200/mo to RRSP', s2: 'Retire 2 years later', s3: 'CPP at 70'
},
r: {
verdictH: 'Your plan, in one line',
depletion: 'On this exact path you run out of money at <b>age {age}</b>. ' +
'You can safely spend <b>{ss}/yr</b> to {le} (you’re planning {sp}). The three cards below close the gap — each one, by itself.',
good: 'This plan survives to <b>{le}</b> with {bal} left over — you can sustain about <b>{ss}/yr</b>. ' +
'That’s {hr}/yr of headroom above your plan.',
yearlySummary: 'Year-by-year detail ({n} years)',
whatif: '<b>Try a what-if</b> — move a slider, watch the plan update',
stamp: 'tax rates 2026 · CPP max {cpp}/mo · OAS max {oas}/mo · GIS max {gis}/mo · constants as of {asOf} · RRIF table CRA Reg. 8301(1)',
wiSpend: 'Annual spending', wiCtrb: 'Monthly savings', wiRet: 'Retirement age', wiCpp: 'CPP starts at',
wiYears: '{v} yrs', wiAge: 'age {v}'
},
g: {
gapH: 'The gap', gapP: 'between your planned spending and what the plan can safely fund.',
saveH: '💪 Save more',
saveP: 'extra RRSP room from today closes the gap entirely. ~{pct} of income.',
saveP2: 'Not closable by saving alone — see the other two levers.',
workH: '🕰 Work longer', workY1: '{n} year', workYN: '{n} years',
workP: 'retiring at {at} instead of {now} closes the gap — and CPP/OAS grow while you do.',
workP2: 'Within 10 extra years, delaying retirement alone can’t close this gap — combine levers.',
trimH: '🌿 Trim spending', trimP: 'to {to}. The cheapest lever, and the one nobody talks about at 45.',
statusH: '✅ Status', onTrack: 'On track',
statusP: 'No gap at {sp}/yr. Consider testing a bigger number with the what-if sliders.',
headH: 'Headroom', headP: 'you could add to spending — or save for the cottage.',
stressH: '🛡 Stress-tested', stressP: 'sustainable real spending to {age} with RRIF minimums and taxes enforced.'
},
t: {
age: 'Age', rrsp: 'RRSP', tfsa: 'TFSA', nonreg: 'Non-reg', total: 'Total', wdr: 'Withdraw',
cpp: 'CPP', oas: 'OAS', gis: 'GIS', tax: 'Tax+claw', unfunded: 'Unfunded',
mixedNote: '† One partner is retired, the other is still working: their savings grow at the pre-retirement rate — this row is not in full retirement mode.'
},
d: {
main: '<b>Estimates only — not financial advice.</b> Simplified model: GIS uses a simplified 50% phase-out; Quebec shown with the 16.5% federal abatement (QPP and Quebec payroll taxes not modelled); federal public service pension (PSSA) modelled when enabled — other DB pensions not; returns are constant real rates; Ontario surtax modelled on base tax.',
privacy: '🔒 100% client-side — answers never leave your browser. Your answers are saved only in this browser (localStorage).'
},
res: {
print: '🖨 Print / save PDF',
share: '🔗 Copy link with my answers',
edit: '✎ Edit answers'
},
share: {
copied: '✅ Link copied',
shown: '🔗 Link shown — copy it',
prompt: 'Copy your link:'
},
restart: { confirm: 'Start over? Clears saved answers in this browser.' },
vision: {
alt: 'Your retirement vision',
label: 'Retirement Vision — {slug}',
ai: 'AI-generated preview (z-image-turbo). Your numbers: below.'
}
};
var FR = {
doc: {
title: 'Planificateur de retraite — la retraite canadienne, modélisée comme il faut',
desc: 'Une calculatrice de retraite canadienne avec les vraies mécaniques RPC/SV/SRG/FERR et les taux d\'impôt 2026 en direct. 100 % côté client — vos réponses ne quittent jamais votre navigateur.'
},
brand: 'Planificateur de retraite',
hdr: {
pill: '🔒 100 % côté client — vos réponses ne quittent jamais votre navigateur',
pillTitle: 'Aucun serveur, aucun suivi, aucun compte',
startOver: 'Recommencer'
},
intro: {
h1: 'Allez-vous vraiment manquer d\'argent ?',
lede: 'La plupart des calculateurs de retraite canadiens s\'arrêtent à « votre REER croît de 6 % ». Celui-ci modélise <b>les vraies mécaniques RPC/SV/SRG/FERR</b>, les impôts provinciaux obtenus en direct et le palier fiscal que la plupart des planificateurs ne vous montrent pas.',
tick1: 'RPC hâtive ou tardive (−0,6 %/mois avant 65 ans, +0,7 % après), report de la SV, récupération et SRG',
tick2: 'Barèmes fédéraux 2026 en direct + les 13 tables fiscales provinciales et territoriales',
tick3: 'Minimums de FERR à 71 ans, fractionnement du revenu de pension, retraits fiscalement intelligents',
tick4: 'Couples : impôts conjoints, pensions fractionnées, SRG du ménage',
privacy: '🔒 100 % côté client — vos réponses ne quittent jamais votre navigateur. Aucun compte, aucun suivi, rien n\'est envoyé nulle part.',
cta: 'Élaborer mon plan de retraite →',
fine: 'Environ 2 minutes. Un écran à la fois; vous pourrez tout modifier plus tard.'
},
quiz: {
back: '← Précédent',
next: 'Suivant →',
seePlan: 'Voir mon plan →',
stepCount: '{i} / {n}'
},
panel: {
title: 'Votre plan jusqu\'ici',
savingsAt: 'Épargne à la retraite',
cppOas67: 'RPC + SV à 67',
lastsTo: 'L\'argent dure jusqu\'à',
runsOut: 'épuisé ~{age}',
lifetimeTax: 'Impôt à vie'
},
unit: { yr: '/an', mo: '/mois' },
fields: { unit: '($)' },
i: {
depletion: '⚠️ Sur ce parcours, vous manquez d\'argent vers <b>{age} ans</b>. Continuez — le plan se termine par trois façons concrètes de combler l\'écart.',
cppEarly: 'La RPC à {age} ans est réduite de façon permanente — reporter à 70 ans équivaut à +42 % à l\'abri de l\'inflation. Bougez le curseur et regardez le panneau.',
oasLate: 'SV à {age} ans : +36 % pour la vie, et une plus grande fenêtre de SRG entre-temps. Un bon compromis pour la plupart des gens.',
qc: 'ℹ️ Québec : le RRQ <b>n\'est pas</b> modélisé — ajoutez votre estimation du RRQ sous « Pension ($/an) ». Le RRQ remplace habituellement ~35 % du revenu.',
cliff: 'Vous heurterez le palier fiscal du minimum de FERR à 71 ans. Du CELI aujourd\'hui en atténue l\'effet plus tard — voyez les cartes d\'écart.',
couple: 'Avec le fractionnement du revenu de pension et la SRG du ménage, vos impôts conjoints baissent — le panneau affiche le total conjoint.',
psCover: 'ℹ️ Votre pension de la fonction publique couvre environ <b>{pct}%</b> de votre objectif de dépenses de retraite.',
live: 'Chiffres en direct — changez une réponse et ce panneau bouge.'
},
prov: {
ON: 'Ontario', BC: 'Colombie-Britannique', AB: 'Alberta', SK: 'Saskatchewan',
MB: 'Manitoba', QC: 'Québec', NS: 'Nouvelle-Écosse', NB: 'Nouveau-Brunswick',
NL: 'Terre-Neuve-et-Labrador', PE: 'Île-du-Prince-Édouard', YT: 'Yukon', NT: 'Territoires du Nord-Ouest', NU: 'Nunavut'
},
int: {
rockies: '🏔 Montagnes', maritimes: '🌊 Océan', toronto: '🏙 Grande ville',
dock: '🛶 Lac / quai', muskoka: '🌲 Pays des chalets', prairies: '🌾 Grands espaces',
winter: '⛷ Neige / ski', workshop: '🪵 Atelier', garden: '🌺 Jardin',
travel: '✈️ Voyages', quebec: '🇫🇷 Québec'
},
q: {
province: { title: 'Où habitez-vous ?', hint: 'Vos impôts — et votre décor de retraite.' },
age: { title: 'Votre âge', hint: 'Années.' },
household: {
title: 'Ménage', hint: 'Un couple : impôts conjoints, fractionnement du revenu de pension et SRG du ménage.',
single: 'Célibataire', couple: 'Couple'
},
p_age: { title: 'Âge du conjoint' },
p_income: { title: 'Revenu d\'emploi annuel du conjoint', hint: '$/an avant impôt. 0 si sans emploi.' },
p_savings: {
title: 'Épargne du conjoint', hint: 'Soldes actuels, en dollars d\'aujourd\'hui.',
rrsp: 'REER / FERR', tfsa: 'CELI', nonreg: 'Non enregistré'
},
income: { title: 'Votre revenu d\'emploi annuel', hint: '$/an. Sert à estimer votre historique RPC.' },
rrsp: { title: 'Solde du REER / FERR' },
savings2: {
title: 'Épargne CELI et non enregistrée',
tfsa: 'CELI', nonreg: 'Non enregistré (comptant, actions, fonds communs)'
},
contrib: {
title: 'Épargne mensuelle au REER', hint: 'Votre cotisation + la contrepartie de l\'employeur.',
you: 'Vous cotisez', match: 'Contrepartie de l\'employeur'
},
retire: { title: 'Âge de la retraite', hint: 'Quand vous cessez de gagner un revenu d\'emploi. La retraite anticipée (FIRE) est prise en charge — le plan doit autofinancer les années sans RPC/SV.', years: '{v} ans' },
spend: { title: 'Dépenses annuelles prévues à la retraite', hint: 'En dollars d\'aujourd\'hui, après impôt. Soyez honnête — c\'est le chiffre qui décide de tout.' },
cppstart: {
title: 'Débuter la RPC à…', hint: 'À {age} ans : {adj}',
m60: '−36 % pour la vie', m65: 'Taux plein', m70: '+42 % pour la vie', mCustom: 'ajustement personnalisé',
os60: '−36 % pour la vie', os65: 'taux plein', os70: '+42 % pour la vie'
},
oasstart: {
title: 'Débuter la SV à…', hint: 'Reporter à 70 ans : +36 % pour la vie, et une plus grande fenêtre de SRG.',
os65: 'dès l\'admissibilité', os70: '+36 % pour la vie'
},
extra: {
title: 'Autres revenus', hint: 'Pension à prestations, location, redevances… laissez 0 sinon.',
pension: 'Pension par année', rental: 'Location par année', other: 'Autre par année',
psNote: 'Couvert par votre module de pension fédérale : {v} — voir votre plan.',
psNotePending: 'Répondez aux questions du module de pension — ce montant en découle.'
},
ps: {
title: 'Avez-vous une pension de la fonction publique fédérale?',
hint: 'Facultatif — pour les fonctionnaires fédéraux (LRFP). Nous la calculons selon les règles de la PSCC et l\'ajoutons à votre plan.',
yes: 'Oui — je suis (ou j\'étais) fonctionnaire fédéral', yesD: 'Régime de retraite de la fonction publique (LRFP)',
no: 'Non / je ne sais pas', noD: 'Passer le module de pension'
},
ps_join: {
title: 'En quelle année avez-vous adhéré à la fonction publique fédérale?',
hint: 'Cela détermine votre groupe : avant le 31 déc. 2012 = groupe 1 (allocation annuelle dès 50 ans); à partir du 1er janv. 2013 = groupe 2 (dès 55 ans).'
},
ps_service: {
title: 'Années de service ouvrant droit à pension',
hint: 'Reconnues par la PSCC (le calcul est plafonné à 35 années).',
auto: 'Calculé : entrée en poste en {join} à {joinAge} ans, départ à {ret} ans → {years} années de service. À ajuster seulement si vous avez acheté des services antérieurs.',
override: 'Votre valeur ({years} années) est utilisée. Le calcul à partir de votre année d\'entrée donne {auto} ans.',
useCalc: 'Utiliser le calcul'
},
ps_salary: {
title: 'Salaire moyen — vos 5 années consécutives les mieux rémunérées',
hint: 'La moyenne annuelle de vos cinq années de service consécutives les mieux rémunérées, en dollars d\'aujourd\'hui.',
hintIt: 'Votre barre TI affiche {lo}–{hi} par année (SECT–PSAC, en vigueur le {asof}) — choisissez votre échelon, ou saisissez votre propre salaire.'
},
ps_occ: {
title: 'Faites-vous partie du groupe TI (technologies de l\'information)?',
hint: 'La formule de pension est la même pour tous les groupes professionnels — les barèmes officiels SECT–PSAC aident seulement à estimer le salaire.',
it: 'Oui — groupe TI', itD: 'TI-01 à TI-05, taux SECT–PSAC',
other: 'Non — un autre groupe', otherD: 'Je saisirai mon salaire moyen'
},
ps_it: {
title: 'Quel niveau TI occupez-vous?',
hint: 'Taux annuels officiels, en vigueur le {asof} — vous pouvez les remplacer par votre propre salaire.',
stepTitle: 'Quel échelon du niveau {level}?',
stepHint: 'Fourchette {lo}–{hi} · taux en vigueur le {asof}. Votre échelon fixe la moyenne sur 5 ans (modifiable à l\'étape du salaire).',
stepMin: 'premier échelon', stepTop: 'échelon supérieur',
skip: 'Ce n\'est pas mon barème', skipD: 'Je saisirai mon salaire moyen'
},
assumptions: {
title: 'Hypothèses', hint: 'Rendements réels (supérieurs à l\'inflation). Les valeurs par défaut correspondent à un portefeuille équilibré.',
pre: 'Rendement avant la retraite', post: 'Rendement à la retraite', inf: 'Inflation',
life: 'Planifier jusqu\'à', yrs: '{v} ans'
},
vision: { title: 'Vers quoi prenez-vous votre retraite ?', hint: 'Choisissez-en quelques-uns. Nous en imprégnerons votre plan.' }
},
v: {
number: 'Entrez un nombre entre {min} et {max}.',
province: 'Choisissez une province pour continuer.'
},
chart: {
age: 'âge', swipe: 'glissez →',
runsOut: 'épuisé ≈ {age}', retire: 'retraite'
},
g1: {
h: 'L\'épargne tiendra-t-elle ?',
cap: 'Portefeuille complet (REER + CELI + non enregistré), en dollars d\'aujourd\'hui. Zone ombrée : épargne épuisée.',
legend: 'Épargne totale ($ d\'aujourd\'hui)'
},
g2: {
h: 'D\'où vient le revenu de retraite',
cap: 'RPC, SV, SRG et ce que vous retirez de l\'épargne — empilé, après impôt.',
empty: 'Retraite après l\'horizon du plan.',
cpp: 'RPC', oas: 'SV (net)', gis: 'SRG', savings: 'Épargne', psPension: 'Pension de la fonction publique'
},
ps: {
card: {
title: 'Votre pension de la fonction publique fédérale',
group: 'Groupe {n} (règles selon votre année d\'adhésion)',
service: 'Service ouvrant droit à pension retenu : {y} années (max 35)',
avg: 'Salaire moyen sur 5 ans (les mieux rémunérées)',
ampe: 'MMGP (proxy = PGMA {v}, {y})',
lifetime: 'Pension viagère annuelle',
bridge: 'Prestation de raccordement (jusqu\'à 65 ans)',
reduction: 'Réduction hâtive (allocation annuelle) : {pct} — {f}',
none: 'Aucune — vous avez droit à une pension sans réduction',
fF1: 'Formule 1', fF2: 'Formule 2', fMin: 'la plus faible de F1 et F2',
atRet: 'Votre pension à la retraite (dès {age} ans)',
from65: 'À partir de 65 ans (fin du raccordement)',
lt2: 'Moins de 2 années de service : remboursement des cotisations seulement — pas de rente.',
deferred: 'Vous prenez votre retraite avant 55 ans (groupe 2) : modélisé comme pension différée — pension sans réduction dès 65 ans. Autre option : une allocation annuelle dès 55 ans serait réduite de 50 % de façon permanente.',
under50: 'Retraite avant 50 ans (groupe 1) : l\'allocation annuelle est payable à partir de votre 50e anniversaire, calculée à 50 ans selon la formule 1 — réduction de 50 %.',
under55: 'Payable dès 55 ans, calculée à 55 ans selon la formule 1 — réduction de 50 %.',
bridgeEnds: 'La prestation de raccordement prend fin le mois de votre 65e anniversaire (avant 65 ans seulement en cas d\'invalidité RPC/RRQ).',
nSurvivor: 'Conjoint survivant : 50 % de la pension avant 65 ans calculée avant toute réduction, plus les allocations aux enfants jusqu\'à 18 ans (25 ans aux études).',
nIndex: 'Indexation : la pension est augmentée chaque 1er janvier selon l\'IPC.',
nPartTime: 'Le service à temps partiel est prorogé selon les heures de service assignées — non modélisé; temps plein présumé.',
nSplit: 'La prestation de raccordement est une rente temporaire — exclue du fractionnement du revenu de pension (choix prudent). La pension viagère est fractionnable à tout âge.',
sources: 'Sources :',
srcPspc: 'PSCC — formule et réductions ({d})',
srcGlance: 'SECT — la pension en bref',
srcGloss: 'SECT — glossaire (MMGP)',
srcCra: 'ARC — fractionnement du revenu de pension',
srcYmpe: 'ARC — PGMA {v} ({y})',
srcIt: 'Convention collective TI — SECT–PSAC ({d})',
dataAsOf: 'constantes au {d}'
}
},
g3: {
h: 'Dépenses vs ce que vous aurez',
cap: 'Encaisse annuelle (revenu après impôt et récupération, plus retraits) vs dépenses prévues.',
shortfall: 'déficit dès ~{age}',
cash: 'Encaisse disponible (après impôt)', need: 'Dépenses prévues'
},
g4: {
h: 'Quatre scénarios, un chiffre',
cap: 'Dépenses annuelles durables jusqu\'à <span id="lifeexp-lbl">{age}</span> ans, par scénario.',
calc: 'Calcul des scénarios…',
computed: 'Calculé à partir des réponses actuelles.',
best: 'Le levier le plus puissant ici : « {name} » → {money}/an, pour toujours.',
s0: 'Plan actuel', s1: '+200 $/mois au REER', s2: 'Retard de 2 ans', s3: 'RPC à 70'
},
r: {
verdictH: 'Votre plan, en une phrase',
depletion: 'Sur ce parcours exact, vous manquez d\'argent à <b>{age} ans</b>. ' +
'Vous pouvez dépenser en toute sûreté <b>{ss}/an</b> jusqu\'à {le} ans (vous prévoyez {sp}). Les trois cartes ci-dessous comblent l\'écart — chacune, à elle seule.',
good: 'Ce plan tient jusqu\'à <b>{le} ans</b> avec {bal} restants — vous pouvez soutenir environ <b>{ss}/an</b>. ' +
'Cela fait {hr}/an de marge au-dessus de votre plan.',
yearlySummary: 'Détail année par année ({n} ans)',
whatif: '<b>Essayer un scénario</b> — déplacez un curseur, regardez le plan se mettre à jour',
stamp: 'barèmes 2026 · max. RPC {cpp}/mois · max. SV {oas}/mois · max. SRG {gis}/mois · constantes au {asOf} · table FERR Règl. 8301(1) ARC',
wiSpend: 'Dépenses annuelles', wiCtrb: 'Épargne mensuelle', wiRet: 'Âge de la retraite', wiCpp: 'La RPC commence à',
wiYears: '{v} ans', wiAge: 'à {v} ans'
},
g: {
gapH: 'L\'écart', gapP: 'entre vos dépenses prévues et ce que le plan peut financer de façon sûre.',
saveH: '💪 Épargner plus',
saveP: 'de REER en plus dès aujourd\'hui comble l\'écart à lui seul. ~{pct} du revenu.',
saveP2: 'Impossible à combler par l\'épargne seule — voyez les deux autres leviers.',
workH: '🕰 Travailler plus', workY1: '{n} an', workYN: '{n} ans',
workP: 'partir à {at} ans au lieu de {now} comble l\'écart — et RPC/SV grossissent pendant ce temps.',
workP2: 'En 10 ans de plus, retarder seul la retraite ne comble pas cet écart — combinez les leviers.',
trimH: '🌿 Réduire les dépenses', trimP: 'à {to}. Le levier le moins coûteux, et celui dont personne ne parle à 45 ans.',
statusH: '✅ Statut', onTrack: 'Sur la bonne voie',
statusP: 'Aucun écart à {sp}/an. Essayez un chiffre plus élevé avec les curseurs « et si ».',
headH: 'Marge', headP: 'que vous pourriez ajouter aux dépenses — ou épargner pour le chalet.',
stressH: '🛡 Test de résistance', stressP: 'dépenses réelles durables jusqu\'à {age} ans, minimums FERR et impôts respectés.'
},
t: {
age: 'Âge', rrsp: 'REER', tfsa: 'CELI', nonreg: 'Non enreg.', total: 'Total', wdr: 'Retrait',
cpp: 'RPC', oas: 'SV', gis: 'SRG', tax: 'Impôt + récup.', unfunded: 'Non financé',
mixedNote: '† Un partenaire est à la retraite, l\'autre travaille encore : son épargne croît au taux pré-retraite — cette ligne n\'est pas en mode retraite complet.'
},
d: {
main: '<b>Estimations seulement — il ne s\'agit pas de conseils financiers.</b> Modèle simplifié : retrait progressif simplifié de 50 % pour la SRG; Québec présenté avec le dégagement fédéral de 16,5 % (RRQ et cotisations sociales québécoises non modélisés); pension de la fonction publique fédérale (LRFP) modélisée si activée — autres pensions à prestations non modélisées; rendements à taux réels constants; surtaxe de l\'Ontario calculée sur l\'impôt de base.',
privacy: '🔒 100 % côté client — vos réponses ne quittent jamais votre navigateur. Vos réponses ne sont enregistrées que dans ce navigateur (localStorage).'
},
res: {
print: '🖨 Imprimer / enregistrer en PDF',
share: '🔗 Copier le lien avec mes réponses',
edit: '✎ Modifier les réponses'
},
share: {
copied: '✅ Lien copié',
shown: '🔗 Lien affiché — copiez-le',
prompt: 'Copiez votre lien :'
},
restart: { confirm: 'Recommencer ? Les réponses enregistrées dans ce navigateur seront effacées.' },
vision: {
alt: 'Votre vision de retraite',
label: 'Vision de retraite — {slug}',
ai: 'Aperçu généré par IA (z-image-turbo). Vos chiffres : ci-dessous.'
}
};
/* French typography pass: ' between letters, no-break space before : ; ! ? %
and inside « » guillemets. Applied to every fr leaf at load. */
function frNorm(s) {
return s
.replace(/(\p{L})'(\p{L})/gu, '$1\u2019$2')
.replace(/« /g, '\u00ab' + NB)
.replace(/ »/g, NB + '\u00bb')
.replace(/ ([:;!?%])/g, NB + '$1');
}
(function walkFr(o) {
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) {
if (typeof o[k] === 'string') o[k] = frNorm(o[k]);
else if (o[k] && typeof o[k] === 'object') walkFr(o[k]);
}
})(FR);
var I18N = { en: EN, fr: FR };
/* ---------- language state ---------- */
var lang = 'en';
function detectLang() {
var n = (typeof navigator !== 'undefined' && navigator.language) || 'en';
return String(n).toLowerCase().indexOf('fr') === 0 ? 'fr' : 'en';
}
/* payload lang > saved state lang > detectLang(); old payloads (no lang) → en */
function initialLang() {
var m = typeof location !== 'undefined' && location.hash ? location.hash.match(/[#&]p=([^&]+)/) : null;
if (m) {
var o = null;
try { o = JSON.parse(decodeURIComponent(escape(atob(m[1])))); } catch (e) { o = null; }
if (o && typeof o === 'object') return o.lang === 'fr' ? 'fr' : 'en';
}
try {
var raw = localStorage.getItem(LS_KEY);
if (raw) {
var s = JSON.parse(raw);
if (s && (s.lang === 'en' || s.lang === 'fr')) return s.lang;
}
} catch (e) { /* fresh */ }
return detectLang();
}
function getLang() { return lang; }
/* merge lang into the persisted state object, keeping {a, step, screen, lang} shape */
function persistLang() {
try {
var o = {};
var raw = localStorage.getItem(LS_KEY);
if (raw) { var p = JSON.parse(raw); if (p && typeof p === 'object') o = p; }
o.lang = lang;
localStorage.setItem(LS_KEY, JSON.stringify(o));
} catch (e) { /* private mode etc. — language just won't persist */ }
}
function setLang(l) {
l = (l === 'fr') ? 'fr' : 'en';
if (l === lang) return;
lang = l;
persistLang();
applyI18n();
if (typeof window !== 'undefined' && typeof window.__onLangChange === 'function') {
try { window.__onLangChange(); } catch (e) { /* re-render hook must not break toggle */ }
}
}
/* ---------- t() ---------- */
function lookup(obj, key) {
var p = key.split('.'), o = obj;
for (var i = 0; i < p.length; i++) {
if (o === null || typeof o !== 'object') return null;
o = o[p[i]];
}
return (typeof o === 'string') ? o : null;
}
function t(key, vars) {
var s = lookup(I18N[lang], key);
if (s === null) s = lookup(I18N.en, key);
if (s === null) return key;
if (vars) s = s.replace(/\{(\w+)\}/g, function (m0, k) { return vars[k] != null ? String(vars[k]) : m0; });
return s;
}
/* ---------- locale-aware formatters (Intl.NumberFormat) ---------- */
var nfCache = {};
function nf(kind, opts) {
var key = kind + '|' + lang;
if (!nfCache[key]) nfCache[key] = new Intl.NumberFormat(lang === 'fr' ? 'fr-CA' : 'en-CA', opts);
return nfCache[key];
}
function fmtInt(n) { return nf('int', { maximumFractionDigits: 0 }).format(n); }
function fmtMoney(n) {
n = Math.round(n);
return (n < 0 ? MINUS : '') + nf('c0', { style: 'currency', currency: 'CAD', maximumFractionDigits: 0 }).format(Math.abs(n));
}
function fmtMoney2(n) {
n = +n;
return (n < 0 ? MINUS : '') + nf('c2', { style: 'currency', currency: 'CAD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Math.abs(n));
}
function fmtPct(x, digits) {
var d = digits == null ? 0 : digits;
return nf('p' + d, { style: 'percent', maximumFractionDigits: d }).format(x);
}
/* compact currency, ≤3 significant digits: $523K / $1.05M — 523 k$ / 1,05 M$ */
function fmtMoneyK(n) {
n = Math.round(n);
var s = n < 0 ? MINUS : '';
n = Math.abs(n);
var num, unit;
if (n >= 1e6) { num = sig3(n / 1e6); unit = 'M'; }
else if (n >= 1000) { num = String(Math.round(n / 1000)); unit = 'K'; }
else { num = String(n); unit = ''; }
if (lang === 'fr') {
num = nf('kd', { maximumFractionDigits: 2 }).format(Number(num)); /* 1.05 → 1,05 */
unit = unit === 'K' ? 'k' : unit;
return s + num + NNB + (unit || '') + '$';
}
return s + '$' + num + unit;
}
function sig3(x) {
var d = x >= 100 ? 0 : (x >= 10 ? 1 : 2);
var s = x.toFixed(d);
if (s.indexOf('.') >= 0) s = s.replace(/0+$/, '').replace(/\.$/, '');
return s;
}
/* ---------- static DOM ---------- */
function forEachEl(sel, fn) {
if (!document.querySelectorAll) return;
var nodes = document.querySelectorAll(sel);
for (var i = 0; i < nodes.length; i++) fn(nodes[i]);
}
function applyI18n() {
var de = document.documentElement;
if (de) { if (de.setAttribute) de.setAttribute('lang', lang); de.lang = lang; }
document.title = t('doc.title');
forEachEl('[data-i18n]', function (el) { el.textContent = t(el.getAttribute('data-i18n')); });
forEachEl('[data-i18n-html]', function (el) { el.innerHTML = t(el.getAttribute('data-i18n-html')); });
forEachEl('[data-i18n-attr]', function (el) {
var spec = el.getAttribute('data-i18n-attr') || '';
spec.split(',').forEach(function (pair) {
var i = pair.indexOf(':');
if (i < 0) return;
el.setAttribute(pair.slice(0, i).trim(), t(pair.slice(i + 1).trim()));
});
});
var be = document.getElementById('lang-en'), bf = document.getElementById('lang-fr');
if (be) be.setAttribute('aria-pressed', lang === 'en' ? 'true' : 'false');
if (bf) bf.setAttribute('aria-pressed', lang === 'fr' ? 'true' : 'false');
}
function wireToggle() {
var be = document.getElementById('lang-en'), bf = document.getElementById('lang-fr');
if (be) be.onclick = function () { setLang('en'); };
if (bf) bf.onclick = function () { setLang('fr'); };
}
/* ---------- expose (browser window / Node globalThis in the test shims) ---------- */
var exp = {
I18N: I18N, t: t, getLang: getLang, setLang: setLang, detectLang: detectLang,
applyI18n: applyI18n, fmtMoney: fmtMoney, fmtMoneyK: fmtMoneyK, fmtMoney2: fmtMoney2,
fmtInt: fmtInt, fmtPct: fmtPct
};
lang = initialLang();
try { if (typeof window !== 'undefined') for (var k in exp) window[k] = exp[k]; } catch (e) { }
try { for (var k2 in exp) globalThis[k2] = exp[k2]; } catch (e) { }
if (typeof document !== 'undefined' && document.querySelectorAll) {
applyI18n();
wireToggle();
}
})();