@jjlmoya/utils-finance 1.34.0 → 1.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +1 -0
  5. package/src/pages/[locale]/[slug].astro +1 -1
  6. package/src/tests/locale_completeness.test.ts +2 -2
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tool/inflationPurchasingPower/bibliography.astro +14 -0
  9. package/src/tool/inflationPurchasingPower/bibliography.ts +12 -0
  10. package/src/tool/inflationPurchasingPower/component.astro +79 -0
  11. package/src/tool/inflationPurchasingPower/controller.ts +82 -0
  12. package/src/tool/inflationPurchasingPower/dom-views.ts +99 -0
  13. package/src/tool/inflationPurchasingPower/entry.ts +31 -0
  14. package/src/tool/inflationPurchasingPower/evaluator.ts +18 -0
  15. package/src/tool/inflationPurchasingPower/i18n/de.ts +56 -0
  16. package/src/tool/inflationPurchasingPower/i18n/en.ts +131 -0
  17. package/src/tool/inflationPurchasingPower/i18n/es.ts +44 -0
  18. package/src/tool/inflationPurchasingPower/i18n/fr.ts +44 -0
  19. package/src/tool/inflationPurchasingPower/i18n/id.ts +44 -0
  20. package/src/tool/inflationPurchasingPower/i18n/it.ts +44 -0
  21. package/src/tool/inflationPurchasingPower/i18n/ja.ts +44 -0
  22. package/src/tool/inflationPurchasingPower/i18n/ko.ts +44 -0
  23. package/src/tool/inflationPurchasingPower/i18n/nl.ts +44 -0
  24. package/src/tool/inflationPurchasingPower/i18n/pl.ts +44 -0
  25. package/src/tool/inflationPurchasingPower/i18n/pt.ts +44 -0
  26. package/src/tool/inflationPurchasingPower/i18n/ru.ts +44 -0
  27. package/src/tool/inflationPurchasingPower/i18n/sv.ts +44 -0
  28. package/src/tool/inflationPurchasingPower/i18n/tr.ts +44 -0
  29. package/src/tool/inflationPurchasingPower/i18n/zh.ts +44 -0
  30. package/src/tool/inflationPurchasingPower/index.ts +11 -0
  31. package/src/tool/inflationPurchasingPower/inflation-purchasing-power-timeline.css +402 -0
  32. package/src/tool/inflationPurchasingPower/logic.test.ts +35 -0
  33. package/src/tool/inflationPurchasingPower/logic.ts +92 -0
  34. package/src/tool/inflationPurchasingPower/seo.astro +14 -0
  35. package/src/tool/inflationPurchasingPower/storage.ts +29 -0
  36. package/src/tool/inflationPurchasingPower/ui.ts +42 -0
  37. package/src/tool/mortgageExtraPaymentPlanner/bibliography.astro +14 -0
  38. package/src/tool/mortgageExtraPaymentPlanner/bibliography.ts +12 -0
  39. package/src/tool/mortgageExtraPaymentPlanner/component.astro +138 -0
  40. package/src/tool/mortgageExtraPaymentPlanner/controller.ts +150 -0
  41. package/src/tool/mortgageExtraPaymentPlanner/currency-defaults.ts +25 -0
  42. package/src/tool/mortgageExtraPaymentPlanner/dom-views.ts +161 -0
  43. package/src/tool/mortgageExtraPaymentPlanner/entry.ts +31 -0
  44. package/src/tool/mortgageExtraPaymentPlanner/evaluator.ts +24 -0
  45. package/src/tool/mortgageExtraPaymentPlanner/i18n/de.ts +74 -0
  46. package/src/tool/mortgageExtraPaymentPlanner/i18n/en.ts +185 -0
  47. package/src/tool/mortgageExtraPaymentPlanner/i18n/es.ts +49 -0
  48. package/src/tool/mortgageExtraPaymentPlanner/i18n/fr.ts +25 -0
  49. package/src/tool/mortgageExtraPaymentPlanner/i18n/id.ts +18 -0
  50. package/src/tool/mortgageExtraPaymentPlanner/i18n/it.ts +20 -0
  51. package/src/tool/mortgageExtraPaymentPlanner/i18n/ja.ts +18 -0
  52. package/src/tool/mortgageExtraPaymentPlanner/i18n/ko.ts +18 -0
  53. package/src/tool/mortgageExtraPaymentPlanner/i18n/nl.ts +18 -0
  54. package/src/tool/mortgageExtraPaymentPlanner/i18n/pl.ts +18 -0
  55. package/src/tool/mortgageExtraPaymentPlanner/i18n/pt.ts +18 -0
  56. package/src/tool/mortgageExtraPaymentPlanner/i18n/ru.ts +18 -0
  57. package/src/tool/mortgageExtraPaymentPlanner/i18n/sv.ts +18 -0
  58. package/src/tool/mortgageExtraPaymentPlanner/i18n/tr.ts +18 -0
  59. package/src/tool/mortgageExtraPaymentPlanner/i18n/zh.ts +18 -0
  60. package/src/tool/mortgageExtraPaymentPlanner/index.ts +11 -0
  61. package/src/tool/mortgageExtraPaymentPlanner/logic.test.ts +67 -0
  62. package/src/tool/mortgageExtraPaymentPlanner/logic.ts +263 -0
  63. package/src/tool/mortgageExtraPaymentPlanner/mortgage-amortization-extra-payment-planner.css +551 -0
  64. package/src/tool/mortgageExtraPaymentPlanner/seo.astro +14 -0
  65. package/src/tool/mortgageExtraPaymentPlanner/storage.ts +65 -0
  66. package/src/tool/mortgageExtraPaymentPlanner/ui.ts +48 -0
  67. package/src/tools.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-finance",
3
- "version": "1.34.0",
3
+ "version": "1.36.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -17,10 +17,11 @@ import { revolvingCardCalculator } from '../tool/revolvingCardCalculator/entry';
17
17
  import { pesetaValue } from '../tool/pesetaValue/entry';
18
18
  import { salaryEmployerCost } from '../tool/salaryEmployerCost/entry';
19
19
  import { dividendYield } from '../tool/dividendYield/entry';
20
+ import { inflationPurchasingPower } from '../tool/inflationPurchasingPower/entry';
20
21
 
21
22
  export const financeCategory: FinanceCategoryEntry = {
22
23
  icon: 'mdi:finance',
23
- tools: [compoundInterest, financialFreedom, profitability, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball, revolvingCardCalculator, pesetaValue, salaryEmployerCost, dividendYield],
24
+ tools: [compoundInterest, financialFreedom, profitability, mortgage, inflation, percentageCalculator, lateInterest, ibanBicSwift, rentIncrease, lotteryOptimizer, courtFeeCalculator, legalInterestRate, fireCalculator, debtSnowball, revolvingCardCalculator, pesetaValue, salaryEmployerCost, dividendYield, inflationPurchasingPower],
24
25
  i18n: {
25
26
  en: () => import('./i18n/en').then((m) => m.content),
26
27
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -34,6 +34,8 @@ export { salaryEmployerCost } from './tool/salaryEmployerCost/entry';
34
34
  export type { SalaryEmployerCostLocaleContent } from './tool/salaryEmployerCost/entry';
35
35
  export { dividendYield } from './tool/dividendYield/entry';
36
36
  export type { DividendYieldLocaleContent } from './tool/dividendYield/entry';
37
+ export { inflationPurchasingPower } from './tool/inflationPurchasingPower/entry';
38
+ export type { InflationPurchasingPowerLocaleContent } from './tool/inflationPurchasingPower/entry';
37
39
  export { financeCategory } from './category';
38
40
  import { compoundInterest } from './tool/compoundInterest/entry';
39
41
  import { financialFreedom } from './tool/financialFreedom/entry';
@@ -53,4 +55,6 @@ import { revolvingCardCalculator } from './tool/revolvingCardCalculator/entry';
53
55
  import { pesetaValue } from './tool/pesetaValue/entry';
54
56
  import { salaryEmployerCost } from './tool/salaryEmployerCost/entry';
55
57
  import { dividendYield } from './tool/dividendYield/entry';
56
- export const ALL_ENTRIES = [compoundInterest, financialFreedom, profitability, courtFeeCalculator, debtSnowball, fireCalculator, ibanBicSwift, inflation, lateInterest, legalInterestRate, lotteryOptimizer, mortgage, percentageCalculator, rentIncrease, revolvingCardCalculator, pesetaValue, salaryEmployerCost, dividendYield];
58
+ import { mortgageExtraPaymentPlanner } from './tool/mortgageExtraPaymentPlanner/entry';
59
+ import { inflationPurchasingPower } from './tool/inflationPurchasingPower/entry';
60
+ export const ALL_ENTRIES = [compoundInterest, financialFreedom, profitability, courtFeeCalculator, debtSnowball, fireCalculator, ibanBicSwift, inflation, lateInterest, legalInterestRate, lotteryOptimizer, mortgage, percentageCalculator, rentIncrease, revolvingCardCalculator, pesetaValue, salaryEmployerCost, dividendYield, mortgageExtraPaymentPlanner, inflationPurchasingPower];
package/src/index.ts CHANGED
@@ -33,3 +33,4 @@ export { REVOLVING_CARD_CALCULATOR_TOOL } from './tool/revolvingCardCalculator';
33
33
  export { PESETA_VALUE_TOOL } from './tool/pesetaValue';
34
34
  export { SALARY_EMPLOYER_COST_TOOL } from './tool/salaryEmployerCost';
35
35
  export { DIVIDEND_YIELD_TOOL } from './tool/dividendYield';
36
+ export { MORTGAGE_EXTRA_PAYMENT_PLANNER_TOOL } from './tool/mortgageExtraPaymentPlanner';
@@ -124,7 +124,7 @@ const titleBase = words.slice(1).join(" ") || "";
124
124
  />
125
125
 
126
126
  <section class="section-tool">
127
- <Component ui={content.ui} />
127
+ <Component ui={content.ui} locale={locale} />
128
128
  </section>
129
129
 
130
130
  <section class="section-seo">
@@ -25,7 +25,7 @@ describe('Locale Completeness Validation', () => {
25
25
  });
26
26
  });
27
27
 
28
- it('all 18 tools registered', () => {
29
- expect(ALL_TOOLS.length).toBe(18);
28
+ it('all 20 tools registered', () => {
29
+ expect(ALL_TOOLS.length).toBe(20);
30
30
  });
31
31
  });
@@ -4,8 +4,8 @@ import { financeCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 18 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(18);
7
+ it('should have 20 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(20);
9
9
  });
10
10
 
11
11
  it('financeCategory should be defined', () => {
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { inflationPurchasingPower } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await inflationPurchasingPower.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Banco de España, ¿Qué es la inflación?',
6
+ url: 'https://www.bde.es/wbe/es/areas-actuacion/politica-monetaria/preguntas-frecuentes/politica-monetaria-y-estabilidad-precios/que-es-la-inflacion.html',
7
+ },
8
+ {
9
+ name: 'Bank of Canada, Inflation Calculator',
10
+ url: 'https://www.bankofcanada.ca/rates/related/inflation-calculator/',
11
+ },
12
+ ];
@@ -0,0 +1,79 @@
1
+ ---
2
+ import type { InflationPurchasingPowerUI } from './ui';
3
+ import type { InflationPurchasingPowerInput } from './logic';
4
+ import { calculateInflationTimeline } from './logic';
5
+ import { renderTimelineResults, renderTimelineScene } from './dom-views';
6
+ import './inflation-purchasing-power-timeline.css';
7
+
8
+ interface Props {
9
+ ui: InflationPurchasingPowerUI;
10
+ locale?: string;
11
+ }
12
+
13
+ const { ui } = Astro.props;
14
+ const initial: InflationPurchasingPowerInput = {
15
+ startingAmount: 1000,
16
+ annualInflationRate: 2.5,
17
+ years: 20,
18
+ annualCashFlow: 0,
19
+ cashFlowMode: 'none',
20
+ };
21
+ const initialResult = calculateInflationTimeline(initial);
22
+ const config = JSON.stringify({ ui, initial });
23
+ ---
24
+
25
+ <div class="inflation-purchasing-power-tool" data-ipp-tool>
26
+ <form class="ipp-controls" aria-label="Inflation purchasing power inputs">
27
+ <div class="ipp-field">
28
+ <label for="ipp-amount">{ui.amountLabel}</label>
29
+ <input id="ipp-amount" data-ipp-input="amount" type="number" min="0.01" step="0.01" value={initial.startingAmount} inputmode="decimal" />
30
+ <small>{ui.amountHelp}</small>
31
+ </div>
32
+ <div class="ipp-field">
33
+ <label for="ipp-inflation">{ui.inflationLabel}</label>
34
+ <input id="ipp-inflation" data-ipp-input="inflation" type="number" min="-99.99" max="100" step="0.1" value={initial.annualInflationRate} inputmode="decimal" />
35
+ <small>{ui.inflationHelp}</small>
36
+ </div>
37
+ <div class="ipp-field">
38
+ <label for="ipp-years">{ui.yearsLabel}</label>
39
+ <input id="ipp-years" data-ipp-input="years" type="number" min="1" max="80" step="1" value={initial.years} inputmode="numeric" />
40
+ <small>{ui.yearsHelp}</small>
41
+ </div>
42
+ <div class="ipp-field">
43
+ <label for="ipp-cash-flow">{ui.cashFlowLabel}</label>
44
+ <input id="ipp-cash-flow" data-ipp-input="cash-flow" type="number" min="0" step="0.01" value={initial.annualCashFlow} inputmode="decimal" disabled />
45
+ <small>{ui.cashFlowHelp}</small>
46
+ </div>
47
+ <fieldset class="ipp-flow">
48
+ <legend>{ui.cashFlowModeLabel}</legend>
49
+ <div class="ipp-flow-options">
50
+ <button type="button" data-ipp-mode="none" class="is-active" aria-pressed="true">{ui.noCashFlowOption}</button>
51
+ <button type="button" data-ipp-mode="contribution" aria-pressed="false">{ui.contributionOption}</button>
52
+ <button type="button" data-ipp-mode="withdrawal" aria-pressed="false">{ui.withdrawalOption}</button>
53
+ </div>
54
+ </fieldset>
55
+ </form>
56
+
57
+ <section class="ipp-output" aria-live="polite">
58
+ <div class="ipp-scene-wrap" data-ipp-scene set:html={initialResult ? renderTimelineScene(initialResult, ui) : ''}></div>
59
+ <div class="ipp-result-wrap" data-ipp-results set:html={initialResult ? renderTimelineResults(initialResult, ui) : ''}></div>
60
+ <p class="ipp-error" data-ipp-error hidden>{ui.emptyMessage}</p>
61
+ </section>
62
+
63
+ <details class="ipp-method">
64
+ <summary>{ui.methodTitle}</summary>
65
+ <p>{ui.methodText}</p>
66
+ <p>{ui.limitationText}</p>
67
+ </details>
68
+ </div>
69
+
70
+ <script is:inline type="application/json" data-ipp-config set:html={config}></script>
71
+ <script>
72
+ import { initInflationPurchasingPower } from './controller';
73
+
74
+ const root = document.querySelector<HTMLElement>('[data-ipp-tool]');
75
+ const configElement = document.querySelector<HTMLScriptElement>('[data-ipp-config]');
76
+ if (root && configElement?.textContent) {
77
+ initInflationPurchasingPower(root, JSON.parse(configElement.textContent));
78
+ }
79
+ </script>
@@ -0,0 +1,82 @@
1
+ import { calculateInflationTimeline, type CashFlowMode, type InflationPurchasingPowerInput } from './logic';
2
+ import { renderTimelineResults, renderTimelineScene } from './dom-views';
3
+ import { loadInflationPurchasingPowerState, saveInflationPurchasingPowerState } from './storage';
4
+ import type { InflationPurchasingPowerUI } from './ui';
5
+
6
+ interface Config {
7
+ ui: InflationPurchasingPowerUI;
8
+ initial: InflationPurchasingPowerInput;
9
+ }
10
+
11
+ function readNumber(root: HTMLElement, name: string): number {
12
+ const field = root.querySelector<HTMLInputElement>(`[data-ipp-input="${name}"]`);
13
+ return field?.value === '' ? Number.NaN : Number(field?.value);
14
+ }
15
+
16
+ function collectState(root: HTMLElement): InflationPurchasingPowerInput {
17
+ const activeMode = root.querySelector<HTMLButtonElement>('[data-ipp-mode].is-active')?.dataset.ippMode;
18
+ return {
19
+ startingAmount: readNumber(root, 'amount'),
20
+ annualInflationRate: readNumber(root, 'inflation'),
21
+ years: readNumber(root, 'years'),
22
+ annualCashFlow: readNumber(root, 'cash-flow'),
23
+ cashFlowMode: activeMode === 'contribution' || activeMode === 'withdrawal' ? activeMode : 'none',
24
+ };
25
+ }
26
+
27
+ function setMode(root: HTMLElement, mode: CashFlowMode): void {
28
+ root.querySelectorAll<HTMLButtonElement>('[data-ipp-mode]').forEach((button) => {
29
+ const isActive = button.dataset.ippMode === mode;
30
+ button.classList.toggle('is-active', isActive);
31
+ button.setAttribute('aria-pressed', String(isActive));
32
+ });
33
+ const cashFlowField = root.querySelector<HTMLInputElement>('[data-ipp-input="cash-flow"]');
34
+ if (cashFlowField) cashFlowField.disabled = mode === 'none';
35
+ }
36
+
37
+ function setInputValues(root: HTMLElement, state: InflationPurchasingPowerInput): void {
38
+ const values: Record<string, number> = { amount: state.startingAmount, inflation: state.annualInflationRate, years: state.years, 'cash-flow': state.annualCashFlow };
39
+ Object.entries(values).forEach(([name, value]) => {
40
+ const field = root.querySelector<HTMLInputElement>(`[data-ipp-input="${name}"]`);
41
+ if (field) field.value = String(value);
42
+ });
43
+ setMode(root, state.cashFlowMode);
44
+ }
45
+
46
+ function render(root: HTMLElement, state: InflationPurchasingPowerInput, ui: InflationPurchasingPowerUI): void {
47
+ const result = calculateInflationTimeline(state);
48
+ const scene = root.querySelector<HTMLElement>('[data-ipp-scene]');
49
+ const results = root.querySelector<HTMLElement>('[data-ipp-results]');
50
+ const error = root.querySelector<HTMLElement>('[data-ipp-error]');
51
+ if (!scene || !results || !error) return;
52
+ if (!result) {
53
+ scene.innerHTML = '';
54
+ results.innerHTML = '';
55
+ error.hidden = false;
56
+ return;
57
+ }
58
+ error.hidden = true;
59
+ scene.innerHTML = renderTimelineScene(result, ui);
60
+ results.innerHTML = renderTimelineResults(result, ui);
61
+ }
62
+
63
+ export function initInflationPurchasingPower(root: HTMLElement, config: Config): void {
64
+ const initialState = loadInflationPurchasingPowerState(config.initial);
65
+ setInputValues(root, initialState);
66
+ render(root, initialState, config.ui);
67
+ root.addEventListener('input', () => {
68
+ const state = collectState(root);
69
+ if (calculateInflationTimeline(state)) saveInflationPurchasingPowerState(state);
70
+ render(root, state, config.ui);
71
+ });
72
+ root.addEventListener('click', (event) => {
73
+ const target = event.target as HTMLElement;
74
+ const button = target.closest<HTMLButtonElement>('[data-ipp-mode]');
75
+ const mode = button?.dataset.ippMode as CashFlowMode | undefined;
76
+ if (!button || !mode) return;
77
+ setMode(root, mode);
78
+ const state = collectState(root);
79
+ saveInflationPurchasingPowerState(state);
80
+ render(root, state, config.ui);
81
+ });
82
+ }
@@ -0,0 +1,99 @@
1
+ import type { InflationPurchasingPowerResult, InflationTimelinePoint } from './logic';
2
+ import { evaluateInflationTimeline } from './evaluator';
3
+ import type { InflationPurchasingPowerUI } from './ui';
4
+
5
+ const numberFormat = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
6
+
7
+ export function formatAmount(value: number): string {
8
+ return numberFormat.format(value);
9
+ }
10
+
11
+ function escapeHtml(value: string): string {
12
+ return value.replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character] ?? character);
13
+ }
14
+
15
+ function getChartBounds(result: InflationPurchasingPowerResult): { min: number; max: number } {
16
+ const values = result.points.flatMap((point) => [point.costEquivalent, point.unchangedRealValue, point.realBalance]);
17
+ const max = Math.max(...values, result.input.startingAmount) * 1.08;
18
+ const min = Math.max(0, Math.min(...values) * 0.92);
19
+ return { min, max: max === min ? min + 1 : max };
20
+ }
21
+
22
+ function createPath(points: InflationTimelinePoint[], value: (point: InflationTimelinePoint) => number, bounds: { min: number; max: number }): string {
23
+ const chartWidth = 560;
24
+ const chartHeight = 220;
25
+ return points.map((point, index) => {
26
+ const x = 70 + (index / Math.max(points.length - 1, 1)) * chartWidth;
27
+ const y = 275 - ((value(point) - bounds.min) / (bounds.max - bounds.min)) * chartHeight;
28
+ return `${index === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${Math.max(40, Math.min(275, y)).toFixed(1)}`;
29
+ }).join(' ');
30
+ }
31
+
32
+ function renderLegend(label: string, className: string): string {
33
+ return `<span class="ipp-legend-item"><i class="ipp-legend-line ${className}"></i>${escapeHtml(label)}</span>`;
34
+ }
35
+
36
+ function getEndY(value: number, bounds: { min: number; max: number }): string {
37
+ return Math.max(40, Math.min(275, 275 - ((value - bounds.min) / (bounds.max - bounds.min)) * 220)).toFixed(1);
38
+ }
39
+
40
+ function renderChart(result: InflationPurchasingPowerResult, ui: InflationPurchasingPowerUI): string {
41
+ const bounds = getChartBounds(result);
42
+ const costPath = createPath(result.points, (point) => point.costEquivalent, bounds);
43
+ const realPath = createPath(result.points, (point) => point.unchangedRealValue, bounds);
44
+ const balancePath = createPath(result.points, (point) => point.realBalance, bounds);
45
+ const end = result.points.at(-1);
46
+ if (!end) return '';
47
+ return `<svg class="ipp-chart" viewBox="0 0 700 340" role="img" aria-label="${escapeHtml(ui.chartDescription)}">
48
+ <line class="ipp-grid-line" x1="70" y1="95" x2="630" y2="95" />
49
+ <line class="ipp-grid-line" x1="70" y1="185" x2="630" y2="185" />
50
+ <line class="ipp-axis" x1="70" y1="275" x2="630" y2="275" />
51
+ <path class="ipp-line ipp-line-cost" d="${costPath}" />
52
+ <path class="ipp-line ipp-line-real" d="${realPath}" />
53
+ <path class="ipp-line ipp-line-balance" d="${balancePath}" />
54
+ <circle class="ipp-end-dot ipp-dot-cost" cx="630" cy="${getEndY(end.costEquivalent, bounds)}" r="6" />
55
+ <circle class="ipp-end-dot ipp-dot-real" cx="630" cy="${getEndY(end.unchangedRealValue, bounds)}" r="6" />
56
+ <text class="ipp-axis-label" x="70" y="310">${escapeHtml(ui.todayLabel)}</text>
57
+ <text class="ipp-axis-label" x="630" y="310" text-anchor="end">${escapeHtml(ui.endLabel)}</text>
58
+ <text class="ipp-chart-caption" x="70" y="28">${escapeHtml(ui.chartNote)}</text>
59
+ </svg>`;
60
+ }
61
+
62
+ export function renderTimelineScene(result: InflationPurchasingPowerResult, ui: InflationPurchasingPowerUI): string {
63
+ const evaluation = evaluateInflationTimeline(result, ui);
64
+ return `<div class="ipp-scene" data-ipp-tone="${evaluation.tone}">
65
+ <div class="ipp-scene-heading">
66
+ <span>${escapeHtml(ui.todayLabel)}</span>
67
+ <span class="ipp-scene-rule"></span>
68
+ <span>${escapeHtml(ui.endLabel)}</span>
69
+ </div>
70
+ ${renderChart(result, ui)}
71
+ <div class="ipp-legend" aria-label="${escapeHtml(ui.chartDescription)}">
72
+ ${renderLegend(ui.costLineLabel, 'ipp-legend-cost')}
73
+ ${renderLegend(ui.realLineLabel, 'ipp-legend-real')}
74
+ ${renderLegend(ui.balanceLineLabel, 'ipp-legend-balance')}
75
+ </div>
76
+ </div>`;
77
+ }
78
+
79
+ function renderMetric(label: string, value: string, className = ''): string {
80
+ return `<div class="ipp-metric ${className}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
81
+ }
82
+
83
+ export function renderTimelineResults(result: InflationPurchasingPowerResult, ui: InflationPurchasingPowerUI): string {
84
+ const end = result.points.at(-1);
85
+ if (!end) return '';
86
+ const evaluation = evaluateInflationTimeline(result, ui);
87
+ const rows = result.milestones.map((point) => `<tr><th scope="row">${point.year}</th><td>${formatAmount(point.costEquivalent)}</td><td>${formatAmount(point.unchangedRealValue)}</td><td>${formatAmount(point.realBalance)}</td></tr>`).join('');
88
+ return `<div class="ipp-result-content">
89
+ <div class="ipp-primary-metric"><span>${escapeHtml(ui.costEquivalentLabel)}</span><strong>${formatAmount(end.costEquivalent)}</strong><small>${escapeHtml(ui.amountSuffix)}</small></div>
90
+ <div class="ipp-metrics">
91
+ ${renderMetric(ui.realValueLabel, formatAmount(end.unchangedRealValue))}
92
+ ${renderMetric(ui.nominalBalanceLabel, formatAmount(end.nominalBalance))}
93
+ ${renderMetric(ui.purchasingPowerLossLabel, `${result.purchasingPowerLossPercent.toFixed(1)}%`, 'ipp-metric-accent')}
94
+ </div>
95
+ <p class="ipp-scenario" data-ipp-tone="${evaluation.tone}">${escapeHtml(evaluation.message)}</p>
96
+ <h3>${escapeHtml(ui.milestoneTitle)}</h3>
97
+ <div class="ipp-table-wrap"><table><thead><tr><th scope="col">${escapeHtml(ui.yearHeader)}</th><th scope="col">${escapeHtml(ui.costHeader)}</th><th scope="col">${escapeHtml(ui.realHeader)}</th><th scope="col">${escapeHtml(ui.balanceHeader)}</th></tr></thead><tbody>${rows}</tbody></table></div>
98
+ </div>`;
99
+ }
@@ -0,0 +1,31 @@
1
+ import type { FinanceToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { InflationPurchasingPowerUI } from './ui';
3
+
4
+ export type InflationPurchasingPowerLocaleContent = ToolLocaleContent<InflationPurchasingPowerUI>;
5
+
6
+ export const inflationPurchasingPower: FinanceToolEntry<InflationPurchasingPowerUI> = {
7
+ id: 'inflation-purchasing-power-timeline',
8
+ icons: {
9
+ bg: 'mdi:chart-timeline-variant-shimmer',
10
+ fg: 'mdi:cash-sync',
11
+ },
12
+ i18n: {
13
+ en: () => import('./i18n/en').then((m) => m.content),
14
+ de: () => import('./i18n/de').then((m) => m.content),
15
+ es: () => import('./i18n/es').then((m) => m.content),
16
+ fr: () => import('./i18n/fr').then((m) => m.content),
17
+ id: () => import('./i18n/id').then((m) => m.content),
18
+ it: () => import('./i18n/it').then((m) => m.content),
19
+ ja: () => import('./i18n/ja').then((m) => m.content),
20
+ ko: () => import('./i18n/ko').then((m) => m.content),
21
+ nl: () => import('./i18n/nl').then((m) => m.content),
22
+ pl: () => import('./i18n/pl').then((m) => m.content),
23
+ pt: () => import('./i18n/pt').then((m) => m.content),
24
+ ru: () => import('./i18n/ru').then((m) => m.content),
25
+ sv: () => import('./i18n/sv').then((m) => m.content),
26
+ tr: () => import('./i18n/tr').then((m) => m.content),
27
+ zh: () => import('./i18n/zh').then((m) => m.content),
28
+ },
29
+ };
30
+
31
+ export { bibliography } from './bibliography';
@@ -0,0 +1,18 @@
1
+ import type { InflationPurchasingPowerResult } from './logic';
2
+ import type { InflationPurchasingPowerUI } from './ui';
3
+
4
+ export type TimelineTone = 'neutral' | 'warning';
5
+
6
+ export interface TimelineEvaluation {
7
+ tone: TimelineTone;
8
+ message: string;
9
+ }
10
+
11
+ export function evaluateInflationTimeline(result: InflationPurchasingPowerResult, ui: InflationPurchasingPowerUI): TimelineEvaluation {
12
+ if (result.depletionYear !== null) {
13
+ return { tone: 'warning', message: ui.depletionWarning.replace('{year}', String(result.depletionYear)) };
14
+ }
15
+ if (result.input.cashFlowMode === 'contribution') return { tone: 'neutral', message: ui.contributionSummary };
16
+ if (result.input.cashFlowMode === 'withdrawal') return { tone: 'warning', message: ui.withdrawalSummary };
17
+ return { tone: 'neutral', message: ui.noCashFlowSummary };
18
+ }
@@ -0,0 +1,56 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
3
+ import type { ToolLocaleContent } from '../../../types';
4
+ import type { InflationPurchasingPowerUI } from '../ui';
5
+
6
+ const title = 'Rechner für Inflation und Kaufkraft im Zeitverlauf';
7
+ const description = 'Zeigt, wie eine jährliche Inflationsannahme den benötigten Betrag für dieselbe Kaufkraft und den realen Wert von Bargeld verändert.';
8
+ const ui: InflationPurchasingPowerUI = {
9
+ amountLabel: 'Startbetrag', amountHelp: 'Beliebige Waehrung verwenden und beibehalten',
10
+ inflationLabel: 'Jaehrliche Inflationsrate (%)', inflationHelp: 'Eine Szenarioannahme, keine Prognose',
11
+ yearsLabel: 'Zeitraum (Jahre)', yearsHelp: 'Von 1 bis 80 Jahre waehlen',
12
+ cashFlowLabel: 'Jaehrlicher Cashflow', cashFlowHelp: 'Wird jedes Jahr zum Guthaben addiert oder davon abgezogen',
13
+ cashFlowModeLabel: 'Cashflow-Szenario', noCashFlowOption: 'Kein regelmaessiger Cashflow', contributionOption: 'Jedes Jahr einzahlen', withdrawalOption: 'Jedes Jahr entnehmen',
14
+ costEquivalentLabel: 'Kostenwert am Ende', realValueLabel: 'Endwert in heutigem Geld', nominalBalanceLabel: 'Nominales Endguthaben', purchasingPowerLossLabel: 'Verlorene Kaufkraft',
15
+ todayLabel: 'Heute', endLabel: 'Ende des Zeitraums', costLineLabel: 'Kosten fuer denselben Warenkorb', realLineLabel: 'Unveraenderte Kaufkraft des Bargelds', balanceLineLabel: 'Szenarioguthaben in heutigem Geld',
16
+ chartDescription: 'Inflationsverlauf mit steigenden Kosten, sinkender Kaufkraft und Szenarioguthaben', chartNote: 'Jede Linie wird am Startbetrag gemessen', milestoneTitle: 'Meilensteine in derselben Waehrung',
17
+ yearHeader: 'Jahr', costHeader: 'Kostenwert', realHeader: 'Unveraendertes Bargeld', balanceHeader: 'Reales Guthaben', methodTitle: 'So funktioniert der Zeitverlauf',
18
+ methodText: 'Der Kostenwert ist der Startbetrag multipliziert mit (1 + jaehrliche Inflationsrate) hoch der Jahreszahl. Der Wert unveraenderten Bargelds ist der Startbetrag geteilt durch denselben Faktor. Einzahlungen werden am Jahresende addiert, Entnahmen abgezogen; das Guthaben endet bei null.',
19
+ limitationText: 'Dies ist ein transparentes Szenariomodell, keine historische CPI-Abfrage, Renditeprognose oder persoenliche Finanzberatung. Dein Warenkorb kann von einem veroeffentlichten Verbraucherpreisindex abweichen, und die tatsaechliche Inflation kann sich jaehrlich aendern.',
20
+ emptyMessage: 'Gib einen positiven Betrag, eine Rate ueber -100 %, eine ganze Jahreszahl von 1 bis 80 und einen nicht negativen Cashflow ein.', contributionSummary: 'Die gestrichelte Linie zeigt den heutigen Wert des Guthabens nach den regelmaessigen Einzahlungen ohne Anlagerendite.', withdrawalSummary: 'Die gestrichelte Linie zeigt den heutigen Wert des Guthabens nach den regelmaessigen Entnahmen ohne Anlagerendite.', noCashFlowSummary: 'Die durchgezogenen Linien zeigen nur die Inflation: eine die kuenftigen Kosten desselben Warenkorbs, die andere die Kaufkraft unveraenderten Bargelds.', depletionWarning: 'Das Entnahmeszenario erreicht im Jahr {year} null; danach bleibt es bei null.',
21
+ scenarioNote: 'Nur ein Szenario', currencyHint: 'Dieselbe Waehrung wie der Startbetrag', yearsSuffix: 'Jahre', amountSuffix: 'in derselben Waehrung wie dein Startbetrag',
22
+ };
23
+ const faq = [
24
+ { question: 'Wie senkt Inflation die Kaufkraft?', answer: 'Wenn das allgemeine Preisniveau steigt, kauft derselbe Betrag weniger Waren und Dienstleistungen. Der Rechner zeigt diesen Effekt, indem er unveraendertes Bargeld durch den kumulierten Inflationsfaktor teilt.' },
25
+ { question: 'Was ist der Unterschied zwischen Kostenwert und Realwert?', answer: 'Der Kostenwert ist der kuenftige Betrag fuer denselben Warenkorb. Der Realwert zeigt, was der unveraenderte Startbetrag am Ende in heutigem Geld kaufen kann.' },
26
+ { question: 'Was enthalten Einzahlungs- und Entnahmeszenarien?', answer: 'Der Cashflow wird am Ende jedes Jahres addiert oder abgezogen. Renditen, Steuern, Gebuehren und Zinsen sind nicht enthalten; Entnahmen koennen das Guthaben nicht unter null senken.' },
27
+ { question: 'Ist die Inflationsrate eine Vorhersage?', answer: 'Nein. Sie ist eine Annahme zur Sensitivitaetsanalyse. Die tatsaechliche Inflation kann abweichen, ebenso wie dein persoenlicher Warenkorb.' },
28
+ ];
29
+ const howTo = [
30
+ { name: 'Startbetrag eingeben', text: 'Verwende den Betrag, dessen kuenftige Kaufkraft du untersuchen willst, und bleibe bei einer Waehrung.' },
31
+ { name: 'Inflationsszenario festlegen', text: 'Gib eine jaehrliche Rate und einen ganzen Zeitraum ein. Vergleiche mehrere Raten, statt dich auf eine Annahme zu verlassen.' },
32
+ { name: 'Optionalen Cashflow hinzufuegen', text: 'Waehle Einzahlen oder Entnehmen und gib den wiederkehrenden Betrag ein. Ohne regelmaessigen Cashflow bleibt nur der Inflationseffekt.' },
33
+ { name: 'Kurven und Meilensteine lesen', text: 'Vergleiche Kostenwert und Endwert in heutigem Geld. Die Tabelle zeigt, wann die Luecke deutlich wird.' },
34
+ ];
35
+ const faqSchema: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
36
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: title, description, step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })) };
37
+ const appSchema: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'FinanceApplication', operatingSystem: 'All', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, inLanguage: 'de' };
38
+ const seo = [
39
+ { type: 'title' as const, text: 'Inflationsverlauf fuer eine bessere Kaufkraftplanung', level: 2 },
40
+ { type: 'paragraph' as const, html: '<p>Inflation veraendert das Preisniveau. Ein unveraenderter Geldbetrag kann deshalb spaeter weniger kaufen. Dieser Rechner trennt die benoetigten Kosten fuer denselben Warenkorb von der Kaufkraft, die Bargeld behaelt.</p>' },
41
+ { type: 'paragraph' as const, html: '<p>Gib Startbetrag, jaehrliche Inflationsannahme und Zeitraum ein. Die Rate wird jedes Jahr kumuliert. Die Ergebnisse bleiben in derselben Waehrung und behaupten nicht, den kuenftigen Preisverlauf zu kennen.</p>' },
42
+ { type: 'title' as const, text: 'Die zwei wichtigsten Werte', level: 3 },
43
+ { type: 'table' as const, headers: ['Wert', 'Formel', 'Frage'], rows: [['Kostenwert', 'Startbetrag x (1 + Inflationsrate)^Jahre', 'Wie viel kostet derselbe Warenkorb am Ende?'], ['Kaufkraft von Bargeld', 'Startbetrag / (1 + Inflationsrate)^Jahre', 'Was kann der alte Betrag in heutigem Geld kaufen?'], ['Verlorene Kaufkraft', '(1 - 1 / Preisfaktor) x 100', 'Welcher Anteil der Kaufkraft fehlt?']] },
44
+ { type: 'paragraph' as const, html: '<p>Bei 3 % Inflation werden aus 1.000 nach zehn Jahren ungefaehr 1.344 Kostenwert. Bleibt der Betrag als Bargeld unveraendert, entspricht seine Kaufkraft dann etwa 744 in heutigem Geld.</p>' },
45
+ { type: 'title' as const, text: 'Warum die Aufzinsung zaehlt', level: 3 },
46
+ { type: 'paragraph' as const, html: '<p>Jedes Jahrespreisniveau bildet die Basis fuer das naechste. Zehn Jahre mit 3 % sind daher nicht einfach 30 %. Der Faktor ist 1,03 hoch zehn, also ungefaehr 1,344. Kleine Ratenunterschiede werden ueber lange Zeitraeume sichtbar.</p>' },
47
+ { type: 'list' as const, items: ['Vergleiche niedrige, mittlere und hohe Raten.', 'Nutze die Meilensteintabelle fuer wichtige Ausgaben.', 'Vergleiche den Kostenwert mit Sparziel, Einkommen oder Rente.', 'Notiere Annahme und Datum und ersetze sie bei neuen Erkenntnissen.'] },
48
+ { type: 'title' as const, text: 'Einzahlungen und Entnahmen', level: 3 },
49
+ { type: 'paragraph' as const, html: '<p>Eine Einzahlung wird am Jahresende zum Nominalguthaben addiert. Eine Entnahme wird dort abgezogen; wenn sie das Guthaben aufbraucht, bleibt es bei null. So bleibt der Inflationseffekt sichtbar.</p>' },
50
+ { type: 'paragraph' as const, html: '<p>Die gestrichelte Guthabenlinie wird mit demselben Faktor in heutiges Geld umgerechnet. Sie ist keine Anlageprojektion und enthält weder Zinsen noch Markttrend, Steuer, Gebuehr oder Wachstum der Einzahlung.</p>' },
51
+ { type: 'tip' as const, title: 'Nutze eine Bandbreite statt eines Versprechens', html: '<p>Ein Verbraucherpreisindex beschreibt einen Durchschnittswarenkorb. Deine Miete, Energie, Lebensmittel und Gesundheit koennen sich anders entwickeln. Teste mehrere Raten.</p>' },
52
+ { type: 'title' as const, text: 'Was der Zeitverlauf nicht sagt', level: 3 },
53
+ { type: 'paragraph' as const, html: '<p>Das Tool ruft keine historischen CPI-Daten ab, prognostiziert keine Inflation und empfiehlt keine Anlage. Veraenderliche Jahresraten, Renditen, Kreditzinsen und unregelmaessige Cashflows sind nicht modelliert.</p>' },
54
+ { type: 'paragraph' as const, html: '<p>Bei wichtigen Entscheidungen vergleiche das Ergebnis mit einem passenden Verbraucherpreisindex, deinen Ausgabenaufzeichnungen und den Vertragsbedingungen. Ein mathematisch konsistentes Ergebnis muss deinen Haushalt nicht genau beschreiben.</p>' },
55
+ ];
56
+ export const content: ToolLocaleContent<InflationPurchasingPowerUI> = { slug: 'inflation-kaufkraft-zeitstrahl-rechner', title, description, ui, faq, bibliography, howTo, schemas: [faqSchema, howToSchema, appSchema], seo };
@@ -0,0 +1,131 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
3
+ import type { ToolLocaleContent } from '../../../types';
4
+ import type { InflationPurchasingPowerUI } from '../ui';
5
+
6
+ const slug = 'inflation-purchasing-power-timeline-calculator';
7
+ const title = 'Inflation Purchasing Power Timeline Calculator';
8
+ const description = 'See how an annual inflation assumption changes the amount needed to preserve purchasing power and the real value of cash over time.';
9
+
10
+ const ui: InflationPurchasingPowerUI = {
11
+ amountLabel: 'Starting amount',
12
+ amountHelp: 'Use any currency and keep it consistent',
13
+ inflationLabel: 'Annual inflation rate (%)',
14
+ inflationHelp: 'A scenario assumption, not a forecast',
15
+ yearsLabel: 'Time horizon (years)',
16
+ yearsHelp: 'Choose from 1 to 80 years',
17
+ cashFlowLabel: 'Annual cash flow',
18
+ cashFlowHelp: 'Added to or taken from the balance each year',
19
+ cashFlowModeLabel: 'Cash flow scenario',
20
+ noCashFlowOption: 'No recurring flow',
21
+ contributionOption: 'Add each year',
22
+ withdrawalOption: 'Withdraw each year',
23
+ costEquivalentLabel: 'End cost equivalent',
24
+ realValueLabel: 'End value in today\'s money',
25
+ nominalBalanceLabel: 'End nominal balance',
26
+ purchasingPowerLossLabel: 'Purchasing power lost',
27
+ todayLabel: 'Today',
28
+ endLabel: 'End of horizon',
29
+ costLineLabel: 'Cost to preserve the same basket',
30
+ realLineLabel: 'Unchanged cash buying power',
31
+ balanceLineLabel: 'Scenario balance in today\'s money',
32
+ chartDescription: 'Inflation timeline showing the rising cost equivalent, falling buying power and scenario balance',
33
+ chartNote: 'Each line is measured against the starting amount',
34
+ milestoneTitle: 'Milestones in the same currency',
35
+ yearHeader: 'Year',
36
+ costHeader: 'Cost equivalent',
37
+ realHeader: 'Unchanged cash',
38
+ balanceHeader: 'Real balance',
39
+ methodTitle: 'How the timeline works',
40
+ methodText: 'The cost equivalent is the starting amount multiplied by (1 + annual inflation rate) to the power of the number of years. The value of unchanged cash is the starting amount divided by that same factor. A contribution is added at the end of each year and a withdrawal is subtracted at the end of each year, with the balance stopping at zero.',
41
+ limitationText: 'This is a transparent scenario model, not a historical CPI lookup, investment return forecast or personal financial advice. Your household basket can differ from a published consumer price index, and actual inflation can change from year to year.',
42
+ emptyMessage: 'Enter a positive amount, a rate above -100%, a whole number of years from 1 to 80 and a non-negative cash flow.',
43
+ contributionSummary: 'The dashed line shows the value today of the balance after the recurring contributions, with no investment return included.',
44
+ withdrawalSummary: 'The dashed line shows the value today of the balance after the recurring withdrawals, with no investment return included.',
45
+ noCashFlowSummary: 'The solid lines isolate inflation itself: one shows the future cost of the same basket and the other shows what unchanged cash can buy.',
46
+ depletionWarning: 'The withdrawal scenario reaches zero in year {year}; later years remain at zero.',
47
+ scenarioNote: 'Scenario only',
48
+ currencyHint: 'Same currency as the starting amount',
49
+ yearsSuffix: 'years',
50
+ amountSuffix: 'in the same currency as your starting amount',
51
+ };
52
+
53
+ const faq = [
54
+ {
55
+ question: 'How does inflation reduce purchasing power?',
56
+ answer: 'When the general price level rises, the same amount buys fewer goods and services. This calculator models that effect by dividing unchanged cash by the compounded inflation factor.',
57
+ },
58
+ {
59
+ question: 'What is the difference between cost equivalent and real value?',
60
+ answer: 'Cost equivalent is the future amount needed to buy what the starting amount buys today. Real value is what the unchanged starting amount can buy at the end of the selected horizon, expressed in today\'s money.',
61
+ },
62
+ {
63
+ question: 'What do the contribution and withdrawal scenarios include?',
64
+ answer: 'They add or subtract the cash flow at the end of each year. They do not include investment returns, taxes, fees or interest, and a withdrawal balance cannot fall below zero.',
65
+ },
66
+ {
67
+ question: 'Is the inflation rate a prediction?',
68
+ answer: 'No. It is an assumption for exploring sensitivity. Actual inflation can vary, and a published consumer price index may not match the spending pattern of your household.',
69
+ },
70
+ ];
71
+
72
+ const howTo = [
73
+ { name: 'Enter a starting amount', text: 'Use the amount whose future purchasing power you want to inspect, keeping one currency throughout the calculation.' },
74
+ { name: 'Set an inflation scenario', text: 'Enter an annual rate and a whole-number time horizon. Use a range of rates when you want to test sensitivity instead of relying on one assumption.' },
75
+ { name: 'Add an optional cash flow', text: 'Choose Add each year or Withdraw each year, then enter the recurring amount. Leave No recurring flow selected to isolate inflation.' },
76
+ { name: 'Read the curves and milestones', text: 'Compare the end cost equivalent with the end value in today\'s money, then use the milestone table to see when the gap becomes material.' },
77
+ ];
78
+
79
+ const faqSchema: WithContext<FAQPage> = {
80
+ '@context': 'https://schema.org',
81
+ '@type': 'FAQPage',
82
+ mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })),
83
+ };
84
+
85
+ const howToSchema: WithContext<HowTo> = {
86
+ '@context': 'https://schema.org',
87
+ '@type': 'HowTo',
88
+ name: title,
89
+ description,
90
+ step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })),
91
+ };
92
+
93
+ const appSchema: WithContext<SoftwareApplication> = {
94
+ '@context': 'https://schema.org',
95
+ '@type': 'SoftwareApplication',
96
+ name: title,
97
+ description,
98
+ applicationCategory: 'FinanceApplication',
99
+ operatingSystem: 'All',
100
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
101
+ inLanguage: 'en',
102
+ };
103
+
104
+ export const content: ToolLocaleContent<InflationPurchasingPowerUI> = {
105
+ slug,
106
+ title,
107
+ description,
108
+ ui,
109
+ faq,
110
+ bibliography,
111
+ howTo,
112
+ schemas: [faqSchema, howToSchema, appSchema],
113
+ seo: [
114
+ { type: 'title', text: 'Use an inflation timeline to plan for a changing price level', level: 2 },
115
+ { type: 'paragraph', html: '<p>Inflation changes the price level over time, so a cash amount that looks unchanged can buy less in the future. This calculator separates two questions that are often mixed together: how much money may be needed to buy the same basket, and how much purchasing power unchanged cash retains.</p>' },
116
+ { type: 'paragraph', html: '<p>Enter a starting amount, an annual inflation assumption and a time horizon. The timeline compounds the rate each year, then shows the cost equivalent rising while the buying power of unchanged cash falls. The result is expressed in the same currency as your input and does not pretend to know the future path of prices.</p>' },
117
+ { type: 'title', text: 'The two core readings', level: 3 },
118
+ { type: 'table', headers: ['Reading', 'Formula', 'Use it to answer'], rows: [['Cost equivalent', 'Starting amount x (1 + inflation rate)^years', 'How much would be needed at the end to buy the same basket?'], ['Unchanged cash buying power', 'Starting amount / (1 + inflation rate)^years', 'What can the original cash buy at the end, measured in today\'s money?'], ['Purchasing power lost', '(1 - 1 / price factor) x 100', 'What share of the starting buying power is no longer available?']] },
119
+ { type: 'paragraph', html: '<p>For example, at 3% annual inflation, 1,000 becomes a cost equivalent of about 1,344 after ten years. If the 1,000 stays unchanged in cash, its purchasing power is about 744 in today\'s money. Those are two views of the same price-level change, not two separate investment returns.</p>' },
120
+ { type: 'title', text: 'Why compounding matters', level: 3 },
121
+ { type: 'paragraph', html: '<p>Annual inflation rates compound because each year\'s prices become the base for the next year. Ten years at 3% is not a flat 30% adjustment: the factor is 1.03 to the tenth power, or about 1.344. Small differences in the assumed rate become more visible over longer horizons.</p>' },
122
+ { type: 'list', items: ['Run a low, central and high rate instead of trusting one scenario.', 'Use the milestone table to identify when a planned expense becomes materially more expensive.', 'Compare the cost equivalent with an income, pension or savings target expressed in the same currency.', 'Treat the result as a planning reference and replace the assumption when your evidence changes.'] },
123
+ { type: 'title', text: 'Adding contributions or withdrawals', level: 3 },
124
+ { type: 'paragraph', html: '<p>The optional cash-flow scenario makes the timeline more useful for a savings or spending plan without hiding the inflation effect. A contribution is added to the nominal balance at the end of each year. A withdrawal is subtracted at the same point, and the model stops the balance at zero when the withdrawals exhaust it.</p>' },
125
+ { type: 'paragraph', html: '<p>The dashed balance line is converted back into today\'s money using the same inflation factor. It is not an investment projection: no interest, market return, tax, fee or contribution growth is included. If you need to plan a real portfolio, keep the inflation assumption and the return assumption as separate scenarios.</p>' },
126
+ { type: 'tip', title: 'Use the rate as a range, not a promise', html: '<p>Published consumer price indexes describe an average basket, while your own rent, energy, food, travel and healthcare mix may move differently. Test several rates and label the result with the assumption and date you used.</p>' },
127
+ { type: 'title', text: 'What the timeline cannot tell you', level: 3 },
128
+ { type: 'paragraph', html: '<p>This tool does not retrieve historical CPI data, forecast inflation, estimate a personal cost of living, calculate taxes or recommend an investment. It also does not model changing annual rates, asset returns, debt interest or irregular cash flows. Its value is making one assumption visible and showing how strongly the horizon changes the result.</p>' },
129
+ { type: 'paragraph', html: '<p>For an important decision, compare the output with an appropriate official price index, your actual spending records and the terms of the product or income you are planning around. A result can be mathematically consistent and still be a poor description of your household or country.</p>' },
130
+ ],
131
+ };