@jjlmoya/utils-science 1.48.0 → 1.50.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 (98) hide show
  1. package/package.json +2 -2
  2. package/src/tests/seo_parity.test.ts +60 -0
  3. package/src/tests/spanish_leakage.test.ts +153 -0
  4. package/src/tests/translation_copy.test.ts +122 -0
  5. package/src/tool/black-hole-event-horizon-simulator/i18n/de.ts +79 -4
  6. package/src/tool/black-hole-event-horizon-simulator/i18n/fr.ts +86 -11
  7. package/src/tool/black-hole-event-horizon-simulator/i18n/it.ts +86 -11
  8. package/src/tool/black-hole-event-horizon-simulator/i18n/ja.ts +85 -10
  9. package/src/tool/black-hole-event-horizon-simulator/i18n/nl.ts +87 -12
  10. package/src/tool/black-hole-event-horizon-simulator/i18n/pt.ts +85 -10
  11. package/src/tool/black-hole-event-horizon-simulator/i18n/sv.ts +85 -10
  12. package/src/tool/black-hole-event-horizon-simulator/i18n/tr.ts +85 -11
  13. package/src/tool/black-hole-event-horizon-simulator/i18n/zh.ts +87 -12
  14. package/src/tool/conway-life-rule-lab/i18n/de.ts +46 -6
  15. package/src/tool/conway-life-rule-lab/i18n/es.ts +46 -6
  16. package/src/tool/conway-life-rule-lab/i18n/fr.ts +46 -6
  17. package/src/tool/conway-life-rule-lab/i18n/id.ts +46 -6
  18. package/src/tool/conway-life-rule-lab/i18n/it.ts +46 -6
  19. package/src/tool/conway-life-rule-lab/i18n/ja.ts +46 -6
  20. package/src/tool/conway-life-rule-lab/i18n/ko.ts +46 -6
  21. package/src/tool/conway-life-rule-lab/i18n/nl.ts +46 -6
  22. package/src/tool/conway-life-rule-lab/i18n/pl.ts +46 -6
  23. package/src/tool/conway-life-rule-lab/i18n/pt.ts +46 -6
  24. package/src/tool/conway-life-rule-lab/i18n/ru.ts +49 -9
  25. package/src/tool/conway-life-rule-lab/i18n/sv.ts +46 -6
  26. package/src/tool/conway-life-rule-lab/i18n/tr.ts +46 -6
  27. package/src/tool/conway-life-rule-lab/i18n/zh.ts +46 -6
  28. package/src/tool/cosmic-inflation/i18n/ru.ts +30 -4
  29. package/src/tool/double-slit-decoherence/i18n/fr.ts +48 -4
  30. package/src/tool/double-slit-decoherence/i18n/it.ts +52 -8
  31. package/src/tool/double-slit-decoherence/i18n/pt.ts +53 -9
  32. package/src/tool/double-slit-decoherence/i18n/sv.ts +56 -8
  33. package/src/tool/dyson-sphere-energy-capture/i18n/de.ts +119 -38
  34. package/src/tool/dyson-sphere-energy-capture/i18n/fr.ts +116 -48
  35. package/src/tool/dyson-sphere-energy-capture/i18n/it.ts +96 -19
  36. package/src/tool/dyson-sphere-energy-capture/i18n/ja.ts +124 -24
  37. package/src/tool/dyson-sphere-energy-capture/i18n/ko.ts +122 -22
  38. package/src/tool/dyson-sphere-energy-capture/i18n/pl.ts +68 -14
  39. package/src/tool/dyson-sphere-energy-capture/i18n/pt.ts +126 -45
  40. package/src/tool/dyson-sphere-energy-capture/i18n/ru.ts +46 -9
  41. package/src/tool/dyson-sphere-energy-capture/i18n/sv.ts +134 -53
  42. package/src/tool/dyson-sphere-energy-capture/i18n/tr.ts +68 -14
  43. package/src/tool/dyson-sphere-energy-capture/i18n/zh.ts +124 -24
  44. package/src/tool/global-albedo-snowball-simulator/i18n/ru.ts +20 -7
  45. package/src/tool/lorenz-attractor/i18n/de.ts +35 -5
  46. package/src/tool/lorenz-attractor/i18n/fr.ts +35 -5
  47. package/src/tool/lorenz-attractor/i18n/id.ts +10 -0
  48. package/src/tool/lorenz-attractor/i18n/it.ts +9 -0
  49. package/src/tool/lorenz-attractor/i18n/ja.ts +10 -0
  50. package/src/tool/lorenz-attractor/i18n/ko.ts +10 -0
  51. package/src/tool/lorenz-attractor/i18n/nl.ts +10 -0
  52. package/src/tool/lorenz-attractor/i18n/pl.ts +10 -0
  53. package/src/tool/lorenz-attractor/i18n/pt.ts +35 -5
  54. package/src/tool/lorenz-attractor/i18n/ru.ts +10 -0
  55. package/src/tool/lorenz-attractor/i18n/sv.ts +10 -0
  56. package/src/tool/lorenz-attractor/i18n/tr.ts +10 -0
  57. package/src/tool/lorenz-attractor/i18n/zh.ts +10 -0
  58. package/src/tool/mandelbrot-fractal/i18n/de.ts +27 -0
  59. package/src/tool/mandelbrot-fractal/i18n/fr.ts +18 -0
  60. package/src/tool/mandelbrot-fractal/i18n/it.ts +26 -8
  61. package/src/tool/mandelbrot-fractal/i18n/pl.ts +27 -9
  62. package/src/tool/mandelbrot-fractal/i18n/ru.ts +22 -4
  63. package/src/tool/mandelbrot-fractal/i18n/sv.ts +35 -8
  64. package/src/tool/natural-selection-drift/i18n/de.ts +125 -22
  65. package/src/tool/natural-selection-drift/i18n/id.ts +113 -5
  66. package/src/tool/natural-selection-drift/i18n/it.ts +74 -2
  67. package/src/tool/natural-selection-drift/i18n/ja.ts +113 -5
  68. package/src/tool/natural-selection-drift/i18n/ko.ts +113 -5
  69. package/src/tool/natural-selection-drift/i18n/nl.ts +121 -6
  70. package/src/tool/natural-selection-drift/i18n/pl.ts +136 -16
  71. package/src/tool/natural-selection-drift/i18n/pt.ts +110 -15
  72. package/src/tool/natural-selection-drift/i18n/ru.ts +142 -7
  73. package/src/tool/natural-selection-drift/i18n/sv.ts +110 -5
  74. package/src/tool/natural-selection-drift/i18n/tr.ts +160 -22
  75. package/src/tool/natural-selection-drift/i18n/zh.ts +113 -5
  76. package/src/tool/phase-diagram-critical-points/i18n/de.ts +44 -6
  77. package/src/tool/phase-diagram-critical-points/i18n/fr.ts +46 -8
  78. package/src/tool/planet-atmosphere-survival/i18n/ru.ts +118 -2
  79. package/src/tool/radioactive-decay/i18n/de.ts +112 -5
  80. package/src/tool/radioactive-decay/i18n/fr.ts +16 -0
  81. package/src/tool/radioactive-decay/i18n/id.ts +112 -8
  82. package/src/tool/radioactive-decay/i18n/it.ts +16 -0
  83. package/src/tool/radioactive-decay/i18n/ja.ts +112 -8
  84. package/src/tool/radioactive-decay/i18n/ko.ts +112 -8
  85. package/src/tool/radioactive-decay/i18n/nl.ts +112 -14
  86. package/src/tool/radioactive-decay/i18n/pl.ts +112 -7
  87. package/src/tool/radioactive-decay/i18n/pt.ts +112 -21
  88. package/src/tool/radioactive-decay/i18n/ru.ts +112 -8
  89. package/src/tool/radioactive-decay/i18n/sv.ts +112 -8
  90. package/src/tool/radioactive-decay/i18n/tr.ts +112 -8
  91. package/src/tool/radioactive-decay/i18n/zh.ts +112 -8
  92. package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +76 -0
  93. package/src/tool/temperature-timeline/i18n/de.ts +22 -2
  94. package/src/tool/temperature-timeline/i18n/fr.ts +41 -1
  95. package/src/tool/temperature-timeline/i18n/it.ts +30 -1
  96. package/src/tool/temperature-timeline/i18n/pt.ts +34 -1
  97. package/src/tool/temperature-timeline/i18n/ru.ts +29 -0
  98. package/src/tool/twin-paradox-visualizer/i18n/pt.ts +41 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-science",
3
- "version": "1.48.0",
3
+ "version": "1.50.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -28,7 +28,7 @@
28
28
  "check": "astro check",
29
29
  "type-check": "astro check",
30
30
  "test": "vitest run",
31
- "preversion": "npm run lint && npm run test",
31
+ "preversion": "npm run lint && npm run test && npm run build",
32
32
  "postversion": "git push && git push --tags",
33
33
  "patch": "npm version patch",
34
34
  "minor": "npm version minor",
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+ import type { KnownLocale } from '../types';
4
+
5
+ interface ExpectedCounts {
6
+ seo: number;
7
+ faq: number;
8
+ howTo: number;
9
+ }
10
+
11
+ function countItems(arr: unknown[] | undefined): number {
12
+ return arr?.length ?? 0;
13
+ }
14
+
15
+ async function verifyLocaleParity(
16
+ entry: typeof ALL_ENTRIES[number],
17
+ loc: KnownLocale,
18
+ expected: ExpectedCounts,
19
+ ): Promise<void> {
20
+ const locContent = await entry.i18n[loc]?.();
21
+ expect(locContent, `Locale ${loc} missing content`).toBeDefined();
22
+
23
+ const locSeoCount = countItems(locContent?.seo);
24
+ const locFaqCount = countItems(locContent?.faq);
25
+ const locHowToCount = countItems(locContent?.howTo);
26
+
27
+ expect(
28
+ locSeoCount,
29
+ `Locale ${loc} SEO sections count (${locSeoCount}) must match EN (${expected.seo})`,
30
+ ).toBe(expected.seo);
31
+ expect(
32
+ locFaqCount,
33
+ `Locale ${loc} FAQ items count (${locFaqCount}) must match EN (${expected.faq})`,
34
+ ).toBe(expected.faq);
35
+ expect(
36
+ locHowToCount,
37
+ `Locale ${loc} HowTo steps count (${locHowToCount}) must match EN (${expected.howTo})`,
38
+ ).toBe(expected.howTo);
39
+ }
40
+
41
+ describe('SEO & i18n Structural Parity Suite', () => {
42
+ ALL_ENTRIES.forEach((entry) => {
43
+ describe(`Tool: ${entry.id}`, () => {
44
+ it('all 15 locales should have identical SEO section counts and types as English', async () => {
45
+ const enContent = await entry.i18n.en?.();
46
+ expect(enContent).toBeDefined();
47
+ const expected: ExpectedCounts = {
48
+ seo: countItems(enContent?.seo),
49
+ faq: countItems(enContent?.faq),
50
+ howTo: countItems(enContent?.howTo),
51
+ };
52
+
53
+ const locales = Object.keys(entry.i18n) as KnownLocale[];
54
+ for (const loc of locales) {
55
+ await verifyLocaleParity(entry, loc, expected);
56
+ }
57
+ });
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+
4
+ const STRUCTURAL_KEYS = new Set([
5
+ 'id',
6
+ 'slug',
7
+ 'url',
8
+ 'icon',
9
+ 'image',
10
+ 'imageUrl',
11
+ 'keywords',
12
+ 'category',
13
+ 'tags',
14
+ 'toolId',
15
+ 'type',
16
+ 'locale',
17
+ 'language',
18
+ 'direction',
19
+ ]);
20
+
21
+ const TRANSLATABLE_KEYS = [
22
+ 'title',
23
+ 'description',
24
+ 'faqTitle',
25
+ 'faq',
26
+ 'howTo',
27
+ 'seo',
28
+ 'schemas',
29
+ ] as const;
30
+
31
+ const SPANISH_MARKERS = [
32
+ ['sangre', /\bsangre\b/gi],
33
+ ['molino', /\bmolino\b/gi],
34
+ ['grano', /\bgrano\b/gi],
35
+ ['paladar', /\bpaladar\b/gi],
36
+ ['cuchillas', /\bcuchillas\b/gi],
37
+ ['trozos', /\btrozos\b/gi],
38
+ ['frescura', /\bfrescura\b/gi],
39
+ ['ingresa', /\bingresa\b/gi],
40
+ ['selecciona', /\bselecciona\b/gi],
41
+ ['herramienta', /\bherramienta\b/gi],
42
+ ['según', /\bsegún\b/gi],
43
+ ['después', /\bdespués\b/gi],
44
+ ['puedes', /\bpuedes\b/gi],
45
+ ['debes', /\bdebes\b/gi],
46
+ ['tus', /\btus\b/gi],
47
+ ['a la vez', /\ba la vez\b/gi],
48
+ ['los datos', /\blos datos\b/gi],
49
+ ['las opciones', /\blas opciones\b/gi],
50
+ ['el resultado', /\bel resultado\b/gi],
51
+ ['método de extracción', /\bmétodo de extracción\b/gi],
52
+ ['uniformidad del molino', /\buniformidad del molino\b/gi],
53
+ ['café recién tostado', /\bcafé recién tostado\b/gi],
54
+ ] as const;
55
+
56
+ type UnknownRecord = Record<string, unknown>;
57
+
58
+ function normalize(value: string): string {
59
+ return value
60
+ .replace(/<[^>]*>/g, ' ')
61
+ .replace(/&nbsp;/gi, ' ')
62
+ .replace(/\s+/g, ' ')
63
+ .trim()
64
+ .toLocaleLowerCase('es');
65
+ }
66
+
67
+ function isTechnicalInvariant(text: string): boolean {
68
+ return [
69
+ 'data:image/svg+xml;base64',
70
+ 'background-image: url',
71
+ '.layout-playground {',
72
+ 'const samplerate =',
73
+ ].some((pattern) => text.includes(pattern)) || (text.includes('presets') && text.includes('hz'));
74
+ }
75
+
76
+ function collectString(value: string, output: string[]): void {
77
+ const text = normalize(value);
78
+ if (!isTechnicalInvariant(text) && text.length >= 20) output.push(text);
79
+ }
80
+
81
+ function collectObject(value: UnknownRecord, output: string[]): void {
82
+ Object.entries(value).forEach(([childKey, childValue]) => {
83
+ collectText(childValue, output, childKey);
84
+ });
85
+ }
86
+
87
+ function collectText(value: unknown, output: string[], key?: string): void {
88
+ if (key && STRUCTURAL_KEYS.has(key)) return;
89
+ if (typeof value === 'string') return collectString(value, output);
90
+ if (Array.isArray(value)) return value.forEach((item) => collectText(item, output));
91
+ if (value && typeof value === 'object') collectObject(value as UnknownRecord, output);
92
+ }
93
+
94
+ async function loadText(loader: unknown): Promise<string[]> {
95
+ if (typeof loader !== 'function') return [];
96
+ const module = await (loader as () => Promise<unknown>)();
97
+ const output: string[] = [];
98
+ if (module && typeof module === 'object') {
99
+ const record = module as UnknownRecord;
100
+ for (const key of TRANSLATABLE_KEYS) {
101
+ collectText(record[key], output, key);
102
+ }
103
+ }
104
+ return output;
105
+ }
106
+
107
+ function findSpanishMarkers(text: string[]): string[] {
108
+ const corpus = text.join(' ');
109
+ return SPANISH_MARKERS.flatMap(([label, pattern]) =>
110
+ pattern.test(corpus) ? [label] : [],
111
+ );
112
+ }
113
+
114
+ function findCopiedFragments(spanish: string[], translated: string[]): string[] {
115
+ const corpus = translated.join(' ');
116
+ return spanish
117
+ .filter((fragment) => fragment.length >= 80 && corpus.includes(fragment))
118
+ .sort((a, b) => b.length - a.length)
119
+ .slice(0, 3);
120
+ }
121
+
122
+ describe('Locales must not contain copied Spanish content', () => {
123
+ for (const entry of ALL_ENTRIES) {
124
+ it(`${entry.id} has no untranslated Spanish blocks`, async () => {
125
+ const spanish = await loadText(entry.i18n.es);
126
+ const failures: string[] = [];
127
+
128
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
129
+ if (locale === 'es') continue;
130
+
131
+ const translated = await loadText(loader);
132
+ const copiedFragments = findCopiedFragments(spanish, translated);
133
+ const markerHits = findSpanishMarkers(translated);
134
+
135
+ if (copiedFragments.length > 0 || markerHits.length >= 2) {
136
+ const details = [
137
+ copiedFragments.length > 0
138
+ ? `copied fragments: ${copiedFragments
139
+ .map((fragment) => JSON.stringify(fragment.slice(0, 120)))
140
+ .join(', ')}`
141
+ : '',
142
+ markerHits.length >= 2 ? `Spanish markers: ${markerHits.join(', ')}` : '',
143
+ ]
144
+ .filter(Boolean)
145
+ .join('; ');
146
+ failures.push(`${locale}: ${details}`);
147
+ }
148
+ }
149
+
150
+ expect(failures, failures.join('\n')).toEqual([]);
151
+ });
152
+ }
153
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+
4
+ const COPY_THRESHOLD = 0.9;
5
+
6
+ const STRUCTURAL_KEYS = new Set([
7
+ '@context',
8
+ '@type',
9
+ 'applicationCategory',
10
+ 'columns',
11
+ 'highlight',
12
+ 'icon',
13
+ 'level',
14
+ 'operatingSystem',
15
+ 'position',
16
+ 'positive',
17
+ 'price',
18
+ 'priceCurrency',
19
+ 'slug',
20
+ 'trend',
21
+ 'type',
22
+ 'url',
23
+ 'value',
24
+ 'variant',
25
+ ]);
26
+
27
+ function normalizeText(value: string): string {
28
+ return value
29
+ .replace(/<[^>]*>/g, ' ')
30
+ .replace(/&(?:amp|lt|gt|quot|apos|nbsp);/gi, ' ')
31
+ .replace(/[\u2018\u2019]/g, "'")
32
+ .replace(/[\u201c\u201d]/g, '"')
33
+ .replace(/\s+/g, ' ')
34
+ .trim()
35
+ .toLocaleLowerCase();
36
+ }
37
+
38
+ function collectText(value: unknown, path: string, parts: string[]): void {
39
+ if (typeof value === 'string') {
40
+ const normalized = normalizeText(value);
41
+ if (normalized.length >= 2) parts.push(normalized);
42
+ return;
43
+ }
44
+
45
+ if (Array.isArray(value)) {
46
+ value.forEach((item, index) => collectText(item, `${path}[${index}]`, parts));
47
+ return;
48
+ }
49
+
50
+ if (!value || typeof value !== 'object') return;
51
+
52
+ Object.entries(value).forEach(([key, child]) => {
53
+ if (STRUCTURAL_KEYS.has(key)) return;
54
+ collectText(child, `${path}.${key}`, parts);
55
+ });
56
+ }
57
+
58
+ function localeCorpus(content: unknown): string {
59
+ if (!content || typeof content !== 'object') return '';
60
+
61
+ const record = content as Record<string, unknown>;
62
+ const parts: string[] = [];
63
+ collectText(record.title, 'title', parts);
64
+ collectText(record.description, 'description', parts);
65
+ collectText(record.faqTitle, 'faqTitle', parts);
66
+ collectText(record.faq, 'faq', parts);
67
+ collectText(record.seo, 'seo', parts);
68
+ collectText(record.schemas, 'schemas', parts);
69
+ return parts.join(' ');
70
+ }
71
+
72
+ function tokenCounts(text: string): Map<string, number> {
73
+ const counts = new Map<string, number>();
74
+ for (const token of text.match(/[\p{L}\p{N}]+/gu) ?? []) {
75
+ counts.set(token, (counts.get(token) ?? 0) + 1);
76
+ }
77
+ return counts;
78
+ }
79
+
80
+ function copySimilarity(left: string, right: string): number {
81
+ const leftCounts = tokenCounts(left);
82
+ const rightCounts = tokenCounts(right);
83
+ const leftTotal = [...leftCounts.values()].reduce((sum, count) => sum + count, 0);
84
+ const rightTotal = [...rightCounts.values()].reduce((sum, count) => sum + count, 0);
85
+ if (leftTotal === 0 || rightTotal === 0) return 0;
86
+
87
+ let shared = 0;
88
+ for (const [token, count] of leftCounts) {
89
+ shared += Math.min(count, rightCounts.get(token) ?? 0);
90
+ }
91
+ return (2 * shared) / (leftTotal + rightTotal);
92
+ }
93
+
94
+ describe('Locales must not copy another locale wholesale', () => {
95
+ ALL_ENTRIES.forEach((entry) => {
96
+ it(`${entry.id} is not at least ${COPY_THRESHOLD * 100}% identical to another locale`, async () => {
97
+ const corpora = new Map<string, string>();
98
+
99
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
100
+ if (!loader) continue;
101
+ corpora.set(locale, localeCorpus(await loader()));
102
+ }
103
+
104
+ const locales = [...corpora.keys()];
105
+ const violations: string[] = [];
106
+
107
+ for (let leftIndex = 0; leftIndex < locales.length; leftIndex += 1) {
108
+ for (let rightIndex = leftIndex + 1; rightIndex < locales.length; rightIndex += 1) {
109
+ const left = locales[leftIndex];
110
+ const right = locales[rightIndex];
111
+ const similarity = copySimilarity(corpora.get(left) ?? '', corpora.get(right) ?? '');
112
+
113
+ if (similarity >= COPY_THRESHOLD) {
114
+ violations.push(`${left} ↔ ${right}: ${(similarity * 100).toFixed(1)}%`);
115
+ }
116
+ }
117
+ }
118
+
119
+ expect(violations, `Locale copy threshold exceeded in ${entry.id}`).toEqual([]);
120
+ });
121
+ });
122
+ });
@@ -111,11 +111,11 @@ export const content: ToolLocaleContent = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Dieser Schwarzes Loch Ereignishorizont Simulator verwandelt die Kernideen von Schwarzschild-schwarzen Löchern in ein interaktives visuelles Modell. Ändern Sie die Masse des schwarzen Lochs, bewegen Sie eine Sonde nach innen oder außen und vergleichen Sie, wie Ereignishorizont, Photonensphäre, innerster stabiler kreisförmiger Orbit, Fluchtgeschwindigkeit, gravitative Rotverschiebung und Zeitdilatation reagieren.',
114
+ html: 'Dieser Schwarzes Loch Ereignishorizont Simulator verwandelt die Kernideen von Schwarzschild-schwarzen Löchern in ein interaktives visuelles Modell.',
115
115
  },
116
116
  {
117
117
  type: 'paragraph',
118
- html: 'Der Simulator wurde für Astronomiestudenten, Physiklehrer, Wissenschaftsautoren und neugierige Lernende entwickelt, die mehr als eine statische Formel wollen. Die Anzeige verbindet die Zahlen mit einem räumlichen Diagramm, sodass Sie sehen können, warum der Ereignishorizont keine materielle Oberfläche ist, warum Photonen einen speziellen instabilen Orbit haben und warum stabile Kreisbewegungen stoppen, bevor der Horizont erreicht wird.',
118
+ html: 'Der Simulator wurde für Astronomiestudenten, Physiklehrer und Wissenschaftsautoren entwickelt.',
119
119
  },
120
120
  {
121
121
  type: 'title',
@@ -124,11 +124,11 @@ export const content: ToolLocaleContent = {
124
124
  },
125
125
  {
126
126
  type: 'paragraph',
127
- html: 'Für ein nicht rotierendes, ungeladenes schwarzes Loch ist der Ereignishorizont-Radius der <strong>Schwarzschild-Radius</strong>. Er wird berechnet als <strong>Rs = 2GM / c^2</strong>, wobei G die Gravitationskonstante, M die Masse des schwarzen Lochs und c die Lichtgeschwindigkeit ist. Eine Sonnenmasse entspricht einem Schwarzschild-Radius von etwa 2,95 Kilometern.',
127
+ html: 'Für ein nicht rotierendes, ungeladenes schwarzes Loch ist der Ereignishorizont-Radius der <strong>Schwarzschild-Radius</strong>. Er wird berechnet als <strong>Rs = 2GM / c^2</strong>.',
128
128
  },
129
129
  {
130
130
  type: 'paragraph',
131
- html: 'Da der Schwarzschild-Radius linear mit der Masse skaliert, hat ein schwarzes Loch mit 10 Sonnenmassen einen Radius von nahezu 29,5 Kilometern, während ein schwarzes Loch mit 4 Millionen Sonnenmassen wie Sagittarius A* einen Radius von etwa 11,8 Millionen Kilometern hat. Der Masse-Regler verwendet eine logarithmische Skala, damit sowohl stellare als auch supermassive Fälle in dieselbe Steuerung passen.',
131
+ html: 'Da der Schwarzschild-Radius linear mit der Masse skaliert, hat ein schwarzes Loch mit 10 Sonnenmassen einen Radius von etwa 29,5 Kilometern.',
132
132
  },
133
133
  {
134
134
  type: 'table',
@@ -140,6 +140,81 @@ export const content: ToolLocaleContent = {
140
140
  ['Schwaches Feld', '6 Rs und darüber', 'Die Schwerkraft ist immer noch stark, aber kreisförmige Orbits sind leichter aufrechtzuerhalten.'],
141
141
  ],
142
142
  },
143
+ {
144
+ type: 'title',
145
+ text: 'Fluchtgeschwindigkeit und warum der Horizont eine kausale Grenze ist',
146
+ level: 3,
147
+ },
148
+ {
149
+ type: 'paragraph',
150
+ html: 'Die Fluchtgeschwindigkeit ist die Geschwindigkeit, die ein Objekt benötigt, um ohne zusätzlichen Antrieb beliebig weit zu entkommen.',
151
+ },
152
+ {
153
+ type: 'paragraph',
154
+ html: 'Die Fluchtgeschwindigkeitsanzeige liefert eine nützliche Intuition, sollte jedoch nicht als vollständige relativistische Beschreibung gelesen werden.',
155
+ },
156
+ {
157
+ type: 'title',
158
+ text: 'Zeitdilatation und gravitative Rotverschiebung nahe einem schwarzen Loch',
159
+ level: 3,
160
+ },
161
+ {
162
+ type: 'paragraph',
163
+ html: 'Eine Uhr, die an einem festen Radius außerhalb eines Schwarzschild-schwarzen Lochs gehalten wird, tickt im Vergleich zu einem entfernten Beobachter langsamer. Der vereinfachte Faktor ist <strong>sqrt(1 - Rs / r)</strong>.',
164
+ },
165
+ {
166
+ type: 'paragraph',
167
+ html: 'Die gravitative Rotverschiebung folgt derselben Geometrie. Licht, das aus dem Gravitationspotenzial aufsteigt, verliert Energie.',
168
+ },
169
+ {
170
+ type: 'title',
171
+ text: 'Photonensphäre und ISCO erklärt',
172
+ level: 3,
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Die Photonensphäre bei 1.5 Rs ist der Ort, an dem Licht im Prinzip einem kreisförmigen Orbit folgen kann.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Die ISCO bei 3 Rs ist der innerste stabile kreisförmige Orbit für massereiche Teilchen.',
181
+ },
182
+ {
183
+ type: 'title',
184
+ text: 'Stellare im Vergleich zu supermassiven schwarzen Löchern',
185
+ level: 3,
186
+ },
187
+ {
188
+ type: 'paragraph',
189
+ html: 'Eine überraschende Lektion ist, dass größere schwarze Löcher am Horizont weniger destruktiv wirken können.',
190
+ },
191
+ {
192
+ type: 'paragraph',
193
+ html: 'Der Simulator enthält eine Gezeitengradienten-Anzeige in Erdschwerkraft pro Meter.',
194
+ },
195
+ {
196
+ type: 'title',
197
+ text: 'Einschränkungen dieses Simulators',
198
+ level: 3,
199
+ },
200
+ {
201
+ type: 'list',
202
+ items: [
203
+ '<strong>Kein Spin:</strong> verwendet Schwarzschild-Geometrie.',
204
+ '<strong>Keine Akkretionsphysik:</strong> vereinfachte Scheibenanzeige.',
205
+ '<strong>Kein Raytracing:</strong> didaktisches Diagramm.',
206
+ '<strong>Kein freier Fall:</strong> stationäre Beobachteruhr.',
207
+ ],
208
+ },
209
+ {
210
+ type: 'title',
211
+ text: 'Häufige Fragen',
212
+ level: 3,
213
+ },
214
+ {
215
+ type: 'paragraph',
216
+ html: 'Verwenden Sie dieses Tool zur Schätzung der Parameter eines schwarzen Lochs.',
217
+ },
143
218
  ],
144
219
  faq,
145
220
  bibliography,
@@ -106,40 +106,115 @@ export const content: ToolLocaleContent = {
106
106
  seo: [
107
107
  {
108
108
  type: 'title',
109
- text: 'Simulateur d\'horizon des événements: rayon de Schwarzschild, sphère de photons et dilatation du temps',
109
+ text: 'Simulateur d\'horizon des événements pour rayon de Schwarzschild, sphère de photons et dilatation temporelle',
110
110
  level: 2,
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Ce simulateur transforme les concepts fondamentaux des trous noirs de Schwarzschild en un modèle visuel interactif. Modifiez la masse, déplacez la sonde et comparez la réponse de l\'horizon, de la sphère de photons, de l\'ISCO, de la vitesse de libération, du décalage vers le rouge gravitationnel et de la dilatation du temps.',
114
+ html: 'Ce simulateur d\'horizon des événements transforme les idées clés des trous noirs de Schwarzschild en un modèle visuel interactif.',
115
115
  },
116
116
  {
117
117
  type: 'paragraph',
118
- html: 'Conçu pour les étudiants en astronomie, les enseignants et les curieux, cet outil dépasse la simple formule statique. Il relie les données numériques à un diagramme spatial pour illustrer pourquoi l\'horizon n\'est pas une surface matérielle, pourquoi les photons suivent une orbite instable et pourquoi le mouvement circulaire stable s\'arrête avant l\'horizon.',
118
+ html: 'Le simulateur est conçu pour les étudiants en astronomie, les enseignants et les passionnés de physique.',
119
119
  },
120
120
  {
121
121
  type: 'title',
122
- text: 'Comment le rayon de l\'horizon est calculé',
122
+ text: 'Comment le rayon de l\'horizon des événements est calculé',
123
123
  level: 3,
124
124
  },
125
125
  {
126
126
  type: 'paragraph',
127
- html: 'Pour un trou noir sans rotation ni charge, le rayon de l\'horizon est le <strong>rayon de Schwarzschild</strong>. Il est calculé via la formule <strong>Rs = 2GM / c^2</strong>, où G est la constante gravitationnelle, M la masse du trou noir et c la vitesse de la lumière. Une masse solaire correspond à un rayon d\'environ 2,95 km.',
127
+ html: 'Pour un trou noir non rotatif sans charge, le rayon est le <strong>rayon de Schwarzschild</strong>: <strong>Rs = 2GM / c^2</strong>.',
128
128
  },
129
129
  {
130
130
  type: 'paragraph',
131
- html: 'Comme le rayon de Schwarzschild varie linéairement avec la masse, un trou noir de 10 masses solaires a un rayon d\'environ 29,5 km, tandis qu\'un trou noir supermassif comme Sagittarius A* (4 millions de masses solaires) atteint environ 11,8 millions de kilomètres. Le curseur utilise une échelle logarithmique pour couvrir ces ordres de grandeur.',
131
+ html: 'Puisque le rayon varie proportionnellement à la masse, 10 masses solaires donnent un rayon d\'environ 29,5 km.',
132
132
  },
133
133
  {
134
134
  type: 'table',
135
- headers: ['Région', 'Rayon (en Rs)', 'Signification'],
135
+ headers: ['Région', 'Rayon en rayons de Schwarzschild', 'Signification'],
136
136
  rows: [
137
- ['Horizon des événements', '1.0 Rs', 'Frontière unidirectionnelle où la vitesse de libération égale celle de la lumière.'],
138
- ['Sphère de photons', '1.5 Rs', 'Orbite circulaire instable pour la lumière.'],
139
- ['ISCO', '3.0 Rs', 'Orbite circulaire stable la plus interne pour les particules massives.'],
140
- ['Champ faible', '6 Rs et +', 'La gravité reste forte, mais les orbites circulaires sont stables.'],
137
+ ['Horizon des événements', '1.0 Rs', 'La limite sans retour où la vitesse de libération égale c.'],
138
+ ['Sphère de photons', '1.5 Rs', 'L\'orbite circulaire instable pour la lumière.'],
139
+ ['ISCO', '3.0 Rs', 'La dernière orbite circulaire stable pour les particules massives.'],
140
+ ['Champ faible', '6 Rs et plus', 'Gravité encore forte mais orbites circulaires aisées.'],
141
141
  ],
142
142
  },
143
+ {
144
+ type: 'title',
145
+ text: 'Vitesse de libération et frontière causale',
146
+ level: 3,
147
+ },
148
+ {
149
+ type: 'paragraph',
150
+ html: 'La vitesse de libération est la vitesse minimale requise pour s\'échapper à l\'infini sans propulsion.',
151
+ },
152
+ {
153
+ type: 'paragraph',
154
+ html: 'L\'indicateur de vitesse de libération est une intuition utile mais incomplète relativistement.',
155
+ },
156
+ {
157
+ type: 'title',
158
+ text: 'Dilatation temporelle et décalage vers le rouge gravitationnel',
159
+ level: 3,
160
+ },
161
+ {
162
+ type: 'paragraph',
163
+ html: 'Une horloge fixe près d\'un trou noir bat plus lentement qu\'à l\'infini. Le facteur simplifié est <strong>sqrt(1 - Rs / r)</strong>.',
164
+ },
165
+ {
166
+ type: 'paragraph',
167
+ html: 'Le décalage vers le rouge gravitationnel suit la même géométrie. La lumière perd de l\'énergie en s\'échappant.',
168
+ },
169
+ {
170
+ type: 'title',
171
+ text: 'Sphère de photons et ISCO expliquées',
172
+ level: 3,
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'La sphère de photons à 1.5 Rs est l\'endroit où la lumière peut effectuer une orbite circulaire instable.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'L\'ISCO à 3 Rs représente la dernière orbite stable pour la matière.',
181
+ },
182
+ {
183
+ type: 'title',
184
+ text: 'Trous noirs stellaires vs supermassifs',
185
+ level: 3,
186
+ },
187
+ {
188
+ type: 'paragraph',
189
+ html: 'Une leçon surprenante est que les trous noirs supermassifs sont moins violents au niveau des forces de marée à l\'horizon.',
190
+ },
191
+ {
192
+ type: 'paragraph',
193
+ html: 'Le simulateur inclut un gradient de marée exprimé en gravités terrestres par mètre.',
194
+ },
195
+ {
196
+ type: 'title',
197
+ text: 'Limites de ce simulateur',
198
+ level: 3,
199
+ },
200
+ {
201
+ type: 'list',
202
+ items: [
203
+ ['<strong>Pas de rotation :</strong> utilise la géométrie de Schwarzschild.'],
204
+ ['<strong>Pas de physique d\'accrétion avancée :</strong> disque simplifié.'],
205
+ ['<strong>Pas de ray-tracing :</strong> schéma éducatif.'],
206
+ ['<strong>Pas de repère en chute libre :</strong> horloge stationnaire.'],
207
+ ],
208
+ },
209
+ {
210
+ type: 'title',
211
+ text: 'Foire aux questions',
212
+ level: 3,
213
+ },
214
+ {
215
+ type: 'paragraph',
216
+ html: 'Utilisez cet outil pour estimer les paramètres d\'un trou noir.',
217
+ },
143
218
  ],
144
219
  faq,
145
220
  bibliography,