@jjlmoya/utils-sports 1.31.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.
- package/package.json +1 -1
- package/src/category/i18n/fr.ts +7 -7
- package/src/category/i18n/ru.ts +3 -3
- package/src/layouts/PreviewLayout.astro +1 -1
- package/src/tests/diacritics_density.test.ts +118 -0
- package/src/tests/inverted_punctuation.test.ts +84 -0
- package/src/tests/no_en_dash.test.ts +35 -6
- package/src/tests/script_density.test.ts +94 -0
- package/src/tool/baseballScoreKeeper/i18n/de.ts +35 -35
- package/src/tool/baseballScoreKeeper/i18n/es.ts +37 -36
- package/src/tool/baseballScoreKeeper/i18n/fr.ts +38 -38
- package/src/tool/baseballScoreKeeper/i18n/it.ts +8 -8
- package/src/tool/baseballScoreKeeper/i18n/pl.ts +54 -54
- package/src/tool/baseballScoreKeeper/i18n/pt.ts +27 -27
- package/src/tool/baseballScoreKeeper/i18n/tr.ts +50 -50
- package/src/tool/basketScoreKeeper/i18n/de.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/en.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/es.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/fr.ts +14 -14
- package/src/tool/basketScoreKeeper/i18n/id.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/it.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/ja.ts +9 -9
- package/src/tool/basketScoreKeeper/i18n/ko.ts +9 -9
- package/src/tool/basketScoreKeeper/i18n/nl.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/pl.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/pt.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/ru.ts +17 -17
- package/src/tool/basketScoreKeeper/i18n/sv.ts +11 -11
- package/src/tool/basketScoreKeeper/i18n/tr.ts +12 -12
- package/src/tool/basketScoreKeeper/i18n/zh.ts +2 -2
- package/src/tool/beachVolleyballScoreKeeper/i18n/de.ts +26 -25
- package/src/tool/beachVolleyballScoreKeeper/i18n/es.ts +29 -28
- package/src/tool/beachVolleyballScoreKeeper/i18n/fr.ts +37 -36
- package/src/tool/beachVolleyballScoreKeeper/i18n/it.ts +15 -14
- package/src/tool/beachVolleyballScoreKeeper/i18n/pt.ts +32 -31
- package/src/tool/dartsScoreKeeper/i18n/de.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/es.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/fr.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/id.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/it.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/ja.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/ko.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/nl.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/pl.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/pt.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/ru.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/sv.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/tr.ts +1 -1
- package/src/tool/dartsScoreKeeper/i18n/zh.ts +1 -1
- package/src/tool/footballScoreKeeper/i18n/de.ts +15 -15
- package/src/tool/footballScoreKeeper/i18n/en.ts +3 -3
- package/src/tool/footballScoreKeeper/i18n/es.ts +3 -3
- package/src/tool/footballScoreKeeper/i18n/fr.ts +5 -5
- package/src/tool/footballScoreKeeper/i18n/id.ts +3 -3
- package/src/tool/footballScoreKeeper/i18n/it.ts +3 -3
- package/src/tool/footballScoreKeeper/i18n/ko.ts +3 -3
- package/src/tool/footballScoreKeeper/i18n/nl.ts +8 -8
- package/src/tool/footballScoreKeeper/i18n/pl.ts +14 -14
- package/src/tool/footballScoreKeeper/i18n/pt.ts +3 -3
- package/src/tool/footballScoreKeeper/i18n/ru.ts +14 -14
- package/src/tool/footballScoreKeeper/i18n/sv.ts +9 -9
- package/src/tool/footballScoreKeeper/i18n/tr.ts +3 -3
- package/src/tool/gymTracker/i18n/fr.ts +25 -24
- package/src/tool/gymTracker/i18n/ru.ts +4 -4
- package/src/tool/padelScoreKeeper/i18n/de.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/en.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/es.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/fr.ts +3 -3
- package/src/tool/padelScoreKeeper/i18n/id.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/it.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/ja.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/ko.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/nl.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/pl.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/pt.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/ru.ts +2 -2
- package/src/tool/padelScoreKeeper/i18n/sv.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/tr.ts +1 -1
- package/src/tool/padelScoreKeeper/i18n/zh.ts +1 -1
- package/src/tool/pingPongScoreKeeper/i18n/de.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/en.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/es.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/fr.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/id.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/it.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/ja.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/ko.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/nl.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/pl.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/pt.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/ru.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/sv.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/tr.ts +2 -2
- package/src/tool/pingPongScoreKeeper/i18n/zh.ts +2 -2
- package/src/tool/reactionTester/i18n/de.ts +4 -4
- package/src/tool/reactionTester/i18n/fr.ts +1 -1
- package/src/tool/reactionTester/i18n/ru.ts +4 -4
- package/src/tool/scoreKeeper/i18n/fr.ts +2 -2
- package/src/tool/scoreKeeper/i18n/ru.ts +7 -7
- package/src/tool/scoreKeeper/i18n/zh.ts +4 -4
- package/src/tool/snookerScoreKeeper/i18n/ru.ts +1 -1
- package/src/tool/streetballScoreKeeper/logic.ts +1 -2
- package/src/tool/streetballScoreKeeper/streetball-3x3-basketball-scorekeeper.css +121 -0
- package/src/tool/tennisScoreKeeper/i18n/de.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/es.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/fr.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/id.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/it.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/ja.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/ko.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/nl.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/pl.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/pt.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/ru.ts +5 -5
- package/src/tool/tennisScoreKeeper/i18n/sv.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/tr.ts +1 -1
- package/src/tool/tennisScoreKeeper/i18n/zh.ts +1 -1
- package/src/tool/tournamentBracket/i18n/es.ts +1 -1
- package/src/tool/tournamentBracket/i18n/fr.ts +2 -2
- package/src/tool/tournamentBracket/i18n/ja.ts +1 -1
- package/src/tool/tournamentBracket/i18n/pl.ts +8 -8
- package/src/tool/tournamentBracket/i18n/ru.ts +8 -8
- package/src/tool/tournamentBracket/i18n/zh.ts +7 -7
package/package.json
CHANGED
package/src/category/i18n/fr.ts
CHANGED
|
@@ -8,7 +8,7 @@ export const content: CategoryLocaleContent = {
|
|
|
8
8
|
seo: [
|
|
9
9
|
{
|
|
10
10
|
type: 'title',
|
|
11
|
-
text: 'Science de la Performance Sportive
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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',
|
package/src/category/i18n/ru.ts
CHANGED
|
@@ -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: 'Ваше
|
|
74
|
+
html: 'Ваше сердце: самый точный индикатор усилий. Максимальная частота сердечных сокращений (МЧСС) зависит от возраста, генетики и сердечно-сосудистых способностей. Правильный расчет позволяет тренироваться в определенных зонах: аэробная зона (60-70% МЧСС) для базовой выносливости, анаэробная зона (80-90% МЧСС) для взрывной активности, зона VO2 max (95-100%) для кардиореспираторной способности.',
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
type: 'paragraph',
|
|
78
|
-
html: 'Распространенная
|
|
78
|
+
html: 'Распространенная ошибка: тренироваться слишком интенсивно слишком часто. Большая часть тренировок должна проходить в аэробной зоне (низкая интенсивность, большая продолжительность), создавая прочный фундамент. Оптимально проводить всего 1-2 сессии в неделю в высокоинтенсивных зонах во избежание перетренированности. Наши инструменты позволяют рассчитать зоны на основе возраста и уровня физической подготовки, гарантируя, что каждая сессия имеет четкую физиологическую цель.',
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
type: 'title',
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 = 'Baseball und Softball Spielstand mit
|
|
7
|
-
const description = 'Erfasse Live Baseballergebnisse mit Runs, Hits und Errors. Visuelles Diamantfeld mit
|
|
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
|
|
12
|
-
answer: 'Der
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
31
|
-
text: 'Tippe auf die Basen im Diamantfeld, um
|
|
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
|
|
39
|
-
text: '
|
|
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
|
|
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
|
|
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
|
|
112
|
-
description: 'Automatisierte Ball und Strike Verfolgung mit Walk und Strikeout Erkennung
|
|
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
|
|
114
|
+
points: ['Balls bis 4 gezählt', 'Strikes bis 3 gezählt', 'Auto zurücksetzen bei Entscheidung'],
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
|
-
title: '
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
143
|
-
'<strong>
|
|
144
|
-
'<strong>Inning
|
|
145
|
-
'<strong>Keine Einrichtung Erforderlich:</strong>
|
|
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
|
|
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
|
|
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
|
|
161
|
-
{ type: 'card', title: 'Freiwillige', html: '<p>Keine Erfahrung in der
|
|
162
|
-
{ type: 'card', title: 'Eltern', html: '<p>Verfolge das Spiel von den
|
|
163
|
-
{ type: 'card', title: 'Spieler', html: '<p>Sieh dir die Inning
|
|
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: '
|
|
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
|
|
191
|
-
resetConfirm: 'Das aktuelle Spiel
|
|
190
|
+
resetMatch: 'Spiel Zurücksetzen',
|
|
191
|
+
resetConfirm: 'Das aktuelle Spiel zurücksetzen? Alle Ergebnisse gehen verloren.',
|
|
192
192
|
cancel: 'Abbrechen',
|
|
193
|
-
confirm: '
|
|
193
|
+
confirm: 'Bestätigen',
|
|
194
194
|
total: 'Gesamt',
|
|
195
195
|
fullscreen: 'Vollbild',
|
|
196
196
|
toggleSound: 'Ton Ein Aus',
|