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;
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
{
|
||||
}
|
|
@ -14,7 +14,7 @@ const camelCase = require('camelcase');
|
|||
const slugify = require('slugify');
|
||||
const readline = require('readline');
|
||||
|
||||
const localeFile = 'en/common.json';
|
||||
const localeFile = 'en_US/common.json';
|
||||
const searchDirs = [
|
||||
'../client/src',
|
||||
'../server',
|
||||
|
@ -121,7 +121,7 @@ function allowedDirOrFile(item) {
|
|||
pp.base !== 'node_modules'
|
||||
) ||
|
||||
(item.stats.isFile() &&
|
||||
( pp.ext === '.js' || pp.ext === '.jsx')
|
||||
( pp.ext === '.js' || pp.ext === '.jsx' || pp.ext === '.hbs')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -148,11 +148,12 @@ function parseSpec(specStr) {
|
|||
|
||||
// see http://blog.stevenlevithan.com/archives/match-quoted-string
|
||||
const tMatcher = /(^|[ {+(=.\[])((?:tUI|tLog|t|tMark)\s*\(\s*(?:\/\*(.*?)\*\/)?\s*)(["'])((?:(?!\4)[^\\]|\\.)*)(\4)/;
|
||||
const transMatcher = /(\/\*(.*?)\*\/\s*)?(\<Trans[ >][\s\S]*?\<\/Trans\>)/;
|
||||
const jsxTransMatcher = /(\/\*(.*?)\*\/\s*)?(\<Trans[ >][\s\S]*?\<\/Trans\>)/;
|
||||
const hbsTranslateMatcher = /(\{\{!--(.*?)--\}\}\s*)?\{\{#translate\}\}([\s\S]*?)\{\{\/translate\}\}/;
|
||||
|
||||
const jsxParser = acorn.Parser.extend(acornJsx());
|
||||
function parseTrans(fragment) {
|
||||
const match = fragment.match(transMatcher);
|
||||
function parseJsxTrans(fragment) {
|
||||
const match = fragment.match(jsxTransMatcher);
|
||||
const spec = parseSpec(match[2]);
|
||||
const jsxStr = match[3];
|
||||
|
||||
|
@ -220,6 +221,27 @@ function parseTrans(fragment) {
|
|||
}
|
||||
|
||||
|
||||
function parseHbsTranslate(fragment) {
|
||||
const match = fragment.match(hbsTranslateMatcher);
|
||||
const spec = parseSpec(match[2]);
|
||||
const originalKey = match[3];
|
||||
|
||||
let value;
|
||||
const originalValue = findInDict(originalResDict, originalKey);
|
||||
|
||||
if (originalValue === undefined) {
|
||||
value = originalKey;
|
||||
} else {
|
||||
value = originalValue;
|
||||
}
|
||||
|
||||
const key = getKeyFromValue(spec, value);
|
||||
|
||||
const replacement = `${match[1] || ''}${key}`;
|
||||
|
||||
return { key, originalKey, value, replacement };
|
||||
}
|
||||
|
||||
function parseT(fragment) {
|
||||
const match = fragment.match(tMatcher);
|
||||
|
||||
|
@ -295,8 +317,11 @@ function processFile(file) {
|
|||
update(fragments, parseT);
|
||||
}
|
||||
|
||||
const fragments = source.match(new RegExp(transMatcher, 'g'));
|
||||
update(fragments, parseTrans);
|
||||
const hbsFragments = source.match(new RegExp(hbsTranslateMatcher, 'g'));
|
||||
update(hbsFragments, parseHbsTranslate);
|
||||
|
||||
const jsxFragments = source.match(new RegExp(jsxTransMatcher, 'g'));
|
||||
update(jsxFragments, parseJsxTrans);
|
||||
|
||||
if (anyUpdates) {
|
||||
console.log(`Updating ${file}`);
|
||||
|
|
|
@ -11,7 +11,6 @@ const favicon = require('serve-favicon');
|
|||
const logger = require('morgan');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const RedisStore = require('connect-redis')(session);
|
||||
const flash = require('connect-flash');
|
||||
const hbs = require('hbs');
|
||||
const compression = require('compression');
|
||||
|
@ -157,17 +156,32 @@ function createApp(appType) {
|
|||
}));
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(session({
|
||||
store: config.redis.enabled ? new RedisStore(config.redis) : false,
|
||||
secret: config.www.secret,
|
||||
saveUninitialized: false,
|
||||
resave: false
|
||||
}));
|
||||
|
||||
if (config.redis.enabled) {
|
||||
const RedisStore = require('connect-redis')(session);
|
||||
|
||||
app.use(session({
|
||||
store: new RedisStore(config.redis),
|
||||
secret: config.www.secret,
|
||||
saveUninitialized: false,
|
||||
resave: false
|
||||
}));
|
||||
} else {
|
||||
app.use(session({
|
||||
store: false,
|
||||
secret: config.www.secret,
|
||||
saveUninitialized: false,
|
||||
resave: false
|
||||
}));
|
||||
}
|
||||
|
||||
app.use(expressLocale({
|
||||
priority: ['query', 'accept-language', 'default'],
|
||||
priority: ['query', 'cookie', 'accept-language', 'default'],
|
||||
query: {
|
||||
name: 'language'
|
||||
name: 'locale'
|
||||
},
|
||||
cookie: {
|
||||
name: 'i18nextLng'
|
||||
},
|
||||
default: config.defaultLanguage
|
||||
}));
|
||||
|
|
|
@ -37,12 +37,12 @@ editors:
|
|||
- codeeditor
|
||||
|
||||
# Default language to use
|
||||
defaultLanguage: en_US
|
||||
defaultLanguage: en-US
|
||||
|
||||
# Enabled languages
|
||||
enabledLanguages:
|
||||
- en_US
|
||||
- es
|
||||
- en-US
|
||||
- fk-FK
|
||||
|
||||
# Inject custom scripts in subscription/layout.mjml.hbs
|
||||
# customSubscriptionScripts: [/custom/hello-world.js]
|
||||
|
|
|
@ -67,7 +67,7 @@ async function _sendTransactionalMail(transport, mail, template) {
|
|||
}
|
||||
mail.headers['X-Sending-Zone'] = 'transactional';
|
||||
|
||||
const htmlRenderer = await tools.getTemplate(template.html);
|
||||
const htmlRenderer = await tools.getTemplate(template.html, template.locale);
|
||||
|
||||
if (htmlRenderer) {
|
||||
mail.html = htmlRenderer(template.data || {});
|
||||
|
@ -79,7 +79,7 @@ async function _sendTransactionalMail(transport, mail, template) {
|
|||
mail.html = preparedHtml;
|
||||
}
|
||||
|
||||
const textRenderer = await tools.getTemplate(template.text);
|
||||
const textRenderer = await tools.getTemplate(template.text, template.locale);
|
||||
|
||||
if (textRenderer) {
|
||||
mail.text = textRenderer(template.data || {});
|
||||
|
|
|
@ -20,49 +20,49 @@ module.exports = {
|
|||
sendUnsubscriptionConfirmed
|
||||
};
|
||||
|
||||
async function sendSubscriptionConfirmed(lang, list, email, subscription) {
|
||||
async function sendSubscriptionConfirmed(locale, list, email, subscription) {
|
||||
const relativeUrls = {
|
||||
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||
};
|
||||
|
||||
await _sendMail(list, email, 'subscription_confirmed', lang, tMark('subscriptionconfirmed'), relativeUrls, subscription);
|
||||
await _sendMail(list, email, 'subscription_confirmed', locale, tMark('subscriptionconfirmed'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendAlreadySubscribed(lang, list, email, subscription) {
|
||||
async function sendAlreadySubscribed(locale, list, email, subscription) {
|
||||
const relativeUrls = {
|
||||
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||
};
|
||||
await _sendMail(list, email, 'already_subscribed', lang, tMark('listEmailAddressAlreadyRegistered'), relativeUrls, subscription);
|
||||
await _sendMail(list, email, 'already_subscribed', locale, tMark('listEmailAddressAlreadyRegistered'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendConfirmAddressChange(lang, list, email, cid, subscription) {
|
||||
async function sendConfirmAddressChange(locale, list, email, cid, subscription) {
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/change-address/' + cid
|
||||
};
|
||||
await _sendMail(list, email, 'confirm_address_change', lang, tMark('listPleaseConfirmEmailChangeIn'), relativeUrls, subscription);
|
||||
await _sendMail(list, email, 'confirm_address_change', locale, tMark('listPleaseConfirmEmailChangeIn'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendConfirmSubscription(lang, list, email, cid, subscription) {
|
||||
async function sendConfirmSubscription(locale, list, email, cid, subscription) {
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/subscribe/' + cid
|
||||
};
|
||||
await _sendMail(list, email, 'confirm_subscription', lang, tMark('pleaseConfirmSubscription'), relativeUrls, subscription);
|
||||
await _sendMail(list, email, 'confirm_subscription', locale, tMark('pleaseConfirmSubscription'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendConfirmUnsubscription(lang, list, email, cid, subscription) {
|
||||
async function sendConfirmUnsubscription(locale, list, email, cid, subscription) {
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/unsubscribe/' + cid
|
||||
};
|
||||
await _sendMail(list, email, 'confirm_unsubscription', lang, tMark('listPleaseConfirmUnsubscription'), relativeUrls, subscription);
|
||||
await _sendMail(list, email, 'confirm_unsubscription', locale, tMark('listPleaseConfirmUnsubscription'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendUnsubscriptionConfirmed(lang, list, email, subscription) {
|
||||
async function sendUnsubscriptionConfirmed(locale, list, email, subscription) {
|
||||
const relativeUrls = {
|
||||
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
|
||||
};
|
||||
await _sendMail(list, email, 'unsubscription_confirmed', lang, tMark('listUnsubscriptionConfirmed'), relativeUrls, subscription);
|
||||
await _sendMail(list, email, 'unsubscription_confirmed', locale, tMark('listUnsubscriptionConfirmed'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
function getDisplayName(flds, subscription) {
|
||||
|
@ -95,7 +95,7 @@ function getDisplayName(flds, subscription) {
|
|||
}
|
||||
}
|
||||
|
||||
async function _sendMail(list, email, template, language, subjectKey, relativeUrls, subscription) {
|
||||
async function _sendMail(list, email, template, locale, subjectKey, relativeUrls, subscription) {
|
||||
const flds = await fields.list(contextHelpers.getAdminContext(), list.id);
|
||||
|
||||
const encryptionKeys = [];
|
||||
|
@ -114,7 +114,7 @@ async function _sendMail(list, email, template, language, subjectKey, relativeUr
|
|||
};
|
||||
|
||||
for (let relativeUrlKey in relativeUrls) {
|
||||
data[relativeUrlKey] = getPublicUrl(relativeUrls[relativeUrlKey], {language});
|
||||
data[relativeUrlKey] = getPublicUrl(relativeUrls[relativeUrlKey], {locale});
|
||||
}
|
||||
|
||||
const fsTemplate = template.replace(/_/g, '-');
|
||||
|
@ -148,11 +148,12 @@ async function _sendMail(list, email, template, language, subjectKey, relativeUr
|
|||
name: getDisplayName(flds, subscription),
|
||||
address: email
|
||||
},
|
||||
subject: tUI(subjectKey, language, { list: list.name }),
|
||||
subject: tUI(subjectKey, locale, { list: list.name }),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
locale,
|
||||
data
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -19,17 +19,34 @@ const he = require('he');
|
|||
const fs = require('fs-extra');
|
||||
|
||||
const { JSDOM } = require('jsdom');
|
||||
const { tUI, tLog } = require('./translate');
|
||||
const { tUI, tLog, getLangCodeFromExpressLocale } = require('./translate');
|
||||
|
||||
|
||||
const templates = new Map();
|
||||
|
||||
async function getTemplate(template) {
|
||||
async function getLocalizedFile(basePath, fileName, language) {
|
||||
try {
|
||||
const locFn = path.join(basePath, language, fileName);
|
||||
const stats = await fs.stat(locFn);
|
||||
|
||||
if (stats.isFile()) {
|
||||
return locFn;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(basePath, fileName)
|
||||
}
|
||||
|
||||
async function getTemplate(template, locale) {
|
||||
if (!template) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = (typeof template === 'object') ? hasher.hash(template) : template;
|
||||
const key = getLangCodeFromExpressLocale(locale) + ':' + ((typeof template === 'object') ? hasher.hash(template) : template);
|
||||
|
||||
if (templates.has(key)) {
|
||||
return templates.get(key);
|
||||
|
@ -37,9 +54,9 @@ async function getTemplate(template) {
|
|||
|
||||
let source;
|
||||
if (typeof template === 'object') {
|
||||
source = await mergeTemplateIntoLayout(template.template, template.layout);
|
||||
source = await mergeTemplateIntoLayout(template.template, template.layout, locale);
|
||||
} else {
|
||||
source = await fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8');
|
||||
source = await fs.readFile(await getLocalizedFile(path.join(__dirname, '..', 'views'), template, getLangCodeFromExpressLocale(locale)), 'utf-8');
|
||||
}
|
||||
|
||||
if (template.type === 'mjml') {
|
||||
|
@ -53,17 +70,35 @@ async function getTemplate(template) {
|
|||
}
|
||||
|
||||
const renderer = hbs.handlebars.compile(source);
|
||||
templates.set(key, renderer);
|
||||
|
||||
return renderer;
|
||||
const localizedRenderer = (data, options) => {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (!options.helpers) {
|
||||
options.helpers = {};
|
||||
}
|
||||
|
||||
options.helpers.translate = function (opts) { // eslint-disable-line prefer-arrow-callback
|
||||
const result = tUI(opts.fn(this), locale, opts.hash); // eslint-disable-line no-invalid-this
|
||||
return new hbs.handlebars.SafeString(result);
|
||||
};
|
||||
|
||||
return renderer(data, options);
|
||||
};
|
||||
|
||||
templates.set(key, localizedRenderer);
|
||||
|
||||
return localizedRenderer;
|
||||
}
|
||||
|
||||
|
||||
async function mergeTemplateIntoLayout(template, layout) {
|
||||
async function mergeTemplateIntoLayout(template, layout, locale) {
|
||||
layout = layout || '{{{body}}}';
|
||||
|
||||
async function readFile(relPath) {
|
||||
return await fs.readFile(path.join(__dirname, '..', 'views', relPath), 'utf-8');
|
||||
return await fs.readFile(await getLocalizedFile(path.join(__dirname, '..', 'views'), relPath, getLangCodeFromExpressLocale(locale)), 'utf-8');
|
||||
}
|
||||
|
||||
// Please dont end your custom messages with .hbs ...
|
||||
|
@ -184,7 +219,6 @@ function getMessageLinks(campaign, list, subscription) {
|
|||
module.exports = {
|
||||
validateEmail,
|
||||
validateEmailGetMessage,
|
||||
mergeTemplateIntoLayout,
|
||||
getTemplate,
|
||||
prepareHtml,
|
||||
getMessageLinks,
|
||||
|
|
|
@ -4,25 +4,24 @@ const config = require('config');
|
|||
const i18n = require("i18next");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {convertToFake, langCodes} = require('../../shared/langs');
|
||||
const {convertToFake, getLang} = require('../../shared/langs');
|
||||
|
||||
const resourcesCommon = {};
|
||||
|
||||
function loadLanguage(shortCode) {
|
||||
resourcesCommon[shortCode] = {
|
||||
common: JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'locales', shortCode, 'common.json')))
|
||||
function loadLanguage(longCode) {
|
||||
resourcesCommon[longCode] = {
|
||||
common: JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'locales', longCode, 'common.json')))
|
||||
};
|
||||
}
|
||||
|
||||
loadLanguage('en');
|
||||
loadLanguage('es');
|
||||
resourcesCommon.fake = convertToFake(resourcesCommon.en);
|
||||
loadLanguage('en-US');
|
||||
resourcesCommon['fk-FK'] = convertToFake(resourcesCommon['en-US']);
|
||||
|
||||
const resources = {};
|
||||
for (const lng of config.enabledLanguages) {
|
||||
const shortCode = langCodes[lng].shortCode;
|
||||
resources[shortCode] = {
|
||||
common: resourcesCommon[shortCode]
|
||||
const langDesc = getLang(lng);
|
||||
resources[langDesc.longCode] = {
|
||||
common: resourcesCommon[langDesc.longCode]
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -34,6 +33,9 @@ i18n
|
|||
fallbackLng: config.defaultLanguage,
|
||||
defaultNS: 'common',
|
||||
|
||||
whitelist: config.enabledLanguages,
|
||||
load: 'currentOnly',
|
||||
|
||||
debug: false
|
||||
})
|
||||
|
||||
|
@ -47,18 +49,29 @@ function tLog(key, args) {
|
|||
return JSON.stringify([key, args]);
|
||||
}
|
||||
|
||||
function tUI(key, lang, args) {
|
||||
function tUI(key, locale, args) {
|
||||
if (!args) {
|
||||
args = {};
|
||||
}
|
||||
|
||||
return i18n.t(key, { ...args, lng: lang });
|
||||
return i18n.t(key, { ...args, lng: getLangCodeFromExpressLocale(locale) });
|
||||
}
|
||||
|
||||
function tMark(key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
function getLangCodeFromExpressLocale(locale) {
|
||||
const longCode = locale.toString().replace('_', '-');
|
||||
if (longCode in resources) {
|
||||
return longCode;
|
||||
} else {
|
||||
return config.defaultLanguage
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.tLog = tLog;
|
||||
module.exports.tUI = tUI;
|
||||
module.exports.tMark = tMark;
|
||||
module.exports.tMark = tMark;
|
||||
module.exports.getLangCodeFromExpressLocale = getLangCodeFromExpressLocale;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
const config = require('config');
|
||||
const urllib = require('url');
|
||||
const {anonymousRestrictedAccessToken} = require('../../shared/urls');
|
||||
const {getLangCodeFromExpressLocale} = require('./translate');
|
||||
|
||||
function getTrustedUrlBase() {
|
||||
return urllib.resolve(config.www.trustedUrlBase, '');
|
||||
|
@ -19,8 +20,8 @@ function getPublicUrlBase() {
|
|||
function _getUrl(urlBase, path, opts) {
|
||||
const url = new URL(path || '', urlBase);
|
||||
|
||||
if (opts && opts.language) {
|
||||
url.searchParams.append('lang', opts.language)
|
||||
if (opts && opts.locale) {
|
||||
url.searchParams.append('locale', getLangCodeFromExpressLocale(opts.locale));
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
|
|
|
@ -29,7 +29,7 @@ function getFilePath(type, subType, entityId, filename) {
|
|||
}
|
||||
|
||||
function getFileUrl(context, type, subType, entityId, filename) {
|
||||
return getPublicUrl(`files/${type}/${subType}/${entityId}/${filename}`, context)
|
||||
return getPublicUrl(`files/${type}/${subType}/${entityId}/${filename}`)
|
||||
}
|
||||
|
||||
function getFilesTable(type, subType) {
|
||||
|
@ -109,7 +109,7 @@ async function getFileByFilename(context, type, subType, entityId, name) {
|
|||
}
|
||||
|
||||
async function getFileByUrl(context, url) {
|
||||
const urlPrefix = getPublicUrl('files/', context);
|
||||
const urlPrefix = getPublicUrl('files/');
|
||||
if (url.startsWith(urlPrefix)) {
|
||||
const path = url.substring(urlPrefix.length);
|
||||
const pathElem = path.split('/');
|
||||
|
|
|
@ -286,7 +286,7 @@ async function resetAccessToken(userId) {
|
|||
return token;
|
||||
}
|
||||
|
||||
async function sendPasswordReset(language, usernameOrEmail) {
|
||||
async function sendPasswordReset(locale, usernameOrEmail) {
|
||||
enforce(passport.isAuthMethodLocal, 'Local user management is required');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
|
@ -310,12 +310,13 @@ async function sendPasswordReset(language, usernameOrEmail) {
|
|||
to: {
|
||||
address: user.email
|
||||
},
|
||||
subject: tUI('mailerPasswordChangeRequest', language)
|
||||
subject: tUI('mailerPasswordChangeRequest', locale)
|
||||
}, {
|
||||
html: 'emails/password-reset-html.hbs',
|
||||
text: 'emails/password-reset-text.hbs',
|
||||
html: 'users/password-reset-html.hbs',
|
||||
text: 'users/password-reset-text.hbs',
|
||||
locale,
|
||||
data: {
|
||||
title: 'Mailtrain',
|
||||
title: tUI('Mailtrain', locale),
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
confirmUrl: getTrustedUrl(`/account/reset/${encodeURIComponent(user.username)}/${encodeURIComponent(resetToken)}`)
|
||||
|
|
|
@ -62,7 +62,7 @@ router.postAsync('/subscribe/:listCid', passport.loggedIn, async (req, res) => {
|
|||
};
|
||||
|
||||
const confirmCid = await confirmations.addConfirmation(list.id, 'subscribe', req.ip, data);
|
||||
await mailHelpers.sendConfirmSubscription(req.language, list, input.EMAIL, confirmCid, subscription);
|
||||
await mailHelpers.sendConfirmSubscription(req.locale, list, input.EMAIL, confirmCid, subscription);
|
||||
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
|
|
@ -44,7 +44,10 @@ router.post('/login', passport.csrfProtection, passport.restLogin);
|
|||
router.post('/logout', passport.csrfProtection, passport.restLogout);
|
||||
|
||||
router.postAsync('/password-reset-send', passport.csrfProtection, async (req, res) => {
|
||||
await users.sendPasswordReset(req.language, req.body.usernameOrEmail);
|
||||
// FIXME
|
||||
console.log(req.locale);
|
||||
console.log(req.cookies);
|
||||
await users.sendPasswordReset(req.locale, req.body.usernameOrEmail);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
|
|
@ -185,7 +185,7 @@ function getRouter(appType) {
|
|||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
|
||||
|
||||
let languageStrings = null;
|
||||
const lang = req.language;
|
||||
const lang = req.locale.language;
|
||||
if (lang && lang !== 'en') {
|
||||
try {
|
||||
const file = path.join(__dirname, '..', '..', 'client', 'static', 'mosaico', 'lang', 'mosaico-' + lang + '.json');
|
||||
|
|
|
@ -126,7 +126,7 @@ router.getAsync('/confirm/subscribe/:cid', async (req, res) => {
|
|||
|
||||
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
|
||||
subscription.cid = meta.cid;
|
||||
await mailHelpers.sendSubscriptionConfirmed(req.language, list, subscription.email, subscription);
|
||||
await mailHelpers.sendSubscriptionConfirmed(req.locale, list, subscription.email, subscription);
|
||||
|
||||
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/subscribed-notice');
|
||||
});
|
||||
|
@ -139,9 +139,9 @@ router.getAsync('/confirm/change-address/:cid', async (req, res) => {
|
|||
|
||||
const subscription = await subscriptions.updateAddressAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionId, data.emailNew);
|
||||
|
||||
await mailHelpers.sendSubscriptionConfirmed(req.language, list, data.emailNew, subscription);
|
||||
await mailHelpers.sendSubscriptionConfirmed(req.locale, list, data.emailNew, subscription);
|
||||
|
||||
req.flash('info', tUI('emailAddressChanged', req.language));
|
||||
req.flash('info', tUI('emailAddressChanged', req.locale));
|
||||
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/manage/' + subscription.cid);
|
||||
});
|
||||
|
||||
|
@ -153,7 +153,7 @@ router.getAsync('/confirm/unsubscribe/:cid', async (req, res) => {
|
|||
|
||||
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionCid, data.campaignCid);
|
||||
|
||||
await mailHelpers.sendUnsubscriptionConfirmed(req.language, list, subscription.email, subscription);
|
||||
await mailHelpers.sendUnsubscriptionConfirmed(req.locale, list, subscription.email, subscription);
|
||||
|
||||
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/unsubscribed-notice');
|
||||
});
|
||||
|
@ -181,7 +181,7 @@ async function _renderSubscribe(req, res, list, subscription) {
|
|||
|
||||
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-subscribe', data);
|
||||
|
||||
const htmlRenderer = await tools.getTemplate(data.template);
|
||||
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
|
||||
|
||||
data.isWeb = true;
|
||||
data.needsJsWarning = true;
|
||||
|
@ -245,7 +245,7 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
|
|||
throw new Error('Email address not set');
|
||||
}
|
||||
|
||||
req.flash('danger', tUI('emailAddressNotSet', req.language));
|
||||
req.flash('danger', tUI('emailAddressNotSet', req.locale));
|
||||
return await _renderSubscribe(req, res, list, subscriptionData);
|
||||
}
|
||||
|
||||
|
@ -282,7 +282,7 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
|
|||
}
|
||||
|
||||
if (existingSubscription && existingSubscription.status === SubscriptionStatus.SUBSCRIBED) {
|
||||
await mailHelpers.sendAlreadySubscribed(req.language, list, email, existingSubscription);
|
||||
await mailHelpers.sendAlreadySubscribed(req.locale, list, email, existingSubscription);
|
||||
res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '/confirm-subscription-notice');
|
||||
|
||||
} else {
|
||||
|
@ -296,12 +296,12 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
|
|||
if (!testsPass) {
|
||||
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
|
||||
} else {
|
||||
await mailHelpers.sendConfirmSubscription(req.language, list, email, confirmCid, subscriptionData);
|
||||
await mailHelpers.sendConfirmSubscription(req.locale, list, email, confirmCid, subscriptionData);
|
||||
}
|
||||
|
||||
if (req.xhr) {
|
||||
return res.status(200).json({
|
||||
msg: tUI('pleaseConfirmSubscription', req.language)
|
||||
msg: tUI('pleaseConfirmSubscription', req.locale)
|
||||
});
|
||||
}
|
||||
res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '/confirm-subscription-notice');
|
||||
|
@ -385,7 +385,7 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
|
|||
|
||||
await injectCustomFormData(req.query.fid || list.default_form, 'data/web-manage', data);
|
||||
|
||||
const htmlRenderer = await tools.getTemplate(data.template);
|
||||
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
|
||||
|
||||
data.isWeb = true;
|
||||
data.needsJsWarning = true;
|
||||
|
@ -435,7 +435,7 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r
|
|||
|
||||
await injectCustomFormData(req.query.fid || list.default_form, 'data/web-manage-address', data);
|
||||
|
||||
const htmlRenderer = await tools.getTemplate(data.template);
|
||||
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
|
||||
|
||||
data.isWeb = true;
|
||||
data.needsJsWarning = true;
|
||||
|
@ -458,7 +458,7 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
|
|||
}
|
||||
|
||||
if (subscription.email === emailNew) {
|
||||
req.flash('info', tUI('nothingSeemsToBeChanged', req.language));
|
||||
req.flash('info', tUI('nothingSeemsToBeChanged', req.locale));
|
||||
|
||||
} else {
|
||||
const emailErr = await tools.validateEmail(emailNew);
|
||||
|
@ -479,7 +479,7 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
|
|||
}
|
||||
|
||||
if (newSubscription && newSubscription.status === SubscriptionStatus.SUBSCRIBED) {
|
||||
await mailHelpers.sendAlreadySubscribed(req.language, list, emailNew, subscription);
|
||||
await mailHelpers.sendAlreadySubscribed(req.locale, list, emailNew, subscription);
|
||||
} else {
|
||||
const data = {
|
||||
subscriptionId: subscription.id,
|
||||
|
@ -487,10 +487,10 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
|
|||
};
|
||||
|
||||
const confirmCid = await confirmations.addConfirmation(list.id, 'change-address', req.ip, data);
|
||||
await mailHelpers.sendConfirmAddressChange(req.language, list, emailNew, confirmCid, subscription);
|
||||
await mailHelpers.sendConfirmAddressChange(req.locale, list, emailNew, confirmCid, subscription);
|
||||
}
|
||||
|
||||
req.flash('info', tUI('anEmailWithFurtherInstructionsHasBeen', req.language));
|
||||
req.flash('info', tUI('anEmailWithFurtherInstructionsHasBeen', req.locale));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -535,7 +535,7 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
|
|||
|
||||
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-unsubscribe', data);
|
||||
|
||||
const htmlRenderer = await tools.getTemplate(data.template);
|
||||
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
|
||||
|
||||
data.isWeb = true;
|
||||
data.needsJsWarning = true;
|
||||
|
@ -565,7 +565,7 @@ async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaig
|
|||
try {
|
||||
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, subscriptionCid, campaignCid);
|
||||
|
||||
await mailHelpers.sendUnsubscriptionConfirmed(req.language, list, subscription.email, subscription);
|
||||
await mailHelpers.sendUnsubscriptionConfirmed(req.locale, list, subscription.email, subscription);
|
||||
|
||||
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/unsubscribed-notice');
|
||||
|
||||
|
@ -590,7 +590,7 @@ async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaig
|
|||
};
|
||||
|
||||
const confirmCid = await confirmations.addConfirmation(list.id, 'unsubscribe', ip, data);
|
||||
await mailHelpers.sendConfirmUnsubscription(req.language, list, subscription.email, confirmCid, subscription);
|
||||
await mailHelpers.sendConfirmUnsubscription(req.locale, list, subscription.email, confirmCid, subscription);
|
||||
|
||||
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/confirm-unsubscription-notice');
|
||||
|
||||
|
@ -684,7 +684,7 @@ async function webNotice(type, req, res) {
|
|||
|
||||
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-' + type + '-notice', data);
|
||||
|
||||
const htmlRenderer = await tools.getTemplate(data.template);
|
||||
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = await captureFlashMessages(res);
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
{{#if typeSubscriptionEmail}}
|
||||
<div class="form-group email">
|
||||
<label for="EMAIL">Email Address</label>
|
||||
<label for="EMAIL">{{#translate}}Email Address{{/translate}}</label>
|
||||
{{#if ../isManagePreferences}}
|
||||
<div class="input-group">
|
||||
<input type="email" name="EMAIL" id="email" placeholder="" value="{{../email}}" readonly>
|
||||
<div class="input-group-addon"><a href="/subscription/{{../lcid}}/manage-address/{{../cid}}">want to change it?</a></div>
|
||||
<div class="input-group-addon"><a href="/subscription/{{../lcid}}/manage-address/{{../cid}}">{{#translate}}want to change it?{{/translate}}</a></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="email" name="EMAIL" id="email" placeholder="" value="{{../email}}" required>
|
||||
|
@ -53,11 +53,11 @@
|
|||
<div class="form-group gpg {{key}}">
|
||||
<label for="{{key}}">{{name}}</label>
|
||||
{{#if ../hasPubkey}}
|
||||
<button class="btn-download-pubkey" type="submit" form="download-pubkey">Download signature verification key</button>
|
||||
<button class="btn-download-pubkey" type="submit" form="download-pubkey">{{#translate}}Download signature verification key{{/translate}}</button>
|
||||
{{/if}}
|
||||
<textarea class="form-control gpg-text" rows="4" name="{{key}}" placeholder="Begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'">{{value}}</textarea>
|
||||
<textarea class="form-control gpg-text" rows="4" name="{{key}}" placeholder="{{#translate}}Begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'{{/translate}}">{{value}}</textarea>
|
||||
<span class="help-block">
|
||||
Insert your GPG public key here to encrypt messages sent to your address <em>(optional)</em>
|
||||
{{#translate}}Insert your GPG public key here to encrypt messages sent to your address <em>(optional)</em>{{/translate}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -95,7 +95,7 @@
|
|||
<label for="{{key}}">{{name}}</label>
|
||||
<select name="{{key}}" class="form-control">
|
||||
<option value="">
|
||||
–– Select ––
|
||||
{{#translate}}–– Select ––{{/translate}}
|
||||
</option>
|
||||
{{#each options}}
|
||||
<option value="{{key}}" {{#if value}} selected {{/if}}>{{name}}</option>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{{#if needsJsWarning}}
|
||||
<div class="alert alert-danger js-warning" role="alert">
|
||||
<strong>Warning!</strong>
|
||||
JavaScript must be enabled in order for this form to work
|
||||
<strong>{{#translate}}Warning!{{/translate}}</strong>
|
||||
{{#translate}}JavaScript must be enabled in order for this form to work{{/translate}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -4,19 +4,19 @@
|
|||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
|
||||
<div class="form-group email">
|
||||
<label for="EMAIL">Existing Email Address</label>
|
||||
<label for="EMAIL">{{#translate}}Existing Email Address{{/translate}}</label>
|
||||
<input type="email" name="EMAIL" id="email" placeholder="" value="{{email}}" readonly>
|
||||
</div>
|
||||
|
||||
<div class="form-group email">
|
||||
<label for="EMAIL_NEW">New Email Address</label>
|
||||
<label for="EMAIL_NEW">{{#translate}}New Email Address{{/translate}}</label>
|
||||
<input type="email" name="EMAIL_NEW" id="email-new" placeholder="Your new email address" value="{{email}}">
|
||||
</div>
|
||||
|
||||
<p>
|
||||
You will receive a confirmation request to your new email address that you need to accept before your email is actually changed
|
||||
{{#translate}}You will receive a confirmation request to your new email address that you need to accept before your email is actually changed{{/translate}}
|
||||
</p>
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">Update Email Address</button>
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Update Email Address{{/translate}}</button>
|
||||
|
||||
</form>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
{{> subscription_custom_fields}}
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">Update Profile</button>
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Update Profile{{/translate}}</button>
|
||||
</form>
|
||||
|
||||
<script src="/moment/moment.min.js"></script>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
{{> subscription_custom_fields}}
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">Subscribe to list</button>
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Subscribe to list{{/translate}}</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
<input type="hidden" name="ucid" value="{{ucid}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email address</label>
|
||||
<label for="email">{{#translate}}Email address{{/translate}}</label>
|
||||
<input type="email" name="email" id="email" placeholder="" value="{{email}}" readonly>
|
||||
</div>
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">Unsubscribe</button>
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Unsubscribe{{/translate}}</button>
|
||||
</form>
|
||||
|
||||
|
|
18
server/views/users/password-reset-html.hbs
Normal file
18
server/views/users/password-reset-html.hbs
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h3>Change your password</h3>
|
||||
|
||||
<p>We have received a password change request for your Mailtrain account: <strong>{{username}}</strong>.</p>
|
||||
|
||||
<p><a href="{{confirmUrl}}" role="button">Reset password</a></p>
|
||||
|
||||
<p>If you did not ask to change your password, then you can ignore this email and your password will not be changed.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
9
server/views/users/password-reset-text.hbs
Normal file
9
server/views/users/password-reset-text.hbs
Normal file
|
@ -0,0 +1,9 @@
|
|||
{{{title}}}
|
||||
Change your password
|
||||
====================
|
||||
|
||||
We have received a password change request for your Mailtrain account: ({{{username}}}).
|
||||
|
||||
Reset password: {{{confirmUrl}}}
|
||||
|
||||
If you did not ask to change your password, then you can ignore this email and your password will not be changed.
|
|
@ -41,27 +41,26 @@ function convertToFake(dict) {
|
|||
|
||||
// The langugage labels below are intentionally not localized so that they are always native in the langugae of their speaker (regardless of the currently selected language)
|
||||
const langCodes = {
|
||||
en_US: {
|
||||
'en-US': {
|
||||
getShortLabel: t => 'EN',
|
||||
getLabel: t => 'English',
|
||||
shortCode: 'en',
|
||||
longCode: 'en_US'
|
||||
longCode: 'en-US'
|
||||
},
|
||||
es: {
|
||||
'es-ES': {
|
||||
getShortLabel: t => 'ES',
|
||||
getLabel: t => 'Español',
|
||||
shortCode: 'es',
|
||||
longCode: 'es'
|
||||
longCode: 'es-ES'
|
||||
},
|
||||
fake: {
|
||||
getShortLabel: t => 'FAKE',
|
||||
'fk-FK': {
|
||||
getShortLabel: t => 'FK',
|
||||
getLabel: t => 'Fake',
|
||||
shortCode: 'fake',
|
||||
longCode: 'fake'
|
||||
longCode: 'fk-FK'
|
||||
}
|
||||
}
|
||||
|
||||
langCodes.en = langCodes['en-US'] = langCodes.en_US;
|
||||
function getLang(lng) {
|
||||
return langCodes[lng];
|
||||
}
|
||||
|
||||
module.exports.convertToFake = convertToFake;
|
||||
module.exports.langCodes = langCodes;
|
||||
module.exports.getLang = getLang;
|
Loading…
Reference in a new issue