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);
......@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FlatList, Alert, Image, ScrollView, Text, View, RefreshControl, Button, TouchableHighlight, Platform, Linking } from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import Moment from 'moment';
import styles, { memberSize } from './style/event';
......@@ -17,17 +18,16 @@ import * as pizzaActions from '../actions/pizza';
class Event extends Component {
cancelPrompt = (pk) => {
const cancelDeadlineDate = new Date(this.props.data.cancel_deadline);
let message = 'Are you sure you want to cancel your registration?';
let message = this.props.t('Are you sure you want to cancel your registration?');
if (this.props.data.cancel_deadline !== null && cancelDeadlineDate <= new Date()) {
message = 'The deadline has passed, are you sure you want to cancel your registration and'
+ ` pay the full costs of €${this.props.data.fine}? You will not be able to undo this!`;
message = this.props.t('The deadline has passed, are you sure you want to cancel your registration and pay the full costs of €{{ fine }}? You will not be able to undo this!', { fine: this.props.data.fine });
}
return Alert.alert(
'Cancel registration?',
this.props.t('Cancel registration?'),
message,
[
{ text: 'Dismiss' },
{ text: 'Cancel registration', onPress: () => this.props.cancel(pk) },
{ text: this.props.t('No') },
{ text: this.props.t('Yes'), onPress: () => this.props.cancel(pk) },
],
);
};
......@@ -40,25 +40,25 @@ class Event extends Component {
infoTexts.push(
<View key="start-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="start-title">Van:</Text>
<Text style={styles.infoText} key="start-title">{this.props.t('From')}:</Text>
<Text style={styles.infoValueText} key="start-value">{startDate}</Text>
</View>,
);
infoTexts.push(
<View key="end-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="end-title">Tot:</Text>
<Text style={styles.infoText} key="end-title">{this.props.t('Until')}:</Text>
<Text style={styles.infoValueText} key="end-value">{endDate}</Text>
</View>,
);
infoTexts.push(
<View key="loc-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="loc-title">Locatie:</Text>
<Text style={styles.infoText} key="loc-title">{this.props.t('Location')}:</Text>
<Text style={styles.infoValueText} key="loc-value">{data.location}</Text>
</View>,
);
infoTexts.push(
<View key="price-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="price-title">Prijs:</Text>
<Text style={styles.infoText} key="price-title">{this.props.t('Price')}:</Text>
<Text style={styles.infoValueText} key="price-value">{data.price}</Text>
</View>,
);
......@@ -69,25 +69,25 @@ class Event extends Component {
infoTexts.push(
<View key="registrationend-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="registrationend-title">Aanmelddeadline:</Text>
<Text style={styles.infoText} key="registrationend-title">{this.props.t('Registration deadline')}:</Text>
<Text style={styles.infoValueText} key="registrationend-value">{registrationDeadline}</Text>
</View>,
);
infoTexts.push(
<View key="canceldeadline-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="canceldeadline-title">Afmelddeadline:</Text>
<Text style={styles.infoText} key="canceldeadline-title">{this.props.t('Cancellation deadline')}:</Text>
<Text style={styles.infoValueText} key="canceldeadline-value">{cancelDeadline}</Text>
</View>,
);
let participantsText = `${data.num_participants} aanmeldingen`;
let participantsText = `${data.num_participants} ${this.props.t('registrations')}`;
if (data.max_participants) {
participantsText += ` (${data.max_participants} max)`;
participantsText += ` (${data.max_participants} ${this.props.t('max')})`;
}
infoTexts.push(
<View key="participants-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="participants-title">Aantal aanmeldingen:</Text>
<Text style={styles.infoText} key="participants-title">{this.props.t('Number of registrations')}:</Text>
<Text style={styles.infoValueText} key="participants-value">{participantsText}</Text>
</View>,
);
......@@ -95,20 +95,20 @@ class Event extends Component {
if (data.user_registration) {
let registrationState;
if (data.user_registration.is_late_cancellation) {
registrationState = 'Je bent afgemeld na de afmelddeadline';
registrationState = this.props.t('Your registration is cancelled after the cancellation deadline');
} else if (data.user_registration.is_cancelled) {
registrationState = 'Je bent afgemeld';
registrationState = this.props.t('Your registration is cancelled');
} else if (data.user_registration.queue_position === null) {
registrationState = 'Je bent aangemeld';
registrationState = this.props.t('You are registered');
} else if (data.user_registration.queue_position > 0) {
registrationState = `Wachtlijst positie ${data.user_registration.queue_position}`;
registrationState = this.props.t('Queue position {{pos}}', { pos: data.user_registration.queue_position });
} else {
registrationState = 'Je bent afgemeld';
registrationState = this.props.t('Your registration is cancelled');
}
infoTexts.push(
<View key="status-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="status-title">Aanmeldstatus:</Text>
<Text style={styles.infoText} key="status-title">{this.props.t('Registration status')}:</Text>
<Text style={styles.infoValueText} key="status-value">{registrationState}</Text>
</View>,
);
......@@ -149,23 +149,26 @@ class Event extends Component {
const afterCancelDeadline = event.cancel_deadline !== null && cancelDeadlineDate <= nowDate;
if (!regRequired) {
text = 'Geen aanmelding vereist.';
text = this.props.t('No registration required.');
if (event.no_registration_message) {
text = event.no_registration_message;
}
} else if (!regStarted) {
const registrationStart = Moment(event.registration_start).format('D MMM YYYY, HH:mm');
text = `Aanmelden opent ${registrationStart}.`;
const registrationStart = Moment(event.registration_start).format('D MMM YYYY, HH:m');
text = this.props.t('Registration will open {{start}}', { start: registrationStart });
} else if (!regAllowed) {
text = 'Aanmelden is niet meer mogelijk.';
text = this.props.t('Registration is not possible anymore.');
}
if (afterCancelDeadline) {
if (text.length > 0) {
text += ' ';
}
text += `Afmelden is niet meer mogelijk zonder de volledige kosten van €${event.fine} te ` +
'betalen. Let op: je kunt je hierna niet meer aanmelden.';
text += this.props.t(
'Cancellation isn\'t possible anymore without having to pay the full ' +
'costs of €{{fine}}. Also note that you will be unable to re-register.',
{ fine: event.fine },
);
}
if (text.length > 0) {
......@@ -188,7 +191,7 @@ class Event extends Component {
if (regAllowed) {
if (event.user_registration === null || event.user_registration.is_cancelled) {
const text = event.max_participants && event.max_participants <= event.num_participants ?
'Zet me op de wachtlijst' : 'Aanmelden';
this.props.t('Put me on the waiting list') : this.props.t('Register');
return (
<View style={styles.registrationActions}>
<Button
......@@ -206,13 +209,13 @@ class Event extends Component {
<View style={styles.registrationActions}>
<Button
color={colors.magenta}
title="Aanmelding bijwerken"
title={this.props.t('Update registration')}
onPress={() => this.props.fields(event.user_registration.pk)}
/>
<View style={styles.secondButtonMargin}>
<Button
color={colors.magenta}
title="Afmelden"
title={this.props.t('Cancel registration')}
onPress={() => this.cancelPrompt(event.user_registration.pk)}
/>
</View>
......@@ -221,7 +224,7 @@ class Event extends Component {
}
return (
<View style={styles.registrationActions}>
<Button color={colors.magenta} title="Afmelden" onPress={() => this.cancelPrompt(event.user_registration.pk)} />
<Button color={colors.magenta} title={this.props.t('Cancel registration')} onPress={() => this.cancelPrompt(event.user_registration.pk)} />
</View>
);
}
......@@ -235,7 +238,7 @@ class Event extends Component {
return (
<View>
<View style={styles.divider} />
<Text style={styles.registrationsTitle}>Aanmeldingen</Text>
<Text style={styles.registrationsTitle}>{this.props.t('Registrations')}</Text>
<FlatList
numColumns={3}
data={this.props.registrations}
......@@ -295,7 +298,7 @@ class Event extends Component {
{this.eventInfo(this.props.data)}
<View style={styles.divider} />
<Text style={styles.descText}>{this.props.data.description}</Text>
{this.registrationsGrid(this.props.registrations)}
{this.registrationsGrid(this.props.registrations, this.props.t)}
</ScrollView>
);
}
......@@ -310,7 +313,7 @@ class Event extends Component {
/>
)}
>
<ErrorScreen message="Could not load the event..." />
<ErrorScreen message={this.props.t('Could not load the event...')} />
</ScrollView>
);
}
......@@ -364,6 +367,7 @@ Event.propTypes = {
fields: PropTypes.func.isRequired,
openMaps: PropTypes.func.isRequired,
retrievePizzaInfo: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
......@@ -382,4 +386,4 @@ const mapDispatchToProps = dispatch => ({
retrievePizzaInfo: () => dispatch(pizzaActions.retrievePizzaInfo()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Event);
export default connect(mapStateToProps, mapDispatchToProps)(translate('event')(Event));
......@@ -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]}>