mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
multi-lang init
This commit is contained in:
parent
9877f513e2
commit
9c37ad8b94
10 changed files with 374 additions and 27 deletions
1
App.tsx
1
App.tsx
|
|
@ -13,6 +13,7 @@ import {
|
|||
Platform,
|
||||
LogBox
|
||||
} from 'react-native';
|
||||
import './src/i18n'; // Initialize i18n
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
|
|
|
|||
85
package-lock.json
generated
85
package-lock.json
generated
|
|
@ -64,10 +64,13 @@
|
|||
"expo-system-ui": "~6.0.7",
|
||||
"expo-updates": "~29.0.12",
|
||||
"expo-web-browser": "~15.0.8",
|
||||
"i18next": "^25.7.3",
|
||||
"intl-pluralrules": "^2.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-react-native": "~7.3.1",
|
||||
"posthog-react-native": "^4.4.0",
|
||||
"react": "19.1.0",
|
||||
"react-i18next": "^16.5.1",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-boost": "^0.6.2",
|
||||
"react-native-bottom-tabs": "^1.0.2",
|
||||
|
|
@ -7505,6 +7508,15 @@
|
|||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2-without-node-native": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2-without-node-native/-/htmlparser2-without-node-native-3.9.2.tgz",
|
||||
|
|
@ -7616,6 +7628,37 @@
|
|||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.7.3",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz",
|
||||
"integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
|
@ -7743,6 +7786,12 @@
|
|||
"css-in-js-utils": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/intl-pluralrules": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/intl-pluralrules/-/intl-pluralrules-2.0.1.tgz",
|
||||
"integrity": "sha512-astxTLzIdXPeN0K9Rumi6LfMpm3rvNO0iJE+h/k8Kr/is+wPbRe4ikyDjlLr6VTh/mEfNv8RjN+gu3KwDiuhqg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
|
|
@ -10537,6 +10586,33 @@
|
|||
"react": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.1.tgz",
|
||||
"integrity": "sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 25.6.2",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",
|
||||
|
|
@ -13250,6 +13326,15 @@
|
|||
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||
|
|
|
|||
|
|
@ -64,10 +64,13 @@
|
|||
"expo-system-ui": "~6.0.7",
|
||||
"expo-updates": "~29.0.12",
|
||||
"expo-web-browser": "~15.0.8",
|
||||
"i18next": "^25.7.3",
|
||||
"intl-pluralrules": "^2.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-react-native": "~7.3.1",
|
||||
"posthog-react-native": "^4.4.0",
|
||||
"react": "19.1.0",
|
||||
"react-i18next": "^16.5.1",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-boost": "^0.6.2",
|
||||
"react-native-bottom-tabs": "^1.0.2",
|
||||
|
|
|
|||
21
src/i18n/index.ts
Normal file
21
src/i18n/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import 'intl-pluralrules';
|
||||
import languageDetector from './languageDetector';
|
||||
import { resources } from './resources';
|
||||
|
||||
i18n
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
29
src/i18n/languageDetector.ts
Normal file
29
src/i18n/languageDetector.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { getLocales } from 'expo-localization';
|
||||
import { LanguageDetectorModule } from 'i18next';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
|
||||
const languageDetector = {
|
||||
type: 'languageDetector',
|
||||
async: true,
|
||||
detect: async (callback: any) => {
|
||||
try {
|
||||
const savedLanguage = await mmkvStorage.getItem('user_language');
|
||||
if (savedLanguage) {
|
||||
callback(savedLanguage);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error reading language from storage', error);
|
||||
}
|
||||
|
||||
const locales = getLocales();
|
||||
const languageCode = locales[0]?.languageCode ?? 'en';
|
||||
callback(languageCode);
|
||||
},
|
||||
init: () => { },
|
||||
cacheUserLanguage: (language: string) => {
|
||||
mmkvStorage.setItem('user_language', language);
|
||||
},
|
||||
};
|
||||
|
||||
export default languageDetector;
|
||||
42
src/i18n/locales/en.json
Normal file
42
src/i18n/locales/en.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"search": "Search",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"ok": "OK"
|
||||
},
|
||||
"addons": {
|
||||
"title": "Addons",
|
||||
"reorder_mode": "Reorder Mode",
|
||||
"reorder_info": "Addons at the top have higher priority when loading content",
|
||||
"add_addon_placeholder": "Addon URL",
|
||||
"add_button": "Add Addon",
|
||||
"my_addons": "My Addons",
|
||||
"community_addons": "Community Addons",
|
||||
"no_addons": "No addons installed",
|
||||
"uninstall_title": "Uninstall Addon",
|
||||
"uninstall_message": "Are you sure you want to uninstall {{name}}?",
|
||||
"uninstall_button": "Uninstall",
|
||||
"install_success": "Addon installed successfully",
|
||||
"install_error": "Failed to install addon",
|
||||
"load_error": "Failed to load addons",
|
||||
"fetch_error": "Failed to fetch addon details",
|
||||
"invalid_url": "Please enter an addon URL",
|
||||
"configure": "Configure",
|
||||
"version": "Version: {{version}}",
|
||||
"installed_addons": "INSTALLED ADDONS",
|
||||
"reorder_drag_title": "DRAG ADDONS TO REORDER",
|
||||
"install": "Install"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Language",
|
||||
"select_language": "Select Language",
|
||||
"english": "English",
|
||||
"portuguese": "Portuguese"
|
||||
}
|
||||
}
|
||||
42
src/i18n/locales/pt.json
Normal file
42
src/i18n/locales/pt.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"common": {
|
||||
"loading": "Carregando...",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Salvar",
|
||||
"delete": "Excluir",
|
||||
"edit": "Editar",
|
||||
"search": "Buscar",
|
||||
"error": "Erro",
|
||||
"success": "Sucesso",
|
||||
"ok": "OK"
|
||||
},
|
||||
"addons": {
|
||||
"title": "Addons",
|
||||
"reorder_mode": "Modo de Reordenação",
|
||||
"reorder_info": "Arraste e solte para reordenar seus addons.",
|
||||
"add_addon_placeholder": "Digite a URL do addon (comece com https://)",
|
||||
"add_button": "Adicionar",
|
||||
"my_addons": "Meus Addons",
|
||||
"community_addons": "Addons da Comunidade",
|
||||
"no_addons": "Nenhum addon instalado",
|
||||
"uninstall_title": "Desinstalar Addon",
|
||||
"uninstall_message": "Tem certeza que deseja desinstalar {{name}}?",
|
||||
"uninstall_button": "Desinstalar",
|
||||
"installed_addons": "ADDONS INSTALADOS",
|
||||
"reorder_drag_title": "ARRASTE PARA REORDENAR",
|
||||
"install": "Instalar",
|
||||
"install_success": "Addon instalado com sucesso",
|
||||
"install_error": "Falha ao instalar addon",
|
||||
"load_error": "Falha ao carregar addons",
|
||||
"fetch_error": "Falha ao buscar detalhes do addon",
|
||||
"invalid_url": "Por favor, digite uma URL de addon",
|
||||
"configure": "Configurar",
|
||||
"version": "Versão: {{version}}"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Idioma",
|
||||
"select_language": "Selecionar Idioma",
|
||||
"english": "Inglês",
|
||||
"portuguese": "Português"
|
||||
}
|
||||
}
|
||||
7
src/i18n/resources.ts
Normal file
7
src/i18n/resources.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import en from './locales/en.json';
|
||||
import pt from './locales/pt.json';
|
||||
|
||||
export const resources = {
|
||||
en: { translation: en },
|
||||
pt: { translation: pt },
|
||||
};
|
||||
|
|
@ -30,6 +30,7 @@ import { logger } from '../utils/logger';
|
|||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Optional iOS Glass effect (expo-glass-effect) with safe fallback for AddonsScreen
|
||||
let GlassViewComp: any = null;
|
||||
|
|
@ -536,6 +537,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
|
||||
|
||||
const AddonsScreen = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const [addons, setAddons] = useState<ExtendedManifest[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -603,9 +605,9 @@ const AddonsScreen = () => {
|
|||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load addons:', error);
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Failed to load addons');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage(t('addons.load_error'));
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -617,9 +619,9 @@ const AddonsScreen = () => {
|
|||
const handleAddAddon = async (url?: string) => {
|
||||
let urlToInstall = url || addonUrl;
|
||||
if (!urlToInstall) {
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Please enter an addon URL');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage(t('addons.invalid_url'));
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
return;
|
||||
}
|
||||
|
|
@ -637,9 +639,9 @@ const AddonsScreen = () => {
|
|||
setShowConfirmModal(true);
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch addon details:', error);
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage(`Failed to fetch addon details from ${urlToInstall}`);
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage(`${t('addons.fetch_error')} ${urlToInstall}`);
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
|
|
@ -656,9 +658,9 @@ const AddonsScreen = () => {
|
|||
setShowConfirmModal(false);
|
||||
setAddonDetails(null);
|
||||
loadAddons();
|
||||
setAlertTitle('Success');
|
||||
setAlertMessage('Addon installed successfully');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertTitle(t('common.success'));
|
||||
setAlertMessage(t('addons.install_success'));
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} catch (error) {
|
||||
logger.error('Failed to install addon:', error);
|
||||
|
|
@ -691,12 +693,12 @@ const AddonsScreen = () => {
|
|||
};
|
||||
|
||||
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
||||
setAlertTitle('Uninstall Addon');
|
||||
setAlertMessage(`Are you sure you want to uninstall ${addon.name}?`);
|
||||
setAlertTitle(t('addons.uninstall_title'));
|
||||
setAlertMessage(t('addons.uninstall_message', { name: addon.name }));
|
||||
setAlertActions([
|
||||
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
|
||||
{ label: t('common.cancel'), onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
|
||||
{
|
||||
label: 'Uninstall',
|
||||
label: t('addons.uninstall_button'),
|
||||
onPress: async () => {
|
||||
await stremioService.removeAddon(addon.id);
|
||||
setAddons(prev => prev.filter(a => a.id !== addon.id));
|
||||
|
|
@ -997,15 +999,15 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
|
||||
<Text style={styles.headerTitle}>
|
||||
Addons
|
||||
{reorderMode && <Text style={styles.reorderModeText}> (Reorder Mode)</Text>}
|
||||
{t('addons.title')}
|
||||
{reorderMode && <Text style={styles.reorderModeText}>{t('addons.reorder_mode')}</Text>}
|
||||
</Text>
|
||||
|
||||
{reorderMode && (
|
||||
<View style={styles.reorderInfoBanner}>
|
||||
<MaterialIcons name="info-outline" size={18} color={colors.primary} />
|
||||
<Text style={styles.reorderInfoText}>
|
||||
Addons at the top have higher priority when loading content
|
||||
{t('addons.reorder_info')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -1040,7 +1042,7 @@ const AddonsScreen = () => {
|
|||
<View style={styles.addAddonContainer}>
|
||||
<TextInput
|
||||
style={styles.addonInput}
|
||||
placeholder="Addon URL"
|
||||
placeholder={t('addons.add_addon_placeholder')}
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
value={addonUrl}
|
||||
onChangeText={setAddonUrl}
|
||||
|
|
@ -1053,7 +1055,7 @@ const AddonsScreen = () => {
|
|||
disabled={installing || !addonUrl}
|
||||
>
|
||||
<Text style={styles.addButtonText}>
|
||||
{installing ? 'Loading...' : 'Add Addon'}
|
||||
{installing ? 'Loading...' : t('addons.add_button')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -1063,13 +1065,13 @@ const AddonsScreen = () => {
|
|||
{/* Installed Addons Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
{reorderMode ? "DRAG ADDONS TO REORDER" : "INSTALLED ADDONS"}
|
||||
{reorderMode ? t('addons.reorder_drag_title') : t('addons.installed_addons')}
|
||||
</Text>
|
||||
<View style={styles.addonList}>
|
||||
{addons.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="extension-off" size={32} color={colors.mediumGray} />
|
||||
<Text style={styles.emptyText}>No addons installed</Text>
|
||||
<Text style={styles.emptyText}>{t('addons.no_addons')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
addons.map((addon, index) => (
|
||||
|
|
@ -1083,7 +1085,8 @@ const AddonsScreen = () => {
|
|||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
</ScrollView >
|
||||
)}
|
||||
|
||||
{/* Addon Details Confirmation Modal */}
|
||||
|
|
@ -1189,7 +1192,7 @@ const AddonsScreen = () => {
|
|||
setAddonDetails(null);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.modalButtonText}>Cancel</Text>
|
||||
<Text style={styles.modalButtonText}>{t('common.cancel')}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalButton, styles.installButton]}
|
||||
|
|
@ -1199,7 +1202,7 @@ const AddonsScreen = () => {
|
|||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<Text style={styles.modalButtonText}>Install</Text>
|
||||
<Text style={styles.modalButtonText}>{t('addons.install')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -1216,7 +1219,7 @@ const AddonsScreen = () => {
|
|||
onClose={() => setAlertVisible(false)}
|
||||
actions={alertActions}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</SafeAreaView >
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ import {
|
|||
Platform,
|
||||
Dimensions,
|
||||
Linking,
|
||||
Modal,
|
||||
FlatList,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
|
|
@ -142,8 +145,10 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
|
|||
|
||||
|
||||
const SettingsScreen: React.FC = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { settings, updateSetting } = useSettings();
|
||||
const [hasUpdateBadge, setHasUpdateBadge] = useState(false);
|
||||
const [languageModalVisible, setLanguageModalVisible] = useState(false);
|
||||
// CustomAlert state
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertTitle, setAlertTitle] = useState('');
|
||||
|
|
@ -577,6 +582,13 @@ const SettingsScreen: React.FC = () => {
|
|||
(settingsConfig?.categories?.['playback']?.visible !== false)
|
||||
) && (
|
||||
<SettingsCard title="GENERAL">
|
||||
<SettingItem
|
||||
title={t('settings.language')}
|
||||
description={i18n.language === 'pt' ? t('settings.portuguese') : t('settings.english')}
|
||||
icon="globe"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => setLanguageModalVisible(true)}
|
||||
/>
|
||||
{(settingsConfig?.categories?.['content']?.visible !== false) && (
|
||||
<SettingItem
|
||||
title="Content & Discovery"
|
||||
|
|
@ -791,6 +803,69 @@ const SettingsScreen: React.FC = () => {
|
|||
actions={alertActions}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
visible={languageModalVisible}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
onRequestClose={() => setLanguageModalVisible(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setLanguageModalVisible(false)}
|
||||
>
|
||||
<View style={[styles.modalContent, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<Text style={[styles.modalTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{t('settings.select_language')}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.languageOption,
|
||||
i18n.language === 'en' && { backgroundColor: currentTheme.colors.primary + '20' }
|
||||
]}
|
||||
onPress={() => {
|
||||
i18n.changeLanguage('en');
|
||||
setLanguageModalVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={[
|
||||
styles.languageText,
|
||||
{ color: currentTheme.colors.highEmphasis },
|
||||
i18n.language === 'en' && { color: currentTheme.colors.primary, fontWeight: 'bold' }
|
||||
]}>
|
||||
{t('settings.english')}
|
||||
</Text>
|
||||
{i18n.language === 'en' && (
|
||||
<Feather name="check" size={20} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.languageOption,
|
||||
i18n.language === 'pt' && { backgroundColor: currentTheme.colors.primary + '20' }
|
||||
]}
|
||||
onPress={() => {
|
||||
i18n.changeLanguage('pt');
|
||||
setLanguageModalVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={[
|
||||
styles.languageText,
|
||||
{ color: currentTheme.colors.highEmphasis },
|
||||
i18n.language === 'pt' && { color: currentTheme.colors.primary, fontWeight: 'bold' }
|
||||
]}>
|
||||
{t('settings.portuguese')}
|
||||
</Text>
|
||||
{i18n.language === 'pt' && (
|
||||
<Feather name="check" size={20} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -799,6 +874,45 @@ const styles = StyleSheet.create({
|
|||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
modalContent: {
|
||||
width: '100%',
|
||||
maxWidth: 340,
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
languageOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
languageText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
// Mobile styles
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
|
|
|
|||
Loading…
Reference in a new issue