From e431c42df1246f38183d286011a2cc45cf076843 Mon Sep 17 00:00:00 2001 From: Nurmuhammet Allanov Date: Sat, 16 Aug 2025 22:24:47 +0500 Subject: [PATCH] Implement multilingual support by integrating i18n for dynamic text rendering across the app. Added language selection modal in HomeScreen and updated various components to utilize localized strings. Updated package dependencies for async storage and localization. --- app/(tabs)/_layout.tsx | 5 +- app/(tabs)/home.tsx | 135 +++++++++++++++++++++++-- app/(tabs)/services.tsx | 31 +++--- app/_layout.tsx | 17 +++- components/FeatureCard.tsx | 3 +- components/PrayerTimeCard.tsx | 3 +- components/ServicesGrid.tsx | 11 ++- i18n.ts | 29 ++++++ locales/en.json | 26 +++++ locales/ru.json | 26 +++++ locales/tk.json | 26 +++++ package-lock.json | 181 +++++++++++++++++++++++++++++++++- package.json | 5 + 13 files changed, 460 insertions(+), 38 deletions(-) create mode 100644 i18n.ts create mode 100644 locales/en.json create mode 100644 locales/ru.json create mode 100644 locales/tk.json diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 75914cd..bb24762 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -3,6 +3,7 @@ import { Tabs } from 'expo-router'; import { useColorScheme } from 'react-native'; import Colors from '@/constants/Colors'; +import i18n from '@/i18n'; /** * You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ @@ -30,14 +31,14 @@ export default function TabLayout() { , }} /> , }} /> diff --git a/app/(tabs)/home.tsx b/app/(tabs)/home.tsx index 82dbf9a..3c21ac4 100644 --- a/app/(tabs)/home.tsx +++ b/app/(tabs)/home.tsx @@ -1,31 +1,82 @@ -import { StyleSheet, SafeAreaView, ScrollView } from 'react-native'; +import { StyleSheet, SafeAreaView, ScrollView, I18nManager, Modal, Pressable } from 'react-native'; import { Text, View } from '@/components/Themed'; import FeatureCard from '@/components/FeatureCard'; import PrayerTimeCard from '@/components/PrayerTimeCard'; import ServicesGrid from '@/components/ServicesGrid'; +import i18n from '@/i18n'; +import * as Updates from 'expo-updates'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import Colors from '@/constants/Colors'; +import { useState } from 'react'; + export default function HomeScreen() { + const [modalVisible, setModalVisible] = useState(false); + + const changeLanguage = async (lang: 'en' | 'tk' | 'ru') => { + if (!lang) return; + setModalVisible(false); + if (lang === i18n.locale.substring(0, 2)) return; + await AsyncStorage.setItem('user-language', lang); + i18n.locale = lang; + I18nManager.forceRTL(false); // Assuming LTR for all three languages + Updates.reloadAsync(); + }; + + const languages = [ + { label: 'English', value: 'en' }, + { label: 'Türkmen', value: 'tk' }, + { label: 'Русский', value: 'ru' }, + ]; + + const currentLanguage = languages.find(l => l.value === i18n.locale.substring(0, 2))?.label; + return ( + + {i18n.t('home')} + setModalVisible(true)} style={pickerSelectStyles.inputIOS}> + {currentLanguage} + + + { + setModalVisible(!modalVisible); + }}> + setModalVisible(false)}> + + {languages.map((lang) => ( + changeLanguage(lang.value as 'en' | 'tk' | 'ru')}> + {lang.label} + + ))} + + + - Home @@ -44,13 +95,81 @@ const styles = StyleSheet.create({ innerContainer: { paddingHorizontal: 15, }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 15, + }, title: { fontSize: 22, fontWeight: 'bold', marginVertical: 15, }, + langSwitcher: { + flexDirection: 'row', + }, + langText: { + color: '#fff', + paddingHorizontal: 5, + fontWeight: 'bold', + }, cardRow: { flexDirection: 'row', justifyContent: 'space-between', }, + modalOverlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + }, + modalView: { + margin: 20, + backgroundColor: Colors.dark.secondary, + borderRadius: 20, + padding: 35, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + modalButton: { + padding: 10, + borderBottomWidth: 1, + borderBottomColor: '#333', + width: '100%', + }, + modalButtonText: { + color: 'white', + textAlign: 'center', + }, +}); + +const pickerSelectStyles = StyleSheet.create({ + inputIOS: { + fontSize: 16, + paddingVertical: 12, + paddingHorizontal: 10, + borderWidth: 1, + borderColor: Colors.dark.secondary, + borderRadius: 4, + color: 'white', + paddingRight: 30, // to ensure the text is never behind the icon + }, + inputAndroid: { + fontSize: 16, + paddingHorizontal: 10, + paddingVertical: 8, + borderWidth: 0.5, + borderColor: Colors.dark.secondary, + borderRadius: 8, + color: 'white', + paddingRight: 30, // to ensure the text is never behind the icon + }, }); diff --git a/app/(tabs)/services.tsx b/app/(tabs)/services.tsx index cd83519..54348ac 100644 --- a/app/(tabs)/services.tsx +++ b/app/(tabs)/services.tsx @@ -2,19 +2,20 @@ import { StyleSheet, SafeAreaView, ScrollView } from 'react-native'; import { Text, View } from '@/components/Themed'; import AdhkarCard from '@/components/AdhkarCard'; import SupplicationListItem from '@/components/SupplicationListItem'; +import i18n from '@/i18n'; const adhkarCategories = [ - 'Thikr said in the morning and evening', - 'Thikr before sleeping', - 'Thikr after salam', + 'morningEveningThikr', + 'beforeSleepingThikr', + 'afterSalamThikr', ]; const supplications = [ - 'Upon breaking fast', - 'Supplication said by one fasting when presented with food and does not break his fast', - 'When insulted while fasting', - 'Supplication upon seeing the early or premature fruit', - 'Supplication upon sneezing', + 'breakingFastSupplication', + 'fastingPersonSupplication', + 'insultedWhileFasting', + 'seeingFruitSupplication', + 'sneezingSupplication', ] export default function ServicesScreen() { @@ -22,15 +23,15 @@ export default function ServicesScreen() { - Services - Adhkar - {adhkarCategories.map((title, index) => ( - + {i18n.t('services')} + {i18n.t('adhkar')} + {adhkarCategories.map((titleKey, index) => ( + ))} - Hisn Al-Muslim - {supplications.map((text, index) => ( - {}} /> + {i18n.t('hisnAlMuslim')} + {supplications.map((textKey, index) => ( + {}} /> ))} diff --git a/app/_layout.tsx b/app/_layout.tsx index 40cdc48..4a8d119 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -3,10 +3,11 @@ import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import 'react-native-reanimated'; import { useColorScheme } from '@/components/useColorScheme'; +import { initializeLanguage } from '@/i18n'; export { // Catch any errors thrown by the Layout component. @@ -26,6 +27,8 @@ export default function RootLayout() { SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), ...FontAwesome.font, }); + const [langLoaded, setLangLoaded] = React.useState(false); + // Expo Router uses Error Boundaries to catch errors in the navigation tree. useEffect(() => { @@ -33,12 +36,18 @@ export default function RootLayout() { }, [error]); useEffect(() => { - if (loaded) { + initializeLanguage().then(() => { + setLangLoaded(true); + }); + }, []); + + useEffect(() => { + if (loaded && langLoaded) { SplashScreen.hideAsync(); } - }, [loaded]); + }, [loaded, langLoaded]); - if (!loaded) { + if (!loaded || !langLoaded) { return null; } diff --git a/components/FeatureCard.tsx b/components/FeatureCard.tsx index 5bcb3fc..2f76abd 100644 --- a/components/FeatureCard.tsx +++ b/components/FeatureCard.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import Colors from '@/constants/Colors'; import { FontAwesome } from '@expo/vector-icons'; +import i18n from '@/i18n'; type FeatureCardProps = { title: string; @@ -16,7 +17,7 @@ export default function FeatureCard({ title, description, isNew = false }: Featu {isNew && ( - New Experience + {i18n.t('newExperience')} )} diff --git a/components/PrayerTimeCard.tsx b/components/PrayerTimeCard.tsx index 4b660f5..56f1507 100644 --- a/components/PrayerTimeCard.tsx +++ b/components/PrayerTimeCard.tsx @@ -3,6 +3,7 @@ import { View, Text, StyleSheet, ImageBackground, Pressable } from 'react-native import Colors from '@/constants/Colors'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import prayerTimeCalculator, { cities } from '@/utils/prayerTimeCalculator'; +import i18n from '@/i18n'; type Prayer = { name: string; @@ -97,7 +98,7 @@ export default function PrayerTimeCard() { - {nextPrayer ? `Left on ${nextPrayer.name} prayer` : 'Prayer Times'} + {nextPrayer ? i18n.t('leftOnPrayer', { prayerName: nextPrayer.name }) : i18n.t('prayerTimes')} {remainingTime} diff --git a/components/ServicesGrid.tsx b/components/ServicesGrid.tsx index 326bc59..b4d1e47 100644 --- a/components/ServicesGrid.tsx +++ b/components/ServicesGrid.tsx @@ -2,11 +2,12 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import Colors from '@/constants/Colors'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import i18n from '@/i18n'; const services = [ - { name: 'Qur\'an', icon: 'book-open-variant' }, - { name: 'Hadith', icon: 'book-open-page-variant' }, - { name: 'Du\'a', icon: 'human-greeting' }, + { name: 'quran', icon: 'book-open-variant' }, + { name: 'hadith', icon: 'book-open-page-variant' }, + { name: 'dua', icon: 'human-greeting' }, ]; export default function ServicesGrid() { @@ -14,14 +15,14 @@ export default function ServicesGrid() { return ( - Services to Enrich Your Spiritual Experience + {i18n.t('servicesToEnrich')} {services.map((service, index) => ( - {service.name} + {i18n.t(service.name)} ))} diff --git a/i18n.ts b/i18n.ts new file mode 100644 index 0000000..58c7728 --- /dev/null +++ b/i18n.ts @@ -0,0 +1,29 @@ +import { I18n } from 'i18n-js'; +import * as Localization from 'expo-localization'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +import en from './locales/en.json'; +import tk from './locales/tk.json'; +import ru from './locales/ru.json'; + +const i18n = new I18n({ + en, + tk, + ru, +}); + +i18n.enableFallback = true; + +// Function to initialize the language +export const initializeLanguage = async () => { + const savedLanguage = await AsyncStorage.getItem('user-language'); + if (savedLanguage) { + i18n.locale = savedLanguage; + } else { + // If no language is saved, detect from device and default to Turkmen + const userLanguageCode = Localization.getLocales()[0]?.languageCode; + i18n.locale = ['en', 'tk', 'ru'].includes(userLanguageCode || '') ? userLanguageCode! : 'tk'; + } +}; + +export default i18n; diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..beef237 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,26 @@ +{ + "home": "Home", + "services": "Services", + "yourJourneyToHajj": "Your Journey to Hajj", + "hajjEssentials": "Everything you need for Hajj essentials.", + "umrah": "Umrah", + "bookPermit": "Book Permit", + "nobleRawdah": "Noble Rawdah", + "newExperience": "New Experience", + "prayerTimes": "Prayer Times", + "leftOnPrayer": "Left on {{prayerName}} prayer", + "servicesToEnrich": "Services to Enrich Your Spiritual Experience", + "quran": "Qur'an", + "hadith": "Hadith", + "dua": "Du'a", + "adhkar": "Adhkar", + "hisnAlMuslim": "Hisn Al-Muslim", + "morningEveningThikr": "Thikr said in the morning and evening", + "beforeSleepingThikr": "Thikr before sleeping", + "afterSalamThikr": "Thikr after salam", + "breakingFastSupplication": "Upon breaking fast", + "fastingPersonSupplication": "Supplication said by one fasting when presented with food and does not break his fast", + "insultedWhileFasting": "When insulted while fasting", + "seeingFruitSupplication": "Supplication upon seeing the early or premature fruit", + "sneezingSupplication": "Supplication upon sneezing" +} diff --git a/locales/ru.json b/locales/ru.json new file mode 100644 index 0000000..28608fa --- /dev/null +++ b/locales/ru.json @@ -0,0 +1,26 @@ +{ + "home": "Главная", + "services": "Услуги", + "yourJourneyToHajj": "Ваше путешествие в Хадж", + "hajjEssentials": "Все, что вам нужно для Хаджа.", + "umrah": "Умра", + "bookPermit": "Забронировать разрешение", + "nobleRawdah": "Благородная Равда", + "newExperience": "Новый опыт", + "prayerTimes": "Время молитв", + "leftOnPrayer": "Осталось до молитвы {{prayerName}}", + "servicesToEnrich": "Услуги для обогащения вашего духовного опыта", + "quran": "Коран", + "hadith": "Хадисы", + "dua": "Дуа", + "adhkar": "Азкар", + "hisnAlMuslim": "Крепость мусульманина", + "morningEveningThikr": "Зикр, читаемый утром и вечером", + "beforeSleepingThikr": "Зикр перед сном", + "afterSalamThikr": "Зикр после салама", + "breakingFastSupplication": "При разговении", + "fastingPersonSupplication": "Мольба, произносимая постящимся, когда ему преподносят еду, и он не прерывает свой пост", + "insultedWhileFasting": "Когда оскорбляют во время поста", + "seeingFruitSupplication": "Мольба при виде ранних или незрелых плодов", + "sneezingSupplication": "Мольба при чихании" +} diff --git a/locales/tk.json b/locales/tk.json new file mode 100644 index 0000000..9f875ec --- /dev/null +++ b/locales/tk.json @@ -0,0 +1,26 @@ +{ + "home": "Baş sahypa", + "services": "Hyzmatlar", + "yourJourneyToHajj": "Haj syýahatyňyz", + "hajjEssentials": "Haj üçin zerur zatlar.", + "umrah": "Umra", + "bookPermit": "Rugsat kitaby", + "nobleRawdah": "Asylly Rawdah", + "newExperience": "Täze tejribe", + "prayerTimes": "Namaz wagtlary", + "leftOnPrayer": "{{prayerName}} namazyna çenli galdy", + "servicesToEnrich": "Ruhy tejribäňizi baýlaşdyrmak üçin hyzmatlar", + "quran": "Kuran", + "hadith": "Hadys", + "dua": "Doga", + "adhkar": "Zikir", + "hisnAlMuslim": "Musulmanyň goragy", + "morningEveningThikr": "Ertir we agşam aýdylýan zikir", + "beforeSleepingThikr": "Uklamazdan öňki zikir", + "afterSalamThikr": "Salamdan soňky zikir", + "breakingFastSupplication": "Agzyňy açanyňda", + "fastingPersonSupplication": "Agzy bekli adama iýmit hödürlenende we orazasyny bozmasa aýdylýan doga", + "insultedWhileFasting": "Orazaly wagtyň kemsidilende", + "seeingFruitSupplication": "Irki ýa-da bişmedik miwäni göreniňde aýdylýan doga", + "sneezingSupplication": "Asgyranyňda aýdylýan doga" +} diff --git a/package-lock.json b/package-lock.json index 2ca7522..61c2cb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,19 +9,24 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^14.1.0", + "@react-native-async-storage/async-storage": "2.1.2", "@react-navigation/native": "^7.1.6", "expo": "~53.0.20", "expo-font": "~13.3.2", "expo-linking": "~7.1.7", + "expo-localization": "^16.1.6", "expo-router": "~5.1.4", "expo-splash-screen": "~0.30.10", "expo-status-bar": "~2.2.3", "expo-system-ui": "~5.0.10", + "expo-updates": "~0.28.17", "expo-web-browser": "~14.2.0", "hijri-date": "^0.2.2", + "i18n-js": "^4.5.1", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.5", + "react-native-picker-select": "^9.3.1", "react-native-reanimated": "~3.17.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", @@ -2454,6 +2459,30 @@ } } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz", + "integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, + "node_modules/@react-native-picker/picker": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz", + "integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==", + "peer": true, + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz", @@ -3704,6 +3733,14 @@ "node": ">=0.6" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "engines": { + "node": "*" + } + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -4983,6 +5020,11 @@ "react-native": "*" } }, + "node_modules/expo-eas-client": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-0.14.4.tgz", + "integrity": "sha512-TSL1BbBFIuXchJmPgbPnB7cGpOOuSGJcQ/L7gij/+zPjExwvKm5ckA5dlSulwoFhH8zQt4vb7bfISPSAWQVWBw==" + }, "node_modules/expo-file-system": { "version": "18.1.11", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.1.11.tgz", @@ -5004,6 +5046,11 @@ "react": "*" } }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==" + }, "node_modules/expo-keep-awake": { "version": "14.1.4", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz", @@ -5026,6 +5073,30 @@ "react-native": "*" } }, + "node_modules/expo-localization": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-16.1.6.tgz", + "integrity": "sha512-v4HwNzs8QvyKHwl40MvETNEKr77v1o9/eVC8WCBY++DIlBAvonHyJe2R9CfqpZbC4Tlpl7XV+07nLXc8O5PQsA==", + "dependencies": { + "rtl-detect": "^1.0.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-manifests": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.16.6.tgz", + "integrity": "sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==", + "dependencies": { + "@expo/config": "~11.0.12", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "2.1.14", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz", @@ -5127,6 +5198,11 @@ "react-native": "*" } }, + "node_modules/expo-structured-headers": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-4.1.0.tgz", + "integrity": "sha512-2X+aUNzC/qaw7/WyUhrVHNDB0uQ5rE12XA2H/rJXaAiYQSuOeU90ladaN0IJYV9I2XlhYrjXLktLXWbO7zgbag==" + }, "node_modules/expo-system-ui": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-5.0.10.tgz", @@ -5146,6 +5222,46 @@ } } }, + "node_modules/expo-updates": { + "version": "0.28.17", + "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-0.28.17.tgz", + "integrity": "sha512-OiKDrKk6EoBRP9AoK7/4tyj9lVtHw2IfaETIFeUCHMgx5xjgKGX/jjSwqhk8N9BJgLDIy0oD0Sb0MaEbSBb3lg==", + "dependencies": { + "@expo/code-signing-certificates": "0.0.5", + "@expo/config": "~11.0.13", + "@expo/config-plugins": "~10.1.2", + "@expo/spawn-async": "^1.7.2", + "arg": "4.1.0", + "chalk": "^4.1.2", + "expo-eas-client": "~0.14.4", + "expo-manifests": "~0.16.6", + "expo-structured-headers": "~4.1.0", + "expo-updates-interface": "~1.1.0", + "glob": "^10.4.2", + "ignore": "^5.3.1", + "resolve-from": "^5.0.0" + }, + "bin": { + "expo-updates": "bin/cli.js" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-updates-interface": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz", + "integrity": "sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-updates/node_modules/arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==" + }, "node_modules/expo-web-browser": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.2.0.tgz", @@ -5653,6 +5769,16 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" }, + "node_modules/i18n-js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/i18n-js/-/i18n-js-4.5.1.tgz", + "integrity": "sha512-n7jojFj1WC0tztgr0I8jqTXuIlY1xNzXnC3mjKX/YjJhimdM+jXM8vOmn9d3xQFNC6qDHJ4ovhdrGXrRXLIGkA==", + "dependencies": { + "bignumber.js": "*", + "lodash": "*", + "make-plural": "*" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5855,6 +5981,14 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -7199,14 +7333,24 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." + }, + "node_modules/lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -7333,6 +7477,11 @@ "node": ">=10" } }, + "node_modules/make-plural": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz", + "integrity": "sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==" + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -7360,6 +7509,17 @@ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8812,6 +8972,18 @@ "react-native": "*" } }, + "node_modules/react-native-picker-select": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-9.3.1.tgz", + "integrity": "sha512-o621HcsKJfJkpYeP/PZQiZTKbf8W7FT08niLFL0v1pGkIQyak5IfzfinV2t+/l1vktGwAH2Tt29LrP/Hc5fk3A==", + "dependencies": { + "lodash.isequal": "^4.5.0", + "lodash.isobject": "^3.0.2" + }, + "peerDependencies": { + "@react-native-picker/picker": "^2.4.0" + } + }, "node_modules/react-native-reanimated": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz", @@ -9356,6 +9528,11 @@ "node": "*" } }, + "node_modules/rtl-detect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", + "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", diff --git a/package.json b/package.json index f2b76a3..b3c5407 100644 --- a/package.json +++ b/package.json @@ -14,19 +14,24 @@ }, "dependencies": { "@expo/vector-icons": "^14.1.0", + "@react-native-async-storage/async-storage": "2.1.2", "@react-navigation/native": "^7.1.6", "expo": "~53.0.20", "expo-font": "~13.3.2", "expo-linking": "~7.1.7", + "expo-localization": "^16.1.6", "expo-router": "~5.1.4", "expo-splash-screen": "~0.30.10", "expo-status-bar": "~2.2.3", "expo-system-ui": "~5.0.10", + "expo-updates": "~0.28.17", "expo-web-browser": "~14.2.0", "hijri-date": "^0.2.2", + "i18n-js": "^4.5.1", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.5", + "react-native-picker-select": "^9.3.1", "react-native-reanimated": "~3.17.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1",