@jjlmoya/utils-sports 1.30.0 → 1.32.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 (159) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/fr.ts +7 -7
  3. package/src/category/i18n/ru.ts +3 -3
  4. package/src/layouts/PreviewLayout.astro +1 -1
  5. package/src/tests/diacritics_density.test.ts +118 -0
  6. package/src/tests/inverted_punctuation.test.ts +84 -0
  7. package/src/tests/no_en_dash.test.ts +35 -6
  8. package/src/tests/script_density.test.ts +94 -0
  9. package/src/tool/baseballScoreKeeper/i18n/de.ts +35 -35
  10. package/src/tool/baseballScoreKeeper/i18n/en.ts +1 -1
  11. package/src/tool/baseballScoreKeeper/i18n/es.ts +37 -36
  12. package/src/tool/baseballScoreKeeper/i18n/fr.ts +39 -39
  13. package/src/tool/baseballScoreKeeper/i18n/id.ts +1 -1
  14. package/src/tool/baseballScoreKeeper/i18n/it.ts +9 -9
  15. package/src/tool/baseballScoreKeeper/i18n/nl.ts +1 -1
  16. package/src/tool/baseballScoreKeeper/i18n/pl.ts +54 -54
  17. package/src/tool/baseballScoreKeeper/i18n/pt.ts +28 -28
  18. package/src/tool/baseballScoreKeeper/i18n/sv.ts +1 -1
  19. package/src/tool/baseballScoreKeeper/i18n/tr.ts +50 -50
  20. package/src/tool/basketScoreKeeper/i18n/de.ts +11 -11
  21. package/src/tool/basketScoreKeeper/i18n/en.ts +11 -11
  22. package/src/tool/basketScoreKeeper/i18n/es.ts +11 -11
  23. package/src/tool/basketScoreKeeper/i18n/fr.ts +14 -14
  24. package/src/tool/basketScoreKeeper/i18n/id.ts +11 -11
  25. package/src/tool/basketScoreKeeper/i18n/it.ts +11 -11
  26. package/src/tool/basketScoreKeeper/i18n/ja.ts +9 -9
  27. package/src/tool/basketScoreKeeper/i18n/ko.ts +9 -9
  28. package/src/tool/basketScoreKeeper/i18n/nl.ts +11 -11
  29. package/src/tool/basketScoreKeeper/i18n/pl.ts +11 -11
  30. package/src/tool/basketScoreKeeper/i18n/pt.ts +11 -11
  31. package/src/tool/basketScoreKeeper/i18n/ru.ts +17 -17
  32. package/src/tool/basketScoreKeeper/i18n/sv.ts +11 -11
  33. package/src/tool/basketScoreKeeper/i18n/tr.ts +12 -12
  34. package/src/tool/basketScoreKeeper/i18n/zh.ts +2 -2
  35. package/src/tool/beachVolleyballScoreKeeper/i18n/de.ts +26 -25
  36. package/src/tool/beachVolleyballScoreKeeper/i18n/es.ts +29 -28
  37. package/src/tool/beachVolleyballScoreKeeper/i18n/fr.ts +37 -36
  38. package/src/tool/beachVolleyballScoreKeeper/i18n/it.ts +15 -14
  39. package/src/tool/beachVolleyballScoreKeeper/i18n/pt.ts +32 -31
  40. package/src/tool/dartsScoreKeeper/i18n/de.ts +1 -1
  41. package/src/tool/dartsScoreKeeper/i18n/es.ts +1 -1
  42. package/src/tool/dartsScoreKeeper/i18n/fr.ts +1 -1
  43. package/src/tool/dartsScoreKeeper/i18n/id.ts +1 -1
  44. package/src/tool/dartsScoreKeeper/i18n/it.ts +1 -1
  45. package/src/tool/dartsScoreKeeper/i18n/ja.ts +1 -1
  46. package/src/tool/dartsScoreKeeper/i18n/ko.ts +1 -1
  47. package/src/tool/dartsScoreKeeper/i18n/nl.ts +1 -1
  48. package/src/tool/dartsScoreKeeper/i18n/pl.ts +1 -1
  49. package/src/tool/dartsScoreKeeper/i18n/pt.ts +1 -1
  50. package/src/tool/dartsScoreKeeper/i18n/ru.ts +1 -1
  51. package/src/tool/dartsScoreKeeper/i18n/sv.ts +1 -1
  52. package/src/tool/dartsScoreKeeper/i18n/tr.ts +1 -1
  53. package/src/tool/dartsScoreKeeper/i18n/zh.ts +1 -1
  54. package/src/tool/footballScoreKeeper/i18n/de.ts +16 -16
  55. package/src/tool/footballScoreKeeper/i18n/en.ts +3 -3
  56. package/src/tool/footballScoreKeeper/i18n/es.ts +3 -3
  57. package/src/tool/footballScoreKeeper/i18n/fr.ts +5 -5
  58. package/src/tool/footballScoreKeeper/i18n/id.ts +3 -3
  59. package/src/tool/footballScoreKeeper/i18n/it.ts +3 -3
  60. package/src/tool/footballScoreKeeper/i18n/ko.ts +3 -3
  61. package/src/tool/footballScoreKeeper/i18n/nl.ts +8 -8
  62. package/src/tool/footballScoreKeeper/i18n/pl.ts +14 -14
  63. package/src/tool/footballScoreKeeper/i18n/pt.ts +3 -3
  64. package/src/tool/footballScoreKeeper/i18n/ru.ts +14 -14
  65. package/src/tool/footballScoreKeeper/i18n/sv.ts +9 -9
  66. package/src/tool/footballScoreKeeper/i18n/tr.ts +5 -5
  67. package/src/tool/gymTracker/i18n/fr.ts +25 -24
  68. package/src/tool/gymTracker/i18n/ru.ts +4 -4
  69. package/src/tool/padelScoreKeeper/i18n/de.ts +1 -1
  70. package/src/tool/padelScoreKeeper/i18n/en.ts +1 -1
  71. package/src/tool/padelScoreKeeper/i18n/es.ts +1 -1
  72. package/src/tool/padelScoreKeeper/i18n/fr.ts +3 -3
  73. package/src/tool/padelScoreKeeper/i18n/id.ts +1 -1
  74. package/src/tool/padelScoreKeeper/i18n/it.ts +1 -1
  75. package/src/tool/padelScoreKeeper/i18n/ja.ts +1 -1
  76. package/src/tool/padelScoreKeeper/i18n/ko.ts +1 -1
  77. package/src/tool/padelScoreKeeper/i18n/nl.ts +1 -1
  78. package/src/tool/padelScoreKeeper/i18n/pl.ts +1 -1
  79. package/src/tool/padelScoreKeeper/i18n/pt.ts +1 -1
  80. package/src/tool/padelScoreKeeper/i18n/ru.ts +2 -2
  81. package/src/tool/padelScoreKeeper/i18n/sv.ts +1 -1
  82. package/src/tool/padelScoreKeeper/i18n/tr.ts +1 -1
  83. package/src/tool/padelScoreKeeper/i18n/zh.ts +1 -1
  84. package/src/tool/pingPongScoreKeeper/i18n/de.ts +2 -2
  85. package/src/tool/pingPongScoreKeeper/i18n/en.ts +2 -2
  86. package/src/tool/pingPongScoreKeeper/i18n/es.ts +2 -2
  87. package/src/tool/pingPongScoreKeeper/i18n/fr.ts +2 -2
  88. package/src/tool/pingPongScoreKeeper/i18n/id.ts +2 -2
  89. package/src/tool/pingPongScoreKeeper/i18n/it.ts +2 -2
  90. package/src/tool/pingPongScoreKeeper/i18n/ja.ts +2 -2
  91. package/src/tool/pingPongScoreKeeper/i18n/ko.ts +2 -2
  92. package/src/tool/pingPongScoreKeeper/i18n/nl.ts +2 -2
  93. package/src/tool/pingPongScoreKeeper/i18n/pl.ts +2 -2
  94. package/src/tool/pingPongScoreKeeper/i18n/pt.ts +2 -2
  95. package/src/tool/pingPongScoreKeeper/i18n/ru.ts +2 -2
  96. package/src/tool/pingPongScoreKeeper/i18n/sv.ts +2 -2
  97. package/src/tool/pingPongScoreKeeper/i18n/tr.ts +2 -2
  98. package/src/tool/pingPongScoreKeeper/i18n/zh.ts +2 -2
  99. package/src/tool/reactionTester/i18n/de.ts +4 -4
  100. package/src/tool/reactionTester/i18n/fr.ts +1 -1
  101. package/src/tool/reactionTester/i18n/ru.ts +4 -4
  102. package/src/tool/rugbyScoreKeeper/i18n/de.ts +1 -1
  103. package/src/tool/rugbyScoreKeeper/i18n/en.ts +1 -1
  104. package/src/tool/rugbyScoreKeeper/i18n/es.ts +1 -1
  105. package/src/tool/rugbyScoreKeeper/i18n/fr.ts +1 -1
  106. package/src/tool/rugbyScoreKeeper/i18n/id.ts +1 -1
  107. package/src/tool/rugbyScoreKeeper/i18n/it.ts +1 -1
  108. package/src/tool/rugbyScoreKeeper/i18n/nl.ts +1 -1
  109. package/src/tool/rugbyScoreKeeper/i18n/pt.ts +1 -1
  110. package/src/tool/rugbyScoreKeeper/i18n/sv.ts +1 -1
  111. package/src/tool/rugbyScoreKeeper/i18n/tr.ts +1 -1
  112. package/src/tool/scoreKeeper/i18n/fr.ts +2 -2
  113. package/src/tool/scoreKeeper/i18n/ru.ts +7 -7
  114. package/src/tool/scoreKeeper/i18n/zh.ts +4 -4
  115. package/src/tool/snookerScoreKeeper/i18n/de.ts +1 -1
  116. package/src/tool/snookerScoreKeeper/i18n/en.ts +1 -1
  117. package/src/tool/snookerScoreKeeper/i18n/es.ts +1 -1
  118. package/src/tool/snookerScoreKeeper/i18n/fr.ts +1 -1
  119. package/src/tool/snookerScoreKeeper/i18n/id.ts +1 -1
  120. package/src/tool/snookerScoreKeeper/i18n/it.ts +1 -1
  121. package/src/tool/snookerScoreKeeper/i18n/nl.ts +1 -1
  122. package/src/tool/snookerScoreKeeper/i18n/pt.ts +1 -1
  123. package/src/tool/snookerScoreKeeper/i18n/ru.ts +1 -1
  124. package/src/tool/snookerScoreKeeper/i18n/sv.ts +1 -1
  125. package/src/tool/snookerScoreKeeper/i18n/tr.ts +1 -1
  126. package/src/tool/streetballScoreKeeper/i18n/de.ts +1 -1
  127. package/src/tool/streetballScoreKeeper/i18n/en.ts +1 -1
  128. package/src/tool/streetballScoreKeeper/i18n/es.ts +1 -1
  129. package/src/tool/streetballScoreKeeper/i18n/fr.ts +1 -1
  130. package/src/tool/streetballScoreKeeper/i18n/id.ts +1 -1
  131. package/src/tool/streetballScoreKeeper/i18n/it.ts +1 -1
  132. package/src/tool/streetballScoreKeeper/i18n/nl.ts +1 -1
  133. package/src/tool/streetballScoreKeeper/i18n/pt.ts +1 -1
  134. package/src/tool/streetballScoreKeeper/i18n/sv.ts +1 -1
  135. package/src/tool/streetballScoreKeeper/i18n/tr.ts +1 -1
  136. package/src/tool/streetballScoreKeeper/logic.ts +1 -2
  137. package/src/tool/streetballScoreKeeper/streetball-3x3-basketball-scorekeeper.css +121 -0
  138. package/src/tool/tennisScoreKeeper/i18n/de.ts +1 -1
  139. package/src/tool/tennisScoreKeeper/i18n/es.ts +1 -1
  140. package/src/tool/tennisScoreKeeper/i18n/fr.ts +1 -1
  141. package/src/tool/tennisScoreKeeper/i18n/id.ts +1 -1
  142. package/src/tool/tennisScoreKeeper/i18n/it.ts +1 -1
  143. package/src/tool/tennisScoreKeeper/i18n/ja.ts +1 -1
  144. package/src/tool/tennisScoreKeeper/i18n/ko.ts +1 -1
  145. package/src/tool/tennisScoreKeeper/i18n/nl.ts +1 -1
  146. package/src/tool/tennisScoreKeeper/i18n/pl.ts +1 -1
  147. package/src/tool/tennisScoreKeeper/i18n/pt.ts +1 -1
  148. package/src/tool/tennisScoreKeeper/i18n/ru.ts +5 -5
  149. package/src/tool/tennisScoreKeeper/i18n/sv.ts +1 -1
  150. package/src/tool/tennisScoreKeeper/i18n/tr.ts +1 -1
  151. package/src/tool/tennisScoreKeeper/i18n/zh.ts +1 -1
  152. package/src/tool/tournamentBracket/i18n/de.ts +1 -1
  153. package/src/tool/tournamentBracket/i18n/es.ts +1 -1
  154. package/src/tool/tournamentBracket/i18n/fr.ts +2 -2
  155. package/src/tool/tournamentBracket/i18n/ja.ts +1 -1
  156. package/src/tool/tournamentBracket/i18n/pl.ts +8 -8
  157. package/src/tool/tournamentBracket/i18n/ru.ts +8 -8
  158. package/src/tool/tournamentBracket/i18n/tr.ts +1 -1
  159. package/src/tool/tournamentBracket/i18n/zh.ts +7 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-sports",
3
- "version": "1.30.0",
3
+ "version": "1.32.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -8,7 +8,7 @@ export const content: CategoryLocaleContent = {
8
8
  seo: [
9
9
  {
10
10
  type: 'title',
11
- text: 'Science de la Performance Sportive : Les Données pour Gagner',
11
+ text: 'Science de la Performance Sportive: Les Données pour Gagner',
12
12
  level: 2,
13
13
  },
14
14
  {
@@ -17,11 +17,11 @@ export const content: CategoryLocaleContent = {
17
17
  },
18
18
  {
19
19
  type: 'paragraph',
20
- html: 'De la mesure de la vitesse de réaction neuromusculaire à la gestion des tableaux d\'élimination de tournois, nos calculateurs appliquent une logique algorithmique et statistique pour que vous vous concentriez sur l\'essentiel : dépasser vos limites.',
20
+ html: 'De la mesure de la vitesse de réaction neuromusculaire à la gestion des tableaux d\'élimination de tournois, nos calculateurs appliquent une logique algorithmique et statistique pour que vous vous concentriez sur l\'essentiel: dépasser vos limites.',
21
21
  },
22
22
  {
23
23
  type: 'title',
24
- text: 'Logistique Compétitive : Tableaux de Score et Organisateurs',
24
+ text: 'Logistique Compétitive: Tableaux de Score et Organisateurs',
25
25
  level: 2,
26
26
  },
27
27
  {
@@ -34,7 +34,7 @@ export const content: CategoryLocaleContent = {
34
34
  },
35
35
  {
36
36
  type: 'title',
37
- text: 'Entraînement en Force : Suivi Basé sur la Progression',
37
+ text: 'Entraînement en Force: Suivi Basé sur la Progression',
38
38
  level: 2,
39
39
  },
40
40
  {
@@ -43,7 +43,7 @@ export const content: CategoryLocaleContent = {
43
43
  },
44
44
  {
45
45
  type: 'title',
46
- text: 'Neurologie et Réflexes : Vitesse de Réaction',
46
+ text: 'Neurologie et Réflexes: Vitesse de Réaction',
47
47
  level: 2,
48
48
  },
49
49
  {
@@ -66,12 +66,12 @@ export const content: CategoryLocaleContent = {
66
66
  },
67
67
  {
68
68
  type: 'title',
69
- text: 'Physiologie de l\'Entraînement : Zones de Fréquence Cardiaque',
69
+ text: 'Physiologie de l\'Entraînement: Zones de Fréquence Cardiaque',
70
70
  level: 2,
71
71
  },
72
72
  {
73
73
  type: 'paragraph',
74
- html: 'Votre cœur est l\'indicateur le plus précis de l\'effort. La fréquence cardiaque maximale (FCM) varie selon l\'âge, la génétique et la capacité cardiovasculaire. La calculer correctement permet de s\'entraîner dans des zones spécifiques : zone aérobie (60-70% FCM) pour l\'endurance de base, zone anaérobie (80-90% FCM) pour l\'explosivité, zone VO2 max (95-100%) pour la capacité cardio-respiratoire.',
74
+ html: 'Votre cœur est l\'indicateur le plus précis de l\'effort. La fréquence cardiaque maximale (FCM) varie selon l\'âge, la génétique et la capacité cardiovasculaire. La calculer correctement permet de s\'entraîner dans des zones spécifiques: zone aérobie (60-70% FCM) pour l\'endurance de base, zone anaérobie (80-90% FCM) pour l\'explosivité, zone VO2 max (95-100%) pour la capacité cardio-respiratoire.',
75
75
  },
76
76
  {
77
77
  type: 'title',
@@ -17,7 +17,7 @@ export const content: CategoryLocaleContent = {
17
17
  },
18
18
  {
19
19
  type: 'paragraph',
20
- html: 'От измерения скорости нервно-мышечной реакции до управления турнирными сетками на выбывание : наши калькуляторы применяют алгоритмическую и статистическую логику, чтобы вы могли сосредоточиться на самом важном: раздвижении своих границ.',
20
+ html: 'От измерения скорости нервно-мышечной реакции до управления турнирными сетками на выбывание: наши калькуляторы применяют алгоритмическую и статистическую логику, чтобы вы могли сосредоточиться на самом важном: раздвижении своих границ.',
21
21
  },
22
22
  {
23
23
  type: 'title',
@@ -71,11 +71,11 @@ export const content: CategoryLocaleContent = {
71
71
  },
72
72
  {
73
73
  type: 'paragraph',
74
- html: 'Ваше сердце : самый точный индикатор усилий. Максимальная частота сердечных сокращений (МЧСС) зависит от возраста, генетики и сердечно-сосудистых способностей. Правильный расчет позволяет тренироваться в определенных зонах: аэробная зона (60-70% МЧСС) для базовой выносливости, анаэробная зона (80-90% МЧСС) для взрывной активности, зона VO2 max (95-100%) для кардиореспираторной способности.',
74
+ html: 'Ваше сердце: самый точный индикатор усилий. Максимальная частота сердечных сокращений (МЧСС) зависит от возраста, генетики и сердечно-сосудистых способностей. Правильный расчет позволяет тренироваться в определенных зонах: аэробная зона (60-70% МЧСС) для базовой выносливости, анаэробная зона (80-90% МЧСС) для взрывной активности, зона VO2 max (95-100%) для кардиореспираторной способности.',
75
75
  },
76
76
  {
77
77
  type: 'paragraph',
78
- html: 'Распространенная ошибка : тренироваться слишком интенсивно слишком часто. Большая часть тренировок должна проходить в аэробной зоне (низкая интенсивность, большая продолжительность), создавая прочный фундамент. Оптимально проводить всего 1-2 сессии в неделю в высокоинтенсивных зонах во избежание перетренированности. Наши инструменты позволяют рассчитать зоны на основе возраста и уровня физической подготовки, гарантируя, что каждая сессия имеет четкую физиологическую цель.',
78
+ html: 'Распространенная ошибка: тренироваться слишком интенсивно слишком часто. Большая часть тренировок должна проходить в аэробной зоне (низкая интенсивность, большая продолжительность), создавая прочный фундамент. Оптимально проводить всего 1-2 сессии в неделю в высокоинтенсивных зонах во избежание перетренированности. Наши инструменты позволяют рассчитать зоны на основе возраста и уровня физической подготовки, гарантируя, что каждая сессия имеет четкую физиологическую цель.',
79
79
  },
80
80
  {
81
81
  type: 'title',
@@ -78,7 +78,7 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
78
78
  transition:
79
79
  background-color 0.3s ease,
80
80
  color 0.3s ease;
81
- }
81
+ font-family: Inter, sans-serif;}
82
82
 
83
83
  main {
84
84
  padding: 0 2rem;
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ type LocaleWithDiacritics = keyof typeof DIACRITIC_RULES;
5
+
6
+ const DIACRITIC_RULES = {
7
+ de: {
8
+ language: 'German',
9
+ expectedCharacters: 'ä ö ü ß',
10
+ characters: /[äöüÄÖÜß]/g,
11
+ minPerThousandLetters: 0.1,
12
+ },
13
+ es: {
14
+ language: 'Spanish',
15
+ expectedCharacters: 'á é í ó ú ü ñ',
16
+ characters: /[áéíóúüñÁÉÍÓÚÜÑ]/g,
17
+ minPerThousandLetters: 0.1,
18
+ },
19
+ fr: {
20
+ language: 'French',
21
+ expectedCharacters: 'à â æ ç é è ê ë î ï ô œ ù û ü ÿ',
22
+ characters: /[àâæçéèêëîïôœùûüÿÀÂÆÇÉÈÊËÎÏÔŒÙÛÜŸ]/g,
23
+ minPerThousandLetters: 0.1,
24
+ },
25
+ it: {
26
+ language: 'Italian',
27
+ expectedCharacters: 'à è é ì í î ò ó ù ú',
28
+ characters: /[àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]/g,
29
+ minPerThousandLetters: 0.1,
30
+ },
31
+ pl: {
32
+ language: 'Polish',
33
+ expectedCharacters: 'ą ć ę ł ń ó ś ź ż',
34
+ characters: /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/g,
35
+ minPerThousandLetters: 0.1,
36
+ },
37
+ pt: {
38
+ language: 'Portuguese',
39
+ expectedCharacters: 'á â ã à ç é ê í ó ô õ ú ü',
40
+ characters: /[áâãàçéêíóôõúüÁÂÃÀÇÉÊÍÓÔÕÚÜ]/g,
41
+ minPerThousandLetters: 0.1,
42
+ },
43
+ sv: {
44
+ language: 'Swedish',
45
+ expectedCharacters: 'å ä ö',
46
+ characters: /[åäöÅÄÖ]/g,
47
+ minPerThousandLetters: 0.1,
48
+ },
49
+ tr: {
50
+ language: 'Turkish',
51
+ expectedCharacters: 'ç ğ ı İ ö ş ü',
52
+ characters: /[çğıöşüÇĞİÖŞÜ]/g,
53
+ minPerThousandLetters: 0.1,
54
+ },
55
+ } as const;
56
+
57
+ const LETTERS = /\p{L}/gu;
58
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
59
+
60
+ function collectStrings(value: unknown): string[] {
61
+ if (typeof value === 'string') return [value];
62
+ if (!value || typeof value !== 'object') return [];
63
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
64
+ return Object.values(value).flatMap(collectStrings);
65
+ }
66
+
67
+ function normalizeText(value: unknown): string {
68
+ return collectStrings(value).join(' ').normalize('NFC');
69
+ }
70
+
71
+ function translatableContent(content: Record<string, unknown>) {
72
+ return TRANSLATABLE_KEYS.map((key) => content[key]);
73
+ }
74
+
75
+ function letterCount(text: string): number {
76
+ return text.match(LETTERS)?.length ?? 0;
77
+ }
78
+
79
+ function diacriticCount(text: string, locale: LocaleWithDiacritics): number {
80
+ return text.match(DIACRITIC_RULES[locale].characters)?.length ?? 0;
81
+ }
82
+
83
+ function diacriticsPerThousandLetters(text: string, locale: LocaleWithDiacritics): number {
84
+ const letters = letterCount(text);
85
+ if (letters === 0) return 0;
86
+ return diacriticCount(text, locale) / letters * 1000;
87
+ }
88
+
89
+ describe('Diacritics density validation', () => {
90
+ ALL_TOOLS.forEach((tool) => {
91
+ describe(`Tool: ${tool.entry.id}`, () => {
92
+ Object.keys(DIACRITIC_RULES).forEach((locale) => {
93
+ it(`${locale} keeps the expected accent and special-letter set`, async () => {
94
+ const typedLocale = locale as LocaleWithDiacritics;
95
+ const loader = tool.entry.i18n[typedLocale];
96
+ if (!loader) return;
97
+
98
+ const content = await loader();
99
+ const text = normalizeText(translatableContent(content as Record<string, unknown>));
100
+ const rule = DIACRITIC_RULES[typedLocale];
101
+ const letters = letterCount(text);
102
+ const matches = diacriticCount(text, typedLocale);
103
+ const density = diacriticsPerThousandLetters(text, typedLocale);
104
+
105
+ expect(
106
+ density,
107
+ [
108
+ `Possible spelling or encoding issue detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
109
+ `The text has ${matches} special characters (${density.toFixed(2)} per 1000 letters, ${letters} letters analyzed).`,
110
+ `This locale should include some of these characters: ${rule.expectedCharacters}.`,
111
+ 'If the count is 0 or near 0, accents, tildes, or special letters were probably stripped by encoding or normalization.',
112
+ ].join(' '),
113
+ ).toBeGreaterThanOrEqual(rule.minPerThousandLetters);
114
+ });
115
+ });
116
+ });
117
+ });
118
+ });
@@ -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
+ });
@@ -22,20 +22,49 @@ function getFiles(dir: string): string[] {
22
22
  return results;
23
23
  }
24
24
 
25
+ function isContentFile(filePath: string): boolean {
26
+ return /\\i18n\\/.test(filePath) || filePath.endsWith('bibliography.ts');
27
+ }
28
+
25
29
  const srcDir = path.join(process.cwd(), 'src');
26
30
  const scriptsDir = path.join(process.cwd(), 'scripts');
27
31
  const filesToTest = [
28
- ...getFiles(srcDir),
29
- ...getFiles(scriptsDir),
32
+ ...getFiles(srcDir).filter(isContentFile),
33
+ ...getFiles(scriptsDir).filter(isContentFile),
30
34
  ];
31
35
 
32
- describe('En-Dash Character Validation', () => {
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', () => {
33
51
  filesToTest.forEach((filePath) => {
34
52
  const relativePath = path.relative(process.cwd(), filePath);
35
- it(`should not contain en-dash in ${relativePath}`, () => {
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}`, () => {
36
66
  const content = fs.readFileSync(filePath, 'utf-8');
37
- const hasEnDash = content.includes('\u2013') || content.includes('–') || content.includes('');
38
- expect(hasEnDash).toBe(false);
67
+ expect(content).not.toContain('--');
39
68
  });
40
69
  });
41
70
  });
@@ -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
+ });
@@ -3,40 +3,40 @@ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dt
3
3
  import type { BaseballScoreKeeperLocaleContent } from '../entry';
4
4
 
5
5
  const slug = 'baseball-spielstand';
6
- const title = 'Premium Baseball und Softball Spielstand mit Laeuferverfolgung';
7
- const description = 'Erfasse Live Baseballergebnisse mit Runs, Hits und Errors. Visuelles Diamantfeld mit Laeuferpositionen, Ball Strike Zaehler und Inning fuer Inning Verlaufsanzeige.';
6
+ const title = 'Baseball und Softball Spielstand mit Läuferverfolgung';
7
+ const description = 'Erfasse Live Baseballergebnisse mit Runs, Hits und Errors. Visuelles Diamantfeld mit Läuferpositionen, Ball Strike Zähler und Inning für Inning Verlaufsanzeige.';
8
8
 
9
9
  const faqData = [
10
10
  {
11
- question: 'Wie funktioniert der Zaehler im Baseball?',
12
- answer: 'Der Zaehler zeigt die Anzahl der Balls und Strikes fuer den aktuellen Schlagmann. Balls erhoehen sich bis 4 fuer einen Walk. Strikes erhoehen sich bis 3 fuer ein Strikeout. Anpassbare Grenzen fuer Jugendligen.',
11
+ question: 'Wie funktioniert der Zähler im Baseball?',
12
+ answer: 'Der Zähler zeigt die Anzahl der Balls und Strikes für den aktuellen Schlagmann. Balls erhöhen sich bis 4 für einen Walk. Strikes erhöhen sich bis 3 für ein Strikeout. Anpassbare Grenzen für Jugendligen.',
13
13
  },
14
14
  {
15
15
  question: 'Was zeigt das interaktive Baseball Diamantfeld?',
16
- answer: 'Das Diamantfeld zeigt das erste, zweite und dritte Base. Ein Tippen auf ein Base hebt es orange hervor, um einen Laeufer auf diesem Base anzuzeigen. Laeufer ruecken bei Hits automatisch vor.',
16
+ answer: 'Das Diamantfeld zeigt das erste, zweite und dritte Base. Ein Tippen auf ein Base hebt es orange hervor, um einen Läufer auf diesem Base anzuzeigen. Läufer rücken bei Hits automatisch vor.',
17
17
  },
18
18
  {
19
19
  question: 'Wie werden Runs, Hits und Errors erfasst?',
20
- answer: 'Die R H E Matrix zeigt Runs, Hits und Errors fuer beide Mannschaften. Die Inning fuer Inning Verlaufshistorie zeigt, wie sich das Ergebnis ueber alle Innings aufgebaut hat.',
20
+ answer: 'Die R H E Matrix zeigt Runs, Hits und Errors für beide Mannschaften. Die Inning für Inning Verlaufshistorie zeigt, wie sich das Ergebnis über alle Innings aufgebaut hat.',
21
21
  },
22
22
  ];
23
23
 
24
24
  const howToData = [
25
25
  {
26
26
  name: 'Jeden Pitch Erfassen',
27
- text: 'Tippe auf Strike, Ball, Foul, Hit oder Out, um jeden Pitch zu erfassen. Der Zaehler aktualisiert sich automatisch basierend auf dem Ergebnis.',
27
+ text: 'Tippe auf Strike, Ball, Foul, Hit oder Out, um jeden Pitch zu erfassen. Der Zähler aktualisiert sich automatisch basierend auf dem Ergebnis.',
28
28
  },
29
29
  {
30
- name: 'Laeufer Verwalten',
31
- text: 'Tippe auf die Basen im Diamantfeld, um Laeufer zu setzen oder zu entfernen. Bei einem Hit ruecken Laeufer automatisch vor.',
30
+ name: 'Läufer Verwalten',
31
+ text: 'Tippe auf die Basen im Diamantfeld, um Läufer zu setzen oder zu entfernen. Bei einem Hit rücken Läufer automatisch vor.',
32
32
  },
33
33
  {
34
34
  name: 'Inning Fortschritt Verfolgen',
35
35
  text: 'Die Inning Anzeige zeigt das aktuelle Halbinnning. Nach drei Outs wechselt das Spiel automatisch zwischen dem oberen und unteren Halbinnning.',
36
36
  },
37
37
  {
38
- name: 'Spielstand Pruefen',
39
- text: 'Ueberpruefe die R H E Zusammenfassung und die scrollende Inning fuer Inning Verlaufstabelle, um den vollstaendigen Spielverlauf zu sehen.',
38
+ name: 'Spielstand Prüfen',
39
+ text: 'Überprüfe die R H E Zusammenfassung und die scrollende Inning für Inning Verlaufstabelle, um den vollständigen Spielverlauf zu sehen.',
40
40
  },
41
41
  ];
42
42
 
@@ -92,7 +92,7 @@ export const content: BaseballScoreKeeperLocaleContent = {
92
92
  },
93
93
  {
94
94
  type: 'paragraph',
95
- html: 'Brauchst du einen zuverlaessigen Baseball Spielstand fuer dein naechstes Spiel? Dieses kostenlose Online Tool erfasst Runs, Hits und Errors und zeigt ein interaktives Live Diamantfeld mit Echtzeit Laeuferpositionen. Jeder Pitch zaehlt und unser digitales Scoreboard stellt sicher, dass du nie den Ueberblick ueber den Zaehler, die Outs oder das Inning verlierst. Egal ob du eine Jugendliga trainierst, den Spielstand fuer ein Softball Turnier fuehrst oder ein High School Spiel leitest, dieses Tool verwaltet die gesamte Spielstandstabelle automatisch, damit du dich auf das Geschehen auf dem Feld konzentrieren kannst.',
95
+ html: 'Brauchst du einen zuverlässigen Baseball Spielstand für dein nächstes Spiel? Dieses kostenlose Online Tool erfasst Runs, Hits und Errors und zeigt ein interaktives Live Diamantfeld mit Echtzeit Läuferpositionen. Jeder Pitch zählt und unser digitales Scoreboard stellt sicher, dass du nie den Überblick über den Zähler, die Outs oder das Inning verlierst. Egal ob du eine Jugendliga trainierst, den Spielstand für ein Softball Turnier führst oder ein High School Spiel leitest, dieses Tool verwaltet die gesamte Spielstandstabelle automatisch, damit du dich auf das Geschehen auf dem Feld konzentrieren kannst.',
96
96
  },
97
97
  {
98
98
  type: 'title',
@@ -101,29 +101,29 @@ export const content: BaseballScoreKeeperLocaleContent = {
101
101
  },
102
102
  {
103
103
  type: 'paragraph',
104
- html: 'Manuelle Spielstandsfuehrung ist fehleranfaellig, besonders bei schnellen Spielen. Ein uebersehener Strike oder ein uebersehener Laeufer kann die gesamte Spielstandstabelle durcheinanderbringen. Dieser digitale Spielstand automatisiert die muhevollen Teile. Tippe auf Strike, Ball, Foul, Hit oder Out und das Board aktualisiert sofort den Zaehler. Wenn ein Schlagmann einen Walk erhaelt oder ausgestriket wird, setzt das Tool den Zaehler automatisch zurueck. Nach drei Outs wechselt es das Inning von oben nach unten und zeichnet die Runs auf. Die R H E Matrix und die Inning fuer Inning Verlaufstabelle geben dir einen vollstaendigen Ueberblick ueber das Spiel auf einen Blick.',
104
+ html: 'Manuelle Spielstandsführung ist fehleranfällig, besonders bei schnellen Spielen. Ein übersehener Strike oder ein übersehener Läufer kann die gesamte Spielstandstabelle durcheinanderbringen. Dieser digitale Spielstand automatisiert die mühevollen Teile. Tippe auf Strike, Ball, Foul, Hit oder Out und das Board aktualisiert sofort den Zähler. Wenn ein Schlagmann einen Walk erhält oder ausgestriket wird, setzt das Tool den Zähler automatisch zurück. Nach drei Outs wechselt es das Inning von oben nach unten und zeichnet die Runs auf. Die R H E Matrix und die Inning für Inning Verlaufstabelle geben dir einen vollständigen Überblick über das Spiel auf einen Blick.',
105
105
  },
106
106
  {
107
107
  type: 'comparative',
108
108
  columns: 3,
109
109
  items: [
110
110
  {
111
- title: 'Live Pitch Zaehler',
112
- description: 'Automatisierte Ball und Strike Verfolgung mit Walk und Strikeout Erkennung fuer jeden At Bat.',
111
+ title: 'Live Pitch Zähler',
112
+ description: 'Automatisierte Ball und Strike Verfolgung mit Walk und Strikeout Erkennung für jeden At Bat.',
113
113
  icon: 'mdi:baseball',
114
- points: ['Balls bis 4 gezaehlt', 'Strikes bis 3 gezaehlt', 'Auto zuruecksetzen bei Entscheidung'],
114
+ points: ['Balls bis 4 gezählt', 'Strikes bis 3 gezählt', 'Auto zurücksetzen bei Entscheidung'],
115
115
  },
116
116
  {
117
- title: 'Laeufer Verwaltung',
117
+ title: 'Läufer Verwaltung',
118
118
  description: 'Interaktives Diamantfeld zeigt genau, wer auf dem ersten, zweiten oder dritten Base ist.',
119
119
  icon: 'mdi:diamond-stone',
120
- points: ['Tippe auf Basen um Laeufer zu setzen', 'Visuelle Hervorhebung bei Besetzung', 'Leeren bei Inning Wechsel'],
120
+ points: ['Tippe auf Basen um Läufer zu setzen', 'Visuelle Hervorhebung bei Besetzung', 'Leeren bei Inning Wechsel'],
121
121
  },
122
122
  {
123
123
  title: 'Komplette Spielstandstabelle',
124
- description: 'Vollstaendige R H E Statistiken mit scrollendem Inning fuer Inning Spielverlauf.',
124
+ description: 'Vollständige R H E Statistiken mit scrollendem Inning für Inning Spielverlauf.',
125
125
  icon: 'mdi:scoreboard-outline',
126
- points: ['Runs Hits und Errors', 'Inning fuer Inning Tabelle', 'Laufende Summen fuer beide Teams'],
126
+ points: ['Runs Hits und Errors', 'Inning für Inning Tabelle', 'Laufende Summen für beide Teams'],
127
127
  },
128
128
  ],
129
129
  },
@@ -134,40 +134,40 @@ export const content: BaseballScoreKeeperLocaleContent = {
134
134
  },
135
135
  {
136
136
  type: 'paragraph',
137
- html: 'Dieses Tool ist fuer alle gedacht, die den Spielstand fuehren muessen: Jugendbaseball Trainer, die eine klare digitale Anzeige fuer ihre Spieler wollen, Softball Liga Freiwillige, die Spiele ohne eigenen Spielstandsfuehrer organisieren, Eltern, die die Spiele ihrer Kinder von den Tribuenen aus verfolgen, und Schiedsrichter, die ein sekundaeres Ueberpruefungssystem wuenschen. Die Oberflaeche funktioniert auf jedem Geraet, von Smartphones im Spielerraum bis zu Tablets am Zaun oder Laptops auf der Bank. Keine Installation erforderlich, einfach den Browser oeffnen und mit der Spielstandserfassung beginnen.',
137
+ html: 'Dieses Tool ist für alle gedacht, die den Spielstand führen müssen: Jugendbaseball Trainer, die eine klare digitale Anzeige für ihre Spieler wollen, Softball Liga Freiwillige, die Spiele ohne eigenen Spielstandsführer organisieren, Eltern, die die Spiele ihrer Kinder von den Tribünen aus verfolgen, und Schiedsrichter, die ein sekundäres Überprüfungssystem wünschen. Die Oberfläche funktioniert auf jedem Gerät, von Smartphones im Spielerraum bis zu Tablets am Zaun oder Laptops auf der Bank. Keine Installation erforderlich, einfach den Browser öffnen und mit der Spielstandserfassung beginnen.',
138
138
  },
139
139
  {
140
140
  type: 'list',
141
141
  items: [
142
- '<strong>Automatische Zaehlerverwaltung:</strong> Balls und Strikes werden automatisch nach Walks, Strikeouts, Hits und Outs zurueckgesetzt. Keine manuellen Ruecksetzungen erforderlich.',
143
- '<strong>Beruehrungsempfindliches Diamantfeld:</strong> Tippe auf das erste, zweite oder dritte Base, um Laeufer zu setzen oder zu entfernen. Das Diamantfeld leuchtet in Gold, um besetzte Basen anzuzeigen.',
144
- '<strong>Inning fuer Inning Ergebnisse:</strong> Jedes Halbinnning wird in der Tabelle aufgezeichnet. Sieh genau, wie jedes Team in allen neun Innings gepunktet hat.',
145
- '<strong>Keine Einrichtung Erforderlich:</strong> Oeffne die Seite und beginne sofort mit der Spielstandserfassung. Passe Teamnamen an, indem du auf die Beschriftungen ueber den Ergebnissen tippst.',
142
+ '<strong>Automatische Zählerverwaltung:</strong> Balls und Strikes werden automatisch nach Walks, Strikeouts, Hits und Outs zurückgesetzt. Keine manuellen Rücksetzungen erforderlich.',
143
+ '<strong>Berührungsempfindliches Diamantfeld:</strong> Tippe auf das erste, zweite oder dritte Base, um Läufer zu setzen oder zu entfernen. Das Diamantfeld leuchtet in Gold, um besetzte Basen anzuzeigen.',
144
+ '<strong>Inning für Inning Ergebnisse:</strong> Jedes Halbinnning wird in der Tabelle aufgezeichnet. Sieh genau, wie jedes Team in allen neun Innings gepunktet hat.',
145
+ '<strong>Keine Einrichtung Erforderlich:</strong> Öffne die Seite und beginne sofort mit der Spielstandserfassung. Passe Teamnamen an, indem du auf die Beschriftungen über den Ergebnissen tippst.',
146
146
  ],
147
147
  },
148
148
  {
149
149
  type: 'title',
150
- text: 'Baseball Spielstandsfuehrung Vereinfacht: Zaehler Diamant und Tabelle an Einem Ort',
150
+ text: 'Baseball Spielstandsführung Vereinfacht: Zähler Diamant und Tabelle an Einem Ort',
151
151
  level: 2,
152
152
  },
153
153
  {
154
154
  type: 'paragraph',
155
- html: 'Die Spielstandsfuehrung im Baseball erfordert die gleichzeitige Verfolgung mehrerer Dinge: den Ball und Strike Zaehler, die Anzahl der Outs, welche Basen besetzt sind, die Runs jedes Teams und das aktuelle Inning. Den Ueberblick ueber eine dieser Groessen zu verlieren, fuehrt zu Verwirrung und ungenauen Aufzeichnungen. Dieses Tool buendelt alles auf einem einzigen Bildschirm. Die Zaehlerpunkte zeigen Balls und Strikes auf einen Blick. Das Diamantfeld zeigt die Laeuferpositionen. Die R H E Tabelle zeigt die vollstaendige Spielstandstabelle. Und die Inning Tabelle scrollt horizontal, um den kompletten Spielverlauf anzuzeigen. Alles aktualisiert sich in Echtzeit bei jeder Beruehrung.',
155
+ html: 'Die Spielstandsführung im Baseball erfordert die gleichzeitige Verfolgung mehrerer Dinge: den Ball und Strike Zähler, die Anzahl der Outs, welche Basen besetzt sind, die Runs jedes Teams und das aktuelle Inning. Den Überblick über eine dieser Größen zu verlieren, führt zu Verwirrung und ungenauen Aufzeichnungen. Dieses Tool bündelt alles auf einem einzigen Bildschirm. Die Zählerpunkte zeigen Balls und Strikes auf einen Blick. Das Diamantfeld zeigt die Läuferpositionen. Die R H E Tabelle zeigt die vollständige Spielstandstabelle. Und die Inning Tabelle scrollt horizontal, um den kompletten Spielverlauf anzuzeigen. Alles aktualisiert sich in Echtzeit bei jeder Berührung.',
156
156
  },
157
157
  {
158
158
  type: 'grid',
159
159
  columns: [
160
- { type: 'card', title: 'Trainer', html: '<p>Behalte eine klare digitale Anzeige, die fuer dein gesamtes Team vom Spielerraum aus sichtbar ist.</p>' },
161
- { type: 'card', title: 'Freiwillige', html: '<p>Keine Erfahrung in der Spielstandsfuehrung erforderlich. Das Tool uebernimmt die gesamte komplexe Verfolgung automatisch.</p>' },
162
- { type: 'card', title: 'Eltern', html: '<p>Verfolge das Spiel von den Tribuenen aus mit einer zuverlaessigen Echtzeit Spielstandsanzeige auf deinem Telefon.</p>' },
163
- { type: 'card', title: 'Spieler', html: '<p>Sieh dir die Inning fuer Inning Ergebnisse nach dem Spiel an, um die Leistung zu analysieren.</p>' },
160
+ { type: 'card', title: 'Trainer', html: '<p>Behalte eine klare digitale Anzeige, die für dein gesamtes Team vom Spielerraum aus sichtbar ist.</p>' },
161
+ { type: 'card', title: 'Freiwillige', html: '<p>Keine Erfahrung in der Spielstandsführung erforderlich. Das Tool übernimmt die gesamte komplexe Verfolgung automatisch.</p>' },
162
+ { type: 'card', title: 'Eltern', html: '<p>Verfolge das Spiel von den Tribünen aus mit einer zuverlässigen Echtzeit Spielstandsanzeige auf deinem Telefon.</p>' },
163
+ { type: 'card', title: 'Spieler', html: '<p>Sieh dir die Inning für Inning Ergebnisse nach dem Spiel an, um die Leistung zu analysieren.</p>' },
164
164
  ],
165
165
  },
166
166
  ],
167
167
  ui: {
168
168
  title: 'Baseball Spielstand',
169
169
  description: 'Erfasse Runs, Hits und Errors mit Diamantansicht.',
170
- away: 'Auswaerts',
170
+ away: 'Auswärts',
171
171
  home: 'Heim',
172
172
  runs: 'R',
173
173
  hits: 'H',
@@ -187,10 +187,10 @@ export const content: BaseballScoreKeeperLocaleContent = {
187
187
  runBtn: '+1 Run',
188
188
  errorBtn: 'Error',
189
189
  newBatter: 'Neuer Schlagmann',
190
- resetMatch: 'Spiel Zuruecksetzen',
191
- resetConfirm: 'Das aktuelle Spiel zuruecksetzen? Alle Ergebnisse gehen verloren.',
190
+ resetMatch: 'Spiel Zurücksetzen',
191
+ resetConfirm: 'Das aktuelle Spiel zurücksetzen? Alle Ergebnisse gehen verloren.',
192
192
  cancel: 'Abbrechen',
193
- confirm: 'Bestaetigen',
193
+ confirm: 'Bestätigen',
194
194
  total: 'Gesamt',
195
195
  fullscreen: 'Vollbild',
196
196
  toggleSound: 'Ton Ein Aus',
@@ -3,7 +3,7 @@ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dt
3
3
  import type { BaseballScoreKeeperLocaleContent } from '../entry';
4
4
 
5
5
  const slug = 'baseball-scorekeeper';
6
- const title = 'Premium Baseball and Softball Scorekeeper with Diamond Tracker';
6
+ const title = 'Baseball and Softball Scorekeeper with Diamond Tracker';
7
7
  const description = 'Track live baseball scores with runs, hits and errors. Visual diamond with base runner positions, ball strike count tracker, and inning by inning history grid.';
8
8
 
9
9
  const faqData = [