This commit is contained in:
foXaCe 2026-03-29 18:31:15 +02:00 committed by GitHub
commit 46f2db7289
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 464 additions and 180 deletions

View file

@ -16,6 +16,7 @@ import QualityBadge from './metadata/QualityBadge';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { useDownloads } from '../contexts/DownloadsContext'; import { useDownloads } from '../contexts/DownloadsContext';
import { useToast } from '../contexts/ToastContext'; import { useToast } from '../contexts/ToastContext';
import { useTranslation } from 'react-i18next';
interface StreamCardProps { interface StreamCardProps {
stream: Stream; stream: Stream;
@ -63,6 +64,7 @@ const StreamCard = memo(({
const { settings } = useSettings(); const { settings } = useSettings();
const { startDownload } = useDownloads(); const { startDownload } = useDownloads();
const { showSuccess, showInfo } = useToast(); const { showSuccess, showInfo } = useToast();
const { t } = useTranslation();
// Handle long press to copy stream URL to clipboard // Handle long press to copy stream URL to clipboard
const handleLongPress = useCallback(async () => { const handleLongPress = useCallback(async () => {
@ -72,10 +74,10 @@ const StreamCard = memo(({
// Use toast for Android, custom alert for iOS // Use toast for Android, custom alert for iOS
if (Platform.OS === 'android') { 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 { } else {
// iOS uses custom alert // 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) { } catch (error) {
// Fallback: show URL in alert if clipboard fails // Fallback: show URL in alert if clipboard fails
@ -131,7 +133,7 @@ const StreamCard = memo(({
try { try {
const downloadsModule = require('../contexts/DownloadsContext'); const downloadsModule = require('../contexts/DownloadsContext');
if (downloadsModule && downloadsModule.isDownloadingUrl && downloadsModule.isDownloadingUrl(url)) { 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; return;
} }
} catch { } } catch { }
@ -178,9 +180,9 @@ const StreamCard = memo(({
imdbId: parentImdbId || parent.imdbId || undefined, imdbId: parentImdbId || parent.imdbId || undefined,
tmdbId: tmdbId, 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) { } 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]); }, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]);

View file

@ -17,6 +17,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
import { MalApiService } from '../../services/mal/MalApi'; import { MalApiService } from '../../services/mal/MalApi';
import { MalListStatus, MalAnimeNode } from '../../types/mal'; import { MalListStatus, MalAnimeNode } from '../../types/mal';
import { useTranslation } from 'react-i18next';
import { useToast } from '../../contexts/ToastContext'; import { useToast } from '../../contexts/ToastContext';
interface MalEditModalProps { interface MalEditModalProps {
@ -33,6 +34,7 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
onUpdateSuccess, onUpdateSuccess,
}) => { }) => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { t } = useTranslation();
const { showSuccess, showError } = useToast(); const { showSuccess, showError } = useToast();
const [status, setStatus] = useState<MalListStatus>(anime.list_status.status); const [status, setStatus] = useState<MalListStatus>(anime.list_status.status);
@ -62,11 +64,11 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
await MalApiService.updateStatus(anime.node.id, status, epNum, scoreNum, isRewatching); 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(); onUpdateSuccess();
onClose(); onClose();
} catch (error) { } catch (error) {
showError('Update Failed', 'Could not update MAL status'); showError(t('mal.update_failed'), t('mal.update_failed_msg'));
} finally { } finally {
setIsUpdating(false); setIsUpdating(false);
} }
@ -74,22 +76,22 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
const handleRemove = async () => { const handleRemove = async () => {
Alert.alert( Alert.alert(
'Remove from List', t('mal.remove_title'),
`Are you sure you want to remove ${anime.node.title} from your MyAnimeList?`, t('mal.remove_confirm', { title: anime.node.title }),
[ [
{ text: 'Cancel', style: 'cancel' }, { text: t('common.cancel'), style: 'cancel' },
{ {
text: 'Remove', text: t('common.delete'),
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
setIsRemoving(true); setIsRemoving(true);
try { try {
await MalApiService.removeFromList(anime.node.id); 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(); onUpdateSuccess();
onClose(); onClose();
} catch (error) { } catch (error) {
showError('Remove Failed', 'Could not remove from MAL'); showError(t('mal.remove_failed'), t('mal.remove_failed_msg'));
} finally { } finally {
setIsRemoving(false); setIsRemoving(false);
} }
@ -100,11 +102,11 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
}; };
const statusOptions: { label: string; value: MalListStatus }[] = [ const statusOptions: { label: string; value: MalListStatus }[] = [
{ label: 'Watching', value: 'watching' }, { label: t('mal.watching'), value: 'watching' },
{ label: 'Completed', value: 'completed' }, { label: t('mal.completed'), value: 'completed' },
{ label: 'On Hold', value: 'on_hold' }, { label: t('mal.on_hold'), value: 'on_hold' },
{ label: 'Dropped', value: 'dropped' }, { label: t('mal.dropped'), value: 'dropped' },
{ label: 'Plan to Watch', value: 'plan_to_watch' }, { label: t('mal.plan_to_watch'), value: 'plan_to_watch' },
]; ];
return ( return (
@ -122,7 +124,7 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
<View style={[styles.modalContent, { backgroundColor: currentTheme.colors.darkGray || '#1A1A1A' }]}> <View style={[styles.modalContent, { backgroundColor: currentTheme.colors.darkGray || '#1A1A1A' }]}>
<View style={styles.header}> <View style={styles.header}>
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}> <Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>
Edit {anime.node.title} {t('mal.edit_title', { title: anime.node.title })}
</Text> </Text>
<TouchableOpacity onPress={onClose}> <TouchableOpacity onPress={onClose}>
<MaterialIcons name="close" size={24} color={currentTheme.colors.mediumEmphasis} /> <MaterialIcons name="close" size={24} color={currentTheme.colors.mediumEmphasis} />

View file

@ -161,7 +161,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
}, [onClose, resumeTrailer]); }, [onClose, resumeTrailer]);
const handleTrailerError = useCallback(() => { const handleTrailerError = useCallback(() => {
setError('Failed to play trailer'); setError(t('trailers.error_play'));
setIsPlaying(false); setIsPlaying(false);
}, []); }, []);
@ -171,7 +171,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
if (isUnsupportedIosMediaFormat(error)) { if (isUnsupportedIosMediaFormat(error)) {
logger.error('TrailerModal', 'Unsupported iOS trailer format:', 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); setLoading(false);
setIsPlaying(false); setIsPlaying(false);
return; return;
@ -187,7 +187,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
} }
logger.error('TrailerModal', 'Video error after retries:', error); logger.error('TrailerModal', 'Video error after retries:', error);
setError('Unable to play trailer. Please try again.'); setError(t('trailers.error_retry'));
setLoading(false); setLoading(false);
}, [retryCount, loadTrailer, trailer?.key]); }, [retryCount, loadTrailer, trailer?.key]);

View file

@ -176,7 +176,12 @@
"added_to_collection": "Added to Collection", "added_to_collection": "Added to Collection",
"added_to_collection_desc": "Added to your Trakt collection", "added_to_collection_desc": "Added to your Trakt collection",
"removed_from_collection": "Removed from 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": { "metadata": {
"unable_to_load": "Unable to Load Content", "unable_to_load": "Unable to Load Content",
@ -275,7 +280,9 @@
"removed_from_collection_hero": "Removed from Collection", "removed_from_collection_hero": "Removed from Collection",
"removed_from_collection_desc_hero": "Removed from your Trakt collection", "removed_from_collection_desc_hero": "Removed from your Trakt collection",
"mark_as_watched": "Mark as Watched", "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": { "cast": {
"biography": "Biography", "biography": "Biography",
@ -334,7 +341,10 @@
"unavailable": "Trailer Unavailable", "unavailable": "Trailer Unavailable",
"unavailable_desc": "This trailer could not be loaded at this time. Please try again later.", "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.", "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": { "catalog": {
"no_content_found": "No content found", "no_content_found": "No content found",
@ -360,7 +370,9 @@
"still_fetching": "Still fetching streams…", "still_fetching": "Still fetching streams…",
"no_streams_available": "No streams available", "no_streams_available": "No streams available",
"starting_best_stream": "Starting best stream...", "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": { "player_ui": {
"via": "via {{name}}", "via": "via {{name}}",
@ -469,13 +481,13 @@
"profanity": "Profanity", "profanity": "Profanity",
"alcohol": "Alcohol/Drugs", "alcohol": "Alcohol/Drugs",
"frightening": "Frightening" "frightening": "Frightening"
}, },
"severity": { "severity": {
"severe": "Severe", "severe": "Severe",
"moderate": "Moderate", "moderate": "Moderate",
"mild": "Mild", "mild": "Mild",
"none": "None" "none": "None"
} }
}, },
"addons": { "addons": {
"title": "Addons", "title": "Addons",
@ -542,7 +554,12 @@
"sign_out_error": "Failed to sign out of Trakt.", "sign_out_error": "Failed to sign out of Trakt.",
"sync_complete_title": "Sync Complete", "sync_complete_title": "Sync Complete",
"sync_success_msg": "Successfully synced your watch progress with Trakt.", "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": { "simkl": {
"title": "Simkl Settings", "title": "Simkl Settings",
@ -707,7 +724,9 @@
"media": "MEDIA", "media": "MEDIA",
"notifications": "NOTIFICATIONS", "notifications": "NOTIFICATIONS",
"testing": "TESTING", "testing": "TESTING",
"danger_zone": "DANGER ZONE" "danger_zone": "DANGER ZONE",
"data": "DATA",
"general": "GENERAL"
}, },
"items": { "items": {
"legal": "Legal & Disclaimer", "legal": "Legal & Disclaimer",
@ -840,7 +859,18 @@
"env_warning": "Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync." "env_warning": "Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync."
}, },
"connection": "Connection" "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": { "privacy": {
"title": "Privacy & Data", "title": "Privacy & Data",
@ -915,7 +945,8 @@
"confirm_remove_title": "Remove API Key", "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.", "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", "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": { "catalog_settings": {
"title": "Catalogs", "title": "Catalogs",
@ -1378,7 +1409,20 @@
"got_it": "Got it!", "got_it": "Got it!",
"repo_format_hint": "Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch", "repo_format_hint": "Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch",
"cancel": "Cancel", "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": { "theme": {
"title": "App Themes", "title": "App Themes",
@ -1504,5 +1548,84 @@
"provider_logs": "Provider Logs", "provider_logs": "Provider Logs",
"no_logs_captured": "No logs captured." "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"
} }
} }

View file

@ -176,7 +176,12 @@
"added_to_collection": "Ajouté à la collection", "added_to_collection": "Ajouté à la collection",
"added_to_collection_desc": "Ajouté à votre collection Trakt", "added_to_collection_desc": "Ajouté à votre collection Trakt",
"removed_from_collection": "Retiré de la collection", "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": { "metadata": {
"unable_to_load": "Impossible de charger le contenu", "unable_to_load": "Impossible de charger le contenu",
@ -275,7 +280,9 @@
"removed_from_collection_hero": "Retiré de la collection", "removed_from_collection_hero": "Retiré de la collection",
"removed_from_collection_desc_hero": "Retiré de votre collection Trakt", "removed_from_collection_desc_hero": "Retiré de votre collection Trakt",
"mark_as_watched": "Marquer comme vu", "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": { "cast": {
"biography": "Biographie", "biography": "Biographie",
@ -334,7 +341,10 @@
"unavailable": "Bande-annonce indisponible", "unavailable": "Bande-annonce indisponible",
"unavailable_desc": "Cette bande-annonce n'a pas pu être chargée pour le moment. Veuillez réessayer plus tard.", "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.", "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": { "catalog": {
"no_content_found": "Aucun contenu trouvé", "no_content_found": "Aucun contenu trouvé",
@ -360,7 +370,9 @@
"still_fetching": "Récupération des flux en cours...", "still_fetching": "Récupération des flux en cours...",
"no_streams_available": "Aucun flux disponible", "no_streams_available": "Aucun flux disponible",
"starting_best_stream": "Démarrage du meilleur flux...", "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": { "player_ui": {
"via": "via {{name}}", "via": "via {{name}}",
@ -419,7 +431,12 @@
"timing_offset": "Décalage temporel (s)", "timing_offset": "Décalage temporel (s)",
"visual_sync": "Synchronisation visuelle", "visual_sync": "Synchronisation visuelle",
"timing_hint": "Avancez (-) ou retardez (+) les sous-titres pour synchroniser si nécessaire.", "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": { "downloads": {
"title": "Téléchargements", "title": "Téléchargements",
@ -522,7 +539,12 @@
"sign_out_error": "Échec de la déconnexion de Trakt.", "sign_out_error": "Échec de la déconnexion de Trakt.",
"sync_complete_title": "Synchronisation terminée", "sync_complete_title": "Synchronisation terminée",
"sync_success_msg": "Votre progression a été synchronisée avec succès avec Trakt.", "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": { "simkl": {
"title": "Paramètres Simkl", "title": "Paramètres Simkl",
@ -685,7 +707,9 @@
"media": "MÉDIA", "media": "MÉDIA",
"notifications": "NOTIFICATIONS", "notifications": "NOTIFICATIONS",
"testing": "TESTS", "testing": "TESTS",
"danger_zone": "ZONE DE DANGER" "danger_zone": "ZONE DE DANGER",
"data": "DONNÉES",
"general": "GÉNÉRAL"
}, },
"items": { "items": {
"legal": "Mentions Légales", "legal": "Mentions Légales",
@ -818,7 +842,20 @@
"env_warning": "Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync." "env_warning": "Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync."
}, },
"connection": "Connection" "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": { "privacy": {
"title": "Confidentialité et Données", "title": "Confidentialité et Données",
@ -893,7 +930,8 @@
"confirm_remove_title": "Supprimer la clé API", "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.", "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", "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": { "catalog_settings": {
"title": "Catalogues", "title": "Catalogues",
@ -1069,7 +1107,9 @@
"error_load": "Échec du chargement des catalogues", "error_load": "Échec du chargement des catalogues",
"movies": "Films", "movies": "Films",
"tv_shows": "Séries TV" "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": { "calendar": {
"title": "Calendrier", "title": "Calendrier",
@ -1354,7 +1394,20 @@
"got_it": "Compris !", "got_it": "Compris !",
"repo_format_hint": "Format : https://raw.githubusercontent.com/user/repo/branch", "repo_format_hint": "Format : https://raw.githubusercontent.com/user/repo/branch",
"cancel": "Annuler", "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": { "theme": {
"title": "Thèmes de l'App", "title": "Thèmes de l'App",
@ -1480,5 +1533,99 @@
"provider_logs": "Logs du Fournisseur", "provider_logs": "Logs du Fournisseur",
"no_logs_captured": "Aucun log capturé." "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"
}
} }
} }

View file

@ -16,6 +16,7 @@ import {
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native'; import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import { BlurView as ExpoBlurView } from 'expo-blur'; import { BlurView as ExpoBlurView } from 'expo-blur';
@ -428,6 +429,7 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress && prev.index === next.index); }, (prev, next) => prev.text === next.text && prev.onPress === next.onPress && prev.index === next.index);
const AIChatScreen: React.FC = () => { const AIChatScreen: React.FC = () => {
const { t } = useTranslation();
// CustomAlert state // CustomAlert state
const [alertVisible, setAlertVisible] = useState(false); const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState(''); const [alertTitle, setAlertTitle] = useState('');
@ -597,7 +599,7 @@ const AIChatScreen: React.FC = () => {
} }
} catch (error) { } catch (error) {
if (__DEV__) console.error('Error loading context:', 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 { } finally {
setIsLoadingContext(false); setIsLoadingContext(false);
{/* CustomAlert at root */ } {/* CustomAlert at root */ }

View file

@ -9,6 +9,7 @@ import { useNavigation, useRoute } from '@react-navigation/native';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { useToast } from '../contexts/ToastContext'; import { useToast } from '../contexts/ToastContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__'; const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__';
const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24'] as const; 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 safeTopInset = Math.max(insets.top, Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : 0);
const backButtonTop = safeTopInset + 8; const backButtonTop = safeTopInset + 8;
const { showError, showSuccess } = useToast(); const { showError, showSuccess } = useToast();
const { t } = useTranslation();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@ -163,21 +165,21 @@ const AuthScreen: React.FC = () => {
if (!isEmailValid) { if (!isEmailValid) {
const msg = 'Enter a valid email address'; const msg = 'Enter a valid email address';
setError(msg); 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(() => {}); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return; return;
} }
if (!isPasswordValid) { if (!isPasswordValid) {
const msg = 'Password must be at least 6 characters'; const msg = 'Password must be at least 6 characters';
setError(msg); 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(() => {}); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return; return;
} }
if (mode === 'signup' && !passwordsMatch) { if (mode === 'signup' && !passwordsMatch) {
const msg = 'Passwords do not match'; const msg = 'Passwords do not match';
setError(msg); 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(() => {}); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return; return;
} }
@ -197,11 +199,11 @@ const AuthScreen: React.FC = () => {
const cleanError = normalizeAuthErrorMessage(err); const cleanError = normalizeAuthErrorMessage(err);
setError(cleanError); setError(cleanError);
showError('Authentication Failed', cleanError); showError(t('auth.auth_failed'), cleanError);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
} else { } else {
const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful'; const msg = mode === 'signin' ? t('auth.login_success') : t('auth.signup_success');
showSuccess('Success', msg); showSuccess(t('common.success'), msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
// Navigate to main tabs after successful authentication // Navigate to main tabs after successful authentication

View file

@ -15,6 +15,7 @@ import FastImage from '@d11/react-native-fast-image';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { TMDBService } from '../services/tmdbService'; import { TMDBService } from '../services/tmdbService';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { useTranslation } from 'react-i18next';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@ -40,6 +41,7 @@ const BackdropGalleryScreen: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { tmdbId, type, title } = route.params as RouteParams; const { tmdbId, type, title } = route.params as RouteParams;
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { t } = useTranslation();
const { settings } = useSettings(); const { settings } = useSettings();
const [backdrops, setBackdrops] = useState<BackdropItem[]>([]); const [backdrops, setBackdrops] = useState<BackdropItem[]>([]);
@ -75,10 +77,10 @@ const BackdropGalleryScreen: React.FC = () => {
if (images && images.backdrops && images.backdrops.length > 0) { if (images && images.backdrops && images.backdrops.length > 0) {
setBackdrops(images.backdrops); setBackdrops(images.backdrops);
} else { } else {
setError('No backdrops found'); setError(t('metadata.no_backdrops'));
} }
} catch (err) { } catch (err) {
setError('Failed to load backdrops'); setError(t('metadata.error_backdrops'));
console.error('Backdrop fetch error:', err); console.error('Backdrop fetch error:', err);
} finally { } finally {
setLoading(false); setLoading(false);

View file

@ -161,8 +161,8 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
// Fallback: show alert with Discord info // Fallback: show alert with Discord info
Alert.alert( Alert.alert(
mention.name, mention.name,
`Discord: @${mention.username}\n\nDo you want to open Discord and search for this user?`, t('mal.discord_open_prompt', { username: mention.username }),
[{ text: 'OK' }] [{ text: t('common.ok') }]
); );
} }
}); });

View file

@ -464,7 +464,7 @@ const LibraryScreen = () => {
// Sync Trakt watchlist to local library // Sync Trakt watchlist to local library
const syncTraktWatchlistToLibrary = useCallback(async () => { const syncTraktWatchlistToLibrary = useCallback(async () => {
if (!traktAuthenticated) { if (!traktAuthenticated) {
showError('Sync Failed', 'Please connect to Trakt first'); showError(t('library.sync_failed'), t('library.sync_connect_first'));
return; return;
} }
@ -667,11 +667,11 @@ const LibraryScreen = () => {
showInfo('Sync Complete', message); showInfo('Sync Complete', message);
logger.log(`[LibraryScreen] Sync complete: ${message}`); logger.log(`[LibraryScreen] Sync complete: ${message}`);
} else { } else {
showInfo('Sync Complete', 'Library is up to date'); showInfo(t('library.sync_complete'), t('library.sync_up_to_date'));
} }
} catch (error) { } catch (error) {
logger.error('[LibraryScreen] Sync failed:', 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 { } finally {
setIsSyncing(false); setIsSyncing(false);
} }

View file

@ -98,24 +98,24 @@ const MalSettingsScreen: React.FC = () => {
const result = await MalAuth.login(); const result = await MalAuth.login();
if (result === true) { if (result === true) {
await checkAuthStatus(); await checkAuthStatus();
openAlert('Success', 'Connected to MyAnimeList'); openAlert(t('common.success'), t('mal.connected'));
} else { } else {
const errorMessage = typeof result === 'string' ? result : 'Failed to connect to MyAnimeList'; const errorMessage = typeof result === 'string' ? result : t('mal.error_connect');
openAlert('Error', errorMessage); openAlert(t('common.error'), errorMessage);
} }
} catch (e: any) { } catch (e: any) {
console.error(e); 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 { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
const handleSignOut = () => { const handleSignOut = () => {
openAlert('Sign Out', 'Are you sure you want to disconnect?', [ openAlert(t('mal.sign_out'), t('mal.sign_out_confirm'), [
{ label: 'Cancel', onPress: () => setAlertVisible(false) }, { label: t('common.cancel'), onPress: () => setAlertVisible(false) },
{ {
label: 'Sign Out', label: t('mal.sign_out'),
onPress: () => { onPress: () => {
MalAuth.clearToken(); MalAuth.clearToken();
setIsAuthenticated(false); setIsAuthenticated(false);
@ -168,13 +168,13 @@ const MalSettingsScreen: React.FC = () => {
color={currentTheme.colors.highEmphasis} color={currentTheme.colors.highEmphasis}
/> />
<Text style={[styles.backText, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.backText, { color: currentTheme.colors.highEmphasis }]}>
Settings {t('common.settings')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
MyAnimeList {t('mal.title')}
</Text> </Text>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}> <ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
@ -232,47 +232,47 @@ const MalSettingsScreen: React.FC = () => {
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}> <Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
{userProfile.anime_statistics.num_items} {userProfile.anime_statistics.num_items}
</Text> </Text>
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Total</Text> <Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('mal.stat_total')}</Text>
</View> </View>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}> <Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
{userProfile.anime_statistics.num_days_watched.toFixed(1)} {userProfile.anime_statistics.num_days_watched.toFixed(1)}
</Text> </Text>
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Days</Text> <Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('mal.stat_days')}</Text>
</View> </View>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}> <Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
{userProfile.anime_statistics.mean_score.toFixed(1)} {userProfile.anime_statistics.mean_score.toFixed(1)}
</Text> </Text>
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Mean</Text> <Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('mal.stat_mean')}</Text>
</View> </View>
</View> </View>
<View style={[styles.statGrid, { borderColor: currentTheme.colors.border }]}> <View style={[styles.statGrid, { borderColor: currentTheme.colors.border }]}>
<View style={styles.statGridItem}> <View style={styles.statGridItem}>
<View style={[styles.statusDot, { backgroundColor: '#2DB039' }]} /> <View style={[styles.statusDot, { backgroundColor: '#2DB039' }]} />
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Watching</Text> <Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>{t('mal.watching')}</Text>
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
{userProfile.anime_statistics.num_items_watching} {userProfile.anime_statistics.num_items_watching}
</Text> </Text>
</View> </View>
<View style={styles.statGridItem}> <View style={styles.statGridItem}>
<View style={[styles.statusDot, { backgroundColor: '#26448F' }]} /> <View style={[styles.statusDot, { backgroundColor: '#26448F' }]} />
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Completed</Text> <Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>{t('mal.completed')}</Text>
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
{userProfile.anime_statistics.num_items_completed} {userProfile.anime_statistics.num_items_completed}
</Text> </Text>
</View> </View>
<View style={styles.statGridItem}> <View style={styles.statGridItem}>
<View style={[styles.statusDot, { backgroundColor: '#F9D457' }]} /> <View style={[styles.statusDot, { backgroundColor: '#F9D457' }]} />
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>On Hold</Text> <Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>{t('mal.on_hold')}</Text>
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
{userProfile.anime_statistics.num_items_on_hold} {userProfile.anime_statistics.num_items_on_hold}
</Text> </Text>
</View> </View>
<View style={styles.statGridItem}> <View style={styles.statGridItem}>
<View style={[styles.statusDot, { backgroundColor: '#A12F31' }]} /> <View style={[styles.statusDot, { backgroundColor: '#A12F31' }]} />
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Dropped</Text> <Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>{t('mal.dropped')}</Text>
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
{userProfile.anime_statistics.num_items_dropped} {userProfile.anime_statistics.num_items_dropped}
</Text> </Text>
@ -289,26 +289,26 @@ const MalSettingsScreen: React.FC = () => {
try { try {
const synced = await MalSync.syncMalToLibrary(); const synced = await MalSync.syncMalToLibrary();
if (synced) { if (synced) {
openAlert('Sync Complete', 'MAL data has been refreshed.'); openAlert(t('mal.sync_complete'), t('mal.sync_complete_msg'));
} else { } else {
openAlert('Sync Failed', 'Could not refresh MAL data.'); openAlert(t('mal.sync_failed'), t('mal.sync_failed_msg'));
} }
} catch { } catch {
openAlert('Sync Failed', 'Could not refresh MAL data.'); openAlert(t('mal.sync_failed'), t('mal.sync_failed_msg'));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}} }}
> >
<MaterialIcons name="sync" size={18} color="white" style={{ marginRight: 6 }} /> <MaterialIcons name="sync" size={18} color="white" style={{ marginRight: 6 }} />
<Text style={styles.buttonText}>Sync</Text> <Text style={styles.buttonText}>{t('mal.sync_button')}</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.smallButton, { backgroundColor: currentTheme.colors.error, width: 100 }]} style={[styles.smallButton, { backgroundColor: currentTheme.colors.error, width: 100 }]}
onPress={handleSignOut} onPress={handleSignOut}
> >
<Text style={styles.buttonText}>Sign Out</Text> <Text style={styles.buttonText}>{t('mal.sign_out')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@ -320,16 +320,16 @@ const MalSettingsScreen: React.FC = () => {
resizeMode="contain" resizeMode="contain"
/> />
<Text style={[styles.signInTitle, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.signInTitle, { color: currentTheme.colors.highEmphasis }]}>
Connect MyAnimeList {t('mal.connect_title')}
</Text> </Text>
<Text style={[styles.signInDescription, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.signInDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Sync your watch history and manage your anime list. {t('mal.connect_desc')}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]} style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
onPress={handleSignIn} onPress={handleSignIn}
> >
<Text style={styles.buttonText}>Sign In with MAL</Text> <Text style={styles.buttonText}>{t('mal.sign_in')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
@ -339,17 +339,17 @@ const MalSettingsScreen: React.FC = () => {
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation2 }]}> <View style={[styles.card, { backgroundColor: currentTheme.colors.elevation2 }]}>
<View style={styles.settingsSection}> <View style={styles.settingsSection}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
Sync Settings {t('mal.sync_settings')}
</Text> </Text>
<View style={styles.settingItem}> <View style={styles.settingItem}>
<View style={styles.settingContent}> <View style={styles.settingContent}>
<View style={styles.settingTextContainer}> <View style={styles.settingTextContainer}>
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
Enable MAL Sync {t('mal.enable_sync')}
</Text> </Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Global switch to enable or disable all MyAnimeList features. {t('mal.enable_sync_desc')}
</Text> </Text>
</View> </View>
<Switch <Switch
@ -365,10 +365,10 @@ const MalSettingsScreen: React.FC = () => {
<View style={styles.settingContent}> <View style={styles.settingContent}>
<View style={styles.settingTextContainer}> <View style={styles.settingTextContainer}>
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
Auto Episode Update {t('mal.auto_episode')}
</Text> </Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Automatically update your progress on MAL when you finish watching an episode (&gt;=90% completion). {t('mal.auto_episode_desc')}
</Text> </Text>
</View> </View>
<Switch <Switch
@ -384,10 +384,10 @@ const MalSettingsScreen: React.FC = () => {
<View style={styles.settingContent}> <View style={styles.settingContent}>
<View style={styles.settingTextContainer}> <View style={styles.settingTextContainer}>
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
Auto Add Anime {t('mal.auto_add')}
</Text> </Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
If an anime is not in your MAL list, it will be added automatically when you start watching. {t('mal.auto_add_desc')}
</Text> </Text>
</View> </View>
<Switch <Switch
@ -403,10 +403,10 @@ const MalSettingsScreen: React.FC = () => {
<View style={styles.settingContent}> <View style={styles.settingContent}>
<View style={styles.settingTextContainer}> <View style={styles.settingTextContainer}>
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
Auto-Sync to Library {t('mal.auto_sync_library')}
</Text> </Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Automatically add items from your MAL 'Watching' list to your Nuvio Library. {t('mal.auto_sync_library_desc')}
</Text> </Text>
</View> </View>
<Switch <Switch
@ -422,10 +422,10 @@ const MalSettingsScreen: React.FC = () => {
<View style={styles.settingContent}> <View style={styles.settingContent}>
<View style={styles.settingTextContainer}> <View style={styles.settingTextContainer}>
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
Include NSFW Content {t('mal.include_nsfw')}
</Text> </Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Allow NSFW entries to be returned when fetching your MAL list. {t('mal.include_nsfw_desc')}
</Text> </Text>
</View> </View>
<Switch <Switch

View file

@ -917,16 +917,16 @@ const PluginsScreen: React.FC = () => {
// Small delay to ensure UI is ready // Small delay to ensure UI is ready
setTimeout(() => { setTimeout(() => {
openAlert( openAlert(
'Add Repository', t('plugins.add_repo'),
`Do you want to add the repository from:\n${url}`, t('plugins.add_repo_confirm', { url }),
[ [
{ {
label: 'Cancel', label: t('common.cancel'),
onPress: () => { }, onPress: () => { },
style: { color: colors.error } style: { color: colors.error }
}, },
{ {
label: 'Add', label: t('plugins.add'),
onPress: () => { onPress: () => {
handleAddRepository(url); handleAddRepository(url);
} }
@ -950,7 +950,7 @@ const PluginsScreen: React.FC = () => {
) => { ) => {
setAlertTitle(title); setAlertTitle(title);
setAlertMessage(message); setAlertMessage(message);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]);
setAlertVisible(true); setAlertVisible(true);
}; };
@ -1060,7 +1060,7 @@ const PluginsScreen: React.FC = () => {
openAlert(t('plugins.success'), `${enabled ? t('plugins.enabled') : t('plugins.disabled')} ${filteredPlugins.length} extensions`); openAlert(t('plugins.success'), `${enabled ? t('plugins.enabled') : t('plugins.disabled')} ${filteredPlugins.length} extensions`);
} catch (error) { } catch (error) {
logger.error('[PluginSettings] Failed to bulk toggle:', 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 { } finally {
setIsRefreshing(false); setIsRefreshing(false);
} }
@ -1076,7 +1076,7 @@ const PluginsScreen: React.FC = () => {
const inputUrl = validUrlOverride || newRepositoryUrl; const inputUrl = validUrlOverride || newRepositoryUrl;
if (!inputUrl.trim()) { if (!inputUrl.trim()) {
openAlert('Error', 'Please enter a valid repository URL'); openAlert(t('common.error'), t('plugins.error_valid_url'));
return; return;
} }
@ -1085,7 +1085,7 @@ const PluginsScreen: React.FC = () => {
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
openAlert( openAlert(
t('plugins.alert_invalid_url'), 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; 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`); openAlert(t('plugins.success'), `Repository "${repo?.name || t('plugins.unknown')}" ${enabled ? t('plugins.enabled').toLowerCase() : t('plugins.disabled').toLowerCase()} successfully`);
} catch (error) { } catch (error) {
logger.error('[PluginSettings] Failed to toggle repository:', 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 { } finally {
setSwitchingRepository(null); setSwitchingRepository(null);
} }
@ -1179,30 +1179,30 @@ const PluginsScreen: React.FC = () => {
// Special handling for the last repository // Special handling for the last repository
const isLastRepository = repositories.length === 1; 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 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.` ? t('plugins.remove_last_repo_desc', { name: repo.name })
: `Are you sure you want to remove "${repo.name}"? This will also remove all extensions from this repository.`; : t('plugins.remove_repo_desc', { name: repo.name });
openAlert( openAlert(
alertTitle, alertTitleText,
alertMessage, alertMessage,
[ [
{ label: 'Cancel', onPress: () => { } }, { label: t('common.cancel'), onPress: () => { } },
{ {
label: 'Remove', label: t('plugins.remove'),
onPress: async () => { onPress: async () => {
try { try {
await pluginService.removeRepository(repoId); await pluginService.removeRepository(repoId);
await loadRepositories(); await loadRepositories();
await loadPlugins(); await loadPlugins();
const successMessage = isLastRepository const successMessage = isLastRepository
? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.' ? t('plugins.remove_last_repo_success')
: 'Repository removed successfully'; : t('plugins.remove_repo_success');
openAlert('Success', successMessage); openAlert(t('common.success'), successMessage);
} catch (error) { } catch (error) {
logger.error('[PluginSettings] Failed to remove repository:', 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 () => { const handleSaveRepository = async () => {
if (!repositoryUrl.trim()) { if (!repositoryUrl.trim()) {
openAlert('Error', 'Please enter a valid repository URL'); openAlert(t('common.error'), t('plugins.error_valid_url'));
return; return;
} }
@ -1290,8 +1290,8 @@ const PluginsScreen: React.FC = () => {
const url = repositoryUrl.trim(); const url = repositoryUrl.trim();
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
openAlert( openAlert(
'Invalid URL Format', t('plugins.alert_invalid_url'),
'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.error_valid_url_format')
); );
return; return;
} }
@ -1304,7 +1304,7 @@ const PluginsScreen: React.FC = () => {
openAlert(t('plugins.success'), t('plugins.alert_repo_saved')); openAlert(t('plugins.success'), t('plugins.alert_repo_saved'));
} catch (error) { } catch (error) {
logger.error('[PluginSettings] Failed to save repository:', 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 { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -1312,7 +1312,7 @@ const PluginsScreen: React.FC = () => {
const handleRefreshRepository = async () => { const handleRefreshRepository = async () => {
if (!repositoryUrl.trim()) { if (!repositoryUrl.trim()) {
openAlert('Error', 'Please set a repository URL first'); openAlert(t('common.error'), t('plugins.error_set_repo_first'));
return; return;
} }
@ -1331,8 +1331,8 @@ const PluginsScreen: React.FC = () => {
logger.error('[PluginsScreen] Failed to refresh repository:', error); logger.error('[PluginsScreen] Failed to refresh repository:', error);
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
openAlert( openAlert(
'Repository Error', t('plugins.error_repo'),
`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_refresh_repo', { error: errorMessage })
); );
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
@ -1358,7 +1358,7 @@ const PluginsScreen: React.FC = () => {
await loadPlugins(); await loadPlugins();
} catch (error) { } catch (error) {
logger.error('[PluginSettings] Failed to toggle plugin:', 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); setIsRefreshing(false);
} }
}; };
@ -1368,9 +1368,9 @@ const PluginsScreen: React.FC = () => {
t('plugins.clear_all'), t('plugins.clear_all'),
t('plugins.clear_all_desc'), t('plugins.clear_all_desc'),
[ [
{ label: 'Cancel', onPress: () => { } }, { label: t('common.cancel'), onPress: () => { } },
{ {
label: 'Clear', label: t('plugins.clear'),
onPress: async () => { onPress: async () => {
try { try {
await pluginService.clearScrapers(); await pluginService.clearScrapers();
@ -1378,7 +1378,7 @@ const PluginsScreen: React.FC = () => {
openAlert(t('plugins.success'), t('plugins.alert_plugins_cleared')); openAlert(t('plugins.success'), t('plugins.alert_plugins_cleared'));
} catch (error) { } catch (error) {
logger.error('[PluginSettings] Failed to clear plugins:', 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'),
t('plugins.clear_cache_desc'), t('plugins.clear_cache_desc'),
[ [
{ label: 'Cancel', onPress: () => { } }, { label: t('common.cancel'), onPress: () => { } },
{ {
label: 'Clear Cache', label: t('plugins.clear_cache'),
onPress: async () => { onPress: async () => {
try { try {
await pluginService.clearScrapers(); await pluginService.clearScrapers();
@ -1405,7 +1405,7 @@ const PluginsScreen: React.FC = () => {
openAlert(t('plugins.success'), t('plugins.alert_cache_cleared')); openAlert(t('plugins.success'), t('plugins.alert_cache_cleared'));
} catch (error) { } catch (error) {
logger.error('[PluginSettings] Failed to clear cache:', 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); setShowboxSavedToken(showboxUiToken);
openAlert('Saved', 'ShowBox settings updated'); openAlert(t('plugins.showbox_saved'), t('plugins.showbox_saved_msg'));
}} }}
> >
<Text style={styles.buttonText}>{t('plugins.save')}</Text> <Text style={styles.buttonText}>{t('plugins.save')}</Text>

View file

@ -14,6 +14,7 @@ import {
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { useTranslation } from 'react-i18next';
import { useTraktContext } from '../contexts/TraktContext'; import { useTraktContext } from '../contexts/TraktContext';
import { mmkvStorage } from '../services/mmkvStorage'; import { mmkvStorage } from '../services/mmkvStorage';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
@ -33,6 +34,7 @@ const ProfilesScreen: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
const { t } = useTranslation();
const [profiles, setProfiles] = useState<Profile[]>([]); const [profiles, setProfiles] = useState<Profile[]>([]);
const [showAddModal, setShowAddModal] = useState(false); const [showAddModal, setShowAddModal] = useState(false);
@ -76,7 +78,7 @@ const ProfilesScreen: React.FC = () => {
} }
} catch (error) { } catch (error) {
if (__DEV__) console.error('Error loading profiles:', error); if (__DEV__) console.error('Error loading profiles:', error);
openAlert('Error', 'Failed to load profiles'); openAlert(t('common.error'), t('profiles.error_load'));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -102,7 +104,7 @@ const ProfilesScreen: React.FC = () => {
await mmkvStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles)); await mmkvStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles));
} catch (error) { } catch (error) {
if (__DEV__) console.error('Error saving profiles:', 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(() => { const handleAddProfile = useCallback(() => {
if (!newProfileName.trim()) { if (!newProfileName.trim()) {
openAlert('Error', 'Please enter a profile name'); openAlert(t('common.error'), t('profiles.error_empty_name'));
return; return;
} }
@ -150,23 +152,23 @@ const ProfilesScreen: React.FC = () => {
// Prevent deleting the active profile // Prevent deleting the active profile
const isActiveProfile = profiles.find(p => p.id === id)?.isActive; const isActiveProfile = profiles.find(p => p.id === id)?.isActive;
if (isActiveProfile) { if (isActiveProfile) {
openAlert('Error', 'Cannot delete the active profile. Switch to another profile first.'); openAlert(t('common.error'), t('profiles.error_delete_active'));
return; return;
} }
// Prevent deleting the last profile // Prevent deleting the last profile
if (profiles.length <= 1) { if (profiles.length <= 1) {
openAlert('Error', 'Cannot delete the only profile'); openAlert(t('common.error'), t('profiles.error_delete_only'));
return; return;
} }
openAlert( openAlert(
'Delete Profile', t('profiles.delete_title'),
'Are you sure you want to delete this profile? This action cannot be undone.', t('profiles.delete_confirm'),
[ [
{ label: 'Cancel', onPress: () => { } }, { label: t('common.cancel'), onPress: () => { } },
{ {
label: 'Delete', label: t('common.delete'),
onPress: () => { onPress: () => {
const updatedProfiles = profiles.filter(profile => profile.id !== id); const updatedProfiles = profiles.filter(profile => profile.id !== id);
setProfiles(updatedProfiles); setProfiles(updatedProfiles);

View file

@ -186,7 +186,7 @@ const SettingsScreen: React.FC = () => {
) => { ) => {
setAlertTitle(title); setAlertTitle(title);
setAlertMessage(message); setAlertMessage(message);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]);
setAlertVisible(true); setAlertVisible(true);
}; };
@ -334,18 +334,18 @@ const SettingsScreen: React.FC = () => {
const handleClearMDBListCache = () => { const handleClearMDBListCache = () => {
openAlert( openAlert(
'Clear MDBList Cache', t('settings.clear_mdblist_cache_title'),
'Are you sure you want to clear all cached MDBList data? This cannot be undone.', t('settings.clear_mdblist_cache_confirm'),
[ [
{ label: 'Cancel', onPress: () => { } }, { label: t('common.cancel'), onPress: () => { } },
{ {
label: 'Clear', label: t('common.delete'),
onPress: async () => { onPress: async () => {
try { try {
await mmkvStorage.removeItem('mdblist_cache'); await mmkvStorage.removeItem('mdblist_cache');
openAlert('Success', 'MDBList cache has been cleared.'); openAlert(t('common.success'), t('settings.clear_mdblist_cache_success'));
} catch (error) { } 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); if (__DEV__) console.error('Error clearing MDBList cache:', error);
} }
} }
@ -420,8 +420,8 @@ const SettingsScreen: React.FC = () => {
)} )}
{isItemVisible('mal') && ( {isItemVisible('mal') && (
<SettingItem <SettingItem
title="MyAnimeList" title={t('mal.title')}
description="Sync with MyAnimeList" description={t('mal.description')}
customIcon={<Image source={require('../../assets/rating-icons/mal-icon.png')} style={{ width: isTablet ? 24 : 20, height: isTablet ? 24 : 20, borderRadius: 4 }} resizeMode="contain" />} customIcon={<Image source={require('../../assets/rating-icons/mal-icon.png')} style={{ width: isTablet ? 24 : 20, height: isTablet ? 24 : 20, borderRadius: 4 }} resizeMode="contain" />}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('MalSettings')} onPress={() => navigation.navigate('MalSettings')}
@ -438,7 +438,7 @@ const SettingsScreen: React.FC = () => {
case 'appearance': case 'appearance':
return ( return (
<> <>
<SettingsCard title="GENERAL" isTablet={isTablet}> <SettingsCard title={t('settings.sections.general')} isTablet={isTablet}>
<SettingItem <SettingItem
title={t('settings.language')} title={t('settings.language')}
description={t(`settings.${LOCALES.find(l => l.code === i18n.language)?.key}`)} description={t(`settings.${LOCALES.find(l => l.code === i18n.language)?.key}`)}
@ -476,8 +476,8 @@ const SettingsScreen: React.FC = () => {
isTablet={isTablet} isTablet={isTablet}
/> />
<SettingItem <SettingItem
title={'Plugin Tester'} title={t('settings.plugin_tester')}
description={'Run a plugin and inspect logs/streams'} description={t('settings.plugin_tester_desc')}
icon="terminal" icon="terminal"
onPress={() => navigation.navigate('PluginTester')} onPress={() => navigation.navigate('PluginTester')}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
@ -489,9 +489,9 @@ const SettingsScreen: React.FC = () => {
onPress={async () => { onPress={async () => {
try { try {
await mmkvStorage.removeItem('hasCompletedOnboarding'); 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) { } catch (error) {
openAlert('Error', 'Failed to reset onboarding.'); openAlert(t('common.error'), t('settings.reset_onboarding_error'));
} }
}} }}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
@ -503,7 +503,7 @@ const SettingsScreen: React.FC = () => {
icon="refresh-cw" icon="refresh-cw"
onPress={async () => { onPress={async () => {
await campaignService.resetCampaigns(); await campaignService.resetCampaigns();
openAlert('Success', 'Campaign history reset. Restart app to see posters again.'); openAlert(t('common.success'), t('settings.reset_campaigns_success'));
}} }}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
isTablet={isTablet} isTablet={isTablet}
@ -516,15 +516,15 @@ const SettingsScreen: React.FC = () => {
t('settings.clear_data'), t('settings.clear_data'),
t('settings.clear_data_desc'), t('settings.clear_data_desc'),
[ [
{ label: 'Cancel', onPress: () => { } }, { label: t('common.cancel'), onPress: () => { } },
{ {
label: 'Clear', label: t('common.delete'),
onPress: async () => { onPress: async () => {
try { try {
await mmkvStorage.clear(); await mmkvStorage.clear();
openAlert('Success', 'All data cleared. Please restart the app.'); openAlert(t('common.success'), t('settings.clear_data_success'));
} catch (error) { } 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 = () => {
<SettingsCard title={t('settings.account').toUpperCase()}> <SettingsCard title={t('settings.account').toUpperCase()}>
{showCloudSyncItem && ( {showCloudSyncItem && (
<SettingItem <SettingItem
title="Nuvio Sync" title={t('settings.cloud_sync.title')}
description="Sync data across your Nuvio devices" description={t('settings.cloud_sync.description')}
customIcon={ customIcon={
<FastImage <FastImage
source={require('../../assets/nuvio-sync-icon-og.png')} source={require('../../assets/nuvio-sync-icon-og.png')}
@ -753,8 +753,8 @@ const SettingsScreen: React.FC = () => {
)} )}
{isItemVisible('mal') && ( {isItemVisible('mal') && (
<SettingItem <SettingItem
title="MyAnimeList" title={t('mal.title')}
description="Sync with MyAnimeList" description={t('mal.description')}
customIcon={<Image source={require('../../assets/rating-icons/mal-icon.png')} style={{ width: 20, height: 20, borderRadius: 4 }} resizeMode="contain" />} customIcon={<Image source={require('../../assets/rating-icons/mal-icon.png')} style={{ width: 20, height: 20, borderRadius: 4 }} resizeMode="contain" />}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('MalSettings')} onPress={() => navigation.navigate('MalSettings')}
@ -771,7 +771,7 @@ const SettingsScreen: React.FC = () => {
(settingsConfig?.categories?.['integrations']?.visible !== false) || (settingsConfig?.categories?.['integrations']?.visible !== false) ||
(settingsConfig?.categories?.['playback']?.visible !== false) (settingsConfig?.categories?.['playback']?.visible !== false)
) && ( ) && (
<SettingsCard title="GENERAL"> <SettingsCard title={t('settings.sections.general')}>
<SettingItem <SettingItem
title={t('settings.language')} title={t('settings.language')}
description={t(`settings.${LOCALES.find(l => l.code === i18n.language)?.key}`) description={t(`settings.${LOCALES.find(l => l.code === i18n.language)?.key}`)
@ -825,11 +825,11 @@ const SettingsScreen: React.FC = () => {
(settingsConfig?.categories?.['backup']?.visible !== false) || (settingsConfig?.categories?.['backup']?.visible !== false) ||
(settingsConfig?.categories?.['updates']?.visible !== false) (settingsConfig?.categories?.['updates']?.visible !== false)
) && ( ) && (
<SettingsCard title="DATA"> <SettingsCard title={t('settings.sections.data')}>
{(settingsConfig?.categories?.['backup']?.visible !== false) && ( {(settingsConfig?.categories?.['backup']?.visible !== false) && (
<SettingItem <SettingItem
title={t('settings.backup_restore')} title={t('settings.backup_restore')}
description="Create and restore app backups" description={t('settings.backup_restore_desc')}
icon="archive" icon="archive"
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('Backup')} onPress={() => navigation.navigate('Backup')}

View file

@ -233,7 +233,7 @@ const TraktSettingsScreen: React.FC = () => {
const handleSignIn = () => { const handleSignIn = () => {
if (isSimklAuthenticated) { 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; return;
} }
promptAsync(); // Trigger the authentication flow promptAsync(); // Trigger the authentication flow
@ -289,18 +289,18 @@ const TraktSettingsScreen: React.FC = () => {
// Show confirmation // Show confirmation
const modeLabel = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode)?.label || mode; const modeLabel = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode)?.label || mode;
openAlert( openAlert(
'Library Sync Mode Updated', t('trakt.library_sync_updated'),
`Trakt library sync is now set to: ${modeLabel}` t('trakt.library_sync_updated_msg', { mode: modeLabel })
); );
} catch (error) { } catch (error) {
logger.error('[TraktSettingsScreen] Failed to save library sync mode:', 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 getLibrarySyncModeLabel = (mode: string): string => {
const option = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode); const option = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode);
return option?.label || 'Off'; return option?.label || t('common.disable');
}; };
return ( return (

View file

@ -44,38 +44,38 @@ const DeveloperSettingsScreen: React.FC = () => {
) => { ) => {
setAlertTitle(title); setAlertTitle(title);
setAlertMessage(message); setAlertMessage(message);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]);
setAlertVisible(true); setAlertVisible(true);
}; };
const handleResetOnboarding = async () => { const handleResetOnboarding = async () => {
try { try {
await mmkvStorage.removeItem('hasCompletedOnboarding'); 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) { } catch (error) {
openAlert('Error', 'Failed to reset onboarding.'); openAlert(t('common.error'), t('settings.reset_onboarding_error'));
} }
}; };
const handleResetCampaigns = async () => { const handleResetCampaigns = async () => {
await campaignService.resetCampaigns(); 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 = () => { const handleClearAllData = () => {
openAlert( openAlert(
'Clear All Data', t('settings.items.clear_all_data'),
'This will reset all settings and clear all cached data. Are you sure?', t('settings.clear_data_desc'),
[ [
{ label: 'Cancel', onPress: () => { } }, { label: t('common.cancel'), onPress: () => { } },
{ {
label: 'Clear', label: t('common.delete'),
onPress: async () => { onPress: async () => {
try { try {
await mmkvStorage.clear(); await mmkvStorage.clear();
openAlert('Success', 'All data cleared. Please restart the app.'); openAlert(t('common.success'), t('settings.clear_data_success'));
} catch (error) { } catch (error) {
openAlert('Error', 'Failed to clear data.'); openAlert(t('common.error'), t('settings.clear_data_error'));
} }
} }
} }

View file

@ -9,6 +9,7 @@ import { useMetadataAssets } from '../../hooks/useMetadataAssets';
import { useSettings } from '../../hooks/useSettings'; import { useSettings } from '../../hooks/useSettings';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
import { useTrailer } from '../../contexts/TrailerContext'; import { useTrailer } from '../../contexts/TrailerContext';
import { useTranslation } from 'react-i18next';
import { useToast } from '../../contexts/ToastContext'; import { useToast } from '../../contexts/ToastContext';
import { useDominantColor } from '../../hooks/useDominantColor'; import { useDominantColor } from '../../hooks/useDominantColor';
import { Stream } from '../../types/metadata'; import { Stream } from '../../types/metadata';
@ -49,6 +50,7 @@ export const useStreamsScreen = () => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { colors } = currentTheme; const { colors } = currentTheme;
const { pauseTrailer, resumeTrailer } = useTrailer(); const { pauseTrailer, resumeTrailer } = useTrailer();
const { t } = useTranslation();
const { showSuccess, showInfo } = useToast(); const { showSuccess, showInfo } = useToast();
// Dimension tracking // Dimension tracking
@ -465,7 +467,7 @@ export const useStreamsScreen = () => {
// Block magnet links // Block magnet links
if (typeof stream.url === 'string' && stream.url.startsWith('magnet:')) { 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; return;
} }