@jjlmoya/utils-finance 1.1.0 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-finance",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,16 +1,36 @@
1
1
  ---
2
- import { Icon } from 'astro-icon/components';
3
- import type { KnownLocale } from '../../types';
2
+ import { Icon } from "astro-icon/components";
4
3
 
5
4
  interface Props {
6
- locale?: KnownLocale;
7
- ui?: Record<string, unknown>;
5
+ ui?: Record<string, unknown>;
8
6
  }
9
7
 
10
8
  const { ui } = Astro.props;
11
9
  ---
12
10
 
13
- <div class="snow-container" id="debt-calc-container" data-currency-symbol={ui.currencySymbol} data-currency-code={ui.currencyCode} data-currency-locale={ui.currencyLocale} data-clear-confirm={ui.labelClearDataConfirm}>
11
+ <div
12
+ class="snow-container"
13
+ id="debt-calc-container"
14
+ data-currency-symbol={ui.currencySymbol}
15
+ data-currency-code={ui.currencyCode}
16
+ data-currency-locale={ui.currencyLocale}
17
+ data-clear-confirm={ui.labelClearDataConfirm}
18
+ data-label-debt-infinite={ui.labelDebtInfinite}
19
+ data-label-priority={ui.labelPriority}
20
+ data-label-step={ui.labelStep}
21
+ data-label-monthly-payment={ui.labelMonthlyPayment}
22
+ data-label-free={ui.labelFree}
23
+ data-label-active-method={ui.labelActiveMethod}
24
+ data-label-date={ui.labelDate}
25
+ data-label-starting-balance={ui.labelStartingBalance}
26
+ data-label-interest={ui.labelInterest}
27
+ data-label-amortization={ui.labelAmortization}
28
+ data-label-total-month={ui.labelTotalMonth}
29
+ data-label-remaining={ui.labelRemaining}
30
+ data-label-delete-debt={ui.labelDeleteDebt}
31
+ data-label-snowball-method={ui.labelSnowballMethod}
32
+ data-label-avalanche-method={ui.labelAvalancheMethod}
33
+ >
14
34
  <div class="snow-main-card">
15
35
  <aside class="snow-sidebar">
16
36
  <h2 class="snow-title">
@@ -19,46 +39,94 @@ const { ui } = Astro.props;
19
39
  </h2>
20
40
 
21
41
  <div class="snow-input-group">
22
- <label class="snow-label" for="extra-budget">{ui.labelExtraBudget}</label>
42
+ <label class="snow-label" for="extra-budget"
43
+ >{ui.labelExtraBudget}</label
44
+ >
23
45
  <div class="snow-input-wrapper">
24
- <input type="number" id="extra-budget" class="snow-input" value="100" min="0" step="50" />
46
+ <input
47
+ type="number"
48
+ id="extra-budget"
49
+ class="snow-input"
50
+ value="100"
51
+ min="0"
52
+ step="50"
53
+ />
25
54
  <span class="snow-input-unit">{ui.currencySymbol}</span>
26
55
  </div>
27
56
  <p class="snow-help-text">{ui.labelExtraBudgetHelp}</p>
28
57
  </div>
29
58
 
30
59
  <div class="snow-method-tabs" id="method-switcher">
31
- <button class="snow-tab active" data-method="snowball">{ui.labelSnowballMethod}</button>
32
- <button class="snow-tab" data-method="avalanche">{ui.labelAvalancheMethod}</button>
60
+ <button class="snow-tab active" data-method="snowball"
61
+ >{ui.labelSnowballMethod}</button
62
+ >
63
+ <button class="snow-tab" data-method="avalanche"
64
+ >{ui.labelAvalancheMethod}</button
65
+ >
33
66
  </div>
34
67
 
35
68
  <div class="snow-adder-card">
36
69
  <span class="snow-section-label">{ui.labelAddNewDebt}</span>
37
70
  <div class="snow-adder-content">
38
71
  <div class="snow-input-group">
39
- <input type="text" id="debt-name" class="snow-input" placeholder={ui.labelDebtNamePlaceholder} />
72
+ <input
73
+ type="text"
74
+ id="debt-name"
75
+ class="snow-input"
76
+ placeholder={ui.labelDebtNamePlaceholder}
77
+ />
40
78
  </div>
41
79
  <div class="snow-grid-2">
42
80
  <div class="snow-input-group">
43
- <label class="snow-label" for="debt-balance">{ui.labelDebtBalance}</label>
81
+ <label class="snow-label" for="debt-balance"
82
+ >{ui.labelDebtBalance}</label
83
+ >
44
84
  <div class="snow-input-wrapper">
45
- <input type="number" id="debt-balance" class="snow-input" placeholder="0" min="0" />
46
- <span class="snow-input-unit">{ui.currencySymbol}</span>
85
+ <input
86
+ type="number"
87
+ id="debt-balance"
88
+ class="snow-input"
89
+ placeholder="0"
90
+ min="0"
91
+ />
92
+ <span class="snow-input-unit"
93
+ >{ui.currencySymbol}</span
94
+ >
47
95
  </div>
48
96
  </div>
49
97
  <div class="snow-input-group">
50
- <label class="snow-label" for="debt-min-payment">{ui.labelMinPayment}</label>
98
+ <label class="snow-label" for="debt-min-payment"
99
+ >{ui.labelMinPayment}</label
100
+ >
51
101
  <div class="snow-input-wrapper">
52
- <input type="number" id="debt-min-payment" class="snow-input" placeholder="0" min="0" />
53
- <span class="snow-input-unit">{ui.currencySymbol}/mes</span>
102
+ <input
103
+ type="number"
104
+ id="debt-min-payment"
105
+ class="snow-input"
106
+ placeholder="0"
107
+ min="0"
108
+ />
109
+ <span class="snow-input-unit"
110
+ >{ui.currencySymbol}/mes</span
111
+ >
54
112
  </div>
55
113
  </div>
56
114
  </div>
57
115
  <div class="snow-input-group">
58
- <label class="snow-label" for="debt-rate">{ui.labelInterestRate}</label>
116
+ <label class="snow-label" for="debt-rate"
117
+ >{ui.labelInterestRate}</label
118
+ >
59
119
  <div class="snow-input-wrapper">
60
- <input type="number" id="debt-rate" class="snow-input" value="15" step="0.1" />
61
- <span class="snow-input-unit">% {ui.labelInterestHelp}</span>
120
+ <input
121
+ type="number"
122
+ id="debt-rate"
123
+ class="snow-input"
124
+ value="15"
125
+ step="0.1"
126
+ />
127
+ <span class="snow-input-unit"
128
+ >% {ui.labelInterestHelp}</span
129
+ >
62
130
  </div>
63
131
  </div>
64
132
  <p class="snow-input-help">{ui.labelAddNewDebt}</p>
@@ -81,12 +149,16 @@ const { ui } = Astro.props;
81
149
  <Icon name="mdi:bank-off-outline" size={48} />
82
150
  </div>
83
151
  <h3 class="snow-empty-title">{ui.labelEmptyStateTitle}</h3>
84
- <p class="snow-empty-description">{ui.labelEmptyStateDescription}</p>
152
+ <p class="snow-empty-description">
153
+ {ui.labelEmptyStateDescription}
154
+ </p>
85
155
  </div>
86
156
 
87
157
  <div id="results-content" class="snow-hidden">
88
158
  <div class="snow-inventory-area">
89
- <span class="snow-section-label">{ui.labelDebtInventory}</span>
159
+ <span class="snow-section-label"
160
+ >{ui.labelDebtInventory}</span
161
+ >
90
162
  <div class="snow-mini-table">
91
163
  <div class="snow-table-row snow-table-header">
92
164
  <span>{ui.labelDebtName}</span>
@@ -101,46 +173,70 @@ const { ui } = Astro.props;
101
173
 
102
174
  <div class="snow-results-hero">
103
175
  <div class="snow-hero-main">
104
- <span class="snow-hero-label">{ui.labelFreedomDate}</span>
105
- <div class="snow-hero-value" id="summary-date">ENERO 2026</div>
176
+ <span class="snow-hero-label"
177
+ >{ui.labelFreedomDate}</span
178
+ >
179
+ <div class="snow-hero-value" id="summary-date">
180
+ ENERO 2026
181
+ </div>
106
182
  </div>
107
183
  <div class="snow-hero-badge">
108
184
  <Icon name="mdi:check-decagram" size={20} />
109
- <span id="active-method-label">{ui.labelSnowballMethod}</span>
185
+ <span id="active-method-label"
186
+ >{ui.labelSnowballMethod}</span
187
+ >
110
188
  </div>
111
189
  </div>
112
190
 
113
191
  <div class="snow-grid-stats">
114
192
  <div class="snow-stat-box">
115
193
  <div class="snow-stat-value" id="stat-months">--</div>
116
- <div class="snow-stat-label">{ui.labelMonthsRemaining}</div>
194
+ <div class="snow-stat-label">
195
+ {ui.labelMonthsRemaining}
196
+ </div>
117
197
  </div>
118
198
  <div class="snow-stat-box">
119
199
  <div class="snow-stat-value" id="stat-interest">--</div>
120
- <div class="snow-stat-label">{ui.labelTotalInterest}</div>
200
+ <div class="snow-stat-label">
201
+ {ui.labelTotalInterest}
202
+ </div>
121
203
  </div>
122
204
  <div class="snow-stat-box">
123
- <div class="snow-stat-value" id="stat-total-paid">--</div>
205
+ <div class="snow-stat-value" id="stat-total-paid">
206
+ --
207
+ </div>
124
208
  <div class="snow-stat-label">{ui.labelTotalPaid}</div>
125
209
  </div>
126
210
  <div class="snow-stat-box snow-stat-highlight">
127
- <div class="snow-stat-value" id="stat-extra-saved">--</div>
128
- <div class="snow-stat-label">{ui.labelInterestSaved}</div>
211
+ <div class="snow-stat-value" id="stat-extra-saved">
212
+ --
213
+ </div>
214
+ <div class="snow-stat-label">
215
+ {ui.labelInterestSaved}
216
+ </div>
129
217
  </div>
130
218
  </div>
131
219
 
132
220
  <div class="snow-action-plan">
133
221
  <div class="snow-action-header">
134
- <h4 class="snow-action-title">{ui.labelYourStrategy}</h4>
135
- <div class="snow-action-help">{ui.labelStrategyHelp}</div>
222
+ <h4 class="snow-action-title">
223
+ {ui.labelYourStrategy}
224
+ </h4>
225
+ <div class="snow-action-help">
226
+ {ui.labelStrategyHelp}
227
+ </div>
136
228
  </div>
137
229
  <div id="action-plan-list" class="snow-action-list"></div>
138
230
  </div>
139
231
 
140
232
  <div class="snow-table-container">
141
233
  <div class="snow-table-header-action">
142
- <h4 class="snow-table-title">{ui.labelDetailedAmortization}</h4>
143
- <button id="export-csv" class="snow-btn-export">{ui.labelExportCSV}</button>
234
+ <h4 class="snow-table-title">
235
+ {ui.labelDetailedAmortization}
236
+ </h4>
237
+ <button id="export-csv" class="snow-btn-export"
238
+ >{ui.labelExportCSV}</button
239
+ >
144
240
  </div>
145
241
  <div class="snow-table-overlay">
146
242
  <div class="snow-table-scroll">
@@ -180,7 +276,7 @@ const { ui } = Astro.props;
180
276
  }
181
277
 
182
278
  :global(.theme-dark),
183
- :global([data-theme='dark']) {
279
+ :global([data-theme="dark"]) {
184
280
  --snow-bg-card: rgba(15, 23, 42, 0.9);
185
281
  --snow-border: rgba(30, 41, 59, 1);
186
282
  --snow-text-main: #f8fafc;
@@ -323,7 +419,7 @@ const { ui } = Astro.props;
323
419
  }
324
420
 
325
421
  :global(.theme-dark) .snow-tab.active,
326
- :global([data-theme='dark']) .snow-tab.active {
422
+ :global([data-theme="dark"]) .snow-tab.active {
327
423
  background: rgba(255, 255, 255, 0.1);
328
424
  color: white;
329
425
  }
@@ -365,7 +461,11 @@ const { ui } = Astro.props;
365
461
  }
366
462
 
367
463
  .snow-btn-add {
368
- background: linear-gradient(135deg, var(--snow-emerald), var(--snow-blue));
464
+ background: linear-gradient(
465
+ 135deg,
466
+ var(--snow-emerald),
467
+ var(--snow-blue)
468
+ );
369
469
  color: white;
370
470
  }
371
471
 
@@ -467,7 +567,7 @@ const { ui } = Astro.props;
467
567
  overflow-y: auto;
468
568
  }
469
569
 
470
- .snow-debt-item {
570
+ :global(.snow-debt-item) {
471
571
  display: grid;
472
572
  grid-template-columns: 1fr 1fr 1fr 1fr 60px;
473
573
  padding: 1rem;
@@ -476,7 +576,7 @@ const { ui } = Astro.props;
476
576
  gap: 1rem;
477
577
  }
478
578
 
479
- .snow-debt-remove {
579
+ :global(.snow-debt-remove) {
480
580
  background: none;
481
581
  border: none;
482
582
  color: var(--snow-rose);
@@ -487,12 +587,16 @@ const { ui } = Astro.props;
487
587
  transition: opacity 0.3s ease;
488
588
  }
489
589
 
490
- .snow-debt-remove:hover {
590
+ :global(.snow-debt-remove:hover) {
491
591
  opacity: 1;
492
592
  }
493
593
 
494
594
  .snow-results-hero {
495
- background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(59, 130, 246, 0.1));
595
+ background: linear-gradient(
596
+ 135deg,
597
+ rgba(16, 185, 129, 0.1),
598
+ rgba(59, 130, 246, 0.1)
599
+ );
496
600
  border: 1.5px solid var(--snow-border);
497
601
  border-radius: 1.75rem;
498
602
  padding: 3rem;
@@ -609,7 +713,7 @@ const { ui } = Astro.props;
609
713
  gap: 1rem;
610
714
  }
611
715
 
612
- .snow-plan-item {
716
+ :global(.snow-plan-item) {
613
717
  display: grid;
614
718
  grid-template-columns: 50px 1fr 1fr;
615
719
  gap: 1.5rem;
@@ -620,12 +724,12 @@ const { ui } = Astro.props;
620
724
  border-radius: 1rem;
621
725
  }
622
726
 
623
- .snow-plan-item.active-step {
727
+ :global(.snow-plan-item.active-step) {
624
728
  background: rgba(16, 185, 129, 0.05);
625
729
  border-color: rgba(16, 185, 129, 0.3);
626
730
  }
627
731
 
628
- .snow-step-marker {
732
+ :global(.snow-step-marker) {
629
733
  width: 50px;
630
734
  height: 50px;
631
735
  background: var(--snow-emerald);
@@ -638,13 +742,13 @@ const { ui } = Astro.props;
638
742
  font-size: 1.25rem;
639
743
  }
640
744
 
641
- .snow-step-body {
745
+ :global(.snow-step-body) {
642
746
  display: flex;
643
747
  flex-direction: column;
644
748
  gap: 0.25rem;
645
749
  }
646
750
 
647
- .snow-step-label {
751
+ :global(.snow-step-label) {
648
752
  font-size: 0.65rem;
649
753
  font-weight: 900;
650
754
  text-transform: uppercase;
@@ -652,13 +756,13 @@ const { ui } = Astro.props;
652
756
  opacity: 0.6;
653
757
  }
654
758
 
655
- .snow-step-name {
759
+ :global(.snow-step-name) {
656
760
  font-size: 1rem;
657
761
  font-weight: 900;
658
762
  color: var(--snow-text-main);
659
763
  }
660
764
 
661
- .snow-step-amount {
765
+ :global(.snow-step-amount) {
662
766
  display: flex;
663
767
  flex-direction: column;
664
768
  align-items: flex-end;
@@ -666,13 +770,13 @@ const { ui } = Astro.props;
666
770
  text-align: right;
667
771
  }
668
772
 
669
- .snow-step-amount span {
773
+ :global(.snow-step-amount span) {
670
774
  font-size: 0.65rem;
671
775
  opacity: 0.6;
672
776
  text-transform: uppercase;
673
777
  }
674
778
 
675
- .snow-step-amount strong {
779
+ :global(.snow-step-amount strong) {
676
780
  font-size: 1.25rem;
677
781
  font-weight: 900;
678
782
  color: var(--snow-emerald);
@@ -746,21 +850,21 @@ const { ui } = Astro.props;
746
850
  border-bottom: 1px solid var(--snow-border);
747
851
  }
748
852
 
749
- table td {
853
+ :global(table td) {
750
854
  padding: 1rem;
751
855
  border-bottom: 1px solid var(--snow-border);
752
856
  }
753
857
 
754
- table tr:last-child td {
858
+ :global(table tr:last-child td) {
755
859
  border-bottom: none;
756
860
  }
757
861
 
758
- .snow-table-free {
862
+ :global(.snow-table-free) {
759
863
  color: var(--snow-emerald);
760
864
  font-weight: 900;
761
865
  }
762
866
 
763
- .snow-table-muted {
867
+ :global(.snow-table-muted) {
764
868
  opacity: 0.6;
765
869
  }
766
870
 
@@ -785,7 +889,7 @@ const { ui } = Astro.props;
785
889
  .snow-table-header {
786
890
  display: none;
787
891
  }
788
- .snow-debt-item {
892
+ :global(.snow-debt-item) {
789
893
  grid-template-columns: 1fr;
790
894
  padding: 1rem 0;
791
895
  border-left: 4px solid var(--snow-emerald);
@@ -795,18 +899,56 @@ const { ui } = Astro.props;
795
899
  </style>
796
900
 
797
901
  <script>
798
- import { DebtSnowballLogic, type Debt, type SimulationResult } from './logic';
799
- import { format, addMonths } from 'date-fns';
800
- import { es } from 'date-fns/locale';
801
-
802
- const container = document.getElementById('debt-calc-container');
803
- const currencyLocale = container?.getAttribute('data-currency-locale') || 'es-ES';
804
- const currencyCode = container?.getAttribute('data-currency-code') || 'EUR';
805
- const clearConfirm = container?.getAttribute('data-clear-confirm') || '¿Vaciar plan?';
902
+ import {
903
+ DebtSnowballLogic,
904
+ type Debt,
905
+ type SimulationResult,
906
+ } from "./logic";
907
+ import { format, addMonths } from "date-fns";
908
+ import { es, enUS, fr, type Locale } from "date-fns/locale";
909
+
910
+ const container = document.getElementById("debt-calc-container");
911
+ const currencyLocale =
912
+ container?.getAttribute("data-currency-locale") || "es-ES";
913
+ const currencyCode = container?.getAttribute("data-currency-code") || "EUR";
914
+ const clearConfirm =
915
+ container?.getAttribute("data-clear-confirm") || "¿Vaciar plan?";
916
+ const labelDebtInfinite =
917
+ container?.getAttribute("data-label-debt-infinite") || "DEUDA INFINITA";
918
+ const labelPriority =
919
+ container?.getAttribute("data-label-priority") || "PRIORIDAD MÁXIMA";
920
+ const labelStep = container?.getAttribute("data-label-step") || "PASO";
921
+ const labelMonthlyPayment =
922
+ container?.getAttribute("data-label-monthly-payment") || "Pago mensual";
923
+ const labelFree = container?.getAttribute("data-label-free") || "LIBRE";
924
+ const labelActiveMethod =
925
+ container?.getAttribute("data-label-active-method") || "MÉTODO";
926
+ const labelDate = container?.getAttribute("data-label-date") || "Fecha";
927
+ const labelStartingBalance =
928
+ container?.getAttribute("data-label-starting-balance") ||
929
+ "Saldo Inicial";
930
+ const labelInterestCsv =
931
+ container?.getAttribute("data-label-interest") || "Interes";
932
+ const labelAmortization =
933
+ container?.getAttribute("data-label-amortization") || "Amortizacion";
934
+ const labelTotalMonth =
935
+ container?.getAttribute("data-label-total-month") || "Pago Total";
936
+ const labelRemaining =
937
+ container?.getAttribute("data-label-remaining") || "Saldo Final";
938
+ const labelDeleteDebt =
939
+ container?.getAttribute("data-label-delete-debt") || "Eliminar";
940
+ const labelSnowballMethod =
941
+ container?.getAttribute("data-label-snowball-method") || "SNOWBALL";
942
+ const labelAvalancheMethod =
943
+ container?.getAttribute("data-label-avalanche-method") || "AVALANCHE";
944
+
945
+ const localeKey = document.documentElement.lang || "es";
946
+ const dateLocales: Record<string, Locale> = { es, en: enUS, fr };
947
+ const dateLocale = dateLocales[localeKey] ?? es;
806
948
 
807
949
  function formatCurrency(val: number): string {
808
950
  return new Intl.NumberFormat(currencyLocale, {
809
- style: 'currency',
951
+ style: "currency",
810
952
  currency: currencyCode,
811
953
  maximumFractionDigits: 0,
812
954
  }).format(val);
@@ -815,7 +957,7 @@ const { ui } = Astro.props;
815
957
  class SnowballEngine {
816
958
  private debts: Debt[] = [];
817
959
  private extra: number = 100;
818
- private method: 'snowball' | 'avalanche' = 'snowball';
960
+ private method: "snowball" | "avalanche" = "snowball";
819
961
 
820
962
  constructor() {
821
963
  this.load();
@@ -826,42 +968,49 @@ const { ui } = Astro.props;
826
968
  private bind() {
827
969
  const get = (id: string) => document.getElementById(id);
828
970
 
829
- get('add-debt-btn')?.addEventListener('click', () => this.add());
971
+ get("add-debt-btn")?.addEventListener("click", () => this.add());
830
972
 
831
- const extraInput = get('extra-budget') as HTMLInputElement;
832
- extraInput?.addEventListener('input', (e) => {
973
+ const extraInput = get("extra-budget") as HTMLInputElement;
974
+ extraInput?.addEventListener("input", (e) => {
833
975
  const val = parseFloat((e.target as HTMLInputElement).value);
834
976
  this.extra = isNaN(val) ? 0 : val;
835
977
  this.save();
836
978
  });
837
979
 
838
- document.querySelectorAll('.snow-tab').forEach((tab) => {
839
- tab.addEventListener('click', () => {
840
- document.querySelectorAll('.snow-tab').forEach((t) => t.classList.remove('active'));
841
- tab.classList.add('active');
842
- this.method = (tab as HTMLElement).dataset.method as 'snowball' | 'avalanche' || 'snowball';
980
+ document.querySelectorAll(".snow-tab").forEach((tab) => {
981
+ tab.addEventListener("click", () => {
982
+ document
983
+ .querySelectorAll(".snow-tab")
984
+ .forEach((t) => t.classList.remove("active"));
985
+ tab.classList.add("active");
986
+ this.method =
987
+ ((tab as HTMLElement).dataset.method as
988
+ | "snowball"
989
+ | "avalanche") || "snowball";
843
990
  this.render();
844
991
  });
845
992
  });
846
993
 
847
- get('clear-data')?.addEventListener('click', () => {
994
+ get("clear-data")?.addEventListener("click", () => {
848
995
  if (confirm(clearConfirm)) {
849
996
  this.debts = [];
850
997
  this.save();
851
998
  }
852
999
  });
853
1000
 
854
- get('export-csv')?.addEventListener('click', () => this.export());
1001
+ get("export-csv")?.addEventListener("click", () => this.export());
855
1002
  }
856
1003
 
857
1004
  private load() {
858
1005
  try {
859
- const d = localStorage.getItem('snow_debts_v2');
1006
+ const d = localStorage.getItem("snow_debts_v2");
860
1007
  if (d) this.debts = JSON.parse(d);
861
- const e = localStorage.getItem('snow_extra_v2');
1008
+ const e = localStorage.getItem("snow_extra_v2");
862
1009
  if (e) {
863
1010
  this.extra = parseFloat(e);
864
- const input = document.getElementById('extra-budget') as HTMLInputElement;
1011
+ const input = document.getElementById(
1012
+ "extra-budget",
1013
+ ) as HTMLInputElement;
865
1014
  if (input) input.value = e;
866
1015
  }
867
1016
  } catch {
@@ -870,29 +1019,52 @@ const { ui } = Astro.props;
870
1019
  }
871
1020
 
872
1021
  private save() {
873
- localStorage.setItem('snow_debts_v2', JSON.stringify(this.debts));
874
- localStorage.setItem('snow_extra_v2', this.extra.toString());
1022
+ localStorage.setItem("snow_debts_v2", JSON.stringify(this.debts));
1023
+ localStorage.setItem("snow_extra_v2", this.extra.toString());
875
1024
  this.render();
876
1025
  }
877
1026
 
878
1027
  private getDebtInputs() {
879
1028
  return {
880
- nameEl: document.getElementById('debt-name') as HTMLInputElement,
881
- balEl: document.getElementById('debt-balance') as HTMLInputElement,
882
- minEl: document.getElementById('debt-min-payment') as HTMLInputElement,
883
- rateEl: document.getElementById('debt-rate') as HTMLInputElement,
1029
+ nameEl: document.getElementById(
1030
+ "debt-name",
1031
+ ) as HTMLInputElement,
1032
+ balEl: document.getElementById(
1033
+ "debt-balance",
1034
+ ) as HTMLInputElement,
1035
+ minEl: document.getElementById(
1036
+ "debt-min-payment",
1037
+ ) as HTMLInputElement,
1038
+ rateEl: document.getElementById(
1039
+ "debt-rate",
1040
+ ) as HTMLInputElement,
884
1041
  };
885
1042
  }
886
1043
 
887
- private clearDebtInputs(nameEl: HTMLInputElement, balEl: HTMLInputElement, minEl: HTMLInputElement) {
888
- balEl.value = '';
889
- minEl.value = '';
890
- nameEl.value = '';
1044
+ private clearDebtInputs(
1045
+ nameEl: HTMLInputElement,
1046
+ balEl: HTMLInputElement,
1047
+ minEl: HTMLInputElement,
1048
+ ) {
1049
+ balEl.value = "";
1050
+ minEl.value = "";
1051
+ nameEl.value = "";
891
1052
  }
892
1053
 
893
- private validateAndCreateDebt(b: number, m: number, name: string, rate: number): boolean {
1054
+ private validateAndCreateDebt(
1055
+ b: number,
1056
+ m: number,
1057
+ name: string,
1058
+ rate: number,
1059
+ ): boolean {
894
1060
  if (b > 0 && m > 0) {
895
- this.debts.push({ id: Math.random().toString(36).substring(2, 11), name, balance: b, minPayment: m, rate });
1061
+ this.debts.push({
1062
+ id: Math.random().toString(36).substring(2, 11),
1063
+ name,
1064
+ balance: b,
1065
+ minPayment: m,
1066
+ rate,
1067
+ });
896
1068
  return true;
897
1069
  }
898
1070
  return false;
@@ -904,7 +1076,7 @@ const { ui } = Astro.props;
904
1076
 
905
1077
  const b = parseFloat(balEl.value);
906
1078
  const m = parseFloat(minEl.value);
907
- const name = nameEl?.value || `Deuda ${this.debts.length + 1}`;
1079
+ const name = nameEl?.value || `D${this.debts.length + 1}`;
908
1080
  const rate = parseFloat(rateEl?.value) || 0;
909
1081
 
910
1082
  if (this.validateAndCreateDebt(b, m, name, rate)) {
@@ -914,24 +1086,36 @@ const { ui } = Astro.props;
914
1086
  }
915
1087
 
916
1088
  private render() {
917
- const snow = DebtSnowballLogic.simulate(this.debts, this.extra, 'snowball');
918
- const aval = DebtSnowballLogic.simulate(this.debts, this.extra, 'avalanche');
919
- const res = this.method === 'snowball' ? snow : aval;
1089
+ const snow = DebtSnowballLogic.simulate(
1090
+ this.debts,
1091
+ this.extra,
1092
+ "snowball",
1093
+ );
1094
+ const aval = DebtSnowballLogic.simulate(
1095
+ this.debts,
1096
+ this.extra,
1097
+ "avalanche",
1098
+ );
1099
+ const res = this.method === "snowball" ? snow : aval;
920
1100
  this.renderView(res, snow, aval);
921
1101
  }
922
1102
 
923
- private renderView(res: SimulationResult | null, snow: SimulationResult | null, aval: SimulationResult | null) {
924
- const noMsg = document.getElementById('no-debts-msg');
925
- const results = document.getElementById('results-content');
1103
+ private renderView(
1104
+ res: SimulationResult | null,
1105
+ snow: SimulationResult | null,
1106
+ aval: SimulationResult | null,
1107
+ ) {
1108
+ const noMsg = document.getElementById("no-debts-msg");
1109
+ const results = document.getElementById("results-content");
926
1110
 
927
1111
  if (!res || !snow || !aval) {
928
- noMsg?.classList.remove('snow-hidden');
929
- results?.classList.add('snow-hidden');
1112
+ noMsg?.classList.remove("snow-hidden");
1113
+ results?.classList.add("snow-hidden");
930
1114
  return;
931
1115
  }
932
1116
 
933
- noMsg?.classList.add('snow-hidden');
934
- results?.classList.remove('snow-hidden');
1117
+ noMsg?.classList.add("snow-hidden");
1118
+ results?.classList.remove("snow-hidden");
935
1119
  this.updateDisplay(res);
936
1120
  this.updateDebtList();
937
1121
  this.updateActionPlan();
@@ -939,109 +1123,134 @@ const { ui } = Astro.props;
939
1123
  }
940
1124
 
941
1125
  private updateDisplay(res: SimulationResult) {
942
- const dateEl = document.getElementById('summary-date');
1126
+ const dateEl = document.getElementById("summary-date");
943
1127
  if (dateEl) {
944
1128
  if (res.isGrowing) {
945
- dateEl.textContent = 'DEUDA INFINITA';
946
- dateEl.style.color = 'var(--snow-rose)';
1129
+ dateEl.textContent = labelDebtInfinite;
1130
+ dateEl.style.color = "var(--snow-rose)";
947
1131
  } else {
948
- dateEl.textContent = format(addMonths(new Date(), res.mo), 'MMMM yyyy', { locale: es }).toUpperCase();
949
- dateEl.style.color = 'var(--snow-emerald)';
1132
+ dateEl.textContent = format(
1133
+ addMonths(new Date(), res.mo),
1134
+ "MMMM yyyy",
1135
+ { locale: dateLocale },
1136
+ ).toUpperCase();
1137
+ dateEl.style.color = "var(--snow-emerald)";
950
1138
  }
951
1139
  }
952
1140
 
953
- const methEl = document.getElementById('active-method-label');
954
- if (methEl) methEl.textContent = `MÉTODO ${this.method.toUpperCase()}`;
1141
+ const methEl = document.getElementById("active-method-label");
1142
+ if (methEl) {
1143
+ const methodLabel = this.method === 'snowball' ? labelSnowballMethod : labelAvalancheMethod;
1144
+ methEl.textContent = `${labelActiveMethod} ${methodLabel}`;
1145
+ }
955
1146
 
956
- document.getElementById('stat-months')!.textContent = res.mo.toString();
957
- document.getElementById('stat-interest')!.textContent = formatCurrency(res.totalI);
958
- document.getElementById('stat-total-paid')!.textContent = formatCurrency(res.totalP);
1147
+ document.getElementById("stat-months")!.textContent =
1148
+ res.mo.toString();
1149
+ document.getElementById("stat-interest")!.textContent =
1150
+ formatCurrency(res.totalI);
1151
+ document.getElementById("stat-total-paid")!.textContent =
1152
+ formatCurrency(res.totalP);
959
1153
 
960
1154
  const baseSim = DebtSnowballLogic.simulateNoExtra(this.debts);
961
1155
  const saved = baseSim ? baseSim.totalI - res.totalI : 0;
962
- document.getElementById('stat-extra-saved')!.textContent = formatCurrency(Math.max(0, saved));
1156
+ document.getElementById("stat-extra-saved")!.textContent =
1157
+ formatCurrency(Math.max(0, saved));
963
1158
  }
964
1159
 
965
1160
  private updateDebtList() {
966
- const list = document.getElementById('debt-items-list');
1161
+ const list = document.getElementById("debt-items-list");
967
1162
  if (!list) return;
968
- list.innerHTML = '';
1163
+ list.innerHTML = "";
969
1164
  this.debts.forEach((d) => {
970
- const row = document.createElement('div');
971
- row.className = 'snow-debt-item';
1165
+ const row = document.createElement("div");
1166
+ row.className = "snow-debt-item";
972
1167
  row.innerHTML = `
973
1168
  <span>${d.name}</span>
974
1169
  <span>${formatCurrency(d.balance)}</span>
975
1170
  <span>${formatCurrency(d.minPayment)}</span>
976
1171
  <span>${d.rate}%</span>
977
- <button class="snow-debt-remove" title={ui.labelDeleteDebt}>&times;</button>
1172
+ <button class="snow-debt-remove" title="${labelDeleteDebt}">&times;</button>
978
1173
  `;
979
- row.querySelector('.snow-debt-remove')?.addEventListener('click', () => {
980
- this.debts = this.debts.filter((item) => item.id !== d.id);
981
- this.save();
982
- });
1174
+ row.querySelector(".snow-debt-remove")?.addEventListener(
1175
+ "click",
1176
+ () => {
1177
+ this.debts = this.debts.filter(
1178
+ (item) => item.id !== d.id,
1179
+ );
1180
+ this.save();
1181
+ },
1182
+ );
983
1183
  list.appendChild(row);
984
1184
  });
985
1185
  }
986
1186
 
987
1187
  private updateActionPlan() {
988
- const planList = document.getElementById('action-plan-list');
1188
+ const planList = document.getElementById("action-plan-list");
989
1189
  if (!planList) return;
990
1190
  const ordered = [...this.debts].sort((a, b) =>
991
- this.method === 'snowball' ? a.balance - b.balance : b.rate - a.rate
1191
+ this.method === "snowball"
1192
+ ? a.balance - b.balance
1193
+ : b.rate - a.rate,
992
1194
  );
993
1195
  let cumulativeExtra = this.extra;
994
1196
  planList.innerHTML = ordered
995
1197
  .map((d, index) => {
996
1198
  const totalForThis = d.minPayment + cumulativeExtra;
997
1199
  cumulativeExtra += d.minPayment;
998
- const stepLabel = index === 0 ? 'PRIORIDAD MÁXIMA' : `PASO ${index + 1}`;
1200
+ const stepLabel =
1201
+ index === 0
1202
+ ? labelPriority
1203
+ : `${labelStep} ${index + 1}`;
999
1204
  return `
1000
- <div class="snow-plan-item ${index === 0 ? 'active-step' : ''}">
1205
+ <div class="snow-plan-item ${index === 0 ? "active-step" : ""}">
1001
1206
  <div class="snow-step-marker">${index + 1}</div>
1002
1207
  <div class="snow-step-body">
1003
1208
  <span class="snow-step-label">${stepLabel}</span>
1004
1209
  <div class="snow-step-name">${d.name}</div>
1005
1210
  </div>
1006
1211
  <div class="snow-step-amount">
1007
- <span>Pago mensual</span>
1212
+ <span>${labelMonthlyPayment}</span>
1008
1213
  <strong>${formatCurrency(totalForThis)}</strong>
1009
1214
  </div>
1010
1215
  </div>
1011
1216
  `;
1012
1217
  })
1013
- .join('');
1218
+ .join("");
1014
1219
  }
1015
1220
 
1016
1221
  private updateProjectionTable(res: SimulationResult) {
1017
- const tbody = document.querySelector('#projection-table tbody');
1222
+ const tbody = document.querySelector("#projection-table tbody");
1018
1223
  if (!tbody) return;
1019
1224
  tbody.innerHTML = res.proj
1020
1225
  .map(
1021
1226
  (p) => `
1022
1227
  <tr>
1023
- <td>${format(addMonths(new Date(), p.mo), 'MMM yy', { locale: es }).toUpperCase()}</td>
1228
+ <td>${format(addMonths(new Date(), p.mo), "MMM yy", { locale: dateLocale }).toUpperCase()}</td>
1024
1229
  <td>${formatCurrency(p.iniB)}</td>
1025
1230
  <td>${formatCurrency(p.int)}</td>
1026
1231
  <td>${formatCurrency(p.princ)}</td>
1027
1232
  <td>${formatCurrency(p.total)}</td>
1028
- <td class="${p.finB < 1 ? 'snow-table-free' : 'snow-table-muted'}">${p.finB < 1 ? 'LIBRE' : formatCurrency(p.finB)}</td>
1233
+ <td class="${p.finB < 1 ? "snow-table-free" : "snow-table-muted"}">${p.finB < 1 ? labelFree : formatCurrency(p.finB)}</td>
1029
1234
  </tr>
1030
- `
1235
+ `,
1031
1236
  )
1032
- .join('');
1237
+ .join("");
1033
1238
  }
1034
1239
 
1035
1240
  private export() {
1036
- const res = DebtSnowballLogic.simulate(this.debts, this.extra, this.method);
1241
+ const res = DebtSnowballLogic.simulate(
1242
+ this.debts,
1243
+ this.extra,
1244
+ this.method,
1245
+ );
1037
1246
  if (!res) return;
1038
- let csv = '\uFEFFFecha,Saldo Inicial,Interes,Amortizacion,Pago Total,Saldo Final\n';
1247
+ let csv = `\uFEFF${labelDate},${labelStartingBalance},${labelInterestCsv},${labelAmortization},${labelTotalMonth},${labelRemaining}\n`;
1039
1248
  res.proj.forEach((p) => {
1040
- const d = format(addMonths(new Date(), p.mo), 'MM/yyyy');
1249
+ const d = format(addMonths(new Date(), p.mo), "MM/yyyy");
1041
1250
  csv += `${d},${p.iniB.toFixed(2)},${p.int.toFixed(2)},${p.princ.toFixed(2)},${p.total.toFixed(2)},${p.finB.toFixed(2)}\n`;
1042
1251
  });
1043
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
1044
- const link = document.createElement('a');
1252
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
1253
+ const link = document.createElement("a");
1045
1254
  link.href = URL.createObjectURL(blob);
1046
1255
  link.download = `plan_deuda_${this.method}.csv`;
1047
1256
  link.click();
@@ -11,7 +11,7 @@ const { ui } = Astro.props;
11
11
  const availableYears = InflationCalculator.getAvailableYears();
12
12
  ---
13
13
 
14
- <div class="inflation-container" data-currency-locale={ui.currencyLocale} data-currency-code={ui.currencyCode}>
14
+ <div class="inflation-container" data-currency-locale={ui.currencyLocale} data-currency-code={ui.currencyCode} data-label-inflation-accumulated={ui.labelInflationAccumulated}>
15
15
  <div class="inflation-header">
16
16
  <div class="inflation-header-title-group">
17
17
  <div class="inflation-header-icon">
@@ -78,6 +78,7 @@ const availableYears = InflationCalculator.getAvailableYears();
78
78
  const container = document.querySelector('.inflation-container') as HTMLElement;
79
79
  const currencyLocale = container.dataset.currencyLocale || 'es-ES';
80
80
  const currencyCode = container.dataset.currencyCode || 'EUR';
81
+ const labelInflationAccumulated = container.dataset.labelInflationAccumulated || 'Inflación Acumulada';
81
82
 
82
83
  const amountEl = document.getElementById('inflation-amount') as HTMLInputElement;
83
84
  const yearEl = document.getElementById('inflation-year') as HTMLSelectElement;
@@ -107,7 +108,7 @@ const availableYears = InflationCalculator.getAvailableYears();
107
108
  const sign = result.percentChange > 0 ? '+' : '';
108
109
  const span = percentEl.querySelector('span');
109
110
  if (span) {
110
- span.innerText = `${sign}${result.percentChange} % Inflación Acumulada`;
111
+ span.innerText = `${sign}${result.percentChange} % ${labelInflationAccumulated}`;
111
112
  }
112
113
  }
113
114
  }
@@ -2,7 +2,7 @@ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dt
2
2
  import type { ToolLocaleContent } from '../../../types';
3
3
  import type { LateInterestUI } from '../ui';
4
4
 
5
- const slug = 'intereses-demora';
5
+ const slug = 'calculadora-intereses-demora';
6
6
  const title = 'Calculadora de Intereses de Demora y Legales España 2026';
7
7
  const description =
8
8
  'Calcula intereses de demora tributarios (Hacienda) y legales por impagos. Simulador actualizado para 2025 y 2026 basado en la Ley de Presupuestos.';
@@ -2,7 +2,7 @@ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dt
2
2
  import type { ToolLocaleContent } from '../../../types';
3
3
  import type { MortgageUI } from '../ui';
4
4
 
5
- const slug = 'simulador-hipoteca';
5
+ const slug = 'hipoteca';
6
6
  const title = 'Simulador de Hipoteca y Tabla de Amortización Francesa';
7
7
  const description =
8
8
  'Calcula tu cuota hipotecaria mensual, descubre cuánto pagarás en intereses y visualiza tu cuadro de amortización completo al instante.';