feat(TournamentStats): enhance internal tournament statistics with member profile integration
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s

- Integrated member profile data (birth date and gender) into the internal tournament statistics calculations for more accurate age class filtering.
- Removed deprecated age class filtering functions and streamlined the statistics computation process.
- Updated the InternalTournamentStats component to reflect changes in age class options and improved sorting logic.
- Enhanced localization strings across multiple languages to support new terminology related to age classes and gender, improving user accessibility and understanding.
This commit is contained in:
Torsten Schulz (local)
2026-04-08 13:48:41 +02:00
parent 30994adee8
commit bbd9f08e97
19 changed files with 213 additions and 138 deletions

View File

@@ -1,3 +1,5 @@
import { memberToCanonicalTtBucket } from '../utils/ttInternalStatsBuckets.js';
/**
* Statistik-Werte für interne Einzel-Turniere (Gruppenphase + K.-o.-Runde).
*
@@ -6,6 +8,7 @@
* bei N = 1: 100 %. Nur Vereinsmitglieder werden gewertet; N zählt alle Platzierten.
* K.-o.: Wer die K.-o.-Runde erreicht: höchster Gruppen-Prozentwert dieser Klasse + 1,
* danach +1 pro gewonnenes K.-o.-Spiel (wie bisher, jetzt auf %-Skala).
* Alters-/Geschlechtsfilter: pro Spieler aus Vereinsmitglied Geburtsdatum + Geschlecht (nicht aus Turnierklassen-Name).
*/
export function parseWinnerFromMatch(match) {
@@ -81,35 +84,6 @@ export function groupPercentFromRankings(rankings, nInGroup) {
return map;
}
/** Schlüssel für Gruppen/Spiele ohne zugeordnete Klasse (classId null) */
export const NO_CLASS_AGE_KEY = 'ak|__noclass__';
/**
* Stabiler Filter-Schlüssel aus einer Einzel-Klasse (Geburtsjahre + Geschlecht).
* @param {object} classRow TournamentClass-Plain
* @returns {string|null} null bei Doppel
*/
export function ageClassFilterKey(classRow) {
if (!classRow || classRow.isDoubles) return null;
const min = classRow.minBirthYear ?? classRow.min_birth_year ?? null;
const max = classRow.maxBirthYear ?? classRow.max_birth_year ?? null;
const g = classRow.gender ?? '';
return `ak|${min ?? ''}|${max ?? ''}|${g}`;
}
/**
* @param {number|null|undefined} classIdNum
* @param {Array<object>} classes
* @returns {string|null} null wenn unbekannte ID oder Doppel-Klasse
*/
export function ageKeyForClassSlice(classIdNum, classes) {
if (classIdNum == null || classIdNum === undefined) return NO_CLASS_AGE_KEY;
const c = (classes || []).find((x) => Number(x.id) === Number(classIdNum));
if (!c) return null;
if (c.isDoubles) return null;
return ageClassFilterKey(c);
}
/**
* @param {object} opts
* @param {Array} opts.groups Aus getGroupsWithParticipants (participants: flache Objekte mit id, position, isExternal, …)
@@ -117,7 +91,9 @@ export function ageKeyForClassSlice(classIdNum, classes) {
* @param {Array<{ id: number, isDoubles?: boolean }>} opts.classes
* @param {Map<number, number>} opts.tmToMemberId Turnier-Mitglied-ID -> Vereins-Mitglied-ID
* @param {Map<number, { firstName: string, lastName: string }>} [opts.tmToName] optional Namen
* @param {Set<string>|null} [opts.allowedAgeKeys] nur diese Altersklassen-Schlüssel werten; null = alle
* @param {Map<number, { birthDate?: string|null, gender?: string|null }>} [opts.tmToMemberProfile] pro TM: Mitglied Geburtsdatum + Geschlecht (für Filter)
* @param {Set<string>|null} [opts.allowedAgeKeys] tt|9|female … tt|adult|open; null = alle
* @param {string} [opts.tournamentDateIso] Turnierdatum für J/Erwachsenen-Zuordnung
* @returns {Map<number, { points: number, firstName: string, lastName: string }>} Vereins-Mitglied-ID -> Aggregation
*/
export function computeInternalSinglesStatsForTournament({
@@ -126,7 +102,9 @@ export function computeInternalSinglesStatsForTournament({
classes,
tmToMemberId,
tmToName,
tmToMemberProfile,
allowedAgeKeys = null,
tournamentDateIso,
}) {
const doublesClassIds = new Set(
(classes || []).filter((c) => c.isDoubles).map((c) => Number(c.id))
@@ -164,18 +142,12 @@ export function computeInternalSinglesStatsForTournament({
return { firstName: n?.firstName || '', lastName: n?.lastName || '' };
};
const profileForTm = (tmId) => tmToMemberProfile?.get(Number(tmId)) ?? null;
for (const classKey of classKeys) {
const classIdNum = classKey === 'null' ? null : Number(classKey);
if (classIdNum != null && doublesClassIds.has(classIdNum)) continue;
const sliceAgeKey = ageKeyForClassSlice(classIdNum, classes);
if (sliceAgeKey == null) continue;
if (allowedAgeKeys != null) {
if (allowedAgeKeys.size === 0) continue;
if (!allowedAgeKeys.has(sliceAgeKey)) continue;
}
const classGroups = (groups || []).filter((g) => {
const gc = g.classId != null ? Number(g.classId) : null;
const key = gc != null ? String(gc) : 'null';
@@ -235,6 +207,12 @@ export function computeInternalSinglesStatsForTournament({
const mid = tmToMemberId.get(Number(tmId));
if (mid == null) continue;
const bucket = memberToCanonicalTtBucket(profileForTm(tmId), tournamentDateIso);
if (allowedAgeKeys != null) {
if (allowedAgeKeys.size === 0) continue;
if (bucket == null || !allowedAgeKeys.has(bucket)) continue;
}
const gPts = groupPointsByTm.get(tmId) ?? 0;
const wins = koWins.get(tmId) || 0;
const inKo = playedKo.has(tmId);
@@ -258,7 +236,4 @@ export default {
groupPercentFromRankings,
computeInternalSinglesStatsForTournament,
parseWinnerFromMatch,
ageClassFilterKey,
ageKeyForClassSlice,
NO_CLASS_AGE_KEY,
};

View File

@@ -15,11 +15,8 @@ import { Op, literal } from 'sequelize';
import { devLog } from '../utils/logger.js';
import {
computeInternalSinglesStatsForTournament,
ageClassFilterKey,
NO_CLASS_AGE_KEY,
} from './internalTournamentStatsService.js';
import { computeInternalSinglesStatsForTournament } from './internalTournamentStatsService.js';
import { getCanonicalTtAgeClassOptions } from '../utils/ttInternalStatsBuckets.js';
/** @param {unknown} val Query ageClassKeys: fehlend = alle; leer = keine; sonst kommagetrennte Schlüssel */
function parseInternalStatsAgeClassKeys(val) {
@@ -4326,8 +4323,6 @@ Ve // 2. Neues Turnier anlegen
});
const list = JSON.parse(JSON.stringify(tournaments));
const ageClassOptionsByKey = new Map();
const memberAgg = new Map();
const rawKeys = parseInternalStatsAgeClassKeys(ageClassKeysQuery);
@@ -4338,48 +4333,6 @@ Ve // 2. Neues Turnier anlegen
const groups = await this.getGroupsWithParticipants(userToken, clubId, t.id);
const matches = await this.getTournamentMatches(userToken, clubId, t.id);
for (const c of classesJson) {
if (c.isDoubles) continue;
const k = ageClassFilterKey(c);
if (!k) continue;
if (!ageClassOptionsByKey.has(k)) {
ageClassOptionsByKey.set(k, {
key: k,
name: c.name || '',
minBirthYear: c.minBirthYear ?? c.min_birth_year ?? null,
maxBirthYear: c.maxBirthYear ?? c.max_birth_year ?? null,
gender: c.gender ?? null,
isNoClass: false,
});
}
}
let hasNoClassSlice = false;
for (const g of groups || []) {
if (g.classId == null || g.classId === undefined) {
hasNoClassSlice = true;
break;
}
}
if (!hasNoClassSlice) {
for (const ma of matches || []) {
if (ma.round === 'group') continue;
if (ma.classId == null || ma.classId === undefined) {
hasNoClassSlice = true;
break;
}
}
}
if (hasNoClassSlice && !ageClassOptionsByKey.has(NO_CLASS_AGE_KEY)) {
ageClassOptionsByKey.set(NO_CLASS_AGE_KEY, {
key: NO_CLASS_AGE_KEY,
name: '',
minBirthYear: null,
maxBirthYear: null,
gender: null,
isNoClass: true,
});
}
let allowedAgeKeys = null;
if (rawKeys !== null) {
allowedAgeKeys = new Set(rawKeys);
@@ -4398,10 +4351,11 @@ Ve // 2. Neues Turnier anlegen
const tmList = [...tmIds];
const tmToMemberId = new Map();
const tmToName = new Map();
const tmToMemberProfile = new Map();
if (tmList.length > 0) {
const tms = await TournamentMember.findAll({
where: { tournamentId: t.id, id: { [Op.in]: tmList } },
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }],
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName', 'birthDate', 'gender'] }],
});
for (const row of tms) {
const plain = row.toJSON();
@@ -4411,6 +4365,10 @@ Ve // 2. Neues Turnier anlegen
firstName: plain.member.firstName || '',
lastName: plain.member.lastName || '',
});
tmToMemberProfile.set(plain.id, {
birthDate: plain.member.birthDate ?? null,
gender: plain.member.gender ?? null,
});
}
}
}
@@ -4421,7 +4379,9 @@ Ve // 2. Neues Turnier anlegen
classes: classesJson,
tmToMemberId,
tmToName,
tmToMemberProfile,
allowedAgeKeys,
tournamentDateIso: t.date,
});
for (const [mid, row] of memberTotals) {
@@ -4458,13 +4418,7 @@ Ve // 2. Neues Turnier anlegen
b.averagePoints - a.averagePoints || (a.lastName || '').localeCompare(b.lastName || '', 'de')
);
const ageClassOptions = [...ageClassOptionsByKey.values()].sort((a, b) => {
if (a.isNoClass && !b.isNoClass) return 1;
if (!a.isNoClass && b.isNoClass) return -1;
const an = (a.name || '').localeCompare(b.name || '', 'de');
if (an !== 0) return an;
return (a.key || '').localeCompare(b.key || '', 'de');
});
const ageClassOptions = getCanonicalTtAgeClassOptions();
return {
months: monthsNum,

View File

@@ -0,0 +1,147 @@
/**
* Feste TT-Einzel-Filter (J9J19, Erwachsene × Weiblich/Alle).
* Zuordnung über **Teilnehmer/Mitglied**: Geburtsdatum + Geschlecht, Saison-Stichtag 01.01.
* (vgl. frontend/src/utils/ttAgeClass.js)
*/
const YOUTH_BANDS = [9, 11, 13, 15, 19];
/**
* @param {string|undefined} isoDateStr Turnierdatum (DATEONLY)
* @returns {number} erstes Saisonjahr (01.08.31.07.)
*/
export function getSeasonStartYearFromTournamentDate(isoDateStr) {
if (!isoDateStr) {
const n = new Date();
const month = n.getMonth() + 1;
const year = n.getFullYear();
return month >= 8 ? year : year - 1;
}
const d = new Date(String(isoDateStr).length <= 10 ? `${isoDateStr}T12:00:00` : isoDateStr);
if (Number.isNaN(d.getTime())) {
const n = new Date();
const month = n.getMonth() + 1;
const year = n.getFullYear();
return month >= 8 ? year : year - 1;
}
const month = d.getMonth() + 1;
const year = d.getFullYear();
return month >= 8 ? year : year - 1;
}
function getStichtagDate(seasonStartYear, classNum) {
const y = seasonStartYear - (classNum - 1);
return new Date(y, 0, 1);
}
/**
* @param {number} ms Geburtszeitpunkt
* @param {number} seasonStartYear
* @returns {'J9'|'J11'|'J13'|'J15'|'J19'|'adult'}
*/
function getExclusiveJugendLabelFromBirthTime(ms, seasonStartYear) {
const c = (k) => getStichtagDate(seasonStartYear, k).getTime();
const c9 = c(9);
const c11 = c(11);
const c13 = c(13);
const c15 = c(15);
const c19 = c(19);
if (ms < c19) return 'adult';
if (ms >= c9) return 'J9';
if (ms >= c11 && ms < c9) return 'J11';
if (ms >= c13 && ms < c11) return 'J13';
if (ms >= c15 && ms < c13) return 'J15';
if (ms >= c19 && ms < c15) return 'J19';
return 'adult';
}
/**
* @param {string|Date|null|undefined} birthDate wie Member.birthDate (Klartext nach get)
* @returns {number|null} ms seit Epoch
*/
export function parseMemberBirthDateToMs(birthDate) {
if (birthDate == null || birthDate === '') return null;
if (birthDate instanceof Date && !Number.isNaN(birthDate.getTime())) {
return birthDate.getTime();
}
const t = String(birthDate).trim();
if (/^\d{4}-\d{2}-\d{2}/.test(t)) {
const [a, b, c] = t.split(/[T\s]/)[0].split('-').map(Number);
if (Number.isFinite(a) && Number.isFinite(b) && Number.isFinite(c)) {
const d = new Date(a, b - 1, c);
return Number.isNaN(d.getTime()) ? null : d.getTime();
}
}
if (t.includes('.')) {
const parts = t.split('.');
if (parts.length >= 3) {
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10);
const year = parseInt(parts[2], 10);
if (Number.isFinite(day) && Number.isFinite(month) && Number.isFinite(year)) {
const d = new Date(year, month - 1, day);
return Number.isNaN(d.getTime()) ? null : d.getTime();
}
}
}
return null;
}
/** TT: nur Weiblich vs. Alle (alles außer female → open) */
function memberGenderMode(gender) {
return gender === 'female' ? 'female' : 'open';
}
/**
* Kanonischer Filter-Schlüssel aus Vereinsmitglied (Teilnehmer).
* @param {{ birthDate?: string|Date|null, gender?: string|null }|null|undefined} memberProfile
* @param {string|undefined} tournamentDateIso
* @returns {string|null} z. B. tt|13|open; null ohne auswertbares Geburtsdatum
*/
export function memberToCanonicalTtBucket(memberProfile, tournamentDateIso) {
if (!memberProfile) return null;
const ms = parseMemberBirthDateToMs(memberProfile.birthDate);
if (ms == null) return null;
const seasonStartYear = getSeasonStartYearFromTournamentDate(tournamentDateIso);
const jc = getExclusiveJugendLabelFromBirthTime(ms, seasonStartYear);
const gMode = memberGenderMode(memberProfile.gender);
if (jc === 'adult') return `tt|adult|${gMode}`;
const n = parseInt(jc.slice(1), 10);
if (!YOUTH_BANDS.includes(n)) return `tt|adult|${gMode}`;
return `tt|${n}|${gMode}`;
}
export function getCanonicalTtAgeClassOptions() {
const out = [];
for (const n of YOUTH_BANDS) {
out.push({
key: `tt|${n}|female`,
band: 'youth',
bandNum: n,
genderMode: 'female',
isNoClass: false,
});
out.push({
key: `tt|${n}|open`,
band: 'youth',
bandNum: n,
genderMode: 'open',
isNoClass: false,
});
}
out.push({
key: 'tt|adult|female',
band: 'adult',
bandNum: null,
genderMode: 'female',
isNoClass: false,
});
out.push({
key: 'tt|adult|open',
band: 'adult',
bandNum: null,
genderMode: 'open',
isNoClass: false,
});
return out;
}

View File

@@ -35,7 +35,7 @@
</p>
<p class="stats-explain">{{ $t('tournaments.internalStatsPointsExplain') }}</p>
<fieldset v-if="sortedAgeOptions.length > 1" class="age-filter">
<fieldset v-if="sortedAgeOptions.length" class="age-filter">
<legend class="age-filter-legend">{{ $t('tournaments.internalStatsAgeFilter') }}</legend>
<div class="age-filter-actions">
<button type="button" class="age-filter-link" @click="selectAllAgeKeys" :disabled="loading">
@@ -154,13 +154,10 @@ export default {
sortedAgeOptions() {
const list = [...(this.stats.ageClassOptions || [])];
list.sort((a, b) => {
const va = this.ageClassSortValue(a);
const vb = this.ageClassSortValue(b);
const va = this.ttOptionSortKey(a);
const vb = this.ttOptionSortKey(b);
if (va !== vb) return va - vb;
const ga = this.tournamentGenderSortOrder(a.gender);
const gb = this.tournamentGenderSortOrder(b.gender);
if (ga !== gb) return ga - gb;
return (a.name || '').localeCompare(b.name || '', 'de', { sensitivity: 'base' });
return (a.key || '').localeCompare(b.key || '', 'de');
});
return list;
},
@@ -232,48 +229,35 @@ export default {
this.selectedAgeKeys = [];
this.load();
},
/** TT: nur Weiblich vs. Alle (offen); kein „Männlich“ mehr in Turnierklassen */
tournamentClassGenderLabel(gender) {
if (gender === 'female') return this.$t('tournaments.tournamentClassGenderFemale');
/** TT: nur Weiblich vs. Alle (offen) */
tournamentClassGenderLabelFromMode(genderMode) {
if (genderMode === 'female') return this.$t('tournaments.tournamentClassGenderFemale');
return this.$t('tournaments.tournamentClassGenderOpen');
},
tournamentGenderSortOrder(gender) {
return gender === 'female' ? 0 : 1;
},
/**
* Reihenfolge: J9 … J19 (bzw. U…), dann Erwachsene/Senioren, Sonstiges, ohne Klasse zuletzt.
*/
ageClassSortValue(opt) {
if (opt.isNoClass) return 99999;
const name = (opt.name || '').trim();
const ju = name.match(/\bJ\s*(\d+)\b/i) || name.match(/\bU\s*(\d+)\b/i);
if (ju) {
const num = parseInt(ju[1], 10);
return 100 + num;
/** Sortierung: J9w, J9o, J11w … Erwachsene, zuletzt ohne Zuordnung */
ttOptionSortKey(opt) {
if (!opt) return 0;
if (opt.isNoClass) return 1e6;
if (opt.band === 'adult') return 5000 + (opt.genderMode === 'female' ? 0 : 1);
if (opt.band === 'youth' && opt.bandNum != null) {
return opt.bandNum * 10 + (opt.genderMode === 'female' ? 0 : 1);
}
if (/erwachsen|senioren/i.test(name)) return 9000;
if (/damen|herren|aktive|mixed|offen|frei/i.test(name)) return 9100;
return 5000;
return 9999;
},
formatAgeOption(opt) {
if (!opt) return '';
if (opt.isNoClass) return this.$t('tournaments.internalStatsAgeNoClass');
const bits = [];
const nameTrim = (opt.name || '').trim();
if (nameTrim) bits.push(nameTrim);
const min = opt.minBirthYear;
const max = opt.maxBirthYear;
if (!nameTrim) {
if (min != null && max != null) bits.push(`${min}${max}`);
else if (max != null) bits.push(`${max}`);
else if (min != null) bits.push(`${min}`);
if (opt.band === 'youth' && opt.bandNum != null) {
return `J${opt.bandNum} · ${this.tournamentClassGenderLabelFromMode(opt.genderMode)}`;
}
bits.push(this.tournamentClassGenderLabel(opt.gender));
return bits.join(' · ');
if (opt.band === 'adult') {
return `${this.$t('tournaments.internalStatsTtAdult')} · ${this.tournamentClassGenderLabelFromMode(opt.genderMode)}`;
}
return opt.key || '';
},
ageFilterPdfLine() {
const opts = this.stats.ageClassOptions || [];
if (opts.length <= 1) return '';
if (opts.length === 0) return '';
const allKeys = opts.map((o) => o.key);
if (this.selectedAgeKeys.length === allKeys.length) {
return this.$t('tournaments.internalStatsAgeFilterAll');

View File

@@ -181,6 +181,7 @@
"internalStatsAgeFilter": "Altersklassä & Gschlächt (Einzel)",
"tournamentClassGenderFemale": "Weiblich",
"tournamentClassGenderOpen": "Alli",
"internalStatsTtAdult": "Erwachsene",
"internalStatsAgeNoClass": "Ohni Klassäzuordnig",
"internalStatsAgeSelectAll": "Alli",
"internalStatsAgeSelectNone": "Keini",

View File

@@ -412,6 +412,7 @@
"internalStatsAgeFilter": "Altersklassen & Geschlecht (Einzel)",
"tournamentClassGenderFemale": "Weiblich",
"tournamentClassGenderOpen": "Alle",
"internalStatsTtAdult": "Erwachsene",
"internalStatsAgeNoClass": "Ohne Klassenzuordnung",
"internalStatsAgeSelectAll": "Alle",
"internalStatsAgeSelectNone": "Keine",

View File

@@ -704,6 +704,7 @@
"internalStatsAgeFilter": "Altersklassen & Geschlecht (Einzel)",
"tournamentClassGenderFemale": "Weiblich",
"tournamentClassGenderOpen": "Alle",
"internalStatsTtAdult": "Erwachsene",
"internalStatsAgeNoClass": "Ohne Klassenzuordnung",
"internalStatsAgeSelectAll": "Alle",
"internalStatsAgeSelectNone": "Keine",
@@ -714,7 +715,7 @@
"internalStatsLast6Months": "Letzte 6 Monate",
"internalStatsLast3Months": "Letzte 3 Monate",
"internalStatsTournamentsInPeriod": "{count} Turnier(e) im Zeitraum (ohne Minimeisterschaften).",
"internalStatsPointsExplain": "Wertung: Pro Gruppe wird die Platzierung als Prozentzahl ausgedrückt (bei N Teilnehmern mit Platzierung: 1. = 100 %, Letzter = 0 %, dazwischen linear; gleiche Platzierung = gleicher Wert). N umfasst alle Platzierten in der Gruppe (inkl. Gäste). Bei nur einem Teilnehmer: 100 %. Wer die K.-o.-Runde erreicht, erhält den höchsten Gruppenwert der Klasse plus 1, danach je gewonnenes K.-o.-Spiel einen weiteren Punkt. Nur Vereinsmitglieder (Einzel).",
"internalStatsPointsExplain": "Wertung: Pro Gruppe wird die Platzierung als Prozentzahl ausgedrückt (bei N Teilnehmern mit Platzierung: 1. = 100 %, Letzter = 0 %, dazwischen linear; gleiche Platzierung = gleicher Wert). N umfasst alle Platzierten in der Gruppe (inkl. Gäste). Bei nur einem Teilnehmer: 100 %. Wer die K.-o.-Runde erreicht, erhält den höchsten Gruppenwert der Klasse plus 1, danach je gewonnenes K.-o.-Spiel einen weiteren Punkt. Nur Vereinsmitglieder (Einzel). Die Filter J9J19 / Erwachsene und Weiblich/Alle beziehen sich auf das jeweilige Mitglied (Geburtsdatum und Geschlecht laut Vereinsdaten), nicht auf die Bezeichnung der Turnierklasse.",
"internalStatsAbsoluteRank": "Rangliste Gesamtwertung",
"internalStatsAverageRank": "Rangliste Durchschnitt (pro Turnier)",
"internalStatsPoints": "Summe",

View File

@@ -181,6 +181,7 @@
"internalStatsAgeFilter": "Age group & gender (singles)",
"tournamentClassGenderFemale": "Female",
"tournamentClassGenderOpen": "Open (all)",
"internalStatsTtAdult": "Adults",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",

View File

@@ -362,6 +362,7 @@
"internalStatsAgeFilter": "Age group & gender (singles)",
"tournamentClassGenderFemale": "Female",
"tournamentClassGenderOpen": "Open (all)",
"internalStatsTtAdult": "Adults",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",
@@ -372,7 +373,7 @@
"internalStatsLast6Months": "Last 6 months",
"internalStatsLast3Months": "Last 3 months",
"internalStatsTournamentsInPeriod": "{count} tournament(s) in this period (excluding mini championships).",
"internalStatsPointsExplain": "Scoring: In each group, placement is expressed as a percentage (with N ranked players: 1st = 100%, last = 0%, linear in between; tied ranks share the same value). N counts everyone ranked in that group (including guests). With only one player: 100%. Players who reach the knockout get the highest group score in that class plus 1, then one extra point per knockout match won. Club members in singles classes only.",
"internalStatsPointsExplain": "Scoring: In each group, placement is expressed as a percentage (with N ranked players: 1st = 100%, last = 0%, linear in between; tied ranks share the same value). N counts everyone ranked in that group (including guests). With only one player: 100%. Players who reach the knockout get the highest group score in that class plus 1, then one extra point per knockout match won. Club members in singles classes only. The J9J19 / adults and female/open filters use each members birth date and gender from club records, not the tournament class name.",
"internalStatsAbsoluteRank": "Total score ranking",
"internalStatsAverageRank": "Average per tournament",
"internalStatsPoints": "Total",

View File

@@ -181,6 +181,7 @@
"internalStatsAgeFilter": "Age group & gender (singles)",
"tournamentClassGenderFemale": "Female",
"tournamentClassGenderOpen": "Open (all)",
"internalStatsTtAdult": "Adults",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "Edad y género (individual)",
"tournamentClassGenderFemale": "Femenino",
"tournamentClassGenderOpen": "Abierta (todos)",
"internalStatsTtAdult": "Adultos",
"internalStatsAgeNoClass": "Sin clase asignada",
"internalStatsAgeSelectAll": "Todas",
"internalStatsAgeSelectNone": "Ninguna",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "Edad at kasarian (singles)",
"tournamentClassGenderFemale": "Babae",
"tournamentClassGenderOpen": "Bukas (lahat)",
"internalStatsTtAdult": "Adults",
"internalStatsAgeNoClass": "Walang klase",
"internalStatsAgeSelectAll": "Lahat",
"internalStatsAgeSelectNone": "Wala",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "Âge et genre (simple)",
"tournamentClassGenderFemale": "Féminin",
"tournamentClassGenderOpen": "Tous",
"internalStatsTtAdult": "Adultes",
"internalStatsAgeNoClass": "Sans classe assignée",
"internalStatsAgeSelectAll": "Tout",
"internalStatsAgeSelectNone": "Aucune",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "Età e genere (singolo)",
"tournamentClassGenderFemale": "Femminile",
"tournamentClassGenderOpen": "Aperta (tutti)",
"internalStatsTtAdult": "Adulti",
"internalStatsAgeNoClass": "Senza classe assegnata",
"internalStatsAgeSelectAll": "Tutte",
"internalStatsAgeSelectNone": "Nessuna",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "年齢・性別(シングルス)",
"tournamentClassGenderFemale": "女子",
"tournamentClassGenderOpen": "オープン(全員)",
"internalStatsTtAdult": "一般・シニア",
"internalStatsAgeNoClass": "クラス未設定",
"internalStatsAgeSelectAll": "すべて",
"internalStatsAgeSelectNone": "なし",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "Grupa wiekowa i płeć (singel)",
"tournamentClassGenderFemale": "Kobiety",
"tournamentClassGenderOpen": "Otwarta (wszyscy)",
"internalStatsTtAdult": "Dorośli",
"internalStatsAgeNoClass": "Bez przypisania do klasy",
"internalStatsAgeSelectAll": "Wszystkie",
"internalStatsAgeSelectNone": "Żadna",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "รุ่นอายุและเพศ (เดี่ยว)",
"tournamentClassGenderFemale": "หญิง",
"tournamentClassGenderOpen": "เปิด (ทุกคน)",
"internalStatsTtAdult": "ประเภทใหญ่",
"internalStatsAgeNoClass": "ไม่มีคลาส",
"internalStatsAgeSelectAll": "ทั้งหมด",
"internalStatsAgeSelectNone": "ไม่มี",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "Edad at kasarian (singles)",
"tournamentClassGenderFemale": "Babae",
"tournamentClassGenderOpen": "Bukas (lahat)",
"internalStatsTtAdult": "Adults",
"internalStatsAgeNoClass": "Walang klase",
"internalStatsAgeSelectAll": "Lahat",
"internalStatsAgeSelectNone": "Wala",

View File

@@ -180,6 +180,7 @@
"internalStatsAgeFilter": "年龄与性别(单打)",
"tournamentClassGenderFemale": "女子",
"tournamentClassGenderOpen": "公开组(全体)",
"internalStatsTtAdult": "成人组",
"internalStatsAgeNoClass": "未分配级别",
"internalStatsAgeSelectAll": "全选",
"internalStatsAgeSelectNone": "全不选",