metascreen/streamscrean localization init

This commit is contained in:
tapframe 2026-01-06 14:46:11 +05:30
parent 6ef047db3c
commit 5e3198c9c6
13 changed files with 391 additions and 93 deletions

View file

@ -8,6 +8,7 @@ import {
ActivityIndicator,
Dimensions,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import FastImage from '@d11/react-native-fast-image';
import Animated, {
FadeIn,
@ -35,6 +36,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
onSelectCastMember,
isTmdbEnrichmentEnabled = true,
}) => {
const { t } = useTranslation();
const { currentTheme } = useTheme();
// Enhanced responsive sizing for tablets and TV screens
@ -137,7 +139,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 18,
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>Cast</Text>
]}>{t('metadata.cast')}</Text>
</View>
<FlatList
horizontal

View file

@ -8,6 +8,7 @@ import {
ActivityIndicator,
Dimensions,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import FastImage from '@d11/react-native-fast-image';
import { useNavigation, StackActions } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
@ -39,6 +40,7 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
collectionMovies,
loadingCollection
}) => {
const { t } = useTranslation();
const { currentTheme } = useTheme();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -109,9 +111,9 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
}
} catch (error) {
if (__DEV__) console.error('Error navigating to collection item:', error);
setAlertTitle('Error');
setAlertMessage('Unable to load this content. Please try again later.');
setAlertActions([{ label: 'OK', onPress: () => {} }]);
setAlertTitle(t('common.error'));
setAlertMessage(t('metadata.something_went_wrong'));
setAlertActions([{ label: t('common.ok'), onPress: () => {} }]);
setAlertVisible(true);
}
};

View file

@ -12,6 +12,7 @@ import {
Animated,
Linking,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { MaterialIcons } from '@expo/vector-icons';
import TraktIcon from '../../../assets/rating-icons/trakt.svg';
import { useTheme } from '../../contexts/ThemeContext';
@ -186,6 +187,7 @@ const CompactCommentCard: React.FC<{
isSpoilerRevealed: boolean;
onSpoilerPress: () => void;
}> = ({ comment, theme, onPress, isSpoilerRevealed, onSpoilerPress }) => {
const { t } = useTranslation();
const [isPressed, setIsPressed] = useState(false);
const fadeInOpacity = useRef(new Animated.Value(0)).current;
@ -262,7 +264,7 @@ const CompactCommentCard: React.FC<{
// Handle missing user data gracefully
const user = comment.user || {};
const username = user.name || user.username || 'Anonymous';
const username = user.name || user.username || t('common.anonymous_user');
// Handle spoiler content
const hasSpoiler = comment.spoiler;
@ -280,10 +282,10 @@ const CompactCommentCard: React.FC<{
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 1) return 'now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
if (diffMins < 1) return t('common.time.now');
if (diffMins < 60) return t('common.time.minutes_ago', { count: diffMins });
if (diffHours < 24) return t('common.time.hours_ago', { count: diffHours });
if (diffDays < 7) return t('common.time.days_ago', { count: diffDays });
// For older dates, show month/day
return commentDate.toLocaleDateString('en-US', {
@ -725,6 +727,7 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
episode,
onCommentPress,
}) => {
const { t } = useTranslation();
const { currentTheme } = useTheme();
const { settings } = useSettings();
const [hasLoadedOnce, setHasLoadedOnce] = React.useState(false);
@ -823,12 +826,12 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
<View style={styles.emptyContainer}>
<MaterialIcons name="chat-bubble-outline" size={48} color={currentTheme.colors.mediumEmphasis} />
<Text style={[styles.emptyText, { color: currentTheme.colors.mediumEmphasis }]}>
{error ? 'Comments unavailable' : 'No comments on Trakt yet'}
{error ? t('comments.unavailable') : t('comments.no_comments')}
</Text>
<Text style={[styles.emptySubtext, { color: currentTheme.colors.disabled }]}>
{error
? 'This content may not be in Trakt\'s database yet'
: 'Be the first to comment on Trakt.tv'
? t('comments.not_in_database')
: t('comments.check_trakt')
}
</Text>
</View>
@ -930,7 +933,7 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20
}
]}>
Trakt Comments
{t('comments.title')}
</Text>
</View>
@ -945,7 +948,7 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
onPress={refresh}
>
<Text style={[styles.retryButtonText, { color: currentTheme.colors.error }]}>
Retry
{t('common.retry')}
</Text>
</TouchableOpacity>
</View>
@ -993,7 +996,7 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
) : (
<>
<Text style={[styles.loadMoreText, { color: currentTheme.colors.primary }]}>
Load More
{t('common.load_more')}
</Text>
<MaterialIcons name="chevron-right" size={20} color={currentTheme.colors.primary} />
</>

View file

@ -9,6 +9,7 @@ import {
Dimensions,
Platform,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import FastImage from '@d11/react-native-fast-image';
import { useNavigation, StackActions } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
@ -39,6 +40,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
recommendations,
loadingRecommendations
}) => {
const { t } = useTranslation();
const { currentTheme } = useTheme();
const { settings } = useSettings();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -112,9 +114,9 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
}
} catch (error) {
if (__DEV__) console.error('Error navigating to recommendation:', error);
setAlertTitle('Error');
setAlertMessage('Unable to load this content. Please try again later.');
setAlertActions([{ label: 'OK', onPress: () => { } }]);
setAlertTitle(t('common.error'));
setAlertMessage(t('metadata.something_went_wrong'));
setAlertActions([{ label: t('common.ok'), onPress: () => { } }]);
setAlertVisible(true);
}
};
@ -149,7 +151,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
return (
<View style={[styles.container, { paddingLeft: 0 }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis, fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20, paddingHorizontal: horizontalPadding }]}>More Like This</Text>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis, fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20, paddingHorizontal: horizontalPadding }]}>{t('metadata.more_like_this')}</Text>
<FlatList
data={recommendations}
renderItem={renderItem}

View file

@ -10,6 +10,7 @@ import {
Platform,
Alert,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useTheme } from '../../contexts/ThemeContext';
import { useTrailer } from '../../contexts/TrailerContext';
import { logger } from '../../utils/logger';
@ -19,24 +20,6 @@ import Video, { VideoRef, OnLoadData, OnProgressData } from 'react-native-video'
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
// Helper function to format trailer type
const formatTrailerType = (type: string): string => {
switch (type) {
case 'Trailer':
return 'Official Trailer';
case 'Teaser':
return 'Teaser';
case 'Clip':
return 'Clip';
case 'Featurette':
return 'Featurette';
case 'Behind the Scenes':
return 'Behind the Scenes';
default:
return type;
}
};
interface TrailerVideo {
id: string;
key: string;
@ -61,8 +44,28 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
trailer,
contentTitle
}) => {
const { t } = useTranslation();
const { currentTheme } = useTheme();
const { pauseTrailer, resumeTrailer } = useTrailer();
// Helper function to format trailer type with translations
const formatTrailerType = useCallback((type: string): string => {
switch (type) {
case 'Trailer':
return t('trailers.official_trailer');
case 'Teaser':
return t('trailers.teaser');
case 'Clip':
return t('trailers.clip');
case 'Featurette':
return t('trailers.featurette');
case 'Behind the Scenes':
return t('trailers.behind_the_scenes');
default:
return type;
}
}, [t]);
const videoRef = React.useRef<VideoRef>(null);
const [trailerUrl, setTrailerUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@ -126,9 +129,9 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
logger.error('TrailerModal', 'Error loading trailer:', err);
Alert.alert(
'Trailer Unavailable',
'This trailer could not be loaded at this time. Please try again later.',
[{ text: 'OK', style: 'default' }]
t('trailers.unavailable'),
t('trailers.unavailable_desc'),
[{ text: t('common.ok'), style: 'default' }]
);
}
}, [trailer, contentTitle, pauseTrailer]);
@ -232,7 +235,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
hitSlop={{ top: 10, left: 10, right: 10, bottom: 10 }}
>
<Text style={[styles.closeButtonText, { color: currentTheme.colors.highEmphasis }]}>
Close
{t('common.close')}
</Text>
</TouchableOpacity>
</View>
@ -257,7 +260,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={loadTrailer}
>
<Text style={styles.retryButtonText}>Try Again</Text>
<Text style={styles.retryButtonText}>{t('common.try_again')}</Text>
</TouchableOpacity>
</View>
)}

View file

@ -11,6 +11,7 @@ import {
ScrollView,
Modal,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { MaterialIcons } from '@expo/vector-icons';
import FastImage from '@d11/react-native-fast-image';
import { useTheme } from '../../contexts/ThemeContext';
@ -59,6 +60,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
contentId,
contentTitle
}) => {
const { t } = useTranslation();
const { currentTheme } = useTheme();
const { settings } = useSettings();
const { pauseTrailer } = useTrailer();
@ -414,22 +416,22 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
};
// Format trailer type for display
const formatTrailerType = (type: string): string => {
const formatTrailerType = useCallback((type: string): string => {
switch (type) {
case 'Trailer':
return 'Official Trailers';
return t('trailers.official_trailers');
case 'Teaser':
return 'Teasers';
return t('trailers.teasers');
case 'Clip':
return 'Clips & Scenes';
return t('trailers.clips_scenes');
case 'Featurette':
return 'Featurettes';
return t('trailers.featurettes');
case 'Behind the Scenes':
return 'Behind the Scenes';
return t('trailers.behind_the_scenes');
default:
return type;
}
};
}, [t]);
// Get icon for trailer type
const getTrailerTypeIcon = (type: string): string => {
@ -483,12 +485,12 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
<View style={styles.header}>
<MaterialIcons name="movie" size={20} color={currentTheme.colors.primary} />
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
Trailers
{t('trailers.title')}
</Text>
</View>
<View style={styles.noTrailersContainer}>
<Text style={[styles.noTrailersText, { color: currentTheme.colors.textMuted }]}>
No trailers available
{t('trailers.no_trailers')}
</Text>
</View>
</View>
@ -512,7 +514,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20
}
]}>
Trailers & Videos
{t('trailers.title')}
</Text>
{/* Category Selector - Right Aligned */}

View file

@ -9,7 +9,22 @@
"error": "Error",
"success": "Success",
"ok": "OK",
"unknown": "Unknown"
"unknown": "Unknown",
"retry": "Retry",
"try_again": "Try Again",
"go_back": "Go Back",
"close": "Close",
"show_more": "Show More",
"show_less": "Show Less",
"load_more": "Load More",
"unknown_date": "Unknown date",
"anonymous_user": "Anonymous User",
"time": {
"now": "Just now",
"minutes_ago": "{{count}}m ago",
"hours_ago": "{{count}}h ago",
"days_ago": "{{count}}d ago"
}
},
"home": {
"categories": {
@ -145,6 +160,128 @@
"removed_from_collection": "Removed from Collection",
"removed_from_collection_desc": "Removed from your Trakt collection"
},
"metadata": {
"unable_to_load": "Unable to Load Content",
"error_code": "Error Code: {{code}}",
"content_not_found": "Content not found",
"content_not_found_desc": "This content doesn't exist or may have been removed.",
"server_error": "Server error",
"server_error_desc": "The server is temporarily unavailable. Please try again later.",
"bad_gateway": "Bad gateway",
"bad_gateway_desc": "The server is experiencing issues. Please try again later.",
"service_unavailable": "Service unavailable",
"service_unavailable_desc": "The service is currently down for maintenance. Please try again later.",
"too_many_requests": "Too many requests",
"too_many_requests_desc": "You're making too many requests. Please wait a moment and try again.",
"request_timeout": "Request timeout",
"request_timeout_desc": "The request took too long. Please try again.",
"network_error": "Network error",
"network_error_desc": "Please check your internet connection and try again.",
"auth_error": "Authentication error",
"auth_error_desc": "Please check your account settings and try again.",
"access_denied": "Access denied",
"access_denied_desc": "You don't have permission to access this content.",
"connection_error": "Connection error",
"streams_unavailable": "Streams unavailable",
"streams_unavailable_desc": "Streaming sources are currently unavailable. Please try again later.",
"unknown_error": "Unknown error",
"something_went_wrong": "Something went wrong. Please try again.",
"cast": "Cast",
"more_like_this": "More Like This",
"collection": "Collection",
"episodes": "Episodes",
"seasons": "Seasons",
"posters": "Posters",
"banners": "Banners",
"specials": "Specials",
"season_number": "Season {{number}}",
"episode_count": "{{count}} Episode",
"episode_count_plural": "{{count}} Episodes",
"no_episodes": "No episodes available",
"no_episodes_for_season": "No episodes available for Season {{season}}",
"episodes_not_released": "Episodes may not be released yet",
"no_description": "No description available",
"episode_label": "EPISODE {{number}}",
"watch_again": "Watch Again",
"completed": "Completed",
"play_episode": "Play S{{season}}E{{episode}}",
"play": "Play",
"watched": "Watched",
"watched_on_trakt": "Watched on Trakt",
"synced_with_trakt": "Synced with Trakt",
"saved": "Saved",
"director": "Director",
"directors": "Directors",
"creator": "Creator",
"creators": "Creators",
"production": "Production",
"network": "Network",
"mark_watched": "Mark as Watched",
"mark_unwatched": "Mark as Unwatched",
"marking": "Marking...",
"removing": "Removing...",
"unmark_season": "Unmark Season {{season}}",
"mark_season": "Mark Season {{season}}",
"resume": "Resume"
},
"cast": {
"biography": "Biography",
"known_for": "Known For",
"personal_info": "Personal Info",
"born_in": "Born in {{place}}",
"filmography": "Filmography",
"also_known_as": "Also Known As",
"no_info_available": "No additional information available"
},
"comments": {
"title": "Trakt Comments",
"spoiler_warning": "⚠️ This comment contains spoilers. Tap to reveal.",
"spoiler": "Spoiler",
"contains_spoilers": "Contains spoilers",
"reveal": "Reveal",
"vip": "VIP",
"unavailable": "Comments unavailable",
"no_comments": "No comments on Trakt yet",
"not_in_database": "This content may not be in Trakt's database yet",
"check_trakt": "Check Trakt"
},
"trailers": {
"title": "Trailers",
"official_trailers": "Official Trailers",
"official_trailer": "Official Trailer",
"teasers": "Teasers",
"teaser": "Teaser",
"clips_scenes": "Clips & Scenes",
"clip": "Clip",
"featurettes": "Featurettes",
"featurette": "Featurette",
"behind_the_scenes": "Behind the Scenes",
"no_trailers": "No trailers available",
"unavailable": "Trailer Unavailable",
"unavailable_desc": "This trailer could not be loaded at this time. Please try again later.",
"unable_to_play": "Unable to play trailer. Please try again.",
"watch_on_youtube": "Watch on YouTube"
},
"catalog": {
"no_content_found": "No content found",
"no_content_filters": "No content found for the selected filters",
"loading_content": "Loading content...",
"back": "Back"
},
"streams": {
"back_to_episodes": "Back to Episodes",
"back_to_info": "Back to Info",
"fetching_from": "Fetching from:",
"no_sources_available": "No streaming sources available",
"add_sources_desc": "Please add streaming sources in settings",
"add_sources": "Add Sources",
"finding_streams": "Finding available streams...",
"finding_best_stream": "Finding best stream for autoplay...",
"still_fetching": "Still fetching streams…",
"no_streams_available": "No streams available",
"starting_best_stream": "Starting best stream...",
"loading_more_sources": "Loading more sources..."
},
"downloads": {
"title": "Downloads",
"no_downloads": "No Downloads Yet",

View file

@ -9,7 +9,22 @@
"error": "Erro",
"success": "Sucesso",
"ok": "OK",
"unknown": "Desconhecido"
"unknown": "Desconhecido",
"retry": "Tentar Novamente",
"try_again": "Tentar Novamente",
"go_back": "Voltar",
"close": "Fechar",
"show_more": "Mostrar Mais",
"show_less": "Mostrar Menos",
"load_more": "Carregar Mais",
"unknown_date": "Data desconhecida",
"anonymous_user": "Usuário Anônimo",
"time": {
"now": "Agora",
"minutes_ago": "{{count}}m atrás",
"hours_ago": "{{count}}h atrás",
"days_ago": "{{count}}d atrás"
}
},
"home": {
"categories": {
@ -145,6 +160,128 @@
"removed_from_collection": "Removido da Coleção",
"removed_from_collection_desc": "Removido da sua coleção Trakt"
},
"metadata": {
"unable_to_load": "Não foi possível carregar o conteúdo",
"error_code": "Código de Erro: {{code}}",
"content_not_found": "Conteúdo não encontrado",
"content_not_found_desc": "Este conteúdo não existe ou pode ter sido removido.",
"server_error": "Erro do servidor",
"server_error_desc": "O servidor está temporariamente indisponível. Por favor, tente novamente mais tarde.",
"bad_gateway": "Gateway inválido",
"bad_gateway_desc": "O servidor está com problemas. Por favor, tente novamente mais tarde.",
"service_unavailable": "Serviço indisponível",
"service_unavailable_desc": "O serviço está em manutenção. Por favor, tente novamente mais tarde.",
"too_many_requests": "Muitas requisições",
"too_many_requests_desc": "Você está fazendo muitas requisições. Por favor, aguarde um momento e tente novamente.",
"request_timeout": "Tempo limite da requisição",
"request_timeout_desc": "A requisição demorou muito. Por favor, tente novamente.",
"network_error": "Erro de rede",
"network_error_desc": "Por favor, verifique sua conexão com a internet e tente novamente.",
"auth_error": "Erro de autenticação",
"auth_error_desc": "Por favor, verifique as configurações da sua conta e tente novamente.",
"access_denied": "Acesso negado",
"access_denied_desc": "Você não tem permissão para acessar este conteúdo.",
"connection_error": "Erro de conexão",
"streams_unavailable": "Streams indisponíveis",
"streams_unavailable_desc": "Fontes de streaming estão temporariamente indisponíveis. Por favor, tente novamente mais tarde.",
"unknown_error": "Erro desconhecido",
"something_went_wrong": "Algo deu errado. Por favor, tente novamente.",
"cast": "Elenco",
"more_like_this": "Mais Como Este",
"collection": "Coleção",
"episodes": "Episódios",
"seasons": "Temporadas",
"posters": "Pôsteres",
"banners": "Banners",
"specials": "Especiais",
"season_number": "Temporada {{number}}",
"episode_count": "{{count}} Episódio",
"episode_count_plural": "{{count}} Episódios",
"no_episodes": "Nenhum episódio disponível",
"no_episodes_for_season": "Nenhum episódio disponível para a Temporada {{season}}",
"episodes_not_released": "Os episódios podem ainda não ter sido lançados",
"no_description": "Nenhuma descrição disponível",
"episode_label": "EPISÓDIO {{number}}",
"watch_again": "Assistir Novamente",
"completed": "Concluído",
"play_episode": "Reproduzir T{{season}}E{{episode}}",
"play": "Reproduzir",
"watched": "Assistido",
"watched_on_trakt": "Assistido no Trakt",
"synced_with_trakt": "Sincronizado com Trakt",
"saved": "Salvo",
"director": "Diretor",
"directors": "Diretores",
"creator": "Criador",
"creators": "Criadores",
"production": "Produção",
"network": "Emissora",
"mark_watched": "Marcar como Assistido",
"mark_unwatched": "Marcar como Não Assistido",
"marking": "Marcando...",
"removing": "Removendo...",
"unmark_season": "Desmarcar Temporada {{season}}",
"mark_season": "Marcar Temporada {{season}}",
"resume": "Continuar"
},
"cast": {
"biography": "Biografia",
"known_for": "Conhecido Por",
"personal_info": "Informações Pessoais",
"born_in": "Nascido em {{place}}",
"filmography": "Filmografia",
"also_known_as": "Também Conhecido Como",
"no_info_available": "Nenhuma informação adicional disponível"
},
"comments": {
"title": "Comentários do Trakt",
"spoiler_warning": "⚠️ Este comentário contém spoilers. Toque para revelar.",
"spoiler": "Spoiler",
"contains_spoilers": "Contém spoilers",
"reveal": "Revelar",
"vip": "VIP",
"unavailable": "Comentários indisponíveis",
"no_comments": "Ainda não há comentários no Trakt",
"not_in_database": "Este conteúdo pode ainda não estar no banco de dados do Trakt",
"check_trakt": "Ver no Trakt"
},
"trailers": {
"title": "Trailers",
"official_trailers": "Trailers Oficiais",
"official_trailer": "Trailer Oficial",
"teasers": "Teasers",
"teaser": "Teaser",
"clips_scenes": "Clipes e Cenas",
"clip": "Clipe",
"featurettes": "Featurettes",
"featurette": "Featurette",
"behind_the_scenes": "Bastidores",
"no_trailers": "Nenhum trailer disponível",
"unavailable": "Trailer Indisponível",
"unavailable_desc": "Este trailer não pôde ser carregado no momento. Por favor, tente novamente mais tarde.",
"unable_to_play": "Não foi possível reproduzir o trailer. Por favor, tente novamente.",
"watch_on_youtube": "Assistir no YouTube"
},
"catalog": {
"no_content_found": "Nenhum conteúdo encontrado",
"no_content_filters": "Nenhum conteúdo encontrado para os filtros selecionados",
"loading_content": "Carregando conteúdo...",
"back": "Voltar"
},
"streams": {
"back_to_episodes": "Voltar aos Episódios",
"back_to_info": "Voltar às Informações",
"fetching_from": "Buscando de:",
"no_sources_available": "Nenhuma fonte de streaming disponível",
"add_sources_desc": "Por favor, adicione fontes de streaming nas configurações",
"add_sources": "Adicionar Fontes",
"finding_streams": "Procurando streams disponíveis...",
"finding_best_stream": "Procurando melhor stream para reprodução automática...",
"still_fetching": "Ainda buscando streams…",
"no_streams_available": "Nenhum stream disponível",
"starting_best_stream": "Iniciando melhor stream...",
"loading_more_sources": "Carregando mais fontes..."
},
"downloads": {
"title": "Downloads",
"no_downloads": "Nenhum Download Ainda",

View file

@ -13,6 +13,7 @@ import {
InteractionManager,
ScrollView
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { FlashList } from '@shopify/flash-list';
import { RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
@ -267,6 +268,7 @@ const createStyles = (colors: any) => StyleSheet.create({
const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
const { addonId, type, id, name: originalName, genreFilter } = route.params;
const { t } = useTranslation();
const [items, setItems] = useState<Meta[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
@ -495,7 +497,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
return;
} else {
InteractionManager.runAfterInteractions(() => {
setError("No content found for the selected filters");
setError(t('catalog.no_content_filters'));
setItems([]);
setLoading(false);
setRefreshing(false);
@ -665,7 +667,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
if (!foundItems) {
InteractionManager.runAfterInteractions(() => {
setError("No content found for the selected filters");
setError(t('catalog.no_content_filters'));
logger.log('[CatalogScreen] No items found after loading');
});
}
@ -845,13 +847,13 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
<View style={styles.centered}>
<MaterialIcons name="search-off" size={56} color={colors.mediumGray} />
<Text style={styles.emptyText}>
No content found
{t('catalog.no_content_found')}
</Text>
<TouchableOpacity
style={styles.button}
onPress={handleRefresh}
>
<Text style={styles.buttonText}>Try Again</Text>
<Text style={styles.buttonText}>{t('common.try_again')}</Text>
</TouchableOpacity>
</View>
);
@ -866,7 +868,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
style={styles.button}
onPress={() => loadItems(true)}
>
<Text style={styles.buttonText}>Retry</Text>
<Text style={styles.buttonText}>{t('common.retry')}</Text>
</TouchableOpacity>
</View>
);
@ -874,7 +876,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
const renderLoadingState = () => (
<View style={styles.centered}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>Loading content...</Text>
<Text style={styles.loadingText}>{t('catalog.loading_content')}</Text>
</View>
);

View file

@ -12,6 +12,7 @@ import {
Platform,
Alert,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
@ -88,6 +89,7 @@ const MetadataScreen: React.FC = () => {
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string; addonId?: string }>, string>>();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { id, type, episodeId, addonId } = route.params;
const { t } = useTranslation();
// Log route parameters for debugging
React.useEffect(() => {
@ -780,19 +782,19 @@ const MetadataScreen: React.FC = () => {
console.log('✅ Found status code:', code);
switch (code) {
case 404:
return { code: '404', message: 'Content not found', userMessage: 'This content doesn\'t exist or may have been removed.' };
return { code: '404', message: t('metadata.content_not_found'), userMessage: t('metadata.content_not_found_desc') };
case 500:
return { code: '500', message: 'Server error', userMessage: 'The server is temporarily unavailable. Please try again later.' };
return { code: '500', message: t('metadata.server_error'), userMessage: t('metadata.server_error_desc') };
case 502:
return { code: '502', message: 'Bad gateway', userMessage: 'The server is experiencing issues. Please try again later.' };
return { code: '502', message: t('metadata.bad_gateway'), userMessage: t('metadata.bad_gateway_desc') };
case 503:
return { code: '503', message: 'Service unavailable', userMessage: 'The service is currently down for maintenance. Please try again later.' };
return { code: '503', message: t('metadata.service_unavailable'), userMessage: t('metadata.service_unavailable_desc') };
case 429:
return { code: '429', message: 'Too many requests', userMessage: 'You\'re making too many requests. Please wait a moment and try again.' };
return { code: '429', message: t('metadata.too_many_requests'), userMessage: t('metadata.too_many_requests_desc') };
case 408:
return { code: '408', message: 'Request timeout', userMessage: 'The request took too long. Please try again.' };
return { code: '408', message: t('metadata.request_timeout'), userMessage: t('metadata.request_timeout_desc') };
default:
return { code: code.toString(), message: `Error ${code}`, userMessage: 'Something went wrong. Please try again.' };
return { code: code.toString(), message: `Error ${code}`, userMessage: t('metadata.something_went_wrong') };
}
}
@ -801,7 +803,7 @@ const MetadataScreen: React.FC = () => {
error.includes('ERR_BAD_RESPONSE') ||
error.includes('Request failed') ||
error.includes('ERR_NETWORK')) {
return { code: 'NETWORK', message: 'Network error', userMessage: 'Please check your internet connection and try again.' };
return { code: 'NETWORK', message: t('metadata.network_error'), userMessage: t('metadata.network_error_desc') };
}
// Check for timeout errors
@ -809,36 +811,36 @@ const MetadataScreen: React.FC = () => {
error.includes('timed out') ||
error.includes('ECONNABORTED') ||
error.includes('ETIMEDOUT')) {
return { code: 'TIMEOUT', message: 'Request timeout', userMessage: 'The request took too long. Please try again.' };
return { code: 'TIMEOUT', message: t('metadata.request_timeout'), userMessage: t('metadata.request_timeout_desc') };
}
// Check for authentication errors
if (error.includes('401') || error.includes('Unauthorized') || error.includes('authentication')) {
return { code: '401', message: 'Authentication error', userMessage: 'Please check your account settings and try again.' };
return { code: '401', message: t('metadata.auth_error'), userMessage: t('metadata.auth_error_desc') };
}
// Check for permission errors
if (error.includes('403') || error.includes('Forbidden') || error.includes('permission')) {
return { code: '403', message: 'Access denied', userMessage: 'You don\'t have permission to access this content.' };
return { code: '403', message: t('metadata.access_denied'), userMessage: t('metadata.access_denied_desc') };
}
// Check for "not found" errors - but only if no status code was found
if (!statusCodeMatch && (error.includes('Content not found') || error.includes('not found'))) {
return { code: '404', message: 'Content not found', userMessage: 'This content doesn\'t exist or may have been removed.' };
return { code: '404', message: t('metadata.content_not_found'), userMessage: t('metadata.content_not_found_desc') };
}
// Check for retry/attempt errors
if (error.includes('attempts') || error.includes('Please check your connection')) {
return { code: 'CONNECTION', message: 'Connection error', userMessage: 'Please check your internet connection and try again.' };
return { code: 'CONNECTION', message: t('metadata.connection_error'), userMessage: t('metadata.network_error_desc') };
}
// Check for streams-related errors
if (error.includes('streams') || error.includes('Failed to load streams')) {
return { code: 'STREAMS', message: 'Streams unavailable', userMessage: 'Streaming sources are currently unavailable. Please try again later.' };
return { code: 'STREAMS', message: t('metadata.streams_unavailable'), userMessage: t('metadata.streams_unavailable_desc') };
}
// Default case
return { code: 'UNKNOWN', message: 'Unknown error', userMessage: 'An unexpected error occurred. Please try again.' };
return { code: 'UNKNOWN', message: t('metadata.unknown_error'), userMessage: t('metadata.something_went_wrong') };
};
const errorInfo = parseError(metadataError);
@ -852,10 +854,10 @@ const MetadataScreen: React.FC = () => {
<View style={styles.errorContainer}>
<MaterialIcons name="error-outline" size={64} color={currentTheme.colors.error || '#FF6B6B'} />
<Text style={[styles.errorTitle, { color: currentTheme.colors.highEmphasis }]}>
Unable to Load Content
{t('metadata.unable_to_load')}
</Text>
<Text style={[styles.errorCode, { color: currentTheme.colors.textMuted }]}>
Error Code: {errorInfo.code}
{t('metadata.error_code', { code: errorInfo.code })}
</Text>
<Text style={[styles.errorMessage, { color: currentTheme.colors.highEmphasis }]}>
{errorInfo.userMessage}
@ -870,13 +872,13 @@ const MetadataScreen: React.FC = () => {
onPress={loadMetadata}
>
<MaterialIcons name="refresh" size={20} color={currentTheme.colors.white} style={{ marginRight: 8 }} />
<Text style={styles.retryButtonText}>Try Again</Text>
<Text style={styles.retryButtonText}>{t('common.try_again')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.backButton, { borderColor: currentTheme.colors.primary }]}
onPress={handleBack}
>
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text>
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>{t('common.go_back')}</Text>
</TouchableOpacity>
</View>
</SafeAreaView>

View file

@ -1,5 +1,6 @@
import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, StatusBar, Platform } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialIcons } from '@expo/vector-icons';
import { PaperProvider } from 'react-native-paper';
@ -88,6 +89,7 @@ export const StreamsScreen = () => {
gradientColors,
} = useStreamsScreen();
const { t } = useTranslation();
const styles = React.useMemo(() => createStyles(colors), [colors]);
return (
@ -106,8 +108,8 @@ export const StreamsScreen = () => {
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
<Text style={styles.backButtonText}>
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode
? 'Back to Episodes'
: 'Back to Info'}
? t('streams.back_to_episodes')
: t('streams.back_to_info')}
</Text>
</TouchableOpacity>
</View>

View file

@ -1,5 +1,6 @@
import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Platform } from 'react-native';
import { useTranslation } from 'react-i18next';
import { LinearGradient } from 'expo-linear-gradient';
import { BlurView as ExpoBlurView } from 'expo-blur';
import { MaterialIcons } from '@expo/vector-icons';
@ -129,6 +130,7 @@ const MobileStreamsLayout = memo(
id,
imdbId,
}: MobileStreamsLayoutProps) => {
const { t } = useTranslation();
const styles = React.useMemo(() => createStyles(colors), [colors]);
const isEpisode = metadata?.videos && metadata.videos.length > 1 && selectedEpisode;
@ -227,7 +229,7 @@ const MobileStreamsLayout = memo(
{/* Active Scrapers Status */}
{activeFetchingScrapers.length > 0 && (
<View style={styles.activeScrapersContainer}>
<Text style={styles.activeScrapersTitle}>Fetching from:</Text>
<Text style={styles.activeScrapersTitle}>{t('streams.fetching_from')}</Text>
<View style={styles.activeScrapersRow}>
{activeFetchingScrapers.map((scraperName, index) => (
<PulsingChip key={scraperName} text={scraperName} delay={index * 200} />
@ -240,13 +242,13 @@ const MobileStreamsLayout = memo(
{showNoSourcesError ? (
<View style={styles.noStreams}>
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
<Text style={styles.noStreamsText}>No streaming sources available</Text>
<Text style={styles.noStreamsSubText}>Please add streaming sources in settings</Text>
<Text style={styles.noStreamsText}>{t('streams.no_sources_available')}</Text>
<Text style={styles.noStreamsSubText}>{t('streams.add_sources_desc')}</Text>
<TouchableOpacity
style={styles.addSourcesButton}
onPress={() => navigation.navigate('Addons' as never)}
>
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
<Text style={styles.addSourcesButtonText}>{t('streams.add_sources')}</Text>
</TouchableOpacity>
</View>
) : streamsEmpty ? (
@ -254,18 +256,18 @@ const MobileStreamsLayout = memo(
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>
{isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
{isAutoplayWaiting ? t('streams.finding_best_stream') : t('streams.finding_streams')}
</Text>
</View>
) : showStillFetching ? (
<View style={styles.loadingContainer}>
<MaterialIcons name="hourglass-bottom" size={32} color={colors.primary} />
<Text style={styles.loadingText}>Still fetching streams</Text>
<Text style={styles.loadingText}>{t('streams.still_fetching')}</Text>
</View>
) : (
<View style={styles.noStreams}>
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
<Text style={styles.noStreamsText}>No streams available</Text>
<Text style={styles.noStreamsText}>{t('streams.no_streams_available')}</Text>
</View>
)
) : (

View file

@ -6,6 +6,7 @@ import {
ActivityIndicator,
Platform,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { LegendList } from '@legendapp/list';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -59,6 +60,7 @@ const StreamsList = memo(
id,
imdbId,
}: StreamsListProps) => {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const styles = React.useMemo(() => createStyles(colors), [colors]);
@ -91,7 +93,7 @@ const StreamsList = memo(
<View style={styles.sectionLoadingIndicator}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={[styles.sectionLoadingText, { color: colors.primary }]}>
Loading...
{t('common.loading')}
</Text>
</View>
)}
@ -157,21 +159,21 @@ const StreamsList = memo(
<View style={styles.autoplayOverlay}>
<View style={styles.autoplayIndicator}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={styles.autoplayText}>Starting best stream...</Text>
<Text style={styles.autoplayText}>{t('streams.starting_best_stream')}</Text>
</View>
</View>
);
}, [isAutoplayWaiting, autoplayTriggered, styles, colors.primary]);
}, [isAutoplayWaiting, autoplayTriggered, styles, colors.primary, t]);
const ListFooterComponent = useMemo(() => {
if (!(loadingStreams || loadingEpisodeStreams) || !hasStremioStreamProviders) return null;
return (
<View style={styles.footerLoading}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
<Text style={styles.footerLoadingText}>{t('streams.loading_more_sources')}</Text>
</View>
);
}, [loadingStreams, loadingEpisodeStreams, hasStremioStreamProviders, styles, colors.primary]);
}, [loadingStreams, loadingEpisodeStreams, hasStremioStreamProviders, styles, colors.primary, t]);
return (
<View collapsable={false} style={{ flex: 1 }}>