mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +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,
|
Platform,
|
||||||
LogBox
|
LogBox
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import './src/i18n'; // Initialize i18n
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
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-system-ui": "~6.0.7",
|
||||||
"expo-updates": "~29.0.12",
|
"expo-updates": "~29.0.12",
|
||||||
"expo-web-browser": "~15.0.8",
|
"expo-web-browser": "~15.0.8",
|
||||||
|
"i18next": "^25.7.3",
|
||||||
|
"intl-pluralrules": "^2.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lottie-react-native": "~7.3.1",
|
"lottie-react-native": "~7.3.1",
|
||||||
"posthog-react-native": "^4.4.0",
|
"posthog-react-native": "^4.4.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-i18next": "^16.5.1",
|
||||||
"react-native": "0.81.4",
|
"react-native": "0.81.4",
|
||||||
"react-native-boost": "^0.6.2",
|
"react-native-boost": "^0.6.2",
|
||||||
"react-native-bottom-tabs": "^1.0.2",
|
"react-native-bottom-tabs": "^1.0.2",
|
||||||
|
|
@ -7505,6 +7508,15 @@
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/htmlparser2-without-node-native": {
|
||||||
"version": "3.9.2",
|
"version": "3.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2-without-node-native/-/htmlparser2-without-node-native-3.9.2.tgz",
|
"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==",
|
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
|
@ -7743,6 +7786,12 @@
|
||||||
"css-in-js-utils": "^3.1.0"
|
"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": {
|
"node_modules/invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
|
|
@ -10537,6 +10586,33 @@
|
||||||
"react": ">=17.0.0"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",
|
||||||
|
|
@ -13250,6 +13326,15 @@
|
||||||
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/walker": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,13 @@
|
||||||
"expo-system-ui": "~6.0.7",
|
"expo-system-ui": "~6.0.7",
|
||||||
"expo-updates": "~29.0.12",
|
"expo-updates": "~29.0.12",
|
||||||
"expo-web-browser": "~15.0.8",
|
"expo-web-browser": "~15.0.8",
|
||||||
|
"i18next": "^25.7.3",
|
||||||
|
"intl-pluralrules": "^2.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lottie-react-native": "~7.3.1",
|
"lottie-react-native": "~7.3.1",
|
||||||
"posthog-react-native": "^4.4.0",
|
"posthog-react-native": "^4.4.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-i18next": "^16.5.1",
|
||||||
"react-native": "0.81.4",
|
"react-native": "0.81.4",
|
||||||
"react-native-boost": "^0.6.2",
|
"react-native-boost": "^0.6.2",
|
||||||
"react-native-bottom-tabs": "^1.0.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 { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
// Optional iOS Glass effect (expo-glass-effect) with safe fallback for AddonsScreen
|
// Optional iOS Glass effect (expo-glass-effect) with safe fallback for AddonsScreen
|
||||||
let GlassViewComp: any = null;
|
let GlassViewComp: any = null;
|
||||||
|
|
@ -536,6 +537,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
|
|
||||||
|
|
||||||
const AddonsScreen = () => {
|
const AddonsScreen = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const [addons, setAddons] = useState<ExtendedManifest[]>([]);
|
const [addons, setAddons] = useState<ExtendedManifest[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -603,9 +605,9 @@ const AddonsScreen = () => {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load addons:', error);
|
logger.error('Failed to load addons:', error);
|
||||||
setAlertTitle('Error');
|
setAlertTitle(t('common.error'));
|
||||||
setAlertMessage('Failed to load addons');
|
setAlertMessage(t('addons.load_error'));
|
||||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||||
setAlertVisible(true);
|
setAlertVisible(true);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -617,9 +619,9 @@ const AddonsScreen = () => {
|
||||||
const handleAddAddon = async (url?: string) => {
|
const handleAddAddon = async (url?: string) => {
|
||||||
let urlToInstall = url || addonUrl;
|
let urlToInstall = url || addonUrl;
|
||||||
if (!urlToInstall) {
|
if (!urlToInstall) {
|
||||||
setAlertTitle('Error');
|
setAlertTitle(t('common.error'));
|
||||||
setAlertMessage('Please enter an addon URL');
|
setAlertMessage(t('addons.invalid_url'));
|
||||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||||
setAlertVisible(true);
|
setAlertVisible(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -637,9 +639,9 @@ const AddonsScreen = () => {
|
||||||
setShowConfirmModal(true);
|
setShowConfirmModal(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch addon details:', error);
|
logger.error('Failed to fetch addon details:', error);
|
||||||
setAlertTitle('Error');
|
setAlertTitle(t('common.error'));
|
||||||
setAlertMessage(`Failed to fetch addon details from ${urlToInstall}`);
|
setAlertMessage(`${t('addons.fetch_error')} ${urlToInstall}`);
|
||||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||||
setAlertVisible(true);
|
setAlertVisible(true);
|
||||||
} finally {
|
} finally {
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
|
|
@ -656,9 +658,9 @@ const AddonsScreen = () => {
|
||||||
setShowConfirmModal(false);
|
setShowConfirmModal(false);
|
||||||
setAddonDetails(null);
|
setAddonDetails(null);
|
||||||
loadAddons();
|
loadAddons();
|
||||||
setAlertTitle('Success');
|
setAlertTitle(t('common.success'));
|
||||||
setAlertMessage('Addon installed successfully');
|
setAlertMessage(t('addons.install_success'));
|
||||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||||
setAlertVisible(true);
|
setAlertVisible(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to install addon:', error);
|
logger.error('Failed to install addon:', error);
|
||||||
|
|
@ -691,12 +693,12 @@ const AddonsScreen = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
||||||
setAlertTitle('Uninstall Addon');
|
setAlertTitle(t('addons.uninstall_title'));
|
||||||
setAlertMessage(`Are you sure you want to uninstall ${addon.name}?`);
|
setAlertMessage(t('addons.uninstall_message', { name: addon.name }));
|
||||||
setAlertActions([
|
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 () => {
|
onPress: async () => {
|
||||||
await stremioService.removeAddon(addon.id);
|
await stremioService.removeAddon(addon.id);
|
||||||
setAddons(prev => prev.filter(a => a.id !== addon.id));
|
setAddons(prev => prev.filter(a => a.id !== addon.id));
|
||||||
|
|
@ -997,15 +999,15 @@ const AddonsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.headerTitle}>
|
<Text style={styles.headerTitle}>
|
||||||
Addons
|
{t('addons.title')}
|
||||||
{reorderMode && <Text style={styles.reorderModeText}> (Reorder Mode)</Text>}
|
{reorderMode && <Text style={styles.reorderModeText}>{t('addons.reorder_mode')}</Text>}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{reorderMode && (
|
{reorderMode && (
|
||||||
<View style={styles.reorderInfoBanner}>
|
<View style={styles.reorderInfoBanner}>
|
||||||
<MaterialIcons name="info-outline" size={18} color={colors.primary} />
|
<MaterialIcons name="info-outline" size={18} color={colors.primary} />
|
||||||
<Text style={styles.reorderInfoText}>
|
<Text style={styles.reorderInfoText}>
|
||||||
Addons at the top have higher priority when loading content
|
{t('addons.reorder_info')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1040,7 +1042,7 @@ const AddonsScreen = () => {
|
||||||
<View style={styles.addAddonContainer}>
|
<View style={styles.addAddonContainer}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.addonInput}
|
style={styles.addonInput}
|
||||||
placeholder="Addon URL"
|
placeholder={t('addons.add_addon_placeholder')}
|
||||||
placeholderTextColor={colors.mediumGray}
|
placeholderTextColor={colors.mediumGray}
|
||||||
value={addonUrl}
|
value={addonUrl}
|
||||||
onChangeText={setAddonUrl}
|
onChangeText={setAddonUrl}
|
||||||
|
|
@ -1053,7 +1055,7 @@ const AddonsScreen = () => {
|
||||||
disabled={installing || !addonUrl}
|
disabled={installing || !addonUrl}
|
||||||
>
|
>
|
||||||
<Text style={styles.addButtonText}>
|
<Text style={styles.addButtonText}>
|
||||||
{installing ? 'Loading...' : 'Add Addon'}
|
{installing ? 'Loading...' : t('addons.add_button')}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1063,13 +1065,13 @@ const AddonsScreen = () => {
|
||||||
{/* Installed Addons Section */}
|
{/* Installed Addons Section */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>
|
<Text style={styles.sectionTitle}>
|
||||||
{reorderMode ? "DRAG ADDONS TO REORDER" : "INSTALLED ADDONS"}
|
{reorderMode ? t('addons.reorder_drag_title') : t('addons.installed_addons')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.addonList}>
|
<View style={styles.addonList}>
|
||||||
{addons.length === 0 ? (
|
{addons.length === 0 ? (
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<MaterialIcons name="extension-off" size={32} color={colors.mediumGray} />
|
<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>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
addons.map((addon, index) => (
|
addons.map((addon, index) => (
|
||||||
|
|
@ -1083,7 +1085,8 @@ const AddonsScreen = () => {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
|
||||||
|
</ScrollView >
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Addon Details Confirmation Modal */}
|
{/* Addon Details Confirmation Modal */}
|
||||||
|
|
@ -1189,7 +1192,7 @@ const AddonsScreen = () => {
|
||||||
setAddonDetails(null);
|
setAddonDetails(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.modalButtonText}>Cancel</Text>
|
<Text style={styles.modalButtonText}>{t('common.cancel')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.modalButton, styles.installButton]}
|
style={[styles.modalButton, styles.installButton]}
|
||||||
|
|
@ -1199,7 +1202,7 @@ const AddonsScreen = () => {
|
||||||
{installing ? (
|
{installing ? (
|
||||||
<ActivityIndicator size="small" color={colors.white} />
|
<ActivityIndicator size="small" color={colors.white} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.modalButtonText}>Install</Text>
|
<Text style={styles.modalButtonText}>{t('addons.install')}</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1216,7 +1219,7 @@ const AddonsScreen = () => {
|
||||||
onClose={() => setAlertVisible(false)}
|
onClose={() => setAlertVisible(false)}
|
||||||
actions={alertActions}
|
actions={alertActions}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ import {
|
||||||
Platform,
|
Platform,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Linking,
|
Linking,
|
||||||
|
Modal,
|
||||||
|
FlatList,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } 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 SettingsScreen: React.FC = () => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
const { settings, updateSetting } = useSettings();
|
const { settings, updateSetting } = useSettings();
|
||||||
const [hasUpdateBadge, setHasUpdateBadge] = useState(false);
|
const [hasUpdateBadge, setHasUpdateBadge] = useState(false);
|
||||||
|
const [languageModalVisible, setLanguageModalVisible] = useState(false);
|
||||||
// CustomAlert state
|
// CustomAlert state
|
||||||
const [alertVisible, setAlertVisible] = useState(false);
|
const [alertVisible, setAlertVisible] = useState(false);
|
||||||
const [alertTitle, setAlertTitle] = useState('');
|
const [alertTitle, setAlertTitle] = useState('');
|
||||||
|
|
@ -577,6 +582,13 @@ const SettingsScreen: React.FC = () => {
|
||||||
(settingsConfig?.categories?.['playback']?.visible !== false)
|
(settingsConfig?.categories?.['playback']?.visible !== false)
|
||||||
) && (
|
) && (
|
||||||
<SettingsCard title="GENERAL">
|
<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) && (
|
{(settingsConfig?.categories?.['content']?.visible !== false) && (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Content & Discovery"
|
title="Content & Discovery"
|
||||||
|
|
@ -791,6 +803,69 @@ const SettingsScreen: React.FC = () => {
|
||||||
actions={alertActions}
|
actions={alertActions}
|
||||||
onClose={() => setAlertVisible(false)}
|
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>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -799,6 +874,45 @@ const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
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
|
// Mobile styles
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue