Fixed various language translations

This commit is contained in:
Gianluca Sorbello 2026-03-14 08:40:10 +01:00
parent e0077244c6
commit ded57baeab
6 changed files with 737 additions and 541 deletions

View file

@ -18,6 +18,7 @@ import Animated, {
interpolate,
Extrapolate,
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
import { useTheme } from '../../contexts/ThemeContext';
import { isMDBListEnabled } from '../../screens/MDBListSettingsScreen';
import { getAgeRatingColor } from '../../utils/ageRatingColors';
@ -53,6 +54,7 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
contentId,
loadingMetadata = false,
}) => {
const { t } = useTranslation();
const { currentTheme } = useTheme();
const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false);
const [isMDBEnabled, setIsMDBEnabled] = useState(false);
@ -407,7 +409,7 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14
}
]}>
{isFullDescriptionOpen ? 'Show Less' : 'Show More'}
{isFullDescriptionOpen ? t('common.show_less') : t('common.show_more')}
</Text>
<MaterialIcons
name={isFullDescriptionOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"}
@ -533,4 +535,4 @@ const styles = StyleSheet.create({
},
});
export default React.memo(MetadataDetails);
export default React.memo(MetadataDetails);

View file

@ -469,13 +469,13 @@
"profanity": "Profanity",
"alcohol": "Alcohol/Drugs",
"frightening": "Frightening"
},
},
"severity": {
"severe": "Severe",
"moderate": "Moderate",
"mild": "Mild",
"none": "None"
}
}
},
"addons": {
"title": "Addons",
@ -565,6 +565,43 @@
"conflict_msg": "You cannot connect to Simkl while Trakt is connected. Please disconnect Trakt first.",
"disclaimer": "Nuvio is not affiliated with Simkl."
},
"mal_settings": {
"title": "MyAnimeList",
"connect_title": "Connect MyAnimeList",
"connect_desc": "Sync your watch history and manage your anime list.",
"sign_in": "Sign In with MyAnimeList",
"sign_out": "Disconnect",
"sign_out_confirm": "Are you sure you want to disconnect your MyAnimeList account?",
"auth_success_title": "Successfully Connected",
"auth_success_msg": "Your MyAnimeList account has been connected successfully.",
"auth_error_title": "Authentication Error",
"auth_error_msg": "Failed to complete authentication with MyAnimeList.",
"auth_error_generic": "An error occurred during authentication.",
"sync_complete_title": "Sync Complete",
"sync_error_title": "Sync Failed",
"sync_success_msg": "MyAnimeList data refreshed successfully.",
"sync_error_msg": "Failed to refresh MyAnimeList data. Please try again.",
"sync_now_button": "Sync Now",
"sync_settings_title": "Sync Settings",
"profile_id": "ID: {{id}}",
"stats_total": "Total",
"stats_days": "Days",
"stats_mean": "Mean",
"status_watching": "Watching",
"status_completed": "Completed",
"status_on_hold": "On Hold",
"status_dropped": "Dropped",
"sync_enabled_label": "Enable MyAnimeList Sync",
"sync_enabled_desc": "Global switch to enable or disable all MyAnimeList features.",
"auto_update_label": "Auto Episode Update",
"auto_update_desc": "Automatically update your progress on MyAnimeList when you finish watching an episode (>=90% completion).",
"auto_add_label": "Auto Add Anime",
"auto_add_desc": "If an anime is not in your MyAnimeList list, it will be added automatically when you start watching.",
"auto_library_sync_label": "Auto-Sync to Library",
"auto_library_sync_desc": "Automatically add items from your MyAnimeList \"Watching\" list to your Nuvio Library.",
"include_nsfw_label": "Include NSFW Content",
"include_nsfw_desc": "Allow NSFW entries to be returned when fetching your MyAnimeList list."
},
"tmdb_settings": {
"title": "TMDb Settings",
"metadata_enrichment": "Metadata Enrichment",
@ -916,6 +953,46 @@
"success_removed": "API key removed successfully",
"error_remove": "Failed to remove API key"
},
"ai_chat": {
"title": "AI Chat",
"loading_context": "Loading AI context...",
"welcome_title": "Ask me anything about",
"welcome_description": "I have detailed knowledge about this content and can answer questions about plot, characters, themes, and more.",
"try_asking": "Try asking:",
"placeholder": "Ask about this content...",
"errors": {
"load_content_details": "Failed to load content details for AI chat",
"generic": "Sorry, I encountered an error. Please try again.",
"not_configured": "Please configure your OpenRouter API key in Settings > AI Assistant.",
"invalid_api_key": "OpenRouter rejected your API key. Please verify the key in Settings > AI Assistant.",
"quota": "OpenRouter quota/credits were rejected for this request. Please check your OpenRouter usage and limits.",
"model_unavailable": "The selected OpenRouter model is unavailable. Retry with `openrouter/free` or choose another custom model in Settings > AI Assistant.",
"connection": "Failed to connect to AI service. Please check your internet connection, API key, and OpenRouter model availability."
},
"starters": {
"series": {
"overall": "What is {{title}} about overall?",
"arcs": "Summarize key arcs across all seasons",
"rated": "Which episodes are the highest rated and why?",
"pivotal": "List pivotal episodes for character development",
"themes": "How did themes evolve from Season 1 onward?"
},
"episode": {
"overview": "What happened in this episode of {{showTitle}}?",
"plot_points": "Explain the main plot points of \"{{episodeTitle}}\"",
"character_development": "What character development occurred in this episode?",
"hidden_details": "Are there any hidden details or easter eggs I might have missed?",
"story_arc": "How does this episode connect to the overall story arc?"
},
"movie": {
"overview": "What is {{title}} about?",
"themes": "Explain the themes in this movie",
"ending": "What's the significance of the ending?",
"characters": "Tell me about the main characters and their development",
"production_facts": "Are there any interesting production facts about this film?"
}
}
},
"catalog_settings": {
"title": "Catalogs",
"layout_phone": "LAYOUT CATALOGSCREEN (PHONE)",
@ -1504,4 +1581,4 @@
"no_logs_captured": "No logs captured."
}
}
}
}

View file

@ -550,6 +550,43 @@
"conflict_msg": "Non puoi connettere Simkl mentre Trakt è connesso. Disconnetti prima Trakt.",
"disclaimer": "Nuvio non è affiliato con Simkl."
},
"mal_settings": {
"title": "MyAnimeList",
"connect_title": "Connetti MyAnimeList",
"connect_desc": "Sincronizza la tua cronologia di visione e gestisci la tua lista anime.",
"sign_in": "Accedi con MyAnimeList",
"sign_out": "Disconnetti",
"sign_out_confirm": "Sei sicuro di voler disconnettere il tuo account MyAnimeList?",
"auth_success_title": "Connessione riuscita",
"auth_success_msg": "Il tuo account MyAnimeList e stato collegato correttamente.",
"auth_error_title": "Errore di autenticazione",
"auth_error_msg": "Impossibile completare l'autenticazione con MyAnimeList.",
"auth_error_generic": "Si \u00e8 verificato un errore durante l'autenticazione.",
"sync_complete_title": "Sincronizzazione completata",
"sync_error_title": "Sincronizzazione fallita",
"sync_success_msg": "I dati MyAnimeList sono stati aggiornati correttamente.",
"sync_error_msg": "Impossibile aggiornare i dati MyAnimeList. Riprova.",
"sync_now_button": "Sincronizza ora",
"sync_settings_title": "Impostazioni sincronizzazione",
"profile_id": "ID: {{id}}",
"stats_total": "Totale",
"stats_days": "Giorni",
"stats_mean": "Media",
"status_watching": "In visione",
"status_completed": "Completati",
"status_on_hold": "In pausa",
"status_dropped": "Interrotti",
"sync_enabled_label": "Abilita sincronizzazione MyAnimeList",
"sync_enabled_desc": "Interruttore globale per abilitare o disabilitare tutte le funzioni MyAnimeList.",
"auto_update_label": "Aggiornamento automatico episodi",
"auto_update_desc": "Aggiorna automaticamente i tuoi progressi su MyAnimeList quando completi un episodio (>=90% completamento).",
"auto_add_label": "Aggiunta automatica anime",
"auto_add_desc": "Se un anime non e presente nella tua lista MyAnimeList, verra aggiunto automaticamente quando inizi a guardarlo.",
"auto_library_sync_label": "Sincronizzazione automatica con la Libreria",
"auto_library_sync_desc": "Aggiunge automaticamente alla Libreria Nuvio gli elementi presenti nella tua lista MyAnimeList \"Watching\".",
"include_nsfw_label": "Includi contenuti NSFW",
"include_nsfw_desc": "Consenti il recupero di contenuti NSFW quando viene caricata la tua lista MyAnimeList."
},
"tmdb_settings": {
"title": "Impostazioni TMDb",
"metadata_enrichment": "Arricchimento metadati",
@ -660,6 +697,7 @@
"integrations": "Integrazioni",
"playback": "Riproduzione",
"backup_restore": "Backup e Ripristino",
"backup_restore_desc": "Crea e ripristina backup dell'app",
"updates": "Aggiornamenti",
"about": "Informazioni",
"developer": "Sviluppatore",
@ -690,7 +728,8 @@
"media": "MEDIA",
"notifications": "NOTIFICHE",
"testing": "TEST",
"danger_zone": "ZONA PERICOLOSA"
"danger_zone": "ZONA PERICOLOSA",
"introdb_contribution": "Contributi IntroDB"
},
"items": {
"legal": "Note Legali & Disclaimer",
@ -751,7 +790,9 @@
"reset_campaigns": "Ripristina Campagne",
"reset_campaigns_desc": "Cancella le impressioni delle campagne",
"clear_all_data": "Cancella tutti i dati",
"clear_all_data_desc": "Ripristina tutte le impostazioni e i dati memorizzati"
"clear_all_data_desc": "Ripristina tutte le impostazioni e i dati memorizzati",
"enable_intro_submission": "Abilita l'aggiunta di dati sulle intro",
"enable_intro_submission_desc": "Contribuisci alla community IntroDB"
},
"options": {
"horizontal": "Orizzontale",
@ -794,7 +835,7 @@
"description": "Aggiorna dal cloud (pull) o usa le impostazioni di questo dispositivo come sorgente principale (push).",
"pull_btn": "Pull dal Cloud",
"push_btn": "Push dal Dispositivo",
"manage_account": "Gestiscci Account",
"manage_account": "Gestisci Account",
"sign_out": "Disconnetti",
"sign_in_up": "Accedi / Registrati"
},
@ -900,6 +941,46 @@
"success_removed": "Chiave API rimossa con successo",
"error_remove": "Impossibile rimuovere la chiave API"
},
"ai_chat": {
"title": "Chat IA",
"loading_context": "Caricamento contesto IA...",
"welcome_title": "Chiedimi qualsiasi cosa su",
"welcome_description": "Ho una conoscenza dettagliata di questo contenuto e posso rispondere a domande su trama, personaggi, temi e altro.",
"try_asking": "Prova a chiedere:",
"placeholder": "Chiedi qualcosa su questo contenuto...",
"errors": {
"load_content_details": "Impossibile caricare i dettagli del contenuto per la chat IA",
"generic": "Mi dispiace, si \u00e8 verificato un errore. Riprova.",
"not_configured": "Configura la tua chiave API OpenRouter in Impostazioni > Assistente IA.",
"invalid_api_key": "OpenRouter ha rifiutato la tua chiave API. Verificala in Impostazioni > Assistente IA.",
"quota": "OpenRouter ha rifiutato la richiesta per limiti o crediti. Controlla utilizzo e limiti del tuo account.",
"model_unavailable": "Il modello OpenRouter selezionato non \u00e8 disponibile. Riprova con `openrouter/free` oppure scegli un altro modello personalizzato in Impostazioni > Assistente IA.",
"connection": "Impossibile connettersi al servizio IA. Controlla connessione internet, chiave API e disponibilit\u00e0 del modello OpenRouter."
},
"starters": {
"series": {
"overall": "Di cosa parla {{title}} nel complesso?",
"arcs": "Riassumi gli archi narrativi principali di tutte le stagioni",
"rated": "Quali episodi hanno la valutazione pi\u00f9 alta e perch\u00e9?",
"pivotal": "Elenca gli episodi chiave per lo sviluppo dei personaggi",
"themes": "Come si evolvono i temi dalla Stagione 1 in poi?"
},
"episode": {
"overview": "Cosa succede in questo episodio di {{showTitle}}?",
"plot_points": "Spiegami i punti principali della trama di \"{{episodeTitle}}\"",
"character_development": "Quale sviluppo dei personaggi avviene in questo episodio?",
"hidden_details": "Ci sono dettagli nascosti o easter egg che potrei essermi perso?",
"story_arc": "Come si collega questo episodio all'arco narrativo generale?"
},
"movie": {
"overview": "Di cosa parla {{title}}?",
"themes": "Spiegami i temi di questo film",
"ending": "Qual \u00e8 il significato del finale?",
"characters": "Parlami dei personaggi principali e della loro evoluzione",
"production_facts": "Ci sono curiosit\u00e0 interessanti sulla produzione di questo film?"
}
}
},
"catalog_settings": {
"title": "Cataloghi",
"layout_phone": "LAYOUT SCHERMATA CATALOGO (TELEFONO)",
@ -1144,7 +1225,7 @@
"sync_desc": "Sincronizza automaticamente le notifiche per tutte le serie nella tua libreria e nella watchlist/collezione di Trakt.",
"section_advanced": "Avanzate",
"reset_button": "Ripristina tutte le notifiche",
"test_button": "Test Notifica (5 sec)",
"test_button": "Test Invio Notifica (5 sec)",
"test_notification_in": "Notifica tra {{seconds}}s...",
"test_notification_text": "La notifica apparirà tra {{seconds}} secondi",
"alert_reset_title": "Ripristina Notifiche",
@ -1488,4 +1569,4 @@
"no_logs_captured": "Nessun log catturato."
}
}
}
}

View file

@ -20,6 +20,7 @@ import { useTheme } from '../contexts/ThemeContext';
import FastImage from '@d11/react-native-fast-image';
import { BlurView as ExpoBlurView } from 'expo-blur';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
// Lazy-safe community blur import (avoid bundling issues on web)
let AndroidBlurView: any = null;
if (Platform.OS === 'android') {
@ -46,7 +47,7 @@ if (Platform.OS === 'ios') {
}
}
import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
import { aiService, ChatMessage, ContentContext, createMovieContext, createEpisodeContext, createSeriesContext, generateConversationStarters } from '../services/aiService';
import { aiService, ChatMessage, ContentContext, createMovieContext, createEpisodeContext, createSeriesContext } from '../services/aiService';
import { tmdbService } from '../services/tmdbService';
import Markdown from 'react-native-markdown-display';
import Animated, {
@ -428,13 +429,17 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress && prev.index === next.index);
const AIChatScreen: React.FC = () => {
const { t } = useTranslation();
// CustomAlert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([
{ label: 'OK', onPress: () => setAlertVisible(false) },
]);
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([]);
const defaultAlertActions = useMemo(() => ([
{ label: t('common.ok'), onPress: () => setAlertVisible(false) },
]), [t]);
const openAlert = (
title: string,
@ -452,7 +457,7 @@ const AIChatScreen: React.FC = () => {
}))
);
} else {
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertActions(defaultAlertActions);
}
setAlertVisible(true);
};
@ -536,11 +541,42 @@ const AIChatScreen: React.FC = () => {
useEffect(() => {
if (context && messages.length === 0) {
// Generate conversation starters
const starters = generateConversationStarters(context);
const starters = (() => {
if ('episodesBySeason' in (context as any)) {
const series = context as any;
return [
t('ai_chat.starters.series.overall', { title: series.title }),
t('ai_chat.starters.series.arcs'),
t('ai_chat.starters.series.rated'),
t('ai_chat.starters.series.pivotal'),
t('ai_chat.starters.series.themes'),
];
}
if ('showTitle' in (context as any)) {
const episode = context as any;
return [
t('ai_chat.starters.episode.overview', { showTitle: episode.showTitle }),
t('ai_chat.starters.episode.plot_points', { episodeTitle: episode.episodeTitle }),
t('ai_chat.starters.episode.character_development'),
t('ai_chat.starters.episode.hidden_details'),
t('ai_chat.starters.episode.story_arc'),
];
}
const movie = context as any;
return [
t('ai_chat.starters.movie.overview', { title: movie.title }),
t('ai_chat.starters.movie.themes'),
t('ai_chat.starters.movie.ending'),
t('ai_chat.starters.movie.characters'),
t('ai_chat.starters.movie.production_facts'),
];
})();
setSuggestions(starters);
}
}, [context, messages.length]);
}, [context, messages.length, t]);
const loadContext = async () => {
try {
@ -597,7 +633,7 @@ const AIChatScreen: React.FC = () => {
}
} catch (error) {
if (__DEV__) console.error('Error loading context:', error);
openAlert('Error', 'Failed to load content details for AI chat');
openAlert(t('common.error'), t('ai_chat.errors.load_content_details'));
} finally {
setIsLoadingContext(false);
{/* CustomAlert at root */ }
@ -692,18 +728,18 @@ const AIChatScreen: React.FC = () => {
} catch (error) {
if (__DEV__) console.error('Error sending message:', error);
let errorMessage = 'Sorry, I encountered an error. Please try again.';
let errorMessage = t('ai_chat.errors.generic');
if (error instanceof Error) {
if (error.message.includes('not configured')) {
errorMessage = 'Please configure your OpenRouter API key in Settings > AI Assistant.';
errorMessage = t('ai_chat.errors.not_configured');
} else if (/401|unauthorized|invalid api key|authentication/i.test(error.message)) {
errorMessage = 'OpenRouter rejected your API key. Please verify the key in Settings > AI Assistant.';
errorMessage = t('ai_chat.errors.invalid_api_key');
} else if (/insufficient|credit|quota|429/i.test(error.message)) {
errorMessage = 'OpenRouter quota/credits were rejected for this request. Please check your OpenRouter usage and limits.';
errorMessage = t('ai_chat.errors.quota');
} else if (/model|provider|endpoint|unsupported|unavailable|not found/i.test(error.message)) {
errorMessage = 'The selected OpenRouter model is unavailable. Retry with `openrouter/free` or choose another custom model in Settings > AI Assistant.';
errorMessage = t('ai_chat.errors.model_unavailable');
} else if (error.message.includes('API request failed')) {
errorMessage = 'Failed to connect to AI service. Please check your internet connection, API key, and OpenRouter model availability.';
errorMessage = t('ai_chat.errors.connection');
}
}
@ -718,7 +754,7 @@ const AIChatScreen: React.FC = () => {
} finally {
setIsLoading(false);
}
}, [context, messages, isLoading]);
}, [context, messages, isLoading, t]);
const handleSendPress = useCallback(() => {
sendMessage(inputText);
@ -767,7 +803,7 @@ const AIChatScreen: React.FC = () => {
<StatusBar barStyle="light-content" />
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.mediumEmphasis }]}>
Loading AI context...
{t('ai_chat.loading_context')}
</Text>
</View>
);
@ -820,7 +856,7 @@ const AIChatScreen: React.FC = () => {
<View style={styles.headerInfo}>
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
AI Chat
{t('ai_chat.title')}
</Text>
<Text style={[styles.headerSubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
{getDisplayTitle()}
@ -867,18 +903,18 @@ const AIChatScreen: React.FC = () => {
<MaterialIcons name="auto-awesome" size={34} color="white" />
</LinearGradient>
<Text style={[styles.welcomeTitle, { color: currentTheme.colors.highEmphasis }]}>
Ask me anything about
{t('ai_chat.welcome_title')}
</Text>
<Text style={[styles.welcomeSubtitle, { color: currentTheme.colors.primary }]}>
{getDisplayTitle()}
</Text>
<Text style={[styles.welcomeDescription, { color: currentTheme.colors.mediumEmphasis }]}>
I have detailed knowledge about this content and can answer questions about plot, characters, themes, and more.
{t('ai_chat.welcome_description')}
</Text>
<View style={styles.suggestionsContainer}>
<Text style={[styles.suggestionsTitle, { color: currentTheme.colors.mediumEmphasis }]}>
Try asking:
{t('ai_chat.try_asking')}
</Text>
<View style={styles.suggestionsGrid}>
{suggestions.map((suggestion, index) => (
@ -942,7 +978,7 @@ const AIChatScreen: React.FC = () => {
]}
value={inputText}
onChangeText={setInputText}
placeholder="Ask about this content..."
placeholder={t('ai_chat.placeholder')}
placeholderTextColor={currentTheme.colors.mediumEmphasis}
multiline
maxLength={500}

File diff suppressed because it is too large Load diff

View file

@ -420,8 +420,8 @@ const SettingsScreen: React.FC = () => {
)}
{isItemVisible('mal') && (
<SettingItem
title="MyAnimeList"
description="Sync with MyAnimeList"
title={t('mal_settings.title')}
description={t('mal_settings.connect_title')}
customIcon={<Image source={require('../../assets/rating-icons/mal-icon.png')} style={{ width: isTablet ? 24 : 20, height: isTablet ? 24 : 20, borderRadius: 4 }} resizeMode="contain" />}
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('MalSettings')}
@ -717,8 +717,8 @@ const SettingsScreen: React.FC = () => {
<SettingsCard title={t('settings.account').toUpperCase()}>
{showCloudSyncItem && (
<SettingItem
title="Nuvio Sync"
description="Sync data across your Nuvio devices"
title={t('settings.cloud_sync.title')}
description={t('settings.cloud_sync.description')}
customIcon={
<FastImage
source={require('../../assets/nuvio-sync-icon-og.png')}
@ -829,7 +829,7 @@ const SettingsScreen: React.FC = () => {
{(settingsConfig?.categories?.['backup']?.visible !== false) && (
<SettingItem
title={t('settings.backup_restore')}
description="Create and restore app backups"
description={t('settings.backup_restore_desc')}
icon="archive"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('Backup')}