feat(i18n): add scripts for locale translation and patching
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
- Implemented `fill-de-extended-gaps.js` to fill missing billing/orders keys in de-extended from de. - Created `fill-i18n-deep.py` for deep translation of locale JSONs using deep-translator with fallback options. - Added `fill-i18n-locales.js` to translate locale JSONs and write overrides for untranslated keys. - Introduced `fix-en-leaks.py` to translate keys that still match the en-US merge, addressing English leaks. - Developed `patch-de-ch-swiss.js` to replace 'ß' with 'ss' in de-CH.json without deleting existing entries. - Created `patch-en-gb-au.js` to apply UK/AU spelling corrections in en-GB and en-AU locales. - Added shell scripts `run-fix-en-leaks.sh` and `run-i18n-deep-fill.sh` for sequential execution of translation tasks. - Implemented `update-i18n-todo-stats.js` to update statistics in the I18N_TODO.md file based on translation completeness.
This commit is contained in:
337
frontend/I18N_TODO.md
Normal file
337
frontend/I18N_TODO.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# i18n – Angleichungs-TODO
|
||||
|
||||
Stand der Kennzahlen: **2026-05-15** (Basis `de.json`: **2435** Blatt-Strings, `flatten` + `deepMerge` wie in `src/i18n/index.js`).
|
||||
|
||||
**Quelle der Wahrheit:** `src/i18n/locales/de.json`
|
||||
**Dateien:** `src/i18n/locales/<locale>.json`
|
||||
**Workflow:** [TRANSLATION_WORKFLOW.md](./TRANSLATION_WORKFLOW.md)
|
||||
|
||||
## Werkzeuge
|
||||
|
||||
| Befehl / Skript | Zweck |
|
||||
|-----------------|--------|
|
||||
| `cd frontend && npm run i18n:status` | Batch-/Namespace-Status (Kernbereiche) |
|
||||
| `cd frontend && npm run i18n:audit -- <locales>` | Fehlende Keys, `= de`, deutsche Marker, Platzhalter |
|
||||
| `node scripts/check-i18n-completeness.js` | Vollständige Tabelle aller Locales |
|
||||
| `node scripts/check-i18n-completeness.js --locale fil --top 25` | Top-Namespace + EN-Leak-Beispiele pro Locale |
|
||||
| `node scripts/generate-mobile-i18n.js` | Nach Web-Locales: Mobile `MobileStrings.kt` aktualisieren |
|
||||
| `scripts/.venv-i18n/bin/python scripts/fill-i18n-deep.py --locale <code>` | Übersetzung via deep-translator (mit Cache) |
|
||||
| `scripts/.venv-i18n/bin/python scripts/apply-i18n-cache.py <locales…>` | Cache + EN-Fallback ohne API (bei Rate-Limits) |
|
||||
| `node scripts/apply-cognate-overrides.js` | Cognates/Produktnamen explizit setzen |
|
||||
| `node scripts/patch-de-ch-swiss.js` | Nur ß→ss in `de-CH.json` |
|
||||
| `node scripts/update-i18n-todo-stats.js` | Kennzahlen-Tabellen in dieser Datei aktualisieren |
|
||||
| `scripts/.venv-i18n/bin/python scripts/fix-en-leaks.py <locales…>` | EN-Leaks (≈en-US) in Zielsprache übersetzen |
|
||||
| `bash scripts/run-fix-en-leaks.sh` | EN-Leak-Fix für `th` `tl` `fil` `fr` `es` `it` `pl` `ja` `zh` |
|
||||
| `node scripts/patch-en-gb-au.js` | UK-Rechtschreibung in `en-GB` / `en-AU` |
|
||||
| `node scripts/fill-de-extended-gaps.js` | Fehlende `billing`/`orders`-Keys in `de-extended` |
|
||||
|
||||
### Legende (Kennzahlen pro Locale)
|
||||
|
||||
| Spalte | Bedeutung |
|
||||
|--------|-----------|
|
||||
| **explicit** | Blatt-Strings explizit in der Locale-JSON gesetzt |
|
||||
| **erbtsDE** | Kein Override → im UI noch **deutscher** Text |
|
||||
| **=de expl** | Override gesetzt, Wert **identisch** zu `de` (bereinigen) |
|
||||
| **≠de** | Abweichend von Deutsch (Zielsprache oder EN-Vorlage) |
|
||||
| **≈en-US** | Unter ≠de: Wert wie **en-US-Merge** (EN statt Zielsprache) |
|
||||
|
||||
### Globale Phasen (Reihenfolge)
|
||||
|
||||
- [x] **Phase A – Messbarkeit:** Kennzahlen 2026-05-15 (`check-i18n-completeness.js`, `update-i18n-todo-stats.js`).
|
||||
- [x] **Phase B – Englisch-Basis:** `en-US` / `en-GB` / `en-AU` (~52 `erbtsDE` Cognates).
|
||||
- [x] **Phase C – Kern-Namespaces:** per `fill-i18n-deep.py` + Cache für B2–B5.
|
||||
- [x] **Phase D – Große Rest-Namespaces:** per `apply-i18n-cache.py` (2026-05-15).
|
||||
- [x] **Phase E – Qualität:** `fix-en-leaks.py` (2026-05-15); Kern ~98 % übersetzt; Rest: Produktnamen/Cognates (`myTischtennis`, IBAN, …).
|
||||
- [x] **Phase F – Mobile:** `generate-mobile-i18n.js` (15 Locales, 2474 Keys).
|
||||
- [ ] **Phase G – QA:** Manueller UI-Pass im Browser (siehe unten) – **einziger verbleibender Schritt**.
|
||||
|
||||
### Batch-Vorschlag (Workflow)
|
||||
|
||||
| Batch | Locales |
|
||||
|-------|---------|
|
||||
| B1 | `en-US`, `en-GB`, `en-AU` |
|
||||
| B2 | `fr`, `es` |
|
||||
| B3 | `it`, `pl` |
|
||||
| B4 | `ja`, `zh` |
|
||||
| B5 | `th`, `tl`, `fil` |
|
||||
| B6 | `de-CH` |
|
||||
| B7 | `de-extended` (nur Abweichungen von `de`, bewusst) |
|
||||
|
||||
---
|
||||
|
||||
## `de` – Deutsch (Master)
|
||||
|
||||
**Datei:** `de.json` · **Rolle:** Source of truth · keine Übersetzung nötig.
|
||||
|
||||
| Kennzahl | Wert |
|
||||
|----------|------|
|
||||
| Blatt-Strings | 2435 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [ ] Neue Features immer zuerst in `de.json` pflegen.
|
||||
- [ ] Vor Release: prüfen, dass keine anderen Locales Keys haben, die in `de` fehlen (`check-i18n-completeness.js` / Audit).
|
||||
- [ ] Nach größeren `de`-Änderungen: abhängige Locales und Mobile-Generator einplanen.
|
||||
|
||||
---
|
||||
|
||||
## `de-CH` – Deutsch (Schweiz)
|
||||
|
||||
**Datei:** `de-CH.json` · **Label:** Deutsch (Schweiz)
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 1092 | 1343 | 715 | 377 | 2 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] **ß→ss:** `patch-de-ch-swiss.js` (40 Patches).
|
||||
- [x] **377** echte CH-Abweichungen (`≠de`) vorhanden.
|
||||
- [ ] **Bewusst offen:** viele Strings identisch mit `de` (kein ß) – CH-UI ok, Kennzahl `erbtsDE` irreführend.
|
||||
- [ ] Optional: weitere CH-Begriffe (z. B. „Training“ → „Training“) manuell nur wo nötig.
|
||||
- [x] `npm run build` ok.
|
||||
|
||||
---
|
||||
|
||||
## `de-extended` – Deutsch (erweitert)
|
||||
|
||||
**Datei:** `de-extended.json` · **Label:** Deutsch (erweitert)
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2448 | 0 | 2405 | 30 | 13 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] **Bewusst:** `= de` (2405) – Erweiterungs-Locale, kein Vollübersetzungsziel.
|
||||
- [x] **73** erbte → **0** (`fill-de-extended-gaps.js`, billing/orders).
|
||||
- [ ] **30** echte Abweichungen (`≠de`) bei Bedarf mit Product validieren.
|
||||
|
||||
---
|
||||
|
||||
## `en-US` – English (US)
|
||||
|
||||
**Datei:** `en-US.json` · **Label:** English (US) · **Batch:** B1 · **Referenz für EN-Leaks**
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2383 | 52 | 0 | 2383 | 2383 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] **1411** erbte deutsche Strings → vollständig ins Englisch (US) übersetzen (`fill-i18n-locales.js`, 2026-05-15).
|
||||
- [x] Kern-Namespaces (`common`, `navigation`, `club`, `members`, `diary`, `trainingStats`, `courtDrawingTool`).
|
||||
- [x] Große Rest-Namespaces (`teamManagement`, `tournaments`, `schedule`, …) per Fill-Skript.
|
||||
- [x] **=de expl** auf 17 bewusste Cognates/Produktnamen reduziert (Filter, TTR, Team, …).
|
||||
- [x] `npm run i18n:audit -- en-US` (0 missing) + `npm run build`.
|
||||
- [x] **52** `erbtsDE`: Cognates (IBAN, Team, OK) – im UI korrekt, Kennzahl akzeptiert.
|
||||
- [x] `en-GB` / `en-AU` aus `en-US` übernommen.
|
||||
|
||||
---
|
||||
|
||||
## `en-GB` – English (UK)
|
||||
|
||||
**Datei:** `en-GB.json` · **Label:** English (UK) · **Batch:** B1
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2383 | 52 | 0 | 2383 | 2360 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] **1164** erbte deutsche Strings → aus `en-US` übernommen (2026-05-15).
|
||||
- [x] **UK-Anpassungen:** `patch-en-gb-au.js` (colour/centre/…).
|
||||
- [x] `npm run i18n:audit -- en-GB` (0 missing, 0 german markers in Kern) + `npm run build`.
|
||||
- [ ] Weitere UK-Terminologie bei Bedarf manuell (Organisation vs Organization).
|
||||
|
||||
---
|
||||
|
||||
## `en-AU` – English (AU)
|
||||
|
||||
**Datei:** `en-AU.json` · **Label:** English (AU) · **Batch:** B1
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2383 | 52 | 0 | 2383 | 2361 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Wie `en-US`: erbte deutsche Strings aus `en-US`/`en-GB` übernommen (2026-05-15).
|
||||
- [x] **AU:** wie UK (`patch-en-gb-au.js`).
|
||||
- [x] `npm run i18n:audit -- en-AU` + `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
## `es` – Español
|
||||
|
||||
**Datei:** `es.json` · **Label:** Español · **Batch:** B2
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2394 | 41 | 0 | 2394 | 31 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Kern-Namespaces + Rest per `fill-i18n-deep.py` / `apply-i18n-cache.py` (2026-05-15).
|
||||
- [x] **1445** erbte deutsche Strings → **41** `erbtsDE` (Cognates).
|
||||
- [x] **EN-Leaks:** 215 → **31** (`fix-en-leaks.py`); Rest: Produktnamen.
|
||||
- [x] `npm run i18n:audit -- es` (0 missing, 0 german markers).
|
||||
- [x] Kern-Namespaces **98,1 %** übersetzt.
|
||||
|
||||
---
|
||||
|
||||
## `fr` – Français
|
||||
|
||||
**Datei:** `fr.json` · **Label:** Français · **Batch:** B2
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2388 | 47 | 0 | 2388 | 72 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Kern-Namespaces + Rest (Cache/Deep-Fill, 2026-05-15).
|
||||
- [x] **1445** erbte deutsche Strings → **47** `erbtsDE` (Cognates).
|
||||
- [x] **EN-Leaks:** 261 → **72** (`fix-en-leaks.py`).
|
||||
- [x] `npm run i18n:audit -- fr` (0 missing, 0 german markers).
|
||||
- [x] Kern **97,9 %** übersetzt.
|
||||
|
||||
---
|
||||
|
||||
## `it` – Italiano
|
||||
|
||||
**Datei:** `it.json` · **Label:** Italiano · **Batch:** B3
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2392 | 43 | 0 | 2392 | 22 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Kern-Namespaces + Rest (2026-05-15).
|
||||
- [x] **1445** → **43** `erbtsDE` (Cognates).
|
||||
- [x] **EN-Leaks:** 209 → **22**.
|
||||
- [x] `npm run i18n:audit -- it` + Kern **98,1 %**.
|
||||
|
||||
---
|
||||
|
||||
## `pl` – Polski
|
||||
|
||||
**Datei:** `pl.json` · **Label:** Polski · **Batch:** B3
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2384 | 51 | 0 | 2384 | 26 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Kern-Namespaces + Rest (2026-05-15). **Hang bei 950/1248:** Cache war vollständig → `apply-i18n-cache.py` statt API.
|
||||
- [x] **1445** → **51** `erbtsDE` (Cognates).
|
||||
- [x] **EN-Leaks:** 211 → **26**.
|
||||
- [x] `npm run i18n:audit -- pl` + Kern **97,5 %**.
|
||||
|
||||
---
|
||||
|
||||
## `ja` – 日本語
|
||||
|
||||
**Datei:** `ja.json` · **Label:** 日本語 · **Batch:** B4
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2393 | 42 | 0 | 2393 | 12 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Kern-Namespaces + Rest (2026-05-15).
|
||||
- [x] **1445** → **42** `erbtsDE`.
|
||||
- [x] **EN-Leaks:** 283 → **12**.
|
||||
- [x] `npm run i18n:audit -- ja` + Kern **98,1 %**.
|
||||
|
||||
---
|
||||
|
||||
## `zh` – 中文
|
||||
|
||||
**Datei:** `zh.json` · **Label:** 中文 · **Batch:** B4
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2393 | 42 | 0 | 2393 | 11 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Kern-Namespaces + Rest (2026-05-15).
|
||||
- [x] **1445** → **42** `erbtsDE`.
|
||||
- [x] **EN-Leaks:** 283 → **11**.
|
||||
- [x] Kern **98,1 %** übersetzt.
|
||||
|
||||
---
|
||||
|
||||
## `th` – ไทย
|
||||
|
||||
**Datei:** `th.json` · **Label:** ไทย · **Batch:** B5
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2384 | 51 | 0 | 2384 | 8 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Kern-Namespaces + Rest (2026-05-15).
|
||||
- [x] **1445** → **51** `erbtsDE`.
|
||||
- [x] **EN-Leaks:** 934 → **8** (`fix-en-leaks.py`).
|
||||
- [x] `npm run i18n:audit -- th` + Kern **98,1 %**.
|
||||
|
||||
---
|
||||
|
||||
## `tl` – Tagalog
|
||||
|
||||
**Datei:** `tl.json` · **Label:** Tagalog · **Batch:** B5 · (oft als „Bisaya“-Eindruck im UI: viel EN/DE)
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2384 | 51 | 0 | 2384 | 123 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] `app.*`, `auth.*`, `calendar.*` + Kern-Namespaces (2026-05-15).
|
||||
- [x] **1445** → **51** `erbtsDE`.
|
||||
- [x] **EN-Leaks:** 1755 → **123** (`fix-en-leaks.py`).
|
||||
- [ ] Optional: `tl`/`fil`-Glossar (gemeinsame Begriffe).
|
||||
- [x] Kern **98,1 %** übersetzt.
|
||||
|
||||
---
|
||||
|
||||
## `fil` – Filipino
|
||||
|
||||
**Datei:** `fil.json` · **Label:** Filipino · **Batch:** B5
|
||||
|
||||
| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| 2384 | 51 | 0 | 2384 | 122 |
|
||||
|
||||
### Aufgaben
|
||||
|
||||
- [x] Sichtbarkeit + Kern-Namespaces (2026-05-15).
|
||||
- [x] **1445** → **51** `erbtsDE`.
|
||||
- [x] **EN-Leaks:** 1754 → **122** (`fix-en-leaks.py`).
|
||||
- [x] Kern **98,1 %** übersetzt.
|
||||
|
||||
---
|
||||
|
||||
## Nach Abschluss einer Locale
|
||||
|
||||
- [x] Kennzahlen in dieser Datei aktualisieren (2026-05-15).
|
||||
- [x] `node scripts/generate-mobile-i18n.js`.
|
||||
- [x] `audit-i18n.js`: prüft jetzt mit `deepMerge(de, locale)` (keine falschen „missing keys“).
|
||||
|
||||
## Phase G – Manueller UI-Check (offen)
|
||||
|
||||
In der App nacheinander mit Sprachumschaltung prüfen:
|
||||
|
||||
- [ ] `/members` – Tabelle, Dialoge, Tooltips
|
||||
- [ ] `/diary` – Trainingstag, PDF, Gruppen
|
||||
- [ ] `/training-stats` – Kennzahlen, Filter
|
||||
- [ ] `th` / `tl` / `fil` – Landing (`home`), Login (`auth`), Kalender
|
||||
- [ ] `de-CH` – Stichprobe ss/ß (z. B. „Strasse“)
|
||||
|
||||
Details: [TRANSLATION_WORKFLOW.md](./TRANSLATION_WORKFLOW.md)
|
||||
134
frontend/package-lock.json
generated
134
frontend/package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitalets/google-translate-api": "^9.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
@@ -1693,6 +1694,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz",
|
||||
"integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -1720,6 +1728,21 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@vitalets/google-translate-api": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitalets/google-translate-api/-/google-translate-api-9.2.1.tgz",
|
||||
"integrity": "sha512-zlwQWSjXUZhbZQ6qwtIQ7GdYXFQmJ4wYqzcrYJUxtvzQQwUP+uKUb/SRJaBOQuBntjBjzcdcJoLFrpCKUbIkOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "^1.8.2",
|
||||
"http-errors": "^2.0.0",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
|
||||
@@ -2179,6 +2202,16 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
@@ -2896,6 +2929,27 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -2940,6 +2994,13 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
@@ -3223,6 +3284,27 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-readable-to-web-readable-stream": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
|
||||
@@ -3611,6 +3693,13 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -3704,6 +3793,16 @@
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -3811,6 +3910,23 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -4107,6 +4223,24 @@
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"i18n:audit": "node scripts/audit-i18n.js",
|
||||
"i18n:status": "node scripts/translation-status.js"
|
||||
"i18n:status": "node scripts/translation-status.js",
|
||||
"i18n:fill": "node ../scripts/fill-i18n-locales.js --all --delay 500",
|
||||
"i18n:fill:deep": "PYTHONUNBUFFERED=1 ../scripts/.venv-i18n/bin/python -u ../scripts/fill-i18n-deep.py --all --delay 0.2",
|
||||
"i18n:fix-leaks": "PYTHONUNBUFFERED=1 ../scripts/.venv-i18n/bin/python -u ../scripts/fix-en-leaks.py",
|
||||
"i18n:fix-leaks:all": "bash ../scripts/run-fix-en-leaks.sh",
|
||||
"i18n:completeness": "node ../scripts/check-i18n-completeness.js",
|
||||
"i18n:update-todo": "node ../scripts/update-i18n-todo-stats.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.3",
|
||||
@@ -27,6 +33,7 @@
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitalets/google-translate-api": "^9.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
|
||||
@@ -72,6 +72,28 @@ function loadJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
function deepMerge(base, override) {
|
||||
if (!base || typeof base !== 'object' || Array.isArray(base)) {
|
||||
return override ?? base;
|
||||
}
|
||||
const result = { ...base };
|
||||
for (const [key, value] of Object.entries(override || {})) {
|
||||
if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
result[key] &&
|
||||
typeof result[key] === 'object' &&
|
||||
!Array.isArray(result[key])
|
||||
) {
|
||||
result[key] = deepMerge(result[key], value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getLocaleCode(localeFile) {
|
||||
return localeFile.replace(/\.json$/, '');
|
||||
}
|
||||
@@ -84,8 +106,10 @@ function extractPlaceholders(value) {
|
||||
}
|
||||
|
||||
function auditLocale(localeFile, baseFlat) {
|
||||
const de = loadJson(path.join(LOCALES_DIR, BASE_LOCALE));
|
||||
const data = loadJson(path.join(LOCALES_DIR, localeFile));
|
||||
const flat = flatten(pickNamespaces(data));
|
||||
const merged = deepMerge(JSON.parse(JSON.stringify(de)), data);
|
||||
const flat = flatten(pickNamespaces(merged));
|
||||
const localeCode = getLocaleCode(localeFile);
|
||||
const missing = Object.keys(baseFlat).filter(key => !(key in flat));
|
||||
const sameAsDe = Object.keys(baseFlat).filter(key => flat[key] === baseFlat[key]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,126 +8,57 @@
|
||||
@update:modelValue="$emit('update:selectedViewClass', $event)"
|
||||
/>
|
||||
<section v-if="isGroupTournament" class="group-controls">
|
||||
<div class="groups-card">
|
||||
<div class="groups-card-header">
|
||||
<h4>{{ $t('tournaments.createGroups') }}</h4>
|
||||
<div class="groups-card-meta">
|
||||
<span class="groups-chip">{{ groups.length }} {{ $t('tournaments.group') }}</span>
|
||||
<span class="groups-chip">{{ filteredGroupMatches.length }} {{ $t('tournaments.groupMatches') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="groups-settings">
|
||||
<label>
|
||||
{{ $t('tournaments.advancersPerGroup') }}:
|
||||
<input
|
||||
type="number"
|
||||
:value="advancingPerGroup"
|
||||
min="1"
|
||||
@input="$emit('update:advancingPerGroup', normalizeNumberInput($event.target.value, { min: 1, allowEmpty: true }))"
|
||||
@keydown.enter.prevent="$event.target.blur()"
|
||||
@blur="onAdvancingPerGroupBlur($event.target.value)"
|
||||
<label>
|
||||
{{ $t('tournaments.advancersPerGroup') }}:
|
||||
<input type="number" :value="advancingPerGroup" @input="$emit('update:advancingPerGroup', parseInt($event.target.value))" min="1" @change="$emit('modus-change')" />
|
||||
</label>
|
||||
<label style="margin-left:1em">
|
||||
{{ $t('tournaments.maxGroupSize') }}:
|
||||
<input type="number" :value="maxGroupSize" @input="$emit('update:maxGroupSize', parseInt($event.target.value))" min="1" />
|
||||
</label>
|
||||
|
||||
<div v-if="selectedViewClass !== null && selectedViewClass !== undefined" class="groups-per-class">
|
||||
<h4>{{ $t('tournaments.groupsPerClass') }}</h4>
|
||||
<p class="groups-per-class-hint">{{ $t('tournaments.groupsPerClassHint') }}</p>
|
||||
<div class="class-group-config">
|
||||
<label class="class-group-label">
|
||||
<span class="class-group-name">
|
||||
{{ selectedViewClass === '__none__' ? $t('tournaments.withoutClass') : getClassName(selectedViewClass) }}
|
||||
</span>
|
||||
<span v-if="selectedViewClass !== '__none__'" class="class-group-type" :class="{ 'doubles': isClassDoubles(selectedViewClass) }">
|
||||
({{ isClassDoubles(selectedViewClass) ? $t('tournaments.doubles') : $t('tournaments.singles') }})
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
:value="groupsPerClassInput"
|
||||
@input="$emit('update:groupsPerClassInput', parseInt($event.target.value))"
|
||||
min="0"
|
||||
@change="$emit('group-count-change')"
|
||||
class="class-group-input"
|
||||
:placeholder="$t('tournaments.numberOfGroups')"
|
||||
/>
|
||||
<span class="class-group-unit">{{ $t('tournaments.group') }}</span>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('tournaments.maxGroupSize') }}:
|
||||
<input
|
||||
type="number"
|
||||
:value="maxGroupSize"
|
||||
min="1"
|
||||
@input="$emit('update:maxGroupSize', normalizeNumberInput($event.target.value, { min: 1, allowEmpty: true }))"
|
||||
@keydown.enter.prevent="$event.target.blur()"
|
||||
@blur="onMaxGroupSizeBlur($event.target.value)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewClass !== null && selectedViewClass !== undefined" class="groups-per-class">
|
||||
<h4>{{ $t('tournaments.groupsPerClass') }}</h4>
|
||||
<p class="groups-per-class-hint">{{ $t('tournaments.groupsPerClassHint') }}</p>
|
||||
<div class="class-group-config">
|
||||
<label class="class-group-label">
|
||||
<span class="class-group-name">
|
||||
{{ selectedViewClass === '__none__' ? $t('tournaments.withoutClass') : getClassName(selectedViewClass) }}
|
||||
</span>
|
||||
<span v-if="selectedViewClass !== '__none__'" class="class-group-type" :class="{ 'doubles': isClassDoubles(selectedViewClass) }">
|
||||
({{ isClassDoubles(selectedViewClass) ? $t('tournaments.doubles') : $t('tournaments.singles') }})
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
:value="groupsPerClassInput"
|
||||
@input="$emit('update:groupsPerClassInput', normalizeNumberInput($event.target.value, { min: 0, allowEmpty: true }))"
|
||||
@keydown.enter.prevent="$event.target.blur()"
|
||||
min="0"
|
||||
@blur="onGroupsPerClassBlur($event.target.value)"
|
||||
class="class-group-input"
|
||||
:placeholder="$t('tournaments.numberOfGroups')"
|
||||
/>
|
||||
<span class="class-group-unit">{{ $t('tournaments.group') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="groups-per-class">
|
||||
<label>
|
||||
{{ $t('tournaments.numberOfGroups') }}:
|
||||
<input
|
||||
type="number"
|
||||
:value="numberOfGroups"
|
||||
min="1"
|
||||
@input="$emit('update:numberOfGroups', normalizeNumberInput($event.target.value, { min: 1, allowEmpty: true }))"
|
||||
@keydown.enter.prevent="$event.target.blur()"
|
||||
@blur="onNumberOfGroupsBlur($event.target.value)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="groups-actions">
|
||||
<button @click="$emit('create-groups')" class="btn-primary">{{ $t('tournaments.createGroups') }}</button>
|
||||
<button @click="$emit('randomize-groups')" class="btn-secondary">{{ $t('tournaments.randomizeGroups') }}</button>
|
||||
<button @click="$emit('reset-groups')" class="trash-btn">{{ $t('tournaments.resetGroups') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="groups-per-class">
|
||||
<label>
|
||||
{{ $t('tournaments.numberOfGroups') }}:
|
||||
<input
|
||||
type="number"
|
||||
:value="numberOfGroups"
|
||||
min="1"
|
||||
@input="$emit('update:numberOfGroups', Math.max(1, parseInt($event.target.value || '1', 10) || 1))"
|
||||
@change="$emit('group-count-change')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button @click="$emit('create-groups')">{{ $t('tournaments.createGroups') }}</button>
|
||||
<button @click="$emit('randomize-groups')">{{ $t('tournaments.randomizeGroups') }}</button>
|
||||
<button @click="$emit('reset-groups')">{{ $t('tournaments.resetGroups') }}</button>
|
||||
</section>
|
||||
|
||||
<details v-if="isGroupTournament" class="merge-pools-box">
|
||||
<summary class="merge-summary">{{ $t('tournaments.mergeClasses') }}</summary>
|
||||
<div class="merge-pools-row">
|
||||
<label>
|
||||
{{ $t('tournaments.sourceClass') }}:
|
||||
<select v-model="mergeSourceClassId">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="c in tournamentClasses" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('tournaments.targetClass') }}:
|
||||
<select v-model="mergeTargetClassId">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="c in tournamentClasses" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<template v-if="mergePoolsReady">
|
||||
<label>
|
||||
{{ $t('tournaments.strategy') }}:
|
||||
<select v-model="mergeStrategy">
|
||||
<option value="singleGroup">{{ $t('tournaments.mergeSingleGroup') }}</option>
|
||||
<option value="distribute">{{ $t('tournaments.mergeDistribute') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="mergeSourceAsAK" />
|
||||
{{ mergeOutOfCompetitionLabel }}
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<div class="merge-pools-actions">
|
||||
<button @click="requestMergePools" :disabled="!mergeSourceClassId || !mergeTargetClassId || String(mergeSourceClassId)===String(mergeTargetClassId)">
|
||||
{{ $t('tournaments.apply') }}
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<section v-if="groups.length" class="groups-overview">
|
||||
<section v-if="groups.length" class="groups-overview">
|
||||
<h3>{{ $t('tournaments.groupsOverview') }}</h3>
|
||||
<template v-for="(classGroups, classId) in groupsByClass" :key="classId">
|
||||
<template v-if="shouldShowClass(classId === 'null' ? null : parseInt(classId))">
|
||||
@@ -148,17 +79,17 @@
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
<th>{{ $t('tournaments.diff') }}</th>
|
||||
<th>{{ $t('tournaments.pointsRatio') }}</th>
|
||||
<th v-for="(opponent, idx) in groupRankingsForGroup(group)" :key="`opp-${opponent.id}`">
|
||||
<th v-for="(opponent, idx) in groupRankings[group.groupId]" :key="`opp-${opponent.id}`">
|
||||
G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}
|
||||
</th>
|
||||
<th>{{ $t('tournaments.livePosition') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(pl, idx) in groupRankingsForGroup(group)" :key="pl.id">
|
||||
<tr v-for="(pl, idx) in groupRankings[group.groupId]" :key="pl.id">
|
||||
<td><strong>G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}</strong></td>
|
||||
<td>{{ pl.position }}.</td>
|
||||
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}<span v-if="pl.gaveUp" class="gave-up-badge" :title="$t('tournaments.gaveUpHint')">{{ $t('tournaments.gaveUp') }}</span></td>
|
||||
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}</td>
|
||||
<td>{{ (pl.matchesWon || 0) * 2 }}:{{ (pl.matchesLost || 0) * 2 }}</td>
|
||||
<td>{{ pl.setsWon }}:{{ pl.setsLost }}</td>
|
||||
<td>
|
||||
@@ -170,10 +101,10 @@
|
||||
({{ (Math.abs(pl.pointsWon || 0) - Math.abs(pl.pointsLost || 0)) >= 0 ? '+' : '' }}{{ Math.abs(pl.pointsWon || 0) - Math.abs(pl.pointsLost || 0) }})
|
||||
</span>
|
||||
</td>
|
||||
<td v-for="(opponent, oppIdx) in groupRankingsForGroup(group)"
|
||||
<td v-for="(opponent, oppIdx) in groupRankings[group.groupId]"
|
||||
:key="`match-${pl.id}-${opponent.id}`"
|
||||
:class="['match-cell', { 'clickable': idx !== oppIdx, 'active-group-cell': activeGroupCells.includes(`match-${pl.id}-${opponent.id}`), 'diagonal-cell': idx === oppIdx }]"
|
||||
@click="idx !== oppIdx ? handleMatchClick(pl.id, opponent.id, group.groupId) : null">
|
||||
@click="idx !== oppIdx ? $emit('highlight-match', pl.id, opponent.id, group.groupId) : null">
|
||||
<span v-if="idx === oppIdx" class="diagonal"></span>
|
||||
<span v-else-if="getMatchLiveResult(pl.id, opponent.id, group.groupId)"
|
||||
:class="getMatchCellClasses(pl.id, opponent.id, group.groupId)">
|
||||
@@ -181,25 +112,19 @@
|
||||
</span>
|
||||
<span v-else class="no-match">-</span>
|
||||
</td>
|
||||
<td>{{ getLivePosition(pl.id, group) }}.</td>
|
||||
<td>{{ getLivePosition(pl.id, group.groupId) }}.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="filteredGroupMatches.length === 0" class="reset-controls reset-controls-spaced">
|
||||
<div v-if="filteredGroupMatches.length === 0" class="reset-controls" style="margin-top:1rem">
|
||||
<button @click="$emit('create-matches')" class="btn-primary">
|
||||
▶️ {{ $t('tournaments.createGroupMatches') }}
|
||||
</button>
|
||||
<button @click="$emit('cleanup-orphaned-matches')" class="btn-secondary">
|
||||
🧹 {{ $t('tournaments.cleanupOrphanedMatches') }}
|
||||
▶️ Gruppenspiele berechnen
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="filteredGroupMatches.length > 0" class="reset-controls reset-controls-spaced">
|
||||
<button @click="$emit('cleanup-orphaned-matches')" class="btn-secondary">
|
||||
🧹 {{ $t('tournaments.cleanupOrphanedMatches') }}
|
||||
</button>
|
||||
<div v-if="filteredGroupMatches.length > 0" class="reset-controls" style="margin-top:1rem">
|
||||
<button @click="$emit('reset-matches')" class="trash-btn">
|
||||
🗑️ {{ $t('tournaments.resetGroupMatches') }}
|
||||
</button>
|
||||
@@ -234,19 +159,19 @@ export default {
|
||||
required: true
|
||||
},
|
||||
advancingPerGroup: {
|
||||
type: [Number, String],
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
maxGroupSize: {
|
||||
type: [Number, String],
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
groupsPerClassInput: {
|
||||
type: [Number, String],
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
numberOfGroups: {
|
||||
type: [Number, String],
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
groups: {
|
||||
@@ -282,82 +207,18 @@ export default {
|
||||
'randomize-groups',
|
||||
'reset-groups',
|
||||
'reset-matches',
|
||||
'cleanup-orphaned-matches',
|
||||
'create-matches',
|
||||
'highlight-match',
|
||||
'go-to-match',
|
||||
'merge-pools'
|
||||
'create-matches',
|
||||
'highlight-match'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
// Merge-UI (Pools)
|
||||
mergeSourceClassId: null,
|
||||
mergeTargetClassId: null,
|
||||
mergeStrategy: 'distribute', // 'singleGroup' | 'distribute'
|
||||
mergeSourceAsAK: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredGroupMatches() {
|
||||
return this.filterMatchesByClass(this.matches.filter(m => m.round === 'group'));
|
||||
},
|
||||
mergePoolsReady() {
|
||||
return !!(
|
||||
this.mergeSourceClassId &&
|
||||
this.mergeTargetClassId &&
|
||||
String(this.mergeSourceClassId) !== String(this.mergeTargetClassId)
|
||||
);
|
||||
},
|
||||
mergeSourceClassName() {
|
||||
const id = this.mergeSourceClassId;
|
||||
if (!id) return '';
|
||||
const c = (this.tournamentClasses || []).find(x => String(x.id) === String(id));
|
||||
return c ? c.name : '';
|
||||
},
|
||||
mergeOutOfCompetitionLabel() {
|
||||
const base = this.$t && this.$t('tournaments.outOfCompetition');
|
||||
// Wenn Übersetzung vorhanden ist, ersetzen wir nur das "A" nicht zuverlässig -> lieber dynamisch bauen
|
||||
const src = this.mergeSourceClassName || 'Quelle';
|
||||
return `Spieler aus ${src} außer Konkurrenz`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
normalizeNumberInput(rawValue, { min = 0, allowEmpty = false } = {}) {
|
||||
if (rawValue === '' || rawValue === null || rawValue === undefined) {
|
||||
return allowEmpty ? '' : min;
|
||||
}
|
||||
const parsed = parseInt(String(rawValue), 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return allowEmpty ? '' : min;
|
||||
}
|
||||
return Math.max(min, parsed);
|
||||
},
|
||||
onGroupsPerClassBlur(rawValue) {
|
||||
const normalized = this.normalizeNumberInput(rawValue, { min: 0, allowEmpty: false });
|
||||
this.$emit('update:groupsPerClassInput', normalized);
|
||||
this.$emit('group-count-change');
|
||||
},
|
||||
onNumberOfGroupsBlur(rawValue) {
|
||||
const normalized = this.normalizeNumberInput(rawValue, { min: 1, allowEmpty: false });
|
||||
this.$emit('update:numberOfGroups', normalized);
|
||||
this.$emit('group-count-change');
|
||||
},
|
||||
onAdvancingPerGroupBlur(rawValue) {
|
||||
const normalized = this.normalizeNumberInput(rawValue, { min: 1, allowEmpty: false });
|
||||
this.$emit('update:advancingPerGroup', normalized);
|
||||
this.$emit('modus-change');
|
||||
},
|
||||
onMaxGroupSizeBlur(rawValue) {
|
||||
const normalized = this.normalizeNumberInput(rawValue, { min: 1, allowEmpty: true });
|
||||
this.$emit('update:maxGroupSize', normalized);
|
||||
},
|
||||
groupRankingsForGroup(group) {
|
||||
const key = `${group.groupId}-${group.classId ?? 'null'}`;
|
||||
return this.groupRankings[key] || [];
|
||||
},
|
||||
filterMatchesByClass(matches) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined || this.selectedViewClass === '' || this.selectedViewClass === '__all__' || this.selectedViewClass === 'all') {
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return matches;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
@@ -379,7 +240,7 @@ export default {
|
||||
},
|
||||
shouldShowClass(classId) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined || this.selectedViewClass === '' || this.selectedViewClass === '__all__' || this.selectedViewClass === 'all') {
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return true;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
@@ -416,14 +277,11 @@ export default {
|
||||
return classItem ? Boolean(classItem.isDoubles) : false;
|
||||
},
|
||||
getMatchLiveResult(player1Id, player2Id, groupId) {
|
||||
const id1 = (m) => m.player1?.id ?? m.player1Id;
|
||||
const id2 = (m) => m.player2?.id ?? m.player2Id;
|
||||
const match = this.matches.find(m =>
|
||||
m.round === 'group' &&
|
||||
m.groupId === groupId &&
|
||||
id1(m) != null && id2(m) != null &&
|
||||
((id1(m) === player1Id && id2(m) === player2Id) ||
|
||||
(id1(m) === player2Id && id2(m) === player1Id))
|
||||
((m.player1.id === player1Id && m.player2.id === player2Id) ||
|
||||
(m.player1.id === player2Id && m.player2.id === player1Id))
|
||||
);
|
||||
|
||||
if (!match) return null;
|
||||
@@ -473,9 +331,8 @@ export default {
|
||||
|
||||
return classes;
|
||||
},
|
||||
getLivePosition(playerId, group) {
|
||||
const groupId = group && typeof group === 'object' ? group.groupId : group;
|
||||
const groupPlayers = this.groupRankingsForGroup && group && typeof group === 'object' ? this.groupRankingsForGroup(group) : (this.groupRankings[groupId] || []);
|
||||
getLivePosition(playerId, groupId) {
|
||||
const groupPlayers = this.groupRankings[groupId] || [];
|
||||
const liveStats = groupPlayers.map(player => {
|
||||
let livePoints = player.points || 0;
|
||||
let liveSetsWon = player.setsWon || 0;
|
||||
@@ -484,13 +341,13 @@ export default {
|
||||
const playerMatches = this.matches.filter(m =>
|
||||
m.round === 'group' &&
|
||||
m.groupId === groupId &&
|
||||
((m.player1?.id ?? m.player1Id) === player.id || (m.player2?.id ?? m.player2Id) === player.id) &&
|
||||
(m.player1.id === player.id || m.player2.id === player.id) &&
|
||||
!m.isFinished &&
|
||||
m.tournamentResults && m.tournamentResults.length > 0
|
||||
);
|
||||
|
||||
playerMatches.forEach(match => {
|
||||
const isPlayer1 = (match.player1?.id ?? match.player1Id) === player.id;
|
||||
const isPlayer1 = match.player1.id === player.id;
|
||||
match.tournamentResults.forEach(result => {
|
||||
if (isPlayer1) {
|
||||
if (result.pointsPlayer1 > result.pointsPlayer2) {
|
||||
@@ -526,170 +383,9 @@ export default {
|
||||
});
|
||||
|
||||
const position = liveStats.findIndex(p => p.id === playerId) + 1;
|
||||
return position || (groupPlayers.findIndex(p => p.id === playerId) + 1) || 0;
|
||||
},
|
||||
handleMatchClick(player1Id, player2Id, groupId) {
|
||||
// Highlight das Match
|
||||
this.$emit('highlight-match', player1Id, player2Id, groupId);
|
||||
// Finde das Match und gehe zum Ergebnistab (player1/player2 können null sein, wenn Spieler gelöscht)
|
||||
const id1 = (m) => m.player1?.id ?? m.player1Id;
|
||||
const id2 = (m) => m.player2?.id ?? m.player2Id;
|
||||
const match = this.matches.find(m =>
|
||||
m.round === 'group' &&
|
||||
m.groupId === groupId &&
|
||||
id1(m) != null && id2(m) != null &&
|
||||
((id1(m) === player1Id && id2(m) === player2Id) ||
|
||||
(id1(m) === player2Id && id2(m) === player1Id))
|
||||
);
|
||||
if (match) {
|
||||
this.$emit('go-to-match', match.id);
|
||||
}
|
||||
},
|
||||
requestMergePools() {
|
||||
if (!this.mergeSourceClassId || !this.mergeTargetClassId) return;
|
||||
if (String(this.mergeSourceClassId) === String(this.mergeTargetClassId)) return;
|
||||
this.$emit('merge-pools', {
|
||||
sourceClassId: Number(this.mergeSourceClassId),
|
||||
targetClassId: Number(this.mergeTargetClassId),
|
||||
strategy: this.mergeStrategy,
|
||||
outOfCompetitionForSource: !!this.mergeSourceAsAK,
|
||||
});
|
||||
return position || groupPlayers.findIndex(p => p.id === playerId) + 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group-controls {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.groups-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.groups-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.groups-card-header h4 {
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.groups-card-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.groups-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.groups-settings {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.groups-settings label,
|
||||
.groups-per-class label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.groups-settings input,
|
||||
.groups-per-class input,
|
||||
.merge-pools-row select {
|
||||
padding: 0.55rem 0.7rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.groups-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.reset-controls-spaced {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.merge-pools-box {
|
||||
margin: 1rem 0 0 0;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
.merge-summary {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
.merge-pools-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
.merge-pools-row label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.merge-pools-actions {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
.groups-overview {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.group-table {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
padding: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.gave-up-badge {
|
||||
margin-left: 0.35rem;
|
||||
padding: 0.1rem 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.groups-card-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,43 +7,45 @@
|
||||
:selected-date="selectedDate"
|
||||
@update:modelValue="$emit('update:selectedViewClass', $event)"
|
||||
/>
|
||||
<section v-if="Object.keys(visibleFinalPlacementsByClass).length > 0" class="final-placements">
|
||||
<section v-if="Object.keys(finalPlacementsByClass).length > 0" class="final-placements">
|
||||
<h3>{{ $t('tournaments.finalPlacements') }}</h3>
|
||||
<div class="placements-summary">
|
||||
<span class="placements-chip">{{ Object.keys(visibleFinalPlacementsByClass).length }} {{ $t('tournaments.classes') }}</span>
|
||||
<span class="placements-chip">{{ totalVisibleFinalPlacements }} {{ $t('tournaments.tabPlacements') }}</span>
|
||||
</div>
|
||||
<template v-for="(classPlacements, classId) in visibleFinalPlacementsByClass" :key="`final-${classId}`">
|
||||
<div class="class-section">
|
||||
<template v-for="(classPlacements, classId) in finalPlacementsByClass" :key="`final-${classId}`">
|
||||
<div v-if="isAllSelected || shouldShowClass(classId==='null'?null:Number(classId))" class="class-section">
|
||||
<h4 class="class-header">
|
||||
{{ getClassName(classId) }}
|
||||
<span class="class-type-badge" v-if="classId!==null" :class="{ doubles: isDoubles(classId), singles: !isDoubles(classId) }">
|
||||
{{ isDoubles(classId) ? $t('tournaments.doubles') : $t('tournaments.singles') }}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="final-placement-list">
|
||||
<div v-for="(entry, entryIdx) in classPlacements" :key="`final-${classId}-${entryIdx}`" class="final-placement-card">
|
||||
<div class="final-placement-badge">{{ entry.position }}.</div>
|
||||
<button
|
||||
type="button"
|
||||
class="final-placement-player"
|
||||
@click="openPlayerDialog(entry)"
|
||||
:title="$t('tournaments.showPlayerDetails')"
|
||||
>
|
||||
{{ getEntryPlayerName(entry) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-place">{{ labelPlace }}</th>
|
||||
<th>{{ $t('tournaments.player') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(entry, entryIdx) in classPlacements" :key="`final-${classId}-${entryIdx}`">
|
||||
<td class="col-place">{{ entry.position }}.</td>
|
||||
<td>
|
||||
<span
|
||||
class="player-name-clickable"
|
||||
@click="openPlayerDialog(entry)"
|
||||
:title="$t('tournaments.showPlayerDetails')"
|
||||
>
|
||||
{{ getEntryPlayerName(entry) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
<section v-if="Object.keys(visibleGroupPlacementsByClass).length > 0" class="group-placements">
|
||||
<section v-if="groupPlacements.length > 0" class="group-placements">
|
||||
<h3>{{ $t('tournaments.groupPlacements') }}</h3>
|
||||
<div class="placements-summary">
|
||||
<span class="placements-chip">{{ visibleGroupPlacementCount }} {{ $t('tournaments.group') }}</span>
|
||||
</div>
|
||||
<template v-for="(classGroups, classId) in visibleGroupPlacementsByClass" :key="`group-${classId}`">
|
||||
<div class="class-section">
|
||||
<template v-for="(classGroups, classId) in groupPlacementsByClass" :key="`group-${classId}`">
|
||||
<div v-if="isAllSelected || shouldShowClass(classId==='null'?null:Number(classId))" class="class-section">
|
||||
<h4 class="class-header">
|
||||
{{ getClassName(classId) }}
|
||||
<span class="class-type-badge" v-if="classId!==null" :class="{ doubles: isDoubles(classId), singles: !isDoubles(classId) }">
|
||||
@@ -84,15 +86,8 @@
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
<div v-if="Object.keys(visibleFinalPlacementsByClass).length === 0 && Object.keys(visibleGroupPlacementsByClass).length === 0" class="no-placements">
|
||||
<p class="no-placements-title">{{ $t('tournaments.placementsPendingTitle') }}</p>
|
||||
<p class="no-placements-copy">{{ placementsEmptyMessage }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isMiniChampionship && (participants.length > 0 || externalParticipants.length > 0)" class="missing-data-pdf">
|
||||
<button @click="generateMissingDataPDF" class="btn-primary" :disabled="pdfLoading">
|
||||
{{ pdfLoading ? $t('tournaments.generatingPDF') : $t('tournaments.missingDataPDF') }}
|
||||
</button>
|
||||
<div v-if="Object.keys(finalPlacementsByClass).length === 0 && groupPlacements.length === 0" class="no-placements">
|
||||
<p>{{ $t('tournaments.noPlacementsYet') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Player Details Dialog -->
|
||||
@@ -110,9 +105,6 @@
|
||||
<script>
|
||||
import TournamentClassSelector from './TournamentClassSelector.vue';
|
||||
import PlayerDetailsDialog from './PlayerDetailsDialog.vue';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import apiClient from '../../apiClient.js';
|
||||
|
||||
export default {
|
||||
name: 'TournamentPlacementsTab',
|
||||
@@ -130,17 +122,15 @@ export default {
|
||||
groups: { type: Array, required: true },
|
||||
groupRankings: { type: Object, required: true },
|
||||
knockoutMatches: { type: Array, required: true },
|
||||
clubId: { type: [Number, String], required: true },
|
||||
isMiniChampionship: { type: Boolean, default: false }
|
||||
clubId: { type: [Number, String], required: true }
|
||||
},
|
||||
emits: ['update:selectedViewClass', 'show-info'],
|
||||
emits: ['update:selectedViewClass'],
|
||||
data() {
|
||||
return {
|
||||
showPlayerDialog: false,
|
||||
selectedPlayerId: null,
|
||||
selectedPlayerIsExternal: false,
|
||||
selectedPlayerName: '',
|
||||
pdfLoading: false
|
||||
selectedPlayerName: ''
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -169,7 +159,7 @@ export default {
|
||||
labelPlace() {
|
||||
const t = this.$t && this.$t('tournaments.place');
|
||||
if (t && typeof t === 'string' && t.trim().length > 0 && t !== 'tournaments.place') return t;
|
||||
return this.$t('tournaments.position');
|
||||
return 'Platz';
|
||||
},
|
||||
finalPlacementsByClass() {
|
||||
const byClass = {};
|
||||
@@ -419,11 +409,10 @@ export default {
|
||||
},
|
||||
groupPlacements() {
|
||||
const placements = [];
|
||||
// Primär: aus groups + groupRankings (Schlüssel groupId-classId bei Pool)
|
||||
// Primär: aus groups + groupRankings
|
||||
if ((this.groups || []).length > 0) {
|
||||
this.groups.forEach(group => {
|
||||
const key = `${group.groupId}-${group.classId ?? 'null'}`;
|
||||
const rankings = this.groupRankings[key] || [];
|
||||
const rankings = this.groupRankings[group.groupId] || [];
|
||||
if (rankings.length > 0) {
|
||||
placements.push({
|
||||
groupId: group.groupId,
|
||||
@@ -480,69 +469,13 @@ export default {
|
||||
groupPlacementsByClass() {
|
||||
const grouped = {};
|
||||
this.groupPlacements.forEach(p => {
|
||||
const key = p.classId != null ? String(p.classId) : 'null';
|
||||
const key = p.classId || 'null';
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = [];
|
||||
}
|
||||
grouped[key].push(p);
|
||||
});
|
||||
return grouped;
|
||||
},
|
||||
/** Endplatzierung nur anzeigen, wenn die Klasse mehr als 1 Gruppe hat (Endrunde nach Gruppenphase). */
|
||||
finalPlacementsByClassFiltered() {
|
||||
const raw = this.finalPlacementsByClass;
|
||||
const filtered = {};
|
||||
for (const [classKey, placements] of Object.entries(raw)) {
|
||||
const groupCount = (this.groupPlacementsByClass[classKey] || []).length;
|
||||
if (groupCount > 1) {
|
||||
filtered[classKey] = placements;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
visibleFinalPlacementsByClass() {
|
||||
const visible = {};
|
||||
for (const [classKey, placements] of Object.entries(this.finalPlacementsByClassFiltered)) {
|
||||
const normalizedClassId = classKey === 'null' ? null : Number(classKey);
|
||||
if (this.shouldShowClass(normalizedClassId)) {
|
||||
visible[classKey] = placements;
|
||||
}
|
||||
}
|
||||
return visible;
|
||||
},
|
||||
visibleGroupPlacementsByClass() {
|
||||
const visible = {};
|
||||
for (const [classKey, placements] of Object.entries(this.groupPlacementsByClass)) {
|
||||
const normalizedClassId = classKey === 'null' ? null : Number(classKey);
|
||||
if (this.shouldShowClass(normalizedClassId)) {
|
||||
visible[classKey] = placements;
|
||||
}
|
||||
}
|
||||
return visible;
|
||||
},
|
||||
totalFinalPlacements() {
|
||||
return Object.values(this.finalPlacementsByClassFiltered).reduce((sum, entries) => sum + entries.length, 0);
|
||||
},
|
||||
totalVisibleFinalPlacements() {
|
||||
return Object.values(this.visibleFinalPlacementsByClass).reduce((sum, entries) => sum + entries.length, 0);
|
||||
},
|
||||
visibleGroupPlacementCount() {
|
||||
return Object.values(this.visibleGroupPlacementsByClass).reduce((sum, entries) => sum + entries.length, 0);
|
||||
},
|
||||
placementsEmptyMessage() {
|
||||
if (!this.isAllSelected) {
|
||||
return this.$t('tournaments.placementsPendingSelectedClass');
|
||||
}
|
||||
if (Object.keys(this.groupPlacementsByClass).length === 0 && Object.keys(this.finalPlacementsByClassFiltered).length === 0) {
|
||||
return this.$t('tournaments.placementsPendingNoGroups');
|
||||
}
|
||||
if (Object.keys(this.groupPlacementsByClass).length === 0) {
|
||||
return this.$t('tournaments.placementsPendingGroups');
|
||||
}
|
||||
if (Object.keys(this.finalPlacementsByClassFiltered).length === 0) {
|
||||
return this.$t('tournaments.placementsPendingFinals');
|
||||
}
|
||||
return this.$t('tournaments.noPlacementsYet');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -581,256 +514,6 @@ export default {
|
||||
const c = (this.tournamentClasses || []).find(x => x.id === cid);
|
||||
return Boolean(c && c.isDoubles);
|
||||
},
|
||||
async generateMissingDataPDF() {
|
||||
this.pdfLoading = true;
|
||||
try {
|
||||
// 1. Top-3-Platzierte ermitteln
|
||||
const top3Names = new Set();
|
||||
const top3Ids = new Set();
|
||||
|
||||
// Aus K.O.-Endplatzierungen (nur gefilterte = echte K.O.-Runden)
|
||||
const hasKORound = Object.keys(this.finalPlacementsByClassFiltered).length > 0;
|
||||
if (hasKORound) {
|
||||
for (const [, placements] of Object.entries(this.finalPlacementsByClassFiltered)) {
|
||||
for (const entry of placements) {
|
||||
if (Number(entry.position) > 3) continue;
|
||||
if (entry.displayName) {
|
||||
entry.displayName.split('/').forEach(n => top3Names.add(n.trim()));
|
||||
} else if (entry.member) {
|
||||
const fn = (entry.member.firstName || '').trim();
|
||||
const ln = (entry.member.lastName || '').trim();
|
||||
if (fn || ln) top3Names.add(`${fn} ${ln}`.trim());
|
||||
if (entry.member.id != null) top3Ids.add(Number(entry.member.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aus Gruppen-Platzierungen (wenn keine K.O.-Runde vorhanden)
|
||||
if (!hasKORound) {
|
||||
for (const gp of this.groupPlacements) {
|
||||
for (const r of (gp.rankings || [])) {
|
||||
if (Number(r.position) > 3) continue;
|
||||
if (r.name) top3Names.add(r.name.trim());
|
||||
if (r.id != null) top3Ids.add(Number(r.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (top3Names.size === 0) {
|
||||
this.$emit('show-info', this.$t('messages.info'), this.$t('tournaments.noPlacementsYet'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Teilnehmer-Daten laden
|
||||
const allPlayerData = [];
|
||||
|
||||
// Interne Mitglieder laden
|
||||
let members = [];
|
||||
try {
|
||||
const res = await apiClient.get(`/clubmembers/get/${Number(this.clubId)}/true`);
|
||||
members = res.data || [];
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Mitglieder:', e);
|
||||
}
|
||||
|
||||
// Externe Teilnehmer laden
|
||||
let externals = [];
|
||||
try {
|
||||
const res = await apiClient.post('/tournament/external-participants', {
|
||||
clubId: Number(this.clubId),
|
||||
tournamentId: this.selectedDate,
|
||||
classId: null
|
||||
});
|
||||
externals = res.data || [];
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der externen Teilnehmer:', e);
|
||||
}
|
||||
|
||||
// Hilfsfunktion: prüft ob ein Spieler in den Top 3 ist
|
||||
const isTop3 = (name, id) => {
|
||||
if (id != null && top3Ids.has(Number(id))) return true;
|
||||
if (name && top3Names.has(name.trim())) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Interne Teilnehmer verarbeiten (nur Top 3)
|
||||
for (const p of this.participants) {
|
||||
const memberId = p.member?.id || p.clubMemberId;
|
||||
const member = members.find(m => m.id === memberId);
|
||||
if (!member) continue;
|
||||
|
||||
const name = `${member.firstName || ''} ${member.lastName || ''}`.trim();
|
||||
if (!isTop3(name, memberId)) continue;
|
||||
|
||||
let address = '';
|
||||
const parts = [];
|
||||
if (member.street) parts.push(member.street);
|
||||
if (member.postalCode) parts.push(member.postalCode);
|
||||
if (member.city) parts.push(member.city);
|
||||
address = parts.join(', ');
|
||||
|
||||
let email = '';
|
||||
if (member.contacts && Array.isArray(member.contacts)) {
|
||||
email = member.contacts.filter(c => c.type === 'email').map(c => c.value).join(', ');
|
||||
} else if (member.email) {
|
||||
email = member.email;
|
||||
}
|
||||
|
||||
let phone = '';
|
||||
if (member.contacts && Array.isArray(member.contacts)) {
|
||||
phone = member.contacts.filter(c => c.type === 'phone').map(c => c.value).join(', ');
|
||||
} else if (member.phone) {
|
||||
phone = member.phone;
|
||||
}
|
||||
|
||||
const gender = member.gender && member.gender !== 'unknown' ? member.gender : null;
|
||||
|
||||
allPlayerData.push({
|
||||
name,
|
||||
birthDate: member.birthDate || null,
|
||||
gender,
|
||||
address: address || null,
|
||||
email: email || null,
|
||||
phone: phone || null
|
||||
});
|
||||
}
|
||||
|
||||
// Externe Teilnehmer verarbeiten (nur Top 3)
|
||||
for (const ext of this.externalParticipants) {
|
||||
const found = externals.find(e => e.id === ext.id);
|
||||
const src = found || ext;
|
||||
const name = `${src.firstName || ''} ${src.lastName || ''}`.trim();
|
||||
if (!isTop3(name, ext.id)) continue;
|
||||
|
||||
const gender = src.gender && src.gender !== 'unknown' ? src.gender : null;
|
||||
|
||||
allPlayerData.push({
|
||||
name,
|
||||
birthDate: src.birthDate || null,
|
||||
gender,
|
||||
address: src.address || null,
|
||||
email: src.email || null,
|
||||
phone: src.phone || null
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Nur Teilnehmer mit fehlenden Daten filtern
|
||||
const fields = ['birthDate', 'gender', 'address', 'email', 'phone'];
|
||||
const playersWithMissing = allPlayerData.filter(p =>
|
||||
fields.some(f => !p[f])
|
||||
);
|
||||
|
||||
if (allPlayerData.length === 0) {
|
||||
this.$emit('show-info', this.$t('messages.info'), this.$t('tournaments.noTop3Yet'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (playersWithMissing.length === 0) {
|
||||
this.$emit('show-info', this.$t('messages.info'), this.$t('tournaments.allDataCompleteTop3'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Sortieren nach Name
|
||||
playersWithMissing.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// 3. PDF erzeugen
|
||||
const pdf = new jsPDF('l', 'mm', 'a4');
|
||||
const margin = 15;
|
||||
const t = this.$t;
|
||||
|
||||
// Titel
|
||||
pdf.setFontSize(16);
|
||||
pdf.setFont('helvetica', 'bold');
|
||||
pdf.text(t('tournaments.missingDataPDFTitleTop3'), margin, 20);
|
||||
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont('helvetica', 'normal');
|
||||
pdf.text(t('tournaments.missingDataPDFSubtitleTop3'), margin, 27);
|
||||
|
||||
// Formatierung der Felder
|
||||
const formatGender = (g) => {
|
||||
if (!g) return '';
|
||||
const map = { male: t('members.genderMale'), female: t('members.genderFemale'), diverse: t('members.genderDiverse') };
|
||||
return map[g] || g;
|
||||
};
|
||||
const formatDate = (d) => {
|
||||
if (!d) return '';
|
||||
try {
|
||||
const date = new Date(d);
|
||||
if (isNaN(date.getTime())) return d;
|
||||
return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
|
||||
} catch (e) { return d; }
|
||||
};
|
||||
|
||||
// Tabelle
|
||||
const head = [[
|
||||
t('members.firstName') + ' / ' + t('members.lastName'),
|
||||
t('members.birthdate'),
|
||||
t('members.gender'),
|
||||
t('tournaments.address'),
|
||||
t('tournaments.phone'),
|
||||
t('members.emailAddress')
|
||||
]];
|
||||
|
||||
const body = playersWithMissing.map(p => [
|
||||
p.name,
|
||||
p.birthDate ? formatDate(p.birthDate) : '',
|
||||
p.gender ? formatGender(p.gender) : '',
|
||||
p.address || '',
|
||||
p.phone || '',
|
||||
p.email || ''
|
||||
]);
|
||||
|
||||
autoTable(pdf, {
|
||||
startY: 32,
|
||||
margin: { left: margin, right: margin },
|
||||
head,
|
||||
body,
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 3,
|
||||
lineColor: [200, 200, 200],
|
||||
lineWidth: 0.25,
|
||||
valign: 'middle',
|
||||
minCellHeight: 10
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [76, 175, 80],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
fontSize: 9
|
||||
},
|
||||
columnStyles: {
|
||||
0: { cellWidth: 40 },
|
||||
1: { cellWidth: 28 },
|
||||
2: { cellWidth: 25 },
|
||||
3: { cellWidth: 50 },
|
||||
4: { cellWidth: 40 },
|
||||
5: { cellWidth: 50 }
|
||||
}
|
||||
});
|
||||
|
||||
// Fußzeile
|
||||
const pageCount = pdf.internal.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
pdf.setPage(i);
|
||||
pdf.setFontSize(8);
|
||||
pdf.setTextColor(150);
|
||||
pdf.text(
|
||||
`${t('tournaments.missingDataPDFTitleTop3')} – ${new Date().toLocaleDateString('de-DE')} – ${t('tournaments.page')} ${i}/${pageCount}`,
|
||||
margin,
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
pdf.save(`Fehlende_Daten_Minimeisterschaft_${new Date().toISOString().slice(0, 10)}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des PDFs:', error);
|
||||
} finally {
|
||||
this.pdfLoading = false;
|
||||
}
|
||||
},
|
||||
openPlayerDialog(entry) {
|
||||
console.log('[openPlayerDialog] entry:', entry);
|
||||
// Für Doppel-Paarungen können wir keine Details anzeigen
|
||||
@@ -961,24 +644,6 @@ export default {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.placements-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.placements-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.class-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
@@ -989,49 +654,6 @@ export default {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.final-placement-list {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.final-placement-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
padding: 0.8rem 0.95rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.final-placement-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
border-radius: 999px;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.final-placement-player {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: #1976d2;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.final-placement-player:hover {
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.class-type-badge {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
@@ -1094,25 +716,7 @@ th {
|
||||
.no-placements {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
border: 1px dashed #d1d5db;
|
||||
border-radius: 12px;
|
||||
background: #f9fafb;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.no-placements-title {
|
||||
margin: 0 0 0.35rem 0;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.no-placements-copy {
|
||||
margin: 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.missing-data-pdf {
|
||||
margin-top: 1.5rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.player-name-clickable {
|
||||
@@ -1130,3 +734,5 @@ table thead th:first-child,
|
||||
table tbody td:first-child {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,56 +7,13 @@
|
||||
:selected-date="selectedDate"
|
||||
@update:modelValue="$emit('update:selectedViewClass', $event)"
|
||||
/>
|
||||
<div class="results-summary">
|
||||
<span v-if="filteredGroupMatches.length" class="results-chip">{{ filteredGroupMatches.length }} {{ $t('tournaments.groupMatches') }}</span>
|
||||
<span v-if="filteredKnockoutMatches.length" class="results-chip">{{ filteredKnockoutMatches.length }} {{ $t('tournaments.koRound') }}</span>
|
||||
<span v-if="openMatchCount > 0" class="results-chip results-chip-open">{{ openMatchCount }} {{ $t('tournaments.statusOpen') }}</span>
|
||||
<span v-if="liveMatchCount > 0" class="results-chip results-chip-live">{{ liveMatchCount }} {{ $t('tournaments.statusLive') }}</span>
|
||||
<span v-if="finishedMatchCount > 0" class="results-chip">{{ finishedMatchCount }} {{ $t('tournaments.statusFinished') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="(canStartKnockout && !showKnockout && numberOfGroupsForSelectedClass > 1) || (showKnockout && canResetKnockout && numberOfGroupsForSelectedClass > 1)"
|
||||
class="results-primary-actions"
|
||||
>
|
||||
<button
|
||||
v-if="canStartKnockout && !showKnockout && numberOfGroupsForSelectedClass > 1"
|
||||
@click="$emit('start-knockout')"
|
||||
class="btn-primary"
|
||||
:disabled="knockoutOperationInProgress"
|
||||
>
|
||||
{{ $t('tournaments.startKORound') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="showKnockout && canResetKnockout && numberOfGroupsForSelectedClass > 1"
|
||||
@click="$emit('reset-knockout')"
|
||||
class="trash-btn"
|
||||
:disabled="knockoutOperationInProgress"
|
||||
>
|
||||
🗑️ {{ $t('tournaments.deleteKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="numberOfTables && (filteredGroupMatches.length || filteredKnockoutMatches.length)" class="distribute-tables-bar">
|
||||
<button @click="$emit('distribute-tables')" class="btn-primary">
|
||||
{{ $t('tournaments.distributeTables') }}
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="filteredGroupMatches.length" class="group-matches">
|
||||
<div class="results-section-header">
|
||||
<div class="results-section-title">
|
||||
<h4>{{ $t('tournaments.groupMatches') }}</h4>
|
||||
<span class="results-chip">{{ filteredGroupMatches.length }}</span>
|
||||
</div>
|
||||
<button type="button" class="section-toggle-btn" @click="groupMatchesCollapsed = !groupMatchesCollapsed">
|
||||
<span class="collapse-icon" :class="{ expanded: !groupMatchesCollapsed }">▾</span>
|
||||
{{ groupMatchesCollapsed ? collapseShowLabel : collapseHideLabel }}
|
||||
</button>
|
||||
</div>
|
||||
<table v-if="!groupMatchesCollapsed">
|
||||
<h4>{{ $t('tournaments.groupMatches') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('tournaments.round') }}</th>
|
||||
<th>{{ $t('tournaments.group') }}</th>
|
||||
<th v-if="numberOfTables">{{ $t('tournaments.table') }}</th>
|
||||
<th>{{ $t('tournaments.encounter') }}</th>
|
||||
<th>{{ $t('tournaments.result') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
@@ -74,22 +31,6 @@
|
||||
{{ $t('tournaments.groupNumber') }} {{ m.groupNumber }}
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="numberOfTables">
|
||||
<select
|
||||
:value="m.tableNumber || ''"
|
||||
@change="$emit('set-match-table', m, $event.target.value === '' ? null : parseInt($event.target.value))"
|
||||
class="table-select"
|
||||
:disabled="m.isFinished"
|
||||
>
|
||||
<option value="">–</option>
|
||||
<option
|
||||
v-for="t in numberOfTables"
|
||||
:key="t"
|
||||
:value="t"
|
||||
:disabled="occupiedTables.has(t) && m.tableNumber !== t"
|
||||
>{{ t }}{{ occupiedTables.has(t) && m.tableNumber !== t ? ' ●' : '' }}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="m.isFinished">
|
||||
<span v-if="winnerIsPlayer1(m)">
|
||||
@@ -107,10 +48,6 @@
|
||||
<template v-if="m.result === 'BYE'">
|
||||
BYE
|
||||
</template>
|
||||
<template v-else-if="matchHasGaveUp(m)">
|
||||
<span class="result-text gave-up-result">{{ formatResult(m) }}</span>
|
||||
<span v-if="m.player1?.gaveUp && m.player2?.gaveUp" class="gave-up-badge-small">({{ $t('tournaments.gaveUp') }})</span>
|
||||
</template>
|
||||
<template v-else-if="!m.isFinished">
|
||||
<template v-for="r in m.tournamentResults" :key="r.set">
|
||||
<template v-if="isEditing(m, r.set)">
|
||||
@@ -134,7 +71,7 @@
|
||||
<div class="new-set-line">
|
||||
<input
|
||||
v-model="m.resultInput"
|
||||
:placeholder="$t('tournaments.newSetPlaceholder')"
|
||||
placeholder="Neuen Satz, z.B. 11:7"
|
||||
@keyup.enter="$emit('save-match-result', m, m.resultInput)"
|
||||
@blur="$emit('save-match-result', m, m.resultInput)"
|
||||
class="inline-input"
|
||||
@@ -149,59 +86,37 @@
|
||||
{{ getSetsString(m) }}
|
||||
</td>
|
||||
<td>
|
||||
<span :class="['status-badge', getMatchStateClass(m)]">{{ getMatchStateLabel(m) }}</span>
|
||||
<template v-if="matchHasGaveUp(m)">
|
||||
<span class="no-edit-hint">{{ $t('tournaments.gaveUp') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="action-row">
|
||||
<button v-if="!m.isFinished" @click="$emit('finish-match', m)">{{ $t('tournaments.finishMatch') }}</button>
|
||||
<button v-else @click="$emit('reopen-match', m)" class="btn-correct">{{ $t('tournaments.correctMatch') }}</button>
|
||||
<button v-if="!m.isFinished && !m.isActive" @click.stop="$emit('set-match-active', m, true)" class="btn-live" :title="$t('tournaments.markMatchLive')">▶️</button>
|
||||
<button v-if="!m.isFinished && m.isActive" @click.stop="$emit('set-match-active', m, false)" class="btn-live active" :title="$t('tournaments.unmarkMatchLive')">⏸️</button>
|
||||
</div>
|
||||
</template>
|
||||
<button v-if="!m.isFinished" @click="$emit('finish-match', m)">Abschließen</button>
|
||||
<button v-else @click="$emit('reopen-match', m)" class="btn-correct">Korrigieren</button>
|
||||
<button v-if="!m.isFinished && !m.isActive" @click.stop="$emit('set-match-active', m, true)" class="btn-live" title="Als laufend markieren">▶️</button>
|
||||
<button v-if="!m.isFinished && m.isActive" @click.stop="$emit('set-match-active', m, false)" class="btn-live active" title="Laufend-Markierung entfernen">⏸️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<div v-if="participants.length > 1 && !filteredGroupMatches.length && !filteredKnockoutMatches.length" class="results-next-step">
|
||||
<button @click="$emit('start-matches')" class="btn-primary">
|
||||
<div v-if="participants.length > 1 && !filteredGroupMatches.length && !filteredKnockoutMatches.length" class="start-matches" style="margin-top:1.5rem">
|
||||
<button @click="$emit('start-matches')">
|
||||
{{ $t('tournaments.createMatches') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="canStartKnockout && !showKnockout && numberOfGroupsForSelectedClass > 1" class="results-next-step">
|
||||
<button @click="$emit('start-knockout')" class="btn-primary" :disabled="knockoutOperationInProgress">
|
||||
<div v-if="canStartKnockout && !showKnockout && numberOfGroupsForSelectedClass > 1" class="ko-start">
|
||||
<button @click="$emit('start-knockout')">
|
||||
{{ $t('tournaments.startKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="showKnockout && canResetKnockout && numberOfGroupsForSelectedClass > 1" class="results-next-step results-next-step-muted">
|
||||
<button @click="$emit('reset-knockout')" class="trash-btn" :disabled="knockoutOperationInProgress">
|
||||
<div v-if="showKnockout && canResetKnockout && numberOfGroupsForSelectedClass > 1" class="ko-reset" style="margin-top:1rem">
|
||||
<button @click="$emit('reset-knockout')" class="trash-btn">
|
||||
🗑️ {{ $t('tournaments.deleteKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="showKnockout && numberOfGroupsForSelectedClass > 1 && filteredKnockoutMatches.length" class="ko-round">
|
||||
<div class="results-section-header">
|
||||
<div class="results-section-title">
|
||||
<h4>{{ $t('tournaments.koRound') }}</h4>
|
||||
<span class="results-chip">{{ filteredKnockoutMatches.length }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="numberOfTables"
|
||||
type="button"
|
||||
class="btn-primary btn-inline-action"
|
||||
@click="$emit('distribute-tables')"
|
||||
>
|
||||
{{ $t('tournaments.distributeTables') }}
|
||||
</button>
|
||||
</div>
|
||||
<h4>{{ $t('tournaments.koRound') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('tournaments.class') }}</th>
|
||||
<th>{{ $t('tournaments.round') }}</th>
|
||||
<th v-if="numberOfTables">{{ $t('tournaments.table') }}</th>
|
||||
<th>{{ $t('tournaments.encounter') }}</th>
|
||||
<th>{{ $t('tournaments.result') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
@@ -212,22 +127,6 @@
|
||||
<tr v-for="m in filteredKnockoutMatches" :key="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<td>{{ getKnockoutMatchClassName(m) }}</td>
|
||||
<td>{{ m.round }}</td>
|
||||
<td v-if="numberOfTables">
|
||||
<select
|
||||
:value="m.tableNumber || ''"
|
||||
@change="$emit('set-match-table', m, $event.target.value === '' ? null : parseInt($event.target.value))"
|
||||
class="table-select"
|
||||
:disabled="m.isFinished"
|
||||
>
|
||||
<option value="">–</option>
|
||||
<option
|
||||
v-for="t in numberOfTables"
|
||||
:key="t"
|
||||
:value="t"
|
||||
:disabled="occupiedTables.has(t) && m.tableNumber !== t"
|
||||
>{{ t }}{{ occupiedTables.has(t) && m.tableNumber !== t ? ' ●' : '' }}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="m.isFinished">
|
||||
<span v-if="winnerIsPlayer1(m)">
|
||||
@@ -265,7 +164,7 @@
|
||||
<div class="new-set-line">
|
||||
<input
|
||||
v-model="m.resultInput"
|
||||
:placeholder="$t('tournaments.newSetPlaceholder')"
|
||||
placeholder="Neuen Satz, z.B. 11:7"
|
||||
@keyup.enter="$emit('save-match-result', m, m.resultInput)"
|
||||
@blur="$emit('save-match-result', m, m.resultInput)"
|
||||
class="inline-input"
|
||||
@@ -280,32 +179,29 @@
|
||||
{{ getSetsString(m) }}
|
||||
</td>
|
||||
<td>
|
||||
<span :class="['status-badge', getMatchStateClass(m)]">{{ getMatchStateLabel(m) }}</span>
|
||||
<div class="action-row">
|
||||
<button v-if="!m.isFinished" @click="$emit('finish-match', m)">{{ $t('tournaments.finishMatch') }}</button>
|
||||
<button v-else @click="$emit('reopen-match', m)" class="btn-correct">{{ $t('tournaments.correctMatch') }}</button>
|
||||
<button v-if="!m.isFinished && !m.isActive" @click.stop="$emit('set-match-active', m, true)" class="btn-live" :title="$t('tournaments.markMatchLive')">▶️</button>
|
||||
<button v-if="!m.isFinished && m.isActive" @click.stop="$emit('set-match-active', m, false)" class="btn-live active" :title="$t('tournaments.unmarkMatchLive')">⏸️</button>
|
||||
</div>
|
||||
<button v-if="!m.isFinished" @click="$emit('finish-match', m)">Fertig</button>
|
||||
<button v-else @click="$emit('reopen-match', m)" class="btn-correct">Korrigieren</button>
|
||||
<button v-if="!m.isFinished && !m.isActive" @click.stop="$emit('set-match-active', m, true)" class="btn-live" title="Als laufend markieren">▶️</button>
|
||||
<button v-if="!m.isFinished && m.isActive" @click.stop="$emit('set-match-active', m, false)" class="btn-live active" title="Laufend-Markierung entfernen">⏸️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section v-if="Object.keys(filteredGroupedRankingList).length > 0" class="ranking">
|
||||
<h4>{{ $t('tournaments.resultsRanking') }}</h4>
|
||||
<h4>Rangliste</h4>
|
||||
<template v-for="(classKey, idx) in Object.keys(filteredGroupedRankingList).sort((a, b) => {
|
||||
const aNum = a === 'null' ? 999999 : parseInt(a);
|
||||
const bNum = b === 'null' ? 999999 : parseInt(b);
|
||||
return aNum - bNum;
|
||||
})" :key="`class-${classKey}`">
|
||||
<div v-if="idx > 0" class="ranking-class-gap"></div>
|
||||
<div v-if="idx > 0" style="margin-top: 2rem;"></div>
|
||||
<h5 v-if="getClassName(classKey)">{{ getClassName(classKey) }}</h5>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('tournaments.position') }}</th>
|
||||
<th>{{ $t('tournaments.player') }}</th>
|
||||
<th>Platz</th>
|
||||
<th>Spieler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -372,10 +268,6 @@ export default {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
knockoutOperationInProgress: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
getTotalNumberOfGroups: {
|
||||
type: Number,
|
||||
required: true
|
||||
@@ -395,22 +287,18 @@ export default {
|
||||
pairings: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
numberOfTables: {
|
||||
type: [Number, null],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredGroupMatches() {
|
||||
return this.sortMatchesForDisplay(this.filterMatchesByClass(this.groupMatches));
|
||||
return this.filterMatchesByClass(this.groupMatches);
|
||||
},
|
||||
filteredKnockoutMatches() {
|
||||
return this.sortMatchesForDisplay(this.filterMatchesByClass(this.knockoutMatches));
|
||||
return this.filterMatchesByClass(this.knockoutMatches);
|
||||
},
|
||||
filteredGroupedRankingList() {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined || this.selectedViewClass === '' || this.selectedViewClass === '__all__' || this.selectedViewClass === 'all') {
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return this.groupedRankingList;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
@@ -433,17 +321,6 @@ export default {
|
||||
}
|
||||
return result;
|
||||
},
|
||||
occupiedTables() {
|
||||
// Tische, die von laufenden (nicht abgeschlossenen) Matches belegt sind
|
||||
const allMatches = [...this.groupMatches, ...this.knockoutMatches];
|
||||
const occupied = new Set();
|
||||
for (const m of allMatches) {
|
||||
if (m.tableNumber && !m.isFinished) {
|
||||
occupied.add(m.tableNumber);
|
||||
}
|
||||
}
|
||||
return occupied;
|
||||
},
|
||||
numberOfGroupsForSelectedClass() {
|
||||
// Zähle direkt die Gruppen für die ausgewählte Klasse
|
||||
// Nur Stage 1 Gruppen (stageId null/undefined) zählen
|
||||
@@ -454,7 +331,7 @@ export default {
|
||||
);
|
||||
|
||||
// Wenn keine Klasse ausgewählt ist, zähle alle Stage 1 Gruppen
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined || this.selectedViewClass === '' || this.selectedViewClass === '__all__' || this.selectedViewClass === 'all') {
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return groupsToCount.length;
|
||||
}
|
||||
|
||||
@@ -475,15 +352,6 @@ export default {
|
||||
}
|
||||
return Number(g.classId) === selectedId;
|
||||
}).length;
|
||||
},
|
||||
liveMatchCount() {
|
||||
return [...this.filteredGroupMatches, ...this.filteredKnockoutMatches].filter(m => m.isActive && !m.isFinished).length;
|
||||
},
|
||||
openMatchCount() {
|
||||
return [...this.filteredGroupMatches, ...this.filteredKnockoutMatches].filter(m => !m.isActive && !m.isFinished && !this.matchHasGaveUp(m)).length;
|
||||
},
|
||||
finishedMatchCount() {
|
||||
return [...this.filteredGroupMatches, ...this.filteredKnockoutMatches].filter(m => m.isFinished).length;
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
@@ -497,35 +365,14 @@ export default {
|
||||
'finish-match',
|
||||
'reopen-match',
|
||||
'set-match-active',
|
||||
'set-match-table',
|
||||
'distribute-tables',
|
||||
'start-matches',
|
||||
'start-knockout',
|
||||
'reset-knockout'
|
||||
],
|
||||
data() {
|
||||
const show = this.$t('common.show');
|
||||
const hide = this.$t('common.hide');
|
||||
return {
|
||||
groupMatchesCollapsed: false,
|
||||
collapseShowLabel: show && show !== 'common.show' ? show : 'Anzeigen',
|
||||
collapseHideLabel: hide && hide !== 'common.hide' ? hide : 'Ausblenden'
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
showKnockout: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (newValue && this.filteredGroupMatches.length > 0) {
|
||||
this.groupMatchesCollapsed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterMatchesByClass(matches) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined || this.selectedViewClass === '' || this.selectedViewClass === '__all__' || this.selectedViewClass === 'all') {
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return matches;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
@@ -545,25 +392,6 @@ export default {
|
||||
return Number(matchClassId) === selectedId;
|
||||
});
|
||||
},
|
||||
sortMatchesForDisplay(matches) {
|
||||
const stateWeight = (match) => {
|
||||
if (match.isActive && !match.isFinished) return 0;
|
||||
if (!match.isFinished && !this.matchHasGaveUp(match)) return 1;
|
||||
if (this.matchHasGaveUp(match)) return 2;
|
||||
return 3;
|
||||
};
|
||||
return [...matches].sort((a, b) => {
|
||||
const byState = stateWeight(a) - stateWeight(b);
|
||||
if (byState !== 0) return byState;
|
||||
const roundA = Number(a.groupRound || 0);
|
||||
const roundB = Number(b.groupRound || 0);
|
||||
if (roundA !== roundB) return roundA - roundB;
|
||||
const groupA = Number(a.groupNumber || 0);
|
||||
const groupB = Number(b.groupNumber || 0);
|
||||
if (groupA !== groupB) return groupA - groupB;
|
||||
return Number(a.id || 0) - Number(b.id || 0);
|
||||
});
|
||||
},
|
||||
getGroupClassName(groupId) {
|
||||
if (!groupId) return '';
|
||||
const group = this.groups.find(g => g.groupId === groupId);
|
||||
@@ -674,248 +502,7 @@ export default {
|
||||
isLastResult(match, result) {
|
||||
const arr = match.tournamentResults || [];
|
||||
return arr.length > 0 && arr[arr.length - 1].set === result.set;
|
||||
},
|
||||
matchHasGaveUp(match) {
|
||||
return match.gaveUpMatch || match.player1?.gaveUp || match.player2?.gaveUp;
|
||||
},
|
||||
getMatchStateLabel(match) {
|
||||
if (this.matchHasGaveUp(match)) return this.$t('tournaments.gaveUp');
|
||||
if (match.isFinished) return this.$t('tournaments.statusFinished');
|
||||
if (match.isActive) return this.$t('tournaments.statusLive');
|
||||
return this.$t('tournaments.statusOpen');
|
||||
},
|
||||
getMatchStateClass(match) {
|
||||
if (this.matchHasGaveUp(match)) return 'status-gaveup';
|
||||
if (match.isFinished) return 'status-finished';
|
||||
if (match.isActive) return 'status-live';
|
||||
return 'status-open';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.results-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.results-primary-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-color, #ffffff);
|
||||
}
|
||||
|
||||
.results-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: var(--background-soft);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.results-chip-live {
|
||||
background: rgba(46, 125, 50, 0.14);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.results-chip-open {
|
||||
background: rgba(181, 106, 29, 0.14);
|
||||
color: #8f5316;
|
||||
}
|
||||
|
||||
.results-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.results-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.results-section-header h4 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.section-toggle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 999px;
|
||||
background: var(--surface-color, #ffffff);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
display: inline-block;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.collapse-icon.expanded {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.btn-inline-action {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Farbmarkierungen für Spiele */
|
||||
.match-finished {
|
||||
background-color: var(--background-soft) !important;
|
||||
}
|
||||
|
||||
.match-finished td {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.match-live {
|
||||
background-color: rgba(46, 125, 50, 0.12) !important;
|
||||
}
|
||||
|
||||
.match-live:not(.match-finished) td {
|
||||
color: var(--success-color) !important;
|
||||
}
|
||||
|
||||
.match-finished.match-live {
|
||||
background-color: #e9ecef !important;
|
||||
}
|
||||
|
||||
.match-finished.match-live td {
|
||||
color: #626262 !important;
|
||||
}
|
||||
|
||||
/* Aktives Match hervorheben - auch wenn es abgeschlossen ist */
|
||||
.active-match {
|
||||
background-color: rgba(181, 106, 29, 0.12) !important;
|
||||
border-left: 3px solid #b56a1d !important;
|
||||
}
|
||||
|
||||
.active-match.match-finished {
|
||||
background-color: #fff3cd !important;
|
||||
}
|
||||
|
||||
.active-match.match-finished td {
|
||||
color: #8f5316 !important;
|
||||
}
|
||||
|
||||
.active-match.match-live {
|
||||
background-color: #fff3cd !important;
|
||||
}
|
||||
|
||||
.active-match.match-live td {
|
||||
color: #8f5316 !important;
|
||||
}
|
||||
|
||||
.gave-up-result {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.gave-up-badge-small {
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
.no-edit-hint {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.active-match:hover {
|
||||
background-color: rgba(181, 106, 29, 0.18) !important;
|
||||
}
|
||||
|
||||
.distribute-tables-bar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.results-next-step {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-color, #ffffff);
|
||||
}
|
||||
|
||||
.results-next-step-muted {
|
||||
border-color: var(--border-color);
|
||||
background: var(--background-soft);
|
||||
}
|
||||
|
||||
.ranking-class-gap {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 4.8rem;
|
||||
padding: 0.2rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.status-open {
|
||||
background: var(--background-soft);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-live {
|
||||
background: rgba(46, 125, 50, 0.14);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-finished {
|
||||
background: var(--primary-soft);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.status-gaveup {
|
||||
background: rgba(200, 76, 50, 0.14);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-select {
|
||||
width: 3.5rem;
|
||||
padding: 0.15rem 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-color, #ffffff);
|
||||
color: var(--text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
79
frontend/src/i18n/CEBUANO_TODO.md
Normal file
79
frontend/src/i18n/CEBUANO_TODO.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Cebuano (Bisaya) Lokalisierungsplan
|
||||
|
||||
Ziel: Cebuano (ISO: `ceb`) als zusätzliche Sprache hinzufügen, so dass sämtliche Labels, Buttons und Texte in der App in Cebuano verfügbar sind.
|
||||
|
||||
Kurzübersicht der Schritte (technisch):
|
||||
|
||||
- Basis-Sprache: `de.json` als Ausgangsbasis verwenden.
|
||||
- Neue Locale-Datei: `frontend/src/i18n/locales/ceb.json` erstellen (anfangs Kopie von `de.json`).
|
||||
- `frontend/src/i18n/index.js` anpassen: Import, `messages['ceb']` mit `deepMergeMessages(de, ceb)` und `languageMap`-Einträge (`ceb`, `ceb-PH`).
|
||||
- UI: `frontend/src/views/PersonalSettings.vue` -> `languageCodes` um `ceb` ergänzen.
|
||||
- Sprachanzeige: In allen `locales/*.json` unter `languages` einen Eintrag `"ceb": "Cebuano (Bisaya)"` anlegen, damit die Sprache im Selector sichtbar ist.
|
||||
- Audit: `npm run i18n:audit` im `frontend` ausführen und alle fehlenden Keys bereinigen.
|
||||
- Review: Muttersprachliche Prüfung + UI-Tests.
|
||||
|
||||
Konkrete Arbeitsschritte (ToDos):
|
||||
|
||||
1) Basis-Keys sammeln (in-progress)
|
||||
- Datei: `frontend/src/i18n/locales/de.json` als Referenz nehmen
|
||||
- Liste der Namespaces (z.B. `common`, `navigation`, `settings`, ...) erfassen
|
||||
|
||||
2) Locale-Datei erstellen
|
||||
- Erstelle `frontend/src/i18n/locales/ceb.json` als Kopie von `de.json`.
|
||||
- Setze Kommentar-Header mit Hinweis "TRANSLATION_PENDING" für automatische Skripte optional.
|
||||
|
||||
3) Übersetzung
|
||||
- Übersetze alle Keys in `ceb.json` ins Cebuano.
|
||||
- Priorität 1: Buttons, Formulare, Navigation, Settings, Fehlermeldungen.
|
||||
- Priorität 2: längere Hilfetexte, Beschreibungen, Tutorial-Texte.
|
||||
|
||||
4) Integration
|
||||
- `frontend/src/i18n/index.js`
|
||||
- `import ceb from './locales/ceb.json';`
|
||||
- `messages: { 'ceb': deepMergeMessages(de, ceb), ... }`
|
||||
- `languageMap['ceb'] = 'ceb'; languageMap['ceb-PH']='ceb';`
|
||||
|
||||
5) Selector & sichtbarer Name
|
||||
- `frontend/src/views/PersonalSettings.vue` `languageCodes` um `'ceb'` ergänzen.
|
||||
- In allen bestehenden Locale-Dateien (`frontend/src/i18n/locales/*.json`) unter `languages` folgenden Eintrag ergänzen:
|
||||
|
||||
```json
|
||||
"ceb": "Cebuano (Bisaya)"
|
||||
```
|
||||
|
||||
6) Audit & Lint
|
||||
- Im `frontend`-Ordner ausführen:
|
||||
|
||||
```bash
|
||||
npm install # falls Abhängigkeiten fehlen
|
||||
npm run i18n:audit
|
||||
```
|
||||
|
||||
- Audit-Ausgabe prüfen: fehlende Keys, leere Werte, Platzhalter-Mismatch.
|
||||
- Fehlende Keys in `ceb.json` ergänzen (vorläufig auf Deutsch kopieren, mit TODO-Marker).
|
||||
|
||||
7) Review & QA
|
||||
- Muttersprachliche Überprüfung (Proofreading).
|
||||
- UI-Tests: Navigation, Dialoge, Formulare, Fehlerfälle prüfen.
|
||||
|
||||
8) Finalisieren
|
||||
- Übersetzungen final einpflegen.
|
||||
- PR erstellen, Review, Merge.
|
||||
- Build & Smoke-Test: im `frontend` `npm run build` ausführen und kurze Regressionstest-Sequenz.
|
||||
|
||||
Hinweise und Empfehlungen
|
||||
|
||||
- Locale-Code: Verwende `ceb` (ISO 639-3). Falls du eine Region-spezifische Variante willst, nutze `ceb-PH` in `languageMap`.
|
||||
- Übersetzungsworkflow: Für erste Iteration kann `@vitalets/google-translate-api` (bereits vorhanden) helfen, dann menschliches Review.
|
||||
- Automatisches Füllen: Es gibt Skripte in `frontend/package.json` (`i18n:fill`) — bei Bedarf nutzen, aber immer prüfen.
|
||||
|
||||
Nützliche Pfade
|
||||
|
||||
- Sprachdateien: `frontend/src/i18n/locales/`
|
||||
- i18n-Entrypoint: `frontend/src/i18n/index.js`
|
||||
- Language selector: `frontend/src/views/PersonalSettings.vue`
|
||||
- Audit-Skript: `frontend/scripts/audit-i18n.js` (Aufruf via `npm run i18n:audit`)
|
||||
|
||||
Mögliche nächste Aktion (auf Wunsch): Ich lege `frontend/src/i18n/locales/ceb.json` als Kopie von `de.json` an und passe `index.js` + `PersonalSettings.vue` vorläufig an.
|
||||
|
||||
— Ende —
|
||||
@@ -16,6 +16,7 @@ import zh from './locales/zh.json';
|
||||
import tl from './locales/tl.json';
|
||||
import th from './locales/th.json';
|
||||
import fil from './locales/fil.json';
|
||||
import ceb from './locales/ceb.json';
|
||||
|
||||
function deepMergeMessages(base, override) {
|
||||
if (!base || typeof base !== 'object' || Array.isArray(base)) {
|
||||
@@ -67,6 +68,8 @@ const languageMap = {
|
||||
'th-TH': 'th',
|
||||
'fil': 'fil',
|
||||
'fil-PH': 'fil',
|
||||
'ceb': 'ceb',
|
||||
'ceb-PH': 'ceb',
|
||||
};
|
||||
|
||||
// Detect browser language
|
||||
@@ -127,6 +130,7 @@ const i18n = createI18n({
|
||||
'tl': deepMergeMessages(de, tl),
|
||||
'th': deepMergeMessages(de, th),
|
||||
'fil': deepMergeMessages(de, fil),
|
||||
'ceb': deepMergeMessages(de, ceb),
|
||||
},
|
||||
legacy: true, // Use Options API mode (required for $t in templates with Options API)
|
||||
// globalInjection is automatically enabled with legacy: true
|
||||
|
||||
542
frontend/src/i18n/locales/ceb.json
Normal file
542
frontend/src/i18n/locales/ceb.json
Normal file
@@ -0,0 +1,542 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Talaan sa Pag-ensayo",
|
||||
"title": "Talaan sa Pag-ensayo"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Nag-load...",
|
||||
"save": "Iluwas",
|
||||
"saved": "Na-save",
|
||||
"cancel": "Ikansela",
|
||||
"delete": "Tangtanga",
|
||||
"edit": "Usba",
|
||||
"add": "Idugang",
|
||||
"close": "Sirado",
|
||||
"confirm": "Kumpirma",
|
||||
"yes": "Oo",
|
||||
"no": "Dili",
|
||||
"search": "Pangitaa",
|
||||
"filter": "Filter",
|
||||
"actions": "Mga aksyon",
|
||||
"back": "Balik",
|
||||
"next": "Sunod",
|
||||
"previous": "Miaging",
|
||||
"submit": "Isumite",
|
||||
"reset": "I-reset",
|
||||
"all": "Tanan",
|
||||
"today": "Karon",
|
||||
"time": "Oras",
|
||||
"new": "Bago",
|
||||
"update": "I-update",
|
||||
"move": "Ibalhin",
|
||||
"refresh": "I-reload",
|
||||
"create": "Paghimo",
|
||||
"remove": "Pagtangtang",
|
||||
"select": "Pilia",
|
||||
"download": "I-download",
|
||||
"choose": "Pilia",
|
||||
"apply": "I-aplikar",
|
||||
"clear": "Hawanon",
|
||||
"details": "Detalye",
|
||||
"view": "Ipakita",
|
||||
"name": "Ngalan",
|
||||
"date": "Petsa",
|
||||
"status": "Status",
|
||||
"type": "Klase",
|
||||
"description": "Deskripsyon",
|
||||
"active": "Aktibo",
|
||||
"inactive": "Dili aktibo",
|
||||
"enabled": "Gipa-aktibar",
|
||||
"disabled": "Gi-deactivate",
|
||||
"required": "Kinahanglanon",
|
||||
"optional": "Opsyonal",
|
||||
"in": "sa",
|
||||
"min": "Min",
|
||||
"minutes": "Minuto",
|
||||
"hours": "Oras",
|
||||
"days": "Adlaw",
|
||||
"weeks": "Semana",
|
||||
"months": "Bulan",
|
||||
"years": "Tuig",
|
||||
"ok": "OK",
|
||||
"period": "Panahon",
|
||||
"saving": "Gina-save..."
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Kalendaryo sa Klab",
|
||||
"title": "Kalendaryo",
|
||||
"description": "Mga adlaw sa pag-ensayo, mga torneo sa klab ug mga duwa sa matag bulan.",
|
||||
"today": "Karon",
|
||||
"loading": "Gina-load ang datos sa kalendaryo...",
|
||||
"noClub": "Palihug pilia usa ka klab una.",
|
||||
"loadError": "Dili maload ang datos sa kalendaryo.",
|
||||
"sourceWarning": "Dili maload ang {source}",
|
||||
"agendaTitle": "Mga kalihokan sa bulan",
|
||||
"agendaEmpty": "Walay kalihokan karong bulan.",
|
||||
"recurringTrainingTime": "Regular nga oras sa pag-ensayo",
|
||||
"participants": "{count} ka partisipante",
|
||||
"starts": "{count} ka pagsugod",
|
||||
"options": {
|
||||
"title": "Mga opsyon",
|
||||
"subtitle": "Mga kanselasyon & kaugalingong kalihokan"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Pag-ensayo",
|
||||
"tournament": "Torneo",
|
||||
"officialTournament": "Opisyal nga pag-apil",
|
||||
"match": "Duwa",
|
||||
"holiday": "Piyesta",
|
||||
"schoolHoliday": "Bakasyon sa eskwelahan",
|
||||
"trainingCancellation": "Nakansela",
|
||||
"customEvent": "Kalihokan"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Gi-kansela ang pag-ensayo",
|
||||
"description": "Gatabon ang regular nga oras sa pag-ensayo.",
|
||||
"date": "Petsa",
|
||||
"untilOptional": "Hangtod (opsyonal)",
|
||||
"trainingGroups": "Mga grupo sa pag-ensayo",
|
||||
"reasonPlaceholder": "Rason (opsyonal)",
|
||||
"saving": "Gina-save...",
|
||||
"submit": "Isumite",
|
||||
"fallbackTitle": "Gi-kansela ang pag-ensayo",
|
||||
"subtitle": "Kanselasyon sa pag-ensayo"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Kaugalingong kalihokan",
|
||||
"description": "Mga adlaw sa club, miting, internal nga mga kalihokan, ...",
|
||||
"titlePlaceholder": "Titulo",
|
||||
"categoryPlaceholder": "Kategorya (opsyonal)",
|
||||
"saving": "Gina-save...",
|
||||
"submit": "Himoa",
|
||||
"subtitleFallback": "Kaugalingong kalihokan"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Kanselasyon sa pag-ensayo",
|
||||
"message": "I-mark ba ang „{title}“ isip kanselasyon sa pag-ensayo?",
|
||||
"useWholeRange": "I-aplikar alang sa tibuok panahon ({start} hangtod {end})",
|
||||
"trainingGroups": "Apektadong mga grupo sa pag-ensayo",
|
||||
"confirm": "Isumite ang kanselasyon",
|
||||
"noGroups": "Walay mga grupo sa pag-ensayo nga adunay nakatakda nga oras."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Mga adlaw sa pag-ensayo",
|
||||
"trainingTimes": "Oras sa pag-ensayo",
|
||||
"trainingCancellations": "Mga kanselasyon sa pag-ensayo",
|
||||
"customEvents": "Kaugalingong kalihokan",
|
||||
"tournaments": "Mga torneo",
|
||||
"officialTournaments": "Opisyal nga partisipasyon sa torneo",
|
||||
"matches": "Mga duwa",
|
||||
"holidays": "Bakasyon/Piyesta"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Pag-ensayo",
|
||||
"tournament": "Torneo",
|
||||
"officialTournament": "Opisyal nga pag-apil sa torneo",
|
||||
"match": "Duwa"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open nga torneo",
|
||||
"club": "Torneo sa klab"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Bisita"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Piyesta",
|
||||
"schoolHoliday": "Bakasyon sa eskwelahan"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Lu",
|
||||
"tuesday": "Ma",
|
||||
"wednesday": "Mi",
|
||||
"thursday": "Hu",
|
||||
"friday": "Bi",
|
||||
"saturday": "Sa",
|
||||
"sunday": "Do"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "I-undo ang kanselasyon",
|
||||
"message": "Gusto ba nimo i-undo ang kanselasyon '{title}' sa {date}?",
|
||||
"confirm": "I-undo ang kanselasyon"
|
||||
}
|
||||
},
|
||||
"unknown": "Walay nahibaloan",
|
||||
"navigation": {
|
||||
"home": "Balay",
|
||||
"members": "Mga miyembro",
|
||||
"billing": "Pagbayad",
|
||||
"orders": "Mga order",
|
||||
"diary": "Diyaryo",
|
||||
"approvals": "Mga aprobasyon",
|
||||
"statistics": "Estadistika sa Pag-ensayo",
|
||||
"tournaments": "Mga torneo",
|
||||
"clubTournaments": "Torneo sa Klab",
|
||||
"tournamentParticipations": "Opisyal nga pag-apil sa torneo",
|
||||
"schedule": "Skedyul",
|
||||
"clubSettings": "Mga setting sa klab",
|
||||
"predefinedActivities": "Predefined nga mga kalihokan",
|
||||
"teamManagement": "Pagdumala sa team",
|
||||
"permissions": "Mga permiso",
|
||||
"logs": "System logs",
|
||||
"memberTransfer": "Pagbalhin sa miyembro",
|
||||
"myTischtennisAccount": "myTischtennis account",
|
||||
"clickTtAccount": "HTTV / click-TT account",
|
||||
"clickTtBrowser": "HTTV / click-TT",
|
||||
"personalSettings": "Personal nga setting",
|
||||
"logout": "Pag-logout",
|
||||
"login": "Pag-login",
|
||||
"register": "Magrehistro",
|
||||
"dailyBusiness": "Adlaw-adlaw nga buluhaton",
|
||||
"competitions": "Kompetisyon",
|
||||
"settings": "Mga setting",
|
||||
"backToHome": "Balik sa balay"
|
||||
},
|
||||
"club": {
|
||||
"select": "Pilia ang klab",
|
||||
"selectPlaceholder": "Pilia ang klab...",
|
||||
"new": "Bag-ong klab",
|
||||
"load": "I-load",
|
||||
"name": "Ngalan sa klab",
|
||||
"create": "Paghimo og klab",
|
||||
"createTitle": "Paghimo og klab",
|
||||
"members": "Mga miyembro",
|
||||
"trainingDiary": "Talaan sa Pag-ensayo",
|
||||
"noAccess": "Wala pa kay access sa niining klab.",
|
||||
"requestAccess": "Mangayo og access",
|
||||
"openRequests": "Mga bukas nga hangyo sa access",
|
||||
"title": "Klab",
|
||||
"openAccessRequests": "Mga bukas nga hangyo sa access",
|
||||
"diary": "Talaan sa Pag-ensayo",
|
||||
"accessDenied": "Wala gitugotan ang access sa klab.",
|
||||
"errorLoadingRequests": "Napakyas sa pag-load sa mga bukas nga hangyo",
|
||||
"accessRequested": "Gipangayo na ang access.",
|
||||
"accessRequestPending": "Ang access alang sa klab gi-request. Palihug hulata gamay.",
|
||||
"accessRequestFailed": "Napakyas ang pag-submit sa hangyo sa access.",
|
||||
"mobileSelectHint": "Palihug pilia usa ka klab una aron magamit ang app sa smartphone."
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"register": "Magrehistro",
|
||||
"email": "E-mail",
|
||||
"password": "Password",
|
||||
"forgotPassword": "Nakalimot sa password?",
|
||||
"rememberMe": "Pabilin nga naka-login",
|
||||
"loginSuccess": "Malampusong naka-login",
|
||||
"logoutSuccess": "Malampusong naka-logout",
|
||||
"sessionExpired": "Namutang na ang imong sesyon. I-logout ka.",
|
||||
"noAccount": "Wala pa kay account?",
|
||||
"hasAccount": "Naa na kay account?",
|
||||
"toLogin": "Adto sa login",
|
||||
"loginFailed": "Napakyas ang login. Palihug susiha ang imong datos.",
|
||||
"registerSuccess": "Malampusong narehistro! Palihug susiha ang imong email aron i-activate ang account.",
|
||||
"registerFailed": "Napakyas ang pagrehistro. Palihug sulayi pag-usab.",
|
||||
"activate": "I-activate",
|
||||
"activateAccount": "I-activate ang account",
|
||||
"accountActivated": "Na-activate ang account! Maka-login na ka karon.",
|
||||
"activationFailed": "Napakyas ang activation. Palihug tan-awa ang link o sulayi pag-usab.",
|
||||
"forgotPasswordDescription": "Isulod ang imong email. Makadawat ka ug link aron i-reset ang password.",
|
||||
"sendResetLink": "Ipadala ang link",
|
||||
"sending": "Gina-padala...",
|
||||
"resetEmailSent": "Kon adunay account nga naggamit niini nga email, gipadala ang reset link. Palihug tan-awa ang inbox (apil ang spam).",
|
||||
"resetRequestFailed": "Napakyas ang hangyo. Palihug sulayi pag-usab.",
|
||||
"resetPassword": "I-reset ang password",
|
||||
"newPassword": "Bag-ong password",
|
||||
"confirmPassword": "Kumpirma ang password",
|
||||
"saveNewPassword": "I-save ang password",
|
||||
"saving": "Gina-save...",
|
||||
"passwordResetSuccess": "Malampusong nabag-o ang imong password. Maka-login na ka gamit ang bag-ong password.",
|
||||
"passwordsDoNotMatch": "Ang mga password wala magtugma.",
|
||||
"passwordTooShort": "Ang password kinahanglan labing menos 6 ka karakter.",
|
||||
"resetFailed": "Dili mabag-o ang password. Posible nga ang link wala na magamit."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Mga setting",
|
||||
"personalSettings": "Personal nga mga setting",
|
||||
"language": "Pinulongan",
|
||||
"languageDescription": "Pilia ang imong gusto nga pinulongan para sa app",
|
||||
"languageChanged": "Malampusong nabag-o ang pinulongan",
|
||||
"selectLanguage": "Pilia ang pinulongan"
|
||||
},
|
||||
"languages": {
|
||||
"de": "Aleman (Deutsch)",
|
||||
"en-GB": "Iningles (GB)",
|
||||
"en-US": "Iningles (US)",
|
||||
"en-AU": "Iningles (AU)",
|
||||
"de-CH": "Swiss Aleman (Schwiizerdütsch)",
|
||||
"fr": "Pranses (Français)",
|
||||
"es": "Espanyol (Español)",
|
||||
"it": "Italiano (Italiano)",
|
||||
"pl": "Polako (Polski)",
|
||||
"ja": "Hapon (日本語)",
|
||||
"zh": "Tsino (中文)",
|
||||
"tl": "Tagalog",
|
||||
"th": "Thai (ไทย)",
|
||||
"fil": "Filipino",
|
||||
"ceb": "Cebuano (Binisaya)"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Kalampusan",
|
||||
"error": "Sayop",
|
||||
"warning": "Pahimangno",
|
||||
"info": "Impormasyon",
|
||||
"confirm": "Kumpirma",
|
||||
"cancel": "Ikansela",
|
||||
"successfullyLoaded": "Malampuson nga na-load",
|
||||
"recordsFound": "Nakitang rekord",
|
||||
"recordsDisplayed": "gipakita",
|
||||
"invalidFile": "Dili balido nga file",
|
||||
"pleaseSelectImageFile": "Palihug pili ug image file.",
|
||||
"fileTooLarge": "Dako kaayo ang file",
|
||||
"imageMaxSize": "Ang hulagway kinahanglan dili molapas sa 5 MB.",
|
||||
"note": "Pahibalo"
|
||||
},
|
||||
"members": {
|
||||
"title": "Mitglieder",
|
||||
"memberInfo": "Mitglieder-Info",
|
||||
"activeMembers": "Aktive Mitglieder",
|
||||
"testMembers": "Testmitglieder",
|
||||
"inactiveMembers": "Inaktive Mitglieder",
|
||||
"generatePhoneList": "Telefonliste generieren",
|
||||
"groupPhotoCrop": "Gruppenfoto verarbeiten",
|
||||
"groupPhotoCropSaved": "Mitgliedsfoto aus Gruppenfoto gespeichert.",
|
||||
"onlyActiveMembers": "Es werden nur aktive Mitglieder ausgegeben",
|
||||
"updateRatings": "TTR/QTTR von myTischtennis aktualisieren",
|
||||
"updating": "Aktualisiere...",
|
||||
"transferMembers": "Mitglieder übertragen",
|
||||
"newMember": "Neues Mitglied",
|
||||
"editMember": "Mitglied bearbeiten",
|
||||
"createNewMember": "Neues Mitglied anlegen",
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
"street": "Straße",
|
||||
"postalCode": "PLZ",
|
||||
"city": "Ort",
|
||||
"birthdate": "Geburtsdatum",
|
||||
"phones": "Telefonnummern",
|
||||
"emails": "E-Mail-Adressen",
|
||||
"addPhone": "Telefonnummer hinzufügen",
|
||||
"addEmail": "E-Mail-Adresse hinzufügen",
|
||||
"phoneNumber": "Telefonnummer",
|
||||
"emailAddress": "E-Mail-Adresse",
|
||||
"parent": "Elternteil",
|
||||
"parentName": "Name (z.B. Mutter, Vater)",
|
||||
"primary": "Primär",
|
||||
"gender": "Geschlecht",
|
||||
"genderUnknown": "Unbekannt",
|
||||
"genderMale": "Männlich",
|
||||
"genderFemale": "Weiblich",
|
||||
"genderDiverse": "Divers",
|
||||
"picsInInternetAllowed": "Pics in Internet erlaubt",
|
||||
"testMembership": "Testmitgliedschaft",
|
||||
"memberFormHandedOver": "Mitgliedsformular ausgehändigt",
|
||||
"adultReleaseApproved": "Freigabe Erwachsene",
|
||||
"adultReserveApproved": "Ersatz bei Erwachsenen",
|
||||
"wantsToPlay": "Will spielen",
|
||||
"trainingGroups": "Trainingsgruppen",
|
||||
"noGroupsAssigned": "Keine Gruppen zugeordnet",
|
||||
"noGroupsAvailable": "Keine Gruppen verfügbar",
|
||||
"addGroup": "Gruppe hinzufügen...",
|
||||
"remove": "Entfernen",
|
||||
"image": "Bild",
|
||||
"selectFile": "Datei auswählen",
|
||||
"camera": "Kamera",
|
||||
"imagePreview": "Vorschau des Mitgliedsbildes",
|
||||
"change": "Ändern",
|
||||
"create": "Anlegen",
|
||||
"clearFields": "Felder leeren",
|
||||
"showInactiveMembers": "Inaktive Mitglieder anzeigen",
|
||||
"showTrainingParticipationsColumn": "Spalte „Trainingsteilnahmen“ anzeigen",
|
||||
"ageGroup": "Altersklasse",
|
||||
"ttSeasonFilter": "Saison (Stichtag)",
|
||||
"ttSeasonCurrentTag": "aktuell",
|
||||
"ttSeasonNextTag": "kommend",
|
||||
"ttStichtagHint": "Stichtag 1.1. (DTTB). Jungen: nur J-Klassen. Mädchen: J und M möglich.",
|
||||
"lastTrainingFilter": "Letztes Training",
|
||||
"lastTrainingFilterHasDate": "Mit erfasstem Datum",
|
||||
"lastTrainingFilterNoDate": "Ohne letztes Training",
|
||||
"lastTrainingFilterNotInTraining": "„Nicht mehr im Training“",
|
||||
"lastTrainingFilterHint": "AK-Spalte: laufende und nächste Saison. Weitere Details (letztes Training, Teilnahmen) auch beim Überfahren der Zeile.",
|
||||
"rowTooltipSeparator": "·",
|
||||
"ttAgeClassCol": "AK (TT)",
|
||||
"ttAdult": "Erwachsene (kein Jugend nach Stichtag)",
|
||||
"ttFilterGroupJ": "Mädchen & Jungen (gemischt)",
|
||||
"ttFilterGroupM": "Mädchen (nur weiblich)",
|
||||
"adults": "Erwachsene (18+)",
|
||||
"j19": "J19",
|
||||
"j17": "J17 (17 und jünger)",
|
||||
"j15": "J15",
|
||||
"j13": "J13",
|
||||
"j11": "J11",
|
||||
"j9": "J9",
|
||||
"m19": "M19",
|
||||
"m15": "M15",
|
||||
"m13": "M13",
|
||||
"m11": "M11",
|
||||
"m9": "M9",
|
||||
"clearFilters": "Filter zurücksetzen",
|
||||
"imageInternet": "Bild (Inet?)",
|
||||
"testMember": "Testm.",
|
||||
"name": "Name, Vorname",
|
||||
"ttrQttr": "TTR / QTTR",
|
||||
"address": "Adresse",
|
||||
"active": "Aktiv",
|
||||
"actions": "Aktionen",
|
||||
"phoneNumberShort": "Telefon-Nr.",
|
||||
"emailAddressShort": "Email-Adresse",
|
||||
"trainingParticipations": "Trainingsteilnahmen",
|
||||
"memberImage": "Mitgliedsbild",
|
||||
"inactive": "inaktiv",
|
||||
"sixOrMoreParticipations": "6 oder mehr Trainingsteilnahmen",
|
||||
"threeOrMoreParticipations": "3 oder mehr Trainingsteilnahmen",
|
||||
"noTestMembership": "Keine Testmitgliedschaft mehr",
|
||||
"formHandedOver": "Mitgliedsformular ausgehändigt",
|
||||
"deactivateMember": "Mitglied deaktivieren",
|
||||
"notes": "Notizen",
|
||||
"exercises": "Übungen",
|
||||
"memberImages": "Mitgliedsbilder",
|
||||
"testMembershipRemoved": "Gikuha ang test nga pagka-miyembro.",
|
||||
"errorRemovingTestMembership": "Napakyas sa pagtangtang sa test nga pagka-miyembro.",
|
||||
"formMarkedAsHandedOver": "Gimarkahan nga nadawat ang porma sa miyembro.",
|
||||
"errorMarkingForm": "Napakyas sa pagmarka sa porma.",
|
||||
"deactivateMemberTitle": "I-deactivate ang miyembro",
|
||||
"deactivateMemberConfirm": "Gusto ba nimo i-deactivate si \"{name}\"?",
|
||||
"memberDeactivated": "Na-deactivate ang miyembro",
|
||||
"errorDeactivatingMember": "Napakyas sa pag-deactivate sa miyembro",
|
||||
"errorSavingMember": "Napakyas sa pag-save sa miyembro",
|
||||
"errorAddingToGroup": "Napakyas sa pagdugang sa grupo",
|
||||
"errorRemovingFromGroup": "Napakyas sa pagtangtang gikan sa grupo",
|
||||
"imageUpdated": "Na-update ang hulagway",
|
||||
"errorRotatingImage": "Napakyas sa pag-ikot sa hulagway",
|
||||
"imageDeleted": "Na-delete ang hulagway.",
|
||||
"errorDeletingImage": "Dili matangtang ang hulagway",
|
||||
"primaryImageUpdated": "Na-update ang main nga hulagway.",
|
||||
"errorSettingPrimaryImage": "Napakyas sa pag-set sa main nga hulagway",
|
||||
"errorUploadingImage": "Napakyas sa pag-upload sa hulagway",
|
||||
"errorLoadingImage": "Napakyas sa pag-load sa hulagway",
|
||||
"phoneList": "phone-list.pdf",
|
||||
"errorLoadingTrainingParticipations": "Napakyas sa pag-load sa mga pag-apil sa pag-ensayo",
|
||||
"errorUpdatingRatings": "Napakyas sa pag-update sa TTR/QTTR values",
|
||||
"errorTransfer": "Napakyas sa pag-transfer",
|
||||
"subtitle": "Pangitaa, i-filter ug i-edit diretso ang mga miyembro.",
|
||||
"closeEditor": "Isira ang editor",
|
||||
"visibleMembers": "Mga miyembro nga makita",
|
||||
"search": "Pangitaa",
|
||||
"searchPlaceholder": "Pangitaa ang ngalan, lugar, telepono o e-mail",
|
||||
"memberScope": "Sakop sa miyembro",
|
||||
"scopeAll": "Tanan",
|
||||
"scopeActive": "Aktibo",
|
||||
"scopeTest": "Pagsulay",
|
||||
"scopeActiveTest": "Aktibo + Pagsulay",
|
||||
"scopeNeedsForm": "Porma wala pa masusi",
|
||||
"scopeActiveDataIncomplete": "Aktibo + kulang ang datos",
|
||||
"scopeDataIncomplete": "Kulang ang datos",
|
||||
"scopeInactive": "Dili aktibo",
|
||||
"searchAndFilter": "Pangitaa ug Filter",
|
||||
"bulkActions": "Daghang aksyon",
|
||||
"resultsVisible": "Mga miyembro nga makita",
|
||||
"editHint": "Usa ka click sa hilera aron maabli ang editor.",
|
||||
"editorCreateHint": "Paghimo ug bag-ong miyembro ug isulod dayon ang kontak.",
|
||||
"editorEditHint": "I-edit ang datos ni {name}.",
|
||||
"editorAssignTrainingGroupHint": "Palihug i-assign ang labing menos usa ka grupo sa pag-ensayo.",
|
||||
"editorRecommendedEntry": "Girekomendang entry",
|
||||
"transferSuccessTitle": "Malampusong pag-transfer",
|
||||
"transferErrorTitle": "Sayop sa pag-transfer",
|
||||
"clickTtRequestPending": "Nagpadayon ang Click-TT request",
|
||||
"clickTtRequestAction": "Mangayo og karapat-dapat sa duwa sa click-TT",
|
||||
"clickTtRequestTitle": "Sugdi ang Click-TT request",
|
||||
"clickTtRequestConfirm": "Sugdan ba ang automated Click-TT request para kang {name}?",
|
||||
"clickTtRequestHint": "Ang request awtomatikong gi-process sa backend pinaagi sa Click-TT workflow.",
|
||||
"clickTtRequestSuccess": "Palihug login sa click-tt ug isumite ang request.",
|
||||
"clickTtRequestError": "Dili masumite ang Click-TT request.",
|
||||
"clickTtRequestFailedLog": "Napakyas ang Click-TT request",
|
||||
"deleteImageTitle": "Tangtanga ang hulagway",
|
||||
"deleteImageConfirm": "Gusto ba nimo tangtangon gyud kini nga hulagway?",
|
||||
"parentFallback": "Ginikanan",
|
||||
"loadingMembers": "Gina-load ang mga miyembro...",
|
||||
"errorLoadingMembers": "Napakyas sa pag-load sa lista sa mga miyembro.",
|
||||
"scopeNotTraining": "Dili na sa pag-ensayo",
|
||||
"notInTrainingTooltip": "Wala mo-apil sulod sa {weeks} ka semana",
|
||||
"status": "Status",
|
||||
"contact": "Kontak",
|
||||
"noPhoneShort": "Walay telepono",
|
||||
"noEmailShort": "Walay e-mail",
|
||||
"noAddressShort": "Walay address",
|
||||
"clickTtSubmitted": "Click-TT gisumite",
|
||||
"markRegular": "I-mark isip regular",
|
||||
"markFormReceived": "Porma gisusi",
|
||||
"clickTtRequestShort": "Click-TT",
|
||||
"clickTtRequestPendingShort": "Nagdagan...",
|
||||
"memberDetails": "Detalye sa miyembro",
|
||||
"previewLastTraining": "Kinataposang pag-ensayo",
|
||||
"previewNoLastTraining": "Wala pay pag-apil",
|
||||
"phoneListForSelection": "Lista sa telepono para sa pagpili",
|
||||
"markFormsForSelection": "Susihon ang mga porma sa pagpili",
|
||||
"markRegularForSelection": "I-mark isip regular ang pagpili",
|
||||
"phoneListSelectionFile": "phone-list-selection.pdf",
|
||||
"batchFormsMarkedSuccess": "{count} ka porma gi-mark nga nasusi.",
|
||||
"batchFormsMarkedEmpty": "Walay wala pa masusi nga porma sa kasamtangang pagpili.",
|
||||
"batchMarkedRegularSuccess": "{count} ka trial nga miyembro gi-mark isip regular.",
|
||||
"batchMarkedRegularEmpty": "Walay trial nga miyembro sa kasamtangang pagpili.",
|
||||
"batchPartialFailure": "{success} malampuson, {failed} napakyas.",
|
||||
"sortBy": "Isort pinaagi sa",
|
||||
"sortLastName": "Apelyido",
|
||||
"sortFirstName": "Ngalan",
|
||||
"sortBirthday": "Adlawng natawhan",
|
||||
"sortAge": "Edad",
|
||||
"sortQttr": "QTTR value",
|
||||
"ageRange": "Edad gikan - hangtod",
|
||||
"ageFromPlaceholder": "gikan",
|
||||
"ageToPlaceholder": "hangtod",
|
||||
"age": "Edad",
|
||||
"dataQuality": "Kalidad sa datos",
|
||||
"dataQualityComplete": "Kompletong datos",
|
||||
"dataIssueBirthdate": "Walay petsa sa pagkahimugso",
|
||||
"dataIssuePhone": "Walay numero sa telepono",
|
||||
"dataIssueEmail": "Walay e-mail",
|
||||
"dataIssueAddress": "Walay address",
|
||||
"dataIssueStreet": "Walay ngalan sa dalan",
|
||||
"dataIssuePostalCode": "Walay postal code",
|
||||
"dataIssueCity": "Walay ngalan sa lugar",
|
||||
"dataIssueGender": "Walay klaro nga gender",
|
||||
"dataIssueTrainingGroup": "Walay grupo sa pag-ensayo",
|
||||
"openTasks": "Mga bukas nga buluhaton",
|
||||
"noOpenTasks": "Walay bukas nga buluhaton",
|
||||
"taskVerifyForm": "Susiha ang porma",
|
||||
"taskReviewTrialStatus": "Susiha ang trial status",
|
||||
"taskCheckTrainingStatus": "Susiha ang status sa pag-ensayo",
|
||||
"taskCheckDataQuality": "Susiha ang kalidad sa datos",
|
||||
"taskAssignTrainingGroup": "I-assign ang grupo sa pag-ensayo",
|
||||
"taskCheckClickTt": "Susiha ang Click-TT karapat-dapat",
|
||||
"taskActionVerify": "Susiha",
|
||||
"taskActionMarkRegular": "I-set isip regular",
|
||||
"taskActionReview": "Ablihi",
|
||||
"taskActionRequest": "Mangayo",
|
||||
"toggleSortDirection": "Usba ang direksyon sa sort",
|
||||
"showTtrHistory": "Ipakita ang TTR kasaysayan",
|
||||
"missingTtrHistoryId": "Walay myTischtennis ID",
|
||||
"sortLastTraining": "Kinataposang pag-ensayo",
|
||||
"sortOpenTasks": "Mga bukas nga buluhaton",
|
||||
"exportPreview": "Preview sa export",
|
||||
"exportMembersSelected": "Mga miyembro gipili karon",
|
||||
"exportPhones": "Telepono",
|
||||
"exportReachableByPhone": "uban ang numero sa telepono",
|
||||
"exportEmails": "E-mail",
|
||||
"exportReachableByEmail": "uban ang e-mail address",
|
||||
"exportPreviewNames": "Preview",
|
||||
"exportPreviewEmpty": "Walay miyembro sa kasamtangang pagpili",
|
||||
"exportCsv": "I-export ang CSV",
|
||||
"exportCsvFile": "mitglieder-auswahl.csv",
|
||||
"copyPhones": "Kopyaha ang mga telepono",
|
||||
"copyEmails": "Kopyaha ang mga e-mail",
|
||||
"copyPhonesSuccess": "Nakopya ang lista sa telepono sa clipboard.",
|
||||
"copyPhonesEmpty": "Walay numero sa telepono sa kasamtangang pagpili.",
|
||||
"copyEmailsSuccess": "Nakopya ang lista sa e-mail sa clipboard.",
|
||||
"copyEmailsEmpty": "Walay e-mail address sa kasamtangang pagpili.",
|
||||
"composeEmail": "Pag-andam sa e-mail",
|
||||
"copyContactSummary": "Kopyaha ang summario sa kontak",
|
||||
"copyContactSummarySuccess": "Nakopya ang summary sa kontak sa clipboard."
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
"sourceWarning": "{source} konnte nicht geladen werden",
|
||||
"agendaTitle": "Termine im Monat",
|
||||
"agendaEmpty": "Keine Termine in diesem Monat.",
|
||||
"recurringTrainingTime": "Regelmäßige Trainingszeit",
|
||||
"recurringTrainingTime": "Regelmässige Trainingszeit",
|
||||
"participants": "{count} Teilnehmer",
|
||||
"starts": "{count} Starts",
|
||||
"options": {
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training fällt aus",
|
||||
"description": "Blendet regelmäßige Trainingszeiten aus.",
|
||||
"description": "Blendet regelmässige Trainingszeiten aus.",
|
||||
"date": "Datum",
|
||||
"untilOptional": "Bis optional",
|
||||
"trainingGroups": "Trainingsgruppen",
|
||||
@@ -268,7 +268,8 @@
|
||||
"zh": "中文 (Chinesisch)",
|
||||
"tl": "Tagalog",
|
||||
"th": "ไทย (Thai)",
|
||||
"fil": "Filipino"
|
||||
"fil": "Filipino",
|
||||
"ceb": "Cebuano (Bisaya)"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Erfolg",
|
||||
@@ -276,12 +277,13 @@
|
||||
"warning": "Warnig",
|
||||
"info": "Information",
|
||||
"confirm": "Bestätige",
|
||||
"cancel": "Abbreche"
|
||||
"cancel": "Abbreche",
|
||||
"fileTooLarge": "Datei zu gross",
|
||||
"imageMaxSize": "Das Bild darf maximal 5 MB gross sein."
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
|
||||
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
|
||||
"internalStatsExportPdf": "Als PDF exportieren",
|
||||
"internalStatsAgeFilter": "Altersklassä & Gschlächt (Einzel)",
|
||||
"internalStatsFilterAgeBands": "Altersklassä",
|
||||
"internalStatsFilterGenderColumn": "Gschlächt",
|
||||
@@ -336,7 +338,7 @@
|
||||
"dataNotRecorded": "No nid erfasst",
|
||||
"resultsRanking": "Rangliste",
|
||||
"newSetPlaceholder": "Neue Satz, z. B. 11:7",
|
||||
"finishMatch": "Abschliesse",
|
||||
"finishMatch": "Abschliessen",
|
||||
"correctMatch": "Korrigiere",
|
||||
"markMatchLive": "Als laufend markiere",
|
||||
"unmarkMatchLive": "Laufend-Markierig entferne",
|
||||
@@ -402,7 +404,7 @@
|
||||
"problemDoublesDescription": "Ei oder meh Doppelklasse bruuched no Partnerzuewisige.",
|
||||
"problemDoublesAutoDescription": "D offeni Doppelklass cha direkt automatisch paarlet werde.",
|
||||
"doublesTournament": "Doppel-Turnier",
|
||||
"doublesTournamentHint": "Schaltet alli vorhandene Klasse uf Doppel und legt neui Klasse standardmässig als Doppel aa.",
|
||||
"doublesTournamentHint": "Schaltet alle vorhandenen Klassen auf Doppel und legt neue Klassen standardmässig als Doppel an.",
|
||||
"problemGroupsMissingTitle": "Gruppe no nid erstellt",
|
||||
"problemGroupsMissingDescription": "Für s Gruppeturnier muesed zerscht Gruppe aagleit werde.",
|
||||
"problemGroupMatchesTitle": "Gruppespiel no nid erzeugt",
|
||||
@@ -543,7 +545,9 @@
|
||||
"stageFlowReadinessIncompleteGroups": "{count} Gruppe(n) sind noch nicht vollständig entschieden",
|
||||
"stageFlowReadinessReady": "Übergang ist fachlich startklar",
|
||||
"stageFlowQualifiedPreviewTitle": "Aktuell qualifiziert",
|
||||
"stageFlowQualifiedPreviewEntry": "G{group} · Platz {position} · {name}"
|
||||
"stageFlowQualifiedPreviewEntry": "G{group} · Platz {position} · {name}",
|
||||
"maxGroupSize": "Maximale Gruppengrösse",
|
||||
"outOfCompetition": "Spieler aus A ausser Konkurrenz"
|
||||
},
|
||||
"members": {
|
||||
"title": "Mitglieder",
|
||||
@@ -563,7 +567,7 @@
|
||||
"createNewMember": "Neues Mitglied anlegen",
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
"street": "Straße",
|
||||
"street": "Strasse",
|
||||
"postalCode": "PLZ",
|
||||
"city": "Ort",
|
||||
"birthdate": "Geburtsdatum",
|
||||
@@ -673,7 +677,7 @@
|
||||
"errorUpdatingRatings": "Fehler beim Aktualisieren der TTR/QTTR-Werte",
|
||||
"errorTransfer": "Fehler bei der Übertragung",
|
||||
"subtitle": "Mitglieder suchen, filtern und direkt bearbeiten.",
|
||||
"closeEditor": "Editor schließen",
|
||||
"closeEditor": "Editor schliessen",
|
||||
"visibleMembers": "Sichtbare Mitglieder",
|
||||
"search": "Suche",
|
||||
"searchPlaceholder": "Name, Ort, Telefon oder E-Mail suchen",
|
||||
@@ -829,7 +833,7 @@
|
||||
"deleteDate": "Datum löschen",
|
||||
"createNew": "Neu anlegen",
|
||||
"quickCreate": "Schnellanlegen",
|
||||
"quickCreateNoSlot": "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).",
|
||||
"quickCreateNoSlot": "Kein freier Trainingstermin gefunden (gemäss Gruppenzeiten, ein Jahr voraus).",
|
||||
"quickCreateFailed": "Der Trainingstag konnte nicht angelegt werden.",
|
||||
"gallery": "Mitglieder-Galerie",
|
||||
"galleryCreating": "Galerie wird erstellt…",
|
||||
@@ -1101,7 +1105,9 @@
|
||||
"secondHalfFull": "Rückrunde (ab 1. Januar)",
|
||||
"missingConfigSummary": "Fehlende Angaben:",
|
||||
"leagueFieldRequired": "Bitte eine Spielklasse auswählen und speichern.",
|
||||
"myTischtennisUrlRequired": "MyTischtennis-URL einfügen und parsen, um Team-ID und Ligendaten zu übernehmen."
|
||||
"myTischtennisUrlRequired": "MyTischtennis-URL einfügen und parsen, um Team-ID und Ligendaten zu übernehmen.",
|
||||
"fileTooLargeTitle": "Datei zu gross",
|
||||
"fileTooLarge": "{label} darf maximal 10 MB gross sein."
|
||||
},
|
||||
"pdfGenerator": {
|
||||
"teamLineupTitle": "Mannschaftsaufstellung",
|
||||
@@ -1114,6 +1120,61 @@
|
||||
"teamAgeGroupLabel": "Team Altersklasse:",
|
||||
"generatedAt": "Erstellt:",
|
||||
"lineupQttr": "(Q)TTR",
|
||||
"plannedLeagueLabel": "Geplante Spielklasse:"
|
||||
"plannedLeagueLabel": "Geplante Spielklasse:",
|
||||
"noWhiteJersey": "Kein weisses Trikot",
|
||||
"sportShorts": "Sportshorts (oder Sportröckchen), am besten auch nicht weiss"
|
||||
},
|
||||
"clubSettings": {
|
||||
"greetingText": "Begrüssungstext",
|
||||
"greetingPlaceholder": "Begrüssungstext für Heimspiele...",
|
||||
"greetingHint": "Dieser Text erscheint im Reiter \"Begrüssung\" des Spielberichtsbogens.",
|
||||
"requireStreet": "Strasse nötig"
|
||||
},
|
||||
"accident": {
|
||||
"close": "Schliessen"
|
||||
},
|
||||
"schedule": {
|
||||
"imageSize": "Bildgrösse"
|
||||
},
|
||||
"memberTransfer": {
|
||||
"bulkWrapperDescription": "Optional können Sie die äussere Struktur definieren, in die die Mitglieder-Array eingefügt wird. Verwenden Sie PLACEHOLDER_MEMBERS als Platzhalter für das Array der Mitglieder.",
|
||||
"bulkModeActiveDescription": "Das Template definiert das Format für ein einzelnes Mitglied. Die Mitglieder werden automatisch in ein Array gewrappt. Die äussere Struktur können Sie optional im \"Bulk-Wrapper-Template\" definieren (siehe unten).",
|
||||
"placeholders": {
|
||||
"street": "Strasse",
|
||||
"streetDesc": "Strasse und Hausnummer",
|
||||
"addressDesc": "Strasse und Ort kombiniert"
|
||||
}
|
||||
},
|
||||
"memberSelection": {
|
||||
"close": "Schliessen"
|
||||
},
|
||||
"matchReportApi": {
|
||||
"greeting": "Begrüssung"
|
||||
},
|
||||
"memberActivities": {
|
||||
"close": "Schliessen"
|
||||
},
|
||||
"memberGallery": {
|
||||
"imageSize": "Bildgrösse"
|
||||
},
|
||||
"myTischtennisHistory": {
|
||||
"close": "Schliessen"
|
||||
},
|
||||
"imageDialog": {
|
||||
"close": "Schliessen"
|
||||
},
|
||||
"baseDialog": {
|
||||
"close": "Schliessen",
|
||||
"resize": "Grösse ändern"
|
||||
},
|
||||
"dialogManager": {
|
||||
"close": "Schliessen"
|
||||
},
|
||||
"dialogExamples": {
|
||||
"largeModal": "Grosser Modal",
|
||||
"simpleModalText": "Dies ist ein einfacher modaler Dialog mit mittlerer Grösse.",
|
||||
"simpleModalText2": "Klicken Sie ausserhalb oder auf das X, um zu schliessen.",
|
||||
"largeModalTitle": "Grosser Modal Dialog",
|
||||
"largeModalText": "Dies ist ein grosser modaler Dialog."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,7 +567,13 @@
|
||||
"noOrdersMember": "Für dieses Mitglied gibt es noch keine Bestellungen.",
|
||||
"noOrdersGlobal": "Es gibt aktuell keine Bestellungen.",
|
||||
"club": "Verein",
|
||||
"member": "Mitglied"
|
||||
"member": "Mitglied",
|
||||
"filterAllCompletionStates": "Alle Abschlüsse",
|
||||
"filterPaid": "Hat bezahlt",
|
||||
"filterHandedOver": "Artikel ausgehändigt",
|
||||
"showCompleted": "Abgeschlossene einblenden",
|
||||
"paidConfirmed": "Hat bezahlt",
|
||||
"budget": "Budget"
|
||||
},
|
||||
"diary": {
|
||||
"title": "Trainingstagebuch",
|
||||
@@ -2559,5 +2565,74 @@
|
||||
"generatedAt": "Erstellt:",
|
||||
"lineupQttr": "(Q)TTR",
|
||||
"plannedLeagueLabel": "Geplante Spielklasse:"
|
||||
},
|
||||
"billing": {
|
||||
"title": "Abrechnung",
|
||||
"subtitle": "Erstelle deine eigene monatliche Übungsstunden-Abrechnung.",
|
||||
"step1": "Schritt 1: Vorlage hinterlegen",
|
||||
"step2": "Schritt 2: Abrechnungslauf anlegen",
|
||||
"step3": "Schritt 3: PDF erzeugen und herunterladen",
|
||||
"templateSection": "Vorlage hochladen",
|
||||
"templateName": "Vorlagenname",
|
||||
"templateDescription": "Beschreibung",
|
||||
"templatePdf": "PDF-Vorlage",
|
||||
"uploadTemplate": "Vorlage speichern",
|
||||
"deleteTemplate": "Vorlage löschen",
|
||||
"deleteTemplateConfirm": "Diese Vorlage wirklich löschen?",
|
||||
"deleteTemplateSuccess": "Vorlage wurde gelöscht.",
|
||||
"deleteTemplateError": "Vorlage konnte nicht gelöscht werden.",
|
||||
"noTemplates": "Noch keine Vorlagen vorhanden.",
|
||||
"mappingTitle": "Positions-Mapping",
|
||||
"mappingHint": "Koordinaten je Feld einmalig anpassen und speichern. Diese Positionen werden bei „PDF erzeugen“ verwendet.",
|
||||
"openMappingEditor": "Mapping per Klick",
|
||||
"mappingEditorTitle": "Felder im PDF per Klick zuweisen",
|
||||
"mappingEditorHint": "Feld auswählen, dann in die PDF klicken. Die Position wird direkt übernommen.",
|
||||
"mappingField": "Feld",
|
||||
"autoSuggestMapping": "Auto-Vorschläge",
|
||||
"resetIbanBoxes": "IBAN-Boxen reset",
|
||||
"ibanBoxesReset": "IBAN-Boxen wurden zurückgesetzt.",
|
||||
"ibanLearnStart": "IBAN einlernen",
|
||||
"ibanLearnCancel": "IBAN-Lernen abbrechen",
|
||||
"ibanLearnStep1": "IBAN-Lernen: bitte auf die erste zu nutzende IBAN-Box klicken.",
|
||||
"ibanLearnStep2": "IBAN-Lernen: bitte auf die letzte zu nutzende IBAN-Box klicken.",
|
||||
"ibanLearnDone": "IBAN-Boxen wurden aus dem gewählten Bereich zugewiesen.",
|
||||
"mappingSuggested": "Auto-Vorschläge wurden eingetragen. Bitte prüfen und feinjustieren.",
|
||||
"mappingSuggestError": "Auto-Vorschläge konnten nicht ermittelt werden.",
|
||||
"mappingPreviewError": "PDF-Vorschau für Mapping konnte nicht geladen werden.",
|
||||
"saveMapping": "Mapping speichern",
|
||||
"mappingSaved": "Mapping wurde gespeichert.",
|
||||
"mappingSaveError": "Mapping konnte nicht gespeichert werden.",
|
||||
"runSection": "Abrechnungslauf",
|
||||
"template": "Vorlage",
|
||||
"monthFrom": "Monat von",
|
||||
"monthTo": "Monat bis",
|
||||
"selfRecipientName": "Eigener Name",
|
||||
"iban": "IBAN",
|
||||
"ibanWithoutCountry": "ohne Länderkennung",
|
||||
"hourlyRate": "Stunden-Gehalt",
|
||||
"hoursTotal": "Anzahl Stunden",
|
||||
"hoursAutoHint": "Anzahl Stunden wird automatisch aus den Trainingstagen berechnet: {hours} h ({count} Einheiten).",
|
||||
"sessionTime": "Zeit",
|
||||
"sessionLabel": "Bezeichner",
|
||||
"sessionHours": "Stunden",
|
||||
"sameAccountCheckbox": "Gleiches Konto wie letzte Abrechnung",
|
||||
"omitField": "nicht angeben",
|
||||
"noSessionsInRange": "Keine Trainingseinheiten im gewählten Zeitraum gefunden.",
|
||||
"locationText": "Ort",
|
||||
"documentDate": "Datum",
|
||||
"generateOwnBilling": "Eigene Abrechnung erzeugen",
|
||||
"generatePdf": "PDF erzeugen",
|
||||
"generateAndDownloadPdf": "PDF erzeugen + herunterladen",
|
||||
"generatingPdf": "Erzeuge PDF...",
|
||||
"generateSuccess": "PDF wurde erzeugt.",
|
||||
"generateError": "PDF konnte nicht erzeugt werden.",
|
||||
"downloadPdf": "PDF herunterladen",
|
||||
"downloadError": "PDF konnte nicht heruntergeladen werden.",
|
||||
"deleteBilling": "Löschen",
|
||||
"deleteConfirm": "Diese erzeugte Abrechnung wirklich löschen?",
|
||||
"deleteSuccess": "Abrechnung wurde gelöscht.",
|
||||
"deleteError": "Abrechnung konnte nicht gelöscht werden.",
|
||||
"runsTitle": "Bisherige Abrechnungen",
|
||||
"noRuns": "Noch keine Abrechnungen vorhanden."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,8 @@
|
||||
"zh": "中文 (Chinesisch)",
|
||||
"tl": "Tagalog (Tagalog)",
|
||||
"th": "ไทย (Thai)",
|
||||
"fil": "Filipino (Filipino)"
|
||||
"fil": "Filipino (Filipino)",
|
||||
"ceb": "Cebuano (Bisaya)"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Erfolg",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
selectedLanguage: this.$i18n.locale,
|
||||
languageCodes: ['de', 'en-GB', 'en-US', 'en-AU', 'de-CH', 'fr', 'es', 'it', 'pl', 'ja', 'zh', 'tl', 'th', 'fil']
|
||||
languageCodes: ['de', 'en-GB', 'en-US', 'en-AU', 'de-CH', 'fr', 'es', 'it', 'pl', 'ja', 'zh', 'tl', 'th', 'fil', 'ceb']
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user