Front/translate refactor (#110)

* Front: i18n: Create component

* Front: Use new translation component

* Front: Translation COmpoent: Change props name

* Front: Fix merge
This commit is contained in:
Arthur Jamet
2022-11-24 17:20:45 +00:00
committed by GitHub
parent 620dae8316
commit 8546c86332
10 changed files with 124 additions and 56 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import { Heading, VStack } from 'native-base';
type SongCardGrid = {
songs: Parameters<typeof SongCard>[0][];
maxItemPerRow?: number,
heading?: string
heading?: JSX.Element,
}
const SongCardGrid = (props: SongCardGrid) => {
+20
View File
@@ -0,0 +1,20 @@
import { translate } from "../i18n/i18n";
import { en } from "../i18n/Translations";
import { RootState, useSelector } from "../state/Store";
type TranslateProps = {
translationKey: keyof typeof en;
format?: (translated: string) => string;
}
/**
* Translation component
* @param param0
* @returns
*/
const Translate = ({ translationKey, format }: TranslateProps) => {
const selectedLanguage = useSelector((state: RootState) => state.language.value);
const translated = translate(translationKey, selectedLanguage);
return <>{format ? format(translated) : translated}</>;
}
export default Translate;
+8 -4
View File
@@ -1,7 +1,7 @@
// a form for sign in
import React from "react";
import { translate } from "../../i18n/i18n";
import { Translate, translate } from "../../i18n/i18n";
import { string } from "yup";
import {
FormControl,
@@ -51,7 +51,9 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
formData.password.error !== null
}
>
<FormControl.Label>{translate("username")}</FormControl.Label>
<FormControl.Label>
<Translate translationKey='username'/>
</FormControl.Label>
<Input
isRequired
type="text"
@@ -72,7 +74,9 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
>
{formData.username.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("password")}</FormControl.Label>
<FormControl.Label>
<Translate translationKey='password'/>
</FormControl.Label>
<Input
isRequired
type="password"
@@ -117,7 +121,7 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
}
}}
>
{translate("login")}
<Translate translationKey='login'/>
</Button>
</FormControl>
</Stack>
+14 -6
View File
@@ -1,7 +1,7 @@
// a form for sign up
import React from "react";
import { translate } from "../../i18n/i18n";
import { Translate, translate } from "../../i18n/i18n";
import { string } from "yup";
import {
FormControl,
@@ -74,7 +74,9 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => {
formData.email.error !== null
}
>
<FormControl.Label>{translate("username")}</FormControl.Label>
<FormControl.Label>
<Translate translationKey='username'/>
</FormControl.Label>
<Input
isRequired
type="text"
@@ -95,7 +97,9 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => {
>
{formData.username.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("email")}</FormControl.Label>
<FormControl.Label>
<Translate translationKey='email'/>
</FormControl.Label>
<Input
isRequired
type="text"
@@ -116,7 +120,9 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => {
>
{formData.email.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("password")}</FormControl.Label>
<FormControl.Label>
<Translate translationKey='password'/>
</FormControl.Label>
<Input
isRequired
type="password"
@@ -137,7 +143,9 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => {
>
{formData.password.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("repeatPassword")}</FormControl.Label>
<FormControl.Label>
<Translate translationKey='repeatPassword'/>
</FormControl.Label>
<Input
isRequired
type="password"
@@ -193,7 +201,7 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => {
}
}}
>
{translate("signUp")}
<Translate translationKey='signUp'/>
</Button>
</FormControl>
</Stack>
+9 -2
View File
@@ -1,7 +1,7 @@
import { en, fr, sp } from './Translations';
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Translate from '../components/Translate';
export type AvailableLanguages = 'en' | 'fr' | 'sp';
export const DefaultLanguage: AvailableLanguages = 'en';
@@ -28,9 +28,16 @@ i18n
});
export default i18n;
/**
* Typesafe translation method
* @param textKey the key of th text to translate
* @returns the translated text
*/
export const translate = (textKey: keyof typeof en) => i18n.t(textKey);
export const translate = (key: keyof typeof en, language?: AvailableLanguages) => {
return i18n.t(key, {
lng: language
});
}
export { Translate };
+1 -1
View File
@@ -3,7 +3,7 @@ import Model from "./Model";
import UserSettings from "./UserSettings";
interface User extends Model {
username: string;
name: string;
email: string;
xp: number;
premium: boolean;
+5 -2
View File
@@ -1,6 +1,6 @@
import React from "react";
import { useDispatch } from '../state/Store';
import { translate } from "../i18n/i18n";
import { Translate, translate } from "../i18n/i18n";
import API from "../API";
import { setAccessToken } from "../state/UserSlice";
import { Center, Button, Text } from 'native-base';
@@ -33,13 +33,16 @@ const AuthenticationView = () => {
return (
<Center style={{ flex: 1 }}>
<Text><Translate translationKey='welcome'/></Text>
{mode === "signin"
? <SigninForm onSubmit={(username, password) => hanldeSignin(username, password, (accessToken) => dispatch(setAccessToken(accessToken)))} />
: <SignupForm onSubmit={(username, password, email) => handleSignup(username, password, email, (accessToken) => dispatch(setAccessToken(accessToken)))} />
}
{ mode ==="signin" && <Button variant="outline" marginTop={5} colorScheme="error" >{translate("forgottenPassword")}</Button> }
<Button variant='outline' marginTop={5} colorScheme='primary' onPress={() => setMode(mode === "signin" ? "signup" : "signin")}>
<Text>{translate(mode === "signin" ? "signUp" : "signIn")}</Text>
<Text>
<Translate translationKey={mode === "signin" ? "signUp" : "signIn"}/>
</Text>
</Button>
</Center>
);
+20 -10
View File
@@ -6,7 +6,7 @@ import { Box, ScrollView, Flex, useBreakpointValue, Text, VStack, Progress, Butt
import { useNavigation } from "@react-navigation/native";
import SongCardGrid from '../components/SongCardGrid';
import CompetenciesTable from '../components/CompetenciesTable'
import { translate } from "../i18n/i18n";
import { Translate } from "../i18n/i18n";
const ProgressBar = ({ xp }: { xp: number}) => {
const level = Math.floor(xp / 1000);
@@ -16,11 +16,17 @@ const ProgressBar = ({ xp }: { xp: number}) => {
return (
<VStack alignItems={'center'}>
<Text>{`${translate('level')} ${level}`}</Text>
<Text>
<Translate translationKey='level' format={(id) => `${id} ${level}`}/>
</Text>
<Box w="90%" maxW="400">
<Progress value={progessValue} mx="4" />
</Box>
<Text>{xp} / {nextLevelThreshold} {translate('levelProgress')}</Text>
<Text>
<Translate translationKey='levelProgress'
format={(levelProgress) => `${xp} / ${nextLevelThreshold} ${levelProgress}`}
/>
</Text>
</VStack>
);
}
@@ -46,7 +52,9 @@ const HomeView = () => {
return <ScrollView>
<Box style={{ display: 'flex', padding: 30 }}>
<Box textAlign={ screenSize == 'small' ? 'center' : undefined } style={{ flexDirection, justifyContent: 'center', display: 'flex' }}>
<Text fontSize="xl" flex={screenSize == 'small' ? 1 : 2}>{`${translate('welcome')} ${userQuery.data.username}!`} </Text>
<Text fontSize="xl" flex={screenSize == 'small' ? 1 : 2}>
<Translate translationKey="welcome" format={(welcome) => `${welcome} ${userQuery.data.name}!`}/>
</Text>
<Box flex={1}>
<ProgressBar xp={userQuery.data.xp}/>
</Box>
@@ -55,7 +63,7 @@ const HomeView = () => {
<Box paddingY={5} style={{ flexDirection }}>
<Box flex={2}>
<SongCardGrid
heading={translate('goNextStep')}
heading={<Translate translationKey='goNextStep'/>}
songs={nextStepQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
@@ -68,7 +76,7 @@ const HomeView = () => {
<Flex style={{ flexDirection }}>
<Box flex={1} paddingY={5}>
<Heading>{translate('mySkillsToImprove')}</Heading>
<Heading><Translate translationKey='mySkillsToImprove'/></Heading>
<Box padding={5}>
<CompetenciesTable {...skillsQuery.data}/>
</Box>
@@ -76,7 +84,7 @@ const HomeView = () => {
<Box flex={1} padding={5}>
<SongCardGrid
heading={translate('recentlyPlayed')}
heading={<Translate translationKey='recentlyPlayed'/>}
maxItemPerRow={2}
songs={playHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
@@ -96,11 +104,11 @@ const HomeView = () => {
<Box flex="2" padding={5}>
<Box style={{ flexDirection: 'row', justifyContent:'center' }}>
<Button backgroundColor={theme.colors.secondary[600]} rounded={"full"} size="sm" onPress={() => navigation.navigate('Search')} >{translate('search')}</Button>
<Button backgroundColor={theme.colors.secondary[600]} rounded={"full"} size="sm" onPress={() => navigation.navigate('Search')} ><Translate translationKey='search'/></Button>
</Box>
<SongCardGrid
maxItemPerRow={2}
heading={translate('lastSearched')}
heading={<Translate translationKey='lastSearched'/>}
songs={searchHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
@@ -113,7 +121,9 @@ const HomeView = () => {
</Box>
</Box>
<Box style={{ flexDirection: 'row', justifyContent:'center' }}>
<Button backgroundColor={theme.colors.primary[600]} rounded={"full"} size="sm" onPress={() => navigation.navigate('Settings')} >{translate('settingsBtn')}</Button>
<Button backgroundColor={theme.colors.primary[600]} rounded={"full"} size="sm" onPress={() => navigation.navigate('Settings')} >
<Translate translationKey='settingsBtn'/>
</Button>
</Box>
</VStack>
</Box>
+29 -22
View File
@@ -6,7 +6,7 @@ import { unsetAccessToken } from '../state/UserSlice';
import { useDispatch } from "react-redux";
import { useSelector } from '../state/Store';
import { useLanguage } from "../state/LanguageSlice";
import i18n, { AvailableLanguages, DefaultLanguage, translate } from "../i18n/i18n";
import { AvailableLanguages, DefaultLanguage, translate, Translate } from "../i18n/i18n";
const SettingsStack = createNativeStackNavigator();
@@ -16,31 +16,31 @@ const MainView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Button variant='ghost' onPress={() => navigation.navigate('Preferences')}>
{ translate('prefBtn')}
<Translate translationKey='prefBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('Notifications')}>
{ translate('notifBtn')}
<Translate translationKey='notifBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('Privacy')}>
{ translate('privBtn')}
<Translate translationKey='privBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('ChangePassword')}>
{ translate('changepasswdBtn')}
<Translate translationKey='changepasswdBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('ChangeEmail')}>
{ translate('changeemailBtn')}
<Translate translationKey='changeemailBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('GoogleAccount')}>
{ translate('googleacctBtn')}
<Translate translationKey='googleacctBtn'/>
</Button>
<Button variant='ghost' onPress={() => dispatch(unsetAccessToken())} >
{ translate('signoutBtn')}
<Translate translationKey='signoutBtn'/>
</Button>
</Center>
)
@@ -52,9 +52,12 @@ const PreferencesView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Heading style={{ textAlign: "center" }}>{ translate('prefBtn')}</Heading>
<Button onPress={() => navigation.navigate('Main')} style={{ margin: 10}}>{ translate('backBtn') }</Button>
<Heading style={{ textAlign: "center" }}>
<Translate translationKey='prefBtn'/>
</Heading>
<Button onPress={() => navigation.navigate('Main')} style={{ margin: 10}}>
<Translate translationKey='backBtn'/>
</Button>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Select selectedValue={undefined}
@@ -62,9 +65,9 @@ const PreferencesView = ({navigation}) => {
style={{ alignSelf: 'center'}}
// onValueChange={(itemValue, itemIndex) => switch themes}
>
<Select.Item label={ translate('dark') } value='dark'/>
<Select.Item label={ translate('light') } value='light'/>
<Select.Item label={ translate('system') } value='system'/>
<Select.Item label={translate('dark')} value='dark'/>
<Select.Item label={translate('light')} value='light'/>
<Select.Item label={translate('system')} value='system'/>
</Select>
</View>
@@ -131,24 +134,24 @@ const NotificationsView = ({navigation}) => {
return (
<Center style={{ flex: 1, justifyContent: 'center' }}>
<Heading style={{ textAlign: "center" }}>{ translate('notifBtn')}</Heading>
<Button style={{ margin: 10}} onPress={() => navigation.navigate('Main')} >{ translate('backBtn') }</Button>
<Heading style={{ textAlign: "center" }}>
<Translate translationKey='notifBtn'/>
</Heading>
<Button style={{ margin: 10}} onPress={() => navigation.navigate('Main')} >
<Translate translationKey='backBtn'/>
</Button>
<View style={{margin: 20}} >
<Text style={{ textAlign: "center" }}>Push notifications</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Email notifications</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Training reminder</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>New songs</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/>
@@ -160,9 +163,13 @@ const NotificationsView = ({navigation}) => {
const PrivacyView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Heading style={{ textAlign: "center" }}>{ translate('privBtn')}</Heading>
<Heading style={{ textAlign: "center" }}>
<Translate translationKey='privBtn'/>
</Heading>
<Button onPress={() => navigation.navigate('Main')} style={{ margin: 10 }}>{ translate('backBtn') }</Button>
<Button onPress={() => navigation.navigate('Main')} style={{ margin: 10 }}>
<Translate translationKey='backBtn'/>
</Button>
<View style={{margin: 20}} >
<Text style={{ textAlign: "center" }}>Data Collection</Text>
+17 -8
View File
@@ -3,10 +3,9 @@ import { Button, Divider, Box, Center, Image, Text, VStack, PresenceTransition,
import { useQuery } from 'react-query';
import LoadingComponent from "../components/Loading";
import React, { useEffect, useState } from "react";
import { translate } from "../i18n/i18n";
import { Translate, translate } from "../i18n/i18n";
import formatDuration from "format-duration";
import { Ionicons } from '@expo/vector-icons';
import { useSelector } from "../state/Store";
import API from "../API";
interface SongLobbyProps {
@@ -39,19 +38,29 @@ const SongLobbyView = () => {
<Box style={{ flex: 0.5 }}/>
<Box style={{ flex: 3, padding: 10, flexDirection: 'column', justifyContent: 'space-between' }}>
<Box flex={1}>
<Text bold fontSize='lg'>{songQuery.data!.name}</Text>
<Text>{'3:20'} - {translate('level')} { chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / chaptersQuery.data!.length }</Text>
<Button width='auto' rightIcon={<Icon as={Ionicons} name="play-outline"/>}>{ translate('playBtn') }</Button>
<Text bold fontSize='lg'>{songQuery.data!.title}</Text>
<Text>
<Translate translationKey='level'
format={(level) => `3:20 - ${level} - ${ chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / chaptersQuery.data!.length }`}
/>
</Text>
<Button width='auto' rightIcon={<Icon as={Ionicons} name="play-outline"/>}>
<Translate translationKey='playBtn'/>
</Button>
</Box>
</Box>
</Box>
<Box style={{ flexDirection: 'row', justifyContent: 'space-between', padding: 30}}>
<Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize='lg'>{translate('bestScore') }</Text>
<Text bold fontSize='lg'>
<Translate translationKey='bestScore'/>
</Text>
<Text>{scoresQuery.data!.sort()[0]?.score}</Text>
</Box>
<Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize='lg'>{translate('lastScore') }</Text>
<Text bold fontSize='lg'>
<Translate translationKey='lastScore'/>
</Text>
<Text>{scoresQuery.data!.slice(-1)[0]!.score}</Text>
</Box>
</Box>
@@ -62,7 +71,7 @@ const SongLobbyView = () => {
onPress={() => setChaptersOpen(!chaptersOpen)}
endIcon={<Icon as={Ionicons} name={chaptersOpen ? "chevron-up-outline" : "chevron-down-outline"}/>}
>
{translate('chapters')}
<Translate translationKey='chapters'/>
</Button>
</Box>
<PresenceTransition visible={chaptersOpen} initial={{ opacity: 0 }}>