diff --git a/src/cloudron.js b/src/cloudron.js index 7727354c8..9513b5f69 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -5,7 +5,6 @@ exports = module.exports = { uninitialize, getConfig, getLogs, - getLanguages, reboot, isRebootRequired, @@ -280,28 +279,6 @@ function getLogs(unit, options, callback) { return callback(null, transformStream); } -function getLanguages(callback) { - assert.strictEqual(typeof callback, 'function'); - - // we always return english to avoid dashboard breakage - var languages = ['en']; - - fs.readdir(path.join(paths.DASHBOARD_DIR, 'translation'), function (error, result) { - if (error) { - console.error('Failed to list translations', error); - return callback(null, languages); - } - - var jsonFiles = result.filter(function (file) { return path.extname(file) === '.json'; }); - console.log(jsonFiles); - - languages = jsonFiles.map(function (file) { return path.basename(file, '.json'); }); - console.log(jsonFiles); - - callback(null, languages); - }); -} - function prepareDashboardDomain(domain, auditSource, callback) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); diff --git a/src/mail_templates/welcome_user-html.ejs b/src/mail_templates/welcome_user-html.ejs index 0a49b7384..47bfb18a2 100644 --- a/src/mail_templates/welcome_user-html.ejs +++ b/src/mail_templates/welcome_user-html.ejs @@ -4,7 +4,7 @@

Hi <%= user.displayName || user.username || user.email %>,

-

Welcome to <%= cloudronName %>!

+

{{ welcomeEmail.welcomeTo }}

Get started. diff --git a/src/mailer.js b/src/mailer.js index 7e98d149e..d39a55585 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -34,6 +34,7 @@ var assert = require('assert'), safe = require('safetydance'), settings = require('./settings.js'), showdown = require('showdown'), + translation = require('./translation.js'), smtpTransport = require('nodemailer-smtp-transport'), util = require('util'); @@ -91,14 +92,21 @@ function sendMail(mailOptions, callback) { }); } -function render(templateFile, params) { +function render(templateFile, params, translationAssets) { assert.strictEqual(typeof templateFile, 'string'); assert.strictEqual(typeof params, 'object'); var content = null; + var raw = safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'); + if (raw === null) { + debug(`Error loading ${templateFile}`); + return ''; + } + + if (typeof translationAssets === 'object') raw = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {}); try { - content = ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params); + content = ejs.render(raw, params); } catch (e) { debug(`Error rendering ${templateFile}`, e); } @@ -135,24 +143,28 @@ function sendInvite(user, invitor, inviteLink) { getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); - var templateData = { - user: user, - webadminUrl: settings.adminOrigin(), - inviteLink: inviteLink, - invitor: invitor, - cloudronName: mailConfig.cloudronName, - cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar' - }; + translation.getTranslations(function (error, translationAssets) { + if (error) return debug('Error getting translations:', error); - var mailOptions = { - from: mailConfig.notificationFrom, - to: user.fallbackEmail, - subject: util.format('Welcome to %s', mailConfig.cloudronName), - text: render('welcome_user-text.ejs', templateData), - html: render('welcome_user-html.ejs', templateData) - }; + var templateData = { + user: user, + webadminUrl: settings.adminOrigin(), + inviteLink: inviteLink, + invitor: invitor, + cloudronName: mailConfig.cloudronName, + cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar' + }; - sendMail(mailOptions); + var mailOptions = { + from: mailConfig.notificationFrom, + to: user.fallbackEmail, + subject: util.format('Welcome to %s', mailConfig.cloudronName), + text: render('welcome_user-text.ejs', templateData, translationAssets), + html: render('welcome_user-html.ejs', templateData, translationAssets) + }; + + sendMail(mailOptions); + }); }); } diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index 02b097d87..5ec867ae3 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -37,6 +37,7 @@ let assert = require('assert'), system = require('../system.js'), tokendb = require('../tokendb.js'), tokens = require('../tokens.js'), + translation = require('../translation.js'), updater = require('../updater.js'), users = require('../users.js'), updateChecker = require('../updatechecker.js'); @@ -316,7 +317,7 @@ function getServerIp(req, res, next) { } function getLanguages(req, res, next) { - cloudron.getLanguages(function (error, languages) { + translation.getLanguages(function (error, languages) { if (error) return next(new BoxError.toHttpError(error)); next(new HttpSuccess(200, { languages })); diff --git a/src/settings.js b/src/settings.js index 1ebaa1497..5b538f8f6 100644 --- a/src/settings.js +++ b/src/settings.js @@ -123,7 +123,6 @@ var addons = require('./addons.js'), assert = require('assert'), backups = require('./backups.js'), BoxError = require('./boxerror.js'), - cloudron = require('./cloudron.js'), constants = require('./constants.js'), cron = require('./cron.js'), CronJob = require('cron').CronJob, @@ -135,6 +134,7 @@ var addons = require('./addons.js'), safe = require('safetydance'), settingsdb = require('./settingsdb.js'), sysinfo = require('./sysinfo.js'), + translation = require('./translation.js'), util = require('util'), _ = require('underscore'); @@ -670,7 +670,7 @@ function setLanguage(language, callback) { assert.strictEqual(typeof language, 'string'); assert.strictEqual(typeof callback, 'function'); - cloudron.getLanguages(function (error, languages) { + translation.getLanguages(function (error, languages) { if (error) return callback(error); if (languages.indexOf(language) === -1) return callback(new BoxError(BoxError.NOT_FOUND)); diff --git a/src/test/translation-test.js b/src/test/translation-test.js new file mode 100644 index 000000000..10bf588fc --- /dev/null +++ b/src/test/translation-test.js @@ -0,0 +1,65 @@ +/* jslint node:true */ +/* global it:false */ +/* global describe:false */ +/* global before:false */ + +'use strict'; + +var expect = require('expect.js'), + translation = require('../translation.js'); + +describe('translation', function () { + + describe('translate', function () { + before(function (done) { + done(); + }); + + it('nonexisting token', function () { + var out = translation.translate('Foo {{ bar }}', {}, {}); + expect(out).to.contain('{{ bar }}'); + }); + + it('existing token', function () { + var out = translation.translate('Foo {{ bar }}', { bar: 'here' }, {}); + expect(out).to.contain('here'); + }); + + it('existing token as fallback', function () { + var out = translation.translate('Foo {{ bar }}', {}, { bar: 'here' }); + expect(out).to.contain('here'); + }); + + it('existing token deep', function () { + var out = translation.translate('Foo {{ bar.baz.foo }}', { bar: { baz: { foo: 'here' }}}, {}); + expect(out).to.contain('here'); + }); + + it('existing token deep as fallback', function () { + var out = translation.translate('Foo {{ bar.baz.foo }}', { bar: '' }, { bar: { baz: { foo: 'here' }}}); + expect(out).to.contain('here'); + }); + + it('with whitespace tokens', function () { + var obj = { + something: { + missing: { + there: '1' + } + }, + here: '2', + there: '3', + foo: '4', + bar: '5' + }; + var input = 'Hello {{ something.missing.there}} and some more {{here}} and {{ there }} with odd spacing {{foo }} lots of{{ bar }}'; + + var out = translation.translate(input, obj, {}); + expect(out).to.contain('1'); + expect(out).to.contain('2'); + expect(out).to.contain('3'); + expect(out).to.contain('4'); + expect(out).to.contain('5'); + }); + }); +}); diff --git a/src/translation.js b/src/translation.js new file mode 100644 index 000000000..b27729b1e --- /dev/null +++ b/src/translation.js @@ -0,0 +1,92 @@ +'use strict'; + +exports = module.exports = { + translate, + getTranslations, + getLanguages +}; + +var assert = require('assert'), + debug = require('debug')('box:translation'), + fs = require('fs'), + path = require('path'), + paths = require('./paths.js'), + settings = require('./settings.js'); + +const TRANSLATION_FOLDER = path.join(paths.DASHBOARD_DIR, 'translation'); + +// to be used together with getTranslations() => { translations, fallback } +function translate(input, translations, fallbackTranslations) { + assert.strictEqual(typeof input, 'string'); + assert.strictEqual(typeof translations, 'object'); + assert.strictEqual(typeof fallbackTranslations, 'object'); + + var tokens = input.match(/{{(.*?)}}/gm); + if (!tokens) return input; + + var output = input; + tokens.forEach(function (token) { + var key = token.slice(2).slice(0, -2).trim(); + var value = key.split('.').reduce(function (acc, cur) { + if (acc === null) return null; + return typeof acc[cur] !== 'undefined' ? acc[cur] : null; + }, translations); + + // try fallback + if (value === null) value = key.split('.').reduce(function (acc, cur) { + if (acc === null) return null; + return typeof acc[cur] !== 'undefined' ? acc[cur] : null; + }, fallbackTranslations); + + if (value === null) value = token; + + output = output.replace(token, value); + }); + + return output; +} + +function getTranslations(callback) { + assert.strictEqual(typeof callback, 'function'); + + var fallback = {}; + try { + fallback = require(path.join(TRANSLATION_FOLDER, 'en.json')); + } catch (e) { + console.error('Fallback language en not found', e); + } + + settings.getLanguage(function (error, lang) { + if (error) return callback(error); + + var translations = {}; + try { + translations = require(path.join(TRANSLATION_FOLDER, lang + '.json')); + } catch (e) { + console.error(`Requested language ${lang} not found`, e); + } + + return callback(null, { translations, fallback }); + }); +} + +function getLanguages(callback) { + assert.strictEqual(typeof callback, 'function'); + + // we always return english to avoid dashboard breakage + var languages = ['en']; + + fs.readdir(TRANSLATION_FOLDER, function (error, result) { + if (error) { + console.error('Failed to list translations', error); + return callback(null, languages); + } + + var jsonFiles = result.filter(function (file) { return path.extname(file) === '.json'; }); + languages = jsonFiles.map(function (file) { return path.basename(file, '.json'); }); + + debug('Languages found:', jsonFiles); + + callback(null, languages); + }); +}