Merge branch 'add-i18n' into 'main'
feat: add english support and language switcher See merge request torpenn/boxons!5merge-requests/10/head
commit
fca50e267b
|
|
@ -1,20 +1,25 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { Text } from '@/components/shared/Themed';
|
||||
import styled from '@emotion/native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '@/app/RootStackParamList';
|
||||
import { loadUserSettings, saveUserSettings } from '@/components/shared/business/AsyncStorage';
|
||||
import { Switch } from 'react-native';
|
||||
import { i18n } from './i18n/i18n';
|
||||
import { LanguageContext } from '@/app/shared/providers/LanguageProvider';
|
||||
import DropDownPicker from "react-native-dropdown-picker";
|
||||
import { VerticalSpacer } from '@/components/shared/Spacers';
|
||||
import Button from '@/components/shared/Button';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Dashboard'>;
|
||||
const t = i18n.scoped('settings');
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const [soundEnabled, setSoundEnabled] = useState<boolean>(false);
|
||||
const { userChangeLanguage } = useContext(LanguageContext)
|
||||
const [languageOpen, setLanguageOpen] = useState(false);
|
||||
const [languageValue, setLanguageValue] = useState(i18n.localI18n.locale);
|
||||
const [language, setLanguage] = useState([
|
||||
{ label: t('french'), value: "fr" },
|
||||
{ label: t('english'), value: "en" },
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
|
|
@ -40,20 +45,34 @@ export default function Dashboard() {
|
|||
|
||||
return (
|
||||
<Container>
|
||||
<Title>Preferences</Title>
|
||||
|
||||
<Button label="Retour" onPress={() => navigation.navigate('Dashboard')} />
|
||||
|
||||
<VerticalSpacer heightUnits={8} />
|
||||
|
||||
<Row>
|
||||
<Text>Son activé ?</Text>
|
||||
<Text>{t('soundActivate')}</Text>
|
||||
|
||||
<Switch
|
||||
onValueChange={() => setSoundEnabled(previousState => !previousState)}
|
||||
value={soundEnabled}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<VerticalSpacer heightUnits={5} />
|
||||
|
||||
<Text>{t('language')}</Text>
|
||||
<VerticalSpacer heightUnits={2} />
|
||||
<Row>
|
||||
<DropDownPicker
|
||||
open={languageOpen}
|
||||
value={languageValue}
|
||||
items={language}
|
||||
setOpen={setLanguageOpen}
|
||||
setValue={setLanguageValue}
|
||||
setItems={setLanguage}
|
||||
placeholder={languageValue ?? ''}
|
||||
onChangeValue={(value: any) => {
|
||||
const formattedValue = value === 'fr' ? 'fr' : 'en'
|
||||
userChangeLanguage(formattedValue)
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { Stack } from 'expo-router';
|
|||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { useEffect } from 'react';
|
||||
import 'react-native-reanimated';
|
||||
import { i18n } from './i18n/i18n';
|
||||
import { LanguageProvider } from '@/app/shared/providers/LanguageProvider';
|
||||
|
||||
export {
|
||||
// Catch any errors thrown by the Layout component.
|
||||
|
|
@ -46,12 +48,14 @@ export default function RootLayout() {
|
|||
|
||||
function RootLayoutNav() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Stack initialRouteName="Dashboard">
|
||||
<Stack.Screen name="Dashboard" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="Timer" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="Setting" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Stack initialRouteName="Dashboard">
|
||||
<Stack.Screen name="Dashboard" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="Timer" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="Settings" options={{ headerShown: true, title: i18n.t('settings.title') }} />
|
||||
</Stack>
|
||||
</ThemeProvider>
|
||||
</LanguageProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ import { VerticalSpacer } from '@/components/shared/Spacers';
|
|||
import { TimerPickerModal } from 'react-native-timer-picker';
|
||||
import Button from '@/components/shared/Button';
|
||||
import NumberSelector from '@/components/shared/NumberSelector';
|
||||
import { i18n } from '@/app/i18n/i18n';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Timer'>;
|
||||
|
||||
const t = i18n.scoped('dashboard');
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
|
||||
|
|
@ -27,21 +30,21 @@ export default function Dashboard() {
|
|||
|
||||
return (
|
||||
<Container>
|
||||
<Title>Boxons</Title>
|
||||
<Title>{i18n.t('appTitle')}</Title>
|
||||
|
||||
<VerticalSpacer heightUnits={8} />
|
||||
|
||||
<CardContainer>
|
||||
<Card backgroundColor="black">
|
||||
<CardTextContainer>
|
||||
<CustomText>Reps.</CustomText>
|
||||
<CustomText>{t('repetition')}</CustomText>
|
||||
<NumberSelector reps={reps} setReps={setReps} />
|
||||
</CardTextContainer>
|
||||
</Card>
|
||||
|
||||
<Card backgroundColor="red" onPress={() => setShowWorkTimePicker(true)}>
|
||||
<CardTextContainer>
|
||||
<CustomText>Fight</CustomText>
|
||||
<CustomText>{t('fight')}</CustomText>
|
||||
<CustomText>{formatTime(workTime)}</CustomText>
|
||||
</CardTextContainer>
|
||||
</Card>
|
||||
|
|
@ -54,7 +57,7 @@ export default function Dashboard() {
|
|||
setWorkTime(pickedDuration.minutes * 60 + pickedDuration.seconds);
|
||||
setShowWorkTimePicker(false);
|
||||
}}
|
||||
modalTitle="Set Round Time"
|
||||
modalTitle={t('setWorkTime')}
|
||||
onCancel={() => setShowWorkTimePicker(false)}
|
||||
closeOnOverlayPress
|
||||
styles={{
|
||||
|
|
@ -67,7 +70,7 @@ export default function Dashboard() {
|
|||
|
||||
<Card backgroundColor="green" onPress={() => setShowRestTimePicker(true)}>
|
||||
<CardTextContainer>
|
||||
<CustomText>Repos</CustomText>
|
||||
<CustomText>{t('rest')}</CustomText>
|
||||
<CustomText>{formatTime(restTime)}</CustomText>
|
||||
</CardTextContainer>
|
||||
</Card>
|
||||
|
|
@ -80,7 +83,7 @@ export default function Dashboard() {
|
|||
setRestTime(pickedDuration.minutes * 60 + pickedDuration.seconds);
|
||||
setShowRestTimePicker(false);
|
||||
}}
|
||||
modalTitle="Set Rest Time"
|
||||
modalTitle={t('setRestTime')}
|
||||
onCancel={() => setShowRestTimePicker(false)}
|
||||
closeOnOverlayPress
|
||||
styles={{
|
||||
|
|
@ -93,15 +96,15 @@ export default function Dashboard() {
|
|||
</CardContainer>
|
||||
|
||||
<VerticalSpacer heightUnits={5} />
|
||||
<Text>Temps total de travail : {totalWorkTime}</Text>
|
||||
<Text>{t('totalTime')}: {totalWorkTime}</Text>
|
||||
|
||||
<VerticalSpacer heightUnits={5} />
|
||||
|
||||
<Button label="Commencer" onPress={() => navigation.navigate('Timer', { reps, restTime, workTime})} />
|
||||
<Button label={t('begin')} onPress={() => navigation.navigate('Timer', { reps, restTime, workTime})} />
|
||||
|
||||
<VerticalSpacer heightUnits={5} />
|
||||
|
||||
<Button label="Préférences" onPress={() => navigation.navigate('Settings')} />
|
||||
<Button label={t('settings')} onPress={() => navigation.navigate('Settings')} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { I18n, Scope, TranslateOptions } from "i18n-js";
|
||||
|
||||
import { translations } from './translations';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
const localI18n = new I18n(translations);
|
||||
|
||||
localI18n.locale = 'fr';
|
||||
localI18n.defaultLocale = 'fr';
|
||||
|
||||
AsyncStorage.getItem('storedLanguage').then(data => {
|
||||
if (data === null) {
|
||||
localI18n.locale = 'fr'
|
||||
} else {
|
||||
localI18n.locale = data
|
||||
}
|
||||
|
||||
}).catch((error) => console.log(error))
|
||||
|
||||
localI18n.enableFallback = true
|
||||
|
||||
type ScopedTranslationFunction = (
|
||||
scope: Scope,
|
||||
options?: Omit<TranslateOptions, 'scope'>
|
||||
) => string;
|
||||
|
||||
const scoped = (key: string): ScopedTranslationFunction => {
|
||||
return (scope, options) => localI18n.t(scope, { ...options, scope: key });
|
||||
};
|
||||
|
||||
const t: ScopedTranslationFunction = (scope, options?) => localI18n.t(scope, options)
|
||||
|
||||
export const i18n = { ...localI18n, localI18n, t, scoped };
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
export const en = {
|
||||
appTitle: 'Boxons',
|
||||
back: 'Back',
|
||||
dashboard: {
|
||||
repetition: 'Reps',
|
||||
fight: 'Fight time',
|
||||
rest: 'Rest',
|
||||
totalTime: 'Total time',
|
||||
begin: 'Start',
|
||||
settings: 'Settings',
|
||||
setWorkTime: 'Set fight time',
|
||||
setRestTime: 'Set rest time',
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
soundActivate: 'Activate sounds',
|
||||
language: 'Language',
|
||||
french: 'French',
|
||||
english: 'English'
|
||||
},
|
||||
timer: {
|
||||
timerContent: {
|
||||
fight: 'Fight',
|
||||
rest: 'Rest',
|
||||
stop: 'Stop',
|
||||
start: 'Start',
|
||||
},
|
||||
finishContent: {
|
||||
finish: 'Finished',
|
||||
restart: 'Restart',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
export const fr = {
|
||||
appTitle: 'Boxons',
|
||||
back: 'Retour',
|
||||
dashboard: {
|
||||
repetition: 'Reps.',
|
||||
fight: 'Temps du combat',
|
||||
rest: 'Repos',
|
||||
totalTime: 'Temps total',
|
||||
begin: 'Commencer',
|
||||
settings: 'Paramètres',
|
||||
setWorkTime: 'Définir le temps de combat',
|
||||
setRestTime: 'Définir le temps de repos',
|
||||
},
|
||||
settings: {
|
||||
title: 'Paramètres',
|
||||
soundActivate: 'Activer les sons',
|
||||
language: 'Langue',
|
||||
french: 'Français',
|
||||
english: 'Anglais'
|
||||
},
|
||||
timer: {
|
||||
timerContent: {
|
||||
fight: 'Combat',
|
||||
rest: 'Repos',
|
||||
stop: 'Arrêter',
|
||||
start: 'Commencer',
|
||||
},
|
||||
finishContent: {
|
||||
finish: 'Terminé',
|
||||
restart: 'Recommencer'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { fr } from './fr';
|
||||
import { en } from './en';
|
||||
|
||||
export const translations = { fr, en };
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import React, { useState, useEffect, createContext } from 'react'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { i18n } from '@/app/i18n/i18n';
|
||||
|
||||
|
||||
export const LanguageContext = createContext({ userChangeLanguage: async (lang: string) => { } })
|
||||
|
||||
interface LanguageProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const LanguageProviderComponent: React.FC<LanguageProviderProps> = ({ children }) => {
|
||||
const [_language, setLanguage] = useState('fr')
|
||||
|
||||
// In the beginning of App, check if user has selected language before.
|
||||
// If not, use system default language
|
||||
useEffect(() => {
|
||||
i18n.localI18n.locale = 'fr'
|
||||
AsyncStorage.getItem('storedLanguage').then(data => {
|
||||
if (data === null) {
|
||||
i18n.localI18n.locale = 'fr'
|
||||
}
|
||||
else {
|
||||
i18n.localI18n.locale = data
|
||||
setLanguage(data)
|
||||
}
|
||||
}).catch((error) => console.log(error))
|
||||
}, [])
|
||||
|
||||
|
||||
const userChangeLanguage = async (language: string) => {
|
||||
i18n.localI18n.locale = language
|
||||
await AsyncStorage.setItem('storedLanguage', language)
|
||||
setLanguage(language)
|
||||
}
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ userChangeLanguage }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>)
|
||||
}
|
||||
|
||||
export const LanguageProvider = LanguageProviderComponent;
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { i18n } from '@/app/i18n/i18n'; // Assure-toi que ce chemin est correct
|
||||
import { LanguageProvider, LanguageContext } from '../LanguageProvider';
|
||||
|
||||
jest.mock('@react-native-async-storage/async-storage', () => ({
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/app/i18n/i18n', () => ({
|
||||
i18n: {
|
||||
localI18n: {
|
||||
locale: 'fr',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LanguageProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should initialize with default language as "fr" if no language is stored', async () => {
|
||||
AsyncStorage.getItem.mockResolvedValueOnce(null);
|
||||
|
||||
const { getByText } = render(
|
||||
<LanguageProvider>
|
||||
<LanguageContext.Consumer>
|
||||
{({ userChangeLanguage }) => (
|
||||
<>{i18n.localI18n.locale}</> // Affiche la langue courante
|
||||
)}
|
||||
</LanguageContext.Consumer>
|
||||
</LanguageProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(i18n.localI18n.locale).toBe('fr');
|
||||
});
|
||||
});
|
||||
|
||||
test('should load stored language from AsyncStorage if available', async () => {
|
||||
AsyncStorage.getItem.mockResolvedValueOnce('en');
|
||||
|
||||
const { getByText } = render(
|
||||
<LanguageProvider>
|
||||
<LanguageContext.Consumer>
|
||||
{({ userChangeLanguage }) => (
|
||||
<>{i18n.localI18n.locale}</>
|
||||
)}
|
||||
</LanguageContext.Consumer>
|
||||
</LanguageProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(i18n.localI18n.locale).toBe('en');
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow user to change language and save it to AsyncStorage', async () => {
|
||||
AsyncStorage.getItem.mockResolvedValueOnce(null);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LanguageProvider>
|
||||
<LanguageContext.Consumer>
|
||||
{({ userChangeLanguage }) => (
|
||||
<>
|
||||
<>{i18n.localI18n.locale}</> {/* Affiche la langue courante */}
|
||||
<button
|
||||
onPress={() => userChangeLanguage('en')}
|
||||
testID="change-language-button"
|
||||
>
|
||||
Change to English
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</LanguageContext.Consumer>
|
||||
</LanguageProvider>
|
||||
);
|
||||
|
||||
const button = getByTestId('change-language-button');
|
||||
button.props.onPress();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(i18n.localI18n.locale).toBe('en');
|
||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith('storedLanguage', 'en');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,6 +6,7 @@ import { NativeStackNavigationProp } from 'react-native-screens/lib/typescript/n
|
|||
import Button from '@/components/shared/Button';
|
||||
import { HorizontalSpacer, VerticalSpacer } from '@/components/shared/Spacers';
|
||||
import { RootStackParamList } from '@/app/RootStackParamList';
|
||||
import { i18n } from '@/app/i18n/i18n';
|
||||
|
||||
type FinishContentProps = {
|
||||
handleStart: () => void;
|
||||
|
|
@ -13,6 +14,8 @@ type FinishContentProps = {
|
|||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Dashboard'>;
|
||||
|
||||
const t = i18n.scoped('timer.finishContent');
|
||||
|
||||
export default function FinishContent({
|
||||
handleStart
|
||||
}: FinishContentProps) {
|
||||
|
|
@ -20,16 +23,16 @@ export default function FinishContent({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Time>FIN</Time>
|
||||
<Time>{t('finish')}</Time>
|
||||
|
||||
<VerticalSpacer heightUnits={8} />
|
||||
|
||||
<Row>
|
||||
<Button label="Recommencer" onPress={handleStart} />
|
||||
<Button label={t('restart')} onPress={handleStart} />
|
||||
|
||||
<HorizontalSpacer widthUnits={3} />
|
||||
|
||||
<Button label="Retour" onPress={() => navigation.navigate('Dashboard')} />
|
||||
<Button label={i18n.t('back')} onPress={() => navigation.navigate('Dashboard')} />
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
|
@ -41,7 +44,7 @@ const Row = styled(View)(() => ({
|
|||
}));
|
||||
|
||||
const Time = styled(Text)(({ theme }) => ({
|
||||
fontSize: 100,
|
||||
fontSize: 70,
|
||||
fontWeight: 'bold',
|
||||
color: theme.colors.white
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { TimerBgColor } from '@/components/useCases/timer/business/type';
|
|||
import { useNavigation } from 'expo-router';
|
||||
import { NativeStackNavigationProp } from 'react-native-screens/lib/typescript/native-stack/types';
|
||||
import { RootStackParamList } from '@/app/RootStackParamList';
|
||||
import { i18n } from '@/app/i18n/i18n';
|
||||
|
||||
interface TimerContentProps {
|
||||
isWorkPhase: boolean;
|
||||
|
|
@ -21,6 +22,8 @@ interface TimerContentProps {
|
|||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Dashboard'>;
|
||||
|
||||
const t = i18n.scoped('timer.timerContent');
|
||||
|
||||
export default function TimerContent({
|
||||
isWorkPhase,
|
||||
timeLeft,
|
||||
|
|
@ -35,7 +38,7 @@ export default function TimerContent({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title>{isWorkPhase ? 'Fight' : 'Repos'}</Title>
|
||||
<Title>{isWorkPhase ? t('fight') : t('rest')}</Title>
|
||||
|
||||
<VerticalSpacer heightUnits={8} />
|
||||
|
||||
|
|
@ -51,14 +54,14 @@ export default function TimerContent({
|
|||
|
||||
<ButtonContainer bgColor={bgColor}>
|
||||
{isRunning ? (
|
||||
<Button label="Stop" onPress={handleStop} />
|
||||
<Button label={t('stop')} onPress={handleStop} />
|
||||
) : (
|
||||
<Button label="Start" onPress={handleContine} />
|
||||
<Button label={t('start')} onPress={handleContine} />
|
||||
)}
|
||||
|
||||
<VerticalSpacer heightUnits={3} />
|
||||
|
||||
<Button label="Retour" onPress={() => navigation.navigate('Dashboard')} />
|
||||
<Button label={i18n.t('back')} onPress={() => navigation.navigate('Dashboard')} />
|
||||
</ButtonContainer>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@
|
|||
"expo-system-ui": "~3.0.7",
|
||||
"expo-updates": "~0.25.27",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"i18n-js": "^4.4.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.74.5",
|
||||
"react-native-dropdown-picker": "^5.4.6",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
|
|
@ -9322,6 +9324,15 @@
|
|||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
|
||||
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
|
|
@ -13579,6 +13590,17 @@
|
|||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/i18n-js": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/i18n-js/-/i18n-js-4.4.3.tgz",
|
||||
"integrity": "sha512-QIIyvJ+wOKdigL4BlgwiFFrpoXeGdlC8EYgori64YSWm1mnhNYYjIfRu5wETFrmiNP2fyD6xIjVG8dlzaiQr/A==",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
|
|
@ -17756,6 +17778,12 @@
|
|||
"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==",
|
||||
"license": "Unicode-DFS-2016"
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
|
||||
|
|
@ -19850,6 +19878,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-dropdown-picker": {
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/react-native-dropdown-picker/-/react-native-dropdown-picker-5.4.6.tgz",
|
||||
"integrity": "sha512-T1XBHbE++M6aRU3wFYw3MvcOuabhWZ29RK/Ivdls2r1ZkZ62iEBZknLUPeVLMX3x6iUxj4Zgr3X2DGlEGXeHsA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-helmet-async": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-helmet-async/-/react-native-helmet-async-2.0.4.tgz",
|
||||
|
|
|
|||
|
|
@ -42,9 +42,11 @@
|
|||
"expo-system-ui": "~3.0.7",
|
||||
"expo-updates": "~0.25.27",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"i18n-js": "^4.4.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.74.5",
|
||||
"react-native-dropdown-picker": "^5.4.6",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
|
|
|
|||
Loading…
Reference in New Issue