From 83294406a45b18baccb5112fc8206a08ee54db97 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 14 May 2026 22:35:29 +0200 Subject: [PATCH] feat(Diary): implement quick create functionality for training days and enhance localization - Added a new button for quick creation of training days in the DiaryView, improving user experience. - Implemented logic to find the next available training slot across groups and create a training day entry. - Enhanced localization by adding new keys for quick create messages in multiple languages, ensuring better accessibility for users. - Updated the DiaryManager to handle quick create operations and clear errors effectively. --- frontend/src/i18n/locales/de-CH.json | 3 + frontend/src/i18n/locales/de-extended.json | 3 + frontend/src/i18n/locales/de.json | 3 + frontend/src/i18n/locales/en-AU.json | 3 + frontend/src/i18n/locales/en-GB.json | 3 + frontend/src/i18n/locales/en-US.json | 3 + frontend/src/i18n/locales/es.json | 3 + frontend/src/i18n/locales/fil.json | 3 + frontend/src/i18n/locales/fr.json | 3 + frontend/src/i18n/locales/it.json | 3 + frontend/src/i18n/locales/ja.json | 3 + frontend/src/i18n/locales/pl.json | 3 + frontend/src/i18n/locales/th.json | 3 + frontend/src/i18n/locales/tl.json | 3 + frontend/src/i18n/locales/zh.json | 3 + frontend/src/views/DiaryView.vue | 105 +- mobile-app/DEVELOPMENT.md | 6 + mobile-app/REGRESSION_CHECKLIST.md | 60 + mobile-app/TODO.md | 15 +- mobile-app/composeApp/build.gradle.kts | 4 + mobile-app/composeApp/proguard-rules.pro | 29 + .../de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt | 383 ++++- mobile-app/gradle/libs.versions.toml | 7 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- mobile-app/shared/build.gradle.kts | 5 + .../InternalTournamentSerializationTest.kt | 43 + .../tt_tagebuch/shared/i18n/MobileStrings.kt | 1302 ++++++++++++++++- .../tt_tagebuch/shared/state/DiaryManager.kt | 13 +- scripts/generate-mobile-i18n.js | 3 +- 29 files changed, 1976 insertions(+), 46 deletions(-) create mode 100644 mobile-app/REGRESSION_CHECKLIST.md create mode 100644 mobile-app/composeApp/proguard-rules.pro create mode 100644 mobile-app/shared/src/androidUnitTest/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/InternalTournamentSerializationTest.kt diff --git a/frontend/src/i18n/locales/de-CH.json b/frontend/src/i18n/locales/de-CH.json index 8b1a8e96..7d9d9f61 100644 --- a/frontend/src/i18n/locales/de-CH.json +++ b/frontend/src/i18n/locales/de-CH.json @@ -828,6 +828,9 @@ "noEntries": "Keine Einträge", "deleteDate": "Datum löschen", "createNew": "Neu anlegen", + "quickCreate": "Schnellanlegen", + "quickCreateNoSlot": "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).", + "quickCreateFailed": "Der Trainingstag konnte nicht angelegt werden.", "gallery": "Mitglieder-Galerie", "galleryCreating": "Galerie wird erstellt…", "selectTrainingGroup": "Trainingsgruppe auswählen", diff --git a/frontend/src/i18n/locales/de-extended.json b/frontend/src/i18n/locales/de-extended.json index 924d06bf..827e779d 100644 --- a/frontend/src/i18n/locales/de-extended.json +++ b/frontend/src/i18n/locales/de-extended.json @@ -575,6 +575,9 @@ "noEntries": "Keine Einträge", "deleteDate": "Datum löschen", "createNew": "Neu anlegen", + "quickCreate": "Schnellanlegen", + "quickCreateNoSlot": "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).", + "quickCreateFailed": "Der Trainingstag konnte nicht angelegt werden.", "gallery": "Mitglieder-Galerie", "galleryCreating": "Galerie wird erstellt…", "selectTrainingGroup": "Trainingsgruppe auswählen", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 381419ad..4723f8fd 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -650,6 +650,9 @@ "noEntries": "Keine Einträge", "deleteDate": "Datum löschen", "createNew": "Neu anlegen", + "quickCreate": "Schnellanlegen", + "quickCreateNoSlot": "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).", + "quickCreateFailed": "Der Trainingstag konnte nicht angelegt werden.", "gallery": "Mitglieder-Galerie", "galleryCreating": "Galerie wird erstellt…", "selectTrainingGroup": "Trainingsgruppe auswählen", diff --git a/frontend/src/i18n/locales/en-AU.json b/frontend/src/i18n/locales/en-AU.json index 7775ddb7..a6652023 100644 --- a/frontend/src/i18n/locales/en-AU.json +++ b/frontend/src/i18n/locales/en-AU.json @@ -827,6 +827,9 @@ "noEntries": "No entries", "deleteDate": "Delete date", "createNew": "Create new", + "quickCreate": "Quick add", + "quickCreateNoSlot": "No free training slot found (per group schedule, one year ahead).", + "quickCreateFailed": "Could not create the training day entry.", "gallery": "Member gallery", "galleryCreating": "Creating gallery…", "selectTrainingGroup": "Select training group", diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json index e95e64ed..00c0563c 100644 --- a/frontend/src/i18n/locales/en-GB.json +++ b/frontend/src/i18n/locales/en-GB.json @@ -320,6 +320,9 @@ "noEntries": "No entries", "deleteDate": "Delete date", "createNew": "Create new", + "quickCreate": "Quick add", + "quickCreateNoSlot": "No free training slot found (per group schedule, one year ahead).", + "quickCreateFailed": "Could not create the training day entry.", "gallery": "Member gallery", "galleryCreating": "Creating gallery…", "selectTrainingGroup": "Select training group", diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index 1874f96b..2f24dc45 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -827,6 +827,9 @@ "noEntries": "No entries", "deleteDate": "Delete date", "createNew": "Create new", + "quickCreate": "Quick add", + "quickCreateNoSlot": "No free training slot found (per group schedule, one year ahead).", + "quickCreateFailed": "Could not create the training day entry.", "gallery": "Member gallery", "galleryCreating": "Creating gallery…", "selectTrainingGroup": "Select training group", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 8f315ff1..98c44ced 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -793,6 +793,9 @@ "noEntries": "No hay entradas", "deleteDate": "Eliminar fecha", "createNew": "Crear", + "quickCreate": "Alta rápida", + "quickCreateNoSlot": "No se encontró un día de entrenamiento libre (según horarios de grupos, un año adelante).", + "quickCreateFailed": "No se pudo crear el día de entrenamiento.", "gallery": "Galería de miembros", "galleryCreating": "Creando galería…", "selectTrainingGroup": "Seleccionar grupo de entrenamiento", diff --git a/frontend/src/i18n/locales/fil.json b/frontend/src/i18n/locales/fil.json index 4a70df21..98966458 100644 --- a/frontend/src/i18n/locales/fil.json +++ b/frontend/src/i18n/locales/fil.json @@ -793,6 +793,9 @@ "noEntries": "Walang entry", "deleteDate": "Burahin ang petsa", "createNew": "Lumikha ng bago", + "quickCreate": "Mabilis na idagdag", + "quickCreateNoSlot": "Walang nahanap na libreng araw ng training (ayon sa iskedyul ng grupo, isang taon pasulong).", + "quickCreateFailed": "Hindi malikha ang araw ng training.", "gallery": "Gallery ng mga kasapi", "galleryCreating": "Ginagawa ang gallery…", "selectTrainingGroup": "Pumili ng grupo ng pagsasanay", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 7b30d91e..bff851c8 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -793,6 +793,9 @@ "noEntries": "Aucune entrée", "deleteDate": "Supprimer la date", "createNew": "Créer", + "quickCreate": "Création rapide", + "quickCreateNoSlot": "Aucun créneau d’entraînement libre trouvé (selon les horaires des groupes, sur un an).", + "quickCreateFailed": "Impossible de créer la journée d’entraînement.", "gallery": "Galerie des membres", "galleryCreating": "Création de la galerie…", "selectTrainingGroup": "Sélectionner un groupe d'entraînement", diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index 1a5bc9ab..72fa9da0 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -793,6 +793,9 @@ "noEntries": "Nessuna voce", "deleteDate": "Elimina data", "createNew": "Crea", + "quickCreate": "Creazione rapida", + "quickCreateNoSlot": "Nessun giorno di allenamento libero trovato (in base agli orari dei gruppi, entro un anno).", + "quickCreateFailed": "Impossibile creare la giornata di allenamento.", "gallery": "Galleria membri", "galleryCreating": "Creazione galleria…", "selectTrainingGroup": "Seleziona gruppo di allenamento", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index a3761617..34f2bb48 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -793,6 +793,9 @@ "noEntries": "エントリがありません", "deleteDate": "日付を削除", "createNew": "新規作成", + "quickCreate": "クイック作成", + "quickCreateNoSlot": "空きの練習日が見つかりません(グループのスケジュールに基づき、1年先まで)。", + "quickCreateFailed": "練習日を作成できませんでした。", "gallery": "メンバーギャラリー", "galleryCreating": "ギャラリーを作成中…", "selectTrainingGroup": "練習グループを選択", diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index ee96c583..9bbf5067 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -793,6 +793,9 @@ "noEntries": "Brak wpisów", "deleteDate": "Usuń datę", "createNew": "Utwórz", + "quickCreate": "Szybkie dodawanie", + "quickCreateNoSlot": "Nie znaleziono wolnego terminu treningu (wg harmonogramów grup, rok do przodu).", + "quickCreateFailed": "Nie udało się utworzyć dnia treningowego.", "gallery": "Galeria członków", "galleryCreating": "Tworzenie galerii…", "selectTrainingGroup": "Wybierz grupę treningową", diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index 9836d769..aa6021d7 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -793,6 +793,9 @@ "noEntries": "ไม่มีรายการ", "deleteDate": "ลบวันที่", "createNew": "สร้างใหม่", + "quickCreate": "เพิ่มด่วน", + "quickCreateNoSlot": "ไม่พบวันฝึกที่ว่าง (ตามตารางกลุ่ม ล่วงหน้าหนึ่งปี)", + "quickCreateFailed": "ไม่สามารถสร้างวันฝึกได้", "gallery": "แกลเลอรีสมาชิก", "galleryCreating": "กำลังสร้างแกลเลอรี…", "selectTrainingGroup": "เลือกกลุ่มฝึกซ้อม", diff --git a/frontend/src/i18n/locales/tl.json b/frontend/src/i18n/locales/tl.json index de650285..93171322 100644 --- a/frontend/src/i18n/locales/tl.json +++ b/frontend/src/i18n/locales/tl.json @@ -793,6 +793,9 @@ "noEntries": "Walang entry", "deleteDate": "Burahin ang petsa", "createNew": "Lumikha ng bago", + "quickCreate": "Mabilis na idagdag", + "quickCreateNoSlot": "Walang nahanap na libreng araw ng training (ayon sa iskedyul ng grupo, isang taon pasulong).", + "quickCreateFailed": "Hindi malikha ang araw ng training.", "gallery": "Gallery ng mga miyembro", "galleryCreating": "Ginagawa ang gallery…", "selectTrainingGroup": "Pumili ng grupo ng pagsasanay", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 42aa3c66..e29cab6b 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -793,6 +793,9 @@ "noEntries": "没有条目", "deleteDate": "删除日期", "createNew": "新建", + "quickCreate": "快速创建", + "quickCreateNoSlot": "未找到空闲训练日(按小组时间表,向前查找一年)。", + "quickCreateFailed": "无法创建训练日。", "gallery": "成员图库", "galleryCreating": "正在生成图库…", "selectTrainingGroup": "选择训练组", diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index e01a3266..14c3d6e6 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -11,6 +11,9 @@ + @@ -1052,7 +1055,8 @@ export default { isMobileView: typeof window !== 'undefined' ? window.innerWidth <= 768 : false, participantSearchQuery: '', participantFilter: 'all', - planGroupFilter: '__all__' + planGroupFilter: '__all__', + quickCreateBusy: false }; }, watch: { @@ -1623,6 +1627,76 @@ export default { this.showTrainingGroupDialog = true; this.showForm = false; }, + + diaryDateKey(entry) { + if (!entry || entry.date == null) return ''; + return String(entry.date).substring(0, 10); + }, + + localYyyyMmDd(d) { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; + }, + + findNextQuickSlotAcrossGroups(groups) { + if (!Array.isArray(groups) || groups.length === 0) return null; + const sorted = [...groups].sort((a, b) => { + const oa = Number.isFinite(Number(a.sortOrder)) ? Number(a.sortOrder) : 1e9; + const ob = Number.isFinite(Number(b.sortOrder)) ? Number(b.sortOrder) : 1e9; + if (oa !== ob) return oa - ob; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }); + const existing = new Set((this.dates || []).map((d) => this.diaryDateKey(d))); + const today = new Date(); + for (let dayOffset = 0; dayOffset <= 366; dayOffset++) { + const checkDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + dayOffset); + const checkWeekday = checkDate.getDay(); + const dateStr = this.localYyyyMmDd(checkDate); + if (existing.has(dateStr)) continue; + for (const g of sorted) { + const times = (g.trainingTimes || []) + .filter((tt) => tt.weekday === checkWeekday && tt.startTime && String(tt.startTime).trim()); + if (!times.length) continue; + times.sort((a, b) => String(a.startTime).localeCompare(String(b.startTime)) + || (Number(a.id) || 0) - (Number(b.id) || 0)); + const time = times[0]; + return { + date: dateStr, + startTime: this.formatTimeForInput(time.startTime), + endTime: this.formatTimeForInput(time.endTime), + }; + } + } + return null; + }, + + async quickCreateNextTraining() { + if (!this.currentClub || this.quickCreateBusy) return; + this.quickCreateBusy = true; + try { + const response = await apiClient.get(`/training-times/${this.currentClub}`); + const groups = Array.isArray(response.data) ? response.data : []; + const slot = this.findNextQuickSlotAcrossGroups(groups); + if (!slot) { + this.showInfo(this.$t('messages.info'), this.$t('diary.quickCreateNoSlot'), '', 'info'); + return; + } + const post = await apiClient.post(`/diary/${this.currentClub}`, { + date: slot.date, + trainingStart: slot.startTime || null, + trainingEnd: slot.endTime || null, + }); + await this.refreshDates(post.data.id); + await this.handleDateChange(); + } catch (error) { + console.error('[quickCreateNextTraining]', error); + this.showInfo(this.$t('messages.error'), this.$t('diary.quickCreateFailed'), '', 'error'); + } finally { + this.quickCreateBusy = false; + } + }, async handleDateChange() { this.showForm = false; @@ -1780,8 +1854,8 @@ export default { // Nimm die erste Trainingszeit für diesen Tag const time = timesForWeekday[0]; // Prüfe, ob dieses Datum bereits existiert - const dateStr = checkDate.toISOString().split('T')[0]; - const exists = this.dates.some(d => d.date === dateStr); + const dateStr = this.localYyyyMmDd(checkDate); + const exists = this.dates.some(d => this.diaryDateKey(d) === dateStr); if (!exists) { return { @@ -4403,6 +4477,31 @@ h3 { margin-bottom: 1rem; } +/* Global `label { display: inline-flex }` würde sonst alle Kinder in eine Zeile zwängen und Buttons schrumpfen lassen. */ +.diary-header-row > label { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0.75rem; + flex: 1 1 auto; + min-width: 0; + max-width: 100%; + margin: 0; + cursor: default; +} + +.diary-header-row > label > select { + flex: 1 1 12rem; + min-width: min(12rem, 100%); + max-width: 100%; +} + +.diary-header-row > label > .btn-primary, +.diary-header-row > label > .btn-secondary { + flex-shrink: 0; + white-space: nowrap; +} + .gallery-trigger { align-self: flex-end; } diff --git a/mobile-app/DEVELOPMENT.md b/mobile-app/DEVELOPMENT.md index 19761c97..8f118ddc 100644 --- a/mobile-app/DEVELOPMENT.md +++ b/mobile-app/DEVELOPMENT.md @@ -98,6 +98,12 @@ adb reverse tcp:3005 tcp:3005 4. Login → Club auswählen → Rolle wird angezeigt 5. Open `Tagebuch` → die letzten Einträge werden angezeigt +## Phase 14 – Unit-Tests, Regressionsliste, R8 + +- **Automatisierte Tests (shared):** `./gradlew :shared:testDebugUnitTest` (JVM-Unit-Tests unter `shared/src/androidUnitTest/`, u. a. Serialisierung kritischer DTOs). +- **Manuelle Regression:** `REGRESSION_CHECKLIST.md` im Ordner `mobile-app/`. +- **R8/ProGuard:** `composeApp/proguard-rules.pro` ist an die Release-Variante angebunden; **`isMinifyEnabled`** bleibt vorerst **`false`**, bis ein Release mit Schrumpfung gezielt getestet wurde. + ## Gradle Wrapper Use the wrapper from `mobile-app/`: ```bash diff --git a/mobile-app/REGRESSION_CHECKLIST.md b/mobile-app/REGRESSION_CHECKLIST.md new file mode 100644 index 00000000..28b00f25 --- /dev/null +++ b/mobile-app/REGRESSION_CHECKLIST.md @@ -0,0 +1,60 @@ +# Mobile App – Regressions-Checkliste (manuell, Phase 14) + +Kurztest vor Release oder nach größeren Änderungen. Angelehnt an `mobile-app/TODO.md` (Phasen 0–12). + +## Vorbedingungen + +- Backend erreichbar (Staging oder Produktion wie gebaut). +- Testverein mit typischen Rechten (Tagebuch, Mitglieder, Kalender, ggf. Team-Verwaltung). + +## Phase 0 / Shell + +- [ ] Login, Club wählen, **Start**-Hub sichtbar +- [ ] Bottom-Navigation (Handy) bzw. **Navigation Rail** (Tablet): alle sichtbaren Tabs erreichbar +- [ ] **Zurück** aus Tagebuch-Detail / Mitglieder-Detail schließt nur die Ebene, nicht die ganze App + +## Auth & Club (1–2) + +- [ ] Abmelden, erneut anmelden +- [ ] Anderer Club wählen: Daten wechseln plausibel + +## Tagebuch (3) + +- [ ] Tagebuch: Liste, Tag öffnen, **Zurück** +- [ ] **Neu**: Dialog Datum/Zeiten → Anlegen → **Sprung** in den neuen Tag +- [ ] Trainingsplan: Eintrag anlegen, Reihenfolge, löschen (Stichprobe) +- [ ] Teilnehmer: hinzufügen, Status (Stichprobe) +- [ ] PDF teilen (Intent öffnet sich) + +## Mitglieder (4) + +- [ ] Liste, Suche, Detail, Bearbeiten (Stichprobe eines Felds) + +## Statistik / Terminplan / Turniere (5–7) + +- [ ] Statistik: Kennzahlen laden +- [ ] Terminplan: Spiel anzeigen; bei Schreibrecht: Aufstellung ändern (falls vorhanden) +- [ ] Turniere: Liste ohne Absturz; Detail (Stichprobe) + +## Kalender (12) + +- [ ] Monat wechseln, Legende, mindestens eine Aktion (z. B. Tagebuch aus Event) + +## Mehr / Einstellungen (8–11) + +- [ ] **Mehr**: persönliche Einstellungen, Impressum/Datenschutz (Browser) +- [ ] Optional: Abrechnung/Bestellungen nur öffnen, wenn genutzt + +## Barrierefreiheit (Stichprobe) + +- [ ] **TalkBack** (oder ähnlich): Haupt-Navigation pro Tab **sinnvolle** Ansage (Label) +- [ ] Wichtige Buttons mind. ca. **48 dp** Höhe (Touch-Ziel) + +## Performance (Stichprobe) + +- [ ] Tagebuch mit **vielen** Tagen: Liste scrollt flüssig +- [ ] Mitgliederliste mit vielen Einträgen: bedienbar + +## Store / Compliance (Hinweis) + +- Play Console: **Datenschutzerklärung**, ggf. **Berechtigungen** (Kamera/Galerie für Profil/UCrop, Internet) mit Text in der Store-Listing-Beschreibung konsistent halten. diff --git a/mobile-app/TODO.md b/mobile-app/TODO.md index 53a5d062..47c49458 100644 --- a/mobile-app/TODO.md +++ b/mobile-app/TODO.md @@ -221,7 +221,7 @@ Web-Route: `/calendar` · Referenz: `CalendarView.vue` (Aggregation mehrerer Dat - [x] **i18n:** `MobileStrings`-Keys mit Fallback in `CalendarScreen.kt` / `AppRoot.kt` (`navigation.calendar`, `home.tileCalendar`, `mobile.calendar*`) - [x] **Web-Parität (Detail):** Meldung wenn **alle** Kalender-Quellen fehlschlagen; Legenden-Zähler aus **allen** Events (unabhängig von den Schaltern); Agenda-Datum **kurz/lokalisiert**; Ausfall-Liste nur wenn **Startdatum im Monat** liegt (wie `CalendarView.vue`) – `CalendarScreen.kt` -### Phase 12 – Backlog / offen +### Phase 13 – Backlog / offen - [ ] **i18n:** Kalender-Keys in `MobileStrings.kt` für alle unterstützten Sprachen ergänzen (nicht nur Fallback im Code) - [ ] **Kalender vs. Web:** Offizielle Teilnahmen mobil per Browser vs. Web in-app – bewusst lassen oder später angleichen @@ -230,13 +230,14 @@ Web-Route: `/calendar` · Referenz: `CalendarView.vue` (Aggregation mehrerer Dat --- -## Phase 13 – Qualität, Tests, Release +## Phase 14 – Qualität, Tests, Release -- [ ] **Regression-Checkliste** pro Phase (manuell) -- [ ] Automatisierte Tests: `shared` (Serialisierung, Mapper), wo möglich UI-Tests kritische Flows -- [ ] **Barrierefreiheit:** Talkback, Kontraste, Touch-Ziele -- [ ] **Performance:** große Listen (Paging), Bildcache -- [ ] Store-Texte, Datenverarbeitung (Play Policy) für Medien und Kontakte +- [x] **Regression-Checkliste** pro Phase (manuell) – `mobile-app/REGRESSION_CHECKLIST.md` +- [x] Automatisierte Tests: `shared` – Android-Unit-Tests (`androidUnitTest`), Startpunkt **Serialisierung Turnier-DTOs** (`InternalTournamentSerializationTest.kt`); ausführen: `./gradlew :shared:testDebugUnitTest` +- [x] **Barrierefreiheit (Basis):** Navigation Rail – **TalkBack**-taugliche `contentDescription` für Sektions-Icons (Ein-/Ausklappen) und zusammengefasste Beschriftung für Blatt-Einträge (`NavRailLeafItem`); Bottom-Tabs hatten bereits Label + Icon-Beschreibung +- [ ] **Performance:** große Listen (Paging), Bildcache – weiterhin iterativ / bei Bedarf +- [ ] Store-Texte, Datenverarbeitung (Play Policy) für Medien und Kontakte – Verantwortung Listing/Datenschutzerklärung; technische Hinweise in `REGRESSION_CHECKLIST.md` +- [x] **Release / R8-Vorbereitung:** `composeApp/proguard-rules.pro` (Ktor, `kotlinx.serialization`, Koin, BuildConfig) + `proguardFiles` in `composeApp/build.gradle.kts`; **`isMinifyEnabled`** bleibt `false` bis gezielter Release-Test --- diff --git a/mobile-app/composeApp/build.gradle.kts b/mobile-app/composeApp/build.gradle.kts index 1d16dee2..6c050da8 100644 --- a/mobile-app/composeApp/build.gradle.kts +++ b/mobile-app/composeApp/build.gradle.kts @@ -74,6 +74,10 @@ android { buildTypes { getByName("release") { isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } } compileOptions { diff --git a/mobile-app/composeApp/proguard-rules.pro b/mobile-app/composeApp/proguard-rules.pro new file mode 100644 index 00000000..c5886340 --- /dev/null +++ b/mobile-app/composeApp/proguard-rules.pro @@ -0,0 +1,29 @@ +# Release / R8 (ProGuard) – Vorbereitung (Phase 14) + +# Kotlin Serialization: @Serializable-Klassen & generierte Serializer +-keepattributes *Annotation*, InnerClasses, EnclosingMethod +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations +-keepclassmembers class kotlinx.serialization.json.** { *; } +-dontnote kotlinx.serialization.** + +-keepclassmembers @kotlinx.serialization.Serializable class ** { + *** Companion; + ; +} +-if class **$$serializer { public static ** INSTANCE; } +-keepclassmembers class <1>$$serializer { + (kotlinx.serialization.encoding.CompositeEncoder,kotlinx.serialization.descriptors.SerialDescriptor,int); + private static final ** INSTANCE; +} + +# Ktor Client (OkHttp, Engines) +-keep class io.ktor.client.** { *; } +-keep class io.ktor.http.** { *; } +-keep class io.ktor.util.** { *; } +-keep class io.ktor.serialization.** { *; } + +# Koin (Module / Reflection) +-keep class org.koin.** { *; } + +# BuildConfig (falls R8 später aktiviert wird) +-keep class de.tsschulz.tt_tagebuch.BuildConfig { *; } diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt index 433f1fa4..329f082e 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tsschulz/tt_tagebuch/app/ui/AppRoot.kt @@ -2,6 +2,7 @@ package de.tsschulz.tt_tagebuch.app.ui import android.content.Intent import android.net.Uri +import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -746,7 +747,11 @@ private fun NavRailSectionHeader( ) { Icon( imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, - contentDescription = null, + contentDescription = if (expanded) { + tr("mobile.collapseSection", "Abschnitt einklappen") + ": $title" + } else { + tr("mobile.expandSection", "Abschnitt ausklappen") + ": $title" + }, modifier = Modifier.size(18.dp), tint = MaterialTheme.colors.onSurface.copy(alpha = 0.7f), ) @@ -771,6 +776,9 @@ private fun NavRailLeafItem( modifier = Modifier .fillMaxWidth() .padding(vertical = 2.dp, horizontal = 4.dp) + .semantics(mergeDescendants = true) { + contentDescription = label + } .clickable(onClick = onClick), color = if (selected) { MaterialTheme.colors.primary.copy(alpha = 0.1f) @@ -1376,8 +1384,59 @@ private fun DiaryListScreen( ) { val clubState by dependencies.clubManager.state.collectAsState() val diaryState by dependencies.diaryManager.state.collectAsState() + val languageState by dependencies.languageManager.state.collectAsState() + val androidCtx = LocalContext.current val clubId = clubState.currentClubId ?: return var dayMenuExpanded by rememberSaveable { mutableStateOf(false) } + var showNewDateDialog by rememberSaveable { mutableStateOf(false) } + var newDiaryDateStr by rememberSaveable { mutableStateOf("") } + var newDiaryStart by rememberSaveable { mutableStateOf("") } + var newDiaryEnd by rememberSaveable { mutableStateOf("") } + val newDateScope = rememberCoroutineScope() + var newDateScheduleGroups by remember { mutableStateOf>(emptyList()) } + var newDateScheduleLoading by remember { mutableStateOf(false) } + var newDateScheduleLoadError by remember { mutableStateOf(null) } + var newDateGroupMenuExpanded by remember { mutableStateOf(false) } + var selectedNewDateGroupId by remember { mutableStateOf(null) } + var quickCreateBusy by remember { mutableStateOf(false) } + + val diaryDatesNormKey = remember(diaryState.dates) { + diaryState.dates.map { it.date.take(10).trim() }.sorted().joinToString("|") + } + val existingDiaryDatesNorm = remember(diaryDatesNormKey) { + diaryDatesNormKey.split("|").filter { it.isNotBlank() }.toSet() + } + val newDateSlotSuggestion = remember(selectedNewDateGroupId, newDateScheduleGroups, existingDiaryDatesNorm) { + val gid = selectedNewDateGroupId ?: return@remember null + val g = newDateScheduleGroups.find { it.id == gid } ?: return@remember null + nextDiarySlotFromTrainingTimes(g.trainingTimes, existingDiaryDatesNorm) + } + + /** Sobald eine Gruppe gewählt ist und ein Vorschlag existiert: Felder wie in der Web-App füllen (nächster freier Wochentag). */ + LaunchedEffect(selectedNewDateGroupId, newDateSlotSuggestion, showNewDateDialog) { + if (!showNewDateDialog) return@LaunchedEffect + val sug = newDateSlotSuggestion + if (selectedNewDateGroupId != null && sug != null) { + newDiaryDateStr = sug.date + newDiaryStart = sug.trainingStart + newDiaryEnd = sug.trainingEnd + } + } + + LaunchedEffect(showNewDateDialog, clubId) { + if (!showNewDateDialog) return@LaunchedEffect + newDateScheduleLoadError = null + newDateScheduleLoading = true + newDateScheduleGroups = runCatching { dependencies.membersManager.trainingScheduleGroups(clubId) } + .fold( + onSuccess = { it }, + onFailure = { t -> + newDateScheduleLoadError = t.message ?: t.javaClass.simpleName + emptyList() + }, + ) + newDateScheduleLoading = false + } LaunchedEffect(clubId) { onSelectedEntryId(null) @@ -1437,16 +1496,66 @@ private fun DiaryListScreen( } Button( onClick = { - val today = kotlin.runCatching { java.time.LocalDate.now().toString() }.getOrElse { "2026-01-01" } - val defaultStart = diaryState.dates.firstOrNull()?.trainingStart?.takeIf { it.isNotBlank() } ?: "17:30:00" - val defaultEnd = diaryState.dates.firstOrNull()?.trainingEnd?.takeIf { it.isNotBlank() } ?: "19:30:00" - dependencies.applicationScope.launch { - dependencies.diaryManager.createDate(clubId, today, defaultStart, defaultEnd) - dependencies.diaryManager.loadDates(clubId) - } + dependencies.diaryManager.clearError() + selectedNewDateGroupId = null + newDateGroupMenuExpanded = false + val tmpl = diaryState.dates.firstOrNull() + newDiaryDateStr = kotlin.runCatching { java.time.LocalDate.now().toString() }.getOrElse { "" } + newDiaryStart = diaryTimeForFormField(tmpl?.trainingStart).ifBlank { "17:30" } + newDiaryEnd = diaryTimeForFormField(tmpl?.trainingEnd).ifBlank { "19:30" } + showNewDateDialog = true }, modifier = Modifier.heightIn(min = TouchMinHeight), ) { Text(tr("mobile.new", "Neu")) } + OutlinedButton( + onClick = { + if (quickCreateBusy || diaryState.isLoading) return@OutlinedButton + newDateScope.launch { + quickCreateBusy = true + dependencies.diaryManager.clearError() + val lang = languageState.currentLanguageCode + val groups = runCatching { dependencies.membersManager.trainingScheduleGroups(clubId) }.fold( + onSuccess = { it }, + onFailure = { t -> + quickCreateBusy = false + Toast.makeText( + androidCtx, + t.message?.takeIf { it.isNotBlank() } + ?: MobileStrings.get(lang, "diary.quickCreateFailed", "Der Trainingstag konnte nicht angelegt werden."), + Toast.LENGTH_LONG, + ).show() + return@launch + }, + ) + val slot = findNextQuickDiarySlotAcrossGroups(groups, existingDiaryDatesNorm) + if (slot == null) { + quickCreateBusy = false + Toast.makeText( + androidCtx, + MobileStrings.get(lang, "diary.quickCreateNoSlot", "Kein freier Trainingstermin gefunden."), + Toast.LENGTH_LONG, + ).show() + return@launch + } + val id = dependencies.diaryManager.createDate( + clubId, + slot.date, + diaryTimeFieldToApi(slot.trainingStart), + diaryTimeFieldToApi(slot.trainingEnd), + ) + quickCreateBusy = false + if (id != null) { + onSelectedEntryId(id) + } else { + val err = dependencies.diaryManager.state.value.error + ?: MobileStrings.get(lang, "diary.quickCreateFailed", "Der Trainingstag konnte nicht angelegt werden.") + Toast.makeText(androidCtx, err, Toast.LENGTH_LONG).show() + } + } + }, + enabled = !quickCreateBusy && !diaryState.isLoading, + modifier = Modifier.heightIn(min = TouchMinHeight), + ) { Text(tr("diary.quickCreate", "Schnellanlegen")) } } if (diaryState.isLoading) LoadingInline() ErrorText(diaryState.error) @@ -1455,6 +1564,175 @@ private fun DiaryListScreen( } Spacer(modifier = Modifier.weight(1f)) } + + if (showNewDateDialog) { + AlertDialog( + onDismissRequest = { + if (!diaryState.isLoading) { + showNewDateDialog = false + selectedNewDateGroupId = null + newDateGroupMenuExpanded = false + } + }, + title = { Text(tr("diary.createNewDate", "Neuen Trainingstag anlegen")) }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + ) { + Text( + tr("diary.selectTrainingGroup", "Trainingsgruppe"), + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.75f), + ) + Spacer(modifier = Modifier.height(6.dp)) + if (newDateScheduleLoading) { + CircularProgressIndicator( + modifier = Modifier + .size(28.dp) + .padding(vertical = 4.dp), + strokeWidth = 3.dp, + ) + } + newDateScheduleLoadError?.let { err -> + Text( + err, + color = MaterialTheme.colors.error, + style = MaterialTheme.typography.caption, + modifier = Modifier.padding(bottom = 8.dp), + ) + } + Box(modifier = Modifier.fillMaxWidth()) { + OutlinedButton( + onClick = { newDateGroupMenuExpanded = true }, + enabled = !diaryState.isLoading && !newDateScheduleLoading, + modifier = Modifier.fillMaxWidth(), + ) { + val label = selectedNewDateGroupId?.let { gid -> + newDateScheduleGroups.find { it.id == gid }?.name?.takeIf { it.isNotBlank() } + } ?: tr("diary.selectTrainingGroupPlaceholder", "Gruppe wählen (optional)") + Text(label, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + DropdownMenu( + expanded = newDateGroupMenuExpanded, + onDismissRequest = { newDateGroupMenuExpanded = false }, + ) { + DropdownMenuItem( + onClick = { + selectedNewDateGroupId = null + newDateGroupMenuExpanded = false + }, + ) { + Text(tr("diary.noTrainingGroupManual", "Keine / manuell")) + } + newDateScheduleGroups.forEach { g -> + DropdownMenuItem( + onClick = { + selectedNewDateGroupId = g.id + newDateGroupMenuExpanded = false + }, + ) { + Text(g.name.ifBlank { "Gruppe ${g.id}" }) + } + } + } + } + newDateSlotSuggestion?.let { sug -> + Spacer(modifier = Modifier.height(8.dp)) + Text( + tr("diary.suggestion", "Vorschlag"), + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.SemiBold, + ) + Text( + "${tr("diary.nextAppointment", "Nächster Termin")}: ${formatDate(sug.date)} ${sug.trainingStart} – ${sug.trainingEnd}", + style = MaterialTheme.typography.body2, + modifier = Modifier.padding(top = 4.dp), + ) + TextButton( + onClick = { + newDiaryDateStr = sug.date + newDiaryStart = sug.trainingStart + newDiaryEnd = sug.trainingEnd + }, + enabled = !diaryState.isLoading, + ) { Text(tr("diary.applySuggestion", "Vorschlag übernehmen")) } + } + Divider(modifier = Modifier.padding(vertical = 12.dp)) + OutlinedTextField( + value = newDiaryDateStr, + onValueChange = { newDiaryDateStr = it }, + label = { Text(tr("diary.date", "Datum (YYYY-MM-DD)")) }, + singleLine = true, + enabled = !diaryState.isLoading, + modifier = Modifier.fillMaxWidth(), + ) + TextButton( + onClick = { + newDiaryDateStr = kotlin.runCatching { java.time.LocalDate.now().toString() }.getOrElse { newDiaryDateStr } + }, + enabled = !diaryState.isLoading, + ) { Text(tr("diary.today", "Heute")) } + OutlinedTextField( + value = newDiaryStart, + onValueChange = { newDiaryStart = it }, + label = { Text(tr("diary.trainingStart", "Trainingsbeginn (HH:mm)")) }, + singleLine = true, + enabled = !diaryState.isLoading, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = newDiaryEnd, + onValueChange = { newDiaryEnd = it }, + label = { Text(tr("diary.trainingEnd", "Trainingsende (HH:mm)")) }, + singleLine = true, + enabled = !diaryState.isLoading, + modifier = Modifier.fillMaxWidth(), + ) + diaryState.error?.let { err -> + Text( + err, + color = MaterialTheme.colors.error, + style = MaterialTheme.typography.caption, + modifier = Modifier.padding(top = 8.dp), + ) + } + } + }, + confirmButton = { + Button( + onClick = { + newDateScope.launch { + val id = dependencies.diaryManager.createDate( + clubId, + newDiaryDateStr.trim(), + diaryTimeFieldToApi(newDiaryStart), + diaryTimeFieldToApi(newDiaryEnd), + ) + if (id != null) { + showNewDateDialog = false + selectedNewDateGroupId = null + newDateGroupMenuExpanded = false + onSelectedEntryId(id) + } + } + }, + enabled = newDiaryDateStr.trim().length >= 8 && !diaryState.isLoading, + ) { Text(tr("diary.createDate", "Anlegen")) } + }, + dismissButton = { + TextButton( + onClick = { + showNewDateDialog = false + selectedNewDateGroupId = null + newDateGroupMenuExpanded = false + }, + enabled = !diaryState.isLoading, + ) { Text(tr("messages.cancel", "Abbrechen")) } + }, + ) + } } @Composable @@ -6260,6 +6538,95 @@ private fun formatTimeRange(start: String?, end: String?): String { return if (parts.isEmpty()) "Keine Zeiten" else parts.joinToString(" - ") } +/** Anzeige wie HTML time (HH:mm); API erwartet oft HH:mm:ss. */ +private fun diaryTimeForFormField(apiTime: String?): String { + val t = apiTime?.trim().orEmpty() + if (t.isEmpty()) return "" + return t.take(5) +} + +private fun diaryTimeFieldToApi(value: String): String? { + val t = value.trim() + if (t.isEmpty()) return null + return when { + t.matches(Regex("""\d{2}:\d{2}:\d{2}""")) -> t + t.matches(Regex("""\d{2}:\d{2}""")) -> "$t:00" + else -> t + } +} + +/** Nächster freier Trainingstag aus [TrainingTimeDto] (Wochentag 0=So … 6=Sa, wie Web/Kalender). */ +private data class NextDiarySlotSuggestion( + val date: String, + val trainingStart: String, + val trainingEnd: String, +) + +private fun localDateToJsWeekday(d: java.time.LocalDate): Int = + when (d.dayOfWeek) { + java.time.DayOfWeek.SUNDAY -> 0 + java.time.DayOfWeek.MONDAY -> 1 + java.time.DayOfWeek.TUESDAY -> 2 + java.time.DayOfWeek.WEDNESDAY -> 3 + java.time.DayOfWeek.THURSDAY -> 4 + java.time.DayOfWeek.FRIDAY -> 5 + java.time.DayOfWeek.SATURDAY -> 6 + } + +/** Nächster freier Kalendertag ab heute: erstes Datum ohne Tagebuch-Eintrag, dann erste passende Gruppe (Sortierung) mit Trainingszeit. */ +private fun findNextQuickDiarySlotAcrossGroups( + groups: List, + existingDiaryDatesYyyyMmDd: Set, +): NextDiarySlotSuggestion? { + val sortedGroups = groups.sortedWith(compareBy { it.sortOrder }.thenBy { it.id }) + val today = java.time.LocalDate.now() + for (offset in 0..366) { + val check = today.plusDays(offset.toLong()) + val wd = localDateToJsWeekday(check) + val norm = check.toString().take(10) + if (norm in existingDiaryDatesYyyyMmDd) continue + for (g in sortedGroups) { + val timesForDay = g.trainingTimes + .filter { it.weekday == wd && it.startTime.isNotBlank() } + .sortedWith(compareBy { it.startTime }.thenBy { it.id }) + val time = timesForDay.firstOrNull() ?: continue + return NextDiarySlotSuggestion( + date = norm, + trainingStart = time.startTime.trim().take(5), + trainingEnd = time.endTime.trim().take(5), + ) + } + } + return null +} + +private fun nextDiarySlotFromTrainingTimes( + trainingTimes: List, + existingDiaryDatesYyyyMmDd: Set, +): NextDiarySlotSuggestion? { + if (trainingTimes.isEmpty()) return null + val sorted = trainingTimes.sortedWith( + compareBy { it.weekday }.thenBy { it.startTime }.thenBy { it.id }, + ) + val today = java.time.LocalDate.now() + for (offset in 0..13) { + val check = today.plusDays(offset.toLong()) + val wd = localDateToJsWeekday(check) + val timesForDay = sorted.filter { it.weekday == wd } + if (timesForDay.isEmpty()) continue + val time = timesForDay.first() + val norm = check.toString().take(10) + if (norm !in existingDiaryDatesYyyyMmDd) { + return NextDiarySlotSuggestion( + date = norm, + trainingStart = time.startTime.trim().take(5), + trainingEnd = time.endTime.trim().take(5), + ) + } + } + return null +} + @Composable private fun tr(key: String, fallback: String): String = MobileStrings.get(LocalLanguageCode.current, key, fallback) diff --git a/mobile-app/gradle/libs.versions.toml b/mobile-app/gradle/libs.versions.toml index e7b7a7e3..9e4fb8c9 100644 --- a/mobile-app/gradle/libs.versions.toml +++ b/mobile-app/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.1.1" +agp = "9.2.1" android-compileSdk = "35" android-minSdk = "24" android-targetSdk = "35" @@ -14,9 +14,10 @@ androidx-test-junit = "1.1.5" compose = "1.6.1" compose-plugin = "1.10.3" junit = "4.13.2" -kotlin = "2.1.21" +kotlin = "2.2.10" ktor = "2.3.10" coroutines = "1.8.0" +kotlinx-serialization-json = "1.7.3" koin = "3.5.3" voyager = "1.0.0" socket-io = "2.1.0" @@ -25,6 +26,7 @@ ucrop = "2.2.11" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +junit = { module = "junit:junit", version.ref = "junit" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" } @@ -38,6 +40,7 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } diff --git a/mobile-app/gradle/wrapper/gradle-wrapper.properties b/mobile-app/gradle/wrapper/gradle-wrapper.properties index fd4d1f2b..4d7db4ce 100644 --- a/mobile-app/gradle/wrapper/gradle-wrapper.properties +++ b/mobile-app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://downloads.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://downloads.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/mobile-app/shared/build.gradle.kts b/mobile-app/shared/build.gradle.kts index 705d1a9c..89953118 100644 --- a/mobile-app/shared/build.gradle.kts +++ b/mobile-app/shared/build.gradle.kts @@ -43,6 +43,11 @@ kotlin { iosMain.dependencies { implementation(libs.ktor.client.darwin) } + androidUnitTest.dependencies { + implementation(libs.junit) + implementation(kotlin("test-junit")) + implementation(libs.kotlinx.serialization.json) + } } } diff --git a/mobile-app/shared/src/androidUnitTest/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/InternalTournamentSerializationTest.kt b/mobile-app/shared/src/androidUnitTest/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/InternalTournamentSerializationTest.kt new file mode 100644 index 00000000..4f7cce52 --- /dev/null +++ b/mobile-app/shared/src/androidUnitTest/kotlin/de/tsschulz/tt_tagebuch/shared/api/models/InternalTournamentSerializationTest.kt @@ -0,0 +1,43 @@ +package de.tsschulz.tt_tagebuch.shared.api.models + +import kotlinx.serialization.json.Json +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Phase 14: Regression gegen API-Inkonsistenzen (Boolean als 0/1/String) – siehe [FlexibleNullableBooleanSerializer]. + */ +class InternalTournamentSerializationTest { + + private val json = Json { + ignoreUnknownKeys = true + isLenient = true + } + + @Test + fun internalSummary_deserializesNumericBooleans() { + val raw = """{"id":1,"name":"X","allowsExternal":1,"isDoublesTournament":0}""" + val dto = json.decodeFromString(InternalTournamentSummaryDto.serializer(), raw) + assertEquals(1, dto.id) + assertEquals(true, dto.allowsExternal) + assertEquals(false, dto.isDoublesTournament) + } + + @Test + fun internalSummary_deserializesStringBooleans() { + val raw = """{"id":2,"allowsExternal":"true","isDoublesTournament":"no"}""" + val dto = json.decodeFromString(InternalTournamentSummaryDto.serializer(), raw) + assertTrue(dto.allowsExternal == true) + assertTrue(dto.isDoublesTournament == false) + } + + @Test + fun internalDetail_deserializesMixedBooleanForms() { + val raw = """{"id":3,"allowsExternal":0,"isDoublesTournament":"1"}""" + val dto = json.decodeFromString(InternalTournamentDetailDto.serializer(), raw) + assertFalse(dto.allowsExternal == true) + assertTrue(dto.isDoublesTournament == true) + } +} diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt index 47901647..59ddbda2 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt @@ -143,6 +143,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "Keine Termine in diesem Monat.", + "calendar.agendaTitle" to "Termine im Monat", + "calendar.cancelCancellation.confirm" to "Ausfall stornieren", + "calendar.cancelCancellation.message" to "Trainingsausfall „{title}“ am {date} wirklich stornieren?", + "calendar.cancelCancellation.title" to "Trainingsausfall stornieren", + "calendar.cancellation.date" to "Datum", + "calendar.cancellation.description" to "Blendet regelmäßige Trainingszeiten aus.", + "calendar.cancellation.fallbackTitle" to "Training fällt aus", + "calendar.cancellation.reasonPlaceholder" to "Grund (optional)", + "calendar.cancellation.saving" to "Speichern...", + "calendar.cancellation.submit" to "Eintragen", + "calendar.cancellation.subtitle" to "Trainingsausfall", + "calendar.cancellation.title" to "Training fällt aus", + "calendar.cancellation.trainingGroups" to "Trainingsgruppen", + "calendar.cancellation.untilOptional" to "Bis optional", + "calendar.customEvent.categoryPlaceholder" to "Kategorie (optional)", + "calendar.customEvent.description" to "Kreistage, Sitzungen, interne Meetings, ...", + "calendar.customEvent.saving" to "Speichern...", + "calendar.customEvent.submit" to "Anlegen", + "calendar.customEvent.subtitleFallback" to "Eigener Termin", + "calendar.customEvent.title" to "Eigene Termine", + "calendar.customEvent.titlePlaceholder" to "Titel", + "calendar.description" to "Trainingstage, Vereinsturniere und Punktspiele in einer Monatsansicht.", + "calendar.eventTitles.match" to "Punktspiel", + "calendar.eventTitles.officialTournament" to "Turnierteilnahme", + "calendar.eventTitles.tournament" to "Turnier", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Vereinskalender", + "calendar.holidayTypes.holiday" to "Feiertag", + "calendar.holidayTypes.schoolHoliday" to "Schulferien", + "calendar.legend.customEvent" to "Termin", + "calendar.legend.holiday" to "Feiertag", + "calendar.legend.match" to "Punktspiel", + "calendar.legend.officialTournament" to "Teilnahme", + "calendar.legend.schoolHoliday" to "Ferien", + "calendar.legend.tournament" to "Turnier", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Ausfall", + "calendar.loadError" to "Kalenderdaten konnten nicht geladen werden.", + "calendar.loading" to "Kalenderdaten werden geladen...", + "calendar.match.guest" to "Gast", + "calendar.match.home" to "Heim", + "calendar.noClub" to "Bitte zuerst einen Verein auswählen.", + "calendar.options.subtitle" to "Ausfälle & eigene Termine", + "calendar.options.title" to "Optionen", + "calendar.participants" to "{count} Teilnehmer", + "calendar.quickCancellation.confirm" to "Ausfall eintragen", + "calendar.quickCancellation.message" to "„{title}“ als Trainingsausfall markieren?", + "calendar.quickCancellation.noGroups" to "Es sind keine Trainingsgruppen mit Trainingszeiten vorhanden.", + "calendar.quickCancellation.title" to "Trainingsausfall", + "calendar.quickCancellation.trainingGroups" to "Betroffene Trainingsgruppen", + "calendar.quickCancellation.useWholeRange" to "Für den gesamten Zeitraum ({start} bis {end}) eintragen", + "calendar.recurringTrainingTime" to "Regelmäßige Trainingszeit", + "calendar.sources.customEvents" to "Eigene Termine", + "calendar.sources.holidays" to "Ferien/Feiertage", + "calendar.sources.matches" to "Punktspiele", + "calendar.sources.officialTournaments" to "Turnierteilnahmen", + "calendar.sources.tournaments" to "Turniere", + "calendar.sources.trainingCancellations" to "Trainingsausfälle", + "calendar.sources.trainingDays" to "Trainingstage", + "calendar.sources.trainingTimes" to "Trainingszeiten", + "calendar.sourceWarning" to "{source} konnte nicht geladen werden", + "calendar.starts" to "{count} Starts", + "calendar.title" to "Kalender", + "calendar.today" to "Heute", + "calendar.tournament.club" to "Vereinsturnier", + "calendar.tournament.open" to "Offenes Turnier", + "calendar.weekdays.friday" to "Fr", + "calendar.weekdays.monday" to "Mo", + "calendar.weekdays.saturday" to "Sa", + "calendar.weekdays.sunday" to "So", + "calendar.weekdays.thursday" to "Do", + "calendar.weekdays.tuesday" to "Di", + "calendar.weekdays.wednesday" to "Mi", "club.accessDenied" to "Zugriff uf de Verein verweigeret.", "club.accessRequested" to "Zugriff isch aagfragt worde.", "club.accessRequestFailed" to "Zugriffsafrag het nöd chönne gstellt werde.", @@ -473,6 +547,9 @@ object MobileStrings { "diary.planAddHint" to "Neue Plan-Elemente fügst du über die Aktionen oben hinzu.", "diary.planEmptyState" to "Im Trainingsplan ist noch nichts eingetragen.", "diary.quickAdd" to "+ Schnell hinzufügen", + "diary.quickCreate" to "Schnellanlegen", + "diary.quickCreateFailed" to "Der Trainingstag konnte nicht angelegt werden.", + "diary.quickCreateNoSlot" to "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).", "diary.searchParticipants" to "Teilnehmer suchen", "diary.selectGroup" to "Gruppe auswählen...", "diary.selectGroupAndActivity" to "Bitte wählen Sie eine Gruppe und geben Sie eine Aktivität ein.", @@ -1387,7 +1464,7 @@ object MobileStrings { "navigation.settings" to "Iistellige", "navigation.statistics" to "Trainings-Statistik", "navigation.teamManagement" to "Team-Verwaltig", - "navigation.tournamentParticipations" to "Turnierteilnahmen", + "navigation.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "navigation.tournaments" to "Turnier", "nuscoreAnalyzer.analysisComplete" to "✅ Analyse abgeschlossen: {count} Ressourcen gefunden", "nuscoreAnalyzer.analyze" to "🔍 nuscore analysieren", @@ -2321,7 +2398,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Weiblich", "tournaments.tournamentClassGenderOpen" to "Alli", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Qualifizierti Spieler i d Endrundi übernäh", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Brucht d Regle Zwüscherundi -> Endrundi und übernimmt d qualifizierti Spieler.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Qualifizierti Spieler direkt i d Endrundi übernäh", @@ -2379,6 +2456,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Mitglieder-Teilnahmen", "trainingStats.name" to "Name", "trainingStats.noParticipants" to "Keine Teilnehmer", + "trainingStats.panelAgeGroups" to "Anwesenheit nach Altersklasse", + "trainingStats.panelBestDay" to "Stärkster Trainingstag", + "trainingStats.panelGroupPerformance" to "Entwicklung pro Gruppe", + "trainingStats.panelMemberStructure" to "Mitgliederstruktur", + "trainingStats.panelMonthlyTrend" to "Monatlicher Verlauf", + "trainingStats.panelSummary" to "Kennzahlen (Filter)", + "trainingStats.panelWeekdayStats" to "Trainingstage nach Wochentag", "trainingStats.participants" to "Teilnehmer", "trainingStats.participations12Months" to "Teilnahmen (12 Monate)", "trainingStats.participations3Months" to "Teilnahmen (3 Monate)", @@ -2533,6 +2617,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "Keine Termine in diesem Monat.", + "calendar.agendaTitle" to "Termine im Monat", + "calendar.cancelCancellation.confirm" to "Ausfall stornieren", + "calendar.cancelCancellation.message" to "Trainingsausfall „{title}“ am {date} wirklich stornieren?", + "calendar.cancelCancellation.title" to "Trainingsausfall stornieren", + "calendar.cancellation.date" to "Datum", + "calendar.cancellation.description" to "Blendet regelmäßige Trainingszeiten aus.", + "calendar.cancellation.fallbackTitle" to "Training fällt aus", + "calendar.cancellation.reasonPlaceholder" to "Grund (optional)", + "calendar.cancellation.saving" to "Speichern...", + "calendar.cancellation.submit" to "Eintragen", + "calendar.cancellation.subtitle" to "Trainingsausfall", + "calendar.cancellation.title" to "Training fällt aus", + "calendar.cancellation.trainingGroups" to "Trainingsgruppen", + "calendar.cancellation.untilOptional" to "Bis optional", + "calendar.customEvent.categoryPlaceholder" to "Kategorie (optional)", + "calendar.customEvent.description" to "Kreistage, Sitzungen, interne Meetings, ...", + "calendar.customEvent.saving" to "Speichern...", + "calendar.customEvent.submit" to "Anlegen", + "calendar.customEvent.subtitleFallback" to "Eigener Termin", + "calendar.customEvent.title" to "Eigene Termine", + "calendar.customEvent.titlePlaceholder" to "Titel", + "calendar.description" to "Trainingstage, Vereinsturniere und Punktspiele in einer Monatsansicht.", + "calendar.eventTitles.match" to "Punktspiel", + "calendar.eventTitles.officialTournament" to "Turnierteilnahme", + "calendar.eventTitles.tournament" to "Turnier", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Vereinskalender", + "calendar.holidayTypes.holiday" to "Feiertag", + "calendar.holidayTypes.schoolHoliday" to "Schulferien", + "calendar.legend.customEvent" to "Termin", + "calendar.legend.holiday" to "Feiertag", + "calendar.legend.match" to "Punktspiel", + "calendar.legend.officialTournament" to "Teilnahme", + "calendar.legend.schoolHoliday" to "Ferien", + "calendar.legend.tournament" to "Turnier", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Ausfall", + "calendar.loadError" to "Kalenderdaten konnten nicht geladen werden.", + "calendar.loading" to "Kalenderdaten werden geladen...", + "calendar.match.guest" to "Gast", + "calendar.match.home" to "Heim", + "calendar.noClub" to "Bitte zuerst einen Verein auswählen.", + "calendar.options.subtitle" to "Ausfälle & eigene Termine", + "calendar.options.title" to "Optionen", + "calendar.participants" to "{count} Teilnehmer", + "calendar.quickCancellation.confirm" to "Ausfall eintragen", + "calendar.quickCancellation.message" to "„{title}“ als Trainingsausfall markieren?", + "calendar.quickCancellation.noGroups" to "Es sind keine Trainingsgruppen mit Trainingszeiten vorhanden.", + "calendar.quickCancellation.title" to "Trainingsausfall", + "calendar.quickCancellation.trainingGroups" to "Betroffene Trainingsgruppen", + "calendar.quickCancellation.useWholeRange" to "Für den gesamten Zeitraum ({start} bis {end}) eintragen", + "calendar.recurringTrainingTime" to "Regelmäßige Trainingszeit", + "calendar.sources.customEvents" to "Eigene Termine", + "calendar.sources.holidays" to "Ferien/Feiertage", + "calendar.sources.matches" to "Punktspiele", + "calendar.sources.officialTournaments" to "Turnierteilnahmen", + "calendar.sources.tournaments" to "Turniere", + "calendar.sources.trainingCancellations" to "Trainingsausfälle", + "calendar.sources.trainingDays" to "Trainingstage", + "calendar.sources.trainingTimes" to "Trainingszeiten", + "calendar.sourceWarning" to "{source} konnte nicht geladen werden", + "calendar.starts" to "{count} Starts", + "calendar.title" to "Kalender", + "calendar.today" to "Heute", + "calendar.tournament.club" to "Vereinsturnier", + "calendar.tournament.open" to "Offenes Turnier", + "calendar.weekdays.friday" to "Fr", + "calendar.weekdays.monday" to "Mo", + "calendar.weekdays.saturday" to "Sa", + "calendar.weekdays.sunday" to "So", + "calendar.weekdays.thursday" to "Do", + "calendar.weekdays.tuesday" to "Di", + "calendar.weekdays.wednesday" to "Mi", "club.accessDenied" to "Zugriff auf den Verein nicht gestattet.", "club.accessRequested" to "Zugriff wurde angefragt.", "club.accessRequestFailed" to "Zugriffsanfrage konnte nicht gestellt werden.", @@ -2863,6 +3021,9 @@ object MobileStrings { "diary.planAddHint" to "Neue Plan-Elemente fügst du über die Aktionen oben hinzu.", "diary.planEmptyState" to "Im Trainingsplan ist noch nichts eingetragen.", "diary.quickAdd" to "+ Schnell hinzufügen", + "diary.quickCreate" to "Schnellanlegen", + "diary.quickCreateFailed" to "Der Trainingstag konnte nicht angelegt werden.", + "diary.quickCreateNoSlot" to "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).", "diary.searchParticipants" to "Teilnehmer suchen", "diary.selectGroup" to "Gruppe auswählen...", "diary.selectGroupAndActivity" to "Bitte wählen Sie eine Gruppe und geben Sie eine Aktivität ein.", @@ -3783,7 +3944,7 @@ object MobileStrings { "navigation.settings" to "Einstellungen", "navigation.statistics" to "Trainings-Statistik", "navigation.teamManagement" to "Team-Verwaltung", - "navigation.tournamentParticipations" to "Turnierteilnahmen", + "navigation.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "navigation.tournaments" to "Turniere", "nuscoreAnalyzer.analysisComplete" to "✅ Analyse abgeschlossen: {count} Ressourcen gefunden", "nuscoreAnalyzer.analyze" to "🔍 nuscore analysieren", @@ -4724,7 +4885,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Weiblich", "tournaments.tournamentClassGenderOpen" to "Alle", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Qualifizierte Spieler in die Endrunde übernehmen", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Nutzt die Regeln Zwischenrunde -> Endrunde und übernimmt die qualifizierten Teilnehmer.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Qualifizierte Spieler direkt in die Endrunde übernehmen", @@ -4782,6 +4943,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Mitglieder-Teilnahmen", "trainingStats.name" to "Name", "trainingStats.noParticipants" to "Keine Teilnehmer", + "trainingStats.panelAgeGroups" to "Anwesenheit nach Altersklasse", + "trainingStats.panelBestDay" to "Stärkster Trainingstag", + "trainingStats.panelGroupPerformance" to "Entwicklung pro Gruppe", + "trainingStats.panelMemberStructure" to "Mitgliederstruktur", + "trainingStats.panelMonthlyTrend" to "Monatlicher Verlauf", + "trainingStats.panelSummary" to "Kennzahlen (Filter)", + "trainingStats.panelWeekdayStats" to "Trainingstage nach Wochentag", "trainingStats.participants" to "Teilnehmer", "trainingStats.participations12Months" to "Teilnahmen (12 Monate)", "trainingStats.participations3Months" to "Teilnahmen (3 Monate)", @@ -4936,6 +5104,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "Keine Termine in diesem Monat.", + "calendar.agendaTitle" to "Termine im Monat", + "calendar.cancelCancellation.confirm" to "Ausfall stornieren", + "calendar.cancelCancellation.message" to "Trainingsausfall „{title}“ am {date} wirklich stornieren?", + "calendar.cancelCancellation.title" to "Trainingsausfall stornieren", + "calendar.cancellation.date" to "Datum", + "calendar.cancellation.description" to "Blendet regelmäßige Trainingszeiten aus.", + "calendar.cancellation.fallbackTitle" to "Training fällt aus", + "calendar.cancellation.reasonPlaceholder" to "Grund (optional)", + "calendar.cancellation.saving" to "Speichern...", + "calendar.cancellation.submit" to "Eintragen", + "calendar.cancellation.subtitle" to "Trainingsausfall", + "calendar.cancellation.title" to "Training fällt aus", + "calendar.cancellation.trainingGroups" to "Trainingsgruppen", + "calendar.cancellation.untilOptional" to "Bis optional", + "calendar.customEvent.categoryPlaceholder" to "Kategorie (optional)", + "calendar.customEvent.description" to "Kreistage, Sitzungen, interne Meetings, ...", + "calendar.customEvent.saving" to "Speichern...", + "calendar.customEvent.submit" to "Anlegen", + "calendar.customEvent.subtitleFallback" to "Eigener Termin", + "calendar.customEvent.title" to "Eigene Termine", + "calendar.customEvent.titlePlaceholder" to "Titel", + "calendar.description" to "Trainingstage, Vereinsturniere und Punktspiele in einer Monatsansicht.", + "calendar.eventTitles.match" to "Punktspiel", + "calendar.eventTitles.officialTournament" to "Turnierteilnahme", + "calendar.eventTitles.tournament" to "Turnier", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Vereinskalender", + "calendar.holidayTypes.holiday" to "Feiertag", + "calendar.holidayTypes.schoolHoliday" to "Schulferien", + "calendar.legend.customEvent" to "Termin", + "calendar.legend.holiday" to "Feiertag", + "calendar.legend.match" to "Punktspiel", + "calendar.legend.officialTournament" to "Teilnahme", + "calendar.legend.schoolHoliday" to "Ferien", + "calendar.legend.tournament" to "Turnier", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Ausfall", + "calendar.loadError" to "Kalenderdaten konnten nicht geladen werden.", + "calendar.loading" to "Kalenderdaten werden geladen...", + "calendar.match.guest" to "Gast", + "calendar.match.home" to "Heim", + "calendar.noClub" to "Bitte zuerst einen Verein auswählen.", + "calendar.options.subtitle" to "Ausfälle & eigene Termine", + "calendar.options.title" to "Optionen", + "calendar.participants" to "{count} Teilnehmer", + "calendar.quickCancellation.confirm" to "Ausfall eintragen", + "calendar.quickCancellation.message" to "„{title}“ als Trainingsausfall markieren?", + "calendar.quickCancellation.noGroups" to "Es sind keine Trainingsgruppen mit Trainingszeiten vorhanden.", + "calendar.quickCancellation.title" to "Trainingsausfall", + "calendar.quickCancellation.trainingGroups" to "Betroffene Trainingsgruppen", + "calendar.quickCancellation.useWholeRange" to "Für den gesamten Zeitraum ({start} bis {end}) eintragen", + "calendar.recurringTrainingTime" to "Regelmäßige Trainingszeit", + "calendar.sources.customEvents" to "Eigene Termine", + "calendar.sources.holidays" to "Ferien/Feiertage", + "calendar.sources.matches" to "Punktspiele", + "calendar.sources.officialTournaments" to "Turnierteilnahmen", + "calendar.sources.tournaments" to "Turniere", + "calendar.sources.trainingCancellations" to "Trainingsausfälle", + "calendar.sources.trainingDays" to "Trainingstage", + "calendar.sources.trainingTimes" to "Trainingszeiten", + "calendar.sourceWarning" to "{source} konnte nicht geladen werden", + "calendar.starts" to "{count} Starts", + "calendar.title" to "Kalender", + "calendar.today" to "Heute", + "calendar.tournament.club" to "Vereinsturnier", + "calendar.tournament.open" to "Offenes Turnier", + "calendar.weekdays.friday" to "Fr", + "calendar.weekdays.monday" to "Mo", + "calendar.weekdays.saturday" to "Sa", + "calendar.weekdays.sunday" to "So", + "calendar.weekdays.thursday" to "Do", + "calendar.weekdays.tuesday" to "Di", + "calendar.weekdays.wednesday" to "Mi", "club.accessDenied" to "Zugriff auf den Verein nicht gestattet.", "club.accessRequested" to "Zugriff wurde angefragt.", "club.accessRequestFailed" to "Zugriffsanfrage konnte nicht gestellt werden.", @@ -5266,6 +5508,9 @@ object MobileStrings { "diary.planAddHint" to "Neue Plan-Elemente fügst du über die Aktionen oben hinzu.", "diary.planEmptyState" to "Im Trainingsplan ist noch nichts eingetragen.", "diary.quickAdd" to "+ Schnell hinzufügen", + "diary.quickCreate" to "Schnellanlegen", + "diary.quickCreateFailed" to "Der Trainingstag konnte nicht angelegt werden.", + "diary.quickCreateNoSlot" to "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).", "diary.searchParticipants" to "Teilnehmer suchen", "diary.selectGroup" to "Gruppe auswählen...", "diary.selectGroupAndActivity" to "Bitte wählen Sie eine Gruppe und geben Sie eine Aktivität ein.", @@ -6180,7 +6425,7 @@ object MobileStrings { "navigation.settings" to "Einstellungen", "navigation.statistics" to "Trainings-Statistik", "navigation.teamManagement" to "Team-Verwaltung", - "navigation.tournamentParticipations" to "Turnierteilnahmen", + "navigation.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "navigation.tournaments" to "Turniere", "nuscoreAnalyzer.analysisComplete" to "✅ Analyse abgeschlossen: {count} Ressourcen gefunden", "nuscoreAnalyzer.analyze" to "🔍 nuscore analysieren", @@ -7114,7 +7359,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Weiblich", "tournaments.tournamentClassGenderOpen" to "Alle", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Qualifizierte Spieler in die Endrunde übernehmen", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Nutzt die Regeln Zwischenrunde -> Endrunde und übernimmt die qualifizierten Teilnehmer.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Qualifizierte Spieler direkt in die Endrunde übernehmen", @@ -7172,6 +7417,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Mitglieder-Teilnahmen", "trainingStats.name" to "Name", "trainingStats.noParticipants" to "Keine Teilnehmer", + "trainingStats.panelAgeGroups" to "Anwesenheit nach Altersklasse", + "trainingStats.panelBestDay" to "Stärkster Trainingstag", + "trainingStats.panelGroupPerformance" to "Entwicklung pro Gruppe", + "trainingStats.panelMemberStructure" to "Mitgliederstruktur", + "trainingStats.panelMonthlyTrend" to "Monatlicher Verlauf", + "trainingStats.panelSummary" to "Kennzahlen (Filter)", + "trainingStats.panelWeekdayStats" to "Trainingstage nach Wochentag", "trainingStats.participants" to "Teilnehmer", "trainingStats.participations12Months" to "Teilnahmen (12 Monate)", "trainingStats.participations3Months" to "Teilnahmen (3 Monate)", @@ -7326,6 +7578,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "No events this month.", + "calendar.agendaTitle" to "Events this month", + "calendar.cancelCancellation.confirm" to "Cancel cancellation", + "calendar.cancelCancellation.message" to "Really cancel the training cancellation “{title}” on {date}?", + "calendar.cancelCancellation.title" to "Cancel training cancellation", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Club calendar", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Loading calendar data...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Please select a club first.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Calendar", + "calendar.today" to "Today", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Fri", + "calendar.weekdays.monday" to "Mon", + "calendar.weekdays.saturday" to "Sat", + "calendar.weekdays.sunday" to "Sun", + "calendar.weekdays.thursday" to "Thu", + "calendar.weekdays.tuesday" to "Tue", + "calendar.weekdays.wednesday" to "Wed", "club.accessDenied" to "Access to the club was denied.", "club.accessRequested" to "Access has been requested.", "club.accessRequestFailed" to "Access request could not be submitted.", @@ -7656,6 +7982,9 @@ object MobileStrings { "diary.planAddHint" to "Add new plan items using the actions above.", "diary.planEmptyState" to "Nothing has been entered in the training plan yet.", "diary.quickAdd" to "+ Quick add", + "diary.quickCreate" to "Quick add", + "diary.quickCreateFailed" to "Could not create the training day entry.", + "diary.quickCreateNoSlot" to "No free training slot found (per group schedule, one year ahead).", "diary.searchParticipants" to "Search participants", "diary.selectGroup" to "Select group...", "diary.selectGroupAndActivity" to "Please select a group and enter an activity.", @@ -8570,7 +8899,7 @@ object MobileStrings { "navigation.settings" to "Settings", "navigation.statistics" to "Training Statistics", "navigation.teamManagement" to "Team Management", - "navigation.tournamentParticipations" to "Tournament Participations", + "navigation.tournamentParticipations" to "Official tournaments & participations", "navigation.tournaments" to "Tournaments", "nuscoreAnalyzer.analysisComplete" to "✅ Analyse abgeschlossen: {count} Ressourcen gefunden", "nuscoreAnalyzer.analyze" to "🔍 nuscore analysieren", @@ -9504,7 +9833,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Female", "tournaments.tournamentClassGenderOpen" to "Open (all)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -9562,6 +9891,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Member participations", "trainingStats.name" to "Name", "trainingStats.noParticipants" to "No participants", + "trainingStats.panelAgeGroups" to "Attendance by age class", + "trainingStats.panelBestDay" to "Busiest training day", + "trainingStats.panelGroupPerformance" to "Progress by group", + "trainingStats.panelMemberStructure" to "Member structure", + "trainingStats.panelMonthlyTrend" to "Monthly trend", + "trainingStats.panelSummary" to "Key figures (filtered)", + "trainingStats.panelWeekdayStats" to "Training days by weekday", "trainingStats.participants" to "Participants", "trainingStats.participations12Months" to "Participations (12 months)", "trainingStats.participations3Months" to "Participations (3 months)", @@ -9716,6 +10052,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "No events this month.", + "calendar.agendaTitle" to "Events this month", + "calendar.cancelCancellation.confirm" to "Cancel cancellation", + "calendar.cancelCancellation.message" to "Really cancel the training cancellation “{title}” on {date}?", + "calendar.cancelCancellation.title" to "Cancel training cancellation", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Club calendar", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Loading calendar data...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Please select a club first.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Calendar", + "calendar.today" to "Today", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Fri", + "calendar.weekdays.monday" to "Mon", + "calendar.weekdays.saturday" to "Sat", + "calendar.weekdays.sunday" to "Sun", + "calendar.weekdays.thursday" to "Thu", + "calendar.weekdays.tuesday" to "Tue", + "calendar.weekdays.wednesday" to "Wed", "club.accessDenied" to "Access to the club was denied.", "club.accessRequested" to "Access has been requested.", "club.accessRequestFailed" to "Access request could not be submitted.", @@ -10046,6 +10456,9 @@ object MobileStrings { "diary.planAddHint" to "Add new plan items using the actions above.", "diary.planEmptyState" to "Nothing has been entered in the training plan yet.", "diary.quickAdd" to "+ Quick add", + "diary.quickCreate" to "Quick add", + "diary.quickCreateFailed" to "Could not create the training day entry.", + "diary.quickCreateNoSlot" to "No free training slot found (per group schedule, one year ahead).", "diary.searchParticipants" to "Search participants", "diary.selectGroup" to "Select group...", "diary.selectGroupAndActivity" to "Please select a group and enter an activity.", @@ -10960,7 +11373,7 @@ object MobileStrings { "navigation.settings" to "Settings", "navigation.statistics" to "Training Statistics", "navigation.teamManagement" to "Team Management", - "navigation.tournamentParticipations" to "Tournament Participations", + "navigation.tournamentParticipations" to "Official tournaments & participations", "navigation.tournaments" to "Tournaments", "nuscoreAnalyzer.analysisComplete" to "✅ Analyse abgeschlossen: {count} Ressourcen gefunden", "nuscoreAnalyzer.analyze" to "🔍 nuscore analysieren", @@ -11894,7 +12307,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Female", "tournaments.tournamentClassGenderOpen" to "Open (all)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -11952,6 +12365,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Member participations", "trainingStats.name" to "Name", "trainingStats.noParticipants" to "No participants", + "trainingStats.panelAgeGroups" to "Attendance by age class", + "trainingStats.panelBestDay" to "Busiest training day", + "trainingStats.panelGroupPerformance" to "Progress by group", + "trainingStats.panelMemberStructure" to "Member structure", + "trainingStats.panelMonthlyTrend" to "Monthly trend", + "trainingStats.panelSummary" to "Key figures (filtered)", + "trainingStats.panelWeekdayStats" to "Training days by weekday", "trainingStats.participants" to "Participants", "trainingStats.participations12Months" to "Participations (12 months)", "trainingStats.participations3Months" to "Participations (3 months)", @@ -12106,6 +12526,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "No events this month.", + "calendar.agendaTitle" to "Events this month", + "calendar.cancelCancellation.confirm" to "Cancel cancellation", + "calendar.cancelCancellation.message" to "Really cancel the training cancellation “{title}” on {date}?", + "calendar.cancelCancellation.title" to "Cancel training cancellation", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Club calendar", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Loading calendar data...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Please select a club first.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Calendar", + "calendar.today" to "Today", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Fri", + "calendar.weekdays.monday" to "Mon", + "calendar.weekdays.saturday" to "Sat", + "calendar.weekdays.sunday" to "Sun", + "calendar.weekdays.thursday" to "Thu", + "calendar.weekdays.tuesday" to "Tue", + "calendar.weekdays.wednesday" to "Wed", "club.accessDenied" to "Access to the club was denied.", "club.accessRequested" to "Access has been requested.", "club.accessRequestFailed" to "Access request could not be submitted.", @@ -12436,6 +12930,9 @@ object MobileStrings { "diary.planAddHint" to "Add new plan items using the actions above.", "diary.planEmptyState" to "Nothing has been entered in the training plan yet.", "diary.quickAdd" to "+ Quick add", + "diary.quickCreate" to "Quick add", + "diary.quickCreateFailed" to "Could not create the training day entry.", + "diary.quickCreateNoSlot" to "No free training slot found (per group schedule, one year ahead).", "diary.searchParticipants" to "Search participants", "diary.selectGroup" to "Select group...", "diary.selectGroupAndActivity" to "Please select a group and enter an activity.", @@ -13350,7 +13847,7 @@ object MobileStrings { "navigation.settings" to "Settings", "navigation.statistics" to "Training Statistics", "navigation.teamManagement" to "Team Management", - "navigation.tournamentParticipations" to "Tournament Participations", + "navigation.tournamentParticipations" to "Official tournaments & participations", "navigation.tournaments" to "Tournaments", "nuscoreAnalyzer.analysisComplete" to "✅ Analyse abgeschlossen: {count} Ressourcen gefunden", "nuscoreAnalyzer.analyze" to "🔍 nuscore analysieren", @@ -14284,7 +14781,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Female", "tournaments.tournamentClassGenderOpen" to "Open (all)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -14342,6 +14839,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Member participations", "trainingStats.name" to "Name", "trainingStats.noParticipants" to "No participants", + "trainingStats.panelAgeGroups" to "Attendance by age class", + "trainingStats.panelBestDay" to "Busiest training day", + "trainingStats.panelGroupPerformance" to "Progress by group", + "trainingStats.panelMemberStructure" to "Member structure", + "trainingStats.panelMonthlyTrend" to "Monthly trend", + "trainingStats.panelSummary" to "Key figures (filtered)", + "trainingStats.panelWeekdayStats" to "Training days by weekday", "trainingStats.participants" to "Participants", "trainingStats.participations12Months" to "Participations (12 months)", "trainingStats.participations3Months" to "Participations (3 months)", @@ -14496,6 +15000,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "No hay eventos este mes.", + "calendar.agendaTitle" to "Eventos del mes", + "calendar.cancelCancellation.confirm" to "Cancelar ausencia", + "calendar.cancelCancellation.message" to "¿Cancelar realmente la ausencia “{title}” el {date}?", + "calendar.cancelCancellation.title" to "Cancelar ausencia de entrenamiento", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Calendario del club", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Cargando datos del calendario...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Seleccione primero un club.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Calendario", + "calendar.today" to "Hoy", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Vie", + "calendar.weekdays.monday" to "Lun", + "calendar.weekdays.saturday" to "Sáb", + "calendar.weekdays.sunday" to "Dom", + "calendar.weekdays.thursday" to "Jue", + "calendar.weekdays.tuesday" to "Mar", + "calendar.weekdays.wednesday" to "Mié", "club.accessDenied" to "Acceso al club denegado.", "club.accessRequested" to "Se ha solicitado el acceso.", "club.accessRequestFailed" to "No se pudo enviar la solicitud de acceso.", @@ -14826,6 +15404,9 @@ object MobileStrings { "diary.planAddHint" to "Añade nuevos elementos del plan con las acciones superiores.", "diary.planEmptyState" to "Todavía no hay nada registrado en el plan de entrenamiento.", "diary.quickAdd" to "+ Añadir rápido", + "diary.quickCreate" to "Alta rápida", + "diary.quickCreateFailed" to "No se pudo crear el día de entrenamiento.", + "diary.quickCreateNoSlot" to "No se encontró un día de entrenamiento libre (según horarios de grupos, un año adelante).", "diary.searchParticipants" to "Buscar participantes", "diary.selectGroup" to "Seleccionar grupo...", "diary.selectGroupAndActivity" to "Selecciona un grupo e introduce una actividad.", @@ -16674,7 +17255,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Femenino", "tournaments.tournamentClassGenderOpen" to "Abierta (todos)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -16732,6 +17313,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Participaciones de los miembros", "trainingStats.name" to "Nombre", "trainingStats.noParticipants" to "Sin participantes", + "trainingStats.panelAgeGroups" to "Asistencia por categoría de edad", + "trainingStats.panelBestDay" to "Día de entreno con más asistencia", + "trainingStats.panelGroupPerformance" to "Evolución por grupo", + "trainingStats.panelMemberStructure" to "Estructura de miembros", + "trainingStats.panelMonthlyTrend" to "Tendencia mensual", + "trainingStats.panelSummary" to "Cifras clave (filtro)", + "trainingStats.panelWeekdayStats" to "Días de entreno por día de la semana", "trainingStats.participants" to "Participantes", "trainingStats.participations12Months" to "Participaciones (12 meses)", "trainingStats.participations3Months" to "Participaciones (3 meses)", @@ -16886,6 +17474,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "Walang event ngayong buwan.", + "calendar.agendaTitle" to "Mga event ngayong buwan", + "calendar.cancelCancellation.confirm" to "Bawiin ang pagkansela", + "calendar.cancelCancellation.message" to "Bawiin talaga ang pagkansela ng training “{title}” sa {date}?", + "calendar.cancelCancellation.title" to "Bawiin ang pagkansela ng training", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Kalendaryo ng club", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Nilo-load ang datos ng kalendaryo...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Pumili muna ng club.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Kalendaryo", + "calendar.today" to "Ngayon", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Biy", + "calendar.weekdays.monday" to "Lun", + "calendar.weekdays.saturday" to "Sab", + "calendar.weekdays.sunday" to "Lin", + "calendar.weekdays.thursday" to "Huw", + "calendar.weekdays.tuesday" to "Mar", + "calendar.weekdays.wednesday" to "Miy", "club.accessDenied" to "Hindi pinapayagan ang access sa club na ito.", "club.accessRequested" to "Naipadala na ang hiling sa access.", "club.accessRequestFailed" to "Hindi naipadala ang hiling sa access.", @@ -17216,6 +17878,9 @@ object MobileStrings { "diary.planAddHint" to "Magdagdag ng bagong item sa plano gamit ang mga aksyon sa itaas.", "diary.planEmptyState" to "Wala pang nakalagay sa plano ng pagsasanay.", "diary.quickAdd" to "+ Quick add", + "diary.quickCreate" to "Mabilis na idagdag", + "diary.quickCreateFailed" to "Hindi malikha ang araw ng training.", + "diary.quickCreateNoSlot" to "Walang nahanap na libreng araw ng training (ayon sa iskedyul ng grupo, isang taon pasulong).", "diary.searchParticipants" to "Maghanap ng kalahok", "diary.selectGroup" to "Pumili ng grupo...", "diary.selectGroupAndActivity" to "Mangyaring pumili ng grupo at maglagay ng aktibidad.", @@ -19064,7 +19729,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Babae", "tournaments.tournamentClassGenderOpen" to "Bukas (lahat)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -19122,6 +19787,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Paglahok ng miyembro", "trainingStats.name" to "Pangalan", "trainingStats.noParticipants" to "Walang kalahok", + "trainingStats.panelAgeGroups" to "Dalo ayon sa edad", + "trainingStats.panelBestDay" to "Pinakamataong araw ng ensayo", + "trainingStats.panelGroupPerformance" to "Pag-unlad ayon sa grupo", + "trainingStats.panelMemberStructure" to "Istraktura ng miyembro", + "trainingStats.panelMonthlyTrend" to "Buwanang uso", + "trainingStats.panelSummary" to "Mahahalagang numero (salain)", + "trainingStats.panelWeekdayStats" to "Mga araw ng ensayo ayon sa araw ng linggo", "trainingStats.participants" to "Mga kalahok", "trainingStats.participations12Months" to "Paglahok (12 buwan)", "trainingStats.participations3Months" to "Paglahok (3 buwan)", @@ -19276,6 +19948,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "Aucun événement ce mois-ci.", + "calendar.agendaTitle" to "Événements du mois", + "calendar.cancelCancellation.confirm" to "Annuler l’annulation", + "calendar.cancelCancellation.message" to "Vraiment annuler l’annulation « {title} » le {date} ?", + "calendar.cancelCancellation.title" to "Annuler l’annulation de l’entraînement", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Calendrier du club", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Chargement des données du calendrier...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Veuillez d’abord sélectionner un club.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Calendrier", + "calendar.today" to "Aujourd’hui", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Ven", + "calendar.weekdays.monday" to "Lun", + "calendar.weekdays.saturday" to "Sam", + "calendar.weekdays.sunday" to "Dim", + "calendar.weekdays.thursday" to "Jeu", + "calendar.weekdays.tuesday" to "Mar", + "calendar.weekdays.wednesday" to "Mer", "club.accessDenied" to "Accès au club refusé.", "club.accessRequested" to "L'accès a été demandé.", "club.accessRequestFailed" to "La demande d'accès n'a pas pu être envoyée.", @@ -19606,6 +20352,9 @@ object MobileStrings { "diary.planAddHint" to "Ajoutez de nouveaux éléments du plan avec les actions ci-dessus.", "diary.planEmptyState" to "Aucun élément n'a encore été saisi dans le plan d'entraînement.", "diary.quickAdd" to "+ Ajout rapide", + "diary.quickCreate" to "Création rapide", + "diary.quickCreateFailed" to "Impossible de créer la journée d’entraînement.", + "diary.quickCreateNoSlot" to "Aucun créneau d’entraînement libre trouvé (selon les horaires des groupes, sur un an).", "diary.searchParticipants" to "Rechercher des participants", "diary.selectGroup" to "Sélectionner un groupe...", "diary.selectGroupAndActivity" to "Veuillez sélectionner un groupe et saisir une activité.", @@ -21454,7 +22203,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Féminin", "tournaments.tournamentClassGenderOpen" to "Tous", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -21512,6 +22261,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Participations des membres", "trainingStats.name" to "Nom", "trainingStats.noParticipants" to "Aucun participant", + "trainingStats.panelAgeGroups" to "Présence par catégorie d'âge", + "trainingStats.panelBestDay" to "Jour d'entraînement le plus fréquenté", + "trainingStats.panelGroupPerformance" to "Progression par groupe", + "trainingStats.panelMemberStructure" to "Structure des membres", + "trainingStats.panelMonthlyTrend" to "Évolution mensuelle", + "trainingStats.panelSummary" to "Chiffres clés (filtre)", + "trainingStats.panelWeekdayStats" to "Jours d'entraînement par jour de la semaine", "trainingStats.participants" to "Participants", "trainingStats.participations12Months" to "Participations (12 mois)", "trainingStats.participations3Months" to "Participations (3 mois)", @@ -21666,6 +22422,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "Nessun evento questo mese.", + "calendar.agendaTitle" to "Eventi del mese", + "calendar.cancelCancellation.confirm" to "Annulla cancellazione", + "calendar.cancelCancellation.message" to "Annullare davvero la cancellazione “{title}” del {date}?", + "calendar.cancelCancellation.title" to "Annulla cancellazione allenamento", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Calendario del club", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Caricamento dati calendario...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Seleziona prima un club.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Calendario", + "calendar.today" to "Oggi", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Ven", + "calendar.weekdays.monday" to "Lun", + "calendar.weekdays.saturday" to "Sab", + "calendar.weekdays.sunday" to "Dom", + "calendar.weekdays.thursday" to "Gio", + "calendar.weekdays.tuesday" to "Mar", + "calendar.weekdays.wednesday" to "Mer", "club.accessDenied" to "Accesso al club negato.", "club.accessRequested" to "Accesso richiesto.", "club.accessRequestFailed" to "Impossibile inviare la richiesta di accesso.", @@ -21996,6 +22826,9 @@ object MobileStrings { "diary.planAddHint" to "Aggiungi nuovi elementi del piano usando le azioni sopra.", "diary.planEmptyState" to "Non è ancora stato inserito nulla nel piano di allenamento.", "diary.quickAdd" to "+ Aggiunta rapida", + "diary.quickCreate" to "Creazione rapida", + "diary.quickCreateFailed" to "Impossibile creare la giornata di allenamento.", + "diary.quickCreateNoSlot" to "Nessun giorno di allenamento libero trovato (in base agli orari dei gruppi, entro un anno).", "diary.searchParticipants" to "Cerca partecipanti", "diary.selectGroup" to "Seleziona gruppo...", "diary.selectGroupAndActivity" to "Seleziona un gruppo e inserisci un’attività.", @@ -23844,7 +24677,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Femminile", "tournaments.tournamentClassGenderOpen" to "Aperta (tutti)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -23902,6 +24735,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Partecipazioni dei membri", "trainingStats.name" to "Nome", "trainingStats.noParticipants" to "Nessun partecipante", + "trainingStats.panelAgeGroups" to "Presenze per fascia d'età", + "trainingStats.panelBestDay" to "Giorno di allenamento più frequentato", + "trainingStats.panelGroupPerformance" to "Andamento per gruppo", + "trainingStats.panelMemberStructure" to "Struttura membri", + "trainingStats.panelMonthlyTrend" to "Andamento mensile", + "trainingStats.panelSummary" to "Cifre chiave (filtro)", + "trainingStats.panelWeekdayStats" to "Giorni di allenamento per giorno", "trainingStats.participants" to "Partecipanti", "trainingStats.participations12Months" to "Partecipazioni (12 mesi)", "trainingStats.participations3Months" to "Partecipazioni (3 mesi)", @@ -24056,6 +24896,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "今月の予定はありません。", + "calendar.agendaTitle" to "今月の予定", + "calendar.cancelCancellation.confirm" to "中止を取り消す", + "calendar.cancelCancellation.message" to "{date} の「{title}」の練習中止を本当に取り消しますか?", + "calendar.cancelCancellation.title" to "練習中止を取り消す", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "クラブカレンダー", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "カレンダーデータを読み込んでいます...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "最初にクラブを選択してください。", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "カレンダー", + "calendar.today" to "今日", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "金", + "calendar.weekdays.monday" to "月", + "calendar.weekdays.saturday" to "土", + "calendar.weekdays.sunday" to "日", + "calendar.weekdays.thursday" to "木", + "calendar.weekdays.tuesday" to "火", + "calendar.weekdays.wednesday" to "水", "club.accessDenied" to "このクラブへのアクセスは許可されていません。", "club.accessRequested" to "アクセスを申請しました。", "club.accessRequestFailed" to "アクセス申請を送信できませんでした。", @@ -24386,6 +25300,9 @@ object MobileStrings { "diary.planAddHint" to "新しい計画項目は上の操作から追加します。", "diary.planEmptyState" to "練習計画にはまだ何も登録されていません。", "diary.quickAdd" to "+ クイック追加", + "diary.quickCreate" to "クイック作成", + "diary.quickCreateFailed" to "練習日を作成できませんでした。", + "diary.quickCreateNoSlot" to "空きの練習日が見つかりません(グループのスケジュールに基づき、1年先まで)。", "diary.searchParticipants" to "参加者を検索", "diary.selectGroup" to "グループを選択...", "diary.selectGroupAndActivity" to "グループを選択し、アクティビティを入力してください。", @@ -26234,7 +27151,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "女子", "tournaments.tournamentClassGenderOpen" to "オープン(全員)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -26292,6 +27209,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "メンバー参加数", "trainingStats.name" to "名前", "trainingStats.noParticipants" to "参加者なし", + "trainingStats.panelAgeGroups" to "年齢クラス別の出席", + "trainingStats.panelBestDay" to "最も参加が多い練習日", + "trainingStats.panelGroupPerformance" to "グループ別の推移", + "trainingStats.panelMemberStructure" to "メンバー構成", + "trainingStats.panelMonthlyTrend" to "月次の推移", + "trainingStats.panelSummary" to "主要指標(フィルター)", + "trainingStats.panelWeekdayStats" to "曜日別の練習日", "trainingStats.participants" to "参加者", "trainingStats.participations12Months" to "参加回数(12 か月)", "trainingStats.participations3Months" to "参加回数(3 か月)", @@ -26446,6 +27370,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "Brak wydarzeń w tym miesiącu.", + "calendar.agendaTitle" to "Wydarzenia w tym miesiącu", + "calendar.cancelCancellation.confirm" to "Cofnij odwołanie", + "calendar.cancelCancellation.message" to "Na pewno cofnąć odwołanie „{title}” w dniu {date}?", + "calendar.cancelCancellation.title" to "Cofnij odwołanie treningu", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Kalendarz klubu", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Ładowanie danych kalendarza...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Najpierw wybierz klub.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Kalendarz", + "calendar.today" to "Dzisiaj", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Pt", + "calendar.weekdays.monday" to "Pon", + "calendar.weekdays.saturday" to "Sob", + "calendar.weekdays.sunday" to "Nd", + "calendar.weekdays.thursday" to "Czw", + "calendar.weekdays.tuesday" to "Wt", + "calendar.weekdays.wednesday" to "Śr", "club.accessDenied" to "Dostęp do klubu został odrzucony.", "club.accessRequested" to "Poproszono o dostęp.", "club.accessRequestFailed" to "Nie udało się wysłać prośby o dostęp.", @@ -26776,6 +27774,9 @@ object MobileStrings { "diary.planAddHint" to "Dodawaj nowe elementy planu za pomocą powyższych akcji.", "diary.planEmptyState" to "W planie treningowym nic jeszcze nie wpisano.", "diary.quickAdd" to "+ Szybkie dodawanie", + "diary.quickCreate" to "Szybkie dodawanie", + "diary.quickCreateFailed" to "Nie udało się utworzyć dnia treningowego.", + "diary.quickCreateNoSlot" to "Nie znaleziono wolnego terminu treningu (wg harmonogramów grup, rok do przodu).", "diary.searchParticipants" to "Szukaj uczestników", "diary.selectGroup" to "Wybierz grupę...", "diary.selectGroupAndActivity" to "Wybierz grupę i wpisz aktywność.", @@ -28624,7 +29625,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Kobiety", "tournaments.tournamentClassGenderOpen" to "Otwarta (wszyscy)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -28682,6 +29683,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Udziały członków", "trainingStats.name" to "Nazwa", "trainingStats.noParticipants" to "Brak uczestników", + "trainingStats.panelAgeGroups" to "Frekwencja wg kategorii wiekowej", + "trainingStats.panelBestDay" to "Najbardziej uczęszczany trening", + "trainingStats.panelGroupPerformance" to "Rozwój wg grup", + "trainingStats.panelMemberStructure" to "Struktura członków", + "trainingStats.panelMonthlyTrend" to "Trend miesięczny", + "trainingStats.panelSummary" to "Kluczowe liczby (filtr)", + "trainingStats.panelWeekdayStats" to "Dni treningowe wg dnia tygodnia", "trainingStats.participants" to "Uczestnicy", "trainingStats.participations12Months" to "Udziały (12 miesięcy)", "trainingStats.participations3Months" to "Udziały (3 miesiące)", @@ -28836,6 +29844,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "ไม่มีกิจกรรมในเดือนนี้", + "calendar.agendaTitle" to "กิจกรรมในเดือนนี้", + "calendar.cancelCancellation.confirm" to "ยกเลิกการงดซ้อม", + "calendar.cancelCancellation.message" to "ต้องการยกเลิกการงดซ้อม “{title}” วันที่ {date} หรือไม่?", + "calendar.cancelCancellation.title" to "ยกเลิกการงดซ้อม", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "ปฏิทินสโมสร", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "กำลังโหลดข้อมูลปฏิทิน...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "โปรดเลือกสโมสรก่อน", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "ปฏิทิน", + "calendar.today" to "วันนี้", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "ศ.", + "calendar.weekdays.monday" to "จ.", + "calendar.weekdays.saturday" to "ส.", + "calendar.weekdays.sunday" to "อา.", + "calendar.weekdays.thursday" to "พฤ.", + "calendar.weekdays.tuesday" to "อ.", + "calendar.weekdays.wednesday" to "พ.", "club.accessDenied" to "ไม่ได้รับอนุญาตให้เข้าถึงสโมสรนี้", "club.accessRequested" to "ส่งคำขอเข้าถึงแล้ว", "club.accessRequestFailed" to "ไม่สามารถส่งคำขอเข้าถึงได้", @@ -29166,6 +30248,9 @@ object MobileStrings { "diary.planAddHint" to "คุณสามารถเพิ่มรายการใหม่ได้จากปุ่มด้านบน", "diary.planEmptyState" to "ยังไม่มีรายการในแผนการฝึกซ้อม", "diary.quickAdd" to "+ เพิ่มด่วน", + "diary.quickCreate" to "เพิ่มด่วน", + "diary.quickCreateFailed" to "ไม่สามารถสร้างวันฝึกได้", + "diary.quickCreateNoSlot" to "ไม่พบวันฝึกที่ว่าง (ตามตารางกลุ่ม ล่วงหน้าหนึ่งปี)", "diary.searchParticipants" to "ค้นหาผู้เข้าร่วม", "diary.selectGroup" to "เลือกกลุ่ม...", "diary.selectGroupAndActivity" to "กรุณาเลือกกลุ่มและกรอกกิจกรรม", @@ -31014,7 +32099,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "หญิง", "tournaments.tournamentClassGenderOpen" to "เปิด (ทุกคน)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -31072,6 +32157,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "การเข้าร่วมของสมาชิก", "trainingStats.name" to "ชื่อ", "trainingStats.noParticipants" to "ไม่มีผู้เข้าร่วม", + "trainingStats.panelAgeGroups" to "การเข้าร่วมตามกลุ่มอายุ", + "trainingStats.panelBestDay" to "วันซ้อมที่คึกคักที่สุด", + "trainingStats.panelGroupPerformance" to "พัฒนาการตามกลุ่ม", + "trainingStats.panelMemberStructure" to "โครงสร้างสมาชิก", + "trainingStats.panelMonthlyTrend" to "แนวโน้มรายเดือน", + "trainingStats.panelSummary" to "ตัวเลขสำคัญ (กรอง)", + "trainingStats.panelWeekdayStats" to "วันซ้อมตามวันในสัปดาห์", "trainingStats.participants" to "ผู้เข้าร่วม", "trainingStats.participations12Months" to "การเข้าร่วม (12 เดือน)", "trainingStats.participations3Months" to "การเข้าร่วม (3 เดือน)", @@ -31226,6 +32318,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "Walang event ngayong buwan.", + "calendar.agendaTitle" to "Mga event ngayong buwan", + "calendar.cancelCancellation.confirm" to "Bawiin ang pagkansela", + "calendar.cancelCancellation.message" to "Bawiin talaga ang pagkansela ng training “{title}” sa {date}?", + "calendar.cancelCancellation.title" to "Bawiin ang pagkansela ng training", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "Kalendaryo ng club", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "Nilo-load ang datos ng kalendaryo...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "Pumili muna ng club.", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "Kalendaryo", + "calendar.today" to "Ngayon", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "Biy", + "calendar.weekdays.monday" to "Lun", + "calendar.weekdays.saturday" to "Sab", + "calendar.weekdays.sunday" to "Lin", + "calendar.weekdays.thursday" to "Huw", + "calendar.weekdays.tuesday" to "Mar", + "calendar.weekdays.wednesday" to "Miy", "club.accessDenied" to "Hindi pinapayagan ang access sa club na ito.", "club.accessRequested" to "Naipadala na ang hiling sa access.", "club.accessRequestFailed" to "Hindi naipadala ang hiling sa access.", @@ -31556,6 +32722,9 @@ object MobileStrings { "diary.planAddHint" to "Magdagdag ng bagong item sa plano gamit ang mga aksyon sa itaas.", "diary.planEmptyState" to "Wala pang nakalagay sa plano ng pagsasanay.", "diary.quickAdd" to "+ Quick add", + "diary.quickCreate" to "Mabilis na idagdag", + "diary.quickCreateFailed" to "Hindi malikha ang araw ng training.", + "diary.quickCreateNoSlot" to "Walang nahanap na libreng araw ng training (ayon sa iskedyul ng grupo, isang taon pasulong).", "diary.searchParticipants" to "Maghanap ng kalahok", "diary.selectGroup" to "Pumili ng grupo...", "diary.selectGroupAndActivity" to "Mangyaring pumili ng grupo at maglagay ng aktibidad.", @@ -33404,7 +34573,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "Babae", "tournaments.tournamentClassGenderOpen" to "Bukas (lahat)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -33462,6 +34631,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "Paglahok ng miyembro", "trainingStats.name" to "Pangalan", "trainingStats.noParticipants" to "Walang kalahok", + "trainingStats.panelAgeGroups" to "Dalo ayon sa edad", + "trainingStats.panelBestDay" to "Pinakamataong araw ng ensayo", + "trainingStats.panelGroupPerformance" to "Pag-unlad ayon sa grupo", + "trainingStats.panelMemberStructure" to "Istraktura ng miyembro", + "trainingStats.panelMonthlyTrend" to "Buwanang uso", + "trainingStats.panelSummary" to "Pangunahing bilang (salain)", + "trainingStats.panelWeekdayStats" to "Mga araw ng ensayo ayon sa araw ng linggo", "trainingStats.participants" to "Mga kalahok", "trainingStats.participations12Months" to "Paglahok (12 buwan)", "trainingStats.participations3Months" to "Paglahok (3 buwan)", @@ -33616,6 +34792,80 @@ object MobileStrings { "billing.templateSection" to "Vorlage hochladen", "billing.title" to "Abrechnung", "billing.uploadTemplate" to "Vorlage speichern", + "calendar.agendaEmpty" to "本月没有活动。", + "calendar.agendaTitle" to "本月活动", + "calendar.cancelCancellation.confirm" to "取消停课", + "calendar.cancelCancellation.message" to "确定要取消 {date} 的“{title}”训练停课吗?", + "calendar.cancelCancellation.title" to "取消训练停课", + "calendar.cancellation.date" to "Date", + "calendar.cancellation.description" to "Hides regular training times.", + "calendar.cancellation.fallbackTitle" to "Training cancelled", + "calendar.cancellation.reasonPlaceholder" to "Reason (optional)", + "calendar.cancellation.saving" to "Saving...", + "calendar.cancellation.submit" to "Add", + "calendar.cancellation.subtitle" to "Training cancellation", + "calendar.cancellation.title" to "Training cancelled", + "calendar.cancellation.trainingGroups" to "Training groups", + "calendar.cancellation.untilOptional" to "Until optional", + "calendar.customEvent.categoryPlaceholder" to "Category (optional)", + "calendar.customEvent.description" to "District meetings, sessions, internal meetings, ...", + "calendar.customEvent.saving" to "Saving...", + "calendar.customEvent.submit" to "Create", + "calendar.customEvent.subtitleFallback" to "Own event", + "calendar.customEvent.title" to "Own events", + "calendar.customEvent.titlePlaceholder" to "Title", + "calendar.description" to "Training days, club tournaments and league matches in a monthly view.", + "calendar.eventTitles.match" to "League match", + "calendar.eventTitles.officialTournament" to "Tournament participation", + "calendar.eventTitles.tournament" to "Tournament", + "calendar.eventTitles.training" to "Training", + "calendar.eyebrow" to "俱乐部日历", + "calendar.holidayTypes.holiday" to "Holiday", + "calendar.holidayTypes.schoolHoliday" to "School holidays", + "calendar.legend.customEvent" to "Event", + "calendar.legend.holiday" to "Holiday", + "calendar.legend.match" to "League match", + "calendar.legend.officialTournament" to "Participation", + "calendar.legend.schoolHoliday" to "School holidays", + "calendar.legend.tournament" to "Tournament", + "calendar.legend.training" to "Training", + "calendar.legend.trainingCancellation" to "Cancelled", + "calendar.loadError" to "Calendar data could not be loaded.", + "calendar.loading" to "正在加载日历数据...", + "calendar.match.guest" to "Away", + "calendar.match.home" to "Home", + "calendar.noClub" to "请先选择俱乐部。", + "calendar.options.subtitle" to "Cancellations & own events", + "calendar.options.title" to "Options", + "calendar.participants" to "{count} participants", + "calendar.quickCancellation.confirm" to "Add cancellation", + "calendar.quickCancellation.message" to "Mark “{title}” as training cancellation?", + "calendar.quickCancellation.noGroups" to "No training groups with training times are available.", + "calendar.quickCancellation.title" to "Training cancellation", + "calendar.quickCancellation.trainingGroups" to "Affected training groups", + "calendar.quickCancellation.useWholeRange" to "Apply to the full period ({start} to {end})", + "calendar.recurringTrainingTime" to "Regular training time", + "calendar.sources.customEvents" to "Own events", + "calendar.sources.holidays" to "Holidays/school holidays", + "calendar.sources.matches" to "League matches", + "calendar.sources.officialTournaments" to "Tournament participations", + "calendar.sources.tournaments" to "Tournaments", + "calendar.sources.trainingCancellations" to "Training cancellations", + "calendar.sources.trainingDays" to "Training days", + "calendar.sources.trainingTimes" to "Training times", + "calendar.sourceWarning" to "{source} could not be loaded", + "calendar.starts" to "{count} starts", + "calendar.title" to "日历", + "calendar.today" to "今天", + "calendar.tournament.club" to "Club tournament", + "calendar.tournament.open" to "Open tournament", + "calendar.weekdays.friday" to "周五", + "calendar.weekdays.monday" to "周一", + "calendar.weekdays.saturday" to "周六", + "calendar.weekdays.sunday" to "周日", + "calendar.weekdays.thursday" to "周四", + "calendar.weekdays.tuesday" to "周二", + "calendar.weekdays.wednesday" to "周三", "club.accessDenied" to "无权访问此俱乐部。", "club.accessRequested" to "已提交访问申请。", "club.accessRequestFailed" to "无法提交访问申请。", @@ -33946,6 +35196,9 @@ object MobileStrings { "diary.planAddHint" to "可通过上方操作添加新的计划项目。", "diary.planEmptyState" to "训练计划中还没有任何内容。", "diary.quickAdd" to "+ 快速添加", + "diary.quickCreate" to "快速创建", + "diary.quickCreateFailed" to "无法创建训练日。", + "diary.quickCreateNoSlot" to "未找到空闲训练日(按小组时间表,向前查找一年)。", "diary.searchParticipants" to "搜索参与者", "diary.selectGroup" to "选择分组...", "diary.selectGroupAndActivity" to "请选择一个分组并输入活动。", @@ -35794,7 +37047,7 @@ object MobileStrings { "tournaments.tournamentClassGenderFemale" to "女子", "tournaments.tournamentClassGenderOpen" to "公开组(全体)", "tournaments.tournamentName" to "Turniername", - "tournaments.tournamentParticipations" to "Turnierteilnahmen", + "tournaments.tournamentParticipations" to "Offizielle Turniere & Teilnahmen", "tournaments.transferQualifiedToFinalFromIntermediate" to "Move qualified players into the final round", "tournaments.transferQualifiedToFinalFromIntermediateDesc" to "Uses the intermediate-round to final-round rules and moves the qualified players across.", "tournaments.transferQualifiedToFinalFromPreliminary" to "Move qualified players straight into the final round", @@ -35852,6 +37105,13 @@ object MobileStrings { "trainingStats.memberParticipations" to "成员参与次数", "trainingStats.name" to "姓名", "trainingStats.noParticipants" to "无参与者", + "trainingStats.panelAgeGroups" to "按年龄组的出勤", + "trainingStats.panelBestDay" to "参与人数最多的训练日", + "trainingStats.panelGroupPerformance" to "各组进展", + "trainingStats.panelMemberStructure" to "成员结构", + "trainingStats.panelMonthlyTrend" to "月度趋势", + "trainingStats.panelSummary" to "关键数据(筛选)", + "trainingStats.panelWeekdayStats" to "按星期几的训练日", "trainingStats.participants" to "参与者", "trainingStats.participations12Months" to "参与次数(12 个月)", "trainingStats.participations3Months" to "参与次数(3 个月)", diff --git a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/DiaryManager.kt b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/DiaryManager.kt index 6212f9ef..2f9c735f 100644 --- a/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/DiaryManager.kt +++ b/mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/state/DiaryManager.kt @@ -219,6 +219,10 @@ class DiaryManager( participantsApi.updateParticipantGroup(diaryDateId, memberId, groupId) } + fun clearError() { + _state.value = _state.value.copy(error = null) + } + suspend fun loadDates(clubId: Int) { _state.value = _state.value.copy(isLoading = true, error = null) try { @@ -229,13 +233,16 @@ class DiaryManager( } } - suspend fun createDate(clubId: Int, date: String, trainingStart: String?, trainingEnd: String?) { + /** @return neue `diaryDateId` bei Erfolg, sonst `null` */ + suspend fun createDate(clubId: Int, date: String, trainingStart: String?, trainingEnd: String?): Int? { _state.value = _state.value.copy(isLoading = true, error = null) - try { - diaryApi.createDate(clubId, date, trainingStart, trainingEnd) + return try { + val created = diaryApi.createDate(clubId, date, trainingStart, trainingEnd) loadDates(clubId) + created.id } catch (t: Throwable) { _state.value = _state.value.copy(isLoading = false, error = t.toUserMessage("Eintrag konnte nicht erstellt werden")) + null } } diff --git a/scripts/generate-mobile-i18n.js b/scripts/generate-mobile-i18n.js index 32aa7ebd..4cbdd489 100644 --- a/scripts/generate-mobile-i18n.js +++ b/scripts/generate-mobile-i18n.js @@ -5,7 +5,7 @@ * Source of truth: `frontend/src/i18n/locales/*.json` * * Output: - * - `mobile-app/shared/src/commonMain/kotlin/de/tt_tagebuch/shared/i18n/MobileStrings.kt` + * - `mobile-app/shared/src/commonMain/kotlin/de/tsschulz/tt_tagebuch/shared/i18n/MobileStrings.kt` * * Notes: * - German (`de`) is the canonical key set. @@ -25,6 +25,7 @@ const OUT = path.join( 'commonMain', 'kotlin', 'de', + 'tsschulz', 'tt_tagebuch', 'shared', 'i18n',