<% end %>
diff --git a/backend/app/views/shared/_language_switcher.html.erb b/backend/app/views/shared/_language_switcher.html.erb
index ba8b5df..3594d05 100644
--- a/backend/app/views/shared/_language_switcher.html.erb
+++ b/backend/app/views/shared/_language_switcher.html.erb
@@ -58,10 +58,18 @@
}
toggle.addEventListener("click", function (e) {
+ e.preventDefault();
e.stopPropagation();
setOpen(menu.hidden);
});
+ // Keep parent mobile nav open while interacting with the switcher UI.
+ root.addEventListener("click", function (e) {
+ if (e.target.closest(".lang-switcher__toggle")) {
+ e.stopPropagation();
+ }
+ });
+
document.addEventListener("click", function (e) {
if (!root.contains(e.target)) setOpen(false);
});
diff --git a/backend/app/views/shared/_marketing_nav.html.erb b/backend/app/views/shared/_marketing_nav.html.erb
index 1a34c10..3ad5ecb 100644
--- a/backend/app/views/shared/_marketing_nav.html.erb
+++ b/backend/app/views/shared/_marketing_nav.html.erb
@@ -19,6 +19,15 @@
@@ -78,8 +84,16 @@
if (backdrop) backdrop.addEventListener("click", closeMenu);
+ function shouldCloseNavOnControl(el) {
+ // Language toggle must keep the mobile menu open; only a locale choice may close it.
+ if (!el.closest("[data-lang-switcher]")) return true;
+ return el.classList.contains("lang-switcher__option");
+ }
+
nav.querySelectorAll("a, button").forEach(function (el) {
- el.addEventListener("click", closeMenu);
+ el.addEventListener("click", function () {
+ if (shouldCloseNavOnControl(el)) closeMenu();
+ });
});
window.addEventListener("resize", function () {
diff --git a/backend/config/locales/admin.de.yml b/backend/config/locales/admin.de.yml
index e3e45a3..d65159c 100644
--- a/backend/config/locales/admin.de.yml
+++ b/backend/config/locales/admin.de.yml
@@ -18,6 +18,9 @@ de:
logout_success: Abgemeldet
password_current_incorrect: Das aktuelle Passwort ist falsch
password_too_short: Das neue Passwort muss mindestens 8 Zeichen lang sein
+ password_too_long: Das neue Passwort darf höchstens 72 Zeichen lang sein
+ password_too_weak: Das neue Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
+ password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
password_mismatch: Die Passwörter stimmen nicht überein
password_updated: Passwort aktualisiert
ops_acknowledged: Vorfall übernommen
@@ -53,7 +56,7 @@ de:
edit:
title: Passwort ändern
current_password_label: Aktuelles Passwort
- new_password_label: "Neues Passwort (mind. 8 Zeichen)"
+ new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
confirm_password_label: Neues Passwort bestätigen
submit: Passwort speichern
cancel: Abbrechen
diff --git a/backend/config/locales/admin.en.yml b/backend/config/locales/admin.en.yml
index 8ef9e23..1f8d295 100644
--- a/backend/config/locales/admin.en.yml
+++ b/backend/config/locales/admin.en.yml
@@ -18,6 +18,9 @@ en:
logout_success: Signed out
password_current_incorrect: Current password is incorrect
password_too_short: The new password must be at least 8 characters long
+ password_too_long: New password cannot exceed 72 characters
+ password_too_weak: "New password must include at least 3 of: lowercase, uppercase, numbers and symbols"
+ password_same_as_current: New password must be different from the current password
password_mismatch: Passwords do not match
password_updated: Password updated
ops_acknowledged: Incident acknowledged
@@ -53,7 +56,7 @@ en:
edit:
title: Change password
current_password_label: Current password
- new_password_label: "New password (min. 8 characters)"
+ new_password_label: "New password (min. 8, at least 3 character types)"
confirm_password_label: Confirm new password
submit: Save password
cancel: Cancel
diff --git a/backend/config/locales/admin.es.yml b/backend/config/locales/admin.es.yml
index 728bc45..e1f1c9c 100644
--- a/backend/config/locales/admin.es.yml
+++ b/backend/config/locales/admin.es.yml
@@ -18,6 +18,9 @@ es:
logout_success: Sesión cerrada
password_current_incorrect: La contraseña actual no es correcta
password_too_short: La nueva contraseña debe tener al menos 8 caracteres
+ password_too_long: La nueva contraseña no puede superar los 72 caracteres
+ password_too_weak: "La nueva contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
+ password_same_as_current: La nueva contraseña debe ser distinta de la actual
password_mismatch: Las contraseñas no coinciden
password_updated: Contraseña actualizada
ops_acknowledged: Incidencia asumida
@@ -53,7 +56,7 @@ es:
edit:
title: Cambiar contraseña
current_password_label: Contraseña actual
- new_password_label: "Nueva contraseña (mín. 8 caracteres)"
+ new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
confirm_password_label: Confirma la nueva contraseña
submit: Guardar contraseña
cancel: Cancelar
diff --git a/backend/config/locales/admin.fr.yml b/backend/config/locales/admin.fr.yml
index 68725e6..a9a0915 100644
--- a/backend/config/locales/admin.fr.yml
+++ b/backend/config/locales/admin.fr.yml
@@ -18,6 +18,9 @@ fr:
logout_success: Déconnecté
password_current_incorrect: Le mot de passe actuel est incorrect
password_too_short: Le nouveau mot de passe doit contenir au moins 8 caractères
+ password_too_long: Le nouveau mot de passe ne peut pas dépasser 72 caractères
+ password_too_weak: "Le nouveau mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
+ password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
password_mismatch: Les mots de passe ne correspondent pas
password_updated: Mot de passe mis à jour
ops_acknowledged: Incident pris en charge
@@ -53,7 +56,7 @@ fr:
edit:
title: Changer le mot de passe
current_password_label: Mot de passe actuel
- new_password_label: "Nouveau mot de passe (min. 8 caractères)"
+ new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
confirm_password_label: Confirmer le nouveau mot de passe
submit: Enregistrer le mot de passe
cancel: Annuler
diff --git a/backend/config/locales/admin.it.yml b/backend/config/locales/admin.it.yml
index 466c08c..ffdf74e 100644
--- a/backend/config/locales/admin.it.yml
+++ b/backend/config/locales/admin.it.yml
@@ -18,6 +18,9 @@ it:
logout_success: Disconnesso
password_current_incorrect: Password attuale non corretta
password_too_short: La nuova password deve avere almeno 8 caratteri
+ password_too_long: La nuova password non può superare i 72 caratteri
+ password_too_weak: "La nuova password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
+ password_same_as_current: La nuova password deve essere diversa da quella attuale
password_mismatch: Le password non coincidono
password_updated: Password aggiornata
ops_acknowledged: Incidente preso in carico
@@ -53,7 +56,7 @@ it:
edit:
title: Cambia password
current_password_label: Password attuale
- new_password_label: "Nuova password (min. 8 caratteri)"
+ new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
confirm_password_label: Conferma nuova password
submit: Salva password
cancel: Annulla
diff --git a/backend/config/locales/app.de.yml b/backend/config/locales/app.de.yml
index 68b778e..63e5db2 100644
--- a/backend/config/locales/app.de.yml
+++ b/backend/config/locales/app.de.yml
@@ -1,4 +1,26 @@
de:
+ password_policy:
+ hint: "Mindestens 8 Zeichen, mit mindestens 3 aus: Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen."
+ activerecord:
+ attributes:
+ user:
+ password: Passwort
+ admin_account:
+ password: Passwort
+ errors:
+ models:
+ user:
+ attributes:
+ password:
+ too_short: "ist zu kurz (mindestens %{count} Zeichen)"
+ too_long: "ist zu lang (höchstens %{count} Zeichen)"
+ complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten"
+ admin_account:
+ attributes:
+ password:
+ too_short: "ist zu kurz (mindestens %{count} Zeichen)"
+ too_long: "ist zu lang (höchstens %{count} Zeichen)"
+ complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten"
club:
back_to_club: "← Verein"
sport_label: Hauptsportart
@@ -600,10 +622,16 @@ de:
welcome_back: Willkommen zurück!
invalid_credentials: E-Mail oder Passwort ungültig
logged_out: Abgemeldet
+ unauthorized: Nicht autorisiert
+ invalid_token: Ungültiges Token
password_resets:
email_sent: Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen des Passworts.
invalid_or_expired_link: Link ungültig oder abgelaufen. Fordere ein neues Zurücksetzen des Passworts an.
password_min_length: Das Passwort muss mindestens 8 Zeichen lang sein
+ password_too_short: Das Passwort muss mindestens 8 Zeichen lang sein
+ password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein
+ password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
+ password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
password_mismatch: Die Passwörter stimmen nicht überein
password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden.
accounts:
@@ -611,8 +639,12 @@ de:
profile_updated: Profil aktualisiert.
password_current_incorrect: Das aktuelle Passwort ist falsch
password_too_short: Das Passwort muss mindestens 8 Zeichen haben
+ password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein
+ password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
+ password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
password_mismatch: Die Passwörter stimmen nicht überein
password_updated: Passwort aktualisiert.
+ password_update_failed: Passwort konnte nicht aktualisiert werden
replay:
download_unavailable: Download nicht verfügbar
not_available: Replay nicht verfügbar
diff --git a/backend/config/locales/app.en.yml b/backend/config/locales/app.en.yml
index b5c9355..1150d77 100644
--- a/backend/config/locales/app.en.yml
+++ b/backend/config/locales/app.en.yml
@@ -1,4 +1,21 @@
en:
+ password_policy:
+ hint: "At least 8 characters, including 3 of: lowercase, uppercase, numbers and symbols."
+ activerecord:
+ errors:
+ models:
+ user:
+ attributes:
+ password:
+ too_short: "is too short (minimum is %{count} characters)"
+ too_long: "is too long (maximum is %{count} characters)"
+ complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols"
+ admin_account:
+ attributes:
+ password:
+ too_short: "is too short (minimum is %{count} characters)"
+ too_long: "is too long (maximum is %{count} characters)"
+ complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols"
club:
back_to_club: "← Club"
sport_label: Main sport
@@ -600,10 +617,16 @@ en:
welcome_back: Welcome back!
invalid_credentials: Invalid email or password
logged_out: Logged out
+ unauthorized: Unauthorized
+ invalid_token: Invalid token
password_resets:
email_sent: If the email is registered, you'll receive a password reset link shortly.
invalid_or_expired_link: Invalid or expired link. Request a new password reset.
password_min_length: Password must be at least 8 characters
+ password_too_short: Password must be at least 8 characters
+ password_too_long: Password cannot exceed 72 characters
+ password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols"
+ password_same_as_current: New password must be different from the current password
password_mismatch: Passwords don't match
password_updated: Password updated. You can now log in.
accounts:
@@ -611,8 +634,12 @@ en:
profile_updated: Profile updated.
password_current_incorrect: Current password is incorrect
password_too_short: Password must be at least 8 characters
+ password_too_long: Password cannot exceed 72 characters
+ password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols"
+ password_same_as_current: New password must be different from the current password
password_mismatch: Passwords do not match
password_updated: Password updated.
+ password_update_failed: Unable to update password
replay:
download_unavailable: Download not available
not_available: Replay not available
diff --git a/backend/config/locales/app.es.yml b/backend/config/locales/app.es.yml
index d1d984b..08c1647 100644
--- a/backend/config/locales/app.es.yml
+++ b/backend/config/locales/app.es.yml
@@ -1,4 +1,26 @@
es:
+ password_policy:
+ hint: "Mínimo 8 caracteres, con al menos 3 entre: minúsculas, mayúsculas, números y símbolos."
+ activerecord:
+ attributes:
+ user:
+ password: Contraseña
+ admin_account:
+ password: Contraseña
+ errors:
+ models:
+ user:
+ attributes:
+ password:
+ too_short: "es demasiado corta (mínimo %{count} caracteres)"
+ too_long: "es demasiado larga (máximo %{count} caracteres)"
+ complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
+ admin_account:
+ attributes:
+ password:
+ too_short: "es demasiado corta (mínimo %{count} caracteres)"
+ too_long: "es demasiado larga (máximo %{count} caracteres)"
+ complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
club:
back_to_club: "← Club"
sport_label: Deporte principal
@@ -600,10 +622,16 @@ es:
welcome_back: "¡Bienvenido de nuevo!"
invalid_credentials: Correo o contraseña no válidos
logged_out: Sesión cerrada
+ unauthorized: No autorizado
+ invalid_token: Token no válido
password_resets:
email_sent: Si el correo está registrado, recibirás en breve un enlace para restablecer la contraseña.
invalid_or_expired_link: Enlace no válido o caducado. Solicita un nuevo restablecimiento de contraseña.
password_min_length: La contraseña debe tener al menos 8 caracteres
+ password_too_short: La contraseña debe tener al menos 8 caracteres
+ password_too_long: La contraseña no puede superar los 72 caracteres
+ password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
+ password_same_as_current: La nueva contraseña debe ser distinta de la actual
password_mismatch: Las contraseñas no coinciden
password_updated: Contraseña actualizada. Ya puedes iniciar sesión.
accounts:
@@ -611,8 +639,12 @@ es:
profile_updated: Perfil actualizado.
password_current_incorrect: La contraseña actual no es correcta
password_too_short: La contraseña debe tener al menos 8 caracteres
+ password_too_long: La contraseña no puede superar los 72 caracteres
+ password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
+ password_same_as_current: La nueva contraseña debe ser distinta de la actual
password_mismatch: Las contraseñas no coinciden
password_updated: Contraseña actualizada.
+ password_update_failed: No se pudo actualizar la contraseña
replay:
download_unavailable: Descarga no disponible
not_available: Repetición no disponible
diff --git a/backend/config/locales/app.fr.yml b/backend/config/locales/app.fr.yml
index bd8a4f0..60f7f8c 100644
--- a/backend/config/locales/app.fr.yml
+++ b/backend/config/locales/app.fr.yml
@@ -1,4 +1,26 @@
fr:
+ password_policy:
+ hint: "Au moins 8 caractères, avec au moins 3 parmi : minuscules, majuscules, chiffres et symboles."
+ activerecord:
+ attributes:
+ user:
+ password: Mot de passe
+ admin_account:
+ password: Mot de passe
+ errors:
+ models:
+ user:
+ attributes:
+ password:
+ too_short: "est trop court (minimum %{count} caractères)"
+ too_long: "est trop long (maximum %{count} caractères)"
+ complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
+ admin_account:
+ attributes:
+ password:
+ too_short: "est trop court (minimum %{count} caractères)"
+ too_long: "est trop long (maximum %{count} caractères)"
+ complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
club:
back_to_club: "← Club"
sport_label: Sport principal
@@ -600,10 +622,16 @@ fr:
welcome_back: Bon retour !
invalid_credentials: E-mail ou mot de passe invalide
logged_out: Déconnecté
+ unauthorized: Non autorisé
+ invalid_token: Jeton invalide
password_resets:
email_sent: Si l'e-mail est enregistré, tu recevras bientôt un lien pour réinitialiser le mot de passe.
invalid_or_expired_link: Lien invalide ou expiré. Demande une nouvelle réinitialisation du mot de passe.
password_min_length: Le mot de passe doit comporter au moins 8 caractères
+ password_too_short: Le mot de passe doit comporter au moins 8 caractères
+ password_too_long: Le mot de passe ne peut pas dépasser 72 caractères
+ password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
+ password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
password_mismatch: Les mots de passe ne correspondent pas
password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter.
accounts:
@@ -611,8 +639,12 @@ fr:
profile_updated: Profil mis à jour.
password_current_incorrect: Le mot de passe actuel est incorrect
password_too_short: Le mot de passe doit contenir au moins 8 caractères
+ password_too_long: Le mot de passe ne peut pas dépasser 72 caractères
+ password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
+ password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
password_mismatch: Les mots de passe ne correspondent pas
password_updated: Mot de passe mis à jour.
+ password_update_failed: Impossible de mettre à jour le mot de passe
replay:
download_unavailable: Téléchargement non disponible
not_available: Replay non disponible
diff --git a/backend/config/locales/app.it.yml b/backend/config/locales/app.it.yml
index eee0e1d..7cbb6bf 100644
--- a/backend/config/locales/app.it.yml
+++ b/backend/config/locales/app.it.yml
@@ -1,4 +1,21 @@
it:
+ password_policy:
+ hint: "Minimo 8 caratteri, con almeno 3 tra: minuscole, maiuscole, numeri e simboli."
+ activerecord:
+ errors:
+ models:
+ user:
+ attributes:
+ password:
+ too_short: "è troppo corta (minimo %{count} caratteri)"
+ too_long: "è troppo lunga (massimo %{count} caratteri)"
+ complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli"
+ admin_account:
+ attributes:
+ password:
+ too_short: "è troppo corta (minimo %{count} caratteri)"
+ too_long: "è troppo lunga (massimo %{count} caratteri)"
+ complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli"
club:
back_to_club: "← Società"
sport_label: Sport principale
@@ -600,10 +617,19 @@ it:
welcome_back: Bentornato!
invalid_credentials: Email o password non validi
logged_out: Disconnesso
+ unauthorized: Non autorizzato
+ invalid_token: Token non valido
password_resets:
email_sent: Se l'email è registrata, riceverai a breve un link per reimpostare la password.
invalid_or_expired_link: Link non valido o scaduto. Richiedi un nuovo reset password.
password_min_length: La password deve avere almeno 8 caratteri
+ password_too_short: La password deve avere almeno 8 caratteri
+ password_too_long: La password non può superare i 72 caratteri
+ password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
+ password_same_as_current: La nuova password deve essere diversa da quella attuale
+ password_too_short: La password deve avere almeno 8 caratteri
+ password_too_long: La password non può superare i 72 caratteri
+ password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
password_mismatch: Le password non coincidono
password_updated: Password aggiornata. Ora puoi accedere.
accounts:
@@ -611,8 +637,14 @@ it:
profile_updated: Profilo aggiornato.
password_current_incorrect: La password attuale non è corretta
password_too_short: La password deve avere almeno 8 caratteri
+ password_too_long: La password non può superare i 72 caratteri
+ password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
+ password_same_as_current: La nuova password deve essere diversa da quella attuale
+ password_too_long: La password non può superare i 72 caratteri
+ password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
password_mismatch: Le password non coincidono
password_updated: Password aggiornata.
+ password_update_failed: Impossibile aggiornare la password
replay:
download_unavailable: Download non disponibile
not_available: Replay non disponibile
diff --git a/backend/config/locales/ui.de.yml b/backend/config/locales/ui.de.yml
index 9c3231a..88cf26c 100644
--- a/backend/config/locales/ui.de.yml
+++ b/backend/config/locales/ui.de.yml
@@ -103,7 +103,7 @@ de:
password_reset:
meta_title: "Neues Passwort — Match Live TV"
title: Neues Passwort wählen
- new_password_label: "Neues Passwort (min. 8 Zeichen)"
+ new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
submit: Passwort speichern
back_to_login: Zurück zur Anmeldung
invitation:
@@ -128,7 +128,7 @@ de:
name_label: Name
role_label: "Rolle: %{role}"
current_password_label: Aktuelles Passwort
- new_password_label: "Neues Passwort (mind. 8 Zeichen)"
+ new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
save_profile: Profil speichern
save_password: Passwort aktualisieren
common:
diff --git a/backend/config/locales/ui.en.yml b/backend/config/locales/ui.en.yml
index ea1e0ef..114682c 100644
--- a/backend/config/locales/ui.en.yml
+++ b/backend/config/locales/ui.en.yml
@@ -103,7 +103,7 @@ en:
password_reset:
meta_title: "New password — Match Live TV"
title: Choose a new password
- new_password_label: "New password (min. 8 characters)"
+ new_password_label: "New password (min. 8, at least 3 character types)"
submit: Save password
back_to_login: Back to login
invitation:
@@ -128,7 +128,7 @@ en:
name_label: Name
role_label: "Role: %{role}"
current_password_label: Current password
- new_password_label: "New password (min. 8 characters)"
+ new_password_label: "New password (min. 8, at least 3 character types)"
save_profile: Save profile
save_password: Update password
common:
diff --git a/backend/config/locales/ui.es.yml b/backend/config/locales/ui.es.yml
index d230d9a..a4b3e74 100644
--- a/backend/config/locales/ui.es.yml
+++ b/backend/config/locales/ui.es.yml
@@ -103,7 +103,7 @@ es:
password_reset:
meta_title: "Nueva contraseña — Match Live TV"
title: Elige una nueva contraseña
- new_password_label: "Nueva contraseña (mín. 8 caracteres)"
+ new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
submit: Guardar contraseña
back_to_login: Volver al inicio de sesión
invitation:
@@ -128,7 +128,7 @@ es:
name_label: Nombre
role_label: "Rol: %{role}"
current_password_label: Contraseña actual
- new_password_label: "Nueva contraseña (mín. 8 caracteres)"
+ new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
save_profile: Guardar perfil
save_password: Actualizar contraseña
common:
diff --git a/backend/config/locales/ui.fr.yml b/backend/config/locales/ui.fr.yml
index 4711125..7072601 100644
--- a/backend/config/locales/ui.fr.yml
+++ b/backend/config/locales/ui.fr.yml
@@ -103,7 +103,7 @@ fr:
password_reset:
meta_title: "Nouveau mot de passe — Match Live TV"
title: Choisissez un nouveau mot de passe
- new_password_label: "Nouveau mot de passe (8 caractères min.)"
+ new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
submit: Enregistrer le mot de passe
back_to_login: Retour à la connexion
invitation:
@@ -128,7 +128,7 @@ fr:
name_label: Nom
role_label: "Rôle : %{role}"
current_password_label: Mot de passe actuel
- new_password_label: "Nouveau mot de passe (min. 8 caractères)"
+ new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
save_profile: Enregistrer le profil
save_password: Mettre à jour le mot de passe
common:
diff --git a/backend/config/locales/ui.it.yml b/backend/config/locales/ui.it.yml
index 91e5a68..daf409c 100644
--- a/backend/config/locales/ui.it.yml
+++ b/backend/config/locales/ui.it.yml
@@ -103,7 +103,7 @@ it:
password_reset:
meta_title: "Nuova password — Match Live TV"
title: Scegli una nuova password
- new_password_label: "Nuova password (min. 8 caratteri)"
+ new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
submit: Salva password
back_to_login: Torna al login
invitation:
@@ -128,7 +128,7 @@ it:
name_label: Nome
role_label: "Ruolo: %{role}"
current_password_label: Password attuale
- new_password_label: "Nuova password (min. 8 caratteri)"
+ new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
save_profile: Salva profilo
save_password: Aggiorna password
common:
diff --git a/backend/config/routes.rb b/backend/config/routes.rb
index dd5d8cf..5b37e3a 100644
--- a/backend/config/routes.rb
+++ b/backend/config/routes.rb
@@ -169,6 +169,7 @@ Rails.application.routes.draw do
patch "password/reset", to: "password_resets#update"
get "account", to: "accounts#show", as: :account
patch "account", to: "accounts#update"
+ get "account/password", to: redirect("/account"), as: nil
patch "account/password", to: "accounts#update_password", as: :account_password
get "clubs/new", to: "clubs#new", as: :new_club
post "clubs", to: "clubs#create"
diff --git a/backend/db/seeds.rb b/backend/db/seeds.rb
index ac4d1a3..9ffd5bc 100644
--- a/backend/db/seeds.rb
+++ b/backend/db/seeds.rb
@@ -1,18 +1,18 @@
load Rails.root.join("db/seeds/plans.rb")
AdminAccount.find_or_create_by!(username: "admin") do |a|
- a.password = "admin"
+ a.password = "AdminPass123"
end
coach = User.find_or_create_by!(email: "coach@matchlivetv.test") do |u|
u.name = "Coach Demo"
- u.password = "password123"
+ u.password = "Password123"
u.role = "coach"
end
admin = User.find_or_create_by!(email: "admin@matchlivetv.test") do |u|
u.name = "Admin"
- u.password = "password123"
+ u.password = "Password123"
u.role = "admin"
end
@@ -41,5 +41,5 @@ match = team.matches.find_or_create_by!(opponent_name: "ASD Eagles Pavia") do |m
m.phase = "Semifinale"
end
-puts "Seed OK: coach@matchlivetv.test / password123"
+puts "Seed OK: coach@matchlivetv.test / Password123"
puts "Club: #{club.name}, Team: #{team.name}, Match: #{match.opponent_name}"
diff --git a/backend/public/marketing.css b/backend/public/marketing.css
index bd440db..9a80bcb 100644
--- a/backend/public/marketing.css
+++ b/backend/public/marketing.css
@@ -199,6 +199,17 @@ body.nav-menu-open { overflow: hidden; }
body.nav-menu-open .site-chrome {
z-index: 1300;
}
+ /* Keep the close control above the full-screen sheet; hide duplicate mast brand. */
+ body.nav-menu-open .site-masthead {
+ z-index: 1320;
+ background: transparent;
+ border-bottom-color: transparent;
+ backdrop-filter: none;
+ }
+ body.nav-menu-open .mast-brand {
+ visibility: hidden;
+ pointer-events: none;
+ }
.nav-backdrop {
display: block;
position: fixed;
@@ -236,16 +247,50 @@ body.nav-menu-open { overflow: hidden; }
.nav-panel {
flex: 1;
flex-direction: column;
+ flex-wrap: nowrap;
align-items: stretch;
gap: 0;
width: 100%;
max-width: none;
+ min-height: 0;
margin: 0;
- padding: 72px 24px 32px;
- padding-top: calc(72px + env(safe-area-inset-top, 0px));
+ padding: 16px 24px 32px;
+ padding-top: calc(16px + env(safe-area-inset-top, 0px));
padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px));
+ overflow-x: hidden;
overflow-y: auto;
}
+ .nav-mobile-head {
+ order: -1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ width: 100%;
+ min-height: 44px;
+ margin: 0 0 8px;
+ padding: 0 52px 16px 0; /* room for the close (X) control on the right */
+ border-bottom: 1px solid #252530;
+ flex-shrink: 0;
+ }
+ .nav-mobile-brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+ text-decoration: none;
+ line-height: 1;
+ }
+ .nav-mobile-brand:hover { text-decoration: none; opacity: 0.92; }
+ .nav-mobile-brand .brand { font-size: 1.05rem; }
+ .nav-mobile-brand-logo {
+ display: block;
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ object-fit: contain;
+ flex-shrink: 0;
+ }
.nav-panel > a,
.nav-panel .nav-link-item {
display: block;
@@ -263,11 +308,13 @@ body.nav-menu-open { overflow: hidden; }
}
.nav-actions {
flex-direction: column;
+ flex-wrap: nowrap;
align-items: stretch;
- gap: 14px;
- margin-top: 24px;
- padding: 24px 0 0;
- border-top: 1px solid #252530;
+ gap: 0;
+ margin-top: 8px;
+ padding: 0;
+ border-top: none;
+ width: 100%;
}
.nav-actions .nav-link-item {
display: block;
@@ -289,10 +336,14 @@ body.nav-menu-open { overflow: hidden; }
border-radius: 10px;
}
.nav-lang {
- margin-left: 0;
- margin-top: 4px;
+ margin: 0;
justify-content: flex-end;
- width: 100%;
+ width: auto;
+ flex-shrink: 0;
+ }
+ .nav-lang .lang-switcher__menu {
+ /* Keep the list inside the open mobile sheet */
+ z-index: 1320;
}
}
@@ -342,14 +393,22 @@ body.nav-menu-open { overflow: hidden; }
flex-wrap: nowrap;
gap: 8px 20px;
}
+ .nav-mobile-head {
+ display: contents;
+ }
+ .nav-mobile-brand {
+ display: none;
+ }
+ .nav-lang {
+ order: 2;
+ margin-left: 2px;
+ }
.nav-actions {
+ order: 1;
flex-wrap: nowrap;
gap: 8px 12px;
margin-left: auto;
}
- .nav-lang {
- margin-left: 2px;
- }
.nav-backdrop { display: none !important; }
}
.btn { display: inline-block; padding: 10px 18px; border-radius: 8px; font-weight: 700; font-size: 0.9rem; border: none; cursor: pointer; text-decoration: none; }
@@ -1230,7 +1289,22 @@ body.nav-menu-open { overflow: hidden; }
.hero-split .hero-cta { flex-direction: column; }
.hero-split .hero-cta .btn { width: 100%; text-align: center; }
}
-.section { padding: 40px 0; }
+.section {
+ padding-top: 40px;
+ padding-bottom: 40px;
+}
+/* Keep horizontal inset when .section shares a node with .wrap (padding shorthand must not win). */
+.section.wrap {
+ padding-left: 20px;
+ padding-right: 20px;
+}
+.table-scroll {
+ width: 100%;
+ max-width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ margin-top: 20px;
+}
.section h2 { font-size: 1.6rem; margin: 0 0 20px; text-align: center; }
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
.step { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 22px; }
@@ -1363,7 +1437,7 @@ body.nav-menu-open { overflow: hidden; }
.site-footer__legal { flex: 1 1 100%; margin-top: 4px; }
.site-footer__legal p { margin: 0 0 6px; line-height: 1.45; }
.site-footer__legal p:last-child { margin-bottom: 0; }
-.compare-table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 0.9rem; }
+.compare-table { width: 100%; min-width: 520px; border-collapse: collapse; margin-top: 0; font-size: 0.9rem; }
.compare-table th, .compare-table td { padding: 10px 12px; border-bottom: 1px solid #2a2a36; text-align: left; }
.compare-table th { color: #aaa; font-weight: 600; }
.billing-documents h2 { margin-top: 0; }
@@ -1372,11 +1446,14 @@ body.nav-menu-open { overflow: hidden; }
.card { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 20px; margin-bottom: 16px; }
.seo-prose { max-width: 720px; margin: 0 auto; color: #bbb; line-height: 1.65; }
-.seo-prose h2 { color: #fff; font-size: 1.25rem; margin: 28px 0 12px; }
+.seo-prose h2 { color: #fff; font-size: 1.25rem; margin: 28px 0 12px; text-align: left; }
.seo-prose h2:first-child { margin-top: 0; }
.seo-prose p { margin: 0 0 14px; }
.seo-prose ul { margin: 0 0 16px; padding-left: 1.25rem; }
.seo-prose a { color: #e53935; }
+@media (max-width: 899px) {
+ .seo-prose h2 { font-size: 1.15rem; line-height: 1.35; }
+}
.seo-page .seo-lead { color: #aaa; max-width: 640px; line-height: 1.55; margin-bottom: 28px; }
.faq-list { max-width: 720px; margin: 0 auto; }
.faq-item {
@@ -1412,7 +1489,8 @@ body.nav-menu-open { overflow: hidden; }
.legal-doc a { color: #e53935; }
.legal-meta { color: #888; font-size: 0.88rem; margin-bottom: 24px; }
.legal-back { margin-top: 32px; }
-.legal-table { width: 100%; border-collapse: collapse; margin: 12px 0 16px; font-size: 0.88rem; }
+.legal-doc .table-scroll { margin: 12px 0 16px; }
+.legal-table { width: 100%; min-width: 560px; border-collapse: collapse; margin: 0; font-size: 0.88rem; }
.legal-table th, .legal-table td { border: 1px solid #2a2a36; padding: 10px 12px; text-align: left; vertical-align: top; }
.legal-table th { background: #14141c; color: #ccc; }
.legal-accept {
@@ -1447,6 +1525,19 @@ body.nav-menu-open { overflow: hidden; }
.auth-forgot a { color: #e53935; }
table.data { width: 100%; border-collapse: collapse; }
table.data th, table.data td { padding: 8px; border-bottom: 1px solid #2a2a36; text-align: left; }
+/* Wide roster/match tables: scroll inside the card instead of expanding the page. */
+@media (max-width: 899px) {
+ .card:has(table.data),
+ .team-streaming-staff:has(table.data),
+ .billing-documents:has(table.data) {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ max-width: 100%;
+ }
+ table.data {
+ min-width: 520px;
+ }
+}
input, select {
width: 100%;
padding: 12px 14px;
diff --git a/backend/spec/models/password_complexity_spec.rb b/backend/spec/models/password_complexity_spec.rb
new file mode 100644
index 0000000..4e928f9
--- /dev/null
+++ b/backend/spec/models/password_complexity_spec.rb
@@ -0,0 +1,45 @@
+require "rails_helper"
+
+RSpec.describe PasswordComplexity do
+ describe ".violation" do
+ it "accepts a password with 3 character classes" do
+ expect(described_class.violation("NewPass123")).to be_nil
+ end
+
+ it "accepts symbols instead of one letter class" do
+ expect(described_class.violation("newpass1!")).to be_nil
+ end
+
+ it "rejects passwords that are too short" do
+ expect(described_class.violation("Ab1!")).to eq(:too_short)
+ end
+
+ it "rejects passwords with fewer than 3 classes" do
+ expect(described_class.violation("password123")).to eq(:too_weak)
+ expect(described_class.violation("PASSWORD123")).to eq(:too_weak)
+ expect(described_class.violation("Password")).to eq(:too_weak)
+ end
+ end
+
+ describe ".same_as_current?" do
+ let!(:user) { User.create!(email: "same@example.com", name: "Same", password: "Password123", role: "coach") }
+
+ it "detects when the new password matches the current one" do
+ expect(described_class.same_as_current?(user, "Password123")).to eq(true)
+ expect(described_class.same_as_current?(user, "OtherPass123")).to eq(false)
+ end
+ end
+
+ describe "User validation" do
+ it "blocks weak passwords on create" do
+ user = User.new(email: "weak@example.com", name: "Weak", password: "password123", role: "coach")
+ expect(user).not_to be_valid
+ expect(user.errors[:password]).to be_present
+ end
+
+ it "allows strong passwords on create" do
+ user = User.new(email: "strong@example.com", name: "Strong", password: "NewPass123", role: "coach")
+ expect(user).to be_valid
+ end
+ end
+end
diff --git a/backend/spec/requests/account_spec.rb b/backend/spec/requests/account_spec.rb
index a008fdb..2dc7742 100644
--- a/backend/spec/requests/account_spec.rb
+++ b/backend/spec/requests/account_spec.rb
@@ -1,9 +1,9 @@
require "rails_helper"
RSpec.describe "Account API", type: :request do
- let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "password123", role: "coach") }
+ let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "Password123", role: "coach") }
let(:auth_headers) do
- post "/api/v1/auth/login", params: { email: user.email, password: "password123" }
+ post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
token = JSON.parse(response.body).fetch("access_token")
{ "Authorization" => "Bearer #{token}" }
end
@@ -40,29 +40,67 @@ RSpec.describe "Account API", type: :request do
it "changes the password with the current password" do
patch "/api/v1/account/password",
params: {
- current_password: "password123",
- password: "newpass123",
- password_confirmation: "newpass123"
+ current_password: "Password123",
+ password: "NewPass123",
+ password_confirmation: "NewPass123"
},
headers: auth_headers
expect(response).to have_http_status(:ok)
- expect(user.reload.authenticate("newpass123")).to be_truthy
+ expect(user.reload.authenticate("NewPass123")).to be_truthy
end
it "rejects an incorrect current password" do
patch "/api/v1/account/password",
params: {
current_password: "wrong",
- password: "newpass123",
- password_confirmation: "newpass123"
+ password: "NewPass123",
+ password_confirmation: "NewPass123"
},
headers: auth_headers
expect(response).to have_http_status(:unprocessable_entity)
- expect(user.reload.authenticate("password123")).to be_truthy
+ expect(user.reload.authenticate("Password123")).to be_truthy
+ end
+
+ it "rejects a password that fails complexity rules" do
+ patch "/api/v1/account/password",
+ params: {
+ current_password: "Password123",
+ password: "newpass123",
+ password_confirmation: "newpass123"
+ },
+ headers: auth_headers.merge("Accept-Language" => "en")
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(JSON.parse(response.body)["error"]).to match(/3 of/i)
+ expect(user.reload.authenticate("Password123")).to be_truthy
+ end
+
+ it "rejects reusing the current password" do
+ patch "/api/v1/account/password",
+ params: {
+ current_password: "Password123",
+ password: "Password123",
+ password_confirmation: "Password123"
+ },
+ headers: auth_headers.merge("Accept-Language" => "en")
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(JSON.parse(response.body)["error"]).to match(/different/i)
+ expect(user.reload.authenticate("Password123")).to be_truthy
+ end
+
+ it "localizes password errors from Accept-Language" do
+ patch "/api/v1/account/password",
+ params: {
+ current_password: "Password123",
+ password: "Password123",
+ password_confirmation: "Password123"
+ },
+ headers: auth_headers.merge("Accept-Language" => "it")
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(JSON.parse(response.body)["error"]).to eq("La nuova password deve essere diversa da quella attuale")
end
end
- describe "POST /api/v1/auth/password/forgot" do
+ describe "POST /api/v1/auth/password/forgot" do
it "always returns ok and sends mail when the user exists" do
expect {
post "/api/v1/auth/password/forgot", params: { email: user.email }
@@ -77,5 +115,13 @@ RSpec.describe "Account API", type: :request do
}.not_to change { ActionMailer::Base.deliveries.size }
expect(response).to have_http_status(:ok)
end
+
+ it "localizes the response message from Accept-Language" do
+ post "/api/v1/auth/password/forgot",
+ params: { email: user.email },
+ headers: { "Accept-Language" => "fr" }
+ expect(response).to have_http_status(:ok)
+ expect(JSON.parse(response.body)["message"]).to include("réinitialiser")
+ end
end
end
diff --git a/backend/spec/requests/auth_spec.rb b/backend/spec/requests/auth_spec.rb
index ea29b2e..26c43bf 100644
--- a/backend/spec/requests/auth_spec.rb
+++ b/backend/spec/requests/auth_spec.rb
@@ -1,10 +1,10 @@
require "rails_helper"
RSpec.describe "Auth API", type: :request do
- let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "password123", role: "coach") }
+ let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "Password123", role: "coach") }
it "logs in with valid credentials" do
- post "/api/v1/auth/login", params: { email: user.email, password: "password123" }
+ post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to have_key("access_token")
end
diff --git a/docs/IOS_PASSWORD_POLICY_ALIGN.md b/docs/IOS_PASSWORD_POLICY_ALIGN.md
new file mode 100644
index 0000000..d583452
--- /dev/null
+++ b/docs/IOS_PASSWORD_POLICY_ALIGN.md
@@ -0,0 +1,136 @@
+# iOS — allineamento password policy + errori API localizzati
+
+Documento operativo per **Cursor su Mac**: allineare l’app iOS a backend/Android dopo il ramo `feature/password-policy` (merge su `main`).
+
+**Aggiornato:** 2026-08-08
+**Android di riferimento (testato su emulatore → API locale):** `2.0.10-native` (`versionCode` **31**)
+**iOS oggi:** marketing `2.0.5` / build `26`
+**API produzione:** `https://www.matchlivetv.it`
+
+---
+
+## Contesto (già in produzione backend dopo questo rilascio)
+
+### Policy password (server)
+
+Definita in `backend/app/models/concerns/password_complexity.rb`:
+
+- minimo **8** caratteri, massimo **72** byte (bcrypt)
+- almeno **3 classi su 4**: minuscole, maiuscole, numeri, simboli
+- in **cambio password** e **reset**: la nuova password **non** può coincidere con quella attuale
+
+Validazione ActiveModel su `User` / `AdminAccount`. Endpoint che la applicano:
+
+| Endpoint | Note |
+|----------|------|
+| `PATCH /api/v1/account/password` | App native + stessi codici errore |
+| `PATCH /account/password` (web) | Flash I18n |
+| reset password pubblico | Stesse regole |
+| registrazione (`auth/register`) | Validazione modello |
+
+### Errori API localizzati
+
+`ApplicationController#set_api_locale` legge, in ordine:
+
+1. header `X-Locale`
+2. `Accept-Language`
+3. `I18n.default_locale` (fallback; **retrocompatibile** se l’app vecchia non manda nulla)
+
+Risposta invariata: `{ "error": "" }` (o `{ "message": "..." }` su alcuni successi).
+
+Chiavi rilevanti (`backend/config/locales/app.{it,en,fr,de,es}.yml`):
+
+- `flash.accounts.password_too_short`
+- `flash.accounts.password_too_long`
+- `flash.accounts.password_too_weak`
+- `flash.accounts.password_same_as_current`
+- `flash.accounts.password_current_incorrect`
+- `flash.accounts.password_mismatch`
+- `flash.accounts.password_updated` / `name_required`
+- analoghi in `flash.password_resets.*` e `flash.sessions.*`
+
+---
+
+## Stato iOS vs Android (dopo il merge di questo branch)
+
+| Area | Android 2.0.10 | iOS (repo dopo merge) | Da fare su Mac |
+|------|----------------|------------------------|----------------|
+| Header `Accept-Language` su tutte le request API | `AppContainer` OkHttp interceptor | `MatchLiveAPI.request` + `multipartPatch` | **Verificare** in debug che parta su login/account/password; se manca su altri helper HTTP, allineare |
+| Hint UI nuova password | `account_new_password` in `values*` (5 lingue) | `account.new.password` in `AppLanguage.swift` (già allineato al testo policy) | OK — rivedi solo se cambi copy |
+| Schermata Account | Mostra `error` dal body HTTP | `AccountScreen` + `APIError` estrae `error`/`message` | **Test manuale** (vedi sotto) |
+| Parsing errori API | Regex su `"error"` | `APIError.friendlyHttpMessage` | OK |
+| Client-side pre-check complessità | No (si affida al server) | No | Opzionale: stessa regola di `PasswordComplexity` prima della PATCH |
+| Versione App Store | `2.0.10-native` / 31 | `2.0.5` / 26 | **Bump** marketing + build prima del submit TestFlight/App Store |
+| Signup in-app | Non è il flusso principale | Nessuna UI register dedicata | N/A (signup resta web) |
+
+### File già toccati / da tenere
+
+```
+native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift # Accept-Language
+native/ios/MatchLiveTv/Core/AppLanguage.swift # hint account.new.password (5 lingue)
+native/ios/MatchLiveTv/UI/Account/AccountScreen.swift
+native/ios/MatchLiveTv/Core/UserFacingError.swift
+native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift # enum APIError
+```
+
+Riferimento Android:
+
+```
+native/android/.../data/AppContainer.kt
+native/android/.../ui/account/AccountScreen.kt
+native/android/app/src/main/res/values*/strings.xml # account_new_password
+```
+
+---
+
+## Checklist operativa su Mac
+
+1. **Pull** `main` (dopo merge/push da Linux).
+2. Apri `native/ios/MatchLiveTv.xcodeproj`.
+3. Build debug con `API_BASE_URL=https://www.matchlivetv.it` (o locale se hai tunnel).
+4. **Test Account** (credenziali demo se disponibili, altrimenti account reale di test):
+ - password debole (`password`) → messaggio *too_weak* nella lingua UI
+ - password = corrente → *same_as_current*
+ - conferma diversa → *mismatch*
+ - password valida nuova → successo; login successivo ok
+5. Cambia lingua app (globo) e ripeti un errore: il testo API deve seguire `Accept-Language` / lingua scelta.
+6. **Bump versione** allineata ad Android se rilasci store: es. marketing `2.0.10`, build `≥ 31`.
+7. (Opzionale) Unit test Swift che replica `PasswordComplexity.strong_enough?` se aggiungi validazione client.
+
+### Comandi utili
+
+```bash
+# Clone/pull
+git checkout main && git pull
+
+# Release iOS (script repo)
+./scripts/build_ios_release.sh
+# oppure API staging:
+# API_BASE_URL=https://www.matchlivetv.it ./scripts/build_ios_release.sh
+```
+
+### Cosa non serve rifare su iOS
+
+- Fix CSS/menu mobile web, tabelle scroll, logo nel drawer: **solo sito**.
+- Redirect `GET /account/password` → `/account`: **solo web**.
+- Seed password / concern Rails: già lato server.
+
+---
+
+## Retrocompatibilità (per release iOS)
+
+- App iOS **vecchie** (senza `Accept-Language`): continuano a funzionare; messaggi API nella locale default del server.
+- Login con password già esistenti: invariato.
+- Solo **impostazione/cambio** password debole viene rifiutato (stesso JSON `error`).
+
+---
+
+## Criterio “fatto”
+
+- [ ] `Accept-Language` presente su login, me, account, change password, forgot (Charles/Proxyman o log)
+- [ ] Errori password in it/en/fr/de/es coerenti col backend
+- [ ] Hint campo nuova password parla di “min. 8 / 3 tipi”
+- [ ] Versione bumpata se lo store release è in scope
+- [ ] Nessuna regressione login / lista partite / broadcast
+
+Quando completo, aggiorna questo file con data + versione iOS rilasciata.
diff --git a/native/android/.gitignore b/native/android/.gitignore
index f2cfc63..4f6e5dd 100644
--- a/native/android/.gitignore
+++ b/native/android/.gitignore
@@ -5,6 +5,7 @@
*.iml
/captures/
/app/build/
+/dist/
.DS_Store
keystore.properties
keystore/
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/AppContainer.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/AppContainer.kt
index e1450b1..f15b262 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/AppContainer.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/AppContainer.kt
@@ -2,6 +2,7 @@ package com.matchlivetv.match_live_tv.data
import android.content.Context
import com.matchlivetv.match_live_tv.core.AppConfig
+import com.matchlivetv.match_live_tv.core.AppLocale
import com.matchlivetv.match_live_tv.core.TokenStore
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
import com.matchlivetv.match_live_tv.data.cable.SessionCableService
@@ -37,10 +38,20 @@ class AppContainer(context: Context) {
accessToken?.let { header("Authorization", "Bearer $it") }
header("Accept", "application/json")
header("Content-Type", "application/json")
+ header("Accept-Language", apiLanguageTag())
}.build()
chain.proceed(request)
}
+ private fun apiLanguageTag(): String {
+ val tag = AppLocale.currentTag(appContext)
+ return if (tag.isBlank()) {
+ java.util.Locale.getDefault().language.ifBlank { "it" }
+ } else {
+ tag
+ }
+ }
+
private val okHttp = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
diff --git a/native/android/app/src/main/res/values-de/strings.xml b/native/android/app/src/main/res/values-de/strings.xml
index 444ac2c..34dba73 100644
--- a/native/android/app/src/main/res/values-de/strings.xml
+++ b/native/android/app/src/main/res/values-de/strings.xml
@@ -252,7 +252,7 @@ Dies kann nicht rückgängig gemacht werden.
NameRolle: %1$sAktuelles Passwort
- Neues Passwort (mind. 8 Zeichen)
+ Neues Passwort (mind. 8, mindestens 3 Zeichenarten)Passwort bestätigenProfil speichernPasswort aktualisieren
diff --git a/native/android/app/src/main/res/values-en/strings.xml b/native/android/app/src/main/res/values-en/strings.xml
index d6148fb..7612762 100644
--- a/native/android/app/src/main/res/values-en/strings.xml
+++ b/native/android/app/src/main/res/values-en/strings.xml
@@ -252,7 +252,7 @@ This cannot be undone.
NameRole: %1$sCurrent password
- New password (min. 8 characters)
+ New password (min. 8, at least 3 character types)Confirm passwordSave profileUpdate password
diff --git a/native/android/app/src/main/res/values-es/strings.xml b/native/android/app/src/main/res/values-es/strings.xml
index 38b39e8..70f3564 100644
--- a/native/android/app/src/main/res/values-es/strings.xml
+++ b/native/android/app/src/main/res/values-es/strings.xml
@@ -252,7 +252,7 @@ Esta acción no se puede deshacer.
NombreRol: %1$sContraseña actual
- Nueva contraseña (mín. 8 caracteres)
+ Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)Confirmar contraseñaGuardar perfilActualizar contraseña
diff --git a/native/android/app/src/main/res/values-fr/strings.xml b/native/android/app/src/main/res/values-fr/strings.xml
index bdd5c90..6a4a6fd 100644
--- a/native/android/app/src/main/res/values-fr/strings.xml
+++ b/native/android/app/src/main/res/values-fr/strings.xml
@@ -252,7 +252,7 @@ Cette action est irréversible.
NomRôle : %1$sMot de passe actuel
- Nouveau mot de passe (min. 8 caractères)
+ Nouveau mot de passe (min. 8, au moins 3 types de caractères)Confirmer le mot de passeEnregistrer le profilMettre à jour le mot de passe
diff --git a/native/android/app/src/main/res/values/strings.xml b/native/android/app/src/main/res/values/strings.xml
index 508eb18..15842f1 100644
--- a/native/android/app/src/main/res/values/strings.xml
+++ b/native/android/app/src/main/res/values/strings.xml
@@ -252,7 +252,7 @@ L\'operazione non si può annullare.
NomeRuolo: %1$sPassword attuale
- Nuova password (min. 8 caratteri)
+ Nuova password (min. 8, almeno 3 tipi di caratteri)Conferma passwordSalva profiloAggiorna password
diff --git a/native/ios/MatchLiveTv/Core/AppLanguage.swift b/native/ios/MatchLiveTv/Core/AppLanguage.swift
index f2782f3..3cf4b5b 100644
--- a/native/ios/MatchLiveTv/Core/AppLanguage.swift
+++ b/native/ios/MatchLiveTv/Core/AppLanguage.swift
@@ -81,7 +81,7 @@ enum L10n {
"account.confirm.password": "Conferma password",
"account.current.password": "Password attuale",
"account.name.label": "Nome",
- "account.new.password": "Nuova password (min. 8 caratteri)",
+ "account.new.password": "Nuova password (min. 8, almeno 3 tipi di caratteri)",
"account.password.heading": "Cambia password",
"account.password.saved": "Password aggiornata",
"account.profile.heading": "Profilo",
@@ -372,7 +372,7 @@ enum L10n {
"account.confirm.password": "Confirm password",
"account.current.password": "Current password",
"account.name.label": "Name",
- "account.new.password": "New password (min. 8 characters)",
+ "account.new.password": "New password (min. 8, at least 3 character types)",
"account.password.heading": "Change password",
"account.password.saved": "Password updated",
"account.profile.heading": "Profile",
@@ -663,7 +663,7 @@ enum L10n {
"account.confirm.password": "Confirmer le mot de passe",
"account.current.password": "Mot de passe actuel",
"account.name.label": "Nom",
- "account.new.password": "Nouveau mot de passe (min. 8 caractères)",
+ "account.new.password": "Nouveau mot de passe (min. 8, au moins 3 types de caractères)",
"account.password.heading": "Changer le mot de passe",
"account.password.saved": "Mot de passe mis à jour",
"account.profile.heading": "Profil",
@@ -954,7 +954,7 @@ enum L10n {
"account.confirm.password": "Passwort bestätigen",
"account.current.password": "Aktuelles Passwort",
"account.name.label": "Name",
- "account.new.password": "Neues Passwort (mind. 8 Zeichen)",
+ "account.new.password": "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)",
"account.password.heading": "Passwort ändern",
"account.password.saved": "Passwort aktualisiert",
"account.profile.heading": "Profil",
@@ -1245,7 +1245,7 @@ enum L10n {
"account.confirm.password": "Confirmar contraseña",
"account.current.password": "Contraseña actual",
"account.name.label": "Nombre",
- "account.new.password": "Nueva contraseña (mín. 8 caracteres)",
+ "account.new.password": "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)",
"account.password.heading": "Cambiar contraseña",
"account.password.saved": "Contraseña actualizada",
"account.profile.heading": "Perfil",
diff --git a/native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift b/native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift
index d76b3ba..fa2f3d7 100644
--- a/native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift
+++ b/native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift
@@ -240,6 +240,7 @@ final class MatchLiveAPI: @unchecked Sendable {
request.httpMethod = "PATCH"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
+ request.setValue(AppLanguage.resolvedCode, forHTTPHeaderField: "Accept-Language")
if let accessToken { request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") }
request.httpBody = MultipartBuilder.build(fields: fields, boundary: boundary)
return try await execute(request)
@@ -253,6 +254,7 @@ final class MatchLiveAPI: @unchecked Sendable {
var request = URLRequest(url: baseURL.appendingPathComponent(path))
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Accept")
+ request.setValue(AppLanguage.resolvedCode, forHTTPHeaderField: "Accept-Language")
if body != nil {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try ApiInstant.encoder.encode(body)