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.
This commit is contained in:
@@ -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() {
|
||||
<Tabs.Screen
|
||||
name="home"
|
||||
options={{
|
||||
title: 'Home',
|
||||
title: i18n.t('home'),
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="services"
|
||||
options={{
|
||||
title: 'Services',
|
||||
title: i18n.t('services'),
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="th-large" color={color} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{i18n.t('home')}</Text>
|
||||
<Pressable onPress={() => setModalVisible(true)} style={pickerSelectStyles.inputIOS}>
|
||||
<Text style={{ color: 'white' }}>{currentLanguage}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={() => {
|
||||
setModalVisible(!modalVisible);
|
||||
}}>
|
||||
<Pressable style={styles.modalOverlay} onPress={() => setModalVisible(false)}>
|
||||
<View style={styles.modalView}>
|
||||
{languages.map((lang) => (
|
||||
<Pressable
|
||||
key={lang.value}
|
||||
style={styles.modalButton}
|
||||
onPress={() => changeLanguage(lang.value as 'en' | 'tk' | 'ru')}>
|
||||
<Text style={styles.modalButtonText}>{lang.label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
<ScrollView>
|
||||
<View style={styles.innerContainer}>
|
||||
<Text style={styles.title}>Home</Text>
|
||||
<FeatureCard
|
||||
isNew
|
||||
title="Your Journey to Hajj"
|
||||
description="Everything you need for Hajj essentials."
|
||||
title={i18n.t('yourJourneyToHajj')}
|
||||
description={i18n.t('hajjEssentials')}
|
||||
/>
|
||||
<View style={styles.cardRow}>
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<FeatureCard
|
||||
title="Umrah"
|
||||
description="Book Permit"
|
||||
title={i18n.t('umrah')}
|
||||
description={i18n.t('bookPermit')}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 10 }}>
|
||||
<FeatureCard
|
||||
title="Noble Rawdah"
|
||||
description="Book Permit"
|
||||
title={i18n.t('nobleRawdah')}
|
||||
description={i18n.t('bookPermit')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ScrollView>
|
||||
<View style={styles.innerContainer}>
|
||||
<Text style={styles.title}>Services</Text>
|
||||
<Text style={styles.sectionTitle}>Adhkar</Text>
|
||||
{adhkarCategories.map((title, index) => (
|
||||
<AdhkarCard key={index} title={title} />
|
||||
<Text style={styles.title}>{i18n.t('services')}</Text>
|
||||
<Text style={styles.sectionTitle}>{i18n.t('adhkar')}</Text>
|
||||
{adhkarCategories.map((titleKey, index) => (
|
||||
<AdhkarCard key={index} title={i18n.t(titleKey)} />
|
||||
))}
|
||||
|
||||
<Text style={styles.sectionTitle}>Hisn Al-Muslim</Text>
|
||||
{supplications.map((text, index) => (
|
||||
<SupplicationListItem key={index} text={text} onPress={() => {}} />
|
||||
<Text style={styles.sectionTitle}>{i18n.t('hisnAlMuslim')}</Text>
|
||||
{supplications.map((textKey, index) => (
|
||||
<SupplicationListItem key={index} text={i18n.t(textKey)} onPress={() => {}} />
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user