fix(auth): retirer un CAPTCHA qui ne protegeait rien, filtrer autrement
SEC-AUTH-008. L addition a deux operandes entre 1 et 10 avait dix-neuf reponses possibles et se resolvait en lisant la question comme une chaine. Elle n arretait aucune inscription automatisee. Elle ajoutait en revanche une etape a chaque personne, lecteur d ecran compris, contre une apparence de protection — ce qui est pire que rien, puisque ca se compte comme une protection. L autre branche proposee par l audit etait un vrai service de CAPTCHA : un tiers, une cle d API, une requete a chaque affichage, et un script etranger remis dans script-src, defaisant le travail qui a ferme SEC-WEB-001. Disproportionne pour le site d un club. A la place, deux verifications invisibles pour un visiteur : un champ piege, cache par la feuille de style et hors du parcours clavier, qu un robot remplisseur complete et qu une personne ne voit jamais ; et un delai minimal entre la remise du formulaire et son retour, l horodatage etant dans la session signee et non dans un champ. Le plafond est dit dans le code plutot que sous-entendu : ceci arrete le pourriel de masse, pas quelqu un qui lit la page. Ce qui filtrerait vraiment les inscriptions serait l activation des comptes par le staff — is_active_account vaut True par defaut. C est une decision de produit. L angle « session forgeable » du constat tombe : avec SECRET_KEY compromise (SEC-001), on forge une session connectee sur n importe quel compte et on n a aucune raison de s inscrire. Au passage, ARCH-005 en partie : le bloc « regenerer, purger les mots de passe, re-rendre » etait recopie quatre fois. Un seul helper, et la purge des mots de passe ne peut plus etre oubliee dans la cinquieme copie. Les refus sont journalises avec leur motif — c est le seul endroit ou un abus du formulaire devient visible — mais restent indistinguables pour l expediteur : nommer la regle indique comment la contourner. 429 tests.
This commit is contained in:
+106
-85
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
This module handles user authentication including login with account lockout
|
This module handles user authentication including login with account lockout
|
||||||
protection, logout with session clearing, and new user registration with
|
protection, logout with session clearing, and new user registration with
|
||||||
password policy enforcement and CAPTCHA verification.
|
password policy enforcement and sign-up screening.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import uuid
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
@@ -38,6 +38,18 @@ MAX_LOCKOUT_MINUTES = 240
|
|||||||
#: response time stops telling a caller which usernames exist (SEC-017).
|
#: response time stops telling a caller which usernames exist (SEC-017).
|
||||||
_ABSENT_USER_HASH = None
|
_ABSENT_USER_HASH = None
|
||||||
|
|
||||||
|
#: Session key recording when the registration form was handed out.
|
||||||
|
REGISTRATION_ISSUED_KEY = 'registration_form_issued_at'
|
||||||
|
|
||||||
|
#: Name of the honeypot input. Plausible enough that a form-filler wants it,
|
||||||
|
#: absent from the visible form. Hidden by .honeypot in style.css — not by an
|
||||||
|
#: inline style, so that the rule survives a tightening of style-src.
|
||||||
|
REGISTRATION_HONEYPOT_FIELD = 'website'
|
||||||
|
|
||||||
|
#: Floor on how long a genuine registration takes. Eleven fields and a
|
||||||
|
#: password typed twice; three seconds is generous.
|
||||||
|
MIN_REGISTRATION_SECONDS = 3
|
||||||
|
|
||||||
# Discord OAuth2 configuration
|
# Discord OAuth2 configuration
|
||||||
DISCORD_CLIENT_ID = os.getenv('DISCORD_CLIENT_ID')
|
DISCORD_CLIENT_ID = os.getenv('DISCORD_CLIENT_ID')
|
||||||
DISCORD_CLIENT_SECRET = os.getenv('DISCORD_CLIENT_SECRET')
|
DISCORD_CLIENT_SECRET = os.getenv('DISCORD_CLIENT_SECRET')
|
||||||
@@ -116,41 +128,65 @@ def cooloff_minutes(failed_attempts):
|
|||||||
return min(LOCKOUT_DURATION_MINUTES * (2**steps), MAX_LOCKOUT_MINUTES)
|
return min(LOCKOUT_DURATION_MINUTES * (2**steps), MAX_LOCKOUT_MINUTES)
|
||||||
|
|
||||||
|
|
||||||
def generate_captcha():
|
def issue_registration_challenge():
|
||||||
"""Generate a simple math CAPTCHA challenge.
|
"""Mark that the registration form has been handed out, and when.
|
||||||
|
|
||||||
Creates a random addition problem and stores the answer in the session.
|
Kept in the signed session rather than in a form field, so that the
|
||||||
|
timestamp is not something the submitter can choose. Left in place across
|
||||||
|
a failed submission: someone correcting a typo should not be told to slow
|
||||||
|
down, and a robot has already paid for the round trip by then.
|
||||||
|
"""
|
||||||
|
session.setdefault(REGISTRATION_ISSUED_KEY, time.time())
|
||||||
|
|
||||||
|
|
||||||
|
def check_registration_challenge(form):
|
||||||
|
"""Say why this registration should be refused, or None to accept.
|
||||||
|
|
||||||
|
What replaced the arithmetic CAPTCHA, and why (SEC-AUTH-008).
|
||||||
|
|
||||||
|
`a + b = ?` with both operands between 1 and 10 has nineteen possible
|
||||||
|
answers and is solvable by reading the string. It stopped no automated
|
||||||
|
registration whatsoever. What it did do was add a step for every human,
|
||||||
|
including anyone using a screen reader, in exchange for an appearance of
|
||||||
|
protection — which is worse than no protection, because it gets counted
|
||||||
|
as one.
|
||||||
|
|
||||||
|
The audit's alternative was a real CAPTCHA service. That means a third
|
||||||
|
party, an API key, a request on every page load, and putting a foreign
|
||||||
|
script back into script-src — undoing the CSP work that closed
|
||||||
|
SEC-WEB-001. Disproportionate for a club site.
|
||||||
|
|
||||||
|
So: two checks that cost the visitor nothing.
|
||||||
|
|
||||||
|
- a honeypot field, hidden in the stylesheet, that a form-filling
|
||||||
|
robot completes and a person never sees;
|
||||||
|
- a minimum dwell time between being handed the form and sending it
|
||||||
|
back. Eleven fields and a password typed twice do not get filled in
|
||||||
|
under three seconds, and a POST with no issued form at all never
|
||||||
|
fetched the page.
|
||||||
|
|
||||||
|
Be clear about the ceiling: this stops commodity spam, not somebody who
|
||||||
|
looks at the form for five minutes. The thing that would actually gate
|
||||||
|
registration is staff activation of new accounts, which does not exist —
|
||||||
|
`is_active_account` defaults to True. That is a product decision, not one
|
||||||
|
to slip in here.
|
||||||
|
|
||||||
|
The session-forgery angle in the constat is moot: with SECRET_KEY
|
||||||
|
compromised (SEC-001) an attacker forges a logged-in session for any
|
||||||
|
account and has no reason to register at all.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary with 'question' (e.g., '3 + 7') and 'id' keys.
|
str | None: a short reason for the log, or None to let it through.
|
||||||
"""
|
"""
|
||||||
import random
|
if form.get(REGISTRATION_HONEYPOT_FIELD, '').strip():
|
||||||
|
return 'honeypot'
|
||||||
|
|
||||||
a = random.randint(1, 10)
|
issued_at = session.get(REGISTRATION_ISSUED_KEY)
|
||||||
b = random.randint(1, 10)
|
if issued_at is None:
|
||||||
captcha_id = str(uuid.uuid4())
|
return 'no-form-issued'
|
||||||
session['captcha_id'] = captcha_id
|
if time.time() - issued_at < MIN_REGISTRATION_SECONDS:
|
||||||
session['captcha_answer'] = a + b
|
return 'too-fast'
|
||||||
return {'question': f'{a} + {b} = ?', 'id': captcha_id}
|
return None
|
||||||
|
|
||||||
|
|
||||||
def verify_captcha(user_answer):
|
|
||||||
"""Verify the CAPTCHA answer from the session.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_answer: The user's submitted answer (string or int).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the answer matches the stored CAPTCHA, False otherwise.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
expected = session.pop('captcha_answer', None)
|
|
||||||
session.pop('captcha_id', None)
|
|
||||||
if expected is None:
|
|
||||||
return False
|
|
||||||
return int(user_answer) == expected
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||||
@@ -276,14 +312,33 @@ def login():
|
|||||||
return render_template('pages/login.html')
|
return render_template('pages/login.html')
|
||||||
|
|
||||||
|
|
||||||
|
def _rerender_registration(form_data):
|
||||||
|
"""Re-render the registration form after a refusal.
|
||||||
|
|
||||||
|
Was copied out four times, near-identically (ARCH-005). Dropping the two
|
||||||
|
password fields is the part that must not be forgotten in the fifth copy:
|
||||||
|
echoing a password back into the HTML puts it in the browser's cache and
|
||||||
|
in any proxy log along the way.
|
||||||
|
"""
|
||||||
|
form_data = dict(form_data)
|
||||||
|
form_data.pop('password', None)
|
||||||
|
form_data.pop('confirm_password', None)
|
||||||
|
return render_template(
|
||||||
|
'pages/register.html',
|
||||||
|
esport_games=ESPORT_GAMES,
|
||||||
|
honeypot_field=REGISTRATION_HONEYPOT_FIELD,
|
||||||
|
form_data=form_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||||
@limiter.limit("20 per hour")
|
@limiter.limit("20 per hour")
|
||||||
def register():
|
def register():
|
||||||
"""Handle new player registration with CAPTCHA and password policy.
|
"""Handle new player registration.
|
||||||
|
|
||||||
GET: Render the registration form with E-Sports games list and CAPTCHA.
|
GET: Render the registration form with the E-Sports games list.
|
||||||
POST: Validate all inputs, verify CAPTCHA, enforce password policy,
|
POST: Screen the submission (see check_registration_challenge), validate
|
||||||
and create a new player account.
|
every input against RegisterSchema, and create a new player account.
|
||||||
|
|
||||||
Only players can register through this form. Validates username/email
|
Only players can register through this form. Validates username/email
|
||||||
uniqueness and password confirmation.
|
uniqueness and password confirmation.
|
||||||
@@ -299,20 +354,15 @@ def register():
|
|||||||
form_data = dict(request.form)
|
form_data = dict(request.form)
|
||||||
form_data['games'] = request.form.getlist('games')
|
form_data['games'] = request.form.getlist('games')
|
||||||
|
|
||||||
# Validate CAPTCHA first
|
refusal = check_registration_challenge(request.form)
|
||||||
captcha_answer = request.form.get('captcha_answer', '')
|
if refusal is not None:
|
||||||
if not verify_captcha(captcha_answer):
|
# Logged, because this is the only place abuse of the sign-up
|
||||||
flash(_('Incorrect CAPTCHA answer. Please try again.'), 'danger')
|
# form becomes visible at all. Deliberately vague to the sender:
|
||||||
captcha = generate_captcha()
|
# naming the honeypot tells whoever tripped it how to avoid it.
|
||||||
# Clear password fields only on CAPTCHA failure
|
log_auth_event('account.registration_refused', reason=refusal)
|
||||||
form_data.pop('password', None)
|
flash(_('Your registration could not be processed. Please try again.'), 'danger')
|
||||||
form_data.pop('confirm_password', None)
|
issue_registration_challenge()
|
||||||
return render_template(
|
return _rerender_registration(form_data)
|
||||||
'pages/register.html',
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
captcha=captcha,
|
|
||||||
form_data=form_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate input with marshmallow schema
|
# Validate input with marshmallow schema
|
||||||
register_schema = RegisterSchema()
|
register_schema = RegisterSchema()
|
||||||
@@ -322,16 +372,7 @@ def register():
|
|||||||
for field, messages in err.messages.items():
|
for field, messages in err.messages.items():
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger')
|
flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger')
|
||||||
captcha = generate_captcha()
|
return _rerender_registration(form_data)
|
||||||
# Clear password fields on validation failure
|
|
||||||
form_data.pop('password', None)
|
|
||||||
form_data.pop('confirm_password', None)
|
|
||||||
return render_template(
|
|
||||||
'pages/register.html',
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
captcha=captcha,
|
|
||||||
form_data=form_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
username = validated['username']
|
username = validated['username']
|
||||||
email = validated['email']
|
email = validated['email']
|
||||||
@@ -345,27 +386,11 @@ def register():
|
|||||||
|
|
||||||
if User.query.filter_by(username=username).first():
|
if User.query.filter_by(username=username).first():
|
||||||
flash(_('Username already exists.'), 'danger')
|
flash(_('Username already exists.'), 'danger')
|
||||||
captcha = generate_captcha()
|
return _rerender_registration(form_data)
|
||||||
form_data.pop('password', None)
|
|
||||||
form_data.pop('confirm_password', None)
|
|
||||||
return render_template(
|
|
||||||
'pages/register.html',
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
captcha=captcha,
|
|
||||||
form_data=form_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
if User.query.filter_by(email=email).first():
|
if User.query.filter_by(email=email).first():
|
||||||
flash(_('Email already registered.'), 'danger')
|
flash(_('Email already registered.'), 'danger')
|
||||||
captcha = generate_captcha()
|
return _rerender_registration(form_data)
|
||||||
form_data.pop('password', None)
|
|
||||||
form_data.pop('confirm_password', None)
|
|
||||||
return render_template(
|
|
||||||
'pages/register.html',
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
captcha=captcha,
|
|
||||||
form_data=form_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
hashed_password = hash_password(password)
|
hashed_password = hash_password(password)
|
||||||
user = Player(
|
user = Player(
|
||||||
@@ -404,6 +429,7 @@ def register():
|
|||||||
|
|
||||||
# Clear Discord OAuth data from session after successful registration
|
# Clear Discord OAuth data from session after successful registration
|
||||||
session.pop('discord_oauth', None)
|
session.pop('discord_oauth', None)
|
||||||
|
session.pop(REGISTRATION_ISSUED_KEY, None)
|
||||||
|
|
||||||
log_auth_event('account.registered', username=user.username, user_id=user.id)
|
log_auth_event('account.registered', username=user.username, user_id=user.id)
|
||||||
|
|
||||||
@@ -411,13 +437,8 @@ def register():
|
|||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
# GET request — render empty form
|
# GET request — render empty form
|
||||||
captcha = generate_captcha()
|
issue_registration_challenge()
|
||||||
return render_template(
|
return _rerender_registration({})
|
||||||
'pages/register.html',
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
captcha=captcha,
|
|
||||||
form_data={},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/discord/login')
|
@auth_bp.route('/discord/login')
|
||||||
|
|||||||
@@ -2021,3 +2021,12 @@ a:hover { color: var(--primary-dark); }
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Honeypot: hidden from everyone, filled in only by a robot (SEC-AUTH-008).
|
||||||
|
The opposite of .sr-only above — that one hides from the eye and keeps the
|
||||||
|
announcement, this one has to hide from both. display:none is deliberate:
|
||||||
|
an off-screen input is still reachable by keyboard and by a screen reader,
|
||||||
|
and a person who lands in it gets refused with no idea why. */
|
||||||
|
.honeypot {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -146,9 +146,14 @@
|
|||||||
required>
|
required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
{# Honeypot (SEC-AUTH-008). Not a field anyone is meant to see or fill:
|
||||||
<label for="captcha_answer"><i class="fas fa-calculator"></i> {{ captcha.question }}</label>
|
hidden in the stylesheet, kept out of the tab order, and told to
|
||||||
<input type="number" id="captcha_answer" name="captcha_answer" placeholder="{{ _('Answer') }}" required autocomplete="off">
|
screen readers to skip. A submission that carries a value here is a
|
||||||
|
robot filling every input it can find.
|
||||||
|
Do not add a label, do not translate the name, do not remove
|
||||||
|
aria-hidden — each of those turns it into a trap for a person. #}
|
||||||
|
<div class="honeypot" aria-hidden="true">
|
||||||
|
<input type="text" name="{{ honeypot_field }}" tabindex="-1" autocomplete="off" value="">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-block">{{ _('Create Account') }}</button>
|
<button type="submit" class="btn btn-primary btn-block">{{ _('Create Account') }}</button>
|
||||||
|
|||||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: team-tryouts VERSION\n"
|
"Project-Id-Version: team-tryouts VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-08-08 15:57-0400\n"
|
"POT-Creation-Date: 2026-08-11 12:05-0400\n"
|
||||||
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -19,7 +19,7 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
|
|
||||||
#: app/validators.py:49
|
#: app/validators.py:50
|
||||||
msgid ""
|
msgid ""
|
||||||
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
||||||
"number."
|
"number."
|
||||||
@@ -27,95 +27,95 @@ msgstr ""
|
|||||||
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
||||||
"number."
|
"number."
|
||||||
|
|
||||||
#: app/validators.py:67
|
#: app/validators.py:68
|
||||||
msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
|
msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
|
||||||
msgstr "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
|
msgstr "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
|
||||||
|
|
||||||
#: app/validators.py:86
|
#: app/validators.py:87
|
||||||
msgid "Invalid Discord username format."
|
msgid "Invalid Discord username format."
|
||||||
msgstr "Invalid Discord username format."
|
msgstr "Invalid Discord username format."
|
||||||
|
|
||||||
#: app/validators.py:103
|
#: app/validators.py:104
|
||||||
msgid "Discord User ID must be a 17-20 digit number."
|
msgid "Discord User ID must be a 17-20 digit number."
|
||||||
msgstr "Discord User ID must be a 17-20 digit number."
|
msgstr "Discord User ID must be a 17-20 digit number."
|
||||||
|
|
||||||
#: app/validators.py:121
|
#: app/validators.py:122
|
||||||
msgid "Invalid phone number format."
|
msgid "Invalid phone number format."
|
||||||
msgstr "Invalid phone number format."
|
msgstr "Invalid phone number format."
|
||||||
|
|
||||||
#: app/validators.py:163
|
#: app/validators.py:164
|
||||||
msgid "Username is required."
|
msgid "Username is required."
|
||||||
msgstr "Username is required."
|
msgstr "Username is required."
|
||||||
|
|
||||||
#: app/validators.py:167
|
#: app/validators.py:168
|
||||||
msgid "Password is required."
|
msgid "Password is required."
|
||||||
msgstr "Password is required."
|
msgstr "Password is required."
|
||||||
|
|
||||||
#: app/validators.py:189 app/validators.py:261
|
#: app/validators.py:190 app/validators.py:262
|
||||||
msgid "Username must be 3-80 characters."
|
msgid "Username must be 3-80 characters."
|
||||||
msgstr "Username must be 3-80 characters."
|
msgstr "Username must be 3-80 characters."
|
||||||
|
|
||||||
#: app/validators.py:195
|
#: app/validators.py:196
|
||||||
msgid "Email must be 120 characters or less."
|
msgid "Email must be 120 characters or less."
|
||||||
msgstr "Email must be 120 characters or less."
|
msgstr "Email must be 120 characters or less."
|
||||||
|
|
||||||
#: app/validators.py:208 app/validators.py:276 app/validators.py:307
|
#: app/validators.py:209 app/validators.py:277 app/validators.py:308
|
||||||
#: app/validators.py:371
|
#: app/validators.py:372
|
||||||
msgid "Full name is required."
|
msgid "Full name is required."
|
||||||
msgstr "Full name is required."
|
msgstr "Full name is required."
|
||||||
|
|
||||||
#: app/validators.py:243
|
#: app/validators.py:244
|
||||||
msgid "Passwords do not match."
|
msgid "Passwords do not match."
|
||||||
msgstr "Passwords do not match."
|
msgstr "Passwords do not match."
|
||||||
|
|
||||||
#: app/validators.py:280 app/validators.py:315
|
#: app/validators.py:281 app/validators.py:316
|
||||||
msgid "Invalid role selected."
|
msgid "Invalid role selected."
|
||||||
msgstr "Invalid role selected."
|
msgstr "Invalid role selected."
|
||||||
|
|
||||||
#: app/validators.py:416
|
#: app/validators.py:417
|
||||||
msgid "Player must be selected."
|
msgid "Player must be selected."
|
||||||
msgstr "Player must be selected."
|
msgstr "Player must be selected."
|
||||||
|
|
||||||
#: app/validators.py:419
|
#: app/validators.py:420
|
||||||
msgid "Notes must be 2000 characters or less."
|
msgid "Notes must be 2000 characters or less."
|
||||||
msgstr "Notes must be 2000 characters or less."
|
msgstr "Notes must be 2000 characters or less."
|
||||||
|
|
||||||
#: app/validators.py:438
|
#: app/validators.py:439
|
||||||
msgid "Date must be in YYYY-MM-DD format."
|
msgid "Date must be in YYYY-MM-DD format."
|
||||||
msgstr "Date must be in YYYY-MM-DD format."
|
msgstr "Date must be in YYYY-MM-DD format."
|
||||||
|
|
||||||
#: app/validators.py:443 app/validators.py:470
|
#: app/validators.py:444 app/validators.py:471
|
||||||
msgid "Start time must be in HH:MM format."
|
msgid "Start time must be in HH:MM format."
|
||||||
msgstr "Start time must be in HH:MM format."
|
msgstr "Start time must be in HH:MM format."
|
||||||
|
|
||||||
#: app/validators.py:447
|
#: app/validators.py:448
|
||||||
msgid "End time must be in HH:MM format."
|
msgid "End time must be in HH:MM format."
|
||||||
msgstr "End time must be in HH:MM format."
|
msgstr "End time must be in HH:MM format."
|
||||||
|
|
||||||
#: app/validators.py:450
|
#: app/validators.py:451
|
||||||
msgid "Points must be 2000 characters or less."
|
msgid "Points must be 2000 characters or less."
|
||||||
msgstr "Points must be 2000 characters or less."
|
msgstr "Points must be 2000 characters or less."
|
||||||
|
|
||||||
#: app/validators.py:466
|
#: app/validators.py:467
|
||||||
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
||||||
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
|
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
|
||||||
|
|
||||||
#: app/routes/auth.py:186 app/routes/auth.py:322 app/routes/users.py:106
|
#: app/routes/auth.py:224 app/routes/auth.py:374 app/routes/users/_shared.py:64
|
||||||
#: app/routes/users.py:821
|
#: app/routes/users/contracts.py:95
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(field)s: %(msg)s"
|
msgid "%(field)s: %(msg)s"
|
||||||
msgstr "%(field)s: %(msg)s"
|
msgstr "%(field)s: %(msg)s"
|
||||||
|
|
||||||
#: app/routes/auth.py:203
|
#: app/routes/auth.py:241
|
||||||
msgid "This account has been deactivated."
|
msgid "This account has been deactivated."
|
||||||
msgstr "This account has been deactivated."
|
msgstr "This account has been deactivated."
|
||||||
|
|
||||||
#: app/routes/auth.py:238
|
#: app/routes/auth.py:276
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Welcome back, %(username)s!"
|
msgid "Welcome back, %(username)s!"
|
||||||
msgstr "Welcome back, %(username)s!"
|
msgstr "Welcome back, %(username)s!"
|
||||||
|
|
||||||
#: app/routes/auth.py:268
|
#: app/routes/auth.py:306
|
||||||
msgid ""
|
msgid ""
|
||||||
"Login unsuccessful. Please check your username and password, or ask a "
|
"Login unsuccessful. Please check your username and password, or ask a "
|
||||||
"president for help."
|
"president for help."
|
||||||
@@ -123,27 +123,27 @@ msgstr ""
|
|||||||
"Login unsuccessful. Please check your username and password, or ask a "
|
"Login unsuccessful. Please check your username and password, or ask a "
|
||||||
"president for help."
|
"president for help."
|
||||||
|
|
||||||
#: app/routes/auth.py:303
|
#: app/routes/auth.py:363
|
||||||
msgid "Incorrect CAPTCHA answer. Please try again."
|
msgid "Your registration could not be processed. Please try again."
|
||||||
msgstr "Incorrect CAPTCHA answer. Please try again."
|
msgstr "Your registration could not be processed. Please try again."
|
||||||
|
|
||||||
#: app/routes/auth.py:345 app/routes/users.py:436
|
#: app/routes/auth.py:388 app/routes/users/accounts.py:308
|
||||||
msgid "Username already exists."
|
msgid "Username already exists."
|
||||||
msgstr "Username already exists."
|
msgstr "Username already exists."
|
||||||
|
|
||||||
#: app/routes/auth.py:357 app/routes/users.py:440
|
#: app/routes/auth.py:392 app/routes/users/accounts.py:312
|
||||||
msgid "Email already registered."
|
msgid "Email already registered."
|
||||||
msgstr "Email already registered."
|
msgstr "Email already registered."
|
||||||
|
|
||||||
#: app/routes/auth.py:404
|
#: app/routes/auth.py:436
|
||||||
msgid "Your account has been created! You can now log in."
|
msgid "Your account has been created! You can now log in."
|
||||||
msgstr "Your account has been created! You can now log in."
|
msgstr "Your account has been created! You can now log in."
|
||||||
|
|
||||||
#: app/routes/auth.py:430
|
#: app/routes/auth.py:457
|
||||||
msgid "Discord OAuth2 is not configured."
|
msgid "Discord OAuth2 is not configured."
|
||||||
msgstr "Discord OAuth2 is not configured."
|
msgstr "Discord OAuth2 is not configured."
|
||||||
|
|
||||||
#: app/routes/auth.py:471
|
#: app/routes/auth.py:498
|
||||||
msgid ""
|
msgid ""
|
||||||
"Discord authorization could not be verified. Please start the connection "
|
"Discord authorization could not be verified. Please start the connection "
|
||||||
"again from this page."
|
"again from this page."
|
||||||
@@ -151,418 +151,419 @@ msgstr ""
|
|||||||
"Discord authorization could not be verified. Please start the connection "
|
"Discord authorization could not be verified. Please start the connection "
|
||||||
"again from this page."
|
"again from this page."
|
||||||
|
|
||||||
#: app/routes/auth.py:480
|
#: app/routes/auth.py:507
|
||||||
msgid "Discord authorization failed. No code received."
|
msgid "Discord authorization failed. No code received."
|
||||||
msgstr "Discord authorization failed. No code received."
|
msgstr "Discord authorization failed. No code received."
|
||||||
|
|
||||||
#: app/routes/auth.py:504
|
#: app/routes/auth.py:531
|
||||||
msgid "Failed to connect to Discord. Please try again."
|
msgid "Failed to connect to Discord. Please try again."
|
||||||
msgstr "Failed to connect to Discord. Please try again."
|
msgstr "Failed to connect to Discord. Please try again."
|
||||||
|
|
||||||
#: app/routes/auth.py:508
|
#: app/routes/auth.py:535
|
||||||
msgid "Failed to obtain Discord access token."
|
msgid "Failed to obtain Discord access token."
|
||||||
msgstr "Failed to obtain Discord access token."
|
msgstr "Failed to obtain Discord access token."
|
||||||
|
|
||||||
#: app/routes/auth.py:523
|
#: app/routes/auth.py:550
|
||||||
msgid "Failed to fetch Discord user profile."
|
msgid "Failed to fetch Discord user profile."
|
||||||
msgstr "Failed to fetch Discord user profile."
|
msgstr "Failed to fetch Discord user profile."
|
||||||
|
|
||||||
#: app/routes/auth.py:570
|
#: app/routes/auth.py:597
|
||||||
msgid "Discord account connected! Your profile has been pre-filled."
|
msgid "Discord account connected! Your profile has been pre-filled."
|
||||||
msgstr "Discord account connected! Your profile has been pre-filled."
|
msgstr "Discord account connected! Your profile has been pre-filled."
|
||||||
|
|
||||||
#: app/routes/auth.py:598
|
#: app/routes/auth.py:625
|
||||||
msgid "You have been logged out."
|
msgid "You have been logged out."
|
||||||
msgstr "You have been logged out."
|
msgstr "You have been logged out."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:45
|
#: app/routes/evaluations.py:46
|
||||||
msgid "You do not have permission to view evaluations."
|
msgid "You do not have permission to view evaluations."
|
||||||
msgstr "You do not have permission to view evaluations."
|
msgstr "You do not have permission to view evaluations."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:135
|
#: app/routes/evaluations.py:136
|
||||||
msgid "You do not have permission to evaluate players."
|
msgid "You do not have permission to evaluate players."
|
||||||
msgstr "You do not have permission to evaluate players."
|
msgstr "You do not have permission to evaluate players."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:140 app/routes/evaluations.py:263
|
#: app/routes/evaluations.py:141 app/routes/evaluations.py:264
|
||||||
msgid "You do not have permission to evaluate players in this tryout."
|
msgid "You do not have permission to evaluate players in this tryout."
|
||||||
msgstr "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:151
|
#: app/routes/evaluations.py:152
|
||||||
msgid "Player is not registered for this tryout."
|
msgid "Player is not registered for this tryout."
|
||||||
msgstr "Player is not registered for this tryout."
|
msgstr "Player is not registered for this tryout."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:156
|
#: app/routes/evaluations.py:157
|
||||||
msgid "Can only evaluate players."
|
msgid "Can only evaluate players."
|
||||||
msgstr "Can only evaluate players."
|
msgstr "Can only evaluate players."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:208
|
#: app/routes/evaluations.py:209
|
||||||
msgid "Evaluation updated!"
|
msgid "Evaluation updated!"
|
||||||
msgstr "Evaluation updated!"
|
msgstr "Evaluation updated!"
|
||||||
|
|
||||||
#: app/routes/evaluations.py:228
|
#: app/routes/evaluations.py:229
|
||||||
msgid "Evaluation submitted successfully!"
|
msgid "Evaluation submitted successfully!"
|
||||||
msgstr "Evaluation submitted successfully!"
|
msgstr "Evaluation submitted successfully!"
|
||||||
|
|
||||||
#: app/routes/evaluations.py:258 app/routes/teams.py:268
|
#: app/routes/evaluations.py:259 app/routes/teams.py:270
|
||||||
#: app/routes/teams.py:309 app/routes/teams.py:350 app/routes/teams.py:375
|
#: app/routes/teams.py:311 app/routes/teams.py:352 app/routes/teams.py:377
|
||||||
#: app/routes/teams.py:400 app/routes/teams.py:435 app/routes/tryouts.py:472
|
#: app/routes/teams.py:402 app/routes/teams.py:437 app/routes/tryouts.py:505
|
||||||
#: app/routes/tryouts.py:488 app/routes/tryouts.py:508
|
#: app/routes/tryouts.py:521 app/routes/tryouts.py:541
|
||||||
#: app/routes/tryouts.py:547 app/routes/tryouts.py:583
|
#: app/routes/tryouts.py:580 app/routes/tryouts.py:616
|
||||||
#: app/routes/tryouts.py:602
|
#: app/routes/tryouts.py:635
|
||||||
msgid "Permission denied."
|
msgid "Permission denied."
|
||||||
msgstr "Permission denied."
|
msgstr "Permission denied."
|
||||||
|
|
||||||
#: app/routes/main.py:53
|
#: app/routes/main.py:55
|
||||||
msgid "That language is not available."
|
msgid "That language is not available."
|
||||||
msgstr "That language is not available."
|
msgstr "That language is not available."
|
||||||
|
|
||||||
#: app/routes/matches.py:245
|
#: app/routes/matches.py:307
|
||||||
msgid "You do not have permission to schedule matches for this tryout."
|
msgid "You do not have permission to schedule matches for this tryout."
|
||||||
msgstr "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:249 app/routes/matches.py:422
|
#: app/routes/matches.py:311 app/routes/matches.py:473
|
||||||
msgid "This tryout has ended. Matches can no longer be created or modified."
|
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."
|
msgstr "This tryout has ended. Matches can no longer be created or modified."
|
||||||
|
|
||||||
#: app/routes/matches.py:270
|
#: app/routes/matches.py:332
|
||||||
msgid "Start time is required. Please select a time slot."
|
msgid "Start time is required. Please select a time slot."
|
||||||
msgstr "Start time is required. Please select a time slot."
|
msgstr "Start time is required. Please select a time slot."
|
||||||
|
|
||||||
#: app/routes/matches.py:282 app/routes/matches.py:445
|
#: app/routes/matches.py:344 app/routes/matches.py:496
|
||||||
#: app/routes/team_matches.py:151 app/routes/team_matches.py:255
|
#: app/routes/team_matches.py:153 app/routes/team_matches.py:247
|
||||||
msgid "Invalid date format."
|
msgid "Invalid date format."
|
||||||
msgstr "Invalid date format."
|
msgstr "Invalid date format."
|
||||||
|
|
||||||
#: app/routes/matches.py:302 app/routes/team_matches.py:172
|
#: app/routes/matches.py:364 app/routes/team_matches.py:174
|
||||||
msgid "Invalid time format."
|
msgid "Invalid time format."
|
||||||
msgstr "Invalid time format."
|
msgstr "Invalid time format."
|
||||||
|
|
||||||
#: app/routes/matches.py:398
|
#: app/routes/matches.py:449
|
||||||
msgid "Match scheduled successfully!"
|
msgid "Match scheduled successfully!"
|
||||||
msgstr "Match scheduled successfully!"
|
msgstr "Match scheduled successfully!"
|
||||||
|
|
||||||
#: app/routes/matches.py:418 app/routes/team_matches.py:242
|
#: app/routes/matches.py:469 app/routes/team_matches.py:234
|
||||||
msgid "You do not have permission to edit this match."
|
msgid "You do not have permission to edit this match."
|
||||||
msgstr "You do not have permission to edit this match."
|
msgstr "You do not have permission to edit this match."
|
||||||
|
|
||||||
#: app/routes/matches.py:456
|
#: app/routes/matches.py:507
|
||||||
msgid "Start time is required."
|
msgid "Start time is required."
|
||||||
msgstr "Start time is required."
|
msgstr "Start time is required."
|
||||||
|
|
||||||
#: app/routes/matches.py:578 app/routes/team_matches.py:284
|
#: app/routes/matches.py:616 app/routes/team_matches.py:276
|
||||||
msgid "Match updated successfully!"
|
msgid "Match updated successfully!"
|
||||||
msgstr "Match updated successfully!"
|
msgstr "Match updated successfully!"
|
||||||
|
|
||||||
#: app/routes/matches.py:631 app/routes/team_matches.py:299
|
#: app/routes/matches.py:669 app/routes/team_matches.py:291
|
||||||
msgid "You do not have permission to delete this match."
|
msgid "You do not have permission to delete this match."
|
||||||
msgstr "You do not have permission to delete this match."
|
msgstr "You do not have permission to delete this match."
|
||||||
|
|
||||||
#: app/routes/matches.py:634
|
#: app/routes/matches.py:672
|
||||||
msgid "This tryout has ended. Matches can no longer be deleted."
|
msgid "This tryout has ended. Matches can no longer be deleted."
|
||||||
msgstr "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:647 app/routes/team_matches.py:303
|
#: app/routes/matches.py:685 app/routes/team_matches.py:295
|
||||||
msgid "Match deleted successfully."
|
msgid "Match deleted successfully."
|
||||||
msgstr "Match deleted successfully."
|
msgstr "Match deleted successfully."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:98
|
#: app/routes/team_matches.py:100
|
||||||
msgid "You do not have permission to schedule matches for this team."
|
msgid "You do not have permission to schedule matches for this team."
|
||||||
msgstr "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:140
|
#: app/routes/team_matches.py:142
|
||||||
msgid "Date is required."
|
msgid "Date is required."
|
||||||
msgstr "Date is required."
|
msgstr "Date is required."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:228
|
#: app/routes/team_matches.py:220
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team match \"%(title)s\" scheduled successfully!"
|
msgid "Team match \"%(title)s\" scheduled successfully!"
|
||||||
msgstr "Team match \"%(title)s\" scheduled successfully!"
|
msgstr "Team match \"%(title)s\" scheduled successfully!"
|
||||||
|
|
||||||
#: app/routes/team_matches.py:267
|
#: app/routes/team_matches.py:259
|
||||||
msgid "Invalid start time format."
|
msgid "Invalid start time format."
|
||||||
msgstr "Invalid start time format."
|
msgstr "Invalid start time format."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:275
|
#: app/routes/team_matches.py:267
|
||||||
msgid "Invalid end time format."
|
msgid "Invalid end time format."
|
||||||
msgstr "Invalid end time format."
|
msgstr "Invalid end time format."
|
||||||
|
|
||||||
#: app/routes/teams.py:38
|
#: app/routes/teams.py:40
|
||||||
msgid "Use My Team(s) to view your teams."
|
msgid "Use My Team(s) to view your teams."
|
||||||
msgstr "Use My Team(s) to view your teams."
|
msgstr "Use My Team(s) to view your teams."
|
||||||
|
|
||||||
#: app/routes/teams.py:41
|
#: app/routes/teams.py:43
|
||||||
msgid "You do not have permission to view teams."
|
msgid "You do not have permission to view teams."
|
||||||
msgstr "You do not have permission to view teams."
|
msgstr "You do not have permission to view teams."
|
||||||
|
|
||||||
#: app/routes/teams.py:68
|
#: app/routes/teams.py:70
|
||||||
msgid "This page is for players."
|
msgid "This page is for players."
|
||||||
msgstr "This page is for players."
|
msgstr "This page is for players."
|
||||||
|
|
||||||
#: app/routes/teams.py:121
|
#: app/routes/teams.py:123
|
||||||
msgid "You do not have permission to create teams."
|
msgid "You do not have permission to create teams."
|
||||||
msgstr "You do not have permission to create teams."
|
msgstr "You do not have permission to create teams."
|
||||||
|
|
||||||
#: app/routes/teams.py:129 app/routes/teams.py:174
|
#: app/routes/teams.py:131 app/routes/teams.py:176
|
||||||
msgid "Team name is required."
|
msgid "Team name is required."
|
||||||
msgstr "Team name is required."
|
msgstr "Team name is required."
|
||||||
|
|
||||||
#: app/routes/teams.py:134 app/routes/teams.py:179
|
#: app/routes/teams.py:136 app/routes/teams.py:181
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(name)s\" already exists."
|
msgid "Team \"%(name)s\" already exists."
|
||||||
msgstr "Team \"%(name)s\" already exists."
|
msgstr "Team \"%(name)s\" already exists."
|
||||||
|
|
||||||
#: app/routes/teams.py:156
|
#: app/routes/teams.py:158
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(name)s\" created successfully!"
|
msgid "Team \"%(name)s\" created successfully!"
|
||||||
msgstr "Team \"%(name)s\" created successfully!"
|
msgstr "Team \"%(name)s\" created successfully!"
|
||||||
|
|
||||||
#: app/routes/teams.py:166
|
#: app/routes/teams.py:168
|
||||||
msgid "You do not have permission to edit this team."
|
msgid "You do not have permission to edit this team."
|
||||||
msgstr "You do not have permission to edit this team."
|
msgstr "You do not have permission to edit this team."
|
||||||
|
|
||||||
#: app/routes/teams.py:217
|
#: app/routes/teams.py:219
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(name)s\" updated successfully!"
|
msgid "Team \"%(name)s\" updated successfully!"
|
||||||
msgstr "Team \"%(name)s\" updated successfully!"
|
msgstr "Team \"%(name)s\" updated successfully!"
|
||||||
|
|
||||||
#: app/routes/teams.py:226
|
#: app/routes/teams.py:228
|
||||||
msgid "You do not have permission to delete teams."
|
msgid "You do not have permission to delete teams."
|
||||||
msgstr "You do not have permission to delete teams."
|
msgstr "You do not have permission to delete teams."
|
||||||
|
|
||||||
#: app/routes/teams.py:258
|
#: app/routes/teams.py:260
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(name)s\" deleted successfully."
|
msgid "Team \"%(name)s\" deleted successfully."
|
||||||
msgstr "Team \"%(name)s\" deleted successfully."
|
msgstr "Team \"%(name)s\" deleted successfully."
|
||||||
|
|
||||||
#: app/routes/teams.py:273
|
#: app/routes/teams.py:275
|
||||||
msgid "Please select a coach."
|
msgid "Please select a coach."
|
||||||
msgstr "Please select a coach."
|
msgstr "Please select a coach."
|
||||||
|
|
||||||
#: app/routes/teams.py:278
|
#: app/routes/teams.py:280
|
||||||
msgid "Only coaches can be assigned as coach."
|
msgid "Only coaches can be assigned as coach."
|
||||||
msgstr "Only coaches can be assigned as coach."
|
msgstr "Only coaches can be assigned as coach."
|
||||||
|
|
||||||
#: app/routes/teams.py:284
|
#: app/routes/teams.py:286
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already a coach of %(name)s."
|
msgid "%(username)s is already a coach of %(name)s."
|
||||||
msgstr "%(username)s is already a coach of %(name)s."
|
msgstr "%(username)s is already a coach of %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:297
|
#: app/routes/teams.py:299
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s added as coach of %(name)s."
|
msgid "%(username)s added as coach of %(name)s."
|
||||||
msgstr "%(username)s added as coach of %(name)s."
|
msgstr "%(username)s added as coach of %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:314
|
#: app/routes/teams.py:316
|
||||||
msgid "Please select a manager."
|
msgid "Please select a manager."
|
||||||
msgstr "Please select a manager."
|
msgstr "Please select a manager."
|
||||||
|
|
||||||
#: app/routes/teams.py:319
|
#: app/routes/teams.py:321
|
||||||
msgid "Only managers can be assigned as manager."
|
msgid "Only managers can be assigned as manager."
|
||||||
msgstr "Only managers can be assigned as manager."
|
msgstr "Only managers can be assigned as manager."
|
||||||
|
|
||||||
#: app/routes/teams.py:325
|
#: app/routes/teams.py:327
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already a manager of %(name)s."
|
msgid "%(username)s is already a manager of %(name)s."
|
||||||
msgstr "%(username)s is already a manager of %(name)s."
|
msgstr "%(username)s is already a manager of %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:338
|
#: app/routes/teams.py:340
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s added as manager of %(name)s."
|
msgid "%(username)s added as manager of %(name)s."
|
||||||
msgstr "%(username)s added as manager of %(name)s."
|
msgstr "%(username)s added as manager of %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:365
|
#: app/routes/teams.py:367
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Coach removed from %(name)s."
|
msgid "Coach removed from %(name)s."
|
||||||
msgstr "Coach removed from %(name)s."
|
msgstr "Coach removed from %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:390
|
#: app/routes/teams.py:392
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Manager removed from %(name)s."
|
msgid "Manager removed from %(name)s."
|
||||||
msgstr "Manager removed from %(name)s."
|
msgstr "Manager removed from %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:406 app/routes/tryouts.py:512 app/routes/tryouts.py:613
|
#: app/routes/teams.py:408 app/routes/tryouts.py:545 app/routes/tryouts.py:646
|
||||||
msgid "Please select a player."
|
msgid "Please select a player."
|
||||||
msgstr "Please select a player."
|
msgstr "Please select a player."
|
||||||
|
|
||||||
#: app/routes/teams.py:411
|
#: app/routes/teams.py:413
|
||||||
msgid "Can only assign players to teams."
|
msgid "Can only assign players to teams."
|
||||||
msgstr "Can only assign players to teams."
|
msgstr "Can only assign players to teams."
|
||||||
|
|
||||||
#: app/routes/teams.py:417
|
#: app/routes/teams.py:419
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already on %(name)s."
|
msgid "%(username)s is already on %(name)s."
|
||||||
msgstr "%(username)s is already on %(name)s."
|
msgstr "%(username)s is already on %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:425
|
#: app/routes/teams.py:427
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s added to %(name)s!"
|
msgid "%(username)s added to %(name)s!"
|
||||||
msgstr "%(username)s added to %(name)s!"
|
msgstr "%(username)s added to %(name)s!"
|
||||||
|
|
||||||
#: app/routes/teams.py:442 app/routes/teams.py:515
|
#: app/routes/teams.py:444 app/routes/teams.py:517
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is not on %(name)s."
|
msgid "%(username)s is not on %(name)s."
|
||||||
msgstr "%(username)s is not on %(name)s."
|
msgstr "%(username)s is not on %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:450
|
#: app/routes/teams.py:452
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s removed from %(name)s."
|
msgid "%(username)s removed from %(name)s."
|
||||||
msgstr "%(username)s removed from %(name)s."
|
msgstr "%(username)s removed from %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:486 app/routes/teams.py:504
|
#: app/routes/teams.py:488 app/routes/teams.py:506
|
||||||
msgid "You do not have permission to add notes to this team."
|
msgid "You do not have permission to add notes to this team."
|
||||||
msgstr "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:494
|
#: app/routes/teams.py:496
|
||||||
msgid "Team notes added successfully!"
|
msgid "Team notes added successfully!"
|
||||||
msgstr "Team notes added successfully!"
|
msgstr "Team notes added successfully!"
|
||||||
|
|
||||||
#: app/routes/teams.py:509 app/routes/users.py:1530 app/routes/users.py:1573
|
#: app/routes/teams.py:511 app/routes/users/notes.py:207
|
||||||
|
#: app/routes/users/notes.py:250
|
||||||
msgid "Can only add notes for players."
|
msgid "Can only add notes for players."
|
||||||
msgstr "Can only add notes for players."
|
msgstr "Can only add notes for players."
|
||||||
|
|
||||||
#: app/routes/teams.py:525
|
#: app/routes/teams.py:527
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Note added for %(username)s!"
|
msgid "Note added for %(username)s!"
|
||||||
msgstr "Note added for %(username)s!"
|
msgstr "Note added for %(username)s!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:56
|
#: app/routes/tryouts.py:77
|
||||||
msgid "You do not have permission to create tryouts."
|
msgid "You do not have permission to create tryouts."
|
||||||
msgstr "You do not have permission to create tryouts."
|
msgstr "You do not have permission to create tryouts."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:82 app/routes/tryouts.py:189
|
#: app/routes/tryouts.py:103 app/routes/tryouts.py:210
|
||||||
msgid "Invalid start date format."
|
msgid "Invalid start date format."
|
||||||
msgstr "Invalid start date format."
|
msgstr "Invalid start date format."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:97 app/routes/tryouts.py:204
|
#: app/routes/tryouts.py:118 app/routes/tryouts.py:225
|
||||||
msgid "End date cannot be before start date."
|
msgid "End date cannot be before start date."
|
||||||
msgstr "End date cannot be before start date."
|
msgstr "End date cannot be before start date."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:107 app/routes/tryouts.py:214
|
#: app/routes/tryouts.py:128 app/routes/tryouts.py:235
|
||||||
msgid "Invalid end date format."
|
msgid "Invalid end date format."
|
||||||
msgstr "Invalid end date format."
|
msgstr "Invalid end date format."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:139
|
#: app/routes/tryouts.py:160
|
||||||
msgid "Tryout created successfully!"
|
msgid "Tryout created successfully!"
|
||||||
msgstr "Tryout created successfully!"
|
msgstr "Tryout created successfully!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:159
|
#: app/routes/tryouts.py:180
|
||||||
msgid "You do not have permission to edit this tryout."
|
msgid "You do not have permission to edit this tryout."
|
||||||
msgstr "You do not have permission to edit this tryout."
|
msgstr "You do not have permission to edit this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:163
|
#: app/routes/tryouts.py:184
|
||||||
msgid "This tryout has ended and can no longer be modified."
|
msgid "This tryout has ended and can no longer be modified."
|
||||||
msgstr "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:242
|
#: app/routes/tryouts.py:263
|
||||||
msgid "Tryout updated successfully!"
|
msgid "Tryout updated successfully!"
|
||||||
msgstr "Tryout updated successfully!"
|
msgstr "Tryout updated successfully!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:289
|
#: app/routes/tryouts.py:310
|
||||||
msgid "You do not have permission to view this tryout."
|
msgid "You do not have permission to view this tryout."
|
||||||
msgstr "You do not have permission to view this tryout."
|
msgstr "You do not have permission to view this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:439
|
#: app/routes/tryouts.py:472
|
||||||
msgid "Only players can register for tryouts."
|
msgid "Only players can register for tryouts."
|
||||||
msgstr "Only players can register for tryouts."
|
msgstr "Only players can register for tryouts."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:443
|
#: app/routes/tryouts.py:476
|
||||||
msgid "This tryout is not accepting registrations."
|
msgid "This tryout is not accepting registrations."
|
||||||
msgstr "This tryout is not accepting registrations."
|
msgstr "This tryout is not accepting registrations."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:450
|
#: app/routes/tryouts.py:483
|
||||||
msgid "You are already registered for this tryout."
|
msgid "You are already registered for this tryout."
|
||||||
msgstr "You are already registered for this tryout."
|
msgstr "You are already registered for this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:456 app/routes/tryouts.py:531
|
#: app/routes/tryouts.py:489 app/routes/tryouts.py:564
|
||||||
msgid "This tryout is full."
|
msgid "This tryout is full."
|
||||||
msgstr "This tryout is full."
|
msgstr "This tryout is full."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:462
|
#: app/routes/tryouts.py:495
|
||||||
msgid "Successfully registered for tryout!"
|
msgid "Successfully registered for tryout!"
|
||||||
msgstr "Successfully registered for tryout!"
|
msgstr "Successfully registered for tryout!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:478
|
#: app/routes/tryouts.py:511
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Tryout status updated to %(new_status)s."
|
msgid "Tryout status updated to %(new_status)s."
|
||||||
msgstr "Tryout status updated to %(new_status)s."
|
msgstr "Tryout status updated to %(new_status)s."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:498
|
#: app/routes/tryouts.py:531
|
||||||
msgid "Registration status updated."
|
msgid "Registration status updated."
|
||||||
msgstr "Registration status updated."
|
msgstr "Registration status updated."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:517
|
#: app/routes/tryouts.py:550
|
||||||
msgid "Can only register players."
|
msgid "Can only register players."
|
||||||
msgstr "Can only register players."
|
msgstr "Can only register players."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:523
|
#: app/routes/tryouts.py:556
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already registered for this tryout."
|
msgid "%(username)s is already registered for this tryout."
|
||||||
msgstr "%(username)s is already registered for this tryout."
|
msgstr "%(username)s is already registered for this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:537
|
#: app/routes/tryouts.py:570
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s registered for tryout!"
|
msgid "%(username)s registered for tryout!"
|
||||||
msgstr "%(username)s registered for tryout!"
|
msgstr "%(username)s registered for tryout!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:573
|
#: app/routes/tryouts.py:606
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s removed from tryout."
|
msgid "%(username)s removed from tryout."
|
||||||
msgstr "%(username)s removed from tryout."
|
msgstr "%(username)s removed from tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:591
|
#: app/routes/tryouts.py:624
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(team_name)s\" created!"
|
msgid "Team \"%(team_name)s\" created!"
|
||||||
msgstr "Team \"%(team_name)s\" created!"
|
msgstr "Team \"%(team_name)s\" created!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:622
|
#: app/routes/tryouts.py:655
|
||||||
msgid "That player is not registered for this tryout."
|
msgid "That player is not registered for this tryout."
|
||||||
msgstr "That player is not registered for this tryout."
|
msgstr "That player is not registered for this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:628
|
#: app/routes/tryouts.py:661
|
||||||
msgid "Player is already on this team."
|
msgid "Player is already on this team."
|
||||||
msgstr "Player is already on this team."
|
msgstr "Player is already on this team."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:633
|
#: app/routes/tryouts.py:666
|
||||||
msgid "Player added to team!"
|
msgid "Player added to team!"
|
||||||
msgstr "Player added to team!"
|
msgstr "Player added to team!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:643
|
#: app/routes/tryouts.py:676
|
||||||
msgid "You do not have permission to delete this tryout."
|
msgid "You do not have permission to delete this tryout."
|
||||||
msgstr "You do not have permission to delete this tryout."
|
msgstr "You do not have permission to delete this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:679
|
#: app/routes/tryouts.py:712
|
||||||
msgid "Tryout deleted successfully."
|
msgid "Tryout deleted successfully."
|
||||||
msgstr "Tryout deleted successfully."
|
msgstr "Tryout deleted successfully."
|
||||||
|
|
||||||
#: app/routes/users.py:83
|
#: app/routes/users/_shared.py:46
|
||||||
msgid "No file selected."
|
msgid "No file selected."
|
||||||
msgstr "No file selected."
|
msgstr "No file selected."
|
||||||
|
|
||||||
#: app/routes/users.py:87
|
#: app/routes/users/_shared.py:50
|
||||||
msgid "Only PDF files are allowed for contracts."
|
msgid "Only PDF files are allowed for contracts."
|
||||||
msgstr "Only PDF files are allowed for contracts."
|
msgstr "Only PDF files are allowed for contracts."
|
||||||
|
|
||||||
#: app/routes/users.py:92
|
#: app/routes/users/_shared.py:55
|
||||||
msgid "That file is not a PDF, whatever its name says."
|
msgid "That file is not a PDF, whatever its name says."
|
||||||
msgstr "That file is not a PDF, whatever its name says."
|
msgstr "That file is not a PDF, whatever its name says."
|
||||||
|
|
||||||
#: app/routes/users.py:181
|
#: app/routes/users/accounts.py:53
|
||||||
msgid "Only the president can manage users."
|
msgid "Only the president can manage users."
|
||||||
msgstr "Only the president can manage users."
|
msgstr "Only the president can manage users."
|
||||||
|
|
||||||
#: app/routes/users.py:193
|
#: app/routes/users/accounts.py:65
|
||||||
msgid "Only the president can edit users."
|
msgid "Only the president can edit users."
|
||||||
msgstr "Only the president can edit users."
|
msgstr "Only the president can edit users."
|
||||||
|
|
||||||
#: app/routes/users.py:234
|
#: app/routes/users/accounts.py:106
|
||||||
msgid "Email already in use by another account."
|
msgid "Email already in use by another account."
|
||||||
msgstr "Email already in use by another account."
|
msgstr "Email already in use by another account."
|
||||||
|
|
||||||
#: app/routes/users.py:245
|
#: app/routes/users/accounts.py:117
|
||||||
msgid "You cannot change your own role. Ask another president to do it."
|
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."
|
msgstr "You cannot change your own role. Ask another president to do it."
|
||||||
|
|
||||||
#: app/routes/users.py:258
|
#: app/routes/users/accounts.py:130
|
||||||
msgid ""
|
msgid ""
|
||||||
"This is the last active president. Promote another account before "
|
"This is the last active president. Promote another account before "
|
||||||
"changing this one."
|
"changing this one."
|
||||||
@@ -570,175 +571,176 @@ msgstr ""
|
|||||||
"This is the last active president. Promote another account before "
|
"This is the last active president. Promote another account before "
|
||||||
"changing this one."
|
"changing this one."
|
||||||
|
|
||||||
#: app/routes/users.py:337
|
#: app/routes/users/accounts.py:209
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "User %(username)s updated successfully!"
|
msgid "User %(username)s updated successfully!"
|
||||||
msgstr "User %(username)s updated successfully!"
|
msgstr "User %(username)s updated successfully!"
|
||||||
|
|
||||||
#: app/routes/users.py:358
|
#: app/routes/users/accounts.py:230
|
||||||
msgid "Only the president can delete users."
|
msgid "Only the president can delete users."
|
||||||
msgstr "Only the president can delete users."
|
msgstr "Only the president can delete users."
|
||||||
|
|
||||||
#: app/routes/users.py:362
|
#: app/routes/users/accounts.py:234
|
||||||
msgid "You cannot delete your own account."
|
msgid "You cannot delete your own account."
|
||||||
msgstr "You cannot delete your own account."
|
msgstr "You cannot delete your own account."
|
||||||
|
|
||||||
#: app/routes/users.py:405
|
#: app/routes/users/accounts.py:277
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "User %(deleted_username)s has been removed."
|
msgid "User %(deleted_username)s has been removed."
|
||||||
msgstr "User %(deleted_username)s has been removed."
|
msgstr "User %(deleted_username)s has been removed."
|
||||||
|
|
||||||
#: app/routes/users.py:416
|
#: app/routes/users/accounts.py:288
|
||||||
msgid "Only the president can create users."
|
msgid "Only the president can create users."
|
||||||
msgstr "Only the president can create users."
|
msgstr "Only the president can create users."
|
||||||
|
|
||||||
#: app/routes/users.py:464
|
#: app/routes/users/accounts.py:336
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "User %(full_name)s created as %(role)s!"
|
msgid "User %(full_name)s created as %(role)s!"
|
||||||
msgstr "User %(full_name)s created as %(role)s!"
|
msgstr "User %(full_name)s created as %(role)s!"
|
||||||
|
|
||||||
#: app/routes/users.py:534
|
#: app/routes/users/availability.py:179
|
||||||
msgid "Username already taken."
|
msgid "Only coaches can manage availability."
|
||||||
msgstr "Username already taken."
|
msgstr "Only coaches can manage availability."
|
||||||
|
|
||||||
#: app/routes/users.py:544
|
#: app/routes/users/contracts.py:83
|
||||||
msgid "Email already in use."
|
|
||||||
msgstr "Email already in use."
|
|
||||||
|
|
||||||
#: app/routes/users.py:574
|
|
||||||
msgid "Profile updated successfully!"
|
|
||||||
msgstr "Profile updated successfully!"
|
|
||||||
|
|
||||||
#: app/routes/users.py:809
|
|
||||||
msgid "Only presidents, managers, and coaches can upload contracts."
|
msgid "Only presidents, managers, and coaches can upload contracts."
|
||||||
msgstr "Only presidents, managers, and coaches can upload contracts."
|
msgstr "Only presidents, managers, and coaches can upload contracts."
|
||||||
|
|
||||||
#: app/routes/users.py:828
|
#: app/routes/users/contracts.py:102
|
||||||
msgid "You do not have permission to upload a contract for this player."
|
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."
|
msgstr "You do not have permission to upload a contract for this player."
|
||||||
|
|
||||||
#: app/routes/users.py:869
|
#: app/routes/users/contracts.py:143
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Contract uploaded successfully for %(username)s!"
|
msgid "Contract uploaded successfully for %(username)s!"
|
||||||
msgstr "Contract uploaded successfully for %(username)s!"
|
msgstr "Contract uploaded successfully for %(username)s!"
|
||||||
|
|
||||||
#: app/routes/users.py:883
|
#: app/routes/users/contracts.py:157
|
||||||
msgid "Only the player can upload their signed contract."
|
msgid "Only the player can upload their signed contract."
|
||||||
msgstr "Only the player can upload their signed contract."
|
msgstr "Only the player can upload their signed contract."
|
||||||
|
|
||||||
#: app/routes/users.py:902
|
#: app/routes/users/contracts.py:176
|
||||||
msgid "Signed contract uploaded successfully!"
|
msgid "Signed contract uploaded successfully!"
|
||||||
msgstr "Signed contract uploaded successfully!"
|
msgstr "Signed contract uploaded successfully!"
|
||||||
|
|
||||||
#: app/routes/users.py:912 app/routes/users.py:925
|
#: app/routes/users/contracts.py:186 app/routes/users/contracts.py:199
|
||||||
msgid "You do not have permission to download this contract."
|
msgid "You do not have permission to download this contract."
|
||||||
msgstr "You do not have permission to download this contract."
|
msgstr "You do not have permission to download this contract."
|
||||||
|
|
||||||
#: app/routes/users.py:928
|
#: app/routes/users/contracts.py:202
|
||||||
msgid "No signed contract available."
|
msgid "No signed contract available."
|
||||||
msgstr "No signed contract available."
|
msgstr "No signed contract available."
|
||||||
|
|
||||||
#: app/routes/users.py:1034
|
#: app/routes/users/notes.py:34
|
||||||
msgid "Only players can request One on One sessions."
|
|
||||||
msgstr "Only players can request One on One sessions."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1047
|
|
||||||
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:1082
|
|
||||||
msgid "Cannot request One on One - no coach assigned."
|
|
||||||
msgstr "Cannot request One on One - no coach assigned."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1090
|
|
||||||
msgid "Invalid date or time format."
|
|
||||||
msgstr "Invalid date or time format."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1104
|
|
||||||
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:1132
|
|
||||||
msgid "Your One on One request has been submitted!"
|
|
||||||
msgstr "Your One on One request has been submitted!"
|
|
||||||
|
|
||||||
#: app/routes/users.py:1176
|
|
||||||
msgid "Only coaches can accept One on One requests."
|
|
||||||
msgstr "Only coaches can accept One on One requests."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1182 app/routes/users.py:1232
|
|
||||||
msgid "This request is not for you."
|
|
||||||
msgstr "This request is not for you."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1186 app/routes/users.py:1236
|
|
||||||
msgid "This request has already been processed."
|
|
||||||
msgstr "This request has already been processed."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1213
|
|
||||||
#, 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:1226
|
|
||||||
msgid "Only coaches can reject One on One requests."
|
|
||||||
msgstr "Only coaches can reject One on One requests."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1268
|
|
||||||
#, 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:1286
|
|
||||||
msgid "This page is for players only."
|
msgid "This page is for players only."
|
||||||
msgstr "This page is for players only."
|
msgstr "This page is for players only."
|
||||||
|
|
||||||
#: app/routes/users.py:1328
|
#: app/routes/users/notes.py:71
|
||||||
msgid "Only coaches can manage availability."
|
|
||||||
msgstr "Only coaches can manage availability."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1394
|
|
||||||
msgid "Only coaches can access the notes dashboard."
|
msgid "Only coaches can access the notes dashboard."
|
||||||
msgstr "Only coaches can access the notes dashboard."
|
msgstr "Only coaches can access the notes dashboard."
|
||||||
|
|
||||||
#: app/routes/users.py:1484
|
#: app/routes/users/notes.py:161
|
||||||
msgid "Only coaches can manage team notes."
|
msgid "Only coaches can manage team notes."
|
||||||
msgstr "Only coaches can manage team notes."
|
msgstr "Only coaches can manage team notes."
|
||||||
|
|
||||||
#: app/routes/users.py:1490
|
#: app/routes/users/notes.py:167
|
||||||
msgid "You are not assigned to a team."
|
msgid "You are not assigned to a team."
|
||||||
msgstr "You are not assigned to a team."
|
msgstr "You are not assigned to a team."
|
||||||
|
|
||||||
#: app/routes/users.py:1503
|
#: app/routes/users/notes.py:180
|
||||||
msgid "Team notes saved successfully!"
|
msgid "Team notes saved successfully!"
|
||||||
msgstr "Team notes saved successfully!"
|
msgstr "Team notes saved successfully!"
|
||||||
|
|
||||||
#: app/routes/users.py:1518
|
#: app/routes/users/notes.py:195
|
||||||
msgid "Only coaches can manage personal notes."
|
msgid "Only coaches can manage personal notes."
|
||||||
msgstr "Only coaches can manage personal notes."
|
msgstr "Only coaches can manage personal notes."
|
||||||
|
|
||||||
#: app/routes/users.py:1525 app/routes/users.py:1568 app/routes/users.py:1619
|
#: app/routes/users/notes.py:202 app/routes/users/notes.py:245
|
||||||
#: app/routes/users.py:1673
|
#: app/routes/users/notes.py:296 app/routes/users/notes.py:350
|
||||||
msgid "Player and content are required."
|
msgid "Player and content are required."
|
||||||
msgstr "Player and content are required."
|
msgstr "Player and content are required."
|
||||||
|
|
||||||
#: app/routes/users.py:1534 app/routes/users.py:1577 app/routes/users.py:1623
|
#: app/routes/users/notes.py:211 app/routes/users/notes.py:254
|
||||||
#: app/routes/users.py:1677
|
#: app/routes/users/notes.py:300 app/routes/users/notes.py:354
|
||||||
msgid "You can only write notes about players you work with."
|
msgid "You can only write notes about players you work with."
|
||||||
msgstr "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:1544 app/routes/users.py:1590
|
#: app/routes/users/notes.py:221 app/routes/users/notes.py:267
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Note added for %(username)s."
|
msgid "Note added for %(username)s."
|
||||||
msgstr "Note added for %(username)s."
|
msgstr "Note added for %(username)s."
|
||||||
|
|
||||||
#: app/routes/users.py:1558 app/routes/users.py:1604 app/routes/users.py:1657
|
#: app/routes/users/notes.py:235 app/routes/users/notes.py:281
|
||||||
|
#: app/routes/users/notes.py:334
|
||||||
msgid "Only coaches can add personal notes."
|
msgid "Only coaches can add personal notes."
|
||||||
msgstr "Only coaches can add personal notes."
|
msgstr "Only coaches can add personal notes."
|
||||||
|
|
||||||
#: app/routes/users.py:1634 app/routes/users.py:1688
|
#: app/routes/users/notes.py:311 app/routes/users/notes.py:365
|
||||||
msgid "Note added successfully."
|
msgid "Note added successfully."
|
||||||
msgstr "Note added successfully."
|
msgstr "Note added successfully."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:20
|
||||||
|
msgid "Only players can request One on One sessions."
|
||||||
|
msgstr "Only players can request One on One sessions."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:33
|
||||||
|
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/one_on_one.py:68
|
||||||
|
msgid "Cannot request One on One - no coach assigned."
|
||||||
|
msgstr "Cannot request One on One - no coach assigned."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:76
|
||||||
|
msgid "Invalid date or time format."
|
||||||
|
msgstr "Invalid date or time format."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:90
|
||||||
|
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/one_on_one.py:118
|
||||||
|
msgid "Your One on One request has been submitted!"
|
||||||
|
msgstr "Your One on One request has been submitted!"
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:163
|
||||||
|
msgid "Only coaches can accept One on One requests."
|
||||||
|
msgstr "Only coaches can accept One on One requests."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:169 app/routes/users/one_on_one.py:219
|
||||||
|
msgid "This request is not for you."
|
||||||
|
msgstr "This request is not for you."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:173 app/routes/users/one_on_one.py:223
|
||||||
|
msgid "This request has already been processed."
|
||||||
|
msgstr "This request has already been processed."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:200
|
||||||
|
#, 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/one_on_one.py:213
|
||||||
|
msgid "Only coaches can reject One on One requests."
|
||||||
|
msgstr "Only coaches can reject One on One requests."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:255
|
||||||
|
#, 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/profile.py:83
|
||||||
|
msgid "Username already taken."
|
||||||
|
msgstr "Username already taken."
|
||||||
|
|
||||||
|
#: app/routes/users/profile.py:93
|
||||||
|
msgid "Email already in use."
|
||||||
|
msgstr "Email already in use."
|
||||||
|
|
||||||
|
#: app/routes/users/profile.py:123
|
||||||
|
msgid "Profile updated successfully!"
|
||||||
|
msgstr "Profile updated successfully!"
|
||||||
|
|
||||||
#: app/templates/errors/400.html:2
|
#: app/templates/errors/400.html:2
|
||||||
msgid "400 Bad Request"
|
msgid "400 Bad Request"
|
||||||
msgstr "400 Bad Request"
|
msgstr "400 Bad Request"
|
||||||
@@ -2372,11 +2374,7 @@ msgstr "Confirm Password"
|
|||||||
msgid "Confirm your password"
|
msgid "Confirm your password"
|
||||||
msgstr "Confirm your password"
|
msgstr "Confirm your password"
|
||||||
|
|
||||||
#: app/templates/pages/register.html:151
|
#: app/templates/pages/register.html:159
|
||||||
msgid "Answer"
|
|
||||||
msgstr "Answer"
|
|
||||||
|
|
||||||
#: app/templates/pages/register.html:154
|
|
||||||
msgid "Create Account"
|
msgid "Create Account"
|
||||||
msgstr "Create Account"
|
msgstr "Create Account"
|
||||||
|
|
||||||
@@ -2876,3 +2874,9 @@ msgstr "View Profile"
|
|||||||
#~ msgid "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout."
|
#~ msgid "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout."
|
||||||
#~ msgstr "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout."
|
#~ msgstr "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout."
|
||||||
|
|
||||||
|
#~ msgid "Incorrect CAPTCHA answer. Please try again."
|
||||||
|
#~ msgstr "Incorrect CAPTCHA answer. Please try again."
|
||||||
|
|
||||||
|
#~ msgid "Answer"
|
||||||
|
#~ msgstr "Answer"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: team-tryouts VERSION\n"
|
"Project-Id-Version: team-tryouts VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-08-08 15:57-0400\n"
|
"POT-Creation-Date: 2026-08-11 12:05-0400\n"
|
||||||
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
@@ -19,7 +19,7 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
|
|
||||||
#: app/validators.py:49
|
#: app/validators.py:50
|
||||||
msgid ""
|
msgid ""
|
||||||
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
||||||
"number."
|
"number."
|
||||||
@@ -27,97 +27,97 @@ msgstr ""
|
|||||||
"Le mot de passe doit compter au moins 8 caractères, dont une majuscule, "
|
"Le mot de passe doit compter au moins 8 caractères, dont une majuscule, "
|
||||||
"une minuscule et un chiffre."
|
"une minuscule et un chiffre."
|
||||||
|
|
||||||
#: app/validators.py:67
|
#: app/validators.py:68
|
||||||
msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
|
msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Le nom d’utilisateur doit compter de 3 à 30 caractères (lettres, "
|
"Le nom d’utilisateur doit compter de 3 à 30 caractères (lettres, "
|
||||||
"chiffres, tiret bas, trait d’union)."
|
"chiffres, tiret bas, trait d’union)."
|
||||||
|
|
||||||
#: app/validators.py:86
|
#: app/validators.py:87
|
||||||
msgid "Invalid Discord username format."
|
msgid "Invalid Discord username format."
|
||||||
msgstr "Format de nom d’utilisateur Discord invalide."
|
msgstr "Format de nom d’utilisateur Discord invalide."
|
||||||
|
|
||||||
#: app/validators.py:103
|
#: app/validators.py:104
|
||||||
msgid "Discord User ID must be a 17-20 digit number."
|
msgid "Discord User ID must be a 17-20 digit number."
|
||||||
msgstr "L’identifiant Discord doit être un nombre de 17 à 20 chiffres."
|
msgstr "L’identifiant Discord doit être un nombre de 17 à 20 chiffres."
|
||||||
|
|
||||||
#: app/validators.py:121
|
#: app/validators.py:122
|
||||||
msgid "Invalid phone number format."
|
msgid "Invalid phone number format."
|
||||||
msgstr "Format de numéro de téléphone invalide."
|
msgstr "Format de numéro de téléphone invalide."
|
||||||
|
|
||||||
#: app/validators.py:163
|
#: app/validators.py:164
|
||||||
msgid "Username is required."
|
msgid "Username is required."
|
||||||
msgstr "Le nom d’utilisateur est obligatoire."
|
msgstr "Le nom d’utilisateur est obligatoire."
|
||||||
|
|
||||||
#: app/validators.py:167
|
#: app/validators.py:168
|
||||||
msgid "Password is required."
|
msgid "Password is required."
|
||||||
msgstr "Le mot de passe est obligatoire."
|
msgstr "Le mot de passe est obligatoire."
|
||||||
|
|
||||||
#: app/validators.py:189 app/validators.py:261
|
#: app/validators.py:190 app/validators.py:262
|
||||||
msgid "Username must be 3-80 characters."
|
msgid "Username must be 3-80 characters."
|
||||||
msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères."
|
msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères."
|
||||||
|
|
||||||
#: app/validators.py:195
|
#: app/validators.py:196
|
||||||
msgid "Email must be 120 characters or less."
|
msgid "Email must be 120 characters or less."
|
||||||
msgstr "L’adresse courriel ne doit pas dépasser 120 caractères."
|
msgstr "L’adresse courriel ne doit pas dépasser 120 caractères."
|
||||||
|
|
||||||
#: app/validators.py:208 app/validators.py:276 app/validators.py:307
|
#: app/validators.py:209 app/validators.py:277 app/validators.py:308
|
||||||
#: app/validators.py:371
|
#: app/validators.py:372
|
||||||
msgid "Full name is required."
|
msgid "Full name is required."
|
||||||
msgstr "Le nom complet est obligatoire."
|
msgstr "Le nom complet est obligatoire."
|
||||||
|
|
||||||
#: app/validators.py:243
|
#: app/validators.py:244
|
||||||
msgid "Passwords do not match."
|
msgid "Passwords do not match."
|
||||||
msgstr "Les mots de passe ne concordent pas."
|
msgstr "Les mots de passe ne concordent pas."
|
||||||
|
|
||||||
#: app/validators.py:280 app/validators.py:315
|
#: app/validators.py:281 app/validators.py:316
|
||||||
msgid "Invalid role selected."
|
msgid "Invalid role selected."
|
||||||
msgstr "Rôle sélectionné invalide."
|
msgstr "Rôle sélectionné invalide."
|
||||||
|
|
||||||
#: app/validators.py:416
|
#: app/validators.py:417
|
||||||
msgid "Player must be selected."
|
msgid "Player must be selected."
|
||||||
msgstr "Vous devez choisir un joueur."
|
msgstr "Vous devez choisir un joueur."
|
||||||
|
|
||||||
#: app/validators.py:419
|
#: app/validators.py:420
|
||||||
msgid "Notes must be 2000 characters or less."
|
msgid "Notes must be 2000 characters or less."
|
||||||
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
|
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
|
||||||
|
|
||||||
#: app/validators.py:438
|
#: app/validators.py:439
|
||||||
msgid "Date must be in YYYY-MM-DD format."
|
msgid "Date must be in YYYY-MM-DD format."
|
||||||
msgstr "La date doit être au format AAAA-MM-JJ."
|
msgstr "La date doit être au format AAAA-MM-JJ."
|
||||||
|
|
||||||
#: app/validators.py:443 app/validators.py:470
|
#: app/validators.py:444 app/validators.py:471
|
||||||
msgid "Start time must be in HH:MM format."
|
msgid "Start time must be in HH:MM format."
|
||||||
msgstr "L’heure de début doit être au format HH:MM."
|
msgstr "L’heure de début doit être au format HH:MM."
|
||||||
|
|
||||||
#: app/validators.py:447
|
#: app/validators.py:448
|
||||||
msgid "End time must be in HH:MM format."
|
msgid "End time must be in HH:MM format."
|
||||||
msgstr "L’heure de fin doit être au format HH:MM."
|
msgstr "L’heure de fin doit être au format HH:MM."
|
||||||
|
|
||||||
#: app/validators.py:450
|
#: app/validators.py:451
|
||||||
msgid "Points must be 2000 characters or less."
|
msgid "Points must be 2000 characters or less."
|
||||||
msgstr "Les points ne doivent pas dépasser 2000 caractères."
|
msgstr "Les points ne doivent pas dépasser 2000 caractères."
|
||||||
|
|
||||||
#: app/validators.py:466
|
#: app/validators.py:467
|
||||||
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
||||||
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
|
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
|
||||||
|
|
||||||
#: app/routes/auth.py:186 app/routes/auth.py:322 app/routes/users.py:106
|
#: app/routes/auth.py:224 app/routes/auth.py:374 app/routes/users/_shared.py:64
|
||||||
#: app/routes/users.py:821
|
#: app/routes/users/contracts.py:95
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(field)s: %(msg)s"
|
msgid "%(field)s: %(msg)s"
|
||||||
msgstr "%(field)s : %(msg)s"
|
msgstr "%(field)s : %(msg)s"
|
||||||
|
|
||||||
#: app/routes/auth.py:203
|
#: app/routes/auth.py:241
|
||||||
msgid "This account has been deactivated."
|
msgid "This account has been deactivated."
|
||||||
msgstr "Ce compte a été désactivé."
|
msgstr "Ce compte a été désactivé."
|
||||||
|
|
||||||
#: app/routes/auth.py:238
|
#: app/routes/auth.py:276
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Welcome back, %(username)s!"
|
msgid "Welcome back, %(username)s!"
|
||||||
msgstr "Bon retour, %(username)s !"
|
msgstr "Bon retour, %(username)s !"
|
||||||
|
|
||||||
#: app/routes/auth.py:268
|
#: app/routes/auth.py:306
|
||||||
msgid ""
|
msgid ""
|
||||||
"Login unsuccessful. Please check your username and password, or ask a "
|
"Login unsuccessful. Please check your username and password, or ask a "
|
||||||
"president for help."
|
"president for help."
|
||||||
@@ -125,27 +125,27 @@ msgstr ""
|
|||||||
"Échec de la connexion. Vérifiez le nom d’utilisateur et le mot de passe, "
|
"Échec de la connexion. Vérifiez le nom d’utilisateur et le mot de passe, "
|
||||||
"ou demandez de l’aide à un président."
|
"ou demandez de l’aide à un président."
|
||||||
|
|
||||||
#: app/routes/auth.py:303
|
#: app/routes/auth.py:363
|
||||||
msgid "Incorrect CAPTCHA answer. Please try again."
|
msgid "Your registration could not be processed. Please try again."
|
||||||
msgstr "Réponse au CAPTCHA incorrecte. Veuillez réessayer."
|
msgstr "Votre inscription n'a pas pu être traitée. Veuillez réessayer."
|
||||||
|
|
||||||
#: app/routes/auth.py:345 app/routes/users.py:436
|
#: app/routes/auth.py:388 app/routes/users/accounts.py:308
|
||||||
msgid "Username already exists."
|
msgid "Username already exists."
|
||||||
msgstr "Ce nom d’utilisateur est déjà pris."
|
msgstr "Ce nom d’utilisateur est déjà pris."
|
||||||
|
|
||||||
#: app/routes/auth.py:357 app/routes/users.py:440
|
#: app/routes/auth.py:392 app/routes/users/accounts.py:312
|
||||||
msgid "Email already registered."
|
msgid "Email already registered."
|
||||||
msgstr "Cette adresse courriel est déjà enregistrée."
|
msgstr "Cette adresse courriel est déjà enregistrée."
|
||||||
|
|
||||||
#: app/routes/auth.py:404
|
#: app/routes/auth.py:436
|
||||||
msgid "Your account has been created! You can now log in."
|
msgid "Your account has been created! You can now log in."
|
||||||
msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter."
|
msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter."
|
||||||
|
|
||||||
#: app/routes/auth.py:430
|
#: app/routes/auth.py:457
|
||||||
msgid "Discord OAuth2 is not configured."
|
msgid "Discord OAuth2 is not configured."
|
||||||
msgstr "La connexion Discord n’est pas configurée."
|
msgstr "La connexion Discord n’est pas configurée."
|
||||||
|
|
||||||
#: app/routes/auth.py:471
|
#: app/routes/auth.py:498
|
||||||
msgid ""
|
msgid ""
|
||||||
"Discord authorization could not be verified. Please start the connection "
|
"Discord authorization could not be verified. Please start the connection "
|
||||||
"again from this page."
|
"again from this page."
|
||||||
@@ -153,422 +153,423 @@ msgstr ""
|
|||||||
"L’autorisation Discord n’a pas pu être vérifiée. Relancez la connexion "
|
"L’autorisation Discord n’a pas pu être vérifiée. Relancez la connexion "
|
||||||
"depuis cette page."
|
"depuis cette page."
|
||||||
|
|
||||||
#: app/routes/auth.py:480
|
#: app/routes/auth.py:507
|
||||||
msgid "Discord authorization failed. No code received."
|
msgid "Discord authorization failed. No code received."
|
||||||
msgstr "L’autorisation Discord a échoué : aucun code reçu."
|
msgstr "L’autorisation Discord a échoué : aucun code reçu."
|
||||||
|
|
||||||
#: app/routes/auth.py:504
|
#: app/routes/auth.py:531
|
||||||
msgid "Failed to connect to Discord. Please try again."
|
msgid "Failed to connect to Discord. Please try again."
|
||||||
msgstr "Impossible de joindre Discord. Veuillez réessayer."
|
msgstr "Impossible de joindre Discord. Veuillez réessayer."
|
||||||
|
|
||||||
#: app/routes/auth.py:508
|
#: app/routes/auth.py:535
|
||||||
msgid "Failed to obtain Discord access token."
|
msgid "Failed to obtain Discord access token."
|
||||||
msgstr "Impossible d’obtenir le jeton d’accès Discord."
|
msgstr "Impossible d’obtenir le jeton d’accès Discord."
|
||||||
|
|
||||||
#: app/routes/auth.py:523
|
#: app/routes/auth.py:550
|
||||||
msgid "Failed to fetch Discord user profile."
|
msgid "Failed to fetch Discord user profile."
|
||||||
msgstr "Impossible de récupérer le profil Discord."
|
msgstr "Impossible de récupérer le profil Discord."
|
||||||
|
|
||||||
#: app/routes/auth.py:570
|
#: app/routes/auth.py:597
|
||||||
msgid "Discord account connected! Your profile has been pre-filled."
|
msgid "Discord account connected! Your profile has been pre-filled."
|
||||||
msgstr "Compte Discord connecté. Votre profil a été pré-rempli."
|
msgstr "Compte Discord connecté. Votre profil a été pré-rempli."
|
||||||
|
|
||||||
#: app/routes/auth.py:598
|
#: app/routes/auth.py:625
|
||||||
msgid "You have been logged out."
|
msgid "You have been logged out."
|
||||||
msgstr "Vous avez été déconnecté."
|
msgstr "Vous avez été déconnecté."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:45
|
#: app/routes/evaluations.py:46
|
||||||
msgid "You do not have permission to view evaluations."
|
msgid "You do not have permission to view evaluations."
|
||||||
msgstr "Vous n’avez pas les droits pour consulter les évaluations."
|
msgstr "Vous n’avez pas les droits pour consulter les évaluations."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:135
|
#: app/routes/evaluations.py:136
|
||||||
msgid "You do not have permission to evaluate players."
|
msgid "You do not have permission to evaluate players."
|
||||||
msgstr "Vous n’avez pas les droits pour évaluer des joueurs."
|
msgstr "Vous n’avez pas les droits pour évaluer des joueurs."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:140 app/routes/evaluations.py:263
|
#: app/routes/evaluations.py:141 app/routes/evaluations.py:264
|
||||||
msgid "You do not have permission to evaluate players in this tryout."
|
msgid "You do not have permission to evaluate players in this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour évaluer des joueurs dans cette sélection."
|
msgstr "Vous n’avez pas les droits pour évaluer des joueurs dans cette sélection."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:151
|
#: app/routes/evaluations.py:152
|
||||||
msgid "Player is not registered for this tryout."
|
msgid "Player is not registered for this tryout."
|
||||||
msgstr "Ce joueur n’est pas inscrit à cette sélection."
|
msgstr "Ce joueur n’est pas inscrit à cette sélection."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:156
|
#: app/routes/evaluations.py:157
|
||||||
msgid "Can only evaluate players."
|
msgid "Can only evaluate players."
|
||||||
msgstr "Seuls des joueurs peuvent être évalués."
|
msgstr "Seuls des joueurs peuvent être évalués."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:208
|
#: app/routes/evaluations.py:209
|
||||||
msgid "Evaluation updated!"
|
msgid "Evaluation updated!"
|
||||||
msgstr "Évaluation mise à jour."
|
msgstr "Évaluation mise à jour."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:228
|
#: app/routes/evaluations.py:229
|
||||||
msgid "Evaluation submitted successfully!"
|
msgid "Evaluation submitted successfully!"
|
||||||
msgstr "Évaluation enregistrée."
|
msgstr "Évaluation enregistrée."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:258 app/routes/teams.py:268
|
#: app/routes/evaluations.py:259 app/routes/teams.py:270
|
||||||
#: app/routes/teams.py:309 app/routes/teams.py:350 app/routes/teams.py:375
|
#: app/routes/teams.py:311 app/routes/teams.py:352 app/routes/teams.py:377
|
||||||
#: app/routes/teams.py:400 app/routes/teams.py:435 app/routes/tryouts.py:472
|
#: app/routes/teams.py:402 app/routes/teams.py:437 app/routes/tryouts.py:505
|
||||||
#: app/routes/tryouts.py:488 app/routes/tryouts.py:508
|
#: app/routes/tryouts.py:521 app/routes/tryouts.py:541
|
||||||
#: app/routes/tryouts.py:547 app/routes/tryouts.py:583
|
#: app/routes/tryouts.py:580 app/routes/tryouts.py:616
|
||||||
#: app/routes/tryouts.py:602
|
#: app/routes/tryouts.py:635
|
||||||
msgid "Permission denied."
|
msgid "Permission denied."
|
||||||
msgstr "Accès refusé."
|
msgstr "Accès refusé."
|
||||||
|
|
||||||
#: app/routes/main.py:53
|
#: app/routes/main.py:55
|
||||||
msgid "That language is not available."
|
msgid "That language is not available."
|
||||||
msgstr "Cette langue n’est pas disponible."
|
msgstr "Cette langue n’est pas disponible."
|
||||||
|
|
||||||
#: app/routes/matches.py:245
|
#: app/routes/matches.py:307
|
||||||
msgid "You do not have permission to schedule matches for this tryout."
|
msgid "You do not have permission to schedule matches for this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette sélection."
|
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette sélection."
|
||||||
|
|
||||||
#: app/routes/matches.py:249 app/routes/matches.py:422
|
#: app/routes/matches.py:311 app/routes/matches.py:473
|
||||||
msgid "This tryout has ended. Matches can no longer be created or modified."
|
msgid "This tryout has ended. Matches can no longer be created or modified."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Cette sélection est terminée. Les matchs ne peuvent plus être créés ni "
|
"Cette sélection est terminée. Les matchs ne peuvent plus être créés ni "
|
||||||
"modifiés."
|
"modifiés."
|
||||||
|
|
||||||
#: app/routes/matches.py:270
|
#: app/routes/matches.py:332
|
||||||
msgid "Start time is required. Please select a time slot."
|
msgid "Start time is required. Please select a time slot."
|
||||||
msgstr "L’heure de début est obligatoire. Choisissez une plage horaire."
|
msgstr "L’heure de début est obligatoire. Choisissez une plage horaire."
|
||||||
|
|
||||||
#: app/routes/matches.py:282 app/routes/matches.py:445
|
#: app/routes/matches.py:344 app/routes/matches.py:496
|
||||||
#: app/routes/team_matches.py:151 app/routes/team_matches.py:255
|
#: app/routes/team_matches.py:153 app/routes/team_matches.py:247
|
||||||
msgid "Invalid date format."
|
msgid "Invalid date format."
|
||||||
msgstr "Format de date invalide."
|
msgstr "Format de date invalide."
|
||||||
|
|
||||||
#: app/routes/matches.py:302 app/routes/team_matches.py:172
|
#: app/routes/matches.py:364 app/routes/team_matches.py:174
|
||||||
msgid "Invalid time format."
|
msgid "Invalid time format."
|
||||||
msgstr "Format d’heure invalide."
|
msgstr "Format d’heure invalide."
|
||||||
|
|
||||||
#: app/routes/matches.py:398
|
#: app/routes/matches.py:449
|
||||||
msgid "Match scheduled successfully!"
|
msgid "Match scheduled successfully!"
|
||||||
msgstr "Match planifié."
|
msgstr "Match planifié."
|
||||||
|
|
||||||
#: app/routes/matches.py:418 app/routes/team_matches.py:242
|
#: app/routes/matches.py:469 app/routes/team_matches.py:234
|
||||||
msgid "You do not have permission to edit this match."
|
msgid "You do not have permission to edit this match."
|
||||||
msgstr "Vous n’avez pas les droits pour modifier ce match."
|
msgstr "Vous n’avez pas les droits pour modifier ce match."
|
||||||
|
|
||||||
#: app/routes/matches.py:456
|
#: app/routes/matches.py:507
|
||||||
msgid "Start time is required."
|
msgid "Start time is required."
|
||||||
msgstr "L’heure de début est obligatoire."
|
msgstr "L’heure de début est obligatoire."
|
||||||
|
|
||||||
#: app/routes/matches.py:578 app/routes/team_matches.py:284
|
#: app/routes/matches.py:616 app/routes/team_matches.py:276
|
||||||
msgid "Match updated successfully!"
|
msgid "Match updated successfully!"
|
||||||
msgstr "Match mis à jour."
|
msgstr "Match mis à jour."
|
||||||
|
|
||||||
#: app/routes/matches.py:631 app/routes/team_matches.py:299
|
#: app/routes/matches.py:669 app/routes/team_matches.py:291
|
||||||
msgid "You do not have permission to delete this match."
|
msgid "You do not have permission to delete this match."
|
||||||
msgstr "Vous n’avez pas les droits pour supprimer ce match."
|
msgstr "Vous n’avez pas les droits pour supprimer ce match."
|
||||||
|
|
||||||
#: app/routes/matches.py:634
|
#: app/routes/matches.py:672
|
||||||
msgid "This tryout has ended. Matches can no longer be deleted."
|
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."
|
msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés."
|
||||||
|
|
||||||
#: app/routes/matches.py:647 app/routes/team_matches.py:303
|
#: app/routes/matches.py:685 app/routes/team_matches.py:295
|
||||||
msgid "Match deleted successfully."
|
msgid "Match deleted successfully."
|
||||||
msgstr "Match supprimé."
|
msgstr "Match supprimé."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:98
|
#: app/routes/team_matches.py:100
|
||||||
msgid "You do not have permission to schedule matches for this team."
|
msgid "You do not have permission to schedule matches for this team."
|
||||||
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette équipe."
|
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette équipe."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:140
|
#: app/routes/team_matches.py:142
|
||||||
msgid "Date is required."
|
msgid "Date is required."
|
||||||
msgstr "La date est obligatoire."
|
msgstr "La date est obligatoire."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:228
|
#: app/routes/team_matches.py:220
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team match \"%(title)s\" scheduled successfully!"
|
msgid "Team match \"%(title)s\" scheduled successfully!"
|
||||||
msgstr "Match d’équipe « %(title)s » planifié."
|
msgstr "Match d’équipe « %(title)s » planifié."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:267
|
#: app/routes/team_matches.py:259
|
||||||
msgid "Invalid start time format."
|
msgid "Invalid start time format."
|
||||||
msgstr "Format d’heure de début invalide."
|
msgstr "Format d’heure de début invalide."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:275
|
#: app/routes/team_matches.py:267
|
||||||
msgid "Invalid end time format."
|
msgid "Invalid end time format."
|
||||||
msgstr "Format d’heure de fin invalide."
|
msgstr "Format d’heure de fin invalide."
|
||||||
|
|
||||||
#: app/routes/teams.py:38
|
#: app/routes/teams.py:40
|
||||||
msgid "Use My Team(s) to view your teams."
|
msgid "Use My Team(s) to view your teams."
|
||||||
msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes."
|
msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes."
|
||||||
|
|
||||||
#: app/routes/teams.py:41
|
#: app/routes/teams.py:43
|
||||||
msgid "You do not have permission to view teams."
|
msgid "You do not have permission to view teams."
|
||||||
msgstr "Vous n’avez pas les droits pour consulter les équipes."
|
msgstr "Vous n’avez pas les droits pour consulter les équipes."
|
||||||
|
|
||||||
#: app/routes/teams.py:68
|
#: app/routes/teams.py:70
|
||||||
msgid "This page is for players."
|
msgid "This page is for players."
|
||||||
msgstr "Cette page est réservée aux joueurs."
|
msgstr "Cette page est réservée aux joueurs."
|
||||||
|
|
||||||
#: app/routes/teams.py:121
|
#: app/routes/teams.py:123
|
||||||
msgid "You do not have permission to create teams."
|
msgid "You do not have permission to create teams."
|
||||||
msgstr "Vous n’avez pas les droits pour créer une équipe."
|
msgstr "Vous n’avez pas les droits pour créer une équipe."
|
||||||
|
|
||||||
#: app/routes/teams.py:129 app/routes/teams.py:174
|
#: app/routes/teams.py:131 app/routes/teams.py:176
|
||||||
msgid "Team name is required."
|
msgid "Team name is required."
|
||||||
msgstr "Le nom de l’équipe est obligatoire."
|
msgstr "Le nom de l’équipe est obligatoire."
|
||||||
|
|
||||||
#: app/routes/teams.py:134 app/routes/teams.py:179
|
#: app/routes/teams.py:136 app/routes/teams.py:181
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(name)s\" already exists."
|
msgid "Team \"%(name)s\" already exists."
|
||||||
msgstr "L’équipe « %(name)s » existe déjà."
|
msgstr "L’équipe « %(name)s » existe déjà."
|
||||||
|
|
||||||
#: app/routes/teams.py:156
|
#: app/routes/teams.py:158
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(name)s\" created successfully!"
|
msgid "Team \"%(name)s\" created successfully!"
|
||||||
msgstr "Équipe « %(name)s » créée."
|
msgstr "Équipe « %(name)s » créée."
|
||||||
|
|
||||||
#: app/routes/teams.py:166
|
#: app/routes/teams.py:168
|
||||||
msgid "You do not have permission to edit this team."
|
msgid "You do not have permission to edit this team."
|
||||||
msgstr "Vous n’avez pas les droits pour modifier cette équipe."
|
msgstr "Vous n’avez pas les droits pour modifier cette équipe."
|
||||||
|
|
||||||
#: app/routes/teams.py:217
|
#: app/routes/teams.py:219
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(name)s\" updated successfully!"
|
msgid "Team \"%(name)s\" updated successfully!"
|
||||||
msgstr "Équipe « %(name)s » mise à jour."
|
msgstr "Équipe « %(name)s » mise à jour."
|
||||||
|
|
||||||
#: app/routes/teams.py:226
|
#: app/routes/teams.py:228
|
||||||
msgid "You do not have permission to delete teams."
|
msgid "You do not have permission to delete teams."
|
||||||
msgstr "Vous n’avez pas les droits pour supprimer une équipe."
|
msgstr "Vous n’avez pas les droits pour supprimer une équipe."
|
||||||
|
|
||||||
#: app/routes/teams.py:258
|
#: app/routes/teams.py:260
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(name)s\" deleted successfully."
|
msgid "Team \"%(name)s\" deleted successfully."
|
||||||
msgstr "Équipe « %(name)s » supprimée."
|
msgstr "Équipe « %(name)s » supprimée."
|
||||||
|
|
||||||
#: app/routes/teams.py:273
|
#: app/routes/teams.py:275
|
||||||
msgid "Please select a coach."
|
msgid "Please select a coach."
|
||||||
msgstr "Veuillez choisir un coach."
|
msgstr "Veuillez choisir un coach."
|
||||||
|
|
||||||
#: app/routes/teams.py:278
|
#: app/routes/teams.py:280
|
||||||
msgid "Only coaches can be assigned as coach."
|
msgid "Only coaches can be assigned as coach."
|
||||||
msgstr "Seuls les coachs peuvent être assignés comme coach."
|
msgstr "Seuls les coachs peuvent être assignés comme coach."
|
||||||
|
|
||||||
#: app/routes/teams.py:284
|
#: app/routes/teams.py:286
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already a coach of %(name)s."
|
msgid "%(username)s is already a coach of %(name)s."
|
||||||
msgstr "%(username)s est déjà coach de %(name)s."
|
msgstr "%(username)s est déjà coach de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:297
|
#: app/routes/teams.py:299
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s added as coach of %(name)s."
|
msgid "%(username)s added as coach of %(name)s."
|
||||||
msgstr "%(username)s a été ajouté comme coach de %(name)s."
|
msgstr "%(username)s a été ajouté comme coach de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:314
|
#: app/routes/teams.py:316
|
||||||
msgid "Please select a manager."
|
msgid "Please select a manager."
|
||||||
msgstr "Veuillez choisir un gérant."
|
msgstr "Veuillez choisir un gérant."
|
||||||
|
|
||||||
#: app/routes/teams.py:319
|
#: app/routes/teams.py:321
|
||||||
msgid "Only managers can be assigned as manager."
|
msgid "Only managers can be assigned as manager."
|
||||||
msgstr "Seuls les gérants peuvent être assignés comme gérant."
|
msgstr "Seuls les gérants peuvent être assignés comme gérant."
|
||||||
|
|
||||||
#: app/routes/teams.py:325
|
#: app/routes/teams.py:327
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already a manager of %(name)s."
|
msgid "%(username)s is already a manager of %(name)s."
|
||||||
msgstr "%(username)s est déjà gérant de %(name)s."
|
msgstr "%(username)s est déjà gérant de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:338
|
#: app/routes/teams.py:340
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s added as manager of %(name)s."
|
msgid "%(username)s added as manager of %(name)s."
|
||||||
msgstr "%(username)s a été ajouté comme gérant de %(name)s."
|
msgstr "%(username)s a été ajouté comme gérant de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:365
|
#: app/routes/teams.py:367
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Coach removed from %(name)s."
|
msgid "Coach removed from %(name)s."
|
||||||
msgstr "Coach retiré de %(name)s."
|
msgstr "Coach retiré de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:390
|
#: app/routes/teams.py:392
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Manager removed from %(name)s."
|
msgid "Manager removed from %(name)s."
|
||||||
msgstr "Gérant retiré de %(name)s."
|
msgstr "Gérant retiré de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:406 app/routes/tryouts.py:512 app/routes/tryouts.py:613
|
#: app/routes/teams.py:408 app/routes/tryouts.py:545 app/routes/tryouts.py:646
|
||||||
msgid "Please select a player."
|
msgid "Please select a player."
|
||||||
msgstr "Veuillez choisir un joueur."
|
msgstr "Veuillez choisir un joueur."
|
||||||
|
|
||||||
#: app/routes/teams.py:411
|
#: app/routes/teams.py:413
|
||||||
msgid "Can only assign players to teams."
|
msgid "Can only assign players to teams."
|
||||||
msgstr "Seuls des joueurs peuvent être assignés à une équipe."
|
msgstr "Seuls des joueurs peuvent être assignés à une équipe."
|
||||||
|
|
||||||
#: app/routes/teams.py:417
|
#: app/routes/teams.py:419
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already on %(name)s."
|
msgid "%(username)s is already on %(name)s."
|
||||||
msgstr "%(username)s fait déjà partie de %(name)s."
|
msgstr "%(username)s fait déjà partie de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:425
|
#: app/routes/teams.py:427
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s added to %(name)s!"
|
msgid "%(username)s added to %(name)s!"
|
||||||
msgstr "%(username)s a été ajouté à %(name)s."
|
msgstr "%(username)s a été ajouté à %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:442 app/routes/teams.py:515
|
#: app/routes/teams.py:444 app/routes/teams.py:517
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is not on %(name)s."
|
msgid "%(username)s is not on %(name)s."
|
||||||
msgstr "%(username)s ne fait pas partie de %(name)s."
|
msgstr "%(username)s ne fait pas partie de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:450
|
#: app/routes/teams.py:452
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s removed from %(name)s."
|
msgid "%(username)s removed from %(name)s."
|
||||||
msgstr "%(username)s a été retiré de %(name)s."
|
msgstr "%(username)s a été retiré de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:486 app/routes/teams.py:504
|
#: app/routes/teams.py:488 app/routes/teams.py:506
|
||||||
msgid "You do not have permission to add notes to this team."
|
msgid "You do not have permission to add notes to this team."
|
||||||
msgstr "Vous n’avez pas les droits pour ajouter des notes à cette équipe."
|
msgstr "Vous n’avez pas les droits pour ajouter des notes à cette équipe."
|
||||||
|
|
||||||
#: app/routes/teams.py:494
|
#: app/routes/teams.py:496
|
||||||
msgid "Team notes added successfully!"
|
msgid "Team notes added successfully!"
|
||||||
msgstr "Notes d’équipe ajoutées."
|
msgstr "Notes d’équipe ajoutées."
|
||||||
|
|
||||||
#: app/routes/teams.py:509 app/routes/users.py:1530 app/routes/users.py:1573
|
#: app/routes/teams.py:511 app/routes/users/notes.py:207
|
||||||
|
#: app/routes/users/notes.py:250
|
||||||
msgid "Can only add notes for players."
|
msgid "Can only add notes for players."
|
||||||
msgstr "Il n’est possible d’ajouter des notes que pour des joueurs."
|
msgstr "Il n’est possible d’ajouter des notes que pour des joueurs."
|
||||||
|
|
||||||
#: app/routes/teams.py:525
|
#: app/routes/teams.py:527
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Note added for %(username)s!"
|
msgid "Note added for %(username)s!"
|
||||||
msgstr "Note ajoutée pour %(username)s."
|
msgstr "Note ajoutée pour %(username)s."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:56
|
#: app/routes/tryouts.py:77
|
||||||
msgid "You do not have permission to create tryouts."
|
msgid "You do not have permission to create tryouts."
|
||||||
msgstr "Vous n’avez pas les droits pour créer une sélection."
|
msgstr "Vous n’avez pas les droits pour créer une sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:82 app/routes/tryouts.py:189
|
#: app/routes/tryouts.py:103 app/routes/tryouts.py:210
|
||||||
msgid "Invalid start date format."
|
msgid "Invalid start date format."
|
||||||
msgstr "Format de date de début invalide."
|
msgstr "Format de date de début invalide."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:97 app/routes/tryouts.py:204
|
#: app/routes/tryouts.py:118 app/routes/tryouts.py:225
|
||||||
msgid "End date cannot be before start date."
|
msgid "End date cannot be before start date."
|
||||||
msgstr "La date de fin ne peut pas précéder la date de début."
|
msgstr "La date de fin ne peut pas précéder la date de début."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:107 app/routes/tryouts.py:214
|
#: app/routes/tryouts.py:128 app/routes/tryouts.py:235
|
||||||
msgid "Invalid end date format."
|
msgid "Invalid end date format."
|
||||||
msgstr "Format de date de fin invalide."
|
msgstr "Format de date de fin invalide."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:139
|
#: app/routes/tryouts.py:160
|
||||||
msgid "Tryout created successfully!"
|
msgid "Tryout created successfully!"
|
||||||
msgstr "Sélection créée."
|
msgstr "Sélection créée."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:159
|
#: app/routes/tryouts.py:180
|
||||||
msgid "You do not have permission to edit this tryout."
|
msgid "You do not have permission to edit this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour modifier cette sélection."
|
msgstr "Vous n’avez pas les droits pour modifier cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:163
|
#: app/routes/tryouts.py:184
|
||||||
msgid "This tryout has ended and can no longer be modified."
|
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."
|
msgstr "Cette sélection est terminée et ne peut plus être modifiée."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:242
|
#: app/routes/tryouts.py:263
|
||||||
msgid "Tryout updated successfully!"
|
msgid "Tryout updated successfully!"
|
||||||
msgstr "Sélection mise à jour."
|
msgstr "Sélection mise à jour."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:289
|
#: app/routes/tryouts.py:310
|
||||||
msgid "You do not have permission to view this tryout."
|
msgid "You do not have permission to view this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour consulter cette sélection."
|
msgstr "Vous n’avez pas les droits pour consulter cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:439
|
#: app/routes/tryouts.py:472
|
||||||
msgid "Only players can register for tryouts."
|
msgid "Only players can register for tryouts."
|
||||||
msgstr "Seuls les joueurs peuvent s’inscrire à une sélection."
|
msgstr "Seuls les joueurs peuvent s’inscrire à une sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:443
|
#: app/routes/tryouts.py:476
|
||||||
msgid "This tryout is not accepting registrations."
|
msgid "This tryout is not accepting registrations."
|
||||||
msgstr "Cette sélection n’accepte pas d’inscriptions."
|
msgstr "Cette sélection n’accepte pas d’inscriptions."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:450
|
#: app/routes/tryouts.py:483
|
||||||
msgid "You are already registered for this tryout."
|
msgid "You are already registered for this tryout."
|
||||||
msgstr "Vous êtes déjà inscrit à cette sélection."
|
msgstr "Vous êtes déjà inscrit à cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:456 app/routes/tryouts.py:531
|
#: app/routes/tryouts.py:489 app/routes/tryouts.py:564
|
||||||
msgid "This tryout is full."
|
msgid "This tryout is full."
|
||||||
msgstr "Cette sélection est complète."
|
msgstr "Cette sélection est complète."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:462
|
#: app/routes/tryouts.py:495
|
||||||
msgid "Successfully registered for tryout!"
|
msgid "Successfully registered for tryout!"
|
||||||
msgstr "Inscription à la sélection réussie."
|
msgstr "Inscription à la sélection réussie."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:478
|
#: app/routes/tryouts.py:511
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Tryout status updated to %(new_status)s."
|
msgid "Tryout status updated to %(new_status)s."
|
||||||
msgstr "Statut de la sélection mis à jour : %(new_status)s."
|
msgstr "Statut de la sélection mis à jour : %(new_status)s."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:498
|
#: app/routes/tryouts.py:531
|
||||||
msgid "Registration status updated."
|
msgid "Registration status updated."
|
||||||
msgstr "Statut d’inscription mis à jour."
|
msgstr "Statut d’inscription mis à jour."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:517
|
#: app/routes/tryouts.py:550
|
||||||
msgid "Can only register players."
|
msgid "Can only register players."
|
||||||
msgstr "Seuls des joueurs peuvent être inscrits."
|
msgstr "Seuls des joueurs peuvent être inscrits."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:523
|
#: app/routes/tryouts.py:556
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already registered for this tryout."
|
msgid "%(username)s is already registered for this tryout."
|
||||||
msgstr "%(username)s est déjà inscrit à cette sélection."
|
msgstr "%(username)s est déjà inscrit à cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:537
|
#: app/routes/tryouts.py:570
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s registered for tryout!"
|
msgid "%(username)s registered for tryout!"
|
||||||
msgstr "%(username)s est inscrit à la sélection."
|
msgstr "%(username)s est inscrit à la sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:573
|
#: app/routes/tryouts.py:606
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s removed from tryout."
|
msgid "%(username)s removed from tryout."
|
||||||
msgstr "%(username)s a été retiré de la sélection."
|
msgstr "%(username)s a été retiré de la sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:591
|
#: app/routes/tryouts.py:624
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(team_name)s\" created!"
|
msgid "Team \"%(team_name)s\" created!"
|
||||||
msgstr "Équipe « %(team_name)s » créée."
|
msgstr "Équipe « %(team_name)s » créée."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:622
|
#: app/routes/tryouts.py:655
|
||||||
msgid "That player is not registered for this tryout."
|
msgid "That player is not registered for this tryout."
|
||||||
msgstr "Ce joueur n’est pas inscrit à cette sélection."
|
msgstr "Ce joueur n’est pas inscrit à cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:628
|
#: app/routes/tryouts.py:661
|
||||||
msgid "Player is already on this team."
|
msgid "Player is already on this team."
|
||||||
msgstr "Ce joueur est déjà dans cette équipe."
|
msgstr "Ce joueur est déjà dans cette équipe."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:633
|
#: app/routes/tryouts.py:666
|
||||||
msgid "Player added to team!"
|
msgid "Player added to team!"
|
||||||
msgstr "Joueur ajouté à l’équipe."
|
msgstr "Joueur ajouté à l’équipe."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:643
|
#: app/routes/tryouts.py:676
|
||||||
msgid "You do not have permission to delete this tryout."
|
msgid "You do not have permission to delete this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour supprimer cette sélection."
|
msgstr "Vous n’avez pas les droits pour supprimer cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:679
|
#: app/routes/tryouts.py:712
|
||||||
msgid "Tryout deleted successfully."
|
msgid "Tryout deleted successfully."
|
||||||
msgstr "Sélection supprimée."
|
msgstr "Sélection supprimée."
|
||||||
|
|
||||||
#: app/routes/users.py:83
|
#: app/routes/users/_shared.py:46
|
||||||
msgid "No file selected."
|
msgid "No file selected."
|
||||||
msgstr "Aucun fichier sélectionné."
|
msgstr "Aucun fichier sélectionné."
|
||||||
|
|
||||||
#: app/routes/users.py:87
|
#: app/routes/users/_shared.py:50
|
||||||
msgid "Only PDF files are allowed for contracts."
|
msgid "Only PDF files are allowed for contracts."
|
||||||
msgstr "Seuls les fichiers PDF sont acceptés pour les contrats."
|
msgstr "Seuls les fichiers PDF sont acceptés pour les contrats."
|
||||||
|
|
||||||
#: app/routes/users.py:92
|
#: app/routes/users/_shared.py:55
|
||||||
msgid "That file is not a PDF, whatever its name says."
|
msgid "That file is not a PDF, whatever its name says."
|
||||||
msgstr "Ce fichier n’est pas un PDF, quel que soit son nom."
|
msgstr "Ce fichier n’est pas un PDF, quel que soit son nom."
|
||||||
|
|
||||||
#: app/routes/users.py:181
|
#: app/routes/users/accounts.py:53
|
||||||
msgid "Only the president can manage users."
|
msgid "Only the president can manage users."
|
||||||
msgstr "Seul le président peut gérer les utilisateurs."
|
msgstr "Seul le président peut gérer les utilisateurs."
|
||||||
|
|
||||||
#: app/routes/users.py:193
|
#: app/routes/users/accounts.py:65
|
||||||
msgid "Only the president can edit users."
|
msgid "Only the president can edit users."
|
||||||
msgstr "Seul le président peut modifier des utilisateurs."
|
msgstr "Seul le président peut modifier des utilisateurs."
|
||||||
|
|
||||||
#: app/routes/users.py:234
|
#: app/routes/users/accounts.py:106
|
||||||
msgid "Email already in use by another account."
|
msgid "Email already in use by another account."
|
||||||
msgstr "Cette adresse courriel est déjà utilisée par un autre compte."
|
msgstr "Cette adresse courriel est déjà utilisée par un autre compte."
|
||||||
|
|
||||||
#: app/routes/users.py:245
|
#: app/routes/users/accounts.py:117
|
||||||
msgid "You cannot change your own role. Ask another president to do it."
|
msgid "You cannot change your own role. Ask another president to do it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre "
|
"Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre "
|
||||||
"président de le faire."
|
"président de le faire."
|
||||||
|
|
||||||
#: app/routes/users.py:258
|
#: app/routes/users/accounts.py:130
|
||||||
msgid ""
|
msgid ""
|
||||||
"This is the last active president. Promote another account before "
|
"This is the last active president. Promote another account before "
|
||||||
"changing this one."
|
"changing this one."
|
||||||
@@ -576,177 +577,178 @@ msgstr ""
|
|||||||
"C’est le dernier président actif. Promouvez un autre compte avant de "
|
"C’est le dernier président actif. Promouvez un autre compte avant de "
|
||||||
"modifier celui-ci."
|
"modifier celui-ci."
|
||||||
|
|
||||||
#: app/routes/users.py:337
|
#: app/routes/users/accounts.py:209
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "User %(username)s updated successfully!"
|
msgid "User %(username)s updated successfully!"
|
||||||
msgstr "Utilisateur %(username)s mis à jour."
|
msgstr "Utilisateur %(username)s mis à jour."
|
||||||
|
|
||||||
#: app/routes/users.py:358
|
#: app/routes/users/accounts.py:230
|
||||||
msgid "Only the president can delete users."
|
msgid "Only the president can delete users."
|
||||||
msgstr "Seul le président peut supprimer des utilisateurs."
|
msgstr "Seul le président peut supprimer des utilisateurs."
|
||||||
|
|
||||||
#: app/routes/users.py:362
|
#: app/routes/users/accounts.py:234
|
||||||
msgid "You cannot delete your own account."
|
msgid "You cannot delete your own account."
|
||||||
msgstr "Vous ne pouvez pas supprimer votre propre compte."
|
msgstr "Vous ne pouvez pas supprimer votre propre compte."
|
||||||
|
|
||||||
#: app/routes/users.py:405
|
#: app/routes/users/accounts.py:277
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "User %(deleted_username)s has been removed."
|
msgid "User %(deleted_username)s has been removed."
|
||||||
msgstr "L’utilisateur %(deleted_username)s a été supprimé."
|
msgstr "L’utilisateur %(deleted_username)s a été supprimé."
|
||||||
|
|
||||||
#: app/routes/users.py:416
|
#: app/routes/users/accounts.py:288
|
||||||
msgid "Only the president can create users."
|
msgid "Only the president can create users."
|
||||||
msgstr "Seul le président peut créer des utilisateurs."
|
msgstr "Seul le président peut créer des utilisateurs."
|
||||||
|
|
||||||
#: app/routes/users.py:464
|
#: app/routes/users/accounts.py:336
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "User %(full_name)s created as %(role)s!"
|
msgid "User %(full_name)s created as %(role)s!"
|
||||||
msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
|
msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
|
||||||
|
|
||||||
#: app/routes/users.py:534
|
#: app/routes/users/availability.py:179
|
||||||
msgid "Username already taken."
|
msgid "Only coaches can manage availability."
|
||||||
msgstr "Ce nom d’utilisateur est déjà pris."
|
msgstr "Seuls les coachs peuvent gérer leurs disponibilités."
|
||||||
|
|
||||||
#: app/routes/users.py:544
|
#: app/routes/users/contracts.py:83
|
||||||
msgid "Email already in use."
|
|
||||||
msgstr "Cette adresse courriel est déjà utilisée."
|
|
||||||
|
|
||||||
#: app/routes/users.py:574
|
|
||||||
msgid "Profile updated successfully!"
|
|
||||||
msgstr "Profil mis à jour."
|
|
||||||
|
|
||||||
#: app/routes/users.py:809
|
|
||||||
msgid "Only presidents, managers, and coaches can upload contracts."
|
msgid "Only presidents, managers, and coaches can upload contracts."
|
||||||
msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat."
|
msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat."
|
||||||
|
|
||||||
#: app/routes/users.py:828
|
#: app/routes/users/contracts.py:102
|
||||||
msgid "You do not have permission to upload a contract for this player."
|
msgid "You do not have permission to upload a contract for this player."
|
||||||
msgstr "Vous n’avez pas les droits pour téléverser un contrat pour ce joueur."
|
msgstr "Vous n’avez pas les droits pour téléverser un contrat pour ce joueur."
|
||||||
|
|
||||||
#: app/routes/users.py:869
|
#: app/routes/users/contracts.py:143
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Contract uploaded successfully for %(username)s!"
|
msgid "Contract uploaded successfully for %(username)s!"
|
||||||
msgstr "Contrat téléversé pour %(username)s."
|
msgstr "Contrat téléversé pour %(username)s."
|
||||||
|
|
||||||
#: app/routes/users.py:883
|
#: app/routes/users/contracts.py:157
|
||||||
msgid "Only the player can upload their signed contract."
|
msgid "Only the player can upload their signed contract."
|
||||||
msgstr "Seul le joueur peut téléverser son contrat signé."
|
msgstr "Seul le joueur peut téléverser son contrat signé."
|
||||||
|
|
||||||
#: app/routes/users.py:902
|
#: app/routes/users/contracts.py:176
|
||||||
msgid "Signed contract uploaded successfully!"
|
msgid "Signed contract uploaded successfully!"
|
||||||
msgstr "Contrat signé téléversé."
|
msgstr "Contrat signé téléversé."
|
||||||
|
|
||||||
#: app/routes/users.py:912 app/routes/users.py:925
|
#: app/routes/users/contracts.py:186 app/routes/users/contracts.py:199
|
||||||
msgid "You do not have permission to download this contract."
|
msgid "You do not have permission to download this contract."
|
||||||
msgstr "Vous n’avez pas les droits pour télécharger ce contrat."
|
msgstr "Vous n’avez pas les droits pour télécharger ce contrat."
|
||||||
|
|
||||||
#: app/routes/users.py:928
|
#: app/routes/users/contracts.py:202
|
||||||
msgid "No signed contract available."
|
msgid "No signed contract available."
|
||||||
msgstr "Aucun contrat signé disponible."
|
msgstr "Aucun contrat signé disponible."
|
||||||
|
|
||||||
#: app/routes/users.py:1034
|
#: app/routes/users/notes.py:34
|
||||||
msgid "Only players can request One on One sessions."
|
|
||||||
msgstr "Seuls les joueurs peuvent demander une rencontre individuelle."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1047
|
|
||||||
msgid "You do not have a coach assigned to your team."
|
|
||||||
msgstr "Aucun coach n’est assigné à votre équipe."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1082
|
|
||||||
msgid "Cannot request One on One - no coach assigned."
|
|
||||||
msgstr "Impossible de demander une rencontre : aucun coach assigné."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1090
|
|
||||||
msgid "Invalid date or time format."
|
|
||||||
msgstr "Format de date ou d’heure invalide."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1104
|
|
||||||
msgid "The requested time is not within the coach's availability."
|
|
||||||
msgstr "L’horaire demandé ne correspond à aucune disponibilité du coach."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1132
|
|
||||||
msgid "Your One on One request has been submitted!"
|
|
||||||
msgstr "Votre demande de rencontre a été envoyée."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1176
|
|
||||||
msgid "Only coaches can accept One on One requests."
|
|
||||||
msgstr "Seuls les coachs peuvent accepter une demande de rencontre."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1182 app/routes/users.py:1232
|
|
||||||
msgid "This request is not for you."
|
|
||||||
msgstr "Cette demande ne vous est pas destinée."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1186 app/routes/users.py:1236
|
|
||||||
msgid "This request has already been processed."
|
|
||||||
msgstr "Cette demande a déjà été traitée."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1213
|
|
||||||
#, 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:1226
|
|
||||||
msgid "Only coaches can reject One on One requests."
|
|
||||||
msgstr "Seuls les coachs peuvent refuser une demande de rencontre."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1268
|
|
||||||
#, 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:1286
|
|
||||||
msgid "This page is for players only."
|
msgid "This page is for players only."
|
||||||
msgstr "Cette page est réservée aux joueurs."
|
msgstr "Cette page est réservée aux joueurs."
|
||||||
|
|
||||||
#: app/routes/users.py:1328
|
#: app/routes/users/notes.py:71
|
||||||
msgid "Only coaches can manage availability."
|
|
||||||
msgstr "Seuls les coachs peuvent gérer leurs disponibilités."
|
|
||||||
|
|
||||||
#: app/routes/users.py:1394
|
|
||||||
msgid "Only coaches can access the notes dashboard."
|
msgid "Only coaches can access the notes dashboard."
|
||||||
msgstr "Seuls les coachs ont accès au tableau des notes."
|
msgstr "Seuls les coachs ont accès au tableau des notes."
|
||||||
|
|
||||||
#: app/routes/users.py:1484
|
#: app/routes/users/notes.py:161
|
||||||
msgid "Only coaches can manage team notes."
|
msgid "Only coaches can manage team notes."
|
||||||
msgstr "Seuls les coachs peuvent gérer les notes d’équipe."
|
msgstr "Seuls les coachs peuvent gérer les notes d’équipe."
|
||||||
|
|
||||||
#: app/routes/users.py:1490
|
#: app/routes/users/notes.py:167
|
||||||
msgid "You are not assigned to a team."
|
msgid "You are not assigned to a team."
|
||||||
msgstr "Vous n’êtes assigné à aucune équipe."
|
msgstr "Vous n’êtes assigné à aucune équipe."
|
||||||
|
|
||||||
#: app/routes/users.py:1503
|
#: app/routes/users/notes.py:180
|
||||||
msgid "Team notes saved successfully!"
|
msgid "Team notes saved successfully!"
|
||||||
msgstr "Notes d’équipe enregistrées."
|
msgstr "Notes d’équipe enregistrées."
|
||||||
|
|
||||||
#: app/routes/users.py:1518
|
#: app/routes/users/notes.py:195
|
||||||
msgid "Only coaches can manage personal notes."
|
msgid "Only coaches can manage personal notes."
|
||||||
msgstr "Seuls les coachs peuvent gérer les notes personnelles."
|
msgstr "Seuls les coachs peuvent gérer les notes personnelles."
|
||||||
|
|
||||||
#: app/routes/users.py:1525 app/routes/users.py:1568 app/routes/users.py:1619
|
#: app/routes/users/notes.py:202 app/routes/users/notes.py:245
|
||||||
#: app/routes/users.py:1673
|
#: app/routes/users/notes.py:296 app/routes/users/notes.py:350
|
||||||
msgid "Player and content are required."
|
msgid "Player and content are required."
|
||||||
msgstr "Le joueur et le contenu sont obligatoires."
|
msgstr "Le joueur et le contenu sont obligatoires."
|
||||||
|
|
||||||
#: app/routes/users.py:1534 app/routes/users.py:1577 app/routes/users.py:1623
|
#: app/routes/users/notes.py:211 app/routes/users/notes.py:254
|
||||||
#: app/routes/users.py:1677
|
#: app/routes/users/notes.py:300 app/routes/users/notes.py:354
|
||||||
msgid "You can only write notes about players you work with."
|
msgid "You can only write notes about players you work with."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Vous ne pouvez écrire des notes que sur les joueurs avec qui vous "
|
"Vous ne pouvez écrire des notes que sur les joueurs avec qui vous "
|
||||||
"travaillez."
|
"travaillez."
|
||||||
|
|
||||||
#: app/routes/users.py:1544 app/routes/users.py:1590
|
#: app/routes/users/notes.py:221 app/routes/users/notes.py:267
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Note added for %(username)s."
|
msgid "Note added for %(username)s."
|
||||||
msgstr "Note ajoutée pour %(username)s."
|
msgstr "Note ajoutée pour %(username)s."
|
||||||
|
|
||||||
#: app/routes/users.py:1558 app/routes/users.py:1604 app/routes/users.py:1657
|
#: app/routes/users/notes.py:235 app/routes/users/notes.py:281
|
||||||
|
#: app/routes/users/notes.py:334
|
||||||
msgid "Only coaches can add personal notes."
|
msgid "Only coaches can add personal notes."
|
||||||
msgstr "Seuls les coachs peuvent ajouter des notes personnelles."
|
msgstr "Seuls les coachs peuvent ajouter des notes personnelles."
|
||||||
|
|
||||||
#: app/routes/users.py:1634 app/routes/users.py:1688
|
#: app/routes/users/notes.py:311 app/routes/users/notes.py:365
|
||||||
msgid "Note added successfully."
|
msgid "Note added successfully."
|
||||||
msgstr "Note ajoutée."
|
msgstr "Note ajoutée."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:20
|
||||||
|
msgid "Only players can request One on One sessions."
|
||||||
|
msgstr "Seuls les joueurs peuvent demander une rencontre individuelle."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:33
|
||||||
|
msgid "You do not have a coach assigned to your team."
|
||||||
|
msgstr "Aucun coach n’est assigné à votre équipe."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:68
|
||||||
|
msgid "Cannot request One on One - no coach assigned."
|
||||||
|
msgstr "Impossible de demander une rencontre : aucun coach assigné."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:76
|
||||||
|
msgid "Invalid date or time format."
|
||||||
|
msgstr "Format de date ou d’heure invalide."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:90
|
||||||
|
msgid "The requested time is not within the coach's availability."
|
||||||
|
msgstr "L’horaire demandé ne correspond à aucune disponibilité du coach."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:118
|
||||||
|
msgid "Your One on One request has been submitted!"
|
||||||
|
msgstr "Votre demande de rencontre a été envoyée."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:163
|
||||||
|
msgid "Only coaches can accept One on One requests."
|
||||||
|
msgstr "Seuls les coachs peuvent accepter une demande de rencontre."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:169 app/routes/users/one_on_one.py:219
|
||||||
|
msgid "This request is not for you."
|
||||||
|
msgstr "Cette demande ne vous est pas destinée."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:173 app/routes/users/one_on_one.py:223
|
||||||
|
msgid "This request has already been processed."
|
||||||
|
msgstr "Cette demande a déjà été traitée."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:200
|
||||||
|
#, 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/one_on_one.py:213
|
||||||
|
msgid "Only coaches can reject One on One requests."
|
||||||
|
msgstr "Seuls les coachs peuvent refuser une demande de rencontre."
|
||||||
|
|
||||||
|
#: app/routes/users/one_on_one.py:255
|
||||||
|
#, 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/profile.py:83
|
||||||
|
msgid "Username already taken."
|
||||||
|
msgstr "Ce nom d’utilisateur est déjà pris."
|
||||||
|
|
||||||
|
#: app/routes/users/profile.py:93
|
||||||
|
msgid "Email already in use."
|
||||||
|
msgstr "Cette adresse courriel est déjà utilisée."
|
||||||
|
|
||||||
|
#: app/routes/users/profile.py:123
|
||||||
|
msgid "Profile updated successfully!"
|
||||||
|
msgstr "Profil mis à jour."
|
||||||
|
|
||||||
#: app/templates/errors/400.html:2
|
#: app/templates/errors/400.html:2
|
||||||
msgid "400 Bad Request"
|
msgid "400 Bad Request"
|
||||||
msgstr "400 Requête incorrecte"
|
msgstr "400 Requête incorrecte"
|
||||||
@@ -2387,11 +2389,7 @@ msgstr "Confirmer le mot de passe"
|
|||||||
msgid "Confirm your password"
|
msgid "Confirm your password"
|
||||||
msgstr "Confirmez votre mot de passe"
|
msgstr "Confirmez votre mot de passe"
|
||||||
|
|
||||||
#: app/templates/pages/register.html:151
|
#: app/templates/pages/register.html:159
|
||||||
msgid "Answer"
|
|
||||||
msgstr "Réponse"
|
|
||||||
|
|
||||||
#: app/templates/pages/register.html:154
|
|
||||||
msgid "Create Account"
|
msgid "Create Account"
|
||||||
msgstr "Créer le compte"
|
msgstr "Créer le compte"
|
||||||
|
|
||||||
@@ -2900,3 +2898,9 @@ msgstr "Voir le profil"
|
|||||||
#~ "%(remaining)s tentative(s) avant le "
|
#~ "%(remaining)s tentative(s) avant le "
|
||||||
#~ "verrouillage."
|
#~ "verrouillage."
|
||||||
|
|
||||||
|
#~ msgid "Incorrect CAPTCHA answer. Please try again."
|
||||||
|
#~ msgstr "Réponse au CAPTCHA incorrecte. Veuillez réessayer."
|
||||||
|
|
||||||
|
#~ msgid "Answer"
|
||||||
|
#~ msgstr "Réponse"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
"""What now stands between a robot and a new account (SEC-AUTH-008).
|
||||||
|
|
||||||
|
The arithmetic CAPTCHA it replaces had nineteen possible answers and could
|
||||||
|
be solved by reading the question as a string. It stopped nothing, cost every
|
||||||
|
human a step, and — being counted as a protection — was worse than nothing.
|
||||||
|
|
||||||
|
Two checks took its place: a honeypot input, and a floor on how fast the
|
||||||
|
form can come back. Both are invisible to a person filling in the form. Both
|
||||||
|
are honest about their ceiling: they stop commodity spam, not somebody who
|
||||||
|
reads the page.
|
||||||
|
|
||||||
|
The tests that matter most here are the ones asserting a *legitimate*
|
||||||
|
sign-up still works. A screening rule that quietly refuses real people is a
|
||||||
|
worse outcome than the CAPTCHA was.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from app.routes.auth import (
|
||||||
|
MIN_REGISTRATION_SECONDS,
|
||||||
|
REGISTRATION_HONEYPOT_FIELD,
|
||||||
|
REGISTRATION_ISSUED_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
FORM = {
|
||||||
|
'username': 'brandnew',
|
||||||
|
'email': '[email protected]',
|
||||||
|
'password': 'Password123',
|
||||||
|
'confirm_password': 'Password123',
|
||||||
|
'full_name': 'Brand New',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _issued_long_ago(client, seconds_ago=None):
|
||||||
|
"""Pretend the form was handed out a while back.
|
||||||
|
|
||||||
|
Backdated rather than slept through: waiting three real seconds per test
|
||||||
|
would add a minute to the suite for nothing.
|
||||||
|
"""
|
||||||
|
if seconds_ago is None:
|
||||||
|
seconds_ago = MIN_REGISTRATION_SECONDS + 1
|
||||||
|
with client.session_transaction() as session:
|
||||||
|
session[REGISTRATION_ISSUED_KEY] = time.time() - seconds_ago
|
||||||
|
|
||||||
|
|
||||||
|
def _account_exists(app, username='brandnew'):
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
return User.query.filter_by(username=username).first() is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestLegitimateSignUp:
|
||||||
|
def test_a_person_filling_the_form_gets_an_account(self, app, client):
|
||||||
|
client.get('/auth/register')
|
||||||
|
_issued_long_ago(client)
|
||||||
|
|
||||||
|
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
|
||||||
|
|
||||||
|
assert _account_exists(app)
|
||||||
|
|
||||||
|
def test_an_empty_honeypot_is_not_a_refusal(self, app, client):
|
||||||
|
"""A browser submits the hidden input as an empty string, not absent."""
|
||||||
|
_issued_long_ago(client)
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
'/auth/register',
|
||||||
|
data=dict(FORM, **{REGISTRATION_HONEYPOT_FIELD: ''}),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert _account_exists(app)
|
||||||
|
|
||||||
|
def test_correcting_a_typo_does_not_restart_the_clock(self, app, client):
|
||||||
|
"""The dwell timer must survive a failed attempt.
|
||||||
|
|
||||||
|
Reissuing it on every re-render would refuse the second submission of
|
||||||
|
anyone who fixes a mistake quickly — a rule that fires on real people
|
||||||
|
and not on robots, which is the wrong way round.
|
||||||
|
"""
|
||||||
|
client.get('/auth/register')
|
||||||
|
_issued_long_ago(client)
|
||||||
|
|
||||||
|
# First attempt fails validation: passwords do not match.
|
||||||
|
client.post(
|
||||||
|
'/auth/register',
|
||||||
|
data=dict(FORM, confirm_password='Different123'),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
# Corrected and sent straight back, well inside the dwell floor.
|
||||||
|
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
|
||||||
|
|
||||||
|
assert _account_exists(app)
|
||||||
|
|
||||||
|
|
||||||
|
class TestScreening:
|
||||||
|
def test_a_filled_honeypot_is_refused(self, app, client):
|
||||||
|
_issued_long_ago(client)
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
'/auth/register',
|
||||||
|
data=dict(FORM, **{REGISTRATION_HONEYPOT_FIELD: 'http://spam.example'}),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not _account_exists(app)
|
||||||
|
|
||||||
|
def test_a_form_returned_instantly_is_refused(self, app, client):
|
||||||
|
client.get('/auth/register')
|
||||||
|
|
||||||
|
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
|
||||||
|
|
||||||
|
assert not _account_exists(app)
|
||||||
|
|
||||||
|
def test_a_post_that_never_fetched_the_form_is_refused(self, app, client):
|
||||||
|
"""The strongest signal available: no form was ever issued."""
|
||||||
|
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
|
||||||
|
|
||||||
|
assert not _account_exists(app)
|
||||||
|
|
||||||
|
def test_the_two_rules_are_indistinguishable_to_the_sender(self, app, client):
|
||||||
|
"""Naming the rule tells whoever tripped it how to avoid it.
|
||||||
|
|
||||||
|
Both refusals must read identically from outside. The log, which the
|
||||||
|
sender cannot see, is where they are told apart.
|
||||||
|
|
||||||
|
One client per scenario, on purpose: the dwell stamp lives in the
|
||||||
|
session and is deliberately kept across a refusal, so reusing a single
|
||||||
|
client would carry the first scenario's backdated stamp into the
|
||||||
|
second and the "too fast" case would never fire.
|
||||||
|
"""
|
||||||
|
_issued_long_ago(client)
|
||||||
|
tripped_honeypot = client.post(
|
||||||
|
'/auth/register',
|
||||||
|
data=dict(FORM, **{REGISTRATION_HONEYPOT_FIELD: 'spam'}),
|
||||||
|
follow_redirects=True,
|
||||||
|
).get_data(as_text=True)
|
||||||
|
|
||||||
|
fresh = app.test_client()
|
||||||
|
fresh.get('/auth/register')
|
||||||
|
too_fast = fresh.post('/auth/register', data=dict(FORM), follow_redirects=True).get_data(
|
||||||
|
as_text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def flashes(page):
|
||||||
|
return [line for line in page.splitlines() if 'alert-danger' in line]
|
||||||
|
|
||||||
|
assert flashes(tripped_honeypot) == flashes(too_fast)
|
||||||
|
assert flashes(tripped_honeypot), 'no message at all is not the same as a uniform one'
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheFormItself:
|
||||||
|
def test_no_arithmetic_question_is_asked(self, client):
|
||||||
|
page = client.get('/auth/register').get_data(as_text=True)
|
||||||
|
|
||||||
|
assert 'captcha' not in page.lower()
|
||||||
|
|
||||||
|
def test_the_honeypot_is_hidden_from_assistive_technology(self, client):
|
||||||
|
"""An input a screen reader announces is a trap for a person.
|
||||||
|
|
||||||
|
aria-hidden, tabindex=-1 and display:none all have to hold. The CSS
|
||||||
|
rule lives in the stylesheet rather than in a style attribute so that
|
||||||
|
it survives a future tightening of style-src.
|
||||||
|
"""
|
||||||
|
page = client.get('/auth/register').get_data(as_text=True)
|
||||||
|
|
||||||
|
assert 'class="honeypot" aria-hidden="true"' in page
|
||||||
|
assert f'name="{REGISTRATION_HONEYPOT_FIELD}" tabindex="-1"' in page
|
||||||
|
|
||||||
|
def test_the_stylesheet_actually_hides_it(self, app):
|
||||||
|
import os
|
||||||
|
|
||||||
|
with open(os.path.join(app.static_folder, 'css', 'style.css'), encoding='utf-8') as handle:
|
||||||
|
css = handle.read()
|
||||||
|
|
||||||
|
block = css.split('.honeypot')[-1]
|
||||||
|
assert 'display: none' in block.split('}')[0]
|
||||||
|
|
||||||
|
|
||||||
|
class TestRefusalIsVisible:
|
||||||
|
def test_a_refusal_is_written_to_the_audit_log(self, app, client, monkeypatch):
|
||||||
|
"""Sign-up abuse leaves a trace or it is not happening, as far as
|
||||||
|
anyone can tell."""
|
||||||
|
recorded = []
|
||||||
|
from app.routes import auth as auth_module
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
auth_module,
|
||||||
|
'log_auth_event',
|
||||||
|
lambda event, **fields: recorded.append((event, fields)),
|
||||||
|
)
|
||||||
|
_issued_long_ago(client)
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
'/auth/register',
|
||||||
|
data=dict(FORM, **{REGISTRATION_HONEYPOT_FIELD: 'spam'}),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert recorded, 'the double was never called — the test would pass on nothing'
|
||||||
|
assert recorded[0][0] == 'account.registration_refused'
|
||||||
|
assert recorded[0][1]['reason'] == 'honeypot'
|
||||||
|
|
||||||
|
def test_the_reason_distinguishes_the_two_rules(self, app, client, monkeypatch):
|
||||||
|
recorded = []
|
||||||
|
from app.routes import auth as auth_module
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
auth_module,
|
||||||
|
'log_auth_event',
|
||||||
|
lambda event, **fields: recorded.append((event, fields)),
|
||||||
|
)
|
||||||
|
client.get('/auth/register')
|
||||||
|
|
||||||
|
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
|
||||||
|
|
||||||
|
assert recorded and recorded[0][1]['reason'] == 'too-fast'
|
||||||
@@ -19,6 +19,8 @@ These tests state the guarantee, so that removing the rollback or
|
|||||||
reintroducing a mid-operation commit fails loudly.
|
reintroducing a mid-operation commit fails loudly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.models import User, UserGamertag
|
from app.models import User, UserGamertag
|
||||||
@@ -78,9 +80,14 @@ class TestRegistrationIsOneOperation:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _submit(self, client, app, **overrides):
|
def _submit(self, client, app, **overrides):
|
||||||
|
# Registration is screened for robots (SEC-AUTH-008): the form has to
|
||||||
|
# have been issued, and long enough ago. Backdated here rather than
|
||||||
|
# slept through, so the suite does not pay three seconds per call.
|
||||||
|
from app.routes.auth import MIN_REGISTRATION_SECONDS, REGISTRATION_ISSUED_KEY
|
||||||
|
|
||||||
with client.session_transaction() as session:
|
with client.session_transaction() as session:
|
||||||
session['captcha_answer'] = 4
|
session[REGISTRATION_ISSUED_KEY] = time.time() - MIN_REGISTRATION_SECONDS - 1
|
||||||
payload = dict(self.FORM, captcha_answer='4')
|
payload = dict(self.FORM)
|
||||||
payload.update(overrides)
|
payload.update(overrides)
|
||||||
return client.post('/auth/register', data=payload, follow_redirects=True)
|
return client.post('/auth/register', data=payload, follow_redirects=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user