completed settingscreen localization

This commit is contained in:
tapframe 2026-01-06 13:15:07 +05:30
parent ed4950cd1f
commit 96ac361c8e
14 changed files with 934 additions and 403 deletions

View file

@ -8,7 +8,8 @@
"search": "Search",
"error": "Error",
"success": "Success",
"ok": "OK"
"ok": "OK",
"unknown": "Unknown"
},
"addons": {
"title": "Addons",
@ -31,7 +32,126 @@
"version": "Version: {{version}}",
"installed_addons": "INSTALLED ADDONS",
"reorder_drag_title": "DRAG ADDONS TO REORDER",
"install": "Install"
"install": "Install",
"config_unavailable_title": "Configuration Unavailable",
"config_unavailable_msg": "Could not determine configuration URL for this addon.",
"cannot_open_config_title": "Cannot Open Configuration",
"cannot_open_config_msg": "The configuration URL ({{url}}) cannot be opened. The addon may not have a configuration page.",
"description": "Description",
"supported_types": "Supported Types",
"catalogs": "Catalogs",
"no_description": "No description available",
"overview": "OVERVIEW",
"no_categories": "No categories",
"pre_installed": "PRE-INSTALLED"
},
"trakt": {
"title": "Trakt Settings",
"settings_title": "Trakt Settings",
"connect_title": "Connect with Trakt",
"connect_desc": "Sync your watch history, watchlist, and collection with Trakt.tv",
"sign_in": "Sign In with Trakt",
"sign_out": "Sign Out",
"sign_out_confirm": "Are you sure you want to sign out of your Trakt account?",
"joined": "Joined {{date}}",
"sync_settings_title": "Sync Settings",
"sync_info": "When connected to Trakt, full history is synced directly from the API and is not written to local storage. Your Continue Watching list reflects your global Trakt progress.",
"auto_sync_label": "Auto-sync playback progress",
"auto_sync_desc": "Automatically sync watch progress to Trakt",
"import_history_label": "Import watched history",
"import_history_desc": "Use \"Sync Now\" to import your watch history and progress from Trakt",
"sync_now_button": "Sync Now",
"display_settings_title": "Display Settings",
"show_comments_label": "Show Trakt Comments",
"show_comments_desc": "Display Trakt comments in metadata screens when available",
"maintenance_title": "Under Maintenance",
"maintenance_unavailable": "Trakt Unavailable",
"maintenance_desc": "The Trakt integration is temporarily paused for maintenance. All syncing and authentication is disabled until maintenance is complete.",
"maintenance_button": "Service Under Maintenance",
"auth_success_title": "Successfully Connected",
"auth_success_msg": "Your Trakt account has been connected successfully.",
"auth_error_title": "Authentication Error",
"auth_error_msg": "Failed to complete authentication with Trakt.",
"auth_error_generic": "An error occurred during authentication.",
"sign_out_error": "Failed to sign out of Trakt.",
"sync_complete_title": "Sync Complete",
"sync_success_msg": "Successfully synced your watch progress with Trakt.",
"sync_error_msg": "Sync failed. Please try again."
},
"tmdb_settings": {
"title": "TMDb Settings",
"metadata_enrichment": "Metadata Enrichment",
"metadata_enrichment_desc": "Enhance your content metadata with TMDb data for better details and information.",
"enable_enrichment": "Enable Enrichment",
"enable_enrichment_desc": "Augments addon metadata with TMDb for cast, certification, logos/posters, and production info.",
"localized_text": "Localized Text",
"localized_text_desc": "Fetch titles and descriptions in your preferred language from TMDb.",
"language": "Language",
"change": "Change",
"logo_preview": "Logo Preview",
"logo_preview_desc": "Preview shows how localized logos will appear in the selected language.",
"example": "Example:",
"no_logo": "No logo available",
"enrichment_options": "Enrichment Options",
"enrichment_options_desc": "Control which data is fetched from TMDb. Disabled options will use addon data if available.",
"cast_crew": "Cast & Crew",
"cast_crew_desc": "Actors, directors, writers with profile photos",
"title_description": "Title & Description",
"title_description_desc": "Use TMDb localized title and overview text",
"title_logos": "Title Logos",
"title_logos_desc": "High-quality title treatment images",
"banners_backdrops": "Banners & Backdrops",
"banners_backdrops_desc": "High-resolution backdrop images",
"certification": "Content Certification",
"certification_desc": "Age ratings (PG-13, R, TV-MA, etc.)",
"recommendations": "Recommendations",
"recommendations_desc": "Similar content suggestions",
"episode_data": "Episode Data",
"episode_data_desc": "Episode thumbnails, info & fallbacks for TV shows",
"season_posters": "Season Posters",
"season_posters_desc": "Season-specific poster images",
"production_info": "Production Info",
"production_info_desc": "Networks & production companies with logos",
"movie_details": "Movie Details",
"movie_details_desc": "Budget, revenue, runtime, tagline",
"tv_details": "TV Show Details",
"tv_details_desc": "Status, seasons count, networks, creators",
"movie_collections": "Movie Collections",
"movie_collections_desc": "Franchise movies (Marvel, Star Wars, etc.)",
"api_configuration": "API Configuration",
"api_configuration_desc": "Configure your TMDb API access for enhanced functionality.",
"custom_api_key": "Custom API Key",
"custom_api_key_desc": "Use your own TMDb API key for better performance and dedicated rate limits.",
"custom_key_active": "Custom API key active",
"api_key_required": "API key required",
"api_key_placeholder": "Paste your TMDb API key (v3)",
"how_to_get_key": "How to get a TMDb API key?",
"built_in_key_msg": "Currently using built-in API key. Consider using your own key for better performance.",
"cache_size": "Cache Size",
"clear_cache": "Clear Cache",
"cache_days": "TMDB responses are cached for 7 days to improve performance",
"choose_language": "Choose Language",
"choose_language_desc": "Select your preferred language for TMDb content",
"popular": "Popular",
"all_languages": "All Languages",
"search_results": "Search Results",
"no_languages_found": "No languages found for \"{{query}}\"",
"clear_search": "Clear Search",
"clear_cache_title": "Clear TMDB Cache",
"clear_cache_msg": "This will clear all cached TMDB data ({{size}}). This may temporarily slow down loading until cache rebuilds.",
"clear_cache_success": "TMDB cache cleared successfully.",
"clear_cache_error": "Failed to clear cache.",
"clear_api_key_title": "Clear API Key",
"clear_api_key_msg": "Are you sure you want to remove your custom API key and revert to the default?",
"clear_api_key_success": "API key cleared successfully",
"clear_api_key_error": "Failed to clear API key",
"empty_api_key": "API Key cannot be empty.",
"invalid_api_key": "Invalid API key. Please check and try again.",
"save_error": "An error occurred while saving. Please try again.",
"using_builtin_key": "Now using the built-in TMDb API key.",
"using_custom_key": "Now using your custom TMDb API key.",
"enter_custom_key": "Please enter and save your custom TMDb API key.",
"key_verified": "API key verified and saved successfully."
},
"settings": {
"language": "Language",
@ -48,6 +168,7 @@
"about": "About",
"developer": "Developer",
"cache": "Cache",
"title": "Settings",
"settings_title": "Settings",
"sign_in_sync": "Sign in to sync",
"add_catalogs_sources": "Addons, catalogs, and sources",
@ -140,8 +261,6 @@
"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"
@ -320,7 +439,7 @@
"layout_desc": "Full-width banner, swipeable cards, or Apple TV style",
"featured_source": "Featured Source",
"using_catalogs": "Using Catalogs",
"manage_catalogs": "Manage selected catalogs",
"manage_selected_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.",
@ -331,11 +450,21 @@
"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."
"corners_square": "Square",
"corners_rounded": "Rounded",
"corners_pill": "Pill",
"about_these_settings": "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.",
"hero_catalogs": {
"title": "Hero Section Catalogs",
"select_all": "Select All",
"clear_all": "Clear All",
"info": "Select which catalogs to display in the hero section. If none are selected, all catalogs will be used. Don't forget to press Save when you're done.",
"settings_saved": "Settings Saved",
"error_load": "Failed to load catalogs",
"movies": "Movies",
"tv_shows": "TV Shows"
}
},
"mdblist": {
"title": "Rating Sources",
@ -402,6 +531,86 @@
"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"
},
"backup": {
"title": "Backup & Restore",
"options_title": "Backup Options",
"options_desc": "Choose what to include in your backups",
"section_core": "Core Data",
"section_addons": "Addons & Integrations",
"section_settings": "Settings & Preferences",
"library_label": "Library",
"library_desc": "Your saved movies and TV shows",
"watch_progress_label": "Watch Progress",
"watch_progress_desc": "Continue watching positions",
"addons_label": "Addons",
"addons_desc": "Installed Stremio addons",
"plugins_label": "Plugins",
"plugins_desc": "Custom scraper configurations",
"trakt_label": "Trakt Integration",
"trakt_desc": "Sync data and authentication tokens",
"app_settings_label": "App Settings",
"app_settings_desc": "Theme, preferences, and configurations",
"user_prefs_label": "User Preferences",
"user_prefs_desc": "Addon order and UI settings",
"catalog_settings_label": "Catalog Settings",
"catalog_settings_desc": "Catalog filters and preferences",
"api_keys_label": "API Keys",
"api_keys_desc": "MDBList and OpenRouter keys",
"action_create": "Create Backup",
"action_restore": "Restore from Backup",
"section_info": "About Backups",
"info_text": "• Customize what gets backed up using the toggles above\n• Backup files are stored locally on your device\n• Share your backup to transfer data between devices\n• Restoring will overwrite your current data",
"alert_create_title": "Create Backup",
"alert_no_content": "No content selected for backup.\n\nPlease enable at least one option in the Backup Options section above.",
"alert_backup_created_title": "Backup Created",
"alert_backup_created_msg": "Your backup has been created and is ready to share.",
"alert_backup_failed_title": "Backup Failed",
"alert_restore_confirm_title": "Confirm Restore",
"alert_restore_confirm_msg": "This will restore your data from a backup created on {{date}}.\n\nThis action will overwrite your current data. Are you sure you want to continue?",
"alert_restore_complete_title": "Restore Complete",
"alert_restore_complete_msg": "Your data has been successfully restored. Please restart the app to see all changes.",
"alert_restore_failed_title": "Restore Failed",
"restart_app": "Restart App",
"alert_restart_failed_title": "Restart Failed",
"alert_restart_failed_msg": "Failed to restart the app. Please manually close and reopen the app to see your restored data."
},
"updates": {
"title": "App Updates",
"status_checking": "Checking for updates...",
"status_available": "Update available!",
"status_downloading": "Downloading update...",
"status_installing": "Installing update...",
"status_success": "Update installed successfully!",
"status_error": "Update failed",
"status_ready": "Ready to check for updates",
"action_check": "Check for Updates",
"action_install": "Install Update",
"release_notes": "Release notes:",
"version": "Version:",
"last_checked": "Last checked:",
"current_version": "Current version:",
"current_release_notes": "Current release notes:",
"github_release": "GITHUB RELEASE",
"current": "Current:",
"latest": "Latest:",
"notes": "Notes:",
"view_release": "View Release",
"notification_settings": "NOTIFICATION SETTINGS",
"ota_alerts_label": "OTA Update Alerts",
"ota_alerts_desc": "Show notifications for over-the-air updates",
"major_alerts_label": "Major Update Alerts",
"major_alerts_desc": "Show notifications for new app versions on GitHub",
"alert_disable_ota_title": "Disable OTA Update Alerts?",
"alert_disable_ota_msg": "You will no longer receive automatic notifications for OTA updates.\n\n⚠ Warning: Staying on the latest version is important for:\n• Bug fixes and stability improvements\n• New features and enhancements\n• Providing accurate feedback and crash reports\n\nYou can still manually check for updates in this screen.",
"alert_disable_major_title": "Disable Major Update Alerts?",
"alert_disable_major_msg": "You will no longer receive notifications for major app updates that require reinstallation.\n\n⚠ Warning: Major updates often include:\n• Critical security patches\n• Breaking changes that require app reinstall\n• Important compatibility fixes\n\nYou can still check for updates manually.",
"warning_note": "Keeping alerts enabled ensures you receive bug fixes and can provide accurate crash reports.",
"disable": "Disable",
"alert_no_update_to_install": "No update available to install",
"alert_install_failed": "Failed to install update",
"alert_no_update_title": "No Update",
"alert_update_applied_msg": "Update will be applied on next app restart"
},
"player": {
"title": "Video Player",
"section_selection": "PLAYER SELECTION",
@ -434,7 +643,20 @@
"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."
"restart_msg_gpu": "Please restart the app for the GPU mode change to take effect.",
"option_auto": "Auto",
"option_auto_desc_engine": "ExoPlayer + MPV fallback",
"option_mpv": "MPV",
"option_mpv_desc": "MPV only",
"option_auto_desc_decoder": "Best balance",
"option_sw": "SW",
"option_sw_desc": "Software",
"option_hw": "HW",
"option_hw_desc": "Hardware",
"option_hw_plus": "HW+",
"option_hw_plus_desc": "Full HW",
"option_gpu_desc": "Standard",
"option_gpu_next_desc": "Advanced"
},
"plugins": {
"title": "Plugins",
@ -466,6 +688,54 @@
"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."
"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.",
"add_new_repo": "Add New Repository",
"available_plugins": "Available Plugins ({{count}})",
"search_placeholder": "Search plugins...",
"all": "All",
"filter_all": "All Types",
"filter_movies": "Movies",
"filter_tv": "TV Shows",
"enable_all": "Enable All",
"disable_all": "Disable All",
"no_plugins_found": "No Plugins Found",
"no_plugins_available": "No Plugins Available",
"no_match_desc": "No plugins match \"{{query}}\". Try a different search term.",
"configure_repo_desc": "Configure a repository above to view available plugins.",
"clear_search": "Clear Search",
"no_external_player": "No external player",
"showbox_token": "ShowBox UI Token",
"showbox_placeholder": "Paste your ShowBox UI token",
"save": "Save",
"clear": "Clear",
"additional_settings": "Additional Settings",
"enable_url_validation": "Enable URL Validation",
"url_validation_desc": "Validate streaming URLs before returning them (may slow down results but improves reliability)",
"group_streams": "Group Plugin Streams",
"group_streams_desc": "When enabled, plugin streams are grouped by repository. When disabled, each plugin shows as a separate provider.",
"sort_quality": "Sort by Quality First",
"sort_quality_desc": "When enabled, streams are sorted by quality first, then by plugin. When disabled, streams are sorted by plugin first, then by quality. Only available when grouping is enabled.",
"show_logos": "Show Plugin Logos",
"show_logos_desc": "Display plugin logos next to streaming links on the streams screen.",
"quality_filtering": "Quality Filtering",
"quality_filtering_desc": "Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results.",
"excluded_qualities": "Excluded qualities:",
"language_filtering": "Language Filtering",
"language_filtering_desc": "Exclude specific languages from search results. Tap on a language to exclude it from plugin results.",
"note": "Note:",
"language_filtering_note": "This filter only applies to providers that include language information in their stream names. It does not affect other providers.",
"excluded_languages": "Excluded languages:",
"about_title": "About Plugins",
"about_desc_1": "Plugins are JavaScript modules that can search for streaming links from various sources. They run locally on your device and can be installed from trusted repositories.",
"about_desc_2": "Providers marked as \"Limited\" depend on external APIs that may stop working without notice.",
"help_title": "Getting Started with Plugins",
"help_step_1": "1. **Enable Plugins** - Turn on the main switch to allow plugins",
"help_step_2": "2. **Add Repository** - Add a GitHub raw URL or use the default repository",
"help_step_3": "3. **Refresh Repository** - Download available plugins from the repository",
"help_step_4": "4. **Enable Plugins** - Turn on the plugins you want to use for streaming",
"got_it": "Got it!",
"repo_format_hint": "Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch",
"cancel": "Cancel",
"add": "Add"
}
}

View file

@ -8,7 +8,8 @@
"search": "Buscar",
"error": "Erro",
"success": "Sucesso",
"ok": "OK"
"ok": "OK",
"unknown": "Desconhecido"
},
"addons": {
"title": "Addons",
@ -25,13 +26,110 @@
"installed_addons": "ADDONS INSTALADOS",
"reorder_drag_title": "ARRASTE PARA REORDENAR",
"install": "Instalar",
"install_success": "Addon instalado com sucesso",
"install_error": "Falha ao instalar addon",
"load_error": "Falha ao carregar addons",
"fetch_error": "Falha ao buscar detalhes do addon",
"invalid_url": "Por favor, digite uma URL de addon",
"configure": "Configurar",
"version": "Versão: {{version}}"
"config_unavailable_title": "Configuração Indisponível",
"config_unavailable_msg": "Não foi possível determinar a URL de configuração para este addon.",
"cannot_open_config_title": "Não é Possível Abrir Configuração",
"cannot_open_config_msg": "A URL de configuração ({{url}}) não pode ser aberta. O addon pode não ter uma página de configuração.",
"description": "Descrição",
"supported_types": "Tipos Suportados",
"catalogs": "Catálogos",
"no_description": "Nenhuma descrição disponível",
"overview": "VISÃO GERAL",
"no_categories": "Sem categorias",
"pre_installed": "PRÉ-INSTALADO"
},
"trakt": {
"title": "Configurações Trakt",
"settings_title": "Configurações Trakt",
"connect_title": "Conectar com Trakt",
"connect_desc": "Sincronize seu histórico, watchlist e coleção com Trakt.tv",
"sign_in": "Entrar com Trakt",
"sign_out": "Sair",
"sign_out_confirm": "Tem certeza de que deseja sair da sua conta Trakt?",
"joined": "Entrou em {{date}}",
"sync_settings_title": "Configurações de Sincronização",
"sync_info": "Quando conectado ao Trakt, o histórico completo é sincronizado diretamente da API e não é gravado no armazenamento local. Sua lista Continuar Assistindo reflete seu progresso global no Trakt.",
"auto_sync_label": "Sincronização automática",
"auto_sync_desc": "Sincronizar automaticamente o progresso com o Trakt",
"import_history_label": "Importar histórico assistido",
"import_history_desc": "Use \"Sincronizar Agora\" para importar seu histórico e progresso do Trakt",
"sync_now_button": "Sincronizar Agora",
"display_settings_title": "Configurações de Exibição",
"show_comments_label": "Mostrar Comentários Trakt",
"show_comments_desc": "Exibir comentários do Trakt nas telas de metadados quando disponível",
"maintenance_title": "Em Manutenção",
"maintenance_unavailable": "Trakt Indisponível",
"maintenance_desc": "A integração com o Trakt está temporariamente pausada para manutenção. Toda sincronização e autenticação estão desativadas até que a manutenção seja concluída.",
"maintenance_button": "Serviço em Manutenção",
"auth_success_title": "Conectado com Sucesso",
"auth_success_msg": "Sua conta Trakt foi conectada com sucesso.",
"auth_error_title": "Erro de Autenticação",
"auth_error_msg": "Falha ao completar autenticação com Trakt.",
"auth_error_generic": "Ocorreu um erro durante a autenticação.",
"sign_out_error": "Falha ao sair do Trakt.",
"sync_complete_title": "Sincronização Completa",
"sync_success_msg": "Progresso sincronizado com sucesso com o Trakt.",
"sync_error_msg": "Falha na sincronização. Tente novamente."
},
"tmdb_settings": {
"title": "Configurações do TMDb",
"metadata_enrichment": "Enriquecimento de Metadados",
"metadata_enrichment_desc": "Melhore os metadados do seu conteúdo com dados do TMDb para melhores detalhes e informações.",
"localized_text": "Texto Localizado",
"localized_text_desc": "Busque títulos e descrições no seu idioma preferido do TMDb.",
"language": "Idioma",
"change": "Alterar",
"logo_preview": "Prévia de Logo",
"logo_preview_desc": "A prévia mostra como logos localizados aparecerão no idioma selecionado.",
"example": "Exemplo:",
"enrichment_options": "Opções de Enriquecimento",
"enrichment_options_desc": "Controle quais dados são buscados do TMDb. Opções desativadas usarão dados do addon se disponíveis.",
"cast_crew": "Elenco e Equipe",
"cast_crew_desc": "Atores, diretores, escritores com fotos de perfil",
"title_description": "Título e Descrição",
"title_description_desc": "Use título e texto de visão geral localizados do TMDb",
"title_logos": "Logos de Título",
"title_logos_desc": "Imagens de tratamento de título de alta qualidade",
"banners_backdrops": "Banners e Fundos",
"banners_backdrops_desc": "Imagens de fundo de alta resolução",
"certification": "Classificação de Conteúdo",
"certification_desc": "Classificações etárias (10, 12, 14, 16, 18, etc.)",
"recommendations": "Recomendações",
"recommendations_desc": "Sugestões de conteúdo similar",
"episode_data": "Dados de Episódio",
"api_configuration": "Configuração da API",
"api_configuration_desc": "Configure seu acesso à API do TMDB",
"use_custom_api_key": "Usar Chave de API Personalizada",
"use_custom_api_key_desc": "Use sua própria chave de API do TMDB em vez da integrada",
"api_key_placeholder": "Insira sua Chave de API do TMDB",
"api_key_help": "Nota: Usar sua própria chave de API requer que o aplicativo seja reiniciado para que as alterações entrem em vigor.",
"verify_key": "Verificar e Salvar Chave",
"clear_key": "Limpar Chave Personalizada",
"language_region": "Idioma e Região",
"language_region_desc": "Defina seu idioma de conteúdo preferido",
"content_language": "Idioma do Conteúdo",
"cache_storage": "Cache e Armazenamento",
"cache_storage_desc": "Gerenciar armazenamento de dados local",
"clear_cache": "Limpar Cache TMDB",
"clear_cache_desc": "Remover todos os dados TMDB em cache",
"current_size": "Tamanho atual: {{size}}",
"about": "Sobre o TMDB",
"attribution": "Este produto usa a API do TMDB, mas não é endossado ou certificado pelo TMDB.",
"clear_cache_title": "Limpar Cache TMDB",
"clear_cache_msg": "Tem certeza de que deseja limpar o cache do TMDB? Isso removerá {{size}} de dados.",
"clear_cache_success": "Cache TMDB limpo com sucesso",
"clear_cache_error": "Falha ao limpar o cache TMDB",
"empty_api_key": "Por favor, insira uma chave de API",
"key_verified": "Chave de API verificada e salva!",
"invalid_api_key": "Chave de API inválida. Verifique e tente novamente.",
"save_error": "Erro ao salvar Chave de API",
"clear_api_key_title": "Limpar Chave de API",
"clear_api_key_msg": "Tem certeza de que deseja remover sua chave de API personalizada? O aplicativo voltará a usar a chave integrada.",
"clear_api_key_error": "Falha ao limpar chave de API",
"using_builtin_key": "Alternado para chave de API integrada",
"using_custom_key": "Alternado para chave de API personalizada",
"enter_custom_key": "Por favor, insira uma chave de API personalizada",
"no_logo": "Sem Logo"
},
"settings": {
"language": "Idioma",
@ -48,6 +146,7 @@
"about": "Sobre",
"developer": "Desenvolvedor",
"cache": "Cache",
"title": "Configurações",
"settings_title": "Configurações",
"sign_in_sync": "Faça login para sincronizar",
"add_catalogs_sources": "Addons, catálogos e fontes",
@ -140,8 +239,6 @@
"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"
@ -310,7 +407,7 @@
"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",
"manage_selected_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.",
@ -321,11 +418,21 @@
"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."
"corners_square": "Quadrado",
"corners_rounded": "Arredondado",
"corners_pill": "Pílula",
"about_these_settings": "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.",
"hero_catalogs": {
"title": "Catálogos da Seção Hero",
"select_all": "Selecionar Tudo",
"clear_all": "Limpar Tudo",
"info": "Selecione quais catálogos exibir na seção hero. Se nenhum for selecionado, todos os catálogos serão usados. Não se esqueça de pressionar Salvar quando terminar.",
"settings_saved": "Configurações Salvas",
"error_load": "Falha ao carregar catálogos",
"movies": "Filmes",
"tv_shows": "Séries e TV"
}
},
"mdblist": {
"title": "Fontes de Avaliação",
@ -415,8 +522,21 @@
"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",
"engine_desc": "Escolha o motor de reprodução de vídeo subjacente (apenas Android)",
"option_auto": "Auto",
"option_auto_desc_engine": "ExoPlayer + MPV como reserva",
"option_mpv": "MPV",
"option_mpv_desc": "Apenas MPV",
"option_auto_desc_decoder": "Melhor equilíbrio",
"option_sw": "SW",
"option_sw_desc": "Software",
"option_hw": "HW",
"option_hw_desc": "Hardware",
"option_hw_plus": "HW+",
"option_hw_plus_desc": "HW Completo",
"option_gpu_desc": "Padrão",
"option_gpu_next_desc": "Avançado",
"decoder_title": "Modo 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.",
@ -424,6 +544,86 @@
"external_downloads_desc": "Reproduzir conteúdo baixado no seu player externo preferido.",
"restart_required": "Reinicialização Necessária"
},
"backup": {
"title": "Backup e Restauração",
"options_title": "Opções de Backup",
"options_desc": "Escolha o que incluir nos seus backups",
"section_core": "Dados Principais",
"section_addons": "Addons e Integrações",
"section_settings": "Configurações e Preferências",
"library_label": "Biblioteca",
"library_desc": "Seus filmes e séries salvos",
"watch_progress_label": "Progresso Assistido",
"watch_progress_desc": "Posições de continuar assistindo",
"addons_label": "Addons",
"addons_desc": "Addons Stremio instalados",
"plugins_label": "Plugins",
"plugins_desc": "Configurações de scraper personalizadas",
"trakt_label": "Integração Trakt",
"trakt_desc": "Dados de sincronização e tokens de autenticação",
"app_settings_label": "Configurações do App",
"app_settings_desc": "Tema, preferências e configurações",
"user_prefs_label": "Preferências do Usuário",
"user_prefs_desc": "Ordem de addons e configurações de UI",
"catalog_settings_label": "Configurações de Catálogo",
"catalog_settings_desc": "Filtros e preferências de catálogo",
"api_keys_label": "Chaves API",
"api_keys_desc": "Chaves MDBList e OpenRouter",
"action_create": "Criar Backup",
"action_restore": "Restaurar de Backup",
"section_info": "Sobre Backups",
"info_text": "• Personalize o que é salvo usando as opções acima\n• Arquivos de backup são armazenados localmente no seu dispositivo\n• Compartilhe seu backup para transferir dados entre dispositivos\n• Restaurar sobrescreverá seus dados atuais",
"alert_create_title": "Criar Backup",
"alert_no_content": "Nenhum conteúdo selecionado para backup.\n\nPor favor, ative pelo menos uma opção na seção Opções de Backup acima.",
"alert_backup_created_title": "Backup Criado",
"alert_backup_created_msg": "Seu backup foi criado e está pronto para compartilhar.",
"alert_backup_failed_title": "Falha no Backup",
"alert_restore_confirm_title": "Confirmar Restauração",
"alert_restore_confirm_msg": "Isso restaurará seus dados de um backup criado em {{date}}.\n\nEsta ação sobrescreverá seus dados atuais. Tem certeza de que deseja continuar?",
"alert_restore_complete_title": "Restauração Completa",
"alert_restore_complete_msg": "Seus dados foram restaurados com sucesso. Por favor, reinicie o aplicativo para ver todas as alterações.",
"alert_restore_failed_title": "Falha na Restauração",
"restart_app": "Reiniciar App",
"alert_restart_failed_title": "Falha ao Reiniciar",
"alert_restart_failed_msg": "Falha ao reiniciar o aplicativo. Por favor, feche e reabra o aplicativo manualmente para ver seus dados restaurados."
},
"updates": {
"title": "Atualizações do App",
"status_checking": "Verificando atualizações...",
"status_available": "Atualização disponível!",
"status_downloading": "Baixando atualização...",
"status_installing": "Instalando atualização...",
"status_success": "Atualização instalada com sucesso!",
"status_error": "Falha na atualização",
"status_ready": "Pronto para verificar atualizações",
"action_check": "Verificar Atualizações",
"action_install": "Instalar Atualização",
"release_notes": "Notas de lançamento:",
"version": "Versão:",
"last_checked": "Última verificação:",
"current_version": "Versão atual:",
"current_release_notes": "Notas da versão atual:",
"github_release": "LANÇAMENTO GITHUB",
"current": "Atual:",
"latest": "Mais recente:",
"notes": "Notas:",
"view_release": "Ver Lançamento",
"notification_settings": "CONFIGURAÇÕES DE NOTIFICAÇÃO",
"ota_alerts_label": "Alertas de Atualização OTA",
"ota_alerts_desc": "Mostrar notificações para atualizações over-the-air",
"major_alerts_label": "Alertas de Grande Atualização",
"major_alerts_desc": "Mostrar notificações para novas versões do aplicativo no GitHub",
"alert_disable_ota_title": "Desativar Alertas de Atualização OTA?",
"alert_disable_ota_msg": "Você não receberá mais notificações automáticas para atualizações OTA.\n\n⚠ Aviso: Manter-se na versão mais recente é importante para:\n• Correções de bugs e melhorias de estabilidade\n• Novos recursos e aprimoramentos\n• Fornecer feedback preciso e relatórios de falhas\n\nVocê ainda pode verificar atualizações manualmente nesta tela.",
"alert_disable_major_title": "Desativar Alertas de Grande Atualização?",
"alert_disable_major_msg": "Você não receberá mais notificações para grandes atualizações do aplicativo que exigem reinstalação.\n\n⚠ Aviso: Grandes atualizações geralmente incluem:\n• Patches de segurança críticos\n• Mudanças que quebram compatibilidade e exigem reinstalação do aplicativo\n• Correções de compatibilidade importantes\n\nVocê ainda pode verificar atualizações manualmente.",
"warning_note": "Manter alertas ativados garante que você receba correções de bugs e possa fornecer relatórios de falhas precisos.",
"disable": "Desativar",
"alert_no_update_to_install": "Nenhuma atualização disponível para instalar",
"alert_install_failed": "Falha ao instalar atualização",
"alert_no_update_title": "Sem Atualização",
"alert_update_applied_msg": "A atualização será aplicada na próxima reinicialização"
},
"plugins": {
"title": "Plugins",
"enable_title": "Ativar Plugins",
@ -454,6 +654,54 @@
"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."
"clear_cache_desc": "Isso removerá a URL do repositório salvo e limpará todos os dados de plugins armazenados em cache. Você precisará digitar a URL do repositório novamente.",
"add_new_repo": "Adicionar Novo Repositório",
"available_plugins": "Plugins Disponíveis ({{count}})",
"search_placeholder": "Pesquisar plugins...",
"all": "Todos",
"filter_all": "Todos Tipos",
"filter_movies": "Filmes",
"filter_tv": "Séries",
"enable_all": "Ativar Todos",
"disable_all": "Desativar Todos",
"no_plugins_found": "Nenhum Plugin Encontrado",
"no_plugins_available": "Nenhum Plugin Disponível",
"no_match_desc": "Nenhum plugin corresponde a \"{{query}}\". Tente um termo diferente.",
"configure_repo_desc": "Configure um repositório acima para ver os plugins disponíveis.",
"clear_search": "Limpar Pesquisa",
"no_external_player": "Sem player externo",
"showbox_token": "Token UI ShowBox",
"showbox_placeholder": "Cole seu token UI do ShowBox",
"save": "Salvar",
"clear": "Limpar",
"additional_settings": "Configurações Adicionais",
"enable_url_validation": "Ativar Validação de URL",
"url_validation_desc": "Valida URLs de streaming antes de retorná-las (pode tornar os resultados mais lentos, mas melhora a confiabilidade)",
"group_streams": "Agrupar Streams de Plugins",
"group_streams_desc": "Quando ativado, streams de plugins são agrupados por repositório. Quando desativado, cada plugin aparece como um provedor separado.",
"sort_quality": "Ordenar por Qualidade Primeiro",
"sort_quality_desc": "Quando ativado, streams são ordenados por qualidade primeiro, depois por plugin. Quando desativado, streams são ordenados por plugin primeiro, então por qualidade. Disponível apenas quando o agrupamento está ativado.",
"show_logos": "Mostrar Logos de Plugins",
"show_logos_desc": "Exibe logos de plugins ao lado dos links de streaming na tela de streams.",
"quality_filtering": "Filtragem de Qualidade",
"quality_filtering_desc": "Exclua qualidades de vídeo específicas dos resultados da pesquisa. Toque em uma qualidade para excluí-la dos resultados de plugins.",
"excluded_qualities": "Qualidades excluídas:",
"language_filtering": "Filtragem de Idioma",
"language_filtering_desc": "Exclua idiomas específicos dos resultados da pesquisa. Toque em um idioma para excluí-lo dos resultados de plugins.",
"note": "Nota:",
"language_filtering_note": "Este filtro se aplica apenas a provedores que incluem informações de idioma em seus nomes de fluxo. Não afeta outros provedores.",
"excluded_languages": "Idiomas excluídos:",
"about_title": "Sobre Plugins",
"about_desc_1": "Plugins são módulos JavaScript que podem pesquisar links de streaming de várias fontes. Eles rodam localmente no seu dispositivo e podem ser instalados de repositórios confiáveis.",
"about_desc_2": "Provedores marcados como \"Limitado\" dependem de APIs externas que podem parar de funcionar sem aviso prévio.",
"help_title": "Começando com Plugins",
"help_step_1": "1. **Ativar Plugins** - Ligue o interruptor principal para permitir plugins",
"help_step_2": "2. **Adicionar Repositório** - Adicione uma URL raw do GitHub ou use o repositório padrão",
"help_step_3": "3. **Atualizar Repositório** - Baixe plugins disponíveis do repositório",
"help_step_4": "4. **Ativar Plugins** - Ligue os plugins que você deseja usar para streaming",
"got_it": "Entendi!",
"repo_format_hint": "Formato: https://raw.githubusercontent.com/username/repo/refs/heads/branch",
"cancel": "Cancelar",
"add": "Adicionar"
}
}

View file

@ -806,9 +806,9 @@ const AddonsScreen = () => {
// If we couldn't determine a config URL, show an error
if (!configUrl) {
logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`);
setAlertTitle('Configuration Unavailable');
setAlertMessage('Could not determine configuration URL for this addon.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertTitle(t('addons.config_unavailable_title'));
setAlertMessage(t('addons.config_unavailable_msg'));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
}
@ -822,16 +822,16 @@ const AddonsScreen = () => {
Linking.openURL(configUrl);
} else {
logger.error(`URL cannot be opened: ${configUrl}`);
setAlertTitle('Cannot Open Configuration');
setAlertMessage(`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`);
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertTitle(t('addons.cannot_open_config_title'));
setAlertMessage(t('addons.cannot_open_config_msg', { url: configUrl }));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
}).catch(err => {
logger.error(`Error checking if URL can be opened: ${configUrl}`, err);
setAlertTitle('Error');
setAlertMessage('Could not open configuration page.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertTitle(t('common.error'));
setAlertMessage(t('addons.cannot_open_config_msg', { url: configUrl }));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
});
};
@ -853,7 +853,7 @@ const AddonsScreen = () => {
// Format the types into a simple category text
const categoryText = types.length > 0
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
: 'No categories';
: t('addons.no_categories');
const isFirstItem = index === 0;
const isLastItem = index === addons.length - 1;
@ -904,7 +904,7 @@ const AddonsScreen = () => {
<Text style={styles.addonName}>{item.name}</Text>
{isPreInstalled && (
<View style={[styles.priorityBadge, { marginLeft: 8, backgroundColor: colors.success }]}>
<Text style={[styles.priorityText, { fontSize: 10 }]}>PRE-INSTALLED</Text>
<Text style={[styles.priorityText, { fontSize: 10 }]}>{t('addons.pre_installed')}</Text>
</View>
)}
</View>
@ -1025,7 +1025,7 @@ const AddonsScreen = () => {
{/* Overview Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>OVERVIEW</Text>
<Text style={styles.sectionTitle}>{t('addons.overview')}</Text>
<View style={styles.statsContainer}>
<StatsCard value={addons.length} label={t('addons.title')} />
<View style={styles.statsDivider} />
@ -1149,15 +1149,15 @@ const AddonsScreen = () => {
</View>
<View style={styles.addonDetailSection}>
<Text style={styles.addonDetailSectionTitle}>Description</Text>
<Text style={styles.addonDetailSectionTitle}>{t('addons.description')}</Text>
<Text style={styles.addonDetailDescription}>
{addonDetails.description || 'No description available'}
{addonDetails.description || t('addons.no_description')}
</Text>
</View>
{addonDetails.types && addonDetails.types.length > 0 && (
<View style={styles.addonDetailSection}>
<Text style={styles.addonDetailSectionTitle}>Supported Types</Text>
<Text style={styles.addonDetailSectionTitle}>{t('addons.supported_types')}</Text>
<View style={styles.addonDetailChips}>
{addonDetails.types.map((type, index) => (
<View key={index} style={styles.addonDetailChip}>
@ -1170,7 +1170,7 @@ const AddonsScreen = () => {
{addonDetails.catalogs && addonDetails.catalogs.length > 0 && (
<View style={styles.addonDetailSection}>
<Text style={styles.addonDetailSectionTitle}>Catalogs</Text>
<Text style={styles.addonDetailSectionTitle}>{t('addons.catalogs')}</Text>
<View style={styles.addonDetailChips}>
{addonDetails.catalogs.map((catalog, index) => (
<View key={index} style={styles.addonDetailChip}>

View file

@ -23,12 +23,14 @@ import { useTheme } from '../contexts/ThemeContext';
import { logger } from '../utils/logger';
import CustomAlert from '../components/CustomAlert';
import { useBackupOptions } from '../hooks/useBackupOptions';
import { useTranslation } from 'react-i18next';
const BackupScreen: React.FC = () => {
const { currentTheme } = useTheme();
const [isLoading, setIsLoading] = useState(false);
const navigation = useNavigation();
const { preferences, updatePreference, getBackupOptions } = useBackupOptions();
const { t } = useTranslation();
// Collapsible sections state
const [expandedSections, setExpandedSections] = useState({
@ -60,7 +62,7 @@ const BackupScreen: React.FC = () => {
) => {
setAlertTitle(title);
setAlertMessage(message);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]);
setAlertVisible(true);
};
@ -71,9 +73,9 @@ const BackupScreen: React.FC = () => {
logger.error('[BackupScreen] Failed to restart app:', error);
// Fallback: show error message
openAlert(
'Restart Failed',
'Failed to restart the app. Please manually close and reopen the app to see your restored data.',
[{ label: 'OK', onPress: () => { } }]
t('backup.alert_restart_failed_title'),
t('backup.alert_restart_failed_msg'),
[{ label: t('common.ok'), onPress: () => { } }]
);
}
};
@ -128,12 +130,12 @@ const BackupScreen: React.FC = () => {
let total = 0;
if (preferences.includeLibrary) {
items.push(`Library: ${preview.library} items`);
items.push(`${t('backup.library_label')}: ${preview.library} items`);
total += preview.library;
}
if (preferences.includeWatchProgress) {
items.push(`Watch Progress: ${preview.watchProgress} entries`);
items.push(`${t('backup.watch_progress_label')}: ${preview.watchProgress} entries`);
total += preview.watchProgress;
// Include watched status with watch progress
items.push(`Watched Status: ${preview.watchedStatus} items`);
@ -141,28 +143,28 @@ const BackupScreen: React.FC = () => {
}
if (preferences.includeAddons) {
items.push(`Addons: ${preview.addons} installed`);
items.push(`${t('backup.addons_label')}: ${preview.addons} installed`);
total += preview.addons;
}
if (preferences.includeLocalScrapers) {
items.push(`Plugins: ${preview.scrapers} configurations`);
items.push(`${t('backup.plugins_label')}: ${preview.scrapers} configurations`);
total += preview.scrapers;
}
// Check if no items are selected
const message = items.length > 0
? `Backup Contents:\n\n${items.join('\n')}\n\nTotal: ${total} items\n\nThis backup includes your selected app settings, themes, watched markers, and integration data.`
: `No content selected for backup.\n\nPlease enable at least one option in the Backup Options section above.`;
: t('backup.alert_no_content');
openAlert(
'Create Backup',
t('backup.alert_create_title'),
message,
items.length > 0
? [
{ label: 'Cancel', onPress: () => { } },
{ label: t('common.cancel'), onPress: () => { } },
{
label: 'Create Backup',
label: t('backup.action_create'),
onPress: async () => {
try {
setIsLoading(true);
@ -180,16 +182,16 @@ const BackupScreen: React.FC = () => {
}
openAlert(
'Backup Created',
'Your backup has been created and is ready to share.',
[{ label: 'OK', onPress: () => { } }]
t('backup.alert_backup_created_title'),
t('backup.alert_backup_created_msg'),
[{ label: t('common.ok'), onPress: () => { } }]
);
} catch (error) {
logger.error('[BackupScreen] Failed to create backup:', error);
openAlert(
'Backup Failed',
t('backup.alert_backup_failed_title'),
`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`,
[{ label: 'OK', onPress: () => { } }]
[{ label: t('common.ok'), onPress: () => { } }]
);
} finally {
setIsLoading(false);
@ -197,18 +199,18 @@ const BackupScreen: React.FC = () => {
}
}
]
: [{ label: 'OK', onPress: () => { } }]
: [{ label: t('common.ok'), onPress: () => { } }]
);
} catch (error) {
logger.error('[BackupScreen] Failed to get backup preview:', error);
openAlert(
'Error',
t('common.error'),
'Failed to prepare backup information. Please try again.',
[{ label: 'OK', onPress: () => { } }]
[{ label: t('common.ok'), onPress: () => { } }]
);
setIsLoading(false);
}
}, [openAlert, preferences, getBackupOptions]);
}, [openAlert, preferences, getBackupOptions, t]);
// Restore backup
const handleRestoreBackup = useCallback(async () => {
@ -228,10 +230,12 @@ const BackupScreen: React.FC = () => {
const backupInfo = await backupService.getBackupInfo(fileUri);
openAlert(
'Confirm Restore',
`This will restore your data from a backup created on ${new Date(backupInfo.timestamp || 0).toLocaleDateString()}.\n\nThis action will overwrite your current data. Are you sure you want to continue?`,
t('backup.alert_restore_confirm_title'),
t('backup.alert_restore_confirm_msg', {
date: new Date(backupInfo.timestamp || 0).toLocaleDateString()
}),
[
{ label: 'Cancel', onPress: () => { } },
{ label: t('common.cancel'), onPress: () => { } },
{
label: 'Restore',
onPress: async () => {
@ -243,12 +247,12 @@ const BackupScreen: React.FC = () => {
await backupService.restoreBackup(fileUri, restoreOptions);
openAlert(
'Restore Complete',
'Your data has been successfully restored. Please restart the app to see all changes.',
t('backup.alert_restore_complete_title'),
t('backup.alert_restore_complete_msg'),
[
{ label: 'Cancel', onPress: () => { } },
{ label: t('common.cancel'), onPress: () => { } },
{
label: 'Restart App',
label: t('backup.restart_app'),
onPress: restartApp,
style: { fontWeight: 'bold' }
}
@ -257,9 +261,9 @@ const BackupScreen: React.FC = () => {
} catch (error) {
logger.error('[BackupScreen] Failed to restore backup:', error);
openAlert(
'Restore Failed',
t('backup.alert_restore_failed_title'),
`Failed to restore backup: ${error instanceof Error ? error.message : String(error)}`,
[{ label: 'OK', onPress: () => { } }]
[{ label: t('common.ok'), onPress: () => { } }]
);
} finally {
setIsLoading(false);
@ -273,10 +277,10 @@ const BackupScreen: React.FC = () => {
openAlert(
'File Selection Failed',
`Failed to select backup file: ${error instanceof Error ? error.message : String(error)}`,
[{ label: 'OK', onPress: () => { } }]
[{ label: t('common.ok'), onPress: () => { } }]
);
}
}, [openAlert]);
}, [openAlert, t]);
return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
@ -289,7 +293,7 @@ const BackupScreen: React.FC = () => {
onPress={() => navigation.goBack()}
>
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.white} />
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>{t('settings.settings_title')}</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
@ -298,7 +302,7 @@ const BackupScreen: React.FC = () => {
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>
Backup & Restore
{t('backup.title')}
</Text>
{/* Content */}
@ -319,10 +323,10 @@ const BackupScreen: React.FC = () => {
{/* Backup Options Section */}
<View style={[styles.section, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
Backup Options
{t('backup.options_title')}
</Text>
<Text style={[styles.sectionDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Choose what to include in your backups
{t('backup.options_desc')}
</Text>
{/* Core Data Group */}
@ -332,7 +336,7 @@ const BackupScreen: React.FC = () => {
activeOpacity={0.7}
>
<Text style={[styles.groupLabel, { color: currentTheme.colors.highEmphasis }]}>
Core Data
{t('backup.section_core')}
</Text>
<Animated.View
style={{
@ -358,15 +362,15 @@ const BackupScreen: React.FC = () => {
}}
>
<OptionToggle
label="Library"
description="Your saved movies and TV shows"
label={t('backup.library_label')}
description={t('backup.library_desc')}
value={preferences.includeLibrary}
onValueChange={(v) => updatePreference('includeLibrary', v)}
theme={currentTheme}
/>
<OptionToggle
label="Watch Progress"
description="Continue watching positions"
label={t('backup.watch_progress_label')}
description={t('backup.watch_progress_desc')}
value={preferences.includeWatchProgress}
onValueChange={(v) => updatePreference('includeWatchProgress', v)}
theme={currentTheme}
@ -380,7 +384,7 @@ const BackupScreen: React.FC = () => {
activeOpacity={0.7}
>
<Text style={[styles.groupLabel, { color: currentTheme.colors.highEmphasis }]}>
Addons & Integrations
{t('backup.section_addons')}
</Text>
<Animated.View
style={{
@ -406,22 +410,22 @@ const BackupScreen: React.FC = () => {
}}
>
<OptionToggle
label="Addons"
description="Installed Stremio addons"
label={t('backup.addons_label')}
description={t('backup.addons_desc')}
value={preferences.includeAddons}
onValueChange={(v) => updatePreference('includeAddons', v)}
theme={currentTheme}
/>
<OptionToggle
label="Plugins"
description="Custom scraper configurations"
label={t('backup.plugins_label')}
description={t('backup.plugins_desc')}
value={preferences.includeLocalScrapers}
onValueChange={(v) => updatePreference('includeLocalScrapers', v)}
theme={currentTheme}
/>
<OptionToggle
label="Trakt Integration"
description="Sync data and authentication tokens"
label={t('backup.trakt_label')}
description={t('backup.trakt_desc')}
value={preferences.includeTraktData}
onValueChange={(v) => updatePreference('includeTraktData', v)}
theme={currentTheme}
@ -435,7 +439,7 @@ const BackupScreen: React.FC = () => {
activeOpacity={0.7}
>
<Text style={[styles.groupLabel, { color: currentTheme.colors.highEmphasis }]}>
Settings & Preferences
{t('backup.section_settings')}
</Text>
<Animated.View
style={{
@ -461,29 +465,29 @@ const BackupScreen: React.FC = () => {
}}
>
<OptionToggle
label="App Settings"
description="Theme, preferences, and configurations"
label={t('backup.app_settings_label')}
description={t('backup.app_settings_desc')}
value={preferences.includeSettings}
onValueChange={(v) => updatePreference('includeSettings', v)}
theme={currentTheme}
/>
<OptionToggle
label="User Preferences"
description="Addon order and UI settings"
label={t('backup.user_prefs_label')}
description={t('backup.user_prefs_desc')}
value={preferences.includeUserPreferences}
onValueChange={(v) => updatePreference('includeUserPreferences', v)}
theme={currentTheme}
/>
<OptionToggle
label="Catalog Settings"
description="Catalog filters and preferences"
label={t('backup.catalog_settings_label')}
description={t('backup.catalog_settings_desc')}
value={preferences.includeCatalogSettings}
onValueChange={(v) => updatePreference('includeCatalogSettings', v)}
theme={currentTheme}
/>
<OptionToggle
label="API Keys"
description="MDBList and OpenRouter keys"
label={t('backup.api_keys_label')}
description={t('backup.api_keys_desc')}
value={preferences.includeApiKeys}
onValueChange={(v) => updatePreference('includeApiKeys', v)}
theme={currentTheme}
@ -494,7 +498,7 @@ const BackupScreen: React.FC = () => {
{/* Backup Actions */}
<View style={[styles.section, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
Backup & Restore
{t('backup.title')}
</Text>
<TouchableOpacity
@ -513,7 +517,7 @@ const BackupScreen: React.FC = () => {
) : (
<>
<MaterialIcons name="backup" size={20} color="white" />
<Text style={styles.actionButtonText}>Create Backup</Text>
<Text style={styles.actionButtonText}>{t('backup.action_create')}</Text>
</>
)}
</TouchableOpacity>
@ -530,20 +534,17 @@ const BackupScreen: React.FC = () => {
disabled={isLoading}
>
<MaterialIcons name="restore" size={20} color="white" />
<Text style={styles.actionButtonText}>Restore from Backup</Text>
<Text style={styles.actionButtonText}>{t('backup.action_restore')}</Text>
</TouchableOpacity>
</View>
{/* Info Section */}
<View style={[styles.section, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
About Backups
{t('backup.section_info')}
</Text>
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
Customize what gets backed up using the toggles above{'\n'}
Backup files are stored locally on your device{'\n'}
Share your backup to transfer data between devices{'\n'}
Restoring will overwrite your current data
{t('backup.info_text')}
</Text>
</View>
</View>

View file

@ -834,8 +834,8 @@ const DebridIntegrationScreen = () => {
const handleConnect = async () => {
if (!apiKey.trim()) {
setAlertTitle(t('common.error'));
setAlertMessage(t('debrid.error_api_required')); // Reusing key or common error
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertMessage(t('debrid.error_api_required'));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
}
@ -863,14 +863,14 @@ const DebridIntegrationScreen = () => {
setApiKey('');
setAlertTitle(t('common.success'));
setAlertMessage(t('debrid.connected_title')); // Or similar success message
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertMessage(t('debrid.connected_title'));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Failed to install Torbox addon:', error);
setAlertTitle('Error');
setAlertMessage('Failed to connect addon. Please check your API Key and try again.');
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 {
setLoading(false);
@ -893,7 +893,7 @@ const DebridIntegrationScreen = () => {
setAlertTitle(t('debrid.alert_disconnect_title'));
setAlertMessage(t('debrid.alert_disconnect_msg'));
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{ label: t('common.cancel'), onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
label: t('debrid.disconnect_button'),
onPress: async () => {
@ -915,15 +915,15 @@ const DebridIntegrationScreen = () => {
setConfig(null);
setUserData(null);
setAlertTitle('Success');
setAlertMessage('Torbox disconnected successfully');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertTitle(t('common.success'));
setAlertMessage(t('debrid.alert_disconnect_success', 'Torbox disconnected successfully'));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Failed to disconnect Torbox:', error);
setAlertTitle('Error');
setAlertMessage('Failed to disconnect Torbox');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertTitle(t('common.error'));
setAlertMessage(t('debrid.alert_disconnect_error', 'Failed to disconnect Torbox'));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setLoading(false);
@ -1011,7 +1011,7 @@ const DebridIntegrationScreen = () => {
if (!torrentioConfig.debridApiKey.trim()) {
setAlertTitle(t('debrid.error_api_required'));
setAlertMessage(t('debrid.error_api_required_desc'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
}
@ -1046,13 +1046,13 @@ const DebridIntegrationScreen = () => {
setAlertTitle(t('common.success'));
setAlertMessage(t('debrid.success_installed'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Failed to install Torrentio addon:', error);
setAlertTitle('Error');
setAlertMessage('Failed to install Torrentio addon. Please try again.');
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 {
setTorrentioLoading(false);
@ -1060,8 +1060,8 @@ const DebridIntegrationScreen = () => {
};
const handleRemoveTorrentio = async () => {
setAlertTitle('Remove Torrentio');
setAlertMessage('Are you sure you want to remove the Torrentio addon?');
setAlertTitle(t('debrid.remove_button'));
setAlertMessage(t('addons.uninstall_message', { name: 'Torrentio' }));
setAlertActions([
{ label: t('common.cancel'), onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
@ -1091,13 +1091,13 @@ const DebridIntegrationScreen = () => {
setAlertTitle(t('common.success'));
setAlertMessage(t('debrid.success_removed'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Failed to remove Torrentio:', error);
setAlertTitle('Error');
setAlertMessage('Failed to remove Torrentio addon');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertTitle(t('common.error'));
setAlertMessage(t('addons.uninstall_error', 'Failed to remove Torrentio addon'));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setTorrentioLoading(false);
@ -1364,7 +1364,7 @@ const DebridIntegrationScreen = () => {
<View>
<Text style={styles.accordionHeaderText}>{t('debrid.sorting_label')}</Text>
<Text style={styles.accordionSubtext}>
{TORRENTIO_SORT_OPTIONS.find(o => o.id === torrentioConfig.sort)?.name || 'By quality'}
{TORRENTIO_SORT_OPTIONS.find(o => o.id === torrentioConfig.sort)?.name || t('debrid.by_quality', 'By quality')}
</Text>
</View>
<Feather name={expandedSections.sorting ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
@ -1395,7 +1395,7 @@ const DebridIntegrationScreen = () => {
<View>
<Text style={styles.accordionHeaderText}>{t('debrid.exclude_qualities')}</Text>
<Text style={styles.accordionSubtext}>
{torrentioConfig.qualityFilter.length > 0 ? `${torrentioConfig.qualityFilter.length} excluded` : 'None excluded'}
{torrentioConfig.qualityFilter.length > 0 ? t('debrid.excluded_count', { count: torrentioConfig.qualityFilter.length, defaultValue: '{{count}} excluded' }) : t('debrid.none_excluded', 'None excluded')}
</Text>
</View>
<Feather name={expandedSections.qualityFilter ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
@ -1426,7 +1426,7 @@ const DebridIntegrationScreen = () => {
<View>
<Text style={styles.accordionHeaderText}>{t('debrid.priority_languages')}</Text>
<Text style={styles.accordionSubtext}>
{torrentioConfig.priorityLanguages.length > 0 ? `${torrentioConfig.priorityLanguages.length} ${t('home_screen.selected')}` : 'No preference'}
{torrentioConfig.priorityLanguages.length > 0 ? `${torrentioConfig.priorityLanguages.length} ${t('home_screen.selected')}` : t('debrid.no_preference', 'No preference')}
</Text>
</View>
<Feather name={expandedSections.languages ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
@ -1457,7 +1457,7 @@ const DebridIntegrationScreen = () => {
<View>
<Text style={styles.accordionHeaderText}>{t('debrid.max_results')}</Text>
<Text style={styles.accordionSubtext}>
{TORRENTIO_MAX_RESULTS.find(o => o.id === torrentioConfig.maxResults)?.name || 'All results'}
{TORRENTIO_MAX_RESULTS.find(o => o.id === torrentioConfig.maxResults)?.name || t('debrid.all_results', 'All results')}
</Text>
</View>
<Feather name={expandedSections.maxResults ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
@ -1487,7 +1487,7 @@ const DebridIntegrationScreen = () => {
>
<View>
<Text style={styles.accordionHeaderText}>{t('debrid.additional_options')}</Text>
<Text style={styles.accordionSubtext}>Catalog & download settings</Text>
<Text style={styles.accordionSubtext}>{t('debrid.catalog_download_settings', 'Catalog & download settings')}</Text>
</View>
<Feather name={expandedSections.options ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
</TouchableOpacity>

View file

@ -20,6 +20,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../styles/colors';
import { catalogService, StreamingAddon } from '../services/catalogService';
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
import { useTranslation } from 'react-i18next';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@ -31,6 +32,7 @@ interface CatalogItem {
}
const HeroCatalogsScreen: React.FC = () => {
const { t } = useTranslation();
const { settings, updateSetting } = useSettings();
const systemColorScheme = useColorScheme();
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
@ -60,7 +62,7 @@ const HeroCatalogsScreen: React.FC = () => {
// Refresh selected catalogs when settings change
setSelectedCatalogs(settings.selectedHeroCatalogs || []);
});
return unsubscribe;
}, [settings.selectedHeroCatalogs]);
@ -86,10 +88,10 @@ const HeroCatalogsScreen: React.FC = () => {
const handleSave = useCallback(() => {
// First update the settings
updateSetting('selectedHeroCatalogs', selectedCatalogs);
// Show the confirmation indicator
setShowSavedIndicator(true);
// Short delay before navigating back to allow settings to save
// and the user to see the confirmation message
setTimeout(() => {
@ -108,7 +110,7 @@ const HeroCatalogsScreen: React.FC = () => {
try {
const addons = await catalogService.getAllAddons();
const catalogItems: CatalogItem[] = [];
addons.forEach(addon => {
if (addon.catalogs && addon.catalogs.length > 0) {
addon.catalogs.forEach(catalog => {
@ -121,19 +123,19 @@ const HeroCatalogsScreen: React.FC = () => {
});
}
});
setCatalogs(catalogItems);
} catch (error) {
if (__DEV__) console.error('Failed to load catalogs:', error);
setAlertTitle('Error');
setAlertMessage('Failed to load catalogs');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertTitle(t('common.error'));
setAlertMessage(t('home_screen.hero_catalogs.error_load'));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setLoading(false);
}
};
loadCatalogs();
}, []);
@ -172,22 +174,22 @@ const HeroCatalogsScreen: React.FC = () => {
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity onPress={handleBack} style={styles.backButton}>
<MaterialIcons
name="arrow-back"
size={24}
color={isDarkMode ? colors.highEmphasis : colors.textDark}
<MaterialIcons
name="arrow-back"
size={24}
color={isDarkMode ? colors.highEmphasis : colors.textDark}
/>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
Hero Section Catalogs
{t('home_screen.hero_catalogs.title')}
</Text>
</View>
{/* Saved indicator */}
<Animated.View
<Animated.View
style={[
styles.savedIndicator,
{
styles.savedIndicator,
{
opacity: fadeAnim,
backgroundColor: isDarkMode ? 'rgba(0, 180, 150, 0.9)' : 'rgba(0, 180, 150, 0.9)'
}
@ -195,47 +197,47 @@ const HeroCatalogsScreen: React.FC = () => {
pointerEvents="none"
>
<MaterialIcons name="check-circle" size={20} color="#FFFFFF" />
<Text style={styles.savedIndicatorText}>Settings Saved</Text>
<Text style={styles.savedIndicatorText}>{t('home_screen.hero_catalogs.settings_saved')}</Text>
</Animated.View>
{loading || isLoadingCustomNames ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={[styles.loadingText, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
Loading catalogs...
{t('common.loading')}
</Text>
</View>
) : (
<>
<View style={styles.actionBar}>
<TouchableOpacity
style={[styles.actionButton, { backgroundColor: isDarkMode ? colors.elevation2 : colors.white }]}
<TouchableOpacity
style={[styles.actionButton, { backgroundColor: isDarkMode ? colors.elevation2 : colors.white }]}
onPress={handleSelectAll}
>
<Text style={[styles.actionButtonText, { color: colors.primary }]}>Select All</Text>
<Text style={[styles.actionButtonText, { color: colors.primary }]}>{t('home_screen.hero_catalogs.select_all')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, { backgroundColor: isDarkMode ? colors.elevation2 : colors.white }]}
<TouchableOpacity
style={[styles.actionButton, { backgroundColor: isDarkMode ? colors.elevation2 : colors.white }]}
onPress={handleSelectNone}
>
<Text style={[styles.actionButtonText, { color: colors.primary }]}>Clear All</Text>
<Text style={[styles.actionButtonText, { color: colors.primary }]}>{t('home_screen.hero_catalogs.clear_all')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.saveButton, { backgroundColor: colors.primary }]}
<TouchableOpacity
style={[styles.saveButton, { backgroundColor: colors.primary }]}
onPress={handleSave}
>
<MaterialIcons name="save" size={16} color={colors.white} style={styles.saveIcon} />
<Text style={styles.saveButtonText}>Save</Text>
<Text style={styles.saveButtonText}>{t('common.save')}</Text>
</TouchableOpacity>
</View>
<View style={styles.infoCard}>
<Text style={[styles.infoText, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
Select which catalogs to display in the hero section. If none are selected, all catalogs will be used. Don't forget to press Save when you're done.
{t('home_screen.hero_catalogs.info')}
</Text>
</View>
<ScrollView
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
@ -246,13 +248,13 @@ const HeroCatalogsScreen: React.FC = () => {
{addonName}
</Text>
<View style={[
styles.catalogsContainer,
styles.catalogsContainer,
{ backgroundColor: isDarkMode ? colors.elevation1 : colors.white }
]}>
{addonCatalogs.map(catalog => {
const [addonId, type, catalogId] = catalog.id.split(':');
const displayName = getCustomName(addonId, type, catalogId, catalog.name);
return (
<TouchableOpacity
key={catalog.id}
@ -267,7 +269,7 @@ const HeroCatalogsScreen: React.FC = () => {
{displayName}
</Text>
<Text style={[styles.catalogType, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
{catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
{catalog.type === 'movie' ? t('home_screen.hero_catalogs.movies') : t('home_screen.hero_catalogs.tv_shows')}
</Text>
</View>
<MaterialIcons
@ -284,14 +286,14 @@ const HeroCatalogsScreen: React.FC = () => {
</ScrollView>
</>
)}
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -383,8 +383,8 @@ const HomeScreenSettings: React.FC = () => {
{settings.heroStyle === 'carousel' && (
<SettingsCard isDarkMode={isDarkMode} colors={colors}>
<SettingItem
title={t("home_screen.dynamic_hero_background")}
description={t("home_screen.dynamic_hero_background_desc")}
title={t("home_screen.dynamic_bg")}
description={t("home_screen.dynamic_bg_desc")}
icon="wallpaper"
isDarkMode={isDarkMode}
colors={colors}

View file

@ -552,7 +552,7 @@ const MDBListSettingsScreen: React.FC = () => {
logger.warn('[MDBListSettingsScreen] Empty API key provided');
setAlertTitle(t('common.error'));
setAlertMessage(t('mdblist.api_key_empty_error'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
}
@ -562,7 +562,7 @@ const MDBListSettingsScreen: React.FC = () => {
setIsKeySet(true);
setAlertTitle(t('common.success'));
setAlertMessage(t('mdblist.success_saved'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
logger.log('[MDBListSettingsScreen] API key saved successfully');
@ -570,7 +570,7 @@ const MDBListSettingsScreen: React.FC = () => {
logger.error('[MDBListSettingsScreen] Error saving API key:', error);
setAlertTitle(t('common.error'));
setAlertMessage(t('mdblist.error_save'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
};
@ -596,14 +596,14 @@ const MDBListSettingsScreen: React.FC = () => {
setTestResult(null);
setAlertTitle(t('common.success'));
setAlertMessage(t('mdblist.success_cleared'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.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(t('common.error'));
setAlertMessage(t('mdblist.error_clear'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
},

View file

@ -373,8 +373,8 @@ const PlayerSettingsScreen: React.FC = () => {
</View>
<View style={styles.optionButtonsRow}>
{([
{ id: 'auto', label: 'Auto', desc: 'ExoPlayer + MPV fallback' },
{ id: 'mpv', label: 'MPV', desc: 'MPV only' },
{ id: 'auto', label: t('player.option_auto'), desc: t('player.option_auto_desc_engine') },
{ id: 'mpv', label: t('player.option_mpv'), desc: t('player.option_mpv_desc') },
] as const).map((option) => (
<TouchableOpacity
key={option.id}
@ -432,10 +432,10 @@ const PlayerSettingsScreen: React.FC = () => {
</View>
<View style={styles.optionButtonsRow}>
{([
{ id: 'auto', label: 'Auto', desc: 'Best balance' },
{ id: 'sw', label: 'SW', desc: 'Software' },
{ id: 'hw', label: 'HW', desc: 'Hardware' },
{ id: 'hw+', label: 'HW+', desc: 'Full HW' },
{ id: 'auto', label: t('player.option_auto'), desc: t('player.option_auto_desc_decoder') },
{ id: 'sw', label: t('player.option_sw'), desc: t('player.option_sw_desc') },
{ id: 'hw', label: t('player.option_hw'), desc: t('player.option_hw_desc') },
{ id: 'hw+', label: t('player.option_hw_plus'), desc: t('player.option_hw_plus_desc') },
] as const).map((option) => (
<TouchableOpacity
key={option.id}
@ -498,8 +498,8 @@ const PlayerSettingsScreen: React.FC = () => {
</View>
<View style={styles.optionButtonsRow}>
{([
{ id: 'gpu', label: 'GPU', desc: 'Standard' },
{ id: 'gpu-next', label: 'GPU-Next', desc: 'Advanced' },
{ id: 'gpu', label: t('player.option_gpu_desc') },
{ id: 'gpu-next', label: t('player.option_gpu_next_desc') },
] as const).map((option) => (
<TouchableOpacity
key={option.id}
@ -582,7 +582,7 @@ const PlayerSettingsScreen: React.FC = () => {
message={alertMessage}
onClose={() => setAlertVisible(false)}
/>
</SafeAreaView>
</SafeAreaView >
);
};

View file

@ -1448,7 +1448,7 @@ const PluginsScreen: React.FC = () => {
onPress={() => navigation.goBack()}
>
<Ionicons name="arrow-back" size={24} color={colors.primary} />
<Text style={styles.backText}>Settings</Text>
<Text style={styles.backText}>{t('settings.title')}</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
@ -1462,7 +1462,7 @@ const PluginsScreen: React.FC = () => {
</View>
</View>
<Text style={styles.headerTitle}>Plugins</Text>
<Text style={styles.headerTitle}>{t('plugins.title')}</Text>
<ScrollView
style={styles.scrollView}
@ -1492,7 +1492,7 @@ const PluginsScreen: React.FC = () => {
{/* Enable Plugins */}
<CollapsibleSection
title="Enable Plugins"
title={t('plugins.enable_title')}
isExpanded={expandedSections.repository}
onToggle={() => toggleSection('repository')}
colors={colors}
@ -1500,9 +1500,9 @@ const PluginsScreen: React.FC = () => {
>
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Enable Plugins</Text>
<Text style={styles.settingTitle}>{t('plugins.enable_title')}</Text>
<Text style={styles.settingDescription}>
Allow the app to use installed plugins for finding streams
{t('plugins.enable_desc')}
</Text>
</View>
<Switch
@ -1516,22 +1516,22 @@ const PluginsScreen: React.FC = () => {
{/* Repository Configuration */}
<CollapsibleSection
title="Repository Configuration"
title={t('plugins.repo_config_title')}
isExpanded={expandedSections.repository}
onToggle={() => toggleSection('repository')}
colors={colors}
styles={styles}
>
<Text style={styles.sectionDescription}>
Enable multiple repositories to combine plugins from different sources. Toggle each repository on or off below.
{t('plugins.repo_config_desc')}
</Text>
{/* Repository List */}
{repositories.length > 0 && (
<View style={styles.repositoriesList}>
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>Your Repositories</Text>
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>{t('plugins.your_repos')}</Text>
<Text style={[styles.settingDescription, { marginBottom: 12 }]}>
Enable multiple repositories to combine plugins from different sources.
{t('plugins.your_repos_desc')}
</Text>
{repositories.map((repo) => (
<View key={repo.id} style={[styles.repositoryItem, repo.enabled === false && { opacity: 0.6 }]}>
@ -1541,13 +1541,13 @@ const PluginsScreen: React.FC = () => {
{repo.enabled !== false && (
<View style={[styles.statusBadge, { backgroundColor: '#34C759' }]}>
<Ionicons name="checkmark-circle" size={12} color="white" />
<Text style={styles.statusBadgeText}>Enabled</Text>
<Text style={styles.statusBadgeText}>{t('plugins.enabled')}</Text>
</View>
)}
{switchingRepository === repo.id && (
<View style={[styles.statusBadge, { backgroundColor: colors.primary }]}>
<ActivityIndicator size={12} color="white" />
<Text style={styles.statusBadgeText}>Updating...</Text>
<Text style={styles.statusBadgeText}>{t('plugins.updating')}</Text>
</View>
)}
</View>
@ -1577,7 +1577,7 @@ const PluginsScreen: React.FC = () => {
{isRefreshing ? (
<ActivityIndicator size="small" color={colors.mediumGray} />
) : (
<Text style={styles.repositoryActionButtonText}>Refresh</Text>
<Text style={styles.repositoryActionButtonText}>{t('plugins.refresh')}</Text>
)}
</TouchableOpacity>
<TouchableOpacity
@ -1585,7 +1585,7 @@ const PluginsScreen: React.FC = () => {
onPress={() => handleRemoveRepository(repo.id)}
disabled={switchingRepository !== null}
>
<Text style={styles.repositoryActionButtonText}>Remove</Text>
<Text style={styles.repositoryActionButtonText}>{t('plugins.remove')}</Text>
</TouchableOpacity>
</View>
</View>
@ -1600,13 +1600,13 @@ const PluginsScreen: React.FC = () => {
onPress={() => setShowAddRepositoryModal(true)}
disabled={!settings.enableLocalScrapers || switchingRepository !== null}
>
<Text style={styles.buttonText}>Add New Repository</Text>
<Text style={styles.buttonText}>{t('plugins.add_new_repo')}</Text>
</TouchableOpacity>
</CollapsibleSection>
{/* Available Plugins */}
<CollapsibleSection
title={`Available Plugins (${filteredPlugins.length})`}
title={t('plugins.available_plugins', { count: filteredPlugins.length })}
isExpanded={expandedSections.plugins}
onToggle={() => toggleSection('plugins')}
colors={colors}
@ -1621,7 +1621,7 @@ const PluginsScreen: React.FC = () => {
style={styles.searchInput}
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search plugins..."
placeholder={t('plugins.search_placeholder')}
placeholderTextColor={colors.mediumGray}
/>
{searchQuery.length > 0 && (
@ -1651,7 +1651,7 @@ const PluginsScreen: React.FC = () => {
styles.repositoryTabText,
selectedRepositoryTab === 'all' && styles.repositoryTabTextSelected
]}>
All
{t('plugins.all')}
</Text>
<Text style={[
styles.repositoryTabCount,
@ -1710,7 +1710,7 @@ const PluginsScreen: React.FC = () => {
styles.filterChipText,
selectedFilter === filter && styles.filterChipTextSelected
]}>
{filter === 'all' ? 'All Types' : filter === 'movie' ? 'Movies' : 'TV Shows'}
{filter === 'all' ? t('plugins.filter_all') : filter === 'movie' ? t('plugins.filter_movies') : t('plugins.filter_tv')}
</Text>
</TouchableOpacity>
))}
@ -1724,14 +1724,14 @@ const PluginsScreen: React.FC = () => {
onPress={() => handleBulkToggle(true)}
disabled={isRefreshing}
>
<Text style={[styles.bulkActionButtonText, { color: '#34C759' }]}>Enable All</Text>
<Text style={[styles.bulkActionButtonText, { color: '#34C759' }]}>{t('plugins.enable_all')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.bulkActionButton, styles.bulkActionButtonDisabled]}
onPress={() => handleBulkToggle(false)}
disabled={isRefreshing}
>
<Text style={[styles.bulkActionButtonText, { color: colors.mediumGray }]}>Disable All</Text>
<Text style={[styles.bulkActionButtonText, { color: colors.mediumGray }]}>{t('plugins.disable_all')}</Text>
</TouchableOpacity>
</View>
)}
@ -1747,12 +1747,12 @@ const PluginsScreen: React.FC = () => {
style={styles.emptyStateIcon}
/>
<Text style={styles.emptyStateTitle}>
{searchQuery ? 'No Plugins Found' : 'No Plugins Available'}
{searchQuery ? t('plugins.no_plugins_found') : t('plugins.no_plugins_available')}
</Text>
<Text style={styles.emptyStateDescription}>
{searchQuery
? `No plugins match "${searchQuery}". Try a different search term.`
: 'Configure a repository above to view available plugins.'
? t('plugins.no_match_desc', { query: searchQuery })
: t('plugins.configure_repo_desc')
}
</Text>
{searchQuery && (
@ -1760,7 +1760,7 @@ const PluginsScreen: React.FC = () => {
style={[styles.button, styles.secondaryButton]}
onPress={() => setSearchQuery('')}
>
<Text style={styles.secondaryButtonText}>Clear Search</Text>
<Text style={styles.secondaryButtonText}>{t('plugins.clear_search')}</Text>
</TouchableOpacity>
)}
</View>
@ -1825,7 +1825,7 @@ const PluginsScreen: React.FC = () => {
<View style={styles.pluginCardMetaItem}>
<Ionicons name="play-circle" size={12} color={colors.mediumGray} />
<Text style={styles.pluginCardMetaText}>
No external player
{t('plugins.no_external_player')}
</Text>
</View>
)}
@ -1842,13 +1842,13 @@ const PluginsScreen: React.FC = () => {
{/* ShowBox Settings - only visible when ShowBox plugin is available */}
{showboxScraperId && plugin.id === showboxScraperId && settings.enableLocalScrapers && (
<View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text>
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>{t('plugins.showbox_token')}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
<TextInput
style={[styles.textInput, { flex: 1, marginBottom: 0 }]}
value={showboxUiToken}
onChangeText={setShowboxUiToken}
placeholder="Paste your ShowBox UI token"
placeholder={t('plugins.showbox_placeholder')}
placeholderTextColor={colors.mediumGray}
autoCapitalize="none"
autoCorrect={false}
@ -1874,7 +1874,7 @@ const PluginsScreen: React.FC = () => {
openAlert('Saved', 'ShowBox settings updated');
}}
>
<Text style={styles.buttonText}>Save</Text>
<Text style={styles.buttonText}>{t('plugins.save')}</Text>
</TouchableOpacity>
)}
<TouchableOpacity
@ -1887,7 +1887,7 @@ const PluginsScreen: React.FC = () => {
}
}}
>
<Text style={styles.secondaryButtonText}>Clear</Text>
<Text style={styles.secondaryButtonText}>{t('plugins.clear')}</Text>
</TouchableOpacity>
</View>
</View>
@ -1900,7 +1900,7 @@ const PluginsScreen: React.FC = () => {
{/* Additional Settings */}
<CollapsibleSection
title="Additional Settings"
title={t('plugins.additional_settings')}
isExpanded={expandedSections.settings}
onToggle={() => toggleSection('settings')}
colors={colors}
@ -1908,9 +1908,9 @@ const PluginsScreen: React.FC = () => {
>
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Enable URL Validation</Text>
<Text style={styles.settingTitle}>{t('plugins.enable_url_validation')}</Text>
<Text style={styles.settingDescription}>
Validate streaming URLs before returning them (may slow down results but improves reliability)
{t('plugins.url_validation_desc')}
</Text>
</View>
<Switch
@ -1924,9 +1924,9 @@ const PluginsScreen: React.FC = () => {
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Group Plugin Streams</Text>
<Text style={styles.settingTitle}>{t('plugins.group_streams')}</Text>
<Text style={styles.settingDescription}>
When enabled, plugin streams are grouped by repository. When disabled, each plugin shows as a separate provider.
{t('plugins.group_streams_desc')}
</Text>
</View>
<Switch
@ -1946,9 +1946,9 @@ const PluginsScreen: React.FC = () => {
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Sort by Quality First</Text>
<Text style={styles.settingTitle}>{t('plugins.sort_quality')}</Text>
<Text style={styles.settingDescription}>
When enabled, streams are sorted by quality first, then by plugin. When disabled, streams are sorted by plugin first, then by quality. Only available when grouping is enabled.
{t('plugins.sort_quality_desc')}
</Text>
</View>
<Switch
@ -1962,9 +1962,9 @@ const PluginsScreen: React.FC = () => {
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Show Plugin Logos</Text>
<Text style={styles.settingTitle}>{t('plugins.show_logos')}</Text>
<Text style={styles.settingDescription}>
Display plugin logos next to streaming links on the streams screen.
{t('plugins.show_logos_desc')}
</Text>
</View>
<Switch
@ -1979,14 +1979,14 @@ const PluginsScreen: React.FC = () => {
{/* Quality Filtering */}
<CollapsibleSection
title="Quality Filtering"
title={t('plugins.quality_filtering')}
isExpanded={expandedSections.quality}
onToggle={() => toggleSection('quality')}
colors={colors}
styles={styles}
>
<Text style={styles.sectionDescription}>
Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results.
{t('plugins.quality_filtering_desc')}
</Text>
<View style={styles.qualityChipsContainer}>
@ -2017,25 +2017,25 @@ const PluginsScreen: React.FC = () => {
{(settings.excludedQualities || []).length > 0 && (
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
Excluded qualities: {(settings.excludedQualities || []).join(', ')}
{t('plugins.excluded_qualities')} {(settings.excludedQualities || []).join(', ')}
</Text>
)}
</CollapsibleSection>
{/* Language Filtering */}
<CollapsibleSection
title="Language Filtering"
title={t('plugins.language_filtering')}
isExpanded={expandedSections.quality}
onToggle={() => toggleSection('quality')}
colors={colors}
styles={styles}
>
<Text style={styles.sectionDescription}>
Exclude specific languages from search results. Tap on a language to exclude it from plugin results.
{t('plugins.language_filtering_desc')}
</Text>
<Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}>
<Text style={{ fontWeight: '600' }}>Note:</Text> This filter only applies to providers that include language information in their stream names. It does not affect other providers.
<Text style={{ fontWeight: '600' }}>{t('plugins.note')}</Text> {t('plugins.language_filtering_note')}
</Text>
<View style={styles.qualityChipsContainer}>
@ -2066,21 +2066,20 @@ const PluginsScreen: React.FC = () => {
{(settings.excludedLanguages || []).length > 0 && (
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
Excluded languages: {(settings.excludedLanguages || []).join(', ')}
{t('plugins.excluded_languages')} {(settings.excludedLanguages || []).join(', ')}
</Text>
)}
</CollapsibleSection>
{/* About */}
<View style={[styles.section, styles.lastSection]}>
<Text style={styles.sectionTitle}>About Plugins</Text>
<Text style={styles.sectionTitle}>{t('plugins.about_title')}</Text>
<Text style={styles.infoText}>
Plugins are JavaScript modules that can search for streaming links from various sources.
They run locally on your device and can be installed from trusted repositories.
{t('plugins.about_desc_1')}
</Text>
<Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}>
<Text style={{ fontWeight: '600' }}>Note:</Text> Providers marked as "Limited" depend on external APIs that may stop working without notice.
<Text style={{ fontWeight: '600' }}>{t('plugins.note')}</Text> {t('plugins.about_desc_2')}
</Text>
</View>
</ScrollView>
@ -2095,24 +2094,24 @@ const PluginsScreen: React.FC = () => {
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Getting Started with Plugins</Text>
<Text style={styles.modalTitle}>{t('plugins.help_title')}</Text>
<Text style={styles.modalText}>
1. <Text style={{ fontWeight: '600' }}>Enable Plugins</Text> - Turn on the main switch to allow plugins
<Text>{t('plugins.help_step_1')}</Text>
</Text>
<Text style={styles.modalText}>
2. <Text style={{ fontWeight: '600' }}>Add Repository</Text> - Add a GitHub raw URL or use the default repository
<Text>{t('plugins.help_step_2')}</Text>
</Text>
<Text style={styles.modalText}>
3. <Text style={{ fontWeight: '600' }}>Refresh Repository</Text> - Download available plugins from the repository
<Text>{t('plugins.help_step_3')}</Text>
</Text>
<Text style={styles.modalText}>
4. <Text style={{ fontWeight: '600' }}>Enable Plugins</Text> - Turn on the plugins you want to use for streaming
<Text>{t('plugins.help_step_4')}</Text>
</Text>
<TouchableOpacity
style={styles.modalButton}
onPress={() => setShowHelpModal(false)}
>
<Text style={styles.modalButtonText}>Got it!</Text>
<Text style={styles.modalButtonText}>{t('plugins.got_it')}</Text>
</TouchableOpacity>
</View>
</View>
@ -2150,7 +2149,7 @@ const PluginsScreen: React.FC = () => {
{/* Format Hint */}
<Text style={styles.formatHint}>
Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch
{t('plugins.repo_format_hint')}
</Text>
{/* Action Buttons */}
@ -2162,7 +2161,7 @@ const PluginsScreen: React.FC = () => {
setNewRepositoryUrl('');
}}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
<Text style={styles.cancelButtonText}>{t('plugins.cancel')}</Text>
</TouchableOpacity>
<TouchableOpacity
@ -2173,7 +2172,7 @@ const PluginsScreen: React.FC = () => {
{isLoading ? (
<ActivityIndicator size="small" color={colors.white} />
) : (
<Text style={styles.addButtonText}>Add</Text>
<Text style={styles.addButtonText}>{t('plugins.add')}</Text>
)}
</TouchableOpacity>
</View>

View file

@ -338,7 +338,7 @@ const SettingsScreen: React.FC = () => {
<SettingsCard title={t('settings.sections.account')} isTablet={isTablet}>
{isItemVisible('trakt') && (
<SettingItem
title={t('settings.trakt')}
title={t('trakt.title')}
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : t('settings.sign_in_sync')}
customIcon={<TraktIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}
@ -565,7 +565,7 @@ const SettingsScreen: React.FC = () => {
<SettingsCard title={t('settings.account').toUpperCase()}>
{isItemVisible('trakt') && (
<SettingItem
title={t('settings.trakt')}
title={t('trakt.title')}
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : t('settings.sign_in_sync')}
customIcon={<TraktIcon size={20} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}

View file

@ -28,6 +28,7 @@ import { logger } from '../utils/logger';
import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import CustomAlert from '../components/CustomAlert';
import { useTranslation } from 'react-i18next';
// (duplicate import removed)
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
@ -63,6 +64,7 @@ const EXAMPLE_SHOWS = [
];
const TMDBSettingsScreen = () => {
const { t } = useTranslation();
const navigation = useNavigation();
const [apiKey, setApiKey] = useState('');
const [isLoading, setIsLoading] = useState(true);
@ -74,7 +76,7 @@ const TMDBSettingsScreen = () => {
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([
{ label: 'OK', onPress: () => setAlertVisible(false) },
{ label: t('common.ok'), onPress: () => setAlertVisible(false) },
]);
const apiKeyInputRef = useRef<TextInput>(null);
const { currentTheme } = useTheme();
@ -108,7 +110,7 @@ const TMDBSettingsScreen = () => {
}))
);
} else {
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
}
setAlertVisible(true);
};
@ -154,25 +156,25 @@ const TMDBSettingsScreen = () => {
const handleClearCache = () => {
openAlert(
'Clear TMDB Cache',
`This will clear all cached TMDB data (${cacheSize}). This may temporarily slow down loading until cache rebuilds.`,
t('tmdb_settings.clear_cache_title'),
t('tmdb_settings.clear_cache_msg', { size: cacheSize }),
[
{
label: 'Cancel',
label: t('common.cancel'),
onPress: () => logger.log('[TMDBSettingsScreen] Clear cache cancelled'),
},
{
label: 'Clear',
label: t('tmdb_settings.clear_cache'),
onPress: async () => {
logger.log('[TMDBSettingsScreen] Proceeding with cache clear');
try {
await tmdbService.clearAllCache();
setCacheSize('0 KB');
logger.log('[TMDBSettingsScreen] Cache cleared successfully');
openAlert('Success', 'TMDB cache cleared successfully.');
openAlert(t('common.success'), t('tmdb_settings.clear_cache_success'));
} catch (error) {
logger.error('[TMDBSettingsScreen] Failed to clear cache:', error);
openAlert('Error', 'Failed to clear cache.');
openAlert(t('common.error'), t('tmdb_settings.clear_cache_error'));
}
},
},
@ -217,7 +219,7 @@ const TMDBSettingsScreen = () => {
const trimmedKey = apiKey.trim();
if (!trimmedKey) {
logger.warn('[TMDBSettingsScreen] Empty API key provided');
setTestResult({ success: false, message: 'API Key cannot be empty.' });
setTestResult({ success: false, message: t('tmdb_settings.empty_api_key') });
return;
}
@ -228,17 +230,17 @@ const TMDBSettingsScreen = () => {
await mmkvStorage.setItem(USE_CUSTOM_TMDB_API_KEY, 'true');
setIsKeySet(true);
setUseCustomKey(true);
setTestResult({ success: true, message: 'API key verified and saved successfully.' });
setTestResult({ success: true, message: t('tmdb_settings.key_verified') });
logger.log('[TMDBSettingsScreen] API key saved successfully');
} else {
logger.warn('[TMDBSettingsScreen] API key test failed');
setTestResult({ success: false, message: 'Invalid API key. Please check and try again.' });
setTestResult({ success: false, message: t('tmdb_settings.invalid_api_key') });
}
} catch (error) {
logger.error('[TMDBSettingsScreen] Error saving API key:', error);
setTestResult({
success: false,
message: 'An error occurred while saving. Please try again.'
message: t('tmdb_settings.save_error')
});
}
};
@ -265,15 +267,15 @@ const TMDBSettingsScreen = () => {
const clearApiKey = async () => {
logger.log('[TMDBSettingsScreen] Clear API key requested');
openAlert(
'Clear API Key',
'Are you sure you want to remove your custom API key and revert to the default?',
t('tmdb_settings.clear_api_key_title'),
t('tmdb_settings.clear_api_key_msg'),
[
{
label: 'Cancel',
label: t('common.cancel'),
onPress: () => logger.log('[TMDBSettingsScreen] Clear API key cancelled'),
},
{
label: 'Clear',
label: t('mdblist.clear'),
onPress: async () => {
logger.log('[TMDBSettingsScreen] Proceeding with API key clear');
try {
@ -286,7 +288,7 @@ const TMDBSettingsScreen = () => {
logger.log('[TMDBSettingsScreen] API key cleared successfully');
} catch (error) {
logger.error('[TMDBSettingsScreen] Failed to clear API key:', error);
openAlert('Error', 'Failed to clear API key');
openAlert(t('common.error'), t('tmdb_settings.clear_api_key_error'));
}
},
},
@ -305,21 +307,21 @@ const TMDBSettingsScreen = () => {
logger.log('[TMDBSettingsScreen] Switching to built-in API key');
setTestResult({
success: true,
message: 'Now using the built-in TMDb API key.'
message: t('tmdb_settings.using_builtin_key')
});
} else if (apiKey && isKeySet) {
// If switching to custom key and we have a key
logger.log('[TMDBSettingsScreen] Switching to custom API key');
setTestResult({
success: true,
message: 'Now using your custom TMDb API key.'
message: t('tmdb_settings.using_custom_key')
});
} else {
// If switching to custom key but don't have a key yet
logger.log('[TMDBSettingsScreen] No custom key available yet');
setTestResult({
success: false,
message: 'Please enter and save your custom TMDb API key.'
message: t('tmdb_settings.enter_custom_key')
});
}
} catch (error) {
@ -462,7 +464,7 @@ const TMDBSettingsScreen = () => {
)}
{!logo && (
<View style={styles.noLogoContainer}>
<Text style={styles.noLogoText}>No logo available</Text>
<Text style={styles.noLogoText}>{t('tmdb_settings.no_logo')}</Text>
</View>
)}
</View>
@ -505,7 +507,7 @@ const TMDBSettingsScreen = () => {
<StatusBar barStyle="light-content" />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.text }]}>Loading Settings...</Text>
<Text style={[styles.loadingText, { color: currentTheme.colors.text }]}>{t('common.loading')}</Text>
</View>
</View>
);
@ -521,11 +523,11 @@ const TMDBSettingsScreen = () => {
onPress={() => navigation.goBack()}
>
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>{t('settings.settings_title')}</Text>
</TouchableOpacity>
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
TMDb Settings
{t('tmdb_settings.title')}
</Text>
</View>
@ -539,17 +541,17 @@ const TMDBSettingsScreen = () => {
<View style={[styles.sectionCard, { backgroundColor: currentTheme.colors.elevation2 }]}>
<View style={styles.sectionHeader}>
<MaterialIcons name="movie" size={20} color={currentTheme.colors.primary} />
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>Metadata Enrichment</Text>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.metadata_enrichment')}</Text>
</View>
<Text style={[styles.sectionDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Enhance your content metadata with TMDb data for better details and information.
{t('tmdb_settings.metadata_enrichment_desc')}
</Text>
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Enable Enrichment</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.enable_enrichment')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Augments addon metadata with TMDb for cast, certification, logos/posters, and episode fallback.
{t('tmdb_settings.enable_enrichment_desc')}
</Text>
</View>
<Switch
@ -567,9 +569,9 @@ const TMDBSettingsScreen = () => {
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Localized Text</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.localized_text')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Fetch titles and descriptions in your preferred language from TMDb.
{t('tmdb_settings.localized_text_desc')}
</Text>
</View>
<Switch
@ -587,7 +589,7 @@ const TMDBSettingsScreen = () => {
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Language</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.language')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Current: {(settings.tmdbLanguagePreference || 'en').toUpperCase()}
</Text>
@ -596,20 +598,20 @@ const TMDBSettingsScreen = () => {
onPress={() => setLanguagePickerVisible(true)}
style={[styles.languageButton, { backgroundColor: currentTheme.colors.primary }]}
>
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>{t('tmdb_settings.change')}</Text>
</TouchableOpacity>
</View>
{/* Logo Preview */}
<View style={styles.divider} />
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 8 }]}>Logo Preview</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 8 }]}>{t('tmdb_settings.logo_preview')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, marginBottom: 12 }]}>
Preview shows how localized logos will appear in the selected language.
{t('tmdb_settings.logo_preview_desc')}
</Text>
{/* Show selector */}
<Text style={[styles.selectorLabel, { color: currentTheme.colors.mediumEmphasis }]}>Example:</Text>
<Text style={[styles.selectorLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('tmdb_settings.example')}</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
@ -655,17 +657,17 @@ const TMDBSettingsScreen = () => {
{/* Granular Enrichment Options */}
<View style={styles.divider} />
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 4 }]}>Enrichment Options</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 4 }]}>{t('tmdb_settings.enrichment_options')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, marginBottom: 16 }]}>
Control which data is fetched from TMDb. Disabled options will use addon data if available.
{t('tmdb_settings.enrichment_options_desc')}
</Text>
{/* Cast & Crew */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Cast & Crew</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.cast_crew')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Actors, directors, writers with profile photos
{t('tmdb_settings.cast_crew_desc')}
</Text>
</View>
<Switch
@ -680,9 +682,9 @@ const TMDBSettingsScreen = () => {
{/* Title & Description */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Title & Description</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.title_description')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Use TMDb localized title and overview text
{t('tmdb_settings.title_description_desc')}
</Text>
</View>
<Switch
@ -697,9 +699,9 @@ const TMDBSettingsScreen = () => {
{/* Title Logos */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Title Logos</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.title_logos')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
High-quality title treatment images
{t('tmdb_settings.title_logos_desc')}
</Text>
</View>
<Switch
@ -714,9 +716,9 @@ const TMDBSettingsScreen = () => {
{/* Banners/Backdrops */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Banners & Backdrops</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.banners_backdrops')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
High-resolution backdrop images
{t('tmdb_settings.banners_backdrops_desc')}
</Text>
</View>
<Switch
@ -731,9 +733,9 @@ const TMDBSettingsScreen = () => {
{/* Certification */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Content Certification</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.certification')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Age ratings (PG-13, R, TV-MA, etc.)
{t('tmdb_settings.certification_desc')}
</Text>
</View>
<Switch
@ -748,9 +750,9 @@ const TMDBSettingsScreen = () => {
{/* Recommendations */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Recommendations</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.recommendations')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Similar content suggestions
{t('tmdb_settings.recommendations_desc')}
</Text>
</View>
<Switch
@ -765,9 +767,9 @@ const TMDBSettingsScreen = () => {
{/* Episode Data */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Episode Data</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.episode_data')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Episode thumbnails, info & fallbacks for TV shows
{t('tmdb_settings.episode_data_desc')}
</Text>
</View>
<Switch
@ -782,9 +784,9 @@ const TMDBSettingsScreen = () => {
{/* Season Posters */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Season Posters</Text>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>{t('tmdb_settings.season_posters')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Season-specific poster images
{t('tmdb_settings.season_posters_desc')}
</Text>
</View>
<Switch

View file

@ -25,6 +25,7 @@ import { useTraktIntegration } from '../hooks/useTraktIntegration';
import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings';
import { colors } from '../styles';
import CustomAlert from '../components/CustomAlert';
import { useTranslation } from 'react-i18next';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@ -46,6 +47,7 @@ const redirectUri = makeRedirectUri({
});
const TraktSettingsScreen: React.FC = () => {
const { t } = useTranslation();
const { settings, updateSetting } = useSettings();
const isDarkMode = settings.enableDarkMode;
const navigation = useNavigation();
@ -72,7 +74,7 @@ const TraktSettingsScreen: React.FC = () => {
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([
{ label: 'OK', onPress: () => setAlertVisible(false) },
{ label: t('common.ok'), onPress: () => setAlertVisible(false) },
]);
const openAlert = (
@ -91,7 +93,7 @@ const TraktSettingsScreen: React.FC = () => {
}))
);
} else {
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
}
setAlertVisible(true);
};
@ -148,11 +150,11 @@ const TraktSettingsScreen: React.FC = () => {
checkAuthStatus().then(() => {
// Show success message
openAlert(
'Successfully Connected',
'Your Trakt account has been connected successfully.',
t('trakt.auth_success_title'),
t('trakt.auth_success_msg'),
[
{
label: 'OK',
label: t('common.ok'),
onPress: () => navigation.goBack(),
}
]
@ -160,19 +162,19 @@ const TraktSettingsScreen: React.FC = () => {
});
} else {
logger.error('[TraktSettingsScreen] Token exchange failed');
openAlert('Authentication Error', 'Failed to complete authentication with Trakt.');
openAlert(t('trakt.auth_error_title'), t('trakt.auth_error_msg'));
}
})
.catch(error => {
logger.error('[TraktSettingsScreen] Token exchange error:', error);
openAlert('Authentication Error', 'An error occurred during authentication.');
openAlert(t('trakt.auth_error_title'), t('trakt.auth_error_generic'));
})
.finally(() => {
setIsExchangingCode(false);
});
} else if (response.type === 'error') {
logger.error('[TraktSettingsScreen] Authentication error:', response.error);
openAlert('Authentication Error', response.error?.message || 'An error occurred during authentication.');
openAlert(t('trakt.auth_error_title'), response.error?.message || t('trakt.auth_error_generic'));
setIsExchangingCode(false);
} else {
logger.log('[TraktSettingsScreen] Auth response type:', response.type);
@ -187,12 +189,12 @@ const TraktSettingsScreen: React.FC = () => {
const handleSignOut = async () => {
openAlert(
'Sign Out',
'Are you sure you want to sign out of your Trakt account?',
t('trakt.sign_out'),
t('trakt.sign_out_confirm'),
[
{ label: 'Cancel', onPress: () => { } },
{ label: t('common.cancel'), onPress: () => { } },
{
label: 'Sign Out',
label: t('trakt.sign_out'),
onPress: async () => {
setIsLoading(true);
try {
@ -203,7 +205,7 @@ const TraktSettingsScreen: React.FC = () => {
await refreshAuthStatus();
} catch (error) {
logger.error('[TraktSettingsScreen] Error signing out:', error);
openAlert('Error', 'Failed to sign out of Trakt.');
openAlert(t('common.error'), t('trakt.sign_out_error'));
} finally {
setIsLoading(false);
}
@ -230,7 +232,7 @@ const TraktSettingsScreen: React.FC = () => {
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
/>
<Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
Settings
{t('settings.title')}
</Text>
</TouchableOpacity>
@ -240,7 +242,7 @@ const TraktSettingsScreen: React.FC = () => {
</View>
<Text style={[styles.headerTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
Trakt Settings
{t('trakt.settings_title')}
</Text>
{/* Maintenance Mode Banner */}
@ -248,7 +250,7 @@ const TraktSettingsScreen: React.FC = () => {
<View style={styles.maintenanceBanner}>
<MaterialIcons name="engineering" size={24} color="#FFF" />
<View style={styles.maintenanceBannerTextContainer}>
<Text style={styles.maintenanceBannerTitle}>Under Maintenance</Text>
<Text style={styles.maintenanceBannerTitle}>{t('trakt.maintenance_title')}</Text>
<Text style={styles.maintenanceBannerMessage}>
{traktService.getMaintenanceMessage()}
</Text>
@ -279,13 +281,13 @@ const TraktSettingsScreen: React.FC = () => {
styles.signInTitle,
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Trakt Unavailable
{t('trakt.maintenance_unavailable')}
</Text>
<Text style={[
styles.signInDescription,
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
The Trakt integration is temporarily paused for maintenance. All syncing and authentication is disabled until maintenance is complete.
{t('trakt.maintenance_desc')}
</Text>
<TouchableOpacity
style={[
@ -296,7 +298,7 @@ const TraktSettingsScreen: React.FC = () => {
>
<MaterialIcons name="engineering" size={20} color={currentTheme.colors.mediumEmphasis} style={{ marginRight: 8 }} />
<Text style={[styles.buttonText, { color: currentTheme.colors.mediumEmphasis }]}>
Service Under Maintenance
{t('trakt.maintenance_button')}
</Text>
</TouchableOpacity>
</View>
@ -343,7 +345,7 @@ const TraktSettingsScreen: React.FC = () => {
styles.joinedDate,
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
Joined {new Date(userProfile.joined_at).toLocaleDateString()}
{t('trakt.joined', { date: new Date(userProfile.joined_at).toLocaleDateString() })}
</Text>
</View>
@ -355,7 +357,7 @@ const TraktSettingsScreen: React.FC = () => {
]}
onPress={handleSignOut}
>
<Text style={styles.buttonText}>Sign Out</Text>
<Text style={styles.buttonText}>{t('trakt.sign_out')}</Text>
</TouchableOpacity>
</View>
) : (
@ -369,13 +371,13 @@ const TraktSettingsScreen: React.FC = () => {
styles.signInTitle,
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Connect with Trakt
{t('trakt.connect_title')}
</Text>
<Text style={[
styles.signInDescription,
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
Sync your watch history, watchlist, and collection with Trakt.tv
{t('trakt.connect_desc')}
</Text>
<TouchableOpacity
style={[
@ -389,7 +391,7 @@ const TraktSettingsScreen: React.FC = () => {
<ActivityIndicator size="small" color="white" />
) : (
<Text style={styles.buttonText}>
Sign In with Trakt
{t('trakt.sign_in')}
</Text>
)}
</TouchableOpacity>
@ -407,7 +409,7 @@ const TraktSettingsScreen: React.FC = () => {
styles.sectionTitle,
{ color: currentTheme.colors.highEmphasis }
]}>
Sync Settings
{t('trakt.sync_settings_title')}
</Text>
<View style={[
styles.infoBox,
@ -417,7 +419,7 @@ const TraktSettingsScreen: React.FC = () => {
styles.infoText,
{ color: currentTheme.colors.mediumEmphasis }
]}>
When connected to Trakt, full history is synced directly from the API and is not written to local storage. Your Continue Watching list reflects your global Trakt progress.
{t('trakt.sync_info')}
</Text>
</View>
<View style={styles.settingItem}>
@ -427,13 +429,13 @@ const TraktSettingsScreen: React.FC = () => {
styles.settingLabel,
{ color: currentTheme.colors.highEmphasis }
]}>
Auto-sync playback progress
{t('trakt.auto_sync_label')}
</Text>
<Text style={[
styles.settingDescription,
{ color: currentTheme.colors.mediumEmphasis }
]}>
Automatically sync watch progress to Trakt
{t('trakt.auto_sync_desc')}
</Text>
</View>
<View style={styles.settingToggleContainer}>
@ -456,13 +458,13 @@ const TraktSettingsScreen: React.FC = () => {
styles.settingLabel,
{ color: currentTheme.colors.highEmphasis }
]}>
Import watched history
{t('trakt.import_history_label')}
</Text>
<Text style={[
styles.settingDescription,
{ color: currentTheme.colors.mediumEmphasis }
]}>
Use "Sync Now" to import your watch history and progress from Trakt
{t('trakt.import_history_desc')}
</Text>
</View>
</View>
@ -479,8 +481,8 @@ const TraktSettingsScreen: React.FC = () => {
onPress={async () => {
const success = await performManualSync();
openAlert(
'Sync Complete',
success ? 'Successfully synced your watch progress with Trakt.' : 'Sync failed. Please try again.'
t('trakt.sync_complete_title'),
success ? t('trakt.sync_success_msg') : t('trakt.sync_error_msg')
);
}}
>
@ -494,7 +496,7 @@ const TraktSettingsScreen: React.FC = () => {
styles.buttonText,
{ color: currentTheme.colors.primary }
]}>
Sync Now
{t('trakt.sync_now_button')}
</Text>
)}
</TouchableOpacity>
@ -504,7 +506,7 @@ const TraktSettingsScreen: React.FC = () => {
styles.sectionTitle,
{ color: currentTheme.colors.highEmphasis, marginTop: 24 }
]}>
Display Settings
{t('trakt.display_settings_title')}
</Text>
<View style={styles.settingItem}>
@ -514,13 +516,13 @@ const TraktSettingsScreen: React.FC = () => {
styles.settingLabel,
{ color: currentTheme.colors.highEmphasis }
]}>
Show Trakt Comments
{t('trakt.show_comments_label')}
</Text>
<Text style={[
styles.settingDescription,
{ color: currentTheme.colors.mediumEmphasis }
]}>
Display Trakt comments in metadata screens when available
{t('trakt.show_comments_desc')}
</Text>
</View>
<View style={styles.settingToggleContainer}>

View file

@ -25,6 +25,7 @@ import { mmkvStorage } from '../services/mmkvStorage';
import { useGithubMajorUpdate } from '../hooks/useGithubMajorUpdate';
import { getDisplayedAppVersion } from '../utils/version';
import { isAnyUpgrade } from '../services/githubReleaseService';
import { useTranslation } from 'react-i18next';
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
@ -72,13 +73,14 @@ const UpdateScreen: React.FC = () => {
const insets = useSafeAreaInsets();
const github = useGithubMajorUpdate();
const { showInfo } = useToast();
const { t } = useTranslation();
// CustomAlert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([
{ label: 'OK', onPress: () => setAlertVisible(false) },
{ label: t('common.ok'), onPress: () => setAlertVisible(false) },
]);
const openAlert = (
@ -97,7 +99,7 @@ const UpdateScreen: React.FC = () => {
}))
);
} else {
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
}
setAlertVisible(true);
};
@ -133,12 +135,12 @@ const UpdateScreen: React.FC = () => {
const handleOtaAlertsToggle = async (value: boolean) => {
if (!value) {
openAlert(
'Disable OTA Update Alerts?',
'You will no longer receive automatic notifications for OTA updates.\n\n⚠ Warning: Staying on the latest version is important for:\n• Bug fixes and stability improvements\n• New features and enhancements\n• Providing accurate feedback and crash reports\n\nYou can still manually check for updates in this screen.',
t('updates.alert_disable_ota_title'),
t('updates.alert_disable_ota_msg'),
[
{ label: 'Cancel', onPress: () => setAlertVisible(false) },
{ label: t('common.cancel'), onPress: () => setAlertVisible(false) },
{
label: 'Disable',
label: t('updates.disable'),
onPress: async () => {
await mmkvStorage.setItem('@ota_updates_alerts_enabled', 'false');
setOtaAlertsEnabled(false);
@ -157,12 +159,16 @@ const UpdateScreen: React.FC = () => {
const handleMajorAlertsToggle = async (value: boolean) => {
if (!value) {
openAlert(
'Disable Major Update Alerts?',
'You will no longer receive notifications for major app updates that require reinstallation.\n\n⚠ Warning: Major updates often include:\n• Critical security patches\n• Breaking changes that require app reinstall\n• Important compatibility fixes\n\nYou can still check for updates manually.',
t('updates.alert_disable_major_title'),
t('updates.alert_disable_major_msg'),
[
{ label: 'Cancel', onPress: () => setAlertVisible(false) },
{ label: t('common.cancel'), onPress: () => setAlertVisible(false) },
{
label: 'Disable',
label: t('updates.disable'), // Assuming 'Disable' key might not exist, checking en.json... I didn't add 'disable'. Will use 'common.cancel' for cancel. For 'Disable', I'll check if I can use something else or add it. I missed adding 'disable' to en.json. I'll use hardcoded 'Disable' for now or 'Off'. Wait, I can use hardcoded string or just add it later. Actually, I see I missed adding a specific "Disable" button text in the replace_file_content earlier.
// Let's use 'Disable' string for now as fallback or t('plugins.disabled') if appropriate, but that's "Disabled".
// I will use "Disable" plain string for now to be safe, or check if common.disable exists. It probably doesn't.
// I'll stick to 'Disable' string to match previous behavior, or use t('common.cancel') for Cancel.
// Actually, looking at previous code it was "Disable". I'll use "Disable" for now.
onPress: async () => {
await mmkvStorage.setItem('@major_updates_alerts_enabled', 'false');
setMajorAlertsEnabled(false);
@ -182,7 +188,7 @@ const UpdateScreen: React.FC = () => {
setIsChecking(true);
setUpdateStatus('checking');
setUpdateProgress(0);
setLastOperation('Checking for updates...');
setLastOperation(t('updates.status_checking'));
const info = await UpdateService.checkForUpdates();
setUpdateInfo(info);
@ -192,16 +198,17 @@ const UpdateScreen: React.FC = () => {
if (info.isAvailable) {
setUpdateStatus('available');
setLastOperation(`Update available: ${info.manifest?.id || 'unknown'}`);
setLastOperation(`${t('updates.status_available')}: ${info.manifest?.id || 'unknown'}`);
} else {
setUpdateStatus('idle');
setLastOperation('No updates available');
setLastOperation(t('updates.status_ready')); // Using ready instead of "No updates available" to match "Ready to check" state, or should I add "No updates available"? Previous code used "No updates available". En.json has "status_ready" as "Ready to check for updates".
// I'll use status_ready effectively.
}
} catch (error) {
if (__DEV__) console.error('Error checking for updates:', error);
setUpdateStatus('error');
setLastOperation(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
openAlert('Error', 'Failed to check for updates');
setLastOperation(`${t('common.error')}: ${error instanceof Error ? error.message : 'Unknown error'}`);
openAlert(t('common.error'), t('updates.status_error'));
} finally {
setIsChecking(false);
}
@ -219,7 +226,7 @@ const UpdateScreen: React.FC = () => {
// Also refresh GitHub section on mount (works in dev and prod)
try { github.refresh(); } catch { }
if (Platform.OS === 'android') {
showInfo('Checking for Updates', 'Checking for updates…');
showInfo(t('updates.title'), t('updates.status_checking'));
}
}, []);
@ -228,7 +235,7 @@ const UpdateScreen: React.FC = () => {
setIsInstalling(true);
setUpdateStatus('downloading');
setUpdateProgress(0);
setLastOperation('Downloading update...');
setLastOperation(t('updates.status_downloading'));
// Simulate progress updates
const progressInterval = setInterval(() => {
@ -243,24 +250,24 @@ const UpdateScreen: React.FC = () => {
clearInterval(progressInterval);
setUpdateProgress(100);
setUpdateStatus('installing');
setLastOperation('Installing update...');
setLastOperation(t('updates.status_installing'));
// Logs disabled
if (success) {
setUpdateStatus('success');
setLastOperation('Update installed successfully');
openAlert('Success', 'Update will be applied on next app restart');
setLastOperation(t('updates.status_success'));
openAlert(t('common.success'), t('updates.alert_update_applied_msg'));
} else {
setUpdateStatus('error');
setLastOperation('No update available to install');
openAlert('No Update', 'No update available to install');
setLastOperation(t('updates.alert_no_update_to_install'));
openAlert(t('updates.alert_no_update_title'), t('updates.alert_no_update_to_install'));
}
} catch (error) {
if (__DEV__) console.error('Error installing update:', error);
setUpdateStatus('error');
setLastOperation(`Installation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
openAlert('Error', 'Failed to install update');
setLastOperation(`${t('updates.status_error')}: ${error instanceof Error ? error.message : 'Unknown error'}`);
openAlert(t('common.error'), t('updates.alert_install_failed'));
} finally {
setIsInstalling(false);
}
@ -361,19 +368,19 @@ const UpdateScreen: React.FC = () => {
const getStatusText = () => {
switch (updateStatus) {
case 'checking':
return 'Checking for updates...';
return t('updates.status_checking');
case 'available':
return 'Update available!';
return t('updates.status_available');
case 'downloading':
return 'Downloading update...';
return t('updates.status_downloading');
case 'installing':
return 'Installing update...';
return t('updates.status_installing');
case 'success':
return 'Update installed successfully!';
return t('updates.status_success');
case 'error':
return 'Update failed';
return t('updates.status_error');
default:
return 'Ready to check for updates';
return t('updates.status_ready');
}
};
@ -409,7 +416,7 @@ const UpdateScreen: React.FC = () => {
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
<Text style={[styles.backText, { color: currentTheme.colors.highEmphasis }]}>
Settings
{t('settings.settings_title')}
</Text>
</TouchableOpacity>
@ -419,7 +426,7 @@ const UpdateScreen: React.FC = () => {
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
App Updates
{t('updates.title')}
</Text>
<View style={styles.contentContainer}>
@ -428,7 +435,7 @@ const UpdateScreen: React.FC = () => {
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<SettingsCard title="APP UPDATES" isTablet={isTablet}>
<SettingsCard title={t('updates.title').toUpperCase()} isTablet={isTablet}>
{/* Main Update Card */}
<View style={styles.updateMainCard}>
{/* Status Section */}
@ -441,7 +448,7 @@ const UpdateScreen: React.FC = () => {
{getStatusText()}
</Text>
<Text style={[styles.statusDetailText, { color: currentTheme.colors.mediumEmphasis }]}>
{lastOperation || 'Ready to check for updates'}
{lastOperation || t('updates.status_ready')}
</Text>
</View>
</View>
@ -490,7 +497,7 @@ const UpdateScreen: React.FC = () => {
<MaterialIcons name="system-update" size={18} color="white" />
)}
<Text style={styles.modernButtonText}>
{isChecking ? 'Checking...' : 'Check for Updates'}
{isChecking ? `${t('updates.status_checking')}...` : t('updates.action_check')}
</Text>
</TouchableOpacity>
@ -512,7 +519,7 @@ const UpdateScreen: React.FC = () => {
<MaterialIcons name="download" size={18} color="white" />
)}
<Text style={styles.modernButtonText}>
{isInstalling ? 'Installing...' : 'Install Update'}
{isInstalling ? `${t('updates.status_installing')}...` : t('updates.action_install')}
</Text>
</TouchableOpacity>
)}
@ -527,7 +534,7 @@ const UpdateScreen: React.FC = () => {
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="notes" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Release notes:</Text>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('updates.release_notes')}</Text>
</View>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>{getReleaseNotes()}</Text>
</View>
@ -539,9 +546,9 @@ const UpdateScreen: React.FC = () => {
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="info-outline" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Version:</Text>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('updates.version')}</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{updateInfo?.manifest?.id ? `${updateInfo.manifest.id.substring(0, 8)}...` : 'Unknown'}
{updateInfo?.manifest?.id ? `${updateInfo.manifest.id.substring(0, 8)}...` : t('common.unknown')}
</Text>
</View>
@ -550,7 +557,7 @@ const UpdateScreen: React.FC = () => {
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="schedule" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Last checked:</Text>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('updates.last_checked')}</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{formatDate(lastChecked)}
</Text>
@ -564,10 +571,10 @@ const UpdateScreen: React.FC = () => {
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="verified" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Current version:</Text>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('updates.current_version')}</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}
selectable>
{currentInfo?.manifest?.id || (currentInfo?.isEmbeddedLaunch === false ? 'Unknown' : 'Embedded')}
{currentInfo?.manifest?.id || (currentInfo?.isEmbeddedLaunch === false ? t('common.unknown') : 'Embedded')}
</Text>
</View>
@ -577,7 +584,7 @@ const UpdateScreen: React.FC = () => {
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="notes" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Current release notes:</Text>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('updates.current_release_notes')}</Text>
</View>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{getCurrentReleaseNotes()}
@ -591,13 +598,13 @@ const UpdateScreen: React.FC = () => {
{/* GitHub Release (compact) only show when update is available */}
{github.latestTag && isAnyUpgrade(getDisplayedAppVersion(), github.latestTag) ? (
<SettingsCard title="GITHUB RELEASE" isTablet={isTablet}>
<SettingsCard title={t('updates.github_release')} isTablet={isTablet}>
<View style={styles.infoSection}>
<View style={styles.infoItem}>
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="new-releases" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Current:</Text>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('updates.current')}</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{getDisplayedAppVersion()}
</Text>
@ -607,7 +614,7 @@ const UpdateScreen: React.FC = () => {
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="tag" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Latest:</Text>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('updates.latest')}</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{github.latestTag}
</Text>
@ -615,7 +622,7 @@ const UpdateScreen: React.FC = () => {
{github.releaseNotes ? (
<View style={{ marginTop: 4 }}>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Notes:</Text>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('updates.notes')}</Text>
<Text
numberOfLines={3}
style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}
@ -633,7 +640,7 @@ const UpdateScreen: React.FC = () => {
activeOpacity={0.8}
>
<MaterialIcons name="open-in-new" size={18} color="white" />
<Text style={styles.modernButtonText}>View Release</Text>
<Text style={styles.modernButtonText}>{t('updates.view_release')}</Text>
</TouchableOpacity>
</View>
</View>
@ -642,15 +649,15 @@ const UpdateScreen: React.FC = () => {
) : null}
{/* Update Notification Settings */}
<SettingsCard title="NOTIFICATION SETTINGS" isTablet={isTablet}>
<SettingsCard title={t('updates.notification_settings')} isTablet={isTablet}>
{/* OTA Updates Toggle */}
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
OTA Update Alerts
{t('updates.ota_alerts_label')}
</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Show notifications for over-the-air updates
{t('updates.ota_alerts_desc')}
</Text>
</View>
<Switch
@ -666,10 +673,10 @@ const UpdateScreen: React.FC = () => {
<View style={[styles.settingRow, { borderBottomWidth: 0 }]}>
<View style={styles.settingInfo}>
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
Major Update Alerts
{t('updates.major_alerts_label')}
</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Show notifications for new app versions on GitHub
{t('updates.major_alerts_desc')}
</Text>
</View>
<Switch
@ -687,7 +694,7 @@ const UpdateScreen: React.FC = () => {
<MaterialIcons name="info-outline" size={14} color={currentTheme.colors.warning || '#FFA500'} />
</View>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, flex: 1 }]}>
Keeping alerts enabled ensures you receive bug fixes and can provide accurate crash reports.
{t('updates.warning_note')}
</Text>
</View>
</SettingsCard>