Extracted strings and fixes on localization support

Language chooser in the UI
This commit is contained in:
Tomas Bures 2018-11-18 21:31:22 +01:00
parent 9f449c0a2f
commit dc7789c17b
126 changed files with 2919 additions and 2028 deletions

View file

@ -1,10 +1,10 @@
'use strict';
import React, {Component} from 'react';
import { withNamespaces } from 'react-i18next';
import { withTranslation } from './lib/i18n';
import { requiresAuthenticatedUser } from './lib/page';
@withNamespaces()
@withTranslation()
@requiresAuthenticatedUser
export default class List extends Component {
constructor(props) {
@ -16,7 +16,7 @@ export default class List extends Component {
return (
<div>
<h2>{t('home.welcome')}</h2>
<h2>{t('welcomeToMailtrain')}</h2>
<div>TODO: some dashboard</div>
</div>
);

View file

@ -1,15 +1,23 @@
'use strict';
import React, { Component } from 'react';
import { translate, Trans } from 'react-i18next';
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import URL from 'url-parse';
import axios from '../lib/axios';
import { Button } from '../lib/bootstrap-components';
import { getUrl } from "../lib/urls";
import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n';
import {Trans} from 'react-i18next';
import {
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../lib/page'
import {
withAsyncErrorHandler,
withErrorHandling
} from '../lib/error-handling';
import axios
from '../lib/axios';
import {Button} from '../lib/bootstrap-components';
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -49,156 +57,156 @@ export default class API extends Component {
let accessTokenMsg;
if (this.state.accessToken) {
accessTokenMsg = <div>{t('Personal access token') + ': '}<code>{accessToken}</code></div>;
accessTokenMsg = <div>{t('personalAccessToken') + ': '}<code>{accessToken}</code></div>;
} else {
accessTokenMsg = <div>{t('Access token not yet generated')}</div>;
accessTokenMsg = <div>{t('accessTokenNotYetGenerated')}</div>;
}
return (
<div>
<Title>{t('API')}</Title>
<Title>{t('api')}</Title>
<div className="panel panel-default">
<div className="panel-body">
<div className="pull-right">
<Button label={this.state.accessToken ? t('Reset Access Token') : t('Generate Access Token')} icon="retweet" className="btn-info" onClickAsync={::this.resetAccessToken} />
<Button label={this.state.accessToken ? t('resetAccessToken') : t('generateAccessToken')} icon="retweet" className="btn-info" onClickAsync={::this.resetAccessToken} />
</div>
{accessTokenMsg}
</div>
</div>
<div className="well">
<h3>{t('Notes about the API')}</h3>
<h3>{t('notesAboutTheApi')}</h3>
<ul>
<li>
<Trans>API response is a JSON structure with <code>error</code> and <code>data</code> properties. If the response <code>error</code> has a value set then the request failed.</Trans>
<Trans i18nKey="apiResponseIsAJsonStructureWithErrorAnd">API response is a JSON structure with <code>error</code> and <code>data</code> properties. If the response <code>error</code> has a value set then the request failed.</Trans>
</li>
<li>
<Trans>You need to define proper <code>Content-Type</code> when making a request. You can either use <code>application/x-www-form-urlencoded</code> for normal form data or <code>application/json</code> for a JSON payload. Using <code>multipart/form-data</code> is not supported.</Trans>
<Trans i18nKey="youNeedToDefineProperContentTypeWhen">You need to define proper <code>Content-Type</code> when making a request. You can either use <code>application/x-www-form-urlencoded</code> for normal form data or <code>application/json</code> for a JSON payload. Using <code>multipart/form-data</code> is not supported.</Trans>
</li>
</ul>
</div>
<h3>POST /api/subscribe/:listId {t('Add subscription')}</h3>
<h3>POST /api/subscribe/:listId {t('addSubscription')}</h3>
<p>
{t('This API call either inserts a new subscription or updates existing. Fields not included are left as is, so if you update only LAST_NAME value, then FIRST_NAME is kept untouched for an existing subscription.')}
{t('thisApiCallEitherInsertsANewSubscription')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('your personal access token')}</li>
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}</li>
</ul>
<p>
<strong>POST</strong> {t('arguments')}
</p>
<ul>
<li><strong>EMAIL</strong> {t('subscriber\'s email address')} (<em>{t('required')}</em>)</li>
<li><strong>FIRST_NAME</strong> {t('subscriber\'s first name')}</li>
<li><strong>LAST_NAME</strong> {t('subscriber\'s last name')}</li>
<li><strong>TIMEZONE</strong> {t('subscriber\'s timezone (eg. "Europe/Tallinn", "PST" or "UTC"). If not set defaults to "UTC"')}</li>
<li><strong>MERGE_TAG_VALUE</strong> {t('custom field value. Use yes/no for option group values (checkboxes, radios, drop downs)')}</li>
<li><strong>EMAIL</strong> {t('subscribersEmailAddress')} (<em>{t('required')}</em>)</li>
<li><strong>FIRST_NAME</strong> {t('subscribersFirstName')}</li>
<li><strong>LAST_NAME</strong> {t('subscribersLastName')}</li>
<li><strong>TIMEZONE</strong> {t('subscribersTimezoneEgEuropeTallinnPstOr')}</li>
<li><strong>MERGE_TAG_VALUE</strong> {t('customFieldValueUseYesnoForOptionGroup')}</li>
</ul>
<p>
{t('Additional POST arguments')}:
{t('additionalPostArguments')}:
</p>
<ul>
<li>
<strong>FORCE_SUBSCRIBE</strong> {t('set to "yes" if you want to make sure the email is marked as subscribed even if it was previously marked as unsubscribed. If the email was already unsubscribed/blocked then subscription status is not changed')}
<strong>FORCE_SUBSCRIBE</strong> {t('setToYesIfYouWantToMakeSureTheEmailIs')}
by default.
</li>
<li>
<strong>REQUIRE_CONFIRMATION</strong> {t('set to "yes" if you want to send confirmation email to the subscriber before actually marking as subscribed')}
<strong>REQUIRE_CONFIRMATION</strong> {t('setToYesIfYouWantToSendConfirmationEmail')}
</li>
</ul>
<p>
<strong>{t('Example')}</strong>
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/subscribe/B16uVTdW?access_token=${accessToken}`)}' \
--data 'EMAIL=test@example.com&amp;MERGE_CHECKBOX=yes&amp;REQUIRE_CONFIRMATION=yes'</pre>
<h3>POST /api/unsubscribe/:listId {t('Remove subscription')}</h3>
<h3>POST /api/unsubscribe/:listId {t('removeSubscription')}</h3>
<p>
{t('This API call marks a subscription as unsubscribed')}
{t('thisApiCallMarksASubscriptionAs')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('your personal access token')}</li>
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}</li>
</ul>
<p>
<strong>POST</strong> {t('arguments')}
</p>
<ul>
<li><strong>EMAIL</strong> {t('subscriber\'s email address')} (<em>{t('required')}</em>)</li>
<li><strong>EMAIL</strong> {t('subscribersEmailAddress')} (<em>{t('required')}</em>)</li>
</ul>
<p>
<strong>{t('Example')}</strong>
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/unsubscribe/B16uVTdW?access_token=${accessToken}`)}' \
--data 'EMAIL=test@example.com'</pre>
<h3>POST /api/delete/:listId {t('Delete subscription')}</h3>
<h3>POST /api/delete/:listId {t('deleteSubscription')}</h3>
<p>
{t('This API call deletes a subscription')}
{t('thisApiCallDeletesASubscription')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('your personal access token')}</li>
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}</li>
</ul>
<p>
<strong>POST</strong> {t('arguments')}
</p>
<ul>
<li><strong>EMAIL</strong> {t('subscriber\'s email address')} (<em>{t('required')}</em>)</li>
<li><strong>EMAIL</strong> {t('subscribersEmailAddress')} (<em>{t('required')}</em>)</li>
</ul>
<p>
<strong>{t('Example')}</strong>
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/delete/B16uVTdW?access_token=${accessToken}`)}' \
--data 'EMAIL=test@example.com'</pre>
<h3>POST /api/field/:listId {t('Add new custom field')}</h3>
<h3>POST /api/field/:listId {t('addNewCustomField')}</h3>
<p>
{t('This API call creates a new custom field for a list.')}
{t('thisApiCallCreatesANewCustomFieldForA')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('your personal access token')}</li>
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}</li>
</ul>
<p>
<strong>POST</strong> {t('arguments')}
</p>
<ul>
<li><strong>NAME</strong> {t('field name')} (<em>{t('required')}</em>)</li>
<li><strong>TYPE</strong> {t('one of the following types:')}
<li><strong>NAME</strong> {t('fieldName')} (<em>{t('required')}</em>)</li>
<li><strong>TYPE</strong> {t('oneOfTheFollowingTypes')}
<ul>
<li><strong>text</strong> &ndash; Text</li>
<li><strong>website</strong> &ndash; Website</li>
@ -216,112 +224,112 @@ export default class API extends Component {
<li><strong>option</strong> &ndash; Option</li>
</ul>
</li>
<li><strong>GROUP</strong> {t('If the type is \'option\' then you also need to specify the parent element ID')}</li>
<li><strong>GROUP_TEMPLATE</strong> {t('Template for the group element. If not set, then values of the elements are joined with commas')}</li>
<li><strong>VISIBLE</strong> yes/no, {t('if not visible then the subscriber can not view or modify this value at the profile page')}</li>
<li><strong>GROUP</strong> {t('ifTheTypeIsOptionThenYouAlsoNeedTo')}</li>
<li><strong>GROUP_TEMPLATE</strong> {t('templateForTheGroupElementIfNotSetThen')}</li>
<li><strong>VISIBLE</strong> yes/no, {t('ifNotVisibleThenTheSubscriberCanNotView')}</li>
</ul>
<p>
<strong>{t('Example')}</strong>
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/field/B16uVTdW?access_token=${accessToken}`)}' \
--data 'NAME=Birthday&amp;TYPE=birthday-us&amp;VISIBLE=yes'</pre>
<h3>GET /api/blacklist/get {t('Get list of blacklisted emails')}</h3>
<h3>GET /api/blacklist/get {t('getListOfBlacklistedEmails')}</h3>
<p>
{t('This API call get list of blacklisted emails.')}
{t('thisApiCallGetListOfBlacklistedEmails')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('your personal access token')}
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}
<ul>
<li><strong>start</strong> {t('Start position')} (<em>{t('optional, default 0')}</em>)</li>
<li><strong>limit</strong> {t('limit emails count in response')} (<em>{t('optional, default 10000')}</em>)</li>
<li><strong>search</strong> {t('filter by part of email')} (<em>{t('optional, default ""')}</em>)</li>
<li><strong>start</strong> {t('startPosition')} (<em>{t('optionalDefault0')}</em>)</li>
<li><strong>limit</strong> {t('limitEmailsCountInResponse')} (<em>{t('optionalDefault10000')}</em>)</li>
<li><strong>search</strong> {t('filterByPartOfEmail')} (<em>{t('optionalDefault')}</em>)</li>
</ul>
</li>
</ul>
<p>
<strong>{t('Example')}</strong>
<strong>{t('example')}</strong>
</p>
<pre>curl -XGET '{getUrl(`api/blacklist/get?access_token=${accessToken}&limit=10&start=10&search=gmail`)}' </pre>
<h3>POST /api/blacklist/add {t('Add email to blacklist')}</h3>
<h3>POST /api/blacklist/add {t('addEmailToBlacklist')}</h3>
<p>
{t('This API call either add emails to blacklist')}
{t('thisApiCallEitherAddEmailsToBlacklist')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('your personal access token')}</li>
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}</li>
</ul>
<p>
<strong>POST</strong> {t('arguments')}
</p>
<ul>
<li><strong>EMAIL</strong> {t('email address')} (<em>{t('required')}</em>)</li>
<li><strong>EMAIL</strong> {t('emailAddress')} (<em>{t('required')}</em>)</li>
</ul>
<p>
<strong>{t('Example')}</strong>
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/blacklist/add?access_token={accessToken}`)}' \
--data 'EMAIL=test@example.com&amp;'</pre>
<h3>POST /api/blacklist/delete {t('Delete email from blacklist')}</h3>
<h3>POST /api/blacklist/delete {t('deleteEmailFromBlacklist')}</h3>
<p>
{t('This API call either delete emails from blacklist')}
{t('thisApiCallEitherDeleteEmailsFrom')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('your personal access token')}</li>
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}</li>
</ul>
<p>
<strong>POST</strong> {t('arguments')}
</p>
<ul>
<li><strong>EMAIL</strong> {t('email address')} (<em>{t('required')}</em>)</li>
<li><strong>EMAIL</strong> {t('emailAddress')} (<em>{t('required')}</em>)</li>
</ul>
<p>
<strong>{t('Example')}</strong>
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/blacklist/delete?access_token=${accessToken}`)}' \
--data 'EMAIL=test@example.com&amp;'</pre>
<h3>GET /api/lists/:email {t('Get the lists a user has subscribed to')}</h3>
<h3>GET /api/lists/:email {t('getTheListsAUserHasSubscribedTo')}</h3>
<p>
{t('Retrieve the lists that the user with :email has subscribed to.')}
{t('retrieveTheListsThatTheUserWithEmailHas')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('your personal access token')}</li>
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}</li>
</ul>
<p>
<strong>{t('Example')}</strong>
<strong>{t('example')}</strong>
</p>
<pre>curl -XGET '{getUrl(`api/lists/test@example.com?access_token=${accessToken}`)}'</pre>

View file

@ -1,7 +1,8 @@
'use strict';
import React, { Component } from 'react';
import { translate, Trans } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { Trans } from 'react-i18next';
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
import {
withForm, Form, Fieldset, FormSendMethod, InputField, ButtonRow, Button
@ -11,7 +12,7 @@ import passwordValidator from '../../../shared/password-validator';
import interoperableErrors from '../../../shared/interoperable-errors';
import mailtrainConfig from 'mailtrainConfig';
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -57,9 +58,9 @@ export default class Account extends Component {
} else if (emailServerValidation && emailServerValidation.invalid) {
state.setIn(['email', 'error'], t('invalidEmailAddress'));
} else if (emailServerValidation && emailServerValidation.exists) {
state.setIn(['email', 'error'], t('account.emailAlreadyRegistered'));
state.setIn(['email', 'error'], t('theEmailIsAlreadyAssociatedWithAnother'));
} else if (!emailServerValidation) {
state.setIn(['email', 'error'], t('validationInProgress'));
state.setIn(['email', 'error'], t('validationIsInProgress'));
} else {
state.setIn(['email', 'error'], null);
}
@ -68,7 +69,7 @@ export default class Account extends Component {
const name = state.getIn(['name', 'value']);
if (!name) {
state.setIn(['name', 'error'], t('account.fullNameMustNotBeEmpty'));
state.setIn(['name', 'error'], t('fullNameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
@ -88,11 +89,11 @@ export default class Account extends Component {
const currentPasswordServerValidation = state.getIn(['currentPassword', 'serverValidation']);
if (!currentPassword) {
state.setIn(['currentPassword', 'error'], t('account.currentPasswordMustNotBeEmpty'));
state.setIn(['currentPassword', 'error'], t('currentPasswordMustNotBeEmpty'));
} else if (currentPasswordServerValidation && currentPasswordServerValidation.incorrect) {
state.setIn(['currentPassword', 'error'], t('account.incorrectPassword'));
state.setIn(['currentPassword', 'error'], t('incorrectPassword'));
} else if (!currentPasswordServerValidation) {
state.setIn(['email', 'error'], t('validationInProgress'));
state.setIn(['email', 'error'], t('validationIsInProgress'));
} else {
state.setIn(['currentPassword', 'error'], null);
}
@ -104,7 +105,7 @@ export default class Account extends Component {
}
state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null);
state.setIn(['password2', 'error'], password !== password2 ? t('account.passwordsMustMatch') : null);
state.setIn(['password2', 'error'], password !== password2 ? t('passwordsMustMatch') : null);
}
async submitHandler() {
@ -112,14 +113,14 @@ export default class Account extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('account.updatingUserProfile'));
this.setFormStatusMessage('info', t('updatingUserProfile'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/account', data => {
delete data.password2;
});
if (submitSuccessful) {
this.setFlashMessage('success', t('account.userProfileUpdated'));
this.setFlashMessage('success', t('userProfileUpdated'));
this.hideFormValidation();
this.updateFormValue('password', '');
this.updateFormValue('password2', '');
@ -130,7 +131,7 @@ export default class Account extends Component {
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('errorsInForm'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
if (error instanceof interoperableErrors.IncorrectPasswordError) {
@ -138,8 +139,8 @@ export default class Account extends Component {
this.setFormStatusMessage('danger',
<span>
<strong>{t('updatesCannotBeSaved')}</strong>{' '}
{t('account.passwordPossiblyChanged')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('thePasswordIsIncorrectPossiblyJust')}
</span>
);
@ -152,8 +153,8 @@ export default class Account extends Component {
this.setFormStatusMessage('danger',
<span>
<strong>{t('updatesCannotBeSaved')}</strong>{' '}
{t('account.emailAlreadyRegisteredTryAgain')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('theEmailIsAlreadyAssignedToAnotherUser')}
</span>
);
@ -171,19 +172,19 @@ export default class Account extends Component {
if (mailtrainConfig.isAuthMethodLocal) {
return (
<div>
<Title>{t('root.account')}</Title>
<Title>{t('account')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<Fieldset label={t('account.generalSettings')}>
<InputField id="name" label={t('account.fullName')}/>
<InputField id="email" label={t('email')} help={t('account.addressUsedForAccountRecovery')}/>
<Fieldset label={t('generalSettings')}>
<InputField id="name" label={t('fullName')}/>
<InputField id="email" label={t('email')} help={t('thisAddressIsUsedForAccountRecoveryIn')}/>
</Fieldset>
<Fieldset label={t('account.passwordChange')}>
<p>{t('account.fillOnlyForPasswordChange')}</p>
<InputField id="currentPassword" label={t('account.currentPassword')} type="password" />
<InputField id="password" label={t('account.newPassword')} type="password" />
<InputField id="password2" label={t('account.confirmPassword')} type="password" />
<Fieldset label={t('passwordChange')}>
<p>{t('youOnlyNeedToFillOutThisFormIfYouWantTo')}</p>
<InputField id="currentPassword" label={t('currentPassword')} type="password" />
<InputField id="password" label={t('newPassword')} type="password" />
<InputField id="password2" label={t('confirmPassword')} type="password" />
</Fieldset>
<ButtonRow>
@ -195,11 +196,11 @@ export default class Account extends Component {
} else {
return (
<div>
<Title>{t('root.account')}</Title>
<Title>{t('account')}</Title>
<p>{t('account.accountManagementNotPossible')}</p>
<p>{t('accountManagementIsNotPossibleBecause')}</p>
{mailtrainConfig.externalPasswordResetLink && <p><Trans i18nKey="useThisLinkToChangePassword">If you want to change the password, use <a href={mailtrainConfig.externalPasswordResetLink}>this link</a>.</Trans></p>}
{mailtrainConfig.externalPasswordResetLink && <p><Trans i18nKey="ifYouWantToChangeThePasswordUseThisLink">If you want to change the password, use <a href={mailtrainConfig.externalPasswordResetLink}>this link</a>.</Trans></p>}
</div>
);
}

View file

@ -1,14 +1,14 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { withPageHelpers, Title } from '../lib/page'
import {
withForm, Form, FormSendMethod, InputField, ButtonRow, Button
} from '../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -32,7 +32,7 @@ export default class Forget extends Component {
const username = state.getIn(['usernameOrEmail', 'value']);
if (!username) {
state.setIn(['usernameOrEmail', 'error'], t('Username or email must not be empty'));
state.setIn(['usernameOrEmail', 'error'], t('usernameOrEmailMustNotBeEmpty'));
} else {
state.setIn(['usernameOrEmail', 'error'], null);
}
@ -42,15 +42,15 @@ export default class Forget extends Component {
const t = this.props.t;
this.disableForm();
this.setFormStatusMessage('info', t('Processing ...'));
this.setFormStatusMessage('info', t('processing'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/password-reset-send');
if (submitSuccessful) {
this.navigateToWithFlashMessage('/account/login', 'success', t('If the username / email exists in the system, password reset link will be sent to the registered email.'));
this.navigateToWithFlashMessage('/account/login', 'success', t('ifTheUsernameEmailExistsInTheSystem'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('Please enter your username / email and try again.'));
this.setFormStatusMessage('warning', t('pleaseEnterYourUsernameEmailAndTryAgain'));
}
}
@ -59,17 +59,17 @@ export default class Forget extends Component {
return (
<div>
<Title>{t('Password Reset')}</Title>
<Title>{t('passwordReset')}</Title>
<p>{t('Please provide the username or email address that is registered with your Mailtrain account.')}</p>
<p>{t('pleaseProvideTheUsernameOrEmailAddress')}</p>
<p>{t('We will send you an email that will allow you to reset your password.')}</p>
<p>{t('weWillSendYouAnEmailThatWillAllowYouTo')}</p>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="usernameOrEmail" label={t('Username or email')}/>
<InputField id="usernameOrEmail" label={t('usernameOrEmail')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Send email')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('sendEmail')}/>
</ButtonRow>
</Form>
</div>

View file

@ -1,7 +1,7 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { withPageHelpers, Title } from '../lib/page'
import { Link } from 'react-router-dom'
import {
@ -13,7 +13,7 @@ import interoperableErrors from '../../../shared/interoperable-errors';
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -39,14 +39,14 @@ export default class Login extends Component {
const username = state.getIn(['username', 'value']);
if (!username) {
state.setIn(['username', 'error'], t('User name must not be empty'));
state.setIn(['username', 'error'], t('userNameMustNotBeEmpty'));
} else {
state.setIn(['username', 'error'], null);
}
const password = state.getIn(['password', 'value']);
if (!username) {
state.setIn(['password', 'error'], t('Password must not be empty'));
state.setIn(['password', 'error'], t('passwordMustNotBeEmpty'));
} else {
state.setIn(['password', 'error'], null);
}
@ -57,7 +57,7 @@ export default class Login extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Verifying credentials ...'));
this.setFormStatusMessage('info', t('verifyingCredentials'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/login');
@ -67,7 +67,7 @@ export default class Login extends Component {
/* This ensures we get config for the authenticated user */
window.location = nextUrl;
} else {
this.setFormStatusMessage('warning', t('Please enter your credentials and try again.'));
this.setFormStatusMessage('warning', t('pleaseEnterYourCredentialsAndTryAgain'));
}
} catch (error) {
if (error instanceof interoperableErrors.IncorrectPasswordError) {
@ -75,7 +75,7 @@ export default class Login extends Component {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Invalid username or password.')}</strong>
<strong>{t('invalidUsernameOrPassword')}</strong>
</span>
);
@ -91,22 +91,22 @@ export default class Login extends Component {
let passwordResetLink;
if (mailtrainConfig.isAuthMethodLocal) {
passwordResetLink = <Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('Forgot your password?')}</Link>;
passwordResetLink = <Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('forgotYourPassword?')}</Link>;
} else if (mailtrainConfig.externalPasswordResetLink) {
passwordResetLink = <a href={mailtrainConfig.externalPasswordResetLink}>{t('Forgot your password?')}</a>;
passwordResetLink = <a href={mailtrainConfig.externalPasswordResetLink}>{t('forgotYourPassword?')}</a>;
}
return (
<div>
<Title>{t('Sign in')}</Title>
<Title>{t('signIn')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="username" label={t('Username')}/>
<InputField id="password" label={t('Password')} type="password" />
<CheckBox id="remember" text={t('Remember me')}/>
<InputField id="username" label={t('username')}/>
<InputField id="password" label={t('password')} type="password" />
<CheckBox id="remember" text={t('rememberMe')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Sign in')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('signIn')}/>
{passwordResetLink}
</ButtonRow>
</Form>

View file

@ -1,7 +1,7 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { withPageHelpers, Title } from '../lib/page'
import { Link } from 'react-router-dom'
import {
@ -19,7 +19,7 @@ const ResetTokenValidationState = {
INVALID: 2
};
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -82,7 +82,7 @@ export default class Account extends Component {
}
state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null);
state.setIn(['password2', 'error'], password !== password2 ? t('Passwords must match') : null);
state.setIn(['password2', 'error'], password !== password2 ? t('passwordsMustMatch') : null);
}
async submitHandler() {
@ -90,24 +90,24 @@ export default class Account extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Resetting password ...'));
this.setFormStatusMessage('info', t('resettingPassword'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/password-reset', data => {
delete data.password2;
});
if (submitSuccessful) {
this.navigateToWithFlashMessage('/account/login', 'success', t('Password reset'));
this.navigateToWithFlashMessage('/account/login', 'success', t('passwordReset-1'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
if (error instanceof interoperableErrors.InvalidTokenError) {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Your password cannot be reset.')}</strong>{' '}
{t('The password reset token has expired.')}{' '}<Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('Click here to request a new password reset link.')}</Link>
<strong>{t('yourPasswordCannotBeReset')}</strong>{' '}
{t('thePasswordResetTokenHasExpired')}{' '}<Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('clickHereToRequestANewPasswordResetLink')}</Link>
</span>
);
return;
@ -122,29 +122,29 @@ export default class Account extends Component {
if (this.state.resetTokenValidationState === ResetTokenValidationState.PENDING) {
return (
<p>{t('Validating password reset token ...')}</p>
<p>{t('validatingPasswordResetToken')}</p>
);
} else if (this.state.resetTokenValidationState === ResetTokenValidationState.INVALID) {
return (
<div>
<Title>{t('The password cannot be reset')}</Title>
<Title>{t('thePasswordCannotBeReset')}</Title>
<p>{t('The password reset token has expired.')}{' '}<Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('Click here to request a new password reset link.')}</Link></p>
<p>{t('thePasswordResetTokenHasExpired')}{' '}<Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('clickHereToRequestANewPasswordResetLink')}</Link></p>
</div>
);
} else {
return (
<div>
<Title>{t('Set new password for') + ' ' + this.getFormValue('username')}</Title>
<Title>{t('setNewPasswordFor') + ' ' + this.getFormValue('username')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="password" label={t('New Password')} type="password"/>
<InputField id="password2" label={t('Confirm Password')} type="password"/>
<InputField id="password" label={t('newPassword')} type="password"/>
<InputField id="password2" label={t('confirmPassword')} type="password"/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Reset password')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('resetPassword')}/>
</ButtonRow>
</Form>
</div>

View file

@ -12,12 +12,12 @@ import mailtrainConfig from 'mailtrainConfig';
function getMenus(t) {
const subPaths = {
login: {
title: t('Sign in'),
title: t('signIn'),
link: '/account/login',
panelComponent: Login,
},
api: {
title: t('API'),
title: t('api'),
link: '/account/api',
panelComponent: API
}
@ -25,14 +25,14 @@ function getMenus(t) {
if (mailtrainConfig.isAuthMethodLocal) {
subPaths.forgot = {
title: t('Password reset'),
title: t('passwordReset-1'),
extraParams: [':username?'],
link: '/account/forgot',
panelComponent: Reset
};
subPaths.reset = {
title: t('Password reset'),
title: t('passwordReset-1'),
extraParams: [':username', ':resetToken'],
link: '/account/reset',
panelComponent: ResetLink
@ -41,7 +41,7 @@ function getMenus(t) {
return {
'account': {
title: t('Account'),
title: t('account'),
link: '/account',
panelComponent: Account,

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from "react";
import {translate} from "react-i18next";
import { withTranslation } from '../lib/i18n';
import {requiresAuthenticatedUser, Title, withPageHelpers} from "../lib/page";
import {withAsyncErrorHandler, withErrorHandling} from "../lib/error-handling";
import {Table} from "../lib/table";
@ -10,7 +10,7 @@ import {Button, Icon} from "../lib/bootstrap-components";
import axios from "../lib/axios";
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -47,13 +47,13 @@ export default class List extends Component {
const emailServerValidation = state.getIn(['email', 'serverValidation']);
if (!email) {
state.setIn(['email', 'error'], t('Email must not be empty'));
state.setIn(['email', 'error'], t('emailMustNotBeEmpty-1'));
} else if (emailServerValidation && emailServerValidation.invalid) {
state.setIn(['email', 'error'], t('Invalid email address.'));
state.setIn(['email', 'error'], t('invalidEmailAddress'));
} else if (emailServerValidation && emailServerValidation.exists) {
state.setIn(['email', 'error'], t('The email is already on blacklist.'));
state.setIn(['email', 'error'], t('theEmailIsAlreadyOnBlacklist'));
} else if (!emailServerValidation) {
state.setIn(['email', 'error'], t('Validation is in progress...'));
state.setIn(['email', 'error'], t('validationIsInProgress'));
} else {
state.setIn(['email', 'error'], null);
}
@ -63,7 +63,7 @@ export default class List extends Component {
const t = this.props.t;
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/blacklist');
@ -77,7 +77,7 @@ export default class List extends Component {
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and try again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd-1'));
}
}
@ -95,11 +95,11 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 0, title: t('Email') },
{ data: 0, title: t('email') },
{
actions: data => [
{
label: <Icon icon="remove" title={t('Remove from blacklist')}/>,
label: <Icon icon="remove" title={t('removeFromBlacklist')}/>,
action: () => this.deleteBlacklisted(data[0])
}
]
@ -108,20 +108,20 @@ export default class List extends Component {
return (
<div>
<Title>{t('Blacklist')}</Title>
<Title>{t('blacklist')}</Title>
<h3 className="legend">{t('Add Email to Blacklist')}</h3>
<h3 className="legend">{t('addEmailToBlacklist-1')}</h3>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="email" label={t('Email')}/>
<InputField id="email" label={t('email')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Add to Blacklist')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('addToBlacklist')}/>
</ButtonRow>
</Form>
<hr/>
<h3 className="legend">{t('Blacklisted Emails')}</h3>
<h3 className="legend">{t('blacklistedEmails')}</h3>
<Table ref={node => this.blacklistTable = node} withHeader dataUrl="rest/blacklist-table" columns={columns} />
</div>

View file

@ -6,7 +6,7 @@ import List from "./List";
function getMenus(t) {
return {
'blacklist': {
title: t('Blacklist'),
title: t('blacklist'),
link: '/blacklist',
panelComponent: List,
}

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -53,7 +53,7 @@ import moment from 'moment';
import {getMailerTypes} from "../send-configurations/helpers";
import {getCampaignLabels} from "./helpers";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -71,23 +71,23 @@ export default class CUD extends Component {
this.campaignTypeLabels = campaignTypeLabels;
this.createTitles = {
[CampaignType.REGULAR]: t('Create Regular Campaign'),
[CampaignType.RSS]: t('Create RSS Campaign'),
[CampaignType.TRIGGERED]: t('Create Triggered Campaign'),
[CampaignType.REGULAR]: t('createRegularCampaign'),
[CampaignType.RSS]: t('createRssCampaign'),
[CampaignType.TRIGGERED]: t('createTriggeredCampaign'),
};
this.editTitles = {
[CampaignType.REGULAR]: t('Edit Regular Campaign'),
[CampaignType.RSS]: t('Edit RSS Campaign'),
[CampaignType.TRIGGERED]: t('Edit Triggered Campaign'),
[CampaignType.REGULAR]: t('editRegularCampaign'),
[CampaignType.RSS]: t('editRssCampaign'),
[CampaignType.TRIGGERED]: t('editTriggeredCampaign'),
};
this.sourceLabels = {
[CampaignSource.TEMPLATE]: t('Template'),
[CampaignSource.CUSTOM_FROM_TEMPLATE]: t('Custom content cloned from template'),
[CampaignSource.CUSTOM_FROM_CAMPAIGN]: t('Custom content cloned from another campaign'),
[CampaignSource.CUSTOM]: t('Custom content'),
[CampaignSource.URL]: t('URL')
[CampaignSource.TEMPLATE]: t('template'),
[CampaignSource.CUSTOM_FROM_TEMPLATE]: t('customContentClonedFromTemplate'),
[CampaignSource.CUSTOM_FROM_CAMPAIGN]: t('customContentClonedFromAnotherCampaign'),
[CampaignSource.CUSTOM]: t('customContent'),
[CampaignSource.URL]: t('url')
};
this.sourceOptions = [];
@ -260,15 +260,15 @@ export default class CUD extends Component {
}
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
}
if (!state.getIn(['send_configuration', 'value'])) {
state.setIn(['send_configuration', 'error'], t('Send configuration must be selected'));
state.setIn(['send_configuration', 'error'], t('sendConfigurationMustBeSelected'));
}
if (state.getIn(['from_email_overriden', 'value']) && !state.getIn(['from_email_override', 'value'])) {
state.setIn(['from_email_override', 'error'], t('"From" email must not be empty'));
state.setIn(['from_email_override', 'error'], t('fromEmailMustNotBeEmpty'));
}
@ -278,19 +278,19 @@ export default class CUD extends Component {
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
if (!state.getIn(['data_sourceTemplate', 'value'])) {
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
state.setIn(['data_sourceTemplate', 'error'], t('templateMustBeSelected'));
}
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
if (!state.getIn(['data_sourceCampaign', 'value'])) {
state.setIn(['data_sourceCampaign', 'error'], t('Campaign must be selected'));
state.setIn(['data_sourceCampaign', 'error'], t('campaignMustBeSelected'));
}
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) {
// The type is used only in create form. In case of CUSTOM_FROM_TEMPLATE or CUSTOM_FROM_CAMPAIGN, it is determined by the source template, so no need to check it here
const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']);
if (!customTemplateTypeKey) {
state.setIn(['data_sourceCustom_type', 'error'], t('Type must be selected'));
state.setIn(['data_sourceCustom_type', 'error'], t('typeMustBeSelected'));
}
if (customTemplateTypeKey) {
@ -299,13 +299,13 @@ export default class CUD extends Component {
} else if (sourceTypeKey === CampaignSource.URL) {
if (!state.getIn(['data_sourceUrl', 'value'])) {
state.setIn(['data_sourceUrl', 'error'], t('URL must not be empty'));
state.setIn(['data_sourceUrl', 'error'], t('urlMustNotBeEmpty'));
}
}
if (campaignTypeKey === CampaignType.RSS) {
if (!state.getIn(['data_feedUrl', 'value'])) {
state.setIn(['data_feedUrl', 'error'], t('RSS feed URL must be given'));
state.setIn(['data_feedUrl', 'error'], t('rssFeedUrlMustBeGiven'));
}
}
@ -313,12 +313,12 @@ export default class CUD extends Component {
const prefix = 'lists_' + lstUid + '_';
if (!state.getIn([prefix + 'list', 'value'])) {
state.setIn([prefix + 'list', 'error'], t('List must be selected'));
state.setIn([prefix + 'list', 'error'], t('listMustBeSelected'));
}
if (campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) {
if (state.getIn([prefix + 'useSegmentation', 'value']) && !state.getIn([prefix + 'segment', 'value'])) {
state.setIn([prefix + 'segment', 'error'], t('Segment must be selected'));
state.setIn([prefix + 'segment', 'error'], t('segmentMustBeSelected'));
}
}
}
@ -340,7 +340,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.source = Number.parseInt(data.source);
@ -403,15 +403,15 @@ export default class CUD extends Component {
if (submitResponse) {
const sourceTypeKey = Number.parseInt(this.getFormValue('source'));
if (this.props.entity) {
this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
this.navigateToWithFlashMessage('/campaigns', 'success', t('campaignSaved'));
} else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/content`, 'success', t('Campaign saved'));
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/content`, 'success', t('campaignSaved'));
} else {
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/status`, 'success', t('Campaign saved'));
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/status`, 'success', t('campaignSaved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -467,19 +467,19 @@ export default class CUD extends Component {
const campaignTypeKey = this.getFormValue('type');
if (campaignTypeKey === CampaignType.RSS) {
extraSettings = <InputField id="data_feedUrl" label={t('RSS Feed Url')}/>
extraSettings = <InputField id="data_feedUrl" label={t('rssFeedUrl')}/>
}
const listsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Subscribers') },
{ data: 4, title: t('Description') },
{ data: 5, title: t('Namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('subscribers') },
{ data: 4, title: t('description') },
{ data: 5, title: t('namespace') }
];
const segmentsColumns = [
{ data: 1, title: t('Name') }
{ data: 1, title: t('name') }
];
const lstsEditEntries = [];
@ -498,21 +498,21 @@ export default class CUD extends Component {
<Button
className="btn-default"
icon="remove"
title={t('Remove')}
title={t('remove')}
onClickAsync={() => this.onRemoveListEntry(lstUid)}
/>
}
<Button
className="btn-default"
icon="plus"
title={t('Insert new entry before this one')}
title={t('insertNewEntryBeforeThisOne')}
onClickAsync={() => this.onAddListEntry(lstOrderIdxClosure)}
/>
{lstOrderIdx > 0 &&
<Button
className="btn-default"
icon="chevron-up"
title={t('Move up')}
title={t('moveUp')}
onClickAsync={() => this.onListEntryMoveUp(lstOrderIdxClosure)}
/>
}
@ -520,17 +520,17 @@ export default class CUD extends Component {
<Button
className="btn-default"
icon="chevron-down"
title={t('Move down')}
title={t('moveDown')}
onClickAsync={() => this.onListEntryMoveDown(lstOrderIdxClosure)}
/>
}
</div>
<div className={campaignsStyles.entryContent}>
<TableSelect id={prefix + 'list'} label={t('List')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
<TableSelect id={prefix + 'list'} label={t('list')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
{(campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) &&
<div>
<CheckBox id={prefix + 'useSegmentation'} label={t('Segment')} text={t('Use a particular segment')}/>
<CheckBox id={prefix + 'useSegmentation'} label={t('segment')} text={t('useAParticularSegment')}/>
{selectedList && this.getFormValue(prefix + 'useSegmentation') &&
<TableSelect id={prefix + 'segment'} withHeader dropdown dataUrl={`rest/segments-table/${selectedList}`} columns={segmentsColumns} selectionLabelIndex={1} />
}
@ -544,13 +544,13 @@ export default class CUD extends Component {
}
const lstsEdit =
<Fieldset label={t('Lists')}>
<Fieldset label={t('lists')}>
{lstsEditEntries}
<div key="newEntry" className={campaignsStyles.newEntry}>
<Button
className="btn-default"
icon="plus"
label={t('Add list')}
label={t('addList')}
onClickAsync={() => this.onAddListEntry(lsts.length)}
/>
</div>
@ -558,12 +558,12 @@ export default class CUD extends Component {
const sendConfigurationsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') }
];
let sendSettings;
@ -572,7 +572,7 @@ export default class CUD extends Component {
sendSettings = [];
const addOverridable = (id, label) => {
sendSettings.push(<CheckBox key={id + '_overriden'} id={id + '_overriden'} label={label} text={t('Override')}/>);
sendSettings.push(<CheckBox key={id + '_overriden'} id={id + '_overriden'} label={label} text={t('override')}/>);
if (this.getFormValue(id + '_overriden')) {
sendSettings.push(<InputField key={id + '_override'} id={id + '_override'}/>);
@ -585,12 +585,12 @@ export default class CUD extends Component {
}
};
addOverridable('from_name', t('"From" name'));
addOverridable('from_email', t('"From" email address'));
addOverridable('reply_to', t('"Reply-to" email address'));
addOverridable('subject', t('"Subject" line'));
addOverridable('from_name', t('fromName'));
addOverridable('from_email', t('fromEmailAddress'));
addOverridable('reply_to', t('replytoEmailAddress'));
addOverridable('subject', t('subjectLine'));
} else {
sendSettings = <AlignedRow>{t('Loading send configuration ...')}</AlignedRow>
sendSettings = <AlignedRow>{t('loadingSendConfiguration')}</AlignedRow>
}
} else {
sendSettings = null;
@ -600,42 +600,42 @@ export default class CUD extends Component {
let sourceEdit = null;
if (isEdit) {
if (!(sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
sourceEdit = <StaticField id="source" className={styles.formDisabled} label={t('Content source')}>{this.sourceLabels[sourceTypeKey]}</StaticField>;
sourceEdit = <StaticField id="source" className={styles.formDisabled} label={t('contentSource')}>{this.sourceLabels[sourceTypeKey]}</StaticField>;
}
} else {
sourceEdit = <Dropdown id="source" label={t('Content source')} options={this.sourceOptions}/>
sourceEdit = <Dropdown id="source" label={t('contentSource')} options={this.sourceOptions}/>
}
let templateEdit = null;
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
const templatesColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') },
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('namespace') },
];
let help = null;
if (sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
help = t('Selecting a template creates a campaign specific copy from it.');
help = t('selectingATemplateCreatesACampaign');
}
// The "key" property here and in the TableSelect below is to tell React that these tables are different and should be rendered by different instances. Otherwise, React will use
// only one instance, which fails because Table does not handle updates in "columns" property
templateEdit = <TableSelect key="templateSelect" id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>;
templateEdit = <TableSelect key="templateSelect" id="data_sourceTemplate" label={t('template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>;
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
const campaignsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.campaignTypeLabels[data] },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') }
];
templateEdit = <TableSelect key="campaignSelect" id="data_sourceCampaign" label={t('Campaign')} withHeader dropdown dataUrl='rest/campaigns-with-content-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('Content of the selected campaign will be copied into this campaign.')}/>;
templateEdit = <TableSelect key="campaignSelect" id="data_sourceCampaign" label={t('campaign')} withHeader dropdown dataUrl='rest/campaigns-with-content-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('contentOfTheSelectedCampaignWillBeCopied')}/>;
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) {
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
@ -647,21 +647,21 @@ export default class CUD extends Component {
}
templateEdit = <div>
<Dropdown id="data_sourceCustom_type" label={t('Type')} options={this.customTemplateTypeOptions}/>
<Dropdown id="data_sourceCustom_type" label={t('type')} options={this.customTemplateTypeOptions}/>
{customTemplateTypeForm}
</div>;
} else if (sourceTypeKey === CampaignSource.URL) {
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')} help={t('If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.')}/>
templateEdit = <InputField id="data_sourceUrl" label={t('renderUrl')} help={t('ifAMessageIsSentThenThisUrlWillBePosTed')}/>
}
let saveButtonLabel;
if (isEdit) {
saveButtonLabel = t('Save');
saveButtonLabel = t('save');
} else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
saveButtonLabel = t('Save and edit content');
saveButtonLabel = t('saveAndEditContent');
} else {
saveButtonLabel = t('Save campaign and go to status');
saveButtonLabel = t('saveCampaignAndGoToStatus');
}
@ -675,28 +675,28 @@ export default class CUD extends Component {
deleteUrl={`rest/campaigns/${this.props.entity.id}`}
backUrl={`/campaigns/${this.props.entity.id}/edit`}
successUrl="/campaigns"
deletingMsg={t('Deleting campaign ...')}
deletedMsg={t('Campaign deleted')}/>
deletingMsg={t('deletingCampaign')}
deletedMsg={t('campaignDeleted')}/>
}
<Title>{isEdit ? this.editTitles[this.getFormValue('type')] : this.createTitles[this.getFormValue('type')]}</Title>
{isEdit && this.props.entity.status === CampaignStatus.SENDING &&
<div className={`alert alert-info`} role="alert">
{t('Form cannot be edited because the campaign is currently being sent out. Wait till the sending is finished and refresh.')}
{t('formCannotBeEditedBecauseTheCampaignIs')}
</div>
}
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<InputField id="name" label={t('name')}/>
{isEdit &&
<StaticField id="cid" className={styles.formDisabled} label={t('ID')} help={t('This is the campaign ID displayed to the subscribers')}>
<StaticField id="cid" className={styles.formDisabled} label={t('id')} help={t('thisIsTheCampaignIdDisplayedToThe')}>
{this.getFormValue('cid')}
</StaticField>
}
<TextArea id="description" label={t('Description')}/>
<TextArea id="description" label={t('description')}/>
{extraSettings}
@ -708,16 +708,16 @@ export default class CUD extends Component {
<hr/>
<TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
<TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
{sendSettings}
<InputField id="unsubscribe_url" label={t('Custom unsubscribe URL')}/>
<InputField id="unsubscribe_url" label={t('customUnsubscribeUrl')}/>
<hr/>
<CheckBox id="open_trackings_disabled" text={t('Disable opened tracking')}/>
<CheckBox id="click_tracking_disabled" text={t('Disable clicked tracking')}/>
<CheckBox id="open_trackings_disabled" text={t('disableOpenedTracking')}/>
<CheckBox id="click_tracking_disabled" text={t('disableClickedTracking')}/>
{sourceEdit && <hr/> }
@ -727,7 +727,7 @@ export default class CUD extends Component {
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={saveButtonLabel}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.entity.id}/delete`}/> }
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/campaigns/${this.props.entity.id}/delete`}/> }
</ButtonRow>
</Form>
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {
requiresAuthenticatedUser,
Title,
@ -30,7 +30,7 @@ import {getUrl} from "../lib/urls";
import {TestSendModalDialog} from "./TestSendModalDialog";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -94,7 +94,7 @@ export default class CustomContent extends Component {
const url = `rest/campaigns-content/${this.props.entity.id}`;
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
Object.assign(data, exportedData);
@ -116,13 +116,13 @@ export default class CustomContent extends Component {
if (submitResponse) {
if (this.props.entity) {
this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
this.navigateToWithFlashMessage('/campaigns', 'success', t('campaignSaved'));
} else {
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('Campaign saved'));
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('campaignSaved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -195,10 +195,10 @@ export default class CustomContent extends Component {
entity={this.props.entity}
/>
<Title>{t('Edit Custom Content')}</Title>
<Title>{t('editCustomContent')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('Custom template editor')}>
<StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('customTemplateEditor')}>
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
</StaticField>
@ -207,8 +207,8 @@ export default class CustomContent extends Component {
{customTemplateTypeKey && getEditForm(this, customTemplateTypeKey, 'data_sourceCustom_')}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
<Button className="btn-danger" icon="send" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
<Button className="btn-danger" icon="send" label={t('testSend')} onClickAsync={async () => this.setState({showTestSendModal: true})}/>
</ButtonRow>
</Form>
</div>

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {
DropdownMenu,
Icon
@ -32,7 +32,7 @@ import {
tableDeleteDialogRender
} from "../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -73,28 +73,28 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.campaignTypeLabels[data] },
{
data: 5,
title: t('Status'),
title: t('status'),
render: (data, display, rowData) => {
if (data === CampaignStatus.SCHEDULED) {
const scheduled = rowData[6];
if (scheduled && new Date(scheduled) > new Date()) {
return t('Sending scheduled');
return t('sendingScheduled');
} else {
return t('Sending');
return t('sending');
}
} else {
return this.campaignStatusLabels[data];
}
}
},
{ data: 8, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 9, title: t('Namespace') },
{ data: 8, title: t('created'), render: data => moment(data).fromNow() },
{ data: 9, title: t('namespace') },
{
actions: data => {
const actions = [];
@ -104,49 +104,49 @@ export default class List extends Component {
if (perms.includes('viewStats')) {
actions.push({
label: <Icon icon="send" title={t('Status')}/>,
label: <Icon icon="send" title={t('status')}/>,
link: `/campaigns/${data[0]}/status`
});
}
if (perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/campaigns/${data[0]}/edit`
});
}
if (perms.includes('edit') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
actions.push({
label: <Icon icon="align-center" title={t('Content')}/>,
label: <Icon icon="align-center" title={t('content')}/>,
link: `/campaigns/${data[0]}/content`
});
}
if (perms.includes('viewFiles') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
actions.push({
label: <Icon icon="hdd" title={t('Files')}/>,
label: <Icon icon="hdd" title={t('files')}/>,
link: `/campaigns/${data[0]}/files`
});
}
if (perms.includes('viewAttachments')) {
actions.push({
label: <Icon icon="paperclip" title={t('Attachments')}/>,
label: <Icon icon="paperclip" title={t('attachments')}/>,
link: `/campaigns/${data[0]}/attachments`
});
}
if (campaignType === CampaignType.TRIGGERED && perms.includes('viewTriggers')) {
actions.push({
label: <Icon icon="flash" title={t('Triggers')}/>,
label: <Icon icon="flash" title={t('triggers')}/>,
link: `/campaigns/${data[0]}/triggers`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/campaigns/${data[0]}/share`
});
}
@ -160,18 +160,18 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/campaigns`, t('Deleting campaign ...'), t('Campaign deleted'))}
{tableDeleteDialogRender(this, `rest/campaigns`, t('deletingCampaign'), t('campaignDeleted'))}
<Toolbar>
{this.state.createPermitted &&
<DropdownMenu className="btn-primary" label={t('Create Campaign')}>
<MenuLink to="/campaigns/create-regular">{t('Regular')}</MenuLink>
<MenuLink to="/campaigns/create-rss">{t('RSS')}</MenuLink>
<MenuLink to="/campaigns/create-triggered">{t('Triggered')}</MenuLink>
<DropdownMenu className="btn-primary" label={t('createCampaign')}>
<MenuLink to="/campaigns/create-regular">{t('regular')}</MenuLink>
<MenuLink to="/campaigns/create-rss">{t('rss')}</MenuLink>
<MenuLink to="/campaigns/create-triggered">{t('triggered')}</MenuLink>
</DropdownMenu>
}
</Toolbar>
<Title>{t('Campaigns')}</Title>
<Title>{t('campaigns')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/campaigns-table" columns={columns} />
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {
requiresAuthenticatedUser,
Title,
@ -41,7 +41,7 @@ import campaignsStyles from "./styles.scss";
import {tableDeleteDialogAddDeleteButton} from "../lib/modals";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -60,7 +60,7 @@ class TestUser extends Component {
const t = this.props.t;
if (!state.getIn(['testUser', 'value'])) {
state.setIn(['testUser', 'error'], t('Subscription has to be selected to show the campaign for a test user.'))
state.setIn(['testUser', 'error'], t('subscriptionHasToBeSelectedToShowThe'))
} else {
state.setIn(['testUser', 'error'], null);
}
@ -87,25 +87,25 @@ class TestUser extends Component {
const t = this.props.t;
const testUsersColumns = [
{ data: 1, title: t('Email') },
{ data: 2, title: t('Subscription ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('List ID'), render: data => <code>{data}</code> },
{ data: 4, title: t('List') },
{ data: 5, title: t('List namespace') }
{ data: 1, title: t('email') },
{ data: 2, title: t('subscriptionId'), render: data => <code>{data}</code> },
{ data: 3, title: t('listId'), render: data => <code>{data}</code> },
{ data: 4, title: t('list') },
{ data: 5, title: t('listNamespace') }
];
return (
<Form stateOwner={this}>
<TableSelect id="testUser" label={t('Preview campaign as')} withHeader dropdown dataUrl={`rest/campaigns-test-users-table/${this.props.entity.id}`} columns={testUsersColumns} selectionLabelIndex={1} />
<TableSelect id="testUser" label={t('previewCampaignAs')} withHeader dropdown dataUrl={`rest/campaigns-test-users-table/${this.props.entity.id}`} columns={testUsersColumns} selectionLabelIndex={1} />
<ButtonRow>
<Button className="btn-primary" label={t('Preview')} onClickAsync={::this.previewAsync}/>
<Button className="btn-primary" label={t('preview')} onClickAsync={::this.previewAsync}/>
</ButtonRow>
</Form>
);
}
}
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -130,16 +130,16 @@ class SendControls extends Component {
if (state.getIn(['sendLater', 'value'])) {
const dateValue = state.getIn(['date', 'value']).trim();
if (!dateValue) {
state.setIn(['date', 'error'], t('Date must not be empty'));
state.setIn(['date', 'error'], t('dateMustNotBeEmpty'));
} else if (!moment(dateValue, 'YYYY-MM-DD', true).isValid()) {
state.setIn(['date', 'error'], t('Date is invalid'));
state.setIn(['date', 'error'], t('dateIsInvalid'));
}
const timeValue = state.getIn(['time', 'value']).trim();
if (!timeValue) {
state.setIn(['time', 'error'], t('Time must not be empty'));
state.setIn(['time', 'error'], t('timeMustNotBeEmpty'));
} else if (!moment(timeValue, 'HH:mm', true).isValid()) {
state.setIn(['time', 'error'], t('Time is invalid'));
state.setIn(['time', 'error'], t('timeIsInvalid'));
}
}
}
@ -231,28 +231,28 @@ class SendControls extends Component {
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers')})`;
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
return (
<div>
<AlignedRow label={t('Send status')}>
{entity.scheduled ? t('Campaign is scheduled for delivery.') : t('Campaign is ready to be sent out.')}
<AlignedRow label={t('sendStatus')}>
{entity.scheduled ? t('campaignIsScheduledForDelivery') : t('campaignIsReadyToBeSentOut')}
</AlignedRow>
<Form stateOwner={this}>
<CheckBox id="sendLater" label={t('Send later')} text={t('Schedule delivery at a particular date/time')}/>
<CheckBox id="sendLater" label={t('sendLater')} text={t('scheduleDeliveryAtAParticularDatetime')}/>
{this.getFormValue('sendLater') &&
<div>
<DatePicker id="date" label={t('Date')} />
<InputField id="time" label={t('Time')} help={t('Enter 24-hour time in format HH:MM (e.g. 13:48)')}/>
<DatePicker id="date" label={t('date')} />
<InputField id="time" label={t('time')} help={t('enter24hourTimeInFormatHhmmEg1348')}/>
</div>
}
</Form>
<ButtonRow className={campaignsStyles.sendButtonRow}>
{this.getFormValue('sendLater') ?
<Button className="btn-primary" icon="send" label={(entity.scheduled ? t('Reschedule send') : t('Schedule send')) + subscrInfo} onClickAsync={::this.scheduleAsync}/>
<Button className="btn-primary" icon="send" label={(entity.scheduled ? t('rescheduleSend') : t('scheduleSend')) + subscrInfo} onClickAsync={::this.scheduleAsync}/>
:
<Button className="btn-primary" icon="send" label={t('Send') + subscrInfo} onClickAsync={::this.startAsync}/>
<Button className="btn-primary" icon="send" label={t('send') + subscrInfo} onClickAsync={::this.startAsync}/>
}
</ButtonRow>
</div>
@ -261,26 +261,26 @@ class SendControls extends Component {
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
return (
<div>
<AlignedRow label={t('Send status')}>
{t('Campaign is being sent out.')}
<AlignedRow label={t('sendStatus')}>
{t('campaignIsBeingSentOut')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="stop" label={t('Stop')} onClickAsync={::this.stopAsync}/>
<Button className="btn-primary" icon="stop" label={t('stop')} onClickAsync={::this.stopAsync}/>
</ButtonRow>
</div>
);
} else if (entity.status === CampaignStatus.FINISHED) {
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers')})`;
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
return (
<div>
<AlignedRow label={t('Send status')}>
{t('All messages sent! Hit "Continue" if you you want to send this campaign to new subscribers.')}
<AlignedRow label={t('sendStatus')}>
{t('allMessagesSent!HitContinueIfYouYouWant')}
</AlignedRow>
<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}/>
<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}/>
</ButtonRow>
</div>
);
@ -288,11 +288,11 @@ class SendControls extends Component {
} else if (entity.status === CampaignStatus.INACTIVE) {
return (
<div>
<AlignedRow label={t('Send status')}>
{t('Your campaign is currently disabled. Click Enable button to start enable it.')}
<AlignedRow label={t('sendStatus')}>
{t('yourCampaignIsCurrentlyDisabledClick')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="play" label={t('Enable')} onClickAsync={::this.enableAsync}/>
<Button className="btn-primary" icon="play" label={t('enable')} onClickAsync={::this.enableAsync}/>
</ButtonRow>
</div>
);
@ -300,11 +300,11 @@ class SendControls extends Component {
} else if (entity.status === CampaignStatus.ACTIVE) {
return (
<div>
<AlignedRow label={t('Send status')}>
{t('Your campaign is enabled and sending messages.')}
<AlignedRow label={t('sendStatus')}>
{t('yourCampaignIsEnabledAndSendingMessages')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="stop" label={t('Disable')} onClickAsync={::this.disableAsync}/>
<Button className="btn-primary" icon="stop" label={t('disable')} onClickAsync={::this.disableAsync}/>
</ButtonRow>
</div>
);
@ -315,7 +315,7 @@ class SendControls extends Component {
}
}
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -389,26 +389,26 @@ export default class Status extends Component {
sendSettings.push(<AlignedRow key={id} label={label}>{entity[id + '_override'] === null ? this.state.sendConfiguration[id] : entity[id + '_override']}</AlignedRow>);
};
addOverridable('from_name', t('"From" name'));
addOverridable('from_email', t('"From" email address'));
addOverridable('reply_to', t('"Reply-to" email address'));
addOverridable('subject', t('"Subject" line'));
addOverridable('from_name', t('fromName'));
addOverridable('from_email', t('fromEmailAddress'));
addOverridable('reply_to', t('replytoEmailAddress'));
addOverridable('subject', t('subjectLine'));
} else {
sendSettings = <AlignedRow>{t('Loading send configuration ...')}</AlignedRow>
sendSettings = <AlignedRow>{t('loadingSendConfiguration')}</AlignedRow>
}
const listsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 4, title: t('Segment') },
{ data: 3, title: t('List namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 4, title: t('segment') },
{ data: 3, title: t('listNamespace') }
];
const campaignsChildrenColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 5, title: t('Status'), render: (data, display, rowData) => this.campaignStatusLabels[data] },
{ data: 8, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 5, title: t('status'), render: (data, display, rowData) => this.campaignStatusLabels[data] },
{ data: 8, title: t('created'), render: data => moment(data).fromNow() },
{
actions: data => {
const actions = [];
@ -418,7 +418,7 @@ export default class Status extends Component {
if (perms.includes('viewStats')) {
actions.push({
label: <Icon icon="send" title={t('Status')}/>,
label: <Icon icon="send" title={t('status')}/>,
link: `/campaigns/${data[0]}/status`
});
}
@ -430,15 +430,15 @@ export default class Status extends Component {
return (
<div>
<Title>{t('Campaign Status')}</Title>
<Title>{t('campaignStatus')}</Title>
<AlignedRow label={t('Name')}>{entity.name}</AlignedRow>
<AlignedRow label={t('Subscribers')}>{entity.subscriptionsTotal === undefined ? t('computing ...') : entity.subscriptionsTotal}</AlignedRow>
<AlignedRow label={t('Status')}>{this.campaignStatusLabels[entity.status]}</AlignedRow>
<AlignedRow label={t('name')}>{entity.name}</AlignedRow>
<AlignedRow label={t('subscribers')}>{entity.subscriptionsTotal === undefined ? t('computing') : entity.subscriptionsTotal}</AlignedRow>
<AlignedRow label={t('status')}>{this.campaignStatusLabels[entity.status]}</AlignedRow>
{sendSettings}
<AlignedRow label={t('Target lists/segments')}>
<AlignedRow label={t('targetListssegments')}>
<Table withHeader dataUrl={`rest/lists-with-segment-by-campaign-table/${this.props.entity.id}`} columns={listsColumns} />
</AlignedRow>
@ -456,7 +456,7 @@ export default class Status extends Component {
<div>
<hr/>
<h3>RSS Entries</h3>
<p>{t('If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here')}</p>
<p>{t('ifANewEntryIsFoundFromCampaignFeedANew')}</p>
<Table withHeader dataUrl={`rest/campaigns-children/${this.props.entity.id}`} columns={campaignsChildrenColumns} />
</div>
}

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import PropTypes
from 'prop-types';
import {ModalDialog} from "../lib/bootstrap-components";
@ -21,7 +21,7 @@ import {} from '../lib/urls';
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -62,7 +62,7 @@ export class TestSendModalDialog extends Component {
try {
this.hideFormValidation();
this.disableForm();
this.setFormStatusMessage('info', t('Sending test email'));
this.setFormStatusMessage('info', t('sendingTestEmail'));
const data = await this.props.getDataAsync();
@ -93,7 +93,7 @@ export class TestSendModalDialog extends Component {
const t = this.props.t;
if (!state.getIn(['testUser', 'value'])) {
state.setIn(['testUser', 'error'], t('Subscription has to be selected.'))
state.setIn(['testUser', 'error'], t('subscriptionHasToBeSelected'))
} else {
state.setIn(['testUser', 'error'], null);
}
@ -103,20 +103,20 @@ export class TestSendModalDialog extends Component {
const t = this.props.t;
const testUsersColumns = [
{ data: 1, title: t('Email') },
{ data: 2, title: t('Subscription ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('List ID'), render: data => <code>{data}</code> },
{ data: 4, title: t('List') },
{ data: 5, title: t('List namespace') }
{ data: 1, title: t('email') },
{ data: 2, title: t('subscriptionId'), render: data => <code>{data}</code> },
{ data: 3, title: t('listId'), render: data => <code>{data}</code> },
{ data: 4, title: t('list') },
{ data: 5, title: t('listNamespace') }
];
return (
<ModalDialog hidden={!this.props.visible} title={t('Send Test Email')} onCloseAsync={() => this.hideModal()} buttons={[
{ label: t('Send'), className: 'btn-danger', onClickAsync: ::this.performAction },
{ label: t('Cancel'), className: 'btn-primary', onClickAsync: ::this.hideModal }
<ModalDialog hidden={!this.props.visible} title={t('sendTestEmail')} onCloseAsync={() => this.hideModal()} buttons={[
{ label: t('send'), className: 'btn-danger', onClickAsync: ::this.performAction },
{ label: t('cancel'), className: 'btn-primary', onClickAsync: ::this.hideModal }
]}>
<Form stateOwner={this} format="wide">
<TableSelect id="testUser" format="wide" label={t('Subscription')} withHeader dropdown dataUrl={`rest/campaigns-test-users-table/${this.props.entity.id}`} columns={testUsersColumns} selectionLabelIndex={1} />
<TableSelect id="testUser" format="wide" label={t('subscription')} withHeader dropdown dataUrl={`rest/campaigns-test-users-table/${this.props.entity.id}`} columns={testUsersColumns} selectionLabelIndex={1} />
</Form>
</ModalDialog>
);

View file

@ -8,20 +8,20 @@ import {
export function getCampaignLabels(t) {
const campaignTypeLabels = {
[CampaignType.REGULAR]: t('Regular'),
[CampaignType.TRIGGERED]: t('Triggered'),
[CampaignType.RSS]: t('RSS')
[CampaignType.REGULAR]: t('regular'),
[CampaignType.TRIGGERED]: t('triggered'),
[CampaignType.RSS]: t('rss')
};
const campaignStatusLabels = {
[CampaignStatus.IDLE]: t('Idle'),
[CampaignStatus.SCHEDULED]: t('Scheduled'),
[CampaignStatus.PAUSED]: t('Paused'),
[CampaignStatus.FINISHED]: t('Finished'),
[CampaignStatus.PAUSED]: t('Paused'),
[CampaignStatus.INACTIVE]: t('Inactive'),
[CampaignStatus.ACTIVE]: t('Active'),
[CampaignStatus.SENDING]: t('Sending')
[CampaignStatus.IDLE]: t('idle'),
[CampaignStatus.SCHEDULED]: t('scheduled'),
[CampaignStatus.PAUSED]: t('paused'),
[CampaignStatus.FINISHED]: t('finished'),
[CampaignStatus.PAUSED]: t('paused'),
[CampaignStatus.INACTIVE]: t('inactive'),
[CampaignStatus.ACTIVE]: t('active'),
[CampaignStatus.SENDING]: t('sending')
};

View file

@ -19,31 +19,31 @@ import TriggersList from './triggers/List';
function getMenus(t) {
return {
'campaigns': {
title: t('Campaigns'),
title: t('campaigns'),
link: '/campaigns',
panelComponent: CampaignsList,
children: {
':campaignId([0-9]+)': {
title: resolved => t('Campaign "{{name}}"', {name: resolved.campaign.name}),
title: resolved => t('campaignName', {name: resolved.campaign.name}),
resolve: {
campaign: params => `rest/campaigns-settings/${params.campaignId}`
},
link: params => `/campaigns/${params.campaignId}/edit`,
navs: {
status: {
title: t('Status'),
title: t('status'),
link: params => `/campaigns/${params.campaignId}/status`,
visible: resolved => resolved.campaign.permissions.includes('viewStats'),
panelRender: props => <Status entity={props.resolved.campaign} />
},
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/campaigns/${params.campaignId}/edit`,
visible: resolved => resolved.campaign.permissions.includes('edit'),
panelRender: props => <CampaignsCUD action={props.match.params.action} entity={props.resolved.campaign} />
},
content: {
title: t('Content'),
title: t('content'),
link: params => `/campaigns/${params.campaignId}/content`,
resolve: {
campaignContent: params => `rest/campaigns-content/${params.campaignId}`
@ -52,61 +52,61 @@ function getMenus(t) {
panelRender: props => <Content entity={props.resolved.campaignContent} />
},
files: {
title: t('Files'),
title: t('files'),
link: params => `/campaigns/${params.campaignId}/files`,
visible: resolved => resolved.campaign.permissions.includes('viewFiles') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN),
panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the content of the campaign.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="file" managePermission="manageFiles"/>
panelRender: props => <Files title={t('files')} help={t('theseFilesArePubliclyAvailableViaHttpSo')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="file" managePermission="manageFiles"/>
},
attachments: {
title: t('Attachments'),
title: t('attachments'),
link: params => `/campaigns/${params.campaignId}/attachments`,
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to the eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
panelRender: props => <Files title={t('attachments')} help={t('theseFilesWillBeAttachedToTheCampaign')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
},
triggers: {
title: t('Triggers'),
title: t('triggers'),
link: params => `/campaigns/${params.campaignId}/triggers/`,
visible: resolved => resolved.campaign.type === CampaignType.TRIGGERED && resolved.campaign.permissions.includes('viewTriggers'),
panelRender: props => <TriggersList campaign={props.resolved.campaign} />,
children: {
':triggerId([0-9]+)': {
title: resolved => t('Trigger "{{name}}"', {name: resolved.trigger.name}),
title: resolved => t('triggerName', {name: resolved.trigger.name}),
resolve: {
trigger: params => `rest/triggers/${params.campaignId}/${params.triggerId}`,
},
link: params => `/campaigns/${params.campaignId}/triggers/${params.triggerId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/campaigns/${params.campaignId}/triggers/${params.triggerId}/edit`,
panelRender: props => <TriggersCUD action={props.match.params.action} entity={props.resolved.trigger} campaign={props.resolved.campaign} />
}
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <TriggersCUD action="create" campaign={props.resolved.campaign} />
}
}
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/campaigns/${params.campaignId}/share`,
visible: resolved => resolved.campaign.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.campaign} entityTypeId="campaign" />
panelRender: props => <Share title={t('share')} entity={props.resolved.campaign} entityTypeId="campaign" />
}
}
},
'create-regular': {
title: t('Create Regular Campaign'),
title: t('createRegularCampaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.REGULAR} />
},
'create-rss': {
title: t('Create RSS Campaign'),
title: t('createRssCampaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.RSS} />
},
'create-triggered': {
title: t('Create Triggered Campaign'),
title: t('createTriggeredCampaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.TRIGGERED} />
}
}

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -33,7 +33,7 @@ import moment from 'moment';
import {getCampaignLabels} from "../helpers";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -119,22 +119,22 @@ export default class CUD extends Component {
const entityKey = state.getIn(['entity', 'value']);
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
const daysAfter = state.getIn(['daysAfter', 'value']).trim();
if (daysAfter === '') {
state.setIn(['daysAfter', 'error'], t('Values must not be empty'));
state.setIn(['daysAfter', 'error'], t('valuesMustNotBeEmpty'));
} else if (isNaN(daysAfter) || Number.parseInt(daysAfter) < 0) {
state.setIn(['daysAfter', 'error'], t('Value must be a non-negative number'));
state.setIn(['daysAfter', 'error'], t('valueMustBeANonnegativeNumber'));
} else {
state.setIn(['daysAfter', 'error'], null);
}
if (entityKey === Entity.CAMPAIGN && !state.getIn(['source_campaign', 'value'])) {
state.setIn(['source_campaign', 'error'], t('Source campaign must not be empty'));
state.setIn(['source_campaign', 'error'], t('sourceCampaignMustNotBeEmpty'));
} else {
state.setIn(['source_campaign', 'error'], null);
}
@ -154,7 +154,7 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.seconds = Number.parseInt(data.daysAfter) * 3600 * 24;
@ -167,10 +167,10 @@ export default class CUD extends Component {
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/campaigns/${this.props.campaign.id}/triggers`, 'success', t('Trigger saved'));
this.navigateToWithFlashMessage(`/campaigns/${this.props.campaign.id}/triggers`, 'success', t('triggerSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
throw error;
@ -184,12 +184,12 @@ export default class CUD extends Component {
const entityKey = this.getFormValue('entity');
const campaignsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.campaignTypeLabels[data] },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') }
];
const campaignLists = this.props.campaign.lists.map(x => x.list).join(';');
@ -203,35 +203,35 @@ export default class CUD extends Component {
deleteUrl={`rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`}
backUrl={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/edit`}
successUrl={`/campaigns/${this.props.campaign.id}/triggers`}
deletingMsg={t('Deleting trigger ...')}
deletedMsg={t('Trigger deleted')}/>
deletingMsg={t('deletingTrigger')}
deletedMsg={t('triggerDeleted')}/>
}
<Title>{isEdit ? t('Edit Trigger') : t('Create Trigger')}</Title>
<Title>{isEdit ? t('editTrigger') : t('createTrigger')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/>
<Dropdown id="entity" label={t('Entity')} options={this.entityOptions} help={t('Select the type of the trigger rule.')}/>
<Dropdown id="entity" label={t('entity')} options={this.entityOptions} help={t('selectTheTypeOfTheTriggerRule')}/>
<InputField id="daysAfter" label={t('Trigger fires')}/>
<InputField id="daysAfter" label={t('triggerFires')}/>
<AlignedRow>days after:</AlignedRow>
{entityKey === Entity.SUBSCRIPTION && <Dropdown id="subscriptionEvent" label={t('Event')} options={this.eventOptions[Entity.SUBSCRIPTION]} help={t('Select the event that triggers sending the campaign.')}/>}
{entityKey === Entity.SUBSCRIPTION && <Dropdown id="subscriptionEvent" label={t('event')} options={this.eventOptions[Entity.SUBSCRIPTION]} help={t('selectTheEventThatTriggersSendingThe')}/>}
{entityKey === Entity.CAMPAIGN && <Dropdown id="campaignEvent" label={t('Event')} options={this.eventOptions[Entity.CAMPAIGN]} help={t('Select the event that triggers sending the campaign.')}/>}
{entityKey === Entity.CAMPAIGN && <Dropdown id="campaignEvent" label={t('event')} options={this.eventOptions[Entity.CAMPAIGN]} help={t('selectTheEventThatTriggersSendingThe')}/>}
{entityKey === Entity.CAMPAIGN &&
<TableSelect id="source_campaign" label={t('Campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${campaignLists}`} columns={campaignsColumns} selectionLabelIndex={1} />
<TableSelect id="source_campaign" label={t('campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${campaignLists}`} columns={campaignsColumns} selectionLabelIndex={1} />
}
<CheckBox id="enabled" text={t('Enabled')}/>
<CheckBox id="enabled" text={t('enabled')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/delete`}/>}
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -21,7 +21,7 @@ import {
tableDeleteDialogRender
} from "../../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -48,19 +48,19 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
{ data: 4, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[3]][data], searchable: false },
{ data: 5, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
{ data: 6, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('entity'), render: data => this.entityLabels[data], searchable: false },
{ data: 4, title: t('event'), render: (data, cmd, rowData) => this.eventLabels[rowData[3]][data], searchable: false },
{ data: 5, title: t('daysAfter'), render: data => Math.round(data / (3600 * 24)) },
{ data: 6, title: t('enabled'), render: data => data ? t('yes') : t('no'), searchable: false},
{
actions: data => {
const actions = [];
if (mailtrainConfig.globalPermissions.setupAutomation && this.props.campaign.permissions.includes('manageTriggers')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
});
}
@ -76,14 +76,14 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/triggers/${this.props.campaign.id}`, t('Deleting trigger ...'), t('Trigger deleted'))}
{tableDeleteDialogRender(this, `rest/triggers/${this.props.campaign.id}`, t('deletingTrigger'), t('triggerDeleted'))}
{mailtrainConfig.globalPermissions.setupAutomation && this.props.campaign.permissions.includes('manageTriggers') &&
<Toolbar>
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('createTrigger')}/>
</Toolbar>
}
<Title>{t('Triggers')}</Title>
<Title>{t('triggers')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl={`rest/triggers-by-campaign-table/${this.props.campaign.id}`} columns={columns} />
</div>

View file

@ -5,8 +5,8 @@ import {Entity, Event} from '../../../../shared/triggers';
export function getTriggerTypes(t) {
const entityLabels = {
[Entity.SUBSCRIPTION]: t('Subscription'),
[Entity.CAMPAIGN]: t('Campaign')
[Entity.SUBSCRIPTION]: t('subscription'),
[Entity.CAMPAIGN]: t('campaign')
};
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
@ -14,16 +14,16 @@ export function getTriggerTypes(t) {
const eventLabels = {
[Entity.SUBSCRIPTION]: {
[SubscriptionEvent.CREATED]: t('Created'),
[SubscriptionEvent.LATEST_OPEN]: t('Latest open'),
[SubscriptionEvent.LATEST_CLICK]: t('Latest click')
[SubscriptionEvent.CREATED]: t('created'),
[SubscriptionEvent.LATEST_OPEN]: t('latestOpen'),
[SubscriptionEvent.LATEST_CLICK]: t('latestClick')
},
[Entity.CAMPAIGN]: {
[CampaignEvent.DELIVERED]: t('Delivered'),
[CampaignEvent.OPENED]: t('Opened'),
[CampaignEvent.CLICKED]: t('Clicked'),
[CampaignEvent.NOT_OPENED]: t('Not opened'),
[CampaignEvent.NOT_CLICKED]: t('Not clicked')
[CampaignEvent.DELIVERED]: t('delivered'),
[CampaignEvent.OPENED]: t('opened'),
[CampaignEvent.CLICKED]: t('clicked'),
[CampaignEvent.NOT_OPENED]: t('notOpened'),
[CampaignEvent.NOT_CLICKED]: t('notClicked')
}
};

View file

@ -1,11 +1,11 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from './i18n';
import PropTypes from 'prop-types';
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
@translate()
@withTranslation()
@withErrorHandling
class DismissibleAlert extends Component {
static propTypes = {
@ -192,7 +192,7 @@ class ActionLink extends Component {
}
@translate()
@withTranslation()
@withErrorHandling
class ModalDialog extends Component {
constructor(props) {

View file

@ -2,7 +2,7 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import {translate} from "react-i18next";
import { withTranslation } from './i18n';
import {
requiresAuthenticatedUser,
Title
@ -16,7 +16,7 @@ import styles from "./styles.scss";
import {withPageHelpers} from "./page";
import {getUrl, getPublicUrl} from "./urls";
@translate()
@withTranslation()
@withErrorHandling
@withPageHelpers
@requiresAuthenticatedUser
@ -50,22 +50,22 @@ export default class Files extends Component {
const t = this.props.t;
const details = [];
if (response.data.added) {
details.push(t('files.filesAdded', {count: response.data.added}));
details.push(t('countFileAdded', {count: response.data.added}));
}
if (response.data.replaced) {
details.push(t('files.filesReplaced', {count: response.data.replaced}));
details.push(t('countFileReplaced', {count: response.data.replaced}));
}
if (response.data.ignored) {
details.push(t('files.filesIgnored', {count: response.data.ignored}));
details.push(t('countFileIgnored', {count: response.data.ignored}));
}
const detailsMessage = details ? ' (' + details.join(', ') + ')' : '';
return t('files.filesUploaded', {count: response.data.uploaded}) + detailsMessage;
return t('countFileUploaded', {count: response.data.uploaded}) + detailsMessage;
}
onDrop(files){
const t = this.props.t;
if (files.length > 0) {
this.setFlashMessage('info', t('files.uploadingFiles', {count: files.length}));
this.setFlashMessage('info', t('uploadingCountFile', {count: files.length}));
const data = new FormData();
for (const file of files) {
data.append('files[]', file)
@ -76,10 +76,10 @@ export default class Files extends Component {
const message = this.getFilesUploadedMessage(res);
this.setFlashMessage('info', message);
})
.catch(res => this.setFlashMessage('danger', t('files.fileUploadFailed') + ' ' + res.message));
.catch(res => this.setFlashMessage('danger', t('fileUploadFailed') + ' ' + res.message));
}
else{
this.setFlashMessage('info', t('files.noFilesToUpload'));
this.setFlashMessage('info', t('noFilesToUpload'));
}
}
@ -97,13 +97,13 @@ export default class Files extends Component {
await this.hideDeleteFile();
try {
this.setFlashMessage('info', t('files.deletingFile'));
this.setFlashMessage('info', t('deletingFile'));
await axios.delete(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${fileToDeleteId}`));
this.filesTable.refresh();
this.setFlashMessage('info', t('files.fileDeleted'));
this.setFlashMessage('info', t('fileDeleted'));
} catch (err) {
this.filesTable.refresh();
this.setFlashMessage('danger', t('files.deleteFileFailed') + ' ' + err.message);
this.setFlashMessage('danger', t('deleteFileFailed') + ' ' + err.message);
}
}
@ -145,13 +145,13 @@ export default class Files extends Component {
<div>
<ModalDialog
hidden={this.state.fileToDeleteId === null}
title={t('files.confirmFileDeletion')}
title={t('confirmFileDeletion')}
onCloseAsync={::this.hideDeleteFile}
buttons={[
{ label: t('no'), className: 'btn-primary', onClickAsync: ::this.hideDeleteFile },
{ label: t('yes'), className: 'btn-danger', onClickAsync: ::this.performDeleteFile }
]}>
{t('files:areYouSureToDeleteFile', {name: this.state.fileToDeleteName})}
{t('filesareYouSureToDeleteFile', {name: this.state.fileToDeleteName})}
</ModalDialog>
{this.props.title && <Title>{this.props.title}</Title>}
@ -161,7 +161,7 @@ export default class Files extends Component {
{
this.props.entity.permissions.includes(this.props.managePermission) &&
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName={styles.dropZoneActive}>
{state => state.isDragActive ? t('files.dropFiles', {count: state.draggedFiles.length}) : t('files.dropFilesHere')}
{state => state.isDragActive ? t('dropCountFile', {count: state.draggedFiles.length}) : t('dropFilesHere')}
</Dropzone>
}

View file

@ -1,9 +1,9 @@
'use strict';
import React, { Component } from 'react';
import { withTranslation } from './i18n';
import axios, {HTTPMethod} from './axios';
import Immutable from 'immutable';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import interoperableErrors from '../../../shared/interoperable-errors';
import { withPageHelpers } from './page'
@ -36,7 +36,7 @@ const FormState = {
const FormSendMethod = HTTPMethod;
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
class Form extends Component {
@ -462,7 +462,7 @@ class TextArea extends Component {
}
@translate()
@withTranslation()
class DatePicker extends Component {
constructor(props) {
super(props);
@ -568,7 +568,7 @@ class DatePicker extends Component {
<div>
<div className="input-group">
<input type="text" value={selectedDateStr} placeholder={placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}/>
<span className="input-group-addon" onClick={::this.toggleDayPicker}><Icon icon="calendar" title={t('form.openCalendar')}/></span>
<span className="input-group-addon" onClick={::this.toggleDayPicker}><Icon icon="calendar" title={t('openCalendar')}/></span>
</div>
{this.state.opened &&
<div className={styles.dayPickerWrapper}>
@ -712,7 +712,7 @@ class TreeTableSelect extends Component {
}
}
@translate(null, { withRef: true })
@withTranslation({delegateFuns: ['refresh']})
class TableSelect extends Component {
constructor(props) {
super(props);
@ -803,7 +803,7 @@ class TableSelect extends Component {
<input type="text" className="form-control" value={this.state.selectedLabel} onClick={::this.toggleOpen} readOnly={!props.disabled} disabled={props.disabled}/>
{!props.disabled &&
<span className="input-group-btn">
<Button label={t('form.select')} className="btn-default" onClickAsync={::this.toggleOpen}/>
<Button label={t('select')} className="btn-default" onClickAsync={::this.toggleOpen}/>
</span>
}
</div>
@ -824,14 +824,6 @@ class TableSelect extends Component {
}
}
/*
Refreshes the table. This method is provided to allow programmatic refresh from a handler outside the table.
The reference to the table can be obtained by ref.
*/
TableSelect.prototype.refresh = function() {
this.getWrappedInstance().refresh()
};
class ACEEditor extends Component {
static propTypes = {
@ -1305,8 +1297,8 @@ function withForm(target) {
this.disableForm();
this.setFormStatusMessage('danger',
<span>
<strong>{t('form.yourUpdatesCannotBeSaved')}</strong>{' '}
{t('form.modificationsInTheMeantime')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('someoneElseHasIntroducedModificationIn')}
</span>
);
return;
@ -1316,8 +1308,8 @@ function withForm(target) {
this.disableForm();
this.setFormStatusMessage('danger',
<span>
<strong>{t('form.yourUpdatesCannotBeSaved')}</strong>{' '}
{t('form.namespaceDeletedInTheMeantime')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('itSeemsThatSomeoneElseHasDeletedThe')}
</span>
);
return;
@ -1327,8 +1319,8 @@ function withForm(target) {
this.disableForm();
this.setFormStatusMessage('danger',
<span>
<strong>{t('form.yourUpdatesCannotBeSaved')}</strong>{' '}
{t('form.deletionInTheMeantime')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('itSeemsThatSomeoneElseHasDeletedThe-1')}
</span>
);
return;

View file

@ -1,62 +1,43 @@
import i18n from 'i18next';
import { reactI18nextModule } from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "./urls";
'use strict';
import commonEn from "../../../locales/common/en";
import React, {Component} from 'react';
import i18n
from 'i18next';
import {withNamespaces} from "react-i18next";
import LanguageDetector
from 'i18next-browser-languagedetector';
import mailtrainConfig
from 'mailtrainConfig';
function convertToFake(dict) {
function convertValueToFakeLang(str) {
let from = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+\\|`~[{]};:'\",<.>/?";
let to = "ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅX⅄Z0123456789¡@#$%ᵥ⅋⁎()-_=+\\|,~[{]};:,„´<.>/¿";
import hoistStatics
from 'hoist-non-react-statics';
return str.replace(/(\{\{[^\}]+\}\}|%s)/g, '\x00\x04$1\x00').split('\x00').map(c => {
if (c.charAt(0) === '\x04') {
return c;
}
let r = '';
for (let i = 0, len = c.length; i < len; i++) {
let pos = from.indexOf(c.charAt(i));
if (pos < 0) {
r += c.charAt(i);
} else {
r += to.charAt(pos);
}
}
return r;
}).join('\x00').replace(/[\x00\x04]/g, '');
}
import {convertToFake, langCodes} from '../../../shared/langs';
function _convertToFake(dict) {
for (const key in dict) {
const val = dict[key];
import commonEn from "../../../locales/en/common";
import commonEs from "../../../locales/es/common";
if (typeof val === 'string') {
dict[key] = convertValueToFakeLang(val);
} else {
_convertToFake(val);
}
}
}
const resourcesCommon = {
en: commonEn,
es: commonEs,
fake: convertToFake(commonEn)
};
return _convertToFake(dict);
const resources = {};
for (const lng of mailtrainConfig.enabledLanguages) {
const shortCode = langCodes[lng].shortCode;
resources[shortCode] = {
common: resourcesCommon[shortCode]
};
}
i18n
.use(LanguageDetector)
.init({
lng: mailtrainConfig.language,
resources: {
en: {
common: commonEn
},
en_fake: {
common: convertToFake(commonEn)
}
},
resources,
fallbackLng: "en",
fallbackLng: mailtrainConfig.defaultLanguage,
defaultNS: 'common',
interpolation: {
@ -79,4 +60,35 @@ i18n
})
export default i18n;
export default i18n;
export function withTranslation(opts) {
if (opts && opts.delegateFuns) {
return function (WrappedComponent) {
class Wrapper extends Component {
constructor(props) {
super(props);
this.WrappedComponentWithNamespaces = withNamespaces(null, {innerRef: ref => this.wrappedComponent = ref})(WrappedComponent);
}
render() {
const WrappedComponentWithNamespaces = this.WrappedComponentWithNamespaces;
return <WrappedComponentWithNamespaces {...this.props}/>;
}
}
for (const fun of opts.delegateFuns) {
Wrapper.prototype[fun] = function (...args) {
return this.wrappedComponent[fun](...args);
}
}
return hoistStatics(Wrapper, WrappedComponent);
}
} else {
return withNamespaces();
}
}

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import axios, { HTTPMethod } from './axios';
import { translate } from 'react-i18next';
import { withTranslation } from './i18n';
import PropTypes from 'prop-types';
import {
Icon,
@ -14,7 +14,7 @@ import styles from './styles.scss';
import interoperableErrors from '../../../shared/interoperable-errors';
import {Link} from "react-router-dom";
@translate()
@withTranslation()
@withPageHelpers
export class RestActionModalDialog extends Component {
static propTypes = {
@ -97,7 +97,7 @@ export class RestActionModalDialog extends Component {
}
@translate()
@withTranslation()
@withPageHelpers
export class DeleteModalDialog extends Component {
constructor(props) {
@ -145,7 +145,7 @@ export class DeleteModalDialog extends Component {
const name = this.props.name !== undefined ? this.props.name : (owner ? owner.getFormValue('name') : '');
this.setFlashMessage('danger',
<div>
<p>{t('deleteDialog.cannotDeleteDueToDependencies', {name})}</p>
<p>{t('cannoteDeleteNameDueToTheFollowing', {name})}</p>
<ul className={styles.dependenciesList}>
{err.data.dependencies.map(dep =>
dep.link ?
@ -153,7 +153,7 @@ export class DeleteModalDialog extends Component {
: // if no dep.link is present, it means the user has no permission to view the entity, thus only id without the link is shown
<li key={dep.id}>{this.entityTypeLabels[dep.entityTypeId]}: [{dep.id}]</li>
)}
{err.data.andMore && <li>{t('deleteDialog.andMore')}</li>}
{err.data.andMore && <li>{t('andMore')}</li>}
</ul>
</div>
);
@ -180,8 +180,8 @@ export class DeleteModalDialog extends Component {
const name = this.props.name !== undefined ? this.props.name : (owner ? owner.getFormValue('name') : '');
return <RestActionModalDialog
title={t('deleteDialog.confirmDeletion')}
message={t('deleteDialog.areYouSureToDelete', {name})}
title={t('confirmDeletion')}
message={t('areYouSureYouWantToDeleteName?', {name})}
stateOwner={this.props.stateOwner}
visible={this.props.visible}
actionMethod={HTTPMethod.DELETE}

View file

@ -1,11 +1,11 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from './i18n';
import { TreeTableSelect } from './form';
@translate()
@withTranslation()
class NamespaceSelect extends Component {
render() {
const t = this.props.t;
@ -18,7 +18,7 @@ class NamespaceSelect extends Component {
function validateNamespace(t, state) {
if (!state.getIn(['namespace', 'value'])) {
state.setIn(['namespace', 'error'], t('namespace.mustBeSelected'));
state.setIn(['namespace', 'error'], t('namespacemustBeSelected'));
} else {
state.setIn(['namespace', 'error'], null);
}

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from "react";
import {translate} from "react-i18next";
import { withTranslation } from './i18n';
import PropTypes from "prop-types";
import {withRouter} from "react-router";
import {BrowserRouter as Router, Link, Redirect, Route, Switch} from "react-router-dom";
@ -14,6 +14,10 @@ import {getRoutes, needsResolve, resolve, withPageHelpers} from "./page-common";
import {getBaseDir} from "./urls";
class Breadcrumb extends Component {
constructor(props) {
super(props);
}
static propTypes = {
route: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
@ -67,6 +71,7 @@ class Breadcrumb extends Component {
class SecondaryNavBar extends Component {
static propTypes = {
route: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
resolved: PropTypes.object.isRequired,
className: PropTypes.string
@ -144,7 +149,7 @@ class SecondaryNavBar extends Component {
}
}
@translate()
@withTranslation()
@withErrorHandling
class RouteContent extends Component {
constructor(props) {
@ -367,17 +372,10 @@ class SectionContent extends Component {
}
}
@translate()
@withTranslation()
class Section extends Component {
constructor(props) {
super(props);
let structure = props.structure;
if (typeof structure === 'function') {
structure = structure(props.t);
}
this.structure = structure;
}
static propTypes = {
@ -386,9 +384,14 @@ class Section extends Component {
}
render() {
let structure = this.props.structure;
if (typeof structure === 'function') {
structure = structure(this.props.t);
}
return (
<Router basename={getBaseDir()}>
<SectionContent root={this.props.root} structure={this.structure} />
<SectionContent root={this.props.root} structure={structure} />
</Router>
);
}

View file

@ -5,12 +5,8 @@ import './public-path';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {
I18nextProvider,
translate,
} from 'react-i18next';
import i18n
from './i18n';
import {I18nextProvider} from 'react-i18next';
import i18n, {withTranslation} from './i18n';
import {
parentRPC,
UntrustedContentRoot
@ -32,10 +28,10 @@ import {
import CKEditor
from "react-ckeditor-component";
import { initialHeight } from "./sandboxed-ckeditor-shared";
import {initialHeight} from "./sandboxed-ckeditor-shared";
@translate(null, { withRef: true })
@withTranslation()
class CKEditorSandbox extends Component {
constructor(props) {
super(props);

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from './i18n';
import PropTypes
from "prop-types";
import styles
@ -14,7 +14,7 @@ import {getTrustedUrl} from "./urls";
import { initialHeight } from "./sandboxed-ckeditor-shared";
const navbarHeight = 34; // Sync this with navbarheight in sandboxed-ckeditor.scss
@translate(null, { withRef: true })
@withTranslation({delegateFuns: ['exportState']})
export class CKEditorHost extends Component {
constructor(props) {
super(props);
@ -97,9 +97,3 @@ export class CKEditorHost extends Component {
);
}
}
CKEditorHost.prototype.exportState = async function() {
return await this.getWrappedInstance().exportState();
};

View file

@ -5,12 +5,8 @@ import './public-path';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {
I18nextProvider,
translate,
} from 'react-i18next';
import i18n
from './i18n';
import {I18nextProvider} from 'react-i18next';
import i18n, {withTranslation} from './i18n';
import {
parentRPC,
UntrustedContentRoot
@ -28,20 +24,21 @@ import {
base,
unbase
} from "../../../shared/templates";
import brace from 'brace';
import ACEEditorRaw from 'react-ace';
import ACEEditorRaw
from 'react-ace';
import 'brace/theme/github';
import 'brace/ext/searchbox';
import 'brace/mode/html';
import {CodeEditorSourceType} from "./sandboxed-codeeditor-shared";
import mjml2html from "mjml4-in-browser";
import juice from "juice";
import mjml2html
from "mjml4-in-browser";
import juice
from "juice";
const refreshTimeout = 1000;
@translate(null, { withRef: true })
@withTranslation()
class CodeEditorSandbox extends Component {
constructor(props) {
super(props);

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from './i18n';
import PropTypes
from "prop-types";
import styles
@ -11,7 +11,7 @@ import {UntrustedContentHost} from './untrusted';
import {Icon} from "./bootstrap-components";
import {getTrustedUrl} from "./urls";
@translate(null, { withRef: true })
@withTranslation({delegateFuns: ['exportState']})
export class CodeEditorHost extends Component {
constructor(props) {
super(props);
@ -83,7 +83,3 @@ export class CodeEditorHost extends Component {
);
}
}
CodeEditorHost.prototype.exportState = async function() {
return await this.getWrappedInstance().exportState();
};

View file

@ -5,12 +5,8 @@ import './public-path';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {
I18nextProvider,
translate,
} from 'react-i18next';
import i18n
from './i18n';
import {I18nextProvider} from 'react-i18next';
import i18n, {withTranslation} from './i18n';
import {
parentRPC,
UntrustedContentRoot
@ -20,17 +16,18 @@ import PropTypes
import {
getPublicUrl,
getSandboxUrl,
getTrustedUrl,
getUrl
getTrustedUrl
} from "./urls";
import {
base,
unbase
} from "../../../shared/templates";
import mjml2html from "mjml4-in-browser";
import mjml2html
from "mjml4-in-browser";
import 'grapesjs/dist/css/grapes.min.css';
import grapesjs from 'grapesjs';
import grapesjs
from 'grapesjs';
import 'grapesjs-mjml';
@ -39,7 +36,8 @@ import 'grapesjs-preset-newsletter/dist/grapesjs-preset-newsletter.css';
import "./sandboxed-grapesjs.scss";
import axios from './axios';
import axios
from './axios';
import {GrapesJSSourceType} from "./sandboxed-grapesjs-shared";
@ -54,7 +52,7 @@ grapesjs.plugins.add('mailtrain-remove-buttons', (editor, opts = {}) => {
});
@translate(null, { withRef: true })
@withTranslation()
export class GrapesJSSandbox extends Component {
constructor(props) {
super(props);

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from './i18n';
import PropTypes
from "prop-types";
import styles
@ -11,7 +11,7 @@ import {UntrustedContentHost} from './untrusted';
import {Icon} from "./bootstrap-components";
import {getTrustedUrl} from "./urls";
@translate(null, { withRef: true })
@withTranslation({delegateFuns: ['exportState']})
export class GrapesJSHost extends Component {
constructor(props) {
super(props);
@ -73,7 +73,3 @@ export class GrapesJSHost extends Component {
);
}
}
GrapesJSHost.prototype.exportState = async function() {
return await this.getWrappedInstance().exportState();
};

View file

@ -5,12 +5,8 @@ import './public-path';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {
I18nextProvider,
translate,
} from 'react-i18next';
import i18n
from './i18n';
import {I18nextProvider} from 'react-i18next';
import i18n, {withTranslation} from './i18n';
import {
parentRPC,
UntrustedContentRoot
@ -28,7 +24,7 @@ import {
} from "../../../shared/templates";
@translate(null, { withRef: true })
@withTranslation()
class MosaicoSandbox extends Component {
constructor(props) {
super(props);

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from './i18n';
import PropTypes
from "prop-types";
import styles
@ -12,7 +12,7 @@ import {Icon} from "./bootstrap-components";
import {getTrustedUrl} from "./urls";
@translate(null, { withRef: true })
@withTranslation({delegateFuns: ['exportState']})
export class MosaicoHost extends Component {
constructor(props) {
super(props);
@ -76,8 +76,3 @@ export class MosaicoHost extends Component {
);
}
}
MosaicoHost.prototype.exportState = async function() {
return await this.getWrappedInstance().exportState();
};

View file

@ -2,8 +2,8 @@
import React, { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import { withTranslation } from './i18n';
import jQuery from 'jquery';
@ -28,8 +28,7 @@ const TableSelectMode = {
MULTI: 2
};
@translate(null, { withRef: true })
@withTranslation({delegateFuns: ['refresh']})
@withPageHelpers
@withErrorHandling
class Table extends Component {
@ -111,7 +110,7 @@ class Table extends Component {
const count = this.selectionMap.size;
if (this.selectionMap.size > 0) {
const jqInfo = jQuery('<span>' + t('{{ count }} entries selected.', { count }) + ' </span>');
const jqInfo = jQuery('<span>' + t('countEntriesSelected', { count }) + ' </span>');
const jqDeselectLink = jQuery('<a href="">Deselect all.</a>').on('click', ::this.deselectAll);
this.jqSelectInfo.empty().append(jqInfo).append(jqDeselectLink);
@ -404,14 +403,6 @@ class Table extends Component {
}
}
/*
Refreshes the table. This method is provided to allow programmatic refresh from a handler outside the table.
The reference to the table can be obtained by ref.
*/
Table.prototype.refresh = function() {
this.getWrappedInstance().refresh();
};
export {
Table,
TableSelectMode

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import { translate } from 'react-i18next';
import { withTranslation } from './i18n';
import PropTypes from 'prop-types';
import jQuery from 'jquery';
@ -23,7 +23,7 @@ const TreeSelectMode = {
MULTI: 2
};
@translate(null, { withRef: true })
@withTranslation({delegateFuns: ['refresh']})
@withPageHelpers
@withErrorHandling
class TreeTable extends Component {
@ -327,8 +327,8 @@ class TreeTable extends Component {
{props.withHeader &&
<thead>
<tr>
<th className="mt-treetable-title">{t('Name')}</th>
{withDescription && <th>{t('Description')}</th>}
<th className="mt-treetable-title">{t('name')}</th>
{withDescription && <th>{t('description')}</th>}
{actions && <th></th>}
</tr>
</thead>
@ -348,14 +348,6 @@ class TreeTable extends Component {
}
}
/*
Refreshes the table. This method is provided to allow programmatic refresh from a handler outside the table.
The reference to the table can be obtained by ref.
*/
TreeTable.prototype.refresh = function() {
this.getWrappedInstance().refresh();
};
export {
TreeTable,

View file

@ -2,7 +2,7 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import {translate} from "react-i18next";
import { withTranslation } from './i18n';
import {
requiresAuthenticatedUser,
withPageHelpers
@ -170,7 +170,7 @@ export class UntrustedContentHost extends Component {
}
@translate()
@withTranslation()
export class UntrustedContentRoot extends Component {
constructor(props) {
super(props);
@ -243,7 +243,7 @@ export class UntrustedContentRoot extends Component {
} else {
return (
<div>
{t('Loading...')}
{t('loading-1')}
</div>
);
}

View file

@ -1,22 +1,43 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page';
import React, {Component} from 'react';
import PropTypes
from 'prop-types';
import {Trans} from 'react-i18next';
import {withTranslation} from '../lib/i18n';
import {
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
Dropdown, StaticField, CheckBox
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../lib/page';
import {
Button,
ButtonRow,
CheckBox,
Dropdown,
Form,
FormSendMethod,
InputField,
StaticField,
TableSelect,
TextArea,
withForm
} from '../lib/form';
import { withErrorHandling } from '../lib/error-handling';
import { DeleteModalDialog } from '../lib/modals';
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
import { UnsubscriptionMode } from '../../../shared/lists';
import styles from "../lib/styles.scss";
import mailtrainConfig from 'mailtrainConfig';
import {withErrorHandling} from '../lib/error-handling';
import {DeleteModalDialog} from '../lib/modals';
import {
NamespaceSelect,
validateNamespace
} from '../lib/namespace';
import {UnsubscriptionMode} from '../../../shared/lists';
import styles
from "../lib/styles.scss";
import mailtrainConfig
from 'mailtrainConfig';
import {getMailerTypes} from "../send-configurations/helpers";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -64,13 +85,13 @@ export default class CUD extends Component {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
if (state.getIn(['form', 'value']) === 'custom' && !state.getIn(['default_form', 'value'])) {
state.setIn(['default_form', 'error'], t('Custom form must be selected'));
state.setIn(['default_form', 'error'], t('customFormMustBeSelected'));
} else {
state.setIn(['default_form', 'error'], null);
}
@ -91,7 +112,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.form === 'default') {
@ -101,10 +122,10 @@ export default class CUD extends Component {
});
if (submitSuccessful) {
this.navigateToWithFlashMessage('/lists', 'success', t('List saved'));
this.navigateToWithFlashMessage('/lists', 'success', t('listSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -116,50 +137,50 @@ export default class CUD extends Component {
const unsubcriptionModeOptions = [
{
key: UnsubscriptionMode.ONE_STEP,
label: t('One-step (i.e. no email with confirmation link)')
label: t('onestepIeNoEmailWithConfirmationLink')
},
{
key: UnsubscriptionMode.ONE_STEP_WITH_FORM,
label: t('One-step with unsubscription form (i.e. no email with confirmation link)')
label: t('onestepWithUnsubscriptionFormIeNoEmail')
},
{
key: UnsubscriptionMode.TWO_STEP,
label: t('Two-step (i.e. an email with confirmation link will be sent)')
label: t('twostepIeAnEmailWithConfirmationLinkWill')
},
{
key: UnsubscriptionMode.TWO_STEP_WITH_FORM,
label: t('Two-step with unsubscription form (i.e. an email with confirmation link will be sent)')
label: t('twostepWithUnsubscriptionFormIeAnEmail')
},
{
key: UnsubscriptionMode.MANUAL,
label: t('Manual (i.e. unsubscription has to be performed by the list administrator)')
label: t('manualIeUnsubscriptionHasToBePerformedBy')
}
];
const formsOptions = [
{
key: 'default',
label: t('Default Mailtrain Forms')
label: t('defaultMailtrainForms')
},
{
key: 'custom',
label: t('Custom Forms (select form below)')
label: t('customFormsSelectFormBelow')
}
];
const customFormsColumns = [
{data: 0, title: "#"},
{data: 1, title: t('Name')},
{data: 2, title: t('Description')},
{data: 3, title: t('Namespace')}
{data: 1, title: t('name')},
{data: 2, title: t('description')},
{data: 3, title: t('namespace')}
];
const sendConfigurationsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 6, title: t('Namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.mailerTypes[data].typeName },
{ data: 6, title: t('namespace') }
];
return (
@ -171,45 +192,45 @@ export default class CUD extends Component {
deleteUrl={`rest/lists/${this.props.entity.id}`}
backUrl={`/lists/${this.props.entity.id}/edit`}
successUrl="/lists"
deletingMsg={t('Deleting list ...')}
deletedMsg={t('List deleted')}/>
deletingMsg={t('deletingList')}
deletedMsg={t('listDeleted')}/>
}
<Title>{isEdit ? t('Edit List') : t('Create List')}</Title>
<Title>{isEdit ? t('editList') : t('createList')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<InputField id="name" label={t('name')}/>
{isEdit &&
<StaticField id="cid" className={styles.formDisabled} label={t('ID')} help={t('This is the list ID displayed to the subscribers')}>
<StaticField id="cid" className={styles.formDisabled} label={t('id')} help={t('thisIsTheListIdDisplayedToTheSubscribers')}>
{this.getFormValue('cid')}
</StaticField>
}
<TextArea id="description" label={t('Description')}/>
<TextArea id="description" label={t('description')}/>
<InputField id="contact_email" label={t('Contact email')} help={t('Contact email used in subscription forms and emails that are sent out. If not filled in, the admin email from the global settings will be used.')}/>
<InputField id="homepage" label={t('Homepage')} help={t('Homepage URL used in subscription forms and emails that are sent out. If not filled in, the default homepage from global settings will be used.')}/>
<InputField id="to_name" label={t('Recipients name template')} help={t('Specify using merge tags of this list how to construct full name of the recipient. This full name is used as "To" header when sending emails.')}/>
<TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('Send configuration that will be used for sending out subscription-related emails.')}/>
<InputField id="contact_email" label={t('contactEmail')} help={t('contactEmailUsedInSubscriptionFormsAnd')}/>
<InputField id="homepage" label={t('homepage')} help={t('homepageUrlUsedInSubscriptionFormsAnd')}/>
<InputField id="to_name" label={t('recipientsNameTemplate')} help={t('specifyUsingMergeTagsOfThisListHowTo')}/>
<TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('sendConfigurationThatWillBeUsedFor')}/>
<NamespaceSelect/>
<Dropdown id="form" label={t('Forms')} options={formsOptions} help={t('Web and email forms and templates used in subscription management process.')}/>
<Dropdown id="form" label={t('forms')} options={formsOptions} help={t('webAndEmailFormsAndTemplatesUsedIn')}/>
{this.getFormValue('form') === 'custom' &&
<TableSelect id="default_form" label={t('Custom forms')} withHeader dropdown dataUrl='rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} help={<Trans>The custom form used for this list. You can create a form <a href={`/lists/forms/create/${this.props.entity.id}`}>here</a>.</Trans>}/>
<TableSelect id="default_form" label={t('customForms')} withHeader dropdown dataUrl='rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} help={<Trans i18nKey="theCustomFormUsedForThisListYouCanCreate">The custom form used for this list. You can create a form <a href={`/lists/forms/create/${this.props.entity.id}`}>here</a>.</Trans>}/>
}
<CheckBox id="public_subscribe" label={t('Subscription')} text={t('Allow public users to subscribe themselves')}/>
<CheckBox id="public_subscribe" label={t('subscription')} text={t('allowPublicUsersToSubscribeThemselves')}/>
<Dropdown id="unsubscription_mode" label={t('Unsubscription')} options={unsubcriptionModeOptions} help={t('Select how an unsuscription request by subscriber is handled.')}/>
<Dropdown id="unsubscription_mode" label={t('unsubscription')} options={unsubcriptionModeOptions} help={t('selectHowAnUnsuscriptionRequestBy')}/>
<CheckBox id="listunsubscribe_disabled" label={t('Unsubscribe header')} text={t('Do not send List-Unsubscribe headers')}/>
<CheckBox id="listunsubscribe_disabled" label={t('unsubscribeHeader')} text={t('doNotSendListUnsubscribeHeaders')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.entity.id}/delete`}/>}
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/lists/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -1,7 +1,7 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import { Table } from '../lib/table';
@ -15,7 +15,7 @@ import {
tableDeleteDialogRender
} from "../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -52,7 +52,7 @@ export default class List extends Component {
const columns = [
{
data: 1,
title: t('Name'),
title: t('name'),
actions: data => {
const perms = data[7];
if (perms.includes('viewSubscriptions')) {
@ -62,10 +62,10 @@ export default class List extends Component {
}
}
},
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Subscribers') },
{ data: 4, title: t('Description') },
{ data: 5, title: t('Namespace') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('subscribers') },
{ data: 4, title: t('description') },
{ data: 5, title: t('namespace') },
{
actions: data => {
const actions = [];
@ -81,42 +81,42 @@ export default class List extends Component {
if (perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/lists/${data[0]}/edit`
});
}
if (perms.includes('viewFields')) {
actions.push({
label: <Icon icon="th-list" title={t('Fields')}/>,
label: <Icon icon="th-list" title={t('fields')}/>,
link: `/lists/${data[0]}/fields`
});
}
if (perms.includes('viewSegments')) {
actions.push({
label: <Icon icon="tag" title={t('Segments')}/>,
label: <Icon icon="tag" title={t('segments')}/>,
link: `/lists/${data[0]}/segments`
});
}
if (perms.includes('viewImports')) {
actions.push({
label: <Icon icon="sort" title={t('Imports')}/>,
label: <Icon icon="sort" title={t('imports')}/>,
link: `/lists/${data[0]}/imports`
});
}
if (triggersCount > 0) {
actions.push({
label: <Icon icon="flash" title={t('Triggers')}/>,
label: <Icon icon="flash" title={t('triggers')}/>,
link: `/lists/${data[0]}/triggers`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/lists/${data[0]}/share`
});
}
@ -130,15 +130,15 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/lists`, t('Deleting list ...'), t('List deleted'))}
{tableDeleteDialogRender(this, `rest/lists`, t('deletingList'), t('listDeleted'))}
{this.state.createPermitted &&
<Toolbar>
<NavButton linkTo="/lists/create" className="btn-primary" icon="plus" label={t('Create List')}/>
<NavButton linkTo="/lists/forms" className="btn-primary" label={t('Custom Forms')}/>
<NavButton linkTo="/lists/create" className="btn-primary" icon="plus" label={t('createList')}/>
<NavButton linkTo="/lists/forms" className="btn-primary" label={t('customForms-1')}/>
</Toolbar>
}
<Title>{t('Lists')}</Title>
<Title>{t('lists')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/lists-table" columns={columns} />
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {
requiresAuthenticatedUser,
Title,
@ -19,7 +19,7 @@ import {
tableDeleteDialogRender
} from "../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -46,13 +46,13 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Campaign') },
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('campaign') },
{ data: 4, title: t('entity'), render: data => this.entityLabels[data], searchable: false },
{ data: 5, title: t('event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
{ data: 6, title: t('daysAfter'), render: data => Math.round(data / (3600 * 24)) },
{ data: 7, title: t('enabled'), render: data => data ? t('yes') : t('no'), searchable: false},
{
actions: data => {
const actions = [];
@ -61,7 +61,7 @@ export default class List extends Component {
if (mailtrainConfig.globalPermissions.setupAutomation && perms.includes('manageTriggers')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
});
}
@ -77,8 +77,8 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/triggers`, t('Deleting trigger ...'), t('Trigger deleted'))}
<Title>{t('Triggers')}</Title>
{tableDeleteDialogRender(this, `rest/triggers`, t('deletingTrigger'), t('triggerDeleted'))}
<Title>{t('triggers')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl={`rest/triggers-by-list-table/${this.props.list.id}`} columns={columns} />
</div>

View file

@ -1,25 +1,47 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
import React, {Component} from 'react';
import PropTypes
from 'prop-types';
import {Trans} from 'react-i18next';
import {withTranslation} from '../../lib/i18n';
import {
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../../lib/page';
import {
ACEEditor,
Button,
ButtonRow,
Dropdown,
Fieldset,
Form,
FormSendMethod,
InputField,
StaticField,
TableSelect,
withForm
} from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {withErrorHandling} from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/modals";
import { getFieldTypes } from './helpers';
import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators';
import slugify from 'slugify';
import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date';
import styles from "../../lib/styles.scss";
import {getFieldTypes} from './helpers';
import validators
from '../../../../shared/validators';
import slugify
from 'slugify';
import {
DateFormat,
parseBirthday,
parseDate
} from '../../../../shared/date';
import styles
from "../../lib/styles.scss";
import 'brace/mode/json';
import 'brace/mode/handlebars';
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -124,18 +146,18 @@ export default class CUD extends Component {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
const keyServerValidation = state.getIn(['key', 'serverValidation']);
if (!validators.mergeTagValid(state.getIn(['key', 'value']))) {
state.setIn(['key', 'error'], t('Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.'));
state.setIn(['key', 'error'], t('mergeTagIsInvalidMayMustBeUppercaseAnd'));
} else if (!keyServerValidation) {
state.setIn(['key', 'error'], t('Validation is in progress...'));
state.setIn(['key', 'error'], t('validationIsInProgress'));
} else if (keyServerValidation.exists) {
state.setIn(['key', 'error'], t('Another field with the same merge tag exists. Please choose another merge tag.'));
state.setIn(['key', 'error'], t('anotherFieldWithTheSameMergeTagExists'));
} else {
state.setIn(['key', 'error'], null);
}
@ -144,7 +166,7 @@ export default class CUD extends Component {
const group = state.getIn(['group', 'value']);
if (type === 'option' && !group) {
state.setIn(['group', 'error'], t('Group has to be selected'));
state.setIn(['group', 'error'], t('groupHasToBeSelected'));
} else {
state.setIn(['group', 'error'], null);
}
@ -153,11 +175,11 @@ export default class CUD extends Component {
if (defaultValue === '') {
state.setIn(['default_value', 'error'], null);
} else if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) {
state.setIn(['default_value', 'error'], t('Default value is not integer number'));
state.setIn(['default_value', 'error'], t('defaultValueIsNotIntegerNumber'));
} else if (type === 'date' && !parseDate(state.getIn(['dateFormat', 'value']), defaultValue)) {
state.setIn(['default_value', 'error'], t('Default value is not a properly formatted date'));
state.setIn(['default_value', 'error'], t('defaultValueIsNotAProperlyFormattedDate'));
} else if (type === 'birthday' && !parseBirthday(state.getIn(['dateFormat', 'value']), defaultValue)) {
state.setIn(['default_value', 'error'], t('Default value is not a properly formatted birthday date'));
state.setIn(['default_value', 'error'], t('defaultValueIsNotAProperlyFormatted'));
} else {
state.setIn(['default_value', 'error'], null);
}
@ -170,7 +192,7 @@ export default class CUD extends Component {
state.setIn(['enumOptions', 'error'], null);
if (defaultValue !== '' && !(enumOptions.options.find(x => x.key === defaultValue))) {
state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options'));
state.setIn(['default_value', 'error'], t('defaultValueIsNotOneOfTheAllowedOptions'));
}
}
} else {
@ -194,7 +216,7 @@ export default class CUD extends Component {
const label = matches[2].trim();
options.push({ key, label });
} else {
errors.push(t('Errror on line {{ line }}', { line: lineIdx + 1}));
errors.push(t('errrorOnLineLine', { line: lineIdx + 1}));
}
}
}
@ -229,7 +251,7 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.default_value.trim() === '') {
@ -275,10 +297,10 @@ export default class CUD extends Component {
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved'));
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('fieldSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
throw error;
@ -292,9 +314,9 @@ export default class CUD extends Component {
const getOrderOptions = fld => {
return [
{key: 'none', label: t('Not visible')},
{key: 'none', label: t('notVisible')},
...this.props.fields.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
{key: 'end', label: t('End of list')}
{key: 'end', label: t('endOfList')}
];
};
@ -311,8 +333,8 @@ export default class CUD extends Component {
case 'gpg':
case 'number':
fieldSettings =
<Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
<Fieldset label={t('fieldSettings')}>
<InputField id="default_value" label={t('defaultValue')} help={t('defaultValueUsedWhenTheFieldIsEmpty')}/>
</Fieldset>;
break;
@ -320,13 +342,13 @@ export default class CUD extends Component {
case 'radio-grouped':
case 'dropdown-grouped':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Fieldset label={t('fieldSettings')}>
<ACEEditor
id="renderTemplate"
label={t('Template')}
label={t('template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
help={<Trans i18nKey="youCanControlTheAppearanceOfTheMergeTag">You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array, for
example <code>{'{{#each values}} {{this}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
@ -337,22 +359,22 @@ export default class CUD extends Component {
case 'radio-enum':
case 'dropdown-enum':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Fieldset label={t('fieldSettings')}>
<ACEEditor
id="enumOptions"
label={t('Options')}
label={t('options')}
height="250px"
mode="text"
help={<Trans><div>Specify the options to select from in the following format:<code>key|label</code>. For example:</div>
help={<Trans i18nKey="specifyTheOptionsToSelectFromInThe"><div>Specify the options to select from in the following format:<code>key|label</code>. For example:</div>
<div><code>au|Australia</code></div><div><code>at|Austria</code></div></Trans>}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<InputField id="default_value" label={t('defaultValue')} help={<Trans i18nKey="defaultKeyEgAuUsedWhenTheFieldIsEmpty">Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
label={t('template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
help={<Trans i18nKey="youCanControlTheAppearanceOfTheMergeTag-1">You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array.
Each entry in the array is an object with attributes <code>key</code> and <code>label</code>.
For example <code>{'{{#each values}} {{this.value}} {{/each}}'}</code>. If template is not defined then
@ -363,39 +385,39 @@ export default class CUD extends Component {
case 'date':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
<Fieldset label={t('fieldSettings')}>
<Dropdown id="dateFormat" label={t('dateFormat')}
options={[
{key: DateFormat.US, label: t('MM/DD/YYYY')},
{key: DateFormat.EU, label: t('DD/MM/YYYY')}
{key: DateFormat.US, label: t('mmddyyyy')},
{key: DateFormat.EU, label: t('ddmmyyyy')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
<InputField id="default_value" label={t('defaultValue')} help={<Trans i18nKey="defaultValueUsedWhenTheFieldIsEmpty">Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'birthday':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
<Fieldset label={t('fieldSettings')}>
<Dropdown id="dateFormat" label={t('dateFormat')}
options={[
{key: DateFormat.US, label: t('MM/DD')},
{key: DateFormat.EU, label: t('DD/MM')}
{key: DateFormat.US, label: t('mmdd')},
{key: DateFormat.EU, label: t('ddmm')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
<InputField id="default_value" label={t('defaultValue')} help={<Trans i18nKey="defaultValueUsedWhenTheFieldIsEmpty">Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'json':
fieldSettings = <Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
fieldSettings = <Fieldset label={t('fieldSettings')}>
<InputField id="default_value" label={t('defaultValue')} help={<Trans i18nKey="defaultKeyEgAuUsedWhenTheFieldIsEmpty">Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
label={t('template')}
height="250px"
mode="json"
help={<Trans>You can use this template to render JSON values (if the JSON is an array then the array is
help={<Trans i18nKey="youCanUseThisTemplateToRenderJsonValues">You can use this template to render JSON values (if the JSON is an array then the array is
exposed as <code>values</code>, otherwise you can access the JSON keys directly).</Trans>}
/>
</Fieldset>;
@ -404,15 +426,15 @@ export default class CUD extends Component {
case 'option':
const fieldsGroupedColumns = [
{ data: 4, title: "#" },
{ data: 1, title: t('Name') },
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') }
{ data: 1, title: t('name') },
{ data: 2, title: t('type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('mergeTag') }
];
fieldSettings =
<Fieldset label={t('Field settings')}>
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
<Fieldset label={t('fieldSettings')}>
<TableSelect id="group" label={t('group')} withHeader dropdown dataUrl={`rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('selectGroupToWhichTheOptionsShouldBelong')}/>
<InputField id="default_value" label={t('defaultValue')} help={t('defaultValueUsedWhenTheFieldIsEmpty')}/>
</Fieldset>;
break;
}
@ -427,36 +449,36 @@ export default class CUD extends Component {
deleteUrl={`rest/fields/${this.props.list.id}/${this.props.entity.id}`}
backUrl={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/edit`}
successUrl={`/lists/${this.props.list.id}/fields`}
deletingMsg={t('Deleting field ...')}
deletedMsg={t('Field deleted')}/>
deletingMsg={t('deletingField')}
deletedMsg={t('fieldDeleted')}/>
}
<Title>{isEdit ? t('Edit Field') : t('Create Field')}</Title>
<Title>{isEdit ? t('editField') : t('createField')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<InputField id="name" label={t('name')}/>
{isEdit ?
<StaticField id="type" className={styles.formDisabled} label={t('Type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
<StaticField id="type" className={styles.formDisabled} label={t('type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
:
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
<Dropdown id="type" label={t('type')} options={typeOptions}/>
}
<InputField id="key" label={t('Merge tag')}/>
<InputField id="key" label={t('mergeTag-1')}/>
{fieldSettings}
{type !== 'option' &&
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={getOrderOptions('order_list')} help={t('Select the field before which this field should appear in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={getOrderOptions('order_subscribe')} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={getOrderOptions('order_manage')} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
<Fieldset label={t('fieldOrder')}>
<Dropdown id="orderListBefore" label={t('listingsBefore')} options={getOrderOptions('order_list')} help={t('selectTheFieldBeforeWhichThisFieldShould')}/>
<Dropdown id="orderSubscribeBefore" label={t('subscriptionFormBefore')} options={getOrderOptions('order_subscribe')} help={t('selectTheFieldBeforeWhichThisFieldShould-1')}/>
<Dropdown id="orderManageBefore" label={t('managementFormBefore')} options={getOrderOptions('order_manage')} help={t('selectTheFieldBeforeWhichThisFieldShould-2')}/>
</Fieldset>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/delete`}/>}
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
import { withErrorHandling } from '../../lib/error-handling';
import { Table } from '../../lib/table';
@ -14,7 +14,7 @@ import {
tableDeleteDialogRender
} from "../../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -40,18 +40,18 @@ export default class List extends Component {
const columns = [
{ data: 4, title: "#" },
{ data: 1, title: t('Name'),
{ data: 1, title: t('name'),
render: (data, cmd, rowData) => rowData[2] === 'option' ? <span><Icon icon="record"/> {data}</span> : data
},
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') },
{ data: 2, title: t('type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('mergeTag') },
{
actions: data => {
const actions = [];
if (this.props.list.permissions.includes('manageFields')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/lists/${this.props.list.id}/fields/${data[0]}/edit`
});
@ -65,14 +65,14 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/fields/${this.props.list.id}`, t('Deleting field ...'), t('Field deleted'))}
{tableDeleteDialogRender(this, `rest/fields/${this.props.list.id}`, t('deletingField'), t('fieldDeleted'))}
{this.props.list.permissions.includes('manageFields') &&
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/fields/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
<NavButton linkTo={`/lists/${this.props.list.id}/fields/create`} className="btn-primary" icon="plus" label={t('createField')}/>
</Toolbar>
}
<Title>{t('Fields')}</Title>
<Title>{t('fields')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl={`rest/fields-table/${this.props.list.id}`} columns={columns} />
</div>

View file

@ -7,46 +7,46 @@ export function getFieldTypes(t) {
const fieldTypes = {
text: {
label: t('Text'),
label: t('text'),
},
website: {
label: t('Website'),
label: t('website'),
},
longtext: {
label: t('Multi-line text'),
label: t('multilineText'),
},
gpg: {
label: t('GPG Public Key'),
label: t('gpgPublicKey'),
},
number: {
label: t('Number'),
label: t('number'),
},
'checkbox-grouped': {
label: t('Checkboxes (from option fields)'),
label: t('checkboxesFromOptionFields'),
},
'radio-grouped': {
label: t('Radio Buttons (from option fields)')
label: t('radioButtonsFromOptionFields')
},
'dropdown-grouped': {
label: t('Drop Down (from option fields)')
label: t('dropDownFromOptionFields')
},
'radio-enum': {
label: t('Radio Buttons (enumerated)')
label: t('radioButtonsEnumerated')
},
'dropdown-enum': {
label: t('Drop Down (enumerated)')
label: t('dropDownEnumerated')
},
'date': {
label: t('Date')
label: t('date')
},
'birthday': {
label: t('Birthday')
label: t('birthday')
},
json: {
label: t('JSON value for custom rendering')
label: t('jsonValueForCustomRendering')
},
option: {
label: t('Option')
label: t('option')
}
};

View file

@ -1,19 +1,40 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
import React, {Component} from 'react';
import PropTypes
from 'prop-types';
import {Trans} from 'react-i18next';
import {withTranslation} from '../../lib/i18n';
import {
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
Fieldset, Dropdown, AlignedRow, ACEEditor
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../../lib/page';
import {
ACEEditor,
AlignedRow,
Button,
ButtonRow,
Dropdown,
Fieldset,
Form,
FormSendMethod,
InputField,
TableSelect,
TextArea,
withForm
} from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import { validateNamespace, NamespaceSelect } from '../../lib/namespace';
import {withErrorHandling} from '../../lib/error-handling';
import {
NamespaceSelect,
validateNamespace
} from '../../lib/namespace';
import {DeleteModalDialog} from "../../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import mailtrainConfig
from 'mailtrainConfig';
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -60,128 +81,128 @@ export default class CUD extends Component {
const t = props.t;
const helpEmailText = t('The plaintext version for this email');
const helpMjmlGeneral = <Trans>Custom forms use MJML for formatting. See the MJML documentation <a className="mjml-documentation">here</a></Trans>;
const helpEmailText = t('thePlaintextVersionForThisEmail');
const helpMjmlGeneral = <Trans i18nKey="customFormsUseMjmlForFormattingSeeThe">Custom forms use MJML for formatting. See the MJML documentation <a className="mjml-documentation">here</a></Trans>;
this.templateSettings = {
layout: {
label: t('Layout'),
label: t('layout'),
mode: 'html',
help: helpMjmlGeneral,
isLayout: true
},
form_input_style: {
label: t('Form Input Style'),
label: t('formInputStyle'),
mode: 'css',
help: t('This CSS stylesheet defines the appearance of form input elements and alerts')
help: t('thisCssStylesheetDefinesTheAppearanceOf')
},
web_subscribe: {
label: t('Web - Subscribe'),
label: t('webSubscribe'),
mode: 'html',
help: helpMjmlGeneral
},
web_confirm_subscription_notice: {
label: t('Web - Confirm Subscription Notice'),
label: t('webConfirmSubscriptionNotice'),
mode: 'html',
help: helpMjmlGeneral
},
mail_confirm_subscription_html: {
label: t('Mail - Confirm Subscription (MJML)'),
label: t('mailConfirmSubscriptionMjml'),
mode: 'html',
help: helpMjmlGeneral
},
mail_confirm_subscription_text: {
label: t('Mail - Confirm Subscription (Text)'),
label: t('mailConfirmSubscriptionText'),
mode: 'text',
help: helpEmailText
},
mail_already_subscribed_html: {
label: t('Mail - Already Subscribed (MJML)'),
label: t('mailAlreadySubscribedMjml'),
mode: 'html',
help: helpMjmlGeneral
},
mail_already_subscribed_text: {
label: t('Mail - Already Subscribed (Text)'),
label: t('mailAlreadySubscribedText'),
mode: 'text',
help: helpEmailText
},
web_subscribed_notice: {
label: t('Web - Subscribed Notice'),
label: t('webSubscribedNotice'),
mode: 'html',
help: helpMjmlGeneral
},
mail_subscription_confirmed_html: {
label: t('Mail - Subscription Confirmed (MJML)'),
label: t('mailSubscriptionConfirmedMjml'),
mode: 'html',
help: helpMjmlGeneral
},
mail_subscription_confirmed_text: {
label: t('Mail - Subscription Confirmed (Text)'),
label: t('mailSubscriptionConfirmedText'),
mode: 'text',
help: helpEmailText
},
web_manage: {
label: t('Web - Manage Preferences'),
label: t('webManagePreferences'),
mode: 'html',
help: helpMjmlGeneral
},
web_manage_address: {
label: t('Web - Manage Address'),
label: t('webManageAddress'),
mode: 'html',
help: helpMjmlGeneral
},
mail_confirm_address_change_html: {
label: t('Mail - Confirm Address Change (MJML)'),
label: t('mailConfirmAddressChangeMjml'),
mode: 'html',
help: helpMjmlGeneral
},
mail_confirm_address_change_text: {
label: t('Mail - Confirm Address Change (Text)'),
label: t('mailConfirmAddressChangeText'),
mode: 'text',
help: helpEmailText
},
web_updated_notice: {
label: t('Web - Updated Notice'),
label: t('webUpdatedNotice'),
mode: 'html',
help: helpMjmlGeneral
},
web_unsubscribe: {
label: t('Web - Unsubscribe'),
label: t('webUnsubscribe'),
mode: 'html',
help: helpMjmlGeneral
},
web_confirm_unsubscription_notice: {
label: t('Web - Confirm Unsubscription Notice'),
label: t('webConfirmUnsubscriptionNotice'),
mode: 'html',
help: helpMjmlGeneral
},
mail_confirm_unsubscription_html: {
label: t('Mail - Confirm Unsubscription (MJML)'),
label: t('mailConfirmUnsubscriptionMjml'),
mode: 'html',
help: helpMjmlGeneral
},
mail_confirm_unsubscription_text: {
label: t('Mail - Confirm Unsubscription (Text)'),
label: t('mailConfirmUnsubscriptionText'),
mode: 'text',
help: helpEmailText
},
web_unsubscribed_notice: {
label: t('Web - Unsubscribed Notice'),
label: t('webUnsubscribedNotice'),
mode: 'html',
help: helpMjmlGeneral
},
mail_unsubscription_confirmed_html: {
label: t('Mail - Unsubscription Confirmed (MJML)'),
label: t('mailUnsubscriptionConfirmedMjml'),
mode: 'html',
help: helpMjmlGeneral
},
mail_unsubscription_confirmed_text: {
label: t('Mail - Unsubscription Confirmed (Text)'),
label: t('mailUnsubscriptionConfirmedText'),
mode: 'text',
help: helpEmailText
},
web_manual_unsubscribe_notice: {
label: t('Web - Manual Unsubscribe Notice'),
label: t('webManualUnsubscribeNotice'),
mode: 'html',
help: helpMjmlGeneral
}
@ -189,14 +210,14 @@ export default class CUD extends Component {
this.templateGroups = {
general: {
label: t('General'),
label: t('general'),
options: [
'layout',
'form_input_style'
]
},
subscribe: {
label: t('Subscribe'),
label: t('subscribe'),
options: [
'web_subscribe',
'web_confirm_subscription_notice',
@ -210,7 +231,7 @@ export default class CUD extends Component {
]
},
manage: {
label: t('Manage'),
label: t('manage'),
options: [
'web_manage',
'web_manage_address',
@ -220,7 +241,7 @@ export default class CUD extends Component {
]
},
unsubscribe: {
label: t('Unsubscribe'),
label: t('unsubscribe'),
options: [
'web_unsubscribe',
'web_confirm_unsubscription_notice',
@ -274,7 +295,7 @@ export default class CUD extends Component {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
@ -295,12 +316,12 @@ export default class CUD extends Component {
}
if (!formsErrors.length && formsServerValidationRunning) {
formsErrors.push(t('Validation is in progress...'));
formsErrors.push(t('validationIsInProgress'));
}
if (formsErrors.length) {
state.setIn(['selectedTemplate', 'error'],
<div><strong>{t('List of errors in templates') + ':'}</strong>
<div><strong>{t('listOfErrorsInTemplates') + ':'}</strong>
<ul>
{formsErrors.map((msg, idx) => <li key={idx}>{msg}</li>)}
</ul>
@ -323,7 +344,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
delete data.selectedTemplate;
@ -331,10 +352,10 @@ export default class CUD extends Component {
});
if (submitSuccessful) {
this.navigateToWithFlashMessage('/lists/forms', 'success', t('Forms saved'));
this.navigateToWithFlashMessage('/lists/forms', 'success', t('formsSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -360,9 +381,9 @@ export default class CUD extends Component {
const listsColumns = [
{ data: 0, title: "#" },
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => `<code>${data}</code>` },
{ data: 5, title: t('Namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => `<code>${data}</code>` },
{ data: 5, title: t('namespace') }
];
const previewListId = this.getFormValue('previewList');
@ -377,21 +398,21 @@ export default class CUD extends Component {
deleteUrl={`rest/forms/${this.props.entity.id}`}
backUrl={`/lists/forms/${this.props.entity.id}/edit`}
successUrl="/lists/forms"
deletingMsg={t('Deleting form ...')}
deletedMsg={t('Form deleted')}/>
deletingMsg={t('deletingForm')}
deletedMsg={t('formDeleted')}/>
}
<Title>{isEdit ? t('Edit Custom Forms') : t('Create Custom Forms')}</Title>
<Title>{isEdit ? t('editCustomForms') : t('createCustomForms')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('Description')}/>
<TextArea id="description" label={t('description')}/>
<NamespaceSelect/>
<Fieldset label={t('Forms Preview')}>
<TableSelect id="previewList" label={t('List To Preview On')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} help={t('Select list whose fields will be used to preview the forms.')}/>
<Fieldset label={t('formsPreview')}>
<TableSelect id="previewList" label={t('listToPreviewOn')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} help={t('selectListWhoseFieldsWillBeUsedToPreview')}/>
{ previewListId &&
<AlignedRow>
@ -426,15 +447,15 @@ export default class CUD extends Component {
</Fieldset>
{ selectedTemplate &&
<Fieldset label={t('Templates')}>
<Dropdown id="selectedTemplate" label={t('Edit')} options={templateOptGroups} help={this.templateSettings[selectedTemplate].help}/>
<Fieldset label={t('templates')}>
<Dropdown id="selectedTemplate" label={t('edit')} options={templateOptGroups} help={this.templateSettings[selectedTemplate].help}/>
<ACEEditor id={selectedTemplate} height="500px" mode={this.templateSettings[selectedTemplate].mode}/>
</Fieldset>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/forms/${this.props.entity.id}/delete`}/>}
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/lists/forms/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -1,7 +1,7 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import { Table } from '../../lib/table';
@ -14,7 +14,7 @@ import {
tableDeleteDialogRender
} from "../../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -49,9 +49,9 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Namespace') },
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('namespace') },
{
actions: data => {
const actions = [];
@ -59,13 +59,13 @@ export default class List extends Component {
if (perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/lists/forms/${data[0]}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/lists/forms/${data[0]}/share`
});
}
@ -79,14 +79,14 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/forms`, t('Deleting form ...'), t('Form deleted'))}
{tableDeleteDialogRender(this, `rest/forms`, t('deletingForm'), t('formDeleted'))}
{this.state.createPermitted &&
<Toolbar>
<NavButton linkTo="/lists/forms/create" className="btn-primary" icon="plus" label={t('Create Custom Form')}/>
<NavButton linkTo="/lists/forms/create" className="btn-primary" icon="plus" label={t('createCustomForm')}/>
</Toolbar>
}
<Title>{t('Forms')}</Title>
<Title>{t('forms')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/forms-table" columns={columns} />
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -52,7 +52,7 @@ function truncate(str, len, ending = '...') {
}
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -160,17 +160,17 @@ export default class CUD extends Component {
}
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
}
if (!isEdit) {
if (source === ImportSource.CSV_FILE) {
if (!this.csvFile || this.csvFile.files.length === 0) {
state.setIn(['csvFileName', 'error'], t('File must be selected'));
state.setIn(['csvFileName', 'error'], t('fileMustBeSelected'));
}
if (!state.getIn(['csvDelimiter', 'value']).trim()) {
state.setIn(['csvDelimiter', 'error'], t('CSV delimiter must not be empty'));
state.setIn(['csvDelimiter', 'error'], t('csvDelimiterMustNotBeEmpty'));
}
}
} else {
@ -178,7 +178,7 @@ export default class CUD extends Component {
if (mappingType === MappingType.BASIC_SUBSCRIBE || mappingType === MappingType.BASIC_UNSUBSCRIBE) {
if (!state.getIn(['mapping_fields_email_column', 'value'])) {
state.setIn(['mapping_fields_email_column', 'error'], t('Email mapping has to be provided'));
state.setIn(['mapping_fields_email_column', 'error'], t('emailMappingHasToBeProvided'));
}
}
}
@ -200,7 +200,7 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.source = Number.parseInt(data.source);
@ -275,12 +275,12 @@ export default class CUD extends Component {
if (!isEdit) {
this.navigateTo(`/lists/${this.props.list.id}/imports/${submitResponse}/edit`);
} else {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/imports/${this.props.entity.id}/status`, 'success', t('Import saved'));
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/imports/${this.props.entity.id}/status`, 'success', t('importSaved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
throw error;
@ -308,14 +308,14 @@ export default class CUD extends Component {
if (isEdit) {
settingsEdit =
<div>
<StaticField id="csvFileName" className={styles.formDisabled} label={t('File')}>{this.getFormValue('csvFileName')}</StaticField>
<StaticField id="csvDelimiter" className={styles.formDisabled} label={t('Delimiter')}>{this.getFormValue('csvDelimiter')}</StaticField>
<StaticField id="csvFileName" className={styles.formDisabled} label={t('file')}>{this.getFormValue('csvFileName')}</StaticField>
<StaticField id="csvDelimiter" className={styles.formDisabled} label={t('delimiter')}>{this.getFormValue('csvDelimiter')}</StaticField>
</div>;
} else {
settingsEdit =
<div>
<StaticField withValidation id="csvFileName" label={t('File')}><input ref={node => this.csvFile = node} type="file" onChange={::this.onFileSelected}/></StaticField>
<InputField id="csvDelimiter" label={t('Delimiter')}/>
<StaticField withValidation id="csvFileName" label={t('file')}><input ref={node => this.csvFile = node} type="file" onChange={::this.onFileSelected}/></StaticField>
<InputField id="csvDelimiter" label={t('delimiter')}/>
</div>;
}
}
@ -324,7 +324,7 @@ export default class CUD extends Component {
if (isEdit) {
if (prepInProgress(status)) {
mappingEdit = (
<div>{t('Preparation in progress. Please wait till it is done or visit this page later.')}</div>
<div>{t('preparationInProgressPleaseWaitTillItIs')}</div>
);
} else {
@ -334,12 +334,12 @@ export default class CUD extends Component {
if (mappingType === MappingType.BASIC_SUBSCRIBE || mappingType === MappingType.BASIC_UNSUBSCRIBE) {
const sampleRow = this.getFormValue('sampleRow');
const sourceOpts = [];
sourceOpts.push({key: '', label: t(' Select ')});
sourceOpts.push({key: '', label: t('Select ')});
if (source === ImportSource.CSV_FILE) {
for (const csvCol of settings.csv.columns) {
let help = '';
if (sampleRow) {
help = ' (' + t('e.g.:', {keySeparator: '>', nsSeparator: '|'}) + ' ' + truncate(sampleRow[csvCol.column], 50) + ')';
help = ' (' + t('eg', {keySeparator: '>', nsSeparator: '|'}) + ' ' + truncate(sampleRow[csvCol.column], 50) + ')';
}
sourceOpts.push({key: csvCol.column, label: csvCol.name + help});
@ -348,11 +348,11 @@ export default class CUD extends Component {
const settingsRows = [];
const mappingRows = [
<Dropdown key="email" id="mapping_fields_email_column" label={t('Email')} options={sourceOpts}/>
<Dropdown key="email" id="mapping_fields_email_column" label={t('email')} options={sourceOpts}/>
];
if (mappingType === MappingType.BASIC_SUBSCRIBE) {
settingsRows.push(<CheckBox key="checkEmails" id="mapping_settings_checkEmails" text={t('Check imported emails')}/>)
settingsRows.push(<CheckBox key="checkEmails" id="mapping_settings_checkEmails" text={t('checkImportedEmails')}/>)
for (const field of this.props.fieldsGrouped) {
if (field.column) {
@ -373,7 +373,7 @@ export default class CUD extends Component {
mappingSettings = (
<div>
{settingsRows}
<Fieldset label={t('Mapping')} className={listStyles.mapping}>
<Fieldset label={t('mapping')} className={listStyles.mapping}>
{mappingRows}
</Fieldset>
</div>
@ -382,7 +382,7 @@ export default class CUD extends Component {
mappingEdit = (
<div>
<Dropdown id="mapping_type" label={t('Type')} options={this.mappingOptions}/>
<Dropdown id="mapping_type" label={t('type')} options={this.mappingOptions}/>
{mappingSettings}
</div>
);
@ -391,9 +391,9 @@ export default class CUD extends Component {
let saveButtonLabel;
if (!isEdit) {
saveButtonLabel = t('Save and edit settings');
saveButtonLabel = t('saveAndEditSettings');
} else {
saveButtonLabel = t('Save');
saveButtonLabel = t('save');
}
return (
@ -405,20 +405,20 @@ export default class CUD extends Component {
deleteUrl={`rest/imports/${this.props.list.id}/${this.props.entity.id}`}
backUrl={`/lists/${this.props.list.id}/imports/${this.props.entity.id}/edit`}
successUrl={`/lists/${this.props.list.id}/imports`}
deletingMsg={t('Deleting import ...')}
deletedMsg={t('Import deleted')}/>
deletingMsg={t('deletingImport')}
deletedMsg={t('importDeleted')}/>
}
<Title>{isEdit ? t('Edit Import') : t('Create Import')}</Title>
<Title>{isEdit ? t('editImport') : t('createImport')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/>
{isEdit ?
<StaticField id="source" className={styles.formDisabled} label={t('Source')}>{this.importSourceLabels[this.getFormValue('source')]}</StaticField>
<StaticField id="source" className={styles.formDisabled} label={t('source')}>{this.importSourceLabels[this.getFormValue('source')]}</StaticField>
:
<Dropdown id="source" label={t('Source')} options={this.importSourceOptions}/>
<Dropdown id="source" label={t('source')} options={this.importSourceOptions}/>
}
{settingsEdit}
@ -428,7 +428,7 @@ export default class CUD extends Component {
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={saveButtonLabel}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.list.id}/imports/${this.props.entity.id}/delete`}/>}
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/lists/${this.props.list.id}/imports/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -23,7 +23,7 @@ import {
tableDeleteDialogRender
} from "../../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -50,11 +50,11 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Source'), render: data => this.importSourceLabels[data], sortable: false, searchable: false },
{ data: 4, title: t('Status'), render: data => this.importStatusLabels[data], sortable: false, searchable: false },
{ data: 5, title: t('Last run'), render: data => moment(data).fromNow() },
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('source'), render: data => this.importSourceLabels[data], sortable: false, searchable: false },
{ data: 4, title: t('status'), render: data => this.importStatusLabels[data], sortable: false, searchable: false },
{ data: 5, title: t('lastRun'), render: data => moment(data).fromNow() },
{
actions: data => {
const actions = [];
@ -68,13 +68,13 @@ export default class List extends Component {
if (mailtrainConfig.globalPermissions.setupAutomation && this.props.list.permissions.includes('manageImports')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/lists/${this.props.list.id}/imports/${data[0]}/edit`
});
}
actions.push({
label: <Icon icon="eye-open" title={t('Detailed status')}/>,
label: <Icon icon="eye-open" title={t('detailedStatus')}/>,
link: `/lists/${this.props.list.id}/imports/${data[0]}/status`
});
@ -89,14 +89,14 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/imports/${this.props.list.id}`, t('Deleting import ...'), t('Import deleted'))}
{tableDeleteDialogRender(this, `rest/imports/${this.props.list.id}`, t('deletingImport'), t('importDeleted'))}
{mailtrainConfig.globalPermissions.setupAutomation && this.props.list.permissions.includes('manageImports') &&
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/>
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('createImport')}/>
</Toolbar>
}
<Title>{t('Imports')}</Title>
<Title>{t('imports')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} />
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {
requiresAuthenticatedUser,
Title,
@ -20,7 +20,7 @@ import moment from "moment";
import {runStatusInProgress} from "../../../../shared/imports";
import {Table} from "../../lib/table";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -84,27 +84,27 @@ export default class Status extends Component {
const imprt = this.props.imprt;
const columns = [
{ data: 1, title: t('Row') },
{ data: 2, title: t('Email') },
{ data: 3, title: t('Reason') }
{ data: 1, title: t('row') },
{ data: 2, title: t('email') },
{ data: 3, title: t('reason') }
];
return (
<div>
<Title>{t('Import Run Status')}</Title>
<Title>{t('importRunStatus')}</Title>
<AlignedRow label={t('Import name')}>{imprt.name}</AlignedRow>
<AlignedRow label={t('Import source')}>{this.importSourceLabels[imprt.source]}</AlignedRow>
<AlignedRow label={t('Run started')}>{moment(entity.created).fromNow()}</AlignedRow>
{entity.finished && <AlignedRow label={t('Run finished')}>{moment(entity.finished).fromNow()}</AlignedRow>}
<AlignedRow label={t('Run status')}>{this.runStatusLabels[entity.status]}</AlignedRow>
<AlignedRow label={t('Processed entries')}>{entity.processed}</AlignedRow>
<AlignedRow label={t('New entries')}>{entity.new}</AlignedRow>
<AlignedRow label={t('Failed entries')}>{entity.failed}</AlignedRow>
{entity.error && <AlignedRow label={t('Error')}><pre>{entity.error}</pre></AlignedRow>}
<AlignedRow label={t('importName')}>{imprt.name}</AlignedRow>
<AlignedRow label={t('importSource')}>{this.importSourceLabels[imprt.source]}</AlignedRow>
<AlignedRow label={t('runStarted')}>{moment(entity.created).fromNow()}</AlignedRow>
{entity.finished && <AlignedRow label={t('runFinished')}>{moment(entity.finished).fromNow()}</AlignedRow>}
<AlignedRow label={t('runStatus')}>{this.runStatusLabels[entity.status]}</AlignedRow>
<AlignedRow label={t('processedEntries')}>{entity.processed}</AlignedRow>
<AlignedRow label={t('newEntries')}>{entity.new}</AlignedRow>
<AlignedRow label={t('failedEntries')}>{entity.failed}</AlignedRow>
{entity.error && <AlignedRow label={t('error')}><pre>{entity.error}</pre></AlignedRow>}
<hr/>
<h3>{t('Failed Rows')}</h3>
<h3>{t('failedRows')}</h3>
<Table ref={node => this.failedTableNode = node} withHeader dataUrl={`rest/import-run-failed-table/${this.props.list.id}/${this.props.imprt.id}/${this.props.entity.id}`} columns={columns} />
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {
requiresAuthenticatedUser,
Title,
@ -34,7 +34,7 @@ import {getUrl} from "../../lib/urls";
import moment from "moment";
import interoperableErrors from '../../../../shared/interoperable-errors';
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -127,12 +127,12 @@ export default class Status extends Component {
const entity = this.state.entity;
const columns = [
{ data: 1, title: t('Started'), render: data => moment(data).fromNow() },
{ data: 2, title: t('Finished'), render: data => data ? moment(data).fromNow() : '' },
{ data: 3, title: t('Status'), render: data => this.runStatusLabels[data], sortable: false, searchable: false },
{ data: 4, title: t('Processed') },
{ data: 5, title: t('New') },
{ data: 6, title: t('Failed') },
{ data: 1, title: t('started'), render: data => moment(data).fromNow() },
{ data: 2, title: t('finished'), render: data => data ? moment(data).fromNow() : '' },
{ data: 3, title: t('status'), render: data => this.runStatusLabels[data], sortable: false, searchable: false },
{ data: 4, title: t('processed') },
{ data: 5, title: t('new') },
{ data: 6, title: t('failed') },
{
actions: data => {
const actions = [];
@ -145,7 +145,7 @@ export default class Status extends Component {
}
actions.push({
label: <Icon icon="eye-open" title={t('Run status')}/>,
label: <Icon icon="eye-open" title={t('runStatus')}/>,
link: `/lists/${this.props.list.id}/imports/${this.props.entity.id}/status/${data[0]}`
});
@ -156,20 +156,20 @@ export default class Status extends Component {
return (
<div>
<Title>{t('Import Status')}</Title>
<Title>{t('importStatus')}</Title>
<AlignedRow label={t('Name')}>{entity.name}</AlignedRow>
<AlignedRow label={t('Source')}>{this.importSourceLabels[entity.source]}</AlignedRow>
<AlignedRow label={t('Status')}>{this.importStatusLabels[entity.status]}</AlignedRow>
{entity.error && <AlignedRow label={t('Error')}><pre>{entity.error}</pre></AlignedRow>}
<AlignedRow label={t('name')}>{entity.name}</AlignedRow>
<AlignedRow label={t('source')}>{this.importSourceLabels[entity.source]}</AlignedRow>
<AlignedRow label={t('status')}>{this.importStatusLabels[entity.status]}</AlignedRow>
{entity.error && <AlignedRow label={t('error')}><pre>{entity.error}</pre></AlignedRow>}
<ButtonRow label={t('Actions')}>
{prepFinishedAndNotInProgress(entity.status) && <Button className="btn-primary" icon="play" label={t('Start')} onClickAsync={::this.startRunAsync}/>}
{runInProgress(entity.status) && <Button className="btn-primary" icon="stop" label={t('Stop')} onClickAsync={::this.stopRunAsync}/>}
<ButtonRow label={t('actions')}>
{prepFinishedAndNotInProgress(entity.status) && <Button className="btn-primary" icon="play" label={t('start')} onClickAsync={::this.startRunAsync}/>}
{runInProgress(entity.status) && <Button className="btn-primary" icon="stop" label={t('stop')} onClickAsync={::this.stopRunAsync}/>}
</ButtonRow>
<hr/>
<h3>{t('Import Runs')}</h3>
<h3>{t('importRuns')}</h3>
<Table ref={node => this.runsTableNode = node} withHeader dataUrl={`rest/import-runs-table/${this.props.list.id}/${this.props.entity.id}`} columns={columns} />
</div>
);

View file

@ -6,34 +6,34 @@ import {ImportSource, MappingType, ImportStatus, RunStatus} from '../../../../sh
export function getImportLabels(t) {
const importSourceLabels = {
[ImportSource.CSV_FILE]: t('CSV file'),
[ImportSource.LIST]: t('List'),
[ImportSource.CSV_FILE]: t('csvFile'),
[ImportSource.LIST]: t('list'),
};
const importStatusLabels = {
[ImportStatus.PREP_SCHEDULED]: t('Created'),
[ImportStatus.PREP_RUNNING]: t('Preparing'),
[ImportStatus.PREP_STOPPING]: t('Stopping'),
[ImportStatus.PREP_FINISHED]: t('Ready'),
[ImportStatus.PREP_FAILED]: t('Preparation failed'),
[ImportStatus.RUN_SCHEDULED]: t('Scheduled'),
[ImportStatus.RUN_RUNNING]: t('Running'),
[ImportStatus.RUN_STOPPING]: t('Stopping'),
[ImportStatus.RUN_FINISHED]: t('Finished'),
[ImportStatus.RUN_FAILED]: t('Failed')
[ImportStatus.PREP_SCHEDULED]: t('created'),
[ImportStatus.PREP_RUNNING]: t('preparing'),
[ImportStatus.PREP_STOPPING]: t('stopping'),
[ImportStatus.PREP_FINISHED]: t('ready'),
[ImportStatus.PREP_FAILED]: t('preparationFailed'),
[ImportStatus.RUN_SCHEDULED]: t('scheduled'),
[ImportStatus.RUN_RUNNING]: t('running'),
[ImportStatus.RUN_STOPPING]: t('stopping'),
[ImportStatus.RUN_FINISHED]: t('finished'),
[ImportStatus.RUN_FAILED]: t('failed')
};
const runStatusLabels = {
[RunStatus.SCHEDULED]: t('Starting'),
[RunStatus.RUNNING]: t('Running'),
[RunStatus.STOPPING]: t('Stopping'),
[RunStatus.FINISHED]: t('Finished'),
[RunStatus.FAILED]: t('Failed')
[RunStatus.SCHEDULED]: t('starting'),
[RunStatus.RUNNING]: t('running'),
[RunStatus.STOPPING]: t('stopping'),
[RunStatus.FINISHED]: t('finished'),
[RunStatus.FAILED]: t('failed')
};
const mappingTypeLabels = {
[MappingType.BASIC_SUBSCRIBE]: t('Basic import of subscribers'),
[MappingType.BASIC_UNSUBSCRIBE]: t('Unsubscribe emails'),
[MappingType.BASIC_SUBSCRIBE]: t('basicImportOfSubscribers'),
[MappingType.BASIC_UNSUBSCRIBE]: t('unsubscribeEmails'),
}
return {

View file

@ -22,19 +22,19 @@ import TriggersList from './TriggersList';
function getMenus(t) {
return {
'lists': {
title: t('Lists'),
title: t('lists'),
link: '/lists',
panelComponent: ListsList,
children: {
':listId([0-9]+)': {
title: resolved => t('List "{{name}}"', {name: resolved.list.name}),
title: resolved => t('listName', {name: resolved.list.name}),
resolve: {
list: params => `rest/lists/${params.listId}`
},
link: params => `/lists/${params.listId}/subscriptions`,
navs: {
subscriptions: {
title: t('Subscribers'),
title: t('subscribers'),
resolve: {
segments: params => `rest/segments/${params.listId}`,
},
@ -51,14 +51,14 @@ function getMenus(t) {
link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
panelRender: props => <SubscriptionsCUD action={props.match.params.action} entity={props.resolved.subscription} list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
}
}
},
create: {
title: t('Create'),
title: t('create'),
resolve: {
fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
},
@ -66,19 +66,19 @@ function getMenus(t) {
}
} },
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/lists/${params.listId}/edit`,
visible: resolved => resolved.list.permissions.includes('edit'),
panelRender: props => <ListsCUD action={props.match.params.action} entity={props.resolved.list} />
},
fields: {
title: t('Fields'),
title: t('fields'),
link: params => `/lists/${params.listId}/fields/`,
visible: resolved => resolved.list.permissions.includes('viewFields'),
panelRender: props => <FieldsList list={props.resolved.list} />,
children: {
':fieldId([0-9]+)': {
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
title: resolved => t('fieldName-1', {name: resolved.field.name}),
resolve: {
field: params => `rest/fields/${params.listId}/${params.fieldId}`,
fields: params => `rest/fields/${params.listId}`
@ -86,14 +86,14 @@ function getMenus(t) {
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
panelRender: props => <FieldsCUD action={props.match.params.action} entity={props.resolved.field} list={props.resolved.list} fields={props.resolved.fields} />
}
}
},
create: {
title: t('Create'),
title: t('create'),
resolve: {
fields: params => `rest/fields/${params.listId}`
},
@ -102,13 +102,13 @@ function getMenus(t) {
}
},
segments: {
title: t('Segments'),
title: t('segments'),
link: params => `/lists/${params.listId}/segments`,
visible: resolved => resolved.list.permissions.includes('viewSegments'),
panelRender: props => <SegmentsList list={props.resolved.list} />,
children: {
':segmentId([0-9]+)': {
title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}),
title: resolved => t('segmentName', {name: resolved.segment.name}),
resolve: {
segment: params => `rest/segments/${params.listId}/${params.segmentId}`,
fields: params => `rest/fields/${params.listId}`
@ -116,14 +116,14 @@ function getMenus(t) {
link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
panelRender: props => <SegmentsCUD action={props.match.params.action} entity={props.resolved.segment} list={props.resolved.list} fields={props.resolved.fields} />
}
}
},
create: {
title: t('Create'),
title: t('create'),
resolve: {
fields: params => `rest/fields/${params.listId}`
},
@ -132,20 +132,20 @@ function getMenus(t) {
}
},
imports: {
title: t('Imports'),
title: t('imports'),
link: params => `/lists/${params.listId}/imports/`,
visible: resolved => resolved.list.permissions.includes('viewImports'),
panelRender: props => <ImportsList list={props.resolved.list} />,
children: {
':importId([0-9]+)': {
title: resolved => t('Import "{{name}}"', {name: resolved.import.name}),
title: resolved => t('importName-1', {name: resolved.import.name}),
resolve: {
import: params => `rest/imports/${params.listId}/${params.importId}`,
},
link: params => `/lists/${params.listId}/imports/${params.importId}/status`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
resolve: {
fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
},
@ -153,12 +153,12 @@ function getMenus(t) {
panelRender: props => <ImportsCUD action={props.match.params.action} entity={props.resolved.import} list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped}/>
},
'status': {
title: t('Status'),
title: t('status'),
link: params => `/lists/${params.listId}/imports/${params.importId}/status`,
panelRender: props => <ImportsStatus entity={props.resolved.import} list={props.resolved.list} />,
children: {
':importRunId([0-9]+)': {
title: resolved => t('Run'),
title: resolved => t('run'),
resolve: {
importRun: params => `rest/import-runs/${params.listId}/${params.importId}/${params.importRunId}`,
},
@ -170,56 +170,56 @@ function getMenus(t) {
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <ImportsCUD action="create" list={props.resolved.list} />
}
}
},
triggers: {
title: t('Triggers'),
title: t('triggers'),
link: params => `/lists/${params.listId}/triggers`,
panelRender: props => <TriggersList list={props.resolved.list} />
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/lists/${params.listId}/share`,
visible: resolved => resolved.list.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.list} entityTypeId="list" />
panelRender: props => <Share title={t('share')} entity={props.resolved.list} entityTypeId="list" />
}
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <ListsCUD action="create" />
},
forms: {
title: t('Custom Forms'),
title: t('customForms-1'),
link: '/lists/forms',
panelComponent: FormsList,
children: {
':formsId([0-9]+)': {
title: resolved => t('Custom Forms "{{name}}"', {name: resolved.forms.name}),
title: resolved => t('customFormsName', {name: resolved.forms.name}),
resolve: {
forms: params => `rest/forms/${params.formsId}`
},
link: params => `/lists/forms/${params.formsId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/lists/forms/${params.formsId}/edit`,
visible: resolved => resolved.forms.permissions.includes('edit'),
panelRender: props => <FormsCUD action={props.match.params.action} entity={props.resolved.forms} />
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/lists/forms/${params.formsId}/share`,
visible: resolved => resolved.forms.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.forms} entityTypeId="customForm" />
panelRender: props => <Share title={t('share')} entity={props.resolved.forms} entityTypeId="customForm" />
}
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <FormsCUD action="create" />
}
}

View file

@ -2,7 +2,7 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import {translate} from "react-i18next";
import { withTranslation } from '../../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -39,7 +39,7 @@ import RuleSettingsPane from "./RuleSettingsPane";
const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
@DragDropContext(isTouchDevice ? TouchBackend : HTML5Backend)
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -135,7 +135,7 @@ export default class CUD extends Component {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
@ -159,7 +159,7 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
const keep = ['name', 'settings', 'originalHash'];
@ -181,13 +181,13 @@ export default class CUD extends Component {
});
});
this.enableForm();
this.setFormStatusMessage('success', t('Segment saved'));
this.setFormStatusMessage('success', t('segmentSaved'));
} else {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/segments`, 'success', t('Segment saved'));
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/segments`, 'success', t('segmentSaved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
throw error;
@ -335,30 +335,30 @@ export default class CUD extends Component {
deleteUrl={`rest/segments/${this.props.list.id}/${this.props.entity.id}`}
backUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
successUrl={`/lists/${this.props.list.id}/segments`}
deletingMsg={t('Deleting segment ...')}
deletedMsg={t('Segment deleted')}/>
deletingMsg={t('deletingSegment')}
deletedMsg={t('segmentDeleted')}/>
}
<Title>{isEdit ? t('Edit Segment') : t('Create Segment')}</Title>
<Title>{isEdit ? t('editSegment') : t('createSegment')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
{isEdit ?
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('saveAndStay')} onClickAsync={::this.submitAndStay}/>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('saveAndLeave')}/>
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/delete`}/>
<NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/delete`}/>
</ButtonRow>
:
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('save')}/>
</ButtonRow>
}
<h3>{t('Segment Options')}</h3>
<h3>{t('segmentOptions')}</h3>
<InputField id="name" label={t('Name')} />
<Dropdown id="rootRuleType" label={t('Toplevel match type')} options={ruleHelpers.getCompositeRuleTypeOptions()} />
<InputField id="name" label={t('name')} />
<Dropdown id="rootRuleType" label={t('toplevelMatchType')} options={ruleHelpers.getCompositeRuleTypeOptions()} />
</Form>
<hr />
@ -367,11 +367,11 @@ export default class CUD extends Component {
<div className={styles.leftPane}>
<div className={styles.leftPaneInner}>
<Toolbar>
<Button className="btn-primary" label={t('Add Composite Rule')} onClickAsync={::this.addCompositeRule}/>
<Button className="btn-primary" label={t('Add Rule')} onClickAsync={::this.addPrimitiveRule}/>
<Button className="btn-primary" label={t('addCompositeRule')} onClickAsync={::this.addCompositeRule}/>
<Button className="btn-primary" label={t('addRule')} onClickAsync={::this.addPrimitiveRule}/>
</Toolbar>
<h3>{t('Rules')}</h3>
<h3>{t('rules')}</h3>
<div className="clearfix"/>
@ -383,8 +383,8 @@ export default class CUD extends Component {
canDrop={ data => !data.nextParent || (ruleHelpers.isCompositeRuleType(data.nextParent.rule.type)) }
generateNodeProps={data => ({
buttons: [
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.showRuleOptions(data.node.rule)} className={styles.ruleActionLink}><Icon icon="edit" title={t('Edit')}/></ActionLink>,
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.deleteRule(data.node.rule)} className={styles.ruleActionLink}><Icon icon="remove" title={t('Delete')}/></ActionLink>
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.showRuleOptions(data.node.rule)} className={styles.ruleActionLink}><Icon icon="edit" title={t('edit')}/></ActionLink>,
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.deleteRule(data.node.rule)} className={styles.ruleActionLink}><Icon icon="remove" title={t('delete')}/></ActionLink>
]
})}
/>

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
import { withErrorHandling } from '../../lib/error-handling';
import { Table } from '../../lib/table';
@ -13,7 +13,7 @@ import {
tableDeleteDialogRender
} from "../../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -36,14 +36,14 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 1, title: t('name') },
{
actions: data => {
const actions = [];
if (this.props.list.permissions.includes('manageSegments')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/lists/${this.props.list.id}/segments/${data[0]}/edit`
});
@ -57,14 +57,14 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/segments/${this.props.list.id}`, t('Deleting segment ...'), t('Segment deleted'))}
{tableDeleteDialogRender(this, `rest/segments/${this.props.list.id}`, t('deletingSegment'), t('segmentDeleted'))}
{this.props.list.permissions.includes('manageSegments') &&
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/segments/create`} className="btn-primary" icon="plus" label={t('Create Segment')}/>
<NavButton linkTo={`/lists/${this.props.list.id}/segments/create`} className="btn-primary" icon="plus" label={t('createSegment')}/>
</Toolbar>
}
<Title>{t('Segment')}</Title>
<Title>{t('segment')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl={`rest/segments-table/${this.props.list.id}`} columns={columns} />
</div>

View file

@ -2,7 +2,7 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import {translate} from "react-i18next";
import { withTranslation } from '../../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers} from "../../lib/page";
import {Button, ButtonRow, Dropdown, Form, TableSelect, withForm} from "../../lib/form";
import {withErrorHandling} from "../../lib/error-handling";
@ -11,7 +11,7 @@ import {getFieldTypes} from "../fields/helpers";
import styles from "./CUD.scss";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -100,10 +100,10 @@ export default class CUD extends Component {
const settings = ruleHelpers.primitiveRuleTypes[colType][ruleType];
settings.validate(state);
} else {
state.setIn(['type', 'error'], t('Type must be selected'));
state.setIn(['type', 'error'], t('typeMustBeSelected'));
}
} else {
state.setIn(['column', 'error'], t('Field must be selected'));
state.setIn(['column', 'error'], t('fieldMustBeSelected'));
}
}
}
@ -170,18 +170,18 @@ export default class CUD extends Component {
let ruleOptions = null;
if (ruleHelpers.isCompositeRuleType(rule.type)) {
ruleOptions = <Dropdown id="type" label={t('Type')} options={ruleHelpers.getCompositeRuleTypeOptions()} />
ruleOptions = <Dropdown id="type" label={t('type')} options={ruleHelpers.getCompositeRuleTypeOptions()} />
} else {
const ruleColumnOptionsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Type') },
{ data: 3, title: t('Merge Tag') }
{ data: 1, title: t('name') },
{ data: 2, title: t('type') },
{ data: 3, title: t('mergeTag') }
];
const ruleColumnOptions = ruleHelpers.fields.map(fld => [ fld.column, fld.name, this.fieldTypes[fld.type].label, fld.key || '' ]);
const ruleColumnSelect = <TableSelect id="column" label={t('Field')} data={ruleColumnOptions} columns={ruleColumnOptionsColumns} dropdown withHeader selectionLabelIndex={1} />;
const ruleColumnSelect = <TableSelect id="column" label={t('field')} data={ruleColumnOptions} columns={ruleColumnOptionsColumns} dropdown withHeader selectionLabelIndex={1} />;
let ruleTypeSelect = null;
let ruleSettings = null;
@ -190,10 +190,10 @@ export default class CUD extends Component {
const colType = ruleHelpers.getColumnType(ruleColumn);
if (colType) {
const ruleTypeOptions = ruleHelpers.getPrimitiveRuleTypeOptions(colType);
ruleTypeOptions.unshift({ key: '', label: t('-- Select --')});
ruleTypeOptions.unshift({ key: '', label: t('select-1')});
if (ruleTypeOptions) {
ruleTypeSelect = <Dropdown id="type" label={t('Type')} options={ruleTypeOptions} />
ruleTypeSelect = <Dropdown id="type" label={t('type')} options={ruleTypeOptions} />
const ruleType = this.getFormValue('type');
if (ruleType) {
@ -215,15 +215,15 @@ export default class CUD extends Component {
return (
<div className={styles.ruleOptions}>
<h3>{t('Rule Options')}</h3>
<h3>{t('ruleOptions')}</h3>
<Form stateOwner={this} onSubmitAsync={::this.closeForm}>
{ruleOptions}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="chevron-left" label={t('OK')}/>
<Button className="btn-primary" icon="remove" label={t('Delete')} onClickAsync={::this.deleteRule}/>
<Button type="submit" className="btn-primary" icon="chevron-left" label={t('ok')}/>
<Button className="btn-primary" icon="remove" label={t('delete')} onClickAsync={::this.deleteRule}/>
</ButtonRow>
</Form>

View file

@ -11,16 +11,16 @@ export function getRuleHelpers(t, fields) {
ruleHelpers.compositeRuleTypes = {
all: {
dropdownLabel: t('All rules must match'),
treeLabel: rule => t('All rules must match')
dropdownLabel: t('allRulesMustMatch'),
treeLabel: rule => t('allRulesMustMatch')
},
some: {
dropdownLabel: t('At least one rule must match'),
treeLabel: rule => t('At least one rule must match')
dropdownLabel: t('atLeastOneRuleMustMatch'),
treeLabel: rule => t('atLeastOneRuleMustMatch')
},
none: {
dropdownLabel: t('No rule may match'),
treeLabel: rule => t('No rule may match')
dropdownLabel: t('noRuleMayMatch'),
treeLabel: rule => t('noRuleMayMatch')
}
};
@ -28,195 +28,195 @@ export function getRuleHelpers(t, fields) {
ruleHelpers.primitiveRuleTypes.text = {
eq: {
dropdownLabel: t('Equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('Match (with SQL LIKE)'),
treeLabel: rule => t('Value in column "{{colName}}" matches (with SQL LIKE) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('matchWithSqlLike'),
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('Match (with regular expressions)'),
treeLabel: rule => t('Value in column "{{colName}}" matches (with regular expressions) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('matchWithRegularExpressions'),
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('Alphabetically before'),
treeLabel: rule => t('Value in column "{{colName}}" is alphabetically before "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('alphabeticallyBefore'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('Alphabetically before or equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is alphabetically before or equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('alphabeticallyBeforeOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('Alphabetically after'),
treeLabel: rule => t('Value in column "{{colName}}" is alphabetically after "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('alphabeticallyAfter'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('Alphabetically after or equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is alphabetically after or equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('alphabeticallyAfterOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
ruleHelpers.primitiveRuleTypes.website = {
eq: {
dropdownLabel: t('Equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('Match (with SQL LIKE)'),
treeLabel: rule => t('Value in column "{{colName}}" matches (with SQL LIKE) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('matchWithSqlLike'),
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('Match (with regular expressions)'),
treeLabel: rule => t('Value in column "{{colName}}" matches (with regular expressions) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('matchWithRegularExpressions'),
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
ruleHelpers.primitiveRuleTypes.number = {
eq: {
dropdownLabel: t('Equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is equal to {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('Less than'),
treeLabel: rule => t('Value in column "{{colName}}" is less than {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('lessThan'),
treeLabel: rule => t('valueInColumnColNameIsLessThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('Less than or equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is less than or equal to {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('lessThanOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsLessThanOrEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('Greater than'),
treeLabel: rule => t('Value in column "{{colName}}" is greater than {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('greaterThan'),
treeLabel: rule => t('valueInColumnColNameIsGreaterThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('Greater than or equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is greater than or equal to {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('greaterThanOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsGreaterThanOrEqual', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
// TODO: This generates strings that cannot be statically detected. It will require dynamic discovery of translatable strings.
function getRelativeDateTreeLabel(rule, textFragment) {
if (rule.value === 0) {
return t('Date in column "{{colName}}" ' + textFragment + ' the current date', {colName: ruleHelpers.getColumnName(rule.column)})
return t('dateInColumnColName' + textFragment + ' the current date', {colName: ruleHelpers.getColumnName(rule.column)})
} else if (rule.value > 0) {
return t('Date in column "{{colName}}" ' + textFragment + ' {{value}}-th day after the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
return t('dateInColumnColName' + textFragment + ' {{value}}-th day after the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
} else {
return t('Date in column "{{colName}}" ' + textFragment + ' {{value}}-th day before the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
return t('dateInColumnColName' + textFragment + ' {{value}}-th day before the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
}
}
ruleHelpers.primitiveRuleTypes.date = {
eq: {
dropdownLabel: t('On'),
treeLabel: rule => t('Date in column "{{colName}}" is {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
dropdownLabel: t('on'),
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
lt: {
dropdownLabel: t('Before'),
treeLabel: rule => t('Date in column "{{colName}}" is before {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
dropdownLabel: t('before'),
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
le: {
dropdownLabel: t('Before or on'),
treeLabel: rule => t('Date in column "{{colName}}" is before or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
dropdownLabel: t('beforeOrOn'),
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
gt: {
dropdownLabel: t('After'),
treeLabel: rule => t('Date in column "{{colName}}" is after {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
dropdownLabel: t('after'),
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
ge: {
dropdownLabel: t('After or on'),
treeLabel: rule => t('Date in column "{{colName}}" is after or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
dropdownLabel: t('afterOrOn'),
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
eqTodayPlusDays: {
dropdownLabel: t('On x-th day before/after current date'),
dropdownLabel: t('onXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is'),
},
ltTodayPlusDays: {
dropdownLabel: t('Before x-th day before/after current date'),
dropdownLabel: t('beforeXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before'),
},
leTodayPlusDays: {
dropdownLabel: t('Before or on x-th day before/after current date'),
dropdownLabel: t('beforeOrOnXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before or on'),
},
gtTodayPlusDays: {
dropdownLabel: t('After x-th day before/after current date'),
dropdownLabel: t('afterXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after'),
},
geTodayPlusDays: {
dropdownLabel: t('After or on x-th day before/after current date'),
dropdownLabel: t('afterOrOnXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after or on'),
}
};
ruleHelpers.primitiveRuleTypes.birthday = {
eq: {
dropdownLabel: t('On'),
treeLabel: rule => t('Date in column "{{colName}}" is {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
dropdownLabel: t('on'),
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
lt: {
dropdownLabel: t('Before'),
treeLabel: rule => t('Date in column "{{colName}}" is before {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
dropdownLabel: t('before'),
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
le: {
dropdownLabel: t('Before or on'),
treeLabel: rule => t('Date in column "{{colName}}" is before or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
dropdownLabel: t('beforeOrOn'),
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
gt: {
dropdownLabel: t('After'),
treeLabel: rule => t('Date in column "{{colName}}" is after {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
dropdownLabel: t('after'),
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
ge: {
dropdownLabel: t('After or on'),
treeLabel: rule => t('Date in column "{{colName}}" is after or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
dropdownLabel: t('afterOrOn'),
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
}
};
ruleHelpers.primitiveRuleTypes.option = {
isTrue: {
dropdownLabel: t('Is selected'),
treeLabel: rule => t('Value in column "{{colName}}" is selected', {colName: ruleHelpers.getColumnName(rule.column)}),
dropdownLabel: t('isSelected'),
treeLabel: rule => t('valueInColumnColNameIsSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
},
isFalse: {
dropdownLabel: t('Is not selected'),
treeLabel: rule => t('Value in column "{{colName}}" is not selected', {colName: ruleHelpers.getColumnName(rule.column)}),
dropdownLabel: t('isNotSelected'),
treeLabel: rule => t('valueInColumnColNameIsNotSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
}
};
ruleHelpers.primitiveRuleTypes['dropdown-enum'] = ruleHelpers.primitiveRuleTypes['radio-enum'] = {
eq: {
dropdownLabel: t('Key equal to'),
treeLabel: rule => t('The selected key in column "{{colName}}" is equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('keyEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIsEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('Key match (with SQL LIKE)'),
treeLabel: rule => t('The selected key in column "{{colName}}" matches (with SQL LIKE) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('keyMatchWithSqlLike'),
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('Key match (with regular expressions)'),
treeLabel: rule => t('The selected key in column "{{colName}}" matches (with regular expressions) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('keyMatchWithRegularExpressions'),
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('Key alphabetically before'),
treeLabel: rule => t('The selected key in column "{{colName}}" is alphabetically before "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('keyAlphabeticallyBefore'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('Key alphabetically before or equal to'),
treeLabel: rule => t('The selected key in column "{{colName}}" is alphabetically before or equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('keyAlphabeticallyBeforeOrEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('Key alphabetically after'),
treeLabel: rule => t('The selected key in column "{{colName}}" is alphabetically after "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('keyAlphabeticallyAfter'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('Key alphabetically after or equal to'),
treeLabel: rule => t('The selected key in column "{{colName}}" is alphabetically after or equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
dropdownLabel: t('keyAlphabeticallyAfterOrEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
const stringValueSettings = allowEmpty => ({
form: <InputField id="value" label={t('Value')} />,
form: <InputField id="value" label={t('value')} />,
getFormData: rule => ({
value: rule.value
}),
@ -225,7 +225,7 @@ export function getRuleHelpers(t, fields) {
},
validate: state => {
if (!allowEmpty && !state.getIn(['value', 'value'])) {
state.setIn(['value', 'error'], t('Value must not be empty'));
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
} else {
state.setIn(['value', 'error'], null);
}
@ -233,7 +233,7 @@ export function getRuleHelpers(t, fields) {
});
const numberValueSettings = {
form: <InputField id="value" label={t('Value')} />,
form: <InputField id="value" label={t('value')} />,
getFormData: rule => ({
value: rule.value.toString()
}),
@ -243,9 +243,9 @@ export function getRuleHelpers(t, fields) {
validate: state => {
const value = state.getIn(['value', 'value']).trim();
if (value === '') {
state.setIn(['value', 'error'], t('Value must not be empty'));
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
} else if (isNaN(value)) {
state.setIn(['value', 'error'], t('Value must be a number'));
state.setIn(['value', 'error'], t('valueMustBeANumber'));
} else {
state.setIn(['value', 'error'], null);
}
@ -253,7 +253,7 @@ export function getRuleHelpers(t, fields) {
};
const birthdayValueSettings = {
form: <DatePicker id="birthday" label={t('Date')} birthday />,
form: <DatePicker id="birthday" label={t('date')} birthday />,
getFormData: rule => ({
birthday: formatBirthday(DateFormat.INTL, rule.value)
}),
@ -264,9 +264,9 @@ export function getRuleHelpers(t, fields) {
const value = state.getIn(['birthday', 'value']);
const date = parseBirthday(DateFormat.INTL, value);
if (!value) {
state.setIn(['birthday', 'error'], t('Date must not be empty'));
state.setIn(['birthday', 'error'], t('dateMustNotBeEmpty'));
} else if (!date) {
state.setIn(['birthday', 'error'], t('Date is invalid'));
state.setIn(['birthday', 'error'], t('dateIsInvalid'));
} else {
state.setIn(['birthday', 'error'], null);
}
@ -274,7 +274,7 @@ export function getRuleHelpers(t, fields) {
};
const dateValueSettings = {
form: <DatePicker id="date" label={t('Date')} />,
form: <DatePicker id="date" label={t('date')} />,
getFormData: rule => ({
date: formatDate(DateFormat.INTL, rule.value)
}),
@ -285,9 +285,9 @@ export function getRuleHelpers(t, fields) {
const value = state.getIn(['date', 'value']);
const date = parseDate(DateFormat.INTL, value);
if (!value) {
state.setIn(['date', 'error'], t('Date must not be empty'));
state.setIn(['date', 'error'], t('dateMustNotBeEmpty'));
} else if (!date) {
state.setIn(['date', 'error'], t('Date is invalid'));
state.setIn(['date', 'error'], t('dateIsInvalid'));
} else {
state.setIn(['date', 'error'], null);
}
@ -297,10 +297,10 @@ export function getRuleHelpers(t, fields) {
const dateRelativeValueSettings = {
form:
<div>
<InputField id="daysValue" label={t('Number of days')}/>
<Dropdown id="direction" label={t('Before/After')} options={[
{ key: 'before', label: t('Before current date') },
{ key: 'after', label: t('After current date') }
<InputField id="daysValue" label={t('numberOfDays')}/>
<Dropdown id="direction" label={t('beforeAfter')} options={[
{ key: 'before', label: t('beforeCurrentDate') },
{ key: 'after', label: t('afterCurrentDate') }
]}/>
</div>,
getFormData: rule => ({
@ -314,9 +314,9 @@ export function getRuleHelpers(t, fields) {
validate: state => {
const value = state.getIn(['daysValue', 'value']);
if (!value) {
state.setIn(['daysValue', 'error'], t('Number of days must not be empty'));
state.setIn(['daysValue', 'error'], t('numberOfDaysMustNotBeEmpty'));
} else if (isNaN(value)) {
state.setIn(['daysValue', 'error'], t('Number of days must be a number'));
state.setIn(['daysValue', 'error'], t('numberOfDaysMustBeANumber'));
} else {
state.setIn(['daysValue', 'error'], null);
}
@ -383,28 +383,28 @@ export function getRuleHelpers(t, fields) {
const predefColumns = [
{
column: 'email',
name: t('Email address'),
name: t('emailAddress-1'),
type: 'text',
key: 'EMAIL'
},
{
column: 'opt_in_country',
name: t('Signup country'),
name: t('signupCountry'),
type: 'text'
},
{
column: 'created',
name: t('Sign up date'),
name: t('signUpDate'),
type: 'date'
},
{
column: 'latest_open',
name: t('Latest open'),
name: t('latestOpen'),
type: 'date'
},
{
column: 'latest_click',
name: t('Latest click'),
name: t('latestClick'),
type: 'date'
}
];

View file

@ -1,24 +1,40 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import PropTypes
from 'prop-types';
import {HTTPMethod} from '../../lib/axios';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
import {withTranslation} from '../../lib/i18n';
import {
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField, CheckBox
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../../lib/page';
import {
AlignedRow,
Button,
ButtonRow,
CheckBox,
Dropdown,
Form,
FormSendMethod,
InputField,
withForm
} from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog, RestActionModalDialog} from "../../lib/modals";
import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators';
import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date';
import { SubscriptionStatus } from '../../../../shared/lists';
import {getFieldTypes, getSubscriptionStatusLabels} from './helpers';
import moment from 'moment-timezone';
import {withErrorHandling} from '../../lib/error-handling';
import {RestActionModalDialog} from "../../lib/modals";
import interoperableErrors
from '../../../../shared/interoperable-errors';
import {SubscriptionStatus} from '../../../../shared/lists';
import {
getFieldTypes,
getSubscriptionStatusLabels
} from './helpers';
import moment
from 'moment-timezone';
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -82,11 +98,11 @@ export default class CUD extends Component {
const emailServerValidation = state.getIn(['email', 'serverValidation']);
if (!state.getIn(['email', 'value'])) {
state.setIn(['email', 'error'], t('Email must not be empty'));
state.setIn(['email', 'error'], t('emailMustNotBeEmpty-1'));
} else if (!emailServerValidation) {
state.setIn(['email', 'error'], t('Validation is in progress...'));
state.setIn(['email', 'error'], t('validationIsInProgress'));
} else if (emailServerValidation.exists) {
state.setIn(['email', 'error'], t('Another subscription with the same email already exists.'));
state.setIn(['email', 'error'], t('anotherSubscriptionWithTheSameEmail'));
} else {
state.setIn(['email', 'error'], null);
}
@ -110,7 +126,7 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.status = parseInt(data.status);
@ -122,17 +138,17 @@ export default class CUD extends Component {
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/subscriptions`, 'success', t('Susbscription saved'));
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/subscriptions`, 'success', t('susbscriptionSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
if (error instanceof interoperableErrors.DuplicitEmailError) {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{t('It seems that another subscription with the same email has been created in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('itSeemsThatAnotherSubscriptionWithThe')}
</span>
);
return;
@ -152,7 +168,7 @@ export default class CUD extends Component {
.map(key => ({key, label: this.subscriptionStatusLabels[key]}));
const tzOptions = [
{ key: '', label: t('Not selected') },
{ key: '', label: t('notSelected') },
...moment.tz.names().map(tz => ({ key: tz.toLowerCase(), label: tz }))
];
@ -166,33 +182,33 @@ export default class CUD extends Component {
{isEdit &&
<div>
<RestActionModalDialog
title={t('Confirm deletion')}
message={t('Are you sure you want to delete subscription for "{{email}}"?', {name: this.getFormValue('email')})}
title={t('confirmDeletion')}
message={t('areYouSureYouWantToDeleteSubscriptionFor', {email: this.getFormValue('email') || ''})}
stateOwner={this}
visible={this.props.action === 'delete'}
actionMethod={HTTPMethod.DELETE}
actionUrl={`rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`}
backUrl={`/lists/${this.props.list.id}/subscriptions/${this.props.entity.id}/edit`}
successUrl={`/lists/${this.props.list.id}/subscriptions`}
actionInProgressMsg={t('Deleting subscription ...')}
actionDoneMsg={t('Subscription deleted')}/>
actionInProgressMsg={t('deletingSubscription')}
actionDoneMsg={t('subscriptionDeleted')}/>
</div>
}
<Title>{isEdit ? t('Edit Subscription') : t('Create Subscription')}</Title>
<Title>{isEdit ? t('editSubscription') : t('createSubscription')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="email" label={t('Email')}/>
<InputField id="email" label={t('email')}/>
{customFields}
<hr />
<Dropdown id="tz" label={t('Timezone')} options={tzOptions}/>
<Dropdown id="tz" label={t('timezone')} options={tzOptions}/>
<Dropdown id="status" label={t('Subscription status')} options={statusOptions}/>
<Dropdown id="status" label={t('subscriptionStatus')} options={statusOptions}/>
<CheckBox id="is_test" text={t('Test user?')} help={t('If checked then this subscription can be used for previewing campaign messages')}/>
<CheckBox id="is_test" text={t('testUser?')} help={t('ifCheckedThenThisSubscriptionCanBeUsed')}/>
{!isEdit &&
<AlignedRow>
@ -203,8 +219,8 @@ export default class CUD extends Component {
</AlignedRow>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.list.id}/subscriptions/${this.props.entity.id}/delete`}/>}
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/lists/${this.props.list.id}/subscriptions/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling';
import { Table } from '../../lib/table';
@ -22,7 +22,7 @@ import {
tableDeleteDialogRender
} from "../../lib/modals";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -86,10 +86,10 @@ export default class List extends Component {
const segments = this.props.segments;
const columns = [
{ data: 1, title: t('ID'), render: data => <code>{data}</code> },
{ data: 2, title: t('Email') },
{ data: 3, title: t('Status'), render: (data, display, rowData) => this.subscriptionStatusLabels[data] + (rowData[5] ? ', ' + t('Blacklisted') : '') },
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
{ data: 1, title: t('id'), render: data => <code>{data}</code> },
{ data: 2, title: t('email') },
{ data: 3, title: t('status'), render: (data, display, rowData) => this.subscriptionStatusLabels[data] + (rowData[5] ? ', ' + t('blacklisted') : '') },
{ data: 4, title: t('created'), render: data => data ? moment(data).fromNow() : '' }
];
let colIdx = 6;
@ -114,20 +114,20 @@ export default class List extends Component {
const actions = [];
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/lists/${this.props.list.id}/subscriptions/${data[0]}/edit`
});
if (data[3] === SubscriptionStatus.SUBSCRIBED) {
actions.push({
label: <Icon icon="off" title={t('Unsubscribe')}/>,
label: <Icon icon="off" title={t('unsubscribe')}/>,
action: () => this.unsubscribeSubscription(data[0])
});
}
if (!data[5]) {
actions.push({
label: <Icon icon="ban-circle" title={t('Blacklist')}/>,
label: <Icon icon="ban-circle" title={t('blacklist')}/>,
action: () => this.blacklistSubscription(data[2])
});
}
@ -140,7 +140,7 @@ export default class List extends Component {
}
const segmentOptions = [
{key: '', label: t('All subscriptions')},
{key: '', label: t('allSubscriptions')},
...segments.map(x => ({ key: x.id.toString(), label: x.name}))
];
@ -154,14 +154,14 @@ export default class List extends Component {
// FIXME - presents segments in a data table as in campaign edit
return (
<div>
{tableDeleteDialogRender(this, `rest/subscriptions/${this.props.list.id}`, t('Deleting subscription ...'), t('Subscription deleted'))}
{tableDeleteDialogRender(this, `rest/subscriptions/${this.props.list.id}`, t('deletingSubscription'), t('subscriptionDeleted'))}
<Toolbar>
<a href={getPublicUrl(`subscription/${this.props.list.cid}`)}><Button label={t('Subscription Form')} className="btn-default"/></a>
<a href={getUrl(`subscriptions/export/${this.props.list.id}/`+ (this.props.segmentId || 0))}><Button label={t('Export as CSV')} className="btn-primary"/></a>
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/>
<a href={getPublicUrl(`subscription/${this.props.list.cid}`)}><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>
<Title>{t('Subscribers')}</Title>
<Title>{t('subscribers')}</Title>
{list.description &&
<div className="well well-sm">{list.description}</div>
@ -169,7 +169,7 @@ export default class List extends Component {
<div className="well well-sm">
<Form format="inline" stateOwner={this}>
<Dropdown format="inline" className="input-sm" id="segment" label={t('Segment')} options={segmentOptions}/>
<Dropdown format="inline" className="input-sm" id="segment" label={t('segment')} options={segmentOptions}/>
</Form>
</div>

View file

@ -10,10 +10,10 @@ import 'brace/mode/json';
export function getSubscriptionStatusLabels(t) {
const subscriptionStatusLabels = {
[SubscriptionStatus.SUBSCRIBED]: t('Subscribed'),
[SubscriptionStatus.UNSUBSCRIBED]: t('Unubscribed'),
[SubscriptionStatus.BOUNCED]: t('Bounced'),
[SubscriptionStatus.COMPLAINED]: t('Complained'),
[SubscriptionStatus.SUBSCRIBED]: t('subscribed'),
[SubscriptionStatus.UNSUBSCRIBED]: t('unubscribed'),
[SubscriptionStatus.BOUNCED]: t('bounced'),
[SubscriptionStatus.COMPLAINED]: t('complained'),
};
return subscriptionStatusLabels;
@ -52,7 +52,7 @@ export function getFieldTypes(t) {
validate: (groupedField, state) => {
const value = state.getIn([getFieldColumn(groupedField), 'value']).trim();
if (value !== '' && isNaN(value)) {
state.setIn([getFieldColumn(groupedField), 'error'], t('Value must be a number'));
state.setIn([getFieldColumn(groupedField), 'error'], t('valueMustBeANumber'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}
@ -77,7 +77,7 @@ export function getFieldTypes(t) {
const value = state.getIn([getFieldColumn(groupedField), 'value']);
const date = parseDate(groupedField.settings.dateFormat, value);
if (value !== '' && !date) {
state.setIn([getFieldColumn(groupedField), 'error'], t('Date is invalid'));
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}
@ -102,7 +102,7 @@ export function getFieldTypes(t) {
const value = state.getIn([getFieldColumn(groupedField), 'value']);
const date = parseBirthday(groupedField.settings.dateFormat, value);
if (value !== '' && !date) {
state.setIn([getFieldColumn(groupedField), 'error'], t('Date is invalid'));
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page';
import { withForm, Form, FormSendMethod, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
import axios from '../lib/axios';
@ -13,7 +13,7 @@ import mailtrainConfig from 'mailtrainConfig';
import {getGlobalNamespaceId} from "../../../shared/namespaces";
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -89,14 +89,14 @@ export default class CUD extends Component {
const t = this.props.t;
if (!state.getIn(['name', 'value']).trim()) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
if (!this.isEditGlobal()) {
if (!state.getIn(['namespace', 'value'])) {
state.setIn(['namespace', 'error'], t('Parent Namespace must be selected'));
state.setIn(['namespace', 'error'], t('parentNamespaceMustBeSelected'));
} else {
state.setIn(['namespace', 'error'], null);
}
@ -117,23 +117,23 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);
if (submitSuccessful) {
this.navigateToWithFlashMessage('/namespaces', 'success', t('Namespace saved'));
this.navigateToWithFlashMessage('/namespaces', 'success', t('namespaceSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
if (error instanceof interoperableErrors.LoopDetectedError) {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{t('There has been a loop detected in the assignment of the parent namespace. This is most likely because someone else has changed the parent of some namespace in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('thereHasBeenALoopDetectedInTheAssignment')}
</span>
);
return;
@ -142,8 +142,8 @@ export default class CUD extends Component {
if (error instanceof interoperableErrors.DependencyNotFoundError) {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{t('It seems that the parent namespace has been deleted in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('itSeemsThatTheParentNamespaceHasBeen')}
</span>
);
return;
@ -167,22 +167,22 @@ export default class CUD extends Component {
deleteUrl={`rest/namespaces/${this.props.entity.id}`}
backUrl={`/namespaces/${this.props.entity.id}/edit`}
successUrl="/namespaces"
deletingMsg={t('Deleting namespace ...')}
deletedMsg={t('Namespace deleted')} />
deletingMsg={t('deletingNamespace')}
deletedMsg={t('namespaceDeleted')} />
}
<Title>{isEdit ? t('Edit Namespace') : t('Create Namespace')}</Title>
<Title>{isEdit ? t('editNamespace') : t('createNamespace')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/>
{!this.isEditGlobal() &&
<TreeTableSelect id="namespace" label={t('Parent Namespace')} data={this.state.treeData}/>}
<TreeTableSelect id="namespace" label={t('parentNamespace')} data={this.state.treeData}/>}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/namespaces/${this.props.entity.id}/delete`}/>}
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/namespaces/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -1,7 +1,7 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
import { TreeTable } from '../lib/tree';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
@ -15,7 +15,7 @@ import {
} from "../lib/modals";
import {getGlobalNamespaceId} from "../../../shared/namespaces";
@translate()
@withTranslation()
@withErrorHandling
@withPageHelpers
@requiresAuthenticatedUser
@ -54,14 +54,14 @@ export default class List extends Component {
if (node.data.permissions.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/namespaces/${node.key}/edit`
});
}
if (node.data.permissions.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/namespaces/${node.key}/share`
});
}
@ -75,14 +75,14 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/namespaces`, t('Deleting namespace ...'), t('Namespace deleted'))}
{tableDeleteDialogRender(this, `rest/namespaces`, t('deletingNamespace'), t('namespaceDeleted'))}
{this.state.createPermitted &&
<Toolbar>
<NavButton linkTo="/namespaces/create" className="btn-primary" icon="plus" label={t('Create Namespace')}/>
<NavButton linkTo="/namespaces/create" className="btn-primary" icon="plus" label={t('createNamespace')}/>
</Toolbar>
}
<Title>{t('Namespaces')}</Title>
<Title>{t('namespaces')}</Title>
<TreeTable ref={node => this.table = node} withHeader withDescription dataUrl="rest/namespaces-tree" actions={actions} />
</div>

View file

@ -8,33 +8,33 @@ import Share from '../shares/Share';
function getMenus(t) {
return {
namespaces: {
title: t('Namespaces'),
title: t('namespaces'),
link: '/namespaces',
panelComponent: List,
children: {
':namespaceId([0-9]+)': {
title: resolved => t('Namespace "{{name}}"', {name: resolved.namespace.name}),
title: resolved => t('namespaceName', {name: resolved.namespace.name}),
resolve: {
namespace: params => `rest/namespaces/${params.namespaceId}`
},
link: params => `/namespaces/${params.namespaceId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/namespaces/${params.namespaceId}/edit`,
visible: resolved => resolved.namespace.permissions.includes('edit'),
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.namespace} />
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/namespaces/${params.namespaceId}/share`,
visible: resolved => resolved.namespace.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.namespace} entityTypeId="namespace" />
panelRender: props => <Share title={t('share')} entity={props.resolved.namespace} entityTypeId="namespace" />
}
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <CUD action="create" />
},
}

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page';
import {
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, TableSelectMode, ButtonRow, Button,
@ -16,7 +16,7 @@ import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -79,13 +79,13 @@ export default class CUD extends Component {
const edit = this.props.entity;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
if (!state.getIn(['report_template', 'value'])) {
state.setIn(['report_template', 'error'], t('Report template must be selected'));
state.setIn(['report_template', 'error'], t('reportTemplateMustBeSelected'));
} else {
state.setIn(['report_template', 'error'], null);
}
@ -104,13 +104,13 @@ export default class CUD extends Component {
if (spec.maxOccurences === 1) {
if (spec.minOccurences === 1 && (selection === null || selection === undefined)) { // FIXME - this does not seem to correspond with selectionAsArray
state.setIn([fldId, 'error'], t('Exactly one item has to be selected'));
state.setIn([fldId, 'error'], t('exactlyOneItemHasToBeSelected'));
}
} else {
if (selection.length < spec.minOccurences) {
state.setIn([fldId, 'error'], t('At least {{ count }} item(s) have to be selected', { count: spec.minOccurences }));
state.setIn([fldId, 'error'], t('atLeastCountItemsHaveToBeSelected', { count: spec.minOccurences }));
} else if (selection.length > spec.maxOccurences) {
state.setIn([fldId, 'error'], t('At most {{ count }} item(s) can to be selected', { count: spec.maxOccurences }));
state.setIn([fldId, 'error'], t('atMostCountItemsCanToBeSelected', { count: spec.maxOccurences }));
}
}
}
@ -123,7 +123,7 @@ export default class CUD extends Component {
const t = this.props.t;
if (this.getFormValue('report_template') && !this.getFormValue('user_fields')) {
this.setFormStatusMessage('warning', t('Report parameters are not selected. Wait for them to get displayed and then fill them in.'));
this.setFormStatusMessage('warning', t('reportParametersAreNotSelectedWaitFor'));
return;
}
@ -137,7 +137,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
const params = {};
@ -153,10 +153,10 @@ export default class CUD extends Component {
});
if (submitSuccessful) {
this.navigateToWithFlashMessage('/reports', 'success', t('Report saved'));
this.navigateToWithFlashMessage('/reports', 'success', t('reportSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -166,9 +166,9 @@ export default class CUD extends Component {
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
const reportTemplateColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() }
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('created'), render: data => moment(data).fromNow() }
];
const userFieldsSpec = this.getFormValue('user_fields');
@ -195,21 +195,21 @@ export default class CUD extends Component {
if (spec.type === 'campaign') {
addUserFieldTableSelect(spec, 'rest/campaigns-table', 1,[
{data: 0, title: "#"},
{data: 1, title: t('Name')},
{data: 2, title: t('Description')},
{data: 3, title: t('Status')},
{data: 4, title: t('Created'), render: data => moment(data).fromNow()}
{data: 1, title: t('name')},
{data: 2, title: t('description')},
{data: 3, title: t('status')},
{data: 4, title: t('created'), render: data => moment(data).fromNow()}
]);
} else if (spec.type === 'list') {
addUserFieldTableSelect(spec, 'rest/lists-table', 1,[
{data: 0, title: "#"},
{data: 1, title: t('Name')},
{data: 2, title: t('ID')},
{data: 3, title: t('Subscribers')},
{data: 4, title: t('Description')}
{data: 1, title: t('name')},
{data: 2, title: t('id')},
{data: 3, title: t('subscribers')},
{data: 4, title: t('description')}
]);
} else {
userFields.push(<div className="alert alert-danger" role="alert">{t('Unknown field type "{{type}}"', { type: spec.type })}</div>)
userFields.push(<div className="alert alert-danger" role="alert">{t('unknownFieldTypeType', { type: spec.type })}</div>)
}
}
}
@ -223,34 +223,34 @@ export default class CUD extends Component {
deleteUrl={`rest/reports/${this.props.entity.id}`}
backUrl={`/reports/${this.props.entity.id}/edit`}
successUrl="/reports"
deletingMsg={t('Deleting report ...')}
deletedMsg={t('Report deleted')}/>
deletingMsg={t('deletingReport')}
deletedMsg={t('reportDeleted')}/>
}
<Title>{isEdit ? t('Edit Report') : t('Create Report')}</Title>
<Title>{isEdit ? t('editReport') : t('createReport')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/>
<TableSelect id="report_template" label={t('Report Template')} withHeader dropdown dataUrl="rest/report-templates-table" columns={reportTemplateColumns} selectionLabelIndex={1}/>
<TableSelect id="report_template" label={t('reportTemplate-1')} withHeader dropdown dataUrl="rest/report-templates-table" columns={reportTemplateColumns} selectionLabelIndex={1}/>
<NamespaceSelect/>
{userFieldsSpec ?
userFields.length > 0 &&
<Fieldset label={t('Report parameters')}>
<Fieldset label={t('reportParameters')}>
{userFields}
</Fieldset>
:
this.getFormValue('report_template') &&
<div className="alert alert-info" role="alert">{t('Loading report template...')}</div>
<div className="alert alert-info" role="alert">{t('loadingReportTemplate')}</div>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{canDelete &&
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/${this.props.entity.id}/delete`}/>
<NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/reports/${this.props.entity.id}/delete`}/>
}
</ButtonRow>
</Form>

View file

@ -1,7 +1,7 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
import { Table } from '../lib/table';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
@ -17,7 +17,7 @@ import {
tableDeleteDialogRender
} from "../lib/modals";
@translate()
@withTranslation()
@withErrorHandling
@withPageHelpers
@requiresAuthenticatedUser
@ -77,11 +77,11 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Template') },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' },
{ data: 5, title: t('Namespace') },
{ data: 1, title: t('name') },
{ data: 2, title: t('template') },
{ data: 3, title: t('description') },
{ data: 4, title: t('created'), render: data => data ? moment(data).fromNow() : '' },
{ data: 5, title: t('namespace') },
{
actions: data => {
const actions = [];
@ -96,11 +96,11 @@ export default class List extends Component {
if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) {
viewContent = {
label: <Icon icon="hourglass" title={t('Processing')}/>,
label: <Icon icon="hourglass" title={t('processing-1')}/>,
};
startStop = {
label: <Icon icon="stop" title={t('Stop')}/>,
label: <Icon icon="stop" title={t('stop')}/>,
action: (table) => this.stop(table, id)
};
@ -108,28 +108,28 @@ export default class List extends Component {
} else if (state === ReportState.FINISHED) {
if (mimeType === 'text/html') {
viewContent = {
label: <Icon icon="eye-open" title={t('View')}/>,
label: <Icon icon="eye-open" title={t('view')}/>,
link: `/reports/${id}/view`
};
} else if (mimeType === 'text/csv') {
viewContent = {
label: <Icon icon="download-alt" title={t('Download')}/>,
label: <Icon icon="download-alt" title={t('download')}/>,
href: `/reports/${id}/download`
};
}
startStop = {
label: <Icon icon="repeat" title={t('Refresh report')}/>,
label: <Icon icon="repeat" title={t('refreshReport')}/>,
action: (table) => this.start(table, id)
};
} else if (state === ReportState.FAILED) {
viewContent = {
label: <Icon icon="thumbs-down" title={t('Report generation failed')}/>,
label: <Icon icon="thumbs-down" title={t('reportGenerationFailed')}/>,
};
startStop = {
label: <Icon icon="repeat" title={t('Regenerate report')}/>,
label: <Icon icon="repeat" title={t('regenerateReport')}/>,
action: (table) => this.start(table, id)
};
}
@ -141,7 +141,7 @@ export default class List extends Component {
if (perms.includes('viewOutput')) {
actions.push(
{
label: <Icon icon="modal-window" title={t('View console output')}/>,
label: <Icon icon="modal-window" title={t('viewConsoleOutput')}/>,
link: `/reports/${id}/output`
}
);
@ -153,14 +153,14 @@ export default class List extends Component {
if (perms.includes('edit') && permsReportTemplate.includes('execute')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/reports/${id}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/reports/${id}/share`
});
}
@ -175,17 +175,17 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/reports`, t('Deleting report ...'), t('Report deleted'))}
{tableDeleteDialogRender(this, `rest/reports`, t('deletingReport'), t('reportDeleted'))}
<Toolbar>
{this.state.createPermitted &&
<NavButton linkTo="/reports/create" className="btn-primary" icon="plus" label={t('Create Report')}/>
<NavButton linkTo="/reports/create" className="btn-primary" icon="plus" label={t('createReport')}/>
}
{this.state.templatesPermitted &&
<NavButton linkTo="/reports/templates" className="btn-primary" label={t('Report Templates')}/>
<NavButton linkTo="/reports/templates" className="btn-primary" label={t('reportTemplates')}/>
}
</Toolbar>
<Title>{t('Reports')}</Title>
<Title>{t('reports')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/reports-table" columns={columns} />
</div>

View file

@ -1,13 +1,13 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import axios from '../lib/axios';
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -44,13 +44,13 @@ export default class Output extends Component {
if (this.state.report) {
return (
<div>
<Title>{t('Output for report {{name}}', { name: this.state.report.name })}</Title>
<Title>{t('outputForReportName', { name: this.state.report.name })}</Title>
<pre>{this.state.output}</pre>
</div>
);
} else {
return <div>{t('Loading report output ...')}</div>;
return <div>{t('loadingReportOutput')}</div>;
}
}

View file

@ -1,14 +1,14 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import axios from '../lib/axios';
import { ReportState } from '../../../shared/reports';
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -46,16 +46,16 @@ export default class View extends Component {
if (this.state.report.state === ReportState.FINISHED) {
return (
<div>
<Title>{t('Report {{name}}', { name: this.state.report.name })}</Title>
<Title>{t('reportName', { name: this.state.report.name })}</Title>
<div dangerouslySetInnerHTML={{ __html: this.state.content }}/>
</div>
);
} else {
return <div className="alert alert-danger" role="alert">{t('Report not generated')}</div>;
return <div className="alert alert-danger" role="alert">{t('reportNotGenerated')}</div>;
}
} else {
return <div>{t('Loading report ...')}</div>;
return <div>{t('loadingReport')}</div>;
}
}
}

View file

@ -15,80 +15,80 @@ import mailtrainConfig from 'mailtrainConfig';
function getMenus(t) {
return {
'reports': {
title: t('Reports'),
title: t('reports'),
link: '/reports',
panelComponent: ReportsList,
children: {
':reportId([0-9]+)': {
title: resolved => t('Report "{{name}}"', {name: resolved.report.name}),
title: resolved => t('reportName-1', {name: resolved.report.name}),
resolve: {
report: params => `rest/reports/${params.reportId}`
},
link: params => `/reports/${params.reportId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/reports/${params.reportId}/edit`,
visible: resolved => resolved.report.permissions.includes('edit'),
panelRender: props => <ReportsCUD action={props.match.params.action} entity={props.resolved.report} />
},
view: {
title: t('View'),
title: t('view'),
link: params => `/reports/${params.reportId}/view`,
visible: resolved => resolved.report.permissions.includes('viewContent') && resolved.report.state === ReportState.FINISHED && resolved.report.mime_type === 'text/html',
panelRender: props => (<ReportsView {...props} />),
},
download: {
title: t('Download'),
title: t('download'),
externalLink: params => `/reports/${params.reportId}/download`,
visible: resolved => resolved.report.permissions.includes('viewContent') && resolved.report.state === ReportState.FINISHED && resolved.report.mime_type === 'text/csv'
},
output: {
title: t('Output'),
title: t('output'),
link: params => `/reports/${params.reportId}/output`,
visible: resolved => resolved.report.permissions.includes('viewOutput'),
panelRender: props => (<ReportsOutput {...props} />)
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/reports/${params.reportId}/share`,
visible: resolved => resolved.report.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.report} entityTypeId="report" />
panelRender: props => <Share title={t('share')} entity={props.resolved.report} entityTypeId="report" />
}
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <ReportsCUD action="create" />
},
templates: {
title: t('Templates'),
title: t('templates'),
link: '/reports/templates',
panelComponent: ReportTemplatesList,
children: {
':templateId([0-9]+)': {
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
title: resolved => t('templateName', {name: resolved.template.name}),
resolve: {
template: params => `rest/report-templates/${params.templateId}`
},
link: params => `/reports/templates/${params.templateId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/reports/templates/${params.templateId}/edit`,
visible: resolved => mailtrainConfig.globalPermissions.createJavascriptWithROAccess && resolved.template.permissions.includes('edit'),
panelRender: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/reports/templates/${params.templateId}/share`,
visible: resolved => resolved.template.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.template} entityTypeId="reportTemplate" />
panelRender: props => <Share title={t('share')} entity={props.resolved.template} entityTypeId="reportTemplate" />
}
}
},
create: {
title: t('Create'),
title: t('create'),
extraParams: [':wizard?'],
panelRender: props => <ReportTemplatesCUD action="create" wizard={props.match.params.wizard} />
}

View file

@ -3,9 +3,9 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Trans,
translate
Trans
} from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -34,7 +34,7 @@ import 'brace/mode/javascript';
import 'brace/mode/json';
import 'brace/mode/handlebars';
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -225,13 +225,13 @@ export default class CUD extends Component {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
if (!state.getIn(['mime_type', 'value'])) {
state.setIn(['mime_type', 'error'], t('MIME Type must be selected'));
state.setIn(['mime_type', 'error'], t('mimeTypeMustBeSelected'));
} else {
state.setIn(['mime_type', 'error'], null);
}
@ -241,7 +241,7 @@ export default class CUD extends Component {
state.setIn(['user_fields', 'error'], null);
} catch (err) {
if (err instanceof SyntaxError) {
state.setIn(['user_fields', 'error'], t('Syntax error in the user fields specification'));
state.setIn(['user_fields', 'error'], t('syntaxErrorInTheUserFieldsSpecification'));
}
}
@ -269,7 +269,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);
@ -277,13 +277,13 @@ export default class CUD extends Component {
if (stay) {
await this.getFormValuesFromURL(`rest/report-templates/${this.props.entity.id}`);
this.enableForm();
this.setFormStatusMessage('success', t('Report template saved'));
this.setFormStatusMessage('success', t('reportTemplateSaved'));
} else {
this.navigateToWithFlashMessage('/reports/templates', 'success', t('Report template saved'));
this.navigateToWithFlashMessage('/reports/templates', 'success', t('reportTemplateSaved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -301,32 +301,32 @@ export default class CUD extends Component {
deleteUrl={`rest/reports/templates/${this.props.entity.id}`}
backUrl={`/reports/templates/${this.props.entity.id}/edit`}
successUrl="/reports/templates"
deletingMsg={t('Deleting report template ...')}
deletedMsg={t('Report template deleted')}/>
deletingMsg={t('deletingReportTemplate')}
deletedMsg={t('reportTemplateDeleted')}/>
}
<Title>{isEdit ? t('Edit Report Template') : t('Create Report Template')}</Title>
<Title>{isEdit ? t('editReportTemplate') : t('createReportTemplate')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<Dropdown id="mime_type" label={t('Type')} options={[{key: 'text/html', label: t('HTML')}, {key: 'text/csv', label: t('CSV')}]}/>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/>
<Dropdown id="mime_type" label={t('type')} options={[{key: 'text/html', label: t('html')}, {key: 'text/csv', label: t('csv')}]}/>
<NamespaceSelect/>
<ACEEditor id="user_fields" height="250px" mode="json" label={t('User selectable fields')} help={t('JSON specification of user selectable fields.')}/>
<ACEEditor id="js" height="700px" mode="javascript" label={t('Data processing code')} help={<Trans>Write the body of the JavaScript function with signature <code>function(inputs, callback)</code> that returns an object to be rendered by the Handlebars template below.</Trans>}/>
<ACEEditor id="hbs" height="700px" mode="handlebars" label={t('Rendering template')} help={<Trans>Use HTML with Handlebars syntax. See documentation <a href="http://handlebarsjs.com/">here</a>.</Trans>}/>
<ACEEditor id="user_fields" height="250px" mode="json" label={t('userSelectableFields')} help={t('jsonSpecificationOfUserSelectableFields')}/>
<ACEEditor id="js" height="700px" mode="javascript" label={t('dataProcessingCode')} help={<Trans i18nKey="writeTheBodyOfTheJavaScriptFunctionWith">Write the body of the JavaScript function with signature <code>function(inputs, callback)</code> that returns an object to be rendered by the Handlebars template below.</Trans>}/>
<ACEEditor id="hbs" height="700px" mode="handlebars" label={t('renderingTemplate')} help={<Trans i18nKey="useHtmlWithHandlebarsSyntaxSee">Use HTML with Handlebars syntax. See documentation <a href="http://handlebarsjs.com/">here</a>.</Trans>}/>
{isEdit ?
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('saveAndStay')} onClickAsync={::this.submitAndStay}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('saveAndLeave')}/>
{canDelete &&
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/templates/${this.props.entity.id}/delete`}/>
<NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/reports/templates/${this.props.entity.id}/delete`}/>
}
</ButtonRow>
:
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
</ButtonRow>
}
</Form>

View file

@ -1,7 +1,7 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {DropdownMenu, Icon} from '../../lib/bootstrap-components';
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, MenuLink } from '../../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
@ -16,7 +16,7 @@ import {
tableDeleteDialogRender
} from "../../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -51,10 +51,10 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 4, title: t('Namespace') },
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('created'), render: data => moment(data).fromNow() },
{ data: 4, title: t('namespace') },
{
actions: data => {
const actions = [];
@ -62,14 +62,14 @@ export default class List extends Component {
if (mailtrainConfig.globalPermissions.createJavascriptWithROAccess && perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/reports/templates/${data[0]}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/reports/templates/${data[0]}/share`
});
}
@ -83,19 +83,19 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/reports/templates`, t('Deleting report template ...'), t('Report template deleted'))}
{tableDeleteDialogRender(this, `rest/reports/templates`, t('deletingReportTemplate'), t('reportTemplateDeleted'))}
{this.state.createPermitted &&
<Toolbar>
<DropdownMenu className="btn-primary" label={t('Create Report Template')}>
<MenuLink to="/reports/templates/create">{t('Blank')}</MenuLink>
<MenuLink to="/reports/templates/create/subscribers-all">{t('All Subscribers')}</MenuLink>
<MenuLink to="/reports/templates/create/subscribers-grouped">{t('Grouped Subscribers')}</MenuLink>
<MenuLink to="/reports/templates/create/export-list-csv">{t('Export List as CSV')}</MenuLink>
<DropdownMenu className="btn-primary" label={t('createReportTemplate')}>
<MenuLink to="/reports/templates/create">{t('blank')}</MenuLink>
<MenuLink to="/reports/templates/create/subscribers-all">{t('allSubscribers')}</MenuLink>
<MenuLink to="/reports/templates/create/subscribers-grouped">{t('groupedSubscribers')}</MenuLink>
<MenuLink to="/reports/templates/create/export-list-csv">{t('exportListAsCsv')}</MenuLink>
</DropdownMenu>
</Toolbar>
}
<Title>{t('Report Templates')}</Title>
<Title>{t('reportTemplates')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/report-templates-table" columns={columns} />
</div>

View file

@ -3,62 +3,102 @@
import './lib/public-path';
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {
I18nextProvider,
withNamespaces
} from 'react-i18next';
import i18n from './lib/i18n';
import ReactDOM
from 'react-dom';
import {I18nextProvider} from 'react-i18next';
import i18n, {withTranslation} from './lib/i18n';
import account from './account/root';
import blacklist from './blacklist/root';
import lists from './lists/root';
import namespaces from './namespaces/root';
import reports from './reports/root';
import campaigns from './campaigns/root';
import templates from './templates/root';
import users from './users/root';
import sendConfigurations from './send-configurations/root';
import settings from './settings/root';
import account
from './account/root';
import blacklist
from './blacklist/root';
import lists
from './lists/root';
import namespaces
from './namespaces/root';
import reports
from './reports/root';
import campaigns
from './campaigns/root';
import templates
from './templates/root';
import users
from './users/root';
import sendConfigurations
from './send-configurations/root';
import settings
from './settings/root';
import {
MenuLink,
Section
} from "./lib/page";
import mailtrainConfig from 'mailtrainConfig';
import Home from "./Home";
import mailtrainConfig
from 'mailtrainConfig';
import Home
from "./Home";
import {
ActionLink,
DropdownMenuItem,
Icon
} from "./lib/bootstrap-components";
import {Link} from "react-router-dom";
import axios from './lib/axios';
import axios
from './lib/axios';
import {getUrl} from "./lib/urls";
import {langCodes} from "../../shared/langs";
const topLevelMenuKeys = ['lists', 'templates', 'campaigns', 'reports'];
@withNamespaces()
@withTranslation()
class Root extends Component {
constructor(props) {
super(props);
}
const t = props.t;
const self = this;
render() {
const t = this.props.t;
const topLevelMenuKeys = ['lists', 'templates', 'campaigns', 'reports'];
const structure = {};
// The MainMenu component is defined here in order to avoid recreating menu structure on every change in the main menu
// This is because Root component depends only on the language, thus it is redrawn (and the structure is recomputed) only when the language changes
class MainMenu extends Component {
constructor(props) {
super(props);
}
async logout() {
await axios.post(getUrl('rest/logout'));
window.location = getUrl();
}
render() {
const languageOptions = [];
for (const lng of mailtrainConfig.enabledLanguages) {
const langDesc = langCodes[lng];
const label = langDesc.getLabel(t);
languageOptions.push(
<li key={lng}><ActionLink onClickAsync={() => i18n.changeLanguage(langDesc.shortCode)}>{label}</ActionLink></li>
)
}
const currentLngCode = langCodes[i18n.language].getShortLabel(t);
const path = this.props.location.pathname;
const topLevelItems = structure[""].children;
const topLevelMenu = [];
const topLevelItems = self.structure[''].children;
for (const entryKey of topLevelMenuKeys) {
const entry = topLevelItems[entryKey];
const link = entry.link || entry.externalLink;
if (link && path.startsWith(link)) {
topLevelMenu.push(<MenuLink key={entryKey} className="active" to={link}>{entry.title} <span className="sr-only">{t('root.current')}</span></MenuLink>);
topLevelMenu.push(<MenuLink key={entryKey} className="active" to={link}>{entry.title} <span className="sr-only">{t('current')}</span></MenuLink>);
} else {
topLevelMenu.push(<MenuLink key={entryKey} to={link}>{entry.title}</MenuLink>);
}
@ -69,7 +109,7 @@ class Root extends Component {
<div className="container-fluid">
<div className="navbar-header">
<button type="button" className="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span className="sr-only">{t('root.toggleNavigation')}</span>
<span className="sr-only">{t('toggleNavigation')}</span>
<span className="icon-bar"></span>
<span className="icon-bar"></span>
<span className="icon-bar"></span>
@ -77,30 +117,41 @@ class Root extends Component {
<Link className="navbar-brand" to="/"><Icon icon="envelope"/> Mailtrain</Link>
</div>
{mailtrainConfig.isAuthenticated &&
<div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul className="nav navbar-nav">
{topLevelMenu}
<DropdownMenuItem label={t('root.administration')}>
<MenuLink to="/users"><Icon icon='cog'/> {t('user_plural')}</MenuLink>
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('namespace_plural')}</MenuLink>
{mailtrainConfig.globalPermissions.manageSettings && <MenuLink to="/settings"><Icon icon='cog'/> {t('globalSetting_plural')}</MenuLink>}
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('sendConfiguration_plural')}</MenuLink>
{mailtrainConfig.globalPermissions.manageBlacklist && <MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('blacklist')}</MenuLink>}
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('api')}</MenuLink>
</DropdownMenuItem>
</ul>
{mailtrainConfig.isAuthenticated ?
<div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul className="nav navbar-nav">
{topLevelMenu}
<DropdownMenuItem label={t('administration')}>
<MenuLink to="/users"><Icon icon='cog'/> {t('users')}</MenuLink>
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('namespaces')}</MenuLink>
{mailtrainConfig.globalPermissions.manageSettings && <MenuLink to="/settings"><Icon icon='cog'/> {t('globalSettings')}</MenuLink>}
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('sendConfigurations')}</MenuLink>
{mailtrainConfig.globalPermissions.manageBlacklist && <MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('blacklist')}</MenuLink>}
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('api')}</MenuLink>
</DropdownMenuItem>
</ul>
<ul className="nav navbar-nav navbar-right">
<DropdownMenuItem label={mailtrainConfig.user.username} icon="user">
<MenuLink to="/account"><Icon icon='user'/> {t('root.account')}</MenuLink>
<li>
<ActionLink onClickAsync={::self.logout}><Icon icon='log-out'/> {t('logout')}</ActionLink>
</li>
</DropdownMenuItem>
</ul>
</div>
<ul className="nav navbar-nav navbar-right">
<DropdownMenuItem label={currentLngCode}>
{languageOptions}
</DropdownMenuItem>
<DropdownMenuItem label={mailtrainConfig.user.username} icon="user">
<MenuLink to="/account"><Icon icon='user'/> {t('account')}</MenuLink>
<li>
<ActionLink onClickAsync={::this.logout}><Icon icon='log-out'/> {t('logOut')}</ActionLink>
</li>
</DropdownMenuItem>
</ul>
</div>
:
<div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul className="nav navbar-nav navbar-right">
<DropdownMenuItem label={currentLngCode}>
{languageOptions}
</DropdownMenuItem>
</ul>
</div>
}
</div>
@ -109,43 +160,32 @@ class Root extends Component {
}
}
this.structure = {
'': {
title: t('Home'),
structure[''] ={
title: t('home'),
link: '/',
panelComponent: Home,
primaryMenuComponent: MainMenu,
children: {
...lists.getMenus(t),
...reports.getMenus(t),
...templates.getMenus(t),
...namespaces.getMenus(t),
...users.getMenus(t),
...blacklist.getMenus(t),
...account.getMenus(t),
...settings.getMenus(t),
...sendConfigurations.getMenus(t),
...campaigns.getMenus(t)
}
...lists.getMenus(t),
...reports.getMenus(t),
...templates.getMenus(t),
...namespaces.getMenus(t),
...users.getMenus(t),
...blacklist.getMenus(t),
...account.getMenus(t),
...settings.getMenus(t),
...sendConfigurations.getMenus(t),
...campaigns.getMenus(t)
}
};
}
async logout() {
await axios.post(getUrl('rest/logout'));
window.location = getUrl();
}
render() {
const t = this.props.t;
return (
<div>
<Section root='/' structure={this.structure}/>
<Section root='/' structure={structure}/>
<footer className="footer">
<div className="container-fluid">
<p className="text-muted">&copy; 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{t('sourceOnGithub')}</a></p>
<p className="text-muted">&copy; 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{t('sourceOnGitHub')}</a></p>
</div>
</footer>
</div>

View file

@ -1,11 +1,10 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Trans,
translate
} from 'react-i18next';
import PropTypes
from 'prop-types';
import {Trans} from 'react-i18next';
import {withTranslation} from '../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -38,12 +37,14 @@ import {
MailerType
} from "../../../shared/send-configurations";
import styles from "../lib/styles.scss";
import styles
from "../lib/styles.scss";
import mailtrainConfig from 'mailtrainConfig';
import mailtrainConfig
from 'mailtrainConfig';
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -107,19 +108,19 @@ export default class CUD extends Component {
const typeKey = state.getIn(['mailer_type', 'value']);
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
if (!typeKey) {
state.setIn(['mailer_type', 'error'], t('Mailer type must be selected'));
state.setIn(['mailer_type', 'error'], t('mailerTypeMustBeSelected'));
} else {
state.setIn(['mailer_type', 'error'], null);
}
if (state.getIn(['verpEnabled', 'value']) && !state.getIn(['verp_hostname', 'value'])) {
state.setIn(['verp_hostname', 'error'], t('VERP hostname must not be empty'));
state.setIn(['verp_hostname', 'error'], t('verpHostnameMustNotBeEmpty'));
} else {
state.setIn(['verp_hostname', 'error'], null);
}
@ -144,7 +145,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
this.mailerTypes[data.mailer_type].beforeSave(data);
@ -154,10 +155,10 @@ export default class CUD extends Component {
});
if (submitSuccessful) {
this.navigateToWithFlashMessage('/send-configurations', 'success', t('Send configuration saved'));
this.navigateToWithFlashMessage('/send-configurations', 'success', t('sendConfigurationSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -183,60 +184,60 @@ export default class CUD extends Component {
deleteUrl={`rest/send-configurations/${this.props.entity.id}`}
backUrl={`/send-configurations/${this.props.entity.id}/edit`}
successUrl="/send-configurations"
deletingMsg={t('Deleting send configuration ...')}
deletedMsg={t('Send configuration deleted')}/>
deletingMsg={t('deletingSendConfiguration')}
deletedMsg={t('sendConfigurationDeleted')}/>
}
<Title>{isEdit ? t('Edit Send Configuration') : t('Create Send Configuration')}</Title>
<Title>{isEdit ? t('editSendConfiguration') : t('createSendConfiguration')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<InputField id="name" label={t('name')}/>
{isEdit &&
<StaticField id="cid" className={styles.formDisabled} label={t('ID')}>
<StaticField id="cid" className={styles.formDisabled} label={t('id')}>
{this.getFormValue('cid')}
</StaticField>
}
<TextArea id="description" label={t('Description')}/>
<TextArea id="description" label={t('description')}/>
<NamespaceSelect/>
<Fieldset label={t('Email Header')}>
<InputField id="from_email" label={t('Default "from" email')}/>
<CheckBox id="from_email_overridable" text={t('Overridable')}/>
<InputField id="from_name" label={t('Default "from" name')}/>
<CheckBox id="from_name_overridable" text={t('Overridable')}/>
<InputField id="reply_to" label={t('Default "reply-to" email')}/>
<CheckBox id="reply_to_overridable" text={t('Overridable')}/>
<InputField id="subject" label={t('Subject')}/>
<CheckBox id="subject_overridable" text={t('Overridable')}/>
<InputField id="x_mailer" label={t('X-Mailer')}/>
<Fieldset label={t('emailHeader')}>
<InputField id="from_email" label={t('defaultFromEmail')}/>
<CheckBox id="from_email_overridable" text={t('overridable')}/>
<InputField id="from_name" label={t('defaultFromName')}/>
<CheckBox id="from_name_overridable" text={t('overridable')}/>
<InputField id="reply_to" label={t('defaultReplytoEmail')}/>
<CheckBox id="reply_to_overridable" text={t('overridable')}/>
<InputField id="subject" label={t('subject')}/>
<CheckBox id="subject_overridable" text={t('overridable')}/>
<InputField id="x_mailer" label={t('xMailer')}/>
</Fieldset>
{mailerForm}
{/* TODO - add "Check mail config" button */}
<Fieldset label={t('VERP Bounce Handling')}>
<Trans><p>Mailtrain is able to use VERP based routing to detect bounces. In this case the message is sent to the recipient using a custom VERP address as the return path of the message. If the message is not accepted a bounce email is sent to this special VERP address and thus a bounce is detected.</p></Trans>
<Trans><p>To get VERP working you need to set up a DNS MX record that points to your Mailtrain hostname. You must also ensure that Mailtrain VERP interface is available from port 25 of your server (port 25 usually requires root user privileges). This way if anyone tries to send email to someuser@verp-hostname then the email should end up to this server.</p></Trans>
<Trans><p className="text-warning">VERP usually only works if you are using your own SMTP server. Regular relay services (SES, SparkPost, Gmail etc.) tend to remove the VERP address from the message.</p></Trans>
<Fieldset label={t('verpBounceHandling')}>
<Trans i18nKey="mailtrainIsAbleToUseVerpBasedRoutingTo"><p>Mailtrain is able to use VERP based routing to detect bounces. In this case the message is sent to the recipient using a custom VERP address as the return path of the message. If the message is not accepted a bounce email is sent to this special VERP address and thus a bounce is detected.</p></Trans>
<Trans i18nKey="toGetVerpWorkingYouNeedToSetUpADnsMx"><p>To get VERP working you need to set up a DNS MX record that points to your Mailtrain hostname. You must also ensure that Mailtrain VERP interface is available from port 25 of your server (port 25 usually requires root user privileges). This way if anyone tries to send email to someuser@verp-hostname then the email should end up to this server.</p></Trans>
<Trans i18nKey="verpUsuallyOnlyWorksIfYouAreUsingYourOwn"><p className="text-warning">VERP usually only works if you are using your own SMTP server. Regular relay services (SES, SparkPost, Gmail etc.) tend to remove the VERP address from the message.</p></Trans>
{mailtrainConfig.verpEnabled ?
<div>
<CheckBox id="verpEnabled" text={t('verpEnabled')}/>
{verpEnabled && <InputField id="verp_hostname" label={t('Server hostname')} placeholder={t('The VERP server hostname, eg. bounces.example.com')} help={t('VERP bounce handling server hostname. This hostname is used in the SMTP envelope FROM address and the MX DNS records should point to this server')}/>}
{verpEnabled && <InputField id="verp_hostname" label={t('serverHostname')} placeholder={t('theVerpServerHostnameEgBouncesexamplecom')} help={t('verpBounceHandlingServerHostnameThis')}/>}
</div>
:
<Trans><p>VERP bounce handling server is not enabled. Modify your server configuration file and restart server to enable it.</p></Trans>
<Trans i18nKey="verpBounceHandlingServerIsNotEnabled"><p>VERP bounce handling server is not enabled. Modify your server configuration file and restart server to enable it.</p></Trans>
}
</Fieldset>
<hr/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{canDelete &&
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/send-configurations/${this.props.entity.id}/delete`}/>
<NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/send-configurations/${this.props.entity.id}/delete`}/>
}
</ButtonRow>
</Form>

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {Icon} from '../lib/bootstrap-components';
import {
NavButton,
@ -26,7 +26,7 @@ import {
} from "../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -63,12 +63,12 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') },
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') },
{
actions: data => {
const actions = [];
@ -76,14 +76,14 @@ export default class List extends Component {
if (perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/send-configurations/${data[0]}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/send-configurations/${data[0]}/share`
});
}
@ -97,14 +97,14 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/send-configurations`, t('Deleting send configuration ...'), t('Send configuration deleted'))}
{tableDeleteDialogRender(this, `rest/send-configurations`, t('deletingSendConfiguration'), t('sendConfigurationDeleted'))}
{this.state.createPermitted &&
<Toolbar>
<NavButton linkTo="/send-configurations/create" className="btn-primary" icon="plus" label={t('Create Send Configuration')}/>
<NavButton linkTo="/send-configurations/create" className="btn-primary" icon="plus" label={t('createSendConfiguration')}/>
</Toolbar>
}
<Title>{t('Send Configurations')}</Title>
<Title>{t('sendConfigurations-1')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/send-configurations-table" columns={columns} />
</div>

View file

@ -43,9 +43,9 @@ export function getMailerTypes(t) {
function validateNumber(state, field, label, emptyAllowed = false) {
const value = state.getIn([field, 'value']);
if (typeof value === 'string' && value.trim() === '' && !emptyAllowed) { // After load, the numerical values can be still numbers
state.setIn([field, 'error'], t('{{label}} must not be empty', {label}));
state.setIn([field, 'error'], t('labelMustNotBeEmpty', {label}));
} else if (isNaN(value)) {
state.setIn([field, 'error'], t('{{label}} must be a number', {label}));
state.setIn([field, 'error'], t('labelMustBeANumber', {label}));
} else {
state.setIn([field, 'error'], null);
}
@ -122,45 +122,45 @@ export function getMailerTypes(t) {
}
const typeOptions = [
{ key: MailerType.GENERIC_SMTP, label: t('Generic SMTP')},
{ key: MailerType.ZONE_MTA, label: t('Zone MTA')},
{ key: MailerType.AWS_SES, label: t('Amazon SES')}
{ key: MailerType.GENERIC_SMTP, label: t('genericSmtp')},
{ key: MailerType.ZONE_MTA, label: t('zoneMta')},
{ key: MailerType.AWS_SES, label: t('amazonSes')}
];
const smtpEncryptionOptions = [
{ key: 'NONE', label: t('Do not use encryption')},
{ key: 'TLS', label: t('Use TLS usually selected for port 465')},
{ key: 'STARTTLS', label: t('Use STARTTLS usually selected for port 587 and 25')}
{ key: 'NONE', label: t('doNotUseEncryption')},
{ key: 'TLS', label: t('useTls UsuallySelectedForPort465')},
{ key: 'STARTTLS', label: t('useStarttls UsuallySelectedForPort587')}
];
const sesRegionOptions = [
{ key: 'us-east-1', label: t('US-EAST-1')},
{ key: 'us-west-2', label: t('US-WEST-2')},
{ key: 'eu-west-1', label: t('EU-WEST-1')}
{ key: 'us-east-1', label: t('useast1')},
{ key: 'us-west-2', label: t('uswest2')},
{ key: 'eu-west-1', label: t('euwest1')}
];
mailerTypes[MailerType.GENERIC_SMTP] = {
getForm: owner =>
<div>
<Fieldset label={t('Mailer Settings')}>
<Dropdown id="mailer_type" label={t('Mailer type')} options={typeOptions}/>
<InputField id="smtpHostname" label={t('Hostname')} placeholder={t('Hostname, eg. smtp.example.com')}/>
<InputField id="smtpPort" label={t('Port')} placeholder={t('Port, eg. 465. Autodetected if left blank')}/>
<Dropdown id="smtpEncryption" label={t('Encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('Enable SMTP authentication')}/>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
{ owner.getFormValue('smtpUseAuth') &&
<div>
<InputField id="smtpUser" label={t('Username')} placeholder={t('Username, eg. myaccount@example.com')}/>
<InputField id="smtpPassword" label={t('Password')} placeholder={t('Username, eg. myaccount@example.com')}/>
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
<InputField id="smtpPassword" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
</div>
}
</Fieldset>
<Fieldset label={t('Advanced Mailer Settings')}>
<CheckBox id="logTransactions" text={t('Log SMTP transactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('Allow self-signed certificates')}/>
<InputField id="maxConnections" label={t('Max connections')} placeholder={t('The count of max connections, eg. 10')} help={t('The count of maximum simultaneous connections to make against the SMTP server (defaults to 5). This limit is per sending process.')}/>
<InputField id="smtpMaxMessages" label={t('Max messages')} placeholder={t('The count of max messages, eg. 100')} help={t('The number of messages to send through a single connection before the connection is closed and reopened (defaults to 100)')}/>
<InputField id="throttling" label={t('Throttling')} placeholder={t('Messages per hour eg. 1000')} help={t('Maximum number of messages to send in an hour. Leave empty or zero for no throttling. If your provider uses a different speed limit (messages/minute or messages/second) then convert this limit into messages/hour (1m/s => 3600m/h). This limit is per sending process.')}/>
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>,
initData: () => ({
@ -184,33 +184,33 @@ export function getMailerTypes(t) {
mailerTypes[MailerType.ZONE_MTA] = {
getForm: owner =>
<div>
<Fieldset label={t('Mailer Settings')}>
<Dropdown id="mailer_type" label={t('Mailer type')} options={typeOptions}/>
<InputField id="smtpHostname" label={t('Hostname')} placeholder={t('Hostname, eg. smtp.example.com')}/>
<InputField id="smtpPort" label={t('Port')} placeholder={t('Port, eg. 465. Autodetected if left blank')}/>
<Dropdown id="smtpEncryption" label={t('Encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('Enable SMTP authentication')}/>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
{ owner.getFormValue('smtpUseAuth') &&
<div>
<InputField id="smtpUser" label={t('Username')} placeholder={t('Username, eg. myaccount@example.com')}/>
<InputField id="smtpPassword" label={t('Password')} placeholder={t('Username, eg. myaccount@example.com')}/>
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
<InputField id="smtpPassword" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
</div>
}
</Fieldset>
<Fieldset label={t('DKIM Signing')}>
<Trans><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages. Other services usually provide their own means to DKIM sign your messages.</p></Trans>
<Trans><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
<InputField id="dkimApiKey" label={t('ZoneMTA DKIM API key')} help={t('Secret value known to ZoneMTA for requesting DKIM key information. If this value was generated by the Mailtrain installation script then you can keep it as it is.')}/>
<InputField id="dkimDomain" label={t('DKIM domain')} help={t('Leave blank to use the sender email address domain.')}/>
<InputField id="dkimSelector" label={t('DKIM key selector')} help={t('Signing is disabled without a valid selector value.')}/>
<TextArea id="dkimPrivateKey" label={t('DKIM private key')} placeholder={t('Begins with "-----BEGIN RSA PRIVATE KEY-----"')} help={t('Signing is disabled without a valid private key.')}/>
<Fieldset label={t('dkimSigning')}>
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages. Other services usually provide their own means to DKIM sign your messages.</p></Trans>
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
<InputField id="dkimApiKey" label={t('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
<TextArea id="dkimPrivateKey" label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
</Fieldset>
<Fieldset label={t('Advanced Mailer Settings')}>
<CheckBox id="logTransactions" text={t('Log SMTP transactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('Allow self-signed certificates')}/>
<InputField id="maxConnections" label={t('Max connections')} placeholder={t('The count of max connections, eg. 10')} help={t('The count of maximum simultaneous connections to make against the SMTP server (defaults to 5). This limit is per sending process.')}/>
<InputField id="smtpMaxMessages" label={t('Max messages')} placeholder={t('The count of max messages, eg. 100')} help={t('The number of messages to send through a single connection before the connection is closed and reopened (defaults to 100)')}/>
<InputField id="throttling" label={t('Throttling')} placeholder={t('Messages per hour eg. 1000')} help={t('Maximum number of messages to send in an hour. Leave empty or zero for no throttling. If your provider uses a different speed limit (messages/minute or messages/second) then convert this limit into messages/hour (1m/s => 3600m/h). This limit is per sending process.')}/>
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>,
initData: () => ({
@ -246,16 +246,16 @@ export function getMailerTypes(t) {
mailerTypes[MailerType.AWS_SES] = {
getForm: owner =>
<div>
<Fieldset label={t('Mailer Settings')}>
<Dropdown id="mailer_type" label={t('Mailer type')} options={typeOptions}/>
<InputField id="sesKey" label={t('Access key')} placeholder={t('AWS access key ID')}/>
<InputField id="sesSecret" label={t('Port')} placeholder={t('AWS secret access key')}/>
<Dropdown id="sesRegion" label={t('Region')} options={sesRegionOptions}/>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/>
<InputField id="sesSecret" label={t('port')} placeholder={t('awsSecretAccessKey')}/>
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
</Fieldset>
<Fieldset label={t('Advanced Mailer Settings')}>
<CheckBox id="logTransactions" text={t('Log SMTP transactions')}/>
<InputField id="maxConnections" label={t('Max connections')} placeholder={t('The count of max connections, eg. 10')} help={t('The count of maximum simultaneous connections to make against the SMTP server (defaults to 5). This limit is per sending process.')}/>
<InputField id="throttling" label={t('Throttling')} placeholder={t('Messages per hour eg. 1000')} help={t('Maximum number of messages to send in an hour. Leave empty or zero for no throttling. If your provider uses a different speed limit (messages/minute or messages/second) then convert this limit into messages/hour (1m/s => 3600m/h). This limit is per sending process.')}/>
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>,
initData: () => ({

View file

@ -10,33 +10,33 @@ import Share from '../shares/Share';
function getMenus(t) {
return {
'send-configurations': {
title: t('Send Configurations'),
title: t('sendConfigurations-1'),
link: '/send-configurations',
panelComponent: List,
children: {
':sendConfigurationId([0-9]+)': {
title: resolved => t('Template "{{name}}"', {name: resolved.sendConfiguration.name}),
title: resolved => t('templateName', {name: resolved.sendConfiguration.name}),
resolve: {
sendConfiguration: params => `rest/send-configurations-private/${params.sendConfigurationId}`
},
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
visible: resolved => resolved.sendConfiguration.permissions.includes('edit'),
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.sendConfiguration} />
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/send-configurations/${params.sendConfigurationId}/share`,
visible: resolved => resolved.sendConfiguration.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.sendConfiguration} entityTypeId="sendConfiguration" />
panelRender: props => <Share title={t('share')} entity={props.resolved.sendConfiguration} entityTypeId="sendConfiguration" />
}
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <CUD action="create" />
}
}

View file

@ -3,9 +3,9 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Trans,
translate
Trans
} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {
requiresAuthenticatedUser,
Title,
@ -23,7 +23,7 @@ import {
} from '../lib/form';
import {withErrorHandling} from '../lib/error-handling';
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -53,17 +53,17 @@ export default class Update extends Component {
const t = this.props.t;
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, 'rest/settings');
if (submitSuccessful) {
await this.getFormValuesFromURL('rest/settings');
this.enableForm();
this.setFormStatusMessage('success', t('Global settings saved'));
this.setFormStatusMessage('success', t('globalSettingsSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -72,26 +72,26 @@ export default class Update extends Component {
return (
<div>
<Title>{t('Global Settings')}</Title>
<Title>{t('globalSettings')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="adminEmail" label={t('Admin email')} help={t('This email is used as the main contact and as a default email address if no email address is specified in list settings.')}/>
<InputField id="defaultHomepage" label={t('Default homepage URL')} help={t('This URL will be used in list subscription forms if no homepage is specified in list settings.')}/>
<InputField id="adminEmail" label={t('adminEmail')} help={t('thisEmailIsUsedAsTheMainContactAndAsA')}/>
<InputField id="defaultHomepage" label={t('defaultHomepageUrl')} help={t('thisUrlWillBeUsedInListSubscriptionForms')}/>
<InputField id="uaCode" label={t('Tracking ID')} placeholder={t('UA-XXXXX-XX')} help={t('Enter Google Analytics tracking code')}/>
<InputField id="uaCode" label={t('trackingId')} placeholder={t('uaxxxxxxx')} help={t('enterGoogleAnalyticsTrackingCode')}/>
<TextArea id="shoutout" label={t('Frontpage shout out')} help={t('HTML code shown in the front page header section')}/>
<TextArea id="shoutout" label={t('frontpageShoutOut')} help={t('htmlCodeShownInTheFrontPageHeaderSection')}/>
<Fieldset label={t('GPG Signing')}>
<Trans><p>Only messages that are encrypted can be signed. Subsribers who have not set up a GPG public key in their profile receive normal email messages. Users with GPG key set receive encrypted messages and if you have signing key also set, the messages are signed with this key.</p></Trans>
<Trans><p className="text-warning">Do not use sensitive keys here. The private key and passphrase are not encrypted in the database.</p></Trans>
<InputField id="pgpPassphrase" label={t('Private key passphrase')} placeholder={t('Passphrase for the key if set')} help={t('Only fill this if your private key is encrypted with a passphrase')}/>
<TextArea id="pgpPrivateKey" label={t('GPG private key')} placeholder={t('Begins with \'-----BEGIN PGP PRIVATE KEY BLOCK-----\'')} help={t('This value is optional. If you do not provide a private key GPG encrypted messages are sent without signing.')}/>
<Fieldset label={t('gpgSigning')}>
<Trans i18nKey="onlyMessagesThatAreEncryptedCanBeSigned"><p>Only messages that are encrypted can be signed. Subsribers who have not set up a GPG public key in their profile receive normal email messages. Users with GPG key set receive encrypted messages and if you have signing key also set, the messages are signed with this key.</p></Trans>
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKey"><p className="text-warning">Do not use sensitive keys here. The private key and passphrase are not encrypted in the database.</p></Trans>
<InputField id="pgpPassphrase" label={t('privateKeyPassphrase')} placeholder={t('passphraseForTheKeyIfSet')} help={t('onlyFillThisIfYourPrivateKeyIsEncrypted')}/>
<TextArea id="pgpPrivateKey" label={t('gpgPrivateKey')} placeholder={t('beginsWithBeginPgpPrivateKeyBlock')} help={t('thisValueIsOptionalIfYouDoNotProvideA')}/>
</Fieldset>
<hr/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
</ButtonRow>
</Form>
</div>

View file

@ -6,7 +6,7 @@ import Update from "./Update";
function getMenus(t) {
return {
'settings': {
title: t('Global Settings'),
title: t('globalSettings'),
link: '/settings',
resolve: {
configItems: params => `rest/settings`

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import {
@ -13,7 +13,7 @@ import axios from '../lib/axios';
import mailtrainConfig from 'mailtrainConfig';
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -61,13 +61,13 @@ export default class Share extends Component {
const t = this.props.t;
if (!state.getIn(['userId', 'value'])) {
state.setIn(['userId', 'error'], t('User must not be empty'));
state.setIn(['userId', 'error'], t('userMustNotBeEmpty'));
} else {
state.setIn(['userId', 'error'], null);
}
if (!state.getIn(['role', 'value'])) {
state.setIn(['role', 'error'], t('Role must be selected'));
state.setIn(['role', 'error'], t('roleMustBeSelected'));
} else {
state.setIn(['role', 'error'], null);
}
@ -77,7 +77,7 @@ export default class Share extends Component {
const t = this.props.t;
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, 'rest/shares');
@ -92,7 +92,7 @@ export default class Share extends Component {
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and try again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd-1'));
}
}
@ -100,11 +100,11 @@ export default class Share extends Component {
const t = this.props.t;
const sharesColumns = [];
sharesColumns.push({ data: 0, title: t('Username') });
sharesColumns.push({ data: 0, title: t('username') });
if (mailtrainConfig.isAuthMethodLocal) {
sharesColumns.push({ data: 1, title: t('Name') });
sharesColumns.push({ data: 1, title: t('name') });
}
sharesColumns.push({ data: 2, title: t('Role') });
sharesColumns.push({ data: 2, title: t('role') });
sharesColumns.push({
actions: data => {
@ -144,18 +144,18 @@ export default class Share extends Component {
<div>
<Title>{this.props.title}</Title>
<h3 className="legend">{t('Add User')}</h3>
<h3 className="legend">{t('addUser')}</h3>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('User')} withHeader dropdown dataUrl={`rest/shares-unassigned-users-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={`rest/shares-roles-table/${this.props.entityTypeId}`} columns={rolesColumns} selectionLabelIndex={1}/>
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('user')} withHeader dropdown dataUrl={`rest/shares-unassigned-users-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
<TableSelect id="role" label={t('role')} withHeader dropdown dataUrl={`rest/shares-roles-table/${this.props.entityTypeId}`} columns={rolesColumns} selectionLabelIndex={1}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Share')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('share')}/>
</ButtonRow>
</Form>
<hr/>
<h3 className="legend">{t('Existing Users')}</h3>
<h3 className="legend">{t('existingUsers')}</h3>
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`rest/shares-table-by-entity/${this.props.entityTypeId}/${this.props.entity.id}`} columns={sharesColumns} />
</div>

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import { Table } from '../lib/table';
@ -11,7 +11,7 @@ import mailtrainConfig from 'mailtrainConfig';
import {Icon} from "../lib/bootstrap-components";
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -48,8 +48,8 @@ export default class UserShares extends Component {
const renderSharesTable = (entityTypeId, title) => {
const columns = [
{ data: 0, title: t('Name') },
{ data: 1, title: t('Role') },
{ data: 0, title: t('name') },
{ data: 1, title: t('role') },
{
actions: data => {
const actions = [];
@ -58,7 +58,7 @@ export default class UserShares extends Component {
if (!autoGenerated && perms.includes('share')) {
actions.push({
label: <Icon icon="remove" title={t('Remove')}/>,
label: <Icon icon="remove" title={t('remove')}/>,
action: () => this.deleteShare(entityTypeId, data[2])
});
}
@ -78,13 +78,13 @@ export default class UserShares extends Component {
return (
<div>
<Title>{t('Shares for user "{{username}}"', {username: this.props.user.username})}</Title>
<Title>{t('sharesForUserUsername', {username: this.props.user.username})}</Title>
{renderSharesTable('namespace', t('Namespaces'))}
{renderSharesTable('list', t('Lists'))}
{renderSharesTable('customForm', t('Custom Forms'))}
{renderSharesTable('report', t('Reports'))}
{renderSharesTable('reportTemplate', t('Report Templates'))}
{renderSharesTable('namespace', t('namespaces'))}
{renderSharesTable('list', t('lists'))}
{renderSharesTable('customForm', t('customForms-1'))}
{renderSharesTable('report', t('reports'))}
{renderSharesTable('reportTemplate', t('reportTemplates'))}
</div>
);
}

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -38,7 +38,7 @@ import {getUrl} from "../lib/urls";
import {TestSendModalDialog} from "./TestSendModalDialog";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -103,12 +103,12 @@ export default class CUD extends Component {
}
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
}
const typeKey = state.getIn(['type', 'value']);
if (!typeKey) {
state.setIn(['type', 'error'], t('Type must be selected'));
state.setIn(['type', 'error'], t('typeMustBeSelected'));
}
validateNamespace(t, state);
@ -137,7 +137,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
Object.assign(data, exportedData);
@ -146,13 +146,13 @@ export default class CUD extends Component {
if (submitResponse) {
if (this.props.entity) {
this.navigateToWithFlashMessage('/templates', 'success', t('Template saved'));
this.navigateToWithFlashMessage('/templates', 'success', t('templateSaved'));
} else {
this.navigateToWithFlashMessage(`/templates/${submitResponse}/edit`, 'success', t('Template saved'));
this.navigateToWithFlashMessage(`/templates/${submitResponse}/edit`, 'success', t('templateSaved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -244,23 +244,23 @@ export default class CUD extends Component {
deleteUrl={`rest/templates/${this.props.entity.id}`}
backUrl={`/templates/${this.props.entity.id}/edit`}
successUrl="/templates"
deletingMsg={t('Deleting template ...')}
deletedMsg={t('Template deleted')}/>
deletingMsg={t('deletingTemplate')}
deletedMsg={t('templateDeleted')}/>
}
<Title>{isEdit ? t('Edit Template') : t('Create Template')}</Title>
<Title>{isEdit ? t('editTemplate') : t('createTemplate')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/>
{isEdit
?
<StaticField id="type" className={styles.formDisabled} label={t('Type')}>
<StaticField id="type" className={styles.formDisabled} label={t('type')}>
{typeKey && this.templateTypes[typeKey].typeName}
</StaticField>
:
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
<Dropdown id="type" label={t('type')} options={typeOptions}/>
}
{typeForm}
@ -270,9 +270,9 @@ export default class CUD extends Component {
{editForm}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={isEdit ? t('Save') : t('Save and edit template')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/templates/${this.props.entity.id}/delete`}/> }
{isEdit && <Button className="btn-danger" icon="send" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/> }
<Button type="submit" className="btn-primary" icon="ok" label={isEdit ? t('save') : t('saveAndEditTemplate')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/templates/${this.props.entity.id}/delete`}/> }
{isEdit && <Button className="btn-danger" icon="send" label={t('testSend')} onClickAsync={async () => this.setState({showTestSendModal: true})}/> }
</ButtonRow>
</Form>
</div>

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {Icon} from '../lib/bootstrap-components';
import {
NavButton,
@ -24,7 +24,7 @@ import {
tableDeleteDialogRender
} from "../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -70,11 +70,11 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') },
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('namespace') },
{
actions: data => {
const actions = [];
@ -82,21 +82,21 @@ export default class List extends Component {
if (perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/templates/${data[0]}/edit`
});
}
if (perms.includes('viewFiles')) {
actions.push({
label: <Icon icon="hdd" title={t('Files')}/>,
label: <Icon icon="hdd" title={t('files')}/>,
link: `/templates/${data[0]}/files`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/templates/${data[0]}/share`
});
}
@ -110,17 +110,17 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/templates`, t('Deleting template ...'), t('Template deleted'))}
{tableDeleteDialogRender(this, `rest/templates`, t('deletingTemplate'), t('templateDeleted'))}
<Toolbar>
{this.state.createPermitted &&
<NavButton linkTo="/templates/create" className="btn-primary" icon="plus" label={t('Create Template')}/>
<NavButton linkTo="/templates/create" className="btn-primary" icon="plus" label={t('createTemplate')}/>
}
{this.state.mosaicoTemplatesPermitted &&
<NavButton linkTo="/templates/mosaico" className="btn-primary" label={t('Mosaico Templates')}/>
<NavButton linkTo="/templates/mosaico" className="btn-primary" label={t('mosaicoTemplates')}/>
}
</Toolbar>
<Title>{t('Templates')}</Title>
<Title>{t('templates')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/templates-table" columns={columns} />
</div>

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import PropTypes
from 'prop-types';
import {ModalDialog} from "../lib/bootstrap-components";
@ -22,7 +22,7 @@ import axios from '../lib/axios';
import {getUrl} from "../lib/urls";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -64,7 +64,7 @@ export class TestSendModalDialog extends Component {
try {
this.hideFormValidation();
this.disableForm();
this.setFormStatusMessage('info', t('Sending test email'));
this.setFormStatusMessage('info', t('sendingTestEmail'));
const data = await this.props.getDataAsync();
data.listCid = this.getFormValue('list');
@ -90,19 +90,19 @@ export class TestSendModalDialog extends Component {
const t = this.props.t;
if (!state.getIn(['sendConfiguration', 'value'])) {
state.setIn(['sendConfiguration', 'error'], t('Send configuration has to be selected.'))
state.setIn(['sendConfiguration', 'error'], t('sendConfigurationHasToBeSelected'))
} else {
state.setIn(['sendConfiguration', 'error'], null);
}
if (!state.getIn(['list', 'value'])) {
state.setIn(['list', 'error'], t('List has to be selected.'))
state.setIn(['list', 'error'], t('listHasToBeSelected'))
} else {
state.setIn(['list', 'error'], null);
}
if (!state.getIn(['testUser', 'value'])) {
state.setIn(['testUser', 'error'], t('Subscription has to be selected.'))
state.setIn(['testUser', 'error'], t('subscriptionHasToBeSelected'))
} else {
state.setIn(['testUser', 'error'], null);
}
@ -114,37 +114,37 @@ export class TestSendModalDialog extends Component {
const listId = this.getFormValue('list');
const testUsersColumns = [
{ data: 1, title: t('Subscription ID'), render: data => <code>{data}</code> },
{ data: 2, title: t('Email') }
{ data: 1, title: t('subscriptionId'), render: data => <code>{data}</code> },
{ data: 2, title: t('email') }
];
const listsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Subscribers') },
{ data: 4, title: t('Description') },
{ data: 5, title: t('Namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('subscribers') },
{ data: 4, title: t('description') },
{ data: 5, title: t('namespace') }
];
const sendConfigurationsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') }
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') }
];
return (
<ModalDialog hidden={!this.props.visible} title={t('Send Test Email')} onCloseAsync={() => this.hideModal()} buttons={[
{ label: t('Send'), className: 'btn-danger', onClickAsync: ::this.performAction },
{ label: t('Cancel'), className: 'btn-primary', onClickAsync: ::this.hideModal }
<ModalDialog hidden={!this.props.visible} title={t('sendTestEmail')} onCloseAsync={() => this.hideModal()} buttons={[
{ label: t('send'), className: 'btn-danger', onClickAsync: ::this.performAction },
{ label: t('cancel'), className: 'btn-primary', onClickAsync: ::this.hideModal }
]}>
<Form stateOwner={this} format="wide">
<TableSelect id="sendConfiguration" format="wide" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-with-send-permission-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
<TableSelect id="list" format="wide" label={t('List')} withHeader dropdown dataUrl={`rest/lists-table`} columns={listsColumns} selectionKeyIndex={2} selectionLabelIndex={1} />
<TableSelect id="sendConfiguration" format="wide" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-with-send-permission-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
<TableSelect id="list" format="wide" label={t('list')} withHeader dropdown dataUrl={`rest/lists-table`} columns={listsColumns} selectionKeyIndex={2} selectionLabelIndex={1} />
{ listId &&
<TableSelect id="testUser" format="wide" label={t('Subscription')} withHeader dropdown dataUrl={`rest/subscriptions-test-user-table/${listId}`} columns={testUsersColumns} selectionKeyIndex={1} selectionLabelIndex={2} />
<TableSelect id="testUser" format="wide" label={t('subscription')} withHeader dropdown dataUrl={`rest/subscriptions-test-user-table/${listId}`} columns={testUsersColumns} selectionKeyIndex={1} selectionLabelIndex={2} />
}
</Form>
</ModalDialog>

View file

@ -1,10 +1,10 @@
'use strict';
import React from "react";
import React
from "react";
import {
ACEEditor,
AlignedRow,
CheckBox,
CKEditor,
Dropdown,
StaticField,
@ -13,10 +13,10 @@ import {
import 'brace/mode/text';
import 'brace/mode/html';
import { MosaicoHost } from "../lib/sandboxed-mosaico";
import { CKEditorHost } from "../lib/sandboxed-ckeditor";
import { GrapesJSHost } from "../lib/sandboxed-grapesjs";
import { CodeEditorHost } from "../lib/sandboxed-codeeditor";
import {MosaicoHost} from "../lib/sandboxed-mosaico";
import {CKEditorHost} from "../lib/sandboxed-ckeditor";
import {GrapesJSHost} from "../lib/sandboxed-grapesjs";
import {CodeEditorHost} from "../lib/sandboxed-codeeditor";
import {
getGrapesJSSourceTypeOptions,
@ -24,8 +24,8 @@ import {
} from "../lib/sandboxed-grapesjs-shared";
import {
getCodeEditorSourceTypeOptions,
CodeEditorSourceType
CodeEditorSourceType,
getCodeEditorSourceTypeOptions
} from "../lib/sandboxed-codeeditor-shared";
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
@ -34,14 +34,16 @@ import {
getSandboxUrl,
getTrustedUrl
} from "../lib/urls";
import mailtrainConfig from 'mailtrainConfig';
import mailtrainConfig
from 'mailtrainConfig';
import {
ActionLink,
Button
} from "../lib/bootstrap-components";
import {Trans} from "react-i18next";
import styles from "../lib/styles.scss";
import styles
from "../lib/styles.scss";
import {
base,
unbase
@ -78,18 +80,18 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
const mosaicoTemplateTypes = getMosaicoTemplateTypes(t);
const mosaicoTemplatesColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => mosaicoTemplateTypes[data].typeName },
{ data: 5, title: t('Namespace') },
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => mosaicoTemplateTypes[data].typeName },
{ data: 5, title: t('namespace') },
];
templateTypes.mosaico = {
typeName: t('Mosaico'),
typeName: t('mosaico'),
getTypeForm: (owner, isEdit) =>
<TableSelect id={prefix + 'mosaicoTemplate'} label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
<TableSelect id={prefix + 'mosaicoTemplate'} label={t('mosaicoTemplate')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<AlignedRow label={t('templateContentHtml')}>
<MosaicoHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
@ -97,7 +99,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
templateId={owner.getFormValue(prefix + 'mosaicoTemplate')}
entityTypeId={entityTypeId}
title={t('Mosaico Template Designer')}
title={t('mosaicoTemplateDesigner')}
onTestSend={::owner.showTestSendModal}
onFullscreenAsync={::owner.setElementInFullscreen}
/>
@ -137,7 +139,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
validate: state => {
const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']);
if (!mosaicoTemplate) {
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('mosaicoTemplateMustBeSelected'));
}
}
};
@ -146,16 +148,16 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates.map(({key, label}) => ([key, label])));
templateTypes.mosaicoWithFsTemplate = {
typeName: t('Mosaico with predefined templates'),
typeName: t('mosaicoWithPredefinedTemplates'),
getTypeForm: (owner, isEdit) => {
if (isEdit) {
return <StaticField id={prefix + 'mosaicoFsTemplate'} className={styles.formDisabled} label={t('Mosaico Template')}>{mosaicoFsTemplatesLabels.get(owner.getFormValue(prefix + 'mosaicoFsTemplate'))}</StaticField>;
return <StaticField id={prefix + 'mosaicoFsTemplate'} className={styles.formDisabled} label={t('mosaicoTemplate-1')}>{mosaicoFsTemplatesLabels.get(owner.getFormValue(prefix + 'mosaicoFsTemplate'))}</StaticField>;
} else {
return <Dropdown id={prefix + 'mosaicoFsTemplate'} label={t('Mosaico Template')} options={mosaicoFsTemplatesOptions}/>;
return <Dropdown id={prefix + 'mosaicoFsTemplate'} label={t('mosaicoTemplate-1')} options={mosaicoFsTemplatesOptions}/>;
}
},
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<AlignedRow label={t('templateContentHtml')}>
<MosaicoHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
@ -163,7 +165,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
templatePath={getSandboxUrl(`static/mosaico/templates/${owner.getFormValue(prefix + 'mosaicoFsTemplate')}/index.html`)}
entityTypeId={entityTypeId}
title={t('Mosaico Template Designer')}
title={t('mosaicoTemplateDesigner')}
onTestSend={::owner.showTestSendModal}
onFullscreenAsync={::owner.setElementInFullscreen}
/>
@ -211,16 +213,16 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
}
templateTypes.grapesjs = {
typeName: t('GrapesJS'),
typeName: t('grapesJs'),
getTypeForm: (owner, isEdit) => {
if (isEdit) {
return <StaticField id={prefix + 'grapesJSSourceType'} className={styles.formDisabled} label={t('Type')}>{grapesJSSourceTypeLabels[owner.getFormValue(prefix + 'grapesJSSourceType')]}</StaticField>;
return <StaticField id={prefix + 'grapesJSSourceType'} className={styles.formDisabled} label={t('type')}>{grapesJSSourceTypeLabels[owner.getFormValue(prefix + 'grapesJSSourceType')]}</StaticField>;
} else {
return <Dropdown id={prefix + 'grapesJSSourceType'} label={t('Type')} options={grapesJSSourceTypes}/>;
return <Dropdown id={prefix + 'grapesJSSourceType'} label={t('type')} options={grapesJSSourceTypes}/>;
}
},
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<AlignedRow label={t('templateContentHtml')}>
<GrapesJSHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
@ -228,7 +230,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
initialSource={owner.getFormValue(prefix + 'grapesJSData').source}
initialStyle={owner.getFormValue(prefix + 'grapesJSData').style}
sourceType={owner.getFormValue(prefix + 'grapesJSSourceType')}
title={t('GrapesJS Template Designer')}
title={t('grapesJsTemplateDesigner')}
onTestSend={::owner.showTestSendModal}
onFullscreenAsync={::owner.setElementInFullscreen}
/>
@ -269,16 +271,16 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
};
templateTypes.ckeditor4 = {
typeName: t('CKEditor 4'),
typeName: t('ckEditor4'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<AlignedRow label={t('templateContentHtml')}>
<CKEditorHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
initialSource={owner.getFormValue(prefix + 'ckeditor4Data').source}
entityTypeId={entityTypeId}
title={t('CKEditor 4 Template Designer')}
title={t('ckEditor4TemplateDesigner')}
onTestSend={::owner.showTestSendModal}
onFullscreenAsync={::owner.setElementInFullscreen}
/>
@ -313,9 +315,9 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
};
templateTypes.ckeditor5 = {
typeName: t('CKEditor 5'),
typeName: t('ckEditor5'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <CKEditor id={prefix + 'ckeditor5Source'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
getHTMLEditor: owner => <CKEditor id={prefix + 'ckeditor5Source'} height="600px" mode="html" label={t('templateContentHtml')}/>,
exportHTMLEditorData: async owner => {
const preHtml = '<!doctype html><html><head><meta charset="utf-8"><title></title></head><body>';
const postHtml = '</body></html>';
@ -362,24 +364,24 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
}
templateTypes.codeeditor = {
typeName: t('Code Editor'),
typeName: t('codeEditor'),
getTypeForm: (owner, isEdit) => {
const sourceType = owner.getFormValue(prefix + 'codeEditorSourceType');
if (isEdit) {
return <StaticField id={prefix + 'codeEditorSourceType'} className={styles.formDisabled} label={t('Type')}>{codeEditorSourceTypeLabels[sourceType]}</StaticField>;
return <StaticField id={prefix + 'codeEditorSourceType'} className={styles.formDisabled} label={t('type')}>{codeEditorSourceTypeLabels[sourceType]}</StaticField>;
} else {
return <Dropdown id={prefix + 'codeEditorSourceType'} label={t('Type')} options={codeEditorSourceTypes}/>;
return <Dropdown id={prefix + 'codeEditorSourceType'} label={t('type')} options={codeEditorSourceTypes}/>;
}
},
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<AlignedRow label={t('templateContentHtml')}>
<CodeEditorHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
entityTypeId={entityTypeId}
initialSource={owner.getFormValue(prefix + 'codeEditorData').source}
sourceType={owner.getFormValue(prefix + 'codeEditorSourceType')}
title={t('Code Editor Template Designer')}
title={t('codeEditorTemplateDesigner')}
onTestSend={::owner.showTestSendModal}
onFullscreenAsync={::owner.setElementInFullscreen}
/>
@ -426,19 +428,19 @@ export function getEditForm(owner, typeKey, prefix = '') {
return <div>
<AlignedRow>
<Button className="btn-default" onClickAsync={::owner.toggleMergeTagReference} label={t('Merge tag reference')}/>
<Button className="btn-default" onClickAsync={::owner.toggleMergeTagReference} label={t('mergeTagReference')}/>
{owner.state.showMergeTagReference &&
<div style={{marginTop: '15px'}}>
<Trans><p>Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <code>[TAG_NAME]</code> or <code>[TAG_NAME/fallback]</code> where <code>fallback</code> is an optional text value used when <code>TAG_NAME</code> is empty.</p></Trans>
<Trans><p>You can use any of the standard merge tags below. In addition to that every custom field has its own merge tag. Check the fields of the list you are going to send to.</p></Trans>
<Trans i18nKey="mergeTagsAreTagsThatAreReplacedBefore"><p>Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <code>[TAG_NAME]</code> or <code>[TAG_NAME/fallback]</code> where <code>fallback</code> is an optional text value used when <code>TAG_NAME</code> is empty.</p></Trans>
<Trans i18nKey="youCanUseAnyOfTheStandardMergeTagsBelow"><p>You can use any of the standard merge tags below. In addition to that every custom field has its own merge tag. Check the fields of the list you are going to send to.</p></Trans>
<table className="table table-bordered table-condensed table-striped">
<thead>
<tr>
<th>
<Trans>Merge tag</Trans>
<Trans i18nKey="mergeTag-1">Merge tag</Trans>
</th>
<th>
<Trans>Description</Trans>
<Trans i18nKey="description">Description</Trans>
</th>
</tr>
</thead>
@ -448,7 +450,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[LINK_UNSUBSCRIBE]
</th>
<td>
<Trans>URL that points to the unsubscribe page</Trans>
<Trans i18nKey="urlThatPointsToTheUnsubscribePage">URL that points to the unsubscribe page</Trans>
</td>
</tr>
<tr>
@ -456,7 +458,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[LINK_PREFERENCES]
</th>
<td>
<Trans>URL that points to the preferences page of the subscriber</Trans>
<Trans i18nKey="urlThatPointsToThePreferencesPageOfThe">URL that points to the preferences page of the subscriber</Trans>
</td>
</tr>
<tr>
@ -464,7 +466,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[LINK_BROWSER]
</th>
<td>
<Trans>URL to preview the message in a browser</Trans>
<Trans i18nKey="urlToPreviewTheMessageInABrowser">URL to preview the message in a browser</Trans>
</td>
</tr>
<tr>
@ -472,7 +474,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[EMAIL]
</th>
<td>
<Trans>Email address</Trans>
<Trans i18nKey="emailAddress-1">Email address</Trans>
</td>
</tr>
<tr>
@ -480,7 +482,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[TO_NAME]
</th>
<td>
<Trans>Recipient name as it appears in email's 'To' header</Trans>
<Trans i18nKey="recipientNameAsItAppearsInEmailsToHeader">Recipient name as it appears in email's 'To' header</Trans>
</td>
</tr>
<tr>
@ -488,7 +490,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[SUBSCRIPTION_ID]
</th>
<td>
<Trans>Unique ID that identifies the recipient</Trans>
<Trans i18nKey="uniqueIdThatIdentifiesTheRecipient">Unique ID that identifies the recipient</Trans>
</td>
</tr>
<tr>
@ -496,7 +498,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[LIST_ID]
</th>
<td>
<Trans>Unique ID that identifies the list used for this campaign</Trans>
<Trans i18nKey="uniqueIdThatIdentifiesTheListUsedForThis">Unique ID that identifies the list used for this campaign</Trans>
</td>
</tr>
<tr>
@ -504,20 +506,20 @@ export function getEditForm(owner, typeKey, prefix = '') {
[CAMPAIGN_ID]
</th>
<td>
<Trans>Unique ID that identifies current campaign</Trans>
<Trans i18nKey="uniqueIdThatIdentifiesCurrentCampaign">Unique ID that identifies current campaign</Trans>
</td>
</tr>
</tbody>
</table>
<Trans><p>For RSS campaigns, the following further tags can be used.</p></Trans>
<Trans i18nKey="forRssCampaignsTheFollowingFurtherTags"><p>For RSS campaigns, the following further tags can be used.</p></Trans>
<table className="table table-bordered table-condensed table-striped">
<thead>
<tr>
<th>
<Trans>Merge tag</Trans>
<Trans i18nKey="mergeTag-1">Merge tag</Trans>
</th>
<th>
<Trans>Description</Trans>
<Trans i18nKey="description">Description</Trans>
</th>
</tr>
</thead>
@ -527,7 +529,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[RSS_ENTRY_TITLE]
</th>
<td>
<Trans>RSS entry title</Trans>
<Trans i18nKey="rssEntryTitle">RSS entry title</Trans>
</td>
</tr>
<tr>
@ -535,7 +537,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[RSS_ENTRY_DATE]
</th>
<td>
<Trans>RSS entry date</Trans>
<Trans i18nKey="rssEntryDate">RSS entry date</Trans>
</td>
</tr>
<tr>
@ -543,7 +545,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[RSS_ENTRY_LINK]
</th>
<td>
<Trans>RSS entry link</Trans>
<Trans i18nKey="rssEntryLink">RSS entry link</Trans>
</td>
</tr>
<tr>
@ -551,7 +553,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[RSS_ENTRY_CONTENT]
</th>
<td>
<Trans>Content of an RSS entry</Trans>
<Trans i18nKey="contentOfAnRssEntry">Content of an RSS entry</Trans>
</td>
</tr>
<tr>
@ -559,7 +561,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[RSS_ENTRY_SUMMARY]
</th>
<td>
<Trans>RSS entry summary</Trans>
<Trans i18nKey="rssEntrySummary">RSS entry summary</Trans>
</td>
</tr>
<tr>
@ -567,7 +569,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
[RSS_ENTRY_IMAGE_URL]
</th>
<td>
<Trans>RSS entry image URL</Trans>
<Trans i18nKey="rssEntryImageUrl">RSS entry image URL</Trans>
</td>
</tr>
</tbody>
@ -577,7 +579,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
{owner.templateTypes[typeKey].getHTMLEditor(owner)}
<ACEEditor id={prefix + 'text'} height="400px" mode="text" label={t('Template content (plain text)')} help={<Trans>To extract the text from HTML click <ActionLink onClickAsync={::owner.extractPlainText}>here</ActionLink>. Please note that your existing plaintext in the field above will be overwritten. This feature uses the <a href="http://premailer.dialect.ca/api">Premailer API</a>, a third party service. Their Terms of Service and Privacy Policy apply.</Trans>}/>
<ACEEditor id={prefix + 'text'} height="400px" mode="text" label={t('templateContentPlainText')} help={<Trans i18nKey="toExtractTheTextFromHtmlClickHerePlease">To extract the text from HTML click <ActionLink onClickAsync={::owner.extractPlainText}>here</ActionLink>. Please note that your existing plaintext in the field above will be overwritten. This feature uses the <a href="http://premailer.dialect.ca/api">Premailer API</a>, a third party service. Their Terms of Service and Privacy Policy apply.</Trans>}/>
</div>;
}

View file

@ -2,7 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {
NavButton,
requiresAuthenticatedUser,
@ -32,7 +32,7 @@ import {
getTemplateTypesOrder
} from "./helpers";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -96,13 +96,13 @@ export default class CUD extends Component {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
if (!state.getIn(['type', 'value'])) {
state.setIn(['type', 'error'], t('Type must be selected'));
state.setIn(['type', 'error'], t('typeMustBeSelected'));
} else {
state.setIn(['type', 'error'], null);
}
@ -131,7 +131,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
this.templateTypes[data.type].beforeSave(data);
@ -143,13 +143,13 @@ export default class CUD extends Component {
this.templateTypes[data.type].afterLoad(data);
});
this.enableForm();
this.setFormStatusMessage('success', t('Mosaico template saved'));
this.setFormStatusMessage('success', t('mosaicoTemplateSaved'));
} else {
this.navigateToWithFlashMessage('/templates/mosaico', 'success', t('Mosaico template saved'));
this.navigateToWithFlashMessage('/templates/mosaico', 'success', t('mosaicoTemplateSaved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
@ -173,31 +173,31 @@ export default class CUD extends Component {
deleteUrl={`rest/mosaico-templates/${this.props.entity.id}`}
backUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
successUrl="/templates/mosaico"
deletingMsg={t('Deleting Mosaico template ...')}
deletedMsg={t('Mosaico template deleted')}/>
deletingMsg={t('deletingMosaicoTemplate')}
deletedMsg={t('mosaicoTemplateDeleted')}/>
}
<Title>{isEdit ? t('Edit Mosaico Template') : t('Create Mosaico Template')}</Title>
<Title>{isEdit ? t('editMosaicoTemplate') : t('createMosaicoTemplate')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<Dropdown id="type" label={t('Type')} options={this.typeOptions}/>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/>
<Dropdown id="type" label={t('type')} options={this.typeOptions}/>
<NamespaceSelect/>
{form}
{isEdit ?
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('saveAndStay')} onClickAsync={::this.submitAndStay}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('saveAndLeave')}/>
{canDelete &&
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/templates/mosaico/${this.props.entity.id}/delete`}/>
<NavButton className="btn-danger" icon="remove" label={t('delete')} linkTo={`/templates/mosaico/${this.props.entity.id}/delete`}/>
}
</ButtonRow>
:
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
</ButtonRow>
}
</Form>

View file

@ -1,7 +1,7 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { withTranslation } from '../../lib/i18n';
import {DropdownMenu, Icon} from '../../lib/bootstrap-components';
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, MenuLink } from '../../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
@ -17,7 +17,7 @@ import {
} from "../../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
@ -54,11 +54,11 @@ export default class List extends Component {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') },
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('namespace') },
{
actions: data => {
const actions = [];
@ -66,28 +66,28 @@ export default class List extends Component {
if (perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/templates/mosaico/${data[0]}/edit`
});
}
if (perms.includes('viewFiles')) {
actions.push({
label: <Icon icon="hdd" title={t('Files')}/>,
label: <Icon icon="hdd" title={t('files')}/>,
link: `/templates/mosaico/${data[0]}/files`
});
}
if (perms.includes('viewFiles')) {
actions.push({
label: <Icon icon="th-large" title={t('Block thumbnails')}/>,
label: <Icon icon="th-large" title={t('blockThumbnails')}/>,
link: `/templates/mosaico/${data[0]}/blocks`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
label: <Icon icon="share-alt" title={t('share')}/>,
link: `/templates/mosaico/${data[0]}/share`
});
}
@ -101,17 +101,17 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/mosaico-templates`, t('Deleting Mosaico template ...'), t('Mosaico template deleted'))}
{tableDeleteDialogRender(this, `rest/mosaico-templates`, t('deletingMosaicoTemplate'), t('mosaicoTemplateDeleted'))}
{this.state.createPermitted &&
<Toolbar>
<DropdownMenu className="btn-primary" label={t('Create Mosaico Template')}>
<MenuLink to="/templates/mosaico/create">{t('Blank')}</MenuLink>
<MenuLink to="/templates/mosaico/create/versafix">{t('Versafix One')}</MenuLink>
<DropdownMenu className="btn-primary" label={t('createMosaicoTemplate')}>
<MenuLink to="/templates/mosaico/create">{t('blank')}</MenuLink>
<MenuLink to="/templates/mosaico/create/versafix">{t('versafixOne')}</MenuLink>
</DropdownMenu>
</Toolbar>
}
<Title>{t('Mosaico Templates')}</Title>
<Title>{t('mosaicoTemplates')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/mosaico-templates-table" columns={columns} />
</div>

View file

@ -18,8 +18,8 @@ export function getTemplateTypes(t) {
}
templateTypes.html = {
typeName: t('HTML'),
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('Template content')}/>,
typeName: t('html'),
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>,
afterLoad: data => {
data.html = data.data.html;
},
@ -33,8 +33,8 @@ export function getTemplateTypes(t) {
};
templateTypes.mjml = {
typeName: t('MJML'),
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('Template content')}/>,
typeName: t('mjml'),
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>,
afterLoad: data => {
data.mjml = data.data.mjml;
},

View file

@ -13,81 +13,81 @@ import MosaicoList from './mosaico/List';
function getMenus(t) {
return {
'templates': {
title: t('Templates'),
title: t('templates'),
link: '/templates',
panelComponent: TemplatesList,
children: {
':templateId([0-9]+)': {
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
title: resolved => t('templateName', {name: resolved.template.name}),
resolve: {
template: params => `rest/templates/${params.templateId}`
},
link: params => `/templates/${params.templateId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/templates/${params.templateId}/edit`,
visible: resolved => resolved.template.permissions.includes('edit'),
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
},
files: {
title: t('Files'),
title: t('files'),
link: params => `/templates/${params.templateId}/files`,
visible: resolved => resolved.template.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the content of the campaign.')} entity={props.resolved.template} entityTypeId="template" entitySubTypeId="file" managePermission="manageFiles"/>
panelRender: props => <Files title={t('files')} help={t('theseFilesArePubliclyAvailableViaHttpSo')} entity={props.resolved.template} entityTypeId="template" entitySubTypeId="file" managePermission="manageFiles"/>
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/templates/${params.templateId}/share`,
visible: resolved => resolved.template.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.template} entityTypeId="template" />
panelRender: props => <Share title={t('share')} entity={props.resolved.template} entityTypeId="template" />
}
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <TemplatesCUD action="create" />
},
mosaico: {
title: t('Mosaico Templates'),
title: t('mosaicoTemplates'),
link: '/templates/mosaico',
panelComponent: MosaicoList,
children: {
':mosaiceTemplateId([0-9]+)': {
title: resolved => t('Mosaico Template "{{name}}"', {name: resolved.mosaicoTemplate.name}),
title: resolved => t('mosaicoTemplateName', {name: resolved.mosaicoTemplate.name}),
resolve: {
mosaicoTemplate: params => `rest/mosaico-templates/${params.mosaiceTemplateId}`
},
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'),
panelRender: props => <MosaicoCUD action={props.match.params.action} entity={props.resolved.mosaicoTemplate} />
},
files: {
title: t('Files'),
title: t('files'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/files`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the Mosaico template.')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="file" managePermission="manageFiles" />
panelRender: props => <Files title={t('files')} help={t('theseFilesArePubliclyAvailableViaHttpSo-1')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="file" managePermission="manageFiles" />
},
blocks: {
title: t('Block thumbnails'),
title: t('blockThumbnails'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/blocks`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Block thumbnails')} help={t('These files will be used by Mosaico to search for block thumbnails (the "edres" directory). Place here one file per block type that you have defined in the Mosaico template. Each file must have the same name as the block id. The file will be used as the thumbnail of the corresponding block.')}entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="block" managePermission="manageFiles" />
panelRender: props => <Files title={t('blockThumbnails')} help={t('theseFilesWillBeUsedByMosaicoToSearchFor')}entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="block" managePermission="manageFiles" />
},
share: {
title: t('Share'),
title: t('share'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/share`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" />
panelRender: props => <Share title={t('share')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" />
}
}
},
create: {
title: t('Create'),
title: t('create'),
extraParams: [':wizard?'],
panelRender: props => <MosaicoCUD action="create" wizard={props.match.params.wizard} />
}

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTranslation } from '../lib/i18n';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page';
import { withForm, Form, FormSendMethod, InputField, ButtonRow, Button, TableSelect } from '../lib/form';
import { withErrorHandling } from '../lib/error-handling';
@ -12,7 +12,7 @@ import mailtrainConfig from 'mailtrainConfig';
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
@translate()
@withTranslation()
@withForm
@withPageHelpers
@withErrorHandling
@ -65,11 +65,11 @@ export default class CUD extends Component {
const usernameServerValidation = state.getIn(['username', 'serverValidation']);
if (!username) {
state.setIn(['username', 'error'], t('User name must not be empty'));
state.setIn(['username', 'error'], t('userNameMustNotBeEmpty'));
} else if (usernameServerValidation && usernameServerValidation.exists) {
state.setIn(['username', 'error'], t('The user name already exists in the system.'));
state.setIn(['username', 'error'], t('theUserNameAlreadyExistsInTheSystem'));
} else if (!usernameServerValidation) {
state.setIn(['email', 'error'], t('Validation is in progress...'));
state.setIn(['email', 'error'], t('validationIsInProgress'));
} else {
state.setIn(['username', 'error'], null);
}
@ -80,13 +80,13 @@ export default class CUD extends Component {
const emailServerValidation = state.getIn(['email', 'serverValidation']);
if (!email) {
state.setIn(['email', 'error'], t('Email must not be empty'));
state.setIn(['email', 'error'], t('emailMustNotBeEmpty-1'));
} else if (emailServerValidation && emailServerValidation.invalid) {
state.setIn(['email', 'error'], t('Invalid email address.'));
state.setIn(['email', 'error'], t('invalidEmailAddress'));
} else if (emailServerValidation && emailServerValidation.exists) {
state.setIn(['email', 'error'], t('The email is already associated with another user in the system.'));
state.setIn(['email', 'error'], t('theEmailIsAlreadyAssociatedWithAnother'));
} else if (!emailServerValidation) {
state.setIn(['email', 'error'], t('Validation is in progress...'));
state.setIn(['email', 'error'], t('validationIsInProgress'));
} else {
state.setIn(['email', 'error'], null);
}
@ -95,7 +95,7 @@ export default class CUD extends Component {
const name = state.getIn(['name', 'value']);
if (!name) {
state.setIn(['name', 'error'], t('Full name must not be empty'));
state.setIn(['name', 'error'], t('fullNameMustNotBeEmpty'));
} else {
state.setIn(['name', 'error'], null);
}
@ -109,7 +109,7 @@ export default class CUD extends Component {
let passwordMsgs = [];
if (!isEdit && !password) {
passwordMsgs.push(t('Password must not be empty'));
passwordMsgs.push(t('passwordMustNotBeEmpty'));
}
if (password) {
@ -121,7 +121,7 @@ export default class CUD extends Component {
}
state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null);
state.setIn(['password2', 'error'], password !== password2 ? t('Passwords must match') : null);
state.setIn(['password2', 'error'], password !== password2 ? t('passwordsMustMatch') : null);
}
validateNamespace(t, state);
@ -141,24 +141,24 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
this.setFormStatusMessage('info', t('saving'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
delete data.password2;
});
if (submitSuccessful) {
this.navigateToWithFlashMessage('/users', 'success', t('User saved'));
this.navigateToWithFlashMessage('/users', 'success', t('userSaved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
} catch (error) {
if (error instanceof interoperableErrors.DuplicitNameError) {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{t('The username is already assigned to another user.')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('theUsernameIsAlreadyAssignedToAnother')}
</span>
);
return;
@ -167,8 +167,8 @@ export default class CUD extends Component {
if (error instanceof interoperableErrors.DuplicitEmailError) {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{t('The email is already assigned to another user.')}
<strong>{t('yourUpdatesCannotBeSaved')}</strong>{' '}
{t('theEmailIsAlreadyAssignedToAnotherUser-1')}
</span>
);
return;
@ -199,28 +199,28 @@ export default class CUD extends Component {
deleteUrl={`rest/users/${this.props.entity.id}`}
backUrl={`/users/${this.props.entity.id}/edit`}
successUrl="/users"
deletingMsg={t('Deleting user ...')}
deletedMsg={t('User deleted')}/>
deletingMsg={t('deletingUser')}
deletedMsg={t('userDeleted')}/>
}
<Title>{isEdit ? t('Edit User') : t('Create User')}</Title>
<Title>{isEdit ? t('editUser') : t('createUser')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="username" label={t('User Name')}/>
<InputField id="username" label={t('userName')}/>
{mailtrainConfig.isAuthMethodLocal &&
<div>
<InputField id="name" label={t('Full Name')}/>
<InputField id="email" label={t('Email')}/>
<InputField id="password" label={t('Password')} type="password"/>
<InputField id="password2" label={t('Repeat Password')} type="password"/>
<InputField id="name" label={t('fullName')}/>
<InputField id="email" label={t('email')}/>
<InputField id="password" label={t('password')} type="password"/>
<InputField id="password2" label={t('repeatPassword')} type="password"/>
</div>
}
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={'rest/shares-roles-table/global'} columns={rolesColumns} selectionLabelIndex={1}/>
<TableSelect id="role" label={t('role')} withHeader dropdown dataUrl={'rest/shares-roles-table/global'} columns={rolesColumns} selectionLabelIndex={1}/>
<NamespaceSelect/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete User')} linkTo={`/users/${this.props.entity.id}/delete`}/>}
<Button type="submit" className="btn-primary" icon="ok" label={t('save')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('deleteUser')} linkTo={`/users/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -1,7 +1,7 @@
'use strict';
import React, {Component} from "react";
import {translate} from "react-i18next";
import { withTranslation } from '../lib/i18n';
import {NavButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from "../lib/page";
import {Table} from "../lib/table";
import mailtrainConfig from "mailtrainConfig";
@ -12,7 +12,7 @@ import {
tableDeleteDialogRender
} from "../lib/modals";
@translate()
@withTranslation()
@withPageHelpers
@requiresAuthenticatedUser
export default class List extends Component {
@ -45,12 +45,12 @@ export default class List extends Component {
const actions = [];
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
label: <Icon icon="edit" title={t('edit')}/>,
link: `/users/${data[0]}/edit`
});
actions.push({
label: <Icon icon="share" title={t('Share')}/>,
label: <Icon icon="share" title={t('share')}/>,
link: `/users/${data[0]}/shares`
});
@ -62,12 +62,12 @@ export default class List extends Component {
return (
<div>
{tableDeleteDialogRender(this, `rest/users`, t('Deleting user ...'), t('User deleted'))}
{tableDeleteDialogRender(this, `rest/users`, t('deletingUser'), t('userDeleted'))}
<Toolbar>
<NavButton linkTo="/users/create" className="btn-primary" icon="plus" label={t('Create User')}/>
<NavButton linkTo="/users/create" className="btn-primary" icon="plus" label={t('createUser')}/>
</Toolbar>
<Title>{t('Users')}</Title>
<Title>{t('users')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/users-table" columns={columns} />
</div>

View file

@ -1,43 +1,42 @@
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import i18n from '../lib/i18n';
import { Section } from '../lib/page';
import CUD from './CUD';
import List from './List';
import UserShares from '../shares/UserShares';
import React
from 'react';
import CUD
from './CUD';
import List
from './List';
import UserShares
from '../shares/UserShares';
function getMenus(t) {
return {
'users': {
title: t('Users'),
title: t('users'),
link: '/users',
panelComponent: List,
children: {
':userId([0-9]+)': {
title: resolved => t('User "{{name}}"', {name: resolved.user.name}),
title: resolved => t('userName-1', {name: resolved.user.name}),
resolve: {
user: params => `rest/users/${params.userId}`
},
link: params => `/users/${params.userId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
title: t('edit'),
link: params => `/users/${params.userId}/edit`,
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.user} />
},
shares: {
title: t('Shares'),
title: t('shares'),
link: params => `/users/${params.userId}/shares`,
panelRender: props => <UserShares user={props.resolved.user} />
}
}
},
create: {
title: t('Create'),
title: t('create'),
panelRender: props => <CUD action="create" />
},
}

View file

@ -1,143 +0,0 @@
{
"subscription": {
"subscriptionConfirmed": "{{list}}: Subscription Confirmed",
"unsubscriptionConfirmed": "{{list}}: Unsubscription Confirmed",
"alreadyRegistered": "{{list}}: Email Address Already Registered",
"confirmEmailChange": "{{list}}: Please Confirm Email Change in Subscription",
"confirmSubscription": "{{list}}: Please Confirm Subscription",
"confirmUnsubscription": "{{list}}: Please Confirm Unsubscription",
"emailChanged": "Email address changed",
"addressNotSet": "Email address not set",
"confirmSubscription": "Please Confirm Subscription",
"nothingChanged": "Nothing seems to be changed",
"furtherInstructionsSent": "An email with further instructions has been sent to the provided address"
},
"feedCheck": {
"campaignsAdded": "Found {{addedMessages}} new campaign messages from feed {{campaignId}}",
"nothingNew": "Found nothing new from the feed"
},
"addressCheck": {
"invalidEmailGeneric": "Invalid email address \"{{email}}\".",
"mxNotFound": "Invalid email address \"{{email}}\": MX record not found for domain",
"domainNotFound": "Invalid email address \"{{email}}\": Address domain not found",
"domainRequired": "Invalid email address \"{{email}}\": Address domain name is required"
},
"account": {
"passwordChangeRequest": "Mailer password change request",
"emailAlreadyRegistered": "The email is already associated with another user in the system.",
"fullNameMustNotBeEmpty": "Full name must not be empty",
"currentPasswordMustNotBeEmpty": "Current password must not be empty.",
"incorrectPassword": "Incorrect password.",
"passwordsMustMatch": "Passwords must match",
"updatingUserProfile": "Updating user profile ...",
"userProfileUpdated": "User profile updated",
"passwordPossiblyChanged": "The password is incorrect (possibly just changed in another window / session). Enter correct password and try again.",
"emailAlreadyRegisteredTryAgain": "The email is already assigned to another user. Enter another email and try again.",
"generalSettings": "General Settings",
"fullName": "Full Name",
"addressUsedForAccountRecovery": "This address is used for account recovery in case you loose your password",
"passwordChange": "Password Change",
"fillOnlyForPasswordChange": "You only need to fill out this form if you want to change your current password",
"currentPassword": "Current Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"accountManagementNotPossible": "Account management is not possible because Mailtrain is configured to use externally managed users.",
"useThisLinkToChangePassword": "If you want to change the password, use <1>this link</1>."
},
"importer": {
"missingEmail": "Missing email"
},
"home": {
"welcome": "Welcome to Mailtrain..."
},
"root": {
"current": "(current)",
"toggleNavigation": "Toggle navigation",
"administration": "Administration",
"account": "Account"
},
"files": {
"filesAdded": "{{count}} file added",
"filesAdded_plural": "{{count}} files added",
"filesReplaced": "{{count}} file replaced",
"filesReplaced_plural": "{{count}} files replaced",
"filesIgnored": "{{count}} file ignored",
"filesIgnored_plural": "{{count}} files ignored",
"filesUploaded": "{{count}} file uploaded",
"filesUploaded_plural": "{{count}} files uploaded",
"uploadingFiles": "Uploading {{count}} file",
"uploadingFiles_plural": "Uploading {{count}} files",
"fileUploadFailed": "File upload failed:",
"noFilesToUpload": "No files to upload",
"deletingFile": "Deleting file ...",
"fileDeleted": "File deleted",
"deleteFileFailed": "Delete file failed:",
"confirmFileDeletion": "Confirm file deletion",
"areYouSureToDeleteFile": "Are you sure you want to delete file \"{{name}}\"?",
"dropFiles": "Drop {{count}} file",
"dropFiles_plural": "Drop {{count}} files",
"dropFilesHere": "Drop files here"
},
"form": {
"openCalendar": "Open calendar",
"select": "Select",
"yourUpdatesCannotBeSaved": "Your updates cannot be saved.",
"modificationsInTheMeantime": "Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.",
"namespaceDeletedInTheMeantime": "It seems that someone else has deleted the target namespace in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.",
"deletionInTheMeantime": "It seems that someone else has deleted the entity in the meantime."
},
"deleteDialog": {
"cannotDeleteDueToDependencies": "Cannote delete \"{{name}}\" due to the following dependencies:",
"andMore": "... and more",
"areYouSureToDelete": "Are you sure you want to delete \"{{name}}\"?",
"confirmDeletion": "Confirm deletion"
},
"namespace": {
"mustBeSelected": "Namespace must be selected"
},
"close": "Close",
"name": "Name",
"size": "Size",
"download": "Download",
"delete": "Delete",
"yes": "Yes",
"no": "No",
"loading": "Loading ...",
"email": "Email",
"update": "Update",
"namespace": "Namespace",
"namespace_plural": "Namespaces",
"list": "List",
"list_plural": "Lists",
"customForms": "Custom forms",
"campaign": "Campaign",
"campaign_plural": "Campaigns",
"template": "Template",
"template_plural": "Templates",
"sendConfiguration": "Send configuration",
"sendConfiguration_plural": "Send configurations",
"report": "Report",
"report_plural": "Reports",
"reportTemplate": "Report template",
"reportTemplate_plural": "Report templates",
"mosaicoTemplate": "Mosaico template",
"mosaicoTemplate_plural": "Mosaico templates",
"user": "User",
"user_plural": "Users",
"globalSetting_plural": "Global Settings",
"blacklist": "Blacklist",
"api": "API",
"logout": "Log out",
"sourceOnGithub": "Source on GitHub",
"emailMustNotBeEmpty": "Email must not be empty.",
"invalidEmailAddress": "Invalid email address.",
"validationInProgress": "Validation is in progress...",
"errorsInForm": "There are errors in the form. Please fix them and submit again.",
"updatesCannotBeSaved": "Your updates cannot be saved.",
"mjml": "MJML",
"html": "HTML"
}

870
locales/en/common.json Normal file
View file

@ -0,0 +1,870 @@
{
"welcomeToMailtrain": "Welcome to Mailtrain...",
"personalAccessToken": "Personal access token",
"accessTokenNotYetGenerated": "Access token not yet generated",
"api": "API",
"resetAccessToken": "Reset Access Token",
"generateAccessToken": "Generate Access Token",
"notesAboutTheApi": "Notes about the API",
"addSubscription": "Add subscription",
"thisApiCallEitherInsertsANewSubscription": "This API call either inserts a new subscription or updates existing. Fields not included are left as is, so if you update only LAST_NAME value, then FIRST_NAME is kept untouched for an existing subscription.",
"arguments": "arguments",
"yourPersonalAccessToken": "your personal access token",
"subscribersEmailAddress": "subscriber\\'s email address",
"required": "required",
"subscribersFirstName": "subscriber\\'s first name",
"subscribersLastName": "subscriber\\'s last name",
"subscribersTimezoneEgEuropeTallinnPstOr": "subscriber\\'s timezone (eg. \"Europe/Tallinn\", \"PST\" or \"UTC\"). If not set defaults to \"UTC\"",
"customFieldValueUseYesnoForOptionGroup": "custom field value. Use yes/no for option group values (checkboxes, radios, drop downs)",
"additionalPostArguments": "Additional POST arguments",
"setToYesIfYouWantToMakeSureTheEmailIs": "set to \"yes\" if you want to make sure the email is marked as subscribed even if it was previously marked as unsubscribed. If the email was already unsubscribed/blocked then subscription status is not changed",
"setToYesIfYouWantToSendConfirmationEmail": "set to \"yes\" if you want to send confirmation email to the subscriber before actually marking as subscribed",
"example": "Example",
"removeSubscription": "Remove subscription",
"thisApiCallMarksASubscriptionAs": "This API call marks a subscription as unsubscribed",
"deleteSubscription": "Delete subscription",
"thisApiCallDeletesASubscription": "This API call deletes a subscription",
"addNewCustomField": "Add new custom field",
"thisApiCallCreatesANewCustomFieldForA": "This API call creates a new custom field for a list.",
"fieldName": "field name",
"oneOfTheFollowingTypes": "one of the following types:",
"ifTheTypeIsOptionThenYouAlsoNeedTo": "If the type is \\'option\\' then you also need to specify the parent element ID",
"templateForTheGroupElementIfNotSetThen": "Template for the group element. If not set, then values of the elements are joined with commas",
"ifNotVisibleThenTheSubscriberCanNotView": "if not visible then the subscriber can not view or modify this value at the profile page",
"getListOfBlacklistedEmails": "Get list of blacklisted emails",
"thisApiCallGetListOfBlacklistedEmails": "This API call get list of blacklisted emails.",
"startPosition": "Start position",
"optionalDefault0": "optional, default 0",
"limitEmailsCountInResponse": "limit emails count in response",
"optionalDefault10000": "optional, default 10000",
"filterByPartOfEmail": "filter by part of email",
"optionalDefault": "optional, default \"\"",
"addEmailToBlacklist": "Add email to blacklist",
"thisApiCallEitherAddEmailsToBlacklist": "This API call either add emails to blacklist",
"emailAddress": "email address",
"deleteEmailFromBlacklist": "Delete email from blacklist",
"thisApiCallEitherDeleteEmailsFrom": "This API call either delete emails from blacklist",
"getTheListsAUserHasSubscribedTo": "Get the lists a user has subscribed to",
"retrieveTheListsThatTheUserWithEmailHas": "Retrieve the lists that the user with :email has subscribed to.",
"apiResponseIsAJsonStructureWithErrorAnd": "API response is a JSON structure with <1>error</1> and <3>data</3> properties. If the response <5>error</5> has a value set then the request failed.",
"youNeedToDefineProperContentTypeWhen": "You need to define proper <1>Content-Type</1> when making a request. You can either use <3>application/x-www-form-urlencoded</3> for normal form data or <5>application/json</5> for a JSON payload. Using <7>multipart/form-data</7> is not supported.",
"emailMustNotBeEmpty": "Email must not be empty.",
"invalidEmailAddress": "Invalid email address.",
"theEmailIsAlreadyAssociatedWithAnother": "The email is already associated with another user in the system.",
"validationIsInProgress": "Validation is in progress...",
"fullNameMustNotBeEmpty": "Full name must not be empty",
"currentPasswordMustNotBeEmpty": "Current password must not be empty.",
"incorrectPassword": "Incorrect password.",
"passwordsMustMatch": "Passwords must match",
"updatingUserProfile": "Updating user profile ...",
"userProfileUpdated": "User profile updated",
"thereAreErrorsInTheFormPleaseFixThemAnd": "There are errors in the form. Please fix them and submit again.",
"yourUpdatesCannotBeSaved": "Your updates cannot be saved.",
"thePasswordIsIncorrectPossiblyJust": "The password is incorrect (possibly just changed in another window / session). Enter correct password and try again.",
"theEmailIsAlreadyAssignedToAnotherUser": "The email is already assigned to another user. Enter another email and try again.",
"account": "Account",
"generalSettings": "General Settings",
"fullName": "Full Name",
"email": "Email",
"thisAddressIsUsedForAccountRecoveryIn": "This address is used for account recovery in case you loose your password",
"passwordChange": "Password Change",
"youOnlyNeedToFillOutThisFormIfYouWantTo": "You only need to fill out this form if you want to change your current password",
"currentPassword": "Current Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"update": "Update",
"accountManagementIsNotPossibleBecause": "Account management is not possible because Mailtrain is configured to use externally managed users.",
"ifYouWantToChangeThePasswordUseThisLink": "If you want to change the password, use <1>this link</1>.",
"usernameOrEmailMustNotBeEmpty": "Username or email must not be empty",
"processing": "Processing ...",
"ifTheUsernameEmailExistsInTheSystem": "If the username / email exists in the system, password reset link will be sent to the registered email.",
"pleaseEnterYourUsernameEmailAndTryAgain": "Please enter your username / email and try again.",
"passwordReset": "Password Reset",
"pleaseProvideTheUsernameOrEmailAddress": "Please provide the username or email address that is registered with your Mailtrain account.",
"weWillSendYouAnEmailThatWillAllowYouTo": "We will send you an email that will allow you to reset your password.",
"usernameOrEmail": "Username or email",
"sendEmail": "Send email",
"userNameMustNotBeEmpty": "User name must not be empty",
"passwordMustNotBeEmpty": "Password must not be empty",
"verifyingCredentials": "Verifying credentials ...",
"pleaseEnterYourCredentialsAndTryAgain": "Please enter your credentials and try again.",
"invalidUsernameOrPassword": "Invalid username or password.",
"forgotYourPassword?": "Forgot your password?",
"signIn": "Sign in",
"username": "Username",
"password": "Password",
"rememberMe": "Remember me",
"resettingPassword": "Resetting password ...",
"passwordReset-1": "Password reset",
"yourPasswordCannotBeReset": "Your password cannot be reset.",
"thePasswordResetTokenHasExpired": "The password reset token has expired.",
"clickHereToRequestANewPasswordResetLink": "Click here to request a new password reset link.",
"validatingPasswordResetToken": "Validating password reset token ...",
"thePasswordCannotBeReset": "The password cannot be reset",
"setNewPasswordFor": "Set new password for",
"resetPassword": "Reset password",
"emailMustNotBeEmpty-1": "Email must not be empty",
"theEmailIsAlreadyOnBlacklist": "The email is already on blacklist.",
"saving": "Saving ...",
"thereAreErrorsInTheFormPleaseFixThemAnd-1": "There are errors in the form. Please fix them and try again.",
"removeFromBlacklist": "Remove from blacklist",
"blacklist": "Blacklist",
"addEmailToBlacklist-1": "Add Email to Blacklist",
"addToBlacklist": "Add to Blacklist",
"blacklistedEmails": "Blacklisted Emails",
"createRegularCampaign": "Create Regular Campaign",
"createRssCampaign": "Create RSS Campaign",
"createTriggeredCampaign": "Create Triggered Campaign",
"editRegularCampaign": "Edit Regular Campaign",
"editRssCampaign": "Edit RSS Campaign",
"editTriggeredCampaign": "Edit Triggered Campaign",
"template": "Template",
"customContentClonedFromTemplate": "Custom content cloned from template",
"customContentClonedFromAnotherCampaign": "Custom content cloned from another campaign",
"customContent": "Custom content",
"url": "URL",
"nameMustNotBeEmpty": "Name must not be empty",
"sendConfigurationMustBeSelected": "Send configuration must be selected",
"fromEmailMustNotBeEmpty": "\"From\" email must not be empty",
"templateMustBeSelected": "Template must be selected",
"campaignMustBeSelected": "Campaign must be selected",
"typeMustBeSelected": "Type must be selected",
"urlMustNotBeEmpty": "URL must not be empty",
"rssFeedUrlMustBeGiven": "RSS feed URL must be given",
"listMustBeSelected": "List must be selected",
"segmentMustBeSelected": "Segment must be selected",
"campaignSaved": "Campaign saved",
"rssFeedUrl": "RSS Feed Url",
"name": "Name",
"id": "ID",
"subscribers": "Subscribers",
"description": "Description",
"namespace": "Namespace",
"remove": "Remove",
"insertNewEntryBeforeThisOne": "Insert new entry before this one",
"moveUp": "Move up",
"moveDown": "Move down",
"list": "List",
"segment": "Segment",
"useAParticularSegment": "Use a particular segment",
"lists": "Lists",
"addList": "Add list",
"type": "Type",
"created": "Created",
"override": "Override",
"fromName": "\"From\" name",
"fromEmailAddress": "\"From\" email address",
"replytoEmailAddress": "\"Reply-to\" email address",
"subjectLine": "\"Subject\" line",
"loadingSendConfiguration": "Loading send configuration ...",
"contentSource": "Content source",
"selectingATemplateCreatesACampaign": "Selecting a template creates a campaign specific copy from it.",
"campaign": "Campaign",
"contentOfTheSelectedCampaignWillBeCopied": "Content of the selected campaign will be copied into this campaign.",
"renderUrl": "Render URL",
"ifAMessageIsSentThenThisUrlWillBePosTed": "If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.",
"save": "Save",
"saveAndEditContent": "Save and edit content",
"saveCampaignAndGoToStatus": "Save campaign and go to status",
"deletingCampaign": "Deleting campaign ...",
"campaignDeleted": "Campaign deleted",
"formCannotBeEditedBecauseTheCampaignIs": "Form cannot be edited because the campaign is currently being sent out. Wait till the sending is finished and refresh.",
"thisIsTheCampaignIdDisplayedToThe": "This is the campaign ID displayed to the subscribers",
"sendConfiguration": "Send configuration",
"customUnsubscribeUrl": "Custom unsubscribe URL",
"disableOpenedTracking": "Disable opened tracking",
"disableClickedTracking": "Disable clicked tracking",
"delete": "Delete",
"editCustomContent": "Edit Custom Content",
"customTemplateEditor": "Custom template editor",
"testSend": "Test send",
"status": "Status",
"sendingScheduled": "Sending scheduled",
"sending": "Sending",
"edit": "Edit",
"content": "Content",
"files": "Files",
"attachments": "Attachments",
"triggers": "Triggers",
"share": "Share",
"createCampaign": "Create Campaign",
"regular": "Regular",
"rss": "RSS",
"triggered": "Triggered",
"campaigns": "Campaigns",
"subscriptionHasToBeSelectedToShowThe": "Subscription has to be selected to show the campaign for a test user.",
"subscriptionId": "Subscription ID",
"listId": "List ID",
"listNamespace": "List namespace",
"previewCampaignAs": "Preview campaign as",
"preview": "Preview",
"dateMustNotBeEmpty": "Date must not be empty",
"dateIsInvalid": "Date is invalid",
"timeMustNotBeEmpty": "Time must not be empty",
"timeIsInvalid": "Time is invalid",
"subscribers-1": "subscribers",
"sendStatus": "Send status",
"campaignIsScheduledForDelivery": "Campaign is scheduled for delivery.",
"campaignIsReadyToBeSentOut": "Campaign is ready to be sent out.",
"sendLater": "Send later",
"scheduleDeliveryAtAParticularDatetime": "Schedule delivery at a particular date/time",
"date": "Date",
"time": "Time",
"enter24hourTimeInFormatHhmmEg1348": "Enter 24-hour time in format HH:MM (e.g. 13:48)",
"rescheduleSend": "Reschedule send",
"scheduleSend": "Schedule send",
"send": "Send",
"campaignIsBeingSentOut": "Campaign is being sent out.",
"stop": "Stop",
"allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if you you want to send this campaign to new subscribers.",
"continue": "Continue",
"reset": "Reset",
"yourCampaignIsCurrentlyDisabledClick": "Your campaign is currently disabled. Click Enable button to start enable it.",
"enable": "Enable",
"yourCampaignIsEnabledAndSendingMessages": "Your campaign is enabled and sending messages.",
"disable": "Disable",
"campaignStatus": "Campaign Status",
"computing": "computing ...",
"targetListssegments": "Target lists/segments",
"ifANewEntryIsFoundFromCampaignFeedANew": "If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here",
"sendingTestEmail": "Sending test email",
"subscriptionHasToBeSelected": "Subscription has to be selected.",
"sendTestEmail": "Send Test Email",
"cancel": "Cancel",
"subscription": "Subscription",
"idle": "Idle",
"scheduled": "Scheduled",
"paused": "Paused",
"finished": "Finished",
"inactive": "Inactive",
"active": "Active",
"campaignName": "Campaign \"{{name}}\"",
"theseFilesArePubliclyAvailableViaHttpSo": "These files are publicly available via HTTP so that they can be linked to from the content of the campaign.",
"theseFilesWillBeAttachedToTheCampaign": "These files will be attached to the campaign emails as proper attachments. This means they count towards to the eventual size of the email.",
"triggerName": "Trigger \"{{name}}\"",
"create": "Create",
"valuesMustNotBeEmpty": "Values must not be empty",
"valueMustBeANonnegativeNumber": "Value must be a non-negative number",
"sourceCampaignMustNotBeEmpty": "Source campaign must not be empty",
"triggerSaved": "Trigger saved",
"deletingTrigger": "Deleting trigger ...",
"triggerDeleted": "Trigger deleted",
"editTrigger": "Edit Trigger",
"createTrigger": "Create Trigger",
"entity": "Entity",
"selectTheTypeOfTheTriggerRule": "Select the type of the trigger rule.",
"triggerFires": "Trigger fires",
"event": "Event",
"selectTheEventThatTriggersSendingThe": "Select the event that triggers sending the campaign.",
"enabled": "Enabled",
"daysAfter": "Days after",
"yes": "Yes",
"no": "No",
"latestOpen": "Latest open",
"latestClick": "Latest click",
"delivered": "Delivered",
"opened": "Opened",
"clicked": "Clicked",
"notOpened": "Not opened",
"notClicked": "Not clicked",
"close": "Close",
"countFileAdded": "{{count}} file added",
"countFileAdded_plural": "{{count}} files added",
"countFileReplaced": "{{count}} file replaced",
"countFileReplaced_plural": "{{count}} files replaced",
"countFileIgnored": "{{count}} file ignored",
"countFileIgnored_plural": "{{count}} files ignored",
"countFileUploaded": "{{count}} file uploaded",
"countFileUploaded_plural": "{{count}} files uploaded",
"uploadingCountFile": "Uploading {{count}} file",
"uploadingCountFile_plural": "Uploading {{count}} files",
"fileUploadFailed": "File upload failed:",
"noFilesToUpload": "No files to upload",
"deletingFile": "Deleting file ...",
"fileDeleted": "File deleted",
"deleteFileFailed": "Delete file failed:",
"size": "Size",
"download": "Download",
"confirmFileDeletion": "Confirm file deletion",
"filesareYouSureToDeleteFile": "files:areYouSureToDeleteFile",
"dropCountFile": "Drop {{count}} file",
"dropCountFile_plural": "Drop {{count}} files",
"dropFilesHere": "Drop files here",
"loading": "Loading ...",
"openCalendar": "Open calendar",
"select": "Select",
"someoneElseHasIntroducedModificationIn": "Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.",
"itSeemsThatSomeoneElseHasDeletedThe": "It seems that someone else has deleted the target namespace in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.",
"itSeemsThatSomeoneElseHasDeletedThe-1": "It seems that someone else has deleted the entity in the meantime.",
"namespace_plural": "Namespaces",
"list_plural": "Lists",
"customForms": "Custom forms",
"campaign_plural": "Campaigns",
"template_plural": "Templates",
"sendConfiguration_plural": "Send configurations",
"report": "Report",
"report_plural": "Reports",
"reportTemplate": "Report template",
"reportTemplate_plural": "Report templates",
"mosaicoTemplate": "Mosaico template",
"mosaicoTemplate_plural": "Mosaico templates",
"cannoteDeleteNameDueToTheFollowing": "Cannote delete \"{{name}}\" due to the following dependencies:",
"andMore": "... and more",
"confirmDeletion": "Confirm deletion",
"areYouSureYouWantToDeleteName?": "Are you sure you want to delete \"{{name}}\"?",
"namespacemustBeSelected": "namespace.mustBeSelected",
"mjml": "MJML",
"html": "HTML",
"countEntriesSelected": "{{ count }} entries selected.",
"loading-1": "Loading...",
"customFormMustBeSelected": "Custom form must be selected",
"listSaved": "List saved",
"onestepIeNoEmailWithConfirmationLink": "One-step (i.e. no email with confirmation link)",
"onestepWithUnsubscriptionFormIeNoEmail": "One-step with unsubscription form (i.e. no email with confirmation link)",
"twostepIeAnEmailWithConfirmationLinkWill": "Two-step (i.e. an email with confirmation link will be sent)",
"twostepWithUnsubscriptionFormIeAnEmail": "Two-step with unsubscription form (i.e. an email with confirmation link will be sent)",
"manualIeUnsubscriptionHasToBePerformedBy": "Manual (i.e. unsubscription has to be performed by the list administrator)",
"defaultMailtrainForms": "Default Mailtrain Forms",
"customFormsSelectFormBelow": "Custom Forms (select form below)",
"deletingList": "Deleting list ...",
"listDeleted": "List deleted",
"editList": "Edit List",
"createList": "Create List",
"thisIsTheListIdDisplayedToTheSubscribers": "This is the list ID displayed to the subscribers",
"contactEmail": "Contact email",
"contactEmailUsedInSubscriptionFormsAnd": "Contact email used in subscription forms and emails that are sent out. If not filled in, the admin email from the global settings will be used.",
"homepage": "Homepage",
"homepageUrlUsedInSubscriptionFormsAnd": "Homepage URL used in subscription forms and emails that are sent out. If not filled in, the default homepage from global settings will be used.",
"recipientsNameTemplate": "Recipients name template",
"specifyUsingMergeTagsOfThisListHowTo": "Specify using merge tags of this list how to construct full name of the recipient. This full name is used as \"To\" header when sending emails.",
"sendConfigurationThatWillBeUsedFor": "Send configuration that will be used for sending out subscription-related emails.",
"forms": "Forms",
"webAndEmailFormsAndTemplatesUsedIn": "Web and email forms and templates used in subscription management process.",
"allowPublicUsersToSubscribeThemselves": "Allow public users to subscribe themselves",
"unsubscription": "Unsubscription",
"selectHowAnUnsuscriptionRequestBy": "Select how an unsuscription request by subscriber is handled.",
"unsubscribeHeader": "Unsubscribe header",
"doNotSendListUnsubscribeHeaders": "Do not send List-Unsubscribe headers",
"theCustomFormUsedForThisListYouCanCreate": "The custom form used for this list. You can create a form <1>here</1>.",
"fields": "Fields",
"segments": "Segments",
"imports": "Imports",
"customForms-1": "Custom Forms",
"mergeTagIsInvalidMayMustBeUppercaseAnd": "Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.",
"anotherFieldWithTheSameMergeTagExists": "Another field with the same merge tag exists. Please choose another merge tag.",
"groupHasToBeSelected": "Group has to be selected",
"defaultValueIsNotIntegerNumber": "Default value is not integer number",
"defaultValueIsNotAProperlyFormattedDate": "Default value is not a properly formatted date",
"defaultValueIsNotAProperlyFormatted": "Default value is not a properly formatted birthday date",
"defaultValueIsNotOneOfTheAllowedOptions": "Default value is not one of the allowed options",
"errrorOnLineLine": "Errror on line {{ line }}",
"fieldSaved": "Field saved",
"notVisible": "Not visible",
"endOfList": "End of list",
"fieldSettings": "Field settings",
"defaultValue": "Default value",
"defaultValueUsedWhenTheFieldIsEmpty": "Default value used when the field is empty.",
"options": "Options",
"dateFormat": "Date format",
"mmddyyyy": "MM/DD/YYYY",
"ddmmyyyy": "DD/MM/YYYY",
"mmdd": "MM/DD",
"ddmm": "DD/MM",
"mergeTag": "Merge Tag",
"group": "Group",
"selectGroupToWhichTheOptionsShouldBelong": "Select group to which the options should belong.",
"deletingField": "Deleting field ...",
"fieldDeleted": "Field deleted",
"editField": "Edit Field",
"createField": "Create Field",
"mergeTag-1": "Merge tag",
"fieldOrder": "Field order",
"listingsBefore": "Listings (before)",
"selectTheFieldBeforeWhichThisFieldShould": "Select the field before which this field should appear in listings. To exclude the field from listings, select \"Not visible\".",
"subscriptionFormBefore": "Subscription form (before)",
"selectTheFieldBeforeWhichThisFieldShould-1": "Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select \"Not visible\".",
"managementFormBefore": "Management form (before)",
"selectTheFieldBeforeWhichThisFieldShould-2": "Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select \"Not visible\".",
"youCanControlTheAppearanceOfTheMergeTag": "You can control the appearance of the merge tag with this template. The template\n uses handlebars syntax and you can find all values from <1>{'{{values}}'}</1> array, for\n example <3>{'{{#each values}} {{this}} {{/each}}'}</3>. If template is not defined then\n multiple values are joined with commas.",
"specifyTheOptionsToSelectFromInThe": "<0>Specify the options to select from in the following format:<1>key|label</1>. For example:</0>\n <2><0>au|Australia</0></2><3><0>at|Austria</0></3>",
"defaultKeyEgAuUsedWhenTheFieldIsEmpty": "Default key (e.g. <1>au</1> used when the field is empty.')",
"youCanControlTheAppearanceOfTheMergeTag-1": "You can control the appearance of the merge tag with this template. The template\n uses handlebars syntax and you can find all values from <1>{'{{values}}'}</1> array.\n Each entry in the array is an object with attributes <3>key</3> and <5>label</5>.\n For example <7>{'{{#each values}} {{this.value}} {{/each}}'}</7>. If template is not defined then\n multiple values are joined with commas.",
"youCanUseThisTemplateToRenderJsonValues": "You can use this template to render JSON values (if the JSON is an array then the array is\n exposed as <1>values</1>, otherwise you can access the JSON keys directly).",
"text": "Text",
"website": "Website",
"multilineText": "Multi-line text",
"gpgPublicKey": "GPG Public Key",
"number": "Number",
"checkboxesFromOptionFields": "Checkboxes (from option fields)",
"radioButtonsFromOptionFields": "Radio Buttons (from option fields)",
"dropDownFromOptionFields": "Drop Down (from option fields)",
"radioButtonsEnumerated": "Radio Buttons (enumerated)",
"dropDownEnumerated": "Drop Down (enumerated)",
"birthday": "Birthday",
"jsonValueForCustomRendering": "JSON value for custom rendering",
"option": "Option",
"thePlaintextVersionForThisEmail": "The plaintext version for this email",
"layout": "Layout",
"formInputStyle": "Form Input Style",
"thisCssStylesheetDefinesTheAppearanceOf": "This CSS stylesheet defines the appearance of form input elements and alerts",
"webSubscribe": "Web - Subscribe",
"webConfirmSubscriptionNotice": "Web - Confirm Subscription Notice",
"mailConfirmSubscriptionMjml": "Mail - Confirm Subscription (MJML)",
"mailConfirmSubscriptionText": "Mail - Confirm Subscription (Text)",
"mailAlreadySubscribedMjml": "Mail - Already Subscribed (MJML)",
"mailAlreadySubscribedText": "Mail - Already Subscribed (Text)",
"webSubscribedNotice": "Web - Subscribed Notice",
"mailSubscriptionConfirmedMjml": "Mail - Subscription Confirmed (MJML)",
"mailSubscriptionConfirmedText": "Mail - Subscription Confirmed (Text)",
"webManagePreferences": "Web - Manage Preferences",
"webManageAddress": "Web - Manage Address",
"mailConfirmAddressChangeMjml": "Mail - Confirm Address Change (MJML)",
"mailConfirmAddressChangeText": "Mail - Confirm Address Change (Text)",
"webUpdatedNotice": "Web - Updated Notice",
"webUnsubscribe": "Web - Unsubscribe",
"webConfirmUnsubscriptionNotice": "Web - Confirm Unsubscription Notice",
"mailConfirmUnsubscriptionMjml": "Mail - Confirm Unsubscription (MJML)",
"mailConfirmUnsubscriptionText": "Mail - Confirm Unsubscription (Text)",
"webUnsubscribedNotice": "Web - Unsubscribed Notice",
"mailUnsubscriptionConfirmedMjml": "Mail - Unsubscription Confirmed (MJML)",
"mailUnsubscriptionConfirmedText": "Mail - Unsubscription Confirmed (Text)",
"webManualUnsubscribeNotice": "Web - Manual Unsubscribe Notice",
"general": "General",
"subscribe": "Subscribe",
"manage": "Manage",
"unsubscribe": "Unsubscribe",
"listOfErrorsInTemplates": "List of errors in templates",
"formsSaved": "Forms saved",
"deletingForm": "Deleting form ...",
"formDeleted": "Form deleted",
"editCustomForms": "Edit Custom Forms",
"createCustomForms": "Create Custom Forms",
"formsPreview": "Forms Preview",
"listToPreviewOn": "List To Preview On",
"selectListWhoseFieldsWillBeUsedToPreview": "Select list whose fields will be used to preview the forms.",
"templates": "Templates",
"customFormsUseMjmlForFormattingSeeThe": "Custom forms use MJML for formatting. See the MJML documentation <1>here</1>",
"createCustomForm": "Create Custom Form",
"fileMustBeSelected": "File must be selected",
"csvDelimiterMustNotBeEmpty": "CSV delimiter must not be empty",
"emailMappingHasToBeProvided": "Email mapping has to be provided",
"importSaved": "Import saved",
"file": "File",
"delimiter": "Delimiter",
"preparationInProgressPleaseWaitTillItIs": "Preparation in progress. Please wait till it is done or visit this page later.",
"Select ": " Select ",
"eg": "e.g.:",
"checkImportedEmails": "Check imported emails",
"mapping": "Mapping",
"saveAndEditSettings": "Save and edit settings",
"deletingImport": "Deleting import ...",
"importDeleted": "Import deleted",
"editImport": "Edit Import",
"createImport": "Create Import",
"source": "Source",
"lastRun": "Last run",
"detailedStatus": "Detailed status",
"row": "Row",
"reason": "Reason",
"importRunStatus": "Import Run Status",
"importName": "Import name",
"importSource": "Import source",
"runStarted": "Run started",
"runFinished": "Run finished",
"runStatus": "Run status",
"processedEntries": "Processed entries",
"newEntries": "New entries",
"failedEntries": "Failed entries",
"error": "Error",
"failedRows": "Failed Rows",
"started": "Started",
"processed": "Processed",
"new": "New",
"failed": "Failed",
"importStatus": "Import Status",
"actions": "Actions",
"start": "Start",
"importRuns": "Import Runs",
"csvFile": "CSV file",
"preparing": "Preparing",
"stopping": "Stopping",
"ready": "Ready",
"preparationFailed": "Preparation failed",
"running": "Running",
"starting": "Starting",
"basicImportOfSubscribers": "Basic import of subscribers",
"unsubscribeEmails": "Unsubscribe emails",
"listName": "List \"{{name}}\"",
"fieldName-1": "Field \"{{name}}\"",
"segmentName": "Segment \"{{name}}\"",
"importName-1": "Import \"{{name}}\"",
"run": "Run",
"customFormsName": "Custom Forms \"{{name}}\"",
"segmentSaved": "Segment saved",
"deletingSegment": "Deleting segment ...",
"segmentDeleted": "Segment deleted",
"editSegment": "Edit Segment",
"createSegment": "Create Segment",
"saveAndStay": "Save and Stay",
"saveAndLeave": "Save and Leave",
"segmentOptions": "Segment Options",
"toplevelMatchType": "Toplevel match type",
"addCompositeRule": "Add Composite Rule",
"addRule": "Add Rule",
"rules": "Rules",
"fieldMustBeSelected": "Field must be selected",
"field": "Field",
"select-1": "-- Select --",
"ruleOptions": "Rule Options",
"ok": "OK",
"allRulesMustMatch": "All rules must match",
"atLeastOneRuleMustMatch": "At least one rule must match",
"noRuleMayMatch": "No rule may match",
"equalTo": "Equal to",
"valueInColumnColNameIsEqualToValue": "Value in column \"{{colName}}\" is equal to \"{{value}}\"",
"matchWithSqlLike": "Match (with SQL LIKE)",
"valueInColumnColNameMatchesWithSqlLike": "Value in column \"{{colName}}\" matches (with SQL LIKE) \"{{value}}\"",
"matchWithRegularExpressions": "Match (with regular expressions)",
"valueInColumnColNameMatchesWithRegular": "Value in column \"{{colName}}\" matches (with regular expressions) \"{{value}}\"",
"alphabeticallyBefore": "Alphabetically before",
"valueInColumnColNameIsAlphabetically": "Value in column \"{{colName}}\" is alphabetically before \"{{value}}\"",
"alphabeticallyBeforeOrEqualTo": "Alphabetically before or equal to",
"valueInColumnColNameIsAlphabetically-1": "Value in column \"{{colName}}\" is alphabetically before or equal to \"{{value}}\"",
"alphabeticallyAfter": "Alphabetically after",
"valueInColumnColNameIsAlphabetically-2": "Value in column \"{{colName}}\" is alphabetically after \"{{value}}\"",
"alphabeticallyAfterOrEqualTo": "Alphabetically after or equal to",
"valueInColumnColNameIsAlphabetically-3": "Value in column \"{{colName}}\" is alphabetically after or equal to \"{{value}}\"",
"valueInColumnColNameIsEqualToValue-1": "Value in column \"{{colName}}\" is equal to {{value}}",
"lessThan": "Less than",
"valueInColumnColNameIsLessThanValue": "Value in column \"{{colName}}\" is less than {{value}}",
"lessThanOrEqualTo": "Less than or equal to",
"valueInColumnColNameIsLessThanOrEqualTo": "Value in column \"{{colName}}\" is less than or equal to {{value}}",
"greaterThan": "Greater than",
"valueInColumnColNameIsGreaterThanValue": "Value in column \"{{colName}}\" is greater than {{value}}",
"greaterThanOrEqualTo": "Greater than or equal to",
"valueInColumnColNameIsGreaterThanOrEqual": "Value in column \"{{colName}}\" is greater than or equal to {{value}}",
"dateInColumnColName": "Date in column \"{{colName}}\" ",
"on": "On",
"dateInColumnColNameIsValue": "Date in column \"{{colName}}\" is {{value}}",
"before": "Before",
"dateInColumnColNameIsBeforeValue": "Date in column \"{{colName}}\" is before {{value}}",
"beforeOrOn": "Before or on",
"dateInColumnColNameIsBeforeOrOnValue": "Date in column \"{{colName}}\" is before or on {{value}}",
"after": "After",
"dateInColumnColNameIsAfterValue": "Date in column \"{{colName}}\" is after {{value}}",
"afterOrOn": "After or on",
"dateInColumnColNameIsAfterOrOnValue": "Date in column \"{{colName}}\" is after or on {{value}}",
"onXthDayBeforeafterCurrentDate": "On x-th day before/after current date",
"beforeXthDayBeforeafterCurrentDate": "Before x-th day before/after current date",
"beforeOrOnXthDayBeforeafterCurrentDate": "Before or on x-th day before/after current date",
"afterXthDayBeforeafterCurrentDate": "After x-th day before/after current date",
"afterOrOnXthDayBeforeafterCurrentDate": "After or on x-th day before/after current date",
"isSelected": "Is selected",
"valueInColumnColNameIsSelected": "Value in column \"{{colName}}\" is selected",
"isNotSelected": "Is not selected",
"valueInColumnColNameIsNotSelected": "Value in column \"{{colName}}\" is not selected",
"keyEqualTo": "Key equal to",
"theSelectedKeyInColumnColNameIsEqualTo": "The selected key in column \"{{colName}}\" is equal to \"{{value}}\"",
"keyMatchWithSqlLike": "Key match (with SQL LIKE)",
"theSelectedKeyInColumnColNameMatchesWith": "The selected key in column \"{{colName}}\" matches (with SQL LIKE) \"{{value}}\"",
"keyMatchWithRegularExpressions": "Key match (with regular expressions)",
"theSelectedKeyInColumnColNameMatchesWith-1": "The selected key in column \"{{colName}}\" matches (with regular expressions) \"{{value}}\"",
"keyAlphabeticallyBefore": "Key alphabetically before",
"theSelectedKeyInColumnColNameIs": "The selected key in column \"{{colName}}\" is alphabetically before \"{{value}}\"",
"keyAlphabeticallyBeforeOrEqualTo": "Key alphabetically before or equal to",
"theSelectedKeyInColumnColNameIs-1": "The selected key in column \"{{colName}}\" is alphabetically before or equal to \"{{value}}\"",
"keyAlphabeticallyAfter": "Key alphabetically after",
"theSelectedKeyInColumnColNameIs-2": "The selected key in column \"{{colName}}\" is alphabetically after \"{{value}}\"",
"keyAlphabeticallyAfterOrEqualTo": "Key alphabetically after or equal to",
"theSelectedKeyInColumnColNameIs-3": "The selected key in column \"{{colName}}\" is alphabetically after or equal to \"{{value}}\"",
"value": "Value",
"valueMustNotBeEmpty": "Value must not be empty",
"valueMustBeANumber": "Value must be a number",
"numberOfDays": "Number of days",
"beforeAfter": "Before/After",
"beforeCurrentDate": "Before current date",
"afterCurrentDate": "After current date",
"numberOfDaysMustNotBeEmpty": "Number of days must not be empty",
"numberOfDaysMustBeANumber": "Number of days must be a number",
"emailAddress-1": "Email address",
"signupCountry": "Signup country",
"signUpDate": "Sign up date",
"anotherSubscriptionWithTheSameEmail": "Another subscription with the same email already exists.",
"susbscriptionSaved": "Susbscription saved",
"itSeemsThatAnotherSubscriptionWithThe": "It seems that another subscription with the same email has been created in the meantime. Refresh your page to start anew. Please note that your changes will be lost.",
"notSelected": "Not selected",
"areYouSureYouWantToDeleteSubscriptionFor": "Are you sure you want to delete subscription for \"{{email}}\"?",
"deletingSubscription": "Deleting subscription ...",
"subscriptionDeleted": "Subscription deleted",
"editSubscription": "Edit Subscription",
"createSubscription": "Create Subscription",
"timezone": "Timezone",
"subscriptionStatus": "Subscription status",
"testUser?": "Test user?",
"ifCheckedThenThisSubscriptionCanBeUsed": "If checked then this subscription can be used for previewing campaign messages",
"blacklisted": "Blacklisted",
"allSubscriptions": "All subscriptions",
"subscriptionForm": "Subscription Form",
"exportAsCsv": "Export as CSV",
"addSubscriber": "Add Subscriber",
"subscribed": "Subscribed",
"unubscribed": "Unubscribed",
"bounced": "Bounced",
"complained": "Complained",
"parentNamespaceMustBeSelected": "Parent Namespace must be selected",
"namespaceSaved": "Namespace saved",
"thereHasBeenALoopDetectedInTheAssignment": "There has been a loop detected in the assignment of the parent namespace. This is most likely because someone else has changed the parent of some namespace in the meantime. Refresh your page to start anew. Please note that your changes will be lost.",
"itSeemsThatTheParentNamespaceHasBeen": "It seems that the parent namespace has been deleted in the meantime. Refresh your page to start anew. Please note that your changes will be lost.",
"deletingNamespace": "Deleting namespace ...",
"namespaceDeleted": "Namespace deleted",
"editNamespace": "Edit Namespace",
"createNamespace": "Create Namespace",
"parentNamespace": "Parent Namespace",
"namespaces": "Namespaces",
"namespaceName": "Namespace \"{{name}}\"",
"reportTemplateMustBeSelected": "Report template must be selected",
"exactlyOneItemHasToBeSelected": "Exactly one item has to be selected",
"atLeastCountItemsHaveToBeSelected": "At least {{ count }} item(s) have to be selected",
"atMostCountItemsCanToBeSelected": "At most {{ count }} item(s) can to be selected",
"reportParametersAreNotSelectedWaitFor": "Report parameters are not selected. Wait for them to get displayed and then fill them in.",
"reportSaved": "Report saved",
"unknownFieldTypeType": "Unknown field type \"{{type}}\"",
"deletingReport": "Deleting report ...",
"reportDeleted": "Report deleted",
"editReport": "Edit Report",
"createReport": "Create Report",
"reportTemplate-1": "Report Template",
"reportParameters": "Report parameters",
"loadingReportTemplate": "Loading report template...",
"processing-1": "Processing",
"view": "View",
"refreshReport": "Refresh report",
"reportGenerationFailed": "Report generation failed",
"regenerateReport": "Regenerate report",
"viewConsoleOutput": "View console output",
"reportTemplates": "Report Templates",
"reports": "Reports",
"outputForReportName": "Output for report {{name}}",
"loadingReportOutput": "Loading report output ...",
"reportName": "Report {{name}}",
"reportNotGenerated": "Report not generated",
"loadingReport": "Loading report ...",
"reportName-1": "Report \"{{name}}\"",
"output": "Output",
"templateName": "Template \"{{name}}\"",
"mimeTypeMustBeSelected": "MIME Type must be selected",
"syntaxErrorInTheUserFieldsSpecification": "Syntax error in the user fields specification",
"reportTemplateSaved": "Report template saved",
"deletingReportTemplate": "Deleting report template ...",
"reportTemplateDeleted": "Report template deleted",
"editReportTemplate": "Edit Report Template",
"createReportTemplate": "Create Report Template",
"csv": "CSV",
"userSelectableFields": "User selectable fields",
"jsonSpecificationOfUserSelectableFields": "JSON specification of user selectable fields.",
"dataProcessingCode": "Data processing code",
"renderingTemplate": "Rendering template",
"writeTheBodyOfTheJavaScriptFunctionWith": "Write the body of the JavaScript function with signature <1>function(inputs, callback)</1> that returns an object to be rendered by the Handlebars template below.",
"useHtmlWithHandlebarsSyntaxSee": "Use HTML with Handlebars syntax. See documentation <1>here</1>.",
"blank": "Blank",
"allSubscribers": "All Subscribers",
"groupedSubscribers": "Grouped Subscribers",
"exportListAsCsv": "Export List as CSV",
"current": "(current)",
"toggleNavigation": "Toggle navigation",
"administration": "Administration",
"users": "Users",
"globalSettings": "Global Settings",
"sendConfigurations": "Send configurations",
"logOut": "Log out",
"home": "Home",
"sourceOnGitHub": "Source on GitHub",
"mailerTypeMustBeSelected": "Mailer type must be selected",
"verpHostnameMustNotBeEmpty": "VERP hostname must not be empty",
"sendConfigurationSaved": "Send configuration saved",
"deletingSendConfiguration": "Deleting send configuration ...",
"sendConfigurationDeleted": "Send configuration deleted",
"editSendConfiguration": "Edit Send Configuration",
"createSendConfiguration": "Create Send Configuration",
"emailHeader": "Email Header",
"defaultFromEmail": "Default \"from\" email",
"overridable": "Overridable",
"defaultFromName": "Default \"from\" name",
"defaultReplytoEmail": "Default \"reply-to\" email",
"subject": "Subject",
"xMailer": "X-Mailer",
"verpBounceHandling": "VERP Bounce Handling",
"verpEnabled": "verpEnabled",
"serverHostname": "Server hostname",
"theVerpServerHostnameEgBouncesexamplecom": "The VERP server hostname, eg. bounces.example.com",
"verpBounceHandlingServerHostnameThis": "VERP bounce handling server hostname. This hostname is used in the SMTP envelope FROM address and the MX DNS records should point to this server",
"mailtrainIsAbleToUseVerpBasedRoutingTo": "<0>Mailtrain is able to use VERP based routing to detect bounces. In this case the message is sent to the recipient using a custom VERP address as the return path of the message. If the message is not accepted a bounce email is sent to this special VERP address and thus a bounce is detected.</0>",
"toGetVerpWorkingYouNeedToSetUpADnsMx": "<0>To get VERP working you need to set up a DNS MX record that points to your Mailtrain hostname. You must also ensure that Mailtrain VERP interface is available from port 25 of your server (port 25 usually requires root user privileges). This way if anyone tries to send email to someuser@verp-hostname then the email should end up to this server.</0>",
"verpUsuallyOnlyWorksIfYouAreUsingYourOwn": "<0>VERP usually only works if you are using your own SMTP server. Regular relay services (SES, SparkPost, Gmail etc.) tend to remove the VERP address from the message.</0>",
"verpBounceHandlingServerIsNotEnabled": "<0>VERP bounce handling server is not enabled. Modify your server configuration file and restart server to enable it.</0>",
"sendConfigurations-1": "Send Configurations",
"labelMustNotBeEmpty": "{{label}} must not be empty",
"labelMustBeANumber": "{{label}} must be a number",
"genericSmtp": "Generic SMTP",
"zoneMta": "Zone MTA",
"amazonSes": "Amazon SES",
"doNotUseEncryption": "Do not use encryption",
"useTls UsuallySelectedForPort465": "Use TLS usually selected for port 465",
"useStarttls UsuallySelectedForPort587": "Use STARTTLS usually selected for port 587 and 25",
"useast1": "US-EAST-1",
"uswest2": "US-WEST-2",
"euwest1": "EU-WEST-1",
"mailerSettings": "Mailer Settings",
"mailerType": "Mailer type",
"hostname": "Hostname",
"hostnameEgSmtpexamplecom": "Hostname, eg. smtp.example.com",
"port": "Port",
"portEg465AutodetectedIfLeftBlank": "Port, eg. 465. Autodetected if left blank",
"encryption": "Encryption",
"enableSmtpAuthentication": "Enable SMTP authentication",
"usernameEgMyaccount@examplecom": "Username, eg. myaccount@example.com",
"advancedMailerSettings": "Advanced Mailer Settings",
"logSmtpTransactions": "Log SMTP transactions",
"allowSelfsignedCertificates": "Allow self-signed certificates",
"maxConnections": "Max connections",
"theCountOfMaxConnectionsEg10": "The count of max connections, eg. 10",
"theCountOfMaximumSimultaneousConnections": "The count of maximum simultaneous connections to make against the SMTP server (defaults to 5). This limit is per sending process.",
"maxMessages": "Max messages",
"theCountOfMaxMessagesEg100": "The count of max messages, eg. 100",
"theNumberOfMessagesToSendThroughASingle": "The number of messages to send through a single connection before the connection is closed and reopened (defaults to 100)",
"throttling": "Throttling",
"messagesPerHourEg1000": "Messages per hour eg. 1000",
"maximumNumberOfMessagesToSendInAnHour": "Maximum number of messages to send in an hour. Leave empty or zero for no throttling. If your provider uses a different speed limit (messages/minute or messages/second) then convert this limit into messages/hour (1m/s => 3600m/h). This limit is per sending process.",
"dkimSigning": "DKIM Signing",
"zoneMtaDkimApiKey": "ZoneMTA DKIM API key",
"secretValueKnownToZoneMtaForRequesting": "Secret value known to ZoneMTA for requesting DKIM key information. If this value was generated by the Mailtrain installation script then you can keep it as it is.",
"dkimDomain": "DKIM domain",
"leaveBlankToUseTheSenderEmailAddress": "Leave blank to use the sender email address domain.",
"dkimKeySelector": "DKIM key selector",
"signingIsDisabledWithoutAValidSelector": "Signing is disabled without a valid selector value.",
"dkimPrivateKey": "DKIM private key",
"beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"",
"signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.",
"accessKey": "Access key",
"awsAccessKeyId": "AWS access key ID",
"awsSecretAccessKey": "AWS secret access key",
"region": "Region",
"ifYouAreUsingZoneMtaThenMailtrainCan": "<0>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages. Other services usually provide their own means to DKIM sign your messages.</0>",
"doNotUseSensitiveKeysHereThePrivateKeyIs": "<0>Do not use sensitive keys here. The private key is not encrypted in the database.</0>",
"globalSettingsSaved": "Global settings saved",
"adminEmail": "Admin email",
"thisEmailIsUsedAsTheMainContactAndAsA": "This email is used as the main contact and as a default email address if no email address is specified in list settings.",
"defaultHomepageUrl": "Default homepage URL",
"thisUrlWillBeUsedInListSubscriptionForms": "This URL will be used in list subscription forms if no homepage is specified in list settings.",
"trackingId": "Tracking ID",
"uaxxxxxxx": "UA-XXXXX-XX",
"enterGoogleAnalyticsTrackingCode": "Enter Google Analytics tracking code",
"frontpageShoutOut": "Frontpage shout out",
"htmlCodeShownInTheFrontPageHeaderSection": "HTML code shown in the front page header section",
"gpgSigning": "GPG Signing",
"privateKeyPassphrase": "Private key passphrase",
"passphraseForTheKeyIfSet": "Passphrase for the key if set",
"onlyFillThisIfYourPrivateKeyIsEncrypted": "Only fill this if your private key is encrypted with a passphrase",
"gpgPrivateKey": "GPG private key",
"beginsWithBeginPgpPrivateKeyBlock": "Begins with \\'-----BEGIN PGP PRIVATE KEY BLOCK-----\\'",
"thisValueIsOptionalIfYouDoNotProvideA": "This value is optional. If you do not provide a private key GPG encrypted messages are sent without signing.",
"onlyMessagesThatAreEncryptedCanBeSigned": "<0>Only messages that are encrypted can be signed. Subsribers who have not set up a GPG public key in their profile receive normal email messages. Users with GPG key set receive encrypted messages and if you have signing key also set, the messages are signed with this key.</0>",
"doNotUseSensitiveKeysHereThePrivateKey": "<0>Do not use sensitive keys here. The private key and passphrase are not encrypted in the database.</0>",
"userMustNotBeEmpty": "User must not be empty",
"roleMustBeSelected": "Role must be selected",
"role": "Role",
"addUser": "Add User",
"user": "User",
"existingUsers": "Existing Users",
"sharesForUserUsername": "Shares for user \"{{username}}\"",
"templateSaved": "Template saved",
"deletingTemplate": "Deleting template ...",
"templateDeleted": "Template deleted",
"editTemplate": "Edit Template",
"createTemplate": "Create Template",
"saveAndEditTemplate": "Save and edit template",
"mosaicoTemplates": "Mosaico Templates",
"sendConfigurationHasToBeSelected": "Send configuration has to be selected.",
"listHasToBeSelected": "List has to be selected.",
"mosaico": "Mosaico",
"templateContentHtml": "Template content (HTML)",
"mosaicoTemplateDesigner": "Mosaico Template Designer",
"mosaicoTemplateMustBeSelected": "Mosaico template must be selected",
"mosaicoWithPredefinedTemplates": "Mosaico with predefined templates",
"mosaicoTemplate-1": "Mosaico Template",
"grapesJs": "GrapesJS",
"grapesJsTemplateDesigner": "GrapesJS Template Designer",
"ckEditor4": "CKEditor 4",
"ckEditor4TemplateDesigner": "CKEditor 4 Template Designer",
"ckEditor5": "CKEditor 5",
"codeEditor": "Code Editor",
"codeEditorTemplateDesigner": "Code Editor Template Designer",
"mergeTagReference": "Merge tag reference",
"templateContentPlainText": "Template content (plain text)",
"mergeTagsAreTagsThatAreReplacedBefore": "<0>Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <1>[TAG_NAME]</1> or <3>[TAG_NAME/fallback]</3> where <5>fallback</5> is an optional text value used when <7>TAG_NAME</7> is empty.</0>",
"youCanUseAnyOfTheStandardMergeTagsBelow": "<0>You can use any of the standard merge tags below. In addition to that every custom field has its own merge tag. Check the fields of the list you are going to send to.</0>",
"urlThatPointsToTheUnsubscribePage": "URL that points to the unsubscribe page",
"urlThatPointsToThePreferencesPageOfThe": "URL that points to the preferences page of the subscriber",
"urlToPreviewTheMessageInABrowser": "URL to preview the message in a browser",
"recipientNameAsItAppearsInEmailsToHeader": "Recipient name as it appears in email's 'To' header",
"uniqueIdThatIdentifiesTheRecipient": "Unique ID that identifies the recipient",
"uniqueIdThatIdentifiesTheListUsedForThis": "Unique ID that identifies the list used for this campaign",
"uniqueIdThatIdentifiesCurrentCampaign": "Unique ID that identifies current campaign",
"forRssCampaignsTheFollowingFurtherTags": "<0>For RSS campaigns, the following further tags can be used.</0>",
"rssEntryTitle": "RSS entry title",
"rssEntryDate": "RSS entry date",
"rssEntryLink": "RSS entry link",
"contentOfAnRssEntry": "Content of an RSS entry",
"rssEntrySummary": "RSS entry summary",
"rssEntryImageUrl": "RSS entry image URL",
"toExtractTheTextFromHtmlClickHerePlease": "To extract the text from HTML click <1>here</1>. Please note that your existing plaintext in the field above will be overwritten. This feature uses the <3>Premailer API</3>, a third party service. Their Terms of Service and Privacy Policy apply.",
"mosaicoTemplateSaved": "Mosaico template saved",
"deletingMosaicoTemplate": "Deleting Mosaico template ...",
"mosaicoTemplateDeleted": "Mosaico template deleted",
"editMosaicoTemplate": "Edit Mosaico Template",
"createMosaicoTemplate": "Create Mosaico Template",
"blockThumbnails": "Block thumbnails",
"versafixOne": "Versafix One",
"templateContent": "Template content",
"mosaicoTemplateName": "Mosaico Template \"{{name}}\"",
"theseFilesArePubliclyAvailableViaHttpSo-1": "These files are publicly available via HTTP so that they can be linked to from the Mosaico template.",
"theseFilesWillBeUsedByMosaicoToSearchFor": "These files will be used by Mosaico to search for block thumbnails (the \"edres\" directory). Place here one file per block type that you have defined in the Mosaico template. Each file must have the same name as the block id. The file will be used as the thumbnail of the corresponding block.",
"theUserNameAlreadyExistsInTheSystem": "The user name already exists in the system.",
"userSaved": "User saved",
"theUsernameIsAlreadyAssignedToAnother": "The username is already assigned to another user.",
"theEmailIsAlreadyAssignedToAnotherUser-1": "The email is already assigned to another user.",
"deletingUser": "Deleting user ...",
"userDeleted": "User deleted",
"editUser": "Edit User",
"createUser": "Create User",
"userName": "User Name",
"repeatPassword": "Repeat Password",
"deleteUser": "Delete User",
"userName-1": "User \"{{name}}\"",
"shares": "Shares",
"subscriptionconfirmed": "subscription.confirmed",
"listEmailAddressAlreadyRegistered": "{{list}}: Email Address Already Registered",
"listPleaseConfirmEmailChangeIn": "{{list}}: Please Confirm Email Change in Subscription",
"pleaseConfirmSubscription": "Please Confirm Subscription",
"listPleaseConfirmUnsubscription": "{{list}}: Please Confirm Unsubscription",
"listUnsubscriptionConfirmed": "{{list}}: Unsubscription Confirmed",
"invalidEmailAddressEmailMxRecordNotFound": "Invalid email address \"{{email}}\": MX record not found for domain",
"invalidEmailAddressEmailAddressDomainNot": "Invalid email address \"{{email}}\": Address domain not found",
"invalidEmailAddressEmailAddressDomain": "Invalid email address \"{{email}}\": Address domain name is required",
"invalidEmailGeneric": "invalidEmailGeneric",
"mailerPasswordChangeRequest": "Mailer password change request",
"emailAddressChanged": "Email address changed",
"emailAddressNotSet": "Email address not set",
"nothingSeemsToBeChanged": "Nothing seems to be changed",
"anEmailWithFurtherInstructionsHasBeen": "An email with further instructions has been sent to the provided address",
"foundAddedMessagesNewCampaignMessages": "Found {{addedMessages}} new campaign messages from feed {{campaignId}}",
"foundNothingNewFromTheFeed": "Found nothing new from the feed",
"missingEmail": "Missing email",
"thePasswordMustBeAtLeastMinLength": "The password must be at least {{ minLength }} characters long",
"thePasswordMustBeFewerThanMaxLength": "The password must be fewer than {{ maxLength }} characters",
"thePasswordMayNotContainSequencesOfThree": "The password may not contain sequences of three or more repeated characters",
"thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter",
"thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter",
"thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number",
"thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character"
}

2
locales/es/common.json Normal file
View file

@ -0,0 +1,2 @@
{
}

View file

@ -142,7 +142,7 @@ function createApp(appType) {
app.disable('x-powered-by');
app.use(compression());
app.use(favicon(path.join(__dirname, 'client', 'static', 'favicon.ico')));
app.use(favicon(path.join(__dirname, '..', 'client', 'static', 'favicon.ico')));
app.use(logger(config.www.log, {
stream: {
@ -167,7 +167,7 @@ function createApp(appType) {
query: {
name: 'language'
},
default: 'en_US'
default: config.defaultLanguage
}));
app.use(flash());
@ -191,9 +191,9 @@ function createApp(appType) {
app.use(passport.tryAuthByRestrictedAccessToken);
}
useWith404Fallback('/static', express.static(path.join(__dirname, 'client', 'static')));
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, 'client', 'dist')));
useWith404Fallback('/locales', express.static(path.join(__dirname, 'client', 'locales')));
useWith404Fallback('/static', express.static(path.join(__dirname, '..', 'client', 'static')));
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, '..', 'client', 'dist')));
useWith404Fallback('/locales', express.static(path.join(__dirname, '..', 'client', 'locales')));
// Make sure flash messages are available

View file

@ -24,7 +24,12 @@ editors:
- codeeditor
# Default language to use
language: en
defaultLanguage: en_US
# Enabled languages
enabledLanguages:
- en_US
- es
# Inject custom scripts in subscription/layout.mjml.hbs
# customSubscriptionScripts: [/custom/hello-world.js]

View file

@ -3,6 +3,7 @@
const config = require('config');
const log = require('./lib/log');
const appBuilder = require('./app-builder');
const translate = require('./lib/translate');
const http = require('http');
const triggers = require('./services/triggers');
const importer = require('./lib/importer');

View file

@ -12,7 +12,8 @@ async function getAnonymousConfig(context, appType) {
authMethod: passport.authMethod,
isAuthMethodLocal: passport.isAuthMethodLocal,
externalPasswordResetLink: config.ldap.passwordresetlink,
language: config.language || 'en',
defaultLanguage: config.defaultLanguage,
enabledLanguages: config.enabledLanguages,
isAuthenticated: !!context.user,
trustedUrlBase: urls.getTrustedUrlBase(),
trustedUrlBaseDir: urls.getTrustedUrlBaseDir(),

View file

@ -2,7 +2,7 @@
const config = require('config');
const knex = require('server/lib/knex')({
const knex = require('knex')({
client: 'mysql2',
connection: config.mysql,
migrations: {

View file

@ -17,8 +17,6 @@ const htmlToText = require('html-to-text');
const bluebird = require('bluebird');
const _ = require('./translate')._;
const transports = new Map();
async function getOrCreateMailer(sendConfigurationId) {

View file

@ -1,6 +1,6 @@
'use strict';
const nodeify = require('server/lib/nodeify');
const nodeify = require('nodeify');
module.exports.nodeifyPromise = nodeify;

View file

@ -2,10 +2,9 @@
const config = require('config');
const log = require('./log');
const _ = require('./translate')._;
const util = require('util');
const passport = require('server/lib/passport');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const csrf = require('csurf');

View file

@ -26,7 +26,7 @@ async function sendSubscriptionConfirmed(lang, list, email, subscription) {
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
};
await _sendMail(list, email, 'subscription_confirmed', lang, tMark('subscription.confirmed'), relativeUrls, subscription);
await _sendMail(list, email, 'subscription_confirmed', lang, tMark('subscriptionconfirmed'), relativeUrls, subscription);
}
async function sendAlreadySubscribed(lang, list, email, subscription) {
@ -34,35 +34,35 @@ async function sendAlreadySubscribed(lang, list, email, subscription) {
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
};
await _sendMail(list, email, 'already_subscribed', lang, tMark('subscription.alreadyRegistered'), relativeUrls, subscription);
await _sendMail(list, email, 'already_subscribed', lang, tMark('listEmailAddressAlreadyRegistered'), relativeUrls, subscription);
}
async function sendConfirmAddressChange(lang, list, email, cid, subscription) {
const relativeUrls = {
confirmUrl: '/subscription/confirm/change-address/' + cid
};
await _sendMail(list, email, 'confirm_address_change', lang, tMark('subscription.confirmEmailChange'), relativeUrls, subscription);
await _sendMail(list, email, 'confirm_address_change', lang, tMark('listPleaseConfirmEmailChangeIn'), relativeUrls, subscription);
}
async function sendConfirmSubscription(lang, list, email, cid, subscription) {
const relativeUrls = {
confirmUrl: '/subscription/confirm/subscribe/' + cid
};
await _sendMail(list, email, 'confirm_subscription', lang, tMark('subscription.confirmSubscription'), relativeUrls, subscription);
await _sendMail(list, email, 'confirm_subscription', lang, tMark('pleaseConfirmSubscription'), relativeUrls, subscription);
}
async function sendConfirmUnsubscription(lang, list, email, cid, subscription) {
const relativeUrls = {
confirmUrl: '/subscription/confirm/unsubscribe/' + cid
};
await _sendMail(list, email, 'confirm_unsubscription', lang, tMark('subscription.confirmUnsubscription'), relativeUrls, subscription);
await _sendMail(list, email, 'confirm_unsubscription', lang, tMark('listPleaseConfirmUnsubscription'), relativeUrls, subscription);
}
async function sendUnsubscriptionConfirmed(lang, list, email, subscription) {
const relativeUrls = {
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
};
await _sendMail(list, email, 'unsubscription_confirmed', lang, tMark('subscription.unsubscriptionConfirmed'), relativeUrls, subscription);
await _sendMail(list, email, 'unsubscription_confirmed', lang, tMark('listUnsubscriptionConfirmed'), relativeUrls, subscription);
}
function getDisplayName(flds, subscription) {

Some files were not shown because too many files have changed in this diff Show more