Further work on localization
This commit is contained in:
parent
fa451fc8da
commit
cb1fc5b28d
35 changed files with 430 additions and 2796 deletions
2674
client/package-lock.json
generated
2674
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||
|
|
|
@ -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')) {
|
||||
|
|
124
client/src/campaigns/Statistics.js
Normal file
124
client/src/campaigns/Statistics.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue