diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index d114853e..9d9ff4a2 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -139,6 +139,333 @@
"external_first_desc": "Prefer addon subtitles, then embedded",
"any_available": "Any Available",
"any_available_desc": "Use first available subtitle track"
- }
+ },
+ "trakt": "Trakt",
+ "clear_data": "Clear All Data",
+ "clear_data_desc": "This will reset all settings and clear all cached data. Are you sure?",
+ "app_updates": "App Updates",
+ "about_nuvio": "About Nuvio"
+ },
+ "ai_settings": {
+ "title": "AI Assistant",
+ "info_title": "AI-Powered Chat",
+ "info_desc": "Ask questions about any movie or TV show episode using advanced AI. Get insights about plot, characters, themes, trivia, and more - all powered by comprehensive TMDB data.",
+ "feature_1": "Episode-specific context and analysis",
+ "feature_2": "Plot explanations and character insights",
+ "feature_3": "Behind-the-scenes trivia and facts",
+ "feature_4": "Your own free OpenRouter API key",
+ "api_key_section": "OPENROUTER API KEY",
+ "api_key_label": "API Key",
+ "api_key_desc": "Enter your OpenRouter API key to enable AI chat features",
+ "save_api_key": "Save API Key",
+ "saving": "Saving...",
+ "update": "Update",
+ "remove": "Remove",
+ "get_free_key": "Get Free API Key from OpenRouter",
+ "enable_chat": "Enable AI Chat",
+ "enable_chat_desc": "When enabled, the Ask AI button will appear on content pages.",
+ "chat_enabled": "AI Chat Enabled",
+ "chat_enabled_desc": "You can now ask questions about movies and TV shows. Look for the \"Ask AI\" button on content pages!",
+ "how_it_works": "How it works",
+ "how_it_works_desc": "• OpenRouter provides access to multiple AI models\n• Your API key stays private and secure\n• Free tier includes generous usage limits\n• Chat with context about specific episodes/movies\n• Get detailed analysis and explanations",
+ "error_invalid_key": "Please enter a valid API key",
+ "error_key_format": "OpenRouter API keys should start with \"sk-or-\"",
+ "success_saved": "OpenRouter API key saved successfully!",
+ "error_save": "Failed to save 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.",
+ "success_removed": "API key removed successfully",
+ "error_remove": "Failed to remove API key"
+ },
+ "catalog_settings": {
+ "title": "Catalogs",
+ "layout_phone": "LAYOUT CATALOGSCREEN (PHONE)",
+ "posters_per_row": "Posters per row",
+ "auto": "Auto",
+ "show_titles": "Show Poster Titles",
+ "show_titles_desc": "Display title text below each poster",
+ "phone_only_hint": "Applies to phones only. Tablets keep adaptive layout.",
+ "catalogs_group": "Catalogs",
+ "enabled_count": "{{enabled}} of {{total}} enabled",
+ "rename_hint": "Long-press a catalog to rename",
+ "rename_modal_title": "Rename Catalog",
+ "rename_placeholder": "Enter new catalog name",
+ "error_save_name": "Could not save the custom name."
+ },
+ "continue_watching_settings": {
+ "title": "Continue Watching",
+ "playback_behavior": "PLAYBACK BEHAVIOR",
+ "use_cached": "Use Cached Streams",
+ "use_cached_desc": "When enabled, clicking Continue Watching items will open the player directly using previously played streams. When disabled, opens a content screen instead.",
+ "open_metadata": "Open Metadata Screen",
+ "open_metadata_desc": "When cached streams are disabled, open the Metadata screen instead of the Streams screen. This shows content details and allows manual stream selection.",
+ "card_appearance": "CARD APPEARANCE",
+ "card_style": "Card Style",
+ "card_style_desc": "Choose how Continue Watching items appear on the home screen",
+ "wide": "Wide",
+ "poster": "Poster",
+ "cache_settings": "CACHE SETTINGS",
+ "cache_duration": "Stream Cache Duration",
+ "cache_duration_desc": "How long to keep cached stream links before they expire",
+ "important_note": "Important Note",
+ "important_note_text": "Not all stream links may remain active for the full cache duration. Longer cache times may result in expired links. If a cached link fails, the app will fall back to fetching fresh streams.",
+ "how_it_works": "How it works",
+ "how_it_works_cached": "• Streams are cached for your selected duration after playing\n• Cached streams are validated before use\n• If cache is invalid or expired, falls back to content screen\n• \"Use Cached Streams\" controls direct player vs screen navigation\n• \"Open Metadata Screen\" appears only when cached streams are disabled",
+ "how_it_works_uncached": "• When cached streams are disabled, clicking Continue Watching items opens content screens\n• \"Open Metadata Screen\" option controls which screen to open\n• Metadata screen shows content details and allows manual stream selection\n• Streams screen shows available streams for immediate playback",
+ "changes_saved": "Changes saved",
+ "min": "min",
+ "hour": "hour",
+ "hours": "hours"
+ },
+ "contributors": {
+ "title": "Contributors",
+ "special_mentions": "Special Mentions",
+ "tab_contributors": "Contributors",
+ "tab_special": "Special Mentions",
+ "manager_role": "Community Manager",
+ "manager_desc": "Manages the Discord & Reddit communities for Nuvio",
+ "sponsor_role": "Server Sponsor",
+ "sponsor_desc": "Sponsored the server infrastructure for Nuvio",
+ "mod_role": "Discord Mod",
+ "mod_desc": "Helps moderate the Nuvio Discord community",
+ "loading": "Loading...",
+ "discord_user": "Discord User",
+ "contributions": "contributions",
+ "gratitude_title": "We're grateful for every contribution",
+ "gratitude_desc": "Each line of code, bug report, and suggestion helps make Nuvio better for everyone",
+ "special_thanks_title": "Special Thanks",
+ "special_thanks_desc": "These amazing people help keep the Nuvio community running and the servers online",
+ "error_rate_limit": "GitHub API rate limit exceeded. Please try again later or pull to refresh.",
+ "error_failed": "Failed to load contributors. Please check your internet connection.",
+ "retry": "Try Again",
+ "no_contributors": "No contributors found",
+ "loading_contributors": "Loading contributors..."
+ },
+ "debrid": {
+ "title": "Debrid Integration",
+ "description_torbox": "Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.",
+ "description_torrentio": "Configure Torrentio to get torrent streams for movies and TV shows. A debrid service is required to stream content.",
+ "tab_torbox": "TorBox",
+ "tab_torrentio": "Torrentio",
+ "status_connected": "Connected",
+ "status_disconnected": "Disconnected",
+ "enable_addon": "Enable Addon",
+ "disconnect_button": "Disconnect & Remove",
+ "disconnect_loading": "Disconnecting...",
+ "account_info": "Account Information",
+ "plan": "Plan",
+ "plan_free": "Free",
+ "plan_essential": "Essential ($3/mo)",
+ "plan_pro": "Pro ($10/mo)",
+ "plan_standard": "Standard ($5/mo)",
+ "plan_unknown": "Unknown",
+ "expires": "Expires",
+ "downloaded": "Downloaded",
+ "status_active": "Active",
+ "connected_title": "✓ Connected to TorBox",
+ "connected_desc": "Your TorBox addon is active and providing premium streams.",
+ "configure_title": "Configure Addon",
+ "configure_desc": "Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.",
+ "open_settings": "Open Settings",
+ "what_is_debrid": "What is a Debrid Service?",
+ "enter_api_key": "Enter your API Key",
+ "connect_button": "Connect & Install",
+ "connecting": "Connecting...",
+ "unlock_speeds_title": "Unlock Premium Speeds",
+ "unlock_speeds_desc": "Get a Torbox subscription to access cached high-quality streams with zero buffering.",
+ "get_subscription": "Get Subscription",
+ "powered_by": "Powered by",
+ "disclaimer_torbox": "Nuvio is not affiliated with Torbox in any way.",
+ "disclaimer_torrentio": "Nuvio is not affiliated with Torrentio in any way.",
+ "installed_badge": "✓ INSTALLED",
+ "promo_title": "⚡ Need a Debrid Service?",
+ "promo_desc": "Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.",
+ "promo_button": "Get TorBox Subscription",
+ "service_label": "Debrid Service *",
+ "api_key_label": "API Key *",
+ "sorting_label": "Sorting",
+ "exclude_qualities": "Exclude Qualities",
+ "priority_languages": "Priority Languages",
+ "max_results": "Max Results",
+ "additional_options": "Additional Options",
+ "no_download_links": "Don't show download links",
+ "no_debrid_catalog": "Don't show debrid catalog",
+ "install_button": "Install Torrentio",
+ "installing": "Installing...",
+ "update_button": "Update Configuration",
+ "updating": "Updating...",
+ "remove_button": "Remove Torrentio",
+ "error_api_required": "API Key Required",
+ "error_api_required_desc": "Please enter your debrid service API key to install Torrentio.",
+ "success_installed": "Torrentio addon installed successfully!",
+ "success_removed": "Torrentio addon removed successfully",
+ "alert_disconnect_title": "Disconnect Torbox",
+ "alert_disconnect_msg": "Are you sure you want to disconnect Torbox? This will remove the addon and clear your saved API key."
+ },
+ "home_screen": {
+ "title": "Home Screen Settings",
+ "changes_applied": "Changes Applied",
+ "display_options": "DISPLAY OPTIONS",
+ "show_hero": "Show Hero Section",
+ "show_hero_desc": "Featured content at the top",
+ "show_this_week": "Show This Week Section",
+ "show_this_week_desc": "New episodes from current week",
+ "select_catalogs": "Select Catalogs",
+ "all_catalogs": "All catalogs",
+ "selected": "selected",
+ "hero_layout": "Hero Layout",
+ "layout_legacy": "Legacy",
+ "layout_carousel": "Carousel",
+ "layout_appletv": "Apple TV",
+ "layout_desc": "Full-width banner, swipeable cards, or Apple TV style",
+ "featured_source": "Featured Source",
+ "using_catalogs": "Using Catalogs",
+ "manage_catalogs": "Manage selected catalogs",
+ "dynamic_bg": "Dynamic Hero Background",
+ "dynamic_bg_desc": "Blurred banner behind carousel",
+ "performance_note": "May impact performance on low-end devices.",
+ "posters": "Posters",
+ "show_titles": "Show Titles",
+ "poster_size": "Poster Size",
+ "poster_corners": "Poster Corners",
+ "size_small": "Small",
+ "size_medium": "Medium",
+ "size_large": "Large",
+ "corner_square": "Square",
+ "corner_rounded": "Rounded",
+ "corner_pill": "Pill",
+ "about_title": "ABOUT THESE SETTINGS",
+ "about_desc": "These settings control how content is displayed on your Home screen. Changes are applied immediately without requiring an app restart."
+ },
+ "mdblist": {
+ "title": "Rating Sources",
+ "status_disabled": "MDBList Disabled",
+ "status_active": "API Key Active",
+ "status_required": "API Key Required",
+ "status_disabled_desc": "MDBList functionality is currently disabled.",
+ "status_active_desc": "Ratings from MDBList are enabled.",
+ "status_required_desc": "Add your key below to enable ratings.",
+ "enable_toggle": "Enable MDBList",
+ "enable_toggle_desc": "Turn on/off all MDBList functionality",
+ "api_section": "API Key",
+ "placeholder": "Paste your MDBList API key",
+ "save": "Save",
+ "clear": "Clear Key",
+ "rating_providers": "Rating Providers",
+ "rating_providers_desc": "Choose which ratings to display in the app",
+ "how_to": "How to get an API key",
+ "step_1": "Log in on the",
+ "step_1_link": "MDBList website",
+ "step_2": "Go to",
+ "step_2_settings": "Settings",
+ "step_2_api": "API",
+ "step_2_end": "section.",
+ "step_3": "Generate a new key and copy it.",
+ "go_to_website": "Go to MDBList",
+ "alert_clear_title": "Clear API Key",
+ "alert_clear_msg": "Are you sure you want to remove the saved API key?",
+ "success_saved": "API key saved successfully.",
+ "error_empty": "API Key cannot be empty.",
+ "error_save": "An error occurred while saving. Please try again.",
+ "api_key_empty_error": "API Key cannot be empty.",
+ "success_cleared": "API key cleared successfully",
+ "error_clear": "Failed to clear API key"
+ },
+ "notification": {
+ "title": "Notification Settings",
+ "section_general": "General",
+ "enable_notifications": "Enable Notifications",
+ "section_types": "Notification Types",
+ "new_episodes": "New Episodes",
+ "upcoming_shows": "Upcoming Shows",
+ "reminders": "Reminders",
+ "section_timing": "Notification Timing",
+ "timing_desc": "When should you be notified before an episode airs?",
+ "hours_1": "1 hour",
+ "hours_suffix": "hours",
+ "section_status": "Notification Status",
+ "stats_upcoming": "Upcoming",
+ "stats_this_week": "This Week",
+ "stats_total": "Total",
+ "sync_button": "Sync Library & Trakt",
+ "syncing": "Syncing...",
+ "sync_desc": "Automatically syncs notifications for all shows in your library and Trakt watchlist/collection.",
+ "section_advanced": "Advanced",
+ "reset_button": "Reset All Notifications",
+ "test_button": "Test Notification (5 sec)",
+ "test_notification_in": "Notification in {{seconds}}s...",
+ "test_notification_text": "Notification will appear in {{seconds}} seconds",
+ "alert_reset_title": "Reset Notifications",
+ "alert_reset_msg": "This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?",
+ "alert_reset_success": "All notifications have been reset",
+ "alert_sync_complete": "Sync Complete",
+ "alert_sync_msg": "Successfully synced notifications for your library and Trakt items.\n\nScheduled: {{upcoming}} upcoming episodes\nThis week: {{thisWeek}} episodes",
+ "alert_test_scheduled": "Test notification scheduled to fire instantly"
+ },
+ "player": {
+ "title": "Video Player",
+ "section_selection": "PLAYER SELECTION",
+ "internal_title": "Built-in Player",
+ "internal_desc": "Use the app's default video player",
+ "vlc_title": "VLC",
+ "vlc_desc": "Open streams in VLC media player",
+ "infuse_title": "Infuse",
+ "infuse_desc": "Open streams in Infuse player",
+ "outplayer_title": "OutPlayer",
+ "outplayer_desc": "Open streams in OutPlayer",
+ "vidhub_title": "VidHub",
+ "vidhub_desc": "Open streams in VidHub player",
+ "infuse_live_title": "Infuse Livecontainer",
+ "infuse_live_desc": "Open streams in Infuse player LiveContainer",
+ "external_title": "External Player",
+ "external_desc": "Open streams in your preferred video player",
+ "section_playback": "PLAYBACK OPTIONS",
+ "autoplay_title": "Auto-play Best Stream",
+ "autoplay_desc": "Automatically start the highest quality stream available.",
+ "resume_title": "Always Resume",
+ "resume_desc": "Skip the resume prompt and automatically continue where you left off (if less than 85% watched).",
+ "engine_title": "Video Player Engine",
+ "engine_desc": "Auto uses ExoPlayer with MPV fallback. Some formats like Dolby Vision and HDR may not be supported by MPV, so Auto is recommended for best compatibility.",
+ "decoder_title": "Decoder Mode",
+ "decoder_desc": "How video is decoded. Auto is recommended for best balance.",
+ "gpu_title": "GPU Rendering",
+ "gpu_desc": "GPU-Next offers better HDR and color management.",
+ "external_downloads_title": "External Player for Downloads",
+ "external_downloads_desc": "Play downloaded content in your preferred external player.",
+ "restart_required": "Restart Required",
+ "restart_msg_decoder": "Please restart the app for the decoder change to take effect.",
+ "restart_msg_gpu": "Please restart the app for the GPU mode change to take effect."
+ },
+ "plugins": {
+ "title": "Plugins",
+ "enable_title": "Enable Plugins",
+ "enable_desc": "Allow the app to use installed plugins for finding streams",
+ "repo_config_title": "Repository Configuration",
+ "repo_config_desc": "Enable multiple repositories to combine plugins from different sources. Toggle each repository on or off below.",
+ "your_repos": "Your Repositories",
+ "your_repos_desc": "Enable multiple repositories to combine plugins from different sources.",
+ "add_repo_button": "Add Repository",
+ "refresh": "Refresh",
+ "remove": "Remove",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "updating": "Updating...",
+ "success": "Success",
+ "error": "Error",
+ "alert_repo_added": "Repository added and plugins loaded successfully",
+ "alert_repo_saved": "Repository URL saved successfully",
+ "alert_repo_refreshed": "Repository refreshed successfully with latest files",
+ "alert_invalid_url": "Invalid URL Format",
+ "alert_plugins_cleared": "All plugins have been removed",
+ "alert_cache_cleared": "Repository cache cleared successfully",
+ "unknown": "Unknown",
+ "active": "Active",
+ "available": "Available",
+ "platform_disabled": "Platform Disabled",
+ "limited": "Limited",
+ "clear_all": "Clear All Plugins",
+ "clear_all_desc": "Are you sure you want to remove all installed plugins? This action cannot be undone.",
+ "clear_cache": "Clear Repository Cache",
+ "clear_cache_desc": "This will remove the saved repository URL and clear all cached plugin data. You will need to re-enter your repository URL."
}
}
\ No newline at end of file
diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json
index 496f5e3a..c535f207 100644
--- a/src/i18n/locales/pt.json
+++ b/src/i18n/locales/pt.json
@@ -139,6 +139,321 @@
"external_first_desc": "Preferir legendas de addons, depois embutidas",
"any_available": "Qualquer Disponível",
"any_available_desc": "Usar primeira legenda disponível"
- }
+ },
+ "trakt": "Trakt",
+ "clear_data": "Limpar Todos os Dados",
+ "clear_data_desc": "Isso redefinirá todas as configurações e limpará todos os dados em cache. Você tem certeza?",
+ "app_updates": "Atualizações do App",
+ "about_nuvio": "Sobre o Nuvio"
+ },
+ "ai_settings": {
+ "title": "Assistente IA",
+ "info_title": "Chat com IA",
+ "info_desc": "Faça perguntas sobre qualquer filme ou episódio usando IA avançada. Obtenha insights sobre enredo, personagens, temas, curiosidades e muito mais - tudo alimentado por dados abrangentes do TMDB.",
+ "feature_1": "Contexto e análise específica de episódios",
+ "feature_2": "Explicações de enredo e insights de personagens",
+ "feature_3": "Curiosidades e fatos de bastidores",
+ "feature_4": "Sua própria chave gratuita OpenRouter",
+ "api_key_section": "CHAVE API OPENROUTER",
+ "api_key_label": "Chave API",
+ "api_key_desc": "Digite sua chave OpenRouter para habilitar recursos de chat IA",
+ "save_api_key": "Salvar Chave API",
+ "saving": "Salvando...",
+ "update": "Atualizar",
+ "remove": "Remover",
+ "get_free_key": "Obter Chave Gratuita no OpenRouter",
+ "enable_chat": "Habilitar Chat IA",
+ "enable_chat_desc": "Quando habilitado, o botão Perguntar IA aparecerá nas páginas de conteúdo.",
+ "chat_enabled": "Chat IA Habilitado",
+ "chat_enabled_desc": "Você agora pode fazer perguntas sobre filmes e séries. Procure o botão \"Perguntar IA\" nas páginas de conteúdo!",
+ "how_it_works": "Como funciona",
+ "how_it_works_desc": "• OpenRouter fornece acesso a múltiplos modelos IA\n• Sua chave API permanece privada e segura\n• Camada gratuita inclui limites generosos de uso\n• Chat com contexto sobre episódios/filmes específicos\n• Obtenha análises detalhadas e explicações",
+ "error_invalid_key": "Por favor, digite uma chave API válida",
+ "error_key_format": "Chaves API OpenRouter devem começar com \"sk-or-\"",
+ "success_saved": "Chave API OpenRouter salva com sucesso!",
+ "error_save": "Falha ao salvar chave API",
+ "confirm_remove_title": "Remover Chave API",
+ "confirm_remove_msg": "Tem certeza que deseja remover sua chave API OpenRouter? Isso desativará os recursos de chat IA.",
+ "success_removed": "Chave API removida com sucesso",
+ "error_remove": "Falha ao remover chave API"
+ },
+ "catalog_settings": {
+ "title": "Catálogos",
+ "layout_phone": "LAYOUT DE CATÁLOGOS (CELULAR)",
+ "posters_per_row": "Pôsteres por linha",
+ "auto": "Auto",
+ "show_titles": "Mostrar Títulos",
+ "show_titles_desc": "Exibir título abaixo de cada pôster",
+ "phone_only_hint": "Aplica-se apenas a celulares. Tablets mantêm layout adaptativo.",
+ "catalogs_group": "Catálogos",
+ "enabled_count": "{{enabled}} de {{total}} habilitados",
+ "rename_hint": "Pressione e segure um catálogo para renomear",
+ "rename_modal_title": "Renomear Catálogo",
+ "rename_placeholder": "Digite o novo nome do catálogo",
+ "error_save_name": "Não foi possível salvar o nome personalizado."
+ },
+ "continue_watching_settings": {
+ "title": "Continuar Assistindo",
+ "playback_behavior": "COMPORTAMENTO DE REPRODUÇÃO",
+ "use_cached": "Usar Streams em Cache",
+ "use_cached_desc": "Quando habilitado, clicar em itens de Continuar Assistindo abrirá o player diretamente usando streams reproduzidos anteriormente. Quando desabilitado, abre uma tela de conteúdo.",
+ "open_metadata": "Abrir Tela de Metadados",
+ "open_metadata_desc": "Quando streams em cache estão desabilitados, abre a tela de Metadados em vez da tela de Streams. Isso mostra detalhes do conteúdo e permite seleção manual de streams.",
+ "card_appearance": "APARÊNCIA DO CARD",
+ "card_style": "Estilo do Card",
+ "card_style_desc": "Escolha como os itens de Continuar Assistindo aparecem na tela inicial",
+ "wide": "Largo",
+ "poster": "Pôster",
+ "cache_settings": "CONFIGURAÇÕES DE CACHE",
+ "cache_duration": "Duração do Cache de Stream",
+ "cache_duration_desc": "Por quanto tempo manter links de stream em cache antes de expirarem",
+ "important_note": "Nota Importante",
+ "important_note_text": "Nem todos os links de stream permanecem ativos por toda a duração do cache. Tempos de cache mais longos podem resultar em links expirados. Se um link em cache falhar, o app buscará novos streams.",
+ "how_it_works": "Como funciona",
+ "how_it_works_cached": "• Streams são armazenados em cache pela duração selecionada após a reprodução\n• Streams em cache são validados antes do uso\n• Se o cache for inválido ou expirado, volta para a tela de conteúdo\n• \"Usar Streams em Cache\" controla player direto vs navegação de tela\n• \"Abrir Tela de Metadados\" aparece apenas quando streams em cache estão desabilitados",
+ "how_it_works_uncached": "• Quando streams em cache estão desabilitados, clicar em itens de Continuar Assistindo abre telas de conteúdo\n• A opção \"Abrir Tela de Metadados\" controla qual tela abrir\n• A tela de Metadados mostra detalhes do conteúdo e permite seleção manual de streams\n• A tela de Streams mostra streams disponíveis para reprodução imediata",
+ "changes_saved": "Alterações salvas",
+ "min": "min",
+ "hour": "hora",
+ "sponsor_desc": "Patrocinou a infraestrutura do servidor para o Nuvio",
+ "mod_role": "Moderador do Discord",
+ "mod_desc": "Ajuda a moderar a comunidade do Nuvio no Discord",
+ "loading": "Carregando...",
+ "discord_user": "Usuário do Discord",
+ "contributions": "contribuições",
+ "gratitude_title": "Somos gratos por cada contribuição",
+ "gratitude_desc": "Cada linha de código, relatório de bug e sugestão ajuda a tornar o Nuvio melhor para todos",
+ "special_thanks_title": "Agradecimentos Especiais",
+ "special_thanks_desc": "Essas pessoas incríveis ajudam a manter a comunidade Nuvio funcionando e os servidores online",
+ "error_rate_limit": "Limite de taxa da API do GitHub excedido. Tente novamente mais tarde.",
+ "error_failed": "Falha ao carregar colaboradores. Verifique sua conexão com a internet.",
+ "retry": "Tentar Novamente",
+ "no_contributors": "Nenhum colaborador encontrado",
+ "loading_contributors": "Carregando colaboradores..."
+ },
+ "debrid": {
+ "title": "Integração Debrid",
+ "description_torbox": "Desbloqueie streams 4K de alta qualidade e velocidades ultra-rápidas integrando o Torbox. Insira sua chave API abaixo para atualizar instantaneamente sua experiência de streaming.",
+ "description_torrentio": "Configure o Torrentio para obter streams de torrent para filmes e séries. Um serviço debrid é necessário para transmitir conteúdo.",
+ "tab_torbox": "TorBox",
+ "tab_torrentio": "Torrentio",
+ "status_connected": "Conectado",
+ "status_disconnected": "Desconectado",
+ "enable_addon": "Ativar Addon",
+ "disconnect_button": "Desconectar e Remover",
+ "disconnect_loading": "Desconectando...",
+ "account_info": "Informações da Conta",
+ "plan": "Plano",
+ "plan_free": "Grátis",
+ "plan_essential": "Essencial ($3/mês)",
+ "plan_pro": "Pro ($10/mês)",
+ "plan_standard": "Padrão ($5/mês)",
+ "plan_unknown": "Desconhecido",
+ "expires": "Expira em",
+ "downloaded": "Baixado",
+ "status_active": "Ativo",
+ "connected_title": "✓ Conectado ao TorBox",
+ "connected_desc": "Seu addon TorBox está ativo e fornecendo streams premium.",
+ "configure_title": "Configurar Addon",
+ "configure_desc": "Personalize sua experiência. Ordene por qualidade, filtre tamanhos de arquivo e gerencie outras configurações.",
+ "open_settings": "Abrir Configurações",
+ "what_is_debrid": "O que é um Serviço Debrid?",
+ "enter_api_key": "Insira sua Chave API",
+ "connect_button": "Conectar e Instalar",
+ "connecting": "Conectando...",
+ "unlock_speeds_title": "Velocidades Premium",
+ "unlock_speeds_desc": "Assine o Torbox para acessar streams em cache de alta qualidade com zero buffering.",
+ "get_subscription": "Obter Assinatura",
+ "powered_by": "Desenvolvido por",
+ "disclaimer_torbox": "O Nuvio não é afiliado ao Torbox de nenhuma forma.",
+ "disclaimer_torrentio": "O Nuvio não é afiliado ao Torrentio de nenhuma forma.",
+ "installed_badge": "✓ INSTALADO",
+ "promo_title": "⚡ Precisa de um Serviço Debrid?",
+ "promo_desc": "Obtenha o TorBox para streaming 4K ultra-rápido com zero buffering. Torrents em cache premium e downloads instantâneos.",
+ "promo_button": "Assinar TorBox",
+ "service_label": "Serviço Debrid *",
+ "api_key_label": "Chave API *",
+ "sorting_label": "Ordenação",
+ "exclude_qualities": "Excluir Qualidades",
+ "priority_languages": "Idiomas Prioritários",
+ "max_results": "Máx. Resultados",
+ "additional_options": "Opções Adicionais",
+ "no_download_links": "Não mostrar links de download",
+ "no_debrid_catalog": "Não mostrar catálogo debrid",
+ "install_button": "Instalar Torrentio",
+ "installing": "Instalando...",
+ "update_button": "Atualizar Configuração",
+ "updating": "Atualizando...",
+ "remove_button": "Remover Torrentio",
+ "error_api_required": "Chave API Necessária",
+ "error_api_required_desc": "Insira a chave API do seu serviço debrid para instalar o Torrentio.",
+ "success_installed": "Addon Torrentio instalado com sucesso!",
+ "success_removed": "Addon Torrentio removido com sucesso",
+ "alert_disconnect_title": "Desconectar Torbox",
+ "alert_disconnect_msg": "Tem certeza que deseja desconectar o Torbox? Isso removerá o addon e limpará sua chave API salva."
+ },
+ "home_screen": {
+ "title": "Configurações da Tela Inicial",
+ "changes_applied": "Alterações Aplicadas",
+ "display_options": "OPÇÕES DE EXIBIÇÃO",
+ "show_hero": "Mostrar Seção Hero",
+ "show_hero_desc": "Conteúdo em destaque no topo",
+ "show_this_week": "Mostrar Seção Desta Semana",
+ "show_this_week_desc": "Novos episódios da semana atual",
+ "select_catalogs": "Selecionar Catálogos",
+ "all_catalogs": "Todos os catálogos",
+ "selected": "selecionados",
+ "hero_layout": "Layout do Hero",
+ "layout_legacy": "Legado",
+ "layout_carousel": "Carrossel",
+ "layout_appletv": "Apple TV",
+ "layout_desc": "Banner largura total, cartões deslizantes ou estilo Apple TV",
+ "featured_source": "Fonte de Destaques",
+ "using_catalogs": "Usando Catálogos",
+ "manage_catalogs": "Gerenciar catálogos selecionados",
+ "dynamic_bg": "Fundo Hero Dinâmico",
+ "dynamic_bg_desc": "Banner desfocado atrás do carrossel",
+ "performance_note": "Pode impactar o desempenho em dispositivos mais lentos.",
+ "posters": "Pôsteres",
+ "show_titles": "Mostrar Títulos",
+ "poster_size": "Tamanho do Pôster",
+ "poster_corners": "Cantos do Pôster",
+ "size_small": "Pequeno",
+ "size_medium": "Médio",
+ "size_large": "Grande",
+ "corner_square": "Quadrado",
+ "corner_rounded": "Arredondado",
+ "corner_pill": "Pílula",
+ "about_title": "SOBRE ESTAS CONFIGURAÇÕES",
+ "about_desc": "Estas configurações controlam como o conteúdo é exibido na sua tela inicial. As alterações são aplicadas imediatamente sem reiniciar o app."
+ },
+ "mdblist": {
+ "title": "Fontes de Avaliação",
+ "status_disabled": "MDBList Desativado",
+ "status_active": "Chave de API Ativa",
+ "status_required": "Chave de API Necessária",
+ "status_disabled_desc": "A funcionalidade MDBList está atualmente desativada.",
+ "status_active_desc": "As avaliações do MDBList estão ativadas.",
+ "status_required_desc": "Adicione sua chave abaixo para ativar as avaliações.",
+ "enable_toggle": "Ativar MDBList",
+ "enable_toggle_desc": "Ligar/desligar toda a funcionalidade do MDBList",
+ "api_section": "Chave de API",
+ "placeholder": "Cole sua chave de API MDBList",
+ "save": "Salvar",
+ "clear": "Limpar Chave",
+ "rating_providers": "Provedores de Avaliação",
+ "rating_providers_desc": "Escolha quais avaliações exibir no aplicativo",
+ "how_to": "Como obter uma chave de API",
+ "step_1": "Faça login no",
+ "step_1_link": "site do MDBList",
+ "step_2": "Vá para a seção",
+ "step_2_settings": "Configurações",
+ "step_2_api": "API",
+ "step_2_end": ".",
+ "step_3": "Gere uma nova chave e copie-a.",
+ "go_to_website": "Ir para o MDBList",
+ "alert_clear_title": "Limpar Chave de API",
+ "alert_clear_msg": "Tem certeza de que deseja remover a chave de API salva?",
+ "success_saved": "Chave de API salva com sucesso.",
+ "error_empty": "Chave API não pode estar vazia.",
+ "error_save": "Ocorreu um erro ao salvar. Por favor, tente novamente.",
+ "api_key_empty_error": "A Chave de API não pode estar vazia.",
+ "success_cleared": "Chave de API limpa com sucesso",
+ "error_clear": "Falha ao limpar a chave de API"
+ },
+ "notification": {
+ "title": "Configurações de Notificação",
+ "section_general": "Geral",
+ "enable_notifications": "Ativar Notificações",
+ "section_types": "Tipos de Notificação",
+ "new_episodes": "Novos Episódios",
+ "upcoming_shows": "Próximos Programas",
+ "reminders": "Lembretes",
+ "section_timing": "Tempo de Notificação",
+ "timing_desc": "Quanto tempo antes de um episódio ir ao ar você deve ser notificado?",
+ "hours_1": "1 hora",
+ "hours_suffix": "horas",
+ "section_status": "Status da Notificação",
+ "stats_upcoming": "Próximos",
+ "stats_this_week": "Esta Semana",
+ "stats_total": "Total",
+ "sync_button": "Sincronizar Biblioteca e Trakt",
+ "syncing": "Sincronizando...",
+ "sync_desc": "Sincroniza automaticamente notificações para todos os programas na sua biblioteca e watchlist/coleção do Trakt.",
+ "section_advanced": "Avançado",
+ "reset_button": "Redefinir Todas as Notificações",
+ "test_button": "Notificação de Teste (5 seg)",
+ "test_notification_in": "Notificação em {{seconds}}s...",
+ "test_notification_text": "A notificação aparecerá em {{seconds}} segundos",
+ "alert_reset_title": "Redefinir Notificações",
+ "alert_reset_msg": "Isso cancelará todas as notificações agendadas, mas não removerá nada da sua biblioteca salva. Tem certeza?",
+ "alert_reset_success": "Todas as notificações foram redefinidas",
+ "alert_sync_complete": "Sincronização Completa",
+ "alert_sync_msg": "Notificações sincronizadas com sucesso para sua biblioteca e itens do Trakt.\n\nAgendadas: {{upcoming}} próximos episódios\nEsta semana: {{thisWeek}} episódios",
+ "alert_test_scheduled": "Notificação de teste agendada para disparar instantaneamente"
+ },
+ "player": {
+ "title": "Player de Vídeo",
+ "section_selection": "SELEÇÃO DE PLAYER",
+ "internal_title": "Player Integrado",
+ "internal_desc": "Usar o player de vídeo padrão do aplicativo",
+ "vlc_title": "VLC",
+ "vlc_desc": "Abrir streams no player de mídia VLC",
+ "infuse_title": "Infuse",
+ "infuse_desc": "Abrir streams no player Infuse",
+ "outplayer_title": "OutPlayer",
+ "outplayer_desc": "Abrir streams no OutPlayer",
+ "vidhub_title": "VidHub",
+ "vidhub_desc": "Abrir streams no player VidHub",
+ "infuse_live_title": "Infuse Livecontainer",
+ "infuse_live_desc": "Abrir streams no player Infuse LiveContainer",
+ "external_title": "Player Externo",
+ "external_desc": "Abrir streams no seu player de vídeo preferido",
+ "section_playback": "OPÇÕES DE REPRODUÇÃO",
+ "autoplay_title": "Reprodução Automática (Melhor Stream)",
+ "autoplay_desc": "Iniciar automaticamente o stream de melhor qualidade disponível.",
+ "resume_title": "Sempre Retomar",
+ "resume_desc": "Pular o aviso de retomar e continuar automaticamente de onde parou (se assistido menos de 85%).",
+ "engine_title": "Motor do Player de Vídeo",
+ "engine_desc": "Auto usa ExoPlayer com fallback para MPV. Alguns formatos como Dolby Vision e HDR podem não ser suportados pelo MPV, então Auto é recomendado para melhor compatibilidade.",
+ "decoder_title": "Modo de Decodificador",
+ "decoder_desc": "Como o vídeo é decodificado. Auto é recomendado para melhor equilíbrio.",
+ "gpu_title": "Renderização GPU",
+ "gpu_desc": "GPU-Next oferece melhor HDR e gerenciamento de cores.",
+ "external_downloads_title": "Player Externo para Downloads",
+ "external_downloads_desc": "Reproduzir conteúdo baixado no seu player externo preferido.",
+ "restart_required": "Reinicialização Necessária"
+ },
+ "plugins": {
+ "title": "Plugins",
+ "enable_title": "Ativar Plugins",
+ "enable_desc": "Permitir que o aplicativo use plugins instalados para encontrar transmissões",
+ "repo_config_title": "Configuração de Repositório",
+ "repo_config_desc": "Ative vários repositórios para combinar plugins de diferentes fontes. Ative ou desative cada repositório abaixo.",
+ "your_repos": "Seus Repositórios",
+ "your_repos_desc": "Ative vários repositórios para combinar plugins de diferentes fontes.",
+ "add_repo_button": "Adicionar Repositório",
+ "refresh": "Atualizar",
+ "remove": "Remover",
+ "enabled": "Ativado",
+ "disabled": "Desativado",
+ "updating": "Atualizando...",
+ "success": "Sucesso",
+ "error": "Erro",
+ "alert_repo_added": "Repositório adicionado e plugins carregados com sucesso",
+ "alert_repo_saved": "URL do repositório salvo com sucesso",
+ "alert_repo_refreshed": "Repositório atualizado com sucesso com arquivos mais recentes",
+ "alert_invalid_url": "Formato de URL inválido",
+ "alert_plugins_cleared": "Todos os plugins foram removidos",
+ "alert_cache_cleared": "Cache do repositório limpo com sucesso",
+ "unknown": "Desconhecido",
+ "active": "Ativo",
+ "available": "Disponível",
+ "platform_disabled": "Plataforma Desativada",
+ "limited": "Limitado",
+ "clear_all": "Limpar Todos os Plugins",
+ "clear_all_desc": "Você tem certeza de que deseja remover todos os plugins instalados? Esta ação não pode ser desfeita.",
+ "clear_cache": "Limpar Cache do Repositório",
+ "clear_cache_desc": "Isso removerá a URL do repositório salva e limpará todos os dados de plugins armazenados em cache. Você precisará digitar a URL do repositório novamente."
}
}
\ No newline at end of file
diff --git a/src/screens/AISettingsScreen.tsx b/src/screens/AISettingsScreen.tsx
index d73a51c5..251134af 100644
--- a/src/screens/AISettingsScreen.tsx
+++ b/src/screens/AISettingsScreen.tsx
@@ -21,11 +21,13 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSettings } from '../hooks/useSettings';
import { SvgXml } from 'react-native-svg';
+import { useTranslation } from 'react-i18next';
const { width } = Dimensions.get('window');
const isTablet = width >= 768;
const AISettingsScreen: React.FC = () => {
+ const { t } = useTranslation();
// CustomAlert state (must be inside the component)
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
@@ -69,7 +71,7 @@ const AISettingsScreen: React.FC = () => {
`;
-
+
const [apiKey, setApiKey] = useState('');
const [loading, setLoading] = useState(false);
const [isKeySet, setIsKeySet] = useState(false);
@@ -92,12 +94,12 @@ const AISettingsScreen: React.FC = () => {
const handleSaveApiKey = async () => {
if (!apiKey.trim()) {
- openAlert('Error', 'Please enter a valid API key');
+ openAlert(t('common.error'), t('ai_settings.error_invalid_key'));
return;
}
if (!apiKey.startsWith('sk-or-')) {
- openAlert('Error', 'OpenRouter API keys should start with "sk-or-"');
+ openAlert(t('common.error'), t('ai_settings.error_key_format'));
return;
}
@@ -105,9 +107,9 @@ const AISettingsScreen: React.FC = () => {
try {
await mmkvStorage.setItem('openrouter_api_key', apiKey.trim());
setIsKeySet(true);
- openAlert('Success', 'OpenRouter API key saved successfully!');
+ openAlert(t('common.success'), t('ai_settings.success_saved'));
} catch (error) {
- openAlert('Error', 'Failed to save API key');
+ openAlert(t('common.error'), t('ai_settings.error_save'));
if (__DEV__) console.error('Error saving OpenRouter API key:', error);
} finally {
setLoading(false);
@@ -116,10 +118,10 @@ const AISettingsScreen: React.FC = () => {
const handleRemoveApiKey = () => {
openAlert(
- 'Remove API Key',
- 'Are you sure you want to remove your OpenRouter API key? This will disable AI chat features.',
+ t('ai_settings.confirm_remove_title'),
+ t('ai_settings.confirm_remove_msg'),
[
- { label: 'Cancel', onPress: () => {} },
+ { label: t('common.cancel'), onPress: () => { } },
{
label: 'Remove',
onPress: async () => {
@@ -127,9 +129,9 @@ const AISettingsScreen: React.FC = () => {
await mmkvStorage.removeItem('openrouter_api_key');
setApiKey('');
setIsKeySet(false);
- openAlert('Success', 'API key removed successfully');
+ openAlert(t('common.success'), t('ai_settings.success_removed'));
} catch (error) {
- openAlert('Error', 'Failed to remove API key');
+ openAlert(t('common.error'), t('ai_settings.error_remove'));
}
}
}
@@ -142,35 +144,35 @@ const AISettingsScreen: React.FC = () => {
};
return (
-
+
-
+
{/* Header */}
- navigation.goBack()}
style={styles.backButton}
>
-
- Settings
+ {t('settings.settings_title')}
-
+
{/* Empty for now, but ready for future actions */}
-
+
- AI Assistant
+ {t('ai_settings.title')}
- {
{/* Info Card */}
-
- AI-Powered Chat
+ {t('ai_settings.info_title')}
- Ask questions about any movie or TV show episode using advanced AI. Get insights about plot, characters, themes, trivia, and more - all powered by comprehensive TMDB data.
+ {t('ai_settings.info_desc')}
-
+
- Episode-specific context and analysis
+ {t('ai_settings.feature_1')}
- Plot explanations and character insights
+ {t('ai_settings.feature_2')}
- Behind-the-scenes trivia and facts
+ {t('ai_settings.feature_3')}
- Your own free OpenRouter API key
+ {t('ai_settings.feature_4')}
@@ -222,21 +224,21 @@ const AISettingsScreen: React.FC = () => {
{/* API Key Configuration */}
- OPENROUTER API KEY
+ {t('ai_settings.api_key_section')}
-
+
- API Key
+ {t('ai_settings.api_key_label')}
- Enter your OpenRouter API key to enable AI chat features
+ {t('ai_settings.api_key_desc')}
-
+
{
onPress={handleSaveApiKey}
disabled={loading}
>
-
- {loading ? 'Saving...' : 'Save API Key'}
+ {loading ? t('ai_settings.saving') : t('ai_settings.save_api_key')}
) : (
@@ -275,27 +277,27 @@ const AISettingsScreen: React.FC = () => {
onPress={handleSaveApiKey}
disabled={loading}
>
-
- Update
+ {t('ai_settings.update')}
-
+
-
- Remove
+ {t('ai_settings.remove')}
@@ -306,23 +308,23 @@ const AISettingsScreen: React.FC = () => {
style={[styles.getKeyButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={handleGetApiKey}
>
-
- Get Free API Key from OpenRouter
+ {t('ai_settings.get_free_key')}
{/* Enable Toggle (top) */}
-
+
- Enable AI Chat
+ {t('ai_settings.enable_chat')}
updateSetting('aiChatEnabled', v)}
@@ -331,24 +333,24 @@ const AISettingsScreen: React.FC = () => {
ios_backgroundColor={currentTheme.colors.elevation2}
/>
- When enabled, the Ask AI button will appear on content pages.
+ {t('ai_settings.enable_chat_desc')}
{/* Status Card */}
{isKeySet && (
-
- AI Chat Enabled
+ {t('ai_settings.chat_enabled')}
- You can now ask questions about movies and TV shows. Look for the "Ask AI" button on content pages!
+ {t('ai_settings.chat_enabled_desc')}
)}
@@ -356,14 +358,10 @@ const AISettingsScreen: React.FC = () => {
{/* Usage Info */}
- How it works
+ {t('ai_settings.how_it_works')}
- • OpenRouter provides access to multiple AI models{'\n'}
- • Your API key stays private and secure{'\n'}
- • Free tier includes generous usage limits{'\n'}
- • Chat with context about specific episodes/movies{'\n'}
- • Get detailed analysis and explanations
+ {t('ai_settings.how_it_works_desc')}
{/* OpenRouter branding */}
diff --git a/src/screens/AddonsScreen.tsx b/src/screens/AddonsScreen.tsx
index 8ee6322c..15c2db1e 100644
--- a/src/screens/AddonsScreen.tsx
+++ b/src/screens/AddonsScreen.tsx
@@ -664,9 +664,9 @@ const AddonsScreen = () => {
setAlertVisible(true);
} catch (error) {
logger.error('Failed to install addon:', error);
- setAlertTitle('Error');
- setAlertMessage('Failed to install addon');
- setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertTitle(t('common.error'));
+ setAlertMessage(t('addons.install_error'));
+ setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setInstalling(false);
@@ -909,7 +909,7 @@ const AddonsScreen = () => {
)}
- v{item.version || '1.0.0'}
+ {t('addons.version', { version: item.version || '1.0.0' })}
•
{categoryText}
@@ -967,7 +967,7 @@ const AddonsScreen = () => {
onPress={() => navigation.goBack()}
>
- Settings
+ {t('settings.settings_title')}
@@ -1027,18 +1027,18 @@ const AddonsScreen = () => {
OVERVIEW
-
+
-
+
-
+
{/* Hide Add Addon Section in reorder mode */}
{!reorderMode && (
- ADD NEW ADDON
+ {t('addons.add_button').toUpperCase()}
{
disabled={installing || !addonUrl}
>
- {installing ? 'Loading...' : t('addons.add_button')}
+ {installing ? t('common.loading') : t('addons.add_button')}
@@ -1115,7 +1115,7 @@ const AddonsScreen = () => {
{addonDetails && (
<>
- Install Addon
+ {t('addons.install')}
{
setShowConfirmModal(false);
@@ -1145,7 +1145,7 @@ const AddonsScreen = () => {
)}
{addonDetails.name}
- v{addonDetails.version || '1.0.0'}
+ {t('addons.version', { version: addonDetails.version || '1.0.0' })}
diff --git a/src/screens/CatalogSettingsScreen.tsx b/src/screens/CatalogSettingsScreen.tsx
index 3c15dfe1..320d7252 100644
--- a/src/screens/CatalogSettingsScreen.tsx
+++ b/src/screens/CatalogSettingsScreen.tsx
@@ -25,6 +25,7 @@ import { logger } from '../utils/logger';
import { clearCustomNameCache } from '../utils/catalogNameUtils';
import { BlurView } from 'expo-blur';
import CustomAlert from '../components/CustomAlert';
+import { useTranslation } from 'react-i18next';
// Optional iOS Glass effect (expo-glass-effect) with safe fallback for CatalogSettingsScreen
let GlassViewComp: any = null;
@@ -275,6 +276,7 @@ const CatalogSettingsScreen = () => {
const colors = currentTheme.colors;
const styles = createStyles(colors);
const isDarkMode = true; // Force dark mode
+ const { t } = useTranslation();
// Modal State
const [isRenameModalVisible, setIsRenameModalVisible] = useState(false);
@@ -489,9 +491,9 @@ const CatalogSettingsScreen = () => {
} catch (error) {
logger.error('Failed to save custom catalog name:', error);
- setAlertTitle('Error');
- setAlertMessage('Could not save the custom name.');
- setAlertActions([{ label: 'OK', onPress: () => { } }]);
+ setAlertTitle(t('common.error'));
+ setAlertMessage(t('catalog_settings.error_save_name'));
+ setAlertActions([{ label: t('common.ok'), onPress: () => { } }]);
setAlertVisible(true);
} finally {
setIsRenameModalVisible(false);
@@ -514,10 +516,10 @@ const CatalogSettingsScreen = () => {
onPress={() => navigation.goBack()}
>
- Settings
+ {t('settings.settings_title')}
- Catalogs
+ {t('catalog_settings.title')}
@@ -534,19 +536,19 @@ const CatalogSettingsScreen = () => {
onPress={() => navigation.goBack()}
>
- Settings
+ {t('settings.settings_title')}
- Catalogs
+ {t('catalog_settings.title')}
{/* Layout (Mobile only) */}
{Platform.OS && (
- LAYOUT CATALOGSCREEN (PHONE)
+ {t('catalog_settings.layout_phone')}
- Posters per row
+ {t('catalog_settings.posters_per_row')}
{/* Only show on phones (approx width < 600) */}
@@ -561,7 +563,7 @@ const CatalogSettingsScreen = () => {
}}
activeOpacity={0.7}
>
- Auto
+ {t('catalog_settings.auto')}
{
- Applies to phones only. Tablets keep adaptive layout.
+ {t('catalog_settings.phone_only_hint')}
{/* Show Titles Toggle */}
- Show Poster Titles
- Display title text below each poster
+ {t('catalog_settings.show_titles')}
+ {t('catalog_settings.show_titles_desc')}
{
onPress={() => toggleExpansion(addonId)}
activeOpacity={0.7}
>
- Catalogs
+ {t('catalog_settings.catalogs_group')}
- {group.enabledCount} of {group.catalogs.length} enabled
+ {t('catalog_settings.enabled_count', { enabled: group.enabledCount, total: group.catalogs.length })}
{
<>
- Long-press a catalog to rename
+ {t('catalog_settings.rename_hint')}
{group.catalogs.map((setting, index) => (
{
{GlassViewComp && liquidGlassAvailable ? (
e.stopPropagation()}>
- Rename Catalog
+ {t('catalog_settings.rename_modal_title')}
-
) : (
e.stopPropagation()}>
- Rename Catalog
+ {t('catalog_settings.rename_modal_title')}
- setIsRenameModalVisible(false)} color={colors.mediumGray} />
-
+ setIsRenameModalVisible(false)} color={colors.mediumGray} />
+
@@ -734,18 +736,18 @@ const CatalogSettingsScreen = () => {
) : (
setIsRenameModalVisible(false)}>
e.stopPropagation()}>
- Rename Catalog
+ {t('catalog_settings.rename_modal_title')}
- setIsRenameModalVisible(false)} color={colors.mediumGray} />
-
+ setIsRenameModalVisible(false)} color={colors.mediumGray} />
+
diff --git a/src/screens/ContinueWatchingSettingsScreen.tsx b/src/screens/ContinueWatchingSettingsScreen.tsx
index 4242ab5a..221143db 100644
--- a/src/screens/ContinueWatchingSettingsScreen.tsx
+++ b/src/screens/ContinueWatchingSettingsScreen.tsx
@@ -17,23 +17,9 @@ import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { useSettings } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator';
+import { useTranslation } from 'react-i18next';
+
-// TTL options in milliseconds - organized in rows
-const TTL_OPTIONS = [
- [
- { label: '15 min', value: 15 * 60 * 1000 },
- { label: '30 min', value: 30 * 60 * 1000 },
- { label: '1 hour', value: 60 * 60 * 1000 },
- ],
- [
- { label: '2 hours', value: 2 * 60 * 60 * 1000 },
- { label: '6 hours', value: 6 * 60 * 60 * 1000 },
- { label: '12 hours', value: 12 * 60 * 60 * 1000 },
- ],
- [
- { label: '24 hours', value: 24 * 60 * 60 * 1000 },
- ],
-];
const ContinueWatchingSettingsScreen: React.FC = () => {
const navigation = useNavigation>();
@@ -43,6 +29,24 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
const styles = createStyles(colors);
const [showSavedIndicator, setShowSavedIndicator] = useState(false);
const fadeAnim = React.useRef(new Animated.Value(0)).current;
+ const { t } = useTranslation();
+
+ // TTL options in milliseconds - organized in rows
+ const TTL_OPTIONS = [
+ [
+ { label: `15 ${t('continue_watching_settings.min')}`, value: 15 * 60 * 1000 },
+ { label: `30 ${t('continue_watching_settings.min')}`, value: 30 * 60 * 1000 },
+ { label: `1 ${t('continue_watching_settings.hour')}`, value: 60 * 60 * 1000 },
+ ],
+ [
+ { label: `2 ${t('continue_watching_settings.hours')}`, value: 2 * 60 * 60 * 1000 },
+ { label: `6 ${t('continue_watching_settings.hours')}`, value: 6 * 60 * 60 * 1000 },
+ { label: `12 ${t('continue_watching_settings.hours')}`, value: 12 * 60 * 60 * 1000 },
+ ],
+ [
+ { label: `24 ${t('continue_watching_settings.hours')}`, value: 24 * 60 * 60 * 1000 },
+ ],
+ ];
// Prevent iOS entrance flicker by restoring a non-translucent StatusBar
useEffect(() => {
@@ -167,12 +171,12 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
onPress={handleBack}
>
- Settings
+ {t('settings.settings_title')}
- Continue Watching
+ {t('continue_watching_settings.title')}
{/* Content */}
@@ -182,19 +186,19 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
contentContainerStyle={styles.contentContainer}
>
- PLAYBACK BEHAVIOR
+ {t('continue_watching_settings.playback_behavior')}
handleUpdateSetting('useCachedStreams', value)}
isLast={!settings.useCachedStreams}
/>
{!settings.useCachedStreams && (
handleUpdateSetting('openMetadataScreenWhenCacheDisabled', value)}
isLast={true}
@@ -205,14 +209,14 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
{/* Card Appearance Section */}
- CARD APPEARANCE
+ {t('continue_watching_settings.card_appearance')}
- Card Style
+ {t('continue_watching_settings.card_style')}
- Choose how Continue Watching items appear on the home screen
+ {t('continue_watching_settings.card_style_desc')}
{
styles.cardStyleLabel,
{ color: settings.continueWatchingCardStyle === 'wide' ? colors.white : colors.highEmphasis }
]}>
- Wide
+ {t('continue_watching_settings.wide')}
{settings.continueWatchingCardStyle === 'wide' && (
@@ -268,7 +272,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
styles.cardStyleLabel,
{ color: settings.continueWatchingCardStyle === 'poster' ? colors.white : colors.highEmphasis }
]}>
- Poster
+ {t('continue_watching_settings.poster')}
{settings.continueWatchingCardStyle === 'poster' && (
@@ -281,14 +285,14 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
{settings.useCachedStreams && (
- CACHE SETTINGS
+ {t('continue_watching_settings.cache_settings')}
- Stream Cache Duration
+ {t('continue_watching_settings.cache_duration')}
- How long to keep cached stream links before they expire
+ {t('continue_watching_settings.cache_duration_desc')}
{TTL_OPTIONS.map((row, rowIndex) => (
@@ -310,11 +314,11 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
- Important Note
+ {t('continue_watching_settings.important_note')}
- Not all stream links may remain active for the full cache duration. Longer cache times may result in expired links. If a cached link fails, the app will fall back to fetching fresh streams.
+ {t('continue_watching_settings.important_note_text')}
@@ -325,24 +329,17 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
- How it works
+ {t('continue_watching_settings.how_it_works')}
{settings.useCachedStreams ? (
<>
- • Streams are cached for your selected duration after playing{'\n'}
- • Cached streams are validated before use{'\n'}
- • If cache is invalid or expired, falls back to content screen{'\n'}
- • "Use Cached Streams" controls direct player vs screen navigation{'\n'}
- • "Open Metadata Screen" appears only when cached streams are disabled
+ {t('continue_watching_settings.how_it_works_cached')}
>
) : (
<>
- • When cached streams are disabled, clicking Continue Watching items opens content screens{'\n'}
- • "Open Metadata Screen" option controls which screen to open{'\n'}
- • Metadata screen shows content details and allows manual stream selection{'\n'}
- • Streams screen shows available streams for immediate playback
+ {t('continue_watching_settings.how_it_works_uncached')}
>
)}
@@ -361,7 +358,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
]}
>
- Changes saved
+ {t('continue_watching_settings.changes_saved')}
);
diff --git a/src/screens/ContributorsScreen.tsx b/src/screens/ContributorsScreen.tsx
index 0e77958a..652809d8 100644
--- a/src/screens/ContributorsScreen.tsx
+++ b/src/screens/ContributorsScreen.tsx
@@ -21,6 +21,7 @@ import { NavigationProp } from '@react-navigation/native';
import FastImage from '@d11/react-native-fast-image';
import { Feather, FontAwesome5 } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
+import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { fetchContributors, GitHubContributor } from '../services/githubReleaseService';
import { RootStackParamList } from '../navigation/AppNavigator';
@@ -58,21 +59,21 @@ interface SpecialMention extends SpecialMentionConfig {
isLoading: boolean;
}
-const SPECIAL_MENTIONS_CONFIG: SpecialMentionConfig[] = [
+const getSpecialMentionsConfig = (t: any) => [
{
discordId: '709281623866081300',
- role: 'Community Manager',
- description: 'Manages the Discord & Reddit communities for Nuvio',
+ role: t('contributors.manager_role'),
+ description: t('contributors.manager_desc'),
},
{
discordId: '777773947071758336',
- role: 'Server Sponsor',
- description: 'Sponsored the server infrastructure for Nuvio',
+ role: t('contributors.sponsor_role'),
+ description: t('contributors.sponsor_desc'),
},
{
discordId: '1395843374241546362',
- role: 'Discord Mod',
- description: 'Helps moderate the Nuvio Discord community',
+ role: t('contributors.mod_role'),
+ description: t('contributors.mod_desc'),
},
];
@@ -86,6 +87,7 @@ interface ContributorCardProps {
}
const ContributorCard: React.FC = ({ contributor, currentTheme, isTablet, isLargeTablet }) => {
+ const { t } = useTranslation();
const handlePress = useCallback(() => {
Linking.openURL(contributor.html_url);
}, [contributor.html_url]);
@@ -121,7 +123,7 @@ const ContributorCard: React.FC = ({ contributor, currentT
{ color: currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletContributions
]}>
- {contributor.contributions} contributions
+ {contributor.contributions} {t('contributors.contributions')}
= ({ mention, currentTheme, isTablet, isLargeTablet }) => {
+ const { t } = useTranslation();
const handlePress = useCallback(() => {
// Try to open Discord profile
const discordUrl = `discord://-/users/${mention.discordId}`;
@@ -153,7 +156,7 @@ const SpecialMentionCard: React.FC = ({ mention, curren
// Fallback: show alert with Discord info
Alert.alert(
mention.name,
- `Discord: @${mention.username}\n\nOpen Discord and search for this user to connect with them.`,
+ `Discord: @${mention.username}\n\nDo you want to open Discord and search for this user?`,
[{ text: 'OK' }]
);
}
@@ -205,7 +208,7 @@ const SpecialMentionCard: React.FC = ({ mention, curren
{ color: currentTheme.colors.highEmphasis },
isTablet && styles.tabletUsername
]}>
- {mention.isLoading ? 'Loading...' : mention.name}
+ {mention.isLoading ? t('contributors.loading') : mention.name}
{!mention.isLoading && mention.username && (
= ({ mention, curren
};
const ContributorsScreen: React.FC = () => {
+ const { t } = useTranslation();
const navigation = useNavigation>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
+ const SPECIAL_MENTIONS_CONFIG = getSpecialMentionsConfig(t);
+
const [activeTab, setActiveTab] = useState('contributors');
const [contributors, setContributors] = useState([]);
const [loading, setLoading] = useState(true);
@@ -254,7 +260,7 @@ const ContributorsScreen: React.FC = () => {
// Initialize with loading state
const initialMentions: SpecialMention[] = SPECIAL_MENTIONS_CONFIG.map(config => ({
...config,
- name: 'Loading...',
+ name: t('contributors.loading'),
username: '',
avatarUrl: '',
isLoading: true,
@@ -283,7 +289,7 @@ const ContributorsScreen: React.FC = () => {
// Return fallback data
return {
...config,
- name: 'Discord User',
+ name: t('contributors.discord_user'),
username: config.discordId,
avatarUrl: '',
isLoading: false,
@@ -363,10 +369,10 @@ const ContributorsScreen: React.FC = () => {
await mmkvStorage.removeItem('github_contributors');
await mmkvStorage.removeItem('github_contributors_timestamp');
} catch { }
- setError('Unable to load contributors. This might be due to GitHub API rate limits.');
+ setError(t('contributors.error_rate_limit'));
}
} catch (err) {
- setError('Failed to load contributors. Please check your internet connection.');
+ setError(t('contributors.error_failed'));
if (__DEV__) console.error('Error loading contributors:', err);
} finally {
setLoading(false);
@@ -427,7 +433,7 @@ const ContributorsScreen: React.FC = () => {
onPress={() => navigation.goBack()}
>
- Settings
+ {t('common.settings')}
{
{ color: currentTheme.colors.text },
isTablet && styles.tabletHeaderTitle
]}>
- Contributors
+ {t('contributors.title')}
- Loading contributors...
+ {t('contributors.loading_contributors')}
@@ -462,7 +468,7 @@ const ContributorsScreen: React.FC = () => {
onPress={() => navigation.goBack()}
>
- Settings
+ {t('common.settings')}
{
{ color: currentTheme.colors.text },
isTablet && styles.tabletHeaderTitle
]}>
- Contributors
+ {t('contributors.title')}
@@ -494,7 +500,7 @@ const ContributorsScreen: React.FC = () => {
{ color: activeTab === 'contributors' ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletTabText
]}>
- Contributors
+ {t('contributors.tab_contributors')}
{
{ color: activeTab === 'special' ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletTabText
]}>
- Special Mentions
+ {t('contributors.tab_special')}
@@ -528,14 +534,14 @@ const ContributorsScreen: React.FC = () => {
{error}
- GitHub API rate limit exceeded. Please try again later or pull to refresh.
+ {t('contributors.error_rate_limit')}
loadContributors()}
>
- Try Again
+ {t('contributors.retry')}
@@ -543,7 +549,7 @@ const ContributorsScreen: React.FC = () => {
- No contributors found
+ {t('contributors.no_contributors')}
) : (
@@ -575,14 +581,14 @@ const ContributorsScreen: React.FC = () => {
{ color: currentTheme.colors.highEmphasis },
isTablet && styles.tabletGratitudeText
]}>
- We're grateful for every contribution
+ {t('contributors.gratitude_title')}
- Each line of code, bug report, and suggestion helps make Nuvio better for everyone
+ {t('contributors.gratitude_desc')}
@@ -622,14 +628,14 @@ const ContributorsScreen: React.FC = () => {
{ color: currentTheme.colors.highEmphasis },
isTablet && styles.tabletGratitudeText
]}>
- Special Thanks
+ {t('contributors.special_thanks_title')}
- These amazing people help keep the Nuvio community running and the servers online
+ {t('contributors.special_thanks_desc')}
diff --git a/src/screens/DebridIntegrationScreen.tsx b/src/screens/DebridIntegrationScreen.tsx
index 193bbf24..e5805745 100644
--- a/src/screens/DebridIntegrationScreen.tsx
+++ b/src/screens/DebridIntegrationScreen.tsx
@@ -21,6 +21,7 @@ import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import { useTheme } from '../contexts/ThemeContext';
+import { useTranslation } from 'react-i18next';
import { Feather, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
import { stremioService } from '../services/stremioService';
import { logger } from '../utils/logger';
@@ -160,13 +161,13 @@ const DEFAULT_TORRENTIO_CONFIG: TorrentioConfig = {
isInstalled: false,
};
-const getPlanName = (plan: number): string => {
+const getPlanName = (plan: number, t: any): string => {
switch (plan) {
- case 0: return 'Free';
- case 1: return 'Essential ($3/mo)';
- case 2: return 'Pro ($10/mo)';
- case 3: return 'Standard ($5/mo)';
- default: return 'Unknown';
+ case 0: return t('debrid.plan_free');
+ case 1: return t('debrid.plan_essential');
+ case 2: return t('debrid.plan_pro');
+ case 3: return t('debrid.plan_standard');
+ default: return t('debrid.plan_unknown');
}
};
@@ -687,6 +688,7 @@ const createStyles = (colors: any) => StyleSheet.create({
});
const DebridIntegrationScreen = () => {
+ const { t } = useTranslation();
const navigation = useNavigation>();
const { currentTheme } = useTheme();
const colors = currentTheme.colors;
@@ -831,8 +833,8 @@ const DebridIntegrationScreen = () => {
// Torbox handlers
const handleConnect = async () => {
if (!apiKey.trim()) {
- setAlertTitle('Error');
- setAlertMessage('Please enter a valid API Key');
+ setAlertTitle(t('common.error'));
+ setAlertMessage(t('debrid.error_api_required')); // Reusing key or common error
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
@@ -860,8 +862,8 @@ const DebridIntegrationScreen = () => {
setConfig(newConfig);
setApiKey('');
- setAlertTitle('Success');
- setAlertMessage('Torbox addon connected successfully!');
+ setAlertTitle(t('common.success'));
+ setAlertMessage(t('debrid.connected_title')); // Or similar success message
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
@@ -888,12 +890,12 @@ const DebridIntegrationScreen = () => {
};
const handleDisconnect = async () => {
- setAlertTitle('Disconnect Torbox');
- setAlertMessage('Are you sure you want to disconnect Torbox? This will remove the addon and clear your saved API key.');
+ setAlertTitle(t('debrid.alert_disconnect_title'));
+ setAlertMessage(t('debrid.alert_disconnect_msg'));
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
- label: 'Disconnect',
+ label: t('debrid.disconnect_button'),
onPress: async () => {
setAlertVisible(false);
setLoading(true);
@@ -1007,8 +1009,8 @@ const DebridIntegrationScreen = () => {
const handleInstallTorrentio = async () => {
// Check if API key is provided
if (!torrentioConfig.debridApiKey.trim()) {
- setAlertTitle('API Key Required');
- setAlertMessage('Please enter your debrid service API key to install Torrentio.');
+ setAlertTitle(t('debrid.error_api_required'));
+ setAlertMessage(t('debrid.error_api_required_desc'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
@@ -1042,8 +1044,8 @@ const DebridIntegrationScreen = () => {
await mmkvStorage.setItem(TORRENTIO_CONFIG_KEY, JSON.stringify(newConfig));
setTorrentioConfig(newConfig);
- setAlertTitle('Success');
- setAlertMessage('Torrentio addon installed successfully!');
+ setAlertTitle(t('common.success'));
+ setAlertMessage(t('debrid.success_installed'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
@@ -1061,9 +1063,9 @@ const DebridIntegrationScreen = () => {
setAlertTitle('Remove Torrentio');
setAlertMessage('Are you sure you want to remove the Torrentio addon?');
setAlertActions([
- { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
+ { label: t('common.cancel'), onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
- label: 'Remove',
+ label: t('debrid.remove_button'),
onPress: async () => {
setAlertVisible(false);
setTorrentioLoading(true);
@@ -1087,8 +1089,8 @@ const DebridIntegrationScreen = () => {
await mmkvStorage.setItem(TORRENTIO_CONFIG_KEY, JSON.stringify(newConfig));
setTorrentioConfig(newConfig);
- setAlertTitle('Success');
- setAlertMessage('Torrentio addon removed successfully');
+ setAlertTitle(t('common.success'));
+ setAlertMessage(t('debrid.success_removed'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
@@ -1114,14 +1116,14 @@ const DebridIntegrationScreen = () => {
<>
- Status
- Connected
+ {t('common.status')}
+ {t('debrid.status_connected')}
- Enable Addon
+ {t('debrid.enable_addon')}
{
disabled={loading}
>
- {loading ? 'Disconnecting...' : 'Disconnect & Remove'}
+ {loading ? t('debrid.disconnect_loading') : t('debrid.disconnect_button')}
{userData && (
- Account Information
+ {t('debrid.account_info')}
{userDataLoading && (
)}
- Email
+ {t('common.email')}
{userData.base_email || userData.email}
- Plan
+ {t('debrid.plan')}
{
styles.planBadgeText,
userData.plan === 0 ? styles.planBadgeTextFree : styles.planBadgeTextPaid
]}>
- {getPlanName(userData.plan)}
+ {getPlanName(userData.plan, t)}
- Status
+ {t('common.status')}
- {userData.is_subscribed ? 'Active' : 'Free'}
+ {userData.is_subscribed ? t('debrid.status_active') : t('debrid.plan_free')}
{userData.premium_expires_at && (
- Expires
+ {t('debrid.expires')}
{new Date(userData.premium_expires_at).toLocaleDateString()}
@@ -1193,7 +1195,7 @@ const DebridIntegrationScreen = () => {
)}
- Downloaded
+ {t('debrid.downloaded')}
{(userData.total_downloaded / (1024 * 1024 * 1024)).toFixed(2)} GB
@@ -1202,40 +1204,40 @@ const DebridIntegrationScreen = () => {
)}
- ✓ Connected to TorBox
+ {t('debrid.connected_title')}
- Your TorBox addon is active and providing premium streams.{config.isEnabled ? '' : ' (Currently disabled)'}
+ {t('debrid.connected_desc')}
- Configure Addon
+ {t('debrid.configure_title')}
- Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.
+ {t('debrid.configure_desc')}
Linking.openURL('https://torbox.app/settings?section=integration-settings')}
>
- Open Settings
+ {t('debrid.open_settings')}
>
) : (
<>
- Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.
+ {t('debrid.description_torbox')}
Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')} style={styles.guideLink}>
- What is a Debrid Service?
+ {t('debrid.what_is_debrid')}
- Torbox API Key
+ {t('debrid.api_key_label')}
{
disabled={loading}
>
- {loading ? 'Connecting...' : 'Connect & Install'}
+ {loading ? t('debrid.connecting') : t('debrid.connect_button')}
- Unlock Premium Speeds
+ {t('debrid.unlock_speeds_title')}
- Get a Torbox subscription to access cached high-quality streams with zero buffering.
+ {t('debrid.unlock_speeds_desc')}
- Get Subscription
+ {t('debrid.get_subscription')}
>
)}
- Powered by
+ {t('debrid.powered_by')}
{
/>
TorBox
- Nuvio is not affiliated with Torbox in any way.
+ {t('debrid.disclaimer_torbox')}
>
);
@@ -1290,34 +1292,34 @@ const DebridIntegrationScreen = () => {
const renderTorrentioTab = () => (
<>
- Configure Torrentio to get torrent streams for movies and TV shows. A debrid service is required to stream content.
+ {t('debrid.description_torrentio')}
{torrentioConfig.isInstalled && (
- ✓ INSTALLED
+ {t('debrid.installed_badge')}
)}
{/* TorBox Promotion Card */}
{!torrentioConfig.debridApiKey && (
- ⚡ Need a Debrid Service?
+ {t('debrid.promo_title')}
- Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.
+ {t('debrid.promo_desc')}
Linking.openURL('https://torbox.app/subscription?referral=493192f2-6403-440f-b414-768f72222ec7')}
>
- Get TorBox Subscription
+ {t('debrid.promo_button')}
)}
{/* Debrid Service Selection */}
- Debrid Service *
+ {t('debrid.service_label')}
{TORRENTIO_DEBRID_SERVICES.map((service: any) => (
{
{/* Debrid API Key */}
- API Key *
+ {t('debrid.api_key_label')}
d.id === torrentioConfig.debridService)?.name || 'Debrid'} API Key`}
@@ -1360,7 +1362,7 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('sorting')}
>
- Sorting
+ {t('debrid.sorting_label')}
{TORRENTIO_SORT_OPTIONS.find(o => o.id === torrentioConfig.sort)?.name || 'By quality'}
@@ -1391,7 +1393,7 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('qualityFilter')}
>
- Exclude Qualities
+ {t('debrid.exclude_qualities')}
{torrentioConfig.qualityFilter.length > 0 ? `${torrentioConfig.qualityFilter.length} excluded` : 'None excluded'}
@@ -1422,9 +1424,9 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('languages')}
>
- Priority Languages
+ {t('debrid.priority_languages')}
- {torrentioConfig.priorityLanguages.length > 0 ? `${torrentioConfig.priorityLanguages.length} selected` : 'No preference'}
+ {torrentioConfig.priorityLanguages.length > 0 ? `${torrentioConfig.priorityLanguages.length} ${t('home_screen.selected')}` : 'No preference'}
@@ -1453,7 +1455,7 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('maxResults')}
>
- Max Results
+ {t('debrid.max_results')}
{TORRENTIO_MAX_RESULTS.find(o => o.id === torrentioConfig.maxResults)?.name || 'All results'}
@@ -1484,7 +1486,7 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('options')}
>
- Additional Options
+ {t('debrid.additional_options')}
Catalog & download settings
@@ -1492,7 +1494,7 @@ const DebridIntegrationScreen = () => {
{expandedSections.options && (
- Don't show download links
+ {t('debrid.no_download_links')}
setTorrentioConfig(prev => ({ ...prev, noDownloadLinks: val }))}
@@ -1501,7 +1503,7 @@ const DebridIntegrationScreen = () => {
/>
- Don't show debrid catalog
+ {t('debrid.no_debrid_catalog')}
setTorrentioConfig(prev => ({ ...prev, noCatalog: val }))}
@@ -1532,7 +1534,7 @@ const DebridIntegrationScreen = () => {
disabled={torrentioLoading}
>
- {torrentioLoading ? 'Updating...' : 'Update Configuration'}
+ {torrentioLoading ? t('debrid.updating') : t('debrid.update_button')}
{
onPress={handleRemoveTorrentio}
disabled={torrentioLoading}
>
- Remove Torrentio
+ {t('debrid.remove_button')}
>
) : (
@@ -1550,14 +1552,14 @@ const DebridIntegrationScreen = () => {
disabled={torrentioLoading}
>
- {torrentioLoading ? 'Installing...' : 'Install Torrentio'}
+ {torrentioLoading ? t('debrid.installing') : t('debrid.install_button')}
)}
- Nuvio is not affiliated with Torrentio in any way.
+ {t('debrid.disclaimer_torrentio')}
>
);
@@ -1584,7 +1586,7 @@ const DebridIntegrationScreen = () => {
>
- Debrid Integration
+ {t('debrid.title')}
{/* Tab Selector */}
@@ -1594,7 +1596,7 @@ const DebridIntegrationScreen = () => {
onPress={() => setActiveTab('torbox')}
>
- TorBox
+ {t('debrid.tab_torbox')}
{
onPress={() => setActiveTab('torrentio')}
>
- Torrentio
+ {t('debrid.tab_torrentio')}
diff --git a/src/screens/HomeScreenSettings.tsx b/src/screens/HomeScreenSettings.tsx
index 6979bffe..330992e6 100644
--- a/src/screens/HomeScreenSettings.tsx
+++ b/src/screens/HomeScreenSettings.tsx
@@ -14,9 +14,11 @@ import {
Dimensions
} from 'react-native';
import { useSettings } from '../hooks/useSettings';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useTranslation } from 'react-i18next';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
-import { MaterialIcons } from '@expo/vector-icons';
+import { MaterialIcons, Feather } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { RootStackParamList } from '../navigation/AppNavigator';
@@ -107,6 +109,7 @@ const SectionHeader: React.FC<{ title: string; isDarkMode: boolean; colors: any
);
const HomeScreenSettings: React.FC = () => {
+ const { t } = useTranslation();
const { settings, updateSetting } = useSettings();
const systemColorScheme = useColorScheme();
const { currentTheme } = useTheme();
@@ -247,11 +250,11 @@ const HomeScreenSettings: React.FC = () => {
// Format selected catalogs text
const getSelectedCatalogsText = useCallback(() => {
if (!settings.selectedHeroCatalogs || settings.selectedHeroCatalogs.length === 0) {
- return "All catalogs";
+ return t("home_screen.all_catalogs");
} else {
- return `${settings.selectedHeroCatalogs.length} selected`;
+ return `${settings.selectedHeroCatalogs.length} ${t("home_screen.selected")}`;
}
- }, [settings.selectedHeroCatalogs]);
+ }, [settings.selectedHeroCatalogs, t]);
const ChevronRight = () => (
{
]}>
-
-
+ navigation.goBack()} style={styles.backButton}>
+
- Settings
+ {t('settings.title')}
@@ -285,7 +284,7 @@ const HomeScreenSettings: React.FC = () => {
- Home Screen Settings
+ {t('home_screen.title')}
{/* Saved indicator */}
@@ -300,7 +299,7 @@ const HomeScreenSettings: React.FC = () => {
pointerEvents="none"
>
- Changes Applied
+ {t('home_screen.changes_applied')}
{
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
-
+
{
)}
/>
{
/>
{settings.showHeroSection && (
{
<>
{!isTabletDevice && (
- Hero Layout
+ {t('home_screen.hero_layout')}
handleUpdateSetting('heroStyle', val as any)}
/>
- Full-width banner, swipeable cards, or Apple TV style
+ {t('home_screen.layout_desc')}
)}
- Featured Source
- Using Catalogs
+ {t('home_screen.featured_source')}
+ {t('home_screen.using_catalogs')}
navigation.navigate('HeroCatalogs')}
style={[styles.manageLink, { backgroundColor: isDarkMode ? colors.elevation1 : 'rgba(0,0,0,0.04)' }]}
activeOpacity={0.8}
>
- Manage selected catalogs
+ {t('home_screen.manage_selected_catalogs')}
@@ -384,8 +383,8 @@ const HomeScreenSettings: React.FC = () => {
{settings.heroStyle === 'carousel' && (
{
/>
)}
/>
- May impact performance on low-end devices.
+ {t('home_screen.performance_note')}
)}
>
)}
- Posters
+ {t('home_screen.posters')}
- Show Titles
+ {t('home_screen.show_titles')}
handleUpdateSetting('showPosterTitles', value)}
/>
- Poster Size
+ {t('home_screen.poster_size')}
handleUpdateSetting('posterSize', val as any)}
/>
- Poster Corners
+ {t('home_screen.poster_corners')}
handleUpdateSetting('posterBorderRadius', Number(val) as any)}
/>
-
+
- These settings control how content is displayed on your Home screen. Changes are applied immediately without requiring an app restart.
+ {t('home_screen.about_desc')}
diff --git a/src/screens/MDBListSettingsScreen.tsx b/src/screens/MDBListSettingsScreen.tsx
index 41ffe943..7eace7de 100644
--- a/src/screens/MDBListSettingsScreen.tsx
+++ b/src/screens/MDBListSettingsScreen.tsx
@@ -14,14 +14,18 @@ import {
Keyboard,
Clipboard,
Switch,
+ useColorScheme,
} from 'react-native';
-import CustomAlert from '../components/CustomAlert';
-import { useNavigation } from '@react-navigation/native';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useTranslation } from 'react-i18next';
+import { useNavigation, useFocusEffect, NavigationProp } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
+import Feather from 'react-native-vector-icons/Feather'; // Added Feather icon import
import { mmkvStorage } from '../services/mmkvStorage';
import { useTheme } from '../contexts/ThemeContext';
import { logger } from '../utils/logger';
import { RATING_PROVIDERS } from '../components/metadata/RatingsSection';
+import CustomAlert from '../components/CustomAlert'; // Moved CustomAlert import here
export const MDBLIST_API_KEY_STORAGE_KEY = 'mdblist_api_key';
export const RATING_PROVIDERS_STORAGE_KEY = 'rating_providers_config';
@@ -47,7 +51,7 @@ export const getMDBListAPIKey = async (): Promise => {
logger.log('[MDBList] MDBList is disabled, not retrieving API key');
return null;
}
-
+
return await mmkvStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
} catch (error) {
logger.error('[MDBList] Error retrieving API key:', error);
@@ -64,9 +68,9 @@ const createStyles = (colors: any) => StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
- justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
+ paddingBottom: 10,
},
backButton: {
flexDirection: 'row',
@@ -87,6 +91,11 @@ const createStyles = (colors: any) => StyleSheet.create({
paddingBottom: 16,
paddingTop: 8,
},
+ title: {
+ fontSize: 20,
+ fontWeight: '600',
+ marginLeft: 10,
+ },
content: {
flex: 1,
},
@@ -134,12 +143,20 @@ const createStyles = (colors: any) => StyleSheet.create({
statusTextContainer: {
flex: 1,
},
+ statusContent: {
+ flex: 1,
+ },
statusTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.white,
marginBottom: 2,
},
+ statusSubtitle: {
+ fontSize: 13,
+ color: colors.mediumGray,
+ lineHeight: 18,
+ },
statusDescription: {
fontSize: 13,
color: colors.mediumGray,
@@ -151,6 +168,11 @@ const createStyles = (colors: any) => StyleSheet.create({
color: colors.lightGray,
marginBottom: 10,
},
+ sectionSubtitle: {
+ fontSize: 13,
+ color: colors.mediumGray,
+ marginBottom: 12,
+ },
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
@@ -159,6 +181,17 @@ const createStyles = (colors: any) => StyleSheet.create({
borderWidth: 1,
borderColor: colors.border,
},
+ inputContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderRadius: 8,
+ borderWidth: 1,
+ paddingHorizontal: 10,
+ marginBottom: 10,
+ },
+ inputIcon: {
+ marginRight: 10,
+ },
input: {
flex: 1,
paddingVertical: 10,
@@ -197,7 +230,7 @@ const createStyles = (colors: any) => StyleSheet.create({
},
buttonContainer: {
marginTop: 12,
- gap: 10,
+ gap: 10,
},
buttonIcon: {
marginRight: 6,
@@ -212,7 +245,7 @@ const createStyles = (colors: any) => StyleSheet.create({
justifyContent: 'center',
},
saveButtonDisabled: {
- backgroundColor: colors.elevation2,
+ backgroundColor: colors.elevation2,
opacity: 0.8,
},
saveButtonText: {
@@ -242,12 +275,15 @@ const createStyles = (colors: any) => StyleSheet.create({
clearButtonTextDisabled: {
color: colors.darkGray,
},
+ buttonDisabled: {
+ opacity: 0.5,
+ },
infoHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
- infoHeaderText: {
+ infoTitle: {
fontSize: 15,
fontWeight: '600',
color: colors.white,
@@ -255,7 +291,38 @@ const createStyles = (colors: any) => StyleSheet.create({
},
infoSteps: {
marginBottom: 12,
- gap: 6,
+ gap: 6,
+ },
+ stepsContainer: {
+ marginBottom: 15,
+ },
+ stepRow: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginBottom: 10,
+ },
+ stepNumber: {
+ width: 20,
+ height: 20,
+ borderRadius: 10,
+ backgroundColor: colors.primary,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 10,
+ },
+ stepNumberText: {
+ color: colors.white,
+ fontSize: 12,
+ fontWeight: 'bold',
+ },
+ stepText: {
+ flex: 1,
+ fontSize: 13,
+ color: colors.mediumGray,
+ lineHeight: 18,
+ },
+ linkText: {
+ fontWeight: '600',
},
infoStep: {
flexDirection: 'row',
@@ -355,12 +422,19 @@ const createStyles = (colors: any) => StyleSheet.create({
},
});
-const MDBListSettingsScreen = () => {
- const navigation = useNavigation();
+interface RootStackParamList {
+ Settings: undefined;
+ // Add other routes if necessary
+}
+
+const MDBListSettingsScreen: React.FC = () => {
+ const { t } = useTranslation();
+ const navigation = useNavigation>();
const { currentTheme } = useTheme();
+ const isDarkMode = useColorScheme() === 'dark';
const colors = currentTheme.colors;
const styles = createStyles(colors);
-
+
// Custom alert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
@@ -471,38 +545,48 @@ const MDBListSettingsScreen = () => {
const saveApiKey = async () => {
logger.log('[MDBListSettingsScreen] Starting API key save');
Keyboard.dismiss();
-
+
try {
const trimmedKey = apiKey.trim();
if (!trimmedKey) {
logger.warn('[MDBListSettingsScreen] Empty API key provided');
- setTestResult({ success: false, message: 'API Key cannot be empty.' });
+ setAlertTitle(t('common.error'));
+ setAlertMessage(t('mdblist.api_key_empty_error'));
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
return;
}
logger.log('[MDBListSettingsScreen] Saving API key');
await mmkvStorage.setItem(MDBLIST_API_KEY_STORAGE_KEY, trimmedKey);
setIsKeySet(true);
- setTestResult({ success: true, message: 'API key saved successfully.' });
+ setAlertTitle(t('common.success'));
+ setAlertMessage(t('mdblist.success_saved'));
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
logger.log('[MDBListSettingsScreen] API key saved successfully');
-
+
} catch (error) {
logger.error('[MDBListSettingsScreen] Error saving API key:', error);
- setTestResult({
- success: false,
- message: 'An error occurred while saving. Please try again.'
- });
+ setAlertTitle(t('common.error'));
+ setAlertMessage(t('mdblist.error_save'));
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
}
};
- const clearApiKey = async () => {
+ const handleClear = async () => {
logger.log('[MDBListSettingsScreen] Clear API key requested');
- setAlertTitle('Clear API Key');
- setAlertMessage('Are you sure you want to remove the saved API key?');
+ setAlertTitle(t('mdblist.alert_clear_title'));
+ setAlertMessage(t('mdblist.alert_clear_msg'));
setAlertActions([
- { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
- label: 'Clear',
+ label: t('common.cancel'),
+ onPress: () => setAlertVisible(false),
+ style: { color: currentTheme.colors.mediumGray }
+ },
+ {
+ label: t('mdblist.clear'),
onPress: async () => {
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
try {
@@ -510,11 +594,15 @@ const MDBListSettingsScreen = () => {
setApiKey('');
setIsKeySet(false);
setTestResult(null);
+ setAlertTitle(t('common.success'));
+ setAlertMessage(t('mdblist.success_cleared'));
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
logger.log('[MDBListSettingsScreen] API key cleared successfully');
} catch (error) {
logger.error('[MDBListSettingsScreen] Failed to clear API key:', error);
- setAlertTitle('Error');
- setAlertMessage('Failed to clear API key');
+ setAlertTitle(t('common.error'));
+ setAlertMessage(t('mdblist.error_clear'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
@@ -554,7 +642,7 @@ const MDBListSettingsScreen = () => {
- Loading Settings...
+ {t('common.loading_settings')}
);
@@ -562,44 +650,38 @@ const MDBListSettingsScreen = () => {
return (
-
+
- navigation.goBack()}
- >
-
- Settings
+ navigation.goBack()} style={styles.backButton}>
+
+
+ {t('mdblist.title')}
+
- Rating Sources
-
-
-
-
- {!isMdbListEnabled
- ? "MDBList Disabled"
- : isKeySet
- ? "API Key Active"
- : "API Key Required"}
-
-
+
+
{!isMdbListEnabled
- ? "MDBList functionality is currently disabled."
- : isKeySet
- ? "Ratings from MDBList are enabled."
- : "Add your key below to enable ratings."}
+ ? t('mdblist.status_disabled')
+ : (isKeySet ? t('mdblist.status_active') : t('mdblist.status_required'))}
+
+
+ {!isMdbListEnabled
+ ? t('mdblist.status_disabled_desc')
+ : (isKeySet ? t('mdblist.status_active_desc') : t('mdblist.status_required_desc'))}
@@ -607,10 +689,8 @@ const MDBListSettingsScreen = () => {
- Enable MDBList
-
- Turn on/off all MDBList functionality
-
+ {t('mdblist.enable_toggle')}
+ {t('mdblist.enable_toggle_desc')}
{
- API Key
-
+
+ {t('mdblist.api_section')}
+
+
+
+
{
setApiKey(text);
if (testResult) setTestResult(null);
}}
- placeholder="Paste your MDBList API key"
+ placeholder={t('mdblist.placeholder')}
placeholderTextColor={!isMdbListEnabled ? colors.darkGray : colors.mediumGray}
autoCapitalize="none"
autoCorrect={false}
@@ -644,66 +732,67 @@ const MDBListSettingsScreen = () => {
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
editable={isMdbListEnabled}
+ secureTextEntry
/>
-
-
+
-
+
{testResult && (
-
{testResult.message}
)}
-
+
- Save
+ {t('mdblist.save')}
-
+
{isKeySet && (
-
- Clear Key
+ {t('mdblist.clear')}
)}
@@ -711,9 +800,11 @@ const MDBListSettingsScreen = () => {
- Rating Providers
-
- Choose which ratings to display in the app
+
+ {t('mdblist.rating_providers')}
+
+
+ {t('mdblist.rating_providers_desc')}
{Object.entries(RATING_PROVIDERS).map(([id, provider]) => (
@@ -738,68 +829,37 @@ const MDBListSettingsScreen = () => {
-
-
- How to get an API key
+
+
+ {t('mdblist.how_to')}
-
-
-
- 1.
+
+
+
+
+ 1
+
+
+ {t('mdblist.step_1')} {t('mdblist.step_1_link')}.
-
- Log in on the MDBList website.
+
+
+
+
+ 2
+
+
+ {t('mdblist.step_2')} {t('mdblist.step_2_settings')} {'>'} {t('mdblist.step_2_api')} {t('mdblist.step_2_end')}
-
-
-
- 2.
-
-
- Go to Settings {'>'} API section.
-
-
-
-
- 3.
-
-
- Generate a new key and copy it.
+
+
+
+
+ 3
+
+
+ {t('mdblist.step_3')}
@@ -811,29 +871,29 @@ const MDBListSettingsScreen = () => {
onPress={openMDBListWebsite}
disabled={!isMdbListEnabled}
>
-
- Go to MDBList
+ {t('mdblist.go_to_website')}
- setAlertVisible(false)}
- actions={alertActions}
- />
-
+ setAlertVisible(false)}
+ actions={alertActions}
+ />
+
);
};
diff --git a/src/screens/NotificationSettingsScreen.tsx b/src/screens/NotificationSettingsScreen.tsx
index 15568cc3..ca00b930 100644
--- a/src/screens/NotificationSettingsScreen.tsx
+++ b/src/screens/NotificationSettingsScreen.tsx
@@ -17,10 +17,12 @@ import { notificationService, NotificationSettings } from '../services/notificat
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { useNavigation } from '@react-navigation/native';
import { logger } from '../utils/logger';
+import { useTranslation } from 'react-i18next';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const NotificationSettingsScreen = () => {
+ const { t } = useTranslation();
const navigation = useNavigation();
const { currentTheme } = useTheme();
const [settings, setSettings] = useState({
@@ -47,7 +49,7 @@ const NotificationSettingsScreen = () => {
try {
const savedSettings = await notificationService.getSettings();
setSettings(savedSettings);
-
+
// Load notification stats
const stats = notificationService.getNotificationStats();
setNotificationStats(stats);
@@ -72,7 +74,7 @@ const NotificationSettingsScreen = () => {
// Add countdown effect
useEffect(() => {
let intervalId: NodeJS.Timeout;
-
+
if (countdown !== null && countdown > 0) {
intervalId = setInterval(() => {
setCountdown(prev => prev !== null ? prev - 1 : null);
@@ -96,23 +98,23 @@ const NotificationSettingsScreen = () => {
...settings,
[key]: value,
};
-
+
// Special case: if enabling notifications, make sure permissions are granted
if (key === 'enabled' && value === true) {
// Permissions are handled in the service
}
-
+
// Update settings in the service
await notificationService.updateSettings({ [key]: value });
-
+
// Update local state
setSettings(updatedSettings);
} catch (error) {
logger.error('Error updating notification settings:', error);
- setAlertTitle('Error');
- setAlertMessage('Failed to update notification settings');
- setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
- setAlertVisible(true);
+ setAlertTitle('Error');
+ setAlertMessage('Failed to update notification settings');
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
}
};
@@ -122,20 +124,20 @@ const NotificationSettingsScreen = () => {
};
const resetAllNotifications = async () => {
- setAlertTitle('Reset Notifications');
- setAlertMessage('This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?');
+ setAlertTitle(t('notification.alert_reset_title'));
+ setAlertMessage(t('notification.alert_reset_msg'));
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: currentTheme.colors.mediumGray } },
{
- label: 'Reset',
+ label: t('mdblist.reset_confirm') || 'Reset', // Using mdblist or common if available, fallback for safely
onPress: async () => {
try {
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
for (const notification of scheduledNotifications) {
await notificationService.cancelNotification(notification.id);
}
- setAlertTitle('Success');
- setAlertMessage('All notifications have been reset');
+ setAlertTitle(t('common.success') || 'Success');
+ setAlertMessage(t('notification.alert_reset_success'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
@@ -154,25 +156,25 @@ const NotificationSettingsScreen = () => {
const handleSyncNotifications = async () => {
if (isSyncing) return;
-
+
setIsSyncing(true);
try {
await notificationService.syncAllNotifications();
-
+
// Refresh stats after sync
const stats = notificationService.getNotificationStats();
setNotificationStats(stats);
-
- setAlertTitle('Sync Complete');
- setAlertMessage(`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`);
+
+ setAlertTitle(t('notification.alert_sync_complete'));
+ setAlertMessage(t('notification.alert_sync_msg', { upcoming: stats.upcoming, thisWeek: stats.thisWeek }));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Error syncing notifications:', error);
- setAlertTitle('Error');
- setAlertMessage('Failed to sync notifications. Please try again.');
- setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
- setAlertVisible(true);
+ setAlertTitle('Error');
+ setAlertMessage('Failed to sync notifications. Please try again.');
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
} finally {
setIsSyncing(false);
}
@@ -224,22 +226,22 @@ const NotificationSettingsScreen = () => {
if (notificationId) {
setTestNotificationId(notificationId);
setCountdown(0); // No countdown for instant notification
- setAlertTitle('Success');
- setAlertMessage('Test notification scheduled to fire instantly');
- setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
- setAlertVisible(true);
+ setAlertTitle(t('common.success') || 'Success');
+ setAlertMessage(t('notification.alert_test_scheduled'));
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
} else {
- setAlertTitle('Error');
- setAlertMessage('Failed to schedule test notification. Make sure notifications are enabled.');
- setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
- setAlertVisible(true);
+ setAlertTitle('Error');
+ setAlertMessage('Failed to schedule test notification. Make sure notifications are enabled.');
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
}
} catch (error) {
logger.error('Error scheduling test notification:', error);
- setAlertTitle('Error');
- setAlertMessage('Failed to schedule test notification');
- setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
- setAlertVisible(true);
+ setAlertTitle('Error');
+ setAlertMessage('Failed to schedule test notification');
+ setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
+ setAlertVisible(true);
}
};
@@ -247,13 +249,13 @@ const NotificationSettingsScreen = () => {
return (
- navigation.goBack()}
>
- Notification Settings
+ {t('notification.title')}
@@ -266,39 +268,39 @@ const NotificationSettingsScreen = () => {
return (
-
+
- navigation.goBack()}
>
- Settings
+ {t('common.settings') || 'Settings'}
-
+
{/* Empty for now, but ready for future actions */}
-
+
- Notification Settings
+ {t('notification.title')}
-
+
-
- General
-
+ {t('notification.section_general')}
+
- Enable Notifications
+ {t('notification.enable_notifications')}
{
/>
-
+
{settings.enabled && (
<>
- Notification Types
-
+ {t('notification.section_types')}
+
- New Episodes
+ {t('notification.new_episodes')}
{
thumbColor={settings.newEpisodeNotifications ? currentTheme.colors.primary : currentTheme.colors.lightGray}
/>
-
+
- Upcoming Shows
+ {t('notification.upcoming_shows')}
{
thumbColor={settings.upcomingShowsNotifications ? currentTheme.colors.primary : currentTheme.colors.lightGray}
/>
-
+
- Reminders
+ {t('notification.reminders')}
{
/>
-
+
- Notification Timing
-
+ {t('notification.section_timing')}
+
- When should you be notified before an episode airs?
+ {t('notification.timing_desc')}
-
+
{[1, 6, 12, 24].map((hours) => (
{
fontWeight: 'bold',
}
]}>
- {hours === 1 ? '1 hour' : `${hours} hours`}
+ {hours === 1 ? t('notification.hours_1') : `${hours} ${t('notification.hours_suffix')}`}
))}
-
+
- Notification Status
-
+ {t('notification.section_status')}
+
- Upcoming
+ {t('notification.stats_upcoming')}
{notificationStats.upcoming}
- This Week
+ {t('notification.stats_this_week')}
{notificationStats.thisWeek}
- Total
+ {t('notification.stats_total')}
{notificationStats.total}
- {
onPress={handleSyncNotifications}
disabled={isSyncing}
>
-
- {isSyncing ? 'Syncing...' : 'Sync Library & Trakt'}
+ {isSyncing ? t('notification.syncing') : t('notification.sync_button')}
-
+
- Automatically syncs notifications for all shows in your library and Trakt watchlist/collection.
+ {t('notification.sync_desc')}
-
+
- Advanced
-
- {t('notification.section_advanced')}
+
+ {
onPress={resetAllNotifications}
>
- Reset All Notifications
+ {t('notification.reset_button')}
- {
>
- {countdown !== null
- ? `Notification in ${countdown}s...`
- : 'Test Notification (5 sec)'}
+ {countdown !== null
+ ? t('notification.test_notification_in', { seconds: countdown })
+ : t('notification.test_button')}
{countdown !== null && (
-
- Notification will appear in {countdown} seconds
+ {t('notification.test_notification_text', { seconds: countdown })}
)}
@@ -496,14 +498,14 @@ const NotificationSettingsScreen = () => {
)}
- setAlertVisible(false)}
- actions={alertActions}
- />
-
+ setAlertVisible(false)}
+ actions={alertActions}
+ />
+
);
};
diff --git a/src/screens/PlayerSettingsScreen.tsx b/src/screens/PlayerSettingsScreen.tsx
index 3dfcc246..eafdb28f 100644
--- a/src/screens/PlayerSettingsScreen.tsx
+++ b/src/screens/PlayerSettingsScreen.tsx
@@ -15,6 +15,7 @@ import { useSettings, AppSettings } from '../hooks/useSettings';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '../contexts/ThemeContext';
import CustomAlert from '../components/CustomAlert';
+import { useTranslation } from 'react-i18next';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@@ -95,6 +96,7 @@ const PlayerSettingsScreen: React.FC = () => {
const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme();
const navigation = useNavigation();
+ const { t } = useTranslation();
// CustomAlert state
const [alertVisible, setAlertVisible] = useState(false);
@@ -110,46 +112,46 @@ const PlayerSettingsScreen: React.FC = () => {
const playerOptions = [
{
id: 'internal',
- title: 'Built-in Player',
- description: 'Use the app\'s default video player',
+ title: t('player.internal_title'),
+ description: t('player.internal_desc'),
icon: 'play-circle-outline',
},
...(Platform.OS === 'ios' ? [
{
id: 'vlc',
- title: 'VLC',
- description: 'Open streams in VLC media player',
+ title: t('player.vlc_title'),
+ description: t('player.vlc_desc'),
icon: 'video-library',
},
{
id: 'infuse',
- title: 'Infuse',
- description: 'Open streams in Infuse player',
+ title: t('player.infuse_title'),
+ description: t('player.infuse_desc'),
icon: 'smart-display',
},
{
id: 'outplayer',
- title: 'OutPlayer',
- description: 'Open streams in OutPlayer',
+ title: t('player.outplayer_title'),
+ description: t('player.outplayer_desc'),
icon: 'slideshow',
},
{
id: 'vidhub',
- title: 'VidHub',
- description: 'Open streams in VidHub player',
+ title: t('player.vidhub_title'),
+ description: t('player.vidhub_desc'),
icon: 'ondemand-video',
},
{
id: 'infuse_livecontainer',
- title: 'Infuse Livecontainer',
- description: 'Open streams in Infuse player LiveContainer',
+ title: t('player.infuse_live_title'),
+ description: t('player.infuse_live_desc'),
icon: 'smart-display',
},
] : [
{
id: 'external',
- title: 'External Player',
- description: 'Open streams in your preferred video player',
+ title: t('player.external_title'),
+ description: t('player.external_desc'),
icon: 'open-in-new',
},
]),
@@ -184,7 +186,7 @@ const PlayerSettingsScreen: React.FC = () => {
color={currentTheme.colors.text}
/>
- Settings
+ {t('common.settings') || 'Settings'}
@@ -194,7 +196,7 @@ const PlayerSettingsScreen: React.FC = () => {
- Video Player
+ {t('player.title')}
{
{ color: currentTheme.colors.textMuted },
]}
>
- PLAYER SELECTION
+ {t('player.section_selection')}
{
{ color: currentTheme.colors.textMuted },
]}
>
- PLAYBACK OPTIONS
+ {t('player.section_playback')}
{
{ color: currentTheme.colors.text },
]}
>
- Auto-play Best Stream
+ {t('player.autoplay_title')}
{
{ color: currentTheme.colors.textMuted },
]}
>
- Automatically start the highest quality stream available.
+ {t('player.autoplay_desc')}
{
{ color: currentTheme.colors.text },
]}
>
- Always Resume
+ {t('player.resume_title')}
{
{ color: currentTheme.colors.textMuted },
]}
>
- Skip the resume prompt and automatically continue where you left off (if less than 85% watched).
+ {t('player.resume_desc')}
{
{ color: currentTheme.colors.text },
]}
>
- Video Player Engine
+ {t('player.engine_title')}
{
{ color: currentTheme.colors.textMuted },
]}
>
- Auto uses ExoPlayer with MPV fallback. Some formats like Dolby Vision and HDR may not be supported by MPV, so Auto is recommended for best compatibility.
+ {t('player.engine_desc')}
@@ -416,7 +418,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
- Decoder Mode
+ {t('player.decoder_title')}
{
{ color: currentTheme.colors.textMuted },
]}
>
- How video is decoded. Auto is recommended for best balance.
+ {t('player.decoder_desc')}
@@ -440,8 +442,8 @@ const PlayerSettingsScreen: React.FC = () => {
onPress={() => {
updateSetting('decoderMode', option.id);
openAlert(
- 'Restart Required',
- 'Please restart the app for the decoder change to take effect.'
+ t('player.restart_required'),
+ t('player.restart_msg_decoder')
);
}}
style={[
@@ -482,7 +484,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
- GPU Rendering
+ {t('player.gpu_title')}
{
{ color: currentTheme.colors.textMuted },
]}
>
- GPU-Next offers better HDR and color management.
+ {t('player.gpu_desc')}
@@ -504,8 +506,8 @@ const PlayerSettingsScreen: React.FC = () => {
onPress={() => {
updateSetting('gpuMode', option.id);
openAlert(
- 'Restart Required',
- 'Please restart the app for the GPU mode change to take effect.'
+ t('player.restart_required'),
+ t('player.restart_msg_gpu')
);
}}
style={[
@@ -551,7 +553,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
- External Player for Downloads
+ {t('player.external_downloads_title')}
{
{ color: currentTheme.colors.textMuted },
]}
>
- Play downloaded content in your preferred external player.
+ {t('player.external_downloads_desc')}
{
const navigation = useNavigation();
const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme();
+ const { t } = useTranslation();
const colors = currentTheme.colors;
const styles = createStyles(colors);
@@ -1025,10 +1027,10 @@ const PluginsScreen: React.FC = () => {
);
await Promise.all(promises);
await loadPlugins();
- openAlert('Success', `${enabled ? 'Enabled' : 'Disabled'} ${filteredPlugins.length} plugins`);
+ openAlert(t('plugins.success'), `${enabled ? t('plugins.enabled') : t('plugins.disabled')} ${filteredPlugins.length} plugins`);
} catch (error) {
logger.error('[PluginSettings] Failed to bulk toggle:', error);
- openAlert('Error', 'Failed to update plugins');
+ openAlert(t('plugins.error'), 'Failed to update plugins');
} finally {
setIsRefreshing(false);
}
@@ -1048,7 +1050,7 @@ const PluginsScreen: React.FC = () => {
const url = newRepositoryUrl.trim();
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
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\nor include manifest.json:\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch/manifest.json\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/master'
);
return;
@@ -1089,10 +1091,10 @@ const PluginsScreen: React.FC = () => {
setNewRepositoryUrl('');
setShowAddRepositoryModal(false);
- openAlert('Success', 'Repository added and plugins loaded successfully');
+ openAlert(t('plugins.success'), t('plugins.alert_repo_added'));
} catch (error) {
logger.error('[PluginsScreen] Failed to add repository:', error);
- openAlert('Error', 'Failed to add repository');
+ openAlert(t('plugins.error'), 'Failed to add repository');
} finally {
setIsLoading(false);
}
@@ -1113,10 +1115,10 @@ const PluginsScreen: React.FC = () => {
await loadPlugins();
const repo = repositories.find(r => r.id === repoId);
- openAlert('Success', `Repository "${repo?.name || 'Unknown'}" ${enabled ? 'enabled' : 'disabled'} successfully`);
+ 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('Error', 'Failed to update repository');
+ openAlert(t('plugins.error'), 'Failed to update repository');
} finally {
setSwitchingRepository(null);
}
@@ -1249,10 +1251,10 @@ const PluginsScreen: React.FC = () => {
await pluginService.setRepositoryUrl(url);
await updateSetting('scraperRepositoryUrl', url);
setHasRepository(true);
- openAlert('Success', 'Repository URL saved successfully');
+ openAlert(t('plugins.success'), t('plugins.alert_repo_saved'));
} catch (error) {
logger.error('[PluginSettings] Failed to save repository:', error);
- openAlert('Error', 'Failed to save repository URL');
+ openAlert(t('plugins.error'), 'Failed to save repository URL');
} finally {
setIsLoading(false);
}
@@ -1274,7 +1276,7 @@ const PluginsScreen: React.FC = () => {
// Load fresh plugins from the updated repository
await loadPlugins();
- openAlert('Success', 'Repository refreshed successfully with latest files');
+ openAlert(t('plugins.success'), t('plugins.alert_repo_refreshed'));
} catch (error) {
logger.error('[PluginsScreen] Failed to refresh repository:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -1306,15 +1308,15 @@ const PluginsScreen: React.FC = () => {
await loadPlugins();
} catch (error) {
logger.error('[PluginSettings] Failed to toggle plugin:', error);
- openAlert('Error', 'Failed to update plugin status');
+ openAlert(t('plugins.error'), 'Failed to update plugin status');
setIsRefreshing(false);
}
};
const handleClearPlugins = () => {
openAlert(
- 'Clear All Plugins',
- 'Are you sure you want to remove all installed plugins? This action cannot be undone.',
+ t('plugins.clear_all'),
+ t('plugins.clear_all_desc'),
[
{ label: 'Cancel', onPress: () => { } },
{
@@ -1323,10 +1325,10 @@ const PluginsScreen: React.FC = () => {
try {
await pluginService.clearScrapers();
await loadPlugins();
- openAlert('Success', 'All plugins have been removed');
+ openAlert(t('plugins.success'), t('plugins.alert_plugins_cleared'));
} catch (error) {
logger.error('[PluginSettings] Failed to clear plugins:', error);
- openAlert('Error', 'Failed to clear plugins');
+ openAlert(t('plugins.error'), 'Failed to clear plugins');
}
},
},
@@ -1336,8 +1338,8 @@ const PluginsScreen: React.FC = () => {
const handleClearPluginCache = () => {
openAlert(
- 'Clear Repository Cache',
- 'This will remove the saved repository URL and clear all cached plugin data. You will need to re-enter your repository URL.',
+ t('plugins.clear_cache'),
+ t('plugins.clear_cache_desc'),
[
{ label: 'Cancel', onPress: () => { } },
{
@@ -1350,10 +1352,10 @@ const PluginsScreen: React.FC = () => {
setRepositoryUrl('');
setHasRepository(false);
await loadPlugins();
- openAlert('Success', 'Repository cache cleared successfully');
+ openAlert(t('plugins.success'), t('plugins.alert_cache_cleared'));
} catch (error) {
logger.error('[PluginSettings] Failed to clear cache:', error);
- openAlert('Error', 'Failed to clear repository cache');
+ openAlert(t('plugins.error'), 'Failed to clear repository cache');
}
},
},
diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx
index 331432c9..0b37253b 100644
--- a/src/screens/SettingsScreen.tsx
+++ b/src/screens/SettingsScreen.tsx
@@ -338,7 +338,7 @@ const SettingsScreen: React.FC = () => {
{isItemVisible('trakt') && (
}
renderControl={() => }
@@ -420,8 +420,8 @@ const SettingsScreen: React.FC = () => {
icon="trash-2"
onPress={() => {
openAlert(
- 'Clear All Data',
- 'This will reset all settings and clear all cached data. Are you sure?',
+ t('settings.clear_data'),
+ t('settings.clear_data_desc'),
[
{ label: 'Cancel', onPress: () => { } },
{
@@ -476,7 +476,7 @@ const SettingsScreen: React.FC = () => {
return (
}
@@ -565,7 +565,7 @@ const SettingsScreen: React.FC = () => {
{isItemVisible('trakt') && (
}
renderControl={() => }
@@ -648,7 +648,7 @@ const SettingsScreen: React.FC = () => {
)}
{(settingsConfig?.categories?.['updates']?.visible !== false) && (
{
{/* About */}
}