From ba8bd1212335cb9bd7ba094beb7b5400f35cae6c Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Sat, 4 Mar 2017 18:15:16 +0200 Subject: [PATCH 1/4] initial translations support --- Gruntfile.js | 16 ++++- README.md | 42 +++++++++++++ app.js | 23 +++++++ config/default.toml | 3 + languages/et.mo | Bin 0 -> 2531 bytes languages/et.po | 106 +++++++++++++++++++++++++++++++ languages/mailtrain.pot | 100 +++++++++++++++++++++++++++++ lib/mailer.js | 2 +- lib/translate.js | 35 +++++++++++ package.json | 32 ++++++---- routes/index.js | 3 +- routes/settings.js | 38 +++++------ setup/sql/mailtrain.sql | 10 ++- setup/sql/upgrade-00021.sql | 4 +- tasks/jsxgettext.js | 122 ++++++++++++++++++++++++++++++++++++ test/frontmail-test.js | 6 +- views/index.hbs | 2 +- views/layout.hbs | 2 +- 18 files changed, 503 insertions(+), 43 deletions(-) create mode 100644 languages/et.mo create mode 100644 languages/et.po create mode 100644 languages/mailtrain.pot create mode 100644 lib/translate.js create mode 100644 tasks/jsxgettext.js diff --git a/Gruntfile.js b/Gruntfile.js index 3dde0d9e..6ac983cc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,13 +10,27 @@ module.exports = function (grunt) { nodeunit: { all: ['test/**/*-test.js'] + }, + + jsxgettext: { + test: { + files: [{ + src: ['views/**/*.hbs', 'lib/**/*.js', 'routes/**/*.js', 'services/**/*.js', 'app.js', 'index.js', '!ignored'], + output: 'mailtrain.pot', + 'output-dir': './languages/' + }], + options: { + keyword: ['translate', '_'] + } + } } }); // Load the plugin(s) grunt.loadNpmTasks('grunt-eslint'); grunt.loadNpmTasks('grunt-contrib-nodeunit'); + grunt.task.loadTasks('tasks'); // Tasks - grunt.registerTask('default', ['eslint', 'nodeunit']); + grunt.registerTask('default', ['eslint', 'nodeunit', 'jsxgettext']); }; diff --git a/README.md b/README.md index 5dcf4a30..c6fee354 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,48 @@ node setup/fakedata.js > somefile.csv This command generates a CSV file with 100 000 subscriber accounts +## Translations + +Mailtrain is currently not translated but it supports translations. To add translations you first need to add translation support for the translatable strings + +### Translating JavaScript files + +To translate JavaScript strings you need to make sure that you have loaded the translating function `_` from *'./lib/translate.js'*. If you want to use variables in strings then you also need the *'util'* module. + +```javascript +const _ = require('./path/to/lib/translate')._; +const util = require('util'); // optional +``` + +All you need to do to translate strings is to enclose these in the `_()` function + +```javascript +let str1 = _('This string will be translated'); +let str2 = util.format( _('My name is "%s"'), 'Mailtrain'); +``` + +### Translating Handlebars files + +Enclose translatable strings to `{{#translate}}` tags + +```handlebars +

+ Mailtrain – {{#translate}}the best newsletter app{{/translate}} +

+``` + +### Managing translations + +Translations are loaded from Gettext MO files. In order to generate such files you need a Gettext translations editor. [POEdit](https://poedit.net/) is a great choice. + +To update the translation catalog ([mailtrain.pot](,/languages/mailtrain.pot)) run `grunt` from command line. This fetches all translatable strings from JavaScript and Handlebars files and merges these into the translation catalog + +To add a new language use this catalog file as source. Once you want to update your translation file from the updated catalog, then select "Catalogue" -> "Update from POT file..." in POEdit and select mailtrain.pot. This would merge all new translations from the POT file to your PO file. + +If you have saved the PO file in [./languages](./langauges) then POEdit should auto generate required MO file whenever you hit save for the PO file. + +Once you have a correct MO file in the languages folder, then edit Mailtrain config and set "language" option to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo + ## License * Versions 1.22.0 and up **GPL-V3.0** diff --git a/app.js b/app.js index 36525f50..52b94b1c 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,9 @@ let config = require('config'); let log = require('npmlog'); +let translate = require('./lib/translate'); +let util = require('util'); + let express = require('express'); let bodyParser = require('body-parser'); let path = require('path'); @@ -93,6 +96,21 @@ hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer ); }); +// {{#translate}}abc{{/translate}} +hbs.registerHelper('translate', function (context, options) { // eslint-disable-line prefer-arrow-callback + if (typeof options === 'undefined' && context) { + options = context; + context = false; + } + + let result = translate._(options.fn(this)); // eslint-disable-line no-invalid-this + + if (Array.isArray(context)) { + result = util.format(result, ...context); + } + return new hbs.handlebars.SafeString(result); +}); + app.use(compression()); app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); @@ -118,6 +136,11 @@ app.use(session({ })); app.use(flash()); +app.use((req, res, next) => { + req._ = str => translate._(str); + next(); +}); + app.use(bodyParser.urlencoded({ extended: true, limit: config.www.postsize diff --git a/config/default.toml b/config/default.toml index aee1c248..c547295e 100644 --- a/config/default.toml +++ b/config/default.toml @@ -27,6 +27,9 @@ editors=[ ["codeeditor", "Code Editor"] ] +# Default language to use +language="en" + # If you start out as a root user (eg. if you want to use ports lower than 1000) # then you can downgrade the user once all services are up and running #user="nobody" diff --git a/languages/et.mo b/languages/et.mo new file mode 100644 index 0000000000000000000000000000000000000000..69f724059f00b75f61725972a28ba2d2589b7e1d GIT binary patch literal 2531 zcmb7EJ8xV?6rO~H!14~SB7jb!1O?%{>^ccCYgxi14zgm$R=f#NP|WU~-5u|}cdq7P zXAu=3DkKVoXb=U8#4m7X(cG2>2~i?Zra-_y;5)MqI}|~TH2dw$ojH&1J9BUnxuYd{g4`2yA^^g$H1HS;C0)7vC z5%?qUB=FbC{ypG4*2j+t(F864`TRDJfS*s+KLDS`x&l59`~`Rd_&bo}yazlBJaJrz zHN-dvT*P|xun=zn_kb6He*!-Qp2cPM^9is6{0_+Xk31s8dEl$SXMr0)=A;W`UOob{ zy>EaG;LpHUfJYx4^Klx;`P~3=ymEpT_!!oo0?z@z1TtTHK!h6yLW_gXv5n~frIAZe z6>UBTM7nV>2Rxp@dCKMAa1^59&Ux2G@>vJlI`4^-iGS9)y<>PelcbdI_M z)g4lB$`&$LB#V?v>D;cdY5m`K3gbz3yQ=hj6IXVWC3UA%UGHeCy?nFXUfUvXsH4~- z3|uiqfv@b)svhr6Jr0cX2Of>}_Z{{+S1Fm`>vSasI5NRfPg}Jsvkb1i*G1pWQK}sN zjZ-RNg|ws6nvU#bBkE+PJLE8~>XbNmtGY_>DC%3K*Z}3qIoVg#vnHP;^gmBidjOZY z?hiZ-WLc_$cEK}sCz->^pGmLc2qK9;#yRQ))_2; zYTcWNR~M_rj?9qM@_SpkDO_*iu=KxD;?+#RvoUlTl))wr#3F;)(iAC9F?}sMbIzT?w{aCvC{qaw;uZ*; zmSJ3gT@hSB$&8RlhDwBmp0Q&kF1~r`0begIT#m0{{*u_VW*c{t)ik+{s3A^^YBv@0 zn5?_LPLp>+-z9B>?ahsLas}+MgvnKe)uQ=E^K#O-kTfn*bD`B}yxy2^G;lDvsdhA5 zd*Hb>>3q^=-7@Kog^M*=^aH9kX)Bo@TDM5~+WPAH%0ZOPdZUIn$T}o#DBdEk?)W## z3@R+*?Mn;omu|JMB?|}V*jG>4WTohu6h+#ig^u>M_mg)OL>mXOX;i8`HR}uY=6r24 z3sz>yHDhzvqN0pTw=};<HCd)< zC|&S!TNY>ov@Ow*tTCBBxa&MAOe(fdsvlgPuFnEAxg;Mll~?=KzSSr$nJSG)e6^>F>Mnvsy?H|4 zi)~U*N)8jFL$6ZvAOh8RG(QnS9- zM0bx3X{XxrXy4+t?n_0P?(4F;TitbmKj^p}n~9#aqZEIc$xW?{_BbY$j z8^|G2prN+gfl2ha6q}HM3?z}{G$)PrG`^!eL6QN=2ofon;R8V|=gMb#h$Xkd*qAg| z8h0iQd5Tmb%xx4RUr`e#LP)h^#}J&X9LB1ewkeiRx@mX9iIhUX3AP}v$D*E9r=D&= h7cCA6$%=Y8bPDweO=GtP58SQ(`xJLA=7`X?{sC;F0ek=e literal 0 HcmV?d00001 diff --git a/languages/et.po b/languages/et.po new file mode 100644 index 00000000..5a82f24d --- /dev/null +++ b/languages/et.po @@ -0,0 +1,106 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-04 18:00+0200\n" +"PO-Revision-Date: 2017-03-04 18:00+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: et\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.12\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: views/index.hbs:1 +msgid "Official Mailtrain Partners" +msgstr "Ametlikud Mailtraini partnerid" + +#: views/layout.hbs:1 routes/index.js:11 +msgid "Self hosted email newsletter app" +msgstr "Enda majutatud e-posti uudiskirjade rakendus" + +#: routes/settings.js:23 +msgid "Need to be logged in to access restricted content" +msgstr "Pead olema sisse logitud, et näha peidetud sisu" + +#: routes/settings.js:39 +msgid "Use TLS" +msgstr "Kasuta TLSi" + +#: routes/settings.js:40 +msgid "usually selected for port 465" +msgstr "tavaliselt valitakse, kui port on 465" + +#: routes/settings.js:44 +msgid "Use STARTTLS" +msgstr "Kasuta STARTTLSi" + +#: routes/settings.js:45 +msgid "usually selected for port 587 and 25" +msgstr "tavaliselt valitakse, kui port on 587 või 25" + +#: routes/settings.js:49 +msgid "Do not use encryption" +msgstr "Ära kasuta ühenduse krüpteerimist" + +#: routes/settings.js:115 +msgid "Settings updated" +msgstr "Seaded uuendatud" + +#: routes/settings.js:173 +msgid "Invalid mail transport type" +msgstr "Viga maili transpordi tüüp" + +#: routes/settings.js:184 +msgid "Invalid Access Key" +msgstr "Vigae ligipääsuvõti" + +#: routes/settings.js:187 +msgid "Invalid AWS credentials" +msgstr "VIgased AWS võtmed" + +#: routes/settings.js:190 +msgid "Connection refused, check hostname and port." +msgstr "Ühendusest keelduti, kontrolli domeeninime ja porti" + +#: routes/settings.js:195 +msgid "" +"Did not receive greeting message from server. This might happen when " +"connecting to a TLS port without using TLS." +msgstr "" +"Ei saanud serverilt vastust, see juhtub tavaliselt kui ühendus TLS " +"serverisse ilma TLS kasutamata" + +#: routes/settings.js:197 +msgid "Did not receive greeting message from server." +msgstr "Ei saanud serverilt vastust" + +#: routes/settings.js:200 +msgid "" +"Connection timed out. Check your firewall settings, destination port is " +"probably blocked." +msgstr "" +"Ühendus aegus. Kontrolli oma tulemüüri seadeid, tõenäoliselt on serveir port " +"blokeeritud" + +#: routes/settings.js:205 +msgid "Authentication not accepted, server expects STARTTLS to be used." +msgstr "Autentimist ei lubatud, server nõuab STARTTLS kasutamist" + +#: routes/settings.js:207 +msgid "Authentication failed, check username and password." +msgstr "Autentimine ebaõnnestus, kontrolli kasutajanime ja parooli" + +#: routes/settings.js:217 +msgid "Failed Mailer verification." +msgstr "E-posti seadistuse kontroll ebaõnnestus" + +#: routes/settings.js:217 +msgid "Server responded with: \"%s\"" +msgstr "Server vastas \"%s\"" + +#: routes/settings.js:221 +msgid "Mailer settings verified, ready to send some mail!" +msgstr "E-posti seaded on kontrollitud, võid hakata kirju saatma" diff --git a/languages/mailtrain.pot b/languages/mailtrain.pot new file mode 100644 index 00000000..9d882c15 --- /dev/null +++ b/languages/mailtrain.pot @@ -0,0 +1,100 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Language-Team: LANGUAGE \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2017-03-04 16:00+0000\n" + +#: views/index.hbs:1 +msgid "Official Mailtrain Partners" +msgstr "" + +#: views/layout.hbs:1 +#: routes/index.js:11 +msgid "Self hosted email newsletter app" +msgstr "" + +#: routes/settings.js:23 +msgid "Need to be logged in to access restricted content" +msgstr "" + +#: routes/settings.js:39 +msgid "Use TLS" +msgstr "" + +#: routes/settings.js:40 +msgid "usually selected for port 465" +msgstr "" + +#: routes/settings.js:44 +msgid "Use STARTTLS" +msgstr "" + +#: routes/settings.js:45 +msgid "usually selected for port 587 and 25" +msgstr "" + +#: routes/settings.js:49 +msgid "Do not use encryption" +msgstr "" + +#: routes/settings.js:115 +msgid "Settings updated" +msgstr "" + +#: routes/settings.js:173 +msgid "Invalid mail transport type" +msgstr "" + +#: routes/settings.js:184 +msgid "Invalid Access Key" +msgstr "" + +#: routes/settings.js:187 +msgid "Invalid AWS credentials" +msgstr "" + +#: routes/settings.js:190 +msgid "Connection refused, check hostname and port." +msgstr "" + +#: routes/settings.js:195 +msgid "" +"Did not receive greeting message from server. This might happen when " +"connecting to a TLS port without using TLS." +msgstr "" + +#: routes/settings.js:197 +msgid "Did not receive greeting message from server." +msgstr "" + +#: routes/settings.js:200 +msgid "" +"Connection timed out. Check your firewall settings, destination port is " +"probably blocked." +msgstr "" + +#: routes/settings.js:205 +msgid "Authentication not accepted, server expects STARTTLS to be used." +msgstr "" + +#: routes/settings.js:207 +msgid "Authentication failed, check username and password." +msgstr "" + +#: routes/settings.js:217 +msgid "Failed Mailer verification." +msgstr "" + +#: routes/settings.js:217 +msgid "Server responded with: \"%s\"" +msgstr "" + +#: routes/settings.js:221 +msgid "Mailer settings verified, ready to send some mail!" +msgstr "" \ No newline at end of file diff --git a/lib/mailer.js b/lib/mailer.js index 1a8e24ae..9f0f695b 100644 --- a/lib/mailer.js +++ b/lib/mailer.js @@ -148,7 +148,7 @@ function createMailer(callback) { let level = args.shift(); args.shift(); args.unshift('Mail'); - log[level].apply(log, args); + log[level](...args); }; if (configItems.mailTransport === 'smtp' || !configItems.mailTransport) { diff --git a/lib/translate.js b/lib/translate.js new file mode 100644 index 00000000..c6f44995 --- /dev/null +++ b/lib/translate.js @@ -0,0 +1,35 @@ +'use strict'; + +const config = require('config'); + +const Gettext = require('node-gettext'); +const gt = new Gettext(); +const fs = require('fs'); +const path = require('path'); +const log = require('npmlog'); +const gettextParser = require('gettext-parser'); + +const language = config.language || 'en'; + +[].concat(config.language || []).forEach(lang => { + let data; + let file = path.join(__dirname, '..', 'languages', lang + '.mo'); + try { + data = gettextParser.mo.parse(fs.readFileSync(file)); + } catch (E) { + // ignore + } + if (data) { + gt.addTranslations(lang, lang, data); + gt.setTextDomain(lang); + gt.setLocale(lang); + log.info('LANG', 'Loaded language file for %s', lang); + } +}); + +module.exports._ = str => { + if (typeof str !== 'string') { + str = String(str); + } + return gt.dgettext(language, str); +}; diff --git a/package.json b/package.json index 48740848..5ca1eca7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "sqlinit": "node setup/sql/init.js", "sqldump": "node setup/sql/dump.js | sed -e '/^\\/\\*.*\\*\\/;$/d' -e 's/.[0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]./NOW()/g' > setup/sql/mailtrain.sql", "sqldrop": "node setup/sql/drop.js", - "sqlgen": "npm run sqldrop && DB_FROM_START=Y npm run sqlinit && npm run sqldump" + "sqlgen": "npm run sqldrop && DB_FROM_START=Y npm run sqlinit && npm run sqldump", + "langs:hbs": "jsxgettext -L handlebars -k translate -o langs/hbs.pot views/layout.hbs views/index.hbs", + "langs:js": "jsxgettext -o languages/js.pot routes/index.js", + "langs": "npm run langs:hbs && npm run langs:js" }, "repository": { "type": "git", @@ -23,15 +26,17 @@ "node": ">=5.0.0" }, "devDependencies": { + "gettext-parser": "^1.2.2", "grunt": "^1.0.1", "grunt-cli": "^1.2.0", "grunt-contrib-nodeunit": "^1.0.0", - "grunt-eslint": "^19.0.0" + "grunt-eslint": "^19.0.0", + "jsxgettext-andris": "^0.9.0-patch.1" }, "dependencies": { - "aws-sdk": "^2.15.0", + "aws-sdk": "^2.22.0", "bcrypt-nodejs": "0.0.3", - "body-parser": "^1.16.1", + "body-parser": "^1.17.0", "bounce-handler": "^7.3.2-fork.2", "compression": "^1.6.2", "config": "^1.25.1", @@ -42,15 +47,15 @@ "csv-generate": "^1.0.0", "csv-parse": "^1.2.0", "escape-html": "^1.0.3", - "express": "^4.14.1", + "express": "^4.15.0", "express-session": "^1.15.1", - "faker": "^3.1.0", + "faker": "^4.1.0", "feedparser": "^2.1.0", - "geoip-ultralight": "^0.1.4", + "geoip-ultralight": "^0.1.5", "handlebars": "^4.0.6", "hbs": "^4.0.1", "he": "^1.1.1", - "html-to-text": "^3.1.0", + "html-to-text": "^3.2.0", "humanize": "0.0.9", "is-url": "^1.2.2", "isemail": "^2.2.1", @@ -63,20 +68,21 @@ "morgan": "^1.8.1", "multer": "^1.3.0", "mysql": "^2.13.0", - "nodemailer": "^3.1.3", + "nodemailer": "^3.1.4", "nodemailer-openpgp": "^1.0.2", "npmlog": "^4.0.2", - "openpgp": "^2.3.7", + "openpgp": "^2.3.8", "passport": "^0.3.2", "passport-local": "^1.0.0", "redfour": "^1.0.0", "redis": "^2.6.5", - "request": "^2.79.0", - "serve-favicon": "^2.3.2", + "request": "^2.80.0", + "serve-favicon": "^2.4.1", "shortid": "^2.2.6", "slugify": "^1.1.0", "smtp-server": "^2.0.2", "striptags": "^3.0.1", - "toml": "^2.3.1" + "toml": "^2.3.2", + "node-gettext": "^2.0.0-rc.0" } } diff --git a/routes/index.js b/routes/index.js index 61b78dd2..bf995165 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,12 +2,13 @@ let express = require('express'); let router = new express.Router(); +let _ = require('../lib/translate')._; /* GET home page. */ router.get('/', (req, res) => { res.render('index', { indexPage: true, - title: 'Self hosted email newsletter app' + title: _('Self hosted email newsletter app') }); }); diff --git a/routes/settings.js b/routes/settings.js index 5dab5aa9..6ac92446 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -11,6 +11,8 @@ let url = require('url'); let multer = require('multer'); let upload = multer(); let aws = require('aws-sdk'); +let util = require('util'); +let _ = require('../lib/translate')._; let settings = require('../lib/models/settings'); @@ -18,7 +20,7 @@ let allowedKeys = ['service_url', 'smtp_hostname', 'smtp_port', 'smtp_encryption router.all('/*', (req, res, next) => { if (!req.user) { - req.flash('danger', 'Need to be logged in to access restricted content'); + req.flash('danger', _('Need to be logged in to access restricted content')); return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('/settings'); @@ -34,17 +36,17 @@ router.get('/', passport.csrfProtection, (req, res, next) => { configItems.smtpEncryption = [{ checked: configItems.smtpEncryption === 'TLS' || !configItems.smtpEncryption, key: 'TLS', - value: 'Use TLS', - description: 'usually selected for port 465' + value: _('Use TLS'), + description: _('usually selected for port 465') }, { checked: configItems.smtpEncryption === 'STARTTLS', key: 'STARTTLS', - value: 'Use STARTTLS', - description: 'usually selected for port 587 and 25' + value: _('Use STARTTLS'), + description: _('usually selected for port 587 and 25') }, { checked: configItems.smtpEncryption === 'NONE', key: 'NONE', - value: 'Do not use encryption' + value: _('Do not use encryption') }]; configItems.sesRegion = [{ @@ -110,7 +112,7 @@ router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) = reload: true }); }); - req.flash('success', 'Settings updated'); + req.flash('success', _('Settings updated')); return res.redirect('/settings'); } let key = keys[i]; @@ -168,7 +170,7 @@ router.post('/smtp-verify', upload.array(), passport.parseForm, passport.csrfPro }; } else { return res.json({ - error: 'Invalid mail transport type' + error: _('Invalid mail transport type') }); } @@ -179,30 +181,30 @@ router.post('/smtp-verify', upload.array(), passport.parseForm, passport.csrfPro let message = ''; switch (err.code) { case 'InvalidClientTokenId': - message = 'Invalid Access Key'; + message = _('Invalid Access Key'); break; case 'SignatureDoesNotMatch': - message = 'Invalid AWS credentials'; + message = _('Invalid AWS credentials'); break; case 'ECONNREFUSED': - message = 'Connection refused, check hostname and port.'; + message = _('Connection refused, check hostname and port.'); break; case 'ETIMEDOUT': if ((err.message || '').indexOf('Greeting never received') === 0) { if (data.smtpEncryption !== 'TLS') { - message = 'Did not receive greeting message from server. This might happen when connecting to a TLS port without using TLS.'; + message = _('Did not receive greeting message from server. This might happen when connecting to a TLS port without using TLS.'); } else { - message = 'Did not receive greeting message from server.'; + message = _('Did not receive greeting message from server.'); } } else { - message = 'Connection timed out. Check your firewall settings, destination port is probably blocked.'; + message = _('Connection timed out. Check your firewall settings, destination port is probably blocked.'); } break; case 'EAUTH': if (/\b5\.7\.0\b/.test(err.message) && data.smtpEncryption !== 'STARTTLS') { - message = 'Authentication not accepted, server expects STARTTLS to be used.'; + message = _('Authentication not accepted, server expects STARTTLS to be used.'); } else { - message = 'Authentication failed, check username and password.'; + message = _('Authentication failed, check username and password.'); } break; @@ -212,11 +214,11 @@ router.post('/smtp-verify', upload.array(), passport.parseForm, passport.csrfPro } res.json({ - error: (message || 'Failed Mailer verification.') + (err.response ? ' Server responded with: "' + err.response + '"' : '') + error: (message || _('Failed Mailer verification.')) + (err.response ? ' ' + util.format(_('Server responded with: "%s"'), err.response) : '') }); } else { res.json({ - message: 'Mailer settings verified, ready to send some mail!' + message: _('Mailer settings verified, ready to send some mail!') }); } }); diff --git a/setup/sql/mailtrain.sql b/setup/sql/mailtrain.sql index 70364321..5408c5e2 100644 --- a/setup/sql/mailtrain.sql +++ b/setup/sql/mailtrain.sql @@ -52,6 +52,8 @@ CREATE TABLE `campaigns` ( `segment` int(11) unsigned DEFAULT NULL, `template` int(11) unsigned NOT NULL, `source_url` varchar(255) CHARACTER SET ascii DEFAULT NULL, + `editor_name` varchar(50) DEFAULT '', + `editor_data` longtext, `last_check` timestamp NULL DEFAULT NULL, `check_status` varchar(255) DEFAULT NULL, `from` varchar(255) DEFAULT '', @@ -201,7 +203,7 @@ CREATE TABLE `segments` ( `created` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `list` (`list`), - KEY `name` (`name`(191)), + KEY `name` (`name`), CONSTRAINT `segments_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `settings` ( @@ -210,7 +212,7 @@ CREATE TABLE `settings` ( `value` text NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `key` (`key`) -) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4; INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','smtp-pulse.com'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','465'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','TLS'); @@ -227,7 +229,7 @@ INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awes INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','http://localhost:3000/'); -INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','20'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','21'); CREATE TABLE `subscription` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `cid` varchar(255) CHARACTER SET ascii NOT NULL, @@ -260,6 +262,8 @@ CREATE TABLE `templates` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT '', `description` text, + `editor_name` varchar(50) DEFAULT '', + `editor_data` longtext, `html` longtext, `text` longtext, `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/setup/sql/upgrade-00021.sql b/setup/sql/upgrade-00021.sql index ca3a85ec..e3c54a48 100644 --- a/setup/sql/upgrade-00021.sql +++ b/setup/sql/upgrade-00021.sql @@ -4,11 +4,11 @@ SET @schema_version = '21'; # Add fields editor_name, editor_data to templates ALTER TABLE `templates` ADD COLUMN `editor_name` varchar(50) DEFAULT '' AFTER `description`; -ALTER TABLE `templates` ADD COLUMN `editor_data` longtext DEFAULT '' AFTER `editor_name`; +ALTER TABLE `templates` ADD COLUMN `editor_data` longtext AFTER `editor_name`; # Add fields editor_name, editor_data to campaigns ALTER TABLE `campaigns` ADD COLUMN `editor_name` varchar(50) DEFAULT '' AFTER `source_url`; -ALTER TABLE `campaigns` ADD COLUMN `editor_data` longtext DEFAULT '' AFTER `editor_name`; +ALTER TABLE `campaigns` ADD COLUMN `editor_data` longtext AFTER `editor_name`; # Footer section LOCK TABLES `settings` WRITE; diff --git a/tasks/jsxgettext.js b/tasks/jsxgettext.js new file mode 100644 index 00000000..996f2c0f --- /dev/null +++ b/tasks/jsxgettext.js @@ -0,0 +1,122 @@ +/* +Original from https://raw.githubusercontent.com/Mindflash/grunt-jsxgettext/master/lib/index.js +License: ISC +*/ + +/* eslint-disable */ + +'use strict'; +let _ = require('lodash'); +let path = require('path'); +let util = require('util'); +let jsxgettext = require('jsxgettext-andris'); +let fs = require('fs'); +let async = require('async'); + +/* + options: { + files: ['file-path','file-path'], + + jsxgettext options + } + */ +let task = function (grunt, options, cb) { + var generators = { + '.ejs': jsxgettext.generateFromEJS, + '.hbs': jsxgettext.generateFromHandlebars, + '.jade': jsxgettext.generateFromJade, + '.swig': jsxgettext.generateFromSwig + }; + + // dynamically update generators mapping + if (options.generators) { + if (!Array.isArray(options.generators)) { + options.generators = [options.generators]; + } + + options.generators.forEach(elem => { + // elem.generator can be either a string or a generator method (i.e. own generator or import from jsxgettext) + if (typeof elem.generator === 'string' || elem.generator instanceof String) { + // elem.generator can be an extension, which is used to remap predefined generators to different extensions + // or elem.generator is the name of an generator method implemented in jsxgettext + if (elem.generator.match(/^\..*/)) { + generators[elem.ext] = generators[elem.generator]; + } else { + generators[elem.ext] = jsxgettext[elem.generator]; + } + } else { + generators[elem.ext] = elem.generator; + } + }); + } + + var files = {}; + var dest = options.dest; + + if (!fs.existsSync(path.dirname(dest))) + return cb(util.format("Destination directory %s does not exist.", dest)); + + options.files.filter(file => grunt.file.exists(file)).forEach(file => { + var ext = path.extname(file); + var content = grunt.file.read(file, { + encoding: 'utf-8' + }); + + // pre-process non js files for use by jsxgettext.generate + if (ext !== '.js') { + var args = {}; + args[file] = content; + files = _.assign(files, generators[ext](args, options).shift()); + return; + } + + files[file] = content; + }); + + if (_.isEmpty(files)) + return cb("No valid input files found, received: " + util.inspect(options.files, { + depth: null + })); + + cb(null, jsxgettext.generate(files, options)); +}; + +function unary(fn) { + if (fn.length === 1) return fn; + return function (args) { + return fn.call(this, args); + }; +} + +module.exports = function (grunt) { + grunt.registerMultiTask('jsxgettext', 'A Grunt task to run jsxgettext against files to extract strings into a POT file.', function () { + var self = this; + var done = self.async(); + + async.forEach(self.files, function (fileSet, eCb) { + var dest; + if (typeof fileSet.dest !== 'undefined' && fileSet.dest) { + dest = fileSet.dest; + } else { + dest = path.join(fileSet['output-dir'] || '', fileSet.output); + } + + var options = _.defaults(self.options(), { + files: fileSet.src, + dest: dest, + 'output-dir': fileSet['output-dir'], + output: fileSet['output'] + }); + task(grunt, options, function (err, res) { + + if (err) return eCb(err); + + grunt.file.write(dest, res); + eCb(); + }); + }, err => { + if (err) grunt.fatal(err); + done(); + }); + + }); +}; diff --git a/test/frontmail-test.js b/test/frontmail-test.js index e7e08d29..54221359 100644 --- a/test/frontmail-test.js +++ b/test/frontmail-test.js @@ -4,7 +4,9 @@ let nodemailer = require('nodemailer'); // This is a dummy test to ensure that nodeunit would not fail on 0 assertions module.exports['Load nodemailer'] = function (test) { - let transport = nodemailer.createTransport(); - test.ok(transport._getVersionString()); + let transport = nodemailer.createTransport({ + streamTransport: true + }); + test.ok(transport); test.done(); }; diff --git a/views/index.hbs b/views/index.hbs index 97dcb74b..6c67c28f 100644 --- a/views/index.hbs +++ b/views/index.hbs @@ -1,6 +1,6 @@
-

Official Mailtrain Partners

+

{{#translate}}Official Mailtrain Partners{{/translate}}

diff --git a/views/layout.hbs b/views/layout.hbs index 214892eb..91b4b9a5 100644 --- a/views/layout.hbs +++ b/views/layout.hbs @@ -6,7 +6,7 @@ - + From 3232d01b39a9134867af94bcde2dca4a920bf7e3 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Sat, 4 Mar 2017 18:20:32 +0200 Subject: [PATCH 2/4] updated readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c6fee354..9a7da569 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,8 @@ If you have saved the PO file in [./languages](./langauges) then POEdit should a Once you have a correct MO file in the languages folder, then edit Mailtrain config and set "language" option to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo +> **NB!** For now translation settings are global, so if you have set a translation in config then this applies to all users. An user can't select another translation than the default even if there is a translation file. This is because current Mailtrain code does not provide request context to functions and the functions generating strings do not know which language to use. + ## License * Versions 1.22.0 and up **GPL-V3.0** From f5ee05c55e146b72233c7f0dfd346637c10ed867 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Sat, 4 Mar 2017 18:21:41 +0200 Subject: [PATCH 3/4] updated readme --- README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9a7da569..f27b067c 100644 --- a/README.md +++ b/README.md @@ -210,15 +210,11 @@ Enclose translatable strings to `{{#translate}}` tags ### Managing translations -Translations are loaded from Gettext MO files. In order to generate such files you need a Gettext translations editor. [POEdit](https://poedit.net/) is a great choice. - -To update the translation catalog ([mailtrain.pot](,/languages/mailtrain.pot)) run `grunt` from command line. This fetches all translatable strings from JavaScript and Handlebars files and merges these into the translation catalog - -To add a new language use this catalog file as source. Once you want to update your translation file from the updated catalog, then select "Catalogue" -> "Update from POT file..." in POEdit and select mailtrain.pot. This would merge all new translations from the POT file to your PO file. - -If you have saved the PO file in [./languages](./langauges) then POEdit should auto generate required MO file whenever you hit save for the PO file. - -Once you have a correct MO file in the languages folder, then edit Mailtrain config and set "language" option to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo +* Translations are loaded from Gettext MO files. In order to generate such files you need a Gettext translations editor. [POEdit](https://poedit.net/) is a great choice. +* To update the translation catalog ([mailtrain.pot](,/languages/mailtrain.pot)) run `grunt` from command line. This fetches all translatable strings from JavaScript and Handlebars files and merges these into the translation catalog +* To add a new language use this catalog file as source. Once you want to update your translation file from the updated catalog, then select "Catalogue" -> "Update from POT file..." in POEdit and select mailtrain.pot. This would merge all new translations from the POT file to your PO file. + *If you have saved the PO file in [./languages](./langauges) then POEdit should auto generate required MO file whenever you hit save for the PO file. +* Once you have a correct MO file in the languages folder, then edit Mailtrain config and set ["language" option](https://github.com/andris9/mailtrain/blob/ba8bd1212335cb9bd7ba094beb7b5400f35cae6c/config/default.toml#L30-L31) to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo > **NB!** For now translation settings are global, so if you have set a translation in config then this applies to all users. An user can't select another translation than the default even if there is a translation file. This is because current Mailtrain code does not provide request context to functions and the functions generating strings do not know which language to use. From ae297e4af63b8fb78c2f2ad6ec52ca208caac3d3 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Sat, 4 Mar 2017 18:22:47 +0200 Subject: [PATCH 4/4] updated readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f27b067c..77c29807 100644 --- a/README.md +++ b/README.md @@ -211,9 +211,9 @@ Enclose translatable strings to `{{#translate}}` tags ### Managing translations * Translations are loaded from Gettext MO files. In order to generate such files you need a Gettext translations editor. [POEdit](https://poedit.net/) is a great choice. -* To update the translation catalog ([mailtrain.pot](,/languages/mailtrain.pot)) run `grunt` from command line. This fetches all translatable strings from JavaScript and Handlebars files and merges these into the translation catalog +* To update the translation catalog ([mailtrain.pot](./languages/mailtrain.pot)) run `grunt` from command line. This fetches all translatable strings from JavaScript and Handlebars files and merges these into the translation catalog * To add a new language use this catalog file as source. Once you want to update your translation file from the updated catalog, then select "Catalogue" -> "Update from POT file..." in POEdit and select mailtrain.pot. This would merge all new translations from the POT file to your PO file. - *If you have saved the PO file in [./languages](./langauges) then POEdit should auto generate required MO file whenever you hit save for the PO file. + *If you have saved the PO file in [./languages](./languages) then POEdit should auto generate required MO file whenever you hit save for the PO file. * Once you have a correct MO file in the languages folder, then edit Mailtrain config and set ["language" option](https://github.com/andris9/mailtrain/blob/ba8bd1212335cb9bd7ba094beb7b5400f35cae6c/config/default.toml#L30-L31) to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo > **NB!** For now translation settings are global, so if you have set a translation in config then this applies to all users. An user can't select another translation than the default even if there is a translation file. This is because current Mailtrain code does not provide request context to functions and the functions generating strings do not know which language to use.