fix(i18n): translate the player, downloads and server-setup text left in English

A Portuguese user reported "Skip Intro" rendering in English on Android TV.
The locale files were not the problem - all 22 were structurally complete.
skip_marker_button.dart simply never imported strings.g.dart and assigned
'Skip Intro' / 'Skip Credits' / 'Next Episode' as plain literals. An audit of
lib/ found ~120 more sites in the same state, in four shapes that need
different fixes:

A literal in a file that never imported the i18n layer is the easy one -
skip_marker_button, performance_stats, track_label_builder and codec_utils all
render text with no `t` in the file at all. TrackLabelBuilder._compose now takes
a fallbackLabel builder instead of an English fallbackPrefix, so the caller
supplies t.audioTracks.track / t.videoControls.subtitleTrack and every unnamed
audio and subtitle row in the track menus is localized.

English reaching the user through an exception message is the widest one, and
it needs care: MediaServerException.message feeds both toString() - logs and
Sentry grouping - and verbatim UI display. Localizing it in place would make
bug-report logs follow the user's locale and split one Sentry issue into 22.
The MediaServer and Seerr families instead gain a nullable `display` alongside
the English `message`, and the six screens that print these errors read
`display ?? message`. PlaybackException keeps the opposite rule, because it
already carries a PlaybackFailureReason for logic and classifyPlaybackFailure
already builds it from t.messages: its stragglers are localized at the throw
site. That also removes the literal "Exception: " prefix Live TV users saw on
a tune failure, since PlaybackException.toString() returns the bare message.

Localized parts hand-concatenated with bare English are the shape no search for
Text('...') can find: '${t.common.pause} auto-scroll' on the home carousel,
'${day} at ${time}' on the Live TV schedule row, and an actor-screen count that
hand-rolled its plural as `n == 1 ? 'title' : 'titles'` - wrong for ru and pl
regardless of translation, now a real Slang plural.

Finally a literal assigned to provider state that a widget renders later:
DownloadProgress.errorMessage, and the four background_downloader notification
bodies, which sit inside a plugin config call where no widget-shaped search
reaches them.

Two things surfaced while converting. track_chapter_controls compared a track
label against 'Audio Track N' to swap in a localized version; once the builder
localized its own fallback that branch became unreachable, so it and the
orphaned _joinTrackLabel are gone. And discovery_view's PeerError fallback arm
looks like a leak but is not - its producers already localize, and a test says
so - so it stays as it is.

All 21 non-base locales are translated, including the 21 keys left empty by
earlier commits that were falling back to English. No locale has an empty value.

scripts/check_hardcoded_strings.py guards the three shapes a structural check
can see, and runs in ci_checks.sh after translation hygiene. Its first draft
passed its own tests while missing this very bug, because 'Skip Intro' is bound
to a local rather than handed to Text(); the name-bound rule that closes that
gap is restricted to phrase-shaped literals, or it cannot tell copy from the
identifiers this codebase binds constantly ('cast_row', 'auto', 'liveTv'). It
cannot see English inside a throw or assigned to a provider field - neither is
distinguishable from a log message without dataflow analysis - and the docstring
says so. label: and actionLabel: are deliberately unscanned: here they name a
diagnostic operation, and a check that is chronically red is a check that gets
switched off.

One commit rather than one per area: the keys, the 22 locale files and the
generated output are a single unit, and any partial split fails the repo's own
unused-key scan on the way through.

close #1856
This commit is contained in:
edde746
2026-08-10 15:32:43 +02:00
parent 3177290083
commit 369c6279d6
98 changed files with 8858 additions and 1110 deletions
+126 -37
View File
@@ -17,7 +17,8 @@
"quickConnectWaiting": "En attente d'approbation…",
"quickConnectCancel": "Annuler",
"quickConnectExpired": "Quick Connect a expiré. Réessayez.",
"localDataRecoveryRequired": "Plezy na pas pu récupérer en toute sécurité les données locales de connexion et de lecture en attente. Veuillez vous reconnecter."
"localDataRecoveryRequired": "Plezy na pas pu récupérer en toute sécurité les données locales de connexion et de lecture en attente. Veuillez vous reconnecter.",
"pinCheckRejected": "La vérification du code PIN Plex a été refusée"
},
"common": {
"cancel": "Annuler",
@@ -77,7 +78,10 @@
"trakt": "Trakt",
"rottenTomatoesCritic": "Critiques Rotten Tomatoes",
"rottenTomatoesAudience": "Public Rotten Tomatoes"
}
},
"notAvailable": "N/A",
"url": "URL",
"letterKeys": "ABC"
},
"screens": {
"licenses": "Licences",
@@ -323,8 +327,8 @@
"showNavBarLabels": "Afficher les libellés de la barre de navigation",
"showNavBarLabelsDescription": "Afficher les libellés sous les icônes de la barre de navigation",
"startupSection": "Section de démarrage",
"showExploreTab": "",
"showExploreTabDescription": "",
"showExploreTab": "Afficher longlet Explorer",
"showExploreTabDescription": "Afficher longlet Explorer avec le contenu de Plex Discover et des services de suivi connectés",
"liveTvDefaultFavorites": "Chaînes favorites par défaut",
"liveTvDefaultFavoritesDescription": "Afficher uniquement les chaînes favorites à l'ouverture de la TV en direct",
"display": "Affichage",
@@ -335,7 +339,8 @@
"player": "Lecteur",
"subtitlesAndConfig": "Sous-titres et configuration",
"seekAndTiming": "Déplacement et minutage",
"behavior": "Comportement"
"behavior": "Comportement",
"exportDialogTitle": "Exporter les paramètres de Plezy"
},
"search": {
"hint": "Rechercher des films, des séries, de la musique...",
@@ -491,7 +496,9 @@
"flagExternal": "Externe",
"flagHearingImpaired": "Sourds et malentendants",
"flagDub": "Doublage",
"flagOriginal": "Originale"
"flagOriginal": "Originale",
"channelsMono": "Mono",
"dolbyVisionProfile": "Profil ${profile}"
},
"mediaMenu": {
"markAsWatched": "Marquer comme vu",
@@ -568,7 +575,12 @@
"alphabetNavigation": "Navigation alphabétique",
"alphabetScrollHint": "Balayez vers le haut ou le bas pour changer de lettre",
"rowColumnPosition": "Ligne ${row} sur ${rowCount}, colonne ${column} sur ${columnCount}",
"rowPosition": "Ligne ${row} sur ${rowCount}"
"rowPosition": "Ligne ${row} sur ${rowCount}",
"autoScrollPlay": "Lancer le défilement automatique",
"autoScrollPause": "Suspendre le défilement automatique",
"hueShort": "T",
"saturationShort": "S",
"valueShort": "V"
},
"tooltips": {
"shufflePlay": "Lecture aléatoire",
@@ -666,7 +678,16 @@
"subtitleDownloaded": "Sous-titre téléchargé",
"subtitleDownloadedNotApplied": "Le sous-titre a été téléchargé, mais na pas pu être sélectionné",
"subtitleDownloadFailed": "Échec du téléchargement du sous-titre",
"searchLanguages": "Rechercher des langues..."
"searchLanguages": "Rechercher des langues...",
"skipIntro": "Passer lintro",
"skipCredits": "Passer le générique",
"nextEpisode": "Épisode suivant",
"subtitleTrack": "Piste ${n}",
"subtitleFile": "Sous-titre ${name}",
"forcedTrack": "${label} (forcé)",
"osdSubtitlesOff": "Sous-titres : désactivés",
"osdSubtitles": "Sous-titres : ${track}",
"osdAudio": "Audio : ${track}"
},
"userStatus": {},
"messages": {
@@ -722,12 +743,18 @@
"serverLimitBody": "Erreur serveur (HTTP 500). Une limite de bande passante/transcodage a probablement rejeté cette session. Demandez au propriétaire de l'ajuster.",
"mediaUnreadableTitle": "Fichier indisponible",
"mediaUnreadableBody": "Le serveur a trouvé cet élément mais n'a pas pu lire son fichier (HTTP 404). Le fichier a probablement été déplacé, supprimé, ou son stockage est hors ligne. Demandez au propriétaire du serveur de vérifier le fichier et de relancer l'analyse de la bibliothèque.",
"serverBusyTitle": "",
"serverBusyBody": "",
"serverBusyTitle": "Flux indisponible",
"serverBusyBody": "Le serveur a refusé à plusieurs reprises de diffuser ce fichier (HTTP 503). Il est peut-être en cours de redémarrage ou occupé, ou le stockage du fichier est peut-être hors ligne. Réessayez dans un instant. Si le problème persiste, demandez au propriétaire du serveur de vérifier le serveur et le stockage du fichier.",
"logsUploaded": "Journaux envoyés",
"logsUploadFailed": "Échec de lenvoi des journaux",
"logId": "Identifiant du journal",
"burnedSubtitlesUseMenu": ""
"burnedSubtitlesUseMenu": "Les sous-titres sont incrustés à limage de ce flux. Modifiez-les depuis le menu des sous-titres.",
"noVideoUrl": "Aucune URL vidéo disponible",
"playbackNoMediaSources": "Le serveur na renvoyé aucune source multimédia lisible",
"playbackDataNotPrepared": "La lecture a été lancée avant que les données nécessaires soient prêtes",
"streamSelectionUnavailable": "La sélection des flux nest pas disponible pour cette source",
"streamSelectionFailed": "Impossible dappliquer les flux sélectionnés",
"serverUnavailableForProfile": "Aucun serveur nest disponible pour le profil actif"
},
"subtitlingStyling": {
"text": "Texte",
@@ -746,8 +773,8 @@
"overrideStrip": "Supprimer le style",
"positionTop": "Haut",
"positionBottom": "Bas",
"anchorToScreen": "",
"anchorToScreenDescription": "",
"anchorToScreen": "Ancrer à l’écran",
"anchorToScreenDescription": "Afficher les sous-titres textuels dans les bandes noires sous les vidéos en écran large",
"bold": "Gras",
"italic": "Italique",
"renderResolution": "Résolution de rendu",
@@ -835,7 +862,8 @@
"pinProtectionOptional": "Protection par PIN (optionnelle)",
"pinExplain": "PIN à 4 chiffres requis pour changer de profil.",
"continueButton": "Continuer",
"pinsDontMatch": "Les PIN ne correspondent pas"
"pinsDontMatch": "Les PIN ne correspondent pas",
"tokenIdentityMismatch": "Le jeton du profil Plex correspond à un serveur inattendu"
},
"connections": {
"sectionTitle": "Connexions",
@@ -872,7 +900,11 @@
"movie": "Film",
"tvShow": "Série TV",
"minutesLeft": "${minutes} min restantes",
"moreLikeThis": "Plus de contenus similaires"
"moreLikeThis": "Plus de contenus similaires",
"titleCount": {
"one": "${n} titre",
"other": "${n} titres"
}
},
"errors": {
"searchFailed": "Échec de la recherche : ${error}",
@@ -1080,10 +1112,10 @@
"characters": "Personnages",
"addToWatchlist": "Ajouter à la liste de suivi",
"removeFromWatchlist": "Retirer de la liste de suivi",
"addedToWatchlist": "",
"removedFromWatchlist": "",
"addedToWatchlist": "Ajouté à la liste de suivi",
"removedFromWatchlist": "Retiré de la liste de suivi",
"watchlistUpdateFailed": "Impossible de mettre à jour la liste de suivi",
"watchlistNoMatch": "",
"watchlistNoMatch": "Impossible dassocier cet élément à une liste de suivi",
"notInLibrary": "Absent de votre bibliothèque",
"inTheseLibraries": "Dans ces bibliothèques",
"checkingLibrary": "Vérification de votre bibliothèque...",
@@ -1253,9 +1285,9 @@
"watchChannel": "Regarder la chaîne",
"favorites": "Favoris",
"reorderFavorites": "Réorganiser les favoris",
"noFavoriteChannels": "",
"noFavoriteChannelsHint": "",
"showAllChannels": "",
"noFavoriteChannels": "Aucune chaîne favorite",
"noFavoriteChannelsHint": "Affichez toutes les chaînes, puis appuyez longuement sur une chaîne pour lajouter à vos favoris.",
"showAllChannels": "Afficher toutes les chaînes",
"favoritesLoadFailed": "Impossible de charger les favoris. Vérifiez votre connexion et réessayez.",
"favoritesUpdateFailed": "Impossible de mettre à jour les favoris. Vérifiez votre connexion et réessayez.",
"joinSession": "Rejoindre la session en cours",
@@ -1294,7 +1326,12 @@
"recordingRuleUpdated": "Règle d'enregistrement mise à jour",
"guideReloadRequested": "Mise à jour du guide demandée",
"rulesProcessRequested": "Réévaluation des règles demandée",
"recordShow": "Enregistrer l'émission"
"recordShow": "Enregistrer l'émission",
"startingInMinutes": "Commence dans ${minutes} min",
"dayAtTime": "${day} à ${time}",
"invalidPlaybackData": "${product} a renvoyé des données de lecture de TV en direct non valides",
"failedToStartChannel": "Impossible de lancer la chaîne en direct",
"failedToBuildStreamUrl": "Impossible de générer lURL du flux"
},
"collections": {
"title": "Collections",
@@ -1371,7 +1408,9 @@
"nextTrack": "Titre suivant",
"repeat": "Répéter",
"repeatAll": "Tout répéter",
"repeatOne": "Répéter le titre"
"repeatOne": "Répéter le titre",
"instantMixNoServer": "Aucun serveur nest disponible pour créer un mix instantané",
"noAudioUrl": "Aucune URL audio nest disponible pour ${track}"
},
"watchTogether": {
"title": "Regarder ensemble",
@@ -1438,7 +1477,8 @@
"renameRoom": "Renommer le salon",
"removeRoom": "Supprimer",
"guestSwitchUnavailable": "Impossible de changer — serveur indisponible pour la synchronisation",
"guestSwitchFailed": "Impossible de changer — contenu introuvable sur ce serveur"
"guestSwitchFailed": "Impossible de changer — contenu introuvable sur ce serveur",
"defaultDisplayName": "Utilisateur"
},
"downloads": {
"title": "Téléchargements",
@@ -1542,7 +1582,17 @@
"statusUnknown": "Pas encore vérifié",
"settingsUnavailable": "Impossible douvrir les paramètres système sur cet appareil",
"linkUnavailable": "Impossible douvrir dontkillmyapp.com sur cet appareil"
}
},
"unknownShow": "Série inconnue",
"unknownSeason": "Saison inconnue",
"unknownAlbum": "Album inconnu",
"completedOfTotal": "${completed}/${total} terminés",
"errorFileNotFound": "Fichier introuvable (404)",
"errorDownloadFailed": "Échec du téléchargement",
"errorPostProcessing": "Échec du post-traitement : ${error}",
"notificationDownloading": "Téléchargement...",
"notificationComplete": "Téléchargement terminé",
"notificationPaused": "Téléchargement en pause"
},
"shaders": {
"title": "Shaders",
@@ -1633,7 +1683,8 @@
"failedToConnectAnyAddress": "Impossible de se connecter à une adresse",
"connectionLostAfterAttempts": "Connexion perdue après ${attempts} tentatives",
"connectionLost": "Connexion perdue"
}
},
"closedBeforeAuth": "La connexion a été fermée avant lauthentification"
},
"videoSettings": {
"playbackSpeed": "Vitesse de lecture",
@@ -1644,13 +1695,13 @@
"audioSync": "Synchronisation audio",
"subtitleSync": "Synchronisation des sous-titres",
"hdr": "HDR",
"hdrUnsupported": "",
"hdrToneMapping": "",
"hdrToneMappingCompositor": "",
"hdrToneMappingCompositorDescription": "",
"hdrToneMappingPlayer": "",
"hdrToneMappingPlayerDescription": "",
"hdrToneMappingFailed": "",
"hdrUnsupported": "Le HDR nest pas disponible ici : ce compositeur de bureau ou cette sortie vidéo ne permet pas de le transmettre.",
"hdrToneMapping": "Mappage tonal HDR",
"hdrToneMappingCompositor": "Compositeur",
"hdrToneMappingCompositorDescription": "Transmettre les métadonnées HDR de la source et laisser le compositeur de bureau effectuer le mappage.",
"hdrToneMappingPlayer": "Lecteur",
"hdrToneMappingPlayerDescription": "Effectuer dans le lecteur le mappage selon la luminosité maximale de l’écran, puis indiquer le résultat au compositeur.",
"hdrToneMappingFailed": "Impossible de modifier le mappage tonal HDR : le mode précédent reste actif.",
"audioOutput": "Sortie audio",
"performanceOverlay": "Données de performance",
"audioOutputDolbyAtmos": "Dolby Atmos",
@@ -1696,7 +1747,17 @@
"speed": "Vitesse",
"player": "Lecteur",
"memory": "Mémoire",
"uiFps": "FPS UI"
"uiFps": "FPS UI",
"fps": "FPS",
"decoderAndroidHw": "Matériel Android",
"decoderNvidiaHw": "Matériel NVIDIA",
"decoderQualcommHw": "Matériel Qualcomm",
"decoderMediatekHw": "Matériel MediaTek",
"decoderExynosHw": "Matériel Exynos",
"decoderSoftware": "Logiciel",
"decoderHardware": "Matériel",
"tunnelingActive": "Actif",
"dvRpuFailed": "${converted} (${failures} échecs)"
},
"externalPlayer": {
"title": "Lecteur externe",
@@ -1854,7 +1915,17 @@
"statusAvailable": "Disponible",
"statusPartiallyAvailable": "Partiellement disponible",
"statusRequested": "Demandé",
"statusProcessing": "En cours de traitement"
"statusProcessing": "En cours de traitement",
"couldNotReach": "Impossible de joindre ${url} : ${error}",
"noInstanceAtUrl": "Aucune instance Seerr à ladresse ${url} (HTTP ${status})",
"notInitialized": "La configuration initiale de cette instance Seerr nest pas terminée",
"noPlexTokenForReauth": "Aucun jeton Plex nest disponible pour se reconnecter",
"noStoredCredentials": "Aucun identifiant enregistré nest disponible pour se reconnecter",
"signInRejected": "La connexion a été refusée",
"noSessionCookie": "Seerr na pas fourni de cookie de session",
"freshCookieRejected": "Seerr a refusé le nouveau cookie de session",
"noUserInformation": "Seerr na renvoyé aucune information sur lutilisateur",
"sessionRejectedAfterReauth": "La session a été refusée après la reconnexion"
},
"services": {
"title": "Services",
@@ -1875,7 +1946,7 @@
},
"deviceCode": {
"title": "Activer Plezy sur ${service}",
"instructions": "",
"instructions": "Scannez le code QR ou accédez à ladresse ci-dessous et saisissez ce code :",
"openToActivate": "Ouvrir ${service} pour activer",
"copyCode": "Copier le code d'activation",
"waitingForAuthorization": "En attente d'autorisation…",
@@ -1933,6 +2004,24 @@
"connectToMediaBrowserCardSubtitle": "Saisissez l'URL de votre serveur, votre nom d'utilisateur et votre mot de passe.",
"connectToMediaBrowserCardSubtitleScoped": "Connectez-vous à votre serveur ${product}. Sera associé à ${name}.",
"borrowFromAnotherProfile": "Emprunter à un autre profil",
"borrowFromAnotherProfileSubtitle": "Réutiliser la connexion d'un autre profil. Les profils protégés par PIN exigent un PIN."
"borrowFromAnotherProfileSubtitle": "Réutiliser la connexion d'un autre profil. Les profils protégés par PIN exigent un PIN.",
"invalidCredentials": "Nom dutilisateur ou mot de passe incorrect",
"authResponseNotJson": "La réponse dauthentification n’était pas au format JSON valide",
"quickConnectRejected": "Quick Connect a été refusé par le serveur",
"quickConnectNotJson": "La réponse de Quick Connect n’était pas au format JSON valide",
"quickConnectMissingFields": "Il manque un code ou un secret dans la réponse de Quick Connect",
"quickConnectPollRejected": "Linterrogation de Quick Connect a été refusée par le serveur",
"serverTimedOut": "Le serveur na pas répondu à temps",
"responseNotJson": "La réponse du serveur n’était pas au format JSON valide",
"responseMissingIdentity": "Il manque un identifiant ou un nom de serveur dans la réponse : sagit-il dun serveur ${product} ?",
"probeFailed": "Impossible de joindre le serveur : ${error}",
"enterAtLeastOneUrl": "Saisissez lURL dau moins un serveur ${product}",
"noReachableServer": "Aucun serveur ${product} joignable na été trouvé",
"urlsPointToDifferentServers": "Ces URL renvoient vers différents serveurs ${product}",
"urlDoesNotMatchServer": "Cette URL ne correspond pas au serveur ${product}",
"redirectUnsupported": "Le serveur a redirigé vers une URL non prise en charge",
"redirectDifferentHost": "Le serveur a redirigé vers un hôte différent. Saisissez directement lURL finale du serveur ${product}.",
"redirectInsecure": "Le serveur a redirigé de HTTPS vers une URL non sécurisée",
"redirectUnsupportedEnterFinal": "Le serveur a redirigé vers une URL non prise en charge. Saisissez directement lURL finale du serveur ${product}."
}
}