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:
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user