From 413bb4736cabb8df203d17fe503a098d719b8d67 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 20 Apr 2017 00:16:06 -0600 Subject: [PATCH 1/5] Save language metadata on build to avoid readdirs later --- src/languages.js | 81 ++++++++++++++++++++++++++----------------- src/meta/languages.js | 19 ++++++++++ 2 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/languages.js b/src/languages.js index c4c3d5ae0e..520ae8bba1 100644 --- a/src/languages.js +++ b/src/languages.js @@ -4,7 +4,7 @@ var fs = require('fs'); var path = require('path'); var async = require('async'); -var Languages = {}; +var Languages = module.exports; var languagesPath = path.join(__dirname, '../build/public/language'); Languages.init = function (next) { @@ -27,10 +27,13 @@ Languages.get = function (language, namespace, callback) { }); }; -Languages.list = function (callback) { - var languages = []; +var codeCache = null; +Languages.listCodes = function (callback) { + if (codeCache && codeCache.length) { + return callback(null, codeCache); + } - fs.readdir(languagesPath, function (err, files) { + fs.readFile(path.join(languagesPath, 'metadata.json'), function (err, buffer) { if (err && err.code === 'ENOENT') { return callback(null, []); } @@ -38,43 +41,59 @@ Languages.list = function (callback) { return callback(err); } - async.each(files, function (folder, next) { - fs.stat(path.join(languagesPath, folder), function (err, stat) { + var parsed; + try { + parsed = JSON.parse(buffer.toString()); + } catch (e) { + return callback(e); + } + + var langs = parsed.languages; + codeCache = langs; + callback(null, langs); + }); +}; + +var listCache = null; +Languages.list = function (callback) { + if (listCache && listCache.length) { + return callback(null, listCache); + } + + Languages.listCodes(function (err, codes) { + if (err) { + return callback(err); + } + + async.map(codes, function (folder, next) { + var configPath = path.join(languagesPath, folder, 'language.json'); + + fs.readFile(configPath, function (err, buffer) { + if (err && err.code === 'ENOENT') { + return next(); + } if (err) { return next(err); } - - if (!stat.isDirectory()) { - return next(); + try { + var lang = JSON.parse(buffer.toString()); + next(null, lang); + } catch (e) { + next(e); } - - var configPath = path.join(languagesPath, folder, 'language.json'); - - fs.readFile(configPath, function (err, buffer) { - if (err && err.code !== 'ENOENT') { - return next(err); - } - if (buffer) { - var lang = JSON.parse(buffer.toString()); - if (lang.name && lang.code && lang.dir) { - languages.push(lang); - } - } - next(); - }); }); - }, function (err) { + }, function (err, languages) { if (err) { return callback(err); } - // Sort alphabetically - languages = languages.sort(function (a, b) { - return a.code > b.code ? 1 : -1; + + // filter out invalid ones + languages = languages.filter(function (lang) { + return lang.code && lang.name && lang.dir; }); - callback(err, languages); + listCache = languages; + callback(null, languages); }); }); }; - -module.exports = Languages; diff --git a/src/meta/languages.js b/src/meta/languages.js index b420445f16..395711cba7 100644 --- a/src/meta/languages.js +++ b/src/meta/languages.js @@ -102,6 +102,25 @@ function getTranslationTree(callback) { }); }, + // save a list of languages to `${buildLanguagesPath}/metadata.json` + // avoids readdirs later on + function (ref, next) { + async.waterfall([ + function (next) { + mkdirp(buildLanguagesPath, next); + }, + function (x, next) { + fs.writeFile(path.join(buildLanguagesPath, 'metadata.json'), JSON.stringify({ + languages: ref.languages.sort(), + namespaces: ref.namespaces.sort(), + }), next); + }, + function (next) { + next(null, ref); + }, + ], next); + }, + // for each language and namespace combination, // run through core and all plugins to generate // a full translation hash From 2476221b792da3ec926fdf35b24acc1c6d358709 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 20 Apr 2017 00:16:10 -0600 Subject: [PATCH 2/5] Close #3462, automatically detect user language based on browser accepts header --- src/webserver.js | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/webserver.js b/src/webserver.js index 94684d8e24..3911846b67 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -55,13 +55,16 @@ module.exports.listen = function (callback) { callback = callback || function () { }; emailer.registerApp(app); - setupExpressApp(app); - - helpers.register(); - - logger.init(app); - async.waterfall([ + function (next) { + setupExpressApp(app, next); + }, + function (next) { + helpers.register(); + + logger.init(app); + next(); + }, initializeNodeBB, function (next) { winston.info('NodeBB Ready'); @@ -110,7 +113,7 @@ function initializeNodeBB(callback) { }); } -function setupExpressApp(app) { +function setupExpressApp(app, callback) { var middleware = require('./middleware'); var relativePath = nconf.get('relative_path'); @@ -155,6 +158,8 @@ function setupExpressApp(app) { var toobusy = require('toobusy-js'); toobusy.maxLag(parseInt(meta.config.eventLoopLagThreshold, 10) || 100); toobusy.interval(parseInt(meta.config.eventLoopInterval, 10) || 500); + + setupAutoLocale(app, callback); } function setupFavicon(app) { @@ -188,6 +193,35 @@ function setupCookie() { return cookie; } +function setupAutoLocale(app, callback) { + languages.listCodes(function (err, codes) { + if (err) { + return callback(err); + } + + var defaultLang = meta.config.defaultLang || 'en-GB'; + + var langs = [defaultLang].concat(codes).filter(function (el, i, arr) { + return arr.indexOf(el) === i; + }); + + app.use(function (req, res, next) { + if (parseInt(req.uid, 10) > 0) { + return next(); + } + + var lang = req.acceptsLanguages(langs); + if (!lang) { + return next(); + } + req.query.lang = lang; + next(); + }); + + callback(); + }); +} + function listen(callback) { callback = callback || function () { }; var port = parseInt(nconf.get('port'), 10); From c7929ec7d83383c99a4847a156b0e1bfab053c33 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 21 Apr 2017 22:10:25 -0600 Subject: [PATCH 3/5] Add option for disabling language autodetection --- install/data/defaults.json | 3 ++- public/language/en-GB/admin/general/languages.json | 3 ++- src/controllers/admin/languages.js | 1 + src/views/admin/general/languages.tpl | 11 +++++++++++ src/webserver.js | 2 +- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/install/data/defaults.json b/install/data/defaults.json index c471db6b89..86e63882cd 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -35,5 +35,6 @@ "allowPrivateGroups": 1, "unreadCutoff": 2, "bookmarkThreshold": 5, - "topicsPerList": 20 + "topicsPerList": 20, + "autoDetectLang": 1 } diff --git a/public/language/en-GB/admin/general/languages.json b/public/language/en-GB/admin/general/languages.json index da45cade2c..bdd57849b3 100644 --- a/public/language/en-GB/admin/general/languages.json +++ b/public/language/en-GB/admin/general/languages.json @@ -1,5 +1,6 @@ { "language-settings": "Language Settings", "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", - "default-language": "Default Language" + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" } \ No newline at end of file diff --git a/src/controllers/admin/languages.js b/src/controllers/admin/languages.js index 0ac4e98e99..e2d848ddae 100644 --- a/src/controllers/admin/languages.js +++ b/src/controllers/admin/languages.js @@ -18,6 +18,7 @@ languagesController.get = function (req, res, next) { res.render('admin/general/languages', { languages: languages, + autoDetectLang: parseInt(meta.config.autoDetectLang, 10) === 1, }); }); }; diff --git a/src/views/admin/general/languages.tpl b/src/views/admin/general/languages.tpl index 310d1a366d..747c5d43af 100644 --- a/src/views/admin/general/languages.tpl +++ b/src/views/admin/general/languages.tpl @@ -16,6 +16,17 @@ + +
+
+
+ +
+
+
diff --git a/src/webserver.js b/src/webserver.js index 3911846b67..412378883c 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -206,7 +206,7 @@ function setupAutoLocale(app, callback) { }); app.use(function (req, res, next) { - if (parseInt(req.uid, 10) > 0) { + if (parseInt(req.uid, 10) > 0 || parseInt(meta.config.autoDetectLang, 10) !== 1) { return next(); } From 6432c02ab80f0832539c02cbc1843a6d755a0a01 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 21 Apr 2017 23:54:59 -0600 Subject: [PATCH 4/5] Add tests --- test/locale-detect.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/locale-detect.js diff --git a/test/locale-detect.js b/test/locale-detect.js new file mode 100644 index 0000000000..84ea5af59c --- /dev/null +++ b/test/locale-detect.js @@ -0,0 +1,41 @@ +'use strict'; + +var assert = require('assert'); +var nconf = require('nconf'); +var request = require('request'); + +var meta = require('../src/meta'); + +describe('Language detection', function () { + it('should detect the language for a guest', function (done) { + request(nconf.get('url') + '/api/config', { + headers: { + 'Accept-Language': 'de-DE,de;q=0.5', + }, + }, function (err, res, body) { + assert.ifError(err); + assert.ok(body); + + assert.strictEqual(JSON.parse(body).userLang, 'de'); + done(); + }); + }); + + it('should do nothing when disabled', function (done) { + meta.configs.set('autoDetectLang', 0, function (err) { + assert.ifError(err); + + request(nconf.get('url') + '/api/config', { + headers: { + 'Accept-Language': 'de-DE,de;q=0.5', + }, + }, function (err, res, body) { + assert.ifError(err); + assert.ok(body); + + assert.strictEqual(JSON.parse(body).userLang, 'en-GB'); + done(); + }); + }); + }); +}); From 64bf542d1705eb3cb78e6a9a573ab8eb2466d1a0 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 21 Apr 2017 23:55:58 -0600 Subject: [PATCH 5/5] Fix HTML `lang` attribute using the `defaultLang` Themes need `lang="{function.localeToHTML, userLang, defaultLang}"` in their header.tpl file --- public/src/modules/helpers.js | 3 ++- src/middleware/header.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index 3f02a86758..e1cf620607 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -176,7 +176,8 @@ }).join(''); }; - helpers.localeToHTML = function (locale) { + helpers.localeToHTML = function (locale, fallback) { + locale = locale || fallback || 'en-GB'; return locale.replace('_', '-'); }; diff --git a/src/middleware/header.js b/src/middleware/header.js index 70c0755def..0eb9cc9a1f 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -133,6 +133,7 @@ module.exports = function (middleware) { templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : ''; templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin; templateValues.defaultLang = meta.config.defaultLang || 'en-GB'; + templateValues.userLang = res.locals.config.userLang; templateValues.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1; templateValues.privateTagListing = parseInt(meta.config.privateTagListing, 10) === 1;