From 3183eababcddc8251be906a4ccbb9345ab26f5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Mon, 11 May 2026 06:31:42 +0200 Subject: [PATCH] feat(i18n): extract remaining hardcoded strings flagged by audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates the long tail of user-facing English literals identified by the audit report — 65 new keys across 14 source files plus the matching FR translations (tutoiement, NBSP, curly apostrophes). Categories: - TMDB filter placeholders in CollectionEditorScreen (10 keys, "28,12", "2020-01-01", "100", "en, ko, ja, hi", etc.) - Collection editor titles & errors in CollectionEditorRepository (TMDB/Trakt fallback titles, validation errors, "Could not load …") - Folder type labels in FolderDetailRepository ("Trakt Series List", "Trakt Movie List") - TMDB resolver errors (api key required, not found, missing IDs, discover no data) in TmdbCollectionSourceResolver - Trakt public list resolver errors and search-result fallbacks in TraktPublicListSourceResolver (rate limit, not public, items/likes counts, credentials missing, error_with_code formatter) - Comments load failure in TraktCommentsRepository - Trakt status messages (Connected/Disconnected) in TraktAuthRepository - AppUpdater exceptions (no release, github api error, missing tag, no APK asset) — also strips the "cmp-rewrite" branch name from the user-facing copy - HTTP error fallbacks shared by both AddonPlatform.{android,ios}.kt ("Request failed with HTTP %d", "Empty response body") - AndroidAppUpdaterPlatform download/install errors - iOS DownloadsPlatformDownloader file/HTTP errors - SubtitleRepository addon-subtitle display format - DownloadsLiveStatusPlatform.android Pause action (reuses compose_action_pause) and the byte-unit table (now includes unit_bytes_tb) Non-Composable suspend bodies use the existing `getString(...)` pattern; synchronous bodies (constructors, lambda inits) use the established `runBlocking { getString(...) }` pattern already in the codebase. Keys that overlap with #943 (the FR overhaul PR) use the exact same names so the eventual merge is a no-op on those. --- .../updater/AndroidAppUpdaterPlatform.kt | 12 ++- .../features/addons/AddonPlatform.android.kt | 9 ++- .../DownloadsLiveStatusPlatform.android.kt | 12 ++- .../composeResources/values-fr/strings.xml | 68 ++++++++++++++++ .../composeResources/values/strings.xml | 68 ++++++++++++++++ .../collection/CollectionEditorRepository.kt | 80 ++++++++++++++----- .../collection/CollectionEditorScreen.kt | 24 +++--- .../collection/FolderDetailRepository.kt | 14 +++- .../TmdbCollectionSourceResolver.kt | 49 +++++++----- .../app/features/player/SubtitleRepository.kt | 6 +- .../app/features/trakt/TraktAuthRepository.kt | 4 +- .../features/trakt/TraktCommentsRepository.kt | 4 +- .../trakt/TraktPublicListSourceResolver.kt | 70 ++++++++++------ .../nuvio/app/features/updater/AppUpdater.kt | 9 ++- .../app/features/addons/AddonPlatform.ios.kt | 21 +++-- .../DownloadsPlatformDownloader.ios.kt | 21 +++-- 16 files changed, 364 insertions(+), 107 deletions(-) diff --git a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/updater/AndroidAppUpdaterPlatform.kt b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/updater/AndroidAppUpdaterPlatform.kt index 72c2e50e..390ce23f 100644 --- a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/updater/AndroidAppUpdaterPlatform.kt +++ b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/updater/AndroidAppUpdaterPlatform.kt @@ -7,9 +7,15 @@ import android.os.Build import android.provider.Settings import androidx.core.content.FileProvider import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.updates_download_failed_http +import nuvio.composeapp.generated.resources.updates_downloaded_file_missing +import nuvio.composeapp.generated.resources.updates_empty_download_body import okhttp3.OkHttpClient import okhttp3.Request +import org.jetbrains.compose.resources.getString import java.io.File import java.io.FileOutputStream import java.util.concurrent.TimeUnit @@ -63,10 +69,10 @@ object AndroidAppUpdaterPlatform { httpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) { - error("Download failed with HTTP ${response.code}") + error(runBlocking { getString(Res.string.updates_download_failed_http, response.code) }) } - val body = response.body ?: error("Empty download body") + val body = response.body ?: error(runBlocking { getString(Res.string.updates_empty_download_body) }) val totalBytes = body.contentLength().takeIf { it > 0L } body.byteStream().use { input -> FileOutputStream(destination).use { output -> @@ -115,7 +121,7 @@ object AndroidAppUpdaterPlatform { fun installDownloadedApk(path: String): Result = runCatching { val context = requireContext() val apkFile = File(path) - check(apkFile.exists()) { "Downloaded update file is missing." } + check(apkFile.exists()) { runBlocking { getString(Res.string.updates_downloaded_file_missing) } } val apkUri = FileProvider.getUriForFile( context, diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt index e7fbe541..56941837 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt @@ -4,7 +4,12 @@ import android.content.Context import android.content.SharedPreferences import com.nuvio.app.core.network.IPv4FirstDns import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.network_empty_response_body +import nuvio.composeapp.generated.resources.network_request_failed_http +import org.jetbrains.compose.resources.getString import okhttp3.ResponseBody import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -153,10 +158,10 @@ private suspend fun executeTextRequest( addonHttpClient.newCall(request).execute().use { response -> val payload = readResponseBody(response.body) if (!response.isSuccessful) { - error("Request failed with HTTP ${response.code}") + error(runBlocking { getString(Res.string.network_request_failed_http, response.code) }) } if (payload.isBlank()) { - throw IllegalStateException("Empty response body") + throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) }) } payload } diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/downloads/DownloadsLiveStatusPlatform.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/downloads/DownloadsLiveStatusPlatform.android.kt index f8f5d9d3..ba21cd68 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/downloads/DownloadsLiveStatusPlatform.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/downloads/DownloadsLiveStatusPlatform.android.kt @@ -111,7 +111,7 @@ internal actual object DownloadsLiveStatusPlatform { .setPriority(NotificationCompat.PRIORITY_LOW) .addAction( 0, - "Pause", + runBlocking { getString(Res.string.compose_action_pause) }, buildActionPendingIntent( context = context, action = DownloadsNotificationActionReceiver.actionPause, @@ -178,7 +178,15 @@ internal actual object DownloadsLiveStatusPlatform { private fun formatBytes(bytes: Long): String { val safe = bytes.coerceAtLeast(0L).toDouble() - val units = arrayOf("B", "KB", "MB", "GB", "TB") + val units = runBlocking { + arrayOf( + getString(Res.string.unit_bytes_b), + getString(Res.string.unit_bytes_kb), + getString(Res.string.unit_bytes_mb), + getString(Res.string.unit_bytes_gb), + getString(Res.string.unit_bytes_tb), + ) + } var value = safe var unitIndex = 0 while (value >= 1024.0 && unitIndex < units.lastIndex) { diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml index 15b373b8..09ee00cb 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -1192,4 +1192,72 @@ Ko Mo Go + To + 28,12 + 18,35 + 2020-01-01 + 2024-12-31 + 7.0 + 10 + 100 + en, ko, ja, hi + US, KR, JP, IN + 2024 + ID %1$s + %1$s (%2$s) + Connecté à Trakt + Déconnecté de Trakt + Saisis un nom, une URL ou un ID de liste Trakt + Saisis un ID ou une URL de liste Trakt + Impossible de charger la liste Trakt + Liste Trakt %1$d + Liste Trakt résolue + Aucune liste Trakt trouvée + Saisis un ID ou une URL TMDB valide. + Liste TMDB %1$s + Collection TMDB %1$s + Production TMDB %1$s + Chaîne TMDB %1$s + Personne TMDB %1$s + Réalisateur TMDB %1$s + Découverte TMDB + Impossible de charger la source TMDB + Liste Trakt de séries + Liste Trakt de films + Ajoute une clé API TMDB dans les Paramètres pour utiliser les sources TMDB. + Liste TMDB introuvable + Collection TMDB introuvable + Société TMDB introuvable + Chaîne TMDB introuvable + Personne TMDB introuvable + ID de liste TMDB manquant + ID de collection TMDB manquant + ID de personne TMDB manquant + Crédits TMDB de la personne introuvables + Découverte TMDB n’a retourné aucune donnée + ID de liste Trakt manquant + Saisis un ID ou une URL de liste Trakt valide + La liste Trakt ne contient pas d’ID numérique + Échec de la requête Trakt + Identifiants Trakt manquants. + %1$d éléments + %1$d j’aime + Liste Trakt publique + Liste Trakt introuvable ou non publique + Limite de requêtes Trakt atteinte + %1$s (%2$d) + Échec du chargement des commentaires Trakt (%1$d) + Aucune mise à jour n’a encore été publiée. + Erreur API releases GitHub : %1$d + La release n’a pas de tag ni de nom + Aucun fichier APK trouvé dans la release + Échec du téléchargement (HTTP %1$d) + Corps de téléchargement vide + Le fichier de mise à jour téléchargé est introuvable. + Échec de la requête (HTTP %1$d) + Corps de réponse vide + Impossible de finaliser le fichier de téléchargement + Impossible d’ouvrir le fichier de téléchargement partiel + Le fichier de téléchargement partiel n’est pas ouvert + Impossible d’écrire dans le fichier de téléchargement partiel diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index c32e32d0..32aefcbf 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1284,4 +1284,72 @@ KB MB GB + TB + 28,12 + 18,35 + 2020-01-01 + 2024-12-31 + 7.0 + 10 + 100 + en, ko, ja, hi + US, KR, JP, IN + 2024 + ID %1$s + %1$s (%2$s) + Connected to Trakt + Disconnected from Trakt + Enter a Trakt list name, URL, or ID + Enter a Trakt list ID or URL + Could not load Trakt list + Trakt List %1$d + Resolved Trakt list + No Trakt lists found + Enter a valid TMDB ID or URL. + TMDB List %1$s + TMDB Collection %1$s + TMDB Production %1$s + TMDB Network %1$s + TMDB Person %1$s + TMDB Director %1$s + TMDB Discover + Could not load TMDB source + Trakt Series List + Trakt Movie List + Add a TMDB API key in Settings to use TMDB sources. + TMDB list not found + TMDB collection not found + TMDB company not found + TMDB network not found + TMDB person not found + Missing TMDB list ID + Missing TMDB collection ID + Missing TMDB person ID + TMDB person credits not found + TMDB discover returned no data + Missing Trakt list ID + Enter a valid Trakt list ID or URL + Trakt list did not include a numeric ID + Trakt request failed + Missing Trakt credentials. + %1$d items + %1$d likes + Trakt public list + Trakt list not found or not public + Trakt rate limit reached + %1$s (%2$d) + Failed to load Trakt comments (%1$d) + No update has been published yet. + GitHub releases API error: %1$d + Release has no tag or name + No APK asset found in the release + Download failed with HTTP %1$d + Empty download body + Downloaded update file is missing. + Request failed with HTTP %1$d + Empty response body + Failed to finalize download file + Failed to open partial download file + Partial download file is not open + Failed to write partial download file diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt index 70b5204f..5f4b41e7 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt @@ -11,6 +11,26 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.collections_editor_tmdb_collection_title_format +import nuvio.composeapp.generated.resources.collections_editor_tmdb_director_title_format +import nuvio.composeapp.generated.resources.collections_editor_tmdb_discover_title +import nuvio.composeapp.generated.resources.collections_editor_tmdb_invalid_id_or_url +import nuvio.composeapp.generated.resources.collections_editor_tmdb_list_title_format +import nuvio.composeapp.generated.resources.collections_editor_tmdb_network_title_format +import nuvio.composeapp.generated.resources.collections_editor_tmdb_person_title_format +import nuvio.composeapp.generated.resources.collections_editor_tmdb_production_title_format +import nuvio.composeapp.generated.resources.collections_editor_tmdb_source_load_failed +import nuvio.composeapp.generated.resources.collections_editor_trakt_enter_id_or_url +import nuvio.composeapp.generated.resources.collections_editor_trakt_enter_name_url_or_id +import nuvio.composeapp.generated.resources.collections_editor_trakt_fallback_title +import nuvio.composeapp.generated.resources.collections_editor_trakt_load_failed +import nuvio.composeapp.generated.resources.collections_editor_trakt_no_lists_found +import nuvio.composeapp.generated.resources.collections_editor_trakt_resolved_subtitle +import nuvio.composeapp.generated.resources.media_movies +import nuvio.composeapp.generated.resources.media_series +import org.jetbrains.compose.resources.getString import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -394,7 +414,9 @@ object CollectionEditorRepository { val state = _uiState.value val query = state.traktInput.trim() if (query.isBlank()) { - _uiState.value = state.copy(traktSearchError = "Enter a Trakt list name, URL, or ID") + _uiState.value = state.copy( + traktSearchError = runBlocking { getString(Res.string.collections_editor_trakt_enter_name_url_or_id) }, + ) return } @@ -402,12 +424,13 @@ object CollectionEditorRepository { val results = if (query.isTraktListIdentifierInput()) { runCatching { val metadata = TraktPublicListSourceResolver.listImportMetadata(query) - val id = metadata.traktListId ?: error("Could not load Trakt list") + val id = metadata.traktListId + ?: error(getString(Res.string.collections_editor_trakt_load_failed)) listOf( TraktPublicListSearchResult( traktListId = id, - title = metadata.title ?: "Trakt List $id", - subtitle = "Resolved Trakt list", + title = metadata.title ?: getString(Res.string.collections_editor_trakt_fallback_title, id), + subtitle = getString(Res.string.collections_editor_trakt_resolved_subtitle), coverImageUrl = metadata.coverImageUrl, ), ) @@ -419,7 +442,7 @@ object CollectionEditorRepository { _uiState.value = _uiState.value.copy( traktSearchResults = mapped, traktSearchError = results.exceptionOrNull()?.message - ?: if (mapped.isEmpty()) "No Trakt lists found" else null, + ?: if (mapped.isEmpty()) getString(Res.string.collections_editor_trakt_no_lists_found) else null, ) } } @@ -624,19 +647,24 @@ object CollectionEditorRepository { } val id = TmdbCollectionSourceResolver.parseTmdbId(state.tmdbInput) if (sourceType != TmdbCollectionSourceType.DISCOVER && id == null) { - _uiState.value = state.copy(tmdbSearchError = "Enter a valid TMDB ID or URL.") + _uiState.value = state.copy( + tmdbSearchError = runBlocking { getString(Res.string.collections_editor_tmdb_invalid_id_or_url) }, + ) return } val mediaTypes = selectedMediaTypes(state, sourceType) val baseTitle = state.tmdbTitleInput.ifBlank { - when (sourceType) { - TmdbCollectionSourceType.LIST -> "TMDB List ${id ?: ""}".trim() - TmdbCollectionSourceType.COLLECTION -> "TMDB Collection ${id ?: ""}".trim() - TmdbCollectionSourceType.COMPANY -> "TMDB Production ${id ?: ""}".trim() - TmdbCollectionSourceType.NETWORK -> "TMDB Network ${id ?: ""}".trim() - TmdbCollectionSourceType.PERSON -> "TMDB Person ${id ?: ""}".trim() - TmdbCollectionSourceType.DIRECTOR -> "TMDB Director ${id ?: ""}".trim() - TmdbCollectionSourceType.DISCOVER -> "TMDB Discover" + runBlocking { + val idArg = id?.toString().orEmpty() + when (sourceType) { + TmdbCollectionSourceType.LIST -> getString(Res.string.collections_editor_tmdb_list_title_format, idArg).trim() + TmdbCollectionSourceType.COLLECTION -> getString(Res.string.collections_editor_tmdb_collection_title_format, idArg).trim() + TmdbCollectionSourceType.COMPANY -> getString(Res.string.collections_editor_tmdb_production_title_format, idArg).trim() + TmdbCollectionSourceType.NETWORK -> getString(Res.string.collections_editor_tmdb_network_title_format, idArg).trim() + TmdbCollectionSourceType.PERSON -> getString(Res.string.collections_editor_tmdb_person_title_format, idArg).trim() + TmdbCollectionSourceType.DIRECTOR -> getString(Res.string.collections_editor_tmdb_director_title_format, idArg).trim() + TmdbCollectionSourceType.DISCOVER -> getString(Res.string.collections_editor_tmdb_discover_title) + } } } val sources = mediaTypes.map { mediaType -> @@ -656,7 +684,8 @@ object CollectionEditorRepository { val resolved = metadata.getOrNull() if (metadata.isFailure) { _uiState.value = _uiState.value.copy( - tmdbSearchError = metadata.exceptionOrNull()?.message ?: "Could not load TMDB source", + tmdbSearchError = metadata.exceptionOrNull()?.message + ?: getString(Res.string.collections_editor_tmdb_source_load_failed), ) return@launch } @@ -701,7 +730,9 @@ object CollectionEditorRepository { val state = _uiState.value val input = state.traktInput.trim() if (input.isBlank()) { - _uiState.value = state.copy(traktSearchError = "Enter a Trakt list ID or URL") + _uiState.value = state.copy( + traktSearchError = runBlocking { getString(Res.string.collections_editor_trakt_enter_id_or_url) }, + ) return } @@ -711,12 +742,15 @@ object CollectionEditorRepository { val listId = resolved?.traktListId if (metadata.isFailure || listId == null) { _uiState.value = _uiState.value.copy( - traktSearchError = metadata.exceptionOrNull()?.message ?: "Could not load Trakt list", + traktSearchError = metadata.exceptionOrNull()?.message + ?: getString(Res.string.collections_editor_trakt_load_failed), ) return@launch } - val title = state.traktTitleInput.ifBlank { resolved.title ?: "Trakt List $listId" } + val title = state.traktTitleInput.ifBlank { + resolved.title ?: getString(Res.string.collections_editor_trakt_fallback_title, listId) + } addTraktSourcesToFolder( sources = selectedTraktMediaTypes(state).map { mediaType -> CollectionSource( @@ -881,9 +915,13 @@ private fun titleForMedia( addSuffix: Boolean, ): String { if (!addSuffix) return title - val suffix = when (mediaType) { - TmdbCollectionMediaType.MOVIE -> "Movies" - TmdbCollectionMediaType.TV -> "Series" + val suffix = runBlocking { + getString( + when (mediaType) { + TmdbCollectionMediaType.MOVIE -> Res.string.media_movies + TmdbCollectionMediaType.TV -> Res.string.media_series + }, + ) } return "$title $suffix" } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt index 7219395a..6a8c6f67 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt @@ -1161,7 +1161,11 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_genres), helper = stringResource(Res.string.collections_editor_tmdb_genres_helper), value = state.tmdbFilters.withGenres.orEmpty(), - placeholder = if (state.tmdbMediaType == TmdbCollectionMediaType.MOVIE) "28,12" else "18,35", + placeholder = if (state.tmdbMediaType == TmdbCollectionMediaType.MOVIE) { + stringResource(Res.string.collections_editor_tmdb_genres_movie_placeholder) + } else { + stringResource(Res.string.collections_editor_tmdb_genres_series_placeholder) + }, onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(withGenres = value.ifBlank { null }) @@ -1172,7 +1176,7 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_date_from), helper = stringResource(Res.string.collections_editor_tmdb_date_helper), value = state.tmdbFilters.releaseDateGte.orEmpty(), - placeholder = "2020-01-01", + placeholder = stringResource(Res.string.collections_editor_tmdb_date_from_placeholder), onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(releaseDateGte = value.ifBlank { null }) @@ -1183,7 +1187,7 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_date_to), helper = stringResource(Res.string.collections_editor_tmdb_date_helper), value = state.tmdbFilters.releaseDateLte.orEmpty(), - placeholder = "2024-12-31", + placeholder = stringResource(Res.string.collections_editor_tmdb_date_to_placeholder), onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(releaseDateLte = value.ifBlank { null }) @@ -1194,7 +1198,7 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_rating_min), helper = stringResource(Res.string.collections_editor_tmdb_rating_helper), value = state.tmdbFilters.voteAverageGte?.toString().orEmpty(), - placeholder = "7.0", + placeholder = stringResource(Res.string.collections_editor_tmdb_rating_min_placeholder), onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(voteAverageGte = value.toDoubleOrNull()) @@ -1205,7 +1209,7 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_rating_max), helper = stringResource(Res.string.collections_editor_tmdb_rating_helper), value = state.tmdbFilters.voteAverageLte?.toString().orEmpty(), - placeholder = "10", + placeholder = stringResource(Res.string.collections_editor_tmdb_rating_max_placeholder), onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(voteAverageLte = value.toDoubleOrNull()) @@ -1216,7 +1220,7 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_votes_min), helper = stringResource(Res.string.collections_editor_tmdb_votes_helper), value = state.tmdbFilters.voteCountGte?.toString().orEmpty(), - placeholder = "100", + placeholder = stringResource(Res.string.collections_editor_tmdb_votes_min_placeholder), onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(voteCountGte = value.toIntOrNull()) @@ -1240,7 +1244,7 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_language), helper = stringResource(Res.string.collections_editor_tmdb_language_helper), value = state.tmdbFilters.withOriginalLanguage.orEmpty(), - placeholder = "en, ko, ja, hi", + placeholder = stringResource(Res.string.collections_editor_tmdb_language_placeholder), onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(withOriginalLanguage = value.ifBlank { null }) @@ -1264,7 +1268,7 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_country), helper = stringResource(Res.string.collections_editor_tmdb_country_helper), value = state.tmdbFilters.withOriginCountry.orEmpty(), - placeholder = "US, KR, JP, IN", + placeholder = stringResource(Res.string.collections_editor_tmdb_country_placeholder), onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(withOriginCountry = value.ifBlank { null }) @@ -1346,7 +1350,7 @@ private fun TmdbSourcePickerScreen( label = stringResource(Res.string.collections_editor_tmdb_year), helper = stringResource(Res.string.collections_editor_tmdb_year_helper), value = state.tmdbFilters.year?.toString().orEmpty(), - placeholder = "2024", + placeholder = stringResource(Res.string.collections_editor_tmdb_year_placeholder), onValueChange = { value -> CollectionEditorRepository.updateTmdbFilters { it.copy(year = value.toIntOrNull()) @@ -2302,7 +2306,7 @@ private fun traktSourceSubtitle(source: CollectionSource): String { media, traktSortLabel(source.sortBy), traktDirectionLabel(source.sortHow), - "ID ${source.traktListId ?: ""}".trim(), + stringResource(Res.string.collections_editor_trakt_list_id_format, source.traktListId ?: ""), ).joinToString(" • ") } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt index d5c7a172..d4e6d9ef 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt @@ -26,6 +26,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.collections_folder_addon_not_found +import nuvio.composeapp.generated.resources.collections_folder_trakt_movie_list +import nuvio.composeapp.generated.resources.collections_folder_trakt_series_list import nuvio.composeapp.generated.resources.collections_tab_all import org.jetbrains.compose.resources.getString @@ -156,10 +158,14 @@ object FolderDetailRepository { } else if (source.isTrakt) { val mediaType = TmdbCollectionMediaType.fromString(source.mediaType) val type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie" - val typeLabel = if (mediaType == TmdbCollectionMediaType.TV) { - "Trakt Series List" - } else { - "Trakt Movie List" + val typeLabel = runBlocking { + getString( + if (mediaType == TmdbCollectionMediaType.TV) { + Res.string.collections_folder_trakt_series_list + } else { + Res.string.collections_folder_trakt_movie_list + }, + ) } add( FolderTab( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt index 3f37d3d8..fd645c60 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt @@ -13,6 +13,19 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.collections_tmdb_api_key_required +import nuvio.composeapp.generated.resources.collections_tmdb_collection_not_found +import nuvio.composeapp.generated.resources.collections_tmdb_company_not_found +import nuvio.composeapp.generated.resources.collections_tmdb_discover_no_data +import nuvio.composeapp.generated.resources.collections_tmdb_list_not_found +import nuvio.composeapp.generated.resources.collections_tmdb_missing_collection_id +import nuvio.composeapp.generated.resources.collections_tmdb_missing_list_id +import nuvio.composeapp.generated.resources.collections_tmdb_missing_person_id +import nuvio.composeapp.generated.resources.collections_tmdb_network_not_found +import nuvio.composeapp.generated.resources.collections_tmdb_person_credits_not_found +import nuvio.composeapp.generated.resources.collections_tmdb_person_not_found +import org.jetbrains.compose.resources.getString import kotlin.math.roundToInt object TmdbCollectionSourceResolver { @@ -22,7 +35,7 @@ object TmdbCollectionSourceResolver { suspend fun resolve(source: CollectionSource, page: Int = 1): CatalogPage = withContext(Dispatchers.Default) { val settings = TmdbSettingsRepository.snapshot() val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() } - ?: error("Add a TMDB API key in Settings to use TMDB sources.") + ?: error(getString(Res.string.collections_tmdb_api_key_required)) val language = normalizeTmdbLanguage(settings.language) val sourceType = source.tmdbType() @@ -41,7 +54,7 @@ object TmdbCollectionSourceResolver { withContext(Dispatchers.Default) { val settings = TmdbSettingsRepository.snapshot() val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() } - ?: error("Add a TMDB API key in Settings to use TMDB sources.") + ?: error(getString(Res.string.collections_tmdb_api_key_required)) val language = normalizeTmdbLanguage(settings.language) when (sourceType) { TmdbCollectionSourceType.LIST -> { @@ -49,7 +62,7 @@ object TmdbCollectionSourceResolver { endpoint = "list/$id", apiKey = apiKey, query = mapOf("language" to language, "page" to "1"), - ) ?: error("TMDB list not found") + ) ?: error(getString(Res.string.collections_tmdb_list_not_found)) TmdbSourceImportMetadata(title = body.name?.takeIf { it.isNotBlank() }) } @@ -58,7 +71,7 @@ object TmdbCollectionSourceResolver { endpoint = "collection/$id", apiKey = apiKey, query = mapOf("language" to language), - ) ?: error("TMDB collection not found") + ) ?: error(getString(Res.string.collections_tmdb_collection_not_found)) TmdbSourceImportMetadata( title = body.name?.takeIf { it.isNotBlank() }, coverImageUrl = imageUrl(body.posterPath, "w500") ?: imageUrl(body.backdropPath, "w1280"), @@ -69,7 +82,7 @@ object TmdbCollectionSourceResolver { val body = fetch( endpoint = "company/$id", apiKey = apiKey, - ) ?: error("TMDB company not found") + ) ?: error(getString(Res.string.collections_tmdb_company_not_found)) TmdbSourceImportMetadata( title = body.name?.takeIf { it.isNotBlank() }, coverImageUrl = imageUrl(body.logoPath, "w500"), @@ -80,7 +93,7 @@ object TmdbCollectionSourceResolver { val body = fetch( endpoint = "network/$id", apiKey = apiKey, - ) ?: error("TMDB network not found") + ) ?: error(getString(Res.string.collections_tmdb_network_not_found)) TmdbSourceImportMetadata( title = body.name?.takeIf { it.isNotBlank() }, coverImageUrl = imageUrl(body.logoPath, "w500"), @@ -93,7 +106,7 @@ object TmdbCollectionSourceResolver { endpoint = "person/$id", apiKey = apiKey, query = mapOf("language" to language), - ) ?: error("TMDB person not found") + ) ?: error(getString(Res.string.collections_tmdb_person_not_found)) TmdbSourceImportMetadata( title = body.name?.takeIf { it.isNotBlank() }, coverImageUrl = imageUrl(body.profilePath, "w500"), @@ -109,7 +122,7 @@ object TmdbCollectionSourceResolver { if (trimmed.isBlank()) return@withContext emptyList() val settings = TmdbSettingsRepository.snapshot() val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() } - ?: error("Add a TMDB API key in Settings to use TMDB sources.") + ?: error(getString(Res.string.collections_tmdb_api_key_required)) fetch( endpoint = "search/company", apiKey = apiKey, @@ -122,7 +135,7 @@ object TmdbCollectionSourceResolver { if (trimmed.isBlank()) return@withContext emptyList() val settings = TmdbSettingsRepository.snapshot() val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() } - ?: error("Add a TMDB API key in Settings to use TMDB sources.") + ?: error(getString(Res.string.collections_tmdb_api_key_required)) val language = normalizeTmdbLanguage(settings.language) fetch( endpoint = "search/collection", @@ -136,7 +149,7 @@ object TmdbCollectionSourceResolver { if (trimmed.isBlank()) return@withContext emptyMap() val settings = TmdbSettingsRepository.snapshot() val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() } - ?: error("Add a TMDB API key in Settings to use TMDB sources.") + ?: error(getString(Res.string.collections_tmdb_api_key_required)) fetch( endpoint = "search/keyword", apiKey = apiKey, @@ -152,7 +165,7 @@ object TmdbCollectionSourceResolver { suspend fun genres(mediaType: TmdbCollectionMediaType): Map = withContext(Dispatchers.Default) { val settings = TmdbSettingsRepository.snapshot() val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() } - ?: error("Add a TMDB API key in Settings to use TMDB sources.") + ?: error(getString(Res.string.collections_tmdb_api_key_required)) val language = normalizeTmdbLanguage(settings.language) val endpoint = when (mediaType) { TmdbCollectionMediaType.MOVIE -> "genre/movie/list" @@ -200,12 +213,12 @@ object TmdbCollectionSourceResolver { language: String, page: Int, ): CatalogPage { - val id = source.tmdbId ?: error("Missing TMDB list ID") + val id = source.tmdbId ?: error(getString(Res.string.collections_tmdb_missing_list_id)) val body = fetch( endpoint = "list/$id", apiKey = apiKey, query = mapOf("language" to language, "page" to page.toString()), - ) ?: error("TMDB list not found") + ) ?: error(getString(Res.string.collections_tmdb_list_not_found)) val items = body.items.orEmpty() .mapNotNull { it.toPreview() } .sortedFor(source.sortBy) @@ -222,12 +235,12 @@ object TmdbCollectionSourceResolver { apiKey: String, language: String, ): CatalogPage { - val id = source.tmdbId ?: error("Missing TMDB collection ID") + val id = source.tmdbId ?: error(getString(Res.string.collections_tmdb_missing_collection_id)) val body = fetch( endpoint = "collection/$id", apiKey = apiKey, query = mapOf("language" to language), - ) ?: error("TMDB collection not found") + ) ?: error(getString(Res.string.collections_tmdb_collection_not_found)) val items = body.parts.orEmpty() .mapNotNull { it.toPreview(TmdbCollectionMediaType.MOVIE) } .sortedFor(source.sortBy) @@ -240,13 +253,13 @@ object TmdbCollectionSourceResolver { apiKey: String, language: String, ): CatalogPage { - val id = source.tmdbId ?: error("Missing TMDB person ID") + val id = source.tmdbId ?: error(getString(Res.string.collections_tmdb_missing_person_id)) val mediaType = source.tmdbMediaType() val body = fetch( endpoint = "person/$id/combined_credits", apiKey = apiKey, query = mapOf("language" to language), - ) ?: error("TMDB person credits not found") + ) ?: error(getString(Res.string.collections_tmdb_person_credits_not_found)) val items = when (source.tmdbType()) { TmdbCollectionSourceType.DIRECTOR -> body.crew.orEmpty() .filter { it.job.equals("Director", ignoreCase = true) } @@ -287,7 +300,7 @@ object TmdbCollectionSourceResolver { endpoint = endpoint, apiKey = apiKey, query = query, - ) ?: error("TMDB discover returned no data") + ) ?: error(getString(Res.string.collections_tmdb_discover_no_data)) val items = body.results.orEmpty() .mapNotNull { it.toPreview(mediaType) } .distinctBy { it.id } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/SubtitleRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/SubtitleRepository.kt index 64d8c879..a7c9f5cf 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/SubtitleRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/SubtitleRepository.kt @@ -84,7 +84,11 @@ object SubtitleRepository { id = id, url = url, language = normalizedLang, - display = "${getLanguageLabelForCode(rawLang)} (${addon.displayTitle})", + display = getString( + Res.string.player_addon_subtitle_display_format, + getLanguageLabelForCode(rawLang), + addon.displayTitle, + ), ) ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt index 3fed8022..cba36b92 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt @@ -283,7 +283,7 @@ object TraktAuthRepository { refreshUserSettings() publish( isLoading = false, - statusMessage = "Connected to Trakt", + statusMessage = localizedString(Res.string.trakt_connected_status), errorMessage = null, ) } @@ -316,7 +316,7 @@ object TraktAuthRepository { persist() publish( isLoading = false, - statusMessage = "Disconnected from Trakt", + statusMessage = localizedString(Res.string.trakt_disconnected_status), errorMessage = null, ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt index a2bd8a03..973e5495 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt @@ -90,7 +90,9 @@ object TraktCommentsRepository { return TraktCommentsPage(emptyList(), page, 0, 0) } if (response.status !in 200..299) { - throw IllegalStateException("Failed to load Trakt comments (${response.status})") + throw IllegalStateException( + getString(Res.string.details_comments_trakt_load_failed_with_code, response.status), + ) } val dtos = commentsJson.decodeFromString>(response.body) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktPublicListSourceResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktPublicListSourceResolver.kt index e1468245..4b66e8f9 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktPublicListSourceResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktPublicListSourceResolver.kt @@ -12,11 +12,27 @@ import com.nuvio.app.features.home.MetaPreview import com.nuvio.app.features.home.PosterShape import io.ktor.http.encodeURLParameter import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.collections_editor_trakt_fallback_title +import nuvio.composeapp.generated.resources.collections_trakt_credentials_missing +import nuvio.composeapp.generated.resources.collections_trakt_error_with_code +import nuvio.composeapp.generated.resources.collections_trakt_invalid_list_id_or_url +import nuvio.composeapp.generated.resources.collections_trakt_list_items_count +import nuvio.composeapp.generated.resources.collections_trakt_list_likes_count +import nuvio.composeapp.generated.resources.collections_trakt_list_not_found_or_private +import nuvio.composeapp.generated.resources.collections_editor_trakt_load_failed +import nuvio.composeapp.generated.resources.collections_trakt_missing_list_id +import nuvio.composeapp.generated.resources.collections_trakt_missing_numeric_id +import nuvio.composeapp.generated.resources.collections_trakt_public_list +import nuvio.composeapp.generated.resources.collections_trakt_rate_limit_reached +import nuvio.composeapp.generated.resources.collections_trakt_request_failed +import org.jetbrains.compose.resources.getString import kotlin.math.roundToInt data class TraktPublicListImportMetadata( @@ -44,7 +60,7 @@ object TraktPublicListSourceResolver { private val json = Json { ignoreUnknownKeys = true } suspend fun resolve(source: CollectionSource, page: Int = 1): CatalogPage = withContext(Dispatchers.Default) { - val listId = source.traktListId?.takeIf { it > 0L } ?: error("Missing Trakt list ID") + val listId = source.traktListId?.takeIf { it > 0L } ?: error(getString(Res.string.collections_trakt_missing_list_id)) val mediaType = TmdbCollectionMediaType.fromString(source.mediaType) val type = mediaType.toTraktType() val sortBy = TraktListSort.normalize(source.sortBy) @@ -60,7 +76,7 @@ object TraktPublicListSourceResolver { ), ) if (response.status !in 200..299) { - error(errorMessageFor(response.status, "Could not load Trakt list")) + error(errorMessageFor(response.status, getString(Res.string.collections_editor_trakt_load_failed))) } val rawItems = json.decodeFromString>(response.body) @@ -76,12 +92,12 @@ object TraktPublicListSourceResolver { } suspend fun listImportMetadata(input: String): TraktPublicListImportMetadata = withContext(Dispatchers.Default) { - val idPath = parseTraktListPath(input) ?: error("Enter a valid Trakt list ID or URL") + val idPath = parseTraktListPath(input) ?: error(getString(Res.string.collections_trakt_invalid_list_id_or_url)) val list = requestJson( endpoint = "lists/$idPath", query = mapOf("extended" to "full,images"), ) - val id = list.ids?.trakt ?: idPath.toLongOrNull() ?: error("Trakt list did not include a numeric ID") + val id = list.ids?.trakt ?: idPath.toLongOrNull() ?: error(getString(Res.string.collections_trakt_missing_numeric_id)) TraktPublicListImportMetadata( title = list.name?.takeIf { it.isNotBlank() }, coverImageUrl = list.images?.posters.firstTraktImageUrl(), @@ -132,7 +148,7 @@ object TraktPublicListSourceResolver { ): T { val response = requestRaw(endpoint = endpoint, query = query) if (response.status !in 200..299) { - error(errorMessageFor(response.status, "Trakt request failed")) + error(errorMessageFor(response.status, getString(Res.string.collections_trakt_request_failed))) } return runCatching { json.decodeFromString(response.body) } .onFailure { error -> log.w(error) { "Failed to parse Trakt response for $endpoint" } } @@ -144,7 +160,7 @@ object TraktPublicListSourceResolver { query: Map = emptyMap(), ): RawHttpResponse { if (TraktConfig.CLIENT_ID.isBlank()) { - error("Missing Trakt credentials in local.properties (TRAKT_CLIENT_ID).") + error(getString(Res.string.collections_trakt_credentials_missing)) } val url = buildTraktUrl(endpoint, query) return httpRequestRaw( @@ -237,21 +253,25 @@ object TraktPublicListSourceResolver { private fun PublicTraktListSummaryDto.toPublicListResult(likeCount: Int? = null): TraktPublicListSearchResult? { val id = ids?.trakt ?: return null - val listTitle = name?.takeIf { it.isNotBlank() } ?: "Trakt List $id" - val owner = user?.username?.takeIf { it.isNotBlank() } - val stats = buildList { - itemCount?.let { add("$it items") } - (likeCount ?: likes)?.let { add("$it likes") } + return runBlocking { + val listTitle = name?.takeIf { it.isNotBlank() } + ?: getString(Res.string.collections_editor_trakt_fallback_title, id) + val owner = user?.username?.takeIf { it.isNotBlank() } + val stats = buildList { + itemCount?.let { add(getString(Res.string.collections_trakt_list_items_count, it)) } + (likeCount ?: likes)?.let { add(getString(Res.string.collections_trakt_list_likes_count, it)) } + } + val subtitle = (listOfNotNull(owner) + stats).joinToString(" • ") + .ifBlank { getString(Res.string.collections_trakt_public_list) } + TraktPublicListSearchResult( + traktListId = id, + title = listTitle, + subtitle = subtitle, + coverImageUrl = images?.posters.firstTraktImageUrl(), + sortBy = sortBy, + sortHow = sortHow, + ) } - val subtitle = (listOfNotNull(owner) + stats).joinToString(" • ").ifBlank { "Trakt public list" } - return TraktPublicListSearchResult( - traktListId = id, - title = listTitle, - subtitle = subtitle, - coverImageUrl = images?.posters.firstTraktImageUrl(), - sortBy = sortBy, - sortHow = sortHow, - ) } private fun parseTraktListPath(input: String): String? { @@ -292,11 +312,11 @@ object TraktPublicListSourceResolver { ?.trim() ?.toIntOrNull() - private fun errorMessageFor(code: Int, fallback: String): String { - return when (code) { - 401, 403, 404 -> "Trakt list not found or not public" - 429 -> "Trakt rate limit reached" - else -> "$fallback ($code)" + private fun errorMessageFor(code: Int, fallback: String): String = runBlocking { + when (code) { + 401, 403, 404 -> getString(Res.string.collections_trakt_list_not_found_or_private) + 429 -> getString(Res.string.collections_trakt_rate_limit_reached) + else -> getString(Res.string.collections_trakt_error_with_code, fallback, code) } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/updater/AppUpdater.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/updater/AppUpdater.kt index 44ce4441..75427d6a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/updater/AppUpdater.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/updater/AppUpdater.kt @@ -49,6 +49,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import kotlinx.coroutines.runBlocking import nuvio.composeapp.generated.resources.* import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource @@ -106,7 +107,7 @@ private val appUpdaterJson = Json { } private class NoChannelReleaseException : IllegalStateException( - "No cmp-rewrite release has been published yet.", + runBlocking { getString(Res.string.updates_no_channel_release) }, ) private object VersionUtils { @@ -158,7 +159,7 @@ private object AppUpdaterRepository { body = "", ) if (response.status !in 200..299) { - error("GitHub releases API error: ${response.status}") + error(getString(Res.string.updates_github_api_error, response.status)) } val releases = appUpdaterJson.decodeFromString>(response.body) @@ -167,10 +168,10 @@ private object AppUpdaterRepository { val tag = release.tagName?.takeIf { it.isNotBlank() } ?: release.name?.takeIf { it.isNotBlank() } - ?: error("Release has no tag or name") + ?: error(getString(Res.string.updates_release_missing_title)) val asset = chooseBestApkAsset(release.assets) - ?: error("No APK asset found in the cmp-rewrite release") + ?: error(getString(Res.string.updates_apk_asset_missing)) AppUpdate( tag = tag, diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.ios.kt index 84943920..f9cf0f2a 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.ios.kt @@ -15,6 +15,11 @@ import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.http.isSuccess +import kotlinx.coroutines.runBlocking +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.network_empty_response_body +import nuvio.composeapp.generated.resources.network_request_failed_http +import org.jetbrains.compose.resources.getString import platform.Foundation.NSUserDefaults actual object AddonStorage { @@ -54,10 +59,10 @@ actual suspend fun httpGetText(url: String): String = .let { response -> val payload = response.bodyAsText() if (!response.status.isSuccess()) { - error("Request failed with HTTP ${response.status.value}") + error(runBlocking { getString(Res.string.network_request_failed_http, response.status.value) }) } if (payload.isBlank()) { - throw IllegalStateException("Empty response body") + throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) }) } payload } @@ -72,10 +77,10 @@ actual suspend fun httpPostJson(url: String, body: String): String = .let { response -> val payload = response.bodyAsText() if (!response.status.isSuccess()) { - error("Request failed with HTTP ${response.status.value}") + error(runBlocking { getString(Res.string.network_request_failed_http, response.status.value) }) } if (payload.isBlank()) { - throw IllegalStateException("Empty response body") + throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) }) } payload } @@ -94,10 +99,10 @@ actual suspend fun httpGetTextWithHeaders( .let { response -> val payload = response.bodyAsText() if (!response.status.isSuccess()) { - error("Request failed with HTTP ${response.status.value}") + error(runBlocking { getString(Res.string.network_request_failed_http, response.status.value) }) } if (payload.isBlank()) { - throw IllegalStateException("Empty response body") + throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) }) } payload } @@ -119,10 +124,10 @@ actual suspend fun httpPostJsonWithHeaders( .let { response -> val payload = response.bodyAsText() if (!response.status.isSuccess()) { - error("Request failed with HTTP ${response.status.value}") + error(runBlocking { getString(Res.string.network_request_failed_http, response.status.value) }) } if (payload.isBlank()) { - throw IllegalStateException("Empty response body") + throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) }) } payload } diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/downloads/DownloadsPlatformDownloader.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/downloads/DownloadsPlatformDownloader.ios.kt index 733bec21..a0bfc724 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/downloads/DownloadsPlatformDownloader.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/downloads/DownloadsPlatformDownloader.ios.kt @@ -10,6 +10,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.download_failed +import nuvio.composeapp.generated.resources.downloads_error_finalize_file_failed +import nuvio.composeapp.generated.resources.downloads_error_open_partial_file_failed +import nuvio.composeapp.generated.resources.downloads_error_partial_file_not_open +import nuvio.composeapp.generated.resources.downloads_error_write_partial_file_failed +import nuvio.composeapp.generated.resources.network_request_failed_http +import org.jetbrains.compose.resources.getString import platform.Foundation.NSError import platform.Foundation.NSDate import platform.Foundation.NSData @@ -99,7 +108,7 @@ internal actual object DownloadsPlatformDownloader { } if (result.statusCode !in 200..299) { - error("Request failed with HTTP ${result.statusCode}") + error(runBlocking { getString(Res.string.network_request_failed_http, result.statusCode) }) } val isPartialResume = attemptedRangeRequest && result.statusCode == 206 && resumeFromBytes > 0L @@ -118,7 +127,7 @@ internal actual object DownloadsPlatformDownloader { error = null, ) if (!moved) { - error("Failed to finalize download file") + error(runBlocking { getString(Res.string.downloads_error_finalize_file_failed) }) } val localFileUri = NSURL.fileURLWithPath(destinationPath).absoluteString ?: "file://$destinationPath" @@ -127,7 +136,7 @@ internal actual object DownloadsPlatformDownloader { } catch (_: CancellationException) { handle.cancelNativeTask() } catch (error: Throwable) { - onFailure(error.message ?: "Download failed") + onFailure(error.message ?: runBlocking { getString(Res.string.download_failed) }) } } @@ -248,7 +257,7 @@ private class IosDownloadDelegate( ) outputFile = fopen(tempPath, if (isPartialResume) "ab" else "wb") ?: run { - fileError = IllegalStateException("Failed to open partial download file") + fileError = IllegalStateException(runBlocking { getString(Res.string.downloads_error_open_partial_file_failed) }) null } @@ -266,7 +275,7 @@ private class IosDownloadDelegate( if (fileError != null) return val file = outputFile ?: run { - fileError = IllegalStateException("Partial download file is not open") + fileError = IllegalStateException(runBlocking { getString(Res.string.downloads_error_partial_file_not_open) }) return } @@ -278,7 +287,7 @@ private class IosDownloadDelegate( file, ).toLong() if (wrote != bytesToWrite) { - fileError = IllegalStateException("Failed to write partial download file") + fileError = IllegalStateException(runBlocking { getString(Res.string.downloads_error_write_partial_file_failed) }) return } fflush(file)