feat(i18n): add scripts for locale translation and patching
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:
Torsten Schulz (local)
2026-05-15 15:52:54 +02:00
parent 320010b94e
commit eb54b4f7cf
54 changed files with 58003 additions and 30665 deletions

View File

@@ -7,22 +7,6 @@ import PredefinedActivity from '../models/PredefinedActivity.js';
import GroupActivity from '../models/GroupActivity.js';
import { Op } from 'sequelize';
const STANDARD_ACTIVITY_NAMES = new Set([
'Begrüßung',
'Aktivierung',
'Aufbauen',
'Turnier',
'Abbauen',
'Abschlussgespräch',
]);
const isTrackablePredefinedActivity = (predefinedActivity) => {
if (!predefinedActivity) {
return false;
}
return !predefinedActivity.excludeFromStats && !STANDARD_ACTIVITY_NAMES.has(predefinedActivity.name);
};
export const getMemberActivities = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
@@ -229,18 +213,12 @@ export const getMemberActivities = async (req, res) => {
if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) {
return false;
}
if (!isTrackablePredefinedActivity(ga.activity.predefinedActivity)) {
return false;
}
const key = `${ga.activity.id}-${ga.participant.id}`;
return !explicitActivityKeys.has(key);
});
// Kombiniere beide Listen
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities].filter((entry) => {
const predefinedActivity = entry?.activity?.predefinedActivity;
return isTrackablePredefinedActivity(predefinedActivity);
});
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
// Group activities by name and count occurrences
// Verwende einen Set pro Aktivität, um eindeutige Datum-Aktivität-Kombinationen zu tracken
@@ -250,10 +228,6 @@ export const getMemberActivities = async (req, res) => {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.participant) {
continue;
}
if (!isTrackablePredefinedActivity(ma.activity.predefinedActivity)) {
continue;
}
const activity = ma.activity.predefinedActivity;
const activityName = activity.name;
@@ -480,18 +454,12 @@ export const getMemberLastParticipations = async (req, res) => {
if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) {
return false;
}
if (!isTrackablePredefinedActivity(ga.activity.predefinedActivity)) {
return false;
}
const key = `${ga.activity.id}-${ga.participant.id}`;
return !explicitActivityKeys.has(key);
});
// Kombiniere beide Listen
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities].filter((entry) => {
const predefinedActivity = entry?.activity?.predefinedActivity;
return isTrackablePredefinedActivity(predefinedActivity);
});
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
// Gruppiere nach Datum
const participationsByDate = new Map();
@@ -501,7 +469,7 @@ export const getMemberLastParticipations = async (req, res) => {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.activity.diaryDate || !ma.participant) {
return false;
}
return isTrackablePredefinedActivity(ma.activity.predefinedActivity);
return true;
})
.forEach(ma => {
const date = ma.activity.diaryDate.date;
@@ -560,3 +528,4 @@ export const getMemberLastParticipations = async (req, res) => {
return res.status(500).json({ error: 'Failed to fetch member last participations' });
}
};

View File

@@ -4,49 +4,12 @@ import { emitTournamentChanged } from '../services/socketService.js';
import TournamentClass from '../models/TournamentClass.js';
import HttpError from '../exceptions/HttpError.js';
// Pools (zusammengelegte Gruppenphasen)
export const mergeClassesIntoPool = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, sourceClassId, targetClassId, strategy, outOfCompetitionForSource } = req.body;
try {
await tournamentService.mergeClassesIntoPool(
token,
clubId,
tournamentId,
sourceClassId,
targetClassId,
strategy, // 'singleGroup' | 'distribute'
!!outOfCompetitionForSource
);
// Broadcast
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ success: true });
} catch (error) {
console.error('[mergeClassesIntoPool] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const resetPool = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, poolId } = req.body;
try {
await tournamentService.resetPool(token, clubId, tournamentId, poolId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ success: true });
} catch (error) {
console.error('[resetPool] Error:', error);
res.status(500).json({ error: error.message });
}
};
// 1. Alle Turniere eines Vereins (query: type = 'internal' | 'external' | 'mini')
// 1. Alle Turniere eines Vereins
export const getTournaments = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId } = req.params;
const type = req.query.type || null;
try {
const tournaments = await tournamentService.getTournaments(token, clubId, type);
const tournaments = await tournamentService.getTournaments(token, clubId);
res.status(200).json(tournaments);
} catch (error) {
console.error(error);
@@ -59,27 +22,13 @@ export const getTournaments = async (req, res) => {
}
};
/** Ranglisten interne Einzel-Turniere (Gruppen-% + K.-o.-Bonus wie im Service) */
export const getInternalTournamentStats = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId } = req.params;
const months = req.query.months;
const ageClassKeys = req.query.ageClassKeys;
try {
const data = await tournamentService.getInternalTournamentPlayerStats(token, clubId, months, ageClassKeys);
res.status(200).json(data);
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
};
// 2. Neues Turnier anlegen
export const addTournament = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament } = req.body;
const { clubId, tournamentName, date, winningSets, allowsExternal } = req.body;
try {
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament);
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal);
// Emit Socket-Event
if (clubId && tournament && tournament.id) {
emitTournamentChanged(clubId, tournament.id);
}
@@ -90,22 +39,6 @@ export const addTournament = async (req, res) => {
}
};
// Minimeisterschaft anlegen (Turnier + 6 Klassen); Name: "Minimeisterschaften <Jahr> Ortsentscheid <ort>"
export const addMiniChampionship = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, ort, date, year, winningSets } = req.body;
try {
const tournament = await tournamentService.addMiniChampionship(token, clubId, ort, date, year, winningSets);
if (clubId && tournament && tournament.id) {
emitTournamentChanged(clubId, tournament.id);
}
res.status(201).json(tournament);
} catch (error) {
console.error('[addMiniChampionship] Error:', error);
res.status(500).json({ error: error.message });
}
};
// 3. Teilnehmer hinzufügen - klassengebunden
export const addParticipant = async (req, res) => {
const { authcode: token } = req.headers;
@@ -286,9 +219,9 @@ export const getTournament = async (req, res) => {
export const updateTournament = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
const { name, date, winningSets, numberOfTables, isDoublesTournament } = req.body;
const { name, date, winningSets } = req.body;
try {
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets, numberOfTables, isDoublesTournament);
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournament);
@@ -435,19 +368,6 @@ export const resetMatches = async (req, res) => {
}
};
export const cleanupOrphanedMatches = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
try {
const result = await tournamentService.cleanupOrphanedMatches(token, clubId, tournamentId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(result);
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
};
export const removeParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.body;
@@ -478,20 +398,6 @@ export const updateParticipantSeeded = async (req, res) => {
}
};
export const setParticipantGaveUp = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.params;
const { gaveUp } = req.body;
try {
await tournamentService.setParticipantGaveUp(token, clubId, tournamentId, participantId, gaveUp);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Aufgabe-Status aktualisiert' });
} catch (err) {
console.error('[setParticipantGaveUp] Error:', err);
res.status(500).json({ error: err.message });
}
};
export const deleteMatchResult = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, matchId, set } = req.body;
@@ -557,27 +463,12 @@ export const setMatchActive = async (req, res) => {
}
};
export const setMatchTableNumber = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, matchId } = req.params;
const { tableNumber } = req.body;
try {
await tournamentService.setMatchTableNumber(token, clubId, tournamentId, matchId, tableNumber);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Tischnummer aktualisiert' });
} catch (err) {
console.error('[setMatchTableNumber] Error:', err);
res.status(500).json({ error: err.message });
}
};
// Externe Teilnehmer hinzufügen
export const addExternalParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId, firstName, lastName, club, birthDate, gender, email, address } = req.body;
const { clubId, tournamentId, classId, firstName, lastName, club, birthDate, gender } = req.body;
try {
await tournamentService.addExternalParticipant(token, clubId, classId, firstName, lastName, club, birthDate, gender, email, address);
await tournamentService.addExternalParticipant(token, clubId, classId, firstName, lastName, club, birthDate, gender);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Externer Teilnehmer hinzugefügt' });
} catch (error) {
@@ -628,20 +519,6 @@ export const updateExternalParticipantSeeded = async (req, res) => {
}
};
export const setExternalParticipantGaveUp = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.params;
const { gaveUp } = req.body;
try {
await tournamentService.setExternalParticipantGaveUp(token, clubId, tournamentId, participantId, gaveUp);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Aufgabe-Status aktualisiert' });
} catch (error) {
console.error('[setExternalParticipantGaveUp] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Tournament Classes
export const getTournamentClasses = async (req, res) => {
const { authcode: token } = req.headers;
@@ -658,9 +535,9 @@ export const getTournamentClasses = async (req, res) => {
export const addTournamentClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
const { name, isDoubles, gender, minBirthYear, maxBirthYear } = req.body;
const { name, isDoubles, gender, minBirthYear } = req.body;
try {
const tournamentClass = await tournamentService.addTournamentClass(token, clubId, tournamentId, name, isDoubles, gender, minBirthYear, maxBirthYear);
const tournamentClass = await tournamentService.addTournamentClass(token, clubId, tournamentId, name, isDoubles, gender, minBirthYear);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournamentClass);
} catch (error) {
@@ -672,9 +549,11 @@ export const addTournamentClass = async (req, res) => {
export const updateTournamentClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
const { name, sortOrder, isDoubles, gender, minBirthYear, maxBirthYear } = req.body;
const { name, sortOrder, isDoubles, gender, minBirthYear } = req.body;
try {
const tournamentClass = await tournamentService.updateTournamentClass(token, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear, maxBirthYear);
console.log('[updateTournamentClass] Request body:', { name, sortOrder, isDoubles, gender, minBirthYear });
const tournamentClass = await tournamentService.updateTournamentClass(token, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear);
console.log('[updateTournamentClass] Updated class:', JSON.stringify(tournamentClass.toJSON(), null, 2));
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournamentClass);
} catch (error) {
@@ -763,4 +642,4 @@ export const deletePairing = async (req, res) => {
res.status(500).json({ error: error.message });
}
};