feat(i18n): enhance localization files with new configuration prompts
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 35s

- Added new localization strings across multiple languages for missing configuration prompts, including messages for league selection and MyTischtennis URL requirements.
- Updated TeamManagementView to display missing configuration alerts, improving user guidance during team setup.
This commit is contained in:
Torsten Schulz (local)
2026-04-02 09:14:07 +02:00
parent a94ad55a2d
commit 50fa07d0b7
16 changed files with 230 additions and 34 deletions

View File

@@ -915,7 +915,10 @@
"firstHalf": "Vorrunde",
"firstHalfFull": "Vorrunde (Juli - Dezember)",
"secondHalf": "Rückrunde",
"secondHalfFull": "Rückrunde (ab 1. Januar)"
"secondHalfFull": "Rückrunde (ab 1. Januar)",
"missingConfigSummary": "Fehlende Angaben:",
"leagueFieldRequired": "Bitte eine Spielklasse auswählen und speichern.",
"myTischtennisUrlRequired": "MyTischtennis-URL einfügen und parsen, um Team-ID und Ligendaten zu übernehmen."
},
"pdfGenerator": {
"teamLineupTitle": "Mannschaftsaufstellung",

View File

@@ -599,7 +599,10 @@
"firstHalf": "Vorrunde",
"firstHalfFull": "Vorrunde (Juli - Dezember)",
"secondHalf": "Rückrunde",
"secondHalfFull": "Rückrunde (ab 1. Januar)"
"secondHalfFull": "Rückrunde (ab 1. Januar)",
"missingConfigSummary": "Fehlende Angaben:",
"leagueFieldRequired": "Bitte eine Spielklasse auswählen und speichern.",
"myTischtennisUrlRequired": "MyTischtennis-URL einfügen und parsen, um Team-ID und Ligendaten zu übernehmen."
},
"pdfGenerator": {
"teamLineupTitle": "Mannschaftsaufstellung",

View File

@@ -1462,6 +1462,9 @@
"notCreated": "Nicht erstellt",
"autoFetchEnabled": "Automatischer Datenabruf ist aktiviert",
"missingItems": "Fehlend: {items}",
"missingConfigSummary": "Fehlende Angaben:",
"leagueFieldRequired": "Bitte eine Spielklasse auswählen und speichern.",
"myTischtennisUrlRequired": "MyTischtennis-URL einfügen und parsen, um Team-ID und Ligendaten zu übernehmen.",
"enterUrlForAutoConfig": "MyTischtennis-URL eingeben für automatische Konfiguration",
"errorLoadingStats": "Statistiken konnten nicht geladen werden.",
"asyncJobStartFailed": "Async-Job konnte nicht gestartet werden.",

View File

@@ -915,7 +915,10 @@
"firstHalf": "First half",
"firstHalfFull": "First half (July - December)",
"secondHalf": "Second half",
"secondHalfFull": "Second half (from 1 January)"
"secondHalfFull": "Second half (from 1 January)",
"missingConfigSummary": "Missing:",
"leagueFieldRequired": "Please select a league class and save.",
"myTischtennisUrlRequired": "Paste and parse the MyTischtennis URL to link the team ID and league data."
},
"pdfGenerator": {
"teamLineupTitle": "Team line-up",

View File

@@ -1153,6 +1153,9 @@
"notCreated": "Not created",
"autoFetchEnabled": "Automatic data retrieval is enabled",
"missingItems": "Missing: {items}",
"missingConfigSummary": "Missing:",
"leagueFieldRequired": "Please select a league class and save.",
"myTischtennisUrlRequired": "Paste and parse the MyTischtennis URL to link the team ID and league data.",
"enterUrlForAutoConfig": "Enter a myTischtennis URL for automatic configuration",
"errorLoadingStats": "Statistics could not be loaded.",
"asyncJobStartFailed": "The async job could not be started.",

View File

@@ -915,7 +915,10 @@
"firstHalf": "First half",
"firstHalfFull": "First half (July - December)",
"secondHalf": "Second half",
"secondHalfFull": "Second half (from 1 January)"
"secondHalfFull": "Second half (from 1 January)",
"missingConfigSummary": "Missing:",
"leagueFieldRequired": "Please select a league class and save.",
"myTischtennisUrlRequired": "Paste and parse the MyTischtennis URL to link the team ID and league data."
},
"pdfGenerator": {
"teamLineupTitle": "Team line-up",

View File

@@ -882,7 +882,10 @@
"firstHalf": "Primera vuelta",
"firstHalfFull": "Primera vuelta (julio - diciembre)",
"secondHalf": "Segunda vuelta",
"secondHalfFull": "Segunda vuelta (desde el 1 de enero)"
"secondHalfFull": "Segunda vuelta (desde el 1 de enero)",
"missingConfigSummary": "Información faltante:",
"leagueFieldRequired": "Seleccione una liga y guarde.",
"myTischtennisUrlRequired": "Pegue y analice la URL de MyTischtennis para vincular el ID del equipo y los datos de la liga."
},
"pdfGenerator": {
"teamLineupTitle": "Alineación del equipo",

View File

@@ -882,7 +882,10 @@
"firstHalf": "Unang yugto",
"firstHalfFull": "Unang yugto (Hulyo - Disyembre)",
"secondHalf": "Ikalawang yugto",
"secondHalfFull": "Ikalawang yugto (mula Enero 1)"
"secondHalfFull": "Ikalawang yugto (mula Enero 1)",
"missingConfigSummary": "Nawawalang impormasyon:",
"leagueFieldRequired": "Mangyaring pumili ng liga at i-save.",
"myTischtennisUrlRequired": "I-paste at i-parse ang URL ng MyTischtennis para i-link ang team ID at datos ng liga."
},
"pdfGenerator": {
"teamLineupTitle": "Line-up ng koponan",

View File

@@ -882,7 +882,10 @@
"firstHalf": "Phase aller",
"firstHalfFull": "Phase aller (juillet - décembre)",
"secondHalf": "Phase retour",
"secondHalfFull": "Phase retour (à partir du 1er janvier)"
"secondHalfFull": "Phase retour (à partir du 1er janvier)",
"missingConfigSummary": "Informations manquantes :",
"leagueFieldRequired": "Veuillez sélectionner une classe de ligue et enregistrer.",
"myTischtennisUrlRequired": "Collez et analysez lURL MyTischtennis pour lier lID déquipe et les données de ligue."
},
"pdfGenerator": {
"teamLineupTitle": "Composition de léquipe",

View File

@@ -882,7 +882,10 @@
"firstHalf": "Girone dandata",
"firstHalfFull": "Girone dandata (luglio - dicembre)",
"secondHalf": "Girone di ritorno",
"secondHalfFull": "Girone di ritorno (dal 1° gennaio)"
"secondHalfFull": "Girone di ritorno (dal 1° gennaio)",
"missingConfigSummary": "Dati mancanti:",
"leagueFieldRequired": "Seleziona una classe di campionato e salva.",
"myTischtennisUrlRequired": "Incolla e analizza lURL MyTischtennis per collegare lID squadra e i dati della lega."
},
"pdfGenerator": {
"teamLineupTitle": "Formazione della squadra",

View File

@@ -882,7 +882,10 @@
"firstHalf": "前期",
"firstHalfFull": "前期7月 - 12月",
"secondHalf": "後期",
"secondHalfFull": "後期1月1日以降"
"secondHalfFull": "後期1月1日以降",
"missingConfigSummary": "不足している情報:",
"leagueFieldRequired": "リーグを選択して保存してください。",
"myTischtennisUrlRequired": "MyTischtennis の URL を貼り付けて解析し、チーム ID とリーグデータを関連付けます。"
},
"pdfGenerator": {
"teamLineupTitle": "チーム登録メンバー",

View File

@@ -882,7 +882,10 @@
"firstHalf": "Pierwsza runda",
"firstHalfFull": "Pierwsza runda (lipiec - grudzień)",
"secondHalf": "Druga runda",
"secondHalfFull": "Druga runda (od 1 stycznia)"
"secondHalfFull": "Druga runda (od 1 stycznia)",
"missingConfigSummary": "Brakujące informacje:",
"leagueFieldRequired": "Wybierz klasę rozgrywkową i zapisz.",
"myTischtennisUrlRequired": "Wklej i przeanalizuj adres URL MyTischtennis, aby powiązać ID drużyny i dane ligi."
},
"pdfGenerator": {
"teamLineupTitle": "Skład drużyny",

View File

@@ -882,7 +882,10 @@
"firstHalf": "ครึ่งแรก",
"firstHalfFull": "ครึ่งแรก (กรกฎาคม - ธันวาคม)",
"secondHalf": "ครึ่งหลัง",
"secondHalfFull": "ครึ่งหลัง (ตั้งแต่ 1 มกราคม)"
"secondHalfFull": "ครึ่งหลัง (ตั้งแต่ 1 มกราคม)",
"missingConfigSummary": "รายการที่ขาด:",
"leagueFieldRequired": "โปรดเลือกระดับลีกและบันทึก",
"myTischtennisUrlRequired": "วางและแยกวิเคราะห์ URL ของ MyTischtennis เพื่อเชื่อมโยง ID ทีมและข้อมูลลีก"
},
"pdfGenerator": {
"teamLineupTitle": "รายชื่อทีม",

View File

@@ -882,7 +882,10 @@
"firstHalf": "Unang yugto",
"firstHalfFull": "Unang yugto (Hulyo - Disyembre)",
"secondHalf": "Ikalawang yugto",
"secondHalfFull": "Ikalawang yugto (mula Enero 1)"
"secondHalfFull": "Ikalawang yugto (mula Enero 1)",
"missingConfigSummary": "Nawawalang impormasyon:",
"leagueFieldRequired": "Mangyaring pumili ng liga at mag-save.",
"myTischtennisUrlRequired": "I-paste at i-parse ang URL ng MyTischtennis para i-link ang team ID at datos ng liga."
},
"pdfGenerator": {
"teamLineupTitle": "Line-up ng koponan",

View File

@@ -882,7 +882,10 @@
"firstHalf": "上半程",
"firstHalfFull": "上半程7月 - 12月",
"secondHalf": "下半程",
"secondHalfFull": "下半程1月1日起"
"secondHalfFull": "下半程1月1日起",
"missingConfigSummary": "缺少的信息:",
"leagueFieldRequired": "请选择联赛级别并保存。",
"myTischtennisUrlRequired": "粘贴并解析 MyTischtennis 链接以获取球队 ID 和联赛数据。"
},
"pdfGenerator": {
"teamLineupTitle": "球队阵容",

View File

@@ -59,8 +59,24 @@
</div>
</div>
<div
v-if="teamToEdit && !getMyTischtennisStatus(teamToEdit).complete && getMyTischtennisStatus(teamToEdit).missingItemLabels.length"
class="workspace-missing-banner"
role="status"
>
<span class="workspace-missing-banner-label">{{ t('teamManagement.missingConfigSummary') }}</span>
<ul class="workspace-missing-list">
<li v-for="(label, idx) in getMyTischtennisStatus(teamToEdit).missingItemLabels" :key="idx">{{ label }}</li>
</ul>
</div>
<div class="workspace-section-switcher">
<button type="button" class="workspace-section-button" :class="{ active: activeEditorSection === 'basic' }" @click="activeEditorSection = 'basic'">
<button
type="button"
class="workspace-section-button"
:class="{ active: activeEditorSection === 'basic', 'needs-attention': editorTabNeedsAttention('basic') }"
@click="activeEditorSection = 'basic'"
>
{{ t('teamManagement.basicSettings') }}
</button>
<button v-if="teamToEdit && teamToEdit.leagueId" type="button" class="workspace-section-button" :class="{ active: activeEditorSection === 'stats' }" @click="activeEditorSection = 'stats'">
@@ -75,7 +91,13 @@
<button v-if="teamToEdit" type="button" class="workspace-section-button" :class="{ active: activeEditorSection === 'jobs' }" @click="activeEditorSection = 'jobs'">
{{ t('teamManagement.automaticJobs') }}
</button>
<button v-if="teamToEdit" type="button" class="workspace-section-button" :class="{ active: activeEditorSection === 'myTischtennis' }" @click="activeEditorSection = 'myTischtennis'">
<button
v-if="teamToEdit"
type="button"
class="workspace-section-button"
:class="{ active: activeEditorSection === 'myTischtennis', 'needs-attention': editorTabNeedsAttention('myTischtennis') }"
@click="activeEditorSection = 'myTischtennis'"
>
{{ t('teamManagement.myTischtennis') }}
</button>
</div>
@@ -92,7 +114,10 @@
<span class="settings-summary-label">{{ t('teamManagement.teamName') }}</span>
<strong>{{ teamToEdit.name }}</strong>
</div>
<div class="settings-summary-card">
<div
class="settings-summary-card"
:class="{ 'settings-summary-card--warning': getMyTischtennisStatus(teamToEdit).needsLeague }"
>
<span class="settings-summary-label">{{ t('teamManagement.league') }}</span>
<strong>{{ teamToEdit.league ? teamToEdit.league.name : t('teamManagement.noLeague') }}</strong>
</div>
@@ -119,14 +144,18 @@
<input type="text" v-model="newTeamName" :placeholder="t('teamManagement.teamNamePlaceholder')">
</label>
<label>
<label
class="settings-field-league"
:class="{ 'field-needs-attention': getMyTischtennisStatus(teamToEdit).needsLeague }"
>
<span>{{ t('teamManagement.league') }}:</span>
<select v-model="newLeagueId">
<select v-model="newLeagueId" :aria-invalid="getMyTischtennisStatus(teamToEdit).needsLeague ? 'true' : 'false'">
<option value="">{{ t('teamManagement.noLeague') }}</option>
<option v-for="league in filteredLeagues" :key="league.id" :value="league.id">
{{ league.name }}
</option>
</select>
<span v-if="getMyTischtennisStatus(teamToEdit).needsLeague" class="field-attention-hint">{{ t('teamManagement.leagueFieldRequired') }}</span>
</label>
<label class="settings-field-span-2">
@@ -458,15 +487,21 @@
<span v-else-if="getMyTischtennisStatus(teamToEdit).partial" class="status-badge partial"> {{ t('teamManagement.partiallyConfigured') }}</span>
<span v-else class="status-badge missing"> {{ t('teamManagement.notConfigured') }}</span>
</div>
<p v-if="getMyTischtennisStatus(teamToEdit).missing" class="mytt-status-copy">
{{ getMyTischtennisStatus(teamToEdit).missing }}
</p>
<p v-else class="mytt-status-copy">
<ul
v-if="!getMyTischtennisStatus(teamToEdit).complete && getMyTischtennisStatus(teamToEdit).missingItemLabels.length"
class="mytt-missing-checklist"
>
<li v-for="(label, idx) in getMyTischtennisStatus(teamToEdit).missingItemLabels" :key="idx">{{ label }}</li>
</ul>
<p v-else-if="getMyTischtennisStatus(teamToEdit).complete" class="mytt-status-copy">
{{ t('teamManagement.noIssues') }}
</p>
</div>
<div class="compact-input-row">
<div
class="compact-input-row"
:class="{ 'field-needs-attention-wrap': getMyTischtennisStatus(teamToEdit).needsTeamId }"
>
<input
type="text"
v-model="myTischtennisUrl"
@@ -474,11 +509,13 @@
:placeholder="t('teamManagement.myTischtennisUrlPlaceholder')"
class="compact-url-input"
:disabled="parsingUrl"
:aria-invalid="getMyTischtennisStatus(teamToEdit).needsTeamId ? 'true' : 'false'"
>
<button type="button" class="btn-secondary btn-upload-sm" @click="parseMyTischtennisUrl" :disabled="parsingUrl || !myTischtennisUrl.trim()">
{{ parsingUrl ? '⏳' : t('teamManagement.parseUrlAction') }}
</button>
</div>
<p v-if="getMyTischtennisStatus(teamToEdit).needsTeamId" class="field-attention-hint field-attention-hint-block">{{ t('teamManagement.myTischtennisUrlRequired') }}</p>
<div v-if="myTischtennisError" class="compact-message error"> {{ myTischtennisError }}</div>
<div v-if="myTischtennisSuccess" class="compact-message success"> {{ myTischtennisSuccess }}</div>
@@ -1651,43 +1688,76 @@ export default {
};
const getMyTischtennisStatus = (team) => {
const empty = {
complete: false,
partial: false,
missing: '',
missingItemLabels: [],
tooltip: '',
needsLeague: false,
needsTeamId: false,
needsLeagueGroupId: false,
needsLeagueAssociation: false,
needsLeagueGroupName: false
};
if (!team) {
return { complete: false, partial: false, missing: '', tooltip: '' };
return { ...empty };
}
const hasTeamId = !!team.myTischtennisTeamId;
const hasLeague = !!team.league;
const hasLeagueConfig = hasLeague &&
!!team.league.myTischtennisGroupId &&
!!team.league.association &&
const hasLeagueConfig = hasLeague &&
!!team.league.myTischtennisGroupId &&
!!team.league.association &&
!!team.league.groupname;
const missingItems = [];
if (!hasTeamId) missingItems.push(t('teamManagement.teamId'));
if (!hasLeague) missingItems.push(t('teamManagement.league'));
if (hasLeague && !team.league.myTischtennisGroupId) missingItems.push(t('teamManagement.groupId'));
if (hasLeague && !team.league.association) missingItems.push(t('teamManagement.association'));
if (hasLeague && !team.league.groupname) missingItems.push(t('teamManagement.groupName'));
const complete = hasTeamId && hasLeagueConfig;
const partial = (hasTeamId || hasLeagueConfig) && !complete;
let tooltip = '';
if (complete) {
tooltip = t('teamManagement.autoFetchEnabled');
} else if (partial) {
tooltip = t('teamManagement.missingItems', { items: missingItems.join(', ') });
} else {
tooltip = t('teamManagement.enterUrlForAutoConfig');
tooltip = missingItems.length > 0
? t('teamManagement.missingItems', { items: missingItems.join(', ') })
: t('teamManagement.enterUrlForAutoConfig');
}
return {
complete,
partial,
missing: missingItems.length > 0 ? t('teamManagement.missingItems', { items: missingItems.join(', ') }) : '',
tooltip
missingItemLabels: [...missingItems],
tooltip,
needsLeague: !hasLeague,
needsTeamId: !hasTeamId,
needsLeagueGroupId: !!(hasLeague && !team.league.myTischtennisGroupId),
needsLeagueAssociation: !!(hasLeague && !team.league.association),
needsLeagueGroupName: !!(hasLeague && !team.league.groupname)
};
};
const editorTabNeedsAttention = (section) => {
const team = teamToEdit.value;
if (!team) return false;
const s = getMyTischtennisStatus(team);
if (s.complete) return false;
if (section === 'basic') return s.needsLeague;
if (section === 'myTischtennis') {
if (s.needsLeague) return false;
return !s.complete;
}
return false;
};
const loadClubMembers = async () => {
if (!teamToEdit.value) {
@@ -2291,6 +2361,7 @@ export default {
configureLeagueFromUrl,
clearParsedData,
getMyTischtennisStatus,
editorTabNeedsAttention,
fetchTeamDataManually,
refreshPlayerStats,
loadClubMembers,
@@ -2519,6 +2590,86 @@ export default {
color: var(--primary-dark);
}
.workspace-section-button.needs-attention:not(.active) {
border-color: #c45c4a;
background: #fff5f2;
color: #8a2e22;
box-shadow: 0 0 0 1px rgba(196, 92, 74, 0.35);
}
.workspace-section-button.needs-attention.active {
box-shadow: 0 0 0 2px rgba(196, 92, 74, 0.45);
}
.workspace-missing-banner {
margin: 0 0 1rem;
padding: 0.65rem 0.85rem;
border-radius: var(--border-radius);
border: 1px solid #e8c9c4;
background: #fff8f6;
color: #5c2e28;
font-size: 0.9rem;
}
.workspace-missing-banner-label {
display: block;
font-weight: 700;
margin-bottom: 0.35rem;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #8a3d34;
}
.workspace-missing-list {
margin: 0;
padding-left: 1.15rem;
}
.workspace-missing-list li {
margin: 0.15rem 0;
}
.settings-summary-card--warning {
border-color: #e8a598;
background: #fff8f6;
}
label.field-needs-attention span:first-of-type,
.settings-field-league.field-needs-attention > span:first-of-type {
color: #8a2e22;
font-weight: 600;
}
.field-needs-attention select,
.field-needs-attention-wrap .compact-url-input {
border-color: #d97b6a !important;
box-shadow: 0 0 0 1px rgba(217, 123, 106, 0.35);
}
.field-attention-hint {
display: block;
font-size: 0.8rem;
color: #9d3b32;
margin-top: 0.35rem;
}
.field-attention-hint-block {
margin-top: 0.25rem;
margin-bottom: 0.35rem;
}
.mytt-missing-checklist {
margin: 0.4rem 0 0;
padding-left: 1.2rem;
color: var(--text-color);
font-size: 0.9rem;
}
.mytt-missing-checklist li {
margin: 0.2rem 0;
}
.workspace-section-panel {
background: white;
border: 1px solid var(--border-color);