From 5ff22c75868289b0cc9207aaf4c3261ac603d1e0 Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Fri, 24 Jul 2026 08:40:52 +0200 Subject: [PATCH] Completa i18n Android su sheet, wizard, broadcast e login. Selettore lingua con icona globo al login; dialog e bottom sheet (es. scegli squadra) tradotti; bump a 2.0.4-native. Co-authored-by: Cursor --- native/android/app/build.gradle.kts | 4 +- .../ui/broadcast/BroadcastControlsOverlay.kt | 80 +++++--- .../ui/broadcast/BroadcastScreen.kt | 63 ++++-- .../ui/broadcast/LiveScoreControls.kt | 21 +- .../ui/broadcast/ScoreOutcomeDialogs.kt | 32 ++- .../match_live_tv/ui/login/LoginScreen.kt | 23 ++- .../match_live_tv/ui/matches/MatchSheets.kt | 125 ++++++----- .../match_live_tv/ui/matches/MatchesScreen.kt | 23 ++- .../ui/wizard/StepMatchScreen.kt | 77 ++++--- .../ui/wizard/StepNetworkTestScreen.kt | 63 +++--- .../ui/wizard/StepTransmissionScreen.kt | 39 ++-- .../ui/wizard/TeamBrandingEditor.kt | 45 ++-- .../ui/wizard/TeamColorPicker.kt | 4 +- .../ui/wizard/WizardComponents.kt | 16 +- .../ui/wizard/WizardShellScreen.kt | 12 +- .../app/src/main/res/values-de/strings.xml | 194 ++++++++++++++++++ .../app/src/main/res/values-en/strings.xml | 194 ++++++++++++++++++ .../app/src/main/res/values-es/strings.xml | 194 ++++++++++++++++++ .../app/src/main/res/values-fr/strings.xml | 194 ++++++++++++++++++ .../app/src/main/res/values/strings.xml | 194 ++++++++++++++++++ 20 files changed, 1363 insertions(+), 234 deletions(-) diff --git a/native/android/app/build.gradle.kts b/native/android/app/build.gradle.kts index 5c5b96f..6c9d0b0 100644 --- a/native/android/app/build.gradle.kts +++ b/native/android/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.matchlivetv.match_live_tv" minSdk = 24 targetSdk = 36 - versionCode = 24 - versionName = "2.0.3-native" + versionCode = 25 + versionName = "2.0.4-native" val apiBaseUrl = project.findProperty("API_BASE_URL") as String? ?: "https://www.matchlivetv.it" diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastControlsOverlay.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastControlsOverlay.kt index bd52792..8725c73 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastControlsOverlay.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastControlsOverlay.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -59,6 +60,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.layout.ContentScale import coil.compose.AsyncImage +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.core.resolveMediaUrl import com.matchlivetv.match_live_tv.core.DeviceHealthSnapshot import com.matchlivetv.match_live_tv.core.ThermalLevel @@ -115,8 +117,8 @@ fun BroadcastControlsOverlay( AlertDialog( onDismissRequest = { showTerminateConfirm = false }, containerColor = MatchColors.SurfaceElevated, - title = { Text("Terminare la diretta?") }, - text = { Text("Lo streaming verrà chiuso per tutti gli spettatori.") }, + title = { Text(stringResource(R.string.broadcast_terminate_title)) }, + text = { Text(stringResource(R.string.broadcast_terminate_message)) }, confirmButton = { TextButton( onClick = { @@ -124,12 +126,12 @@ fun BroadcastControlsOverlay( onTerminate() }, ) { - Text("TERMINA", color = MatchColors.PrimaryRed) + Text(stringResource(R.string.broadcast_terminate_confirm), color = MatchColors.PrimaryRed) } }, dismissButton = { TextButton(onClick = { showTerminateConfirm = false }) { - Text("Annulla", color = MatchColors.TextSecondary) + Text(stringResource(R.string.action_cancel), color = MatchColors.TextSecondary) } }, ) @@ -158,7 +160,11 @@ fun BroadcastControlsOverlay( ) { SideIconButton( icon = if (controlsVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility, - contentDescription = if (controlsVisible) "Nascondi controlli" else "Mostra controlli", + contentDescription = if (controlsVisible) { + stringResource(R.string.broadcast_hide_controls_cd) + } else { + stringResource(R.string.broadcast_show_controls_cd) + }, onClick = onToggleControls, ) BroadcastTelemetryPanel( @@ -180,26 +186,26 @@ fun BroadcastControlsOverlay( ) { SideIconButton( icon = Icons.Default.Share, - contentDescription = "Condividi diretta", + contentDescription = stringResource(R.string.broadcast_share_live_cd), onClick = onShareLive, enabled = shareLiveEnabled, ) SideIconButton( icon = Icons.Default.Videocam, - contentDescription = "Condividi link regia", + contentDescription = stringResource(R.string.broadcast_share_regia_cd), onClick = onShareRegia, ) if (onCloseSet != null) { SideIconButton( icon = Icons.Default.Check, - contentDescription = "Chiudi set", + contentDescription = stringResource(R.string.score_action_close_set), onClick = onCloseSet, ) } if (onAdvancePeriod != null) { SideIconButton( icon = Icons.Default.SkipNext, - contentDescription = "Periodo successivo", + contentDescription = stringResource(R.string.broadcast_next_period_cd), onClick = onAdvancePeriod, ) } @@ -213,13 +219,17 @@ fun BroadcastControlsOverlay( ) { SideIconButton( icon = if (isPaused) Icons.Default.PlayArrow else Icons.Default.Pause, - contentDescription = if (isPaused) "Riprendi diretta" else "Pausa diretta", + contentDescription = if (isPaused) { + stringResource(R.string.broadcast_resume_cd) + } else { + stringResource(R.string.broadcast_pause_cd) + }, onClick = onPauseOrResume, highlighted = isPaused, ) SideIconButton( icon = Icons.Default.Stop, - contentDescription = "Termina diretta", + contentDescription = stringResource(R.string.broadcast_terminate_cd), onClick = { showTerminateConfirm = true }, danger = true, ) @@ -232,7 +242,7 @@ fun BroadcastControlsOverlay( verticalAlignment = Alignment.Bottom, ) { TeamScoreColumn( - teamLabel = "CASA", + teamLabel = stringResource(R.string.broadcast_team_home_label), teamName = homeName, accentColor = homeAccentColor, logoUrl = homeLogoUrl, @@ -251,7 +261,7 @@ fun BroadcastControlsOverlay( pointsTarget = pointsTarget, ) TeamScoreColumn( - teamLabel = "OSPITE", + teamLabel = stringResource(R.string.broadcast_team_away_label), teamName = awayName, accentColor = awayAccentColor, logoUrl = awayLogoUrl, @@ -289,7 +299,11 @@ private fun BroadcastTelemetryPanel( verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( - if (cableConnected) "Tabellone OK" else "Tabellone offline", + if (cableConnected) { + stringResource(R.string.broadcast_scoreboard_connected) + } else { + stringResource(R.string.broadcast_scoreboard_offline) + }, style = labelStyle, color = if (cableConnected) MatchColors.SuccessGreen else MatchColors.TextSecondary, ) @@ -388,7 +402,11 @@ private fun ScoreCenterPanel( when (boardType) { "basket", "timed" -> { Text( - score.periodLabel ?: if (boardType == "basket") "Q${score.period}" else "${score.period}° tempo", + score.periodLabel ?: if (boardType == "basket") { + "Q${score.period}" + } else { + stringResource(R.string.broadcast_period_label_timed, score.period) + }, style = MaterialTheme.typography.labelSmall, color = MatchColors.TextSecondary, ) @@ -396,13 +414,13 @@ private fun ScoreCenterPanel( "generic" -> Unit else -> { Text( - "Set ${score.currentSet} · $pointsTarget pt", + stringResource(R.string.broadcast_set_progress, score.currentSet, pointsTarget), style = MaterialTheme.typography.labelSmall, color = MatchColors.TextSecondary, ) if (score.homeSets > 0 || score.awaySets > 0) { Text( - "Set vinti ${score.homeSets}-${score.awaySets}", + stringResource(R.string.broadcast_sets_won, score.homeSets, score.awaySets), style = MaterialTheme.typography.labelSmall, color = MatchColors.TextSecondary, ) @@ -450,7 +468,15 @@ private fun TeamScoreColumn( modifier = Modifier.padding(vertical = 4.dp), ) Spacer(Modifier.height(4.dp)) - val teamSide = if (alignEnd) "ospite" else "casa" + val teamSide = if (alignEnd) { + stringResource(R.string.broadcast_side_away) + } else { + stringResource(R.string.broadcast_side_home) + } + val addPointTooltip = stringResource(R.string.broadcast_tooltip_add_point, teamSide) + val removePointTooltip = stringResource(R.string.broadcast_tooltip_remove_point, teamSide) + val plus2Tooltip = stringResource(R.string.broadcast_tooltip_plus_side, 2, teamSide) + val plus3Tooltip = stringResource(R.string.broadcast_tooltip_plus_side, 3, teamSide) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = if (alignEnd) Arrangement.End else Arrangement.Start, @@ -459,33 +485,33 @@ private fun TeamScoreColumn( if (alignEnd) { if (showBasketButtons) { onPlus3?.let { - ScoreIconButton(label = "+3", tooltip = "+3 $teamSide", onClick = it, primary = true) + ScoreIconButton(label = "+3", tooltip = plus3Tooltip, onClick = it, primary = true) } onPlus2?.let { - ScoreIconButton(label = "+2", tooltip = "+2 $teamSide", onClick = it, primary = true) + ScoreIconButton(label = "+2", tooltip = plus2Tooltip, onClick = it, primary = true) } } ScoreIconButton( label = "+1", - tooltip = "Aggiungi punto $teamSide", + tooltip = addPointTooltip, onClick = onPlus, primary = !showBasketButtons, ) - ScoreIconButton(label = "−", tooltip = "Togli punto $teamSide", onClick = onMinus) + ScoreIconButton(label = "−", tooltip = removePointTooltip, onClick = onMinus) } else { - ScoreIconButton(label = "−", tooltip = "Togli punto $teamSide", onClick = onMinus) + ScoreIconButton(label = "−", tooltip = removePointTooltip, onClick = onMinus) ScoreIconButton( label = "+1", - tooltip = "Aggiungi punto $teamSide", + tooltip = addPointTooltip, onClick = onPlus, primary = !showBasketButtons, ) if (showBasketButtons) { onPlus2?.let { - ScoreIconButton(label = "+2", tooltip = "+2 $teamSide", onClick = it, primary = true) + ScoreIconButton(label = "+2", tooltip = plus2Tooltip, onClick = it, primary = true) } onPlus3?.let { - ScoreIconButton(label = "+3", tooltip = "+3 $teamSide", onClick = it, primary = true) + ScoreIconButton(label = "+3", tooltip = plus3Tooltip, onClick = it, primary = true) } } } @@ -676,7 +702,7 @@ fun shareBroadcastLink(context: Context, url: String, subject: String) { putExtra(Intent.EXTRA_TEXT, url) putExtra(Intent.EXTRA_SUBJECT, subject) }, - "Condividi", + context.getString(R.string.broadcast_share_chooser_title), ), ) } diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastScreen.kt index 9e52537..ecbe71f 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastScreen.kt @@ -27,8 +27,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.core.DeviceTelemetry import com.matchlivetv.match_live_tv.core.parseColorHex import com.matchlivetv.match_live_tv.core.resolveMediaUrl @@ -99,6 +101,8 @@ fun BroadcastScreen( onFinished() } + val genericError = context.getString(R.string.common_error_generic) + suspend fun pauseStream() { if (pauseInFlight) return pauseInFlight = true @@ -106,9 +110,11 @@ fun BroadcastScreen( val updated = container.sessionRepository.pauseSession(sessionId) session = updated container.broadcastCoordinator.engine.pauseBroadcast() - snackbar.showSnackbar("Diretta in pausa") + snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_paused)) } catch (e: Exception) { - snackbar.showSnackbar("Pausa: ${e.message ?: "errore"}") + snackbar.showSnackbar( + context.getString(R.string.broadcast_snackbar_pause_error, e.message ?: genericError), + ) } finally { pauseInFlight = false } @@ -126,9 +132,11 @@ fun BroadcastScreen( } updated = container.sessionRepository.fetchSession(sessionId) session = updated - snackbar.showSnackbar("Diretta ripresa") + snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_resumed)) } catch (e: Exception) { - snackbar.showSnackbar("Ripresa: ${e.message ?: "errore"}") + snackbar.showSnackbar( + context.getString(R.string.broadcast_snackbar_resume_error, e.message ?: genericError), + ) } finally { resumeInFlight = false } @@ -138,7 +146,7 @@ fun BroadcastScreen( container.broadcastCoordinator.engine.pauseBroadcast() session = runCatching { container.sessionRepository.fetchSession(sessionId) } .getOrNull() ?: session?.copy(status = "paused") - snackbar.showSnackbar("Pausa dalla regia") + snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_paused_remote)) } suspend fun applyRemoteResume() { @@ -150,9 +158,11 @@ fun BroadcastScreen( try { session = current container.broadcastCoordinator.engine.resumeBroadcast(broadcastConfig(current)) - snackbar.showSnackbar("Ripresa dalla regia") + snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_resumed_remote)) } catch (e: Exception) { - snackbar.showSnackbar("Ripresa RTMP: ${e.message ?: "errore"}") + snackbar.showSnackbar( + context.getString(R.string.broadcast_snackbar_resume_rtmp_error, e.message ?: genericError), + ) } finally { resumeInFlight = false } @@ -162,7 +172,7 @@ fun BroadcastScreen( container.sessionCable.disconnect() container.broadcastCoordinator.engine.stopBroadcast() container.broadcastCoordinator.stopService() - snackbar.showSnackbar("Diretta chiusa dalla regia") + snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_closed_remote)) onFinished() } @@ -195,12 +205,12 @@ fun BroadcastScreen( container.broadcastCoordinator.startService() val url = loaded.rtmpIngestUrl if (url.isNullOrBlank()) { - error("URL RTMP mancante") + error(context.getString(R.string.broadcast_error_rtmp_missing)) } else if (!loaded.isPaused) { container.broadcastCoordinator.engine.startBroadcast(broadcastConfig(loaded)) } }.onFailure { - error = it.message ?: "Sessione non disponibile" + error = it.message ?: context.getString(R.string.broadcast_error_session_unavailable) } loading = false } @@ -241,7 +251,7 @@ fun BroadcastScreen( awayScore = score.awayPoints, periodLabel = score.periodLabel ?: when (overlayKind) { OverlayKind.BASKET -> "Q${score.period}" - else -> "${score.period}° tempo" + else -> context.getString(R.string.broadcast_period_label_timed, score.period) }, homeAccentColor = homeColor, awayAccentColor = awayColor, @@ -330,11 +340,11 @@ fun BroadcastScreen( val isPaused = session?.isPaused == true || metrics.phase == BroadcastPhase.PAUSED val statusText = when { - isPaused -> "PAUSA" - metrics.phase == BroadcastPhase.LIVE -> "IN DIRETTA" - metrics.phase == BroadcastPhase.CONNECTING -> "CONNESSIONE…" - metrics.phase == BroadcastPhase.RECONNECTING -> "RICONNESSIONE…" - metrics.phase == BroadcastPhase.ERROR -> metrics.lastError ?: "ERRORE" + isPaused -> stringResource(R.string.broadcast_status_paused) + metrics.phase == BroadcastPhase.LIVE -> stringResource(R.string.broadcast_status_live) + metrics.phase == BroadcastPhase.CONNECTING -> stringResource(R.string.broadcast_status_connecting) + metrics.phase == BroadcastPhase.RECONNECTING -> stringResource(R.string.broadcast_status_reconnecting) + metrics.phase == BroadcastPhase.ERROR -> metrics.lastError ?: stringResource(R.string.broadcast_status_error_fallback) else -> "PREVIEW" } val statusColor = when { @@ -353,7 +363,8 @@ fun BroadcastScreen( container.scoreController.applyAction(action) } val shareUrl = currentSession?.watchShareUrl() - val shareSubject = currentMatch?.let { "${it.teamName} vs ${it.opponentName}" } ?: "Diretta Match Live TV" + val shareSubject = currentMatch?.let { "${it.teamName} vs ${it.opponentName}" } + ?: stringResource(R.string.broadcast_share_subject_fallback) currentMatch?.let { m -> ScoreDialogRouter( @@ -383,12 +394,12 @@ fun BroadcastScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - "Consenti camera e microfono per andare in diretta", + stringResource(R.string.broadcast_permissions_required), color = MatchColors.AccentYellow, ) Spacer(Modifier.height(12.dp)) MatchPrimaryButton( - label = "CONCEDI PERMESSI", + label = stringResource(R.string.broadcast_permissions_grant_action), onClick = permissions.request, ) } @@ -505,7 +516,9 @@ fun BroadcastScreen( onShareLive = { val url = shareUrl if (url.isNullOrBlank()) { - scope.launch { snackbar.showSnackbar("Link diretta non ancora disponibile") } + scope.launch { + snackbar.showSnackbar(context.getString(R.string.broadcast_share_link_unavailable)) + } } else { shareBroadcastLink(context, url, shareSubject) } @@ -514,10 +527,16 @@ fun BroadcastScreen( scope.launch { runCatching { container.sessionRepository.createRegiaLink(sessionId) } .onSuccess { url -> - shareBroadcastLink(context, url, "Link regia — $shareSubject") + shareBroadcastLink( + context, + url, + context.getString(R.string.broadcast_share_regia_subject, shareSubject), + ) } .onFailure { - snackbar.showSnackbar(it.message ?: "Errore link regia") + snackbar.showSnackbar( + it.message ?: context.getString(R.string.broadcast_error_regia_link), + ) } } }, diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/LiveScoreControls.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/LiveScoreControls.kt index 6dbff9d..fe7c1fb 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/LiveScoreControls.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/LiveScoreControls.kt @@ -16,9 +16,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.domain.ScoreState import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton @@ -59,7 +61,7 @@ fun LiveScoreControls( modifier = Modifier.weight(1f), ) Text( - "$pointsTarget pt", + stringResource(R.string.broadcast_points_target, pointsTarget), style = MaterialTheme.typography.labelSmall, color = MatchColors.TextSecondary, modifier = Modifier.padding(top = 24.dp).padding(horizontal = 8.dp), @@ -74,7 +76,7 @@ fun LiveScoreControls( } Spacer(Modifier.height(10.dp)) MatchSecondaryButton( - label = "CHIUDI SET", + label = stringResource(R.string.broadcast_close_set_button), onClick = onCloseSet, ) } @@ -132,7 +134,11 @@ private fun ScoreOverlayBar( ) { Column(Modifier.weight(1f)) { Text(homeName, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) - Text("${score.homeSets} set", style = MaterialTheme.typography.labelSmall, color = MatchColors.TextSecondary) + Text( + stringResource(R.string.broadcast_team_sets_count, score.homeSets), + style = MaterialTheme.typography.labelSmall, + color = MatchColors.TextSecondary, + ) } Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( @@ -140,14 +146,19 @@ private fun ScoreOverlayBar( style = MaterialTheme.typography.titleLarge, ) Text( - "Set ${score.currentSet} · $pointsTarget pt", + stringResource(R.string.broadcast_set_progress, score.currentSet, pointsTarget), style = MaterialTheme.typography.labelSmall, color = MatchColors.TextSecondary, ) } Column(Modifier.weight(1f), horizontalAlignment = Alignment.End) { Text(awayName, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.End) - Text("${score.awaySets} set", style = MaterialTheme.typography.labelSmall, color = MatchColors.TextSecondary, textAlign = TextAlign.End) + Text( + stringResource(R.string.broadcast_team_sets_count, score.awaySets), + style = MaterialTheme.typography.labelSmall, + color = MatchColors.TextSecondary, + textAlign = TextAlign.End, + ) } } } diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/ScoreOutcomeDialogs.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/ScoreOutcomeDialogs.kt index 4d204ad..aa7627a 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/ScoreOutcomeDialogs.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/ScoreOutcomeDialogs.kt @@ -6,6 +6,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.domain.ScoringSide import com.matchlivetv.match_live_tv.ui.theme.MatchColors @@ -19,21 +21,18 @@ fun SetWonDialog( AlertDialog( onDismissRequest = { onDismiss(false) }, containerColor = MatchColors.Surface, - title = { Text("Set concluso") }, + title = { Text(stringResource(R.string.score_set_won_title)) }, text = { - Text( - "$winnerName vince il set $homePoints-$awayPoints.\n\n" + - "Chiudere il set e passare al successivo?", - ) + Text(stringResource(R.string.score_set_won_message, winnerName, homePoints, awayPoints)) }, confirmButton = { FilledTonalButton(onClick = { onDismiss(true) }) { - Text("Chiudi set", color = Color.Black) + Text(stringResource(R.string.score_action_close_set), color = Color.Black) } }, dismissButton = { TextButton(onClick = { onDismiss(false) }) { - Text("Continua a segnare") + Text(stringResource(R.string.score_action_continue_scoring)) } }, ) @@ -44,18 +43,18 @@ fun CloseSetAnywayDialog(onDismiss: (confirmed: Boolean) -> Unit) { AlertDialog( onDismissRequest = { onDismiss(false) }, containerColor = MatchColors.Surface, - title = { Text("Chiudi set") }, + title = { Text(stringResource(R.string.score_action_close_set)) }, text = { - Text("Il punteggio non soddisfa ancora le regole del torneo. Chiudere il set comunque?") + Text(stringResource(R.string.score_close_set_anyway_message)) }, confirmButton = { FilledTonalButton(onClick = { onDismiss(true) }) { - Text("Chiudi comunque") + Text(stringResource(R.string.score_action_close_anyway)) } }, dismissButton = { TextButton(onClick = { onDismiss(false) }) { - Text("Annulla") + Text(stringResource(R.string.action_cancel)) } }, ) @@ -71,21 +70,18 @@ fun MatchWonDialog( AlertDialog( onDismissRequest = { onDismiss(false) }, containerColor = MatchColors.Surface, - title = { Text("Partita terminata") }, + title = { Text(stringResource(R.string.score_match_won_title)) }, text = { - Text( - "$winnerName vince la partita ($homeSets-$awaySets set).\n\n" + - "Chiudere definitivamente la diretta?", - ) + Text(stringResource(R.string.score_match_won_message, winnerName, homeSets, awaySets)) }, confirmButton = { FilledTonalButton(onClick = { onDismiss(true) }) { - Text("Chiudi diretta", color = Color.White) + Text(stringResource(R.string.score_action_close_live), color = Color.White) } }, dismissButton = { TextButton(onClick = { onDismiss(false) }) { - Text("Continua in onda") + Text(stringResource(R.string.score_action_continue_live)) } }, ) diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/login/LoginScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/login/LoginScreen.kt index 7cd0c2f..87a1fb3 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/login/LoginScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/login/LoginScreen.kt @@ -1,5 +1,6 @@ package com.matchlivetv.match_live_tv.ui.login +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -10,6 +11,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Icon @@ -17,7 +19,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -85,6 +86,18 @@ fun LoginScreen( if (showLanguagePicker) { LanguagePickerDialog(onDismiss = { showLanguagePicker = false }) } + Box(modifier = Modifier.fillMaxWidth()) { + IconButton( + onClick = { showLanguagePicker = true }, + modifier = Modifier.align(Alignment.TopEnd), + ) { + Icon( + Icons.Default.Language, + contentDescription = stringResource(R.string.language_label), + tint = MatchColors.TextSecondary, + ) + } + } Column( modifier = Modifier .verticalScroll(rememberScrollState()) @@ -92,11 +105,7 @@ fun LoginScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { MatchLiveWordmark(showSlogan = true) - Spacer(Modifier.height(16.dp)) - TextButton(onClick = { showLanguagePicker = true }) { - Text(stringResource(R.string.language_label), color = MatchColors.TextSecondary) - } - Spacer(Modifier.height(32.dp)) + Spacer(Modifier.height(48.dp)) Text( text = stringResource(R.string.login_submit).uppercase(), style = androidx.compose.material3.MaterialTheme.typography.headlineMedium, @@ -106,7 +115,7 @@ fun LoginScreen( value = email, onValueChange = { email = it }, label = { Text(stringResource(R.string.login_email)) }, - placeholder = { Text("coach@squadra.it") }, + placeholder = { Text(stringResource(R.string.login_email_placeholder)) }, modifier = Modifier.fillMaxWidth(), singleLine = true, keyboardOptions = KeyboardOptions( diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchSheets.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchSheets.kt index 2a5f377..f6da059 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchSheets.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchSheets.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.matchlivetv.match_live_tv.R @@ -77,8 +76,13 @@ data class ScheduleMatchInput( val location: String?, ) -private val sheetDateFormat = - DateTimeFormatter.ofPattern("EEE d MMM yyyy · HH:mm", Locale.ITALY) +@Composable +private fun sheetDateFormatter(): DateTimeFormatter { + val locale = LocalContext.current.resources.configuration.locales[0] ?: Locale.getDefault() + return remember(locale) { + DateTimeFormatter.ofPattern("EEE d MMM yyyy · HH:mm", locale) + } +} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -92,24 +96,24 @@ fun NewMatchBottomSheet( sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), ) { Column(Modifier.padding(horizontal = 20.dp, vertical = 12.dp)) { - Text("Nuova partita", style = MaterialTheme.typography.headlineMedium) + Text(stringResource(R.string.sheet_new_match_title), style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(6.dp)) Text( - "Programma in anticipo o avvia la configurazione diretta subito.", + stringResource(R.string.sheet_new_match_lead), style = MaterialTheme.typography.bodyMedium, ) Spacer(Modifier.height(20.dp)) SheetOptionTile( icon = { Icon(Icons.Default.EventAvailable, null, tint = MatchColors.PrimaryRed) }, - title = "Programma partita", - subtitle = "Data, ora e avversario — visibile anche sul sito", + title = stringResource(R.string.sheet_schedule_option), + subtitle = stringResource(R.string.sheet_schedule_option_sub), onClick = { onChoice(NewMatchChoice.Schedule) }, ) Spacer(Modifier.height(10.dp)) SheetOptionTile( icon = { Icon(Icons.Default.PlayCircleOutline, null, tint = MatchColors.PrimaryRed) }, - title = "Avvia subito", - subtitle = "Crea la partita e passa al wizard senza orario", + title = stringResource(R.string.sheet_quick_option), + subtitle = stringResource(R.string.sheet_quick_option_sub), onClick = { onChoice(NewMatchChoice.QuickStart) }, ) Spacer(Modifier.height(24.dp)) @@ -125,6 +129,7 @@ fun ScheduleMatchBottomSheet( onSubmit: (ScheduleMatchInput) -> Unit, ) { val context = LocalContext.current + val dateFormat = sheetDateFormatter() var opponent by remember { mutableStateOf("") } var location by remember { mutableStateOf("") } var scheduledAt by remember { @@ -159,28 +164,27 @@ fun ScheduleMatchBottomSheet( sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), ) { Column(Modifier.padding(horizontal = 20.dp, vertical = 12.dp)) { - Text("Programma partita", style = MaterialTheme.typography.headlineMedium) + Text(stringResource(R.string.sheet_schedule_title), style = MaterialTheme.typography.headlineMedium) if (!teamName.isNullOrBlank()) { Spacer(Modifier.height(4.dp)) Text(teamName, color = MatchColors.PrimaryRed, style = MaterialTheme.typography.titleMedium) } Spacer(Modifier.height(6.dp)) Text( - "La diretta non parte ora: comparirà sul sito con data e ora. " + - "Avvierai lo streaming dall'app quando sei in palestra.", + stringResource(R.string.sheet_schedule_lead), style = MaterialTheme.typography.bodyMedium, ) Spacer(Modifier.height(20.dp)) MatchOutlinedField( value = opponent, onValueChange = { opponent = it }, - label = "Avversario", + label = stringResource(R.string.sheet_opponent), ) Spacer(Modifier.height(12.dp)) MatchOutlinedField( value = location, onValueChange = { location = it }, - label = "Luogo (opzionale)", + label = stringResource(R.string.sheet_location_optional), ) Spacer(Modifier.height(12.dp)) Row( @@ -191,9 +195,9 @@ fun ScheduleMatchBottomSheet( verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.weight(1f)) { - Text("Data e ora", style = MaterialTheme.typography.bodyMedium) + Text(stringResource(R.string.sheet_date_time), style = MaterialTheme.typography.bodyMedium) Text( - scheduledAt.format(sheetDateFormat), + scheduledAt.format(dateFormat), style = MaterialTheme.typography.titleMedium, ) } @@ -201,7 +205,7 @@ fun ScheduleMatchBottomSheet( } Spacer(Modifier.height(20.dp)) MatchPrimaryButton( - label = "SALVA IN PROGRAMMA", + label = stringResource(R.string.sheet_save_schedule), onClick = { val name = opponent.trim() if (name.isEmpty()) return@MatchPrimaryButton @@ -239,10 +243,10 @@ fun SelectMatchBottomSheet( sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), ) { Column(Modifier.padding(horizontal = 20.dp, vertical = 12.dp)) { - Text("Scegli partita", style = MaterialTheme.typography.headlineMedium) + Text(stringResource(R.string.sheet_choose_match), style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(6.dp)) Text( - "Partite già programmate sul sito o in app.", + stringResource(R.string.sheet_choose_match_lead), style = MaterialTheme.typography.bodyMedium, ) Spacer(Modifier.height(16.dp)) @@ -251,7 +255,12 @@ fun SelectMatchBottomSheet( modifier = Modifier.height(360.dp), ) { if (scheduled.isNotEmpty()) { - item { Text("Programmate", style = MaterialTheme.typography.labelLarge) } + item { + Text( + stringResource(R.string.sheet_section_scheduled), + style = MaterialTheme.typography.labelLarge, + ) + } items(scheduled, key = { it.id }) { match -> SelectMatchTile(match, now) { onSelect(match) } } @@ -259,7 +268,10 @@ fun SelectMatchBottomSheet( if (unscheduled.isNotEmpty()) { item { Spacer(Modifier.height(8.dp)) - Text("Senza orario", style = MaterialTheme.typography.labelLarge) + Text( + stringResource(R.string.sheet_section_unscheduled), + style = MaterialTheme.typography.labelLarge, + ) } items(unscheduled, key = { it.id }) { match -> SelectMatchTile(match, now) { onSelect(match) } @@ -285,7 +297,7 @@ fun TeamPickerBottomSheet( ) { Column(Modifier.padding(bottom = 24.dp)) { Text( - "Scegli squadra", + stringResource(R.string.sheet_choose_team), style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp), ) @@ -329,7 +341,7 @@ fun ResumeSessionBottomSheet( containerColor = MatchColors.Surface, ) { Column(Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) { - Text("Diretta in corso", style = MaterialTheme.typography.headlineMedium) + Text(stringResource(R.string.sheet_live_in_progress), style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(4.dp)) Text( "${match.teamName} vs ${match.opponentName}", @@ -337,19 +349,29 @@ fun ResumeSessionBottomSheet( ) match.activeSessionStatus?.let { status -> Spacer(Modifier.height(8.dp)) - Text("Stato: $status", color = MatchColors.PrimaryRed, style = MaterialTheme.typography.labelLarge) + Text( + stringResource(R.string.sheet_status, status), + color = MatchColors.PrimaryRed, + style = MaterialTheme.typography.labelLarge, + ) } Spacer(Modifier.height(20.dp)) if (match.canResumeCamera) { - MatchPrimaryButton(label = "RIPRENDI CAMERA E TRASMETTI", onClick = onResumeCamera) + MatchPrimaryButton( + label = stringResource(R.string.sheet_resume_camera), + onClick = onResumeCamera, + ) Spacer(Modifier.height(8.dp)) } if (match.activeSessionStatus == "idle") { - MatchSecondaryButton(label = "CONTINUA CONFIGURAZIONE", onClick = onContinueSetup) + MatchSecondaryButton( + label = stringResource(R.string.sheet_continue_setup), + onClick = onContinueSetup, + ) Spacer(Modifier.height(8.dp)) } TextButton(onClick = onDismiss, modifier = Modifier.fillMaxWidth()) { - Text("Annulla", color = MatchColors.TextSecondary) + Text(stringResource(R.string.action_cancel), color = MatchColors.TextSecondary) } Spacer(Modifier.height(8.dp)) } @@ -364,21 +386,21 @@ fun ConfigureScheduledMatchDialog( AlertDialog( onDismissRequest = onLater, containerColor = MatchColors.Surface, - title = { Text("Configurare ora?") }, + title = { Text(stringResource(R.string.sheet_configure_now_title)) }, text = { Text( - "Puoi preparare la trasmissione subito, oppure tornare quando sei in palestra.", + stringResource(R.string.sheet_configure_now_body), style = MaterialTheme.typography.bodyMedium, ) }, confirmButton = { TextButton(onClick = onConfigure) { - Text("Configura", color = MatchColors.PrimaryRed) + Text(stringResource(R.string.sheet_configure), color = MatchColors.PrimaryRed) } }, dismissButton = { TextButton(onClick = onLater) { - Text("Più tardi", color = MatchColors.TextSecondary) + Text(stringResource(R.string.sheet_later), color = MatchColors.TextSecondary) } }, ) @@ -393,22 +415,21 @@ fun DeleteMatchDialog( AlertDialog( onDismissRequest = onDismiss, containerColor = MatchColors.Surface, - title = { Text("Elimina partita") }, + title = { Text(stringResource(R.string.sheet_delete_match_title)) }, text = { Text( - "Eliminare «${match.teamName} vs ${match.opponentName}»?\n\n" + - "L'operazione non si può annullare.", + stringResource(R.string.sheet_delete_match_body, match.teamName, match.opponentName), style = MaterialTheme.typography.bodyMedium, ) }, confirmButton = { TextButton(onClick = onConfirm) { - Text("Elimina", color = MatchColors.PrimaryRed) + Text(stringResource(R.string.sheet_delete), color = MatchColors.PrimaryRed) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Annulla", color = MatchColors.TextSecondary) + Text(stringResource(R.string.action_cancel), color = MatchColors.TextSecondary) } }, ) @@ -444,6 +465,11 @@ private fun SheetOptionTile( private fun SelectMatchTile(match: Match, now: Instant, onClick: () -> Unit) { val scheduledInstant = parseApiInstant(match.scheduledAt) val isFuture = scheduledInstant?.isAfter(now) == true + val badge = when { + scheduledInstant == null -> stringResource(R.string.sheet_badge_draft) + isFuture -> stringResource(R.string.sheet_badge_scheduled) + else -> stringResource(R.string.sheet_badge_calendar) + } Row( Modifier .fillMaxWidth() @@ -462,11 +488,7 @@ private fun SelectMatchTile(match: Match, now: Instant, onClick: () -> Unit) { } } Text( - when { - scheduledInstant == null -> "BOZZA" - isFuture -> "PROGRAMMATA" - else -> "IN CALENDARIO" - }, + badge, style = MaterialTheme.typography.labelLarge, color = if (isFuture) MatchColors.PrimaryRed else MatchColors.TextSecondary, modifier = Modifier @@ -576,7 +598,7 @@ fun ActiveSessionBanner( Spacer(Modifier.width(12.dp)) Column(Modifier.weight(1f)) { Text( - "Riprendi diretta in corso", + stringResource(R.string.sheet_resume_banner), color = MatchColors.PrimaryRed, style = MaterialTheme.typography.titleMedium, ) @@ -610,16 +632,21 @@ fun MatchOutlinedField( ) } +@Composable fun matchStatusLabel(match: Match): String { - if (match.canResumeCamera) return "RIPRENDI" - if (match.hasActiveSession) return "IN CORSO" + if (match.canResumeCamera) return stringResource(R.string.match_status_resume) + if (match.hasActiveSession) return stringResource(R.string.match_status_live) val at = parseApiInstant(match.scheduledAt) - if (at != null && at.isScheduledFuture()) return "PROGRAMMATA" - return "AVVIA" + if (at != null && at.isScheduledFuture()) return stringResource(R.string.match_status_scheduled) + return stringResource(R.string.match_status_start) } -fun formatMatchDate(instant: Instant): String = - sheetDateFormat.format(instant.atZone(ZoneId.systemDefault())) +@Composable +fun formatMatchDate(instant: Instant): String { + val formatter = sheetDateFormatter() + return formatter.format(instant.atZone(ZoneId.systemDefault())) +} -fun formatMatchDate(iso: String?): String? = +@Composable +fun formatMatchDateOrNull(iso: String?): String? = parseApiInstant(iso)?.let { formatMatchDate(it) } diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchesScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchesScreen.kt index 90f7713..6196f22 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchesScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchesScreen.kt @@ -72,6 +72,13 @@ fun MatchesScreen( val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val session by container.authRepository.sessionFlow.collectAsState(initial = null) + val msgLoadError = stringResource(R.string.matches_msg_load_error) + val msgResumeFailed = stringResource(R.string.matches_msg_resume_failed) + val msgCreateFailed = stringResource(R.string.matches_msg_create_failed) + val msgScheduledOk = stringResource(R.string.matches_msg_scheduled_ok) + val msgScheduleError = stringResource(R.string.matches_msg_schedule_error) + val msgDeleted = stringResource(R.string.matches_msg_deleted) + val msgDeleteFailed = stringResource(R.string.matches_msg_delete_failed) var loading by remember { mutableStateOf(true) } var refreshing by remember { mutableStateOf(false) } @@ -108,7 +115,7 @@ fun MatchesScreen( activeTeam = team matches = team?.let { container.matchRepository.fetchMatchesForTeam(it.id) }.orEmpty() }.onFailure { - error = it.message ?: "Errore caricamento" + error = it.message ?: msgLoadError } loading = false refreshing = false @@ -128,7 +135,7 @@ fun MatchesScreen( }.onSuccess { session -> onOpenBroadcast(session.id) }.onFailure { - showMessage(it.message ?: "Impossibile riprendere la diretta") + showMessage(it.message ?: msgResumeFailed) } actionLoading = false } @@ -365,7 +372,7 @@ fun MatchesScreen( openSetup(match) } .onFailure { - showMessage(it.message ?: "Impossibile creare la partita") + showMessage(it.message ?: msgCreateFailed) } actionLoading = false } @@ -393,10 +400,10 @@ fun MatchesScreen( ) }.onSuccess { match -> reload() - showMessage("Partita programmata — visibile sul sito") + showMessage(msgScheduledOk) configureMatch = match }.onFailure { - showMessage(it.message ?: "Errore creazione partita") + showMessage(it.message ?: msgScheduleError) } actionLoading = false } @@ -454,10 +461,10 @@ fun MatchesScreen( runCatching { container.matchRepository.deleteMatch(match.id) } .onSuccess { reload() - showMessage("Partita eliminata") + showMessage(msgDeleted) } .onFailure { - showMessage(it.message ?: "Impossibile eliminare") + showMessage(it.message ?: msgDeleteFailed) } } }, @@ -513,7 +520,7 @@ private fun MatchListCard( style = MaterialTheme.typography.titleMedium, ) Spacer(Modifier.height(4.dp)) - formatMatchDate(match.scheduledAt)?.let { + formatMatchDateOrNull(match.scheduledAt)?.let { Text(it, style = MaterialTheme.typography.bodyMedium) } match.location?.takeIf { it.isNotBlank() }?.let { diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepMatchScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepMatchScreen.kt index a1dabee..7727d4b 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepMatchScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepMatchScreen.kt @@ -26,7 +26,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.core.normalizeHexColor import com.matchlivetv.match_live_tv.data.AppContainer import com.matchlivetv.match_live_tv.data.api.ScoringRulesBody @@ -37,7 +39,7 @@ import com.matchlivetv.match_live_tv.streaming.overlay.OverlayKind import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton import com.matchlivetv.match_live_tv.ui.matches.MatchOutlinedField -import com.matchlivetv.match_live_tv.ui.matches.formatMatchDate +import com.matchlivetv.match_live_tv.ui.matches.formatMatchDateOrNull import com.matchlivetv.match_live_tv.ui.theme.MatchColors import kotlinx.coroutines.launch @@ -181,16 +183,23 @@ fun StepMatchScreen( } } + val opponentNameRequiredError = stringResource(R.string.wizard_error_opponent_name_required) + val pointsPerSetRequiredError = stringResource(R.string.wizard_error_points_per_set_required) + val tiebreakPointsRequiredError = stringResource(R.string.wizard_error_tiebreak_points_required) + val periodDurationRequiredError = stringResource(R.string.wizard_error_period_duration_required) + val overtimeDurationRequiredError = stringResource(R.string.wizard_error_overtime_duration_required) + val saveGenericError = stringResource(R.string.wizard_error_save_generic) + Column( Modifier .verticalScroll(rememberScrollState()) .padding(horizontal = 20.dp, vertical = 12.dp), ) { - Text("Dettagli partita", style = MaterialTheme.typography.headlineMedium) + Text(stringResource(R.string.wizard_match_details_title), style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(20.dp)) TeamBrandingRow( - sectionLabel = "Squadra di casa", + sectionLabel = stringResource(R.string.wizard_match_home_team_label), teamName = match.teamName, nameEditable = false, onTeamNameChange = {}, @@ -206,7 +215,7 @@ fun StepMatchScreen( ) Spacer(Modifier.height(16.dp)) TeamBrandingRow( - sectionLabel = "Squadra avversaria", + sectionLabel = stringResource(R.string.wizard_match_away_team_label), teamName = opponent, nameEditable = true, onTeamNameChange = { opponent = it }, @@ -221,15 +230,19 @@ fun StepMatchScreen( onClearLogo = { opponentLogoUri = null }, ) Spacer(Modifier.height(16.dp)) - MatchOutlinedField(value = location, onValueChange = { location = it }, label = "Luogo") + MatchOutlinedField( + value = location, + onValueChange = { location = it }, + label = stringResource(R.string.wizard_match_location_label), + ) Spacer(Modifier.height(12.dp)) MatchOutlinedField( value = campionato, onValueChange = { campionato = it }, - label = "Campionato (facoltativo)", + label = stringResource(R.string.wizard_match_category_label), ) Text( - "Es. Serie C, torneo estivo — lo useremo in descrizione e overlay.", + stringResource(R.string.wizard_match_category_hint), style = MaterialTheme.typography.bodyMedium, color = MatchColors.TextSecondary, modifier = Modifier.padding(top = 4.dp), @@ -238,8 +251,8 @@ fun StepMatchScreen( if (isScheduledMatch) { Spacer(Modifier.height(16.dp)) WizardReadOnlyField( - label = "Programmata per", - value = formatMatchDate(match.scheduledAt) ?: "—", + label = stringResource(R.string.wizard_match_scheduled_label), + value = formatMatchDateOrNull(match.scheduledAt) ?: "—", ) } @@ -250,7 +263,7 @@ fun StepMatchScreen( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text("Overlay video personalizzato", modifier = Modifier.weight(1f)) + Text(stringResource(R.string.wizard_match_custom_overlay_label), modifier = Modifier.weight(1f)) Switch(checked = customOverlay, onCheckedChange = { customOverlay = it }) } if (customOverlay) { @@ -282,17 +295,17 @@ fun StepMatchScreen( verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.weight(1f)) { - Text("Regole punteggio personalizzate") + Text(stringResource(R.string.wizard_match_custom_rules_label)) Text( when { !customRules && boardType in setOf("basket", "timed") -> - "Regole standard dello sport selezionato." + stringResource(R.string.wizard_match_rules_standard_sport) customRules && boardType in setOf("basket", "timed") -> - "Torneo non standard: tempi e periodi personalizzati." + stringResource(R.string.wizard_match_rules_custom_timed) customRules -> - "Torneo non standard: imposta set e punteggi." + stringResource(R.string.wizard_match_rules_custom_sets) else -> - "Standard FIPAV: 3 set per vincere, set a 25, tie-break a 15." + stringResource(R.string.wizard_match_rules_standard_sets) }, style = MaterialTheme.typography.bodyMedium, color = MatchColors.TextSecondary, @@ -304,7 +317,11 @@ fun StepMatchScreen( if (customRules && boardType in setOf("basket", "timed")) { Spacer(Modifier.height(16.dp)) Text( - if (boardType == "basket") "Quarti" else "Tempi", + if (boardType == "basket") { + stringResource(R.string.wizard_match_periods_basket_label) + } else { + stringResource(R.string.wizard_match_periods_timed_label) + }, style = MaterialTheme.typography.bodyMedium, ) Spacer(Modifier.height(8.dp)) @@ -333,7 +350,11 @@ fun StepMatchScreen( periodDurationText = text.filter { it.isDigit() }.take(3) periodDurationText.toIntOrNull()?.let { periodDurationMins = it.coerceIn(1, 120) } }, - label = if (boardType == "basket") "Minuti per quarto" else "Minuti per tempo", + label = if (boardType == "basket") { + stringResource(R.string.wizard_match_minutes_per_period_basket) + } else { + stringResource(R.string.wizard_match_minutes_per_period_timed) + }, ) Spacer(Modifier.height(12.dp)) MatchOutlinedField( @@ -342,13 +363,13 @@ fun StepMatchScreen( overtimeDurationText = text.filter { it.isDigit() }.take(3) overtimeDurationText.toIntOrNull()?.let { overtimeDurationMins = it.coerceIn(1, 60) } }, - label = "Minuti supplementari", + label = stringResource(R.string.wizard_match_overtime_minutes_label), ) } if (customRules && boardType in setOf("volley", "racket")) { Spacer(Modifier.height(16.dp)) - Text("Set da vincere la partita", style = MaterialTheme.typography.bodyMedium) + Text(stringResource(R.string.wizard_match_sets_to_win_label), style = MaterialTheme.typography.bodyMedium) Spacer(Modifier.height(8.dp)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { listOf(2, 3).forEach { value -> @@ -375,7 +396,7 @@ fun StepMatchScreen( pointsPerSetText = text.filter { it.isDigit() }.take(2) pointsPerSetText.toIntOrNull()?.let { pointsPerSet = it.coerceIn(1, 99) } }, - label = "Punti per vincere un set", + label = stringResource(R.string.wizard_match_points_per_set_label), ) Spacer(Modifier.height(12.dp)) MatchOutlinedField( @@ -384,28 +405,28 @@ fun StepMatchScreen( pointsDecidingSetText = text.filter { it.isDigit() }.take(2) pointsDecidingSetText.toIntOrNull()?.let { pointsDecidingSet = it.coerceIn(1, 99) } }, - label = "Punti tie-break (ultimo set)", + label = stringResource(R.string.wizard_match_tiebreak_points_label), ) } Spacer(Modifier.height(32.dp)) MatchPrimaryButton( - label = "AVANTI >", + label = stringResource(R.string.wizard_action_next), loading = saving, onClick = { if (opponent.isBlank()) { - onError("Inserisci il nome avversario") + onError(opponentNameRequiredError) return@MatchPrimaryButton } if (customRules && boardType in setOf("volley", "racket")) { val perSet = pointsPerSetText.toIntOrNull() val deciding = pointsDecidingSetText.toIntOrNull() if (perSet == null || perSet < 1) { - onError("Inserisci i punti per vincere un set") + onError(pointsPerSetRequiredError) return@MatchPrimaryButton } if (deciding == null || deciding < 1) { - onError("Inserisci i punti del tie-break") + onError(tiebreakPointsRequiredError) return@MatchPrimaryButton } pointsPerSet = perSet @@ -415,11 +436,11 @@ fun StepMatchScreen( val periodMins = periodDurationText.toIntOrNull() val overtimeMins = overtimeDurationText.toIntOrNull() if (periodMins == null || periodMins < 1) { - onError("Inserisci la durata del periodo in minuti") + onError(periodDurationRequiredError) return@MatchPrimaryButton } if (overtimeMins == null || overtimeMins < 1) { - onError("Inserisci la durata dei supplementari in minuti") + onError(overtimeDurationRequiredError) return@MatchPrimaryButton } periodDurationMins = periodMins @@ -484,7 +505,7 @@ fun StepMatchScreen( container.wizardSession.match = updated onNext() }.onFailure { - onError(it.message ?: "Errore nel salvataggio") + onError(it.message ?: saveGenericError) } saving = false } diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepNetworkTestScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepNetworkTestScreen.kt index a63d5e8..6619071 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepNetworkTestScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepNetworkTestScreen.kt @@ -26,8 +26,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.data.AppContainer import com.matchlivetv.match_live_tv.domain.Match import com.matchlivetv.match_live_tv.domain.StreamSession @@ -109,42 +111,52 @@ fun StepNetworkTestScreen( val shareUrl = currentSession.watchShareUrl() + val shareSubject = stringResource(R.string.wizard_network_share_subject, match.teamName, match.opponentName) + val shareChooserLiveTitle = stringResource(R.string.wizard_share_chooser_live) + val shareChooserRegiaTitle = stringResource(R.string.wizard_share_chooser_regia) + val regiaLinkError = stringResource(R.string.wizard_error_regia_link) + val startLiveError = stringResource(R.string.wizard_error_start_live) + Column( Modifier .verticalScroll(rememberScrollState()) .padding(horizontal = 20.dp, vertical = 12.dp), ) { - Text("Test rete", style = MaterialTheme.typography.headlineMedium) + Text(stringResource(R.string.wizard_network_test_title), style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(8.dp)) Text( - "Verifica che la connessione regga l'upload della diretta.", + stringResource(R.string.wizard_network_test_subtitle), style = MaterialTheme.typography.bodyMedium, ) Spacer(Modifier.height(24.dp)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { MetricCard( - label = "Download", + label = stringResource(R.string.wizard_network_download_label), value = if (testing) "..." else "${"%.1f".format(downloadMbps)} Mbps", modifier = Modifier.weight(1f), ) MetricCard( - label = "Upload", + label = stringResource(R.string.wizard_network_upload_label), value = if (testing) "..." else "${"%.1f".format(uploadMbps)} Mbps", highlight = ready, modifier = Modifier.weight(1f), ) MetricCard( - label = "Latenza", + label = stringResource(R.string.wizard_network_latency_label), value = if (testing) "..." else "$latencyMs ms", modifier = Modifier.weight(1f), ) } Spacer(Modifier.height(12.dp)) - MetricCard(label = "Tipo rete", value = networkType, modifier = Modifier.fillMaxWidth()) + MetricCard( + label = stringResource(R.string.wizard_network_type_label), + value = networkType, + modifier = Modifier.fillMaxWidth(), + ) if (testCompleted && ready) { Spacer(Modifier.height(20.dp)) Text( - "PRONTO PER ANDARE IN DIRETTA", + stringResource(R.string.wizard_network_ready_label), color = MatchColors.SuccessGreen, style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, @@ -153,7 +165,7 @@ fun StepNetworkTestScreen( selectedQualityLabel?.let { quality -> Spacer(Modifier.height(12.dp)) WizardReadOnlyField( - label = "Qualità streaming (automatica)", + label = stringResource(R.string.wizard_network_quality_label), value = quality, ) } @@ -161,30 +173,31 @@ fun StepNetworkTestScreen( if (testCompleted && shareUrl != null) { Spacer(Modifier.height(16.dp)) WizardReadOnlyField( - label = if (currentSession.platform == "youtube") "Link YouTube" else "Link diretta", + label = if (currentSession.platform == "youtube") { + stringResource(R.string.wizard_network_link_youtube_label) + } else { + stringResource(R.string.wizard_network_link_live_label) + }, value = shareUrl, ) Spacer(Modifier.height(8.dp)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { MatchSecondaryButton( - label = "COPIA", + label = stringResource(R.string.wizard_action_copy), onClick = { copyToClipboard(context, shareUrl) }, modifier = Modifier.weight(1f), ) MatchSecondaryButton( - label = "CONDIVIDI", + label = stringResource(R.string.wizard_action_share), onClick = { context.startActivity( Intent.createChooser( Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, shareUrl) - putExtra( - Intent.EXTRA_SUBJECT, - "Diretta — ${match.teamName} vs ${match.opponentName}", - ) + putExtra(Intent.EXTRA_SUBJECT, shareSubject) }, - "Condividi diretta", + shareChooserLiveTitle, ), ) }, @@ -193,7 +206,7 @@ fun StepNetworkTestScreen( } Spacer(Modifier.height(8.dp)) MatchSecondaryButton( - label = "CONDIVIDI LINK REGIA", + label = stringResource(R.string.wizard_action_share_regia_link), onClick = { scope.launch { runCatching { container.sessionRepository.createRegiaLink(currentSession.id) } @@ -204,11 +217,11 @@ fun StepNetworkTestScreen( type = "text/plain" putExtra(Intent.EXTRA_TEXT, url) }, - "Condividi regia", + shareChooserRegiaTitle, ), ) } - .onFailure { onError(it.message ?: "Errore link regia") } + .onFailure { onError(it.message ?: regiaLinkError) } } }, ) @@ -216,7 +229,11 @@ fun StepNetworkTestScreen( if (!testCompleted) { Spacer(Modifier.height(24.dp)) MatchSecondaryButton( - label = if (testing) "TEST IN CORSO..." else "AVVIA TEST RETE", + label = if (testing) { + stringResource(R.string.wizard_network_test_running_label) + } else { + stringResource(R.string.wizard_network_test_start_label) + }, enabled = !testing, onClick = { runTest() }, ) @@ -224,14 +241,14 @@ fun StepNetworkTestScreen( Spacer(Modifier.height(32.dp)) Row { MatchSecondaryButton( - label = "Indietro", + label = stringResource(R.string.wizard_action_back), onClick = onBack, enabled = !starting, modifier = Modifier.weight(1f), ) Spacer(Modifier.width(12.dp)) MatchPrimaryButton( - label = "INIZIA >", + label = stringResource(R.string.wizard_action_start), loading = starting, enabled = ready, onClick = { @@ -242,7 +259,7 @@ fun StepNetworkTestScreen( container.wizardSession.setSession(started, match) onStartLive(started) } - .onFailure { onError(it.message ?: "Errore avvio diretta") } + .onFailure { onError(it.message ?: startLiveError) } starting = false } }, diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepTransmissionScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepTransmissionScreen.kt index e5e5122..54715ff 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepTransmissionScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepTransmissionScreen.kt @@ -20,7 +20,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.data.AppContainer import com.matchlivetv.match_live_tv.domain.Match import com.matchlivetv.match_live_tv.domain.Team @@ -57,12 +59,15 @@ fun StepTransmissionScreen( val loadedTeam = team val youtubeReady = loadedTeam?.isYoutubeReady == true + val youtubeActivatingLabel = stringResource(R.string.wizard_transmission_youtube_activating) val youtubeSubtitle = when { - loadedTeam == null -> "Canale in attivazione" - !loadedTeam.canUseYoutube -> "Premium Light o Full" - !loadedTeam.isYoutubeReady -> "Canale in attivazione" + loadedTeam == null -> youtubeActivatingLabel + !loadedTeam.canUseYoutube -> stringResource(R.string.wizard_transmission_youtube_premium_required) + !loadedTeam.isYoutubeReady -> youtubeActivatingLabel else -> loadedTeam.youtubeDestinationLabel } + val youtubeUnavailableError = stringResource(R.string.wizard_error_youtube_unavailable) + val sessionCreateError = stringResource(R.string.wizard_error_session_create) Column( Modifier @@ -71,7 +76,7 @@ fun StepTransmissionScreen( ) { team?.planName?.let { plan -> Text( - "Piano $plan", + stringResource(R.string.wizard_transmission_plan_label, plan), style = MaterialTheme.typography.bodyMedium, color = MatchColors.TextSecondary, modifier = Modifier @@ -79,11 +84,11 @@ fun StepTransmissionScreen( .padding(bottom = 12.dp), ) } - Text("Piattaforma", style = MaterialTheme.typography.headlineMedium) + Text(stringResource(R.string.wizard_transmission_platform_title), style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(12.dp)) WizardPlatformCard( title = "Match Live TV", - subtitle = "Diretta sul nostro sito (incluso)", + subtitle = stringResource(R.string.wizard_transmission_platform_site_subtitle), selected = platform == "matchlivetv", onClick = { platform = "matchlivetv" }, ) @@ -93,27 +98,27 @@ fun StepTransmissionScreen( subtitle = youtubeSubtitle, selected = platform == "youtube", enabled = youtubeReady, - badge = if (team?.canUseYoutube != true) "Premium" else null, + badge = if (team?.canUseYoutube != true) stringResource(R.string.wizard_transmission_youtube_badge) else null, onClick = { if (youtubeReady) { platform = "youtube" } else { - onError("YouTube non disponibile per questa squadra") + onError(youtubeUnavailableError) } }, ) Spacer(Modifier.height(24.dp)) - Text("Visibilità", style = MaterialTheme.typography.headlineMedium) + Text(stringResource(R.string.wizard_transmission_visibility_title), style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(12.dp)) Row(horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp)) { WizardChoiceButton( - label = "PUBBLICO", + label = stringResource(R.string.wizard_transmission_public_label), selected = privacy == "public", onClick = { privacy = "public" }, modifier = Modifier.weight(1f), ) WizardChoiceButton( - label = "NON IN ELENCO", + label = stringResource(R.string.wizard_transmission_unlisted_label), selected = privacy == "unlisted", onClick = { privacy = "unlisted" }, modifier = Modifier.weight(1f), @@ -122,15 +127,15 @@ fun StepTransmissionScreen( Spacer(Modifier.height(8.dp)) Text( if (privacy == "public") { - "Compare nell'elenco dirette su MatchLiveTV.it, nelle ricerche e sul canale YouTube (se selezionato)." + stringResource(R.string.wizard_transmission_public_desc) } else { - "Non compare negli elenchi pubblici né nelle ricerche. Solo chi ha il link può guardare." + stringResource(R.string.wizard_transmission_unlisted_desc) }, style = MaterialTheme.typography.bodyMedium, color = MatchColors.TextSecondary, ) Text( - "La partita resta sempre visibile nel backend della squadra per tutta la durata dell'abbonamento.", + stringResource(R.string.wizard_transmission_backend_note), style = MaterialTheme.typography.bodyMedium, color = MatchColors.TextSecondary, modifier = Modifier.padding(top = 6.dp), @@ -138,13 +143,13 @@ fun StepTransmissionScreen( Spacer(Modifier.height(32.dp)) Row { MatchSecondaryButton( - label = "Indietro", + label = stringResource(R.string.wizard_action_back), onClick = onBack, modifier = Modifier.weight(1f), ) Spacer(Modifier.width(12.dp)) MatchPrimaryButton( - label = "AVANTI >", + label = stringResource(R.string.wizard_action_next), loading = creating, onClick = { creating = true @@ -160,7 +165,7 @@ fun StepTransmissionScreen( container.wizardSession.setSession(session, match) onNext() }.onFailure { - onError(it.message ?: "Errore creazione sessione") + onError(it.message ?: sessionCreateError) } creating = false } diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/TeamBrandingEditor.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/TeamBrandingEditor.kt index 57c6233..6906dad 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/TeamBrandingEditor.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/TeamBrandingEditor.kt @@ -39,9 +39,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.core.parseColorHex import com.matchlivetv.match_live_tv.core.placeholderTeamColor import com.matchlivetv.match_live_tv.core.resolveMediaUrl @@ -71,6 +73,8 @@ fun TeamBrandingRow( val placeholderColor = remember(placeholderColorSeed) { placeholderTeamColor(placeholderColorSeed) } val displayColorHex = primaryColorHex.ifBlank { placeholderColor } val displayColor = Color(parseColorHex(displayColorHex, 0xFFE53935.toInt())) + val teamFallbackName = stringResource(R.string.wizard_branding_team_fallback_name) + val opponentNamePlaceholder = stringResource(R.string.wizard_branding_opponent_name_placeholder) var menuExpanded by remember { mutableStateOf(false) } var showCustomize by remember { mutableStateOf(false) } @@ -99,7 +103,7 @@ fun TeamBrandingRow( ) ColorAccentBar(color = displayColor, height = 36.dp) Text( - text = teamName.ifBlank { "Squadra" }, + text = teamName.ifBlank { teamFallbackName }, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -111,12 +115,12 @@ fun TeamBrandingRow( InlineTeamNameField( value = teamName, onValueChange = onTeamNameChange, - placeholder = "Nome avversario", + placeholder = opponentNamePlaceholder, modifier = Modifier.weight(1f), ) } else { Text( - text = teamName.ifBlank { "Squadra" }, + text = teamName.ifBlank { teamFallbackName }, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -129,7 +133,7 @@ fun TeamBrandingRow( IconButton(onClick = { menuExpanded = true }) { Icon( Icons.Default.MoreVert, - contentDescription = "Modifica squadra", + contentDescription = stringResource(R.string.wizard_branding_edit_team_cd), tint = MatchColors.TextSecondary, ) } @@ -138,7 +142,7 @@ fun TeamBrandingRow( onDismissRequest = { menuExpanded = false }, ) { DropdownMenuItem( - text = { Text("Personalizza") }, + text = { Text(stringResource(R.string.wizard_branding_customize_title)) }, onClick = { menuExpanded = false showCustomize = true @@ -203,7 +207,7 @@ private fun TeamBrandingCustomizeSheet( .padding(horizontal = 20.dp) .padding(bottom = 32.dp), ) { - Text("Personalizza", style = MaterialTheme.typography.headlineSmall) + Text(stringResource(R.string.wizard_branding_customize_title), style = MaterialTheme.typography.headlineSmall) Text( title, style = MaterialTheme.typography.bodyMedium, @@ -212,25 +216,30 @@ private fun TeamBrandingCustomizeSheet( Spacer(Modifier.height(20.dp)) if (nameEditable) { - Text("Nome squadra", style = MaterialTheme.typography.labelLarge) + Text(stringResource(R.string.wizard_branding_team_name_label), style = MaterialTheme.typography.labelLarge) Spacer(Modifier.height(8.dp)) OutlinedTextField( value = draftName, onValueChange = { draftName = it }, singleLine = true, modifier = Modifier.fillMaxWidth(), - placeholder = { Text("Nome avversario", color = MatchColors.TextSecondary) }, + placeholder = { + Text( + stringResource(R.string.wizard_branding_opponent_name_placeholder), + color = MatchColors.TextSecondary, + ) + }, colors = outlinedFieldColors(), ) Spacer(Modifier.height(20.dp)) } else { - Text("Nome squadra", style = MaterialTheme.typography.labelLarge) + Text(stringResource(R.string.wizard_branding_team_name_label), style = MaterialTheme.typography.labelLarge) Spacer(Modifier.height(4.dp)) Text(teamName, style = MaterialTheme.typography.titleMedium) Spacer(Modifier.height(20.dp)) } - Text("Logo", style = MaterialTheme.typography.labelLarge) + Text(stringResource(R.string.wizard_branding_logo_label), style = MaterialTheme.typography.labelLarge) Spacer(Modifier.height(10.dp)) Row( verticalAlignment = Alignment.CenterVertically, @@ -242,15 +251,15 @@ private fun TeamBrandingCustomizeSheet( size = 72.dp, ) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - MatchSecondaryButton(label = "CARICA LOGO", onClick = onPickLogo) + MatchSecondaryButton(label = stringResource(R.string.wizard_branding_upload_logo), onClick = onPickLogo) if (hasLogo) { - MatchSecondaryButton(label = "RIMUOVI", onClick = onClearLogo) + MatchSecondaryButton(label = stringResource(R.string.wizard_branding_remove_logo), onClick = onClearLogo) } } } Spacer(Modifier.height(20.dp)) - Text("Colore squadra", style = MaterialTheme.typography.labelLarge) + Text(stringResource(R.string.wizard_branding_color_label), style = MaterialTheme.typography.labelLarge) Spacer(Modifier.height(12.dp)) TeamColorPickerPanel( initialColorHex = pickerInitialColor, @@ -259,11 +268,11 @@ private fun TeamBrandingCustomizeSheet( Spacer(Modifier.height(24.dp)) MatchPrimaryButton( - label = "SALVA", + label = stringResource(R.string.wizard_branding_save), onClick = { onConfirm(draftName.trim(), draftColor.trim()) }, ) Spacer(Modifier.height(8.dp)) - MatchSecondaryButton(label = "ANNULLA", onClick = onDismiss) + MatchSecondaryButton(label = stringResource(R.string.wizard_branding_cancel), onClick = onDismiss) } } } @@ -295,7 +304,11 @@ private fun TeamLogoPreview( modifier = Modifier.size(size), contentScale = ContentScale.Crop, ) - else -> Text("Nessun logo", style = MaterialTheme.typography.labelMedium, color = MatchColors.TextSecondary) + else -> Text( + stringResource(R.string.wizard_branding_no_logo), + style = MaterialTheme.typography.labelMedium, + color = MatchColors.TextSecondary, + ) } } } diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/TeamColorPicker.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/TeamColorPicker.kt index 39becea..23aa3ac 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/TeamColorPicker.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/TeamColorPicker.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush @@ -33,6 +34,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.core.hsvToHex import com.matchlivetv.match_live_tv.core.parseColorHex import com.matchlivetv.match_live_tv.ui.theme.MatchColors @@ -152,7 +154,7 @@ private fun HueSlider( ) { Column(Modifier.fillMaxWidth()) { Text( - "Tonalità", + stringResource(R.string.wizard_color_hue), style = MaterialTheme.typography.labelMedium, color = MatchColors.TextSecondary, ) diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/WizardComponents.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/WizardComponents.kt index 84fddd0..6babd1d 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/WizardComponents.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/WizardComponents.kt @@ -29,17 +29,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton import com.matchlivetv.match_live_tv.ui.theme.MatchColors -private val stepTitles = listOf( - "01 · Partita", - "02 · Trasmissione", - "03 · Test rete", -) - @Composable fun WizardStepIndicator(currentStep: Int, modifier: Modifier = Modifier) { Row( @@ -62,8 +58,12 @@ fun WizardStepIndicator(currentStep: Int, modifier: Modifier = Modifier) { } } -fun wizardStepTitle(step: Int): String = - stepTitles[(step.coerceIn(1, 3) - 1)] +@Composable +fun wizardStepTitle(step: Int): String = when (step.coerceIn(1, 3)) { + 1 -> stringResource(R.string.wizard_step_title_match) + 2 -> stringResource(R.string.wizard_step_title_transmission) + else -> stringResource(R.string.wizard_step_title_network) +} private fun sportWizardIcon(sportKey: String, boardType: String): ImageVector = when { sportKey.contains("volley") || boardType == "volley" || boardType == "racket" -> diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/WizardShellScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/WizardShellScreen.kt index ca644e6..005a113 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/WizardShellScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/WizardShellScreen.kt @@ -26,7 +26,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.data.AppContainer import com.matchlivetv.match_live_tv.domain.Match import com.matchlivetv.match_live_tv.domain.StreamSession @@ -74,7 +76,11 @@ fun WizardShellScreen( title = { Text(wizardStepTitle(currentStep)) }, navigationIcon = { IconButton(onClick = onClose) { - Icon(Icons.Default.Close, contentDescription = "Chiudi", tint = Color.White) + Icon( + Icons.Default.Close, + contentDescription = stringResource(R.string.common_close), + tint = Color.White, + ) } }, actions = { @@ -104,7 +110,7 @@ fun WizardShellScreen( modifier = Modifier.align(Alignment.Center), ) match == null -> Text( - "Partita non trovata", + stringResource(R.string.wizard_match_not_found), modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.bodyMedium, ) @@ -128,7 +134,7 @@ fun WizardShellScreen( val session = container.wizardSession.session if (session == null) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Completa lo step Trasmissione") + Text(stringResource(R.string.wizard_complete_transmission_step)) } } else { StepNetworkTestScreen( 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 f15d6f2..7a696b5 100644 --- a/native/android/app/src/main/res/values-de/strings.xml +++ b/native/android/app/src/main/res/values-de/strings.xml @@ -47,4 +47,198 @@ Erneute Verbindung… Streaming-Fehler Beenden + + Schließen + Fehler + + 01 · Spiel + 02 · Übertragung + 03 · Netzwerktest + Spiel nicht gefunden + Schließe den Schritt Übertragung ab + WEITER > + Zurück + STARTEN > + Spieldetails + Heimteam + Gastteam + Ort + Liga (optional) + Z. B. Serie C, Sommerturnier — wird in Beschreibung und Overlay verwendet. + Geplant für + Individuelles Video-Overlay + Individuelle Punkteregeln + Standardregeln der gewählten Sportart. + Nicht standardmäßiges Turnier: individuelle Zeiten und Perioden. + Nicht standardmäßiges Turnier: Sätze und Punkte festlegen. + FIVB-Standard: 3 Gewinnsätze, Sätze bis 25, Tie-Break bis 15. + Viertel + Halbzeiten + Minuten pro Viertel + Minuten pro Halbzeit + Verlängerungsminuten + Sätze zum Spielgewinn + Punkte zum Satzgewinn + Tie-Break-Punkte (letzter Satz) + Gib den Namen des Gegners ein + Gib die Punkte zum Satzgewinn ein + Gib die Tie-Break-Punkte ein + Gib die Dauer der Periode in Minuten ein + Gib die Dauer der Verlängerung in Minuten ein + Fehler beim Speichern + Tarif %1$s + Plattform + Live auf unserer Website (inklusive) + Kanal wird aktiviert + Premium Light oder Full + Premium + YouTube für dieses Team nicht verfügbar + Sichtbarkeit + ÖFFENTLICH + NICHT GELISTET + Erscheint in der Live-Liste auf MatchLiveTV.it, in der Suche und im YouTube-Kanal (falls ausgewählt). + Erscheint nicht in öffentlichen Listen oder in der Suche. Nur mit dem Link kann zugesehen werden. + Das Spiel bleibt für die gesamte Abo-Dauer im Team-Backend sichtbar. + Fehler beim Erstellen der Sitzung + Netzwerktest + Prüfe, ob deine Verbindung den Live-Upload verkraftet. + Download + Upload + Latenz + Netzwerktyp + BEREIT FÜR DEN LIVE-START + Streaming-Qualität (automatisch) + YouTube-Link + Live-Link + KOPIEREN + TEILEN + Live — %1$s gegen %2$s + Livestream teilen + REGIE-LINK TEILEN + Regie teilen + Fehler beim Regie-Link + TEST LÄUFT... + NETZWERKTEST STARTEN + Fehler beim Starten des Livestreams + Name des Gegners + Team + Team bearbeiten + Anpassen + Teamname + Logo + LOGO HOCHLADEN + ENTFERNEN + Teamfarbe + SPEICHERN + ABBRECHEN + Kein Logo + + Livestream pausiert + Pause: %1$s + Livestream fortgesetzt + Fortsetzen: %1$s + Von der Regie pausiert + Von der Regie fortgesetzt + RTMP-Fortsetzung: %1$s + Livestream von der Regie beendet + RTMP-URL fehlt + Sitzung nicht verfügbar + PAUSE + LIVE + VERBINDUNG… + NEUVERBINDUNG… + FEHLER + Erlaube Kamera und Mikrofon, um live zu gehen + BERECHTIGUNGEN ERTEILEN + Live-Link noch nicht verfügbar + Match Live TV Livestream + Regie-Link — %1$s + Fehler beim Regie-Link + Livestream beenden? + Der Stream wird für alle Zuschauer beendet. + BEENDEN + Steuerung ausblenden + Steuerung einblenden + Livestream teilen + Regie-Link teilen + Nächste Periode + Livestream fortsetzen + Livestream pausieren + Livestream beenden + HEIM + GAST + Anzeigetafel OK + Anzeigetafel offline + Halbzeit %1$d + Satz %1$d · %2$d Pkt. + %1$d Sätze + Sätze gewonnen %1$d-%2$d + Heim + Gast + +%1$d %2$s + Punkt hinzufügen %1$s + Punkt entfernen %1$s + Teilen + SATZ BEENDEN + %1$d Pkt. + + Satz beendet + %1$s gewinnt den Satz %2$d:%3$d.\n\nSatz beenden und zum nächsten wechseln? + Satz beenden + Weiter punkten + Der Punktestand erfüllt die Turnierregeln noch nicht. Satz trotzdem beenden? + Trotzdem beenden + Spiel beendet + %1$s gewinnt das Spiel (%2$d:%3$d Sätze).\n\nLivestream endgültig beenden? + Livestream beenden + Weiter senden + + Team wählen + Neues Spiel + Im Voraus planen oder die Live-Einrichtung sofort starten. + Spiel planen + Datum, Uhrzeit und Gegner — auch auf der Website sichtbar + Jetzt starten + Spiel erstellen und Wizard ohne Uhrzeit öffnen + Spiel planen + Der Livestream startet jetzt nicht: Er erscheint auf der Website mit Datum und Uhrzeit. Du startest das Streaming in der App, wenn du in der Halle bist. + Gegner + Ort (optional) + Datum und Uhrzeit + IM KALENDER SPEICHERN + Spiel wählen + Bereits auf der Website oder in der App geplante Spiele. + Geplant + Ohne Uhrzeit + ENTWURF + GEPLANT + IM KALENDER + Live läuft + Status: %1$s + KAMERA FORTSETZEN UND STREAMEN + EINRICHTUNG FORTSETZEN + Jetzt einrichten? + Du kannst die Übertragung jetzt vorbereiten oder zurückkommen, wenn du in der Halle bist. + Einrichten + Später + Spiel löschen + «%1$s vs %2$s» löschen? + +Dies kann nicht rückgängig gemacht werden. + Löschen + Laufenden Livestream fortsetzen + FORTSETZEN + LIVE + GEPLANT + STARTEN + Ladefehler + Livestream konnte nicht fortgesetzt werden + Spiel konnte nicht erstellt werden + Spiel geplant — auf der Website sichtbar + Fehler beim Erstellen des Spiels + Spiel gelöscht + Löschen fehlgeschlagen + coach@team.com + Löschen + Farbton 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 828d739..f5ed475 100644 --- a/native/android/app/src/main/res/values-en/strings.xml +++ b/native/android/app/src/main/res/values-en/strings.xml @@ -47,4 +47,198 @@ Reconnecting… Streaming error Stop + + Close + error + + 01 · Match + 02 · Broadcast + 03 · Network test + Match not found + Complete the Broadcast step + NEXT > + Back + START > + Match details + Home team + Away team + Location + Category (optional) + E.g. Serie C, summer tournament — we\'ll use it in the description and overlay. + Scheduled for + Custom video overlay + Custom scoring rules + Standard rules for the selected sport. + Non-standard tournament: custom times and periods. + Non-standard tournament: set your sets and points. + FIVB standard: 3 sets to win, sets to 25, tie-break to 15. + Quarters + Halves + Minutes per quarter + Minutes per half + Overtime minutes + Sets to win the match + Points to win a set + Tie-break points (last set) + Enter the opponent\'s name + Enter the points to win a set + Enter the tie-break points + Enter the period duration in minutes + Enter the overtime duration in minutes + Error while saving + Plan %1$s + Platform + Live on our site (included) + Channel being activated + Premium Light or Full + Premium + YouTube not available for this team + Visibility + PUBLIC + UNLISTED + Shown in the live list on MatchLiveTV.it, in search and on the YouTube channel (if selected). + Not shown in public lists or search. Only those with the link can watch. + The match always stays visible in the team backend for the whole subscription period. + Error creating session + Network test + Check that your connection can handle the live upload. + Download + Upload + Latency + Network type + READY TO GO LIVE + Streaming quality (automatic) + YouTube link + Live link + COPY + SHARE + Live — %1$s vs %2$s + Share live stream + SHARE CONTROL ROOM LINK + Share control room + Control room link error + TEST IN PROGRESS... + START NETWORK TEST + Error starting the live stream + Opponent name + Team + Edit team + Customize + Team name + Logo + UPLOAD LOGO + REMOVE + Team color + SAVE + CANCEL + No logo + + Live paused + Pause: %1$s + Live resumed + Resume: %1$s + Paused from control room + Resumed from control room + RTMP resume: %1$s + Live closed from control room + Missing RTMP URL + Session not available + PAUSED + LIVE + CONNECTING… + RECONNECTING… + ERROR + Allow camera and microphone to go live + GRANT PERMISSIONS + Live link not available yet + Match Live TV stream + Control room link — %1$s + Control room link error + End the live stream? + The stream will be closed for all viewers. + END + Hide controls + Show controls + Share live stream + Share control room link + Next period + Resume live + Pause live + End live + HOME + AWAY + Scoreboard OK + Scoreboard offline + Period %1$d + Set %1$d · %2$d pts + %1$d sets + Sets won %1$d-%2$d + home + away + +%1$d %2$s + Add point %1$s + Remove point %1$s + Share + CLOSE SET + %1$d pts + + Set finished + %1$s wins the set %2$d-%3$d.\n\nClose the set and move to the next one? + Close set + Keep scoring + The score doesn\'t meet the tournament rules yet. Close the set anyway? + Close anyway + Match finished + %1$s wins the match (%2$d-%3$d sets).\n\nEnd the live stream for good? + End live + Keep streaming + + Choose team + New match + Schedule ahead or start live setup right away. + Schedule match + Date, time and opponent — also visible on the site + Start now + Create the match and open the wizard without a time + Schedule match + Streaming does not start now: it will appear on the site with date and time. You will start streaming from the app when you are at the gym. + Opponent + Location (optional) + Date and time + SAVE TO SCHEDULE + Choose match + Matches already scheduled on the site or in the app. + Scheduled + No time set + DRAFT + SCHEDULED + ON CALENDAR + Live in progress + Status: %1$s + RESUME CAMERA AND STREAM + CONTINUE SETUP + Configure now? + You can prepare the stream now, or come back when you are at the gym. + Configure + Later + Delete match + Delete «%1$s vs %2$s»? + +This cannot be undone. + Delete + Resume live stream + RESUME + LIVE + SCHEDULED + START + Loading error + Could not resume the live stream + Could not create the match + Match scheduled — visible on the site + Error creating match + Match deleted + Could not delete + coach@team.com + Delete + Hue 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 bcf3e50..ec5dd36 100644 --- a/native/android/app/src/main/res/values-es/strings.xml +++ b/native/android/app/src/main/res/values-es/strings.xml @@ -47,4 +47,198 @@ Reconectando… Error de streaming Terminar + + Cerrar + error + + 01 · Partido + 02 · Transmisión + 03 · Test de red + Partido no encontrado + Completa el paso Transmisión + SIGUIENTE > + Atrás + EMPEZAR > + Detalles del partido + Equipo local + Equipo visitante + Lugar + Categoría (opcional) + Ej. Serie C, torneo de verano — lo usaremos en la descripción y el overlay. + Programado para + Overlay de vídeo personalizado + Reglas de puntuación personalizadas + Reglas estándar del deporte seleccionado. + Torneo no estándar: tiempos y periodos personalizados. + Torneo no estándar: define sets y puntos. + Estándar FIVB: 3 sets para ganar, sets a 25, tie-break a 15. + Cuartos + Tiempos + Minutos por cuarto + Minutos por tiempo + Minutos de prórroga + Sets para ganar el partido + Puntos para ganar un set + Puntos del tie-break (último set) + Introduce el nombre del rival + Introduce los puntos para ganar un set + Introduce los puntos del tie-break + Introduce la duración del periodo en minutos + Introduce la duración de la prórroga en minutos + Error al guardar + Plan %1$s + Plataforma + En directo en nuestro sitio (incluido) + Canal en activación + Premium Light o Full + Premium + YouTube no disponible para este equipo + Visibilidad + PÚBLICO + NO LISTADO + Aparece en la lista de directos de MatchLiveTV.it, en las búsquedas y en el canal de YouTube (si se selecciona). + No aparece en listas públicas ni en búsquedas. Solo puede verlo quien tenga el enlace. + El partido permanece siempre visible en el backend del equipo durante toda la suscripción. + Error al crear la sesión + Test de red + Comprueba que tu conexión soporta la subida del directo. + Descarga + Subida + Latencia + Tipo de red + LISTO PARA EMITIR EN DIRECTO + Calidad de streaming (automática) + Enlace de YouTube + Enlace del directo + COPIAR + COMPARTIR + Directo — %1$s vs %2$s + Compartir directo + COMPARTIR ENLACE DE REGIE + Compartir regie + Error en el enlace de regie + TEST EN CURSO... + INICIAR TEST DE RED + Error al iniciar el directo + Nombre del rival + Equipo + Editar equipo + Personalizar + Nombre del equipo + Logo + SUBIR LOGO + QUITAR + Color del equipo + GUARDAR + CANCELAR + Sin logo + + Directo en pausa + Pausa: %1$s + Directo reanudado + Reanudación: %1$s + Pausado desde regie + Reanudado desde regie + Reanudación RTMP: %1$s + Directo cerrado desde regie + Falta la URL RTMP + Sesión no disponible + PAUSA + EN DIRECTO + CONECTANDO… + RECONECTANDO… + ERROR + Permite la cámara y el micrófono para salir en directo + CONCEDER PERMISOS + Enlace del directo aún no disponible + Directo de Match Live TV + Enlace de regie — %1$s + Error en el enlace de regie + ¿Terminar el directo? + El streaming se cerrará para todos los espectadores. + TERMINAR + Ocultar controles + Mostrar controles + Compartir directo + Compartir enlace de regie + Periodo siguiente + Reanudar directo + Pausar directo + Terminar directo + LOCAL + VISITANTE + Marcador OK + Marcador sin conexión + Tiempo %1$d + Set %1$d · %2$d pts + %1$d sets + Sets ganados %1$d-%2$d + local + visitante + +%1$d %2$s + Añadir punto %1$s + Quitar punto %1$s + Compartir + CERRAR SET + %1$d pts + + Set finalizado + %1$s gana el set %2$d-%3$d.\n\n¿Cerrar el set y pasar al siguiente? + Cerrar set + Seguir puntuando + El marcador aún no cumple las reglas del torneo. ¿Cerrar el set de todas formas? + Cerrar de todas formas + Partido finalizado + %1$s gana el partido (%2$d-%3$d sets).\n\n¿Cerrar definitivamente el directo? + Cerrar directo + Seguir en directo + + Elegir equipo + Nuevo partido + Programa con antelación o inicia ya la configuración del directo. + Programar partido + Fecha, hora y rival — también visible en el sitio + Empezar ahora + Crea el partido y abre el asistente sin horario + Programar partido + El directo no empieza ahora: aparecerá en el sitio con fecha y hora. Iniciarás el streaming desde la app cuando estés en el pabellón. + Rival + Lugar (opcional) + Fecha y hora + GUARDAR EN EL CALENDARIO + Elegir partido + Partidos ya programados en el sitio o en la app. + Programados + Sin horario + BORRADOR + PROGRAMADO + EN CALENDARIO + Directo en curso + Estado: %1$s + REANUDAR CÁMARA Y EMITIR + CONTINUAR CONFIGURACIÓN + ¿Configurar ahora? + Puedes preparar la emisión ahora o volver cuando estés en el pabellón. + Configurar + Más tarde + Eliminar partido + ¿Eliminar «%1$s vs %2$s»? + +Esta acción no se puede deshacer. + Eliminar + Reanudar directo en curso + REANUDAR + EN DIRECTO + PROGRAMADO + INICIAR + Error de carga + No se pudo reanudar el directo + No se pudo crear el partido + Partido programado — visible en el sitio + Error al crear el partido + Partido eliminado + No se pudo eliminar + coach@equipo.com + Eliminar + Tono 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 7f606fe..57451fa 100644 --- a/native/android/app/src/main/res/values-fr/strings.xml +++ b/native/android/app/src/main/res/values-fr/strings.xml @@ -47,4 +47,198 @@ Reconnexion… Erreur de streaming Arrêter + + Fermer + erreur + + 01 · Match + 02 · Diffusion + 03 · Test réseau + Match introuvable + Complétez l\'étape Diffusion + SUIVANT > + Retour + DÉMARRER > + Détails du match + Équipe à domicile + Équipe adverse + Lieu + Championnat (facultatif) + Ex. Serie C, tournoi d\'été — utilisé dans la description et l\'overlay. + Programmé pour + Overlay vidéo personnalisé + Règles de score personnalisées + Règles standard du sport sélectionné. + Tournoi non standard : temps et périodes personnalisés. + Tournoi non standard : définissez sets et points. + Standard FIVB : 3 sets gagnants, sets à 25, tie-break à 15. + Quart-temps + Mi-temps + Minutes par quart-temps + Minutes par mi-temps + Minutes de prolongation + Sets pour gagner le match + Points pour gagner un set + Points du tie-break (dernier set) + Saisissez le nom de l\'adversaire + Saisissez les points pour gagner un set + Saisissez les points du tie-break + Saisissez la durée de la période en minutes + Saisissez la durée de la prolongation en minutes + Erreur lors de l\'enregistrement + Forfait %1$s + Plateforme + Direct sur notre site (inclus) + Chaîne en cours d\'activation + Premium Light ou Full + Premium + YouTube non disponible pour cette équipe + Visibilité + PUBLIC + NON RÉPERTORIÉ + Apparaît dans la liste des directs sur MatchLiveTV.it, dans les recherches et sur la chaîne YouTube (si sélectionnée). + N\'apparaît pas dans les listes publiques ni dans les recherches. Seules les personnes ayant le lien peuvent regarder. + Le match reste toujours visible dans le backend de l\'équipe pendant toute la durée de l\'abonnement. + Erreur lors de la création de la session + Test réseau + Vérifiez que votre connexion supporte l\'envoi du direct. + Téléchargement + Envoi + Latence + Type de réseau + PRÊT À PASSER EN DIRECT + Qualité de streaming (automatique) + Lien YouTube + Lien du direct + COPIER + PARTAGER + Direct — %1$s vs %2$s + Partager le direct + PARTAGER LE LIEN RÉGIE + Partager la régie + Erreur du lien régie + TEST EN COURS... + LANCER LE TEST RÉSEAU + Erreur au démarrage du direct + Nom de l\'adversaire + Équipe + Modifier l\'équipe + Personnaliser + Nom de l\'équipe + Logo + CHARGER LE LOGO + SUPPRIMER + Couleur de l\'équipe + ENREGISTRER + ANNULER + Aucun logo + + Direct en pause + Pause : %1$s + Direct repris + Reprise : %1$s + Mis en pause depuis la régie + Repris depuis la régie + Reprise RTMP : %1$s + Direct fermé depuis la régie + URL RTMP manquante + Session non disponible + PAUSE + EN DIRECT + CONNEXION… + RECONNEXION… + ERREUR + Autorisez la caméra et le micro pour passer en direct + ACCORDER LES AUTORISATIONS + Lien du direct pas encore disponible + Direct Match Live TV + Lien régie — %1$s + Erreur du lien régie + Terminer le direct ? + Le streaming sera fermé pour tous les spectateurs. + TERMINER + Masquer les commandes + Afficher les commandes + Partager le direct + Partager le lien régie + Période suivante + Reprendre le direct + Mettre le direct en pause + Terminer le direct + DOMICILE + EXTÉRIEUR + Tableau OK + Tableau hors ligne + Période %1$d + Set %1$d · %2$d pts + %1$d sets + Sets gagnés %1$d-%2$d + domicile + extérieur + +%1$d %2$s + Ajouter un point %1$s + Retirer un point %1$s + Partager + FERMER LE SET + %1$d pts + + Set terminé + %1$s remporte le set %2$d-%3$d.\n\nFermer le set et passer au suivant ? + Fermer le set + Continuer à marquer + Le score ne respecte pas encore les règles du tournoi. Fermer le set quand même ? + Fermer quand même + Match terminé + %1$s remporte le match (%2$d-%3$d sets).\n\nFermer définitivement le direct ? + Fermer le direct + Continuer le direct + + Choisir l\'équipe + Nouveau match + Planifiez à l\'avance ou lancez la configuration du direct tout de suite. + Planifier le match + Date, heure et adversaire — aussi visibles sur le site + Démarrer maintenant + Créez le match et ouvrez l\'assistant sans horaire + Planifier le match + Le direct ne démarre pas maintenant : il apparaîtra sur le site avec date et heure. Vous lancerez le streaming depuis l\'app une fois en salle. + Adversaire + Lieu (facultatif) + Date et heure + ENREGISTRER AU CALENDRIER + Choisir le match + Matchs déjà planifiés sur le site ou dans l\'app. + Planifiés + Sans horaire + BROUILLON + PLANIFIÉ + AU CALENDRIER + Direct en cours + État : %1$s + REPRENDRE CAMÉRA ET DIFFUSER + CONTINUER LA CONFIGURATION + Configurer maintenant ? + Vous pouvez préparer la diffusion maintenant, ou revenir une fois en salle. + Configurer + Plus tard + Supprimer le match + Supprimer « %1$s vs %2$s » ? + +Cette action est irréversible. + Supprimer + Reprendre le direct en cours + REPRENDRE + EN DIRECT + PLANIFIÉ + DÉMARRER + Erreur de chargement + Impossible de reprendre le direct + Impossible de créer le match + Match planifié — visible sur le site + Erreur lors de la création du match + Match supprimé + Impossible de supprimer + coach@equipe.com + Supprimer + Teinte diff --git a/native/android/app/src/main/res/values/strings.xml b/native/android/app/src/main/res/values/strings.xml index db6002b..bc37a25 100644 --- a/native/android/app/src/main/res/values/strings.xml +++ b/native/android/app/src/main/res/values/strings.xml @@ -47,4 +47,198 @@ Riconnessione… Errore streaming Termina + + Chiudi + errore + + 01 · Partita + 02 · Trasmissione + 03 · Test rete + Partita non trovata + Completa lo step Trasmissione + AVANTI > + Indietro + INIZIA > + Dettagli partita + Squadra di casa + Squadra avversaria + Luogo + Campionato (facoltativo) + Es. Serie C, torneo estivo — lo useremo in descrizione e overlay. + Programmata per + Overlay video personalizzato + Regole punteggio personalizzate + Regole standard dello sport selezionato. + Torneo non standard: tempi e periodi personalizzati. + Torneo non standard: imposta set e punteggi. + Standard FIPAV: 3 set per vincere, set a 25, tie-break a 15. + Quarti + Tempi + Minuti per quarto + Minuti per tempo + Minuti supplementari + Set da vincere la partita + Punti per vincere un set + Punti tie-break (ultimo set) + Inserisci il nome avversario + Inserisci i punti per vincere un set + Inserisci i punti del tie-break + Inserisci la durata del periodo in minuti + Inserisci la durata dei supplementari in minuti + Errore nel salvataggio + Piano %1$s + Piattaforma + Diretta sul nostro sito (incluso) + Canale in attivazione + Premium Light o Full + Premium + YouTube non disponibile per questa squadra + Visibilità + PUBBLICO + NON IN ELENCO + Compare nell\'elenco dirette su MatchLiveTV.it, nelle ricerche e sul canale YouTube (se selezionato). + Non compare negli elenchi pubblici né nelle ricerche. Solo chi ha il link può guardare. + La partita resta sempre visibile nel backend della squadra per tutta la durata dell\'abbonamento. + Errore creazione sessione + Test rete + Verifica che la connessione regga l\'upload della diretta. + Download + Upload + Latenza + Tipo rete + PRONTO PER ANDARE IN DIRETTA + Qualità streaming (automatica) + Link YouTube + Link diretta + COPIA + CONDIVIDI + Diretta — %1$s vs %2$s + Condividi diretta + CONDIVIDI LINK REGIA + Condividi regia + Errore link regia + TEST IN CORSO... + AVVIA TEST RETE + Errore avvio diretta + Nome avversario + Squadra + Modifica squadra + Personalizza + Nome squadra + Logo + CARICA LOGO + RIMUOVI + Colore squadra + SALVA + ANNULLA + Nessun logo + + Diretta in pausa + Pausa: %1$s + Diretta ripresa + Ripresa: %1$s + Pausa dalla regia + Ripresa dalla regia + Ripresa RTMP: %1$s + Diretta chiusa dalla regia + URL RTMP mancante + Sessione non disponibile + PAUSA + IN DIRETTA + CONNESSIONE… + RICONNESSIONE… + ERRORE + Consenti camera e microfono per andare in diretta + CONCEDI PERMESSI + Link diretta non ancora disponibile + Diretta Match Live TV + Link regia — %1$s + Errore link regia + Terminare la diretta? + Lo streaming verrà chiuso per tutti gli spettatori. + TERMINA + Nascondi controlli + Mostra controlli + Condividi diretta + Condividi link regia + Periodo successivo + Riprendi diretta + Pausa diretta + Termina diretta + CASA + OSPITE + Tabellone OK + Tabellone offline + %1$d° tempo + Set %1$d · %2$d pt + %1$d set + Set vinti %1$d-%2$d + casa + ospite + +%1$d %2$s + Aggiungi punto %1$s + Togli punto %1$s + Condividi + CHIUDI SET + %1$d pt + + Set concluso + %1$s vince il set %2$d-%3$d.\n\nChiudere il set e passare al successivo? + Chiudi set + Continua a segnare + Il punteggio non soddisfa ancora le regole del torneo. Chiudere il set comunque? + Chiudi comunque + Partita terminata + %1$s vince la partita (%2$d-%3$d set).\n\nChiudere definitivamente la diretta? + Chiudi diretta + Continua in onda + + Scegli squadra + Nuova partita + Programma in anticipo o avvia la configurazione diretta subito. + Programma partita + Data, ora e avversario — visibile anche sul sito + Avvia subito + Crea la partita e passa al wizard senza orario + Programma partita + La diretta non parte ora: comparirà sul sito con data e ora. Avvierai lo streaming dall\'app quando sei in palestra. + Avversario + Luogo (opzionale) + Data e ora + SALVA IN PROGRAMMA + Scegli partita + Partite già programmate sul sito o in app. + Programmate + Senza orario + BOZZA + PROGRAMMATA + IN CALENDARIO + Diretta in corso + Stato: %1$s + RIPRENDI CAMERA E TRASMETTI + CONTINUA CONFIGURAZIONE + Configurare ora? + Puoi preparare la trasmissione subito, oppure tornare quando sei in palestra. + Configura + Più tardi + Elimina partita + Eliminare «%1$s vs %2$s»? + +L\'operazione non si può annullare. + Elimina + Riprendi diretta in corso + RIPRENDI + IN CORSO + PROGRAMMATA + AVVIA + Errore caricamento + Impossibile riprendere la diretta + Impossibile creare la partita + Partita programmata — visibile sul sito + Errore creazione partita + Partita eliminata + Impossibile eliminare + coach@team.com + Elimina + Tonalità