diff --git a/app/routes/auth.py b/app/routes/auth.py index 247df7a..4c7f708 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -14,6 +14,7 @@ from flask_login import login_user, logout_user, login_required, current_user from app.extensions import db, hash_password, check_password, limiter from app.models import User, Player, ESPORT_GAMES from app.validators import RegisterSchema, LoginSchema +from app.i18n import LOCALE_SESSION_KEY from app.logging_config import log_auth_event from flask_babel import gettext as _ from marshmallow import ValidationError @@ -126,7 +127,7 @@ def login(): except ValidationError as err: for field, messages in err.messages.items(): for msg in messages: - flash(f'{field}: {msg}', 'danger') + flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger') return render_template('pages/login.html') username = validated['username'] @@ -157,11 +158,19 @@ def login(): db.session.commit() # Clear old session data and preserve CSRF token to prevent - # session fixation attacks (Flask-Login rotates the session ID) - _csrf_token = session.get('csrf_token') + # session fixation attacks (Flask-Login rotates the session ID). + # + # The language choice is carried across too. Someone who reads the + # login page in English and signs in would otherwise be dropped + # back into French — the preference lives in the session, and + # clearing it discards a decision the user just made. + _preserved = { + key: session[key] + for key in ('csrf_token', LOCALE_SESSION_KEY) + if key in session + } session.clear() - if _csrf_token: - session['csrf_token'] = _csrf_token + session.update(_preserved) # Mark the session permanent so PERMANENT_SESSION_LIFETIME applies. # Without this, Flask emits a browser-session cookie with no expiry @@ -255,7 +264,7 @@ def register(): except ValidationError as err: for field, messages in err.messages.items(): for msg in messages: - flash(f'{field}: {msg}', 'danger') + flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger') captcha = generate_captcha() # Clear password fields on validation failure form_data.pop('password', None) @@ -517,6 +526,11 @@ def logout(): """ log_auth_event('logout', username=current_user.username, user_id=current_user.id) logout_user() + # Same reasoning as at login: the language is a display preference, not + # session state belonging to the account being signed out. + _locale = session.get(LOCALE_SESSION_KEY) session.clear() + if _locale: + session[LOCALE_SESSION_KEY] = _locale flash(_('You have been logged out.'), 'info') return redirect(url_for('auth.login')) \ No newline at end of file diff --git a/app/routes/evaluations.py b/app/routes/evaluations.py index 31adfdf..f89057b 100644 --- a/app/routes/evaluations.py +++ b/app/routes/evaluations.py @@ -5,6 +5,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons. from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_required, current_user +from flask_babel import gettext as _ from app.extensions import db from app.models import ( Admin, Player, @@ -37,7 +38,7 @@ def list_evaluations(): user = current_user if isinstance(user, Player): - flash('You do not have permission to view evaluations.', 'danger') + flash(_('You do not have permission to view evaluations.'), 'danger') return redirect(url_for('main.dashboard')) sort_column = request.args.get('sort', 'created_at') @@ -118,24 +119,24 @@ def list_evaluations(): def evaluate_player(tryout_id, player_id): """Evaluate a specific player in a tryout.""" if not current_user.can_evaluate(): - flash('You do not have permission to evaluate players.', 'danger') + flash(_('You do not have permission to evaluate players.'), 'danger') return redirect(url_for('main.dashboard')) tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('You do not have permission to evaluate players in this tryout.', 'danger') + flash(_('You do not have permission to evaluate players in this tryout.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) is_registered = TryoutRegistration.query.filter_by( tryout_id=tryout_id, player_id=player_id, ).first() is not None if not is_registered: - flash('Player is not registered for this tryout.', 'danger') + flash(_('Player is not registered for this tryout.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) player = User.query.get_or_404(player_id) if not isinstance(player, Player): - flash('Can only evaluate players.', 'danger') + flash(_('Can only evaluate players.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) existing_eval = Evaluation.query.filter_by( @@ -173,7 +174,7 @@ def evaluate_player(tryout_id, player_id): existing_eval.overall_score = overall existing_eval.comments = comments existing_eval.position_recommendation = position - flash('Evaluation updated!', 'success') + flash(_('Evaluation updated!'), 'success') else: evaluation = Evaluation( tryout_id=tryout_id, player_id=player_id, @@ -186,7 +187,7 @@ def evaluate_player(tryout_id, player_id): comments=comments, position_recommendation=position, ) db.session.add(evaluation) - flash('Evaluation submitted successfully!', 'success') + flash(_('Evaluation submitted successfully!'), 'success') db.session.commit() return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) @@ -211,12 +212,12 @@ def evaluate_player(tryout_id, player_id): def players_to_evaluate(tryout_id): """List players that need evaluation in a specific tryout.""" if not current_user.can_evaluate(): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('main.dashboard')) tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('You do not have permission to evaluate players in this tryout.', 'danger') + flash(_('You do not have permission to evaluate players in this tryout.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all() diff --git a/app/routes/main.py b/app/routes/main.py index b119eaf..7e4a79f 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -5,6 +5,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons. from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_required, current_user +from flask_babel import gettext as _ from app.extensions import db from app.models import ( Admin, Manager, Coach, Player, Scout, @@ -39,7 +40,7 @@ def set_language(locale): from app.routes.auth import is_safe_url if not set_locale(locale): - flash('That language is not available.', 'warning') + flash(_('That language is not available.'), 'warning') target = request.referrer if target and is_safe_url(target): diff --git a/app/routes/matches.py b/app/routes/matches.py index 35b931c..63a2a2e 100644 --- a/app/routes/matches.py +++ b/app/routes/matches.py @@ -5,6 +5,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons. from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user +from flask_babel import gettext as _ from app.extensions import db from app.models import ( Admin, Manager, Coach, Player, Scout, @@ -197,11 +198,11 @@ def create_match(tryout_id): """Create a new match / scrimmage within a tryout.""" tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('You do not have permission to schedule matches for this tryout.', 'danger') + flash(_('You do not have permission to schedule matches for this tryout.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) if tryout.is_ended: - flash('This tryout has ended. Matches can no longer be created or modified.', 'danger') + flash(_('This tryout has ended. Matches can no longer be created or modified.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) teams = Team.query.filter_by(tryout_id=tryout_id).all() @@ -220,14 +221,14 @@ def create_match(tryout_id): match_type = request.form.get('match_type') if not start_time_str: - flash('Start time is required. Please select a time slot.', 'danger') + flash(_('Start time is required. Please select a time slot.'), 'danger') return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players, prefill_date=prefill_date) try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date except (ValueError, TypeError): - flash('Invalid date format.', 'danger') + flash(_('Invalid date format.'), 'danger') return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players, prefill_date=prefill_date) @@ -242,7 +243,7 @@ def create_match(tryout_id): end_dt = start_dt + timedelta(minutes=30) end_time = end_dt.time() except ValueError: - flash('Invalid time format.', 'danger') + flash(_('Invalid time format.'), 'danger') return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players) match = Match( @@ -313,7 +314,7 @@ def create_match(tryout_id): reference_id=reference_id, ) - flash('Match scheduled successfully!', 'success') + flash(_('Match scheduled successfully!'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return render_template('pages/match_form.html', tryout=tryout, teams=teams, @@ -328,11 +329,11 @@ def edit_match(match_id): tryout = match.tryout if not current_user.can_manage_this_tryout(tryout): - flash('You do not have permission to edit this match.', 'danger') + flash(_('You do not have permission to edit this match.'), 'danger') return redirect(url_for('matches.calendar')) if tryout.is_ended: - flash('This tryout has ended. Matches can no longer be created or modified.', 'danger') + flash(_('This tryout has ended. Matches can no longer be created or modified.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) teams = Team.query.filter_by(tryout_id=tryout.id).all() @@ -355,13 +356,13 @@ def edit_match(match_id): try: match.date = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid date format.', 'danger') + flash(_('Invalid date format.'), 'danger') return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids) if not start_time_str: - flash('Start time is required.', 'danger') + flash(_('Start time is required.'), 'danger') return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids) @@ -457,7 +458,7 @@ def edit_match(match_id): reference_id=reference_id, ) - flash('Match updated successfully!', 'success') + flash(_('Match updated successfully!'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) participants_map = {} @@ -502,10 +503,10 @@ def delete_match(match_id): match = Match.query.get_or_404(match_id) tryout = match.tryout if not current_user.can_manage_this_tryout(tryout): - flash('You do not have permission to delete this match.', 'danger') + flash(_('You do not have permission to delete this match.'), 'danger') return redirect(url_for('matches.calendar')) if tryout.is_ended: - flash('This tryout has ended. Matches can no longer be deleted.', 'danger') + flash(_('This tryout has ended. Matches can no longer be deleted.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) # Notes outlive the match they were taken during: a coach's observation @@ -517,7 +518,7 @@ def delete_match(match_id): db.session.delete(match) db.session.commit() - flash('Match deleted successfully.', 'success') + flash(_('Match deleted successfully.'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) diff --git a/app/routes/team_matches.py b/app/routes/team_matches.py index ef9914c..138080d 100644 --- a/app/routes/team_matches.py +++ b/app/routes/team_matches.py @@ -5,6 +5,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons. from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user +from flask_babel import gettext as _ from app.extensions import db from app.models import ( Admin, Manager, Coach, Player, @@ -93,7 +94,7 @@ def create_match(team_id): """Create a new regular-season team match.""" team = OrgTeam.query.get_or_404(team_id) if not can_manage_team_match(team): - flash('You do not have permission to schedule matches for this team.', 'danger') + flash(_('You do not have permission to schedule matches for this team.'), 'danger') return redirect(url_for('team_matches.list_matches')) team_players = [tp for tp in TeamPlayer.query.filter_by(org_team_id=team_id).all()] @@ -128,14 +129,14 @@ def create_match(team_id): location = request.form.get('location', '') if not date_str: - flash('Date is required.', 'danger') + flash(_('Date is required.'), 'danger') return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date) try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid date format.', 'danger') + flash(_('Invalid date format.'), 'danger') return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date, is_practice=is_practice) @@ -152,7 +153,7 @@ def create_match(team_id): end_dt = start_dt + timedelta(minutes=30) end_time = end_dt.time() except ValueError: - flash('Invalid time format.', 'danger') + flash(_('Invalid time format.'), 'danger') return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date, is_practice=is_practice) @@ -191,7 +192,7 @@ def create_match(team_id): reference_id=reference_id, ) - flash(f'Team match "{title}" scheduled successfully!', 'success') + flash(_('Team match "%(title)s" scheduled successfully!', title=title), 'success') return redirect(url_for('team_matches.list_matches')) return render_template('pages/team_match_form.html', team=team, team_players=team_players) @@ -205,7 +206,7 @@ def edit_match(match_id): team = team_match.org_team if not can_manage_team_match(team): - flash('You do not have permission to edit this match.', 'danger') + flash(_('You do not have permission to edit this match.'), 'danger') return redirect(url_for('team_matches.list_matches')) if request.method == 'POST': @@ -218,7 +219,7 @@ def edit_match(match_id): try: team_match.date = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid date format.', 'danger') + flash(_('Invalid date format.'), 'danger') return redirect(url_for('team_matches.edit_match', match_id=match_id)) start_time_str = request.form.get('start_time') @@ -241,7 +242,7 @@ def edit_match(match_id): team_match.status = status db.session.commit() - flash('Match updated successfully!', 'success') + flash(_('Match updated successfully!'), 'success') return redirect(url_for('team_matches.list_matches')) return render_template('pages/team_match_form.html', @@ -255,11 +256,11 @@ def delete_match(match_id): team_match = TeamMatch.query.get_or_404(match_id) team = team_match.org_team if not can_manage_team_match(team): - flash('You do not have permission to delete this match.', 'danger') + flash(_('You do not have permission to delete this match.'), 'danger') return redirect(url_for('team_matches.list_matches')) db.session.delete(team_match) db.session.commit() - flash('Match deleted successfully.', 'success') + flash(_('Match deleted successfully.'), 'success') return redirect(url_for('team_matches.list_matches')) diff --git a/app/routes/teams.py b/app/routes/teams.py index bd7e75c..fb41938 100644 --- a/app/routes/teams.py +++ b/app/routes/teams.py @@ -5,6 +5,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons. from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user +from flask_babel import gettext as _ from app.extensions import db from app.models import ( Admin, Manager, Coach, Player, @@ -39,10 +40,10 @@ def list_teams(): ) ).order_by(OrgTeam.name).all() elif isinstance(current_user, Player): - flash('Use My Team(s) to view your teams.', 'info') + flash(_('Use My Team(s) to view your teams.'), 'info') return redirect(url_for('teams.my_teams')) else: - flash('You do not have permission to view teams.', 'danger') + flash(_('You do not have permission to view teams.'), 'danger') return redirect(url_for('main.dashboard')) coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all() @@ -57,7 +58,7 @@ def list_teams(): def my_teams(): """View the player's own teams with upcoming matches.""" if not isinstance(current_user, Player): - flash('This page is for players.', 'info') + flash(_('This page is for players.'), 'info') return redirect(url_for('teams.list_teams')) from app.models import TeamMatch, TeamMatchParticipant @@ -99,7 +100,7 @@ def my_teams(): def create_team(): """Create a new organization team.""" if not current_user.can_manage_teams(): - flash('You do not have permission to create teams.', 'danger') + flash(_('You do not have permission to create teams.'), 'danger') return redirect(url_for('teams.list_teams')) name = request.form.get('name') @@ -107,12 +108,12 @@ def create_team(): manager_id = request.form.get('manager_id') if not name: - flash('Team name is required.', 'danger') + flash(_('Team name is required.'), 'danger') return redirect(url_for('teams.list_teams')) existing = OrgTeam.query.filter_by(name=name).first() if existing: - flash(f'Team "{name}" already exists.', 'danger') + flash(_('Team "%(name)s" already exists.', name=name), 'danger') return redirect(url_for('teams.list_teams')) team = OrgTeam( @@ -134,7 +135,7 @@ def create_team(): team.managers.append(manager_user) db.session.commit() - flash(f'Team "{name}" created successfully!', 'success') + flash(_('Team "%(name)s" created successfully!', name=name), 'success') return redirect(url_for('teams.list_teams')) @@ -144,7 +145,7 @@ def edit_team(team_id): """Edit an existing organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('You do not have permission to edit this team.', 'danger') + flash(_('You do not have permission to edit this team.'), 'danger') return redirect(url_for('teams.list_teams')) name = request.form.get('name') @@ -152,12 +153,12 @@ def edit_team(team_id): manager_id = request.form.get('manager_id') if not name: - flash('Team name is required.', 'danger') + flash(_('Team name is required.'), 'danger') return redirect(url_for('teams.list_teams')) existing = OrgTeam.query.filter(OrgTeam.name == name, OrgTeam.id != team_id).first() if existing: - flash(f'Team "{name}" already exists.', 'danger') + flash(_('Team "%(name)s" already exists.', name=name), 'danger') return redirect(url_for('teams.list_teams')) if request.form.get('sync_staff') == '1': @@ -195,7 +196,7 @@ def edit_team(team_id): team.managers.append(manager_user) db.session.commit() - flash(f'Team "{name}" updated successfully!', 'success') + flash(_('Team "%(name)s" updated successfully!', name=name), 'success') return redirect(url_for('teams.list_teams')) @@ -204,7 +205,7 @@ def edit_team(team_id): def delete_team(team_id): """Delete an organization team.""" if not current_user.can_manage_teams(): - flash('You do not have permission to delete teams.', 'danger') + flash(_('You do not have permission to delete teams.'), 'danger') return redirect(url_for('teams.list_teams')) team = OrgTeam.query.get_or_404(team_id) @@ -235,7 +236,7 @@ def delete_team(team_id): db.session.delete(team) db.session.commit() - flash(f'Team "{name}" deleted successfully.', 'success') + flash(_('Team "%(name)s" deleted successfully.', name=name), 'success') return redirect(url_for('teams.list_teams')) @@ -245,28 +246,28 @@ def add_coach(team_id): """Add a coach to an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) coach_id = request.form.get('coach_id') if not coach_id: - flash('Please select a coach.', 'danger') + flash(_('Please select a coach.'), 'danger') return redirect(url_for('teams.list_teams')) coach = User.query.get_or_404(int(coach_id)) if not isinstance(coach, Coach): - flash('Only coaches can be assigned as coach.', 'danger') + flash(_('Only coaches can be assigned as coach.'), 'danger') return redirect(url_for('teams.list_teams')) if team.coaches.filter_by(id=coach.id).first(): - flash(f'{coach.username} is already a coach of {team.name}.', 'info') + flash(_('%(username)s is already a coach of %(name)s.', username=coach.username, name=team.name), 'info') return redirect(url_for('teams.list_teams')) team.coaches.append(coach) if not team.coach_id: team.coach_id = coach.id db.session.commit() - flash(f'{coach.username} added as coach of {team.name}.', 'success') + flash(_('%(username)s added as coach of %(name)s.', username=coach.username, name=team.name), 'success') return redirect(url_for('teams.list_teams')) @@ -276,28 +277,28 @@ def add_manager(team_id): """Add a manager to an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) manager_id = request.form.get('manager_id') if not manager_id: - flash('Please select a manager.', 'danger') + flash(_('Please select a manager.'), 'danger') return redirect(url_for('teams.list_teams')) manager = User.query.get_or_404(int(manager_id)) if not isinstance(manager, Manager): - flash('Only managers can be assigned as manager.', 'danger') + flash(_('Only managers can be assigned as manager.'), 'danger') return redirect(url_for('teams.list_teams')) if team.managers.filter_by(id=manager.id).first(): - flash(f'{manager.username} is already a manager of {team.name}.', 'info') + flash(_('%(username)s is already a manager of %(name)s.', username=manager.username, name=team.name), 'info') return redirect(url_for('teams.list_teams')) team.managers.append(manager) if not team.manager_id: team.manager_id = manager.id db.session.commit() - flash(f'{manager.username} added as manager of {team.name}.', 'success') + flash(_('%(username)s added as manager of %(name)s.', username=manager.username, name=team.name), 'success') return redirect(url_for('teams.list_teams')) @@ -307,7 +308,7 @@ def remove_coach(team_id): """Remove a coach from an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) coach_id = request.form.get('coach_id') @@ -322,7 +323,7 @@ def remove_coach(team_id): team.coach_id = None db.session.commit() - flash(f'Coach removed from {team.name}.', 'success') + flash(_('Coach removed from %(name)s.', name=team.name), 'success') return redirect(url_for('teams.list_teams')) @@ -332,7 +333,7 @@ def remove_manager(team_id): """Remove a manager from an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) manager_id = request.form.get('manager_id') @@ -347,7 +348,7 @@ def remove_manager(team_id): team.manager_id = None db.session.commit() - flash(f'Manager removed from {team.name}.', 'success') + flash(_('Manager removed from %(name)s.', name=team.name), 'success') return redirect(url_for('teams.list_teams')) @@ -357,29 +358,29 @@ def add_player(team_id): """Add a player to an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) player_id = request.form.get('player_id') status = request.form.get('status', 'starter') if not player_id: - flash('Please select a player.', 'danger') + flash(_('Please select a player.'), 'danger') return redirect(url_for('teams.list_teams')) player = User.query.get_or_404(int(player_id)) if not isinstance(player, Player): - flash('Can only assign players to teams.', 'danger') + flash(_('Can only assign players to teams.'), 'danger') return redirect(url_for('teams.list_teams')) existing = TeamPlayer.query.filter_by(player_id=player.id, org_team_id=team.id).first() if existing: - flash(f'{player.username} is already on {team.name}.', 'info') + flash(_('%(username)s is already on %(name)s.', username=player.username, name=team.name), 'info') return redirect(url_for('teams.list_teams')) tp = TeamPlayer(player_id=player.id, org_team_id=team.id, status=status) db.session.add(tp) db.session.commit() - flash(f'{player.username} added to {team.name}!', 'success') + flash(_('%(username)s added to %(name)s!', username=player.username, name=team.name), 'success') return redirect(url_for('teams.list_teams')) @@ -389,18 +390,18 @@ def remove_player(team_id, player_id): """Remove a player from an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) player = User.query.get_or_404(player_id) tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first() if not tp: - flash(f'{player.username} is not on {team.name}.', 'danger') + flash(_('%(username)s is not on %(name)s.', username=player.username, name=team.name), 'danger') return redirect(url_for('teams.list_teams')) db.session.delete(tp) db.session.commit() - flash(f'{player.username} removed from {team.name}.', 'success') + flash(_('%(username)s removed from %(name)s.', username=player.username, name=team.name), 'success') return redirect(url_for('teams.list_teams')) @@ -430,7 +431,7 @@ def add_team_note(team_id): """Add a team improvement note (coaches only).""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('You do not have permission to add notes to this team.', 'danger') + flash(_('You do not have permission to add notes to this team.'), 'danger') return redirect(url_for('teams.list_teams')) content = request.form.get('content', '').strip() @@ -438,7 +439,7 @@ def add_team_note(team_id): note = TeamNote(org_team_id=team_id, coach_id=current_user.id, content=content) db.session.add(note) db.session.commit() - flash('Team notes added successfully!', 'success') + flash(_('Team notes added successfully!'), 'success') return redirect(url_for('teams.list_teams')) @@ -448,17 +449,17 @@ def add_player_note(team_id, player_id): """Add a personal note for a player (coaches only).""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): - flash('You do not have permission to add notes to this team.', 'danger') + flash(_('You do not have permission to add notes to this team.'), 'danger') return redirect(url_for('teams.list_teams')) player = User.query.get_or_404(player_id) if not isinstance(player, Player): - flash('Can only add notes for players.', 'danger') + flash(_('Can only add notes for players.'), 'danger') return redirect(url_for('teams.list_teams')) tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first() if not tp: - flash(f'{player.username} is not on {team.name}.', 'danger') + flash(_('%(username)s is not on %(name)s.', username=player.username, name=team.name), 'danger') return redirect(url_for('teams.list_teams')) content = request.form.get('content', '').strip() @@ -466,5 +467,5 @@ def add_player_note(team_id, player_id): note = PersonalNote(player_id=player_id, coach_id=current_user.id, content=content) db.session.add(note) db.session.commit() - flash(f'Note added for {player.username}!', 'success') + flash(_('Note added for %(username)s!', username=player.username), 'success') return redirect(url_for('teams.list_teams')) \ No newline at end of file diff --git a/app/routes/tryouts.py b/app/routes/tryouts.py index 2a2c36c..f80823f 100644 --- a/app/routes/tryouts.py +++ b/app/routes/tryouts.py @@ -6,6 +6,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons. from flask import Blueprint, render_template, redirect, url_for, flash, request, abort from flask_login import login_required, current_user +from flask_babel import gettext as _ from app.extensions import db from app.models import ( Admin, Manager, Coach, Player, Scout, @@ -39,7 +40,7 @@ def list_tryouts(): def create_tryout(): """Create a new tryout event. Requires Admin or Manager.""" if not can_manage(): - flash('You do not have permission to create tryouts.', 'danger') + flash(_('You do not have permission to create tryouts.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) org_teams = OrgTeam.query.order_by(OrgTeam.name).all() @@ -61,7 +62,7 @@ def create_tryout(): try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid start date format.', 'danger') + flash(_('Invalid start date format.'), 'danger') return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) @@ -70,11 +71,11 @@ def create_tryout(): try: end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date() if end_date_obj < date_obj: - flash('End date cannot be before start date.', 'danger') + flash(_('End date cannot be before start date.'), 'danger') return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) except (ValueError, TypeError): - flash('Invalid end date format.', 'danger') + flash(_('Invalid end date format.'), 'danger') return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) @@ -96,7 +97,7 @@ def create_tryout(): tryout.coaches = coach_users db.session.commit() - flash('Tryout created successfully!', 'success') + flash(_('Tryout created successfully!'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, @@ -110,11 +111,11 @@ def edit_tryout(tryout_id): tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('You do not have permission to edit this tryout.', 'danger') + flash(_('You do not have permission to edit this tryout.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) if tryout.is_ended: - flash('This tryout has ended and can no longer be modified.', 'danger') + flash(_('This tryout has ended and can no longer be modified.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) org_teams = OrgTeam.query.order_by(OrgTeam.name).all() @@ -136,7 +137,7 @@ def edit_tryout(tryout_id): try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid start date format.', 'danger') + flash(_('Invalid start date format.'), 'danger') return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) @@ -145,11 +146,11 @@ def edit_tryout(tryout_id): try: end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date() if end_date_obj < date_obj: - flash('End date cannot be before start date.', 'danger') + flash(_('End date cannot be before start date.'), 'danger') return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) except (ValueError, TypeError): - flash('Invalid end date format.', 'danger') + flash(_('Invalid end date format.'), 'danger') return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) @@ -171,7 +172,7 @@ def edit_tryout(tryout_id): tryout.coaches = [] db.session.commit() - flash('Tryout updated successfully!', 'success') + flash(_('Tryout updated successfully!'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, @@ -203,7 +204,7 @@ def view_tryout(tryout_id): can_view = True if not can_view: - flash('You do not have permission to view this tryout.', 'danger') + flash(_('You do not have permission to view this tryout.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all() @@ -305,29 +306,29 @@ def register_for_tryout(tryout_id): """Register a player for a tryout. Only Players can self-register.""" tryout = Tryout.query.get_or_404(tryout_id) if not isinstance(current_user, Player): - flash('Only players can register for tryouts.', 'danger') + flash(_('Only players can register for tryouts.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) if tryout.status not in ['upcoming', 'in_progress']: - flash('This tryout is not accepting registrations.', 'danger') + flash(_('This tryout is not accepting registrations.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) existing = TryoutRegistration.query.filter_by( tryout_id=tryout_id, player_id=current_user.id).first() if existing: - flash('You are already registered for this tryout.', 'info') + flash(_('You are already registered for this tryout.'), 'info') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) if tryout.max_players: count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count() if count >= tryout.max_players: - flash('This tryout is full.', 'danger') + flash(_('This tryout is full.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) registration = TryoutRegistration(tryout_id=tryout_id, player_id=current_user.id) db.session.add(registration) db.session.commit() - flash('Successfully registered for tryout!', 'success') + flash(_('Successfully registered for tryout!'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) @@ -337,13 +338,13 @@ def update_status(tryout_id): """Update the status of a tryout.""" tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) new_status = request.form.get('status') if new_status in ['upcoming', 'in_progress', 'completed']: tryout.status = new_status db.session.commit() - flash(f'Tryout status updated to {new_status}.', 'success') + flash(_('Tryout status updated to %(new_status)s.', new_status=new_status), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) @@ -353,7 +354,7 @@ def update_registration_status(tryout_id, player_id): """Update a registration's attendance status.""" tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) registration = TryoutRegistration.query.filter_by( @@ -362,7 +363,7 @@ def update_registration_status(tryout_id, player_id): if new_status in ['registered', 'attended', 'no_show']: registration.status = new_status db.session.commit() - flash('Registration status updated.', 'success') + flash(_('Registration status updated.'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) @@ -372,34 +373,34 @@ def register_player(tryout_id): """Manually register a player for a tryout (by managers/coaches).""" tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) player_id = request.form.get('player_id') if not player_id: - flash('Please select a player.', 'danger') + flash(_('Please select a player.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) player = User.query.get_or_404(int(player_id)) if not isinstance(player, Player): - flash('Can only register players.', 'danger') + flash(_('Can only register players.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) existing = TryoutRegistration.query.filter_by( tryout_id=tryout_id, player_id=player.id).first() if existing: - flash(f'{player.username} is already registered for this tryout.', 'info') + flash(_('%(username)s is already registered for this tryout.', username=player.username), 'info') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) if tryout.max_players: count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count() if count >= tryout.max_players: - flash('This tryout is full.', 'danger') + flash(_('This tryout is full.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) registration = TryoutRegistration(tryout_id=tryout_id, player_id=player.id) db.session.add(registration) db.session.commit() - flash(f'{player.username} registered for tryout!', 'success') + flash(_('%(username)s registered for tryout!', username=player.username), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) @@ -409,7 +410,7 @@ def remove_player(tryout_id, player_id): """Remove a registered player from a tryout (cascades to teams/matches).""" tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) player = User.query.get_or_404(player_id) @@ -434,7 +435,7 @@ def remove_player(tryout_id, player_id): ).delete(synchronize_session=False) db.session.commit() - flash(f'{player.username} removed from tryout.', 'success') + flash(_('%(username)s removed from tryout.', username=player.username), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) @@ -444,7 +445,7 @@ def create_team(tryout_id): """Create a tryout-specific team.""" tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) team_name = request.form.get('team_name') @@ -452,7 +453,7 @@ def create_team(tryout_id): team = Team(tryout_id=tryout_id, name=team_name, created_by=current_user.id) db.session.add(team) db.session.commit() - flash(f'Team "{team_name}" created!', 'success') + flash(_('Team "%(team_name)s" created!', team_name=team_name), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) @@ -463,7 +464,7 @@ def add_to_team(tryout_id, team_id): team = Team.query.get_or_404(team_id) tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('Permission denied.', 'danger') + flash(_('Permission denied.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) # The two ids arrive independently in the URL. Without this check, being @@ -474,25 +475,25 @@ def add_to_team(tryout_id, team_id): player_id = request.form.get('player_id', type=int) if not player_id: - flash('Please select a player.', 'danger') + flash(_('Please select a player.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) # Only players registered for this tryout may be placed on its teams. is_registered = TryoutRegistration.query.filter_by( tryout_id=tryout_id, player_id=player_id).first() is not None if not is_registered: - flash('That player is not registered for this tryout.', 'danger') + flash(_('That player is not registered for this tryout.'), 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) position = request.form.get('position', '') existing = TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first() if existing: - flash('Player is already on this team.', 'info') + flash(_('Player is already on this team.'), 'info') else: member = TeamMember(team_id=team_id, player_id=player_id, position=position) db.session.add(member) db.session.commit() - flash('Player added to team!', 'success') + flash(_('Player added to team!'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) @@ -502,7 +503,7 @@ def delete_tryout(tryout_id): """Delete a tryout and all associated data (matches, teams, registrations, evaluations).""" tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): - flash('You do not have permission to delete this tryout.', 'danger') + flash(_('You do not have permission to delete this tryout.'), 'danger') return redirect(url_for('tryouts.list_tryouts')) match_ids = [m.id for m in Match.query.filter_by(tryout_id=tryout_id).all()] @@ -537,5 +538,5 @@ def delete_tryout(tryout_id): db.session.delete(tryout) db.session.commit() - flash('Tryout deleted successfully.', 'success') + flash(_('Tryout deleted successfully.'), 'success') return redirect(url_for('tryouts.list_tryouts')) \ No newline at end of file diff --git a/app/routes/users.py b/app/routes/users.py index bd2c1fb..ca4408f 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -7,6 +7,7 @@ import os import uuid from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file from flask_login import login_required, current_user +from flask_babel import gettext as _ from app.extensions import db, hash_password from app.models import ( Admin, Manager, Coach, Player, Scout, @@ -42,7 +43,7 @@ def _flash_validation_errors(err): """Surface marshmallow errors the same way auth.py already does.""" for field, messages in err.messages.items(): for msg in messages: - flash(f'{field}: {msg}', 'danger') + flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger') def _form_payload(*, checkboxes=(), list_fields=('games',), optional_blank=('password',)): @@ -112,7 +113,7 @@ _USER_CLASS_MAP = { def list_users(): """List all users for management (Admin only).""" if not isinstance(current_user, Admin): - flash('Only the president can manage users.', 'danger') + flash(_('Only the president can manage users.'), 'danger') return redirect(url_for('main.dashboard')) users = User.query.order_by(User.role, User.username).all() @@ -124,7 +125,7 @@ def list_users(): def edit_user(user_id): """Edit an existing user (Admin only).""" if not isinstance(current_user, Admin): - flash('Only the president can edit users.', 'danger') + flash(_('Only the president can edit users.'), 'danger') return redirect(url_for('main.dashboard')) user = User.query.get_or_404(user_id) @@ -164,15 +165,15 @@ def edit_user(user_id): # address surfaced as an IntegrityError, i.e. a 500. clash = User.query.filter(User.email == email, User.id != user.id).first() if clash: - flash('Email already in use by another account.', 'danger') + flash(_('Email already in use by another account.'), 'danger') return _rerender() if user.role != role: # Two ways to lock everyone out of administration, neither of # which any interface can undo afterwards. if user.id == actor_id: - flash('You cannot change your own role. Ask another president ' - 'to do it.', 'danger') + flash(_('You cannot change your own role. Ask another ' + 'president to do it.'), 'danger') return _rerender() if user.role == 'admin': @@ -182,8 +183,8 @@ def edit_user(user_id): User.id != user.id, ).count() if remaining_admins == 0: - flash('This is the last active president. Promote another ' - 'account before changing this one.', 'danger') + flash(_('This is the last active president. Promote ' + 'another account before changing this one.'), 'danger') return _rerender() # Change role via raw SQL to avoid polymorphic identity corruption. @@ -228,7 +229,7 @@ def edit_user(user_id): log_auth_event('account.updated', actor=actor_name, actor_id=actor_id, target=user.username, target_id=user.id, active=is_active) - flash(f'User {user.username} updated successfully!', 'success') + flash(_('User %(username)s updated successfully!', username=user.username), 'success') return redirect(url_for('users.list_users')) user_gamertags = {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} @@ -243,11 +244,11 @@ def edit_user(user_id): def delete_user(user_id): """Delete a user (Admin only).""" if not isinstance(current_user, Admin): - flash('Only the president can delete users.', 'danger') + flash(_('Only the president can delete users.'), 'danger') return redirect(url_for('main.dashboard')) if current_user.id == user_id: - flash('You cannot delete your own account.', 'danger') + flash(_('You cannot delete your own account.'), 'danger') return redirect(url_for('users.list_users')) user = User.query.get_or_404(user_id) @@ -284,7 +285,7 @@ def delete_user(user_id): log_auth_event('account.deleted', actor=current_user.username, actor_id=current_user.id, target=deleted_username, target_id=user_id, role=deleted_role) - flash(f'User {deleted_username} has been removed.', 'success') + flash(_('User %(deleted_username)s has been removed.', deleted_username=deleted_username), 'success') return redirect(url_for('users.list_users')) @@ -293,7 +294,7 @@ def delete_user(user_id): def create_user(): """Create a new user (Admin only). Uses the correct polymorphic subclass.""" if not isinstance(current_user, Admin): - flash('Only the president can create users.', 'danger') + flash(_('Only the president can create users.'), 'danger') return redirect(url_for('main.dashboard')) if request.method == 'POST': @@ -313,11 +314,11 @@ def create_user(): role = validated['role'] if User.query.filter_by(username=username).first(): - flash('Username already exists.', 'danger') + flash(_('Username already exists.'), 'danger') return render_template('pages/create_user.html', roles=USER_TYPES) if User.query.filter_by(email=email).first(): - flash('Email already registered.', 'danger') + flash(_('Email already registered.'), 'danger') return render_template('pages/create_user.html', roles=USER_TYPES) hashed_password = hash_password(password) @@ -332,7 +333,7 @@ def create_user(): log_auth_event('account.created_by_admin', actor=current_user.username, actor_id=current_user.id, target=user.username, target_id=user.id, role=role) - flash(f'User {full_name} created as {role}!', 'success') + flash(_('User %(full_name)s created as %(role)s!', full_name=full_name, role=role), 'success') return redirect(url_for('users.list_users')) return render_template('pages/create_user.html', roles=USER_TYPES) @@ -389,13 +390,13 @@ def edit_profile(): league_os_profile = validated.get('league_os_profile') if username != current_user.username and User.query.filter_by(username=username).first(): - flash('Username already taken.', 'danger') + flash(_('Username already taken.'), 'danger') return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=current_user.get_gamertags()) if email != current_user.email and User.query.filter_by(email=email).first(): - flash('Email already in use.', 'danger') + flash(_('Email already in use.'), 'danger') return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=current_user.get_gamertags()) @@ -420,7 +421,7 @@ def edit_profile(): username=current_user.username, user_id=current_user.id) db.session.commit() - flash('Profile updated successfully!', 'success') + flash(_('Profile updated successfully!'), 'success') return redirect(url_for('users.profile')) return render_template('pages/edit_profile.html', user=current_user, @@ -625,7 +626,7 @@ def list_contracts(): def upload_contract(): """Upload a contract for a player.""" if not isinstance(current_user, (Admin, Manager, Coach)): - flash('Only presidents, managers, and coaches can upload contracts.', 'danger') + flash(_('Only presidents, managers, and coaches can upload contracts.'), 'danger') return redirect(url_for('users.list_contracts')) if isinstance(current_user, Coach): @@ -645,26 +646,26 @@ def upload_contract(): except ValidationError as err: for field, messages in err.messages.items(): for msg in messages: - flash(f'{field}: {msg}', 'danger') + flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger') return render_template('pages/upload_contract.html', players=players) player_id = validated['player_id'] notes = validated.get('notes') if not can_manage_player_contract(current_user, player_id): - flash('You do not have permission to upload a contract for this player.', 'danger') + flash(_('You do not have permission to upload a contract for this player.'), 'danger') return redirect(url_for('users.upload_contract')) if 'contract_file' not in request.files: - flash('No file selected.', 'danger') + flash(_('No file selected.'), 'danger') return redirect(url_for('users.upload_contract')) file = request.files['contract_file'] if file.filename == '': - flash('No file selected.', 'danger') + flash(_('No file selected.'), 'danger') return redirect(url_for('users.upload_contract')) if not file.filename.lower().endswith('.pdf'): - flash('Only PDF files are allowed for contracts.', 'danger') + flash(_('Only PDF files are allowed for contracts.'), 'danger') return redirect(url_for('users.upload_contract')) upload_dir = os.path.join(os.getcwd(), 'documents', 'contrats signés') @@ -697,7 +698,7 @@ def upload_contract(): ) db.session.add(contract) db.session.commit() - flash(f'Contract uploaded successfully for {player.username}!', 'success') + flash(_('Contract uploaded successfully for %(username)s!', username=player.username), 'success') return redirect(url_for('users.list_contracts')) return render_template('pages/upload_contract.html', players=players) @@ -709,16 +710,16 @@ def upload_signed_contract(contract_id): """Upload a signed contract (player only).""" contract = Contract.query.get_or_404(contract_id) if not contract.can_upload_signed(current_user): - flash('Only the player can upload their signed contract.', 'danger') + flash(_('Only the player can upload their signed contract.'), 'danger') return redirect(url_for('users.list_contracts')) if 'signed_file' not in request.files: - flash('No file selected.', 'danger') + flash(_('No file selected.'), 'danger') return redirect(url_for('users.list_contracts')) file = request.files['signed_file'] if file.filename == '': - flash('No file selected.', 'danger') + flash(_('No file selected.'), 'danger') return redirect(url_for('users.list_contracts')) signed_filename = f"signed_{contract.stored_filename}" @@ -729,7 +730,7 @@ def upload_signed_contract(contract_id): contract.status = 'signed' contract.signed_at = datetime.utcnow() db.session.commit() - flash('Signed contract uploaded successfully!', 'success') + flash(_('Signed contract uploaded successfully!'), 'success') return redirect(url_for('users.list_contracts')) @@ -739,7 +740,7 @@ def download_contract(contract_id): """Download a contract file.""" contract = Contract.query.get_or_404(contract_id) if not contract.can_view(current_user): - flash('You do not have permission to download this contract.', 'danger') + flash(_('You do not have permission to download this contract.'), 'danger') return redirect(url_for('users.list_contracts')) return send_file(contract.file_path, as_attachment=True, download_name=contract.original_filename) @@ -750,10 +751,10 @@ def download_signed_contract(contract_id): """Download a signed contract file.""" contract = Contract.query.get_or_404(contract_id) if not contract.can_view(current_user): - flash('You do not have permission to download this contract.', 'danger') + flash(_('You do not have permission to download this contract.'), 'danger') return redirect(url_for('users.list_contracts')) if not contract.signed_file_path: - flash('No signed contract available.', 'danger') + flash(_('No signed contract available.'), 'danger') return redirect(url_for('users.list_contracts')) return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename) @@ -820,7 +821,7 @@ def send_discord_notification(player_name, points, date_str, start_time_str, end def one_on_one(): """One on One request page for players.""" if not isinstance(current_user, Player): - flash('Only players can request One on One sessions.', 'danger') + flash(_('Only players can request One on One sessions.'), 'danger') return redirect(url_for('main.dashboard')) org_teams = current_user.get_org_teams() @@ -828,7 +829,7 @@ def one_on_one(): coach = User.query.get(org_team.coach_id) if org_team and org_team.coach_id else None if not coach: - flash('You do not have a coach assigned to your team.', 'info') + flash(_('You do not have a coach assigned to your team.'), 'info') team_notes = [] if org_team: @@ -855,7 +856,7 @@ def one_on_one(): points = request.form.get('points', '').strip() if not coach: - flash('Cannot request One on One - no coach assigned.', 'danger') + flash(_('Cannot request One on One - no coach assigned.'), 'danger') return redirect(url_for('users.one_on_one')) try: @@ -863,7 +864,7 @@ def one_on_one(): start_time = datetime.strptime(start_time_str, '%H:%M').time() end_time = datetime.strptime(end_time_str, '%H:%M').time() except (ValueError, TypeError): - flash('Invalid date or time format.', 'danger') + flash(_('Invalid date or time format.'), 'danger') return redirect(url_for('users.one_on_one')) check_date = datetime.strptime(date_str, '%Y-%m-%d') @@ -877,7 +878,7 @@ def one_on_one(): ) if not is_available: - flash("The requested time is not within the coach's availability.", 'danger') + flash(_("The requested time is not within the coach's availability."), 'danger') return redirect(url_for('users.one_on_one')) request_obj = OneOnOneRequest( @@ -900,7 +901,7 @@ def one_on_one(): request_id=request_obj.id, ) - flash('Your One on One request has been submitted!', 'success') + flash(_('Your One on One request has been submitted!'), 'success') return redirect(url_for('users.one_on_one')) # Build list of upcoming dates that have coach availability @@ -935,17 +936,17 @@ def one_on_one(): def accept_one_on_one(request_id): """Coach accepts a One on One request.""" if not isinstance(current_user, Coach): - flash('Only coaches can accept One on One requests.', 'danger') + flash(_('Only coaches can accept One on One requests.'), 'danger') return redirect(url_for('main.dashboard')) request_obj = OneOnOneRequest.query.get_or_404(request_id) if request_obj.coach_id != current_user.id: - flash('This request is not for you.', 'danger') + flash(_('This request is not for you.'), 'danger') return redirect(url_for('users.notes_dashboard')) if request_obj.status != 'pending': - flash('This request has already been processed.', 'info') + flash(_('This request has already been processed.'), 'info') return redirect(url_for('users.notes_dashboard')) player = request_obj.player @@ -967,7 +968,7 @@ def accept_one_on_one(request_id): approved=True, ) - flash(f'One on One request from {player.username if player else "Unknown"} has been approved!', 'success') + flash(_('One on One request from %(player)s has been approved!', player=player.username if player else 'Unknown'), 'success') return redirect(url_for('users.notes_dashboard')) @@ -976,17 +977,17 @@ def accept_one_on_one(request_id): def reject_one_on_one(request_id): """Coach rejects a One on One request.""" if not isinstance(current_user, Coach): - flash('Only coaches can reject One on One requests.', 'danger') + flash(_('Only coaches can reject One on One requests.'), 'danger') return redirect(url_for('main.dashboard')) request_obj = OneOnOneRequest.query.get_or_404(request_id) if request_obj.coach_id != current_user.id: - flash('This request is not for you.', 'danger') + flash(_('This request is not for you.'), 'danger') return redirect(url_for('users.notes_dashboard')) if request_obj.status != 'pending': - flash('This request has already been processed.', 'info') + flash(_('This request has already been processed.'), 'info') return redirect(url_for('users.notes_dashboard')) rejection_reason = request.form.get('rejection_reason', '').strip() @@ -1013,7 +1014,7 @@ def reject_one_on_one(request_id): refusal_note=rejection_reason or None, ) - flash(f'One on One request from {player.username if player else "Unknown"} has been rejected.', 'info') + flash(_('One on One request from %(player)s has been rejected.', player=player.username if player else 'Unknown'), 'info') return redirect(url_for('users.notes_dashboard')) @@ -1026,7 +1027,7 @@ def reject_one_on_one(request_id): def my_notes(): """View personal and team notes for the current player.""" if not isinstance(current_user, Player): - flash('This page is for players only.', 'info') + flash(_('This page is for players only.'), 'info') return redirect(url_for('main.dashboard')) org_teams = current_user.get_org_teams() @@ -1057,7 +1058,7 @@ def my_notes(): def manage_coach_availability(): """Manage coach availability for One on One sessions.""" if not isinstance(current_user, Coach): - flash('Only coaches can manage availability.', 'danger') + flash(_('Only coaches can manage availability.'), 'danger') return redirect(url_for('main.dashboard')) if request.method == 'POST': @@ -1119,7 +1120,7 @@ def clear_coach_availability(): def notes_dashboard(): """Notes and One on One dashboard for coaches.""" if not isinstance(current_user, Coach): - flash('Only coaches can access the notes dashboard.', 'danger') + flash(_('Only coaches can access the notes dashboard.'), 'danger') return redirect(url_for('main.dashboard')) org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() @@ -1180,12 +1181,12 @@ def notes_dashboard(): def manage_team_notes(): """Create or update team notes for the coach's org team.""" if not isinstance(current_user, Coach): - flash('Only coaches can manage team notes.', 'danger') + flash(_('Only coaches can manage team notes.'), 'danger') return redirect(url_for('main.dashboard')) org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() if not org_team: - flash('You are not assigned to a team.', 'danger') + flash(_('You are not assigned to a team.'), 'danger') return redirect(url_for('users.notes_dashboard')) content = request.form.get('content', '').strip() @@ -1197,7 +1198,7 @@ def manage_team_notes(): ) db.session.add(note) db.session.commit() - flash('Team notes saved successfully!', 'success') + flash(_('Team notes saved successfully!'), 'success') return redirect(url_for('users.notes_dashboard')) @@ -1211,23 +1212,23 @@ def manage_team_notes(): def manage_personal_notes(): """Create a personal note for a player (coach only, simple form).""" if not isinstance(current_user, Coach): - flash('Only coaches can manage personal notes.', 'danger') + flash(_('Only coaches can manage personal notes.'), 'danger') return redirect(url_for('main.dashboard')) player_id = request.form.get('player_id', type=int) content = request.form.get('content', '').strip() if not player_id or not content: - flash('Player and content are required.', 'danger') + flash(_('Player and content are required.'), 'danger') return redirect(url_for('users.notes_dashboard')) player = User.query.get_or_404(player_id) if not isinstance(player, Player): - flash('Can only add notes for players.', 'danger') + flash(_('Can only add notes for players.'), 'danger') return redirect(url_for('users.notes_dashboard')) if not coach_can_access_player(current_user, player_id): - flash('You can only write notes about players you work with.', 'danger') + flash(_('You can only write notes about players you work with.'), 'danger') return redirect(url_for('users.notes_dashboard')) note = PersonalNote( @@ -1237,7 +1238,7 @@ def manage_personal_notes(): ) db.session.add(note) db.session.commit() - flash(f'Note added for {player.username}.', 'success') + flash(_('Note added for %(username)s.', username=player.username), 'success') return redirect(url_for('users.notes_dashboard')) @@ -1250,7 +1251,7 @@ def manage_personal_notes(): def add_personal_note(): """Create a personal note for a player with optional context (coach only).""" if not isinstance(current_user, Coach): - flash('Only coaches can add personal notes.', 'danger') + flash(_('Only coaches can add personal notes.'), 'danger') return redirect(url_for('main.dashboard')) player_id = request.form.get('player_id', type=int) @@ -1260,16 +1261,16 @@ def add_personal_note(): team_id_str = request.form.get('team_id') if not player_id or not content: - flash('Player and content are required.', 'danger') + flash(_('Player and content are required.'), 'danger') return redirect(url_for('users.notes_dashboard')) player = User.query.get_or_404(player_id) if not isinstance(player, Player): - flash('Can only add notes for players.', 'danger') + flash(_('Can only add notes for players.'), 'danger') return redirect(url_for('users.notes_dashboard')) if not coach_can_access_player(current_user, player_id): - flash('You can only write notes about players you work with.', 'danger') + flash(_('You can only write notes about players you work with.'), 'danger') return redirect(url_for('users.notes_dashboard')) note = PersonalNote( @@ -1282,7 +1283,7 @@ def add_personal_note(): ) db.session.add(note) db.session.commit() - flash(f'Note added for {player.username}.', 'success') + flash(_('Note added for %(username)s.', username=player.username), 'success') return redirect(url_for('users.notes_dashboard')) @@ -1295,7 +1296,7 @@ def add_personal_note(): def add_note_from_tryout(tryout_id): """Add a personal note for a player in the context of a tryout.""" if not isinstance(current_user, Coach): - flash('Only coaches can add personal notes.', 'danger') + flash(_('Only coaches can add personal notes.'), 'danger') return redirect(url_for('main.dashboard')) tryout = Tryout.query.get_or_404(tryout_id) @@ -1310,11 +1311,11 @@ def add_note_from_tryout(tryout_id): content = request.form.get('content', '').strip() if not player_id or not content: - flash('Player and content are required.', 'danger') + flash(_('Player and content are required.'), 'danger') return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id)) if not coach_can_access_player(current_user, player_id): - flash('You can only write notes about players you work with.', 'danger') + flash(_('You can only write notes about players you work with.'), 'danger') return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id)) note = PersonalNote( @@ -1325,7 +1326,7 @@ def add_note_from_tryout(tryout_id): ) db.session.add(note) db.session.commit() - flash('Note added successfully.', 'success') + flash(_('Note added successfully.'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return render_template('pages/add_note.html', @@ -1345,7 +1346,7 @@ def add_note_from_tryout(tryout_id): def add_note_from_match(match_id): """Add a personal note for a player in the context of a match.""" if not isinstance(current_user, Coach): - flash('Only coaches can add personal notes.', 'danger') + flash(_('Only coaches can add personal notes.'), 'danger') return redirect(url_for('main.dashboard')) match_obj = Match.query.get_or_404(match_id) @@ -1361,11 +1362,11 @@ def add_note_from_match(match_id): content = request.form.get('content', '').strip() if not player_id or not content: - flash('Player and content are required.', 'danger') + flash(_('Player and content are required.'), 'danger') return redirect(url_for('users.add_note_from_match', match_id=match_id)) if not coach_can_access_player(current_user, player_id): - flash('You can only write notes about players you work with.', 'danger') + flash(_('You can only write notes about players you work with.'), 'danger') return redirect(url_for('users.add_note_from_match', match_id=match_id)) note = PersonalNote( @@ -1376,7 +1377,7 @@ def add_note_from_match(match_id): ) db.session.add(note) db.session.commit() - flash('Note added successfully.', 'success') + flash(_('Note added successfully.'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=match_obj.tryout_id)) return render_template('pages/add_note.html', diff --git a/app/translations/en/LC_MESSAGES/messages.mo b/app/translations/en/LC_MESSAGES/messages.mo index 7889056..2b21c93 100644 Binary files a/app/translations/en/LC_MESSAGES/messages.mo and b/app/translations/en/LC_MESSAGES/messages.mo differ diff --git a/app/translations/en/LC_MESSAGES/messages.po b/app/translations/en/LC_MESSAGES/messages.po index a11b724..7703d5f 100644 --- a/app/translations/en/LC_MESSAGES/messages.po +++ b/app/translations/en/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: team-tryouts VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-07 21:41-0400\n" +"POT-Creation-Date: 2026-08-07 22:03-0400\n" "PO-Revision-Date: 2026-08-07 20:22-0400\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -19,6 +19,88 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" +#: app/validators.py:43 +msgid "Password must be at least 8 characters with uppercase, lowercase, and a number." +msgstr "Password must be at least 8 characters with uppercase, lowercase, and a number." + +#: app/validators.py:62 +msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." +msgstr "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." + +#: app/validators.py:81 +msgid "Invalid Discord username format." +msgstr "Invalid Discord username format." + +#: app/validators.py:98 +msgid "Discord User ID must be a 17-20 digit number." +msgstr "Discord User ID must be a 17-20 digit number." + +#: app/validators.py:116 +msgid "Invalid phone number format." +msgstr "Invalid phone number format." + +#: app/validators.py:155 +msgid "Username is required." +msgstr "Username is required." + +#: app/validators.py:159 +msgid "Password is required." +msgstr "Password is required." + +#: app/validators.py:180 app/validators.py:251 +msgid "Username must be 3-80 characters." +msgstr "Username must be 3-80 characters." + +#: app/validators.py:186 +msgid "Email must be 120 characters or less." +msgstr "Email must be 120 characters or less." + +#: app/validators.py:199 app/validators.py:266 app/validators.py:299 app/validators.py:365 +msgid "Full name is required." +msgstr "Full name is required." + +#: app/validators.py:234 +msgid "Passwords do not match." +msgstr "Passwords do not match." + +#: app/validators.py:272 app/validators.py:309 +msgid "Invalid role selected." +msgstr "Invalid role selected." + +#: app/validators.py:409 +msgid "Player must be selected." +msgstr "Player must be selected." + +#: app/validators.py:412 +msgid "Notes must be 2000 characters or less." +msgstr "Notes must be 2000 characters or less." + +#: app/validators.py:431 +msgid "Date must be in YYYY-MM-DD format." +msgstr "Date must be in YYYY-MM-DD format." + +#: app/validators.py:438 app/validators.py:473 +msgid "Start time must be in HH:MM format." +msgstr "Start time must be in HH:MM format." + +#: app/validators.py:445 +msgid "End time must be in HH:MM format." +msgstr "End time must be in HH:MM format." + +#: app/validators.py:449 +msgid "Points must be 2000 characters or less." +msgstr "Points must be 2000 characters or less." + +#: app/validators.py:466 +msgid "Day must be 0 (Monday) to 6 (Sunday)." +msgstr "Day must be 0 (Monday) to 6 (Sunday)." + +#: app/routes/auth.py:129 app/routes/auth.py:258 app/routes/users.py:46 +#: app/routes/users.py:649 +#, python-format +msgid "%(field)s: %(msg)s" +msgstr "%(field)s: %(msg)s" + #: app/routes/auth.py:141 #, python-format msgid "" @@ -59,11 +141,11 @@ msgstr "Login unsuccessful. Please check username and password." msgid "Incorrect CAPTCHA answer. Please try again." msgstr "Incorrect CAPTCHA answer. Please try again." -#: app/routes/auth.py:281 +#: app/routes/auth.py:281 app/routes/users.py:317 msgid "Username already exists." msgstr "Username already exists." -#: app/routes/auth.py:293 +#: app/routes/auth.py:293 app/routes/users.py:321 msgid "Email already registered." msgstr "Email already registered." @@ -107,6 +189,554 @@ msgstr "Discord account connected! Your profile has been pre-filled." msgid "You have been logged out." msgstr "You have been logged out." +#: app/routes/evaluations.py:41 +msgid "You do not have permission to view evaluations." +msgstr "You do not have permission to view evaluations." + +#: app/routes/evaluations.py:122 +msgid "You do not have permission to evaluate players." +msgstr "You do not have permission to evaluate players." + +#: app/routes/evaluations.py:127 app/routes/evaluations.py:220 +msgid "You do not have permission to evaluate players in this tryout." +msgstr "You do not have permission to evaluate players in this tryout." + +#: app/routes/evaluations.py:134 +msgid "Player is not registered for this tryout." +msgstr "Player is not registered for this tryout." + +#: app/routes/evaluations.py:139 +msgid "Can only evaluate players." +msgstr "Can only evaluate players." + +#: app/routes/evaluations.py:177 +msgid "Evaluation updated!" +msgstr "Evaluation updated!" + +#: app/routes/evaluations.py:190 +msgid "Evaluation submitted successfully!" +msgstr "Evaluation submitted successfully!" + +#: app/routes/evaluations.py:215 app/routes/teams.py:249 app/routes/teams.py:280 +#: app/routes/teams.py:311 app/routes/teams.py:336 app/routes/teams.py:361 +#: app/routes/teams.py:393 app/routes/tryouts.py:341 app/routes/tryouts.py:357 +#: app/routes/tryouts.py:376 app/routes/tryouts.py:413 app/routes/tryouts.py:448 +#: app/routes/tryouts.py:467 +msgid "Permission denied." +msgstr "Permission denied." + +#: app/routes/main.py:43 +msgid "That language is not available." +msgstr "That language is not available." + +#: app/routes/matches.py:201 +msgid "You do not have permission to schedule matches for this tryout." +msgstr "You do not have permission to schedule matches for this tryout." + +#: app/routes/matches.py:205 app/routes/matches.py:336 +msgid "This tryout has ended. Matches can no longer be created or modified." +msgstr "This tryout has ended. Matches can no longer be created or modified." + +#: app/routes/matches.py:224 +msgid "Start time is required. Please select a time slot." +msgstr "Start time is required. Please select a time slot." + +#: app/routes/matches.py:231 app/routes/matches.py:359 app/routes/team_matches.py:139 +#: app/routes/team_matches.py:222 +msgid "Invalid date format." +msgstr "Invalid date format." + +#: app/routes/matches.py:246 app/routes/team_matches.py:156 +msgid "Invalid time format." +msgstr "Invalid time format." + +#: app/routes/matches.py:317 +msgid "Match scheduled successfully!" +msgstr "Match scheduled successfully!" + +#: app/routes/matches.py:332 app/routes/team_matches.py:209 +msgid "You do not have permission to edit this match." +msgstr "You do not have permission to edit this match." + +#: app/routes/matches.py:365 +msgid "Start time is required." +msgstr "Start time is required." + +#: app/routes/matches.py:461 app/routes/team_matches.py:245 +msgid "Match updated successfully!" +msgstr "Match updated successfully!" + +#: app/routes/matches.py:506 app/routes/team_matches.py:259 +msgid "You do not have permission to delete this match." +msgstr "You do not have permission to delete this match." + +#: app/routes/matches.py:509 +msgid "This tryout has ended. Matches can no longer be deleted." +msgstr "This tryout has ended. Matches can no longer be deleted." + +#: app/routes/matches.py:521 app/routes/team_matches.py:263 +msgid "Match deleted successfully." +msgstr "Match deleted successfully." + +#: app/routes/team_matches.py:97 +msgid "You do not have permission to schedule matches for this team." +msgstr "You do not have permission to schedule matches for this team." + +#: app/routes/team_matches.py:132 +msgid "Date is required." +msgstr "Date is required." + +#: app/routes/team_matches.py:195 +#, python-format +msgid "Team match \"%(title)s\" scheduled successfully!" +msgstr "Team match \"%(title)s\" scheduled successfully!" + +#: app/routes/teams.py:43 +msgid "Use My Team(s) to view your teams." +msgstr "Use My Team(s) to view your teams." + +#: app/routes/teams.py:46 +msgid "You do not have permission to view teams." +msgstr "You do not have permission to view teams." + +#: app/routes/teams.py:61 +msgid "This page is for players." +msgstr "This page is for players." + +#: app/routes/teams.py:103 +msgid "You do not have permission to create teams." +msgstr "You do not have permission to create teams." + +#: app/routes/teams.py:111 app/routes/teams.py:156 +msgid "Team name is required." +msgstr "Team name is required." + +#: app/routes/teams.py:116 app/routes/teams.py:161 +#, python-format +msgid "Team \"%(name)s\" already exists." +msgstr "Team \"%(name)s\" already exists." + +#: app/routes/teams.py:138 +#, python-format +msgid "Team \"%(name)s\" created successfully!" +msgstr "Team \"%(name)s\" created successfully!" + +#: app/routes/teams.py:148 +msgid "You do not have permission to edit this team." +msgstr "You do not have permission to edit this team." + +#: app/routes/teams.py:199 +#, python-format +msgid "Team \"%(name)s\" updated successfully!" +msgstr "Team \"%(name)s\" updated successfully!" + +#: app/routes/teams.py:208 +msgid "You do not have permission to delete teams." +msgstr "You do not have permission to delete teams." + +#: app/routes/teams.py:239 +#, python-format +msgid "Team \"%(name)s\" deleted successfully." +msgstr "Team \"%(name)s\" deleted successfully." + +#: app/routes/teams.py:254 +msgid "Please select a coach." +msgstr "Please select a coach." + +#: app/routes/teams.py:259 +msgid "Only coaches can be assigned as coach." +msgstr "Only coaches can be assigned as coach." + +#: app/routes/teams.py:263 +#, python-format +msgid "%(username)s is already a coach of %(name)s." +msgstr "%(username)s is already a coach of %(name)s." + +#: app/routes/teams.py:270 +#, python-format +msgid "%(username)s added as coach of %(name)s." +msgstr "%(username)s added as coach of %(name)s." + +#: app/routes/teams.py:285 +msgid "Please select a manager." +msgstr "Please select a manager." + +#: app/routes/teams.py:290 +msgid "Only managers can be assigned as manager." +msgstr "Only managers can be assigned as manager." + +#: app/routes/teams.py:294 +#, python-format +msgid "%(username)s is already a manager of %(name)s." +msgstr "%(username)s is already a manager of %(name)s." + +#: app/routes/teams.py:301 +#, python-format +msgid "%(username)s added as manager of %(name)s." +msgstr "%(username)s added as manager of %(name)s." + +#: app/routes/teams.py:326 +#, python-format +msgid "Coach removed from %(name)s." +msgstr "Coach removed from %(name)s." + +#: app/routes/teams.py:351 +#, python-format +msgid "Manager removed from %(name)s." +msgstr "Manager removed from %(name)s." + +#: app/routes/teams.py:367 app/routes/tryouts.py:380 app/routes/tryouts.py:478 +msgid "Please select a player." +msgstr "Please select a player." + +#: app/routes/teams.py:372 +msgid "Can only assign players to teams." +msgstr "Can only assign players to teams." + +#: app/routes/teams.py:377 +#, python-format +msgid "%(username)s is already on %(name)s." +msgstr "%(username)s is already on %(name)s." + +#: app/routes/teams.py:383 +#, python-format +msgid "%(username)s added to %(name)s!" +msgstr "%(username)s added to %(name)s!" + +#: app/routes/teams.py:399 app/routes/teams.py:462 +#, python-format +msgid "%(username)s is not on %(name)s." +msgstr "%(username)s is not on %(name)s." + +#: app/routes/teams.py:404 +#, python-format +msgid "%(username)s removed from %(name)s." +msgstr "%(username)s removed from %(name)s." + +#: app/routes/teams.py:434 app/routes/teams.py:452 +msgid "You do not have permission to add notes to this team." +msgstr "You do not have permission to add notes to this team." + +#: app/routes/teams.py:442 +msgid "Team notes added successfully!" +msgstr "Team notes added successfully!" + +#: app/routes/teams.py:457 app/routes/users.py:1227 app/routes/users.py:1269 +msgid "Can only add notes for players." +msgstr "Can only add notes for players." + +#: app/routes/teams.py:470 +#, python-format +msgid "Note added for %(username)s!" +msgstr "Note added for %(username)s!" + +#: app/routes/tryouts.py:43 +msgid "You do not have permission to create tryouts." +msgstr "You do not have permission to create tryouts." + +#: app/routes/tryouts.py:65 app/routes/tryouts.py:140 +msgid "Invalid start date format." +msgstr "Invalid start date format." + +#: app/routes/tryouts.py:74 app/routes/tryouts.py:149 +msgid "End date cannot be before start date." +msgstr "End date cannot be before start date." + +#: app/routes/tryouts.py:78 app/routes/tryouts.py:153 +msgid "Invalid end date format." +msgstr "Invalid end date format." + +#: app/routes/tryouts.py:100 +msgid "Tryout created successfully!" +msgstr "Tryout created successfully!" + +#: app/routes/tryouts.py:114 +msgid "You do not have permission to edit this tryout." +msgstr "You do not have permission to edit this tryout." + +#: app/routes/tryouts.py:118 +msgid "This tryout has ended and can no longer be modified." +msgstr "This tryout has ended and can no longer be modified." + +#: app/routes/tryouts.py:175 +msgid "Tryout updated successfully!" +msgstr "Tryout updated successfully!" + +#: app/routes/tryouts.py:207 +msgid "You do not have permission to view this tryout." +msgstr "You do not have permission to view this tryout." + +#: app/routes/tryouts.py:309 +msgid "Only players can register for tryouts." +msgstr "Only players can register for tryouts." + +#: app/routes/tryouts.py:313 +msgid "This tryout is not accepting registrations." +msgstr "This tryout is not accepting registrations." + +#: app/routes/tryouts.py:319 +msgid "You are already registered for this tryout." +msgstr "You are already registered for this tryout." + +#: app/routes/tryouts.py:325 app/routes/tryouts.py:397 +msgid "This tryout is full." +msgstr "This tryout is full." + +#: app/routes/tryouts.py:331 +msgid "Successfully registered for tryout!" +msgstr "Successfully registered for tryout!" + +#: app/routes/tryouts.py:347 +#, python-format +msgid "Tryout status updated to %(new_status)s." +msgstr "Tryout status updated to %(new_status)s." + +#: app/routes/tryouts.py:366 +msgid "Registration status updated." +msgstr "Registration status updated." + +#: app/routes/tryouts.py:385 +msgid "Can only register players." +msgstr "Can only register players." + +#: app/routes/tryouts.py:391 +#, python-format +msgid "%(username)s is already registered for this tryout." +msgstr "%(username)s is already registered for this tryout." + +#: app/routes/tryouts.py:403 +#, python-format +msgid "%(username)s registered for tryout!" +msgstr "%(username)s registered for tryout!" + +#: app/routes/tryouts.py:438 +#, python-format +msgid "%(username)s removed from tryout." +msgstr "%(username)s removed from tryout." + +#: app/routes/tryouts.py:456 +#, python-format +msgid "Team \"%(team_name)s\" created!" +msgstr "Team \"%(team_name)s\" created!" + +#: app/routes/tryouts.py:485 +msgid "That player is not registered for this tryout." +msgstr "That player is not registered for this tryout." + +#: app/routes/tryouts.py:491 +msgid "Player is already on this team." +msgstr "Player is already on this team." + +#: app/routes/tryouts.py:496 +msgid "Player added to team!" +msgstr "Player added to team!" + +#: app/routes/tryouts.py:506 +msgid "You do not have permission to delete this tryout." +msgstr "You do not have permission to delete this tryout." + +#: app/routes/tryouts.py:541 +msgid "Tryout deleted successfully." +msgstr "Tryout deleted successfully." + +#: app/routes/users.py:116 +msgid "Only the president can manage users." +msgstr "Only the president can manage users." + +#: app/routes/users.py:128 +msgid "Only the president can edit users." +msgstr "Only the president can edit users." + +#: app/routes/users.py:168 +msgid "Email already in use by another account." +msgstr "Email already in use by another account." + +#: app/routes/users.py:175 +msgid "You cannot change your own role. Ask another president to do it." +msgstr "You cannot change your own role. Ask another president to do it." + +#: app/routes/users.py:186 +msgid "This is the last active president. Promote another account before changing this one." +msgstr "This is the last active president. Promote another account before changing this one." + +#: app/routes/users.py:232 +#, python-format +msgid "User %(username)s updated successfully!" +msgstr "User %(username)s updated successfully!" + +#: app/routes/users.py:247 +msgid "Only the president can delete users." +msgstr "Only the president can delete users." + +#: app/routes/users.py:251 +msgid "You cannot delete your own account." +msgstr "You cannot delete your own account." + +#: app/routes/users.py:288 +#, python-format +msgid "User %(deleted_username)s has been removed." +msgstr "User %(deleted_username)s has been removed." + +#: app/routes/users.py:297 +msgid "Only the president can create users." +msgstr "Only the president can create users." + +#: app/routes/users.py:336 +#, python-format +msgid "User %(full_name)s created as %(role)s!" +msgstr "User %(full_name)s created as %(role)s!" + +#: app/routes/users.py:393 +msgid "Username already taken." +msgstr "Username already taken." + +#: app/routes/users.py:399 +msgid "Email already in use." +msgstr "Email already in use." + +#: app/routes/users.py:424 +msgid "Profile updated successfully!" +msgstr "Profile updated successfully!" + +#: app/routes/users.py:629 +msgid "Only presidents, managers, and coaches can upload contracts." +msgstr "Only presidents, managers, and coaches can upload contracts." + +#: app/routes/users.py:656 +msgid "You do not have permission to upload a contract for this player." +msgstr "You do not have permission to upload a contract for this player." + +#: app/routes/users.py:660 app/routes/users.py:665 app/routes/users.py:717 +#: app/routes/users.py:722 +msgid "No file selected." +msgstr "No file selected." + +#: app/routes/users.py:668 +msgid "Only PDF files are allowed for contracts." +msgstr "Only PDF files are allowed for contracts." + +#: app/routes/users.py:701 +#, python-format +msgid "Contract uploaded successfully for %(username)s!" +msgstr "Contract uploaded successfully for %(username)s!" + +#: app/routes/users.py:713 +msgid "Only the player can upload their signed contract." +msgstr "Only the player can upload their signed contract." + +#: app/routes/users.py:733 +msgid "Signed contract uploaded successfully!" +msgstr "Signed contract uploaded successfully!" + +#: app/routes/users.py:743 app/routes/users.py:754 +msgid "You do not have permission to download this contract." +msgstr "You do not have permission to download this contract." + +#: app/routes/users.py:757 +msgid "No signed contract available." +msgstr "No signed contract available." + +#: app/routes/users.py:824 +msgid "Only players can request One on One sessions." +msgstr "Only players can request One on One sessions." + +#: app/routes/users.py:832 +msgid "You do not have a coach assigned to your team." +msgstr "You do not have a coach assigned to your team." + +#: app/routes/users.py:859 +msgid "Cannot request One on One - no coach assigned." +msgstr "Cannot request One on One - no coach assigned." + +#: app/routes/users.py:867 +msgid "Invalid date or time format." +msgstr "Invalid date or time format." + +#: app/routes/users.py:881 +msgid "The requested time is not within the coach's availability." +msgstr "The requested time is not within the coach's availability." + +#: app/routes/users.py:904 +msgid "Your One on One request has been submitted!" +msgstr "Your One on One request has been submitted!" + +#: app/routes/users.py:939 +msgid "Only coaches can accept One on One requests." +msgstr "Only coaches can accept One on One requests." + +#: app/routes/users.py:945 app/routes/users.py:986 +msgid "This request is not for you." +msgstr "This request is not for you." + +#: app/routes/users.py:949 app/routes/users.py:990 +msgid "This request has already been processed." +msgstr "This request has already been processed." + +#: app/routes/users.py:971 +#, python-format +msgid "One on One request from %(player)s has been approved!" +msgstr "One on One request from %(player)s has been approved!" + +#: app/routes/users.py:980 +msgid "Only coaches can reject One on One requests." +msgstr "Only coaches can reject One on One requests." + +#: app/routes/users.py:1017 +#, python-format +msgid "One on One request from %(player)s has been rejected." +msgstr "One on One request from %(player)s has been rejected." + +#: app/routes/users.py:1030 +msgid "This page is for players only." +msgstr "This page is for players only." + +#: app/routes/users.py:1061 +msgid "Only coaches can manage availability." +msgstr "Only coaches can manage availability." + +#: app/routes/users.py:1123 +msgid "Only coaches can access the notes dashboard." +msgstr "Only coaches can access the notes dashboard." + +#: app/routes/users.py:1184 +msgid "Only coaches can manage team notes." +msgstr "Only coaches can manage team notes." + +#: app/routes/users.py:1189 +msgid "You are not assigned to a team." +msgstr "You are not assigned to a team." + +#: app/routes/users.py:1201 +msgid "Team notes saved successfully!" +msgstr "Team notes saved successfully!" + +#: app/routes/users.py:1215 +msgid "Only coaches can manage personal notes." +msgstr "Only coaches can manage personal notes." + +#: app/routes/users.py:1222 app/routes/users.py:1264 app/routes/users.py:1314 +#: app/routes/users.py:1365 +msgid "Player and content are required." +msgstr "Player and content are required." + +#: app/routes/users.py:1231 app/routes/users.py:1273 app/routes/users.py:1318 +#: app/routes/users.py:1369 +msgid "You can only write notes about players you work with." +msgstr "You can only write notes about players you work with." + +#: app/routes/users.py:1241 app/routes/users.py:1286 +#, python-format +msgid "Note added for %(username)s." +msgstr "Note added for %(username)s." + +#: app/routes/users.py:1254 app/routes/users.py:1299 app/routes/users.py:1349 +msgid "Only coaches can add personal notes." +msgstr "Only coaches can add personal notes." + +#: app/routes/users.py:1329 app/routes/users.py:1380 +msgid "Note added successfully." +msgstr "Note added successfully." + #: app/templates/errors/400.html:2 msgid "400 Bad Request" msgstr "400 Bad Request" @@ -2086,3 +2716,6 @@ msgstr "View Profile" #~ msgid "×" #~ msgstr "×" +#~ msgid "One on One request from %(value)s has been approved!" +#~ msgstr "" + diff --git a/app/translations/fr/LC_MESSAGES/messages.mo b/app/translations/fr/LC_MESSAGES/messages.mo index f68670a..163ef8b 100644 Binary files a/app/translations/fr/LC_MESSAGES/messages.mo and b/app/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/app/translations/fr/LC_MESSAGES/messages.po b/app/translations/fr/LC_MESSAGES/messages.po index b5d8340..f534e30 100644 --- a/app/translations/fr/LC_MESSAGES/messages.po +++ b/app/translations/fr/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: team-tryouts VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-07 21:41-0400\n" +"POT-Creation-Date: 2026-08-07 22:03-0400\n" "PO-Revision-Date: 2026-08-07 20:22-0400\n" "Last-Translator: FULL NAME \n" "Language: fr\n" @@ -19,6 +19,92 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" +#: app/validators.py:43 +msgid "Password must be at least 8 characters with uppercase, lowercase, and a number." +msgstr "" +"Le mot de passe doit compter au moins 8 caractères, dont une majuscule, une minuscule" +" et un chiffre." + +#: app/validators.py:62 +msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." +msgstr "" +"Le nom d’utilisateur doit compter de 3 à 30 caractères (lettres, chiffres, tiret bas," +" trait d’union)." + +#: app/validators.py:81 +msgid "Invalid Discord username format." +msgstr "Format de nom d’utilisateur Discord invalide." + +#: app/validators.py:98 +msgid "Discord User ID must be a 17-20 digit number." +msgstr "L’identifiant Discord doit être un nombre de 17 à 20 chiffres." + +#: app/validators.py:116 +msgid "Invalid phone number format." +msgstr "Format de numéro de téléphone invalide." + +#: app/validators.py:155 +msgid "Username is required." +msgstr "Le nom d’utilisateur est obligatoire." + +#: app/validators.py:159 +msgid "Password is required." +msgstr "Le mot de passe est obligatoire." + +#: app/validators.py:180 app/validators.py:251 +msgid "Username must be 3-80 characters." +msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères." + +#: app/validators.py:186 +msgid "Email must be 120 characters or less." +msgstr "L’adresse courriel ne doit pas dépasser 120 caractères." + +#: app/validators.py:199 app/validators.py:266 app/validators.py:299 app/validators.py:365 +msgid "Full name is required." +msgstr "Le nom complet est obligatoire." + +#: app/validators.py:234 +msgid "Passwords do not match." +msgstr "Les mots de passe ne concordent pas." + +#: app/validators.py:272 app/validators.py:309 +msgid "Invalid role selected." +msgstr "Rôle sélectionné invalide." + +#: app/validators.py:409 +msgid "Player must be selected." +msgstr "Vous devez choisir un joueur." + +#: app/validators.py:412 +msgid "Notes must be 2000 characters or less." +msgstr "Les notes ne doivent pas dépasser 2000 caractères." + +#: app/validators.py:431 +msgid "Date must be in YYYY-MM-DD format." +msgstr "La date doit être au format AAAA-MM-JJ." + +#: app/validators.py:438 app/validators.py:473 +msgid "Start time must be in HH:MM format." +msgstr "L’heure de début doit être au format HH:MM." + +#: app/validators.py:445 +msgid "End time must be in HH:MM format." +msgstr "L’heure de fin doit être au format HH:MM." + +#: app/validators.py:449 +msgid "Points must be 2000 characters or less." +msgstr "Les points ne doivent pas dépasser 2000 caractères." + +#: app/validators.py:466 +msgid "Day must be 0 (Monday) to 6 (Sunday)." +msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)." + +#: app/routes/auth.py:129 app/routes/auth.py:258 app/routes/users.py:46 +#: app/routes/users.py:649 +#, python-format +msgid "%(field)s: %(msg)s" +msgstr "%(field)s : %(msg)s" + #: app/routes/auth.py:141 #, python-format msgid "" @@ -53,17 +139,17 @@ msgstr "Échec de la connexion. Il reste %(remaining)s tentative(s) avant le ver #: app/routes/auth.py:208 msgid "Login unsuccessful. Please check username and password." -msgstr "Échec de la connexion. Vérifiez le nom d'utilisateur et le mot de passe." +msgstr "Échec de la connexion. Vérifiez le nom d’utilisateur et le mot de passe." #: app/routes/auth.py:239 msgid "Incorrect CAPTCHA answer. Please try again." msgstr "Réponse au CAPTCHA incorrecte. Veuillez réessayer." -#: app/routes/auth.py:281 +#: app/routes/auth.py:281 app/routes/users.py:317 msgid "Username already exists." -msgstr "Ce nom d'utilisateur est déjà pris." +msgstr "Ce nom d’utilisateur est déjà pris." -#: app/routes/auth.py:293 +#: app/routes/auth.py:293 app/routes/users.py:321 msgid "Email already registered." msgstr "Cette adresse courriel est déjà enregistrée." @@ -73,19 +159,19 @@ msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter." #: app/routes/auth.py:365 msgid "Discord OAuth2 is not configured." -msgstr "La connexion Discord n'est pas configurée." +msgstr "La connexion Discord n’est pas configurée." #: app/routes/auth.py:405 msgid "" "Discord authorization could not be verified. Please start the connection again from " "this page." msgstr "" -"L'autorisation Discord n'a pas pu être vérifiée. Relancez la connexion depuis cette " +"L’autorisation Discord n’a pas pu être vérifiée. Relancez la connexion depuis cette " "page." #: app/routes/auth.py:413 msgid "Discord authorization failed. No code received." -msgstr "L'autorisation Discord a échoué : aucun code reçu." +msgstr "L’autorisation Discord a échoué : aucun code reçu." #: app/routes/auth.py:437 msgid "Failed to connect to Discord. Please try again." @@ -93,7 +179,7 @@ msgstr "Impossible de joindre Discord. Veuillez réessayer." #: app/routes/auth.py:441 msgid "Failed to obtain Discord access token." -msgstr "Impossible d'obtenir le jeton d'accès Discord." +msgstr "Impossible d’obtenir le jeton d’accès Discord." #: app/routes/auth.py:456 msgid "Failed to fetch Discord user profile." @@ -107,6 +193,558 @@ msgstr "Compte Discord connecté. Votre profil a été pré-rempli." msgid "You have been logged out." msgstr "Vous avez été déconnecté." +#: app/routes/evaluations.py:41 +msgid "You do not have permission to view evaluations." +msgstr "Vous n’avez pas les droits pour consulter les évaluations." + +#: app/routes/evaluations.py:122 +msgid "You do not have permission to evaluate players." +msgstr "Vous n’avez pas les droits pour évaluer des joueurs." + +#: app/routes/evaluations.py:127 app/routes/evaluations.py:220 +msgid "You do not have permission to evaluate players in this tryout." +msgstr "Vous n’avez pas les droits pour évaluer des joueurs dans cette sélection." + +#: app/routes/evaluations.py:134 +msgid "Player is not registered for this tryout." +msgstr "Ce joueur n’est pas inscrit à cette sélection." + +#: app/routes/evaluations.py:139 +msgid "Can only evaluate players." +msgstr "Seuls des joueurs peuvent être évalués." + +#: app/routes/evaluations.py:177 +msgid "Evaluation updated!" +msgstr "Évaluation mise à jour." + +#: app/routes/evaluations.py:190 +msgid "Evaluation submitted successfully!" +msgstr "Évaluation enregistrée." + +#: app/routes/evaluations.py:215 app/routes/teams.py:249 app/routes/teams.py:280 +#: app/routes/teams.py:311 app/routes/teams.py:336 app/routes/teams.py:361 +#: app/routes/teams.py:393 app/routes/tryouts.py:341 app/routes/tryouts.py:357 +#: app/routes/tryouts.py:376 app/routes/tryouts.py:413 app/routes/tryouts.py:448 +#: app/routes/tryouts.py:467 +msgid "Permission denied." +msgstr "Accès refusé." + +#: app/routes/main.py:43 +msgid "That language is not available." +msgstr "Cette langue n’est pas disponible." + +#: app/routes/matches.py:201 +msgid "You do not have permission to schedule matches for this tryout." +msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette sélection." + +#: app/routes/matches.py:205 app/routes/matches.py:336 +msgid "This tryout has ended. Matches can no longer be created or modified." +msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être créés ni modifiés." + +#: app/routes/matches.py:224 +msgid "Start time is required. Please select a time slot." +msgstr "L’heure de début est obligatoire. Choisissez une plage horaire." + +#: app/routes/matches.py:231 app/routes/matches.py:359 app/routes/team_matches.py:139 +#: app/routes/team_matches.py:222 +msgid "Invalid date format." +msgstr "Format de date invalide." + +#: app/routes/matches.py:246 app/routes/team_matches.py:156 +msgid "Invalid time format." +msgstr "Format d’heure invalide." + +#: app/routes/matches.py:317 +msgid "Match scheduled successfully!" +msgstr "Match planifié." + +#: app/routes/matches.py:332 app/routes/team_matches.py:209 +msgid "You do not have permission to edit this match." +msgstr "Vous n’avez pas les droits pour modifier ce match." + +#: app/routes/matches.py:365 +msgid "Start time is required." +msgstr "L’heure de début est obligatoire." + +#: app/routes/matches.py:461 app/routes/team_matches.py:245 +msgid "Match updated successfully!" +msgstr "Match mis à jour." + +#: app/routes/matches.py:506 app/routes/team_matches.py:259 +msgid "You do not have permission to delete this match." +msgstr "Vous n’avez pas les droits pour supprimer ce match." + +#: app/routes/matches.py:509 +msgid "This tryout has ended. Matches can no longer be deleted." +msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés." + +#: app/routes/matches.py:521 app/routes/team_matches.py:263 +msgid "Match deleted successfully." +msgstr "Match supprimé." + +#: app/routes/team_matches.py:97 +msgid "You do not have permission to schedule matches for this team." +msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette équipe." + +#: app/routes/team_matches.py:132 +msgid "Date is required." +msgstr "La date est obligatoire." + +#: app/routes/team_matches.py:195 +#, python-format +msgid "Team match \"%(title)s\" scheduled successfully!" +msgstr "Match d’équipe « %(title)s » planifié." + +#: app/routes/teams.py:43 +msgid "Use My Team(s) to view your teams." +msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes." + +#: app/routes/teams.py:46 +msgid "You do not have permission to view teams." +msgstr "Vous n’avez pas les droits pour consulter les équipes." + +#: app/routes/teams.py:61 +msgid "This page is for players." +msgstr "Cette page est réservée aux joueurs." + +#: app/routes/teams.py:103 +msgid "You do not have permission to create teams." +msgstr "Vous n’avez pas les droits pour créer une équipe." + +#: app/routes/teams.py:111 app/routes/teams.py:156 +msgid "Team name is required." +msgstr "Le nom de l’équipe est obligatoire." + +#: app/routes/teams.py:116 app/routes/teams.py:161 +#, python-format +msgid "Team \"%(name)s\" already exists." +msgstr "L’équipe « %(name)s » existe déjà." + +#: app/routes/teams.py:138 +#, python-format +msgid "Team \"%(name)s\" created successfully!" +msgstr "Équipe « %(name)s » créée." + +#: app/routes/teams.py:148 +msgid "You do not have permission to edit this team." +msgstr "Vous n’avez pas les droits pour modifier cette équipe." + +#: app/routes/teams.py:199 +#, python-format +msgid "Team \"%(name)s\" updated successfully!" +msgstr "Équipe « %(name)s » mise à jour." + +#: app/routes/teams.py:208 +msgid "You do not have permission to delete teams." +msgstr "Vous n’avez pas les droits pour supprimer une équipe." + +#: app/routes/teams.py:239 +#, python-format +msgid "Team \"%(name)s\" deleted successfully." +msgstr "Équipe « %(name)s » supprimée." + +#: app/routes/teams.py:254 +msgid "Please select a coach." +msgstr "Veuillez choisir un coach." + +#: app/routes/teams.py:259 +msgid "Only coaches can be assigned as coach." +msgstr "Seuls les coachs peuvent être assignés comme coach." + +#: app/routes/teams.py:263 +#, python-format +msgid "%(username)s is already a coach of %(name)s." +msgstr "%(username)s est déjà coach de %(name)s." + +#: app/routes/teams.py:270 +#, python-format +msgid "%(username)s added as coach of %(name)s." +msgstr "%(username)s a été ajouté comme coach de %(name)s." + +#: app/routes/teams.py:285 +msgid "Please select a manager." +msgstr "Veuillez choisir un gérant." + +#: app/routes/teams.py:290 +msgid "Only managers can be assigned as manager." +msgstr "Seuls les gérants peuvent être assignés comme gérant." + +#: app/routes/teams.py:294 +#, python-format +msgid "%(username)s is already a manager of %(name)s." +msgstr "%(username)s est déjà gérant de %(name)s." + +#: app/routes/teams.py:301 +#, python-format +msgid "%(username)s added as manager of %(name)s." +msgstr "%(username)s a été ajouté comme gérant de %(name)s." + +#: app/routes/teams.py:326 +#, python-format +msgid "Coach removed from %(name)s." +msgstr "Coach retiré de %(name)s." + +#: app/routes/teams.py:351 +#, python-format +msgid "Manager removed from %(name)s." +msgstr "Gérant retiré de %(name)s." + +#: app/routes/teams.py:367 app/routes/tryouts.py:380 app/routes/tryouts.py:478 +msgid "Please select a player." +msgstr "Veuillez choisir un joueur." + +#: app/routes/teams.py:372 +msgid "Can only assign players to teams." +msgstr "Seuls des joueurs peuvent être assignés à une équipe." + +#: app/routes/teams.py:377 +#, python-format +msgid "%(username)s is already on %(name)s." +msgstr "%(username)s fait déjà partie de %(name)s." + +#: app/routes/teams.py:383 +#, python-format +msgid "%(username)s added to %(name)s!" +msgstr "%(username)s a été ajouté à %(name)s." + +#: app/routes/teams.py:399 app/routes/teams.py:462 +#, python-format +msgid "%(username)s is not on %(name)s." +msgstr "%(username)s ne fait pas partie de %(name)s." + +#: app/routes/teams.py:404 +#, python-format +msgid "%(username)s removed from %(name)s." +msgstr "%(username)s a été retiré de %(name)s." + +#: app/routes/teams.py:434 app/routes/teams.py:452 +msgid "You do not have permission to add notes to this team." +msgstr "Vous n’avez pas les droits pour ajouter des notes à cette équipe." + +#: app/routes/teams.py:442 +msgid "Team notes added successfully!" +msgstr "Notes d’équipe ajoutées." + +#: app/routes/teams.py:457 app/routes/users.py:1227 app/routes/users.py:1269 +msgid "Can only add notes for players." +msgstr "Il n’est possible d’ajouter des notes que pour des joueurs." + +#: app/routes/teams.py:470 +#, python-format +msgid "Note added for %(username)s!" +msgstr "Note ajoutée pour %(username)s." + +#: app/routes/tryouts.py:43 +msgid "You do not have permission to create tryouts." +msgstr "Vous n’avez pas les droits pour créer une sélection." + +#: app/routes/tryouts.py:65 app/routes/tryouts.py:140 +msgid "Invalid start date format." +msgstr "Format de date de début invalide." + +#: app/routes/tryouts.py:74 app/routes/tryouts.py:149 +msgid "End date cannot be before start date." +msgstr "La date de fin ne peut pas précéder la date de début." + +#: app/routes/tryouts.py:78 app/routes/tryouts.py:153 +msgid "Invalid end date format." +msgstr "Format de date de fin invalide." + +#: app/routes/tryouts.py:100 +msgid "Tryout created successfully!" +msgstr "Sélection créée." + +#: app/routes/tryouts.py:114 +msgid "You do not have permission to edit this tryout." +msgstr "Vous n’avez pas les droits pour modifier cette sélection." + +#: app/routes/tryouts.py:118 +msgid "This tryout has ended and can no longer be modified." +msgstr "Cette sélection est terminée et ne peut plus être modifiée." + +#: app/routes/tryouts.py:175 +msgid "Tryout updated successfully!" +msgstr "Sélection mise à jour." + +#: app/routes/tryouts.py:207 +msgid "You do not have permission to view this tryout." +msgstr "Vous n’avez pas les droits pour consulter cette sélection." + +#: app/routes/tryouts.py:309 +msgid "Only players can register for tryouts." +msgstr "Seuls les joueurs peuvent s’inscrire à une sélection." + +#: app/routes/tryouts.py:313 +msgid "This tryout is not accepting registrations." +msgstr "Cette sélection n’accepte pas d’inscriptions." + +#: app/routes/tryouts.py:319 +msgid "You are already registered for this tryout." +msgstr "Vous êtes déjà inscrit à cette sélection." + +#: app/routes/tryouts.py:325 app/routes/tryouts.py:397 +msgid "This tryout is full." +msgstr "Cette sélection est complète." + +#: app/routes/tryouts.py:331 +msgid "Successfully registered for tryout!" +msgstr "Inscription à la sélection réussie." + +#: app/routes/tryouts.py:347 +#, python-format +msgid "Tryout status updated to %(new_status)s." +msgstr "Statut de la sélection mis à jour : %(new_status)s." + +#: app/routes/tryouts.py:366 +msgid "Registration status updated." +msgstr "Statut d’inscription mis à jour." + +#: app/routes/tryouts.py:385 +msgid "Can only register players." +msgstr "Seuls des joueurs peuvent être inscrits." + +#: app/routes/tryouts.py:391 +#, python-format +msgid "%(username)s is already registered for this tryout." +msgstr "%(username)s est déjà inscrit à cette sélection." + +#: app/routes/tryouts.py:403 +#, python-format +msgid "%(username)s registered for tryout!" +msgstr "%(username)s est inscrit à la sélection." + +#: app/routes/tryouts.py:438 +#, python-format +msgid "%(username)s removed from tryout." +msgstr "%(username)s a été retiré de la sélection." + +#: app/routes/tryouts.py:456 +#, python-format +msgid "Team \"%(team_name)s\" created!" +msgstr "Équipe « %(team_name)s » créée." + +#: app/routes/tryouts.py:485 +msgid "That player is not registered for this tryout." +msgstr "Ce joueur n’est pas inscrit à cette sélection." + +#: app/routes/tryouts.py:491 +msgid "Player is already on this team." +msgstr "Ce joueur est déjà dans cette équipe." + +#: app/routes/tryouts.py:496 +msgid "Player added to team!" +msgstr "Joueur ajouté à l’équipe." + +#: app/routes/tryouts.py:506 +msgid "You do not have permission to delete this tryout." +msgstr "Vous n’avez pas les droits pour supprimer cette sélection." + +#: app/routes/tryouts.py:541 +msgid "Tryout deleted successfully." +msgstr "Sélection supprimée." + +#: app/routes/users.py:116 +msgid "Only the president can manage users." +msgstr "Seul le président peut gérer les utilisateurs." + +#: app/routes/users.py:128 +msgid "Only the president can edit users." +msgstr "Seul le président peut modifier des utilisateurs." + +#: app/routes/users.py:168 +msgid "Email already in use by another account." +msgstr "Cette adresse courriel est déjà utilisée par un autre compte." + +#: app/routes/users.py:175 +msgid "You cannot change your own role. Ask another president to do it." +msgstr "" +"Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre président de le " +"faire." + +#: app/routes/users.py:186 +msgid "This is the last active president. Promote another account before changing this one." +msgstr "" +"C’est le dernier président actif. Promouvez un autre compte avant de modifier celui-" +"ci." + +#: app/routes/users.py:232 +#, python-format +msgid "User %(username)s updated successfully!" +msgstr "Utilisateur %(username)s mis à jour." + +#: app/routes/users.py:247 +msgid "Only the president can delete users." +msgstr "Seul le président peut supprimer des utilisateurs." + +#: app/routes/users.py:251 +msgid "You cannot delete your own account." +msgstr "Vous ne pouvez pas supprimer votre propre compte." + +#: app/routes/users.py:288 +#, python-format +msgid "User %(deleted_username)s has been removed." +msgstr "L’utilisateur %(deleted_username)s a été supprimé." + +#: app/routes/users.py:297 +msgid "Only the president can create users." +msgstr "Seul le président peut créer des utilisateurs." + +#: app/routes/users.py:336 +#, python-format +msgid "User %(full_name)s created as %(role)s!" +msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s." + +#: app/routes/users.py:393 +msgid "Username already taken." +msgstr "Ce nom d’utilisateur est déjà pris." + +#: app/routes/users.py:399 +msgid "Email already in use." +msgstr "Cette adresse courriel est déjà utilisée." + +#: app/routes/users.py:424 +msgid "Profile updated successfully!" +msgstr "Profil mis à jour." + +#: app/routes/users.py:629 +msgid "Only presidents, managers, and coaches can upload contracts." +msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat." + +#: app/routes/users.py:656 +msgid "You do not have permission to upload a contract for this player." +msgstr "Vous n’avez pas les droits pour téléverser un contrat pour ce joueur." + +#: app/routes/users.py:660 app/routes/users.py:665 app/routes/users.py:717 +#: app/routes/users.py:722 +msgid "No file selected." +msgstr "Aucun fichier sélectionné." + +#: app/routes/users.py:668 +msgid "Only PDF files are allowed for contracts." +msgstr "Seuls les fichiers PDF sont acceptés pour les contrats." + +#: app/routes/users.py:701 +#, python-format +msgid "Contract uploaded successfully for %(username)s!" +msgstr "Contrat téléversé pour %(username)s." + +#: app/routes/users.py:713 +msgid "Only the player can upload their signed contract." +msgstr "Seul le joueur peut téléverser son contrat signé." + +#: app/routes/users.py:733 +msgid "Signed contract uploaded successfully!" +msgstr "Contrat signé téléversé." + +#: app/routes/users.py:743 app/routes/users.py:754 +msgid "You do not have permission to download this contract." +msgstr "Vous n’avez pas les droits pour télécharger ce contrat." + +#: app/routes/users.py:757 +msgid "No signed contract available." +msgstr "Aucun contrat signé disponible." + +#: app/routes/users.py:824 +msgid "Only players can request One on One sessions." +msgstr "Seuls les joueurs peuvent demander une rencontre individuelle." + +#: app/routes/users.py:832 +msgid "You do not have a coach assigned to your team." +msgstr "Aucun coach n’est assigné à votre équipe." + +#: app/routes/users.py:859 +msgid "Cannot request One on One - no coach assigned." +msgstr "Impossible de demander une rencontre : aucun coach assigné." + +#: app/routes/users.py:867 +msgid "Invalid date or time format." +msgstr "Format de date ou d’heure invalide." + +#: app/routes/users.py:881 +msgid "The requested time is not within the coach's availability." +msgstr "L’horaire demandé ne correspond à aucune disponibilité du coach." + +#: app/routes/users.py:904 +msgid "Your One on One request has been submitted!" +msgstr "Votre demande de rencontre a été envoyée." + +#: app/routes/users.py:939 +msgid "Only coaches can accept One on One requests." +msgstr "Seuls les coachs peuvent accepter une demande de rencontre." + +#: app/routes/users.py:945 app/routes/users.py:986 +msgid "This request is not for you." +msgstr "Cette demande ne vous est pas destinée." + +#: app/routes/users.py:949 app/routes/users.py:990 +msgid "This request has already been processed." +msgstr "Cette demande a déjà été traitée." + +#: app/routes/users.py:971 +#, python-format +msgid "One on One request from %(player)s has been approved!" +msgstr "La demande de rencontre de %(player)s a été approuvée." + +#: app/routes/users.py:980 +msgid "Only coaches can reject One on One requests." +msgstr "Seuls les coachs peuvent refuser une demande de rencontre." + +#: app/routes/users.py:1017 +#, python-format +msgid "One on One request from %(player)s has been rejected." +msgstr "La demande de rencontre de %(player)s a été refusée." + +#: app/routes/users.py:1030 +msgid "This page is for players only." +msgstr "Cette page est réservée aux joueurs." + +#: app/routes/users.py:1061 +msgid "Only coaches can manage availability." +msgstr "Seuls les coachs peuvent gérer leurs disponibilités." + +#: app/routes/users.py:1123 +msgid "Only coaches can access the notes dashboard." +msgstr "Seuls les coachs ont accès au tableau des notes." + +#: app/routes/users.py:1184 +msgid "Only coaches can manage team notes." +msgstr "Seuls les coachs peuvent gérer les notes d’équipe." + +#: app/routes/users.py:1189 +msgid "You are not assigned to a team." +msgstr "Vous n’êtes assigné à aucune équipe." + +#: app/routes/users.py:1201 +msgid "Team notes saved successfully!" +msgstr "Notes d’équipe enregistrées." + +#: app/routes/users.py:1215 +msgid "Only coaches can manage personal notes." +msgstr "Seuls les coachs peuvent gérer les notes personnelles." + +#: app/routes/users.py:1222 app/routes/users.py:1264 app/routes/users.py:1314 +#: app/routes/users.py:1365 +msgid "Player and content are required." +msgstr "Le joueur et le contenu sont obligatoires." + +#: app/routes/users.py:1231 app/routes/users.py:1273 app/routes/users.py:1318 +#: app/routes/users.py:1369 +msgid "You can only write notes about players you work with." +msgstr "Vous ne pouvez écrire des notes que sur les joueurs avec qui vous travaillez." + +#: app/routes/users.py:1241 app/routes/users.py:1286 +#, python-format +msgid "Note added for %(username)s." +msgstr "Note ajoutée pour %(username)s." + +#: app/routes/users.py:1254 app/routes/users.py:1299 app/routes/users.py:1349 +msgid "Only coaches can add personal notes." +msgstr "Seuls les coachs peuvent ajouter des notes personnelles." + +#: app/routes/users.py:1329 app/routes/users.py:1380 +msgid "Note added successfully." +msgstr "Note ajoutée." + #: app/templates/errors/400.html:2 msgid "400 Bad Request" msgstr "400 Requête incorrecte" @@ -123,7 +761,7 @@ msgstr "400 — Requête incorrecte" msgid "" "The request could not be understood by the server. Please check your input and try " "again." -msgstr "Le serveur n'a pas pu interpréter la requête. Vérifiez votre saisie et réessayez." +msgstr "Le serveur n’a pas pu interpréter la requête. Vérifiez votre saisie et réessayez." #: app/templates/errors/400.html:12 app/templates/errors/403.html:12 #: app/templates/errors/429.html:12 @@ -147,8 +785,8 @@ msgid "" "You do not have permission to access this resource. If you believe this is an error, " "please contact an administrator." msgstr "" -"Vous n'avez pas les droits d'accès à cette ressource. Si vous pensez qu'il s'agit " -"d'une erreur, contactez un administrateur." +"Vous n’avez pas les droits d’accès à cette ressource. Si vous pensez qu’il s’agit " +"d’une erreur, contactez un administrateur." #: app/templates/errors/404.html:2 msgid "404 Not Found" @@ -164,11 +802,11 @@ msgstr "404 — Page introuvable" #: app/templates/errors/404.html:10 msgid "The page you are looking for does not exist. It may have been moved or deleted." -msgstr "La page demandée n'existe pas. Elle a peut-être été déplacée ou supprimée." +msgstr "La page demandée n’existe pas. Elle a peut-être été déplacée ou supprimée." #: app/templates/errors/404.html:12 msgid "Return Home" -msgstr "Retour à l'accueil" +msgstr "Retour à l’accueil" #: app/templates/errors/429.html:2 msgid "429 Too Many Requests" @@ -287,7 +925,7 @@ msgstr "Fermer" #: app/templates/layouts/base.html:178 msgid "Team Tryout Management System" -msgstr "Système de gestion des sélections d'équipe" +msgstr "Système de gestion des sélections d’équipe" #: app/templates/layouts/macros.html:116 msgid "Close" @@ -575,7 +1213,7 @@ msgstr "Saisir le nom complet" #: app/templates/pages/login.html:7 app/templates/pages/register.html:15 #: app/templates/pages/users.html:20 msgid "Username" -msgstr "Nom d'utilisateur" +msgstr "Nom d’utilisateur" #: app/templates/pages/create_user.html:18 msgid "Choose username" @@ -996,7 +1634,7 @@ msgstr "Connexion" #: app/templates/pages/login.html:8 msgid "Enter your username" -msgstr "Saisissez votre nom d'utilisateur" +msgstr "Saisissez votre nom d’utilisateur" #: app/templates/pages/login.html:12 msgid "Enter your password" @@ -1008,7 +1646,7 @@ msgstr "Se connecter" #: app/templates/pages/login.html:15 msgid "Don't have an account?" -msgstr "Vous n'avez pas de compte ?" +msgstr "Vous n’avez pas de compte ?" #: app/templates/pages/login.html:15 msgid "Register here" @@ -2108,3 +2746,6 @@ msgstr "Voir le profil" #~ msgid "×" #~ msgstr "×" +#~ msgid "One on One request from %(value)s has been approved!" +#~ msgstr "" + diff --git a/app/validators.py b/app/validators.py index d0acd13..cfa22c7 100644 --- a/app/validators.py +++ b/app/validators.py @@ -11,6 +11,7 @@ Usage: """ import re +from flask_babel import lazy_gettext as _l from marshmallow import Schema, fields, validate, ValidationError, pre_load, validates_schema, EXCLUDE from app.models import USER_TYPES @@ -38,10 +39,10 @@ def validate_password(value): ValidationError: If password does not meet requirements. """ if not PASSWORD_POLICY.match(value): - raise ValidationError( + raise ValidationError(_l( 'Password must be at least 8 characters with uppercase, ' 'lowercase, and a number.' - ) + )) def validate_username(value): @@ -57,9 +58,9 @@ def validate_username(value): ValidationError: If username does not meet requirements. """ if not re.match(r'^[a-zA-Z0-9_-]{3,30}$', value): - raise ValidationError( + raise ValidationError(_l( 'Username must be 3-30 characters (letters, numbers, underscore, hyphen).' - ) + )) def validate_discord_username(value): @@ -77,7 +78,7 @@ def validate_discord_username(value): if not value: return if not re.match(r'^[a-zA-Z0-9_.]{2,32}$', value): - raise ValidationError('Invalid Discord username format.') + raise ValidationError(_l('Invalid Discord username format.')) def validate_discord_user_id(value): @@ -94,7 +95,7 @@ def validate_discord_user_id(value): if not value: return if not re.match(r'^\d{17,20}$', value): - raise ValidationError('Discord User ID must be a 17-20 digit number.') + raise ValidationError(_l('Discord User ID must be a 17-20 digit number.')) def validate_phone(value): @@ -112,7 +113,7 @@ def validate_phone(value): return cleaned = re.sub(r'[\s\-\(\)\.]', '', value) if not re.match(r'^\+?\d{7,15}$', cleaned): - raise ValidationError('Invalid phone number format.') + raise ValidationError(_l('Invalid phone number format.')) # ============================================================================= @@ -151,11 +152,11 @@ class LoginSchema(StripMixin): """ username = fields.String( required=True, - validate=validate.Length(min=1, max=80, error='Username is required.'), + validate=validate.Length(min=1, max=80, error=_l('Username is required.')), ) password = fields.String( required=True, - validate=validate.Length(min=1, error='Password is required.'), + validate=validate.Length(min=1, error=_l('Password is required.')), ) @@ -176,13 +177,13 @@ class RegisterSchema(StripMixin): username = fields.String( required=True, validate=[ - validate.Length(min=3, max=80, error='Username must be 3-80 characters.'), + validate.Length(min=3, max=80, error=_l('Username must be 3-80 characters.')), validate_username, ], ) email = fields.Email( required=True, - validate=validate.Length(max=120, error='Email must be 120 characters or less.'), + validate=validate.Length(max=120, error=_l('Email must be 120 characters or less.')), ) password = fields.String( required=True, @@ -195,7 +196,7 @@ class RegisterSchema(StripMixin): ) full_name = fields.String( required=True, - validate=validate.Length(min=1, max=100, error='Full name is required.'), + validate=validate.Length(min=1, max=100, error=_l('Full name is required.')), ) phone = fields.String( validate=validate_phone, @@ -230,7 +231,7 @@ class RegisterSchema(StripMixin): ValidationError: If passwords do not match. """ if data.get('password') != data.get('confirm_password'): - raise ValidationError('Passwords do not match.', field_name='confirm_password') + raise ValidationError(_l('Passwords do not match.'), field_name='confirm_password') class CreateUserSchema(StripMixin): @@ -247,7 +248,7 @@ class CreateUserSchema(StripMixin): username = fields.String( required=True, validate=[ - validate.Length(min=3, max=80, error='Username must be 3-80 characters.'), + validate.Length(min=3, max=80, error=_l('Username must be 3-80 characters.')), validate_username, ], ) @@ -262,13 +263,13 @@ class CreateUserSchema(StripMixin): ) full_name = fields.String( required=True, - validate=validate.Length(min=1, max=100, error='Full name is required.'), + validate=validate.Length(min=1, max=100, error=_l('Full name is required.')), ) role = fields.String( required=True, validate=validate.OneOf( USER_TYPES, - error='Invalid role selected.' + error=_l('Invalid role selected.') ), ) phone = fields.String( @@ -295,7 +296,7 @@ class EditUserSchema(StripMixin): """ full_name = fields.String( required=True, - validate=validate.Length(min=1, max=100, error='Full name is required.'), + validate=validate.Length(min=1, max=100, error=_l('Full name is required.')), ) email = fields.Email( required=True, @@ -305,7 +306,7 @@ class EditUserSchema(StripMixin): required=True, validate=validate.OneOf( USER_TYPES, - error='Invalid role selected.' + error=_l('Invalid role selected.') ), ) is_active_account = fields.Boolean(load_default=True) @@ -361,7 +362,7 @@ class EditProfileSchema(StripMixin): ) full_name = fields.String( required=True, - validate=validate.Length(min=1, max=100, error='Full name is required.'), + validate=validate.Length(min=1, max=100, error=_l('Full name is required.')), ) email = fields.Email( required=True, @@ -405,10 +406,10 @@ class UploadContractSchema(StripMixin): """ player_id = fields.Integer( required=True, - validate=validate.Range(min=1, error='Player must be selected.'), + validate=validate.Range(min=1, error=_l('Player must be selected.')), ) notes = fields.String( - validate=validate.Length(max=2000, error='Notes must be 2000 characters or less.'), + validate=validate.Length(max=2000, error=_l('Notes must be 2000 characters or less.')), allow_none=True, load_default=None, ) @@ -427,25 +428,25 @@ class OneOnOneRequestSchema(StripMixin): required=True, validate=validate.Regexp( r'^\d{4}-\d{2}-\d{2}$', - error='Date must be in YYYY-MM-DD format.' + error=_l('Date must be in YYYY-MM-DD format.') ), ) start_time = fields.String( required=True, validate=validate.Regexp( r'^\d{2}:\d{2}$', - error='Start time must be in HH:MM format.' + error=_l('Start time must be in HH:MM format.') ), ) end_time = fields.String( required=True, validate=validate.Regexp( r'^\d{2}:\d{2}$', - error='End time must be in HH:MM format.' + error=_l('End time must be in HH:MM format.') ), ) points = fields.String( - validate=validate.Length(max=2000, error='Points must be 2000 characters or less.'), + validate=validate.Length(max=2000, error=_l('Points must be 2000 characters or less.')), allow_none=True, load_default=None, ) @@ -462,13 +463,13 @@ class DisponibilityAddSchema(StripMixin): required=True, validate=validate.Range( min=0, max=6, - error='Day must be 0 (Monday) to 6 (Sunday).' + error=_l('Day must be 0 (Monday) to 6 (Sunday).') ), ) start_time = fields.String( required=True, validate=validate.Regexp( r'^\d{2}:\d{2}$', - error='Start time must be in HH:MM format.' + error=_l('Start time must be in HH:MM format.') ), ) \ No newline at end of file diff --git a/docs/translations.md b/docs/translations.md index 1afa19a..441456c 100644 --- a/docs/translations.md +++ b/docs/translations.md @@ -146,21 +146,18 @@ That second check is what keeps the migration honest: adding ## 5. State of the migration -**Templates are done.** 483 strings, all translated, in both catalogues. -The suite fails if that stops being true. +**Done.** 631 strings, all translated, in both catalogues. The suite fails +if that stops being true. -Covered: navigation and shared layout, login, the five error pages, and -every page template. +Covered: navigation and shared layout, login, the five error pages, every +page template, every flash message, and every validation message. + +**Flash messages are done too** — 198 calls across every route module — +along with the validation messages of `app/validators.py`, which use +`lazy_gettext` because schema fields are built at import time. ### What is not covered yet -**Flash messages outside `routes/auth.py`.** `users.py`, `tryouts.py`, -`teams.py`, `matches.py`, `team_matches.py` and `evaluations.py` still -build them in English. They are the largest remaining block. - -**Validation messages in `app/validators.py`.** Shown to users, not marked. -They need `lazy_gettext`, since schema fields are built at import time. - **Model constants** — `ESPORT_GAMES`, `GAME_POSITIONS` — are data, not interface. Game names stay as they are; positions such as "Support" or "Duelist" are the terms players use in English and are deliberately left @@ -181,3 +178,10 @@ would land in the JavaScript string as `'`. The French wording uses typographic apostrophes (’), which are untouched, so the current strings are safe. Anything added there should be passed through a `data-` attribute or a `