@jjlmoya/utils-finance 1.15.0 → 1.16.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 (222) hide show
  1. package/package.json +1 -1
  2. package/src/pages/[locale]/[slug].astro +29 -13
  3. package/src/tests/locale_completeness.test.ts +6 -16
  4. package/src/tests/shared-test-helpers.ts +56 -0
  5. package/src/tests/tool_exports.test.ts +34 -0
  6. package/src/tool/compoundInterest/bibliography.ts +20 -0
  7. package/src/tool/compoundInterest/entry.ts +2 -0
  8. package/src/tool/compoundInterest/i18n/de.ts +3 -16
  9. package/src/tool/compoundInterest/i18n/en.ts +3 -20
  10. package/src/tool/compoundInterest/i18n/es.ts +2 -20
  11. package/src/tool/compoundInterest/i18n/fr.ts +3 -20
  12. package/src/tool/compoundInterest/i18n/id.ts +3 -16
  13. package/src/tool/compoundInterest/i18n/it.ts +3 -16
  14. package/src/tool/compoundInterest/i18n/ja.ts +3 -16
  15. package/src/tool/compoundInterest/i18n/ko.ts +3 -16
  16. package/src/tool/compoundInterest/i18n/nl.ts +3 -16
  17. package/src/tool/compoundInterest/i18n/pl.ts +2 -16
  18. package/src/tool/compoundInterest/i18n/pt.ts +3 -16
  19. package/src/tool/compoundInterest/i18n/ru.ts +3 -16
  20. package/src/tool/compoundInterest/i18n/sv.ts +3 -16
  21. package/src/tool/compoundInterest/i18n/tr.ts +3 -16
  22. package/src/tool/compoundInterest/i18n/zh.ts +3 -16
  23. package/src/tool/compoundInterest/seo.astro +2 -1
  24. package/src/tool/courtFeeCalculator/bibliography.ts +16 -0
  25. package/src/tool/courtFeeCalculator/entry.ts +2 -0
  26. package/src/tool/courtFeeCalculator/i18n/de.ts +2 -18
  27. package/src/tool/courtFeeCalculator/i18n/en.ts +2 -22
  28. package/src/tool/courtFeeCalculator/i18n/es.ts +2 -22
  29. package/src/tool/courtFeeCalculator/i18n/fr.ts +2 -22
  30. package/src/tool/courtFeeCalculator/i18n/id.ts +2 -18
  31. package/src/tool/courtFeeCalculator/i18n/it.ts +2 -18
  32. package/src/tool/courtFeeCalculator/i18n/ja.ts +2 -18
  33. package/src/tool/courtFeeCalculator/i18n/ko.ts +2 -18
  34. package/src/tool/courtFeeCalculator/i18n/nl.ts +2 -18
  35. package/src/tool/courtFeeCalculator/i18n/pl.ts +2 -18
  36. package/src/tool/courtFeeCalculator/i18n/pt.ts +2 -18
  37. package/src/tool/courtFeeCalculator/i18n/ru.ts +2 -18
  38. package/src/tool/courtFeeCalculator/i18n/sv.ts +2 -18
  39. package/src/tool/courtFeeCalculator/i18n/tr.ts +2 -18
  40. package/src/tool/courtFeeCalculator/i18n/zh.ts +2 -18
  41. package/src/tool/courtFeeCalculator/seo.astro +2 -1
  42. package/src/tool/debtSnowball/bibliography.ts +12 -0
  43. package/src/tool/debtSnowball/entry.ts +2 -0
  44. package/src/tool/debtSnowball/i18n/de.ts +2 -14
  45. package/src/tool/debtSnowball/i18n/en.ts +2 -14
  46. package/src/tool/debtSnowball/i18n/es.ts +2 -14
  47. package/src/tool/debtSnowball/i18n/fr.ts +2 -14
  48. package/src/tool/debtSnowball/i18n/id.ts +2 -14
  49. package/src/tool/debtSnowball/i18n/it.ts +2 -14
  50. package/src/tool/debtSnowball/i18n/ja.ts +2 -14
  51. package/src/tool/debtSnowball/i18n/ko.ts +2 -14
  52. package/src/tool/debtSnowball/i18n/nl.ts +2 -14
  53. package/src/tool/debtSnowball/i18n/pl.ts +2 -14
  54. package/src/tool/debtSnowball/i18n/pt.ts +2 -14
  55. package/src/tool/debtSnowball/i18n/ru.ts +2 -14
  56. package/src/tool/debtSnowball/i18n/sv.ts +2 -14
  57. package/src/tool/debtSnowball/i18n/tr.ts +2 -14
  58. package/src/tool/debtSnowball/i18n/zh.ts +2 -14
  59. package/src/tool/debtSnowball/seo.astro +2 -1
  60. package/src/tool/fireCalculator/bibliography.ts +20 -0
  61. package/src/tool/fireCalculator/entry.ts +2 -0
  62. package/src/tool/fireCalculator/i18n/de.ts +2 -22
  63. package/src/tool/fireCalculator/i18n/en.ts +2 -22
  64. package/src/tool/fireCalculator/i18n/es.ts +2 -22
  65. package/src/tool/fireCalculator/i18n/fr.ts +2 -22
  66. package/src/tool/fireCalculator/i18n/id.ts +2 -22
  67. package/src/tool/fireCalculator/i18n/it.ts +2 -22
  68. package/src/tool/fireCalculator/i18n/ja.ts +2 -22
  69. package/src/tool/fireCalculator/i18n/ko.ts +2 -22
  70. package/src/tool/fireCalculator/i18n/nl.ts +2 -22
  71. package/src/tool/fireCalculator/i18n/pl.ts +2 -22
  72. package/src/tool/fireCalculator/i18n/pt.ts +2 -22
  73. package/src/tool/fireCalculator/i18n/ru.ts +2 -22
  74. package/src/tool/fireCalculator/i18n/sv.ts +2 -22
  75. package/src/tool/fireCalculator/i18n/tr.ts +2 -22
  76. package/src/tool/fireCalculator/i18n/zh.ts +2 -22
  77. package/src/tool/fireCalculator/seo.astro +2 -1
  78. package/src/tool/ibanBicSwiftConverter/bibliography.ts +12 -0
  79. package/src/tool/ibanBicSwiftConverter/entry.ts +2 -0
  80. package/src/tool/ibanBicSwiftConverter/i18n/de.ts +2 -14
  81. package/src/tool/ibanBicSwiftConverter/i18n/en.ts +2 -18
  82. package/src/tool/ibanBicSwiftConverter/i18n/es.ts +2 -18
  83. package/src/tool/ibanBicSwiftConverter/i18n/fr.ts +2 -18
  84. package/src/tool/ibanBicSwiftConverter/i18n/id.ts +2 -14
  85. package/src/tool/ibanBicSwiftConverter/i18n/it.ts +2 -14
  86. package/src/tool/ibanBicSwiftConverter/i18n/ja.ts +2 -14
  87. package/src/tool/ibanBicSwiftConverter/i18n/ko.ts +2 -14
  88. package/src/tool/ibanBicSwiftConverter/i18n/nl.ts +2 -14
  89. package/src/tool/ibanBicSwiftConverter/i18n/pl.ts +2 -14
  90. package/src/tool/ibanBicSwiftConverter/i18n/pt.ts +2 -14
  91. package/src/tool/ibanBicSwiftConverter/i18n/ru.ts +2 -14
  92. package/src/tool/ibanBicSwiftConverter/i18n/sv.ts +2 -14
  93. package/src/tool/ibanBicSwiftConverter/i18n/tr.ts +2 -14
  94. package/src/tool/ibanBicSwiftConverter/i18n/zh.ts +2 -14
  95. package/src/tool/ibanBicSwiftConverter/seo.astro +2 -1
  96. package/src/tool/inflation/bibliography.ts +20 -0
  97. package/src/tool/inflation/entry.ts +2 -0
  98. package/src/tool/inflation/i18n/de.ts +3 -16
  99. package/src/tool/inflation/i18n/en.ts +3 -20
  100. package/src/tool/inflation/i18n/es.ts +3 -20
  101. package/src/tool/inflation/i18n/fr.ts +3 -20
  102. package/src/tool/inflation/i18n/id.ts +3 -16
  103. package/src/tool/inflation/i18n/it.ts +3 -16
  104. package/src/tool/inflation/i18n/ja.ts +3 -16
  105. package/src/tool/inflation/i18n/ko.ts +3 -16
  106. package/src/tool/inflation/i18n/nl.ts +3 -16
  107. package/src/tool/inflation/i18n/pl.ts +3 -16
  108. package/src/tool/inflation/i18n/pt.ts +3 -16
  109. package/src/tool/inflation/i18n/ru.ts +3 -16
  110. package/src/tool/inflation/i18n/sv.ts +3 -16
  111. package/src/tool/inflation/i18n/tr.ts +3 -16
  112. package/src/tool/inflation/i18n/zh.ts +3 -16
  113. package/src/tool/inflation/seo.astro +2 -1
  114. package/src/tool/lateInterest/bibliography.ts +16 -0
  115. package/src/tool/lateInterest/entry.ts +2 -0
  116. package/src/tool/lateInterest/i18n/de.ts +3 -16
  117. package/src/tool/lateInterest/i18n/en.ts +3 -16
  118. package/src/tool/lateInterest/i18n/es.ts +3 -16
  119. package/src/tool/lateInterest/i18n/fr.ts +3 -16
  120. package/src/tool/lateInterest/i18n/id.ts +3 -16
  121. package/src/tool/lateInterest/i18n/it.ts +3 -16
  122. package/src/tool/lateInterest/i18n/ja.ts +3 -16
  123. package/src/tool/lateInterest/i18n/ko.ts +3 -16
  124. package/src/tool/lateInterest/i18n/nl.ts +3 -16
  125. package/src/tool/lateInterest/i18n/pl.ts +3 -16
  126. package/src/tool/lateInterest/i18n/pt.ts +3 -16
  127. package/src/tool/lateInterest/i18n/ru.ts +3 -16
  128. package/src/tool/lateInterest/i18n/sv.ts +3 -16
  129. package/src/tool/lateInterest/i18n/tr.ts +3 -16
  130. package/src/tool/lateInterest/i18n/zh.ts +3 -16
  131. package/src/tool/lateInterest/seo.astro +2 -1
  132. package/src/tool/legalInterestRate/bibliography.ts +16 -0
  133. package/src/tool/legalInterestRate/entry.ts +2 -0
  134. package/src/tool/legalInterestRate/i18n/de.ts +2 -18
  135. package/src/tool/legalInterestRate/i18n/en.ts +2 -22
  136. package/src/tool/legalInterestRate/i18n/es.ts +2 -22
  137. package/src/tool/legalInterestRate/i18n/fr.ts +2 -22
  138. package/src/tool/legalInterestRate/i18n/id.ts +2 -18
  139. package/src/tool/legalInterestRate/i18n/it.ts +2 -18
  140. package/src/tool/legalInterestRate/i18n/ja.ts +2 -18
  141. package/src/tool/legalInterestRate/i18n/ko.ts +2 -18
  142. package/src/tool/legalInterestRate/i18n/nl.ts +2 -18
  143. package/src/tool/legalInterestRate/i18n/pl.ts +2 -18
  144. package/src/tool/legalInterestRate/i18n/pt.ts +2 -18
  145. package/src/tool/legalInterestRate/i18n/ru.ts +2 -18
  146. package/src/tool/legalInterestRate/i18n/sv.ts +2 -18
  147. package/src/tool/legalInterestRate/i18n/tr.ts +2 -18
  148. package/src/tool/legalInterestRate/i18n/zh.ts +2 -18
  149. package/src/tool/legalInterestRate/seo.astro +2 -1
  150. package/src/tool/lotteryOptimizer/bibliography.ts +16 -0
  151. package/src/tool/lotteryOptimizer/entry.ts +2 -0
  152. package/src/tool/lotteryOptimizer/i18n/de.ts +2 -18
  153. package/src/tool/lotteryOptimizer/i18n/en.ts +2 -18
  154. package/src/tool/lotteryOptimizer/i18n/es.ts +2 -18
  155. package/src/tool/lotteryOptimizer/i18n/fr.ts +2 -18
  156. package/src/tool/lotteryOptimizer/i18n/id.ts +2 -18
  157. package/src/tool/lotteryOptimizer/i18n/it.ts +2 -18
  158. package/src/tool/lotteryOptimizer/i18n/ja.ts +2 -18
  159. package/src/tool/lotteryOptimizer/i18n/ko.ts +2 -18
  160. package/src/tool/lotteryOptimizer/i18n/nl.ts +2 -18
  161. package/src/tool/lotteryOptimizer/i18n/pl.ts +2 -18
  162. package/src/tool/lotteryOptimizer/i18n/pt.ts +2 -18
  163. package/src/tool/lotteryOptimizer/i18n/ru.ts +2 -18
  164. package/src/tool/lotteryOptimizer/i18n/sv.ts +2 -18
  165. package/src/tool/lotteryOptimizer/i18n/tr.ts +2 -18
  166. package/src/tool/lotteryOptimizer/i18n/zh.ts +2 -18
  167. package/src/tool/lotteryOptimizer/seo.astro +2 -1
  168. package/src/tool/mortgage/bibliography.ts +20 -0
  169. package/src/tool/mortgage/entry.ts +2 -0
  170. package/src/tool/mortgage/i18n/de.ts +3 -16
  171. package/src/tool/mortgage/i18n/en.ts +3 -20
  172. package/src/tool/mortgage/i18n/es.ts +3 -20
  173. package/src/tool/mortgage/i18n/fr.ts +3 -20
  174. package/src/tool/mortgage/i18n/id.ts +3 -12
  175. package/src/tool/mortgage/i18n/it.ts +3 -16
  176. package/src/tool/mortgage/i18n/ja.ts +3 -12
  177. package/src/tool/mortgage/i18n/ko.ts +3 -12
  178. package/src/tool/mortgage/i18n/nl.ts +3 -12
  179. package/src/tool/mortgage/i18n/pl.ts +3 -12
  180. package/src/tool/mortgage/i18n/pt.ts +3 -12
  181. package/src/tool/mortgage/i18n/ru.ts +3 -12
  182. package/src/tool/mortgage/i18n/sv.ts +3 -12
  183. package/src/tool/mortgage/i18n/tr.ts +3 -12
  184. package/src/tool/mortgage/i18n/zh.ts +3 -12
  185. package/src/tool/mortgage/seo.astro +2 -1
  186. package/src/tool/percentageCalculator/bibliography.ts +16 -0
  187. package/src/tool/percentageCalculator/entry.ts +2 -0
  188. package/src/tool/percentageCalculator/i18n/de.ts +3 -12
  189. package/src/tool/percentageCalculator/i18n/en.ts +3 -16
  190. package/src/tool/percentageCalculator/i18n/es.ts +3 -16
  191. package/src/tool/percentageCalculator/i18n/fr.ts +3 -16
  192. package/src/tool/percentageCalculator/i18n/id.ts +3 -12
  193. package/src/tool/percentageCalculator/i18n/it.ts +3 -12
  194. package/src/tool/percentageCalculator/i18n/ja.ts +3 -12
  195. package/src/tool/percentageCalculator/i18n/ko.ts +3 -12
  196. package/src/tool/percentageCalculator/i18n/nl.ts +3 -12
  197. package/src/tool/percentageCalculator/i18n/pl.ts +3 -12
  198. package/src/tool/percentageCalculator/i18n/pt.ts +3 -12
  199. package/src/tool/percentageCalculator/i18n/ru.ts +3 -12
  200. package/src/tool/percentageCalculator/i18n/sv.ts +3 -12
  201. package/src/tool/percentageCalculator/i18n/tr.ts +3 -12
  202. package/src/tool/percentageCalculator/i18n/zh.ts +3 -12
  203. package/src/tool/percentageCalculator/seo.astro +2 -1
  204. package/src/tool/rentIncreaseCalculator/bibliography.ts +16 -0
  205. package/src/tool/rentIncreaseCalculator/entry.ts +2 -0
  206. package/src/tool/rentIncreaseCalculator/i18n/de.ts +2 -18
  207. package/src/tool/rentIncreaseCalculator/i18n/en.ts +2 -18
  208. package/src/tool/rentIncreaseCalculator/i18n/es.ts +2 -18
  209. package/src/tool/rentIncreaseCalculator/i18n/fr.ts +2 -18
  210. package/src/tool/rentIncreaseCalculator/i18n/id.ts +2 -18
  211. package/src/tool/rentIncreaseCalculator/i18n/it.ts +2 -18
  212. package/src/tool/rentIncreaseCalculator/i18n/ja.ts +2 -18
  213. package/src/tool/rentIncreaseCalculator/i18n/ko.ts +2 -18
  214. package/src/tool/rentIncreaseCalculator/i18n/nl.ts +2 -18
  215. package/src/tool/rentIncreaseCalculator/i18n/pl.ts +2 -18
  216. package/src/tool/rentIncreaseCalculator/i18n/pt.ts +2 -18
  217. package/src/tool/rentIncreaseCalculator/i18n/ru.ts +2 -18
  218. package/src/tool/rentIncreaseCalculator/i18n/sv.ts +2 -18
  219. package/src/tool/rentIncreaseCalculator/i18n/tr.ts +2 -18
  220. package/src/tool/rentIncreaseCalculator/i18n/zh.ts +2 -18
  221. package/src/tool/rentIncreaseCalculator/seo.astro +2 -1
  222. package/src/types.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-finance",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -34,18 +34,28 @@ export async function getStaticPaths() {
34
34
  ]),
35
35
  ) as Partial<Record<KnownLocale, string>>;
36
36
 
37
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
+
37
40
  for (const { locale, content } of localeContents) {
38
- const allToolsNav = await Promise.all(
39
- ALL_TOOLS.map(async ({ entry: navEntry }) => ({
40
- id: navEntry.id,
41
- title: (await navEntry.i18n[locale]!()).title,
42
- href: `/${locale}/${(await navEntry.i18n[locale]!()).slug}`,
43
- isActive: navEntry.id === entry.id,
44
- })),
45
- );
41
+ const allToolsNav = (
42
+ await Promise.all(
43
+ ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
+ const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
+ if (!loader) return null;
46
+ const navContent = await loader();
47
+ return {
48
+ id: navEntry.id,
49
+ title: navContent.title,
50
+ href: `/${locale}/${navContent.slug}`,
51
+ isActive: navEntry.id === entry.id,
52
+ };
53
+ }),
54
+ )
55
+ ).filter(Boolean) as NavItem[];
46
56
  paths.push({
47
57
  params: { locale, slug: content.slug },
48
- props: { Component, locale, content, localeUrls, allToolsNav },
58
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
49
59
  });
50
60
  }
51
61
  }
@@ -66,11 +76,16 @@ interface Props {
66
76
  content: ToolLocaleContent;
67
77
  localeUrls: Partial<Record<KnownLocale, string>>;
68
78
  allToolsNav: NavItem[];
79
+ englishSlug: string;
69
80
  }
70
81
 
71
- const { Component, locale, content, localeUrls, allToolsNav } = Astro.props;
82
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
+
84
+ const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
85
+ const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
+ const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
72
87
 
73
- const seoContent: UtilitySEOContent = { locale, sections: content.seo };
88
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
74
89
 
75
90
  const words = content.title.split(" ");
76
91
  const titleHighlight = words[0] || "";
@@ -89,8 +104,9 @@ const titleBase = words.slice(1).join(" ") || "";
89
104
  tools={allToolsNav}
90
105
  />
91
106
  <Fragment slot="head">
107
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
92
108
  {
93
- content.schemas.map((schema: unknown) => (
109
+ ( content.schemas ?? []).map((schema: unknown) => (
94
110
  <script
95
111
  is:inline
96
112
  type="application/ld+json"
@@ -120,7 +136,7 @@ const titleBase = words.slice(1).join(" ") || "";
120
136
  </section>
121
137
 
122
138
  <section class="section-bibliography">
123
- <Bibliography links={content.bibliography} title={content.bibliographyTitle} />
139
+ <Bibliography links={content.bibliography} />
124
140
  </section>
125
141
  </div>
126
142
  </PreviewLayout>
@@ -7,28 +7,18 @@ describe('Locale Completeness Validation', () => {
7
7
  describe(`Tool: ${tool.entry.id}`, () => {
8
8
  Object.keys(tool.entry.i18n).forEach((locale) => {
9
9
  describe(`Locale: ${locale}`, () => {
10
- it('faqTitle should be defined when faq items exist', async () => {
10
+ it('should have faq items', async () => {
11
11
  const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
12
  const content = (await loader?.()) as ToolLocaleContent;
13
-
14
- if (content.faq.length > 0) {
15
- expect(
16
- content.faqTitle,
17
- `Tool "${tool.entry.id}" locale "${locale}" has ${content.faq.length} FAQ items but is missing faqTitle`,
18
- ).toBeTruthy();
19
- }
13
+ expect(content.faq).toBeDefined();
14
+ expect(Array.isArray(content.faq)).toBe(true);
20
15
  });
21
16
 
22
- it('bibliographyTitle should be defined when bibliography items exist', async () => {
17
+ it('should have bibliography items', async () => {
23
18
  const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
24
19
  const content = (await loader?.()) as ToolLocaleContent;
25
-
26
- if (content.bibliography.length > 0) {
27
- expect(
28
- content.bibliographyTitle,
29
- `Tool "${tool.entry.id}" locale "${locale}" has ${content.bibliography.length} bibliography items but is missing bibliographyTitle`,
30
- ).toBeTruthy();
31
- }
20
+ expect(content.bibliography).toBeDefined();
21
+ expect(Array.isArray(content.bibliography)).toBe(true);
32
22
  });
33
23
  });
34
24
  });
@@ -0,0 +1,56 @@
1
+ import type { ToolDefinition } from '../types';
2
+
3
+ export interface ToolExportValidationResult {
4
+ passed: boolean;
5
+ failures: string[];
6
+ }
7
+
8
+ function validateComponentType(
9
+ toolId: string,
10
+ componentName: string,
11
+ component: unknown,
12
+ failures: string[],
13
+ ): void {
14
+ if (typeof component !== 'function') {
15
+ failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
16
+ }
17
+ }
18
+
19
+ async function validateComponentExecution(
20
+ toolId: string,
21
+ componentName: string,
22
+ fn: () => Promise<unknown>,
23
+ failures: string[],
24
+ ): Promise<void> {
25
+ try {
26
+ const result = await fn();
27
+ if (!result || typeof result !== 'object') {
28
+ failures.push(`${toolId}: ${componentName} import returned invalid result`);
29
+ }
30
+ } catch (error) {
31
+ failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
32
+ }
33
+ }
34
+
35
+ export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
36
+ const failures: string[] = [];
37
+
38
+ for (const tool of tools) {
39
+ validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
40
+ validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
41
+ validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
42
+
43
+ const componentFn = tool.Component as () => Promise<unknown>;
44
+ const seoFn = tool.SEOComponent as () => Promise<unknown>;
45
+ const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
46
+
47
+ await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
48
+ await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
49
+ await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
50
+ }
51
+
52
+ return {
53
+ passed: failures.length === 0,
54
+ failures,
55
+ };
56
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { validateToolExports } from './shared-test-helpers';
4
+
5
+ describe('Tool Exports Pattern Validation', () => {
6
+ describe('Component Exports Format', () => {
7
+ ALL_TOOLS.forEach((tool) => {
8
+ it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
9
+ expect(typeof tool.Component).toBe('function');
10
+ expect(tool.Component).toBeInstanceOf(Function);
11
+ });
12
+
13
+ it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
14
+ expect(typeof tool.SEOComponent).toBe('function');
15
+ expect(tool.SEOComponent).toBeInstanceOf(Function);
16
+ });
17
+
18
+ it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
19
+ expect(typeof tool.BibliographyComponent).toBe('function');
20
+ expect(tool.BibliographyComponent).toBeInstanceOf(Function);
21
+ });
22
+ });
23
+ });
24
+
25
+ describe('Dynamic Import Validation', () => {
26
+ it('all tools must have functional dynamic imports', async () => {
27
+ const result = await validateToolExports(ALL_TOOLS);
28
+ if (!result.passed) {
29
+ throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
30
+ }
31
+ expect(result.passed).toBe(true);
32
+ });
33
+ });
34
+ });
@@ -0,0 +1,20 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Investopedia: Compound Interest Definition',
6
+ url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
7
+ },
8
+ {
9
+ name: 'Investopedia: Rule of 72',
10
+ url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
11
+ },
12
+ {
13
+ name: 'Khan Academy: Interest and Debt',
14
+ url: 'https://www.khanacademy.org/economics-finance-domain/core-finance/interest-tutorial',
15
+ },
16
+ {
17
+ name: 'Banco de España: Portal del Cliente Bancario',
18
+ url: 'https://www.bde.es/bde/es/secciones/servicios/Particulares_y_e/Clientesbancarios/',
19
+ },
20
+ ];
@@ -28,3 +28,5 @@ export const compoundInterest: FinanceToolEntry<CompoundInterestUI> = {
28
28
  zh: () => import('./i18n/zh').then((m) => m.content),
29
29
  },
30
30
  };
31
+
32
+ export { bibliography } from './bibliography';
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Häufig gestellte Fragen',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Quellen und Referenzen',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Khan Academy: Interest and Debt',
104
- url: 'https://www.khanacademy.org/economics-finance-domain/core-finance/interest-tutorial',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,27 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Frequently Asked Questions',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Sources & References',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Khan Academy: Interest and Debt',
104
- url: 'https://www.khanacademy.org/economics-finance-domain/core-finance/interest-tutorial',
105
- },
106
- {
107
- name: 'Investopedia: Time Value of Money',
108
- url: 'https://www.investopedia.com/terms/t/timevalueofmoney.asp',
109
- },
110
- ],
92
+ bibliography,
93
+
111
94
  howTo: howToData,
112
95
  schemas: [faqSchema, howToSchema, appSchema],
113
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,27 +88,8 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Preguntas Frecuentes',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Fuentes y Referencias',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Khan Academy: Interest and Debt',
104
- url: 'https://www.khanacademy.org/economics-finance-domain/core-finance/interest-tutorial',
105
- },
106
- {
107
- name: 'Banco de España: Portal del Cliente Bancario',
108
- url: 'https://www.bde.es/bde/es/secciones/servicios/Particulares_y_e/Clientesbancarios/',
109
- },
110
- ],
92
+ bibliography,
111
93
  howTo: howToData,
112
94
  schemas: [faqSchema, howToSchema, appSchema],
113
95
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,27 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Questions Fréquentes',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Sources et Références',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Khan Academy: Interest and Debt',
104
- url: 'https://www.khanacademy.org/economics-finance-domain/core-finance/interest-tutorial',
105
- },
106
- {
107
- name: 'Banque de France : Éducation financière',
108
- url: 'https://www.banque-france.fr/fr/la-banque-de-france/notre-mission/education-economique-financiere-et-statistique',
109
- },
110
- ],
92
+ bibliography,
93
+
111
94
  howTo: howToData,
112
95
  schemas: [faqSchema, howToSchema, appSchema],
113
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Pertanyaan Umum',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Sumber dan Referensi',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Khan Academy: Bunga dan Hutang',
104
- url: 'https://id.khanacademy.org/economics-finance-domain/core-finance/interest-tutorial',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Domande Frequenti',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Fonti e Riferimenti',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Khan Academy: Interest and Debt',
104
- url: 'https://it.khanacademy.org/economics-finance-domain/core-finance/interest-tutorial',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'よくある質問',
91
91
  faq: faqData,
92
- bibliographyTitle: '情報源とリファレンス',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: '複利の計算 - Wikipedia',
104
- url: 'https://ja.wikipedia.org/wiki/複利',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: '자주 묻는 질문',
91
91
  faq: faqData,
92
- bibliographyTitle: '출처 및 참고 자료',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: '복리의 개념 - 위키백과',
104
- url: 'https://ko.wikipedia.org/wiki/복리',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Veelgestelde vragen',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Bronnen en Referenties',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Samengestelde interest - Wikipedia',
104
- url: 'https://nl.wikipedia.org/wiki/Samengestelde_interest',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,8 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Często zadawane pytania',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Źródła i referencje',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Procent składany - Wikipedia',
104
- url: 'https://pl.wikipedia.org/wiki/Procent_składany',
105
- },
106
- ],
92
+ bibliography,
107
93
  howTo: howToData,
108
94
  schemas: [faqSchema, howToSchema, appSchema],
109
95
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Perguntas Frequentes',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Fontes e Referências',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Juros compostos - Wikipédia',
104
- url: 'https://pt.wikipedia.org/wiki/Juros_compostos',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Часто задаваемые вопросы',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Источники и ссылки',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Сложный процент — Википедия',
104
- url: 'https://ru.wikipedia.org/wiki/Сложный_процент',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography'
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { CompoundInterestUI } from '../ui';
@@ -87,23 +88,9 @@ export const content: ToolLocaleContent<CompoundInterestUI> = {
87
88
  slug,
88
89
  title,
89
90
  description,
90
- faqTitle: 'Vanliga frågor',
91
91
  faq: faqData,
92
- bibliographyTitle: 'Källor och referenser',
93
- bibliography: [
94
- {
95
- name: 'Investopedia: Compound Interest Definition',
96
- url: 'https://www.investopedia.com/terms/c/compoundinterest.asp',
97
- },
98
- {
99
- name: 'Investopedia: Rule of 72',
100
- url: 'https://www.investopedia.com/terms/r/ruleof72.asp',
101
- },
102
- {
103
- name: 'Ränta på ränta - Wikipedia',
104
- url: 'https://sv.wikipedia.org/wiki/Ränta_på_ränta',
105
- },
106
- ],
92
+ bibliography,
93
+
107
94
  howTo: howToData,
108
95
  schemas: [faqSchema, howToSchema, appSchema],
109
96
  seo: [