@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.
- package/package.json +2 -1
- package/src/category/i18n/de.ts +1 -1
- package/src/category/i18n/fr.ts +6 -6
- package/src/category/i18n/ru.ts +1 -1
- package/src/category/index.ts +3 -1
- package/src/category/seo.astro +2 -2
- package/src/entries.ts +5 -1
- package/src/index.ts +2 -0
- package/src/pages/[locale]/[slug].astro +32 -15
- package/src/tests/locale_completeness.test.ts +5 -22
- package/src/tests/no_en_dash.test.ts +70 -0
- package/src/tests/seo_length.test.ts +5 -3
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/title_quality.test.ts +1 -1
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/asteroid-impact/bibliography.astro +2 -2
- package/src/tool/asteroid-impact/bibliography.ts +24 -0
- package/src/tool/asteroid-impact/component.astro +16 -9
- package/src/tool/asteroid-impact/i18n/de.ts +4 -24
- package/src/tool/asteroid-impact/i18n/en.ts +4 -24
- package/src/tool/asteroid-impact/i18n/es.ts +4 -24
- package/src/tool/asteroid-impact/i18n/fr.ts +10 -30
- package/src/tool/asteroid-impact/i18n/id.ts +4 -24
- package/src/tool/asteroid-impact/i18n/it.ts +4 -24
- package/src/tool/asteroid-impact/i18n/ja.ts +4 -24
- package/src/tool/asteroid-impact/i18n/ko.ts +4 -24
- package/src/tool/asteroid-impact/i18n/nl.ts +4 -24
- package/src/tool/asteroid-impact/i18n/pl.ts +4 -24
- package/src/tool/asteroid-impact/i18n/pt.ts +4 -24
- package/src/tool/asteroid-impact/i18n/ru.ts +8 -28
- package/src/tool/asteroid-impact/i18n/sv.ts +4 -24
- package/src/tool/asteroid-impact/i18n/tr.ts +4 -24
- package/src/tool/asteroid-impact/i18n/zh.ts +4 -24
- package/src/tool/asteroid-impact/index.ts +1 -0
- package/src/tool/asteroid-impact/script.ts +13 -7
- package/src/tool/asteroid-impact/seo.astro +1 -1
- package/src/tool/cellular-renewal/bibliography.astro +2 -2
- package/src/tool/cellular-renewal/bibliography.ts +24 -0
- package/src/tool/cellular-renewal/i18n/de.ts +3 -24
- package/src/tool/cellular-renewal/i18n/en.ts +3 -24
- package/src/tool/cellular-renewal/i18n/es.ts +3 -24
- package/src/tool/cellular-renewal/i18n/fr.ts +16 -37
- package/src/tool/cellular-renewal/i18n/id.ts +3 -24
- package/src/tool/cellular-renewal/i18n/it.ts +3 -24
- package/src/tool/cellular-renewal/i18n/ja.ts +3 -24
- package/src/tool/cellular-renewal/i18n/ko.ts +3 -24
- package/src/tool/cellular-renewal/i18n/nl.ts +3 -24
- package/src/tool/cellular-renewal/i18n/pl.ts +3 -24
- package/src/tool/cellular-renewal/i18n/pt.ts +3 -24
- package/src/tool/cellular-renewal/i18n/ru.ts +20 -41
- package/src/tool/cellular-renewal/i18n/sv.ts +3 -24
- package/src/tool/cellular-renewal/i18n/tr.ts +3 -24
- package/src/tool/cellular-renewal/i18n/zh.ts +12 -33
- package/src/tool/cellular-renewal/index.ts +1 -0
- package/src/tool/cellular-renewal/seo.astro +2 -1
- package/src/tool/colony-counter/bibliography.astro +2 -2
- package/src/tool/colony-counter/bibliography.ts +12 -0
- package/src/tool/colony-counter/i18n/de.ts +3 -12
- package/src/tool/colony-counter/i18n/en.ts +3 -12
- package/src/tool/colony-counter/i18n/es.ts +3 -12
- package/src/tool/colony-counter/i18n/fr.ts +3 -12
- package/src/tool/colony-counter/i18n/id.ts +3 -12
- package/src/tool/colony-counter/i18n/it.ts +3 -12
- package/src/tool/colony-counter/i18n/ja.ts +3 -12
- package/src/tool/colony-counter/i18n/ko.ts +3 -12
- package/src/tool/colony-counter/i18n/nl.ts +3 -12
- package/src/tool/colony-counter/i18n/pl.ts +3 -12
- package/src/tool/colony-counter/i18n/pt.ts +3 -12
- package/src/tool/colony-counter/i18n/ru.ts +8 -17
- package/src/tool/colony-counter/i18n/sv.ts +3 -12
- package/src/tool/colony-counter/i18n/tr.ts +3 -12
- package/src/tool/colony-counter/i18n/zh.ts +5 -14
- package/src/tool/colony-counter/index.ts +1 -0
- package/src/tool/colony-counter/seo.astro +1 -1
- package/src/tool/cosmic-inflation/bibliography.astro +14 -0
- package/src/tool/cosmic-inflation/bibliography.ts +12 -0
- package/src/tool/cosmic-inflation/component.astro +270 -0
- package/src/tool/cosmic-inflation/cosmic-inflation-calculator.css +277 -0
- package/src/tool/cosmic-inflation/entry.ts +26 -0
- package/src/tool/cosmic-inflation/i18n/de.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/en.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/es.ts +168 -0
- package/src/tool/cosmic-inflation/i18n/fr.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/id.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/it.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/ja.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/ko.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/nl.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/pl.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/pt.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/ru.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/sv.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/tr.ts +188 -0
- package/src/tool/cosmic-inflation/i18n/zh.ts +188 -0
- package/src/tool/cosmic-inflation/index.ts +11 -0
- package/src/tool/cosmic-inflation/logic/CosmicInflationEngine.ts +21 -0
- package/src/tool/cosmic-inflation/seo.astro +15 -0
- package/src/tool/microwave-detector/bibliography.astro +2 -2
- package/src/tool/microwave-detector/bibliography.ts +16 -0
- package/src/tool/microwave-detector/component.astro +9 -7
- package/src/tool/microwave-detector/i18n/de.ts +3 -16
- package/src/tool/microwave-detector/i18n/en.ts +3 -16
- package/src/tool/microwave-detector/i18n/es.ts +3 -16
- package/src/tool/microwave-detector/i18n/fr.ts +7 -20
- package/src/tool/microwave-detector/i18n/id.ts +3 -16
- package/src/tool/microwave-detector/i18n/it.ts +3 -16
- package/src/tool/microwave-detector/i18n/ja.ts +3 -16
- package/src/tool/microwave-detector/i18n/ko.ts +3 -16
- package/src/tool/microwave-detector/i18n/nl.ts +3 -16
- package/src/tool/microwave-detector/i18n/pl.ts +3 -16
- package/src/tool/microwave-detector/i18n/pt.ts +3 -16
- package/src/tool/microwave-detector/i18n/ru.ts +21 -34
- package/src/tool/microwave-detector/i18n/sv.ts +3 -16
- package/src/tool/microwave-detector/i18n/tr.ts +3 -16
- package/src/tool/microwave-detector/i18n/zh.ts +13 -26
- package/src/tool/microwave-detector/index.ts +1 -0
- package/src/tool/microwave-detector/logic/MicrowaveEngine.ts +5 -1
- package/src/tool/microwave-detector/microwave-leak-detector.css +22 -25
- package/src/tool/microwave-detector/seo.astro +2 -1
- package/src/tool/simulation-probability/bibliography.astro +2 -2
- package/src/tool/simulation-probability/bibliography.ts +24 -0
- package/src/tool/simulation-probability/i18n/de.ts +3 -24
- package/src/tool/simulation-probability/i18n/en.ts +3 -24
- package/src/tool/simulation-probability/i18n/es.ts +3 -24
- package/src/tool/simulation-probability/i18n/fr.ts +8 -29
- package/src/tool/simulation-probability/i18n/id.ts +3 -24
- package/src/tool/simulation-probability/i18n/it.ts +3 -24
- package/src/tool/simulation-probability/i18n/ja.ts +3 -24
- package/src/tool/simulation-probability/i18n/ko.ts +3 -24
- package/src/tool/simulation-probability/i18n/nl.ts +3 -24
- package/src/tool/simulation-probability/i18n/pl.ts +3 -24
- package/src/tool/simulation-probability/i18n/pt.ts +3 -24
- package/src/tool/simulation-probability/i18n/ru.ts +10 -31
- package/src/tool/simulation-probability/i18n/sv.ts +3 -24
- package/src/tool/simulation-probability/i18n/tr.ts +3 -24
- package/src/tool/simulation-probability/i18n/zh.ts +7 -28
- package/src/tool/simulation-probability/index.ts +1 -0
- package/src/tool/simulation-probability/seo.astro +2 -1
- package/src/tool/temperature-timeline/bibliography.astro +14 -0
- package/src/tool/temperature-timeline/bibliography.ts +12 -0
- package/src/tool/temperature-timeline/component.astro +289 -0
- package/src/tool/temperature-timeline/entry.ts +26 -0
- package/src/tool/temperature-timeline/i18n/de.ts +213 -0
- package/src/tool/temperature-timeline/i18n/en.ts +213 -0
- package/src/tool/temperature-timeline/i18n/es.ts +178 -0
- package/src/tool/temperature-timeline/i18n/fr.ts +213 -0
- package/src/tool/temperature-timeline/i18n/id.ts +213 -0
- package/src/tool/temperature-timeline/i18n/it.ts +213 -0
- package/src/tool/temperature-timeline/i18n/ja.ts +213 -0
- package/src/tool/temperature-timeline/i18n/ko.ts +213 -0
- package/src/tool/temperature-timeline/i18n/nl.ts +213 -0
- package/src/tool/temperature-timeline/i18n/pl.ts +213 -0
- package/src/tool/temperature-timeline/i18n/pt.ts +213 -0
- package/src/tool/temperature-timeline/i18n/ru.ts +213 -0
- package/src/tool/temperature-timeline/i18n/sv.ts +213 -0
- package/src/tool/temperature-timeline/i18n/tr.ts +213 -0
- package/src/tool/temperature-timeline/i18n/zh.ts +213 -0
- package/src/tool/temperature-timeline/index.ts +11 -0
- package/src/tool/temperature-timeline/logic/TemperatureTimelineEngine.ts +58 -0
- package/src/tool/temperature-timeline/planet-temperature-timeline.css +158 -0
- package/src/tool/temperature-timeline/seo.astro +15 -0
- package/src/tools.ts +4 -0
- 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.
|
|
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",
|
package/src/category/i18n/de.ts
CHANGED
|
@@ -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
|
|
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',
|
package/src/category/i18n/fr.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const content: CategoryLocaleContent = {
|
|
|
7
7
|
seo: [
|
|
8
8
|
{
|
|
9
9
|
type: 'title',
|
|
10
|
-
text: 'Exploration Scientifique et Simulation
|
|
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
|
|
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
|
|
72
|
-
'Éducation Interactif
|
|
73
|
-
'Rigueur Technique
|
|
74
|
-
'Confidentialité des Données Scientifiques
|
|
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
|
{
|
package/src/category/i18n/ru.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const content: CategoryLocaleContent = {
|
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
type: 'paragraph',
|
|
15
|
-
html: '<p>Наука
|
|
15
|
+
html: '<p>Наука - это не просто статический набор знаний, а динамичный процесс исследования и экспериментирования. В этом разделе мы предлагаем бесплатные онлайн-инструменты, разработанные для того, чтобы донести сложные научные концепции до любого пользователя посредством моделирования и анализа данных. От микробиологии до астрофизики наши утилиты применяют математические модели и физические теории, чтобы предложить вам интерактивный взгляд на Вселенную.</p><p>Если вам нужно подсчитать количество колониеобразующих единиц в лаборатории или вы хотите оценить вероятность того, что мы живем в виртуальной среде, наши калькуляторы обеспечивают техническую точность и интерфейс, созданный для любознательных умов.</p>',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
type: 'title',
|
package/src/category/index.ts
CHANGED
|
@@ -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),
|
package/src/category/seo.astro
CHANGED
|
@@ -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 '
|
|
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
|
-
|
|
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 =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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}
|
|
136
|
+
<FAQSection items={content.faq} />
|
|
120
137
|
</section>
|
|
121
138
|
|
|
122
139
|
<section class="section-bibliography">
|
|
123
|
-
<Bibliography links={content.bibliography}
|
|
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('
|
|
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
|
-
|
|
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
|
|
39
|
-
expect(ALL_TOOLS.length).toBe(
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
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 && <
|
|
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
|
|
275
|
-
const
|
|
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 (
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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
|
+
|