Merge branch 'development' of github.com:Mailtrain-org/mailtrain into development

This commit is contained in:
Tomas Bures 2019-12-07 08:08:54 +01:00
commit c44ed4f7fa
33 changed files with 1492 additions and 242 deletions

View file

@ -1 +1,9 @@
node_modules
node_modules/
docs/
Dockerfile
*.md
.git
.gitignore
.gitmodules
docker-compose.yml
docker-compose-local.yml

View file

@ -1,8 +1,12 @@
FROM node:10.14-alpine
# Mutistaged Node.js Build
FROM node:10-alpine as builder
RUN apk add --update pwgen netcat-openbsd python make gcc git g++ bash imagemagick
# Install system dependencies
RUN set -ex; \
apk add --update --no-cache \
make gcc g++ git python
# First install dependencies
# Copy package.json dependencies
COPY server/package.json /app/server/package.json
COPY server/package-lock.json /app/server/package-lock.json
COPY client/package.json /app/client/package.json
@ -12,15 +16,32 @@ COPY shared/package-lock.json /app/shared/package-lock.json
COPY zone-mta/package.json /app/zone-mta/package.json
COPY zone-mta/package-lock.json /app/zone-mta/package-lock.json
WORKDIR /app/
RUN for idx in client shared server zone-mta; do (cd $idx && npm install); done
# Install dependencies in each directory
RUN cd /app/client && npm install
RUN cd /app/shared && npm install --production
RUN cd /app/server && npm install --production
RUN cd /app/zone-mta && npm install --production
# Later, copy the app files. That improves development speed as buiding the Docker image will not have
# to download and install all the NPM dependencies every time there's a change in the source code
COPY . /app
RUN cd client && npm run build
RUN set -ex; \
cd /app/client && \
npm run build && \
rm -rf node_modules
# Final Image
FROM node:10-alpine
WORKDIR /app/
# Install system dependencies
RUN set -ex; \
apk add --update --no-cache \
pwgen netcat-openbsd bash imagemagick
COPY --from=builder /app/ /app/
EXPOSE 3000 3003 3004
ENTRYPOINT ["bash", "/app/docker-entrypoint.sh"]

View file

@ -193,17 +193,35 @@ These are the steps to start Mailtrain via docker-compose:
docker-compose up
```
You can specify Mailtrain's URL bases via the `MAILTRAIN_SETTINGS` environment variable as follows. The `--withProxy` parameter is to be used when Mailtrain is put behind a reverse proxy.
```
MAILTRAIN_SETTINGS="--trustedUrlBase https://mailtrain.example.com --sandboxUrlBase https://sbox.mailtrain.example.com --publicUrlBase https://lists.example.com --withProxy" docker-compose up
```
3. Open the trusted endpoint http://localhost:3000
4. Authenticate as `admin`:`test`
The instructions above use an automatically built Docker image on DockerHub (https://hub.docker.com/r/mailtrain/mailtrain). If you want to build the Docker image yourself (e.g. when doing development), use the `docker-compose-local.yml` located in the project's root directory.
### Docker Environment Variables
| Parameter | Description |
| --------- | ----------- |
| URL_BASE_TRUSTED | sets the trusted url of the instance (default: http://localhost:3000) |
| URL_BASE_SANDBOX | sets the sandbox url of the instance (default: http://localhost:3003) |
| URL_BASE_PUBLIC | sets the public url of the instance (default: http://localhost:3004) |
| WITH_PROXY | use if Mailtrain is behind an http reverse proxy |
| MONGO_HOST | sets mongo host (default: mongo) |
| REDIS_HOST | sets redis host (default: redis) |
| MYSQL_HOST | sets mysql host (default: mysql) |
| MYSQL_DATABASE | sets mysql database (default: mailtrain) |
| MYSQL_USER | sets mysql user (default: mailtrain) |
| MYSQL_PASSWORD | sets mysql password (default: mailtrain) |
| WITH_LDAP | use if you want to enable LDAP authentication |
| LDAP_HOST | LDAP Host for authentication (default: ldap) |
| LDAP_PORT | LDAP port (default: 389) |
| LDAP_SECURE | use if you want to use LDAP with ldaps protocol |
| LDAP_BIND_USER | User for LDAP connexion |
| LDAP_BIND_PASS | Password for LDAP connexion |
| LDAP_FILTER | LDAP filter |
| LDAP_BASEDN | LDAP base DN |
| LDAP_UIDTAG | LDAP UID tag (e.g. uid/cn/username) |
## License

234
client/package-lock.json generated
View file

@ -2013,6 +2013,12 @@
}
}
},
"ansi-colors": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
"integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
"dev": true
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@ -2121,6 +2127,21 @@
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
"dev": true
},
"array-union": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
"integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
"dev": true,
"requires": {
"array-uniq": "^1.0.1"
}
},
"array-uniq": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
"integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
"dev": true
},
"array-unique": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
@ -3285,6 +3306,143 @@
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
},
"copy-webpack-plugin": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.0.5.tgz",
"integrity": "sha512-7N68eIoQTyudAuxkfPT7HzGoQ+TsmArN/I3HFwG+lVE3FNzqvZKIiaxtYh4o3BIznioxUvx9j26+Rtsc9htQUQ==",
"dev": true,
"requires": {
"cacache": "^12.0.3",
"find-cache-dir": "^2.1.0",
"glob-parent": "^3.1.0",
"globby": "^7.1.1",
"is-glob": "^4.0.1",
"loader-utils": "^1.2.3",
"minimatch": "^3.0.4",
"normalize-path": "^3.0.0",
"p-limit": "^2.2.1",
"schema-utils": "^1.0.0",
"serialize-javascript": "^2.1.0",
"webpack-log": "^2.0.0"
},
"dependencies": {
"cacache": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz",
"integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"chownr": "^1.1.1",
"figgy-pudding": "^3.5.1",
"glob": "^7.1.4",
"graceful-fs": "^4.1.15",
"infer-owner": "^1.0.3",
"lru-cache": "^5.1.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.3",
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"dev": true,
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
},
"dependencies": {
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"dev": true,
"requires": {
"is-extglob": "^2.1.0"
}
}
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"requires": {
"yallist": "^3.0.2"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"p-limit": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
"integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"serialize-javascript": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.0.tgz",
"integrity": "sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ==",
"dev": true
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
}
}
},
"core-js": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
@ -3645,6 +3803,32 @@
"randombytes": "^2.0.0"
}
},
"dir-glob": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz",
"integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==",
"dev": true,
"requires": {
"path-type": "^3.0.0"
},
"dependencies": {
"path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
"integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
"dev": true,
"requires": {
"pify": "^3.0.0"
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
}
}
},
"dnd-core": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-7.7.0.tgz",
@ -5262,6 +5446,34 @@
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
},
"globby": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
"integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
"dev": true,
"requires": {
"array-union": "^1.0.1",
"dir-glob": "^2.0.0",
"glob": "^7.1.2",
"ignore": "^3.3.5",
"pify": "^3.0.0",
"slash": "^1.0.0"
},
"dependencies": {
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
},
"slash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
"dev": true
}
}
},
"globule": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
@ -5614,6 +5826,12 @@
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true
},
"ignore": {
"version": "3.3.10",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
"integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
"dev": true
},
"ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
@ -5673,6 +5891,12 @@
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
"dev": true
},
"infer-owner": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
"integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -12169,6 +12393,16 @@
}
}
},
"webpack-log": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
"integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==",
"dev": true,
"requires": {
"ansi-colors": "^3.0.0",
"uuid": "^3.3.2"
}
},
"webpack-sources": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",

View file

@ -72,6 +72,7 @@
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.6",
"clean-css-cli": "^4.2.1",
"copy-webpack-plugin": "^5.0.5",
"css-loader": "^2.1.0",
"file-loader": "^3.0.1",
"node-sass": "^4.12.0",

View file

@ -292,8 +292,8 @@ export default class API extends Component {
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/blacklist/add?access_token={accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com&amp;'</pre>
<pre>curl -XPOST '{getUrl(`api/blacklist/add?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com'</pre>
<h4>POST /api/blacklist/delete {t('deleteEmailFromBlacklist')}</h4>
@ -320,7 +320,7 @@ export default class API extends Component {
</p>
<pre>curl -XPOST '{getUrl(`api/blacklist/delete?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com&amp;'</pre>
--data 'EMAIL=test@example.com'</pre>
<h4>GET /api/lists/:email {t('getTheListsAUserHasSubscribedTo')}</h4>

View file

@ -401,7 +401,7 @@ export default class CUD extends Component {
}
if (!state.getIn(['data_sourceCustom_tag_language', 'value'])) {
state.setIn(['data_sourceCustom_tag_language', 'error'], t('Tag language must be selected'));
state.setIn(['data_sourceCustom_tag_language', 'error'], t('tagLanguageMustBeSelected'));
}
if (customTemplateTypeKey) {
@ -732,7 +732,7 @@ export default class CUD extends Component {
templateEdit = <div>
<Dropdown id="data_sourceCustom_type" label={t('type')} options={this.customTemplateTypeOptions}/>
<Dropdown id="data_sourceCustom_tag_language" label={t('Tag language')} options={this.customTemplateTagLanguageOptions} disabled={isEdit}/>
<Dropdown id="data_sourceCustom_tag_language" label={t('tagLanguage')} options={this.customTemplateTagLanguageOptions} disabled={isEdit}/>
{customTemplateTypeForm}
</div>;

View file

@ -131,7 +131,7 @@ export default class CustomContent extends Component {
const t = this.props.t;
if (!state.getIn(['data_sourceCustom_tag_language', 'value'])) {
state.setIn(['data_sourceCustom_tag_language', 'error'], t('Tag language must be selected'));
state.setIn(['data_sourceCustom_tag_language', 'error'], t('tagLanguageMustBeSelected'));
} else {
state.setIn(['data_sourceCustom_tag_language', 'error'], null);
}
@ -277,7 +277,7 @@ export default class CustomContent extends Component {
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
</StaticField>
<Dropdown id="data_sourceCustom_tag_language" label={t('Tag language')} options={this.customTemplateTagLanguageOptions} disabled={true}/>
<Dropdown id="data_sourceCustom_tag_language" label={t('tagLanguage')} options={this.customTemplateTagLanguageOptions} disabled={true}/>
{customTemplateTypeKey && getTypeForm(this, customTemplateTypeKey, true)}

View file

@ -12,12 +12,13 @@ import {createComponentMixin} from "./decorator-helpers";
import lang_en_US_common from "../../../locales/en-US/common";
import lang_es_ES_common from "../../../locales/es-ES/common";
import lang_pt_BR_common from "../../../locales/pt-BR/common";
import lang_de_DE_common from "../../../locales/de-DE/common";
const resourcesCommon = {
'en-US': lang_en_US_common,
'es-ES': lang_es_ES_common,
'pt-BR': lang_pt_BR_common,
'de-DE': lang_de_DE_common,
'fk-FK': convertToFake(lang_en_US_common)
};

View file

@ -277,7 +277,7 @@ export default class CUD extends Component {
const label = matches[2].trim();
options.push({ key, label });
} else {
errors.push(t('errrorOnLineLine', { line: lineIdx + 1}));
errors.push(t('errorOnLineLine', { line: lineIdx + 1}));
}
}
}
@ -511,7 +511,7 @@ export default class CUD extends Component {
<InputField id="key" label={t('mergeTag-1')}/>
<TextArea id="help" label={t('Help text')}/>
<TextArea id="help" label={t('helpText')}/>
{fieldSettings}
@ -532,4 +532,4 @@ export default class CUD extends Component {
</div>
);
}
}
}

View file

@ -291,7 +291,7 @@ export function getMailerTypes(t) {
<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')}/>
<InputField id="sesSecret" label={t('accessSecret')} placeholder={t('awsSecretAccessKey')} type="password"/>
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
</Fieldset>
<Fieldset label={t('advancedMailerSettings')}>

View file

@ -171,7 +171,7 @@ export default class CUD extends Component {
}
if (!state.getIn(['tag_language', 'value'])) {
state.setIn(['tag_language', 'error'], t('Tag language must be selected'));
state.setIn(['tag_language', 'error'], t('tagLanguageMustBeSelected'));
}
if (typeKey) {
@ -320,7 +320,7 @@ export default class CUD extends Component {
{ 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('Tag language'), render: data => this.tagLanguages[data].name },
{ data: 4, title: t('tagLanguage'), render: data => this.tagLanguages[data].name },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') },
];
@ -376,7 +376,7 @@ export default class CUD extends Component {
{typeForm}
<Dropdown id="tag_language" label={t('Tag language')} options={tagLanguageOptions} disabled={isEdit}/>
<Dropdown id="tag_language" label={t('tagLanguage')} options={tagLanguageOptions} disabled={isEdit}/>
</>
}

View file

@ -45,7 +45,7 @@ export default class List extends Component {
{ 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('Tag language'), render: data => this.tagLanguages[data].name },
{ data: 4, title: t('tagLanguage'), render: data => this.tagLanguages[data].name },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') },
{

View file

@ -132,7 +132,7 @@ export default class CUD extends Component {
}
if (!state.getIn(['tag_language', 'value'])) {
state.setIn(['tag_language', 'error'], t('Tag language must be selected'));
state.setIn(['tag_language', 'error'], t('tagLanguageMustBeSelected'));
} else {
state.setIn(['tag_language', 'error'], null);
}
@ -218,7 +218,7 @@ export default class CUD extends Component {
<Dropdown id="type" label={t('type')} options={this.typeOptions}/>
}
<Dropdown id="tag_language" label={t('Tag language')} options={tagLanguageOptions}/>
<Dropdown id="tag_language" label={t('tagLanguage')} options={tagLanguageOptions}/>
<NamespaceSelect/>

View file

@ -45,7 +45,7 @@ export default class List extends Component {
{ 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('Tag language'), render: data => this.tagLanguages[data].name },
{ data: 4, title: t('tagLanguage'), render: data => this.tagLanguages[data].name },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') },
{

View file

@ -1,4 +1,5 @@
const webpack = require('webpack');
const CopyPlugin = require('copy-webpack-plugin');
const path = require('path');
module.exports = {
@ -97,7 +98,13 @@ module.exports = {
mailtrainConfig: 'mailtrainConfig'
},
plugins: [
// new webpack.optimize.UglifyJsPlugin(),
new CopyPlugin([
{ from: './node_modules/jquery/dist/jquery.min.js', to: path.resolve(__dirname, 'dist') },
{ from: './node_modules/popper.js/dist/popper.min.js', to: path.resolve(__dirname, 'dist') },
{ from: './node_modules/bootstrap/dist/js/bootstrap.min.js', to: path.resolve(__dirname, 'dist') },
{ from: './node_modules/@coreui/coreui/dist/js/coreui.min.js', to: path.resolve(__dirname, 'dist') },
{ from: './node_modules/@fortawesome/fontawesome-free/webfonts/', to: path.resolve(__dirname, 'dist', 'webfonts'), toType: 'dir'}
]),
],
watchOptions: {
ignored: 'node_modules/',

View file

@ -23,7 +23,6 @@ services:
mailtrain:
build: .
command: ${MAILTRAIN_SETTINGS}
ports:
- "3000:3000"
- "3003:3003"

View file

@ -23,7 +23,6 @@ services:
mailtrain:
image: mailtrain/mailtrain:latest
command: ${MAILTRAIN_SETTINGS}
ports:
- "3000:3000"
- "3003:3003"

View file

@ -1,201 +1,120 @@
#!/bin/bash
# Entrypoint for Docker Container
set -e
function printHelp {
cat <<EOF
Optional parameters:
--trustedUrlBase XXX - sets the trusted url of the instance (default: http://localhost:3000)
--sandboxUrlBase XXX - sets the sandbox url of the instance (default: http://localhost:3003)
--publicUrlBase XXX - sets the public url of the instance (default: http://localhost:3004)
--withProxy - use if Mailtrain is behind an http reverse proxy
--mongoHost XXX - sets mongo host (default: mongo)
--redisHost XXX - sets redis host (default: redis)
--mySqlHost XXX - sets mysql host (default: mysql)
--withLdap - use if you want to enable LDAP authentication
--ldapHost XXX - LDAP Host for authentication (default: ldap)
--ldapPort XXX - LDAP port (default: 389)
--ldapSecure - use if you want to use LDAP with ldaps protocol
--ldapBindUser XXX - User for LDAP connexion
--ldapBindPass XXX - Password for LDAP connexion
--ldapFilter XXX - LDAP filter
--ldapBaseDN XXX - LDAP base DN
--ldapUidTag XXX - LDAP UID tag (e.g. uid/cn/username)
EOF
URL_BASE_TRUSTED=${URL_BASE_TRUSTED:-'http://localhost:3000'}
URL_BASE_SANDBOX=${URL_BASE_SANDBOX:-'http://localhost:3003'}
URL_BASE_PUBLIC=${URL_BASE_PUBLIC:-'http://localhost:3004'}
WWW_PROXY=${WWW_PROXY:-'false'}
WITH_LDAP=${WITH_LDAP:-'false'}
LDAP_HOST=${LDAP_HOST:-'ldap'}
LDAP_PORT=${LDAP_PORT:-'389'}
LDAP_SECURE=${LDAP_SECURE:-'false'}
LDAP_BIND_USER=${LDAP_BIND_USER:-}
LDAP_BIND_PASS=${LDAP_BIND_PASS:-}
LDAP_FILTER=${LDAP_FILTER:-}
LDAP_BASEDN=${LDAP_BASEDN:-}
LDAP_UIDTAG=${LDAP_UIDTAG:-}
MONGO_HOST=${MONG_HOST:-'mongo'}
REDIS_HOST=${REDIS_HOST:-'redis'}
MYSQL_HOST=${MYSQL_HOST:-'mysql'}
MYSQL_DATABASE=${MYSQL_DATABASE:-'mailtrain'}
MYSQL_USER=${MYSQL_USER:-'mailtrain'}
MYSQL_PASSWORD=${MYSQL_PASSWORD:-'mailtrain'}
# Warning for users that already rely on the MAILTRAIN_SETTING variable
# Can probably be removed in the future.
MAILTRAIN_SETTING=${MAILTRAIN_SETTINGS:-}
if [ ! -z "$MAILTRAIN_SETTING" ]; then
echo 'Error: MAILTRAIN_SETTINGS is no longer supported. See README'
exit 1
}
urlBaseTrusted=http://localhost:3000
urlBaseSandbox=http://localhost:3003
urlBasePublic=http://localhost:3004
wwwProxy=false
withLdap=false
ldapHost=ldap
ldapPort=389
ldapSecure=false
ldapBindUser=""
ldapBindPass=""
ldapFilter=""
ldapBaseDN=""
ldapUidTag=""
mongoHost=mongo
redisHost=redis
mySqlHost=mysql
while [ $# -gt 0 ]; do
case "$1" in
--help)
printHelp
;;
--trustedUrlBase)
urlBaseTrusted="$2"
shift 2
;;
--sandboxUrlBase)
urlBaseSandbox="$2"
shift 2
;;
--publicUrlBase)
urlBasePublic="$2"
shift 2
;;
--withProxy)
wwwProxy=true
shift 1
;;
--mongoHost)
mongoHost="$2"
shift 2
;;
--redisHost)
redisHost="$2"
shift 2
;;
--mySqlHost)
mySqlHost="$2"
shift 2
;;
--withLdap)
withLdap=true
shift 1
;;
--ldapHost)
ldapHost="$2"
shift 2
;;
--ldapPort)
ldapPort="$2"
shift 2
;;
--ldapSecure)
ldapSecure=true
shift 1
;;
--ldapBindUser)
ldapBindUser="$2"
shift 2
;;
--ldapBindPass)
ldapBindPass="$2"
shift 2
;;
--ldapFilter)
ldapFilter="$2"
shift 2
;;
--ldapBaseDN)
ldapBaseDN="$2"
shift 2
;;
--ldapUidTag)
ldapUidTag="$2"
shift 2
;;
*)
echo "Error: unrecognized option $1."
printHelp
esac
done
if [ "$ldapBindUser" == "" ]; then
ldapBindUserLine=""
else
ldapBindUserLine="bindUser: $ldapBindUser"
fi
if [ "$ldapBindPass" == "" ]; then
ldapBindPassLine=""
else
ldapBindPassLine="bindPassword: $ldapBindPass"
fi
if [ "$ldapFilter" == "" ]; then
ldapFilterLine=""
else
ldapFilterLine="filter: $ldapFilter"
fi
if [ "$ldapBaseDN" == "" ]; then
ldapBaseDNLine=""
else
ldapBaseDNLine="baseDN: $ldapBaseDN"
fi
if [ "$ldapUidTag" == "" ]; then
ldapUidTagLine=""
else
ldapUidTagLine="uidTag: $ldapUidTag"
fi
cat > server/config/production.yaml <<EOT
www:
host: 0.0.0.0
proxy: $wwwProxy
secret: "`pwgen -1`"
trustedUrlBase: $urlBaseTrusted
sandboxUrlBase: $urlBaseSandbox
publicUrlBase: $urlBasePublic
if [ -f application/config/config.php ]; then
echo 'Info: application/production.yaml already provisioned'
else
echo 'Info: Generating application/production.yaml'
mysql:
host: $mySqlHost
# Basic configuration
cat > server/config/production.yaml <<EOT
www:
host: 0.0.0.0
proxy: $WWW_PROXY
secret: "`pwgen -1`"
trustedUrlBase: $URL_BASE_TRUSTED
sandboxUrlBase: $URL_BASE_SANDBOX
publicUrlBase: $URL_BASE_PUBLIC
redis:
enabled: true
host: $redisHost
mysql:
host: $MYSQL_HOST
database: $MYSQL_DATABASE
user: $MYSQL_USER
password: $MYSQL_PASSWORD
log:
level: info
redis:
enabled: true
host: $REDIS_HOST
builtinZoneMTA:
log:
level: warn
mongo: mongodb://${mongoHost}:27017/zone-mta
redis: redis://${redisHost}:6379/2
log:
level: info
queue:
processes: 5
builtinZoneMTA:
log:
level: warn
mongo: mongodb://${MONGO_HOST}:27017/zone-mta
redis: redis://${REDIS_HOST}:6379/2
ldap:
enabled: $withLdap
host: $ldapHost
port: $ldapPort
secure: $ldapSecure
$ldapBindUserLine
$ldapBindPassLine
$ldapFilterLine
$ldapBaseDNLine
$ldapUidTagLine
queue:
processes: 5
EOT
cat > server/services/workers/reports/config/production.yaml <<EOT
mysql:
host: $mySqlHost
log:
level: warn
# Manage LDAP if enabled
if [ "$WITH_LDAP" = "true" ]; then
echo 'Info: LDAP enabled'
cat >> server/config/production.yaml <<EOT
ldap:
enabled: true
host: $LDAP_HOST
port: $LDAP_PORT
secure: $LDAP_SECURE
bindUser: $LDAP_BIND_USER
bindPasswort: $LDAP_BIND_PASS
filter: $LDAP_FILTER
baseDN: $LDAP_BASEDN
uidTag: $LDAP_UIDTAG
EOT
else
echo 'Info: LDAP not enabled'
cat >> server/config/production.yaml <<EOT
ldap:
enabled: false
EOT
fi
fi
if [ -f server/services/workers/reports/config/production.yaml ]; then
echo 'Info: server/production.yaml already provisioned'
else
echo 'Info: Generating server/production.yaml'
cat > server/services/workers/reports/config/production.yaml <<EOT
mysql:
host: $MYSQL_HOST
log:
level: warn
EOT
fi
# Wait for the other services to start
while ! nc -z $mySqlHost 3306; do sleep 1; done
while ! nc -z $redisHost 6379; do sleep 1; done
while ! nc -z $mongoHost 27017; do sleep 1; done
echo 'Info: Waiting for MySQL Server'
while ! nc -z $MYSQL_HOST 3306; do sleep 1; done
echo 'Info: Waiting for Redis Server'
while ! nc -z $REDIS_HOST 6379; do sleep 1; done
echo 'Info: Waiting for MongoDB Server'
while ! nc -z $MONGO_HOST 27017; do sleep 1; done
cd server
NODE_ENV=production node index.js
NODE_ENV=production node index.js

1032
locales/de-DE/common.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -267,7 +267,7 @@
"viewStatistics": "View statistics",
"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.",
"allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if 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.",
@ -398,7 +398,7 @@
"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 }}",
"errorOnLineLine": "Error on line {{ line }}",
"fieldUpdated": "Field updated",
"fieldCreated": "Field created",
"notVisible": "Not visible",
@ -864,6 +864,7 @@
"beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"",
"signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.",
"accessKey": "Access key",
"accessSecret": "Access Secret",
"awsAccessKeyId": "AWS access key ID",
"awsSecretAccessKey": "AWS secret access key",
"region": "Region",
@ -1025,4 +1026,4 @@
"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"
}
}

View file

@ -267,7 +267,7 @@
"viewStatistics": "View statistics",
"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.",
"allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if 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.",
@ -398,7 +398,7 @@
"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 }}",
"errorOnLineLine": "Error on line {{ line }}",
"fieldUpdated": "Field updated",
"fieldCreated": "Field created",
"notVisible": "Not visible",
@ -864,6 +864,7 @@
"beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"",
"signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.",
"accessKey": "Access key",
"accessSecret": "Access Secret",
"awsAccessKeyId": "AWS access key ID",
"awsSecretAccessKey": "AWS secret access key",
"region": "Region",
@ -1024,5 +1025,8 @@
"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"
}
"thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character",
"tagLanguage": "Tag language",
"tagLanguageMustBeSelected": "Tag language must be selected",
"helpText": "Help text"
}

View file

@ -277,7 +277,7 @@
"viewStatistics": "View statistics",
"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.",
"allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if 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.",
@ -425,7 +425,7 @@
"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 }}",
"errorOnLineLine": "Error on line {{ line }}",
"fieldUpdated": "Field updated",
"fieldUpdated - TODO: update line above and then delete this line to mark that the translation has been fixed": "Field updated",
"fieldCreated": "Field created",
@ -915,6 +915,7 @@
"beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"",
"signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.",
"accessKey": "Access key",
"accessSecret": "Access Secret",
"awsAccessKeyId": "AWS access key ID",
"awsSecretAccessKey": "AWS secret access key",
"region": "Region",
@ -1104,4 +1105,4 @@
"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"
}
}

View file

@ -17,7 +17,7 @@ const deepKeys = require('deep-keys');
const localeMain = 'en-US/common.json';
const localeMainPrevious = 'en-US-last-run/common.json';
const localeTranslations = ['es-ES/common.json', 'pt-BR/common.json'];
const localeTranslations = ['es-ES/common.json', 'pt-BR/common.json', 'de-DE/common.json'];
const searchDirs = [
'../client/src',
'../server',

View file

@ -426,7 +426,7 @@
"defaultValueIsNotAProperlyFormattedDate": "O valor padrão não é uma data formatada corretamente",
"defaultValueIsNotAProperlyFormatted": "O valor padrão não é uma data de aniversário formatada corretamente",
"defaultValueIsNotOneOfTheAllowedOptions": "O valor padrão não é uma das opções permitidas",
"errrorOnLineLine": "Errror on line {{line}}",
"errorOnLineLine": "Error on line {{line}}",
"fieldUpdated": "Field updated",
"fieldUpdated - TODO: update line above and then delete this line to mark that the translation has been fixed": "Field updated",
"fieldCreated": "Field created",
@ -916,6 +916,7 @@
"beginsWithBeginRsaPrivateKey": "Começa com \"----- INICIO DA CHAVE PRIVADA RSA ----- \"",
"signingIsDisabledWithoutAValidPrivateKey": "A assinatura está desativada sem uma chave privada válida.",
"accessKey": "Chave de acesso",
"accessSecret": "Acesso secreto",
"awsAccessKeyId": "ID da chave de acesso da AWS",
"awsSecretAccessKey": "Chave de acesso secreto da AWS",
"region": "Região",
@ -1107,4 +1108,4 @@
"thePasswordMustContainAtLeastOne-1": "A senha deve conter pelo menos uma letra maiúscula",
"thePasswordMustContainAtLeastOneNumber": "A senha deve conter pelo menos um número",
"thePasswordMustContainAtLeastOneSpecial": "A senha deve conter pelo menos um caractere especial"
}
}

View file

@ -237,11 +237,11 @@ async function createApp(appType) {
useWith404Fallback('/static', express.static(path.join(__dirname, '..', 'client', 'static')));
useWith404Fallback('/client', express.static(path.join(__dirname, '..', 'client', 'dist')));
useWith404Fallback('/static-npm/fontawesome', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@fortawesome', 'fontawesome-free', 'webfonts')));
useWith404Fallback('/static-npm/jquery.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'jquery', 'dist', 'jquery.min.js')));
useWith404Fallback('/static-npm/popper.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'popper.js', 'dist', 'umd', 'popper.min.js')));
useWith404Fallback('/static-npm/bootstrap.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'bootstrap', 'dist', 'js', 'bootstrap.min.js')));
useWith404Fallback('/static-npm/coreui.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@coreui', 'coreui', 'dist', 'js', 'coreui.min.js')));
useWith404Fallback('/static-npm/fontawesome', express.static(path.join(__dirname, '..', 'client', 'dist', 'webfonts')));
useWith404Fallback('/static-npm/jquery.min.js', express.static(path.join(__dirname, '..', 'client', 'dist', 'jquery.min.js')));
useWith404Fallback('/static-npm/popper.min.js', express.static(path.join(__dirname, '..', 'client', 'dist', 'popper.min.js')));
useWith404Fallback('/static-npm/bootstrap.min.js', express.static(path.join(__dirname, '..', 'client', 'dist', 'bootstrap.min.js')));
useWith404Fallback('/static-npm/coreui.min.js', express.static(path.join(__dirname, '..', 'client', 'dist', 'coreui.min.js')));
// Make sure flash messages are available

View file

@ -49,6 +49,7 @@ enabledLanguages:
- en-US
- es-ES
- pt-BR
- de-DE
- fk-FK
# Inject custom scripts in subscription/layout.mjml.hbs

View file

@ -863,7 +863,7 @@ async function getListsWithEmail(context, email) {
// FIXME - this methods is rather suboptimal if there are many lists. It quite needs permission caching in shares.js
return await knex.transaction(async tx => {
const lsts = await tx('lists').select(['id', 'name']);
const lsts = await tx('lists').select(['id', 'cid', 'name']);
const result = [];
for (const list of lsts) {

View file

@ -360,7 +360,6 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
@ -3735,7 +3734,6 @@
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@ -3744,8 +3742,7 @@
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
}
}
},
@ -6658,8 +6655,7 @@
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sqlstring": {
"version": "2.3.1",

View file

@ -78,6 +78,7 @@
"humanize": "0.0.9",
"i18next": "^13.1.0",
"isemail": "^3.2.0",
"js-yaml": "^3.13.1",
"jsdom": "^13.1.0",
"juice": "^5.2.0",
"klaw-sync": "^6.0.0",

View file

@ -361,7 +361,7 @@ async function processCampaign(campaignId) {
}
const subs = await knex('campaign_messages')
.where({status: CampaignMessageStatus.SCHEDULED})
.where({status: CampaignMessageStatus.SCHEDULED, campaign: campaignId})
.whereNotIn('hash_email', messagesInProcessing.map(x => x.hash_email))
.limit(retrieveBatchSize);

View file

@ -12,6 +12,7 @@ defaultLanguage: en-US
enabledLanguages:
- en-US
- es-ES
- de-DE
- fk-FK
mysql:

View file

@ -56,6 +56,11 @@ const langCodes = {
getLabel: t => 'Português',
longCode: 'pt-BR'
},
'de-DE': {
getShortLabel: t => 'DE',
getLabel: t => 'Deutsch',
longCode: 'de-DE'
},
'fk-FK': {
getShortLabel: t => 'FK',
getLabel: t => 'Fake',