Commit af963183 authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg Committed by Sébastiaan Versteeg

Add components translations

parent 99cee02f
image: node:6
image: node:8
variables:
GRADLE_USER_HOME: $CI_PROJECT_DIR/.gradle
......
......@@ -6,6 +6,8 @@ import {
global.fetch = jest.fn().mockReturnValue(
Promise.resolve({ status: 200, json: () => 'responseJson' }));
jest.mock('react-native-locale-detector', () => 'en');
describe('url helper', () => {
beforeEach(() => {
});
......@@ -23,27 +25,38 @@ describe('url helper', () => {
it('should do a fetch request', () => {
expect.assertions(2);
return apiRequest('route', 'fetchOpts', null)
return apiRequest('route', {}, null)
.then((response) => {
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/`, 'fetchOpts');
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/`,
{ headers: { 'Accept-Language': 'en' } });
expect(response).toEqual('responseJson');
});
});
it('should do a fetch request with params', () => {
expect.assertions(1);
return apiRequest('route', 'fetchOpts', {
return apiRequest('route', {}, {
params: 'value',
}).then(() => {
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/?params=value`, 'fetchOpts');
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/?params=value`,
{ headers: { 'Accept-Language': 'en' } });
});
});
it('should do a fetch request with headers', () => {
expect.assertions(1);
return apiRequest('route', { headers: { Authorization: 'Token abc' } }, null).then(() => {
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/`,
{ headers: { 'Accept-Language': 'en', Authorization: 'Token abc' } });
});
});
it('should generate the url parameters', () => {
expect.assertions(2);
return apiRequest('route', 'fetchOpts', null)
return apiRequest('route', {}, null)
.then((response) => {
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/`, 'fetchOpts');
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/`,
{ headers: { 'Accept-Language': 'en' } });
expect(response).toEqual('responseJson');
});
});
......@@ -52,7 +65,7 @@ describe('url helper', () => {
expect.assertions(1);
const response = { status: 404, json: () => 'responseJson' };
global.fetch.mockReturnValue(Promise.resolve(response));
return apiRequest('route', 'fetchOpts', null)
return apiRequest('route', {}, null)
.catch(e => expect(e).toEqual(new ServerError('Invalid status code: 404', response)));
});
......@@ -60,7 +73,7 @@ describe('url helper', () => {
expect.assertions(1);
const response = { status: 204, json: () => 'responseJson' };
global.fetch.mockReturnValue(Promise.resolve(response));
return apiRequest('route', 'fetchOpts', null)
return apiRequest('route', {}, null)
.then(res => expect(res).toEqual({}));
});
});
......@@ -2,7 +2,9 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Text, View, SectionList, ScrollView, RefreshControl } from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import Moment from 'moment';
import locale from 'react-native-locale-detector';
import * as calendarActions from '../actions/calendar';
import EventCard from './EventCard';
......@@ -28,7 +30,7 @@ const addEventToSection = (sections, date, event) => {
if (!(day in sections[sectionKey].data)) {
sections[sectionKey].data[day] = {
dayNumber: day,
dayOfWeek: date.format('dd'),
dayOfWeek: locale.startsWith('nl') ? date.format('dd') : date.format('ddd'),
events: [],
};
}
......@@ -41,7 +43,7 @@ const addEventToSection = (sections, date, event) => {
* Any event that spans multiple days will be split into separate events.
* The list of sections is sorted at the end.
*/
const eventListToSections = (eventList) => {
const eventListToSections = (eventList, t) => {
const sections = {};
for (let i = 0; i < eventList.length; i += 1) {
const start = Moment(eventList[i].start);
......@@ -57,7 +59,7 @@ const eventListToSections = (eventList) => {
// Add start day
addEventToSection(sections, start, {
...eventList[i],
title: `${eventList[i].title} (dag 1/${daySpan})`,
title: `${eventList[i].title} (${t('day')} 1/${daySpan})`,
end: null,
});
......@@ -67,13 +69,13 @@ const eventListToSections = (eventList) => {
...eventList[i],
start: null,
end: null,
title: `${eventList[i].title} (dag ${j}/${daySpan})`,
title: `${eventList[i].title} (${t('day')} ${j}/${daySpan})`,
});
}
// Add end day
addEventToSection(sections, end, {
...eventList[i],
title: `${eventList[i].title} (dag ${daySpan}/${daySpan})`,
title: `${eventList[i].title} (${t('day')} ${daySpan}/${daySpan})`,
start: null,
});
}
......@@ -135,7 +137,7 @@ class Calendar extends Component {
/>
)}
>
<ErrorScreen message="Sorry! We couldn't load any data." />
<ErrorScreen message={this.props.t('Sorry, we couldn\'t load any data.')} />
</ScrollView>
);
} else if (this.props.eventList.length === 0) {
......@@ -149,7 +151,7 @@ class Calendar extends Component {
/>
)}
>
<ErrorScreen message="No events found!" />
<ErrorScreen message={this.props.t('No events found!')} />
</ScrollView>
);
}
......@@ -161,7 +163,7 @@ class Calendar extends Component {
renderSectionHeader={
itemHeader => <Text style={styles.sectionHeader}>{itemHeader.section.key}</Text>
}
sections={eventListToSections(this.props.eventList)}
sections={eventListToSections(this.props.eventList, this.props.t)}
keyExtractor={item => item.dayNumber}
stickySectionHeadersEnabled
onRefresh={this.handleRefresh}
......@@ -189,6 +191,7 @@ Calendar.propTypes = {
loading: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
refresh: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
......@@ -201,4 +204,4 @@ const mapDispatchToProps = dispatch => ({
refresh: () => dispatch(calendarActions.refresh()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Calendar);
export default connect(mapStateToProps, mapDispatchToProps)(translate('calendar')(Calendar));
import React from 'react';
import { Image, Text, View } from 'react-native';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import styles from './style/errorScreen';
......@@ -15,12 +16,13 @@ const ErrorScreen = props => (
style={styles.image}
/>
<Text style={styles.text}>{props.message}</Text>
<Text style={styles.text}>Try again later.</Text>
<Text style={styles.text}>{props.t('Try again later.')}</Text>
</View>
);
ErrorScreen.propTypes = {
message: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default ErrorScreen;
export default translate('errorScreen')(ErrorScreen);
This diff is collapsed.
......@@ -2,20 +2,20 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, TouchableHighlight, Linking } from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import Moment from 'moment';
import 'moment/locale/nl';
import * as eventActions from '../actions/event';
import styles from './style/eventCard';
const getEventInfo = (event) => {
const getEventInfo = (event, t) => {
if (event.start === null && event.end === null) {
return event.location;
} else if (event.start === null) {
return `Tot ${Moment(event.end).format('HH:mm')} | ${event.location}`;
return `${t('Until')} ${Moment(event.end).format('HH:mm')} | ${event.location}`;
} else if (event.end === null) {
return `Vanaf ${Moment(event.start).format('HH:mm')} | ${event.location}`;
return `${t('From')} ${Moment(event.start).format('HH:mm')} | ${event.location}`;
}
return `${Moment(event.start).format('HH:mm')} - ${Moment(event.end).format('HH:mm')} | ${event.location}`;
};
......@@ -36,7 +36,7 @@ const EventCard = props => (
{props.event.title}
</Text>
<Text style={[styles.eventInfo, props.event.partner ? styles.partnerEventInfo : null]}>
{getEventInfo(props.event)}
{getEventInfo(props.event, props.t)}
</Text>
</View>
</TouchableHighlight>
......@@ -57,6 +57,7 @@ EventCard.propTypes = {
url: PropTypes.string,
}).isRequired,
loadEvent: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
const mapDispatchToProps = dispatch => ({
......@@ -69,4 +70,4 @@ const mapDispatchToProps = dispatch => ({
},
});
export default connect(() => ({}), mapDispatchToProps)(EventCard);
export default connect(() => ({}), mapDispatchToProps)(translate('eventCard')(EventCard));
......@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, TouchableOpacity, TouchableHighlight } from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import Moment from 'moment';
import * as actions from '../actions/event';
......@@ -45,13 +46,13 @@ const EventDetailCard = props => (
style={styles.description}
>{props.event.description}</Text>
<View style={styles.buttonList}>
<Text style={[styles.moreInfo, styles.button]}>MEER INFO</Text>
<Text style={[styles.moreInfo, styles.button]}>{props.t('MORE INFO')}</Text>
{props.event.pizza ? (
<TouchableOpacity
onPress={() => props.retrievePizzaInfo(props.token)}
style={styles.button}
>
<Text style={styles.orderPizza}>PIZZA</Text>
<Text style={styles.orderPizza}>{props.t('PIZZA')}</Text>
</TouchableOpacity>
) : null}
</View>
......@@ -82,6 +83,7 @@ EventDetailCard.propTypes = {
loadEvent: PropTypes.func.isRequired,
token: PropTypes.string.isRequired,
retrievePizzaInfo: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
......@@ -93,4 +95,4 @@ const mapDispatchToProps = dispatch => ({
retrievePizzaInfo: () => dispatch(retrievePizzaInfo()),
});
export default connect(mapStateToProps, mapDispatchToProps)(EventDetailCard);
export default connect(mapStateToProps, mapDispatchToProps)(translate(['eventDetailCard'])(EventDetailCard));
......@@ -11,6 +11,7 @@ import {
Keyboard,
} from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import styles from './style/login';
import { url } from '../url';
......@@ -29,7 +30,7 @@ class Login extends Component {
}
render() {
const { login } = this.props;
const { login, t } = this.props;
return (
<KeyboardAvoidingView
style={styles.rootWrapper}
......@@ -43,13 +44,13 @@ class Login extends Component {
<View>
<TextInput
style={styles.input}
placeholder="Gebruikersnaam"
placeholder={t('Username')}
autoCapitalize="none"
onChangeText={username => this.setState({ username })}
/>
<TextInput
style={styles.input}
placeholder="Wachtwoord"
placeholder={t('Password')}
autoCapitalize="none"
secureTextEntry
onChangeText={password => this.setState({ password })}
......@@ -62,10 +63,10 @@ class Login extends Component {
style={styles.blackbutton} onPress={() =>
login(this.state.username, this.state.password)}
>
<Text style={styles.loginText}>INLOGGEN</Text>
<Text style={styles.loginText}>{t('LOGIN')}</Text>
</TouchableHighlight>
<Text style={styles.forgotpass} onPress={() => Linking.openURL(`${url}/password_reset/`)}>
Wachtwoord vergeten?
{t('Forgot password?')}
</Text>
</DismissKeyboardView>
</KeyboardAvoidingView>
......@@ -75,6 +76,7 @@ class Login extends Component {
Login.propTypes = {
login: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
const mapStateToProps = state => state.session;
......@@ -85,4 +87,4 @@ const mapDispatchToProps = dispatch => ({
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Login);
export default connect(mapStateToProps, mapDispatchToProps)(translate(['login'])(Login));
......@@ -2,9 +2,9 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { View, Text, ScrollView, RefreshControl, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Moment from 'moment';
import 'moment/locale/nl';
import LoadingScreen from './LoadingScreen';
import ErrorScreen from './ErrorScreen';
......@@ -24,7 +24,7 @@ class Pizza extends Component {
getEventInfo = (title, subtitle) => (
<View style={styles.eventInfo}>
<Text style={styles.title}>Order pizza for {title}</Text>
<Text style={styles.title}>{this.props.t('Order pizza for {{title}}', { title })}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
</View>
);
......@@ -43,7 +43,7 @@ class Pizza extends Component {
</View>
);
}
return <Text style={styles.header}>You did not place an order.</Text>;
return <Text style={styles.header}>{this.props.t('You did not place an order.')}</Text>;
};
getOrder = (order, pizzaList, hasEnded) => {
......@@ -58,7 +58,8 @@ class Pizza extends Component {
style={[styles.orderStatus, order.paid ? styles.paidStatus : styles.notPaidStatus]}
>
<Text style={styles.orderStatusText}>
The order has {order.paid || 'not yet '}been paid for.
{order.paid && this.props.t('The order has been paid for.')}
{!order.paid && this.props.t('The order has not yet been paid for.')}
</Text>
</View>
<View style={[styles.pizzaContainer, styles.orderedPizzaContainer]}>
......@@ -90,7 +91,7 @@ class Pizza extends Component {
getPizzaList = (pizzaList, hasOrder) => (
<View style={styles.section}>
{hasOrder && (
<Text style={styles.header}>Changing your order</Text>
<Text style={styles.header}>{this.props.t('Changing your order')}</Text>
)}
<View style={[styles.card, styles.pizzaList]}>
{pizzaList.map(pizza => (
......@@ -140,7 +141,7 @@ class Pizza extends Component {
}
contentContainerStyle={styles.content}
>
<ErrorScreen message="Sorry! We couldn't load any data." />
<ErrorScreen message={this.props.t('Sorry! We couldn\'t load any data.')} />
</ScrollView>
);
} else if (!this.props.event) {
......@@ -156,7 +157,7 @@ class Pizza extends Component {
>
<Text
style={styles.title}
>There is currently no event for which you can order food.</Text>
>{this.props.t('There is currently no event for which you can order food.')}</Text>
</ScrollView>
);
}
......@@ -170,11 +171,11 @@ class Pizza extends Component {
let subtitle;
if (inFuture) {
subtitle = `It will be possible to order from ${start.format('HH:mm')}`;
subtitle = this.props.t(`It will be possible to order from ${start.format('HH:mm')}`);
} else if (hasEnded) {
subtitle = `It was possible to order until ${end.format('HH:mm')}`;
subtitle = this.props.t(`It was possible to order until ${end.format('HH:mm')}`);
} else {
subtitle = `You can order until ${end.format('HH:mm')}`;
subtitle = this.props.t(`You can order until ${end.format('HH:mm')}`);
}
return (
......@@ -226,6 +227,7 @@ Pizza.propTypes = {
retrievePizzaInfo: PropTypes.func.isRequired,
cancelOrder: PropTypes.func.isRequired,
orderPizza: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
Pizza.defaultProps = {
......@@ -248,4 +250,4 @@ const mapDispatchToProps = dispatch => ({
orderPizza: (pk, hasOrder) => dispatch(orderPizza(pk, hasOrder)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Pizza);
export default connect(mapStateToProps, mapDispatchToProps)(translate('pizza')(Pizza));
......@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Linking, ScrollView, Text, View, Animated, TouchableOpacity, Platform, StatusBar, ImageBackground } from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import Icon from 'react-native-vector-icons/MaterialIcons';
import LinearGradient from 'react-native-linear-gradient';
import Moment from 'moment';
......@@ -16,8 +17,8 @@ import { back } from '../actions/navigation';
import { STATUSBAR_HEIGHT } from './style/standardHeader';
import styles, { HEADER_MIN_HEIGHT, HEADER_MAX_HEIGHT, HEADER_SCROLL_DISTANCE } from './style/profile';
const getDescription = profile => ([
<Text style={[styles.sectionHeader, styles.marginTop]} key="title">{`Over ${profile.display_name}`}</Text>,
const getDescription = (profile, t) => ([
<Text style={[styles.sectionHeader, styles.marginTop]} key="title">{`${t('About')} ${profile.display_name}`}</Text>,
<View style={styles.card} key="content">
<Text
style={[
......@@ -25,26 +26,26 @@ const getDescription = profile => ([
styles.item,
styles.profileText,
!profile.profile_description && styles.italics]}
>{profile.profile_description || 'Dit lid heeft nog geen beschrijving geschreven'}</Text>
>{profile.profile_description || t('This member has not written a description yet.')}</Text>
</View>,
]);
const getPersonalInfo = (profile) => {
const getPersonalInfo = (profile, t) => {
const profileInfo = {
starting_year: {
title: 'Cohort',
title: t('Cohort'),
display: x => x,
},
programme: {
title: 'Studie',
display: x => (x === 'computingscience' ? 'Computing science' : 'Information sciences'),
title: t('Study programme'),
display: x => (x === 'computingscience' ? t('Computing science') : t('Information sciences')),
},
website: {
title: 'Website',
title: t('Website'),
display: x => x,
},
birthday: {
title: 'Verjaardag',
title: t('Birthday'),
display: x => Moment(x).format('D MMMM YYYY'),
},
};
......@@ -61,7 +62,7 @@ const getPersonalInfo = (profile) => {
if (profileData) {
return [
<Text style={styles.sectionHeader} key="title">Persoonlijke gegevens</Text>,
<Text style={styles.sectionHeader} key="title">{t('Personal information')}</Text>,
<View style={styles.card} key="content">
{profileData.map((item, i) => (
<View style={[styles.item, i !== 0 && styles.borderTop]} key={item.title}>
......@@ -80,10 +81,10 @@ const getPersonalInfo = (profile) => {
return <View />;
};
const getAchievements = (profile) => {
const getAchievements = (profile, t) => {
if (profile.achievements.length) {
return [
<Text style={styles.sectionHeader} key="title">Verdiensten voor Thalia</Text>,
<Text style={styles.sectionHeader} key="title">{t('Achievements for Thalia')}</Text>,
<View style={styles.card} key="content">
{profile.achievements.map((achievement, i) => (
<View style={[styles.item, i !== 0 && styles.borderTop]} key={achievement.name}>
......@@ -97,7 +98,7 @@ const getAchievements = (profile) => {
if (period.role) {
text = `${period.role}: `;
} else if (period.chair) {
text = 'Voorzitter: ';
text = `${t('Chair')}: `;
}
text += `${start} - ${end}`;
......@@ -233,7 +234,7 @@ class Profile extends Component {
return (
<View style={styles.container}>
<StandardHeader />
<ErrorScreen message="Sorry! We couldn't load any data." />
<ErrorScreen message={this.props.t('Sorry! We couldn\'t load any data.')} />
</View>
);
}
......@@ -294,6 +295,7 @@ Profile.propTypes = {
success: PropTypes.bool.isRequired,
back: PropTypes.func.isRequired,
hasLoaded: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
......@@ -306,4 +308,4 @@ const mapDispatchToProps = dispatch => ({
back: () => dispatch(back()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
export default connect(mapStateToProps, mapDispatchToProps)(translate('profile')(Profile));
import React, { Component } from 'react';
import { ActivityIndicator, Modal, View, ScrollView, Switch, TextInput, Text, Button } from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import styles from './style/registration';
......@@ -35,12 +36,12 @@ class Registration extends Component {
if (field.type === 'integer' && (value === '' || value === null || !value.match(/^-?\d+$/))) {
return {
isValid: false,
reason: 'This field is required and must be an integer.',
reason: this.props.t('This field is required and must be an integer.'),
};
} else if (field.type === 'text' && (value === '' || value === null)) {
return {
isValid: false,
reason: 'This field is required.',
reason: this.props.t('This field is required.'),
};
}
}
......@@ -67,7 +68,7 @@ class Registration extends Component {
render() {
if (this.props.status === 'failure') {
return <ErrorScreen message="Sorry! We couldn't load any data." />;
return <ErrorScreen message={this.props.t('Sorry! We couldn\'t load any data.')} />;
}
const keys = Object.keys(this.props.fields);
......@@ -126,7 +127,7 @@ class Registration extends Component {
})}
{this.props.status !== 'loading' && <View style={styles.buttonView}>
<Button
title="Aanpassen"
title={this.props.t('Save')}
color={colors.magenta}
onPress={() => this.props.update(this.props.registration, this.state)}
disabled={!this.isFormValid()}
......@@ -142,6 +143,7 @@ Registration.propTypes = {
fields: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
status: PropTypes.string.isRequired,
update: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
......@@ -154,4 +156,4 @@ const mapDispatchToProps = dispatch => ({
update: (registration, fields) => dispatch(registrationActions.update(registration, fields)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Registration);
export default connect(mapStateToProps, mapDispatchToProps)(translate('registration')(Registration));
......@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Alert, Text, View, Image, TouchableHighlight, ImageBackground } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { colors } from '../style';
import styles from './style/sidebar';
......@@ -14,10 +15,10 @@ import * as profileActions from '../actions/profile';
const background = require('../img/huygens.jpg');
const logoutPrompt = logout => () => Alert.alert(
'Log out?',
'Are you sure you want to log out?',
[{ text: 'Cancel' },
{ text: 'Log out', onPress: logout },
this.props.t('Log out?'),
this.props.t('Are you sure you want to log out?'),
[{ text: this.props.t('No') },
{ text: this.props.t('Yes'), onPress: logout },
],
);
......@@ -26,21 +27,21 @@ const Sidebar = (props) => {
{
onPress: () => props.navigate('welcome', true),
iconName: 'home',
text: 'Welkom',
text: props.t