@jjlmoya/utils-forensic-science 1.13.0 → 1.14.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 (138) hide show
  1. package/package.json +2 -1
  2. package/src/category/index.ts +61 -59
  3. package/src/category/seo.astro +1 -1
  4. package/src/components/PreviewNavSidebar.astro +1 -1
  5. package/src/components/PreviewToolbar.astro +1 -1
  6. package/src/data.ts +1 -1
  7. package/src/entries.ts +70 -68
  8. package/src/env.d.ts +1 -1
  9. package/src/index.ts +32 -31
  10. package/src/pages/[locale]/[slug].astro +161 -159
  11. package/src/pages/[locale].astro +8 -5
  12. package/src/pages/index.astro +1 -1
  13. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  14. package/src/tests/diacritics_density.test.ts +1 -1
  15. package/src/tests/faq_count.test.ts +19 -19
  16. package/src/tests/i18n_coverage.test.ts +36 -36
  17. package/src/tests/inverted_punctuation.test.ts +1 -1
  18. package/src/tests/locale_completeness.test.ts +2 -2
  19. package/src/tests/mocks/astro_mock.js +1 -1
  20. package/src/tests/no_h1_in_components.test.ts +1 -1
  21. package/src/tests/script_density.test.ts +94 -94
  22. package/src/tests/seo_length.test.ts +46 -46
  23. package/src/tests/seo_parity.test.ts +2 -7
  24. package/src/tests/seo_translation_completeness.test.ts +68 -0
  25. package/src/tests/seo_wellformed_export.test.ts +65 -0
  26. package/src/tests/tool_validation.test.ts +2 -2
  27. package/src/tests/translation_copy.test.ts +23 -24
  28. package/src/tool/bloodstain-pattern-origin-analyzer/component.astro +9 -9
  29. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/zh.ts +1 -1
  30. package/src/tool/dna-profile-match-probability-lab/bibliography.astro +14 -0
  31. package/src/tool/dna-profile-match-probability-lab/bibliography.ts +16 -0
  32. package/src/tool/dna-profile-match-probability-lab/component.astro +121 -0
  33. package/src/tool/dna-profile-match-probability-lab/controller.ts +159 -0
  34. package/src/tool/dna-profile-match-probability-lab/dna-profile-match-probability-lab.css +485 -0
  35. package/src/tool/dna-profile-match-probability-lab/dom-views.ts +93 -0
  36. package/src/tool/dna-profile-match-probability-lab/entry.ts +29 -0
  37. package/src/tool/dna-profile-match-probability-lab/evaluator.ts +19 -0
  38. package/src/tool/dna-profile-match-probability-lab/i18n/de.ts +43 -0
  39. package/src/tool/dna-profile-match-probability-lab/i18n/en.ts +196 -0
  40. package/src/tool/dna-profile-match-probability-lab/i18n/es.ts +40 -0
  41. package/src/tool/dna-profile-match-probability-lab/i18n/fr.ts +10 -0
  42. package/src/tool/dna-profile-match-probability-lab/i18n/id.ts +41 -0
  43. package/src/tool/dna-profile-match-probability-lab/i18n/it.ts +10 -0
  44. package/src/tool/dna-profile-match-probability-lab/i18n/ja.ts +40 -0
  45. package/src/tool/dna-profile-match-probability-lab/i18n/ko.ts +40 -0
  46. package/src/tool/dna-profile-match-probability-lab/i18n/nl.ts +29 -0
  47. package/src/tool/dna-profile-match-probability-lab/i18n/pl.ts +10 -0
  48. package/src/tool/dna-profile-match-probability-lab/i18n/pt.ts +41 -0
  49. package/src/tool/dna-profile-match-probability-lab/i18n/ru.ts +29 -0
  50. package/src/tool/dna-profile-match-probability-lab/i18n/sv.ts +41 -0
  51. package/src/tool/dna-profile-match-probability-lab/i18n/tr.ts +10 -0
  52. package/src/tool/dna-profile-match-probability-lab/i18n/zh.ts +29 -0
  53. package/src/tool/dna-profile-match-probability-lab/index.ts +11 -0
  54. package/src/tool/dna-profile-match-probability-lab/localize.ts +107 -0
  55. package/src/tool/dna-profile-match-probability-lab/logic.test.ts +56 -0
  56. package/src/tool/dna-profile-match-probability-lab/logic.ts +139 -0
  57. package/src/tool/dna-profile-match-probability-lab/seo.astro +15 -0
  58. package/src/tool/dna-profile-match-probability-lab/storage.ts +26 -0
  59. package/src/tool/dna-profile-match-probability-lab/ui.ts +3 -0
  60. package/src/tool/fire-pattern-origin-analyzer/component.astro +1 -1
  61. package/src/tool/fire-pattern-origin-analyzer/i18n/zh.ts +5 -1
  62. package/src/tool/fire-pattern-origin-analyzer/logic.ts +9 -2
  63. package/src/tool/fire-pattern-origin-analyzer/view-bindings.ts +8 -4
  64. package/src/tool/fire-pattern-origin-analyzer/view-model.ts +6 -2
  65. package/src/tool/forensic-age-estimator/bibliography.astro +1 -1
  66. package/src/tool/forensic-age-estimator/component.astro +1 -1
  67. package/src/tool/forensic-age-estimator/entry.ts +17 -17
  68. package/src/tool/forensic-age-estimator/i18n/de.ts +244 -244
  69. package/src/tool/forensic-age-estimator/i18n/en.ts +244 -244
  70. package/src/tool/forensic-age-estimator/i18n/es.ts +244 -244
  71. package/src/tool/forensic-age-estimator/i18n/fr.ts +244 -244
  72. package/src/tool/forensic-age-estimator/i18n/id.ts +244 -244
  73. package/src/tool/forensic-age-estimator/i18n/it.ts +244 -244
  74. package/src/tool/forensic-age-estimator/i18n/nl.ts +244 -244
  75. package/src/tool/forensic-age-estimator/i18n/pl.ts +244 -244
  76. package/src/tool/forensic-age-estimator/i18n/pt.ts +244 -244
  77. package/src/tool/forensic-age-estimator/i18n/ru.ts +244 -244
  78. package/src/tool/forensic-age-estimator/i18n/sv.ts +244 -244
  79. package/src/tool/forensic-age-estimator/i18n/tr.ts +244 -244
  80. package/src/tool/forensic-age-estimator/i18n/zh.ts +245 -245
  81. package/src/tool/forensic-age-estimator/index.ts +1 -1
  82. package/src/tool/forensic-age-estimator/seo.astro +1 -1
  83. package/src/tool/forensic-blood-test-simulator/component.astro +1 -1
  84. package/src/tool/forensic-fiber-comparison-microscope/component.astro +1 -1
  85. package/src/tool/forensic-fiber-comparison-microscope/render.ts +1 -1
  86. package/src/tool/forensic-fiber-comparison-microscope/view.ts +1 -1
  87. package/src/tool/forensic-fingerprint-minutiae-identifier/component.astro +2 -2
  88. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ja.ts +2 -1
  89. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/zh.ts +2 -0
  90. package/src/tool/forensic-fingerprint-minutiae-identifier/renderer.ts +9 -8
  91. package/src/tool/forensic-glass-becke-line-simulator/component.astro +1 -1
  92. package/src/tool/forensic-glass-becke-line-simulator/view.ts +8 -6
  93. package/src/tool/forensic-image-authenticity-analyzer/component.astro +1 -1
  94. package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -105
  95. package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -105
  96. package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -105
  97. package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -105
  98. package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +95 -90
  99. package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -105
  100. package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +7 -2
  101. package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +4 -1
  102. package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +95 -92
  103. package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +95 -92
  104. package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -105
  105. package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +97 -92
  106. package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +97 -93
  107. package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +98 -93
  108. package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +11 -0
  109. package/src/tool/forensic-microcrystal-drug-simulator/component.astro +1 -1
  110. package/src/tool/forensic-sex-determinator/component.astro +3 -2
  111. package/src/tool/forensic-stature-estimator/component.astro +1 -1
  112. package/src/tool/forensic-stature-estimator/i18n/id.ts +4 -0
  113. package/src/tool/forensic-stature-estimator/i18n/nl.ts +4 -0
  114. package/src/tool/forensic-stature-estimator/i18n/ru.ts +4 -0
  115. package/src/tool/forensic-tlc-ink-simulator/component.astro +1 -1
  116. package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +1 -0
  117. package/src/tool/forensic-toolmark-striation-matcher/component.astro +1 -1
  118. package/src/tool/forensic-toolmark-striation-matcher/logic.ts +1 -1
  119. package/src/tool/forensic-toolmark-striation-matcher/renderer.ts +7 -6
  120. package/src/tool/forensic-toolmark-striation-matcher/view.ts +19 -20
  121. package/src/tool/gsr-dispersion-calculator/component.astro +1 -1
  122. package/src/tool/gsr-dispersion-calculator/logic.ts +1 -1
  123. package/src/tool/time-of-death-algor-mortis-calculator/entry.ts +2 -0
  124. package/src/tool/widmark-alcohol-simulator/component.astro +1 -1
  125. package/src/tool/widmark-alcohol-simulator/i18n/de.ts +5 -0
  126. package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +2 -1
  127. package/src/tool/widmark-alcohol-simulator/i18n/id.ts +1 -0
  128. package/src/tool/widmark-alcohol-simulator/i18n/it.ts +4 -1
  129. package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +4 -1
  130. package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +5 -1
  131. package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +5 -1
  132. package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +5 -1
  133. package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +4 -1
  134. package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +5 -1
  135. package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +4 -1
  136. package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +4 -1
  137. package/src/tools.ts +37 -35
  138. package/src/types.ts +4 -4
@@ -1,162 +1,164 @@
1
- ---
2
- import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
- import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
- import { ALL_TOOLS } from "../../index";
5
- import {
6
- UtilityHeader,
7
- FAQSection,
8
- Bibliography,
9
- SEORenderer,
10
- } from "@jjlmoya/utils-shared";
1
+ ---
2
+ import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
+ import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
+ import { ALL_TOOLS } from "../../index";
5
+ import {
6
+ UtilityHeader,
7
+ FAQSection,
8
+ Bibliography,
9
+ SEORenderer,
10
+ } from "@jjlmoya/utils-shared";
11
11
  import type { KnownLocale, ToolLocaleContent } from "../../types";
12
12
  import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
-
14
- export async function getStaticPaths() {
15
- const paths = [];
16
-
17
- for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
- const { default: Component } = await (lazyComp as () => Promise<{ default: unknown }>)();
19
- const localeEntries = Object.entries(entry.i18n) as [
20
- KnownLocale,
21
- () => Promise<ToolLocaleContent>,
22
- ][];
23
- const localeContents = await Promise.all(
24
- localeEntries.map(async ([locale, loader]) => ({
25
- locale,
26
- content: await loader(),
27
- })),
28
- );
29
-
30
- const localeUrls = Object.fromEntries(
31
- localeContents.map(({ locale, content }) => [
32
- locale,
33
- `/${locale}/${content.slug}`,
34
- ]),
35
- ) as Partial<Record<KnownLocale, string>>;
36
-
37
- const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
- const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
-
40
- for (const { locale, content } of localeContents) {
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[];
56
- paths.push({
57
- params: { locale, slug: content.slug },
58
- props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
59
- });
60
- }
61
- }
62
-
63
- return paths;
64
- }
65
-
66
- interface NavItem {
67
- id: string;
68
- title: string;
69
- href: string;
70
- isActive?: boolean;
71
- }
72
-
73
- interface Props {
74
- Component: unknown;
75
- locale: KnownLocale;
76
- content: ToolLocaleContent;
77
- localeUrls: Partial<Record<KnownLocale, string>>;
78
- allToolsNav: NavItem[];
79
- englishSlug: string;
80
- }
81
-
13
+ import type { AstroComponentFactory } from "astro/runtime/server/index.js";
14
+
15
+ export async function getStaticPaths() {
16
+ const paths = [];
17
+
18
+ for (const { entry, Component: lazyComp } of ALL_TOOLS) {
19
+ const { default: Component } = await (lazyComp as () => Promise<{ default: unknown }>)();
20
+ const localeEntries = Object.entries(entry.i18n) as [
21
+ KnownLocale,
22
+ () => Promise<ToolLocaleContent>,
23
+ ][];
24
+ const localeContents = await Promise.all(
25
+ localeEntries.map(async ([locale, loader]) => ({
26
+ locale,
27
+ content: await loader(),
28
+ })),
29
+ );
30
+
31
+ const localeUrls = Object.fromEntries(
32
+ localeContents.map(({ locale, content }) => [
33
+ locale,
34
+ `/${locale}/${content.slug}`,
35
+ ]),
36
+ ) as Partial<Record<KnownLocale, string>>;
37
+
38
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
39
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
40
+
41
+ for (const { locale, content } of localeContents) {
42
+ const allToolsNav = (
43
+ await Promise.all(
44
+ ALL_TOOLS.map(async ({ entry: navEntry }) => {
45
+ const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
46
+ if (!loader) return null;
47
+ const navContent = await loader();
48
+ return {
49
+ id: navEntry.id,
50
+ title: navContent.title,
51
+ href: `/${locale}/${navContent.slug}`,
52
+ isActive: navEntry.id === entry.id,
53
+ };
54
+ }),
55
+ )
56
+ ).filter(Boolean) as NavItem[];
57
+ paths.push({
58
+ params: { locale, slug: content.slug },
59
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
60
+ });
61
+ }
62
+ }
63
+
64
+ return paths;
65
+ }
66
+
67
+ interface NavItem {
68
+ id: string;
69
+ title: string;
70
+ href: string;
71
+ isActive?: boolean;
72
+ }
73
+
74
+ interface Props {
75
+ Component: unknown;
76
+ locale: KnownLocale;
77
+ content: ToolLocaleContent;
78
+ localeUrls: Partial<Record<KnownLocale, string>>;
79
+ allToolsNav: NavItem[];
80
+ englishSlug: string;
81
+ }
82
+
82
83
  const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props as 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 cssLoader = cssKey ? cssFiles[cssKey] : null;
87
- const toolCss = cssLoader ? await cssLoader() as string : "";
88
-
89
- const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
90
-
91
- const words = content.title.split(" ");
92
- const titleHighlight = words[0] || "";
93
- const titleBase = words.slice(1).join(" ") || "";
94
- ---
95
-
96
- <PreviewLayout
97
- title={content.title}
98
- currentLocale={locale}
99
- localeUrls={localeUrls}
100
- hasSidebar={true}
101
- >
102
- <PreviewNavSidebar
103
- slot="sidebar"
104
- categoryTitle="Tools"
105
- tools={allToolsNav}
106
- />
107
- <Fragment slot="head">
108
- {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
109
- {
110
- ( content.schemas ?? []).map((schema: unknown) => (
111
- <script
112
- is:inline
113
- type="application/ld+json"
114
- set:html={JSON.stringify(schema)}
115
- />
116
- ))
117
- }
118
- </Fragment>
119
-
120
- <div class="tool-page">
121
- <UtilityHeader
122
- titleHighlight={titleHighlight}
123
- titleBase={titleBase}
124
- description={content.description}
125
- />
126
-
127
- <section class="section-tool">
128
- <Component ui={content.ui} />
129
- </section>
130
-
131
- <section class="section-seo">
132
- <SEORenderer content={seoContent} />
133
- </section>
134
-
135
- <section class="section-faq">
136
- <FAQSection items={content.faq} />
137
- </section>
138
-
139
- <section class="section-bibliography">
140
- <Bibliography links={content.bibliography} />
141
- </section>
142
- </div>
143
- </PreviewLayout>
144
-
145
- <style>
146
- .tool-page {
147
- display: flex;
148
- flex-direction: column;
149
- gap: 2rem;
150
- }
151
- .section-tool {
152
- max-width: 1200px;
153
- margin: 0 auto;
154
- width: 100%;
155
- }
156
- .section-seo,
157
- .section-faq,
158
- .section-bibliography {
159
- padding-top: 2rem;
160
- border-top: 1px solid var(--border-color);
161
- }
162
- </style>
84
+ const ToolComponent = Component as AstroComponentFactory;
85
+
86
+ const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
87
+ const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
88
+ const cssLoader = cssKey ? cssFiles[cssKey] : null;
89
+ const toolCss = cssLoader ? await cssLoader() as string : "";
90
+
91
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
92
+
93
+ const words = content.title.split(" ");
94
+ const titleHighlight = words[0] || "";
95
+ const titleBase = words.slice(1).join(" ") || "";
96
+ ---
97
+
98
+ <PreviewLayout
99
+ title={content.title}
100
+ currentLocale={locale}
101
+ localeUrls={localeUrls}
102
+ hasSidebar={true}
103
+ >
104
+ <PreviewNavSidebar
105
+ slot="sidebar"
106
+ categoryTitle="Tools"
107
+ tools={allToolsNav}
108
+ />
109
+ <Fragment slot="head">
110
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
111
+ {
112
+ ( content.schemas ?? []).map((schema: unknown) => (
113
+ <script
114
+ is:inline
115
+ type="application/ld+json"
116
+ set:html={JSON.stringify(schema)}
117
+ />
118
+ ))
119
+ }
120
+ </Fragment>
121
+
122
+ <div class="tool-page">
123
+ <UtilityHeader
124
+ titleHighlight={titleHighlight}
125
+ titleBase={titleBase}
126
+ description={content.description}
127
+ />
128
+
129
+ <section class="section-tool">
130
+ <ToolComponent ui={content.ui} />
131
+ </section>
132
+
133
+ <section class="section-seo">
134
+ <SEORenderer content={seoContent} />
135
+ </section>
136
+
137
+ <section class="section-faq">
138
+ <FAQSection items={content.faq} />
139
+ </section>
140
+
141
+ <section class="section-bibliography">
142
+ <Bibliography links={content.bibliography} />
143
+ </section>
144
+ </div>
145
+ </PreviewLayout>
146
+
147
+ <style>
148
+ .tool-page {
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 2rem;
152
+ }
153
+ .section-tool {
154
+ max-width: 1200px;
155
+ margin: 0 auto;
156
+ width: 100%;
157
+ }
158
+ .section-seo,
159
+ .section-faq,
160
+ .section-bibliography {
161
+ padding-top: 2rem;
162
+ border-top: 1px solid var(--border-color);
163
+ }
164
+ </style>
@@ -28,7 +28,7 @@ const toolsWithContent = tools.length > 0
28
28
  tools.map(async ({ entry, Component }) => {
29
29
  const languages = Object.keys(entry.i18n) as KnownLocale[];
30
30
  const localeEntries = await Promise.all(
31
- languages.map(async (l) => {
31
+ languages.map(async (l) => {
32
32
  const loader = entry.i18n[l];
33
33
  if (!loader) return null;
34
34
  const content = await loader();
@@ -36,7 +36,10 @@ const toolsWithContent = tools.length > 0
36
36
  })
37
37
  );
38
38
 
39
- const localeContents = Object.fromEntries(localeEntries.filter(Boolean)) as Record<string, ToolLocaleContent<Record<string, string>>>;
39
+ const availableLocaleEntries = localeEntries.filter(
40
+ (localeEntry): localeEntry is readonly [KnownLocale, ToolLocaleContent<Record<string, string>>] => localeEntry !== null,
41
+ );
42
+ const localeContents = Object.fromEntries(availableLocaleEntries) as Record<string, ToolLocaleContent<Record<string, string>>>;
40
43
  const currentLocaleContent = localeContents[currentLocale] || localeContents.en;
41
44
  const availableLocales: Record<string, string> = {};
42
45
 
@@ -256,6 +259,6 @@ const toolsWithContent = tools.length > 0
256
259
  border-color: var(--accent);
257
260
  background: rgba(244, 63, 94, 0.1);
258
261
  }
259
- </style>
260
-
261
-
262
+ </style>
263
+
264
+
@@ -1,3 +1,3 @@
1
1
  ---
2
2
  ---
3
- <meta http-equiv="refresh" content="0;url=/en" />
3
+ <meta http-equiv="refresh" content="0;url=/en" />
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync, existsSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+
5
+ function findBibliographyAstroFiles(dir: string): string[] {
6
+ const files: string[] = [];
7
+ if (!existsSync(dir)) return files;
8
+ const entries = readdirSync(dir, { withFileTypes: true });
9
+
10
+ for (const entry of entries) {
11
+ const fullPath = join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ files.push(...findBibliographyAstroFiles(fullPath));
14
+ } else if (entry.isFile() && entry.name === 'bibliography.astro') {
15
+ files.push(fullPath);
16
+ }
17
+ }
18
+
19
+ return files;
20
+ }
21
+
22
+ const toolDir = join(process.cwd(), 'src', 'tool');
23
+ const bibliographyFiles = findBibliographyAstroFiles(toolDir);
24
+
25
+ describe('Bibliography Component Wellformed Export', () => {
26
+ it('found tool bibliography.astro components', () => {
27
+ expect(bibliographyFiles.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ bibliographyFiles.forEach((file) => {
31
+ const relativePath = relative(process.cwd(), file);
32
+
33
+ it(`${relativePath} should use SharedBibliography from @jjlmoya/utils-shared`, () => {
34
+ const content = readFileSync(file, 'utf-8');
35
+
36
+ const usesSharedComponent =
37
+ content.includes('@jjlmoya/utils-shared') &&
38
+ (content.includes('Bibliography') || content.includes('SharedBibliography'));
39
+
40
+ expect(
41
+ usesSharedComponent,
42
+ `File "${relativePath}" does not use the standard Bibliography component from @jjlmoya/utils-shared, resulting in unstyled or raw bibliography output.`,
43
+ ).toBe(true);
44
+ });
45
+ });
46
+ });
@@ -98,7 +98,7 @@ describe('Diacritics density validation', () => {
98
98
  if (!loader) return;
99
99
 
100
100
  const content = await loader();
101
- const text = normalizeText(translatableContent(content as Record<string, unknown>));
101
+ const text = normalizeText(translatableContent(content as unknown as Record<string, unknown>));
102
102
  const rule = DIACRITIC_RULES[typedLocale];
103
103
  const letters = letterCount(text);
104
104
  const matches = diacriticCount(text, typedLocale);
@@ -1,19 +1,19 @@
1
- import { describe, it, expect } from 'vitest';
2
- import type * as DATA from '../data';
3
-
4
- const TOOLS: typeof DATA.scienceCategory[] = [];
5
-
6
- describe('FAQ Content Validation', () => {
7
- TOOLS.forEach((entry) => {
8
- describe(`Tool: ${entry.icon}`, () => {
9
- it('placeholder', () => {
10
- expect(true).toBe(true);
11
- });
12
- });
13
- });
14
-
15
- it('no tools registered yet', () => {
16
- expect(TOOLS.length).toBe(0);
17
- });
18
- });
19
-
1
+ import { describe, it, expect } from 'vitest';
2
+ import type * as DATA from '../data';
3
+
4
+ const TOOLS: typeof DATA.scienceCategory[] = [];
5
+
6
+ describe('FAQ Content Validation', () => {
7
+ TOOLS.forEach((entry) => {
8
+ describe(`Tool: ${entry.icon}`, () => {
9
+ it('placeholder', () => {
10
+ expect(true).toBe(true);
11
+ });
12
+ });
13
+ });
14
+
15
+ it('no tools registered yet', () => {
16
+ expect(TOOLS.length).toBe(0);
17
+ });
18
+ });
19
+
@@ -1,36 +1,36 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
-
4
- const EXPECTED_LOCALES = [
5
- 'de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh'
6
- ];
7
-
8
- describe('I18n Coverage Validation', () => {
9
- it('all tools should be registered', () => {
10
- expect(ALL_TOOLS.length).toBeGreaterThan(0);
11
- });
12
-
13
- ALL_TOOLS.forEach(({ entry }: { entry: any }) => {
14
- describe(`Tool: ${entry.id}`, () => {
15
- it('should have all 15 required locales', () => {
16
- const registeredLocales = Object.keys(entry.i18n);
17
- EXPECTED_LOCALES.forEach((locale) => {
18
- expect(
19
- registeredLocales,
20
- `Tool "${entry.id}" is missing locale "${locale}"`,
21
- ).toContain(locale);
22
- });
23
- });
24
-
25
- it('all locale loaders should be functions', () => {
26
- EXPECTED_LOCALES.forEach((locale) => {
27
- const loader = entry.i18n[locale as keyof typeof entry.i18n];
28
- expect(
29
- typeof loader,
30
- `Tool "${entry.id}" locale "${locale}" loader is not a function`,
31
- ).toBe('function');
32
- });
33
- });
34
- });
35
- });
36
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ const EXPECTED_LOCALES = [
5
+ 'de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh'
6
+ ];
7
+
8
+ describe('I18n Coverage Validation', () => {
9
+ it('all tools should be registered', () => {
10
+ expect(ALL_TOOLS.length).toBeGreaterThan(0);
11
+ });
12
+
13
+ ALL_TOOLS.forEach(({ entry }: { entry: any }) => {
14
+ describe(`Tool: ${entry.id}`, () => {
15
+ it('should have all 15 required locales', () => {
16
+ const registeredLocales = Object.keys(entry.i18n);
17
+ EXPECTED_LOCALES.forEach((locale) => {
18
+ expect(
19
+ registeredLocales,
20
+ `Tool "${entry.id}" is missing locale "${locale}"`,
21
+ ).toContain(locale);
22
+ });
23
+ });
24
+
25
+ it('all locale loaders should be functions', () => {
26
+ EXPECTED_LOCALES.forEach((locale) => {
27
+ const loader = entry.i18n[locale as keyof typeof entry.i18n];
28
+ expect(
29
+ typeof loader,
30
+ `Tool "${entry.id}" locale "${locale}" loader is not a function`,
31
+ ).toBe('function');
32
+ });
33
+ });
34
+ });
35
+ });
36
+ });
@@ -60,7 +60,7 @@ describe('Inverted punctuation validation', () => {
60
60
 
61
61
  const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
62
62
  const content = await loader();
63
- const strings = translatableStrings(content as Record<string, unknown>);
63
+ const strings = translatableStrings(content as unknown as Record<string, unknown>);
64
64
  const missingQuestions = strings.flatMap((text) =>
65
65
  findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd)
66
66
  );
@@ -18,7 +18,7 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
  });
20
20
 
21
- it('all 17 tools registered', () => {
22
- expect(ALL_TOOLS.length).toBe(17);
21
+ it('all 18 tools registered', () => {
22
+ expect(ALL_TOOLS.length).toBe(18);
23
23
  });
24
24
  });
@@ -1,2 +1,2 @@
1
1
  export default function () { return null; }
2
-
2
+
@@ -45,4 +45,4 @@ describe('No H1 in Components', () => {
45
45
  });
46
46
  });
47
47
  });
48
-
48
+