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"