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à