BACKLOG_I18N_LOCALE.md 6821 octets

Backlog — Propagation de la locale utilisateur

Status : ticket différé, à planifier comme chantier dédié. Origine : audit anti-slop post-AddTeams (24 avril 2026) — 9 TODO recensés, tous pré-existants (aucun introduit par la branche AddTeams).

Objectif : remplacer les locales hardcodées Some("fr".to_string()) par la locale réelle de l'utilisateur (préférence DB → cookie → Accept-Language → défaut).


1. Contexte

Plusieurs services envoient des emails transactionnels en utilisant des templates i18n (EmailValidationTemplate, PasswordResetEmailTemplate, WelcomeEmailTemplate, PasswordChangeConfirmationEmailTemplate). Aujourd'hui, la locale passée au template est hardcodée à "fr" avec un TODO. Conséquence : un utilisateur qui a choisi en dans ses préférences reçoit malgré tout un mail en français.

Par ailleurs, l'extracteur Axum Locale n'implémente qu'une partie de la priorité prévue (cookie + Accept-Language) — la lecture de la préférence DB reste à faire.

2. Inventaire des TODO (9)

#FichierLigneContenuImpact
1crates/rustwarden-core/src/i18n/extractor.rs57T009 - Implement full locale extraction logicL'extracteur Locale ne lit jamais user_preferences.preferred_locale. Priorité 1 (source de vérité).
2crates/rustwarden-core/src/services/email_validation_service.rs100Get from user preferences or Accept-Language headerEmail de validation d'adresse toujours en fr.
3crates/rustwarden-core/src/services/email_validation_service.rs220Get from user preferencesEmail de bienvenue toujours en fr.
4crates/rustwarden-core/src/services/password_reset_service.rs115Get from user preferences or Accept-Language headerEmail de reset mot de passe toujours en fr.
5crates/rustwarden-core/src/tasks/password_change_email_retry.rs197Get from user preferencesEmail de confirmation de changement mot de passe (retry) toujours en fr.
6crates/rustwarden-core/src/services/refresh_token_service.rs548Store user_agent in refresh_token tableChamp device_info toujours None dans la liste des sessions actives.
7crates/rustwarden-core/src/services/refresh_token_service.rs549Store IP in refresh_token tableChamp ip_address toujours None.
8crates/rustwarden-core/src/services/refresh_token_service.rs590Blacklist associated JWT if applicableLa révocation d'une session ne blackliste pas le JWT.
9crates/rustwarden-ui/src/handlers/email_handler.rs102Get total count for pagination (requires separate query)La pagination de la file d'emails retourne emails.len() au lieu d'un COUNT(*).

Les items #6, #7, #8 et #9 ne sont pas strictement i18n mais font partie du même backlog "dette technique pré-existante" ; on les garde groupés ici pour un chantier unique "propagation métadonnées requête → services".

3. Ébauche de solution

3.1 Couche extracteur (item #1)

Enrichir Locale::from_request_parts pour lire la préférence DB quand un Claims est disponible :

priorité :
  1. user_preferences.preferred_locale (si Claims présent + DB accessible via Extension<DatabaseConnection>)
  2. cookie `locale`
  3. Accept-Language header
  4. "fr"

Contrainte : from_request_parts ne peut pas faire de requête DB sans extracteur d'état. Deux options :

  • (A) garder Locale sans DB et ajouter un helper Locale::resolve(claims, db, headers) appelable dans les services.
  • (B) créer un Extractor supplémentaire UserLocale qui dépend de AuthUser + Extension<DatabaseConnection>.

Préférence à discuter lors du chantier : option (A) pour garder Locale purement HTTP, et ajouter un petit service LocaleResolver injecté dans les services emails.

3.2 Services emails (items #2, #3, #4, #5)

Remplacer Some("fr".to_string()) par un paramètre locale: Option<String> passé par l'appelant. Les callers sont :

  • EmailValidationService::send_validation_email — appelé après POST /register : on connaît l'utilisateur, on peut lire user_preferences.
  • PasswordResetService::request_reset — appelé depuis /forgot-password (anonyme) : fallback sur Accept-Language depuis le handler.
  • Task PasswordChangeEmailRetryProcessor — appelé périodiquement : doit lire user_preferences du user_id stocké en queue.

Signature cible (exemple) :

pub async fn send_validation_email(
    &self,
    db: &DatabaseConnection,
    user: &user::Model,
    locale: Option<String>, // déjà résolu par l'appelant
) -> Result<(), AppError>

3.3 Stockage métadonnées session (items #6, #7, #8)

  • Migration : ajouter user_agent VARCHAR(512) et ip_address VARCHAR(45) à refresh_token.
  • Handler /login : extraire User-Agent + IP (x-forwarded-for ou ConnectInfo) et passer à RefreshTokenService::create.
  • revoke_session : si refresh_token.jwt_id existe, appeler JwtBlacklistService::blacklist.

3.4 Pagination emails (item #9)

Ajouter une requête COUNT(*) dans email_handler::list et retourner le vrai total.

4. Critères d'acceptation

  • Tous les TODO listés supprimés (ou déplacés vers un backlog plus précis)
  • Un utilisateur avec preferred_locale = "en" reçoit tous les emails en anglais
  • ActiveSession.device_info et ActiveSession.ip_address renseignés pour les nouvelles sessions
  • Révoquer une session blackliste le JWT associé (testé via cargo test)
  • Pagination /admin/emails retourne un total correct même avec offset > len

5. Estimation

  • Refacto locale + tests : ~1 j
  • Migration refresh_token + wiring login + blacklist : ~0.5 j
  • Pagination emails : ~1 h
  • Relecture PO + QA navigateur : ~0.5 j

Total ≈ 2-3 jours de dev.

6. Hors-scope

  • Ajout de locales (IT/PT déjà ajoutés dans la branche AddTeams)
  • UI de sélection de langue (déjà existante dans settings/preferences)
  • Conversion des logs / messages d'erreur internes (reste en anglais par convention tech)