Further work on localization

This commit is contained in:
Tomas Bures 2018-12-15 15:15:48 +01:00
parent fa451fc8da
commit cb1fc5b28d
35 changed files with 430 additions and 2796 deletions

2674
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -37,6 +37,7 @@
"react-dnd-touch-backend": "^0.3.21",
"react-dom": "^15.6.1",
"react-dropzone": "^4.3.0",
"react-google-charts": "^2.0.29",
"react-i18next": "^8.3.8",
"react-router-dom": "^4.3.1",
"react-sortable-tree": "^1.2.0",

View file

@ -100,6 +100,7 @@ export default class List extends Component {
const actions = [];
const perms = data[10];
const campaignType = data[4];
const status = data[5];
const campaignSource = data[7];
if (perms.includes('viewStats')) {
@ -107,6 +108,13 @@ export default class List extends Component {
label: <Icon icon="send" title={t('status')}/>,
link: `/campaigns/${data[0]}/status`
});
if (status === CampaignStatus.SENDING || status === CampaignStatus.PAUSED || status === CampaignStatus.FINISHED) {
actions.push({
label: <Icon icon="signal" title={t('Statistics')}/>,
link: `/campaigns/${data[0]}/statistics`
});
}
}
if (perms.includes('edit')) {

View file

@ -0,0 +1,124 @@
'use strict';
import React, {Component} from 'react';
import PropTypes
from 'prop-types';
import {withTranslation} from '../lib/i18n';
import {
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../lib/page';
import {
withAsyncErrorHandler,
withErrorHandling
} from '../lib/error-handling';
import axios
from "../lib/axios";
import {getUrl} from "../lib/urls";
import Chart from 'react-google-charts';
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class Statistics extends Component {
constructor(props) {
super(props);
const t = props.t;
this.state = {
entity: props.entity
};
this.refreshTimeoutHandler = ::this.periodicRefreshTask;
this.refreshTimeoutId = 0;
}
static propTypes = {
entity: PropTypes.object
}
@withAsyncErrorHandler
async refreshEntity() {
let resp;
resp = await axios.get(getUrl(`rest/campaigns-stats/${this.props.entity.id}`));
const entity = resp.data;
this.setState({
entity
});
}
async periodicRefreshTask() {
// The periodic task runs all the time, so that we don't have to worry about starting/stopping it as a reaction to the buttons.
await this.refreshEntity();
if (this.refreshTimeoutHandler) { // For some reason the task gets rescheduled if server is restarted while the page is shown. That why we have this check here.
this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 10000);
}
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.periodicRefreshTask();
}
componentWillUnmount() {
clearTimeout(this.refreshTimeoutId);
this.refreshTimeoutHandler = null;
}
render() {
const t = this.props.t;
const entity = this.state.entity;
return (
<div>
<Title>{t('Campaign Statistics')}</Title>
<Chart
width="100%"
height="500px"
chartType="PieChart"
loader={<div>Loading Chart</div>}
data={[
['Task', 'Hours per Day'],
['Work', 11],
['Eat', 2],
['Commute', 2],
['Watch TV', 2],
['Sleep', 7],
]}
options={{
title: 'My Daily Activities',
}}
rootProps={{ 'data-testid': '1' }}
/>
<Chart
width="100%"
height="500px"
chartType="GeoChart"
data={[
['Country', 'Popularity'],
['Germany', 200],
['United States', 300],
['Brazil', 400],
['Canada', 500],
['France', 600],
['RU', 700],
]}
// Note: you will need to get a mapsApiKey for your project.
// See: https://developers.google.com/chart/interactive/docs/basic_load_libs#load-settings
mapsApiKey="YOUR_KEY_HERE"
rootProps={{ 'data-testid': '1' }}
/>
</div>
);
}
}

View file

@ -4,6 +4,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from '../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
@ -77,7 +78,7 @@ class TestUser extends Component {
const campaignCid = this.props.entity.cid;
const [listCid, subscriptionCid] = this.getFormValue('testUser').split(':');
window.open(getPublicUrl(`archive/${campaignCid}/${listCid}/${subscriptionCid}`), '_blank');
window.open(getPublicUrl(`archive/${campaignCid}/${listCid}/${subscriptionCid}`, {withLocale: true}), '_blank');
} else {
this.showFormValidation();
}
@ -257,6 +258,7 @@ class SendControls extends Component {
:
<Button className="btn-primary" icon="send" label={t('send') + subscrInfo} onClickAsync={::this.startAsync}/>
}
{entity.status === CampaignStatus.PAUSED && <NavButton className="btn-default" icon="signal" label={t('View statistics')} linkTo={`/campaigns/${entity.id}/statistics`}/>}
</ButtonRow>
</div>
);
@ -269,6 +271,7 @@ class SendControls extends Component {
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="stop" label={t('stop')} onClickAsync={::this.stopAsync}/>
<NavButton className="btn-default" icon="signal" label={t('View statistics')} linkTo={`/campaigns/${entity.id}/statistics`}/>
</ButtonRow>
</div>
);
@ -284,6 +287,7 @@ class SendControls extends Component {
<ButtonRow>
<Button className="btn-primary" icon="play" label={t('continue') + subscrInfo} onClickAsync={::this.startAsync}/>
<Button className="btn-primary" icon="refresh" label={t('reset')} onClickAsync={::this.resetAsync}/>
<NavButton className="btn-default" icon="signal" label={t('View statistics')} linkTo={`/campaigns/${entity.id}/statistics`}/>
</ButtonRow>
</div>
);

View file

@ -3,6 +3,7 @@
import React from 'react';
import Status from './Status';
import Statistics from './Statistics';
import CampaignsCUD from './CUD';
import Content from './Content';
import CampaignsList from './List';
@ -10,6 +11,7 @@ import Share from '../shares/Share';
import Files from "../lib/files";
import {
CampaignSource,
CampaignStatus,
CampaignType
} from "../../../shared/campaigns";
import TriggersCUD from './triggers/CUD';
@ -36,6 +38,12 @@ function getMenus(t) {
visible: resolved => resolved.campaign.permissions.includes('viewStats'),
panelRender: props => <Status entity={props.resolved.campaign} />
},
statistics: {
title: t('Statistics'),
link: params => `/campaigns/${params.campaignId}/statistics`,
visible: resolved => resolved.campaign.permissions.includes('viewStats') && (resolved.campaign.status === CampaignStatus.SENDING || resolved.campaign.status === CampaignStatus.PAUSED || resolved.campaign.status === CampaignStatus.FINISHED),
panelRender: props => <Statistics entity={props.resolved.campaign} />
},
':action(edit|delete)': {
title: t('edit'),
link: params => `/campaigns/${params.campaignId}/edit`,

View file

@ -12,25 +12,24 @@ import mailtrainConfig
import hoistStatics
from 'hoist-non-react-statics';
import {convertToFake, langCodes} from '../../../shared/langs';
import {convertToFake, getLang} from '../../../shared/langs';
import commonEn from "../../../locales/en/common";
import commonEs from "../../../locales/es/common";
import lang_en_US_common from "../../../locales/en-US/common";
const resourcesCommon = {
en: commonEn,
es: commonEs,
fake: convertToFake(commonEn)
'en-US': lang_en_US_common,
'fk-FK': convertToFake(lang_en_US_common)
};
const resources = {};
for (const lng of mailtrainConfig.enabledLanguages) {
const shortCode = langCodes[lng].shortCode;
resources[shortCode] = {
common: resourcesCommon[shortCode]
const langDesc = getLang(lng);
resources[langDesc.longCode] = {
common: resourcesCommon[langDesc.longCode]
};
}
console.log(resources);
i18n
.use(LanguageDetector)
@ -46,17 +45,20 @@ i18n
react: {
wait: true,
defaultTransParent: 'span' // This is because we use React < v16
defaultTransParent: 'span' // This is because we use React < v16 FIXME
},
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
lookupQuerystring: 'language',
lookupCookie: 'i18next',
lookupQuerystring: 'locale',
lookupCookie: 'i18nextLng',
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage']
caches: ['localStorage', 'cookie']
},
whitelist: mailtrainConfig.enabledLanguages,
load: 'currentOnly',
debug: true
})

View file

@ -3,6 +3,7 @@
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
import {AppType} from '../../../shared/app';
import mailtrainConfig from "mailtrainConfig";
import i18n from './i18n';
let restrictedAccessToken = anonymousRestrictedAccessToken;
@ -18,8 +19,14 @@ function getSandboxUrl(path) {
return mailtrainConfig.sandboxUrlBase + restrictedAccessToken + '/' + (path || '');
}
function getPublicUrl(path) {
return mailtrainConfig.publicUrlBase + (path || '');
function getPublicUrl(path, opts) {
const url = new URL(path || '', mailtrainConfig.publicUrlBase);
if (opts && opts.withLocale) {
url.searchParams.append('locale', i18n.language);
}
return url.toString();
}
function getUrl(path) {

View file

@ -164,7 +164,7 @@ export default class List extends Component {
<div>
{tableRestActionDialogRender(this)}
<Toolbar>
<a href={getPublicUrl(`subscription/${this.props.list.cid}`)}><Button label={t('subscriptionForm')} className="btn-default"/></a>
<a href={getPublicUrl(`subscription/${this.props.list.cid}`, {withLocale: true})}><Button label={t('subscriptionForm')} className="btn-default"/></a>
<a href={getUrl(`subscriptions/export/${this.props.list.id}/`+ (this.props.segmentId || 0))}><Button label={t('exportAsCsv')} className="btn-primary"/></a>
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('addSubscriber')}/>
</Toolbar>

View file

@ -46,7 +46,7 @@ import {Link} from "react-router-dom";
import axios
from './lib/axios';
import {getUrl} from "./lib/urls";
import {langCodes} from "../../shared/langs";
import {getLang} from "../../shared/langs";
const topLevelMenuKeys = ['lists', 'templates', 'campaigns'];
@ -81,15 +81,15 @@ class Root extends Component {
render() {
const languageOptions = [];
for (const lng of mailtrainConfig.enabledLanguages) {
const langDesc = langCodes[lng];
const langDesc = getLang(lng);
const label = langDesc.getLabel(t);
languageOptions.push(
<li key={lng}><ActionLink onClickAsync={() => i18n.changeLanguage(langDesc.shortCode)}>{label}</ActionLink></li>
<li key={lng}><ActionLink onClickAsync={() => i18n.changeLanguage(langDesc.longCode)}>{label}</ActionLink></li>
)
}
const currentLngCode = langCodes[i18n.language].getShortLabel(t);
const currentLngCode = getLang(i18n.language).getShortLabel(t);
const path = this.props.location.pathname;