Aggiunge i18n IT/EN/FR/DE/ES, pagine pubbliche squadre e UX archivio.

Il selettore lingua funziona sul web e sulle app native; su Android la preferenza è persistita e applicata al riavvio. Incluse anche eliminazione replay a scope e campi pagina pubblica squadra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-23 19:30:44 +02:00
co-authored by Cursor
parent 7d5d66840f
commit 1d97271555
71 changed files with 3509 additions and 197 deletions
@@ -9,6 +9,8 @@ struct LoginScreen: View {
@State private var error: String?
@State private var loading = false
@State private var passwordVisible = false
@State private var showLanguagePicker = false
@State private var languageTick = 0
var body: some View {
MatchScreenScaffold {
@@ -16,17 +18,17 @@ struct LoginScreen: View {
VStack(spacing: 0) {
MatchLiveWordmark(showSlogan: true)
.padding(.top, 32)
Text("ACCEDI")
Button(L10n.t("language.label")) {
showLanguagePicker = true
}
.foregroundStyle(MatchColors.textSecondary)
.padding(.top, 16)
Text(L10n.t("login.submit").uppercased())
.font(MatchTypography.headlineMedium)
.padding(.top, 48)
Text("Gestisci le dirette della tua squadra")
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
.multilineTextAlignment(.center)
.padding(.top, 8)
.padding(.top, 32)
VStack(spacing: 16) {
MatchTextField(title: "Email", text: $email, placeholder: "coach@squadra.it", keyboard: .emailAddress)
MatchSecureField(title: "Password", text: $password, visible: $passwordVisible)
MatchTextField(title: L10n.t("login.email"), text: $email, placeholder: "coach@squadra.it", keyboard: .emailAddress)
MatchSecureField(title: L10n.t("login.password"), text: $password, visible: $passwordVisible)
}
.padding(.top, 32)
if let error {
@@ -36,7 +38,7 @@ struct LoginScreen: View {
.padding(.top, 12)
}
MatchPrimaryButton(
label: "ACCEDI",
label: L10n.t("login.submit").uppercased(),
action: submitLogin,
enabled: !email.isEmpty && !password.isEmpty,
loading: loading
@@ -47,6 +49,13 @@ struct LoginScreen: View {
.padding(.bottom, 32)
}
}
.sheet(isPresented: $showLanguagePicker) {
LanguagePickerView()
}
.onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
languageTick += 1
}
.id(languageTick)
}
private func submitLogin() {
@@ -59,12 +68,12 @@ struct LoginScreen: View {
onLoggedIn()
} catch {
if let apiError = error as? APIError, case .unauthorized = apiError {
self.error = "Email o password non corretti"
self.error = L10n.t("login.error.credentials")
} else if let message = UserFacingError.message(for: error),
message.localizedCaseInsensitiveContains("timeout") {
self.error = "Server non raggiungibile. Verifica la connessione."
self.error = L10n.t("login.error.unreachable")
} else {
self.error = UserFacingError.message(for: error) ?? "Accesso non riuscito"
self.error = UserFacingError.message(for: error) ?? L10n.t("login.error.generic")
}
}
loading = false
@@ -20,6 +20,8 @@ struct MatchesScreen: View {
@State private var resumeMatch: Match?
@State private var deleteMatch: Match?
@State private var snackbar: String?
@State private var showLanguagePicker = false
@State private var languageTick = 0
var body: some View {
MatchScreenScaffold(
@@ -27,7 +29,14 @@ struct MatchesScreen: View {
HStack {
MatchLiveWordmark(compact: true)
Spacer()
Button("Esci") {
Button {
showLanguagePicker = true
} label: {
Image(systemName: "globe")
.foregroundStyle(MatchColors.textSecondary)
}
.accessibilityLabel(L10n.t("language.label"))
Button(L10n.t("action.logout")) {
Task {
await container.authRepository.logout()
onLogout()
@@ -177,6 +186,13 @@ struct MatchesScreen: View {
showTeamPicker = false
}
}
.sheet(isPresented: $showLanguagePicker) {
LanguagePickerView()
}
.onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
languageTick += 1
}
.id(languageTick)
.overlay {
if actionLoading {
Color.black.opacity(0.35)