diff --git a/src/components/home/AppleTVHero.tsx b/src/components/home/AppleTVHero.tsx index a4161a32..bfaf704a 100644 --- a/src/components/home/AppleTVHero.tsx +++ b/src/components/home/AppleTVHero.tsx @@ -12,6 +12,7 @@ import { Image, } from 'react-native'; import { NavigationProp, useNavigation, useIsFocused } from '@react-navigation/native'; +import { useTranslation } from 'react-i18next'; import { RootStackParamList } from '../../navigation/AppNavigator'; import { LinearGradient } from 'expo-linear-gradient'; import FastImage from '@d11/react-native-fast-image'; @@ -144,6 +145,7 @@ const AppleTVHero: React.FC = ({ onRetry, scrollY: externalScrollY, }) => { + const { t } = useTranslation(); const navigation = useNavigation>(); const isFocused = useIsFocused(); const { currentTheme } = useTheme(); @@ -158,7 +160,7 @@ const AppleTVHero: React.FC = ({ const [inLibrary, setInLibrary] = useState(false); const [isInWatchlist, setIsInWatchlist] = useState(false); const [isWatched, setIsWatched] = useState(false); - const [playButtonText, setPlayButtonText] = useState('Play'); + const [shouldResume, setShouldResume] = useState(false); const [type, setType] = useState<'movie' | 'series'>('movie'); // Create internal scrollY if not provided externally @@ -530,7 +532,8 @@ const AppleTVHero: React.FC = ({ useEffect(() => { if (currentItem) { const buttonText = getProgressPlayButtonText(); - setPlayButtonText(buttonText); + // Use internal state for resume logic instead of string comparison + setShouldResume(buttonText === 'Resume'); // Update watched state based on progress if (watchProgress) { @@ -987,10 +990,10 @@ const AppleTVHero: React.FC = ({ - No featured content available + {t('home.no_featured_available')} {onRetry && ( - Retry + {t('home.retry')} )} @@ -1242,7 +1245,7 @@ const AppleTVHero: React.FC = ({ - {currentItem.type === 'series' ? 'TV Show' : 'Movie'} + {currentItem.type === 'series' ? t('home.tv_show') : t('home.movie')} {currentItem.genres && currentItem.genres.length > 0 && ( <> @@ -1262,11 +1265,11 @@ const AppleTVHero: React.FC = ({ activeOpacity={0.85} > - {playButtonText} + {shouldResume ? t('home.resume') : t('home.play')} {/* Save Button */} diff --git a/src/components/home/CatalogSection.tsx b/src/components/home/CatalogSection.tsx index 14018c07..c83c09d8 100644 --- a/src/components/home/CatalogSection.tsx +++ b/src/components/home/CatalogSection.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useMemo, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions, FlatList } from 'react-native'; import { NavigationProp, useNavigation } from '@react-navigation/native'; +import { useTranslation } from 'react-i18next'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { CatalogContent, StreamingContent } from '../../services/catalogService'; @@ -73,6 +74,7 @@ const posterLayout = calculatePosterLayout(width); const POSTER_WIDTH = posterLayout.posterWidth; const CatalogSection = ({ catalog }: CatalogSectionProps) => { + const { t } = useTranslation(); const navigation = useNavigation>(); const { currentTheme } = useTheme(); @@ -154,7 +156,7 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => { fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14, marginRight: isTV ? 6 : isLargeTablet ? 5 : 4, } - ]}>View All + ]}>{t('home.view_all')} { + const { t } = useTranslation(); // Track inLibrary status locally to force re-render const [inLibrary, setInLibrary] = useState(!!item.inLibrary); @@ -182,10 +184,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe case 'library': if (inLibrary) { catalogService.removeFromLibrary(item.type, item.id); - showInfo('Removed from Library', 'Removed from your local library'); + showInfo(t('library.removed_from_library'), t('library.item_removed')); } else { catalogService.addToLibrary(item); - showSuccess('Added to Library', 'Added to your local library'); + showSuccess(t('library.added_to_library'), t('library.item_added')); } break; case 'watched': { @@ -194,7 +196,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe try { await mmkvStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false'); } catch { } - showInfo(targetWatched ? 'Marked as Watched' : 'Marked as Unwatched', targetWatched ? 'Item marked as watched' : 'Item marked as unwatched'); + showInfo(targetWatched ? t('library.marked_watched') : t('library.marked_unwatched'), targetWatched ? t('library.item_marked_watched') : t('library.item_marked_unwatched')); setTimeout(() => { DeviceEventEmitter.emit('watchedStatusChanged'); }, 100); @@ -240,10 +242,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe case 'trakt-watchlist': { if (isInWatchlist(item.id, item.type as 'movie' | 'show')) { await removeFromWatchlist(item.id, item.type as 'movie' | 'show'); - showInfo('Removed from Watchlist', 'Removed from your Trakt watchlist'); + showInfo(t('library.removed_from_watchlist'), t('library.removed_from_watchlist_desc')); } else { await addToWatchlist(item.id, item.type as 'movie' | 'show'); - showSuccess('Added to Watchlist', 'Added to your Trakt watchlist'); + showSuccess(t('library.added_to_watchlist'), t('library.added_to_watchlist_desc')); } setMenuVisible(false); break; @@ -251,10 +253,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe case 'trakt-collection': { if (isInCollection(item.id, item.type as 'movie' | 'show')) { await removeFromCollection(item.id, item.type as 'movie' | 'show'); - showInfo('Removed from Collection', 'Removed from your Trakt collection'); + showInfo(t('library.removed_from_collection'), t('library.removed_from_collection_desc')); } else { await addToCollection(item.id, item.type as 'movie' | 'show'); - showSuccess('Added to Collection', 'Added to your Trakt collection'); + showSuccess(t('library.added_to_collection'), t('library.added_to_collection_desc')); } setMenuVisible(false); break; diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index c93ef7b8..ef566341 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -11,6 +11,7 @@ import { Platform } from 'react-native'; import { FlatList } from 'react-native'; +import { useTranslation } from 'react-i18next'; import Animated, { FadeIn, Layout } from 'react-native-reanimated'; import BottomSheet, { BottomSheetModal, BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import { Ionicons } from '@expo/vector-icons'; @@ -107,6 +108,7 @@ const isEpisodeReleased = (video: any): boolean => { // Create a proper imperative handle with React.forwardRef and updated type const ContinueWatchingSection = React.forwardRef((props, ref) => { + const { t } = useTranslation(); const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { settings } = useSettings(); @@ -1310,7 +1312,7 @@ const ContinueWatchingSection = React.forwardRef((props, re {/* Up Next Badge */} {item.type === 'series' && item.progress === 0 && ( - UP NEXT + {t('home.up_next_caps')} )} @@ -1441,7 +1443,7 @@ const ContinueWatchingSection = React.forwardRef((props, re Up Next + ]}>{t('home.up_next')} )} @@ -1460,7 +1462,7 @@ const ContinueWatchingSection = React.forwardRef((props, re fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13 } ]}> - Season {item.season} + {t('home.season', { season: item.season })} {item.episodeTitle && ( ((props, re fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13 } ]}> - {item.year} • {item.type === 'movie' ? 'Movie' : 'Series'} + {item.year} • {item.type === 'movie' ? t('home.movie') : t('home.series')} ); } @@ -1519,7 +1521,7 @@ const ContinueWatchingSection = React.forwardRef((props, re fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 11 } ]}> - {Math.round(item.progress)}% watched + {t('home.percent_watched', { percent: Math.round(item.progress) })} )} @@ -1558,7 +1560,7 @@ const ContinueWatchingSection = React.forwardRef((props, re color: currentTheme.colors.text, fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24 } - ]}>Continue Watching + ]}>{t('home.continue_watching')} ((props, re {selectedItem.type === 'series' && selectedItem.season && selectedItem.episode ? ( - Season {selectedItem.season} · Episode {selectedItem.episode} + {t('home.season', { season: selectedItem.season })} · {t('home.episode', { episode: selectedItem.episode })} {selectedItem.episodeTitle && selectedItem.episodeTitle !== `Episode ${selectedItem.episode}` && `\n${selectedItem.episodeTitle}`} ) : ( - {selectedItem.year ? `${selectedItem.type === 'movie' ? 'Movie' : 'Series'} · ${selectedItem.year}` : selectedItem.type === 'movie' ? 'Movie' : 'Series'} + {selectedItem.year ? `${selectedItem.type === 'movie' ? t('home.movie') : t('home.series')} · ${selectedItem.year}` : selectedItem.type === 'movie' ? t('home.movie') : t('home.series')} )} {selectedItem.progress > 0 && ( @@ -1653,7 +1655,7 @@ const ContinueWatchingSection = React.forwardRef((props, re /> - {Math.round(selectedItem.progress)}% watched + {t('home.percent_watched', { percent: Math.round(selectedItem.progress) })} )} @@ -1668,7 +1670,7 @@ const ContinueWatchingSection = React.forwardRef((props, re activeOpacity={0.8} > - View Details + {t('home.view_details')} ((props, re activeOpacity={0.8} > - Remove + {t('home.remove')} diff --git a/src/components/home/DropUpMenu.tsx b/src/components/home/DropUpMenu.tsx index 41054ea3..542639c1 100644 --- a/src/components/home/DropUpMenu.tsx +++ b/src/components/home/DropUpMenu.tsx @@ -10,6 +10,7 @@ import { Dimensions, Platform } from 'react-native'; +import { useTranslation } from 'react-i18next'; import { MaterialIcons } from '@expo/vector-icons'; import FastImage from '@d11/react-native-fast-image'; import { useTraktContext } from '../../contexts/TraktContext'; @@ -39,6 +40,7 @@ interface DropUpMenuProps { } export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: isSavedProp, isWatched: isWatchedProp }: DropUpMenuProps) => { + const { t } = useTranslation(); const translateY = useSharedValue(300); const opacity = useSharedValue(0); const isDarkMode = useColorScheme() === 'dark'; @@ -102,12 +104,12 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is let menuOptions = [ { icon: 'bookmark', - label: isSaved ? 'Remove from Library' : 'Add to Library', + label: isSaved ? t('library.remove_from_library') : t('library.add_to_library'), action: 'library' }, { icon: 'check-circle', - label: isWatched ? 'Mark as Unwatched' : 'Mark as Watched', + label: isWatched ? t('library.mark_unwatched') : t('library.mark_watched'), action: 'watched' }, /* @@ -119,7 +121,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is */ { icon: 'share', - label: 'Share', + label: t('library.share'), action: 'share' } ]; @@ -129,12 +131,12 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is menuOptions.push( { icon: 'playlist-add-check', - label: inTraktWatchlist ? 'Remove from Trakt Watchlist' : 'Add to Trakt Watchlist', + label: inTraktWatchlist ? t('library.remove_from_watchlist') : t('library.add_to_watchlist'), action: 'trakt-watchlist' }, { icon: 'video-library', - label: inTraktCollection ? 'Remove from Trakt Collection' : 'Add to Trakt Collection', + label: inTraktCollection ? t('library.remove_from_collection') : t('library.add_to_collection'), action: 'trakt-collection' } ); diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 31d399ca..13056f01 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -13,6 +13,7 @@ import { Platform } from 'react-native'; import { NavigationProp, useNavigation } from '@react-navigation/native'; +import { useTranslation } from 'react-i18next'; import { RootStackParamList } from '../../navigation/AppNavigator'; import { LinearGradient } from 'expo-linear-gradient'; import FastImage from '@d11/react-native-fast-image'; @@ -52,6 +53,7 @@ const nowMs = () => Date.now(); const since = (start: number) => `${(nowMs() - start).toFixed(0)}ms`; const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => { + const { t } = useTranslation(); const navigation = useNavigation>(); const { currentTheme } = useTheme(); @@ -103,11 +105,11 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => { return ( - {onRetry ? 'Couldn\'t load featured content' : 'No Featured Content'} + {onRetry ? t('home.couldnt_load_featured') : t('home.no_featured_content')} {onRetry - ? 'There was a problem fetching featured content. Please check your connection and try again.' - : 'Install addons with catalogs or change the content source in your settings.'} + ? t('home.load_error_desc') + : t('home.no_featured_desc')} {onRetry ? ( @@ -115,7 +117,7 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => { style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]} onPress={onRetry} > - Retry + {t('home.retry')} ) : ( <> @@ -123,13 +125,13 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => { style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]} onPress={() => navigation.navigate('Addons')} > - Install Addons + {t('home.install_addons')} navigation.navigate('HomeScreenSettings')} > - Settings + {t('home.settings')} )} @@ -139,6 +141,7 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => { }; const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading, onRetry }: FeaturedContentProps) => { + const { t } = useTranslation(); const navigation = useNavigation>(); const { currentTheme } = useTheme(); const [bannerUrl, setBannerUrl] = useState(null); @@ -509,7 +512,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin > - Play Now + {t('home.play_now')} @@ -520,7 +523,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin > - {isSaved ? "Saved" : "My List"} + {isSaved ? t('home.saved') : t('home.my_list')} @@ -531,7 +534,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin > - More Info + {t('home.more_info')} @@ -626,7 +629,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin > - {isSaved ? "Saved" : "Save"} + {isSaved ? t('home.saved') : t('home.save')} @@ -644,7 +647,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin > - Play + {t('home.play')} @@ -655,7 +658,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin > - Info + {t('home.info')} diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index c498b479..6181a5d8 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useState, useEffect, useCallback, memo, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, ScrollView, StyleProp, Platform, Image, useWindowDimensions } from 'react-native'; +import { useTranslation } from 'react-i18next'; import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle, useAnimatedScrollHandler, useAnimatedReaction, runOnJS, SharedValue, interpolate, Extrapolation } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import { BlurView } from 'expo-blur'; @@ -38,6 +39,7 @@ interface HeroCarouselProps { const TOP_TABS_OFFSET = Platform.OS === 'ios' ? 44 : 48; const HeroCarousel: React.FC = ({ items, loading = false }) => { + const { t } = useTranslation(); const navigation = useNavigation>(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); @@ -610,6 +612,7 @@ interface CarouselCardProps { } const CarouselCard: React.FC = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index, flipped, onToggleFlip, interval, cardWidth, cardHeight, isTablet }) => { + const { t } = useTranslation(); const [bannerLoaded, setBannerLoaded] = useState(false); const [logoLoaded, setLogoLoaded] = useState(false); @@ -847,7 +850,7 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail textShadowRadius: 2, } ]}> - {item.description || 'No description available'} + {item.description || t('home.no_description')} @@ -956,7 +959,7 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail textShadowRadius: 2, } ]}> - {item.description || 'No description available'} + {item.description || t('home.no_description')} diff --git a/src/components/home/ThisWeekSection.tsx b/src/components/home/ThisWeekSection.tsx index da75ed9e..77d0539b 100644 --- a/src/components/home/ThisWeekSection.tsx +++ b/src/components/home/ThisWeekSection.tsx @@ -9,6 +9,7 @@ import { Dimensions } from 'react-native'; import { useNavigation } from '@react-navigation/native'; +import { useTranslation } from 'react-i18next'; import { NavigationProp } from '@react-navigation/native'; import FastImage from '@d11/react-native-fast-image'; import { LinearGradient } from 'expo-linear-gradient'; @@ -58,6 +59,7 @@ interface ThisWeekEpisode { } export const ThisWeekSection = React.memo(() => { + const { t } = useTranslation(); const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { calendarData, loading } = useCalendarData(); @@ -176,7 +178,7 @@ export const ThisWeekSection = React.memo(() => { processedItems.push({ ...firstEp, id: `group_${firstEp.seriesId}_${firstEp.releaseDate}`, // Unique ID for the group - title: `${group.length} New Episodes`, + title: t('home.new_episodes', { count: group.length }), isReleased, isGroup: true, episodeCount: group.length, @@ -239,7 +241,7 @@ export const ThisWeekSection = React.memo(() => { const renderEpisodeItem = ({ item, index }: { item: ThisWeekEpisode, index: number }) => { // Handle episodes without release dates gracefully const releaseDate = item.releaseDate ? parseISO(item.releaseDate) : null; - const formattedDate = releaseDate ? format(releaseDate, 'MMM d') : 'TBA'; + const formattedDate = releaseDate ? format(releaseDate, 'MMM d') : t('home.tba'); const isReleased = item.isReleased; // Use episode still image if available, fallback to series poster @@ -294,12 +296,12 @@ export const ThisWeekSection = React.memo(() => { locations={[0, 0.4, 0.7, 1]} > - - {isReleased ? (item.isGroup ? 'Released' : 'New') : formattedDate} + {isReleased ? (item.isGroup ? t('home.released') : t('home.new')) : formattedDate} @@ -357,7 +359,7 @@ export const ThisWeekSection = React.memo(() => { color: currentTheme.colors.text, fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24 } - ]}>This Week + ]}>{t('home.this_week')} { color: currentTheme.colors.textMuted, fontSize: isTV ? 18 : isLargeTablet ? 16 : isTablet ? 15 : 14 } - ]}>View All + ]}>{t('home.view_all')} { - try { - const savedLanguage = await mmkvStorage.getItem('user_language'); - if (savedLanguage) { - callback(savedLanguage); - return; + detect: (callback?: (lng: string) => void): string | undefined => { + const findLanguage = async () => { + try { + const savedLanguage = await mmkvStorage.getItem('user_language'); + if (savedLanguage) { + if (callback) callback(savedLanguage); + return; + } + } catch (error) { + console.log('Error reading language from storage', error); } - } catch (error) { - console.log('Error reading language from storage', error); - } - const locales = getLocales(); - const languageCode = locales[0]?.languageCode ?? 'en'; - callback(languageCode); + const locales = getLocales(); + const languageCode = locales[0]?.languageCode ?? 'en'; + if (callback) callback(languageCode); + }; + findLanguage(); + return undefined; }, init: () => { }, cacheUserLanguage: (language: string) => { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e2fe9244..e31e05e6 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -11,6 +11,56 @@ "ok": "OK", "unknown": "Unknown" }, + "home": { + "categories": { + "movies": "Movies", + "series": "Series", + "channels": "Channels" + }, + "movies": "Movies", + "tv_shows": "TV Shows", + "load_more_catalogs": "Load More Catalogs", + "no_content": "No content available", + "add_catalogs": "Add Catalogs", + "sign_in_available": "Sign In Available", + "sign_in_desc": "You can sign in anytime from Settings → Account", + "view_all": "View All", + "this_week": "This Week", + "continue_watching": "Continue Watching", + "up_next": "Up Next", + "up_next_caps": "UP NEXT", + "released": "Released", + "new": "New", + "tba": "TBA", + "new_episodes": "{{count}} New Episodes", + "season_short": "S{{season}}", + "episode_short": "E{{episode}}", + "season": "Season {{season}}", + "episode": "Episode {{episode}}", + "movie": "Movie", + "series": "Series", + "tv_show": "TV Show", + "percent_watched": "{{percent}}% watched", + "view_details": "View Details", + "remove": "Remove", + "play": "Play", + "play_now": "Play Now", + "resume": "Resume", + "info": "Info", + "more_info": "More Info", + "my_list": "My List", + "save": "Save", + "saved": "Saved", + "retry": "Retry", + "install_addons": "Install Addons", + "settings": "Settings", + "no_featured_content": "No Featured Content", + "couldnt_load_featured": "Couldn't load featured content", + "no_featured_desc": "Install addons with catalogs or change the content source in your settings.", + "load_error_desc": "There was a problem fetching featured content. Please check your connection and try again.", + "no_featured_available": "No featured content available", + "no_description": "No description available" + }, "navigation": { "home": "Home", "library": "Library", @@ -18,6 +68,119 @@ "downloads": "Downloads", "settings": "Settings" }, + "search": { + "title": "Search", + "recent_searches": "Recent Searches", + "discover": "Discover", + "movies": "Movies", + "tv_shows": "TV Shows", + "select_catalog": "Select Catalog", + "all_genres": "All Genres", + "discovering": "Discovering content...", + "show_more": "Show More ({{count}})", + "no_content_found": "No content found", + "try_different": "Try a different genre or catalog", + "select_catalog_desc": "Select a catalog to discover", + "tap_catalog_desc": "Tap the catalog chip above to get started", + "search_placeholder": "Search movies, shows...", + "keep_typing": "Keep typing...", + "type_characters": "Type at least 2 characters to search", + "no_results": "No results found", + "try_keywords": "Try different keywords or check your spelling", + "select_type": "Select Type", + "browse_movies": "Browse movie catalogs", + "browse_tv": "Browse TV series catalogs", + "select_genre": "Select Genre", + "show_all_content": "Show all content", + "genres_count": "{{count}} genres" + }, + "library": { + "title": "Library", + "watched": "Watched", + "continue": "Continue", + "watchlist": "Watchlist", + "collection": "Collection", + "rated": "Rated", + "items": "items", + "trakt_collections": "Trakt collections", + "trakt_collection": "Trakt Collection", + "no_trakt": "No Trakt collections", + "no_trakt_desc": "Your Trakt collections will appear here once you start using Trakt", + "load_collections": "Load Collections", + "empty_folder": "No content in {{folder}}", + "empty_folder_desc": "This collection is empty", + "refresh": "Refresh", + "no_movies": "No movies yet", + "no_series": "No TV shows yet", + "no_content": "No content yet", + "add_content_desc": "Add some content to your library to see it here", + "find_something": "Find something to watch", + "removed_from_library": "Removed from Library", + "item_removed": "Item removed from your library", + "failed_update_library": "Failed to update Library", + "unable_remove": "Unable to remove item from library", + "marked_watched": "Marked as Watched", + "marked_unwatched": "Marked as Unwatched", + "item_marked_watched": "Item marked as watched", + "item_marked_unwatched": "Item marked as unwatched", + "failed_update_watched": "Failed to update watched status", + "unable_update_watched": "Unable to update watched status", + "added_to_library": "Added to Library", + "item_added": "Added to your local library", + "add_to_library": "Add to Library", + "remove_from_library": "Remove from Library", + "mark_watched": "Mark as Watched", + "mark_unwatched": "Mark as Unwatched", + "share": "Share", + "add_to_watchlist": "Add to Trakt Watchlist", + "remove_from_watchlist": "Remove from Trakt Watchlist", + "added_to_watchlist": "Added to Watchlist", + "added_to_watchlist_desc": "Added to your Trakt watchlist", + "removed_from_watchlist": "Removed from Watchlist", + "removed_from_watchlist_desc": "Removed from your Trakt watchlist", + "add_to_collection": "Add to Trakt Collection", + "remove_from_collection": "Remove from Trakt Collection", + "added_to_collection": "Added to Collection", + "added_to_collection_desc": "Added to your Trakt collection", + "removed_from_collection": "Removed from Collection", + "removed_from_collection_desc": "Removed from your Trakt collection" + }, + "downloads": { + "title": "Downloads", + "no_downloads": "No Downloads Yet", + "no_downloads_desc": "Downloaded content will appear here for offline viewing", + "explore": "Explore Content", + "path_copied": "Path Copied", + "path_copied_desc": "Local file path copied to clipboard", + "copied": "Copied", + "incomplete": "Download Incomplete", + "incomplete_desc": "Download is not complete yet", + "not_available": "Not Available", + "not_available_desc": "The local file path is available only after the download is complete.", + "status_downloading": "Downloading", + "status_completed": "Completed", + "status_paused": "Paused", + "status_error": "Error", + "status_queued": "Queued", + "status_unknown": "Unknown", + "provider": "Provider", + "streaming_playlist_warning": "May not play - streaming playlist", + "remaining": "remaining", + "not_ready": "Download not ready", + "not_ready_desc": "Please wait until the download completes.", + "filter_all": "All", + "filter_active": "Active", + "filter_done": "Done", + "filter_paused": "Paused", + "no_filter_results": "No {{filter}} downloads", + "try_different_filter": "Try selecting a different filter", + "limitations_title": "Download Limitations", + "limitations_msg": "• Files smaller than 1MB are typically M3U8 streaming playlists and cannot be downloaded for offline viewing. These only work with online streaming and contain links to video segments, not the actual video content.", + "remove_title": "Remove Download", + "remove_confirm": "Remove \"{{title}}\"{{season_episode}}?", + "cancel": "Cancel", + "remove": "Remove" + }, "addons": { "title": "Addons", "reorder_mode": "Reorder Mode", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 5d4efaeb..ea7f7297 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -11,6 +11,56 @@ "ok": "OK", "unknown": "Desconhecido" }, + "home": { + "categories": { + "movies": "Filmes", + "series": "Séries", + "channels": "Canais" + }, + "movies": "Filmes", + "tv_shows": "Séries de TV", + "load_more_catalogs": "Carregar Mais Catálogos", + "no_content": "Nenhum conteúdo disponível", + "add_catalogs": "Adicionar Catálogos", + "sign_in_available": "Entrar Disponível", + "sign_in_desc": "Você pode entrar a qualquer momento em Configurações → Conta", + "view_all": "Ver Tudo", + "this_week": "Esta Semana", + "continue_watching": "Continue Assistindo", + "up_next": "A Seguir", + "up_next_caps": "A SEGUIR", + "released": "Lançado", + "new": "Novo", + "tba": "A confirmar", + "new_episodes": "{{count}} Novos Episódios", + "season_short": "T{{season}}", + "episode_short": "E{{episode}}", + "season": "Temporada {{season}}", + "episode": "Episódio {{episode}}", + "movie": "Filme", + "series": "Série", + "tv_show": "Série de TV", + "percent_watched": "{{percent}}% assistido", + "view_details": "Ver Detalhes", + "remove": "Remover", + "play": "Reproduzir", + "play_now": "Reproduzir Agora", + "resume": "Continuar", + "info": "Info", + "more_info": "Mais Info", + "my_list": "Minha Lista", + "save": "Salvar", + "saved": "Salvo", + "retry": "Tentar Novamente", + "install_addons": "Instalar Addons", + "settings": "Configurações", + "no_featured_content": "Nenhum Conteúdo em Destaque", + "couldnt_load_featured": "Não foi possível carregar o conteúdo em destaque", + "no_featured_desc": "Instale addons com catálogos ou altere a fonte de conteúdo nas configurações.", + "load_error_desc": "Houve um problema ao buscar o conteúdo em destaque. Verifique sua conexão e tente novamente.", + "no_featured_available": "Nenhum conteúdo em destaque disponível", + "no_description": "Nenhuma descrição disponível" + }, "navigation": { "home": "Início", "library": "Biblioteca", @@ -18,6 +68,119 @@ "downloads": "Downloads", "settings": "Configurações" }, + "search": { + "title": "Buscar", + "recent_searches": "Buscas Recentes", + "discover": "Descobrir", + "movies": "Filmes", + "tv_shows": "Séries", + "select_catalog": "Selecionar Catálogo", + "all_genres": "Todos os Gêneros", + "discovering": "Descobrindo conteúdo...", + "show_more": "Mostrar Mais ({{count}})", + "no_content_found": "Nenhum conteúdo encontrado", + "try_different": "Tente um gênero ou catálogo diferente", + "select_catalog_desc": "Selecione um catálogo para descobrir", + "tap_catalog_desc": "Toque no botão de catálogo acima para começar", + "search_placeholder": "Buscar filmes, séries...", + "keep_typing": "Continue digitando...", + "type_characters": "Digite pelo menos 2 caracteres para buscar", + "no_results": "Nenhum resultado encontrado", + "try_keywords": "Tente palavras-chave diferentes ou verifique a ortografia", + "select_type": "Selecionar Tipo", + "browse_movies": "Navegar catálogos de filmes", + "browse_tv": "Navegar catálogos de séries", + "select_genre": "Selecionar Gênero", + "show_all_content": "Mostrar todo o conteúdo", + "genres_count": "{{count}} gêneros" + }, + "library": { + "title": "Biblioteca", + "watched": "Assistidos", + "continue": "Continuar", + "watchlist": "Lista", + "collection": "Coleção", + "rated": "Avaliado", + "items": "itens", + "trakt_collections": "Coleções Trakt", + "trakt_collection": "Coleção Trakt", + "no_trakt": "Nenhuma coleção Trakt", + "no_trakt_desc": "Suas coleções do Trakt aparecerão aqui quando você começar a usar o Trakt", + "load_collections": "Carregar Coleções", + "empty_folder": "Nenhum conteúdo em {{folder}}", + "empty_folder_desc": "Esta coleção está vazia", + "refresh": "Atualizar", + "no_movies": "Nenhum filme ainda", + "no_series": "Nenhuma série ainda", + "no_content": "Nenhum conteúdo ainda", + "add_content_desc": "Adicione algum conteúdo à sua biblioteca para vê-lo aqui", + "find_something": "Encontrar algo para assistir", + "removed_from_library": "Removido da Biblioteca", + "item_removed": "Item removido da sua biblioteca", + "failed_update_library": "Falha ao atualizar Biblioteca", + "unable_remove": "Não foi possível remover o item da biblioteca", + "marked_watched": "Marcado como Assistido", + "marked_unwatched": "Marcado como Não Assistido", + "item_marked_watched": "Item marcado como assistido", + "item_marked_unwatched": "Item marcado como não assistido", + "failed_update_watched": "Falha ao atualizar status de assistido", + "unable_update_watched": "Não foi possível atualizar o status de assistido", + "added_to_library": "Adicionado à Biblioteca", + "item_added": "Adicionado à sua biblioteca local", + "add_to_library": "Adicionar à Biblioteca", + "remove_from_library": "Remover da Biblioteca", + "mark_watched": "Marcar como Assistido", + "mark_unwatched": "Marcar como Não Assistido", + "share": "Compartilhar", + "add_to_watchlist": "Adicionar à Lista Trakt", + "remove_from_watchlist": "Remover da Lista Trakt", + "added_to_watchlist": "Adicionado à Lista", + "added_to_watchlist_desc": "Adicionado à sua lista Trakt", + "removed_from_watchlist": "Removido da Lista", + "removed_from_watchlist_desc": "Removido da sua lista Trakt", + "add_to_collection": "Adicionar à Coleção Trakt", + "remove_from_collection": "Remover da Coleção Trakt", + "added_to_collection": "Adicionado à Coleção", + "added_to_collection_desc": "Adicionado à sua coleção Trakt", + "removed_from_collection": "Removido da Coleção", + "removed_from_collection_desc": "Removido da sua coleção Trakt" + }, + "downloads": { + "title": "Downloads", + "no_downloads": "Nenhum Download Ainda", + "no_downloads_desc": "Conteúdo baixado aparecerá aqui para visualização offline", + "explore": "Explorar Conteúdo", + "path_copied": "Caminho Copiado", + "path_copied_desc": "Caminho do arquivo local copiado para a área de transferência", + "copied": "Copiado", + "incomplete": "Download Incompleto", + "incomplete_desc": "O download ainda não está completo", + "not_available": "Não Disponível", + "not_available_desc": "O caminho do arquivo local está disponível apenas após a conclusão do download.", + "status_downloading": "Baixando", + "status_completed": "Concluído", + "status_paused": "Pausado", + "status_error": "Erro", + "status_queued": "Na Fila", + "status_unknown": "Desconhecido", + "provider": "Provedor", + "streaming_playlist_warning": "Pode não reproduzir - playlist de streaming", + "remaining": "restantes", + "not_ready": "Download não pronto", + "not_ready_desc": "Por favor aguarde até que o download seja concluído.", + "filter_all": "Todos", + "filter_active": "Ativos", + "filter_done": "Concluídos", + "filter_paused": "Pausados", + "no_filter_results": "Nenhum download {{filter}}", + "try_different_filter": "Tente selecionar um filtro diferente", + "limitations_title": "Limitações de Download", + "limitations_msg": "• Arquivos menores que 1MB são tipicamente playlists de streaming M3U8 e não podem ser baixados para visualização offline. Eles funcionam apenas com streaming online e contêm links para segmentos de vídeo, não o conteúdo de vídeo real.", + "remove_title": "Remover Download", + "remove_confirm": "Remover \"{{title}}\"{{season_episode}}?", + "cancel": "Cancelar", + "remove": "Remover" + }, "addons": { "title": "Addons", "reorder_mode": "Modo de Reordenação", diff --git a/src/screens/DownloadsScreen.tsx b/src/screens/DownloadsScreen.tsx index 4a5d4335..98208100 100644 --- a/src/screens/DownloadsScreen.tsx +++ b/src/screens/DownloadsScreen.tsx @@ -30,6 +30,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import FastImage from '@d11/react-native-fast-image'; import { useDownloads } from '../contexts/DownloadsContext'; import { useSettings } from '../hooks/useSettings'; +import { useTranslation } from 'react-i18next'; import { VideoPlayerService } from '../services/videoPlayerService'; import type { DownloadItem } from '../contexts/DownloadsContext'; import { useToast } from '../contexts/ToastContext'; @@ -65,6 +66,7 @@ const optimizePosterUrl = (poster: string | undefined | null): string => { // Empty state component const EmptyDownloadsState: React.FC<{ navigation: NavigationProp }> = ({ navigation }) => { const { currentTheme } = useTheme(); + const { t } = useTranslation(); return ( @@ -76,10 +78,10 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp - No Downloads Yet + {t('downloads.no_downloads')} - Downloaded content will appear here for offline viewing + {t('downloads.no_downloads_desc')} - Explore Content + {t('downloads.explore')} @@ -105,6 +107,7 @@ const DownloadItemComponent: React.FC<{ const { currentTheme } = useTheme(); const { settings } = useSettings(); const { showSuccess, showInfo } = useToast(); + const { t } = useTranslation(); const [posterUrl, setPosterUrl] = useState(item.posterUrl || null); const borderRadius = settings.posterBorderRadius ?? 12; @@ -121,15 +124,15 @@ const DownloadItemComponent: React.FC<{ if (item.status === 'completed' && item.fileUri) { Clipboard.setString(item.fileUri); if (Platform.OS === 'android') { - showSuccess('Path Copied', 'Local file path copied to clipboard'); + showSuccess(t('downloads.path_copied'), t('downloads.path_copied_desc')); } else { - Alert.alert('Copied', 'Local file path copied to clipboard'); + Alert.alert(t('downloads.copied'), t('downloads.path_copied_desc')); } } else if (item.status !== 'completed') { if (Platform.OS === 'android') { - showInfo('Download Incomplete', 'Download is not complete yet'); + showInfo(t('downloads.incomplete'), t('downloads.incomplete_desc')); } else { - Alert.alert('Not Available', 'The local file path is available only after the download is complete.'); + Alert.alert(t('downloads.not_available'), t('downloads.not_available_desc')); } } }, [item.status, item.fileUri, showSuccess, showInfo]); @@ -163,17 +166,17 @@ const DownloadItemComponent: React.FC<{ switch (item.status) { case 'downloading': const eta = item.etaSeconds ? `${Math.ceil(item.etaSeconds / 60)}m` : undefined; - return eta ? `Downloading • ${eta}` : 'Downloading'; + return eta ? `${t('downloads.status_downloading')} • ${eta}` : t('downloads.status_downloading'); case 'completed': - return 'Completed'; + return t('downloads.status_completed'); case 'paused': - return 'Paused'; + return t('downloads.status_paused'); case 'error': - return 'Error'; + return t('downloads.status_error'); case 'queued': - return 'Queued'; + return t('downloads.status_queued'); default: - return 'Unknown'; + return t('downloads.status_unknown'); } }; @@ -257,7 +260,7 @@ const DownloadItemComponent: React.FC<{ {/* Provider + quality row */} - {item.providerName || 'Provider'} + {item.providerName || t('downloads.provider')} {/* Status row */} @@ -283,7 +286,7 @@ const DownloadItemComponent: React.FC<{ color={currentTheme.colors.warning || '#FF9500'} /> - May not play - streaming playlist + {t('downloads.streaming_playlist_warning')} )} @@ -307,7 +310,7 @@ const DownloadItemComponent: React.FC<{ {item.etaSeconds && item.status === 'downloading' && ( - {Math.ceil(item.etaSeconds / 60)}m remaining + {Math.ceil(item.etaSeconds / 60)}m {t('downloads.remaining')} )} @@ -350,6 +353,7 @@ const DownloadsScreen: React.FC = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { settings } = useSettings(); + const { t } = useTranslation(); const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads(); const { showSuccess, showInfo } = useToast(); @@ -409,7 +413,7 @@ const DownloadsScreen: React.FC = () => { const handleDownloadPress = useCallback(async (item: DownloadItem) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); if (item.status !== 'completed') { - Alert.alert('Download not ready', 'Please wait until the download completes.'); + Alert.alert(t('downloads.not_ready'), t('downloads.not_ready_desc')); return; } const uri = (item as any).fileUri || (item as any).sourceUrl; @@ -636,7 +640,7 @@ const DownloadsScreen: React.FC = () => { {/* ScreenHeader Component */} { > {downloads.length > 0 && ( - {renderFilterButton('all', 'All', stats.total)} - {renderFilterButton('downloading', 'Active', stats.downloading)} - {renderFilterButton('completed', 'Done', stats.completed)} - {renderFilterButton('paused', 'Paused', stats.paused)} + {renderFilterButton('all', t('downloads.filter_all'), stats.total)} + {renderFilterButton('downloading', t('downloads.filter_active'), stats.downloading)} + {renderFilterButton('completed', t('downloads.filter_done'), stats.completed)} + {renderFilterButton('paused', t('downloads.filter_paused'), stats.paused)} )} @@ -697,10 +701,10 @@ const DownloadsScreen: React.FC = () => { color={currentTheme.colors.mediumEmphasis} /> - No {selectedFilter} downloads + {t('downloads.no_filter_results', { filter: selectedFilter })} - Try selecting a different filter + {t('downloads.try_different_filter')} )} @@ -710,19 +714,22 @@ const DownloadsScreen: React.FC = () => { {/* Help Alert */} setShowHelpAlert(false)} /> {/* Remove Download Confirmation */} setShowRemoveAlert(false) }, - { label: 'Remove', onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: {} }, + { label: t('downloads.cancel'), onPress: () => setShowRemoveAlert(false) }, + { label: t('downloads.remove'), onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: {} }, ]} onClose={() => { setShowRemoveAlert(false); setPendingRemoveItem(null); }} /> diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 364ccc1a..81b985f7 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { View, Text, @@ -113,6 +114,7 @@ const SkeletonCatalog = React.memo(() => { }); const HomeScreen = () => { + const { t } = useTranslation(); const navigation = useNavigation>(); const isDarkMode = useColorScheme() === 'dark'; const { currentTheme } = useTheme(); @@ -288,7 +290,7 @@ const HomeScreen = () => { displayName = uniqueWords.join(' '); // Append content type if not present - const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows'; + const contentType = catalog.type === 'movie' ? t('home.movies') : t('home.tv_shows'); if (!displayName.toLowerCase().includes(contentType.toLowerCase())) { displayName = `${displayName} ${contentType}`; } @@ -422,7 +424,7 @@ const HomeScreen = () => { await mmkvStorage.removeItem('showLoginHintToastOnce'); hideTimer = setTimeout(() => setHintVisible(false), 2000); // Also show a global toast for consistency across screens - // showInfo('Sign In Available', 'You can sign in anytime from Settings → Account'); + // showInfo(t('home.sign_in_available'), t('home.sign_in_desc')); } } catch { } })(); @@ -813,7 +815,7 @@ const HomeScreen = () => { > - Load More Catalogs + {t('home.load_more_catalogs')} @@ -835,14 +837,14 @@ const HomeScreen = () => { - No content available + {t('home.no_content')} navigation.navigate('Settings')} > - Add Catalogs + {t('home.add_catalogs')} )} diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 39c0cfab..c0f0d1f4 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -38,6 +38,7 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg'; import { traktService, TraktService, TraktImages } from '../services/traktService'; import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner'; import { useSettings } from '../hooks/useSettings'; +import { useTranslation } from 'react-i18next'; import { useScrollToTop } from '../contexts/ScrollToTopContext'; interface LibraryItem extends StreamingContent { @@ -211,6 +212,7 @@ const SkeletonLoader = () => { }; const LibraryScreen = () => { + const { t } = useTranslation(); const navigation = useNavigation>(); const isDarkMode = useColorScheme() === 'dark'; const { width, height } = useWindowDimensions(); @@ -361,31 +363,31 @@ const LibraryScreen = () => { const folders: TraktFolder[] = [ { id: 'watched', - name: 'Watched', + name: t('library.watched'), icon: 'visibility', itemCount: (watchedMovies?.length || 0) + (watchedShows?.length || 0), }, { id: 'continue-watching', - name: 'Continue', + name: t('library.continue'), icon: 'play-circle-outline', itemCount: continueWatching?.length || 0, }, { id: 'watchlist', - name: 'Watchlist', + name: t('library.watchlist'), icon: 'bookmark', itemCount: (watchlistMovies?.length || 0) + (watchlistShows?.length || 0), }, { id: 'collection', - name: 'Collection', + name: t('library.collection'), icon: 'library-add', itemCount: (collectionMovies?.length || 0) + (collectionShows?.length || 0), }, { id: 'ratings', - name: 'Rated', + name: t('library.rated'), icon: 'star', itemCount: ratedContent?.length || 0, } @@ -457,7 +459,7 @@ const LibraryScreen = () => { {folder.name} - {folder.itemCount} items + {folder.itemCount} {t('library.items')} @@ -487,14 +489,14 @@ const LibraryScreen = () => { {traktAuthenticated && traktFolders.length > 0 && ( - {traktFolders.length} items + {traktFolders.length} {t('library.items')} )} {settings.showPosterTitles && ( - Trakt collections + {t('library.trakt_collections')} )} @@ -720,9 +722,9 @@ const LibraryScreen = () => { return ( - No Trakt collections + {t('library.no_trakt')} - Your Trakt collections will appear here once you start using Trakt + {t('library.no_trakt_desc')} { }} activeOpacity={0.7} > - Load Collections + {t('library.load_collections')} ); @@ -758,13 +760,13 @@ const LibraryScreen = () => { const folderItems = getTraktFolderItems(selectedTraktFolder); if (folderItems.length === 0) { - const folderName = traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection'; + const folderName = traktFolders.find(f => f.id === selectedTraktFolder)?.name || t('library.collection'); return ( - No content in {folderName} + {t('library.empty_folder', { folder: folderName })} - This collection is empty + {t('library.empty_folder_desc')} { } if (filteredItems.length === 0) { - const emptyTitle = filter === 'movies' ? 'No movies yet' : filter === 'series' ? 'No TV shows yet' : 'No content yet'; - const emptySubtitle = 'Add some content to your library to see it here'; + const emptyTitle = filter === 'movies' ? t('library.no_movies') : filter === 'series' ? t('library.no_series') : t('library.no_content'); + const emptySubtitle = t('library.add_content_desc'); return ( { onPress={() => navigation.navigate('Search')} activeOpacity={0.7} > - Find something to watch + {t('library.find_something')} ); @@ -908,9 +910,9 @@ const LibraryScreen = () => { f.id === selectedTraktFolder)?.name || 'Collection' - : 'Trakt Collection') - : 'Library' + ? traktFolders.find(f => f.id === selectedTraktFolder)?.name || t('library.collection') + : t('library.trakt_collection')) + : t('library.title') } showBackButton={showTraktContent} onBackPress={showTraktContent ? () => { @@ -930,8 +932,8 @@ const LibraryScreen = () => { {!showTraktContent && ( {renderFilter('trakt', 'Trakt', 'pan-tool')} - {renderFilter('movies', 'Movies', 'movie')} - {renderFilter('series', 'TV Shows', 'live-tv')} + {renderFilter('movies', t('search.movies'), 'movie')} + {renderFilter('series', t('search.tv_shows'), 'live-tv')} )} @@ -951,11 +953,11 @@ const LibraryScreen = () => { case 'library': { try { await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id); - showInfo('Removed from Library', 'Item removed from your library'); + showInfo(t('library.removed_from_library'), t('library.item_removed')); setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type))); setMenuVisible(false); } catch (error) { - showError('Failed to update Library', 'Unable to remove item from library'); + showError(t('library.failed_update_library'), t('library.unable_remove')); } break; } @@ -964,14 +966,14 @@ const LibraryScreen = () => { const key = `watched:${selectedItem.type}:${selectedItem.id}`; const newWatched = !selectedItem.watched; await mmkvStorage.setItem(key, newWatched ? 'true' : 'false'); - showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched'); + showInfo(newWatched ? t('library.marked_watched') : t('library.marked_unwatched'), newWatched ? t('library.item_marked_watched') : t('library.item_marked_unwatched')); setLibraryItems(prev => prev.map(item => item.id === selectedItem.id && item.type === selectedItem.type ? { ...item, watched: newWatched } : item )); } catch (error) { - showError('Failed to update watched status', 'Unable to update watched status'); + showError(t('library.failed_update_watched'), t('library.unable_update_watched')); } break; } diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index 9c37096e..048b551b 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -21,6 +21,7 @@ import { } from 'react-native'; import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; +import { useTranslation } from 'react-i18next'; import { MaterialIcons, Feather } from '@expo/vector-icons'; import { catalogService, StreamingContent, GroupedSearchResults, AddonSearchResults } from '../services/catalogService'; import FastImage from '@d11/react-native-fast-image'; @@ -85,6 +86,7 @@ const SimpleSearchAnimation = SearchAnimation; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const SearchScreen = () => { + const { t } = useTranslation(); const { settings } = useSettings(); const navigation = useNavigation>(); const isDarkMode = true; @@ -597,7 +599,7 @@ const SearchScreen = () => { style={styles.recentSearchesContainer} > - Recent Searches + {t('search.recent_searches')} {recentSearches.map((search, index) => ( { {/* Section Header */} - Discover + {t('search.discover')} @@ -723,7 +725,7 @@ const SearchScreen = () => { onPress={() => typeSheetRef.current?.present()} > - {selectedDiscoverType === 'movie' ? 'Movies' : 'TV Shows'} + {selectedDiscoverType === 'movie' ? t('search.movies') : t('search.tv_shows')} @@ -734,7 +736,7 @@ const SearchScreen = () => { onPress={() => catalogSheetRef.current?.present()} > - {selectedCatalog ? selectedCatalog.catalogName : 'Select Catalog'} + {selectedCatalog ? selectedCatalog.catalogName : t('search.select_catalog')} @@ -746,7 +748,7 @@ const SearchScreen = () => { onPress={() => genreSheetRef.current?.present()} > - {selectedDiscoverGenre || 'All Genres'} + {selectedDiscoverGenre || t('search.all_genres')} @@ -757,7 +759,7 @@ const SearchScreen = () => { {selectedCatalog && ( - {selectedCatalog.addonName} • {selectedCatalog.type === 'movie' ? 'Movies' : 'TV Shows'} + {selectedCatalog.addonName} • {selectedCatalog.type === 'movie' ? t('search.movies') : t('search.tv_shows')} {selectedDiscoverGenre ? ` • ${selectedDiscoverGenre}` : ''} @@ -768,7 +770,7 @@ const SearchScreen = () => { - Discovering content... + {t('search.discovering')} ) : discoverResults.length > 0 ? ( @@ -804,7 +806,7 @@ const SearchScreen = () => { activeOpacity={0.7} > - Show More ({pendingDiscoverResults.length}) + {t('search.show_more', { count: pendingDiscoverResults.length })} @@ -819,20 +821,20 @@ const SearchScreen = () => { - No content found + {t('search.no_content_found')} - Try a different genre or catalog + {t('search.try_different')} ) : !selectedCatalog && discoverInitialized ? ( - Select a catalog to discover + {t('search.select_catalog_desc')} - Tap the catalog chip above to get started + {t('search.tap_catalog_desc')} ) : null} @@ -906,10 +908,10 @@ const SearchScreen = () => { isGrid && styles.discoverGridItem ]} onPress={() => { - navigation.navigate('Metadata', { - id: item.id, + navigation.navigate('Metadata', { + id: item.id, type: item.type, - addonId: item.addonId + addonId: item.addonId }); }} onLongPress={() => { @@ -1022,7 +1024,7 @@ const SearchScreen = () => { paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16 } ]}> - Movies ({movieResults.length}) + {t('search.movies')} ({movieResults.length}) { paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16 } ]}> - TV Shows ({seriesResults.length}) + {t('search.tv_shows')} ({seriesResults.length}) { styles.searchInput, { color: currentTheme.colors.white } ]} - placeholder="Search movies, shows..." + placeholder={t('search.search_placeholder')} placeholderTextColor={currentTheme.colors.lightGray} value={query} onChangeText={setQuery} @@ -1221,10 +1223,10 @@ const SearchScreen = () => { color={currentTheme.colors.lightGray} /> - Keep typing... + {t('search.keep_typing')} - Type at least 2 characters to search + {t('search.type_characters')} ) : searched && !hasResultsToShow ? ( @@ -1237,10 +1239,10 @@ const SearchScreen = () => { color={currentTheme.colors.lightGray} /> - No results found + {t('search.no_results')} - Try different keywords or check your spelling + {t('search.try_keywords')} ) : ( @@ -1343,7 +1345,7 @@ const SearchScreen = () => { > - Select Catalog + {t('search.select_catalog')} catalogSheetRef.current?.dismiss()}> @@ -1369,8 +1371,8 @@ const SearchScreen = () => { {catalog.catalogName} - {catalog.addonName} • {catalog.type === 'movie' ? 'Movies' : 'TV Shows'} - {catalog.genres.length > 0 ? ` • ${catalog.genres.length} genres` : ''} + {catalog.addonName} • {catalog.type === 'movie' ? t('search.movies') : t('search.tv_shows')} + {catalog.genres.length > 0 ? ` • ${t('search.genres_count', { count: catalog.genres.length })}` : ''} {selectedCatalog?.catalogId === catalog.catalogId && @@ -1403,7 +1405,7 @@ const SearchScreen = () => { > - Select Genre + {t('search.select_genre')} genreSheetRef.current?.dismiss()}> @@ -1423,10 +1425,10 @@ const SearchScreen = () => { > - All Genres + {t('search.all_genres')} - Show all content + {t('search.show_all_content')} {!selectedDiscoverGenre && ( @@ -1476,7 +1478,7 @@ const SearchScreen = () => { > - Select Type + {t('search.select_type')} typeSheetRef.current?.dismiss()}> @@ -1496,10 +1498,10 @@ const SearchScreen = () => { > - Movies + {t('search.movies')} - Browse movie catalogs + {t('search.browse_movies')} {selectedDiscoverType === 'movie' && ( @@ -1517,10 +1519,10 @@ const SearchScreen = () => { > - TV Shows + {t('search.tv_shows')} - Browse TV series catalogs + {t('search.browse_tv')} {selectedDiscoverType === 'series' && (