feat(i18n): extract last deep-cut hardcoded strings to resources

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 "<name>"` 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).
This commit is contained in:
Stéphane 2026-05-11 07:00:56 +02:00
parent 46a82dce9a
commit eebc2cd452
9 changed files with 84 additions and 14 deletions

View file

@ -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 {

View file

@ -1192,4 +1192,17 @@
<string name="unit_bytes_kb">Ko</string>
<string name="unit_bytes_mb">Mo</string>
<string name="unit_bytes_gb">Go</string>
<string name="details_runtime_hours_minutes">%1$d h %2$d min</string>
<string name="details_runtime_hours_only">%1$d h</string>
<string name="details_runtime_minutes_only">%1$d min</string>
<string name="search_error_no_results_for_catalog">Aucun résultat de recherche pour %1$s.</string>
<string name="plugins_repository_already_installed">Ce dépôt de plugin est déjà installé.</string>
<string name="plugins_repository_install_failed">Impossible dinstaller le dépôt de plugin</string>
<string name="plugins_repository_refresh_failed">Impossible dactualiser le dépôt</string>
<string name="plugins_manifest_name_missing">Le nom du manifeste est manquant.</string>
<string name="plugins_manifest_version_missing">La version du manifeste est manquante.</string>
<string name="plugins_manifest_no_providers">Le manifeste ne contient aucun fournisseur.</string>
<string name="addons_manifest_missing_field">Champ « %1$s » manquant dans le manifeste</string>
<string name="player_error_mpv_unavailable">Moteur de lecture MPV indisponible. Reconstruis lapp.</string>
<string name="player_error_unable_to_play_stream">Impossible de lire ce flux.</string>
</resources>

View file

@ -1272,4 +1272,17 @@
<string name="unit_bytes_kb">KB</string>
<string name="unit_bytes_mb">MB</string>
<string name="unit_bytes_gb">GB</string>
<string name="details_runtime_hours_minutes">%1$dh %2$dm</string>
<string name="details_runtime_hours_only">%1$dh</string>
<string name="details_runtime_minutes_only">%1$dm</string>
<string name="search_error_no_results_for_catalog">No search results returned for %1$s.</string>
<string name="plugins_repository_already_installed">That plugin repository is already installed.</string>
<string name="plugins_repository_install_failed">Unable to install plugin repository</string>
<string name="plugins_repository_refresh_failed">Unable to refresh repository</string>
<string name="plugins_manifest_name_missing">Manifest name is missing.</string>
<string name="plugins_manifest_version_missing">Manifest version is missing.</string>
<string name="plugins_manifest_no_providers">Manifest has no providers.</string>
<string name="addons_manifest_missing_field">Manifest missing \"%1$s\"</string>
<string name="player_error_mpv_unavailable">MPV player engine not available. Please rebuild the app.</string>
<string name="player_error_unable_to_play_stream">Unable to play this stream.</string>
</resources>

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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()}",

View file

@ -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<PluginManifest>(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
}
}

View file

@ -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

View file

@ -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
}