Allinea iOS a i18n Android 2.0.5 e aggiunge monitoraggio termico nativo.

Completa L10n su login/hub/wizard/broadcast e introduce ThermalStateManager su iOS/Android con degradazione qualità e indicatore in overlay.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Emiliano Frascaro
2026-07-24 11:19:37 +02:00
co-authored by Cursor
parent ff4de33cc5
commit ea5da1eb86
46 changed files with 2675 additions and 1241 deletions
+49 -150
View File
@@ -4,7 +4,7 @@ Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un
**Aggiornato:** 24 luglio 2026
**Riferimento Android (produzione / telefono / Play):** `2.0.5-native` (`versionCode` **26**), API `https://www.matchlivetv.it`
**Stato iOS attuale:** marketing `2.0.0`, build `21` — i18n **solo Login parziale**
**Stato iOS attuale:** marketing `2.0.5`, build `26` — i18n **allineato** (Login, hub, sheet/dialog, wizard, broadcast)
Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast) + bump versione, **senza** i bug di sessione/crash già risolti su Android 2.0.5.
@@ -12,7 +12,7 @@ Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog,
## Contesto
Su Android (rilasciato) la lingua scelta dallutente vale su **tutta lapp** usabile:
Su Android (rilasciato) la lingua scelta dallutente vale su **tutta** lapp usabile:
- Persistenza + apply live (`AppLocale` / SharedPreferences `mltv_locale` + `ProvideAppLocale`)
- Icona **globo** su Login (TopAppBar) e Matches
@@ -22,7 +22,7 @@ Su Android (rilasciato) la lingua scelta dallutente vale su **tutta lapp**
- Broadcast overlay, snackbar, dialog fine set/partita
- ~230+ chiavi × 5 lingue in `res/values*`
Su iOS esistono già `AppLanguage` + `LanguagePickerView` + Login localizzato, ma **Matches/sheet/wizard/broadcast restano in italiano hardcoded**. Va replicato il lavoro Android in SwiftUI.
Su iOS: `AppLanguage` + `L10n` (~225 chiavi × 5 lingue) + `LanguagePickerView`; Login/Matches/Wizard/Broadcast usano `L10n.t(...)`. Refresh UI via `NotificationCenter` + `.id(languageTick)` **senza** reset sessione/nav.
API produzione: entrambe le app puntano già a `https://www.matchlivetv.it`**nessun gap di endpoint**.
@@ -32,29 +32,12 @@ API produzione: entrambe le app puntano già a `https://www.matchlivetv.it` —
Queste sono regressioni/bug già visti su Android; **non ripeterli** su iOS.
| Problema | Sintomo | Fix Android (da replicare in spirito) |
|----------|---------|----------------------------------------|
| Recreate Activity al cambio lingua | Torna al Login / perde nav | **Niente recreate** obbligatorio: aggiorna UI via stato lingua + refresh stringhe |
| Context “nudo” per le stringhe | Crash wizard: `No ActivityResultRegistryOwner` / picker foto | Mantieni **Activity** come host; localizza solo Resources / environment |
| `findActivity()` su context configurazione | Cambio lingua non applica (apply silent no-op) | Applica lingua con `applicationContext` / store globale, non dipendere da Activity dal dialog |
| Globe sotto lo scroll del form Login | Tap ignorato | Globe in **toolbar/top bar**, non overlay sotto `ScrollView`/`Column` |
| Splash che richiede rete per restare loggato | Dopo cambio lingua → Login se API lenta | Se token locale presente → resta autenticato; refresh rete best-effort |
### Pattern Android di riferimento (Compose)
1. `AppLocale.tagFlow` + prefs `mltv_locale`
2. `ProvideAppLocale`: `LocalConfiguration` + `LocalizedContextWrapper(Activity, localizedResources)`
→ stringhe aggiornate **e** `LocalActivityResultRegistryOwner` integro
3. `LanguagePickerDialog` chiama `AppLocale.apply(context, tag)` (applicationContext), non `findActivity()` obbligatorio
4. Splash: `currentSession() != null` → Matches; `validateOrRefresh()` in background
### Equivalente SwiftUI consigliato
1. `AppLanguage` / `@AppStorage` + `NotificationCenter` (già presente) o `environment(\.locale, …)`
2. Root: `.environment(\.locale, locale)` **e** `.id(languageTick)` sulle schermate che mostrano testo
3. **Non** distruggere `NavigationStack` / session store al cambio lingua
4. Photo picker / `PhotosPicker` / `UIImagePicker`: restano agganciati alla `View` host, non a un context “fake”
5. Cold start: se token in Keychain/UserDefaults → vai a Matches anche se `/me` fallisce temporaneamente
| Problema | Sintomo | Fix Android (da replicare in spirito) | Stato iOS |
|----------|---------|----------------------------------------|-----------|
| Recreate Activity al cambio lingua | Torna al Login / perde nav | **Niente recreate** obbligatorio: aggiorna UI via stato lingua + refresh stringhe | OK — `languageTick` / notification |
| Context “nudo” per le stringhe | Crash wizard: picker foto | Mantieni **Activity** come host | OK — SwiftUI host invariato |
| Globe sotto lo scroll del form Login | Tap ignorato | Globe in **toolbar/top bar** | OK — globo in top bar Login |
| Splash che richiede rete per restare loggato | Dopo cambio lingua → Login se API lenta | Se token locale presente → resta autenticato | OK — `validateOrRefresh` restituisce sessione locale su errore rete |
---
@@ -62,126 +45,62 @@ Queste sono regressioni/bug già visti su Android; **non ripeterli** su iOS.
| Area | Android 2.0.5 | iOS oggi | Da fare su iOS |
|------|---------------|----------|----------------|
| Persistenza lingua | SharedPreferences + `ProvideAppLocale` (no recreate nav) | UserDefaults + notification | Refresh UI su **tutte** le schermate **senza** reset sessione/nav |
| Picker Login | Icona globo in **TopAppBar** | Testo / picker presente | Allineare a **icona globo** in toolbar |
| Picker Matches | Icona globo top bar | Presente | OK se già icona; altrimenti allineare |
| Login stringhe | Sì | Sì (`L10n`) | OK |
| Slogan wordmark | `app_slogan` | Chiave `app.slogan` **non usata** | Collegare `L10n.t("app.slogan")` |
| Hub Matches | Sì | Quasi tutto IT fisso | Localizzare con chiavi `matches.*` |
| Sheet/dialog Matches | Sì (`sheet_*`) | IT fisso | Localizzare (priorità: scegli squadra) |
| Status badge partita | `match_status_*` | IT | Localizzare |
| Snackbar hub | `matches_msg_*` | IT | Localizzare |
| Wizard 3 step | Sì (`wizard_*`) | IT | Localizzare |
| Branding / color picker | Sì | IT | Localizzare |
| Broadcast + score dialog | Sì (`broadcast_*`, `score_*`) | IT | Localizzare |
| Catalogo stringhe | `values` + `values-en/fr/de/es` (~230 chiavi) | ~14 chiavi in `AppLanguage.swift` | Espandere `L10n` (o migrare a `Localizable.xcstrings`) |
| Logout UI | Icona | Testo | Icona SF Symbol |
| Versione | `2.0.5-native` / **26** | `2.0.0` / **21** | Bump a **2.0.5** / **26** (o build successiva se conflitto TestFlight) |
| RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK in prod; allineare solo semantica dev se serve |
| Persistenza lingua | SharedPreferences + `ProvideAppLocale` | UserDefaults + notification | **Fatto** |
| Picker Login | Icona globo in **TopAppBar** | Icona globo top bar | **Fatto** |
| Picker Matches | Icona globo top bar | Icona globo + logout icon | **Fatto** |
| Login stringhe | Sì | Sì (`L10n`) | **Fatto** |
| Slogan wordmark | `app_slogan` | `L10n.t("app.slogan")` | **Fatto** |
| Hub Matches | Sì | `matches.*` | **Fatto** |
| Sheet/dialog Matches | Sì (`sheet_*`) | `sheet.*` | **Fatto** |
| Status badge partita | `match_status_*` | `match.status.*` | **Fatto** |
| Snackbar hub | `matches_msg_*` | `matches.msg.*` | **Fatto** |
| Wizard 3 step | Sì (`wizard_*`) | `wizard.*` | **Fatto** |
| Branding / color picker | Sì | `wizard.branding.*` / `wizard.color.hue` | **Fatto** |
| Broadcast + score dialog | Sì | `broadcast.*` / `score.*` | **Fatto** |
| Catalogo stringhe | `values*` (~230) | `AppLanguage.swift` L10n (~225, no FGS) | **Fatto** |
| Logout UI | Icona | Icona SF Symbol | **Fatto** |
| Versione | `2.0.5-native` / **26** | `2.0.5` / **26** | **Fatto** |
| RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK |
### Residui fuori scope Android (opzionali)
Nessuno per messaggi utente: tutte le stringhe UI/errori/termiche/API/overlay usano `L10n`.
Permission dialog di sistema localizzati via `Resources/{it,en,fr,de,es}.lproj/InfoPlist.strings`.
Brand fissi lasciati intenzionalmente invariati: `MATCH` / `LIVE` / `TV`, `Match Live TV`, `YouTube Live`.
---
## File di riferimento
### Android (sorgente di verità — già fatto)
### Android (sorgente di verità)
| Ruolo | Path |
|-------|------|
| Locale + ProvideAppLocale | `native/android/.../core/AppLocale.kt` |
| Picker | `native/android/.../ui/components/LanguagePickerDialog.kt` |
| Activity | `native/android/.../MainActivity.kt` |
| Splash (sessione locale) | `native/android/.../ui/splash/SplashScreen.kt` |
| Stringhe | `native/android/app/src/main/res/values/strings.xml` + `values-{en,fr,de,es}/` |
| Login | `.../ui/login/LoginScreen.kt` |
| Hub | `.../ui/matches/MatchesScreen.kt` |
| Sheet/dialog | `.../ui/matches/MatchSheets.kt` |
| Wizard | `.../ui/wizard/*.kt` |
| Broadcast | `.../ui/broadcast/*.kt` |
| Versione | `native/android/app/build.gradle.kts` |
| UI | Login / Matches / MatchSheets / Wizard / Broadcast |
### iOS (da aggiornare)
### iOS (allineato)
| Ruolo | Path |
|-------|------|
| Lingua + L10n | `native/ios/MatchLiveTv/Core/AppLanguage.swift` |
| Login | `native/ios/MatchLiveTv/UI/Login/LoginScreen.swift` |
| Hub | `native/ios/MatchLiveTv/UI/Matches/MatchesScreen.swift` |
| Sheet/dialog Matches | file sheet/alert equivalenti sotto `UI/Matches/` (cercare stringhe IT) |
| Wizard | `UI/Wizard/` (o path equivalente) |
| Broadcast | `UI/Broadcast/` (o path equivalente) |
| Hub + sheet | `native/ios/MatchLiveTv/UI/Matches/MatchesScreen.swift` |
| Status badge | `native/ios/MatchLiveTv/Domain/MatchHubFilter.swift` (`MatchPresentation`) |
| Wizard | `native/ios/MatchLiveTv/UI/Wizard/*.swift` |
| Broadcast | `native/ios/MatchLiveTv/UI/Broadcast/*.swift` |
| Wordmark | `native/ios/MatchLiveTv/UI/Components/MatchLiveWordmark.swift` |
| Auth / splash equivalent | entry root / session restore |
| Versioni | `generate_xcodeproj.py`, `project.pbxproj`, `Info.plist` |
| Build | `scripts/build_ios_release.sh` |
| Versioni | `generate_xcodeproj.py`, `Info.plist`, `project.pbxproj` |
---
## Piano di lavoro iOS (replicare Android — ordine obbligato)
## Convenzione chiavi
### Fase A — Infrastruttura stringhe (prima di tutto)
1. Aprire Android `values/strings.xml` e `values-en|fr|de|es/strings.xml` come catalogo.
2. Portare in `L10n.table` (o meglio `Localizable.xcstrings`) **tutte** le chiavi usate da:
- `matches_*`, `sheet_*`, `match_status_*`, `matches_msg_*`
- `wizard_*`, `wizard_color_hue`
- `broadcast_*`, `score_*`
- `login_*`, `language_*`, `action_*`, `app_slogan`, `common_*` se presenti
3. Convenzione chiavi iOS: `matches.hello` ↔ Android `matches_hello` (punto al posto di `_`).
4. Placeholder: Android `%1$s` → Swift `"Ciao, \(name)"` o `String(format: L10n.t("matches.hello"), name)`.
### Fase B — Login + wordmark (rapido)
5. Login: icona globo in toolbar (come Android TopAppBar), non overlay che lo scroll può coprire.
6. `MatchLiveWordmark`: `L10n.t("app.slogan")`.
7. Cambio lingua: aggiorna stringhe **senza** buttare fuori lutente (se già loggato altrove, stessa regola).
### Fase C — Hub Matches + sheet/dialog (priorità UX — screenshot «Scegli squadra»)
8. `MatchesScreen.swift`: ogni `Text("…")` hardcoded → `L10n.t(...)`.
9. Sheet/dialog equivalenti a `MatchSheets.kt`:
- scegli squadra → `sheet.choose_team`
- nuova partita / programma / avvia subito
- scegli partita, badge BOZZA/PROGRAMMATA
- riprendi diretta, continua setup
- configura ora?, elimina partita
10. Snackbar/alert hub → `matches.msg_*`.
11. Badge status riga partita → `match.status_*`.
12. Assicurare `.id(languageTick)` (o environment) su root Matches **e** sheet presentati.
### Fase D — Wizard
13. Localizzare step Partita / Trasmissione / Test rete + branding editor + color hue (`wizard.*`).
14. Errori validazione e CTA AVANTI/INDIETRO/INIZIA.
15. Verificare photo/logo picker dopo cambio lingua (su Android crashava senza Activity host).
### Fase E — Broadcast
16. Overlay controlli, contentDescription, snackbar pausa/ripresa/regia.
17. Dialog termina diretta, set concluso, chiudi set, partita terminata (`broadcast.*`, `score.*`).
### Fase F — Sessione + versione e release
18. Cold start / cambio lingua: token locale → resta in Matches (non forzare Login se `/me` fallisce).
19. Bump `MARKETING_VERSION``2.0.5`, `CURRENT_PROJECT_VERSION``26` (o successivo se conflitto TestFlight).
20. Aggiornare `generate_xcodeproj.py` **e** `Info.plist`, poi `python3 generate_xcodeproj.py`.
21. `./scripts/build_ios_release.sh` (API default già produzione).
---
## Mapping chiavi — gruppi da copiare da Android
Sorgente: `native/android/app/src/main/res/values*/strings.xml`.
| Prefisso Android | Uso | Priorità |
|------------------|-----|----------|
| `language_*`, `login_*`, `app_slogan`, `action_*` | Login / chrome | Alta |
| `matches_*` | Hub | Alta |
| `sheet_*`, `match_status_*`, `matches_msg_*` | Sheet/dialog hub | **Alta** (bug «Scegli squadra» in EN) |
| `wizard_*` | Wizard | Alta (stessa sessione utente dopo hub) |
| `broadcast_*`, `score_*` | Diretta | Alta |
| `streaming_notification_*` | Solo Android FGS | N/A su iOS |
Non lasciare fuori scope wizard/broadcast: su Android **sono già tradotti**; iOS deve seguirli per parità.
- Android `matches_hello` → iOS `matches.hello` (ogni `_``.`)
- Placeholder: `%1$s` / `%1$d``%1$@` / `%1$d` con `L10n.t("key", args...)`
- Catalogo generato da `values*/strings.xml` (escluse `streaming_notification_*`, solo Android FGS)
---
@@ -200,16 +119,14 @@ Non lasciare fuori scope wizard/broadcast: su Android **sono già tradotti**; iO
11. Spot-check FR/DE/ES.
12. Release: `./scripts/build_ios_release.sh`
Test:
```bash
cd native/ios
python3 generate_xcodeproj.py
xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build/DerivedData \
ONLY_ACTIVE_ARCH=YES ARCHS=arm64 EXCLUDED_ARCHS=x86_64 \
test -only-testing:MatchLiveTvTests
build
```
---
@@ -224,28 +141,10 @@ xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \
Nessuna modifica server richiesta.
### Artefatti Android Play (riferimento)
```bash
# APK installazione diretta
./scripts/build_native_android_apk_prod.sh
# AAB per Google Play Console
cd native/android && ./gradlew bundleRelease -PAPI_BASE_URL=https://www.matchlivetv.it
# → app/build/outputs/bundle/release/app-release.aab
```
Versione corrente store: **`2.0.5-native` / versionCode `26`**.
Versione store target: **`2.0.5` / build `26`** (parità con Android `2.0.5-native` / versionCode `26`).
---
## Sintesi priorità (ordine di esecuzione)
## Sintesi
1. **A** Catalogo stringhe completo (copiare da Android `values*`)
2. **C** Hub + sheet/dialog (fix «Scegli squadra» in EN)
3. **B** Slogan + globo Login (toolbar)
4. **D** Wizard (+ no crash picker)
5. **E** Broadcast
6. **F** Sessione stabile al cambio lingua + versione `2.0.5` / `26` + build release
Quando AF sono chiusi, iOS è allineato ad Android **2.0.5-native** per UX multilingua, stabilità sessione e versioning store.
Fasi AF del piano originale sono **chiuse**. Tutti i messaggi utente (UI, errori API/RTMP, termica, overlay, permission system) passano da `L10n` / `InfoPlist.strings` in IT/EN/FR/DE/ES.