697 lines
38 KiB
JavaScript
697 lines
38 KiB
JavaScript
/* 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.', 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).'
|
||
},
|
||
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'
|
||
},
|
||
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.', 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).'
|
||
},
|
||
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é'
|
||
},
|
||
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();
|
||
}
|
||
})();
|