From eebc2cd4520c60caecc3efa095a2c9414beda1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Mon, 11 May 2026 07:00:56 +0200 Subject: [PATCH] feat(i18n): extract last deep-cut hardcoded strings to resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates 13 additional user-facing English literals found across 7 files in a follow-up audit. Adds 13 new keys (EN + FR) and replaces the inline literals with `getString(...)` / `runBlocking { getString(...) }` depending on context. - RuntimeFormat: episode / runtime hours+minutes template (`%dh %dm`, `%dh`, `%dm` → resource-backed; FR uses `%d h %d min`). - SearchRepository: `require {}` empty-result message now hits `search_error_no_results_for_catalog` instead of the inline literal. - PluginRepository: `already installed`, install-failed, refresh-failed fallback messages. - PluginManifestParser: three `require {}` validation messages (name, version, providers). - AddonManifestParser: `Manifest missing ""` exception now uses `addons_manifest_missing_field`. - PlayerEngine.ios: `MPV not available — Please rebuild the app` → `player_error_mpv_unavailable`. - YoutubeChunkedDataSourceFactory: the developer-facing `No DataSpec` exception is replaced by a user-friendly `Unable to play this stream.` message that's safe to render through `onPlayerError.localizedMessage`. FR translations follow existing conventions (tutoiement, curly apostrophes, NBSP inside `« »`, internal branch names kept out of user-facing copy). --- .../trailer/YoutubeChunkedDataSourceFactory.kt | 8 +++++++- .../composeResources/values-fr/strings.xml | 13 +++++++++++++ .../composeResources/values/strings.xml | 13 +++++++++++++ .../app/features/addons/AddonManifestParser.kt | 8 +++++++- .../app/features/details/RuntimeFormat.kt | 17 +++++++++++++---- .../app/features/search/SearchRepository.kt | 4 +++- .../features/plugins/PluginManifestParser.kt | 18 +++++++++++++++--- .../app/features/plugins/PluginRepository.kt | 12 +++++++++--- .../app/features/player/PlayerEngine.ios.kt | 5 ++++- 9 files changed, 84 insertions(+), 14 deletions(-) diff --git a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/trailer/YoutubeChunkedDataSourceFactory.kt b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/trailer/YoutubeChunkedDataSourceFactory.kt index 5d93ab92..79335c17 100644 --- a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/trailer/YoutubeChunkedDataSourceFactory.kt +++ b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/trailer/YoutubeChunkedDataSourceFactory.kt @@ -8,6 +8,10 @@ import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSpec import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.TransferListener +import kotlinx.coroutines.runBlocking +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.player_error_unable_to_play_stream +import org.jetbrains.compose.resources.getString /** * A DataSource.Factory that wraps DefaultHttpDataSource and appends YouTube's @@ -75,7 +79,9 @@ class YoutubeChunkedDataSourceFactory( } private fun openNextChunk(): Long { - val spec = originalDataSpec ?: throw IllegalStateException("No DataSpec") + val spec = originalDataSpec ?: throw IllegalStateException( + runBlocking { getString(Res.string.player_error_unable_to_play_stream) }, + ) val end = if (totalContentLength != C.LENGTH_UNSET.toLong()) { minOf(currentChunkStart + chunkSize - 1, currentChunkStart + totalContentLength - 1) } else { diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml index 15301839..22de317f 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -1192,4 +1192,17 @@ Ko Mo Go + %1$d h %2$d min + %1$d h + %1$d min + Aucun résultat de recherche pour %1$s. + Ce dépôt de plugin est déjà installé. + Impossible d’installer le dépôt de plugin + Impossible d’actualiser le dépôt + Le nom du manifeste est manquant. + La version du manifeste est manquante. + Le manifeste ne contient aucun fournisseur. + Champ « %1$s » manquant dans le manifeste + Moteur de lecture MPV indisponible. Reconstruis l’app. + Impossible de lire ce flux. diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index cd8a97e2..dc4d8771 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1272,4 +1272,17 @@ KB MB GB + %1$dh %2$dm + %1$dh + %1$dm + No search results returned for %1$s. + That plugin repository is already installed. + Unable to install plugin repository + Unable to refresh repository + Manifest name is missing. + Manifest version is missing. + Manifest has no providers. + Manifest missing \"%1$s\" + MPV player engine not available. Please rebuild the app. + Unable to play this stream. diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonManifestParser.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonManifestParser.kt index 414c4cf1..48634dcc 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonManifestParser.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonManifestParser.kt @@ -1,5 +1,6 @@ package com.nuvio.app.features.addons +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject @@ -8,6 +9,9 @@ import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.addons_manifest_missing_field +import org.jetbrains.compose.resources.getString internal object AddonManifestParser { private val json = Json { @@ -92,7 +96,9 @@ internal object AddonManifestParser { private fun JsonObject.requiredString(name: String): String = optionalString(name)?.takeIf { it.isNotBlank() } - ?: throw IllegalArgumentException("Manifest missing \"$name\"") + ?: throw IllegalArgumentException( + runBlocking { getString(Res.string.addons_manifest_missing_field, name) }, + ) private fun JsonObject.optionalString(name: String): String? = this[name]?.jsonPrimitive?.contentOrNull diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/RuntimeFormat.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/RuntimeFormat.kt index 375cba10..05704df9 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/RuntimeFormat.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/RuntimeFormat.kt @@ -1,5 +1,12 @@ package com.nuvio.app.features.details +import kotlinx.coroutines.runBlocking +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.details_runtime_hours_minutes +import nuvio.composeapp.generated.resources.details_runtime_hours_only +import nuvio.composeapp.generated.resources.details_runtime_minutes_only +import org.jetbrains.compose.resources.getString + private val hourTokenRegex = Regex("""(?i)(\d+)\s*h(?:ours?)?""") private val minuteTokenRegex = Regex("""(?i)(\d+)\s*m(?:in(?:ute)?s?)?""") private val hourMinuteColonRegex = Regex("""^\s*(\d+)\s*:\s*(\d{1,2})\s*$""") @@ -16,10 +23,12 @@ internal fun formatRuntimeFromMinutes(totalMinutes: Int): String { val hours = totalMinutes / 60 val minutes = totalMinutes % 60 - return when { - hours > 0 && minutes > 0 -> "${hours}h ${minutes}m" - hours > 0 -> "${hours}h" - else -> "${minutes}m" + return runBlocking { + when { + hours > 0 && minutes > 0 -> getString(Res.string.details_runtime_hours_minutes, hours, minutes) + hours > 0 -> getString(Res.string.details_runtime_hours_only, hours) + else -> getString(Res.string.details_runtime_minutes_only, minutes) + } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchRepository.kt index b71d97a2..f7137da1 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchRepository.kt @@ -327,7 +327,9 @@ object SearchRepository { search = query, ).withUnreleasedFilter() val items = page.items - require(items.isNotEmpty()) { "No search results returned for $catalogName." } + require(items.isNotEmpty()) { + getString(Res.string.search_error_no_results_for_catalog, catalogName) + } return HomeCatalogSection( key = "${manifest.id}:search:$type:$catalogId:${query.lowercase()}", diff --git a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginManifestParser.kt b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginManifestParser.kt index d8b00417..7981f311 100644 --- a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginManifestParser.kt +++ b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginManifestParser.kt @@ -1,6 +1,12 @@ package com.nuvio.app.features.plugins +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.plugins_manifest_name_missing +import nuvio.composeapp.generated.resources.plugins_manifest_no_providers +import nuvio.composeapp.generated.resources.plugins_manifest_version_missing +import org.jetbrains.compose.resources.getString internal object PluginManifestParser { private val json = Json { @@ -9,9 +15,15 @@ internal object PluginManifestParser { fun parse(payload: String): PluginManifest { val manifest = json.decodeFromString(payload) - require(manifest.name.isNotBlank()) { "Manifest name is missing." } - require(manifest.version.isNotBlank()) { "Manifest version is missing." } - require(manifest.scrapers.isNotEmpty()) { "Manifest has no providers." } + require(manifest.name.isNotBlank()) { + runBlocking { getString(Res.string.plugins_manifest_name_missing) } + } + require(manifest.version.isNotBlank()) { + runBlocking { getString(Res.string.plugins_manifest_version_missing) } + } + require(manifest.scrapers.isNotEmpty()) { + runBlocking { getString(Res.string.plugins_manifest_no_providers) } + } return manifest } } diff --git a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRepository.kt b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRepository.kt index 32e0562f..85bdad69 100644 --- a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRepository.kt +++ b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRepository.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -25,6 +26,11 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.put +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.plugins_repository_already_installed +import nuvio.composeapp.generated.resources.plugins_repository_install_failed +import nuvio.composeapp.generated.resources.plugins_repository_refresh_failed +import org.jetbrains.compose.resources.getString @Serializable private data class PluginRow( @@ -149,7 +155,7 @@ actual object PluginRepository { } if (_uiState.value.repositories.any { it.manifestUrl == manifestUrl }) { - return AddPluginRepositoryResult.Error("That plugin repository is already installed.") + return AddPluginRepositoryResult.Error(getString(Res.string.plugins_repository_already_installed)) } return try { @@ -168,7 +174,7 @@ actual object PluginRepository { pushToServer() AddPluginRepositoryResult.Success(repo) } catch (error: Throwable) { - AddPluginRepositoryResult.Error(error.message ?: "Unable to install plugin repository") + AddPluginRepositoryResult.Error(error.message ?: getString(Res.string.plugins_repository_install_failed)) } } @@ -232,7 +238,7 @@ actual object PluginRepository { if (existing.manifestUrl == manifestUrl) { existing.copy( isRefreshing = false, - errorMessage = error.message ?: "Unable to refresh repository", + errorMessage = error.message ?: runBlocking { getString(Res.string.plugins_repository_refresh_failed) }, ) } else { existing diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt index 2877b04c..5bafb9fc 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt @@ -14,6 +14,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.player_error_mpv_unavailable +import org.jetbrains.compose.resources.getString private const val TAG = "NuvioiOSPlayer" @@ -44,7 +47,7 @@ actual fun PlatformPlayerSurface( if (bridge == null) { LaunchedEffect(Unit) { - latestOnError.value("MPV player engine not available. Please rebuild the app.") + latestOnError.value(getString(Res.string.player_error_mpv_unavailable)) } return }