Merge pull request #5616 from NodeBB/auto-lang

Automatically detect user language based on browser accepts header
v1.18.x
Barış Soner Uşaklı 8 years ago committed by GitHub
commit 081578c7f4

@ -35,5 +35,6 @@
"allowPrivateGroups": 1,
"unreadCutoff": 2,
"bookmarkThreshold": 5,
"topicsPerList": 20
"topicsPerList": 20,
"autoDetectLang": 1
}

@ -1,5 +1,6 @@
{
"language-settings": "Language Settings",
"description": "The default language determines the language settings for all users who are visiting your forum. <br />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"
}

@ -176,7 +176,8 @@
}).join('');
};
helpers.localeToHTML = function (locale) {
helpers.localeToHTML = function (locale, fallback) {
locale = locale || fallback || 'en-GB';
return locale.replace('_', '-');
};

@ -18,6 +18,7 @@ languagesController.get = function (req, res, next) {
res.render('admin/general/languages', {
languages: languages,
autoDetectLang: parseInt(meta.config.autoDetectLang, 10) === 1,
});
});
};

@ -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;

@ -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

@ -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;

@ -16,6 +16,17 @@
</select>
</div>
</form>
<form class="row">
<div class="form-group col-sm-6">
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" data-field="autoDetectLang" <!-- IF autoDetectLang -->checked<!-- ENDIF autoDetectLang -->/>
<span class="mdl-switch__label">[[admin/general/languages:auto-detect]]</span>
</label>
</div>
</div>
</form>
</div>
</div>
</div>

@ -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 || parseInt(meta.config.autoDetectLang, 10) !== 1) {
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);

@ -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();
});
});
});
});
Loading…
Cancel
Save