feat(i18n): traduire l'ensemble des gabarits

Le site s'affiche desormais integralement en francais. 483 chaines, aucune
non traduite, dans les deux catalogues.

Couvert : navigation et mise en page partagee, connexion, les cinq pages
d'erreur, et les 29 gabarits de pages.

Methode
  Marquage semi-automatique, conservateur par construction : seuls des
  motifs sans ambiguite sont balises -- contenu de balises de texte,
  attributs placeholder/title/aria-label, texte suivant une icone, blocs
  title et page_title -- et tout contenu comportant du Jinja ou du balisage
  imbrique est laisse de cote. Deux passes, la seconde pour td, li, h6,
  strong, em et caption.

  97 entrees etaient marquees fuzzy par pybabel update, c'est-a-dire
  devinees par similarite. Une entree fuzzy est **ignoree a l'execution** :
  elles ont donc ete traitees comme non traduites, et le drapeau retire une
  fois la traduction ecrite.

Deux pieges rencontres, tous deux documentes dans docs/translations.md

  Une entite HTML n'est pas du texte. ×, utilise comme libelle de
  bouton de fermeture, a ete balise par la passe automatique. Jinja
  l'echappait alors en × et le bouton aurait affiche le texte
  litteral × au lieu de la croix. Corrige dans cinq gabarits.

  Huit chaines subsistent dans des blocs <script>. Elles fonctionnent mais
  restent fragiles : Jinja echappe & < > " ' dans un bloc script, et ces
  entites n'y sont pas decodees -- une traduction contenant une apostrophe
  droite arriverait dans la chaine JavaScript sous la forme &#39;. Le
  francais retenu utilise des apostrophes typographiques, non echappees,
  donc l'existant est sur. Toute nouvelle chaine a cet endroit devra passer
  par un attribut data- ou un bloc <script type="application/json">.

Vocabulaire retenu -- a valider avec le club
  tryout -> selection · manager -> gerant · coach -> coach (conserve, terme
  d'usage en e-sport) · scout -> recruteur · email -> courriel · scrim ->
  scrim. Les noms de jeux et les postes (Support, Duelist, AWPer) restent
  en anglais : ce sont les termes employes par les joueurs.
  Ces choix vivent dans un seul catalogue, donc chacun se change en un
  endroit.

Reste a faire : les messages flash hors routes/auth.py, et les messages de
validation de app/validators.py, qui necessitent lazy_gettext puisque les
champs de schema sont construits a l'import.

Verifie : 13 pages parcourues dans les deux langues, aucune ne laisse de
marqueur Jinja non evalue ni d'entite doublement echappee. 193 tests.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-07 21:43:29 -04:00
co-authored by Claude Opus 5
parent fcb58e8a17
commit 80bc1413f1
34 changed files with 4256 additions and 708 deletions
+30 -24
View File
@@ -146,32 +146,38 @@ That second check is what keeps the migration honest: adding
## 5. State of the migration
Done: navigation, login page, the five error pages, and every flash message
in `app/routes/auth.py`.
**Templates are done.** 483 strings, all translated, in both catalogues.
The suite fails if that stops being true.
Remaining, roughly in order of how often a member sees them:
Covered: navigation and shared layout, login, the five error pages, and
every page template.
| Area | Files |
|---|---|
| Dashboard | `pages/dashboard.html` |
| Tryouts | `pages/tryouts.html`, `view_tryout.html`, `tryout_form.html` |
| Registration | `pages/register.html` |
| Profile | `pages/profile.html`, `edit_profile.html` |
| Teams | `pages/teams.html`, `my_teams.html` |
| Calendar and matches | `pages/calendar.html`, `match_form.html`, `team_matches.html` |
| Notes and One on One | `pages/notes.html`, `one_on_one.html`, `add_note.html`, … |
| Contracts | `pages/contracts.html`, `upload_contract.html` |
| Remaining flash messages | `routes/users.py`, `tryouts.py`, `teams.py`, `matches.py`, … |
### What is not covered yet
Two things to keep in mind while continuing.
**Flash messages outside `routes/auth.py`.** `users.py`, `tryouts.py`,
`teams.py`, `matches.py`, `team_matches.py` and `evaluations.py` still
build them in English. They are the largest remaining block.
**Strings inside `<script>` blocks** cannot be marked with `{{ _() }}` and
left at that — see `SEC-XSS-001`. Pass translated text through a `data-`
attribute, or a `<script type="application/json">` block read by an
external file. This lines up with the work needed to drop `unsafe-inline`
from the CSP (`OPS-010`), so the two are best done together, template by
template.
**Validation messages in `app/validators.py`.** Shown to users, not marked.
They need `lazy_gettext`, since schema fields are built at import time.
**Validation messages in `app/validators.py`** are shown to users and are
not yet marked. They need `lazy_gettext`, since schema fields are built at
import time.
**Model constants**`ESPORT_GAMES`, `GAME_POSITIONS` — are data, not
interface. Game names stay as they are; positions such as "Support" or
"Duelist" are the terms players use in English and are deliberately left
alone.
### Two traps met during this migration
**HTML entities are not text.** `&times;`, used as a close-button label,
was marked by the automated pass. Jinja escaped it to `&amp;times;`, so
the button would have shown the literal text `&times;` instead of ×.
Anything that is markup rather than prose must stay out of `_()`.
**Strings inside `<script>` blocks.** Eight remain, in `match_form.html`,
`one_on_one.html` and `coach_availability.html`. They work, but they are
fragile: Jinja escapes `& < > " '` inside a script block, and those
entities are *not* decoded there — a translation containing an apostrophe
would land in the JavaScript string as `&#39;`. The French wording uses
typographic apostrophes (), which are untouched, so the current strings
are safe. Anything added there should be passed through a `data-`
attribute or a `<script type="application/json">` block instead.