From c47eeb5223c42387bbf6edf80e21be444e0f1593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Sun, 29 Mar 2026 18:30:53 +0200 Subject: [PATCH] feat(i18n): externalize hardcoded strings across 15 components - Add 115 new i18n keys (mal, profiles, auth, stream_card sections + extras) - Externalize ~80 hardcoded strings from alerts, toasts, labels, descriptions - MalSettingsScreen fully localized (35 strings) - PluginsScreen alert messages localized (12 strings) - SettingsScreen descriptions and dialogs localized (21 strings) - All new FR translations use tutoiement consistently - EN/FR parity: 1503/1503 keys, 0 missing --- src/components/StreamCard.tsx | 12 +- src/components/mal/MalEditModal.tsx | 32 ++-- src/components/metadata/TrailerModal.tsx | 6 +- src/i18n/locales/en.json | 145 +++++++++++++-- src/i18n/locales/fr.json | 169 ++++++++++++++++-- src/screens/AIChatScreen.tsx | 4 +- src/screens/AuthScreen.tsx | 14 +- src/screens/BackdropGalleryScreen.tsx | 6 +- src/screens/ContributorsScreen.tsx | 4 +- src/screens/LibraryScreen.tsx | 6 +- src/screens/MalSettingsScreen.tsx | 72 ++++---- src/screens/PluginsScreen.tsx | 68 +++---- src/screens/ProfilesScreen.tsx | 20 ++- src/screens/SettingsScreen.tsx | 52 +++--- src/screens/TraktSettingsScreen.tsx | 10 +- .../settings/DeveloperSettingsScreen.tsx | 20 +-- src/screens/streams/useStreamsScreen.ts | 4 +- 17 files changed, 464 insertions(+), 180 deletions(-) diff --git a/src/components/StreamCard.tsx b/src/components/StreamCard.tsx index 1849c9e0..84339452 100644 --- a/src/components/StreamCard.tsx +++ b/src/components/StreamCard.tsx @@ -16,6 +16,7 @@ import QualityBadge from './metadata/QualityBadge'; import { useSettings } from '../hooks/useSettings'; import { useDownloads } from '../contexts/DownloadsContext'; import { useToast } from '../contexts/ToastContext'; +import { useTranslation } from 'react-i18next'; interface StreamCardProps { stream: Stream; @@ -63,6 +64,7 @@ const StreamCard = memo(({ const { settings } = useSettings(); const { startDownload } = useDownloads(); const { showSuccess, showInfo } = useToast(); + const { t } = useTranslation(); // Handle long press to copy stream URL to clipboard const handleLongPress = useCallback(async () => { @@ -72,10 +74,10 @@ const StreamCard = memo(({ // Use toast for Android, custom alert for iOS if (Platform.OS === 'android') { - showSuccess('URL Copied', 'Stream URL copied to clipboard!'); + showSuccess(t('stream_card.url_copied'), t('stream_card.url_copied_msg')); } else { // iOS uses custom alert - showAlert('Copied!', 'Stream URL has been copied to clipboard.'); + showAlert(t('stream_card.copied'), t('stream_card.copied_msg')); } } catch (error) { // Fallback: show URL in alert if clipboard fails @@ -131,7 +133,7 @@ const StreamCard = memo(({ try { const downloadsModule = require('../contexts/DownloadsContext'); if (downloadsModule && downloadsModule.isDownloadingUrl && downloadsModule.isDownloadingUrl(url)) { - showAlert('Already Downloading', 'This download has already started for this exact link.'); + showAlert(t('stream_card.already_downloading'), t('stream_card.already_downloading_msg')); return; } } catch { } @@ -178,9 +180,9 @@ const StreamCard = memo(({ imdbId: parentImdbId || parent.imdbId || undefined, tmdbId: tmdbId, }); - showAlert('Download Started', 'Your download has been added to the queue.'); + showAlert(t('stream_card.download_started'), t('stream_card.download_started_msg')); } catch (e: any) { - showAlert('Download Failed', e.message || 'Could not start download.'); + showAlert(t('stream_card.download_failed'), e.message || t('stream_card.download_failed')); } }, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]); diff --git a/src/components/mal/MalEditModal.tsx b/src/components/mal/MalEditModal.tsx index 5e41b89f..84bd4d05 100644 --- a/src/components/mal/MalEditModal.tsx +++ b/src/components/mal/MalEditModal.tsx @@ -17,6 +17,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useTheme } from '../../contexts/ThemeContext'; import { MalApiService } from '../../services/mal/MalApi'; import { MalListStatus, MalAnimeNode } from '../../types/mal'; +import { useTranslation } from 'react-i18next'; import { useToast } from '../../contexts/ToastContext'; interface MalEditModalProps { @@ -33,6 +34,7 @@ export const MalEditModal: React.FC = ({ onUpdateSuccess, }) => { const { currentTheme } = useTheme(); + const { t } = useTranslation(); const { showSuccess, showError } = useToast(); const [status, setStatus] = useState(anime.list_status.status); @@ -62,11 +64,11 @@ export const MalEditModal: React.FC = ({ await MalApiService.updateStatus(anime.node.id, status, epNum, scoreNum, isRewatching); - showSuccess('Updated', `${anime.node.title} status updated on MAL`); + showSuccess(t('mal.updated'), t('mal.updated_msg', { title: anime.node.title })); onUpdateSuccess(); onClose(); } catch (error) { - showError('Update Failed', 'Could not update MAL status'); + showError(t('mal.update_failed'), t('mal.update_failed_msg')); } finally { setIsUpdating(false); } @@ -74,22 +76,22 @@ export const MalEditModal: React.FC = ({ const handleRemove = async () => { Alert.alert( - 'Remove from List', - `Are you sure you want to remove ${anime.node.title} from your MyAnimeList?`, + t('mal.remove_title'), + t('mal.remove_confirm', { title: anime.node.title }), [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Remove', + { text: t('common.cancel'), style: 'cancel' }, + { + text: t('common.delete'), style: 'destructive', onPress: async () => { setIsRemoving(true); try { await MalApiService.removeFromList(anime.node.id); - showSuccess('Removed', `${anime.node.title} removed from MAL`); + showSuccess(t('mal.removed'), t('mal.removed_msg', { title: anime.node.title })); onUpdateSuccess(); onClose(); } catch (error) { - showError('Remove Failed', 'Could not remove from MAL'); + showError(t('mal.remove_failed'), t('mal.remove_failed_msg')); } finally { setIsRemoving(false); } @@ -100,11 +102,11 @@ export const MalEditModal: React.FC = ({ }; const statusOptions: { label: string; value: MalListStatus }[] = [ - { label: 'Watching', value: 'watching' }, - { label: 'Completed', value: 'completed' }, - { label: 'On Hold', value: 'on_hold' }, - { label: 'Dropped', value: 'dropped' }, - { label: 'Plan to Watch', value: 'plan_to_watch' }, + { label: t('mal.watching'), value: 'watching' }, + { label: t('mal.completed'), value: 'completed' }, + { label: t('mal.on_hold'), value: 'on_hold' }, + { label: t('mal.dropped'), value: 'dropped' }, + { label: t('mal.plan_to_watch'), value: 'plan_to_watch' }, ]; return ( @@ -122,7 +124,7 @@ export const MalEditModal: React.FC = ({ - Edit {anime.node.title} + {t('mal.edit_title', { title: anime.node.title })} diff --git a/src/components/metadata/TrailerModal.tsx b/src/components/metadata/TrailerModal.tsx index 621a8c3e..f925329e 100644 --- a/src/components/metadata/TrailerModal.tsx +++ b/src/components/metadata/TrailerModal.tsx @@ -161,7 +161,7 @@ const TrailerModal: React.FC = memo(({ }, [onClose, resumeTrailer]); const handleTrailerError = useCallback(() => { - setError('Failed to play trailer'); + setError(t('trailers.error_play')); setIsPlaying(false); }, []); @@ -171,7 +171,7 @@ const TrailerModal: React.FC = memo(({ if (isUnsupportedIosMediaFormat(error)) { logger.error('TrailerModal', 'Unsupported iOS trailer format:', error); - setError('This trailer format is not supported on iOS.'); + setError(t('trailers.error_ios_format')); setLoading(false); setIsPlaying(false); return; @@ -187,7 +187,7 @@ const TrailerModal: React.FC = memo(({ } logger.error('TrailerModal', 'Video error after retries:', error); - setError('Unable to play trailer. Please try again.'); + setError(t('trailers.error_retry')); setLoading(false); }, [retryCount, loadTrailer, trailer?.key]); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 867d490c..9359047b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -176,7 +176,12 @@ "added_to_collection": "Added to Collection", "added_to_collection_desc": "Added to your Trakt collection", "removed_from_collection": "Removed from Collection", - "removed_from_collection_desc": "Removed from your Trakt collection" + "removed_from_collection_desc": "Removed from your Trakt collection", + "sync_failed": "Sync Failed", + "sync_connect_first": "Please connect to Trakt first", + "sync_complete": "Sync Complete", + "sync_up_to_date": "Library is up to date", + "sync_error": "Unable to sync. Please try again." }, "metadata": { "unable_to_load": "Unable to Load Content", @@ -275,7 +280,9 @@ "removed_from_collection_hero": "Removed from Collection", "removed_from_collection_desc_hero": "Removed from your Trakt collection", "mark_as_watched": "Mark as Watched", - "mark_as_unwatched": "Mark as Unwatched" + "mark_as_unwatched": "Mark as Unwatched", + "no_backdrops": "No backdrops found", + "error_backdrops": "Failed to load backdrops" }, "cast": { "biography": "Biography", @@ -334,7 +341,10 @@ "unavailable": "Trailer Unavailable", "unavailable_desc": "This trailer could not be loaded at this time. Please try again later.", "unable_to_play": "Unable to play trailer. Please try again.", - "watch_on_youtube": "Watch on YouTube" + "watch_on_youtube": "Watch on YouTube", + "error_play": "Failed to play trailer", + "error_ios_format": "This trailer format is not supported on iOS.", + "error_retry": "Unable to play trailer. Please try again." }, "catalog": { "no_content_found": "No content found", @@ -360,7 +370,9 @@ "still_fetching": "Still fetching streams…", "no_streams_available": "No streams available", "starting_best_stream": "Starting best stream...", - "loading_more_sources": "Loading more sources..." + "loading_more_sources": "Loading more sources...", + "torrent_not_supported": "Not supported", + "torrent_not_supported_msg": "Torrent streaming is not supported yet." }, "player_ui": { "via": "via {{name}}", @@ -469,13 +481,13 @@ "profanity": "Profanity", "alcohol": "Alcohol/Drugs", "frightening": "Frightening" - }, + }, "severity": { "severe": "Severe", "moderate": "Moderate", "mild": "Mild", "none": "None" - } + } }, "addons": { "title": "Addons", @@ -542,7 +554,12 @@ "sign_out_error": "Failed to sign out of Trakt.", "sync_complete_title": "Sync Complete", "sync_success_msg": "Successfully synced your watch progress with Trakt.", - "sync_error_msg": "Sync failed. Please try again." + "sync_error_msg": "Sync failed. Please try again.", + "conflict_title": "Conflict", + "conflict_simkl_msg": "You cannot connect to Trakt while Simkl is connected. Please disconnect Simkl first.", + "library_sync_updated": "Library Sync Mode Updated", + "library_sync_updated_msg": "Trakt library sync is now set to: {{mode}}", + "library_sync_error": "Failed to update library sync mode" }, "simkl": { "title": "Simkl Settings", @@ -707,7 +724,9 @@ "media": "MEDIA", "notifications": "NOTIFICATIONS", "testing": "TESTING", - "danger_zone": "DANGER ZONE" + "danger_zone": "DANGER ZONE", + "data": "DATA", + "general": "GENERAL" }, "items": { "legal": "Legal & Disclaimer", @@ -840,7 +859,18 @@ "env_warning": "Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync." }, "connection": "Connection" - } + }, + "clear_mdblist_cache_title": "Clear MDBList Cache", + "clear_mdblist_cache_confirm": "Are you sure you want to clear all cached MDBList data? This cannot be undone.", + "clear_mdblist_cache_success": "MDBList cache has been cleared.", + "clear_mdblist_cache_error": "Could not clear MDBList cache.", + "reset_onboarding_success": "Onboarding has been reset. Restart the app to see the onboarding flow.", + "reset_onboarding_error": "Failed to reset onboarding.", + "reset_campaigns_success": "Campaign history reset. Restart app to see posters again.", + "clear_data_success": "All data cleared. Please restart the app.", + "clear_data_error": "Failed to clear data.", + "plugin_tester": "Plugin Tester", + "plugin_tester_desc": "Run a plugin and inspect logs/streams" }, "privacy": { "title": "Privacy & Data", @@ -915,7 +945,8 @@ "confirm_remove_title": "Remove API Key", "confirm_remove_msg": "Are you sure you want to remove your OpenRouter API key? This will disable AI chat features.", "success_removed": "API key removed successfully", - "error_remove": "Failed to remove API key" + "error_remove": "Failed to remove API key", + "error_load_context": "Failed to load content details for AI chat" }, "catalog_settings": { "title": "Catalogs", @@ -1378,7 +1409,20 @@ "got_it": "Got it!", "repo_format_hint": "Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch", "cancel": "Cancel", - "add": "Add" + "add": "Add", + "error_update_extensions": "Failed to update extensions", + "error_valid_url": "Please enter a valid repository URL", + "error_valid_url_format": "Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/user/repo/refs/heads/branch", + "error_update_repo": "Failed to update repository", + "error_remove_repo": "Failed to remove repository", + "error_save_repo": "Failed to save repository URL", + "error_set_repo_first": "Please set a repository URL first", + "error_repo": "Repository Error", + "error_update_status": "Failed to update extension status", + "error_clear_extensions": "Failed to clear extensions", + "error_clear_cache": "Failed to clear repository cache", + "showbox_saved": "Saved", + "showbox_saved_msg": "ShowBox settings updated" }, "theme": { "title": "App Themes", @@ -1504,5 +1548,84 @@ "provider_logs": "Provider Logs", "no_logs_captured": "No logs captured." } + }, + "mal": { + "title": "MyAnimeList", + "connect_title": "Connect MyAnimeList", + "connect_desc": "Sync your watch history and manage your anime list.", + "sign_in": "Sign In with MAL", + "sign_out": "Sign Out", + "sign_out_confirm": "Are you sure you want to disconnect?", + "sync_button": "Sync", + "connected": "Connected to MyAnimeList", + "error_connect": "Failed to connect to MyAnimeList", + "error_sign_in": "An error occurred during sign in: {{message}}", + "sync_complete": "Sync Complete", + "sync_complete_msg": "MAL data has been refreshed.", + "sync_failed": "Sync Failed", + "sync_failed_msg": "Could not refresh MAL data.", + "sync_settings": "Sync Settings", + "enable_sync": "Enable MAL Sync", + "enable_sync_desc": "Global switch to enable or disable all MyAnimeList features.", + "auto_episode": "Auto Episode Update", + "auto_episode_desc": "Automatically update your progress on MAL when you finish watching an episode (>=90% completion).", + "auto_add": "Auto Add Anime", + "auto_add_desc": "If an anime is not in your MAL list, it will be added automatically when you start watching.", + "auto_sync_library": "Auto-Sync to Library", + "auto_sync_library_desc": "Automatically add items from your MAL 'Watching' list to your Nuvio Library.", + "include_nsfw": "Include NSFW Content", + "include_nsfw_desc": "Allow NSFW entries to be returned when fetching your MAL list.", + "stat_total": "Total", + "stat_days": "Days", + "stat_mean": "Mean", + "watching": "Watching", + "completed": "Completed", + "on_hold": "On Hold", + "dropped": "Dropped", + "plan_to_watch": "Plan to Watch", + "edit_title": "Edit {{title}}", + "updated": "Updated", + "updated_msg": "{{title}} status updated on MAL", + "update_failed": "Update Failed", + "update_failed_msg": "Could not update MAL status", + "remove_title": "Remove from List", + "remove_confirm": "Are you sure you want to remove {{title}} from your MyAnimeList?", + "removed": "Removed", + "removed_msg": "{{title}} removed from MAL", + "remove_failed": "Remove Failed", + "remove_failed_msg": "Could not remove from MAL", + "description": "Sync with MyAnimeList", + "discord_open_prompt": "Discord: @{{username}}\n\nDo you want to open Discord and search for this user?" + }, + "profiles": { + "error_load": "Failed to load profiles", + "error_save": "Failed to save profiles", + "error_empty_name": "Please enter a profile name", + "error_delete_active": "Cannot delete the active profile. Switch to another profile first.", + "error_delete_only": "Cannot delete the only profile", + "delete_title": "Delete Profile", + "delete_confirm": "Are you sure you want to delete this profile? This action cannot be undone." + }, + "auth": { + "invalid_email": "Invalid Email", + "invalid_email_msg": "Enter a valid email address", + "password_short": "Password Too Short", + "password_short_msg": "Password must be at least 6 characters", + "passwords_mismatch": "Passwords Don't Match", + "passwords_mismatch_msg": "Passwords do not match", + "auth_failed": "Authentication Failed", + "login_success": "Logged in successfully", + "signup_success": "Sign up successful" + }, + "stream_card": { + "url_copied": "URL Copied", + "url_copied_msg": "Stream URL copied to clipboard!", + "copied": "Copied!", + "copied_msg": "Stream URL has been copied to clipboard.", + "already_downloading": "Already Downloading", + "already_downloading_msg": "This download has already started for this exact link.", + "download_started": "Download Started", + "download_started_msg": "Your download has been added to the queue.", + "download_failed": "Download Failed" } } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 9f8ac03a..ae33ddad 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -176,7 +176,12 @@ "added_to_collection": "Ajouté à la collection", "added_to_collection_desc": "Ajouté à votre collection Trakt", "removed_from_collection": "Retiré de la collection", - "removed_from_collection_desc": "Retiré de votre collection Trakt" + "removed_from_collection_desc": "Retiré de votre collection Trakt", + "sync_failed": "Échec de la synchronisation", + "sync_connect_first": "Connecte-toi d'abord à Trakt", + "sync_complete": "Synchronisation terminée", + "sync_up_to_date": "La bibliothèque est à jour", + "sync_error": "Impossible de synchroniser. Réessaie." }, "metadata": { "unable_to_load": "Impossible de charger le contenu", @@ -275,7 +280,9 @@ "removed_from_collection_hero": "Retiré de la collection", "removed_from_collection_desc_hero": "Retiré de votre collection Trakt", "mark_as_watched": "Marquer comme vu", - "mark_as_unwatched": "Marquer comme non vu" + "mark_as_unwatched": "Marquer comme non vu", + "no_backdrops": "Aucun fond d'écran trouvé", + "error_backdrops": "Échec du chargement des fonds d'écran" }, "cast": { "biography": "Biographie", @@ -334,7 +341,10 @@ "unavailable": "Bande-annonce indisponible", "unavailable_desc": "Cette bande-annonce n'a pas pu être chargée pour le moment. Veuillez réessayer plus tard.", "unable_to_play": "Impossible de lire la bande-annonce. Veuillez réessayer.", - "watch_on_youtube": "Regarder sur YouTube" + "watch_on_youtube": "Regarder sur YouTube", + "error_play": "Échec de la lecture de la bande-annonce", + "error_ios_format": "Ce format de bande-annonce n'est pas supporté sur iOS.", + "error_retry": "Impossible de lire la bande-annonce. Réessaie." }, "catalog": { "no_content_found": "Aucun contenu trouvé", @@ -360,7 +370,9 @@ "still_fetching": "Récupération des flux en cours...", "no_streams_available": "Aucun flux disponible", "starting_best_stream": "Démarrage du meilleur flux...", - "loading_more_sources": "Chargement d'autres sources..." + "loading_more_sources": "Chargement d'autres sources...", + "torrent_not_supported": "Non supporté", + "torrent_not_supported_msg": "Le streaming par torrent n'est pas encore supporté." }, "player_ui": { "via": "via {{name}}", @@ -419,7 +431,12 @@ "timing_offset": "Décalage temporel (s)", "visual_sync": "Synchronisation visuelle", "timing_hint": "Avancez (-) ou retardez (+) les sous-titres pour synchroniser si nécessaire.", - "reset_defaults": "Réinitialiser les paramètres" + "reset_defaults": "Réinitialiser les paramètres", + "mark_intro_start": "Marquer le début de l'intro", + "mark_intro_end": "Marquer la fin de l'intro", + "intro_start_marked": "Début de l'intro marqué", + "intro_submitted": "Intro soumise avec succès", + "intro_submit_failed": "Échec de la soumission de l'intro" }, "downloads": { "title": "Téléchargements", @@ -522,7 +539,12 @@ "sign_out_error": "Échec de la déconnexion de Trakt.", "sync_complete_title": "Synchronisation terminée", "sync_success_msg": "Votre progression a été synchronisée avec succès avec Trakt.", - "sync_error_msg": "La synchronisation a échoué. Veuillez réessayer." + "sync_error_msg": "La synchronisation a échoué. Veuillez réessayer.", + "conflict_title": "Conflit", + "conflict_simkl_msg": "Tu ne peux pas connecter Trakt tant que Simkl est connecté. Déconnecte d'abord Simkl.", + "library_sync_updated": "Mode de synchronisation mis à jour", + "library_sync_updated_msg": "La synchronisation Trakt est maintenant réglée sur : {{mode}}", + "library_sync_error": "Échec de la mise à jour du mode de synchronisation" }, "simkl": { "title": "Paramètres Simkl", @@ -685,7 +707,9 @@ "media": "MÉDIA", "notifications": "NOTIFICATIONS", "testing": "TESTS", - "danger_zone": "ZONE DE DANGER" + "danger_zone": "ZONE DE DANGER", + "data": "DONNÉES", + "general": "GÉNÉRAL" }, "items": { "legal": "Mentions Légales", @@ -818,7 +842,20 @@ "env_warning": "Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync." }, "connection": "Connection" - } + }, + "clear_mdblist_cache_title": "Effacer le cache MDBList", + "clear_mdblist_cache_confirm": "Es-tu sûr de vouloir effacer toutes les données MDBList en cache ? Cette action est irréversible.", + "clear_mdblist_cache_success": "Le cache MDBList a été effacé.", + "clear_mdblist_cache_error": "Impossible d'effacer le cache MDBList.", + "reset_onboarding_success": "L'accueil a été réinitialisé. Redémarre l'application pour voir le flux d'accueil.", + "reset_onboarding_error": "Échec de la réinitialisation de l'accueil.", + "reset_campaigns_success": "Historique des campagnes réinitialisé. Redémarre l'application pour revoir les affiches.", + "clear_data_success": "Toutes les données ont été effacées. Redémarre l'application.", + "clear_data_error": "Échec de l'effacement des données.", + "plugin_tester": "Testeur de plugin", + "plugin_tester_desc": "Exécuter un plugin et inspecter les logs/flux", + "vietnamese": "Vietnamien", + "backup_restore_desc": "Créer et restaurer des sauvegardes" }, "privacy": { "title": "Confidentialité et Données", @@ -893,7 +930,8 @@ "confirm_remove_title": "Supprimer la clé API", "confirm_remove_msg": "Êtes-vous sûr de vouloir supprimer votre clé API OpenRouter ? Cela désactivera les fonctionnalités de chat IA.", "success_removed": "Clé API supprimée avec succès", - "error_remove": "Échec de la suppression de la clé API" + "error_remove": "Échec de la suppression de la clé API", + "error_load_context": "Échec du chargement des détails pour le chat IA" }, "catalog_settings": { "title": "Catalogues", @@ -1069,7 +1107,9 @@ "error_load": "Échec du chargement des catalogues", "movies": "Films", "tv_shows": "Séries TV" - } + }, + "prefer_external_meta": "Préférer les métadonnées externes", + "prefer_external_meta_desc": "Utiliser les métadonnées externes sur la page de détails" }, "calendar": { "title": "Calendrier", @@ -1354,7 +1394,20 @@ "got_it": "Compris !", "repo_format_hint": "Format : https://raw.githubusercontent.com/user/repo/branch", "cancel": "Annuler", - "add": "Ajouter" + "add": "Ajouter", + "error_update_extensions": "Échec de la mise à jour des extensions", + "error_valid_url": "Entre une URL de dépôt valide", + "error_valid_url_format": "Utilise une URL GitHub raw valide :\n\nhttps://raw.githubusercontent.com/user/repo/refs/heads/branch", + "error_update_repo": "Échec de la mise à jour du dépôt", + "error_remove_repo": "Échec de la suppression du dépôt", + "error_save_repo": "Échec de l'enregistrement de l'URL du dépôt", + "error_set_repo_first": "Définis d'abord une URL de dépôt", + "error_repo": "Erreur de dépôt", + "error_update_status": "Échec de la mise à jour du statut de l'extension", + "error_clear_extensions": "Échec de la suppression des extensions", + "error_clear_cache": "Échec du vidage du cache du dépôt", + "showbox_saved": "Enregistré", + "showbox_saved_msg": "Paramètres ShowBox mis à jour" }, "theme": { "title": "Thèmes de l'App", @@ -1480,5 +1533,99 @@ "provider_logs": "Logs du Fournisseur", "no_logs_captured": "Aucun log capturé." } + }, + "mal": { + "title": "MyAnimeList", + "connect_title": "Connecter MyAnimeList", + "connect_desc": "Synchronise ton historique et gère ta liste d'anime.", + "sign_in": "Se connecter avec MAL", + "sign_out": "Se déconnecter", + "sign_out_confirm": "Es-tu sûr de vouloir te déconnecter ?", + "sync_button": "Synchroniser", + "connected": "Connecté à MyAnimeList", + "error_connect": "Échec de la connexion à MyAnimeList", + "error_sign_in": "Une erreur est survenue lors de la connexion : {{message}}", + "sync_complete": "Synchronisation terminée", + "sync_complete_msg": "Les données MAL ont été actualisées.", + "sync_failed": "Échec de la synchronisation", + "sync_failed_msg": "Impossible d'actualiser les données MAL.", + "sync_settings": "Paramètres de synchronisation", + "enable_sync": "Activer la synchronisation MAL", + "enable_sync_desc": "Interrupteur global pour activer ou désactiver toutes les fonctionnalités MyAnimeList.", + "auto_episode": "Mise à jour auto des épisodes", + "auto_episode_desc": "Met automatiquement à jour ta progression sur MAL quand tu finis un épisode (>=90% de complétion).", + "auto_add": "Ajout auto des anime", + "auto_add_desc": "Si un anime n'est pas dans ta liste MAL, il sera ajouté automatiquement quand tu commences à le regarder.", + "auto_sync_library": "Synchronisation auto vers la bibliothèque", + "auto_sync_library_desc": "Ajoute automatiquement les éléments de ta liste MAL \"En cours\" à ta bibliothèque Nuvio.", + "include_nsfw": "Inclure le contenu NSFW", + "include_nsfw_desc": "Autoriser les entrées NSFW lors de la récupération de ta liste MAL.", + "stat_total": "Total", + "stat_days": "Jours", + "stat_mean": "Moyenne", + "watching": "En cours", + "completed": "Terminé", + "on_hold": "En pause", + "dropped": "Abandonné", + "plan_to_watch": "À regarder", + "edit_title": "Modifier {{title}}", + "updated": "Mis à jour", + "updated_msg": "Statut de {{title}} mis à jour sur MAL", + "update_failed": "Échec de la mise à jour", + "update_failed_msg": "Impossible de mettre à jour le statut MAL", + "remove_title": "Retirer de la liste", + "remove_confirm": "Es-tu sûr de vouloir retirer {{title}} de ta liste MyAnimeList ?", + "removed": "Retiré", + "removed_msg": "{{title}} retiré de MAL", + "remove_failed": "Échec du retrait", + "remove_failed_msg": "Impossible de retirer de MAL", + "description": "Synchroniser avec MyAnimeList", + "discord_open_prompt": "Discord : @{{username}}\n\nOuvrir Discord et chercher cet utilisateur ?" + }, + "profiles": { + "error_load": "Échec du chargement des profils", + "error_save": "Échec de l'enregistrement des profils", + "error_empty_name": "Entre un nom de profil", + "error_delete_active": "Impossible de supprimer le profil actif. Bascule d'abord sur un autre profil.", + "error_delete_only": "Impossible de supprimer le seul profil", + "delete_title": "Supprimer le profil", + "delete_confirm": "Es-tu sûr de vouloir supprimer ce profil ? Cette action est irréversible." + }, + "auth": { + "invalid_email": "E-mail invalide", + "invalid_email_msg": "Entre une adresse e-mail valide", + "password_short": "Mot de passe trop court", + "password_short_msg": "Le mot de passe doit contenir au moins 6 caractères", + "passwords_mismatch": "Mots de passe différents", + "passwords_mismatch_msg": "Les mots de passe ne correspondent pas", + "auth_failed": "Échec de l'authentification", + "login_success": "Connexion réussie", + "signup_success": "Inscription réussie" + }, + "stream_card": { + "url_copied": "URL copiée", + "url_copied_msg": "URL du flux copiée dans le presse-papiers !", + "copied": "Copié !", + "copied_msg": "L'URL du flux a été copiée dans le presse-papiers.", + "already_downloading": "Déjà en téléchargement", + "already_downloading_msg": "Ce téléchargement a déjà été lancé pour ce lien.", + "download_started": "Téléchargement lancé", + "download_started_msg": "Ton téléchargement a été ajouté à la file d'attente.", + "download_failed": "Échec du téléchargement" + }, + "parentalGuide": { + "labels": { + "nudity": "Nudité", + "violence": "Violence", + "profanity": "Langage grossier", + "alcohol": "Alcool/Drogues", + "frightening": "Scènes effrayantes" + }, + "severity": { + "severe": "Grave", + "moderate": "Modéré", + "mild": "Léger", + "none": "Aucun" + } } } diff --git a/src/screens/AIChatScreen.tsx b/src/screens/AIChatScreen.tsx index a669bb5c..a2c79d26 100644 --- a/src/screens/AIChatScreen.tsx +++ b/src/screens/AIChatScreen.tsx @@ -16,6 +16,7 @@ import { import CustomAlert from '../components/CustomAlert'; import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; import { useTheme } from '../contexts/ThemeContext'; import FastImage from '@d11/react-native-fast-image'; import { BlurView as ExpoBlurView } from 'expo-blur'; @@ -428,6 +429,7 @@ const SuggestionChip: React.FC = React.memo(({ text, onPres }, (prev, next) => prev.text === next.text && prev.onPress === next.onPress && prev.index === next.index); const AIChatScreen: React.FC = () => { + const { t } = useTranslation(); // CustomAlert state const [alertVisible, setAlertVisible] = useState(false); const [alertTitle, setAlertTitle] = useState(''); @@ -597,7 +599,7 @@ const AIChatScreen: React.FC = () => { } } catch (error) { if (__DEV__) console.error('Error loading context:', error); - openAlert('Error', 'Failed to load content details for AI chat'); + openAlert(t('common.error'), t('ai_settings.error_load_context')); } finally { setIsLoadingContext(false); {/* CustomAlert at root */ } diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx index 7c42cfb0..f091f1c4 100644 --- a/src/screens/AuthScreen.tsx +++ b/src/screens/AuthScreen.tsx @@ -9,6 +9,7 @@ import { useNavigation, useRoute } from '@react-navigation/native'; import * as Haptics from 'expo-haptics'; import { useToast } from '../contexts/ToastContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useTranslation } from 'react-i18next'; const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__'; const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24'] as const; @@ -51,6 +52,7 @@ const AuthScreen: React.FC = () => { const safeTopInset = Math.max(insets.top, Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : 0); const backButtonTop = safeTopInset + 8; const { showError, showSuccess } = useToast(); + const { t } = useTranslation(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -163,21 +165,21 @@ const AuthScreen: React.FC = () => { if (!isEmailValid) { const msg = 'Enter a valid email address'; setError(msg); - showError('Invalid Email', 'Enter a valid email address'); + showError(t('auth.invalid_email'), t('auth.invalid_email_msg')); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } if (!isPasswordValid) { const msg = 'Password must be at least 6 characters'; setError(msg); - showError('Password Too Short', 'Password must be at least 6 characters'); + showError(t('auth.password_short'), t('auth.password_short_msg')); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } if (mode === 'signup' && !passwordsMatch) { const msg = 'Passwords do not match'; setError(msg); - showError('Passwords Don\'t Match', 'Passwords do not match'); + showError(t('auth.passwords_mismatch'), t('auth.passwords_mismatch_msg')); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } @@ -197,11 +199,11 @@ const AuthScreen: React.FC = () => { const cleanError = normalizeAuthErrorMessage(err); setError(cleanError); - showError('Authentication Failed', cleanError); + showError(t('auth.auth_failed'), cleanError); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); } else { - const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful'; - showSuccess('Success', msg); + const msg = mode === 'signin' ? t('auth.login_success') : t('auth.signup_success'); + showSuccess(t('common.success'), msg); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); // Navigate to main tabs after successful authentication diff --git a/src/screens/BackdropGalleryScreen.tsx b/src/screens/BackdropGalleryScreen.tsx index b9b71bb2..d1d1c0a4 100644 --- a/src/screens/BackdropGalleryScreen.tsx +++ b/src/screens/BackdropGalleryScreen.tsx @@ -15,6 +15,7 @@ import FastImage from '@d11/react-native-fast-image'; import { MaterialIcons } from '@expo/vector-icons'; import { TMDBService } from '../services/tmdbService'; import { useTheme } from '../contexts/ThemeContext'; +import { useTranslation } from 'react-i18next'; import { useSettings } from '../hooks/useSettings'; const { width } = Dimensions.get('window'); @@ -40,6 +41,7 @@ const BackdropGalleryScreen: React.FC = () => { const navigation = useNavigation(); const { tmdbId, type, title } = route.params as RouteParams; const { currentTheme } = useTheme(); + const { t } = useTranslation(); const { settings } = useSettings(); const [backdrops, setBackdrops] = useState([]); @@ -75,10 +77,10 @@ const BackdropGalleryScreen: React.FC = () => { if (images && images.backdrops && images.backdrops.length > 0) { setBackdrops(images.backdrops); } else { - setError('No backdrops found'); + setError(t('metadata.no_backdrops')); } } catch (err) { - setError('Failed to load backdrops'); + setError(t('metadata.error_backdrops')); console.error('Backdrop fetch error:', err); } finally { setLoading(false); diff --git a/src/screens/ContributorsScreen.tsx b/src/screens/ContributorsScreen.tsx index 581d6b16..d4f195c1 100644 --- a/src/screens/ContributorsScreen.tsx +++ b/src/screens/ContributorsScreen.tsx @@ -161,8 +161,8 @@ const SpecialMentionCard: React.FC = ({ mention, curren // Fallback: show alert with Discord info Alert.alert( mention.name, - `Discord: @${mention.username}\n\nDo you want to open Discord and search for this user?`, - [{ text: 'OK' }] + t('mal.discord_open_prompt', { username: mention.username }), + [{ text: t('common.ok') }] ); } }); diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index b22ada28..b424220b 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -464,7 +464,7 @@ const LibraryScreen = () => { // Sync Trakt watchlist to local library const syncTraktWatchlistToLibrary = useCallback(async () => { if (!traktAuthenticated) { - showError('Sync Failed', 'Please connect to Trakt first'); + showError(t('library.sync_failed'), t('library.sync_connect_first')); return; } @@ -667,11 +667,11 @@ const LibraryScreen = () => { showInfo('Sync Complete', message); logger.log(`[LibraryScreen] Sync complete: ${message}`); } else { - showInfo('Sync Complete', 'Library is up to date'); + showInfo(t('library.sync_complete'), t('library.sync_up_to_date')); } } catch (error) { logger.error('[LibraryScreen] Sync failed:', error); - showError('Sync Failed', 'Unable to sync. Please try again.'); + showError(t('library.sync_failed'), t('library.sync_error')); } finally { setIsSyncing(false); } diff --git a/src/screens/MalSettingsScreen.tsx b/src/screens/MalSettingsScreen.tsx index 15f1fa1c..bffbc094 100644 --- a/src/screens/MalSettingsScreen.tsx +++ b/src/screens/MalSettingsScreen.tsx @@ -98,24 +98,24 @@ const MalSettingsScreen: React.FC = () => { const result = await MalAuth.login(); if (result === true) { await checkAuthStatus(); - openAlert('Success', 'Connected to MyAnimeList'); + openAlert(t('common.success'), t('mal.connected')); } else { - const errorMessage = typeof result === 'string' ? result : 'Failed to connect to MyAnimeList'; - openAlert('Error', errorMessage); + const errorMessage = typeof result === 'string' ? result : t('mal.error_connect'); + openAlert(t('common.error'), errorMessage); } } catch (e: any) { console.error(e); - openAlert('Error', `An error occurred during sign in: ${e.message || 'Unknown error'}`); + openAlert(t('common.error'), t('mal.error_sign_in', { message: e.message || t('common.unknown') })); } finally { setIsLoading(false); } }; const handleSignOut = () => { - openAlert('Sign Out', 'Are you sure you want to disconnect?', [ - { label: 'Cancel', onPress: () => setAlertVisible(false) }, - { - label: 'Sign Out', + openAlert(t('mal.sign_out'), t('mal.sign_out_confirm'), [ + { label: t('common.cancel'), onPress: () => setAlertVisible(false) }, + { + label: t('mal.sign_out'), onPress: () => { MalAuth.clearToken(); setIsAuthenticated(false); @@ -168,13 +168,13 @@ const MalSettingsScreen: React.FC = () => { color={currentTheme.colors.highEmphasis} /> - Settings + {t('common.settings')} - MyAnimeList + {t('mal.title')} @@ -232,47 +232,47 @@ const MalSettingsScreen: React.FC = () => { {userProfile.anime_statistics.num_items} - Total + {t('mal.stat_total')} {userProfile.anime_statistics.num_days_watched.toFixed(1)} - Days + {t('mal.stat_days')} {userProfile.anime_statistics.mean_score.toFixed(1)} - Mean + {t('mal.stat_mean')} - Watching + {t('mal.watching')} {userProfile.anime_statistics.num_items_watching} - Completed + {t('mal.completed')} {userProfile.anime_statistics.num_items_completed} - On Hold + {t('mal.on_hold')} {userProfile.anime_statistics.num_items_on_hold} - Dropped + {t('mal.dropped')} {userProfile.anime_statistics.num_items_dropped} @@ -289,26 +289,26 @@ const MalSettingsScreen: React.FC = () => { try { const synced = await MalSync.syncMalToLibrary(); if (synced) { - openAlert('Sync Complete', 'MAL data has been refreshed.'); + openAlert(t('mal.sync_complete'), t('mal.sync_complete_msg')); } else { - openAlert('Sync Failed', 'Could not refresh MAL data.'); + openAlert(t('mal.sync_failed'), t('mal.sync_failed_msg')); } } catch { - openAlert('Sync Failed', 'Could not refresh MAL data.'); + openAlert(t('mal.sync_failed'), t('mal.sync_failed_msg')); } finally { setIsLoading(false); } }} > - Sync + {t('mal.sync_button')} - Sign Out + {t('mal.sign_out')} @@ -320,16 +320,16 @@ const MalSettingsScreen: React.FC = () => { resizeMode="contain" /> - Connect MyAnimeList + {t('mal.connect_title')} - Sync your watch history and manage your anime list. + {t('mal.connect_desc')} - Sign In with MAL + {t('mal.sign_in')} )} @@ -339,17 +339,17 @@ const MalSettingsScreen: React.FC = () => { - Sync Settings + {t('mal.sync_settings')} - Enable MAL Sync + {t('mal.enable_sync')} - Global switch to enable or disable all MyAnimeList features. + {t('mal.enable_sync_desc')} { - Auto Episode Update + {t('mal.auto_episode')} - Automatically update your progress on MAL when you finish watching an episode (>=90% completion). + {t('mal.auto_episode_desc')} { - Auto Add Anime + {t('mal.auto_add')} - If an anime is not in your MAL list, it will be added automatically when you start watching. + {t('mal.auto_add_desc')} { - Auto-Sync to Library + {t('mal.auto_sync_library')} - Automatically add items from your MAL 'Watching' list to your Nuvio Library. + {t('mal.auto_sync_library_desc')} { - Include NSFW Content + {t('mal.include_nsfw')} - Allow NSFW entries to be returned when fetching your MAL list. + {t('mal.include_nsfw_desc')} { // Small delay to ensure UI is ready setTimeout(() => { openAlert( - 'Add Repository', - `Do you want to add the repository from:\n${url}`, + t('plugins.add_repo'), + t('plugins.add_repo_confirm', { url }), [ { - label: 'Cancel', + label: t('common.cancel'), onPress: () => { }, style: { color: colors.error } }, { - label: 'Add', + label: t('plugins.add'), onPress: () => { handleAddRepository(url); } @@ -950,7 +950,7 @@ const PluginsScreen: React.FC = () => { ) => { setAlertTitle(title); setAlertMessage(message); - setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]); setAlertVisible(true); }; @@ -1060,7 +1060,7 @@ const PluginsScreen: React.FC = () => { openAlert(t('plugins.success'), `${enabled ? t('plugins.enabled') : t('plugins.disabled')} ${filteredPlugins.length} extensions`); } catch (error) { logger.error('[PluginSettings] Failed to bulk toggle:', error); - openAlert(t('plugins.error'), 'Failed to update extensions'); + openAlert(t('plugins.error'), t('plugins.error_update_extensions')); } finally { setIsRefreshing(false); } @@ -1076,7 +1076,7 @@ const PluginsScreen: React.FC = () => { const inputUrl = validUrlOverride || newRepositoryUrl; if (!inputUrl.trim()) { - openAlert('Error', 'Please enter a valid repository URL'); + openAlert(t('common.error'), t('plugins.error_valid_url')); return; } @@ -1085,7 +1085,7 @@ const PluginsScreen: React.FC = () => { if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { openAlert( t('plugins.alert_invalid_url'), - 'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nor include manifest.json:\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch/manifest.json\n\nExample:\nhttps://raw.githubusercontent.com/your-username/your-repo/refs/heads/main' + t('plugins.error_valid_url_format') ); return; } @@ -1166,7 +1166,7 @@ const PluginsScreen: React.FC = () => { openAlert(t('plugins.success'), `Repository "${repo?.name || t('plugins.unknown')}" ${enabled ? t('plugins.enabled').toLowerCase() : t('plugins.disabled').toLowerCase()} successfully`); } catch (error) { logger.error('[PluginSettings] Failed to toggle repository:', error); - openAlert(t('plugins.error'), 'Failed to update repository'); + openAlert(t('plugins.error'), t('plugins.error_update_repo')); } finally { setSwitchingRepository(null); } @@ -1179,30 +1179,30 @@ const PluginsScreen: React.FC = () => { // Special handling for the last repository const isLastRepository = repositories.length === 1; - const alertTitle = isLastRepository ? 'Remove Last Repository' : 'Remove Repository'; + const alertTitleText = isLastRepository ? t('plugins.remove_last_repo') : t('plugins.remove_repo'); const alertMessage = isLastRepository - ? `Are you sure you want to remove "${repo.name}"? This is your only repository, so you'll have no extensions available until you add a new repository.` - : `Are you sure you want to remove "${repo.name}"? This will also remove all extensions from this repository.`; + ? t('plugins.remove_last_repo_desc', { name: repo.name }) + : t('plugins.remove_repo_desc', { name: repo.name }); openAlert( - alertTitle, + alertTitleText, alertMessage, [ - { label: 'Cancel', onPress: () => { } }, + { label: t('common.cancel'), onPress: () => { } }, { - label: 'Remove', + label: t('plugins.remove'), onPress: async () => { try { await pluginService.removeRepository(repoId); await loadRepositories(); await loadPlugins(); const successMessage = isLastRepository - ? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.' - : 'Repository removed successfully'; - openAlert('Success', successMessage); + ? t('plugins.remove_last_repo_success') + : t('plugins.remove_repo_success'); + openAlert(t('common.success'), successMessage); } catch (error) { logger.error('[PluginSettings] Failed to remove repository:', error); - openAlert('Error', error instanceof Error ? error.message : 'Failed to remove repository'); + openAlert(t('common.error'), error instanceof Error ? error.message : t('plugins.error_remove_repo')); } }, }, @@ -1282,7 +1282,7 @@ const PluginsScreen: React.FC = () => { const handleSaveRepository = async () => { if (!repositoryUrl.trim()) { - openAlert('Error', 'Please enter a valid repository URL'); + openAlert(t('common.error'), t('plugins.error_valid_url')); return; } @@ -1290,8 +1290,8 @@ const PluginsScreen: React.FC = () => { const url = repositoryUrl.trim(); if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { openAlert( - 'Invalid URL Format', - 'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nExample:\nhttps://raw.githubusercontent.com/your-username/your-repo/refs/heads/main' + t('plugins.alert_invalid_url'), + t('plugins.error_valid_url_format') ); return; } @@ -1304,7 +1304,7 @@ const PluginsScreen: React.FC = () => { openAlert(t('plugins.success'), t('plugins.alert_repo_saved')); } catch (error) { logger.error('[PluginSettings] Failed to save repository:', error); - openAlert(t('plugins.error'), 'Failed to save repository URL'); + openAlert(t('plugins.error'), t('plugins.error_save_repo')); } finally { setIsLoading(false); } @@ -1312,7 +1312,7 @@ const PluginsScreen: React.FC = () => { const handleRefreshRepository = async () => { if (!repositoryUrl.trim()) { - openAlert('Error', 'Please set a repository URL first'); + openAlert(t('common.error'), t('plugins.error_set_repo_first')); return; } @@ -1331,8 +1331,8 @@ const PluginsScreen: React.FC = () => { logger.error('[PluginsScreen] Failed to refresh repository:', error); const errorMessage = error instanceof Error ? error.message : String(error); openAlert( - 'Repository Error', - `Failed to refresh repository: ${errorMessage}\n\nPlease ensure your URL is correct and follows this format:\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch` + t('plugins.error_repo'), + t('plugins.error_refresh_repo', { error: errorMessage }) ); } finally { setIsRefreshing(false); @@ -1358,7 +1358,7 @@ const PluginsScreen: React.FC = () => { await loadPlugins(); } catch (error) { logger.error('[PluginSettings] Failed to toggle plugin:', error); - openAlert(t('plugins.error'), 'Failed to update extension status'); + openAlert(t('plugins.error'), t('plugins.error_update_status')); setIsRefreshing(false); } }; @@ -1368,9 +1368,9 @@ const PluginsScreen: React.FC = () => { t('plugins.clear_all'), t('plugins.clear_all_desc'), [ - { label: 'Cancel', onPress: () => { } }, + { label: t('common.cancel'), onPress: () => { } }, { - label: 'Clear', + label: t('plugins.clear'), onPress: async () => { try { await pluginService.clearScrapers(); @@ -1378,7 +1378,7 @@ const PluginsScreen: React.FC = () => { openAlert(t('plugins.success'), t('plugins.alert_plugins_cleared')); } catch (error) { logger.error('[PluginSettings] Failed to clear plugins:', error); - openAlert(t('plugins.error'), 'Failed to clear extensions'); + openAlert(t('plugins.error'), t('plugins.error_clear_extensions')); } }, }, @@ -1391,9 +1391,9 @@ const PluginsScreen: React.FC = () => { t('plugins.clear_cache'), t('plugins.clear_cache_desc'), [ - { label: 'Cancel', onPress: () => { } }, + { label: t('common.cancel'), onPress: () => { } }, { - label: 'Clear Cache', + label: t('plugins.clear_cache'), onPress: async () => { try { await pluginService.clearScrapers(); @@ -1405,7 +1405,7 @@ const PluginsScreen: React.FC = () => { openAlert(t('plugins.success'), t('plugins.alert_cache_cleared')); } catch (error) { logger.error('[PluginSettings] Failed to clear cache:', error); - openAlert(t('plugins.error'), 'Failed to clear repository cache'); + openAlert(t('plugins.error'), t('plugins.error_clear_cache')); } }, }, @@ -1927,7 +1927,7 @@ const PluginsScreen: React.FC = () => { }); } setShowboxSavedToken(showboxUiToken); - openAlert('Saved', 'ShowBox settings updated'); + openAlert(t('plugins.showbox_saved'), t('plugins.showbox_saved_msg')); }} > {t('plugins.save')} diff --git a/src/screens/ProfilesScreen.tsx b/src/screens/ProfilesScreen.tsx index 56a7dbb3..90c24f3f 100644 --- a/src/screens/ProfilesScreen.tsx +++ b/src/screens/ProfilesScreen.tsx @@ -14,6 +14,7 @@ import { import { useNavigation } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { useTheme } from '../contexts/ThemeContext'; +import { useTranslation } from 'react-i18next'; import { useTraktContext } from '../contexts/TraktContext'; import { mmkvStorage } from '../services/mmkvStorage'; import CustomAlert from '../components/CustomAlert'; @@ -33,6 +34,7 @@ const ProfilesScreen: React.FC = () => { const navigation = useNavigation(); const { currentTheme } = useTheme(); const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); + const { t } = useTranslation(); const [profiles, setProfiles] = useState([]); const [showAddModal, setShowAddModal] = useState(false); @@ -76,7 +78,7 @@ const ProfilesScreen: React.FC = () => { } } catch (error) { if (__DEV__) console.error('Error loading profiles:', error); - openAlert('Error', 'Failed to load profiles'); + openAlert(t('common.error'), t('profiles.error_load')); } finally { setIsLoading(false); } @@ -102,7 +104,7 @@ const ProfilesScreen: React.FC = () => { await mmkvStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles)); } catch (error) { if (__DEV__) console.error('Error saving profiles:', error); - openAlert('Error', 'Failed to save profiles'); + openAlert(t('common.error'), t('profiles.error_save')); } }, []); @@ -118,7 +120,7 @@ const ProfilesScreen: React.FC = () => { const handleAddProfile = useCallback(() => { if (!newProfileName.trim()) { - openAlert('Error', 'Please enter a profile name'); + openAlert(t('common.error'), t('profiles.error_empty_name')); return; } @@ -150,23 +152,23 @@ const ProfilesScreen: React.FC = () => { // Prevent deleting the active profile const isActiveProfile = profiles.find(p => p.id === id)?.isActive; if (isActiveProfile) { - openAlert('Error', 'Cannot delete the active profile. Switch to another profile first.'); + openAlert(t('common.error'), t('profiles.error_delete_active')); return; } // Prevent deleting the last profile if (profiles.length <= 1) { - openAlert('Error', 'Cannot delete the only profile'); + openAlert(t('common.error'), t('profiles.error_delete_only')); return; } openAlert( - 'Delete Profile', - 'Are you sure you want to delete this profile? This action cannot be undone.', + t('profiles.delete_title'), + t('profiles.delete_confirm'), [ - { label: 'Cancel', onPress: () => { } }, + { label: t('common.cancel'), onPress: () => { } }, { - label: 'Delete', + label: t('common.delete'), onPress: () => { const updatedProfiles = profiles.filter(profile => profile.id !== id); setProfiles(updatedProfiles); diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index eee4857a..9dff5778 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -186,7 +186,7 @@ const SettingsScreen: React.FC = () => { ) => { setAlertTitle(title); setAlertMessage(message); - setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]); setAlertVisible(true); }; @@ -334,18 +334,18 @@ const SettingsScreen: React.FC = () => { const handleClearMDBListCache = () => { openAlert( - 'Clear MDBList Cache', - 'Are you sure you want to clear all cached MDBList data? This cannot be undone.', + t('settings.clear_mdblist_cache_title'), + t('settings.clear_mdblist_cache_confirm'), [ - { label: 'Cancel', onPress: () => { } }, + { label: t('common.cancel'), onPress: () => { } }, { - label: 'Clear', + label: t('common.delete'), onPress: async () => { try { await mmkvStorage.removeItem('mdblist_cache'); - openAlert('Success', 'MDBList cache has been cleared.'); + openAlert(t('common.success'), t('settings.clear_mdblist_cache_success')); } catch (error) { - openAlert('Error', 'Could not clear MDBList cache.'); + openAlert(t('common.error'), t('settings.clear_mdblist_cache_error')); if (__DEV__) console.error('Error clearing MDBList cache:', error); } } @@ -420,8 +420,8 @@ const SettingsScreen: React.FC = () => { )} {isItemVisible('mal') && ( } renderControl={() => } onPress={() => navigation.navigate('MalSettings')} @@ -438,7 +438,7 @@ const SettingsScreen: React.FC = () => { case 'appearance': return ( <> - + l.code === i18n.language)?.key}`)} @@ -476,8 +476,8 @@ const SettingsScreen: React.FC = () => { isTablet={isTablet} /> navigation.navigate('PluginTester')} renderControl={() => } @@ -489,9 +489,9 @@ const SettingsScreen: React.FC = () => { onPress={async () => { try { await mmkvStorage.removeItem('hasCompletedOnboarding'); - openAlert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.'); + openAlert(t('common.success'), t('settings.reset_onboarding_success')); } catch (error) { - openAlert('Error', 'Failed to reset onboarding.'); + openAlert(t('common.error'), t('settings.reset_onboarding_error')); } }} renderControl={() => } @@ -503,7 +503,7 @@ const SettingsScreen: React.FC = () => { icon="refresh-cw" onPress={async () => { await campaignService.resetCampaigns(); - openAlert('Success', 'Campaign history reset. Restart app to see posters again.'); + openAlert(t('common.success'), t('settings.reset_campaigns_success')); }} renderControl={() => } isTablet={isTablet} @@ -516,15 +516,15 @@ const SettingsScreen: React.FC = () => { t('settings.clear_data'), t('settings.clear_data_desc'), [ - { label: 'Cancel', onPress: () => { } }, + { label: t('common.cancel'), onPress: () => { } }, { - label: 'Clear', + label: t('common.delete'), onPress: async () => { try { await mmkvStorage.clear(); - openAlert('Success', 'All data cleared. Please restart the app.'); + openAlert(t('common.success'), t('settings.clear_data_success')); } catch (error) { - openAlert('Error', 'Failed to clear data.'); + openAlert(t('common.error'), t('settings.clear_data_error')); } } } @@ -717,8 +717,8 @@ const SettingsScreen: React.FC = () => { {showCloudSyncItem && ( { )} {isItemVisible('mal') && ( } renderControl={() => } onPress={() => navigation.navigate('MalSettings')} @@ -771,7 +771,7 @@ const SettingsScreen: React.FC = () => { (settingsConfig?.categories?.['integrations']?.visible !== false) || (settingsConfig?.categories?.['playback']?.visible !== false) ) && ( - + l.code === i18n.language)?.key}`) @@ -825,11 +825,11 @@ const SettingsScreen: React.FC = () => { (settingsConfig?.categories?.['backup']?.visible !== false) || (settingsConfig?.categories?.['updates']?.visible !== false) ) && ( - + {(settingsConfig?.categories?.['backup']?.visible !== false) && ( } onPress={() => navigation.navigate('Backup')} diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index c02119f1..eb65749d 100644 --- a/src/screens/TraktSettingsScreen.tsx +++ b/src/screens/TraktSettingsScreen.tsx @@ -233,7 +233,7 @@ const TraktSettingsScreen: React.FC = () => { const handleSignIn = () => { if (isSimklAuthenticated) { - openAlert('Conflict', 'You cannot connect to Trakt while Simkl is connected. Please disconnect Simkl first.'); + openAlert(t('trakt.conflict_title'), t('trakt.conflict_simkl_msg')); return; } promptAsync(); // Trigger the authentication flow @@ -289,18 +289,18 @@ const TraktSettingsScreen: React.FC = () => { // Show confirmation const modeLabel = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode)?.label || mode; openAlert( - 'Library Sync Mode Updated', - `Trakt library sync is now set to: ${modeLabel}` + t('trakt.library_sync_updated'), + t('trakt.library_sync_updated_msg', { mode: modeLabel }) ); } catch (error) { logger.error('[TraktSettingsScreen] Failed to save library sync mode:', error); - openAlert('Error', 'Failed to update library sync mode'); + openAlert(t('common.error'), t('trakt.library_sync_error')); } }; const getLibrarySyncModeLabel = (mode: string): string => { const option = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode); - return option?.label || 'Off'; + return option?.label || t('common.disable'); }; return ( diff --git a/src/screens/settings/DeveloperSettingsScreen.tsx b/src/screens/settings/DeveloperSettingsScreen.tsx index fcee9a8a..5722d4a3 100644 --- a/src/screens/settings/DeveloperSettingsScreen.tsx +++ b/src/screens/settings/DeveloperSettingsScreen.tsx @@ -44,38 +44,38 @@ const DeveloperSettingsScreen: React.FC = () => { ) => { setAlertTitle(title); setAlertMessage(message); - setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]); setAlertVisible(true); }; const handleResetOnboarding = async () => { try { await mmkvStorage.removeItem('hasCompletedOnboarding'); - openAlert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.'); + openAlert(t('common.success'), t('settings.reset_onboarding_success')); } catch (error) { - openAlert('Error', 'Failed to reset onboarding.'); + openAlert(t('common.error'), t('settings.reset_onboarding_error')); } }; const handleResetCampaigns = async () => { await campaignService.resetCampaigns(); - openAlert('Success', 'Campaign history reset. Restart app to see posters again.'); + openAlert(t('common.success'), t('settings.reset_campaigns_success')); }; const handleClearAllData = () => { openAlert( - 'Clear All Data', - 'This will reset all settings and clear all cached data. Are you sure?', + t('settings.items.clear_all_data'), + t('settings.clear_data_desc'), [ - { label: 'Cancel', onPress: () => { } }, + { label: t('common.cancel'), onPress: () => { } }, { - label: 'Clear', + label: t('common.delete'), onPress: async () => { try { await mmkvStorage.clear(); - openAlert('Success', 'All data cleared. Please restart the app.'); + openAlert(t('common.success'), t('settings.clear_data_success')); } catch (error) { - openAlert('Error', 'Failed to clear data.'); + openAlert(t('common.error'), t('settings.clear_data_error')); } } } diff --git a/src/screens/streams/useStreamsScreen.ts b/src/screens/streams/useStreamsScreen.ts index 1dd04336..aa5c27ba 100644 --- a/src/screens/streams/useStreamsScreen.ts +++ b/src/screens/streams/useStreamsScreen.ts @@ -9,6 +9,7 @@ import { useMetadataAssets } from '../../hooks/useMetadataAssets'; import { useSettings } from '../../hooks/useSettings'; import { useTheme } from '../../contexts/ThemeContext'; import { useTrailer } from '../../contexts/TrailerContext'; +import { useTranslation } from 'react-i18next'; import { useToast } from '../../contexts/ToastContext'; import { useDominantColor } from '../../hooks/useDominantColor'; import { Stream } from '../../types/metadata'; @@ -49,6 +50,7 @@ export const useStreamsScreen = () => { const { currentTheme } = useTheme(); const { colors } = currentTheme; const { pauseTrailer, resumeTrailer } = useTrailer(); + const { t } = useTranslation(); const { showSuccess, showInfo } = useToast(); // Dimension tracking @@ -465,7 +467,7 @@ export const useStreamsScreen = () => { // Block magnet links if (typeof stream.url === 'string' && stream.url.startsWith('magnet:')) { - openAlert('Not supported', 'Torrent streaming is not supported yet.'); + openAlert(t('streams.torrent_not_supported'), t('streams.torrent_not_supported_msg')); return; }