From fc53bb14a0d314eb65ae0401ec52fdaa59293210 Mon Sep 17 00:00:00 2001 From: Maarten Date: Mon, 12 Aug 2024 16:48:41 +0200 Subject: [PATCH] Add multilanguage support --- app/(onboarding)/start.tsx | 32 +++++--- app/(settings)/notifications.tsx | 26 ++++--- app/(tabs)/explore.tsx | 23 +++--- app/(tabs)/index.tsx | 70 +++++++++-------- app/(tabs)/map.tsx | 6 +- app/(tabs)/settings.tsx | 124 +++++++++++++++++++++++++------ app/index.tsx | 10 ++- assets/languages/en.json | 93 +++++++++++++++++++++++ assets/languages/index.tsx | 2 + assets/languages/nl.json | 93 +++++++++++++++++++++++ lib/components/EditModal.tsx | 10 ++- lib/localization/i18n.tsx | 54 ++++++++++++++ lib/store/dataStore.tsx | 1 + package.json | 3 + yarn.lock | 34 ++++++++- 15 files changed, 489 insertions(+), 92 deletions(-) create mode 100644 assets/languages/en.json create mode 100644 assets/languages/index.tsx create mode 100644 assets/languages/nl.json create mode 100644 lib/localization/i18n.tsx diff --git a/app/(onboarding)/start.tsx b/app/(onboarding)/start.tsx index a5f7878..a04542c 100644 --- a/app/(onboarding)/start.tsx +++ b/app/(onboarding)/start.tsx @@ -3,6 +3,8 @@ import { Stack } from 'expo-router'; import { StyleSheet, TextInput, TouchableOpacity } from 'react-native'; import { router } from 'expo-router'; import DeviceInfo from 'react-native-device-info'; +import { useTranslation } from 'react-i18next'; +import Ionicons from '@expo/vector-icons/Ionicons'; import { ThemedText } from '@/lib/components/ThemedText'; import { ThemedView } from '@/lib/components/ThemedView'; @@ -17,13 +19,14 @@ import { setSession } from '@/lib/store/dataStore'; export default function OnboardStartScreen() { const colorScheme = useColorScheme() ?? 'light'; const { setToken } = useToken(); + const { t } = useTranslation(); const [ name, setName ] = React.useState( '' ); const [ zipcode, setZipcode ] = React.useState( '' ); const [ houseNumber, setHouseNumber ] = React.useState( '' ); const start = () => { if (name === '' || zipcode === '' || houseNumber === '') { - Message.error( 'Niet alle gegevens zijn ingevuld!' ); + Message.error( t( "onboarding.missing-info" ) ); return; } @@ -47,7 +50,7 @@ export default function OnboardStartScreen() { setToken( token ); router.replace( "/(tabs)" ); - store.dispatch(setSession(response.session)); + store.dispatch( setSession( response.session ) ); Message.success( response.message ); } @@ -59,32 +62,32 @@ export default function OnboardStartScreen() { - Welkom bij + {t( "onboarding.welcome" )} Kliko - Wat is je naam? + {t( "onboarding.your-name" )} - Wat is je postcode en huisnummer? + {t( "onboarding.your-address" )} @@ -92,7 +95,10 @@ export default function OnboardStartScreen() { - Start + + {t( "onboarding.start" )} + + @@ -129,5 +135,13 @@ const styles = StyleSheet.create( { paddingLeft: 40, paddingRight: 40, marginTop: 30, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', }, + buttonIcon: { + marginLeft: 15, + paddingTop: 2, + color: Colors.white + } } ); diff --git a/app/(settings)/notifications.tsx b/app/(settings)/notifications.tsx index 227da66..1eeb57a 100644 --- a/app/(settings)/notifications.tsx +++ b/app/(settings)/notifications.tsx @@ -3,6 +3,7 @@ import { SafeAreaView, ScrollView, StyleSheet, Switch, TouchableOpacity, } from import { useNavigation } from '@react-navigation/native'; import { DateTimePickerAndroid, DateTimePickerEvent } from '@react-native-community/datetimepicker'; import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; import { Colors } from '@/lib/constants/Colors'; import { useColorScheme } from '@/lib/hooks/useColorScheme'; @@ -19,6 +20,7 @@ export default function CategoryScreen() { const colorScheme = useColorScheme() ?? 'light'; const navigation = useNavigation(); const { token } = useToken(); + const { t } = useTranslation(); const session = useSelector( (state: any) => state.data.session ); const [ sessionSet, setSessionSet ] = useState( false ); @@ -34,7 +36,7 @@ export default function CategoryScreen() { // Set page title useEffect( () => { // Set page title - navigation.setOptions( { title: ( 'Notificaties' ) } ); + navigation.setOptions( { title: ( t( "notifications" ) ) } ); }, [] ); // Set session data @@ -80,8 +82,8 @@ export default function CategoryScreen() { onChange, mode: 'time', is24Hour: true, - positiveButton: { label: 'Kies' }, - negativeButton: { label: 'Sluiten' }, + positiveButton: { label: t( "choose" ) }, + negativeButton: { label: t( "close" ) }, } ); } } @@ -97,14 +99,14 @@ export default function CategoryScreen() { if (currentEdit === 'dayBefore') { const minDate = new Date( `1970-01-01T16:00` ); if (minDate > selectedDate) { - Message.error( 'Meldingen voor 16:00 worden niet ondersteund' ); + Message.error( t( "notifications-before-16-00" ) ); return; } } else { const minDate = new Date( `1970-01-01T09:00` ); if (minDate < selectedDate) { - Message.error( 'Meldingen na 09:00 worden niet ondersteund' ); + Message.error( t( "notifications-after-09-00" ) ); return; } @@ -141,7 +143,7 @@ export default function CategoryScreen() { if (response.success) { store.dispatch( setSession( response.session ) ); } else { - Message.error( 'Er ging iets mis. Probeer het later opnieuw.' ); + Message.error( t( "something-went-wrong" ) ); } } ) } @@ -159,19 +161,19 @@ export default function CategoryScreen() { style={styles.listSwitch} onValueChange={() => toggleDate( 'dayBefore' )} /> - Dag van te voren + {t( "day-before" )} {isDayBeforeEnabled ? ( selectTime( 'dayBefore' )}> - Om {dayBefore} + {t( "at" )} {dayBefore} ) : ( - Uit + {t( "off" )} ) } @@ -188,19 +190,19 @@ export default function CategoryScreen() { style={styles.listSwitch} onValueChange={() => toggleDate( 'sameDay' )} /> - Op de ophaaldag + {t( "same-day" )} {isSameDayEnabled ? ( selectTime( 'sameDay' )}> - Om {sameDay} + {t( "at" )} {sameDay} ) : ( - Uit + {t( "off" )} ) } diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index 9d49c52..d813fdc 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -4,8 +4,7 @@ import { SafeAreaView, StatusBar, Image, - TouchableOpacity, - View, + TouchableOpacity } from 'react-native'; import React, { useState, useEffect, useRef } from 'react'; @@ -13,6 +12,7 @@ import { router } from 'expo-router'; import type { AutocompleteDropdownRef } from 'react-native-autocomplete-dropdown' import { AutocompleteDropdown } from 'react-native-autocomplete-dropdown'; import Modal from "react-native-modal"; +import { useTranslation } from 'react-i18next'; import { ThemedText } from '@/lib/components/ThemedText'; import { ThemedView } from '@/lib/components/ThemedView'; @@ -25,6 +25,7 @@ import { setViewCategory } from '@/lib/store/dataStore'; export default function ExploreScreen() { const colorScheme = useColorScheme() ?? 'light'; + const { t } = useTranslation(); const [ categories, setCategories ] = useState( [] ); const [ types, setTypes ] = useState( [] ); const [ activeCategory, setActiveCategory ] = useState( null ); @@ -78,29 +79,29 @@ export default function ExploreScreen() { - Wat moet waar? + {t( "what-where" )} - en waarom? + {t( "what-why" )} - Wat wilt u scheiden? + {t( "what-separate" )} { dropdownController.current = controller }} textInputProps={{ - placeholder: 'Wat wilt u scheiden?', + placeholder: t( "what-separate" ), autoCorrect: false, autoCapitalize: 'none', }} clearOnFocus={false} closeOnBlur={false} closeOnSubmit={false} - emptyResultText={'Niks gevonden'} + emptyResultText={t( "nothing-found" )} onSelectItem={selectItem} dataSet={categories} showClear={false} @@ -108,7 +109,7 @@ export default function ExploreScreen() { - Of kies een categorie: + {t( "choose-category" )}: @@ -132,7 +133,7 @@ export default function ExploreScreen() { - + {activeCategory?.name} @@ -146,14 +147,14 @@ export default function ExploreScreen() { } - Beschrijving: + {t( "description" )}: {activeCategory?.description} setActiveCategory( null )} style={{ ...styles.modalClose, backgroundColor: Colors[ colorScheme ].tint }}> - Sluiten + {t( "close" )} diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index f75c81c..466c79d 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -5,6 +5,7 @@ import CalendarPicker from 'react-native-calendar-picker'; import { useIsFocused } from '@react-navigation/core'; import { useSelector } from 'react-redux'; import { LogLevel, OneSignal } from 'react-native-onesignal'; +import { useTranslation } from 'react-i18next'; import { ThemedText } from '@/lib/components/ThemedText'; import { ThemedView } from '@/lib/components/ThemedView'; @@ -19,9 +20,10 @@ import { ThemedIcon } from '@/lib/components/ThemedIcon'; export default function HomeScreen() { const colorScheme = useColorScheme() ?? 'light'; const isFocused = useIsFocused(); + const { t } = useTranslation(); const session = useSelector( (state: any) => state.data.session ); const reloadCalendar = useSelector( (state: any) => state.data.reloadCalendar ); - const { token, isLoading } = useToken(); + const { token } = useToken(); const [ name, setName ] = useState( ' ' ); // Default empty space to prevent layout shifting const [ dates, setDates ] = useState( [] ); const [ types, setTypes ] = useState( [] ); @@ -68,6 +70,21 @@ export default function HomeScreen() { } }, [ reloadCalendar, isFocused ] ); + const getGreeting = () => { + const myDate = new Date(); + const hours = myDate.getHours(); + + if (hours < 12) { + return t( "greeting.morning" ); + } else if (hours >= 12 && hours <= 17) { + return t( "greeting.afternoon" ); + } else if (hours >= 17 && hours <= 24) { + return t( "greeting.evening" ); + } + + return t( "greeting.hello" ); + } + // Load calendar data const loadCalendar = () => { Request.post( 'calendar', { token: token } ).then( (response) => { @@ -116,26 +133,34 @@ export default function HomeScreen() { nextComponent={ } - weekdays={[ "Ma", "Di", "Woe", "Do", "Vrij", "Zat", "Zo" ]} + weekdays={[ + t( "days.mon" ), + t( "days.tue" ), + t( "days.wed" ), + t( "days.thu" ), + t( "days.fri" ), + t( "days.sat" ), + t( "days.sun" ), + ]} months={[ - "Januari", - "Februari", - "Maart", - "April", - "Mei", - "Juni", - "Juli", - "Augustus", - "September", - "Oktober", - "November", - "December", + t( "months.january" ), + t( "months.february" ), + t( "months.march" ), + t( "months.april" ), + t( "months.may" ), + t( "months.june" ), + t( "months.july" ), + t( "months.august" ), + t( "months.september" ), + t( "months.october" ), + t( "months.november" ), + t( "months.december" ), ]} /> - Legenda: + {t( "legenda" )}: = 12 && hours <= 17) { - return 'Goedemiddag'; - } else if (hours >= 17 && hours <= 24) { - return 'Goedenavond'; - } - - return 'Hallo'; -} - const styles = StyleSheet.create( { container: { padding: 25, diff --git a/app/(tabs)/map.tsx b/app/(tabs)/map.tsx index f179830..4d02fe8 100644 --- a/app/(tabs)/map.tsx +++ b/app/(tabs)/map.tsx @@ -3,6 +3,7 @@ import { Image, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, View, D import Mapbox, { Callout, Camera, MapView, PointAnnotation } from "@rnmapbox/maps"; import { useSelector } from 'react-redux'; import { useIsFocused } from '@react-navigation/core'; +import { useTranslation } from 'react-i18next'; Mapbox.setAccessToken( "pk.eyJ1IjoibWFhcnRlbnZyOTgiLCJhIjoiY2x6ZDFqMGp1MGVyejJrczhqcXpvYm9iYiJ9.XvYcL62dWiJQiFmG6mOoug" ); @@ -17,6 +18,7 @@ export default function MapScreen() { const colorScheme = useColorScheme() ?? 'light'; const isFocused = useIsFocused(); const session = useSelector((state: any) => state.data.session); + const { t } = useTranslation(); const [ types, setTypes ] = useState( [] ); const [ coordinates, setCoordinates ] = useState( [] ); @@ -85,11 +87,11 @@ export default function MapScreen() { - Afvalcontainers + {t( "garbage-bins" )} - in de buurt + {t( "nearby" )} diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index bd09dd9..3b02f9d 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -1,14 +1,17 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { StyleSheet, ScrollView, SafeAreaView, StatusBar, TouchableOpacity, - Alert, + Alert, Image, } from 'react-native'; import { useRouter } from 'expo-router'; import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { BottomSheet, BottomSheetRefType } from 'react-native-select-bottom-list'; + import { ThemedText } from '@/lib/components/ThemedText'; import { ThemedView } from '@/lib/components/ThemedView'; @@ -21,10 +24,12 @@ import CustomModal from '@/lib/components/EditModal'; import { store } from '@/lib/store/store'; import { setSession, setReloadCalendar } from '@/lib/store/dataStore'; import { ThemedIcon } from '@/lib/components/ThemedIcon'; +import List from '@/lib/components/List'; export default function SettingsScreen() { const colorScheme = useColorScheme() ?? 'light'; const { token, setToken } = useToken(); + const { t, i18n } = useTranslation(); const session = useSelector( (state: any) => state.data.session ); const router = useRouter(); @@ -39,6 +44,10 @@ export default function SettingsScreen() { const [ city, setCity ] = React.useState( '3' ); const [ addressModalVisible, setAddressModalVisible ] = useState( false ); + // Language + const [ language, setLanguage ] = useState( 'nl' ); + const sheetRef = useRef( null ); + useEffect( () => { setSessionData( session ); }, [ session ] ); @@ -53,6 +62,9 @@ export default function SettingsScreen() { setHouseNumber( session.address.houseNumber ); setStreet( session.address.street ); setCity( session.address.city ); + + // Language + setLanguage(session.language); } // Handle save settings @@ -75,6 +87,10 @@ export default function SettingsScreen() { addressChanged = true; } + if (inputValues.language) { + postData[ 'language' ] = inputValues.language; + } + Request.post( 'sessions/update', postData ) .then( (response) => { if (response.success) { @@ -84,32 +100,46 @@ export default function SettingsScreen() { store.dispatch( setSession( response.session ) ) store.dispatch( setReloadCalendar( addressChanged ) ) - Message.success( 'Opgeslagen!' ) + Message.success( t( "saved" ) ) } else { Message.error( response.message ); } } ); }; + const openLanguageSelect = () => { + sheetRef.current?.open(); + } + + const changeLanguage = (lang: string) => { + sheetRef.current?.close(); + + i18n.changeLanguage( lang ).then( () => { + setLanguage(lang); + + handleSave( { language: lang } ); + } ); + } + // Remove session data and logout const logout = () => { - Alert.alert( 'Uitloggen', 'Weet je het zeker?', [ + Alert.alert( t( "logout" ), t( "are-you-sure" ), [ { - text: 'Annuleren', + text: t( "cancel" ), style: 'cancel', }, { - text: 'Ja', + text: t( "ja" ), onPress: () => { Request.post( 'sessions/delete' ).then( (response) => { if (!response.success) { - Message.success( 'Je bent uitgelogd' ); + Message.success( t( "logged-out" ) ); setToken( null ); router.replace( '/(onboarding)/start' ); } else { - Message.error( 'Er is iets mis gegaan. Probeer het later opnieuw!' ) + Message.error( t( "something-went-wrong" ) ) } } ) } @@ -122,14 +152,14 @@ export default function SettingsScreen() { - Instellingen + {t( "settings" )} - Naam + {t( "name" )} setNameModalVisible( true )}> @@ -141,7 +171,7 @@ export default function SettingsScreen() { - Adres + {t( "address" )} setAddressModalVisible( true )}> @@ -153,11 +183,23 @@ export default function SettingsScreen() { - Notificaties + {t( "notifications" )} router.push( '/(settings)/notifications' )}> - Wijzigen + {t( "change" )} + + + + + + + + {t( "language" )} + + + + {t( "languages." + language )} @@ -166,7 +208,7 @@ export default function SettingsScreen() { - Uitloggen + {t( "logout" )} @@ -174,39 +216,63 @@ export default function SettingsScreen() { setNameModalVisible( false )} onSave={handleSave} fields={[ { name: 'name', - placeholder: 'Je naam', + placeholder: t( "modal.name.your-name" ), defaultValue: name, }, ]} /> setAddressModalVisible( false )} onSave={handleSave} fields={[ { name: 'zipcode', - title: 'Postcode', - placeholder: 'Je postcode', + title: t( "modal.address.zipcode" ), + placeholder: t( "modal.address.your-zipcode" ), defaultValue: zipcode, }, { name: 'houseNumber', - title: 'Huisnummer', - placeholder: 'Je huis nummer', + title: t( "modal.address.house-number" ), + placeholder: t( "modal.address.your-house-number" ), defaultValue: houseNumber, }, ]} /> + + + + ( + changeLanguage( item.key )}> + {item.name} + + + )} + /> + + ); } @@ -262,4 +328,20 @@ const styles = StyleSheet.create( { logout: { color: Colors.red, }, + languagesList: { + display: 'flex', + flexDirection: 'column', + padding: 25, + }, + languagesListItem: { + display: 'flex', + gap: 8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingBottom: 15, + marginBottom: 15, + borderBottomWidth: 1, + borderBottomColor: '#f2f2f2', + } } ); diff --git a/app/index.tsx b/app/index.tsx index c7bf588..0c5e1f5 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; -import { Redirect, router, useRouter } from 'expo-router'; +import { Redirect, useRouter } from 'expo-router'; +import { useTranslation } from 'react-i18next'; import { ThemedText } from '@/lib/components/ThemedText'; import { ThemedView } from '@/lib/components/ThemedView'; @@ -7,15 +8,17 @@ import { useToken } from '@/lib/context/AppProvider'; import { Request } from '@/lib/services/request'; import { store } from '@/lib/store/store'; import { setSession } from '@/lib/store/dataStore'; +import '@/lib/localization/i18n'; export default function OnboardStartScreen() { const { token, isLoading } = useToken(); + const { i18n } = useTranslation(); const router = useRouter(); const loadingScreen = () => ( - Laden... + Loading... ); @@ -26,6 +29,9 @@ export default function OnboardStartScreen() { // Save to store store.dispatch(setSession(response.session)) + // Set language + i18n.changeLanguage(response.session.language); + // @ts-ignore router.replace( '/(tabs)' ); } else { diff --git a/assets/languages/en.json b/assets/languages/en.json new file mode 100644 index 0000000..4c9cea9 --- /dev/null +++ b/assets/languages/en.json @@ -0,0 +1,93 @@ +{ + "onboarding": { + "missing-info": "Not all information has been filled in!", + "welcome": "Welcome to", + "your-name": "What is your name?", + "name": "Your name", + "your-address": "What is your address?", + "zipcode": "Zipcode", + "house-number": "House number", + "start": "Start" + }, + "greeting": { + "hello": "Hello", + "morning": "Good morning", + "afternoon": "Good afternoon", + "evening": "Good evening" + }, + "months": { + "january": "January", + "february": "February", + "march": "March", + "april": "April", + "may": "May", + "june": "June", + "july": "July", + "august": "August", + "september": "September", + "october": "October", + "november": "November", + "december": "December" + }, + "days": { + "mon": "Mon", + "tue": "Tue", + "wed": "Wed", + "thu": "Thu", + "fri": "Fri", + "sat": "Sat", + "sun": "Sun" + }, + "legenda": "Legend", + "garbage-bins": "Garbage bins", + "nearby": "nearby", + "what-where": "What goes where?", + "what-why": "and why?", + "what-separate": "What do you want to separate?", + "nothing-found": "Nothing found", + "choose-category": "Or choose a category", + "description": "Description", + "close": "Close", + "saved": "Saved!", + "are-you-sure": "Are you sure?", + "cancel": "Cancel", + "yes": "Yes", + "logged-out": "You are logged out", + "something-went-wrong": "Something went wrong. Please try again later!", + "settings": "Settings", + "name": "Name", + "address": "Address", + "language": "Language", + "languages": { + "nl": "Dutch", + "en": "English" + }, + "notifications": "Notifications", + "change": "Change", + "logout": "Logout", + "modal": { + "name": { + "title": "Change name", + "your-name": "Your name" + }, + "address": { + "title": "Change address", + "zipcode": "Zip code", + "your-zipcode": "Your zip code", + "house-number": "House number", + "your-house-number": "Your house number" + }, + "default": { + "title": "Edit your details:", + "cancel": "Cancel", + "save": "Save" + } + }, + "choose": "Choose", + "notifications-before-16-00": "Meldingen voor 16:00 worden niet ondersteund", + "notifications-after-09-00": "Meldingen na 09:00 worden niet ondersteund", + "day-before": "A day before", + "same-day": "At the same day", + "off": "Off", + "at": "at" +} \ No newline at end of file diff --git a/assets/languages/index.tsx b/assets/languages/index.tsx new file mode 100644 index 0000000..28933a0 --- /dev/null +++ b/assets/languages/index.tsx @@ -0,0 +1,2 @@ +export { default as en } from "./en.json"; +export { default as nl } from "./nl.json"; \ No newline at end of file diff --git a/assets/languages/nl.json b/assets/languages/nl.json new file mode 100644 index 0000000..281680a --- /dev/null +++ b/assets/languages/nl.json @@ -0,0 +1,93 @@ +{ + "onboarding": { + "missing-info": "Niet alle gegevens zijn ingevuld!", + "welcome": "Welkom bij", + "your-name": "Wat is je naam?", + "name": "Je naam", + "your-address": "Wat is je adres?", + "zipcode": "Postcode", + "house-number": "Huisnummer", + "start": "Start" + }, + "greeting": { + "hello": "Hallo", + "morning": "Goedemorgen", + "afternoon": "Goedemiddag", + "evening": "Goedenavond" + }, + "months": { + "january": "Januari", + "february": "Februari", + "march": "Maart", + "april": "April", + "may": "Mei", + "june": "Juni", + "july": "Juli", + "august": "Augustus", + "september": "September", + "october": "Oktober", + "november": "November", + "december": "December" + }, + "days": { + "mon": "Ma", + "tue": "Di", + "wed": "Woe", + "thu": "Do", + "fri": "Vrij", + "sat": "Zat", + "sun": "Zon" + }, + "legenda": "Legenda", + "garbage-bins": "Afvalcontainers", + "nearby": "in de buurt", + "what-where": "Wat moet waar?", + "what-why": "en waarom?", + "what-separate": "Wat wilt u scheiden?", + "nothing-found": "Niks gevonden", + "choose-category": "Of kies een categorie", + "description": "Beschrijving", + "close": "Sluiten", + "saved": "Opgeslagen!", + "are-you-sure": "Weet je het zeker?", + "cancel": "Annuleren", + "yes": "Ja", + "logged-out": "Je bent uitgelogd", + "something-went-wrong": "Er is iets mis gegaan. Probeer het later opnieuw!", + "settings": "Instellingen", + "name": "Naam", + "address": "Adres", + "language": "Taal", + "languages": { + "nl": "Nederlands", + "en": "Engels" + }, + "notifications": "Notificaties", + "change": "Wijzigen", + "logout": "Uitloggen", + "modal": { + "name": { + "title": "Naam wijzigen", + "your-name": "Je naam" + }, + "address": { + "title": "Adres wijzigen", + "zipcode": "Postcode", + "your-zipcode": "Je postcode", + "house-number": "Huisnummer", + "your-house-number": "Je huisnummer" + }, + "default": { + "title": "Pas je gegevens aan:", + "cancel": "Annuleren", + "save": "Opslaan" + } + }, + "choose": "Kies", + "notifications-before-16-00": "Meldingen voor 16:00 worden niet ondersteund", + "notifications-after-09-00": "Meldingen na 09:00 worden niet ondersteund", + "day-before": "Dag van te voren", + "same-day": "Op de ophaaldag", + "off": "Uit", + "at": "Om" +} \ No newline at end of file diff --git a/lib/components/EditModal.tsx b/lib/components/EditModal.tsx index dde469c..bb2a7a7 100644 --- a/lib/components/EditModal.tsx +++ b/lib/components/EditModal.tsx @@ -5,6 +5,7 @@ import { StyleSheet, } from 'react-native'; import Modal from "react-native-modal"; +import { useTranslation } from 'react-i18next'; import { Colors } from '@/lib/constants/Colors'; import { ThemedView } from '@/lib/components/ThemedView'; @@ -29,6 +30,7 @@ interface EditModalProps { const CustomModal: React.FC = ({ title, visible, onClose, onSave, fields }) => { const colorScheme = useColorScheme() ?? 'light'; const [ inputValues, setInputValues ] = useState>( {} ); + const { t } = useTranslation(); useEffect( () => { const initialValues: Record = {}; @@ -54,13 +56,13 @@ const CustomModal: React.FC = ({ title, visible, onClose, onSave return ( - {title ? title : 'Pas je gegevens aan:'} + {title ? title : t( "modal.default.title" )} {fields.map( (field, index) => ( {field.title && {field.title}} handleInputChange( field.name, text )} value={inputValues[ field.name ] || ''} @@ -73,11 +75,11 @@ const CustomModal: React.FC = ({ title, visible, onClose, onSave { } )}> - Annuleren + {t( "modal.default.cancel" )} - Opslaan + {t( "modal.default.save" )} diff --git a/lib/localization/i18n.tsx b/lib/localization/i18n.tsx new file mode 100644 index 0000000..a96502a --- /dev/null +++ b/lib/localization/i18n.tsx @@ -0,0 +1,54 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { en, nl } from "@/assets/languages"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + +const STORE_LANGUAGE_KEY = "settings.lang"; + +const languageDetectorPlugin = { + type: "languageDetector", + async: true, + init: () => { }, + detect: async function (callback: (lang: string) => void) { + try { + // get stored language from Async storage + // put your own language detection logic here + await AsyncStorage.getItem(STORE_LANGUAGE_KEY).then((language) => { + if (language) { + //if language was stored before, use this language in the app + return callback(language); + } else { + //if language was not stored yet, use english + return callback("nl"); + } + }); + } catch (error) { + console.log("Error reading language", error); + } + }, + cacheUserLanguage: async function (language: string) { + try { + //save a user's language choice in Async storage + await AsyncStorage.setItem(STORE_LANGUAGE_KEY, language); + } catch (error) { } + }, +}; +const resources = { + en: { + translation: en, + }, + nl: { + translation: nl, + }, +}; + +// @ts-ignore +i18n.use(initReactI18next).use(languageDetectorPlugin).init({ + resources, + compatibilityJSON: 'v3', + fallbackLng: "nl", + interpolation: { + escapeValue: false, + }, +}); +export default i18n; \ No newline at end of file diff --git a/lib/store/dataStore.tsx b/lib/store/dataStore.tsx index ea3943d..dba6c8a 100644 --- a/lib/store/dataStore.tsx +++ b/lib/store/dataStore.tsx @@ -8,6 +8,7 @@ const dataStore = createSlice( { token: '', name: '', device: '', + language: 'nl', address: { id: 0, zipcode: '', diff --git a/package.json b/package.json index b481023..ada40ad 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,12 @@ "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", "expo-web-browser": "~13.0.3", + "i18next": "^23.12.2", "install": "^0.13.0", "onesignal-expo-plugin": "^2.0.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-i18next": "^15.0.1", "react-native": "0.74.3", "react-native-autocomplete-dropdown": "3.1.5", "react-native-calendar-picker": "^8.0.0", @@ -52,6 +54,7 @@ "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "4.10.5", "react-native-screens": "3.31.1", + "react-native-select-bottom-list": "^1.0.7", "react-native-svg": "15.2.0", "react-native-toast-message": "^2.2.0", "react-native-web": "~0.19.10", diff --git a/yarn.lock b/yarn.lock index 5398c4d..46f8ad1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -773,7 +773,7 @@ resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.8": version "7.25.0" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz" integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== @@ -4950,6 +4950,13 @@ html-escaper@^2.0.0: resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + htmlparser2@^7.1.2: version "7.2.0" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz" @@ -4998,6 +5005,13 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz" integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw== +i18next@^23.12.2: + version "23.12.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.12.2.tgz#c5b44bb95e4d4a5908a51577fa06c63dc2f650a4" + integrity sha512-XIeh5V+bi8SJSWGL3jqbTEBW5oD6rbP5L+E7dVQh1MNTxxYef0x15rhJVcRb7oiuq4jLtgy2SD8eFlf6P2cmqg== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.6.3, iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" @@ -7380,6 +7394,14 @@ react-freeze@^1.0.0: resolved "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz" integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA== +react-i18next@^15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.0.1.tgz#fc662d93829ecb39683fe2757a47ebfbc5c912a0" + integrity sha512-NwxLqNM6CLbeGA9xPsjits0EnXdKgCRSS6cgkgOdNcPXqL+1fYNl8fBg1wmnnHvFy812Bt4IWTPE9zjoPmFj3w== + dependencies: + "@babel/runtime" "^7.24.8" + html-parse-stringify "^3.0.1" + "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -7520,6 +7542,11 @@ react-native-screens@3.31.1: react-freeze "^1.0.0" warn-once "^0.1.0" +react-native-select-bottom-list@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/react-native-select-bottom-list/-/react-native-select-bottom-list-1.0.7.tgz#ef4c9f2520218c442a70c5000fd74e1a12030722" + integrity sha512-yzdQwchhdi1lZO973jxOkRF9BksWK/uzVZBoYUENkWYoLumSIR6iozUiFcpAGpy06XFtvy2dJtM7K3Wah2JpzA== + react-native-size-matters@^0.4.0: version "0.4.2" resolved "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.4.2.tgz" @@ -9017,6 +9044,11 @@ vlq@^1.0.0: resolved "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz" integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz"