feat(i18n): traduire les messages flash et de validation

198 appels flash dans les sept modules de routes, plus les 23 messages de
validation de app/validators.py. Le catalogue compte desormais 631 chaines,
aucune non traduite.

validators.py utilise lazy_gettext : les champs de schema sont construits a
l'import, donc avant qu'une requete existe. Un gettext ordinaire s'y
resoudrait une seule fois, dans la langue active au demarrage.

Un bug introduit par la conversion, puis corrige
  Le convertisseur automatique ne voyait que le premier litteral d'un appel
  flash, ce qui a casse deux chaines concatenees sur plusieurs lignes dans
  users.py -- le resultat n'etait meme pas du Python valide. Ma premiere
  verification ne l'a pas vu : elle enchainait py_compile sur head, or head
  reussit toujours, donc le "OK" s'affichait quoi qu'il arrive. Les deux
  appels sont reecrits et la verification refaite correctement.

Un bug plus interessant, revele par le test de fumee
  La langue choisie ne survivait pas a la connexion. login() et logout()
  appellent tous deux session.clear() -- l'un contre la fixation de session,
  l'autre pour terminer la session -- et le choix de langue partait avec le
  reste. Concretement : quelqu'un qui lisait la page de connexion en anglais
  se retrouvait en francais des qu'il se connectait.

  La langue est une preference d'affichage, pas un etat appartenant au
  compte. Les deux endroits la reportent maintenant explicitement, a cote du
  jeton CSRF. Quatre tests couvrent le cas, dont un qui verifie que corriger
  une cle preservee n'a pas fait tomber l'autre.

Detail de nommage : le convertisseur avait genere %(value)s pour une
expression conditionnelle, ce qui n'aide pas un traducteur. Renomme en
%(player)s.

Les 14 traductions ecrites avec une apostrophe droite sont normalisees en
apostrophe typographique. Sans consequence en HTML, ou ' s'affiche
correctement -- mais les blocs <script> ne decodent pas les entites, et
autant que le catalogue soit homogene.

200 tests.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-07 22:12:27 -04:00
co-authored by Claude Opus 5
parent 80bc1413f1
commit 37f70c89e3
15 changed files with 1639 additions and 249 deletions
+20 -6
View File
@@ -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'))
+10 -9
View File
@@ -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()
+2 -1
View File
@@ -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):
+15 -14
View File
@@ -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))
+11 -10
View File
@@ -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'))
+42 -41
View File
@@ -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'))
+39 -38
View File
@@ -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'))
+72 -71
View File
@@ -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',
Binary file not shown.
+636 -3
View File
@@ -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 <EMAIL@ADDRESS>\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 "&times;"
#~ msgstr "&times;"
#~ msgid "One on One request from %(value)s has been approved!"
#~ msgstr ""
Binary file not shown.
+659 -18
View File
@@ -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 <EMAIL@ADDRESS>\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 dutilisateur doit compter de 3 à 30 caractères (lettres, chiffres, tiret bas,"
" trait dunion)."
#: app/validators.py:81
msgid "Invalid Discord username format."
msgstr "Format de nom dutilisateur Discord invalide."
#: app/validators.py:98
msgid "Discord User ID must be a 17-20 digit number."
msgstr "Lidentifiant 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 dutilisateur 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 dutilisateur doit compter de 3 à 80 caractères."
#: app/validators.py:186
msgid "Email must be 120 characters or less."
msgstr "Ladresse 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 "Lheure de début doit être au format HH:MM."
#: app/validators.py:445
msgid "End time must be in HH:MM format."
msgstr "Lheure 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 dutilisateur 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 dutilisateur 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 nest 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 "
"Lautorisation Discord na 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 "Lautorisation 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 dobtenir le jeton daccè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 navez pas les droits pour consulter les évaluations."
#: app/routes/evaluations.py:122
msgid "You do not have permission to evaluate players."
msgstr "Vous navez 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 navez 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 nest 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 nest pas disponible."
#: app/routes/matches.py:201
msgid "You do not have permission to schedule matches for this tryout."
msgstr "Vous navez 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 "Lheure 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 dheure 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 navez pas les droits pour modifier ce match."
#: app/routes/matches.py:365
msgid "Start time is required."
msgstr "Lheure 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 navez 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 navez 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 navez 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 navez 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 navez 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 navez 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 navez 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 nest possible dajouter 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 navez 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 navez 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 navez 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 sinscrire à une sélection."
#: app/routes/tryouts.py:313
msgid "This tryout is not accepting registrations."
msgstr "Cette sélection naccepte pas dinscriptions."
#: 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 dinscription 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 nest 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 navez 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 ""
"Cest 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 "Lutilisateur %(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 dutilisateur 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 navez 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 navez 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 nest 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 dheure invalide."
#: app/routes/users.py:881
msgid "The requested time is not within the coach's availability."
msgstr "Lhoraire 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 na 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 navez pas les droits daccès à cette ressource. Si vous pensez quil sagit "
"dune 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 nexiste 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 à laccueil"
#: 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 dutilisateur"
#: 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 dutilisateur"
#: 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 navez pas de compte ?"
#: app/templates/pages/login.html:15
msgid "Register here"
@@ -2108,3 +2746,6 @@ msgstr "Voir le profil"
#~ msgid "&times;"
#~ msgstr "&times;"
#~ msgid "One on One request from %(value)s has been approved!"
#~ msgstr ""
+28 -27
View File
@@ -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.')
),
)
+15 -11
View File
@@ -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 `&#39;`. 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 `<script type="application/json">` block instead.
**The language choice must survive `session.clear()`.** Both login and
logout wipe the session — login to prevent session fixation, logout to end
it. The locale is a display preference, not state belonging to the account,
and was being discarded with the rest: someone who read the login page in
English was dropped straight back into French on signing in. Both call
sites now carry `locale` across explicitly, alongside `csrf_token`.
+90
View File
@@ -200,3 +200,93 @@ class TestSelectorUnit:
def test_select_locale_returns_a_supported_code(self, app):
with app.test_request_context('/'):
assert select_locale() in SUPPORTED_LOCALES
class TestLocaleSurvivesSessionRotation:
"""Login and logout both call session.clear() — the language choice is a
display preference, not state belonging to the account, and used to be
discarded along with everything else.
Concretely: someone who read the login page in English and signed in was
dropped straight back into French.
"""
def test_the_choice_survives_logging_in(self, client, as_role):
client.get('/lang/en')
as_role('player')
body = client.get('/users/profile').get_data(as_text=True)
assert 'lang="en"' in body
assert 'Dashboard' in body
def test_the_choice_survives_logging_out(self, client, as_role):
client.get('/lang/en')
as_role('player')
client.get('/auth/logout')
body = client.get('/auth/login').get_data(as_text=True)
assert 'lang="en"' in body
assert 'Sign In' in body
def test_french_also_survives_logging_in(self, client, as_role):
"""The English browser case: an explicit French choice must hold."""
client.get('/lang/fr')
as_role('player')
body = client.get('/users/profile',
headers={'Accept-Language': 'en-CA,en;q=0.9'}
).get_data(as_text=True)
assert 'lang="fr"' in body
def test_the_csrf_token_is_still_preserved(self, app, client, make_user, login):
"""Guard against fixing one preserved key by dropping the other.
Seeded by hand rather than relying on Flask-WTF: the test fixture
runs with CSRF disabled, so no token would exist to preserve.
"""
from app.extensions import db
from app.models import User
user_id = make_user('player')
with app.app_context():
username = db.session.get(User, user_id).username
with client.session_transaction() as sess:
sess['csrf_token'] = 'sentinel-token'
sess['locale'] = 'en'
login(username)
with client.session_transaction() as sess:
assert sess.get('csrf_token') == 'sentinel-token'
assert sess.get('locale') == 'en'
class TestFlashMessagesAreTranslated:
def test_an_access_refusal_is_translated(self, client, as_role):
as_role('player')
body = client.get('/users', follow_redirects=True).get_data(as_text=True)
assert 'Seul le président peut gérer les utilisateurs.' in body
def test_a_validation_message_is_translated(self, client, as_role):
"""From validators.py, which needs lazy_gettext: schema fields are
built at import time, before any request exists."""
as_role('admin')
body = client.post('/users/create', data={
'username': 'x', 'email': 'not-an-email',
'password': 'a', 'full_name': 'X', 'role': 'coach',
}, follow_redirects=True).get_data(as_text=True)
assert 'Le nom d' in body and 'utilisateur doit compter' in body
def test_a_message_with_a_value_keeps_it(self, client, as_role):
as_role('admin')
body = client.post('/users/create', data={
'username': 'recrue', 'email': '[email protected]',
'password': 'Password123', 'full_name': 'Nouvelle Recrue', 'role': 'coach',
}, follow_redirects=True).get_data(as_text=True)
assert 'Nouvelle Recrue' in body
assert 'créé avec le rôle coach' in body