Client - application translation (wip)

This commit is contained in:
Sam 2019-09-16 10:26:02 +02:00
parent 745d102ee2
commit 77bc32d4a5
33 changed files with 651 additions and 374 deletions

View File

@ -1,15 +1,18 @@
import React from 'react'
import { translateSports } from '../../utils/activities'
export default class ActivitiesFilter extends React.PureComponent {
render() {
const { loadActivities, sports, updateParams } = this.props
const { loadActivities, sports, t, updateParams } = this.props
const translatedSports = translateSports(sports, t)
return (
<div className="card">
<div className="card-body activity-filter">
<form onSubmit={event => event.preventDefault()}>
<div className="form-group">
<label>
From:
{t('activities:From')}:
<input
className="form-control col-md"
name="from"
@ -18,7 +21,7 @@ export default class ActivitiesFilter extends React.PureComponent {
/>
</label>
<label>
To:
{t('activities:To')}:
<input
className="form-control col-md"
name="to"
@ -29,14 +32,14 @@ export default class ActivitiesFilter extends React.PureComponent {
</div>
<div className="form-group">
<label>
Sport:
{t('common:Sport')}:
<select
className="form-control input-lg"
name="sport_id"
onChange={e => updateParams(e)}
>
<option value="" />
{sports.map(sport => (
{translatedSports.map(sport => (
<option key={sport.id} value={sport.id}>
{sport.label}
</option>
@ -46,7 +49,7 @@ export default class ActivitiesFilter extends React.PureComponent {
</div>
<div className="form-group">
<label>
Distance (km):
{t('activities:Distance')} (km):
<div className="container">
<div className="row">
<div className="col-5">
@ -59,7 +62,9 @@ export default class ActivitiesFilter extends React.PureComponent {
type="number"
/>
</div>
<div className="col-2 align-middle text-center">to</div>
<div className="col-2 align-middle text-center">
{t('common:to')}
</div>
<div className="col-5">
<input
className="form-control"
@ -76,7 +81,7 @@ export default class ActivitiesFilter extends React.PureComponent {
</div>
<div className="form-group">
<label>
Duration:
{t('activities:Duration')}:
<div className="container">
<div className="row">
<div className="col-5">
@ -89,7 +94,9 @@ export default class ActivitiesFilter extends React.PureComponent {
type="text"
/>
</div>
<div className="col-2 align-middle text-center">to</div>
<div className="col-2 align-middle text-center">
{t('common:to')}
</div>
<div className="col-5">
<input
className="form-control"
@ -106,7 +113,7 @@ export default class ActivitiesFilter extends React.PureComponent {
</div>
<div className="form-group">
<label>
Average speed (km/h):
{t('activities:Average speed')} (km/h):
<div className="container">
<div className="row">
<div className="col-5">
@ -119,7 +126,9 @@ export default class ActivitiesFilter extends React.PureComponent {
type="number"
/>
</div>
<div className="col-2 align-middle text-center">to</div>
<div className="col-2 align-middle text-center">
{t('common:to')}
</div>
<div className="col-5">
<input
className="form-control"
@ -136,7 +145,7 @@ export default class ActivitiesFilter extends React.PureComponent {
</div>
<div className="form-group">
<label>
Max speed (km/h):
{t('activities:Max. speed')} (km/h):
<div className="container">
<div className="row">
<div className="col-5">
@ -149,7 +158,9 @@ export default class ActivitiesFilter extends React.PureComponent {
type="number"
/>
</div>
<div className="col-2 align-middle text-center">to</div>
<div className="col-2 align-middle text-center">
{t('common:to')}
</div>
<div className="col-5">
<input
className="form-control"
@ -168,7 +179,7 @@ export default class ActivitiesFilter extends React.PureComponent {
className="btn btn-primary btn-lg btn-block"
onClick={() => loadActivities()}
type="submit"
value="Filter"
value={t('activities:Filter')}
/>
</form>
</div>

View File

@ -7,7 +7,7 @@ import { getDateWithTZ } from '../../utils'
export default class ActivitiesList extends React.PureComponent {
render() {
const { activities, sports, user } = this.props
const { activities, sports, t, user } = this.props
return (
<div className="card activity-card">
<div className="card-body">
@ -15,12 +15,12 @@ export default class ActivitiesList extends React.PureComponent {
<thead>
<tr>
<th scope="col" />
<th scope="col">Workout</th>
<th scope="col">Date</th>
<th scope="col">Distance</th>
<th scope="col">Duration</th>
<th scope="col">Ave. speed</th>
<th scope="col">Max. speed</th>
<th scope="col">{t('common:Workout')}</th>
<th scope="col">{t('activities:Date')}</th>
<th scope="col">{t('activities:Distance')}</th>
<th scope="col">{t('activities:Duration')}</th>
<th scope="col">{t('activities:Ave. speed')}</th>
<th scope="col">{t('activities:Max. speed')}</th>
</tr>
</thead>
<tbody>

View File

@ -1,10 +1,11 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import ActivitiesFilter from './ActivitiesFilter'
import ActivitiesList from './ActivitiesList'
import NoActivities from '../Common/NoActivities'
import { getOrUpdateData } from '../../actions'
import { getMoreActivities } from '../../actions/activities'
@ -40,6 +41,7 @@ class Activities extends React.Component {
loadMoreActivities,
message,
sports,
t,
user,
} = this.props
const { params } = this.state
@ -50,7 +52,7 @@ class Activities extends React.Component {
return (
<div>
<Helmet>
<title>FitTrackee - Workouts</title>
<title>FitTrackee - {t('common:Workouts')}</title>
</Helmet>
{message ? (
<code>{message}</code>
@ -61,6 +63,7 @@ class Activities extends React.Component {
<ActivitiesFilter
sports={sports}
loadActivities={() => loadActivities(params)}
t={t}
updateParams={e => this.setParams(e)}
/>
</div>
@ -68,6 +71,7 @@ class Activities extends React.Component {
<ActivitiesList
activities={activities}
sports={sports}
t={t}
user={user}
/>
{!paginationEnd && (
@ -82,16 +86,7 @@ class Activities extends React.Component {
}}
/>
)}
{activities.length === 0 && (
<div className="card text-center">
<div className="card-body">
No workouts.{' '}
<Link to={{ pathname: '/activities/add' }}>
Upload one !
</Link>
</div>
</div>
)}
{activities.length === 0 && <NoActivities t={t} />}
</div>
</div>
</div>
@ -101,19 +96,21 @@ class Activities extends React.Component {
}
}
export default connect(
state => ({
activities: state.activities.data,
message: state.message,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadActivities: params => {
dispatch(getOrUpdateData('getData', 'activities', params))
},
loadMoreActivities: params => {
dispatch(getMoreActivities(params))
},
})
)(Activities)
export default withTranslation()(
connect(
state => ({
activities: state.activities.data,
message: state.message,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadActivities: params => {
dispatch(getOrUpdateData('getData', 'activities', params))
},
loadMoreActivities: params => {
dispatch(getMoreActivities(params))
},
})
)(Activities)
)

View File

@ -1,5 +1,6 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import FormWithGpx from './ActivityForms/FormWithGpx'
@ -23,13 +24,16 @@ class ActivityAddEdit extends React.Component {
}
render() {
const { activity, loading, message, sports } = this.props
const { activity, loading, message, sports, t } = this.props
const { withGpx } = this.state
return (
<div>
<Helmet>
<title>
FitTrackee - {activity ? 'Edit a workout' : 'Add a workout'}
FitTrackee -{' '}
{activity
? t('activities:Edit a workout')
: t('activities:Add a workout')}
</title>
</Helmet>
<br />
@ -41,14 +45,20 @@ class ActivityAddEdit extends React.Component {
<div className="col-md-8">
<div className="card add-activity">
<h2 className="card-header text-center">
{activity ? 'Edit a workout' : 'Add a workout'}
{activity
? t('activities:Edit a workout')
: t('activities:Add a workout')}
</h2>
<div className="card-body">
{activity ? (
activity.with_gpx ? (
<FormWithGpx activity={activity} sports={sports} />
<FormWithGpx activity={activity} sports={sports} t={t} />
) : (
<FormWithoutGpx activity={activity} sports={sports} />
<FormWithoutGpx
activity={activity}
sports={sports}
t={t}
/>
)
) : (
<div>
@ -66,7 +76,7 @@ class ActivityAddEdit extends React.Component {
this.handleRadioChange(event)
}
/>
with gpx file
{t('activities:with gpx file')}
</label>
</div>
<div className="col">
@ -81,15 +91,15 @@ class ActivityAddEdit extends React.Component {
this.handleRadioChange(event)
}
/>
without gpx file
{t('activities:without gpx file')}
</label>
</div>
</div>
</form>
{withGpx ? (
<FormWithGpx sports={sports} />
<FormWithGpx sports={sports} t={t} />
) : (
<FormWithoutGpx sports={sports} />
<FormWithoutGpx sports={sports} t={t} />
)}
</div>
)}
@ -104,6 +114,8 @@ class ActivityAddEdit extends React.Component {
}
}
export default connect(state => ({
loading: state.loading,
}))(ActivityAddEdit)
export default withTranslation()(
connect(state => ({
loading: state.loading,
}))(ActivityAddEdit)
)

View File

@ -1,17 +1,21 @@
import React from 'react'
import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { setLoading } from '../../../actions/index'
import { addActivity, editActivity } from '../../../actions/activities'
import { history } from '../../../index'
import { fileSizeLimit, gpxLimit, zipSizeLimit } from '../../../utils'
import { translateSports } from '../../../utils/activities'
function FormWithGpx(props) {
const { activity, loading, onAddActivity, onEditActivity, sports } = props
const { activity, loading, onAddActivity, onEditActivity, sports, t } = props
const sportId = activity ? activity.sport_id : ''
const translatedSports = translateSports(sports, t)
// prettier-ignore
const zipTooltip =
`no folder inside, ${gpxLimit} files max, max size: ${zipSizeLimit}`
`${t('activities:no folder inside')}, ${gpxLimit} ${
t('activities:files max')}, ${t('activities:max size')}: ${zipSizeLimit}`
return (
<form
encType="multipart/form-data"
@ -20,7 +24,7 @@ function FormWithGpx(props) {
>
<div className="form-group">
<label>
Sport:
{t('common:Sport')}:
<select
className="form-control input-lg"
defaultValue={sportId}
@ -29,7 +33,7 @@ function FormWithGpx(props) {
required
>
<option value="" />
{sports.map(sport => (
{translatedSports.map(sport => (
<option key={sport.id} value={sport.id}>
{sport.label}
</option>
@ -40,7 +44,7 @@ function FormWithGpx(props) {
{activity ? (
<div className="form-group">
<label>
Title:
{t('activities:Title')}:
<input
name="title"
defaultValue={activity ? activity.title : ''}
@ -52,17 +56,21 @@ function FormWithGpx(props) {
) : (
<div className="form-group">
<label>
<strong>gpx</strong> file
<Trans i18nKey="activities:gpxFile">
<strong>gpx</strong> file
</Trans>
<sup>
<i
className="fa fa-question-circle"
aria-hidden="true"
data-toggle="tooltip"
title={`max size: ${fileSizeLimit}`}
title={`${t('activities:max size')}: ${fileSizeLimit}`}
/>
</sup>{' '}
or <strong> zip</strong> file containing <strong>gpx </strong>
files
<Trans i18nKey="activities:zipFile">
or <strong> zip</strong> file containing <strong>gpx </strong>
files
</Trans>
<sup>
<i
className="fa fa-question-circle"
@ -86,7 +94,7 @@ function FormWithGpx(props) {
)}
<div className="form-group">
<label>
Notes:
{t('activities:Notes')}:
<textarea
name="notes"
defaultValue={activity ? activity.notes : ''}
@ -106,13 +114,13 @@ function FormWithGpx(props) {
onClick={event =>
activity ? onEditActivity(event, activity) : onAddActivity(event)
}
value="Submit"
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => history.push('/')}
value="Cancel"
value={t('common:Cancel')}
/>
</div>
)}

View File

@ -6,10 +6,11 @@ import {
editActivity,
} from '../../../actions/activities'
import { history } from '../../../index'
import { formatActivityDate } from '../../../utils/activities'
import { formatActivityDate, translateSports } from '../../../utils/activities'
function FormWithoutGpx(props) {
const { activity, onAddOrEdit, sports } = props
const { activity, onAddOrEdit, sports, t } = props
const translatedSports = translateSports(sports, t)
let activityDate,
activityTime,
sportId = ''
@ -27,7 +28,7 @@ function FormWithoutGpx(props) {
<form onSubmit={event => event.preventDefault()}>
<div className="form-group">
<label>
Title:
{t('activities:Title')}:
<input
name="title"
defaultValue={activity ? activity.title : ''}
@ -37,7 +38,7 @@ function FormWithoutGpx(props) {
</div>
<div className="form-group">
<label>
Sport:
{t('common:Sport')}:
<select
className="form-control input-lg"
defaultValue={sportId}
@ -45,7 +46,7 @@ function FormWithoutGpx(props) {
required
>
<option value="" />
{sports.map(sport => (
{translatedSports.map(sport => (
<option key={sport.id} value={sport.id}>
{sport.label}
</option>
@ -55,7 +56,7 @@ function FormWithoutGpx(props) {
</div>
<div className="form-group">
<label>
Activity Date:
{t('activities:Activity Date')}:
<div className="container">
<div className="row">
<input
@ -78,7 +79,7 @@ function FormWithoutGpx(props) {
</div>
<div className="form-group">
<label>
Duration:
{t('activities:Duration')}:
<input
name="duration"
defaultValue={activity ? activity.duration : ''}
@ -92,7 +93,7 @@ function FormWithoutGpx(props) {
</div>
<div className="form-group">
<label>
Distance (km):
{t('activities:Distance')} (km):
<input
name="distance"
defaultValue={activity ? activity.distance : ''}
@ -106,7 +107,7 @@ function FormWithoutGpx(props) {
</div>
<div className="form-group">
<label>
Notes:
{t('activities:Notes')}:
<textarea
name="notes"
defaultValue={activity ? activity.notes : ''}
@ -119,13 +120,13 @@ function FormWithoutGpx(props) {
type="submit"
className="btn btn-primary btn-lg btn-block"
onClick={event => onAddOrEdit(event, activity)}
value="Submit"
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => history.push('/')}
value="Cancel"
value={t('common:Cancel')}
/>
</form>
)

View File

@ -400,7 +400,6 @@ label {
.time-frame label {
float: left;
padding: 0 5px;
width: 4em;
}
.time-frame label input {
@ -413,7 +412,7 @@ label {
color: #7b7b7b;
display: block;
font-size: 0.9em;
padding: 2px 0;
padding: 2px 6px;
text-align: center;
}

View File

@ -0,0 +1,18 @@
import React from 'react'
import { Link } from 'react-router-dom'
export default class NoActivities extends React.PureComponent {
render() {
const { t } = this.props
return (
<div className="card text-center">
<div className="card-body">
{t('common:No workouts.')}{' '}
<Link to={{ pathname: '/activities/add' }}>
{t('dashboard:Upload one !')}
</Link>
</div>
</div>
)
}
}

View File

@ -28,9 +28,9 @@ export default class StatsCharts extends React.PureComponent {
render() {
const { displayedData } = this.state
const { sports, stats } = this.props
const { sports, stats, t } = this.props
if (Object.keys(stats).length === 0) {
return 'No workouts'
return t('common:No workouts.')
}
return (
<div className="chart-stats">
@ -42,7 +42,7 @@ export default class StatsCharts extends React.PureComponent {
checked={displayedData === 'distance'}
onChange={e => this.handleRadioChange(e)}
/>
distance
{t('statistics:distance')}
</label>
<label className="radioLabel col">
<input
@ -51,7 +51,7 @@ export default class StatsCharts extends React.PureComponent {
checked={displayedData === 'duration'}
onChange={e => this.handleRadioChange(e)}
/>
duration
{t('statistics:duration')}
</label>
<label className="radioLabel col">
<input
@ -60,7 +60,7 @@ export default class StatsCharts extends React.PureComponent {
checked={displayedData === 'activities'}
onChange={e => this.handleRadioChange(e)}
/>
activities
{t('statistics:activities')}
</label>
</div>
<ResponsiveContainer height={300}>

View File

@ -37,10 +37,11 @@ class Statistics extends React.PureComponent {
statistics,
statsParams,
displayEmpty,
t,
user,
} = this.props
if (!displayEmpty && Object.keys(statistics).length === 0) {
return 'No workouts'
return <span>{t('common:No workouts.')}</span>
}
const stats = formatStats(
statistics,
@ -49,7 +50,7 @@ class Statistics extends React.PureComponent {
displayedSports,
user.weekm
)
return <StatsChart sports={sports} stats={stats} />
return <StatsChart sports={sports} stats={stats} t={t} />
}
}

View File

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'
import { formatRecord } from '../../utils/activities'
export default function RecordsCard(props) {
const { records, sports, user } = props
const { records, sports, t, user } = props
const recordsBySport = records.reduce((sportList, record) => {
const sport = sports.find(s => s.id === record.sport_id)
if (sportList[sport.label] === void 0) {
@ -22,7 +22,7 @@ export default function RecordsCard(props) {
<div className="card-header">Personal records</div>
<div className="card-body">
{Object.keys(recordsBySport).length === 0
? 'No records'
? t('common:No records.')
: Object.keys(recordsBySport).map(sportLabel => (
<table
className="table table-borderless table-sm record-table"

View File

@ -16,11 +16,12 @@ export default class Statistics extends React.Component {
}
render() {
const { t } = this.props
return (
<div className="card activity-card">
<div className="card-header">This month</div>
<div className="card-header">{t('dashboard:This month')}</div>
<div className="card-body">
<Stats displayEmpty={false} statsParams={this.state} />
<Stats displayEmpty={false} statsParams={this.state} t={t} />
</div>
</div>
)

View File

@ -1,10 +1,12 @@
import React from 'react'
export default function UserStatistics(props) {
const { user } = props
const { t, user } = props
const days = user.total_duration.match(/day/g)
? `${user.total_duration.split(',')[0]},`
: '0 days,'
? `${user.total_duration.split(' ')[0]} ${
user.total_duration.match(/days/g) ? t('common:days') : t('common:day')
}`
: `0 ${t('common:days')},`
let duration = user.total_duration.match(/day/g)
? user.total_duration.split(', ')[1]
: user.total_duration
@ -19,7 +21,11 @@ export default function UserStatistics(props) {
</div>
<div className="col-9 text-right">
<div className="huge">{user.nb_activities}</div>
<div>{`workout${user.nb_activities === 1 ? '' : 's'}`}</div>
<div>{`${
user.nb_activities === 1
? t('common:workout')
: t('common:workouts')
}`}</div>
</div>
</div>
</div>
@ -60,7 +66,9 @@ export default function UserStatistics(props) {
</div>
<div className="col-9 text-right">
<div className="huge">{user.nb_sports}</div>
<div>{`sport${user.nb_sports === 1 ? '' : 's'}`}</div>
<div>{`${
user.nb_sports === 1 ? t('common:sport') : t('common:sports')
}`}</div>
</div>
</div>
</div>

View File

@ -1,10 +1,11 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import ActivityCard from './ActivityCard'
import Calendar from './Calendar'
import NoActivities from '../Common/NoActivities'
import Records from './Records'
import Statistics from './Statistics'
import UserStatistics from './UserStatistics'
@ -30,6 +31,7 @@ class DashBoard extends React.Component {
message,
records,
sports,
t,
user,
} = this.props
const paginationEnd =
@ -40,7 +42,7 @@ class DashBoard extends React.Component {
return (
<div>
<Helmet>
<title>FitTrackee - Dashboard</title>
<title>FitTrackee - {t('common:Dashboard')}</title>
</Helmet>
{message ? (
<code>{message}</code>
@ -48,11 +50,16 @@ class DashBoard extends React.Component {
activities &&
sports.length > 0 && (
<div className="container dashboard">
<UserStatistics user={user} />
<UserStatistics user={user} t={t} />
<div className="row">
<div className="col-md-4">
<Statistics />
<Records records={records} sports={sports} user={user} />
<Statistics t={t} />
<Records
t={t}
records={records}
sports={sports}
user={user}
/>
</div>
<div className="col-md-8">
<Calendar weekm={user.weekm} />
@ -66,14 +73,7 @@ class DashBoard extends React.Component {
/>
))
) : (
<div className="card text-center">
<div className="card-body">
No workouts.{' '}
<Link to={{ pathname: '/activities/add' }}>
Upload one !
</Link>
</div>
</div>
<NoActivities t={t} />
)}
{!paginationEnd && (
<input
@ -96,21 +96,23 @@ class DashBoard extends React.Component {
}
}
export default connect(
state => ({
activities: state.activities.data,
message: state.message,
records: state.records.data,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadActivities: () => {
dispatch(getOrUpdateData('getData', 'activities', { page: 1 }))
dispatch(getOrUpdateData('getData', 'records'))
},
loadMoreActivities: page => {
dispatch(getMoreActivities({ page }))
},
})
)(DashBoard)
export default withTranslation()(
connect(
state => ({
activities: state.activities.data,
message: state.message,
records: state.records.data,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadActivities: () => {
dispatch(getOrUpdateData('getData', 'activities', { page: 1 }))
dispatch(getOrUpdateData('getData', 'records'))
},
loadMoreActivities: page => {
dispatch(getMoreActivities({ page }))
},
})
)(DashBoard)
)

View File

@ -1,6 +1,6 @@
import React from 'react'
import { Translation } from 'react-i18next'
import { connect } from 'react-redux'
import { withTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import LanguageDropdown from './LanguageDropdown'
@ -8,163 +8,161 @@ import { apiUrl } from '../../utils'
class NavBar extends React.PureComponent {
render() {
const { id, isAuthenticated, picture, username } = this.props
const { id, isAuthenticated, picture, t, username } = this.props
return (
<Translation>
{t => (
<header>
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container">
<span className="navbar-brand">FitTrackee</span>
<button
className="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon" />
</button>
<div
className="collapse navbar-collapse"
id="navbarSupportedContent"
>
<ul className="navbar-nav mr-auto">
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/',
}}
>
{t('common:Dashboard')}
</Link>
</li>
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/activities/history',
}}
>
{t('Workouts')}
</Link>
</li>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/activities/statistics',
}}
>
{t('common:Statistics')}
</Link>
</li>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/activities/add',
}}
>
<strong>{t('common:Add workout')}</strong>
</Link>
</li>
)}
{/* {user.admin && ( */}
{/* <li className="nav-item"> */}
{/* <Link */}
{/* className="nav-link" */}
{/* to={{ */}
{/* pathname: '/admin', */}
{/* }} */}
{/* > */}
{/* Admin */}
{/* </Link> */}
{/* </li> */}
{/* )} */}
</ul>
{/* prettier-ignore */}
<ul
className="navbar-nav flex-row ml-md-auto d-none d-md-flex"
<header>
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container">
<span className="navbar-brand">FitTrackee</span>
<button
className="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon" />
</button>
<div
className="collapse navbar-collapse"
id="navbarSupportedContent"
>
<ul className="navbar-nav mr-auto">
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/',
}}
>
{!isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/register',
}}
>
{t('common:Register')}
</Link>
</li>
)}
{!isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/login',
}}
>
{t('common:Login')}
</Link>
</li>
)}
{picture === true && (
<img
alt="Avatar"
src={`${apiUrl}users/${id}/picture?${Date.now()}`}
className="img-fluid App-nav-profile-img"
/>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/profile',
}}
>
{username}
</Link>
</li>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/logout',
}}
>
{t('common:Logout')}
</Link>
</li>
)}
<li><LanguageDropdown /></li>
</ul>
</div>
</div>
</nav>
</header>
)}
</Translation>
{t('common:Dashboard')}
</Link>
</li>
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/activities/history',
}}
>
{t('Workouts')}
</Link>
</li>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/activities/statistics',
}}
>
{t('common:Statistics')}
</Link>
</li>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/activities/add',
}}
>
<strong>{t('common:Add workout')}</strong>
</Link>
</li>
)}
{/* {user.admin && ( */}
{/* <li className="nav-item"> */}
{/* <Link */}
{/* className="nav-link" */}
{/* to={{ */}
{/* pathname: '/admin', */}
{/* }} */}
{/* > */}
{/* Admin */}
{/* </Link> */}
{/* </li> */}
{/* )} */}
</ul>
{/* prettier-ignore */}
<ul
className="navbar-nav flex-row ml-md-auto d-none d-md-flex"
>
{!isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/register',
}}
>
{t('common:Register')}
</Link>
</li>
)}
{!isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/login',
}}
>
{t('common:Login')}
</Link>
</li>
)}
{picture === true && (
<img
alt="Avatar"
src={`${apiUrl}users/${id}/picture?${Date.now()}`}
className="img-fluid App-nav-profile-img"
/>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/profile',
}}
>
{username}
</Link>
</li>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/logout',
}}
>
{t('common:Logout')}
</Link>
</li>
)}
<li><LanguageDropdown /></li>
</ul>
</div>
</div>
</nav>
</header>
)
}
}
export default connect(({ user }) => ({
id: user.id,
isAuthenticated: user.isAuthenticated,
picture: user.picture,
username: user.username,
}))(NavBar)
export default withTranslation()(
connect(({ user }) => ({
id: user.id,
isAuthenticated: user.isAuthenticated,
picture: user.picture,
username: user.username,
}))(NavBar)
)

View File

@ -14,9 +14,10 @@ import {
} from 'date-fns'
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { activityColors } from '../../utils/activities'
import { activityColors, translateSports } from '../../utils/activities'
import Stats from '../Common/Stats'
const durations = ['week', 'month', 'year']
@ -114,15 +115,16 @@ class Statistics extends React.Component {
render() {
const { displayedSports, statsParams } = this.state
const { sports } = this.props
const { sports, t } = this.props
const translatedSports = translateSports(sports, t)
return (
<>
<Helmet>
<title>FitTrackee - Statistics</title>
<title>FitTrackee - {t('statistics:Statistics')}</title>
</Helmet>
<div className="container dashboard">
<div className="card activity-card">
<div className="card-header">Statistics</div>
<div className="card-header">{t('statistics:Statistics')}</div>
<div className="card-body">
<div className="chart-filters row">
<div className="col chart-arrows">
@ -145,7 +147,7 @@ class Statistics extends React.Component {
checked={d === statsParams.duration}
onChange={e => this.handleOnChangeDuration(e)}
/>
<span>{d}</span>
<span>{t(`statistics:${d}`)}</span>
</label>
</div>
))}
@ -164,9 +166,10 @@ class Statistics extends React.Component {
displayEmpty
displayedSports={displayedSports}
statsParams={statsParams}
t={t}
/>
<div className="row chart-activities">
{sports.map(sport => (
{translatedSports.map(sport => (
<label className="col activity-label" key={sport.id}>
<input
type="checkbox"
@ -188,6 +191,8 @@ class Statistics extends React.Component {
}
}
export default connect(state => ({
sports: state.sports.data,
}))(Statistics)
export default withTranslation()(
connect(state => ({
sports: state.sports.data,
}))(Statistics)
)

View File

@ -1,4 +1,5 @@
import React from 'react'
import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
@ -16,8 +17,10 @@ class Logout extends React.Component {
<div className="card col-8">
<div className="card-body">
<div className="text-center">
You are now logged out. Click <Link to="/login">here</Link> to
log back in.
<Trans i18nKey="user:loggedOut">
You are now logged out. Click <Link to="/login">here</Link> to
log back in.
</Trans>
</div>
</div>
</div>

View File

@ -1,13 +1,14 @@
import { format } from 'date-fns'
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { deletePicture, uploadPicture } from '../../actions/user'
import { apiUrl, fileSizeLimit } from '../../utils'
function Profile({ message, onDeletePicture, onUploadPicture, user }) {
function Profile({ message, onDeletePicture, onUploadPicture, t, user }) {
const createdAt = user.created_at
? format(new Date(user.created_at), 'dd/MM/yyyy HH:mm')
: ''
@ -17,11 +18,11 @@ function Profile({ message, onDeletePicture, onUploadPicture, user }) {
return (
<div>
<Helmet>
<title>FitTrackee - Profile</title>
<title>FitTrackee - {t('user:Profile')}</title>
</Helmet>
{message !== '' && <code>{message}</code>}
<div className="container">
<h1 className="page-title">Profile</h1>
<h1 className="page-title">{t('user:Profile')}</h1>
<div className="row">
<div className="col-md-12">
<div className="card">
@ -38,15 +39,34 @@ function Profile({ message, onDeletePicture, onUploadPicture, user }) {
<div className="card-body">
<div className="row">
<div className="col-md-8">
<p>Email: {user.email}</p>
<p>Registration Date: {createdAt}</p>
<p>First Name: {user.first_name}</p>
<p>Last Name: {user.last_name}</p>
<p>Birth Date: {birthDate}</p>
<p>Location: {user.location}</p>
<p>Bio: {user.bio}</p>
<p>Time zone: {user.timezone}</p>
<p>First day of week: {user.weekm ? 'Monday' : 'Sunday'}</p>
<p>
{t('user:Email')}: {user.email}
</p>
<p>
{t('user:Registration Date')}: {createdAt}
</p>
<p>
{t('user:First Name')}: {user.first_name}
</p>
<p>
{t('user:Last Name')}: {user.last_name}
</p>
<p>
{t('user:Birth Date')}: {birthDate}
</p>
<p>
{t('user:Location')}: {user.location}
</p>
<p>
{t('user:Bio')}: {user.bio}
</p>
<p>
{t('user:Timezone')}: {user.timezone}
</p>
<p>
{t('user:First day of week')}:{' '}
{user.weekm ? t('user:Monday') : t('user:Sunday')}
</p>
</div>
<div className="col-md-4">
{user.picture === true && (
@ -61,7 +81,7 @@ function Profile({ message, onDeletePicture, onUploadPicture, user }) {
/>
<br />
<button type="submit" onClick={() => onDeletePicture()}>
Delete picture
{t('user:Delete picture')}
</button>
<br />
<br />
@ -77,8 +97,8 @@ function Profile({ message, onDeletePicture, onUploadPicture, user }) {
accept=".png,.jpg,.gif"
/>
<br />
<button type="submit">Send</button> (max. size:{' '}
{fileSizeLimit})
<button type="submit">{t('user:Send')}</button>
{` (max. size: ${fileSizeLimit})`}
</form>
</div>
</div>
@ -91,17 +111,19 @@ function Profile({ message, onDeletePicture, onUploadPicture, user }) {
)
}
export default connect(
state => ({
message: state.message,
user: state.user,
}),
dispatch => ({
onDeletePicture: () => {
dispatch(deletePicture())
},
onUploadPicture: event => {
dispatch(uploadPicture(event))
},
})
)(Profile)
export default withTranslation()(
connect(
state => ({
message: state.message,
user: state.user,
}),
dispatch => ({
onDeletePicture: () => {
dispatch(deletePicture())
},
onUploadPicture: event => {
dispatch(uploadPicture(event))
},
})
)(Profile)
)

View File

@ -1,6 +1,7 @@
import { format } from 'date-fns'
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import TimezonePicker from 'react-timezone'
@ -49,17 +50,17 @@ class ProfileEdit extends React.Component {
}
render() {
const { onHandleProfileFormSubmit, message, user } = this.props
const { onHandleProfileFormSubmit, message, t, user } = this.props
const { formData } = this.state
return (
<div>
<Helmet>
<title>FitTrackee - Edit Profile</title>
<title>FitTrackee - {t('user:Profile Edition')}</title>
</Helmet>
{message !== '' && <code>{message}</code>}
{formData.isAuthenticated && (
<div className="container">
<h1 className="page-title">Profile Edition</h1>
<h1 className="page-title">{t('user:Profile Edition')}</h1>
<div className="row">
<div className="col-md-2" />
<div className="col-md-8">
@ -76,7 +77,7 @@ class ProfileEdit extends React.Component {
>
<div className="form-group">
<label>
Email:
{t('user:Email')}:
<input
name="email"
className="form-control input-lg"
@ -88,7 +89,7 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
Registration Date:
{t('user:Registration Date')}:
<input
name="createdAt"
className="form-control input-lg"
@ -100,7 +101,7 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
Password:
{t('user:Password')}:
<input
name="password"
className="form-control input-lg"
@ -111,7 +112,7 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
Password Confirmation:
{t('user:Password Confirmation')}:
<input
name="password_conf"
className="form-control input-lg"
@ -123,7 +124,7 @@ class ProfileEdit extends React.Component {
<hr />
<div className="form-group">
<label>
First Name:
{t('user:First Name')}:
<input
name="first_name"
className="form-control input-lg"
@ -135,7 +136,7 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
Last Name:
{t('user:Last Name')}:
<input
name="last_name"
className="form-control input-lg"
@ -147,7 +148,7 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
Birth Date
{t('user:Birth Date')}
<input
name="birth_date"
className="form-control input-lg"
@ -159,7 +160,7 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
Location:
{t('user:Location')}:
<input
name="location"
className="form-control input-lg"
@ -171,7 +172,7 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
Bio:
{t('user:Bio')}:
<textarea
name="bio"
className="form-control input-lg"
@ -183,7 +184,7 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
Timezone:
{t('user:Timezone')}:
<TimezonePicker
className="form-control timezone-custom-height"
onChange={tz => {
@ -201,28 +202,32 @@ class ProfileEdit extends React.Component {
</div>
<div className="form-group">
<label>
First day of week:
{t('user:First day of week')}:
<select
name="weekm"
className="form-control input-lg"
value={formData.weekm ? 'Monday' : 'Sunday'}
onChange={e => this.handleFormChange(e)}
>
<option value="Sunday">Sunday</option>
<option value="Monday">Monday</option>
<option value="Sunday">
{t('user:Sunday')}
</option>
<option value="Monday">
{t('user:Monday')}
</option>
</select>
</label>
</div>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
value="Submit"
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => history.push('/profile')}
value="Cancel"
value={t('common:Cancel')}
/>
</form>
</div>
@ -239,15 +244,17 @@ class ProfileEdit extends React.Component {
}
}
export default connect(
state => ({
location: state.router.location,
message: state.message,
user: state.user,
}),
dispatch => ({
onHandleProfileFormSubmit: formData => {
dispatch(handleProfileFormSubmit(formData))
},
})
)(ProfileEdit)
export default withTranslation(
connect(
state => ({
location: state.router.location,
message: state.message,
user: state.user,
}),
dispatch => ({
onHandleProfileFormSubmit: formData => {
dispatch(handleProfileFormSubmit(formData))
},
})
)(ProfileEdit)
)

View File

@ -2,9 +2,17 @@ import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import XHR from 'i18next-xhr-backend'
import EnActivitiesTranslations from './locales/en/activities.json'
import EnCommonTranslations from './locales/en/common.json'
import EnDashboardTranslations from './locales/en/dashboard.json'
import EnSportsTranslations from './locales/en/sports.json'
import EnStatisticsTranslations from './locales/en/statistics.json'
import EnUserTranslations from './locales/en/user.json'
import FrActivitiesTranslations from './locales/fr/activities.json'
import FrCommonTranslations from './locales/fr/common.json'
import FrDashboardTranslations from './locales/fr/dashboard.json'
import FrSportsTranslations from './locales/fr/sports.json'
import FrStatisticsTranslations from './locales/fr/statistics.json'
import FrUserTranslations from './locales/fr/user.json'
i18n
@ -20,11 +28,19 @@ i18n
},
resources: {
en: {
activities: EnActivitiesTranslations,
common: EnCommonTranslations,
dashboard: EnDashboardTranslations,
sports: EnSportsTranslations,
statistics: EnStatisticsTranslations,
user: EnUserTranslations,
},
fr: {
activities: FrActivitiesTranslations,
common: FrCommonTranslations,
dashboard: FrDashboardTranslations,
sports: FrSportsTranslations,
statistics: FrStatisticsTranslations,
user: FrUserTranslations,
},
},

View File

@ -0,0 +1,23 @@
{
"Activity Date": "Activity Date",
"Add a workout": "Add a workout",
"Ave. speed": "Ave. speed",
"Average speed": "Average speed",
"Date": "Date",
"Distance": "Distance",
"Duration": "Duration",
"Edit a workout": "Edit a workout",
"Filter": "Filter",
"From": "From",
"gpxFile": "<strong>gpx</strong> file",
"Max. speed": "Max. speed",
"no folder inside": "no folder inside",
"files max": "files max",
"max size": "max size",
"Notes": "Notes",
"Title": "Title",
"To": "To",
"with gpx file": "with gpx file",
"without gpx file": "without gpx file",
"zipFile": "or <strong> zip</strong> file containing <strong>gpx </strong> files"
}

View File

@ -1,10 +1,23 @@
{
"Dashboard": "Dashboard",
"Workouts": "Workouts",
"Statistics": "Statistics",
"Add workout": "Add workout",
"Register": "Register",
"Cancel": "Cancel",
"Dashboard": "Dashboard",
"day": "day",
"days": "days",
"Login": "Login",
"Logout": "Logout",
"Submit": "Submit"
"No records.": "No records.",
"No workouts.": "No workouts.",
"Register": "Register",
"Statistics": "Statistics",
"Sport": "Sport",
"sport": "sport",
"Sports": "Sports",
"sports": "sports",
"Submit": "Submit",
"to": "to",
"Workout": "Workout",
"Workouts": "Workouts",
"workout": "workout",
"workouts": "workouts"
}

View File

@ -0,0 +1,5 @@
{
"Personal records": "Personal records",
"This month": "This month",
"Upload one !": "Upload one !"
}

View File

@ -0,0 +1,8 @@
{
"Cycling (Sport)": "Cycling (Sport)",
"Cycling (Transport)": "Cycling (Transport)",
"Hiking": "Hiking",
"Mountain Biking": "Mountain Biking",
"Running": "Running",
"Walking": "Walking"
}

View File

@ -0,0 +1,9 @@
{
"activities": "activities",
"distance": "distance",
"duration": "duration",
"month": "month",
"Statistics": "Statistics",
"year": "year",
"week": "week"
}

View File

@ -1,8 +1,27 @@
{
"login": "login",
"register": "register",
"Bio": "Bio",
"Birth Date": "Birth Date",
"Delete picture": "Delete picture",
"Edit Profile": "Edit Profile",
"Email": "Email",
"Enter a username": "Enter a username",
"Enter an email address": "Enter an email address",
"Enter a password": "Enter a password",
"Enter the password confirmation": "Enter the password confirmation"
"Enter the password confirmation": "Enter the password confirmation",
"First day of week": "First day of week",
"First Name": "First Name",
"Last Name": "Last Name",
"Location": "Location",
"loggedOut": "You are now logged out. Click <1>here</1> to log back in.",
"login": "login",
"Monday": "Monday",
"Password": "Password",
"Password Confirmation": "Password Confirmation",
"Profile": "Profile",
"Profile Edition": "Profile Edition",
"register": "register",
"Registration Date": "Registration Date",
"Send": "Send",
"Sunday": "Sunday",
"Timezone": "Timezone"
}

View File

@ -0,0 +1,23 @@
{
"Activity Date": "Date de l'activité",
"Add a workout": "Ajouter une activité",
"Ave. speed": "Vitesse moyenne",
"Average speed": "Vitesse moyenne",
"Date": "Date",
"Distance": "Distance",
"Duration": "Durée",
"Edit a workout": "Editer une activité",
"Filter": "Filtrer",
"From": "A partir de",
"gpxFile": "fichier <strong>gpx</strong>",
"Max. speed": "Vitesse max",
"no folder inside": "pas de répertoire",
"files max": " fichiers max",
"max size": "taille max",
"Notes": "Notes",
"Title": "Titre",
"To": "Jusqu'au",
"with gpx file": "avec un fichier gpx",
"without gpx file": "sans fichier gpx",
"zipFile": "ou un fichier <strong> zip</strong> contenant des fichiers <strong>gpx</strong>"
}

View File

@ -1,10 +1,23 @@
{
"Dashboard": "Tableau de Bord",
"Workouts": "Activités",
"Statistics": "Statistiques",
"Add workout": "Ajouter une activité",
"Register": "S'inscrire",
"Cancel": "Annuler",
"Dashboard": "Tableau de Bord",
"day": "jour",
"days": "jours",
"Login": "Se connecter",
"Logout": "Se déconnecter",
"Submit": "Valider"
"No records.": "Pas de records.",
"No workouts.": "Pas d'activités.",
"Register": "S'inscrire",
"Statistics": "Statistiques",
"Sport": "Sport",
"sport": "sport",
"Sports": "Sports",
"sports": "sports",
"Submit": "Valider",
"to": "à",
"Workout": "Activité",
"Workouts": "Activités",
"workout": "activité",
"workouts": "activités"
}

View File

@ -0,0 +1,5 @@
{
"Personal records": "Mes records",
"This month": "Ce mois",
"Upload one !": "Ajoutez votre première activité !"
}

View File

@ -0,0 +1,8 @@
{
"Cycling (Sport)": "Vélo (Sport)",
"Cycling (Transport)": "Vélo (Transport)",
"Hiking": "Randonnée",
"Mountain Biking": "VTT",
"Running": "Course",
"Walking": "Marche"
}

View File

@ -0,0 +1,9 @@
{
"activities": "activités",
"distance": "distance",
"duration": "durée",
"month": "mois",
"Statistics": "Statistiques",
"year": "année",
"week": "semaine"
}

View File

@ -1,8 +1,27 @@
{
"login": "se connecter",
"register": "s'nscrire",
"Bio": "Bio",
"Birth Date": "Date de naissance",
"Delete picture": "Supprimer l'image",
"Edit Profile": "Editer le profil",
"Email": "Email",
"Enter a username": "Saisir un nom",
"Enter an email address": "Saisir une adresse e-mail",
"Enter a password": "Saisir un mot de passe",
"Enter the password confirmation": "Confirmer le mot de passe"
"Enter the password confirmation": "Confirmer le mot de passe",
"First day of week": "Premier jour de la semaine",
"First Name": "Prénom",
"Last Name": "Nom",
"Location": "Lieu",
"loggedOut": "Vous êtes déconnecté. Cliquez <1>ici</1> pour vous reconnecter.",
"login": "se connecter",
"Monday": "Lundi",
"Password": "Mot de passe",
"Password Confirmation": "Confirmation du mot de passe",
"Profile": "Profil",
"Profile Edition": "Edition du profil",
"register": "s'inscrire",
"Registration Date": "Date d'inscription",
"Send": "Envoyer",
"Sunday": "Dimanche",
"Timezone": "Fuseau horaire"
}

View File

@ -86,3 +86,17 @@ export const formatRecord = (record, tz) => {
value: value,
}
}
const sortSports = (a, b) => {
const sportALabel = a.label.toLowerCase()
const sportBLabel = b.label.toLowerCase()
return sportALabel > sportBLabel ? 1 : sportALabel < sportBLabel ? -1 : 0
}
export const translateSports = (sports, t) =>
sports
.map(sport => ({
...sport,
label: t(`sports:${sport.label}`),
}))
.sort(sortSports)