@jjlmoya/utils-science 1.42.0 → 1.43.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 (129) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/diacritics_density.test.ts +140 -0
  6. package/src/tests/inverted_punctuation.test.ts +84 -0
  7. package/src/tests/locale_completeness.test.ts +2 -2
  8. package/src/tests/script_density.test.ts +94 -0
  9. package/src/tests/tool_validation.test.ts +2 -3
  10. package/src/tool/conway-life-rule-lab/i18n/de.ts +62 -5
  11. package/src/tool/conway-life-rule-lab/i18n/es.ts +62 -5
  12. package/src/tool/conway-life-rule-lab/i18n/fr.ts +62 -5
  13. package/src/tool/conway-life-rule-lab/i18n/id.ts +102 -45
  14. package/src/tool/conway-life-rule-lab/i18n/it.ts +62 -5
  15. package/src/tool/conway-life-rule-lab/i18n/ja.ts +62 -5
  16. package/src/tool/conway-life-rule-lab/i18n/ko.ts +62 -5
  17. package/src/tool/conway-life-rule-lab/i18n/nl.ts +102 -45
  18. package/src/tool/conway-life-rule-lab/i18n/pl.ts +62 -5
  19. package/src/tool/conway-life-rule-lab/i18n/pt.ts +62 -5
  20. package/src/tool/conway-life-rule-lab/i18n/ru.ts +62 -5
  21. package/src/tool/conway-life-rule-lab/i18n/sv.ts +62 -5
  22. package/src/tool/conway-life-rule-lab/i18n/tr.ts +62 -5
  23. package/src/tool/conway-life-rule-lab/i18n/zh.ts +62 -5
  24. package/src/tool/cosmic-inflation/i18n/es.ts +5 -5
  25. package/src/tool/cosmic-inflation/i18n/it.ts +51 -51
  26. package/src/tool/cosmic-inflation/i18n/ru.ts +42 -150
  27. package/src/tool/double-slit-decoherence/i18n/fr.ts +42 -86
  28. package/src/tool/double-slit-decoherence/i18n/it.ts +28 -72
  29. package/src/tool/double-slit-decoherence/i18n/pt.ts +35 -79
  30. package/src/tool/double-slit-decoherence/i18n/ru.ts +59 -59
  31. package/src/tool/double-slit-decoherence/i18n/sv.ts +33 -81
  32. package/src/tool/double-slit-decoherence/i18n/tr.ts +55 -55
  33. package/src/tool/dyson-sphere-energy-capture/i18n/de.ts +42 -123
  34. package/src/tool/dyson-sphere-energy-capture/i18n/es.ts +6 -6
  35. package/src/tool/dyson-sphere-energy-capture/i18n/fr.ts +63 -131
  36. package/src/tool/dyson-sphere-energy-capture/i18n/it.ts +36 -113
  37. package/src/tool/dyson-sphere-energy-capture/i18n/ja.ts +66 -42
  38. package/src/tool/dyson-sphere-energy-capture/i18n/ko.ts +66 -42
  39. package/src/tool/dyson-sphere-energy-capture/i18n/pl.ts +50 -133
  40. package/src/tool/dyson-sphere-energy-capture/i18n/pt.ts +51 -132
  41. package/src/tool/dyson-sphere-energy-capture/i18n/ru.ts +61 -98
  42. package/src/tool/dyson-sphere-energy-capture/i18n/sv.ts +56 -137
  43. package/src/tool/dyson-sphere-energy-capture/i18n/tr.ts +61 -142
  44. package/src/tool/dyson-sphere-energy-capture/i18n/zh.ts +67 -43
  45. package/src/tool/epidemic-sir-simulator/bibliography.astro +15 -0
  46. package/src/tool/epidemic-sir-simulator/bibliography.ts +37 -0
  47. package/src/tool/epidemic-sir-simulator/component.astro +384 -0
  48. package/src/tool/epidemic-sir-simulator/entry.ts +30 -0
  49. package/src/tool/epidemic-sir-simulator/epidemic-sir-simulator.css +624 -0
  50. package/src/tool/epidemic-sir-simulator/i18n/de.ts +347 -0
  51. package/src/tool/epidemic-sir-simulator/i18n/en.ts +347 -0
  52. package/src/tool/epidemic-sir-simulator/i18n/es.ts +347 -0
  53. package/src/tool/epidemic-sir-simulator/i18n/fr.ts +347 -0
  54. package/src/tool/epidemic-sir-simulator/i18n/id.ts +347 -0
  55. package/src/tool/epidemic-sir-simulator/i18n/it.ts +347 -0
  56. package/src/tool/epidemic-sir-simulator/i18n/ja.ts +347 -0
  57. package/src/tool/epidemic-sir-simulator/i18n/ko.ts +347 -0
  58. package/src/tool/epidemic-sir-simulator/i18n/nl.ts +347 -0
  59. package/src/tool/epidemic-sir-simulator/i18n/pl.ts +347 -0
  60. package/src/tool/epidemic-sir-simulator/i18n/pt.ts +347 -0
  61. package/src/tool/epidemic-sir-simulator/i18n/ru.ts +347 -0
  62. package/src/tool/epidemic-sir-simulator/i18n/sv.ts +347 -0
  63. package/src/tool/epidemic-sir-simulator/i18n/tr.ts +347 -0
  64. package/src/tool/epidemic-sir-simulator/i18n/zh.ts +347 -0
  65. package/src/tool/epidemic-sir-simulator/index.ts +12 -0
  66. package/src/tool/epidemic-sir-simulator/logic.ts +163 -0
  67. package/src/tool/epidemic-sir-simulator/seo.astro +16 -0
  68. package/src/tool/fermi-paradox-filter-lab/bibliography.astro +14 -0
  69. package/src/tool/fermi-paradox-filter-lab/bibliography.ts +24 -0
  70. package/src/tool/fermi-paradox-filter-lab/component.astro +257 -0
  71. package/src/tool/fermi-paradox-filter-lab/entry.ts +26 -0
  72. package/src/tool/fermi-paradox-filter-lab/fermi-paradox-filter-lab.css +228 -0
  73. package/src/tool/fermi-paradox-filter-lab/i18n/de.ts +192 -0
  74. package/src/tool/fermi-paradox-filter-lab/i18n/en.ts +192 -0
  75. package/src/tool/fermi-paradox-filter-lab/i18n/es.ts +193 -0
  76. package/src/tool/fermi-paradox-filter-lab/i18n/fr.ts +192 -0
  77. package/src/tool/fermi-paradox-filter-lab/i18n/id.ts +192 -0
  78. package/src/tool/fermi-paradox-filter-lab/i18n/it.ts +192 -0
  79. package/src/tool/fermi-paradox-filter-lab/i18n/ja.ts +192 -0
  80. package/src/tool/fermi-paradox-filter-lab/i18n/ko.ts +192 -0
  81. package/src/tool/fermi-paradox-filter-lab/i18n/nl.ts +192 -0
  82. package/src/tool/fermi-paradox-filter-lab/i18n/pl.ts +193 -0
  83. package/src/tool/fermi-paradox-filter-lab/i18n/pt.ts +192 -0
  84. package/src/tool/fermi-paradox-filter-lab/i18n/ru.ts +192 -0
  85. package/src/tool/fermi-paradox-filter-lab/i18n/sv.ts +192 -0
  86. package/src/tool/fermi-paradox-filter-lab/i18n/tr.ts +192 -0
  87. package/src/tool/fermi-paradox-filter-lab/i18n/zh.ts +192 -0
  88. package/src/tool/fermi-paradox-filter-lab/index.ts +11 -0
  89. package/src/tool/fermi-paradox-filter-lab/logic.ts +125 -0
  90. package/src/tool/fermi-paradox-filter-lab/seo.astro +15 -0
  91. package/src/tool/global-albedo-snowball-simulator/i18n/ru.ts +50 -63
  92. package/src/tool/lorenz-attractor/i18n/de.ts +52 -78
  93. package/src/tool/lorenz-attractor/i18n/es.ts +5 -5
  94. package/src/tool/lorenz-attractor/i18n/fr.ts +51 -77
  95. package/src/tool/lorenz-attractor/i18n/it.ts +36 -36
  96. package/src/tool/lorenz-attractor/i18n/pt.ts +51 -75
  97. package/src/tool/mandelbrot-fractal/i18n/de.ts +31 -58
  98. package/src/tool/mandelbrot-fractal/i18n/es.ts +5 -5
  99. package/src/tool/mandelbrot-fractal/i18n/fr.ts +39 -57
  100. package/src/tool/mandelbrot-fractal/i18n/it.ts +30 -48
  101. package/src/tool/mandelbrot-fractal/i18n/pl.ts +34 -52
  102. package/src/tool/mandelbrot-fractal/i18n/pt.ts +30 -30
  103. package/src/tool/mandelbrot-fractal/i18n/ru.ts +42 -60
  104. package/src/tool/mandelbrot-fractal/i18n/sv.ts +38 -65
  105. package/src/tool/natural-selection-drift/i18n/it.ts +18 -95
  106. package/src/tool/natural-selection-drift/i18n/pt.ts +76 -19
  107. package/src/tool/natural-selection-drift/i18n/ru.ts +27 -24
  108. package/src/tool/natural-selection-drift/i18n/sv.ts +9 -6
  109. package/src/tool/phase-diagram-critical-points/i18n/de.ts +30 -68
  110. package/src/tool/phase-diagram-critical-points/i18n/es.ts +4 -4
  111. package/src/tool/phase-diagram-critical-points/i18n/fr.ts +37 -75
  112. package/src/tool/phase-diagram-critical-points/i18n/it.ts +8 -8
  113. package/src/tool/planet-atmosphere-survival/i18n/ru.ts +49 -192
  114. package/src/tool/radioactive-decay/i18n/de.ts +16 -32
  115. package/src/tool/radioactive-decay/i18n/es.ts +5 -5
  116. package/src/tool/radioactive-decay/i18n/fr.ts +24 -40
  117. package/src/tool/radioactive-decay/i18n/it.ts +18 -35
  118. package/src/tool/radioactive-decay/i18n/pt.ts +34 -34
  119. package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +52 -128
  120. package/src/tool/temperature-timeline/i18n/de.ts +46 -174
  121. package/src/tool/temperature-timeline/i18n/es.ts +5 -5
  122. package/src/tool/temperature-timeline/i18n/fr.ts +69 -144
  123. package/src/tool/temperature-timeline/i18n/it.ts +34 -133
  124. package/src/tool/temperature-timeline/i18n/pt.ts +35 -174
  125. package/src/tool/temperature-timeline/i18n/ru.ts +48 -174
  126. package/src/tool/twin-paradox-visualizer/i18n/es.ts +5 -5
  127. package/src/tool/twin-paradox-visualizer/i18n/fr.ts +34 -34
  128. package/src/tool/twin-paradox-visualizer/i18n/pt.ts +31 -71
  129. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-science",
3
- "version": "1.42.0",
3
+ "version": "1.43.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -22,10 +22,12 @@ import { dysonSphereEnergyCapture } from '../tool/dyson-sphere-energy-capture/in
22
22
  import { globalAlbedoSnowballSimulator } from '../tool/global-albedo-snowball-simulator/index';
23
23
  import { conwayLifeRuleLab } from '../tool/conway-life-rule-lab/index';
24
24
  import { crystalLatticeStructureFinder } from '../tool/crystal-lattice-structure-finder/index';
25
+ import { fermiParadoxFilterLab } from '../tool/fermi-paradox-filter-lab/index';
26
+ import { epidemicSirSimulator } from '../tool/epidemic-sir-simulator/index';
25
27
 
26
28
  export const scienceCategory: ScienceCategoryEntry = {
27
29
  icon: 'mdi:flask',
28
- tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator, conwayLifeRuleLab, crystalLatticeStructureFinder],
30
+ tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator, conwayLifeRuleLab, crystalLatticeStructureFinder, fermiParadoxFilterLab, epidemicSirSimulator],
29
31
  i18n: {
30
32
  es: () => import('./i18n/es').then((m) => m.content),
31
33
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -22,6 +22,8 @@ export { dysonSphereEnergyCapture } from './tool/dyson-sphere-energy-capture/ent
22
22
  export { globalAlbedoSnowballSimulator } from './tool/global-albedo-snowball-simulator/entry';
23
23
  export { conwayLifeRuleLab } from './tool/conway-life-rule-lab/entry';
24
24
  export { crystalLatticeStructureFinder } from './tool/crystal-lattice-structure-finder/entry';
25
+ export { fermiParadoxFilterLab } from './tool/fermi-paradox-filter-lab/entry';
26
+ export { epidemicSirSimulator } from './tool/epidemic-sir-simulator/entry';
25
27
  export { scienceCategory } from './category';
26
28
  import { asteroidImpact } from './tool/asteroid-impact/entry';
27
29
  import { cellularRenewal } from './tool/cellular-renewal/entry';
@@ -46,4 +48,6 @@ import { dysonSphereEnergyCapture } from './tool/dyson-sphere-energy-capture/ent
46
48
  import { globalAlbedoSnowballSimulator } from './tool/global-albedo-snowball-simulator/entry';
47
49
  import { conwayLifeRuleLab } from './tool/conway-life-rule-lab/entry';
48
50
  import { crystalLatticeStructureFinder } from './tool/crystal-lattice-structure-finder/entry';
49
- export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator, conwayLifeRuleLab, crystalLatticeStructureFinder];
51
+ import { fermiParadoxFilterLab } from './tool/fermi-paradox-filter-lab/entry';
52
+ import { epidemicSirSimulator } from './tool/epidemic-sir-simulator/entry';
53
+ export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator, conwayLifeRuleLab, crystalLatticeStructureFinder, fermiParadoxFilterLab, epidemicSirSimulator];
package/src/index.ts CHANGED
@@ -23,6 +23,8 @@ export { DYSON_SPHERE_ENERGY_CAPTURE_TOOL } from './tool/dyson-sphere-energy-cap
23
23
  export { GLOBAL_ALBEDO_SNOWBALL_SIMULATOR_TOOL } from './tool/global-albedo-snowball-simulator/index';
24
24
  export { CONWAY_LIFE_RULE_LAB_TOOL } from './tool/conway-life-rule-lab/index';
25
25
  export { CRYSTAL_LATTICE_STRUCTURE_FINDER_TOOL } from './tool/crystal-lattice-structure-finder/index';
26
+ export { FERMI_PARADOX_FILTER_LAB_TOOL } from './tool/fermi-paradox-filter-lab/index';
27
+ export { EPIDEMIC_SIR_SIMULATOR_TOOL } from './tool/epidemic-sir-simulator/index';
26
28
 
27
29
  export type {
28
30
  KnownLocale,
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it, afterAll } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ const failures: Map<string, string[]> = new Map();
5
+
6
+ type LocaleWithDiacritics = keyof typeof DIACRITIC_RULES;
7
+
8
+ const DIACRITIC_RULES = {
9
+ de: {
10
+ language: 'German',
11
+ expectedCharacters: 'ä ö ü ß',
12
+ characters: /[äöüÄÖÜß]/g,
13
+ minPerThousandLetters: 0.1,
14
+ },
15
+ es: {
16
+ language: 'Spanish',
17
+ expectedCharacters: 'á é í ó ú ü ñ',
18
+ characters: /[áéíóúüñÁÉÍÓÚÜÑ]/g,
19
+ minPerThousandLetters: 0.1,
20
+ },
21
+ fr: {
22
+ language: 'French',
23
+ expectedCharacters: 'à â æ ç é è ê ë î ï ô œ ù û ü ÿ',
24
+ characters: /[àâæçéèêëîïôœùûüÿÀÂÆÇÉÈÊËÎÏÔŒÙÛÜŸ]/g,
25
+ minPerThousandLetters: 0.1,
26
+ },
27
+ it: {
28
+ language: 'Italian',
29
+ expectedCharacters: 'à è é ì í î ò ó ù ú',
30
+ characters: /[àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]/g,
31
+ minPerThousandLetters: 0.1,
32
+ },
33
+ pl: {
34
+ language: 'Polish',
35
+ expectedCharacters: 'ą ć ę ł ń ó ś ź ż',
36
+ characters: /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/g,
37
+ minPerThousandLetters: 0.1,
38
+ },
39
+ pt: {
40
+ language: 'Portuguese',
41
+ expectedCharacters: 'á â ã à ç é ê í ó ô õ ú ü',
42
+ characters: /[áâãàçéêíóôõúüÁÂÃÀÇÉÊÍÓÔÕÚÜ]/g,
43
+ minPerThousandLetters: 0.1,
44
+ },
45
+ sv: {
46
+ language: 'Swedish',
47
+ expectedCharacters: 'å ä ö',
48
+ characters: /[åäöÅÄÖ]/g,
49
+ minPerThousandLetters: 0.1,
50
+ },
51
+ tr: {
52
+ language: 'Turkish',
53
+ expectedCharacters: 'ç ğ ı İ ö ş ü',
54
+ characters: /[çğıöşüÇĞİÖŞÜ]/g,
55
+ minPerThousandLetters: 0.1,
56
+ },
57
+ } as const;
58
+
59
+ const LETTERS = /\p{L}/gu;
60
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
61
+
62
+ function collectStrings(value: unknown): string[] {
63
+ if (typeof value === 'string') return [value];
64
+ if (!value || typeof value !== 'object') return [];
65
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
66
+ return Object.values(value).flatMap(collectStrings);
67
+ }
68
+
69
+ function normalizeText(value: unknown): string {
70
+ return collectStrings(value).join(' ').normalize('NFC');
71
+ }
72
+
73
+ function translatableContent(content: Record<string, unknown>) {
74
+ return TRANSLATABLE_KEYS.map((key) => content[key]);
75
+ }
76
+
77
+ function letterCount(text: string): number {
78
+ return text.match(LETTERS)?.length ?? 0;
79
+ }
80
+
81
+ function diacriticCount(text: string, locale: LocaleWithDiacritics): number {
82
+ return text.match(DIACRITIC_RULES[locale].characters)?.length ?? 0;
83
+ }
84
+
85
+ function diacriticsPerThousandLetters(text: string, locale: LocaleWithDiacritics): number {
86
+ const letters = letterCount(text);
87
+ if (letters === 0) return 0;
88
+ return diacriticCount(text, locale) / letters * 1000;
89
+ }
90
+
91
+ describe('Diacritics density validation', () => {
92
+ ALL_TOOLS.forEach((tool) => {
93
+ describe(`Tool: ${tool.entry.id}`, () => {
94
+ Object.keys(DIACRITIC_RULES).forEach((locale) => {
95
+ it(`${locale} keeps the expected accent and special-letter set`, async () => {
96
+ const typedLocale = locale as LocaleWithDiacritics;
97
+ const loader = tool.entry.i18n[typedLocale];
98
+ if (!loader) return;
99
+
100
+ const content = await loader();
101
+ const text = normalizeText(translatableContent(content as Record<string, unknown>));
102
+ const rule = DIACRITIC_RULES[typedLocale];
103
+ const letters = letterCount(text);
104
+ const matches = diacriticCount(text, typedLocale);
105
+ const density = diacriticsPerThousandLetters(text, typedLocale);
106
+
107
+ expect(
108
+ density,
109
+ [
110
+ `Possible spelling or encoding issue detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
111
+ `The text has ${matches} special characters (${density.toFixed(2)} per 1000 letters, ${letters} letters analyzed).`,
112
+ `This locale should include some of these characters: ${rule.expectedCharacters}.`,
113
+ 'If the count is 0 or near 0, accents, tildes, or special letters were probably stripped by encoding or normalization.',
114
+ ].join(' '),
115
+ ).toBeGreaterThanOrEqual(rule.minPerThousandLetters);
116
+
117
+ if (density < rule.minPerThousandLetters) {
118
+ const existing = failures.get(tool.entry.id) ?? [];
119
+ existing.push(typedLocale);
120
+ failures.set(tool.entry.id, existing);
121
+ }
122
+ });
123
+ });
124
+ });
125
+ });
126
+ });
127
+
128
+ afterAll(() => {
129
+ if (failures.size > 0) {
130
+ const sorted = [...failures.entries()].sort(([a], [b]) => a.localeCompare(b));
131
+ console.log('\n=== DIACRITICS DENSITY FAILURES (grouped by tool) ===');
132
+ let total = 0;
133
+ for (const [tool, locales] of sorted) {
134
+ locales.sort();
135
+ console.log(` ${tool}: ${locales.join(', ')}`);
136
+ total += locales.length;
137
+ }
138
+ console.log(` Total: ${total} failures across ${failures.size} tools\n`);
139
+ }
140
+ });
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ const INVERTED_PUNCTUATION_LOCALES = {
5
+ es: {
6
+ language: 'Spanish',
7
+ questionStart: '¿',
8
+ questionEnd: '?',
9
+ exclamationStart: '¡',
10
+ exclamationEnd: '!',
11
+ },
12
+ } as const;
13
+
14
+ type InvertedPunctuationLocale = keyof typeof INVERTED_PUNCTUATION_LOCALES;
15
+
16
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
17
+
18
+ function collectStrings(value: unknown): string[] {
19
+ if (typeof value === 'string') return [value];
20
+ if (!value || typeof value !== 'object') return [];
21
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
22
+ return Object.values(value).flatMap(collectStrings);
23
+ }
24
+
25
+ function translatableStrings(content: Record<string, unknown>): string[] {
26
+ return TRANSLATABLE_KEYS.flatMap((key) => collectStrings(content[key]));
27
+ }
28
+
29
+ function sentenceStart(text: string, endIndex: number): string {
30
+ const beforeMark = text.slice(0, endIndex).trimEnd();
31
+ const boundary = Math.max(
32
+ beforeMark.lastIndexOf('.'),
33
+ beforeMark.lastIndexOf(':'),
34
+ beforeMark.lastIndexOf(';'),
35
+ beforeMark.lastIndexOf('\n'),
36
+ );
37
+
38
+ return beforeMark.slice(boundary + 1).trimStart();
39
+ }
40
+
41
+ function findMissingInvertedMarks(
42
+ text: string,
43
+ startMark: string,
44
+ endMark: string,
45
+ ): string[] {
46
+ return [...text.matchAll(new RegExp(`\\${endMark}`, 'g'))]
47
+ .map((match) => sentenceStart(text, match.index ?? 0))
48
+ .filter((segment) => segment.length > 0 && !segment.includes(startMark))
49
+ .map((segment) => `${segment}${endMark}`);
50
+ }
51
+
52
+ describe('Inverted punctuation validation', () => {
53
+ ALL_TOOLS.forEach((tool) => {
54
+ describe(`Tool: ${tool.entry.id}`, () => {
55
+ Object.keys(INVERTED_PUNCTUATION_LOCALES).forEach((locale) => {
56
+ it(`${locale} uses opening punctuation marks for questions and exclamations`, async () => {
57
+ const typedLocale = locale as InvertedPunctuationLocale;
58
+ const loader = tool.entry.i18n[typedLocale];
59
+ if (!loader) return;
60
+
61
+ const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
62
+ const content = await loader();
63
+ const strings = translatableStrings(content as Record<string, unknown>);
64
+ const missingQuestions = strings.flatMap((text) =>
65
+ findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd)
66
+ );
67
+ const missingExclamations = strings.flatMap((text) =>
68
+ findMissingInvertedMarks(text, rule.exclamationStart, rule.exclamationEnd)
69
+ );
70
+ const failures = [...missingQuestions, ...missingExclamations];
71
+
72
+ expect(
73
+ failures,
74
+ [
75
+ `Missing opening punctuation marks in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
76
+ `Questions must use ${rule.questionStart}...${rule.questionEnd} and exclamations must use ${rule.exclamationStart}...${rule.exclamationEnd}.`,
77
+ `Examples: ${failures.slice(0, 5).join(' | ')}`,
78
+ ].join(' '),
79
+ ).toHaveLength(0);
80
+ });
81
+ });
82
+ });
83
+ });
84
+ });
@@ -18,7 +18,7 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
  });
20
20
 
21
- it('all 23 tools registered', () => {
22
- expect(ALL_TOOLS.length).toBe(23);
21
+ it('all 25 tools registered', () => {
22
+ expect(ALL_TOOLS.length).toBe(25);
23
23
  });
24
24
  });
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ type ScriptLocale = keyof typeof SCRIPT_RULES;
5
+
6
+ const SCRIPT_RULES = {
7
+ ja: {
8
+ language: 'Japanese',
9
+ scriptName: 'kana/kanji',
10
+ scriptCharacters: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/gu,
11
+ minScriptRatio: 0.45,
12
+ },
13
+ ko: {
14
+ language: 'Korean',
15
+ scriptName: 'hangul',
16
+ scriptCharacters: /\p{Script=Hangul}/gu,
17
+ minScriptRatio: 0.55,
18
+ },
19
+ ru: {
20
+ language: 'Russian',
21
+ scriptName: 'cyrillic',
22
+ scriptCharacters: /\p{Script=Cyrillic}/gu,
23
+ minScriptRatio: 0.65,
24
+ },
25
+ zh: {
26
+ language: 'Chinese',
27
+ scriptName: 'han',
28
+ scriptCharacters: /\p{Script=Han}/gu,
29
+ minScriptRatio: 0.45,
30
+ },
31
+ } as const;
32
+
33
+ const LETTERS = /\p{L}/gu;
34
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
35
+
36
+ function collectStrings(value: unknown): string[] {
37
+ if (typeof value === 'string') return [value];
38
+ if (!value || typeof value !== 'object') return [];
39
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
40
+ return Object.values(value).flatMap(collectStrings);
41
+ }
42
+
43
+ function normalizeText(value: unknown): string {
44
+ return collectStrings(value).join(' ').normalize('NFC');
45
+ }
46
+
47
+ function translatableContent(content: Record<string, unknown>) {
48
+ return TRANSLATABLE_KEYS.map((key) => content[key]);
49
+ }
50
+
51
+ function letterCount(text: string): number {
52
+ return text.match(LETTERS)?.length ?? 0;
53
+ }
54
+
55
+ function scriptCount(text: string, locale: ScriptLocale): number {
56
+ return text.match(SCRIPT_RULES[locale].scriptCharacters)?.length ?? 0;
57
+ }
58
+
59
+ function scriptRatio(text: string, locale: ScriptLocale): number {
60
+ const letters = letterCount(text);
61
+ if (letters === 0) return 0;
62
+ return scriptCount(text, locale) / letters;
63
+ }
64
+
65
+ describe('Native script density validation', () => {
66
+ ALL_TOOLS.forEach((tool) => {
67
+ describe(`Tool: ${tool.entry.id}`, () => {
68
+ Object.keys(SCRIPT_RULES).forEach((locale) => {
69
+ it(`${locale} keeps most translated text in its native script`, async () => {
70
+ const typedLocale = locale as ScriptLocale;
71
+ const loader = tool.entry.i18n[typedLocale];
72
+ if (!loader) return;
73
+
74
+ const content = await loader();
75
+ const rule = SCRIPT_RULES[typedLocale];
76
+ const text = normalizeText(translatableContent(content as Record<string, unknown>));
77
+ const letters = letterCount(text);
78
+ const matches = scriptCount(text, typedLocale);
79
+ const ratio = scriptRatio(text, typedLocale);
80
+
81
+ expect(
82
+ ratio,
83
+ [
84
+ `Possible broken translation detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
85
+ `The text has ${matches} ${rule.scriptName} characters out of ${letters} analyzed letters (${(ratio * 100).toFixed(1)}%).`,
86
+ `Most translatable content should be written in ${rule.scriptName} script.`,
87
+ 'Non-translatable fields such as slug, bibliography, and schemas are ignored to avoid false positives.',
88
+ ].join(' '),
89
+ ).toBeGreaterThanOrEqual(rule.minScriptRatio);
90
+ });
91
+ });
92
+ });
93
+ });
94
+ });
@@ -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 23 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(23);
7
+ it('should have 25 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(25);
9
9
  });
10
10
 
11
11
  it('scienceCategory should be defined', () => {
@@ -14,4 +14,3 @@ describe('Tool Validation Suite', () => {
14
14
  });
15
15
  });
16
16
  });
17
-
@@ -1,11 +1,52 @@
1
- import { content as enContent } from './en';
1
+ import { bibliography } from '../bibliography';
2
2
  import type { ToolLocaleContent } from '../../../types';
3
3
 
4
+ const slug = 'conway-spiel-des-lebens-regellabor';
5
+ const title = 'Conway Spiel des Lebens Regellabor';
6
+ const description = 'Spiele, bearbeite und vergleiche Conway-artige zelluläre Automaten mit B/S-Regeln, Mustersaaten, Live-Metriken und responsivem Simulationsfeld.';
7
+
8
+ const howTo = [
9
+ {
10
+ name: 'Feld besäen',
11
+ text: 'Wähle ein Muster wie einen Gleiter, Pulsar oder Gosper-Kanone, platziere es auf dem Gitter oder erzeuge eine zufällige Population.',
12
+ },
13
+ {
14
+ name: 'Simulation starten',
15
+ text: 'Nutze Start, Pause, Schritt und Tempo-Steuerung, um Geburten, Tode, Dichte und Stabilität über Zeit zu beobachten.',
16
+ },
17
+ {
18
+ name: 'Regel ändern',
19
+ text: 'Wechsle Voreinstellungen oder gib eine B/S-Regel wie B3/S23, B36/S23 oder B3678/S34678 ein, um verschiedene Automaten zu vergleichen.',
20
+ },
21
+ ];
22
+
23
+ const faq = [
24
+ {
25
+ question: 'Was ist B/S-Notation im Spiel des Lebens?',
26
+ answer: 'Die B/S-Notation listet die Nachbarzahlen, die eine neue lebende Zelle erzeugen, und die Zahlen, die eine bestehende lebende Zelle überleben lassen. Conways Spiel des Lebens ist B3/S23: tote Zellen werden mit genau drei Nachbarn geboren, lebende Zellen überleben mit zwei oder drei Nachbarn.',
27
+ },
28
+ {
29
+ question: 'Warum heißt dieses Werkzeug Regellabor statt nur Simulator?',
30
+ answer: 'Die klassische Conway-Regel ist enthalten, aber der Simulator ist dafür gebaut, Familien von Life-artigen zellulären Automaten zu vergleichen. Das Bearbeiten der Geburts- und Überlebenszahlen ändert das Langzeitverhalten und erzeugt Replikatoren, Wellen, Inselgruppen oder unkontrolliertes Wachstum.',
31
+ },
32
+ {
33
+ question: 'Was bedeutet Stabilität im Simulator?',
34
+ answer: 'Stabilität schätzt, wie stark sich die Kolonie in der letzten Generation verändert hat. Hohe Stabilität bedeutet wenige Geburten und Tode relativ zur Population; niedrige Stabilität bedeutet turbulentes oder schnell reorganisierendes Feld.',
35
+ },
36
+ {
37
+ question: 'Hat das Feld Ränder?',
38
+ answer: 'Die Simulation verwendet toroidales Wickeln: der linke Rand verbindet sich mit dem rechten, und oben verbindet sich mit unten. So bewegen sich Muster natürlich weiter, ohne an harten Grenzen zu verschwinden.',
39
+ },
40
+ {
41
+ question: 'Kann ich ein eigenes Startmuster zeichnen?',
42
+ answer: 'Ja. Pausiere die Simulation und klicke oder tippe auf Zellen, um sie umzuschalten. Du kannst handgezeichnete Zellen mit eingebauten Mustern kombinieren und das Ergebnis dann unter beliebiger Regelvoreinstellung ausführen.',
43
+ },
44
+ ];
45
+
4
46
  export const content: ToolLocaleContent = {
5
- ...enContent,
6
- slug: 'conway-spiel-des-lebens-regellabor',
7
- title: 'Conway Spiel des Lebens Regellabor',
8
- description: 'Spiele, bearbeite und vergleiche Conway-artige zelluläre Automaten mit B/S-Regeln, Mustersaaten, Live-Metriken und responsivem Simulationsfeld.',
47
+ slug,
48
+ title,
49
+ description,
9
50
  ui: {
10
51
  boardLabel: 'Feld für Life-artige zelluläre Automaten',
11
52
  play: 'Start',
@@ -47,4 +88,20 @@ export const content: ToolLocaleContent = {
47
88
  statusChaotic: 'volatil',
48
89
  invalidRule: 'Verwende B/S-Notation wie B3/S23.',
49
90
  },
91
+ seo: [
92
+ { type: 'title', text: 'Conway Spiel des Lebens Simulator mit bearbeitbaren Regeln', level: 2 },
93
+ { type: 'paragraph', html: 'Dieses Regellabor lässt dich den klassischen zellulären Automaten ausführen und seine Regeln gezielt verändern und vergleichen. Das Feld unterstützt direktes Zeichnen, Musterpositionen, Zufallssaaten, schrittweise Inspektion, variables Tempo und Live-Messungen für Population, Dichte, Geburten, Tode und Stabilität.' },
94
+ { type: 'title', text: 'Wie B/S-Regeln das Spiel verändern', level: 3 },
95
+ { type: 'paragraph', html: 'Life-artige zelluläre Automaten werden oft in <strong>B/S-Notation</strong> geschrieben. Die B-Seite definiert, wie viele lebende Nachbarn eine tote Zelle braucht, um zu entstehen; die S-Seite definiert, wie viele Nachbarn eine lebende Zelle zum Überleben braucht. Conways ursprüngliche Regel <strong>B3/S23</strong> ist berühmt, weil sie in einem schmalen Bereich zwischen Aussterben und unkontrolliertem Wachstum liegt.' },
96
+ { type: 'title', text: 'Live-Metriken lesen', level: 3 },
97
+ { type: 'paragraph', html: 'Population und Dichte zeigen, wie viele Zellen am Leben sind, aber sie erzählen nicht die ganze Geschichte. Geburten und Tode zeigen das aktuelle Aktivitätsniveau, während Stabilität abschätzt, ob die Kolonie sich in einen Oszillator einpendelt, in Stilleben einfriert oder turbulent bleibt.' },
98
+ ],
99
+ faq,
100
+ bibliography,
101
+ howTo,
102
+ schemas: [
103
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'EducationalApplication', operatingSystem: 'Any' },
104
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
105
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
106
+ ],
50
107
  };
@@ -1,11 +1,52 @@
1
- import { content as enContent } from './en';
1
+ import { bibliography } from '../bibliography';
2
2
  import type { ToolLocaleContent } from '../../../types';
3
3
 
4
+ const slug = 'laboratorio-reglas-juego-vida-conway';
5
+ const title = 'Laboratorio de Reglas del Juego de la Vida de Conway';
6
+ const description = 'Juega, edita y compara autómatas celulares tipo Conway con reglas B/S, semillas de patrones, métricas en vivo y un tablero de simulación adaptable.';
7
+
8
+ const howTo = [
9
+ {
10
+ name: 'Sembrar el tablero',
11
+ text: 'Elige un patrón como un planeador, pulsar o cañón de Gosper, colócalo en la cuadrícula o aleatoriza una población inicial.',
12
+ },
13
+ {
14
+ name: 'Ejecutar la simulación',
15
+ text: 'Usa los controles de reproducción, pausa, paso y velocidad para observar cómo evolucionan los nacimientos, muertes, densidad y estabilidad.',
16
+ },
17
+ {
18
+ name: 'Cambiar la regla',
19
+ text: 'Cambia entre preajustes o escribe una regla B/S como B3/S23, B36/S23 o B3678/S34678 para comparar diferentes autómatas.',
20
+ },
21
+ ];
22
+
23
+ const faq = [
24
+ {
25
+ question: '¿Qué es la notación B/S en el Juego de la Vida?',
26
+ answer: 'La notación B/S lista los conteos de vecinos que crean una nueva célula viva y los conteos que permiten a una célula viva existente sobrevivir. El Juego de la Vida de Conway es B3/S23: las células muertas nacen con exactamente tres vecinos, y las células vivas sobreviven con dos o tres vecinos.',
27
+ },
28
+ {
29
+ question: '¿Por qué se llama laboratorio de reglas en lugar de solo simulador?',
30
+ answer: 'La regla clásica de Conway está incluida, pero el simulador está diseñado para comparar familias de autómatas celulares tipo Life. Editar los conteos de nacimiento y supervivencia cambia el comportamiento a largo plazo, produciendo replicadores, ondas, islas congeladas, crecimiento caótico o extinción.',
31
+ },
32
+ {
33
+ question: '¿Qué significa estabilidad en el simulador?',
34
+ answer: 'La estabilidad estima cuánto cambió la colonia durante la última generación. Alta estabilidad significa pocas células nacidas o muertas relativas a la población; baja estabilidad significa que el tablero es turbulento o se reorganiza rápidamente.',
35
+ },
36
+ {
37
+ question: '¿El tablero tiene bordes?',
38
+ answer: 'La simulación usa envoltura toroidal, por lo que el borde izquierdo se conecta con el derecho y la parte superior con la inferior. Esto mantiene los patrones moviéndose naturalmente sin desaparecer en bordes duros.',
39
+ },
40
+ {
41
+ question: '¿Puedo dibujar mi propio patrón inicial?',
42
+ answer: 'Sí. Pausa la simulación y haz clic o toca las células del tablero para activarlas o desactivarlas. Puedes combinar células dibujadas a mano con patrones integrados y ejecutar el resultado bajo cualquier preajuste de regla.',
43
+ },
44
+ ];
45
+
4
46
  export const content: ToolLocaleContent = {
5
- ...enContent,
6
- slug: 'laboratorio-reglas-juego-vida-conway',
7
- title: 'Laboratorio de Reglas del Juego de la Vida de Conway',
8
- description: 'Juega, edita y compara autómatas celulares tipo Conway con reglas B/S, semillas de patrones, métricas en vivo y un tablero de simulación adaptable.',
47
+ slug,
48
+ title,
49
+ description,
9
50
  ui: {
10
51
  boardLabel: 'Tablero de autómatas celulares tipo Life',
11
52
  play: 'Reproducir',
@@ -47,4 +88,20 @@ export const content: ToolLocaleContent = {
47
88
  statusChaotic: 'volátil',
48
89
  invalidRule: 'Usa notación B/S como B3/S23.',
49
90
  },
91
+ seo: [
92
+ { type: 'title', text: 'Simulador del Juego de la Vida de Conway con reglas editables', level: 2 },
93
+ { type: 'paragraph', html: 'Este laboratorio de reglas del Juego de la Vida de Conway te permite ejecutar el autómata celular clásico y luego cambiar y comparar sus reglas. El tablero admite dibujo directo, colocación de patrones, semillas aleatorias, inspección paso a paso, tempo variable y mediciones en vivo de población, densidad, nacimientos, muertes y estabilidad.' },
94
+ { type: 'title', text: 'Cómo las reglas B/S cambian el juego', level: 3 },
95
+ { type: 'paragraph', html: 'Los autómatas celulares tipo Life se escriben a menudo en <strong>notación B/S</strong>. El lado B define cuántos vecinos vivos necesita una célula muerta para nacer; el lado S define cuántos vecinos necesita una célula viva para sobrevivir. La regla original de Conway, <strong>B3/S23</strong>, es famosa porque se sitúa en una región estrecha entre la extinción y el crecimiento descontrolado.' },
96
+ { type: 'title', text: 'Leer las métricas en vivo', level: 3 },
97
+ { type: 'paragraph', html: 'La población y la densidad muestran cuántas células están vivas, pero no cuentan la historia completa. Los nacimientos y las muertes exponen el nivel de actividad actual, mientras que la estabilidad estima si la colonia está asentándose en un oscilador, congelándose en figuras estáticas o permaneciendo turbulenta.' },
98
+ ],
99
+ faq,
100
+ bibliography,
101
+ howTo,
102
+ schemas: [
103
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'EducationalApplication', operatingSystem: 'Any' },
104
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
105
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
106
+ ],
50
107
  };
@@ -1,11 +1,52 @@
1
- import { content as enContent } from './en';
1
+ import { bibliography } from '../bibliography';
2
2
  import type { ToolLocaleContent } from '../../../types';
3
3
 
4
+ const slug = 'laboratoire-regles-jeu-vie-conway';
5
+ const title = 'Laboratoire de règles du Jeu de la Vie de Conway';
6
+ const description = 'Jouez, modifiez et comparez des automates cellulaires de type Conway avec règles B/S, motifs de départ, métriques en direct et grille responsive.';
7
+
8
+ const howTo = [
9
+ {
10
+ name: 'Ensemencer le plateau',
11
+ text: 'Choisissez un motif comme un planeur, un pulsar ou le canon de Gosper, placez-le sur la grille ou générez une population aléatoire.',
12
+ },
13
+ {
14
+ name: 'Lancer la simulation',
15
+ text: 'Utilisez les contrôles de lecture, pause, pas et tempo pour observer l\'évolution des naissances, morts, densité et stabilité.',
16
+ },
17
+ {
18
+ name: 'Changer la règle',
19
+ text: 'Changez les préréglages ou tapez une règle B/S comme B3/S23, B36/S23 ou B3678/S34678 pour comparer différents automates.',
20
+ },
21
+ ];
22
+
23
+ const faq = [
24
+ {
25
+ question: 'Qu\'est-ce que la notation B/S dans le Jeu de la Vie ?',
26
+ answer: 'La notation B/S liste les nombres de voisins qui creent une nouvelle cellule vivante et les nombres qui permettent a une cellule vivante existante de survivre. Le Jeu de la Vie de Conway est B3/S23: les cellules mortes naissent avec exactement trois voisins, et les cellules vivantes survivent avec deux ou trois voisins.',
27
+ },
28
+ {
29
+ question: 'Pourquoi cet outil s\'appelle laboratoire de règles plutôt que simulateur ?',
30
+ answer: 'La règle classique de Conway est incluse, mais le simulateur est conçu pour comparer des familles d\'automates cellulaires de type Life. Modifier les comptes de naissance et de survie change le comportement à long terme, produisant des réplicateurs, des vagues, des îles gelées, une croissance chaotique ou l\'extinction.',
31
+ },
32
+ {
33
+ question: 'Que signifie la stabilité dans le simulateur ?',
34
+ answer: 'La stabilité estime combien la colonie a changé lors de la dernière génération. Une stabilité élevée signifie peu de cellules nées ou mortes par rapport à la population ; une faible stabilité signifie que le plateau est turbulent ou se réorganise rapidement.',
35
+ },
36
+ {
37
+ question: 'Le plateau a-t-il des bords ?',
38
+ answer: 'La simulation utilise un enroulement toroidal: le bord gauche se connecte au bord droit et le haut se connecte au bas. Cela maintient les motifs se deplacant naturellement sans disparaitre aux bords.',
39
+ },
40
+ {
41
+ question: 'Puis-je dessiner mon propre motif de départ ?',
42
+ answer: 'Oui. Mettez la simulation en pause et cliquez ou tapez sur les cellules du plateau pour les basculer. Vous pouvez combiner des cellules dessinées à la main avec des motifs intégrés et lancer le résultat sous n\'importe quel préréglage de règle.',
43
+ },
44
+ ];
45
+
4
46
  export const content: ToolLocaleContent = {
5
- ...enContent,
6
- slug: 'laboratoire-regles-jeu-vie-conway',
7
- title: 'Laboratoire de règles du Jeu de la Vie de Conway',
8
- description: 'Jouez, modifiez et comparez des automates cellulaires de type Conway avec règles B/S, motifs de départ, métriques en direct et grille responsive.',
47
+ slug,
48
+ title,
49
+ description,
9
50
  ui: {
10
51
  boardLabel: 'Grille d\'automates cellulaires de type Life',
11
52
  play: 'Lire',
@@ -47,4 +88,20 @@ export const content: ToolLocaleContent = {
47
88
  statusChaotic: 'volatile',
48
89
  invalidRule: 'Utilisez une notation B/S comme B3/S23.',
49
90
  },
91
+ seo: [
92
+ { type: 'title', text: 'Simulateur du Jeu de la Vie de Conway avec règles modifiables', level: 2 },
93
+ { type: 'paragraph', html: 'Ce laboratoire de règles vous permet d\'exécuter l\'automate cellulaire classique et de modifier et comparer ses règles. Le plateau prend en charge le dessin direct, le placement de motifs, les graines aléatoires, l\'inspection pas à pas, le tempo variable et les mesures en direct pour la population, la densité, les naissances, les morts et la stabilité.' },
94
+ { type: 'title', text: 'Comment les règles B/S changent le jeu', level: 3 },
95
+ { type: 'paragraph', html: 'Les automates cellulaires de type Life sont souvent écrits en <strong>notation B/S</strong>. Le côté B définit combien de voisins vivants une cellule morte a besoin pour naître ; le côté S définit combien de voisins une cellule vivante a besoin pour survivre. La règle originale de Conway, <strong>B3/S23</strong>, est célèbre car elle se situe dans une région étroite entre extinction et croissance incontrôlée.' },
96
+ { type: 'title', text: 'Lire les métriques en direct', level: 3 },
97
+ { type: 'paragraph', html: 'La population et la densité montrent combien de cellules sont vivantes, mais elles ne racontent pas toute l\'histoire. Les naissances et les morts exposent le niveau d\'activité actuel, tandis que la stabilité estime si la colonie se stabilise dans un oscillateur, se fige dans des structures statiques ou reste turbulente.' },
98
+ ],
99
+ faq,
100
+ bibliography,
101
+ howTo,
102
+ schemas: [
103
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'EducationalApplication', operatingSystem: 'Any' },
104
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
105
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
106
+ ],
50
107
  };