@jjlmoya/utils-finance 1.41.0 → 1.43.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 (64) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/capitalGainsTaxScenarioCalculator/bibliography.astro +11 -0
  8. package/src/tool/capitalGainsTaxScenarioCalculator/bibliography.ts +12 -0
  9. package/src/tool/capitalGainsTaxScenarioCalculator/capital-gains-tax-scenario-calculator.css +469 -0
  10. package/src/tool/capitalGainsTaxScenarioCalculator/component.astro +328 -0
  11. package/src/tool/capitalGainsTaxScenarioCalculator/entry.ts +26 -0
  12. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/de.ts +3 -0
  13. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/en.ts +181 -0
  14. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/es.ts +3 -0
  15. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/fr.ts +3 -0
  16. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/id.ts +3 -0
  17. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/it.ts +3 -0
  18. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/ja.ts +3 -0
  19. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/ko.ts +3 -0
  20. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/nl.ts +3 -0
  21. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/pl.ts +3 -0
  22. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/pt.ts +3 -0
  23. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/ru.ts +3 -0
  24. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/shared.ts +153 -0
  25. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/sv.ts +3 -0
  26. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/tr.ts +3 -0
  27. package/src/tool/capitalGainsTaxScenarioCalculator/i18n/zh.ts +3 -0
  28. package/src/tool/capitalGainsTaxScenarioCalculator/index.ts +11 -0
  29. package/src/tool/capitalGainsTaxScenarioCalculator/logic.test.ts +71 -0
  30. package/src/tool/capitalGainsTaxScenarioCalculator/logic.ts +137 -0
  31. package/src/tool/capitalGainsTaxScenarioCalculator/seo.astro +11 -0
  32. package/src/tool/capitalGainsTaxScenarioCalculator/ui.ts +40 -0
  33. package/src/tool/capitalGainsTaxScenarioCalculator/validation.ts +34 -0
  34. package/src/tool/rentalDepositReturn/bibliography.astro +11 -0
  35. package/src/tool/rentalDepositReturn/bibliography.ts +6 -0
  36. package/src/tool/rentalDepositReturn/component.astro +53 -0
  37. package/src/tool/rentalDepositReturn/controller.ts +156 -0
  38. package/src/tool/rentalDepositReturn/currency.ts +67 -0
  39. package/src/tool/rentalDepositReturn/dom-views.ts +31 -0
  40. package/src/tool/rentalDepositReturn/entry.ts +28 -0
  41. package/src/tool/rentalDepositReturn/i18n/content-factory.ts +36 -0
  42. package/src/tool/rentalDepositReturn/i18n/de.ts +3 -0
  43. package/src/tool/rentalDepositReturn/i18n/en.ts +116 -0
  44. package/src/tool/rentalDepositReturn/i18n/es.ts +3 -0
  45. package/src/tool/rentalDepositReturn/i18n/fr.ts +3 -0
  46. package/src/tool/rentalDepositReturn/i18n/id.ts +3 -0
  47. package/src/tool/rentalDepositReturn/i18n/it.ts +3 -0
  48. package/src/tool/rentalDepositReturn/i18n/ja.ts +3 -0
  49. package/src/tool/rentalDepositReturn/i18n/ko.ts +3 -0
  50. package/src/tool/rentalDepositReturn/i18n/nl.ts +3 -0
  51. package/src/tool/rentalDepositReturn/i18n/pl.ts +3 -0
  52. package/src/tool/rentalDepositReturn/i18n/pt.ts +3 -0
  53. package/src/tool/rentalDepositReturn/i18n/ru.ts +3 -0
  54. package/src/tool/rentalDepositReturn/i18n/sv.ts +3 -0
  55. package/src/tool/rentalDepositReturn/i18n/tr.ts +3 -0
  56. package/src/tool/rentalDepositReturn/i18n/translations.ts +34 -0
  57. package/src/tool/rentalDepositReturn/i18n/zh.ts +3 -0
  58. package/src/tool/rentalDepositReturn/index.ts +11 -0
  59. package/src/tool/rentalDepositReturn/logic.test.ts +53 -0
  60. package/src/tool/rentalDepositReturn/logic.ts +108 -0
  61. package/src/tool/rentalDepositReturn/rental-deposit-return-calculator.css +591 -0
  62. package/src/tool/rentalDepositReturn/seo.astro +11 -0
  63. package/src/tool/rentalDepositReturn/ui.ts +53 -0
  64. package/src/tools.ts +3 -1
@@ -0,0 +1,328 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { CapitalGainsInput } from './logic';
4
+ import type { CapitalGainsTaxScenarioCalculatorUI } from './ui';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ ui?: CapitalGainsTaxScenarioCalculatorUI;
9
+ }
10
+
11
+ const { ui } = Astro.props;
12
+ const t = ui as CapitalGainsTaxScenarioCalculatorUI;
13
+ const currentLocale = Astro.props.locale ?? 'en';
14
+ const defaultCurrencyByLocale: Record<string, string> = {
15
+ de: 'EUR',
16
+ en: 'USD',
17
+ es: 'EUR',
18
+ fr: 'EUR',
19
+ id: 'IDR',
20
+ it: 'EUR',
21
+ ja: 'JPY',
22
+ ko: 'KRW',
23
+ nl: 'EUR',
24
+ pl: 'PLN',
25
+ pt: 'EUR',
26
+ ru: 'RUB',
27
+ sv: 'SEK',
28
+ tr: 'TRY',
29
+ zh: 'CNY',
30
+ };
31
+ const currencyOptions = [
32
+ { code: 'USD', symbol: '$' },
33
+ { code: 'EUR', symbol: '€' },
34
+ { code: 'GBP', symbol: '£' },
35
+ { code: 'JPY', symbol: '¥' },
36
+ { code: 'CNY', symbol: '¥' },
37
+ { code: 'KRW', symbol: '₩' },
38
+ { code: 'RUB', symbol: '₽' },
39
+ { code: 'INR', symbol: '₹' },
40
+ { code: 'CAD', symbol: 'C$' },
41
+ { code: 'AUD', symbol: 'A$' },
42
+ { code: 'CHF', symbol: 'CHF' },
43
+ { code: 'PLN', symbol: 'zł' },
44
+ { code: 'SEK', symbol: 'kr' },
45
+ { code: 'NOK', symbol: 'kr' },
46
+ { code: 'DKK', symbol: 'kr' },
47
+ { code: 'TRY', symbol: '₺' },
48
+ { code: 'BRL', symbol: 'R$' },
49
+ { code: 'MXN', symbol: 'MX$' },
50
+ { code: 'IDR', symbol: 'Rp' },
51
+ { code: 'CZK', symbol: 'Kč' },
52
+ { code: 'HUF', symbol: 'Ft' },
53
+ { code: 'ILS', symbol: '₪' },
54
+ { code: 'ZAR', symbol: 'R' },
55
+ { code: 'SGD', symbol: 'S$' },
56
+ { code: 'HKD', symbol: 'HK$' },
57
+ ] as const;
58
+ const defaultCurrency = defaultCurrencyByLocale[currentLocale] ?? 'USD';
59
+ const initial: CapitalGainsInput = {
60
+ purchasePrice: 100000,
61
+ acquisitionCosts: 5000,
62
+ improvements: 10000,
63
+ salePrice: 180000,
64
+ sellingCosts: 6000,
65
+ taxRate: 20,
66
+ alternativeSalePrice: 190000,
67
+ alternativeSellingCosts: 7000,
68
+ };
69
+ const config = JSON.stringify({ ui: t, initial, locale: currentLocale, currency: defaultCurrency, currencies: currencyOptions });
70
+ ---
71
+
72
+ <div
73
+ class="cgt-tool"
74
+ data-cgt-tool
75
+ data-status-gain={t.labelGainStatus}
76
+ data-status-loss={t.labelLossStatus}
77
+ data-status-break-even={t.labelBreakEvenStatus}
78
+ data-better-current={t.labelCurrentIsBetter}
79
+ data-better-alternative={t.labelAlternativeIsBetter}
80
+ data-better-same={t.labelSameOutcome}
81
+ data-invalid={t.labelInvalidInput}
82
+ data-copy={t.labelCopy}
83
+ data-copied={t.labelCopied}
84
+ data-locale={Astro.props.locale ?? 'en'}
85
+ >
86
+ <form class="cgt-card" data-cgt-form novalidate>
87
+ <div class="cgt-glance" aria-live="polite">
88
+ <span>{t.labelNetGainAfterTax}</span>
89
+ <strong><small>{t.labelCurrentScenario}</small><span data-cgt-glance-current>0</span></strong>
90
+ <strong><small>{t.labelAlternativeScenario}</small><span data-cgt-glance-alternative>0</span></strong>
91
+ </div>
92
+ <div class="cgt-inputs">
93
+ <div class="cgt-field cgt-currency-field">
94
+ <label for="cgt-currency">{t.labelCurrency}</label>
95
+ <div class="cgt-select-wrap">
96
+ <select id="cgt-currency" data-cgt-currency>
97
+ {currencyOptions.map(({ code, symbol }) => (
98
+ <option value={code} selected={code === defaultCurrency}>{code} ({symbol})</option>
99
+ ))}
100
+ </select>
101
+ <span class="cgt-select-arrow" aria-hidden="true"></span>
102
+ </div>
103
+ </div>
104
+ <div class="cgt-field">
105
+ <label for="cgt-purchase">{t.labelPurchasePrice}</label>
106
+ <div class="cgt-input-wrap"><input id="cgt-purchase" data-field="purchasePrice" type="number" min="0.01" max="1000000000000000" step="0.01" value={initial.purchasePrice} placeholder={t.labelAmountPlaceholder} inputmode="decimal" required /><span data-cgt-unit aria-hidden="true">$</span></div>
107
+ <small data-error-for="purchasePrice" hidden></small>
108
+ </div>
109
+ <div class="cgt-field">
110
+ <label for="cgt-acquisition">{t.labelAcquisitionCosts}</label>
111
+ <div class="cgt-input-wrap"><input id="cgt-acquisition" data-field="acquisitionCosts" type="number" min="0" max="1000000000000000" step="0.01" value={initial.acquisitionCosts} placeholder={t.labelAmountPlaceholder} inputmode="decimal" required /><span data-cgt-unit aria-hidden="true">$</span></div>
112
+ <small data-error-for="acquisitionCosts" hidden></small>
113
+ </div>
114
+ <div class="cgt-field">
115
+ <label for="cgt-improvements">{t.labelImprovements}</label>
116
+ <div class="cgt-input-wrap"><input id="cgt-improvements" data-field="improvements" type="number" min="0" max="1000000000000000" step="0.01" value={initial.improvements} placeholder={t.labelAmountPlaceholder} inputmode="decimal" required /><span data-cgt-unit aria-hidden="true">$</span></div>
117
+ <small data-error-for="improvements" hidden></small>
118
+ </div>
119
+ <div class="cgt-field">
120
+ <label for="cgt-sale">{t.labelSalePrice}</label>
121
+ <div class="cgt-input-wrap"><input id="cgt-sale" data-field="salePrice" type="number" min="0.01" max="1000000000000000" step="0.01" value={initial.salePrice} placeholder={t.labelAmountPlaceholder} inputmode="decimal" required /><span data-cgt-unit aria-hidden="true">$</span></div>
122
+ <small data-error-for="salePrice" hidden></small>
123
+ </div>
124
+ <div class="cgt-field">
125
+ <label for="cgt-selling">{t.labelSellingCosts}</label>
126
+ <div class="cgt-input-wrap"><input id="cgt-selling" data-field="sellingCosts" type="number" min="0" max="1000000000000000" step="0.01" value={initial.sellingCosts} placeholder={t.labelAmountPlaceholder} inputmode="decimal" required /><span data-cgt-unit aria-hidden="true">$</span></div>
127
+ <small data-error-for="sellingCosts" hidden></small>
128
+ </div>
129
+ <div class="cgt-field">
130
+ <label for="cgt-tax">{t.labelTaxRate}</label>
131
+ <div class="cgt-input-wrap"><input id="cgt-tax" data-field="taxRate" type="number" min="0" max="100" step="0.01" value={initial.taxRate} placeholder={t.labelPercentPlaceholder} inputmode="decimal" required /><span aria-hidden="true">%</span></div>
132
+ <small data-error-for="taxRate" hidden></small>
133
+ </div>
134
+ <div class="cgt-field cgt-alternative-field">
135
+ <label for="cgt-alt-sale">{t.labelAlternativeSalePrice}</label>
136
+ <div class="cgt-input-wrap"><input id="cgt-alt-sale" data-field="alternativeSalePrice" type="number" min="0.01" max="1000000000000000" step="0.01" value={initial.alternativeSalePrice} placeholder={t.labelAmountPlaceholder} inputmode="decimal" required /><span data-cgt-unit aria-hidden="true">$</span></div>
137
+ <small data-error-for="alternativeSalePrice" hidden></small>
138
+ </div>
139
+ <div class="cgt-field cgt-alternative-field">
140
+ <label for="cgt-alt-selling">{t.labelAlternativeSellingCosts}</label>
141
+ <div class="cgt-input-wrap"><input id="cgt-alt-selling" data-field="alternativeSellingCosts" type="number" min="0" max="1000000000000000" step="0.01" value={initial.alternativeSellingCosts} placeholder={t.labelAmountPlaceholder} inputmode="decimal" required /><span data-cgt-unit aria-hidden="true">$</span></div>
142
+ <small data-error-for="alternativeSellingCosts" hidden></small>
143
+ </div>
144
+ </div>
145
+
146
+ <div class="cgt-actions">
147
+ <button type="button" class="cgt-button cgt-button-secondary" data-cgt-reset>{t.labelReset}</button>
148
+ <button type="button" class="cgt-button cgt-button-primary" data-cgt-copy>{t.labelCopy}</button>
149
+ <span class="cgt-copy-status" data-cgt-copy-status role="status" aria-live="polite"></span>
150
+ </div>
151
+
152
+ <section class="cgt-output" aria-live="polite" aria-atomic="true">
153
+ <div class="cgt-status" data-cgt-status>{t.labelGainStatus}</div>
154
+ <div class="cgt-visual" data-cgt-visual role="img" aria-label={t.labelVisual}>
155
+ <div class="cgt-visual-heading"><span>{t.labelCurrentScenario}</span><span>{t.labelAlternativeScenario}</span></div>
156
+ <div class="cgt-visual-bars">
157
+ <div class="cgt-bar-side"><strong data-cgt-current-bar-value>0</strong><div class="cgt-bar-track"><i data-cgt-current-bar></i></div></div>
158
+ <div class="cgt-visual-rail" aria-hidden="true"><span>⇄</span></div>
159
+ <div class="cgt-bar-side"><strong data-cgt-alternative-bar-value>0</strong><div class="cgt-bar-track"><i data-cgt-alternative-bar></i></div></div>
160
+ </div>
161
+ </div>
162
+
163
+ <div class="cgt-scenarios">
164
+ <section class="cgt-scenario" data-scenario="current">
165
+ <h2>{t.labelCurrentScenario}</h2>
166
+ <dl>
167
+ <div><dt>{t.labelAcquisitionBasis}</dt><dd data-cgt-current-basis></dd></div>
168
+ <div><dt>{t.labelNetSaleValue}</dt><dd data-cgt-current-sale></dd></div>
169
+ <div><dt>{t.labelGainOrLoss}</dt><dd data-cgt-current-gain></dd></div>
170
+ <div><dt>{t.labelEstimatedTax}</dt><dd data-cgt-current-tax></dd></div>
171
+ <div class="cgt-emphasis"><dt>{t.labelNetGainAfterTax}</dt><dd data-cgt-current-net></dd></div>
172
+ <div><dt>{t.labelNetProceeds}</dt><dd data-cgt-current-proceeds></dd></div>
173
+ </dl>
174
+ <p class="cgt-note" data-cgt-current-note>{t.labelTaxableGainNote}</p>
175
+ </section>
176
+ <section class="cgt-scenario" data-scenario="alternative">
177
+ <h2>{t.labelAlternativeScenario}</h2>
178
+ <dl>
179
+ <div><dt>{t.labelAcquisitionBasis}</dt><dd data-cgt-alternative-basis></dd></div>
180
+ <div><dt>{t.labelNetSaleValue}</dt><dd data-cgt-alternative-sale></dd></div>
181
+ <div><dt>{t.labelGainOrLoss}</dt><dd data-cgt-alternative-gain></dd></div>
182
+ <div><dt>{t.labelEstimatedTax}</dt><dd data-cgt-alternative-tax></dd></div>
183
+ <div class="cgt-emphasis"><dt>{t.labelNetGainAfterTax}</dt><dd data-cgt-alternative-net></dd></div>
184
+ <div><dt>{t.labelNetProceeds}</dt><dd data-cgt-alternative-proceeds></dd></div>
185
+ </dl>
186
+ <p class="cgt-note" data-cgt-alternative-note>{t.labelTaxableGainNote}</p>
187
+ </section>
188
+ </div>
189
+
190
+ <div class="cgt-comparison" data-cgt-comparison>
191
+ <span>{t.labelBetterScenario}</span>
192
+ <strong data-cgt-better></strong>
193
+ <small data-cgt-difference></small>
194
+ </div>
195
+ <p class="cgt-error-summary" data-cgt-error-summary hidden>{t.labelInvalidInput}</p>
196
+ </section>
197
+
198
+ <p class="cgt-privacy">{t.labelPrivacyNote}</p>
199
+ <details class="cgt-method"><summary>{t.labelMethodTitle}</summary><p>{t.labelMethodText}</p></details>
200
+ </form>
201
+ </div>
202
+
203
+ <script is:inline type="application/json" data-cgt-config set:html={config}></script>
204
+ <script>
205
+ import { calculateCapitalGains, validateCapitalGainsInput, type CapitalGainsInput, type CapitalGainsScenarioResult } from './logic';
206
+
207
+ const configElement = document.querySelector<HTMLScriptElement>('[data-cgt-config]');
208
+ const root = document.querySelector<HTMLElement>('[data-cgt-tool]');
209
+ const form = root?.querySelector<HTMLFormElement>('[data-cgt-form]');
210
+ if (root && form && configElement?.textContent) {
211
+ const config = JSON.parse(configElement.textContent) as { initial: CapitalGainsInput; locale: string; currency: string; currencies: Array<{ code: string; symbol: string }> };
212
+ let selectedCurrency = config.currency;
213
+ const fields = Array.from(form.querySelectorAll<HTMLInputElement>('[data-field]'));
214
+ const amountKeys = ['purchasePrice', 'acquisitionCosts', 'improvements', 'salePrice', 'sellingCosts', 'alternativeSalePrice', 'alternativeSellingCosts'] as const;
215
+ const getInput = (key: keyof CapitalGainsInput): number => Number(form.querySelector<HTMLInputElement>(`[data-field="${key}"]`)?.value ?? Number.NaN);
216
+ const readInput = (): CapitalGainsInput => ({
217
+ purchasePrice: getInput('purchasePrice'),
218
+ acquisitionCosts: getInput('acquisitionCosts'),
219
+ improvements: getInput('improvements'),
220
+ salePrice: getInput('salePrice'),
221
+ sellingCosts: getInput('sellingCosts'),
222
+ taxRate: getInput('taxRate'),
223
+ alternativeSalePrice: getInput('alternativeSalePrice'),
224
+ alternativeSellingCosts: getInput('alternativeSellingCosts'),
225
+ });
226
+ const format = (value: number): string => new Intl.NumberFormat(config.locale, { style: 'currency', currency: selectedCurrency, maximumFractionDigits: 2 }).format(value);
227
+ const formatSigned = (value: number): string => `${value > 0 ? '+' : ''}${format(value)}`;
228
+ const text = (selector: string, value: string): void => { const target = root.querySelector<HTMLElement>(selector); if (target) target.textContent = value; };
229
+ const setScenario = (prefix: string, scenario: CapitalGainsScenarioResult): void => {
230
+ text(`[data-cgt-${prefix}-basis]`, format(scenario.acquisitionBasis));
231
+ text(`[data-cgt-${prefix}-sale]`, format(scenario.netSaleValue));
232
+ text(`[data-cgt-${prefix}-gain]`, formatSigned(scenario.gainOrLoss));
233
+ text(`[data-cgt-${prefix}-tax]`, format(scenario.estimatedTax));
234
+ text(`[data-cgt-${prefix}-net]`, formatSigned(scenario.netGainAfterTax));
235
+ text(`[data-cgt-${prefix}-proceeds]`, format(scenario.netProceeds));
236
+ const note = root.querySelector<HTMLElement>(`[data-cgt-${prefix}-note]`);
237
+ if (note) note.hidden = scenario.taxableGain > 0;
238
+ };
239
+ const statusDatasetKey = (status: CapitalGainsScenarioResult['status']): string => {
240
+ if (status === 'gain') return 'statusGain';
241
+ if (status === 'loss') return 'statusLoss';
242
+ return 'statusBreakEven';
243
+ };
244
+ const betterDatasetKey = (scenario: 'current' | 'alternative' | 'same'): string => {
245
+ if (scenario === 'current') return 'betterCurrent';
246
+ if (scenario === 'alternative') return 'betterAlternative';
247
+ return 'betterSame';
248
+ };
249
+ const updateVisual = (result: ReturnType<typeof calculateCapitalGains>): void => {
250
+ if (!result) return;
251
+ const maxGain = Math.max(result.current.netGainAfterTax, result.alternative.netGainAfterTax, 1);
252
+ const getWidth = (value: number): string => `${Math.max(4, Math.min(100, (Math.max(0, value) / maxGain) * 100))}%`;
253
+ root.querySelector<HTMLElement>('[data-cgt-current-bar]')?.style.setProperty('width', getWidth(result.current.netGainAfterTax));
254
+ root.querySelector<HTMLElement>('[data-cgt-alternative-bar]')?.style.setProperty('width', getWidth(result.alternative.netGainAfterTax));
255
+ text('[data-cgt-current-bar-value]', format(result.current.netGainAfterTax));
256
+ text('[data-cgt-alternative-bar-value]', format(result.alternative.netGainAfterTax));
257
+ text('[data-cgt-glance-current]', format(result.current.netGainAfterTax));
258
+ text('[data-cgt-glance-alternative]', format(result.alternative.netGainAfterTax));
259
+ };
260
+ const updateCurrencyUnits = (): void => {
261
+ const symbol = config.currencies.find(({ code }) => code === selectedCurrency)?.symbol ?? selectedCurrency;
262
+ root.querySelectorAll<HTMLElement>('[data-cgt-unit]').forEach((unit) => { unit.textContent = symbol; });
263
+ };
264
+ const updateComparison = (result: ReturnType<typeof calculateCapitalGains>): void => {
265
+ if (!result) return;
266
+ text('[data-cgt-better]', root.dataset[betterDatasetKey(result.comparison.betterScenario)] ?? '');
267
+ text('[data-cgt-difference]', `${formatSigned(result.comparison.netGainDifference)} after estimated tax`);
268
+ root.querySelector<HTMLElement>('[data-cgt-comparison]')?.removeAttribute('hidden');
269
+ };
270
+ const showErrors = (input: CapitalGainsInput): boolean => {
271
+ const errors = validateCapitalGainsInput(input);
272
+ [...amountKeys, 'taxRate' as const].forEach((key) => {
273
+ const field = form.querySelector<HTMLInputElement>(`[data-field="${key}"]`);
274
+ const error = form.querySelector<HTMLElement>(`[data-error-for="${key}"]`);
275
+ const invalid = Boolean(errors[key]);
276
+ if (field) field.setAttribute('aria-invalid', String(invalid));
277
+ if (error) { error.hidden = !invalid; error.textContent = invalid ? root.dataset.invalid ?? '' : ''; }
278
+ });
279
+ const summary = root.querySelector<HTMLElement>('[data-cgt-error-summary]');
280
+ if (summary) summary.hidden = Object.keys(errors).length === 0;
281
+ return Object.keys(errors).length > 0;
282
+ };
283
+ const update = (): void => {
284
+ const input = readInput();
285
+ if (showErrors(input)) return;
286
+ const result = calculateCapitalGains(input);
287
+ if (!result) return;
288
+ const status = result.current.status;
289
+ text('[data-cgt-status]', root.dataset[statusDatasetKey(status)] ?? '');
290
+ root.dataset.status = status;
291
+ setScenario('current', result.current);
292
+ setScenario('alternative', result.alternative);
293
+ updateVisual(result);
294
+ updateComparison(result);
295
+ };
296
+
297
+ fields.forEach((field) => field.addEventListener('input', update));
298
+ form.querySelector('[data-cgt-reset]')?.addEventListener('click', () => {
299
+ fields.forEach((field) => { const key = field.dataset.field as keyof CapitalGainsInput; field.value = String(config.initial[key]); });
300
+ selectedCurrency = config.currency;
301
+ const currencySelect = form.querySelector<HTMLSelectElement>('[data-cgt-currency]');
302
+ if (currencySelect) currencySelect.value = selectedCurrency;
303
+ updateCurrencyUnits();
304
+ update();
305
+ });
306
+ form.querySelector<HTMLSelectElement>('[data-cgt-currency]')?.addEventListener('change', (event) => {
307
+ selectedCurrency = (event.currentTarget as HTMLSelectElement).value;
308
+ updateCurrencyUnits();
309
+ update();
310
+ });
311
+ form.querySelector('[data-cgt-copy]')?.addEventListener('click', () => {
312
+ const result = calculateCapitalGains(readInput());
313
+ if (!result) return;
314
+ const copyText = `${root.dataset.status ?? 'Scenario'}: ${formatSigned(result.current.netGainAfterTax)} after estimated tax. Alternative: ${formatSigned(result.alternative.netGainAfterTax)}. Difference: ${formatSigned(result.comparison.netGainDifference)}.`;
315
+ navigator.clipboard?.writeText(copyText).then(() => {
316
+ text('[data-cgt-copy-status]', root.dataset.copied ?? '');
317
+ }).catch(() => {
318
+ text('[data-cgt-copy-status]', root.dataset.copy ?? '');
319
+ });
320
+ });
321
+ updateCurrencyUnits();
322
+ update();
323
+ }
324
+ </script>
325
+
326
+ <style>
327
+ @import './capital-gains-tax-scenario-calculator.css';
328
+ </style>
@@ -0,0 +1,26 @@
1
+ import type { FinanceToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { CapitalGainsTaxScenarioCalculatorUI } from './ui';
3
+
4
+ export type CapitalGainsTaxScenarioCalculatorLocaleContent = ToolLocaleContent<CapitalGainsTaxScenarioCalculatorUI>;
5
+
6
+ export const capitalGainsTaxScenarioCalculator: FinanceToolEntry<CapitalGainsTaxScenarioCalculatorUI> = {
7
+ id: 'capital-gains-tax-scenario-calculator',
8
+ icons: { bg: 'mdi:chart-timeline-variant', fg: 'mdi:cash-check' },
9
+ i18n: {
10
+ de: () => import('./i18n/de').then((m) => m.content),
11
+ en: () => import('./i18n/en').then((m) => m.content),
12
+ es: () => import('./i18n/es').then((m) => m.content),
13
+ fr: () => import('./i18n/fr').then((m) => m.content),
14
+ id: () => import('./i18n/id').then((m) => m.content),
15
+ it: () => import('./i18n/it').then((m) => m.content),
16
+ ja: () => import('./i18n/ja').then((m) => m.content),
17
+ ko: () => import('./i18n/ko').then((m) => m.content),
18
+ nl: () => import('./i18n/nl').then((m) => m.content),
19
+ pl: () => import('./i18n/pl').then((m) => m.content),
20
+ pt: () => import('./i18n/pt').then((m) => m.content),
21
+ ru: () => import('./i18n/ru').then((m) => m.content),
22
+ sv: () => import('./i18n/sv').then((m) => m.content),
23
+ tr: () => import('./i18n/tr').then((m) => m.content),
24
+ zh: () => import('./i18n/zh').then((m) => m.content),
25
+ },
26
+ };
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('de');
@@ -0,0 +1,181 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { bibliography } from '../bibliography';
3
+ import type { CapitalGainsTaxScenarioCalculatorUI } from '../ui';
4
+
5
+ export const ui: CapitalGainsTaxScenarioCalculatorUI = {
6
+ labelPurchasePrice: 'Purchase price',
7
+ labelAcquisitionCosts: 'Acquisition costs',
8
+ labelImprovements: 'Improvements',
9
+ labelSalePrice: 'Current sale price',
10
+ labelSellingCosts: 'Current selling costs',
11
+ labelTaxRate: 'Estimated tax rate',
12
+ labelAlternativeSalePrice: 'Alternative sale price',
13
+ labelAlternativeSellingCosts: 'Alternative selling costs',
14
+ labelCurrency: 'Currency',
15
+ labelCurrentScenario: 'Current scenario',
16
+ labelAlternativeScenario: 'Alternative scenario',
17
+ labelAcquisitionBasis: 'Estimated acquisition basis',
18
+ labelNetSaleValue: 'Net sale value',
19
+ labelGainOrLoss: 'Gain or loss',
20
+ labelEstimatedTax: 'Estimated tax',
21
+ labelNetGainAfterTax: 'Estimated gain after tax',
22
+ labelNetProceeds: 'Estimated proceeds after tax',
23
+ labelBetterScenario: 'Scenario with more estimated gain',
24
+ labelCurrentIsBetter: 'Current sale',
25
+ labelAlternativeIsBetter: 'Alternative sale',
26
+ labelSameOutcome: 'Same result',
27
+ labelGainStatus: 'Estimated gain',
28
+ labelLossStatus: 'Estimated loss',
29
+ labelBreakEvenStatus: 'Estimated break even',
30
+ labelTaxableGainNote: 'Tax is applied only to a positive gain in this planning model.',
31
+ labelLossNote: 'No estimated tax is applied to this loss. Local rules may treat losses differently.',
32
+ labelVisual: 'Comparison of estimated gain after tax for both scenarios',
33
+ labelComparison: 'Compare scenarios',
34
+ labelDifference: 'Difference',
35
+ labelCopy: 'Copy summary',
36
+ labelCopied: 'Summary copied',
37
+ labelReset: 'Reset example',
38
+ labelInvalidInput: 'Check this value',
39
+ labelMethodTitle: 'How this estimate works',
40
+ labelMethodText: 'The estimated acquisition basis is purchase price plus acquisition costs and improvements. Net sale value is sale price minus selling costs. The tool subtracts the basis from net sale value, then applies the percentage you enter to a positive gain only. It does not look up a tax rate.',
41
+ labelPrivacyNote: 'Your figures stay in this browser. No account, API or server calculation is required.',
42
+ labelAmountPlaceholder: '0.00',
43
+ labelPercentPlaceholder: '0 to 100',
44
+ };
45
+
46
+ const slug = 'capital-gains-tax-scenario-calculator';
47
+ const title = 'Capital Gains Tax Scenario Calculator';
48
+ const description = 'Compare the estimated gain after tax from two sale scenarios using your own purchase price, costs, improvements and tax rate. The calculation is a transparent planning estimate, not a tax filing.';
49
+
50
+ const faq = [
51
+ {
52
+ question: 'What does this capital gains scenario calculator estimate?',
53
+ answer: 'It estimates the gain or loss from a sale, the positive gain used as the taxable amount in this model, the tax produced by the percentage you enter, and the remaining gain after that estimate. It compares a current scenario with an alternative sale price and selling cost.',
54
+ },
55
+ {
56
+ question: 'Which costs can I enter?',
57
+ answer: 'Enter the purchase price, acquisition costs and improvements that you want included in the estimated acquisition basis. Enter selling costs separately because they reduce the net sale value. Only include amounts relevant to your own transaction and keep your supporting records.',
58
+ },
59
+ {
60
+ question: 'Does the calculator know my local capital gains tax rate?',
61
+ answer: 'No. You enter the percentage as a planning assumption. Tax treatment can depend on jurisdiction, asset type, ownership, dates, exemptions, prior losses, depreciation and other facts, so the result is not a tax assessment.',
62
+ },
63
+ {
64
+ question: 'Why is estimated tax zero when the result is a loss?',
65
+ answer: 'This simple comparison applies the entered rate only to a positive gain. It does not decide whether a loss can be offset, carried forward or reported under the rules that apply to you.',
66
+ },
67
+ {
68
+ question: 'Can I use the result for a tax return?',
69
+ answer: 'No. Use it to compare your own scenarios and prepare questions for a tax authority or qualified adviser. It does not include every adjustment, special regime or reporting rule.',
70
+ },
71
+ ];
72
+
73
+ const howTo = [
74
+ {
75
+ name: 'Enter the acquisition figures',
76
+ text: 'Add what you paid, eligible acquisition costs and improvements that you want to test as part of the estimated basis.',
77
+ },
78
+ {
79
+ name: 'Enter the current sale scenario',
80
+ text: 'Provide the current sale price and selling costs. The tool subtracts selling costs before comparing the sale with the estimated basis.',
81
+ },
82
+ {
83
+ name: 'Enter your planning rate',
84
+ text: 'Type the percentage you want to test. It is an assumption supplied by you, not a rate retrieved by the tool.',
85
+ },
86
+ {
87
+ name: 'Test the alternative',
88
+ text: 'Change the alternative sale price or its selling costs and read which scenario leaves the larger estimated gain after the entered percentage.',
89
+ },
90
+ ];
91
+
92
+ export const content: ToolLocaleContent<CapitalGainsTaxScenarioCalculatorUI> = {
93
+ slug,
94
+ title,
95
+ description,
96
+ ui,
97
+ seo: [
98
+ { type: 'title', text: 'Compare two sale decisions with your own numbers', level: 2 },
99
+ {
100
+ type: 'paragraph',
101
+ html: '<p>A sale decision often turns on more than the headline price. Acquisition costs, improvements and selling costs change the amount left after the transaction. This calculator lets you compare a current sale with an alternative using figures you already own and a tax percentage you choose for planning.</p>',
102
+ },
103
+ { type: 'title', text: 'What the estimate includes', level: 2 },
104
+ {
105
+ type: 'list',
106
+ items: [
107
+ 'Estimated acquisition basis: purchase price plus acquisition costs and improvements.',
108
+ 'Net sale value: sale price minus selling costs.',
109
+ 'Gain or loss: net sale value minus estimated acquisition basis.',
110
+ 'Estimated tax: your entered percentage applied to a positive gain in the model.',
111
+ 'Comparison: the difference in estimated gain after tax between the two scenarios.',
112
+ ],
113
+ },
114
+ { type: 'title', text: 'How to interpret the two scenarios', level: 2 },
115
+ {
116
+ type: 'paragraph',
117
+ html: '<p>Use the current scenario for the sale you are considering now. Use the alternative fields to test a different sale price or a different selling cost. A higher price does not automatically mean a larger after tax gain if the associated selling costs also rise; the comparison makes that trade off visible.</p>',
118
+ },
119
+ {
120
+ type: 'comparative',
121
+ columns: 2,
122
+ items: [
123
+ {
124
+ title: 'Current scenario',
125
+ description: 'Your first set of sale assumptions.',
126
+ points: ['Uses the current sale price', 'Subtracts current selling costs', 'Shows the estimated gain after tax'],
127
+ },
128
+ {
129
+ title: 'Alternative scenario',
130
+ description: 'A second set of sale assumptions.',
131
+ points: ['Tests a different sale price', 'Allows different selling costs', 'Shows the difference immediately'],
132
+ highlight: true,
133
+ },
134
+ ],
135
+ },
136
+ { type: 'title', text: 'Important limits of a capital gains estimate', level: 2 },
137
+ {
138
+ type: 'paragraph',
139
+ html: '<p>This is a transparent planning model, not a jurisdiction specific tax calculator. It does not look up rates or decide which costs qualify. It does not model depreciation, exemptions, inflation adjustments, previous losses, ownership splits, special regimes or separate allocation of improvements. Confirm the applicable rules and keep transaction records before filing.</p>',
140
+ },
141
+ { type: 'title', text: 'A private, local comparison', level: 2 },
142
+ {
143
+ type: 'paragraph',
144
+ html: '<p>The figures are entered by you and processed in the browser. That makes the tool useful for a quick what if comparison when a spreadsheet would be overkill, while keeping the assumptions visible so you can take the result into a more complete tax workflow.</p>',
145
+ },
146
+ ],
147
+ faq,
148
+ bibliography,
149
+ howTo,
150
+ schemas: [
151
+ {
152
+ '@context': 'https://schema.org',
153
+ '@type': 'FAQPage',
154
+ mainEntity: faq.map((item) => ({
155
+ '@type': 'Question',
156
+ name: item.question,
157
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
158
+ })),
159
+ },
160
+ {
161
+ '@context': 'https://schema.org',
162
+ '@type': 'HowTo',
163
+ name: title,
164
+ step: howTo.map((step, index) => ({
165
+ '@type': 'HowToStep',
166
+ position: index + 1,
167
+ name: step.name,
168
+ text: step.text,
169
+ })),
170
+ },
171
+ {
172
+ '@context': 'https://schema.org',
173
+ '@type': 'SoftwareApplication',
174
+ name: title,
175
+ description,
176
+ applicationCategory: 'FinanceApplication',
177
+ operatingSystem: 'All',
178
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
179
+ },
180
+ ],
181
+ };
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('es');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('fr');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('id');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('it');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('ja');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('ko');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('nl');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('pl');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('pt');
@@ -0,0 +1,3 @@
1
+ import { getCapitalGainsContent } from './shared';
2
+
3
+ export const content = getCapitalGainsContent('ru');