Files
MatchLiveTv/docs/IOS_ANDROID_I18N_GAP.md
T

252 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Gap Android → iOS: allineamento app nativa
Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare unapp **allineata ad Android `2.0.5-native`**.
**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**
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.
---
## Contesto
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
- Hub Partite localizzato
- Bottom sheet e dialog Matches (es. «Choose team», nuova partita, programma, elimina, riprendi)
- Wizard 3 step + branding colori
- 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.
API produzione: entrambe le app puntano già a `https://www.matchlivetv.it`**nessun gap di endpoint**.
---
## Lezioni Android 2.0.5 (obbligatorie su iOS)
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
---
## Stato a confronto (target = Android 2.0.5)
| 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 |
---
## File di riferimento
### Android (sorgente di verità — già fatto)
| 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` |
### iOS (da aggiornare)
| 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) |
| 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` |
---
## Piano di lavoro iOS (replicare Android — ordine obbligato)
### 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à.
---
## Verifica manuale su Mac (acceptance)
1. `cd native/ios && python3 generate_xcodeproj.py`
2. Build simulatore; login produzione.
3. Login: globo → English → label/errori/slogan in EN.
4. **Loggato:** cambio lingua da Matches → resti su Matches, testi EN (niente ritorno al Login).
5. Matches: saluto, CTA, empty state in EN.
6. Apri «Change» / scegli squadra: titolo **«Choose team»** (non «Scegli squadra»).
7. Nuova partita → Avvia ora → wizard si apre **senza crash**; step in EN.
8. Wizard 3 step in EN; logo picker funziona.
9. (Se possibile) overlay diretta: dialog/termina in EN.
10. Kill app → riapri: lingua persistita **e** sessione resta se token valido.
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' \
-derivedDataPath build/DerivedData \
ONLY_ACTIVE_ARCH=YES ARCHS=arm64 EXCLUDED_ARCHS=x86_64 \
test -only-testing:MatchLiveTvTests
```
---
## Note API / produzione
| | Android | iOS |
|--|---------|-----|
| Default | BuildConfig / `-PAPI_BASE_URL` | `API_BASE_URL` → Info.plist |
| Fallback | `https://www.matchlivetv.it` | idem |
| REST / Cable | `/api/v1`, `wss://…/cable` | idem |
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`**.
---
## Sintesi priorità (ordine di esecuzione)
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.