Skip to content

Commit 0994ace

Browse files
Internationalize app to support more than one language (#58)
1 parent 22f3938 commit 0994ace

26 files changed

+1136
-304
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { render, fireEvent } from '@testing-library/react-native';
3+
import { I18nextProvider } from 'react-i18next';
4+
import i18n from '@/internationalization';
5+
import LanguagePicker from '@/components/LanguagePicker';
6+
7+
jest.mock('react-native/Libraries/Utilities/useColorScheme', () => ({
8+
__esModule: true,
9+
default: jest.fn(() => 'light'),
10+
}));
11+
12+
describe('LanguagePicker', () => {
13+
it('renders the picker component and allows language selection', () => {
14+
const { getByTestId, getByText } = render(
15+
<I18nextProvider i18n={i18n}>
16+
<LanguagePicker />
17+
</I18nextProvider>
18+
);
19+
20+
expect(getByTestId('language-picker')).toBeTruthy();
21+
22+
const picker = getByTestId('language-picker');
23+
expect(picker.props.selectedValue).toBe('en');
24+
fireEvent(picker, 'onValueChange', 'fr');
25+
expect(i18n.language).toBe('fr');
26+
expect(getByText('Français')).toBeTruthy();
27+
});
28+
});

app.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
"pathPrefix": "/register"
3535
}
3636
],
37-
"category": ["BROWSABLE", "DEFAULT"]
37+
"category": [
38+
"BROWSABLE",
39+
"DEFAULT"
40+
]
3841
}
3942
]
4043
},
@@ -43,7 +46,11 @@
4346
"output": "static",
4447
"favicon": "./assets/images/favicon.png"
4548
},
46-
"plugins": ["expo-router", "expo-font"],
49+
"plugins": [
50+
"expo-router",
51+
"expo-font",
52+
"expo-localization"
53+
],
4754
"experiments": {
4855
"typedRoutes": true
4956
},

app/(onboarding)/index.tsx

+35-20
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,30 @@ import { router } from 'expo-router';
44
import { useEffect, useRef, useState } from 'react';
55
import { TouchableOpacity, useColorScheme } from 'react-native';
66
import PagerView from 'react-native-pager-view';
7-
import AsyncStorage from '@react-native-async-storage/async-storage';
7+
import { useTranslation } from 'react-i18next';
8+
import LanguagePicker from '@/components/LanguagePicker';
9+
10+
811
type Page = {
912
image: any;
1013
content: string;
1114
};
1215

13-
const pages: Page[] = [
14-
{
15-
image: require('@/assets/images/onboarding/1.png'),
16-
content: "Optimize your organization's potential with Performance Management/Analytics.",
17-
},
18-
{
19-
image: require('@/assets/images/onboarding/2.png'),
20-
content: 'Identify top performers, discover hidden talent, and optimize your workforce.',
21-
},
22-
{
23-
image: require('@/assets/images/onboarding/3.png'),
24-
content: 'Unlock the potential of a Continuous & Tight Feedback Loop.',
25-
},
26-
];
27-
2816
export default function AppOnboarding() {
17+
const { t, i18n } = useTranslation();
2918
const colorScheme = useColorScheme();
3019
const pagerViewRef = useRef<PagerView>(null);
3120
const [page, setPage] = useState<number>(0);
3221

3322
const textColor = colorScheme === 'dark' ? 'text-gray-100' : 'text-gray-800';
3423
const bgColor = colorScheme === 'dark' ? 'bg-primary-dark' : 'bg-secondary-light';
3524

25+
3626
const getDotColor = (index: number) => (index === page ? 'bg-action-500' : 'bg-white');
3727
const [token, setToken] = useState<string | null>(null);
3828

3929

4030
useEffect(() => {
41-
// check if user have signed in before
4231

4332
const interval = setInterval(() => {
4433
setPage(page === 2 ? 0 : page + 1);
@@ -51,8 +40,24 @@ export default function AppOnboarding() {
5140
pagerViewRef.current?.setPage(page);
5241
}, [page]);
5342

43+
const pages: Page[] = [
44+
{
45+
image: require('@/assets/images/onboarding/1.png'),
46+
content: t('onboarding.page1'),
47+
},
48+
{
49+
image: require('@/assets/images/onboarding/2.png'),
50+
content: t('onboarding.page2'),
51+
},
52+
{
53+
image: require('@/assets/images/onboarding/3.png'),
54+
content: t('onboarding.page3'),
55+
},
56+
];
57+
5458
return (
5559
<>
60+
{/* Pager View for Onboarding Screens */}
5661
<PagerView
5762
initialPage={page}
5863
style={{ minHeight: 580 }}
@@ -65,7 +70,7 @@ export default function AppOnboarding() {
6570
<Image
6671
source={page.image}
6772
contentFit="contain"
68-
className="mb-6 justify-center items-end"
73+
className="items-end justify-center mb-6"
6974
style={{ width: '100%', flex: 1 }}
7075
/>
7176
<Text
@@ -77,18 +82,28 @@ export default function AppOnboarding() {
7782
</View>
7883
))}
7984
</PagerView>
85+
86+
{/* Pagination Dots */}
8087
<View className={`flex-1 flex-row justify-center items-center gap-3 ${bgColor}`}>
81-
<View className={`rounded-full bg-action-500 w-4 h-4 ${getDotColor(0)}`}></View>
88+
<View className={`rounded-full w-4 h-4 ${getDotColor(0)}`}></View>
8289
<View className={`rounded-full w-4 h-4 ${getDotColor(1)}`}></View>
8390
<View className={`rounded-full w-4 h-4 ${getDotColor(2)}`}></View>
8491
</View>
92+
93+
{/* Language Switcher */}
94+
<View className="mb-4">
95+
<LanguagePicker />
96+
</View>
97+
98+
99+
{/* Get Started Button */}
85100
<View className={`flex-1 flex-row justify-center items-center ${bgColor}`}>
86101
<TouchableOpacity>
87102
<Text
88103
className="text-lg font-Inter-Medium dark:text-white"
89104
onPress={() => router.push('/auth/login')}
90105
>
91-
Get Started
106+
{t('onboarding.getStarted')}
92107
</Text>
93108
</TouchableOpacity>
94109
</View>

app/+not-found.tsx

+9-9
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@ import { Link } from 'expo-router';
22

33
import { Text, View } from '@/components/Themed';
44
import { Image } from 'expo-image';
5+
import { useTranslation } from 'react-i18next';
56

67
export default function NotFoundScreen() {
8+
const { t } = useTranslation();
79
return (
8-
<>
9-
<View className="flex-1 justify-center items-center p-8">
10-
<View className="h-60 w-full mb-10">
10+
<View className="items-center justify-center flex-1 p-8">
11+
<View className="w-full mb-10 h-60">
1112
<Image
1213
source={require('@/assets/images/page_not_found.svg')}
1314
contentFit="contain"
14-
className="mb-6 justify-center items-end"
15+
className="items-end justify-center mb-6"
1516
style={{ width: '100%', flex: 1 }}
1617
/>
1718
</View>
18-
<Text className="text-2xl font-Inter-Bold dark:text-white text-center max-w-64">
19-
Oops! We can't find the page you're looking for.
19+
<Text className="text-2xl text-center font-Inter-Bold dark:text-white max-w-64">
20+
{t('notFound.title')}
2021
</Text>
2122

2223
<Link href="/" className="mt-12">
23-
<View className="py-4 px-6 bg-action-500 rounded-full">
24-
<Text className="text-lg text-white">Go to home screen!</Text>
24+
<View className="px-6 py-4 rounded-full bg-action-500">
25+
<Text className="text-lg text-white">{t('notFound.goHome')}</Text>
2526
</View>
2627
</Link>
2728
</View>
28-
</>
2929
);
3030
}

app/_layout.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useEffect } from 'react';
1818
import 'react-native-reanimated';
1919
import { ToastProvider } from 'react-native-toast-notifications';
2020
export { ErrorBoundary } from 'expo-router';
21+
import '../internationalization/index';
2122

2223
export const unstable_settings = {
2324
initialRouteName: '(onboarding)',

app/auth/_layout.tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { logo } from '@/assets/Icons/auth/Icons';
2+
import LanguagePicker from '@/components/LanguagePicker';
23
import { Slot } from 'expo-router';
34
import { useEffect, useState } from 'react';
45
import { KeyboardAvoidingView, Platform, ScrollView, View, useColorScheme } from 'react-native';
@@ -31,12 +32,31 @@ export default function AuthLayout() {
3132
paddingRight: insets.right,
3233
}}
3334
>
34-
<View className={`flex-1 bg-secondary-light-500 dark:bg-primary-dark h-full ${bgColor}`}>
35-
<View className="w-full h-[87px] relative bg-primary-light dark:bg-primary-dark flex items-center justify-center px-12">
35+
<View className={`flex-1 bg-secondary-light-500 dark:bg-primary-dark h-full ${bgColor}`}>
36+
{/* Header Section */}
37+
<View className="w-full h-[87px] bg-primary-light dark:bg-primary-dark flex flex-row items-center justify-between px-12">
38+
{/* Logo */}
3639
<View className="flex-row items-center">
3740
<SvgXml xml={logo} />
3841
</View>
42+
43+
{/* Language Picker */}
44+
<View className="flex-row items-center space-x-2">
45+
<LanguagePicker
46+
// @ts-ignore
47+
style={{
48+
paddingHorizontal: 10,
49+
paddingVertical: 8,
50+
backgroundColor: colorScheme === 'dark' ? '#333' : '#fff',
51+
borderRadius: 8,
52+
borderWidth: 1,
53+
borderColor: colorScheme === 'dark' ? '#444' : '#ccc',
54+
}}
55+
/>
56+
</View>
3957
</View>
58+
59+
{/* Slot for Auth Content */}
4060
<Slot />
4161
</View>
4262
</ScrollView>

app/auth/forgot-password.tsx

+10-12
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ import { View, Text, TextInput, useColorScheme, SafeAreaView, Dimensions, Toucha
33
import { SvgXml } from 'react-native-svg';
44
import { StatusBar } from 'expo-status-bar';
55
import { bottomIcon_dark, bottomIcon_light } from '@/components/icons/icons';
6-
import Button from '@/components/buttons';
76
import { useMutation } from '@apollo/client';
87
import { useToast } from 'react-native-toast-notifications';
98
import { useFormik } from 'formik';
109
import { ResetPasswordSchema } from '@/validations/login.schema';
1110
import { RESET_PASSWORD_EMAIL } from '@/graphql/mutations/resetPassword';
11+
import { useTranslation } from 'react-i18next';
1212

1313
type FormValues = {
1414
email: string;
1515
};
1616

1717
export default function ResetPassword() {
18+
const { t } = useTranslation();
1819
const [loading, setLoading] = useState(false);
1920
const colorScheme = useColorScheme();
2021
const toast = useToast();
@@ -60,28 +61,25 @@ export default function ResetPassword() {
6061
<SafeAreaView>
6162
<StatusBar/>
6263
<View className={`flex ${bgColor} mt-36`}>
63-
<View className="flex p-10 justify-center mt-16">
64+
<View className="flex justify-center p-10 mt-16">
6465
<Text className={`text-3xl font-Inter-Bold mb-6 text-center ${textColor}`}>
65-
Reset Password
66+
{t("forgotPassword.title")}
6667
</Text>
6768

6869
{replace ? (
6970
<View>
7071
<Text className={`text-m font-Inter-regular mb-2 text-center ${textColor}`}>
71-
Password reset request successful!{' '}
72-
</Text>
73-
<Text className={`text-m font-Inter-regular mb-2 text-center ${textColor}`}>
74-
Please check your email for a link to reset your password!
72+
{t("forgotPassword.successMessage")}
7573
</Text>
7674
</View>
7775
) : (
7876
<View>
7977
<Text className={`text-m font-Inter-regular mb-6 text-center ${textColor}`}>
80-
You will receive an email to proceed with resetting password
78+
{t("forgotPassword.instructions")}
8179
</Text>
8280
<View className={`${inputbg} p-3 rounded-lg shadow border-2 border-[#D2D2D2] mb-2`}>
8381
<TextInput
84-
placeholder="Enter Email"
82+
placeholder={t("forgotPassword.placeholder")}
8583
placeholderTextColor="gray"
8684
value={formik.values.email}
8785
onChangeText={formik.handleChange('email')}
@@ -91,18 +89,18 @@ export default function ResetPassword() {
9189
/>
9290
</View>
9391
{formik.errors.email && (
94-
<Text className="text-error-500 mb-4">{formik.errors.email}</Text>
92+
<Text className="mb-4 text-error-500">{t("")}</Text>
9593
)}
9694
<TouchableOpacity
9795
testID="submit-button"
9896
onPress={() => formik.handleSubmit()}
99-
className="bg-action-500 p-4 rounded-lg items-center"
97+
className="items-center p-4 rounded-lg bg-action-500"
10098
disabled={loading}
10199
>
102100
{loading ? (
103101
<ActivityIndicator color="white" />
104102
) : (
105-
<Text className="text-secondary-light-500 text-lg font-semibold">Continue</Text>
103+
<Text className="text-lg font-semibold text-secondary-light-500">{t("forgotPassword.submitButton")}</Text>
106104
)}
107105
</TouchableOpacity>
108106
</View>

0 commit comments

Comments
 (0)