@jjlmoya/utils-science 1.19.0 → 1.21.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 (164) hide show
  1. package/package.json +2 -1
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/fr.ts +6 -6
  4. package/src/category/i18n/ru.ts +1 -1
  5. package/src/category/index.ts +3 -1
  6. package/src/category/seo.astro +2 -2
  7. package/src/entries.ts +5 -1
  8. package/src/index.ts +2 -0
  9. package/src/pages/[locale]/[slug].astro +32 -15
  10. package/src/tests/locale_completeness.test.ts +5 -22
  11. package/src/tests/no_en_dash.test.ts +70 -0
  12. package/src/tests/seo_length.test.ts +5 -3
  13. package/src/tests/shared-test-helpers.ts +56 -0
  14. package/src/tests/title_quality.test.ts +1 -1
  15. package/src/tests/tool_exports.test.ts +34 -0
  16. package/src/tests/tool_validation.test.ts +2 -2
  17. package/src/tool/asteroid-impact/bibliography.astro +2 -2
  18. package/src/tool/asteroid-impact/bibliography.ts +24 -0
  19. package/src/tool/asteroid-impact/component.astro +16 -9
  20. package/src/tool/asteroid-impact/i18n/de.ts +4 -24
  21. package/src/tool/asteroid-impact/i18n/en.ts +4 -24
  22. package/src/tool/asteroid-impact/i18n/es.ts +4 -24
  23. package/src/tool/asteroid-impact/i18n/fr.ts +10 -30
  24. package/src/tool/asteroid-impact/i18n/id.ts +4 -24
  25. package/src/tool/asteroid-impact/i18n/it.ts +4 -24
  26. package/src/tool/asteroid-impact/i18n/ja.ts +4 -24
  27. package/src/tool/asteroid-impact/i18n/ko.ts +4 -24
  28. package/src/tool/asteroid-impact/i18n/nl.ts +4 -24
  29. package/src/tool/asteroid-impact/i18n/pl.ts +4 -24
  30. package/src/tool/asteroid-impact/i18n/pt.ts +4 -24
  31. package/src/tool/asteroid-impact/i18n/ru.ts +8 -28
  32. package/src/tool/asteroid-impact/i18n/sv.ts +4 -24
  33. package/src/tool/asteroid-impact/i18n/tr.ts +4 -24
  34. package/src/tool/asteroid-impact/i18n/zh.ts +4 -24
  35. package/src/tool/asteroid-impact/index.ts +1 -0
  36. package/src/tool/asteroid-impact/script.ts +13 -7
  37. package/src/tool/asteroid-impact/seo.astro +1 -1
  38. package/src/tool/cellular-renewal/bibliography.astro +2 -2
  39. package/src/tool/cellular-renewal/bibliography.ts +24 -0
  40. package/src/tool/cellular-renewal/i18n/de.ts +3 -24
  41. package/src/tool/cellular-renewal/i18n/en.ts +3 -24
  42. package/src/tool/cellular-renewal/i18n/es.ts +3 -24
  43. package/src/tool/cellular-renewal/i18n/fr.ts +16 -37
  44. package/src/tool/cellular-renewal/i18n/id.ts +3 -24
  45. package/src/tool/cellular-renewal/i18n/it.ts +3 -24
  46. package/src/tool/cellular-renewal/i18n/ja.ts +3 -24
  47. package/src/tool/cellular-renewal/i18n/ko.ts +3 -24
  48. package/src/tool/cellular-renewal/i18n/nl.ts +3 -24
  49. package/src/tool/cellular-renewal/i18n/pl.ts +3 -24
  50. package/src/tool/cellular-renewal/i18n/pt.ts +3 -24
  51. package/src/tool/cellular-renewal/i18n/ru.ts +20 -41
  52. package/src/tool/cellular-renewal/i18n/sv.ts +3 -24
  53. package/src/tool/cellular-renewal/i18n/tr.ts +3 -24
  54. package/src/tool/cellular-renewal/i18n/zh.ts +12 -33
  55. package/src/tool/cellular-renewal/index.ts +1 -0
  56. package/src/tool/cellular-renewal/seo.astro +2 -1
  57. package/src/tool/colony-counter/bibliography.astro +2 -2
  58. package/src/tool/colony-counter/bibliography.ts +12 -0
  59. package/src/tool/colony-counter/i18n/de.ts +3 -12
  60. package/src/tool/colony-counter/i18n/en.ts +3 -12
  61. package/src/tool/colony-counter/i18n/es.ts +3 -12
  62. package/src/tool/colony-counter/i18n/fr.ts +3 -12
  63. package/src/tool/colony-counter/i18n/id.ts +3 -12
  64. package/src/tool/colony-counter/i18n/it.ts +3 -12
  65. package/src/tool/colony-counter/i18n/ja.ts +3 -12
  66. package/src/tool/colony-counter/i18n/ko.ts +3 -12
  67. package/src/tool/colony-counter/i18n/nl.ts +3 -12
  68. package/src/tool/colony-counter/i18n/pl.ts +3 -12
  69. package/src/tool/colony-counter/i18n/pt.ts +3 -12
  70. package/src/tool/colony-counter/i18n/ru.ts +8 -17
  71. package/src/tool/colony-counter/i18n/sv.ts +3 -12
  72. package/src/tool/colony-counter/i18n/tr.ts +3 -12
  73. package/src/tool/colony-counter/i18n/zh.ts +5 -14
  74. package/src/tool/colony-counter/index.ts +1 -0
  75. package/src/tool/colony-counter/seo.astro +1 -1
  76. package/src/tool/cosmic-inflation/bibliography.astro +14 -0
  77. package/src/tool/cosmic-inflation/bibliography.ts +12 -0
  78. package/src/tool/cosmic-inflation/component.astro +270 -0
  79. package/src/tool/cosmic-inflation/cosmic-inflation-calculator.css +277 -0
  80. package/src/tool/cosmic-inflation/entry.ts +26 -0
  81. package/src/tool/cosmic-inflation/i18n/de.ts +188 -0
  82. package/src/tool/cosmic-inflation/i18n/en.ts +188 -0
  83. package/src/tool/cosmic-inflation/i18n/es.ts +168 -0
  84. package/src/tool/cosmic-inflation/i18n/fr.ts +188 -0
  85. package/src/tool/cosmic-inflation/i18n/id.ts +188 -0
  86. package/src/tool/cosmic-inflation/i18n/it.ts +188 -0
  87. package/src/tool/cosmic-inflation/i18n/ja.ts +188 -0
  88. package/src/tool/cosmic-inflation/i18n/ko.ts +188 -0
  89. package/src/tool/cosmic-inflation/i18n/nl.ts +188 -0
  90. package/src/tool/cosmic-inflation/i18n/pl.ts +188 -0
  91. package/src/tool/cosmic-inflation/i18n/pt.ts +188 -0
  92. package/src/tool/cosmic-inflation/i18n/ru.ts +188 -0
  93. package/src/tool/cosmic-inflation/i18n/sv.ts +188 -0
  94. package/src/tool/cosmic-inflation/i18n/tr.ts +188 -0
  95. package/src/tool/cosmic-inflation/i18n/zh.ts +188 -0
  96. package/src/tool/cosmic-inflation/index.ts +11 -0
  97. package/src/tool/cosmic-inflation/logic/CosmicInflationEngine.ts +21 -0
  98. package/src/tool/cosmic-inflation/seo.astro +15 -0
  99. package/src/tool/microwave-detector/bibliography.astro +2 -2
  100. package/src/tool/microwave-detector/bibliography.ts +16 -0
  101. package/src/tool/microwave-detector/component.astro +9 -7
  102. package/src/tool/microwave-detector/i18n/de.ts +3 -16
  103. package/src/tool/microwave-detector/i18n/en.ts +3 -16
  104. package/src/tool/microwave-detector/i18n/es.ts +3 -16
  105. package/src/tool/microwave-detector/i18n/fr.ts +7 -20
  106. package/src/tool/microwave-detector/i18n/id.ts +3 -16
  107. package/src/tool/microwave-detector/i18n/it.ts +3 -16
  108. package/src/tool/microwave-detector/i18n/ja.ts +3 -16
  109. package/src/tool/microwave-detector/i18n/ko.ts +3 -16
  110. package/src/tool/microwave-detector/i18n/nl.ts +3 -16
  111. package/src/tool/microwave-detector/i18n/pl.ts +3 -16
  112. package/src/tool/microwave-detector/i18n/pt.ts +3 -16
  113. package/src/tool/microwave-detector/i18n/ru.ts +21 -34
  114. package/src/tool/microwave-detector/i18n/sv.ts +3 -16
  115. package/src/tool/microwave-detector/i18n/tr.ts +3 -16
  116. package/src/tool/microwave-detector/i18n/zh.ts +13 -26
  117. package/src/tool/microwave-detector/index.ts +1 -0
  118. package/src/tool/microwave-detector/logic/MicrowaveEngine.ts +5 -1
  119. package/src/tool/microwave-detector/microwave-leak-detector.css +22 -25
  120. package/src/tool/microwave-detector/seo.astro +2 -1
  121. package/src/tool/simulation-probability/bibliography.astro +2 -2
  122. package/src/tool/simulation-probability/bibliography.ts +24 -0
  123. package/src/tool/simulation-probability/i18n/de.ts +3 -24
  124. package/src/tool/simulation-probability/i18n/en.ts +3 -24
  125. package/src/tool/simulation-probability/i18n/es.ts +3 -24
  126. package/src/tool/simulation-probability/i18n/fr.ts +8 -29
  127. package/src/tool/simulation-probability/i18n/id.ts +3 -24
  128. package/src/tool/simulation-probability/i18n/it.ts +3 -24
  129. package/src/tool/simulation-probability/i18n/ja.ts +3 -24
  130. package/src/tool/simulation-probability/i18n/ko.ts +3 -24
  131. package/src/tool/simulation-probability/i18n/nl.ts +3 -24
  132. package/src/tool/simulation-probability/i18n/pl.ts +3 -24
  133. package/src/tool/simulation-probability/i18n/pt.ts +3 -24
  134. package/src/tool/simulation-probability/i18n/ru.ts +10 -31
  135. package/src/tool/simulation-probability/i18n/sv.ts +3 -24
  136. package/src/tool/simulation-probability/i18n/tr.ts +3 -24
  137. package/src/tool/simulation-probability/i18n/zh.ts +7 -28
  138. package/src/tool/simulation-probability/index.ts +1 -0
  139. package/src/tool/simulation-probability/seo.astro +2 -1
  140. package/src/tool/temperature-timeline/bibliography.astro +14 -0
  141. package/src/tool/temperature-timeline/bibliography.ts +12 -0
  142. package/src/tool/temperature-timeline/component.astro +289 -0
  143. package/src/tool/temperature-timeline/entry.ts +26 -0
  144. package/src/tool/temperature-timeline/i18n/de.ts +213 -0
  145. package/src/tool/temperature-timeline/i18n/en.ts +213 -0
  146. package/src/tool/temperature-timeline/i18n/es.ts +178 -0
  147. package/src/tool/temperature-timeline/i18n/fr.ts +213 -0
  148. package/src/tool/temperature-timeline/i18n/id.ts +213 -0
  149. package/src/tool/temperature-timeline/i18n/it.ts +213 -0
  150. package/src/tool/temperature-timeline/i18n/ja.ts +213 -0
  151. package/src/tool/temperature-timeline/i18n/ko.ts +213 -0
  152. package/src/tool/temperature-timeline/i18n/nl.ts +213 -0
  153. package/src/tool/temperature-timeline/i18n/pl.ts +213 -0
  154. package/src/tool/temperature-timeline/i18n/pt.ts +213 -0
  155. package/src/tool/temperature-timeline/i18n/ru.ts +213 -0
  156. package/src/tool/temperature-timeline/i18n/sv.ts +213 -0
  157. package/src/tool/temperature-timeline/i18n/tr.ts +213 -0
  158. package/src/tool/temperature-timeline/i18n/zh.ts +213 -0
  159. package/src/tool/temperature-timeline/index.ts +11 -0
  160. package/src/tool/temperature-timeline/logic/TemperatureTimelineEngine.ts +58 -0
  161. package/src/tool/temperature-timeline/planet-temperature-timeline.css +158 -0
  162. package/src/tool/temperature-timeline/seo.astro +15 -0
  163. package/src/tools.ts +4 -0
  164. package/src/types.ts +1 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-science",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -50,6 +50,7 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@astrojs/check": "^0.9.8",
53
+ "@types/leaflet": "^1.9.21",
53
54
  "eslint": "^9.39.4",
54
55
  "eslint-plugin-astro": "^1.6.0",
55
56
  "eslint-plugin-no-comments": "^1.1.10",
@@ -12,7 +12,7 @@ export const content: CategoryLocaleContent = {
12
12
  },
13
13
  {
14
14
  type: 'paragraph',
15
- html: '<p>Wissenschaft ist nicht nur ein statisches Wissensgebiet, sondern ein dynamischer Prozess der Erkundung und des Experimentierens. In diesem Bereich bieten wir kostenlose Online-Tools an, die komplexe wissenschaftliche Konzepte durch Simulation und Datenanalyse jedem Nutzer näherbringen. Von der Mikrobiologie bis zur Astrophysik wenden unsere Hilfsprogramme mathematische Modelle und physikalische Theorien an, um Ihnen eine interaktive Perspektive auf das Universum zu bieten.</p><p>Ob Sie nun koloniebildende Einheiten in einem Labor zählen müssen oder die Wahrscheinlichkeit schätzen möchten, dass wir in einer virtuellen Umgebung leben unsere Rechner bieten technische Präzision mit einer auf Neugier ausgerichteten Benutzeroberfläche.</p>',
15
+ html: '<p>Wissenschaft ist nicht nur ein statisches Wissensgebiet, sondern ein dynamischer Prozess der Erkundung und des Experimentierens. In diesem Bereich bieten wir kostenlose Online-Tools an, die komplexe wissenschaftliche Konzepte durch Simulation und Datenanalyse jedem Nutzer näherbringen. Von der Mikrobiologie bis zur Astrophysik wenden unsere Hilfsprogramme mathematische Modelle und physikalische Theorien an, um Ihnen eine interaktive Perspektive auf das Universum zu bieten.</p><p>Ob Sie nun koloniebildende Einheiten in einem Labor zählen müssen oder die Wahrscheinlichkeit schätzen möchten, dass wir in einer virtuellen Umgebung leben - unsere Rechner bieten technische Präzision mit einer auf Neugier ausgerichteten Benutzeroberfläche.</p>',
16
16
  },
17
17
  {
18
18
  type: 'title',
@@ -7,7 +7,7 @@ export const content: CategoryLocaleContent = {
7
7
  seo: [
8
8
  {
9
9
  type: 'title',
10
- text: 'Exploration Scientifique et Simulation : La Science Entre vos Mains',
10
+ text: 'Exploration Scientifique et Simulation: La Science Entre vos Mains',
11
11
  level: 2,
12
12
  },
13
13
  {
@@ -16,7 +16,7 @@ export const content: CategoryLocaleContent = {
16
16
  },
17
17
  {
18
18
  type: 'title',
19
- text: 'Astrophysique et Risque Cosmique : L\'Impact des Astéroïdes',
19
+ text: 'Astrophysique et Risque Cosmique: L\'Impact des Astéroïdes',
20
20
  level: 2,
21
21
  },
22
22
  {
@@ -68,10 +68,10 @@ export const content: CategoryLocaleContent = {
68
68
  type: 'summary',
69
69
  title: 'Ce qui nous définit :',
70
70
  items: [
71
- 'Modélisation Mathématique : Simulations basées sur des lois physiques réelles (gravitation, thermodynamique, balistique).',
72
- 'Éducation Interactif : Outils conçus pour que les étudiants et les passionnés expérimentent avec des variables scientifiques.',
73
- 'Rigueur Technique : Données et formules extraites de publications scientifiques et de bases de données académiques.',
74
- 'Confidentialité des Données Scientifiques : Tous les processus de calcul et de simulation s\'exécutent localement pour garantir la sécurité de votre recherche.',
71
+ 'Modélisation Mathématique: Simulations basées sur des lois physiques réelles (gravitation, thermodynamique, balistique).',
72
+ 'Éducation Interactif: Outils conçus pour que les étudiants et les passionnés expérimentent avec des variables scientifiques.',
73
+ 'Rigueur Technique: Données et formules extraites de publications scientifiques et de bases de données académiques.',
74
+ 'Confidentialité des Données Scientifiques: Tous les processus de calcul et de simulation s\'exécutent localement pour garantir la sécurité de votre recherche.',
75
75
  ],
76
76
  },
77
77
  {
@@ -12,7 +12,7 @@ export const content: CategoryLocaleContent = {
12
12
  },
13
13
  {
14
14
  type: 'paragraph',
15
- html: '<p>Наука это не просто статический набор знаний, а динамичный процесс исследования и экспериментирования. В этом разделе мы предлагаем бесплатные онлайн-инструменты, разработанные для того, чтобы донести сложные научные концепции до любого пользователя посредством моделирования и анализа данных. От микробиологии до астрофизики наши утилиты применяют математические модели и физические теории, чтобы предложить вам интерактивный взгляд на Вселенную.</p><p>Если вам нужно подсчитать количество колониеобразующих единиц в лаборатории или вы хотите оценить вероятность того, что мы живем в виртуальной среде, наши калькуляторы обеспечивают техническую точность и интерфейс, созданный для любознательных умов.</p>',
15
+ html: '<p>Наука - это не просто статический набор знаний, а динамичный процесс исследования и экспериментирования. В этом разделе мы предлагаем бесплатные онлайн-инструменты, разработанные для того, чтобы донести сложные научные концепции до любого пользователя посредством моделирования и анализа данных. От микробиологии до астрофизики наши утилиты применяют математические модели и физические теории, чтобы предложить вам интерактивный взгляд на Вселенную.</p><p>Если вам нужно подсчитать количество колониеобразующих единиц в лаборатории или вы хотите оценить вероятность того, что мы живем в виртуальной среде, наши калькуляторы обеспечивают техническую точность и интерфейс, созданный для любознательных умов.</p>',
16
16
  },
17
17
  {
18
18
  type: 'title',
@@ -4,10 +4,12 @@ import { asteroidImpact } from '../tool/asteroid-impact/index';
4
4
  import { microwaveDetector } from '../tool/microwave-detector/index';
5
5
  import { simulationProbability } from '../tool/simulation-probability/index';
6
6
  import { cellularRenewal } from '../tool/cellular-renewal/index';
7
+ import { cosmicInflation } from '../tool/cosmic-inflation/index';
8
+ import { temperatureTimeline } from '../tool/temperature-timeline/index';
7
9
 
8
10
  export const scienceCategory: ScienceCategoryEntry = {
9
11
  icon: 'mdi:flask',
10
- tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal],
12
+ tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline],
11
13
  i18n: {
12
14
  es: () => import('./i18n/es').then((m) => m.content),
13
15
  en: () => import('./i18n/en').then((m) => m.content),
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  import { SEORenderer } from '@jjlmoya/utils-shared';
3
3
  import { scienceCategory } from './index';
4
- import type { KnownLocale } from '../../types';
4
+ import type { KnownLocale } from '../types';
5
5
 
6
6
  interface Props {
7
7
  locale?: KnownLocale;
8
8
  }
9
9
 
10
10
  const { locale = 'es' } = Astro.props;
11
- const content = await scienceCategory.i18n[locale]?.();
11
+ const content = await scienceCategory.i18n[locale as KnownLocale]?.();
12
12
  if (!content) return null;
13
13
  ---
14
14
 
package/src/entries.ts CHANGED
@@ -4,10 +4,14 @@ export { colonyCounter } from './tool/colony-counter/entry';
4
4
  export type { ColonyCounterUI, ColonyCounterLocaleContent } from './tool/colony-counter/entry';
5
5
  export { microwaveDetector } from './tool/microwave-detector/entry';
6
6
  export { simulationProbability } from './tool/simulation-probability/entry';
7
+ export { cosmicInflation } from './tool/cosmic-inflation/entry';
8
+ export { temperatureTimeline } from './tool/temperature-timeline/entry';
7
9
  export { scienceCategory } from './category';
8
10
  import { asteroidImpact } from './tool/asteroid-impact/entry';
9
11
  import { cellularRenewal } from './tool/cellular-renewal/entry';
10
12
  import { colonyCounter } from './tool/colony-counter/entry';
11
13
  import { microwaveDetector } from './tool/microwave-detector/entry';
12
14
  import { simulationProbability } from './tool/simulation-probability/entry';
13
- export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability];
15
+ import { cosmicInflation } from './tool/cosmic-inflation/entry';
16
+ import { temperatureTimeline } from './tool/temperature-timeline/entry';
17
+ export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline];
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@ export { ASTEROID_IMPACT_TOOL } from './tool/asteroid-impact/index';
5
5
  export { MICROWAVE_DETECTOR_TOOL } from './tool/microwave-detector/index';
6
6
  export { SIMULATION_PROBABILITY_TOOL } from './tool/simulation-probability/index';
7
7
  export { CELLULAR_RENEWAL_TOOL } from './tool/cellular-renewal/index';
8
+ export { COSMIC_INFLATION_TOOL } from './tool/cosmic-inflation/index';
9
+ export { TEMPERATURE_TIMELINE_TOOL } from './tool/temperature-timeline/index';
8
10
 
9
11
  export type {
10
12
  KnownLocale,
@@ -15,7 +15,7 @@ export async function getStaticPaths() {
15
15
  const paths = [];
16
16
 
17
17
  for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
- const { default: Component } = await lazyComp();
18
+ const { default: Component } = await (lazyComp as () => Promise<{ default: unknown }>)();
19
19
  const localeEntries = Object.entries(entry.i18n) as [
20
20
  KnownLocale,
21
21
  () => Promise<ToolLocaleContent>,
@@ -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,17 @@ 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 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 : "";
72
88
 
73
- const seoContent: UtilitySEOContent = { locale, sections: content.seo };
89
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
74
90
 
75
91
  const words = content.title.split(" ");
76
92
  const titleHighlight = words[0] || "";
@@ -89,8 +105,9 @@ const titleBase = words.slice(1).join(" ") || "";
89
105
  tools={allToolsNav}
90
106
  />
91
107
  <Fragment slot="head">
108
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
92
109
  {
93
- content.schemas.map((schema: unknown) => (
110
+ ( content.schemas ?? []).map((schema: unknown) => (
94
111
  <script
95
112
  is:inline
96
113
  type="application/ld+json"
@@ -116,11 +133,11 @@ const titleBase = words.slice(1).join(" ") || "";
116
133
  </section>
117
134
 
118
135
  <section class="section-faq">
119
- <FAQSection items={content.faq} title={content.ui.faqTitle} inLanguage={locale} />
136
+ <FAQSection items={content.faq} />
120
137
  </section>
121
138
 
122
139
  <section class="section-bibliography">
123
- <Bibliography links={content.bibliography} title={content.ui.bibliographyTitle} />
140
+ <Bibliography links={content.bibliography} />
124
141
  </section>
125
142
  </div>
126
143
  </PreviewLayout>
@@ -7,36 +7,19 @@ 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('faq and bibliography should be arrays', 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
- }
20
- });
21
-
22
- it('bibliographyTitle should be defined when bibliography items exist', async () => {
23
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
24
- 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
- }
13
+ expect(Array.isArray(content.faq)).toBe(true);
14
+ expect(Array.isArray(content.bibliography)).toBe(true);
32
15
  });
33
16
  });
34
17
  });
35
18
  });
36
19
  });
37
20
 
38
- it('all 5 tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(5);
21
+ it('all 7 tools registered', () => {
22
+ expect(ALL_TOOLS.length).toBe(7);
40
23
  });
41
24
  });
42
25
 
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ function getFiles(dir: string): string[] {
6
+ const results: string[] = [];
7
+ if (!fs.existsSync(dir)) {
8
+ return results;
9
+ }
10
+ const list = fs.readdirSync(dir);
11
+ for (const file of list) {
12
+ const fullPath = path.join(dir, file);
13
+ const stat = fs.statSync(fullPath);
14
+ if (stat && stat.isDirectory()) {
15
+ if (file !== 'tests' && file !== 'node_modules' && file !== '.astro') {
16
+ results.push(...getFiles(fullPath));
17
+ }
18
+ } else {
19
+ results.push(fullPath);
20
+ }
21
+ }
22
+ return results;
23
+ }
24
+
25
+ function isContentFile(filePath: string): boolean {
26
+ return /\\i18n\\/.test(filePath) || filePath.endsWith('bibliography.ts');
27
+ }
28
+
29
+ const srcDir = path.join(process.cwd(), 'src');
30
+ const scriptsDir = path.join(process.cwd(), 'scripts');
31
+ const filesToTest = [
32
+ ...getFiles(srcDir).filter(isContentFile),
33
+ ...getFiles(scriptsDir).filter(isContentFile),
34
+ ];
35
+
36
+ const aiTypographyGarbage = [
37
+ '\u2013',
38
+ '\u2014',
39
+ '\u2026',
40
+ '\u201C',
41
+ '\u201D',
42
+ '\u2018',
43
+ '\u2019',
44
+ '\u00AB',
45
+ '\u00BB',
46
+ '\u200B',
47
+ '\u201E',
48
+ ];
49
+
50
+ describe('Typography Garbage Character Validation', () => {
51
+ filesToTest.forEach((filePath) => {
52
+ const relativePath = path.relative(process.cwd(), filePath);
53
+ it(`should not contain typography garbage characters in ${relativePath}`, () => {
54
+ const content = fs.readFileSync(filePath, 'utf-8');
55
+ const hasAiPatterns = aiTypographyGarbage.some(char => content.includes(char));
56
+ expect(hasAiPatterns).toBe(false);
57
+ });
58
+
59
+ it(`should not contain space before colon in ${relativePath}`, () => {
60
+ const content = fs.readFileSync(filePath, 'utf-8');
61
+ const spaceBeforeColon = / : /.test(content);
62
+ expect(spaceBeforeColon).toBe(false);
63
+ });
64
+
65
+ it(`should not contain double hyphen in ${relativePath}`, () => {
66
+ const content = fs.readFileSync(filePath, 'utf-8');
67
+ expect(content).not.toContain('--');
68
+ });
69
+ });
70
+ });
@@ -11,9 +11,11 @@ describe('SEO Content Length Validation', () => {
11
11
  Object.keys(entry.i18n).forEach((locale) => {
12
12
  it(`${locale}: SEO section should exist`, async () => {
13
13
  const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
14
- const content = await loader();
15
- if (!content.seo) return;
16
- expect(Array.isArray(content.seo)).toBe(true);
14
+ if (loader) {
15
+ const content = await loader();
16
+ if (!content.seo) return;
17
+ expect(Array.isArray(content.seo)).toBe(true);
18
+ }
17
19
  });
18
20
  });
19
21
  });
@@ -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
+ }
@@ -41,7 +41,7 @@ describe('Project Titles - Separator Validation', () => {
41
41
  let match;
42
42
  while ((match = pattern.exec(content)) !== null) {
43
43
  const title = match[1];
44
- if (title.includes('|') || title.includes('-')) {
44
+ if (title && (title.includes('|') || title.includes('-'))) {
45
45
  findings.push(title);
46
46
  }
47
47
  }
@@ -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
+ });
@@ -4,8 +4,8 @@ import { scienceCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 5 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(5);
7
+ it('should have 7 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(7);
9
9
  });
10
10
 
11
11
  it('scienceCategory should be defined', () => {
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { Bibliography } from '@jjlmoya/utils-shared';
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
3
  import { asteroidImpact } from './index';
4
4
  import type { KnownLocale } from '../../types';
5
5
 
@@ -11,4 +11,4 @@ const { locale = 'es' } = Astro.props;
11
11
  const content = await asteroidImpact.i18n[locale]?.();
12
12
  ---
13
13
 
14
- {content && <Bibliography links={content.bibliography} title={content.ui.bibliographyTitle} />}
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,24 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Collins, G. S., et al. (2005). Earth Impact Effects Program: A Web-based computer program for calculating the regional environmental consequences of a meteoroid impact on Earth.',
6
+ url: 'https://impact.ese.ic.ac.uk/ImpactEarth/',
7
+ },
8
+ {
9
+ name: 'Toon, O. B., et al. (1997). Environmental perturbations caused by the impacts of asteroids and comets. Reviews of Geophysics.',
10
+ url: 'https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/96RG03038',
11
+ },
12
+ {
13
+ name: 'Chapman, C. R., & Morrison, D. (1994). Impacts on the Earth by asteroids and comets: assessing the hazard. Nature.',
14
+ url: 'https://www.nature.com/articles/367033a0',
15
+ },
16
+ {
17
+ name: 'Schulte, P., et al. (2010). The Chicxulub Asteroid Impact and Mass Extinction at the Cretaceous-Paleogene Boundary. Science.',
18
+ url: 'https://www.science.org/doi/10.1126/science.1177265',
19
+ },
20
+ {
21
+ name: 'Brown, P., et al. (2013). A 500-kiloton airburst over Chelyabinsk and an enhanced hazard from small impactors. Nature.',
22
+ url: 'https://www.nature.com/articles/nature12741',
23
+ },
24
+ ];
@@ -255,6 +255,13 @@ const { ui } = Astro.props;
255
255
  clearBtn?.addEventListener("click", clearAll);
256
256
  }
257
257
 
258
+ function getEventCoordinates(e: MouseEvent | TouchEvent) {
259
+ const touch = (e as TouchEvent).changedTouches?.[0];
260
+ const clientX = (e as MouseEvent).clientX || touch?.clientX || 0;
261
+ const clientY = (e as MouseEvent).clientY || touch?.clientY || 0;
262
+ return { clientX, clientY };
263
+ }
264
+
258
265
  function setupGhostDrag() {
259
266
  const source = document.getElementById("drag-source-desktop");
260
267
  if (!source) return;
@@ -271,10 +278,8 @@ const { ui } = Astro.props;
271
278
  const onEnd = (endEvent: MouseEvent | TouchEvent) => {
272
279
  if (targetOverlay) targetOverlay.classList.remove("active");
273
280
 
274
- const clientX = (endEvent as MouseEvent).clientX || (endEvent as TouchEvent).changedTouches[0].clientX;
275
- const clientY = (endEvent as MouseEvent).clientY || (endEvent as TouchEvent).changedTouches[0].clientY;
276
-
277
- const point = map.mouseEventToContainerPoint({ clientX, clientY } as MouseEvent);
281
+ const coords = getEventCoordinates(endEvent);
282
+ const point = map.mouseEventToContainerPoint(coords as MouseEvent);
278
283
  const latlng = map.containerPointToLatLng(point);
279
284
  spawnImpact(latlng);
280
285
 
@@ -404,12 +409,14 @@ const { ui } = Astro.props;
404
409
  const container = document.getElementById("asteroid-verdict-container");
405
410
 
406
411
  const c = VERDICT_CONFIGS[verdict];
407
- if (value) value.textContent = c.text;
408
- if (label) label.textContent = c.label;
409
- if (container) container.className = `asteroid-verdict-container ${c.class}`;
412
+ if (c) {
413
+ if (value) value.textContent = c.text;
414
+ if (label) label.textContent = c.label;
415
+ if (container) container.className = `asteroid-verdict-container ${c.class}`;
410
416
 
411
- const iconSource = document.getElementById(c.iconId);
412
- if (icon && iconSource) icon.innerHTML = iconSource.innerHTML;
417
+ const iconSource = document.getElementById(c.iconId);
418
+ if (icon && iconSource) icon.innerHTML = iconSource.innerHTML;
419
+ }
413
420
  }
414
421
 
415
422
  function updateVerdict() {
@@ -37,14 +37,13 @@ const faq = [
37
37
  answer: 'Kleinere Einschläge (wie Russland 2013) ereignen sich jedes Jahrzehnt. Katastrophale Einschläge (Typ Tunguska) alle paar Jahrhunderte. Ein globales Massenaussterben wie bei Chicxulub findet etwa alle 100 Millionen Jahre statt.',
38
38
  },
39
39
  ];
40
+ import { bibliography } from '../bibliography';
40
41
  import type { ToolLocaleContent } from '../../../types';
41
42
 
42
43
  export const content: ToolLocaleContent = {
43
44
  slug,
44
45
  title,
45
46
  description,
46
- faqTitle: 'Häufig gestellte Fragen',
47
- bibliographyTitle: 'Bibliografie',
48
47
  ui: {
49
48
  copied: 'Kopiert',
50
49
  noHistory: 'Kein Verlauf',
@@ -135,28 +134,7 @@ export const content: ToolLocaleContent = {
135
134
  },
136
135
  ],
137
136
  faq,
138
- bibliography: [
139
- {
140
- name: 'Collins, G. S., et al. (2005). Earth Impact Effects Program: Ein webbasiertes Computerprogramm zur Berechnung der regionalen Umweltfolgen eines Meteoriteneinschlags auf der Erde.',
141
- url: 'https://impact.ese.ic.ac.uk/ImpactEarth/',
142
- },
143
- {
144
- name: 'Toon, O. B., et al. (1997). Environmental perturbations caused by the impacts of asteroids and comets. Reviews of Geophysics.',
145
- url: 'https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/96RG03038',
146
- },
147
- {
148
- name: 'Chapman, C. R., & Morrison, D. (1994). Impacts on the Earth by asteroids and comets: assessing the hazard. Nature.',
149
- url: 'https://www.nature.com/articles/367033a0',
150
- },
151
- {
152
- name: 'Schulte, P., et al. (2010). The Chicxulub Asteroid Impact and Mass Extinction at the Cretaceous-Paleogene Boundary. Science.',
153
- url: 'https://www.science.org/doi/10.1126/science.1177265',
154
- },
155
- {
156
- name: 'Brown, P., et al. (2013). A 500-kiloton airburst over Chelyabinsk and an enhanced hazard from small impactors. Nature.',
157
- url: 'https://www.nature.com/articles/nature12741',
158
- },
159
- ],
137
+ bibliography,
160
138
  howTo,
161
139
 
162
140
  schemas: [
@@ -192,3 +170,5 @@ export const content: ToolLocaleContent = {
192
170
  },
193
171
  ],
194
172
  };
173
+
174
+