Merge pull request #6224 from HabitRPG/api-v3-i18n

API v3 - i18n lib and getUserLanguage middleware
This commit is contained in:
Blade Barringer
2015-11-13 19:37:28 -06:00
24 changed files with 534 additions and 31 deletions
+1 -1
View File
@@ -129,7 +129,7 @@ module.exports = function(grunt) {
grunt.registerTask('test:prepare:translations', function() {
require('babel/register');
var i18n = require('./website/src/libs/i18n'),
var i18n = require('./website/src/libs/api-v3/i18n'),
fs = require('fs');
fs.writeFileSync('test/spec/mocks/translations.js',
"if(!window.env) window.env = {};\n" +
+46
View File
@@ -0,0 +1,46 @@
import {
translations,
localePath,
langCodes,
} from '../../../../../website/src/libs/api-v3/i18n';
import fs from 'fs';
import path from 'path';
describe('i18n', () => {
let listOfLocales = [];
before((done) => {
fs.readdir(localePath, (err, files) => {
if (err) return done(err);
files.forEach((file) => {
if (fs.statSync(path.join(localePath, file)).isDirectory() === false) return;
listOfLocales.push(file);
});
listOfLocales = listOfLocales.sort();
done();
});
});
describe('translations', () => {
it('includes a translation object for each locale', () => {
listOfLocales.forEach((locale) => {
expect(translations[locale]).to.be.an('object');
});
});
});
describe('localePath', () => {
it('is an absolute path to common/locales/', () => {
expect(localePath).to.match(/.*\/common\/locales\//);
expect(localePath)
});
});
describe('langCodes', () => {
it('is a list of all the language codes', () => {
expect(langCodes.sort()).to.eql(listOfLocales);
});
});
});
@@ -0,0 +1,241 @@
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import getUserLanguage from '../../../../../website/src/middlewares/api-v3/getUserLanguage';
import Q from 'q';
import { model as User } from '../../../../../website/src/models/user';
import { translations } from '../../../../../website/src/libs/api-v3/i18n';
import accepts from 'accepts';
describe('getUserLanguage', () => {
let res, req, next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
context('query parameter', () => {
it('uses the language in the query parameter if avalaible', () => {
req.query = {
lang: 'es',
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('es');
});
it('falls back to english if the query parameter language does not exists', () => {
req.query = {
lang: 'bla',
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('en');
});
it('uses query even if the request includes a user and session', () => {
req.query = {
lang: 'es',
};
req.locals = {
user: {
preferences: {
language: 'it',
},
},
};
req.session = {
userId: 123
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('es');
});
});
context('authorized request', () => {
it('uses the user preferred language if avalaible', () => {
req.locals = {
user: {
preferences: {
language: 'it',
},
},
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('it');
});
it('falls back to english if the user preferred language is not avalaible', (done) => {
req.locals = {
user: {
preferences: {
language: 'bla',
},
},
};
getUserLanguage(req, res, () => {
expect(req.language).to.equal('en');
done();
});
});
it('uses the user preferred language even if a session is included in request', () => {
req.locals = {
user: {
preferences: {
language: 'it',
},
},
};
req.session = {
userId: 123
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('it');
});
});
context('request with session', () => {
it('uses the user preferred language if avalaible', (done) => {
sandbox.stub(User, 'findOne').returns({
exec() {
return Q.resolve({
preferences: {
language: 'it',
}
});
}
});
req.session = {
userId: 123
};
getUserLanguage(req, res, () => {
expect(req.language).to.equal('it');
done();
});
});
});
context('browser fallback', () => {
it('uses browser specificed language', (done) => {
req.headers['accept-language'] = 'pt';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('pt');
done();
});
});
it('uses first language in series if browser specifies multiple', (done) => {
req.headers['accept-language'] = 'he, pt, it';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('he');
done();
});
});
it('skips invalid lanaguages and uses first language in series if browser specifies multiple', (done) => {
req.headers['accept-language'] = 'blah, he, pt, it';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('he');
done();
});
});
it('uses normal version of language if specialized locale is passed in', (done) => {
req.headers['accept-language'] = 'fr-CA';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('fr');
done();
});
});
it('uses normal version of language if specialized locale is passed in', (done) => {
req.headers['accept-language'] = 'fr-CA';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('fr');
done();
});
});
it('uses es if es is passed in', (done) => {
req.headers['accept-language'] = 'es';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('es');
done();
});
});
it('uses es_419 if applicable es-languages are passed in', (done) => {
req.headers['accept-language'] = 'es-mx';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('es_419');
done();
});
});
it('uses es_419 if multiple es languages are passed in', (done) => {
req.headers['accept-language'] = 'es-GT, es-MX, es-CR';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('es_419');
done();
});
});
it('zh', (done) => {
req.headers['accept-language'] = 'zh-TW';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('zh_TW');
done();
});
});
it('uses english if browser specified language is not compatible', (done) => {
req.headers['accept-language'] = 'blah';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('en');
done();
});
});
it('uses english if browser does not specify', (done) => {
req.headers['accept-language'] = '';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('en');
done();
});
});
it('uses english if browser does not supply an accept-language header', (done) => {
delete req.headers['accept-language'];
getUserLanguage(req, res, () => {
expect(req.language).to.equal('en');
done();
});
});
});
});
+1 -1
View File
@@ -3,7 +3,7 @@ expect = require 'expect.js'
sinon = require 'sinon'
moment = require 'moment'
shared = require '../../common/script/index.js'
shared.i18n.translations = require('../../website/src/libs/i18n.js').translations
shared.i18n.translations = require('../../website/src/libs/api-v2/i18n.js').translations
test_helper = require './test_helper'
test_helper.addCustomMatchers()
$w = (s)->s.split(' ')
+1 -1
View File
@@ -3,7 +3,7 @@ expect = require 'expect.js'
sinon = require 'sinon'
moment = require 'moment'
shared = require '../../common/script/index.js'
shared.i18n.translations = require('../../website/src/libs/i18n.js').translations
shared.i18n.translations = require('../../website/src/libs/api-v2/i18n.js').translations
repeatWithoutLastWeekday = ()->
repeat = {su:true,m:true,t:true,w:true,th:true,f:true,s:true}
+1 -1
View File
@@ -1,7 +1,7 @@
'use strict';
var shared = require('../../common/script/index.js');
shared.i18n.translations = require('../../website/src/libs/i18n.js').translations
shared.i18n.translations = require('../../website/src/libs/api-v2/i18n.js').translations
require('./test_helper');
+1 -1
View File
@@ -8,7 +8,7 @@ import {MongoClient as mongo} from 'mongodb';
import {v4 as generateUUID} from 'uuid';
import superagent from 'superagent';
import i18n from '../../common/script/src/i18n';
i18n.translations = require('../../website/src/libs/i18n.js').translations;
i18n.translations = require('../../website/src/libs/api-v3/i18n').translations;
const API_TEST_SERVER_PORT = 3003;
+2 -1
View File
@@ -3,7 +3,7 @@ import { model as User } from '../../website/src/models/user'
import { model as Group } from '../../website/src/models/group'
import i18n from '../../common/script/src/i18n';
require('coffee-script');
i18n.translations = require('../../website/src/libs/i18n.js').translations;
i18n.translations = require('../../website/src/libs/api-v3/i18n.js').translations;
afterEach(() => {
sandbox.restore();
@@ -35,6 +35,7 @@ export function generateReq(options={}) {
let defaultReq = {
body: {},
query: {},
headers: {},
};
return defaults(options, defaultReq);
+1 -1
View File
@@ -2,7 +2,7 @@ require('./globals.helper');
import {each} from 'lodash';
import i18n from '../../common/script/src/i18n';
i18n.translations = require('../../website/src/libs/i18n.js').translations;
i18n.translations = require('../../website/src/libs/api-v3/i18n').translations;
export const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.';
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
+1 -1
View File
@@ -10,7 +10,7 @@ var FirebaseTokenGenerator = require('firebase-token-generator');
var User = require('../../models/user').model;
var EmailUnsubscription = require('../../models/emailUnsubscription').model;
var analytics = utils.analytics;
var i18n = require('./../../libs/i18n');
var i18n = require('./../../libs/api-v2/i18n');
var isProd = nconf.get('NODE_ENV') === 'production';
+1 -1
View File
@@ -1,4 +1,4 @@
require('../i18n');
require('./i18n');
var _ = require('lodash');
var Content = require('../../../../common').content;
@@ -1,12 +1,12 @@
var fs = require('fs'),
path = require('path'),
_ = require('lodash'),
User = require('../models/user').model,
User = require('../../models/user').model,
accepts = require('accepts'),
shared = require('../../../common'),
shared = require('../../../../common'),
translations = {};
var localePath = path.join(__dirname, "/../../../common/locales/")
var localePath = path.join(__dirname, "/../../../../common/locales/")
var loadTranslations = function(locale){
var files = fs.readdirSync(path.join(localePath, locale));
@@ -60,18 +60,38 @@ _.each(langCodes, function(code){
}catch (e){}
});
// Remove en_GB from langCodes checked by browser to avaoi it being
// Remove en_GB from langCodes checked by browser to avaoi it being
// used in place of plain original 'en'
var defaultLangCodes = _.without(langCodes, 'en_GB');
// A list of languages that have different versions
var multipleVersionsLanguages = ['es', 'zh'];
var latinAmericanSpanishes = ['es-419', 'es-mx', 'es-gt', 'es-cr', 'es-pa', 'es-do', 'es-ve', 'es-co', 'es-pe',
'es-ar', 'es-ec', 'es-cl', 'es-uy', 'es-py', 'es-bo', 'es-sv', 'es-hn',
'es-ni', 'es-pr'];
var latinAmericanSpanishes = {
'es-419': 'es_419',
'es-mx': 'es_419',
'es-gt': 'es_419',
'es-cr': 'es_419',
'es-pa': 'es_419',
'es-do': 'es_419',
'es-ve': 'es_419',
'es-co': 'es_419',
'es-pe': 'es_419',
'es-ar': 'es_419',
'es-ec': 'es_419',
'es-cl': 'es_419',
'es-uy': 'es_419',
'es-py': 'es_419',
'es-bo': 'es_419',
'es-sv': 'es_419',
'es-hn': 'es_419',
'es-ni': 'es_419',
'es-pr': 'es_419',
};
var chineseVersions = ['zh-tw'];
var chineseVersions = {
'zh-tw': 'zh_TW',
};
var getUserLanguage = function(req, res, next){
var getFromBrowser = function(){
@@ -97,10 +117,9 @@ var getUserLanguage = function(req, res, next){
}
if(matches[0] === 'es'){
return (latinAmericanSpanishes.indexOf(acceptedCompleteLang) !== -1) ? 'es_419' : 'es';
return latinAmericanSpanishes[acceptedCompleteLang] || 'es';
}else if(matches[0] === 'zh'){
var iChinese = chineseVersions.indexOf(acceptedCompleteLang.toLowerCase());
return (iChinese !== -1) ? chineseVersions[iChinese] : 'zh';
return chineseVersions[acceptedCompleteLang] || 'zh';
}else{
return en;
}
@@ -158,4 +177,4 @@ module.exports.enTranslations = function(){ // stringName and vars are the allow
var args = Array.prototype.slice.call(arguments, 0);
args.push(language.code);
return shared.i18n.t.apply(null, args);
};
};
@@ -10,7 +10,6 @@ import {
import { content as Content } from '../../../../common';
require('coffee-script');
require('../../libs/i18n');
const AMPLIUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
const GA_TOKEN = nconf.get('GA_ID');
+118
View File
@@ -0,0 +1,118 @@
import fs from 'fs';
import path from 'path';
import _ from 'lodash';
import shared from '../../../../common';
export const localePath = path.join(__dirname, '/../../../../common/locales/');
// Store translations
export let translations = {};
// Store MomentJS localization files
export let momentLangs = {};
// Handle differencies in language codes between MomentJS and /locales
let momentLangsMapping = {
en: 'en-gb',
en_GB: 'en-gb', // eslint-disable-line camelcase
no: 'nn',
zh: 'zh-cn',
es_419: 'es', // eslint-disable-line camelcase
};
function _loadTranslations (locale) {
let files = fs.readdirSync(path.join(localePath, locale));
translations[locale] = {};
files.forEach((file) => {
if (path.extname(file) !== '.json') return;
// We use require to load and parse a JSON file
_.merge(translations[locale], require(path.join(localePath, locale, file))); // eslint-disable-line global-require
});
}
// First fetch English strings so we can merge them with missing strings in other languages
_loadTranslations('en');
// Then load all other languages
fs.readdirSync(localePath).forEach((file) => {
if (file === 'en' || fs.statSync(path.join(localePath, file)).isDirectory() === false) return;
_loadTranslations(file);
// Merge missing strings from english
_.defaults(translations[file], translations.en);
});
// Add translations to shared
shared.i18n.translations = translations;
export let langCodes = Object.keys(translations);
export let avalaibleLanguages = langCodes.map((langCode) => {
return {
code: langCode,
name: translations[langCode].languageName,
};
});
langCodes.forEach((code) => {
let lang = _.find(avalaibleLanguages, {code});
lang.momentLangCode = momentLangsMapping[code] || code;
try {
// MomentJS lang files are JS files that has to be executed in the browser so we load them as plain text files
// We wrap everything in a try catch because the file might not exist
let f = fs.readFileSync(path.join(__dirname, `/../../../node_modules/moment/locale/${lang.momentLangCode}.js`), 'utf8');
momentLangs[code] = f;
} catch (e) { // eslint-disable-lint no-empty
// TODO implement some type of error loggin?
// The catch block is mandatory so can't be removed
}
});
// Remove en_GB from langCodes checked by browser to avoid it being
// used in place of plain original 'en' (it's an optional language that can be enabled only in setting)
export let defaultLangCodes = _.without(langCodes, 'en_GB');
// A map of languages that have different versions and the relative versions
export let multipleVersionsLanguages = {
es: {
'es-419': 'es_419',
'es-mx': 'es_419',
'es-gt': 'es_419',
'es-cr': 'es_419',
'es-pa': 'es_419',
'es-do': 'es_419',
'es-ve': 'es_419',
'es-co': 'es_419',
'es-pe': 'es_419',
'es-ar': 'es_419',
'es-ec': 'es_419',
'es-cl': 'es_419',
'es-uy': 'es_419',
'es-py': 'es_419',
'es-bo': 'es_419',
'es-sv': 'es_419',
'es-hn': 'es_419',
'es-ni': 'es_419',
'es-pr': 'es_419',
},
zh: {
'zh-tw': 'zh_TW',
},
};
// Export en strings only, temporary solution for mobile
// This is copied from middlewares/locals#t()
// TODO review if this can be removed since the old mobile app is no longer active
// stringName and vars are the allowed parameters
export function enTranslations (...args) {
let language = _.find(avalaibleLanguages, {code: 'en'});
// language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
args.push(language.code);
return shared.i18n.t(...args);
}
@@ -0,0 +1,79 @@
import { model as User } from '../../models/user';
import accepts from 'accepts';
import _ from 'lodash';
import {
translations,
defaultLangCodes,
multipleVersionsLanguages,
} from '../../libs/api-v3/i18n';
function _getUniqueListOfLanguages (languages) {
let acceptableLanguages = _(languages).map((lang) => {
return lang.slice(0, 2);
}).uniq().value();
let uniqueListOfLanguages = _.intersection(acceptableLanguages, defaultLangCodes);
return uniqueListOfLanguages;
}
function _checkForApplicableLanguageVariant (originalLanguageOptions) {
let languageVariant = _.find(originalLanguageOptions, (accepted) => {
let trimmedAccepted = accepted.slice(0, 2);
return multipleVersionsLanguages[trimmedAccepted];
});
return languageVariant;
}
function _getFromBrowser (req) {
let originalLanguageOptions = accepts(req).languages();
let uniqueListOfLanguages = _getUniqueListOfLanguages(originalLanguageOptions);
let baseLanguage = (uniqueListOfLanguages[0] || '').toLowerCase();
let languageMapping = multipleVersionsLanguages[baseLanguage];
if (languageMapping) {
let languageVariant = _checkForApplicableLanguageVariant(originalLanguageOptions);
if (languageVariant) {
languageVariant = languageVariant.toLowerCase();
} else {
return 'en';
}
return languageMapping[languageVariant] || baseLanguage;
} else {
return baseLanguage || 'en';
}
}
function _getFromUser (user, req) {
let preferredLang = user && user.preferences && user.preferences.language;
let lang = translations[preferredLang] ? preferredLang : _getFromBrowser(req);
return lang;
}
export default function getUserLanguage (req, res, next) {
if (req.query.lang) { // In case the language is specified in the request url, use it
req.language = translations[req.query.lang] ? req.query.lang : 'en';
return next();
} else if (req.locals && req.locals.user) { // If the request is authenticated, use the user's preferred language
req.language = _getFromUser(req.locals.user, req);
return next();
} else if (req.session && req.session.userId) { // Same thing if the user has a valid session
User.findOne({
_id: req.session.userId,
}, 'preferences.language')
.exec()
.then((user) => {
req.language = _getFromUser(user, req);
return next();
})
.catch(next);
} else { // Otherwise get from browser
req.language = _getFromUser(null, req);
return next();
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ var nconf = require('nconf');
var _ = require('lodash');
var utils = require('../libs/utils');
var shared = require('../../../common');
var i18n = require('../libs/i18n');
var i18n = require('../libs/api-v2/i18n');
var buildManifest = require('../libs/buildManifest');
var shared = require('../../../common');
var forceRefresh = require('./forceRefresh');
+1 -1
View File
@@ -1,6 +1,6 @@
var auth = require('../../controllers/api-v2/auth');
var express = require('express');
var i18n = require('../../libs/i18n');
var i18n = require('../../libs/api-v2/i18n');
var router = new express.Router();
/* auth.auth*/
+1 -1
View File
@@ -3,7 +3,7 @@ var express = require('express');
var router = new express.Router();
var auth = require('../../controllers/api-v2/auth');
var coupon = require('../../controllers/api-v2/coupon');
var i18n = require('../../libs/i18n');
var i18n = require('../../libs/api-v2/i18n');
router.get('/api/v2/coupons', auth.authWithUrl, i18n.getUserLanguage, coupon.ensureAdmin, coupon.getCoupons);
router.post('/api/v2/coupons/generate/:event', auth.auth, i18n.getUserLanguage, coupon.ensureAdmin, coupon.generateCoupons);
+1 -1
View File
@@ -18,7 +18,7 @@ var nconf = require("nconf");
var cron = user.cron;
var _ = require('lodash');
var content = require('../../../../common').content;
var i18n = require('../../libs/i18n');
var i18n = require('../../libs/api-v2/i18n');
var forceRefresh = require('../../middlewares/forceRefresh').middleware;
module.exports = function(swagger, v2) {
+1 -1
View File
@@ -1,6 +1,6 @@
var express = require('express');
var router = new express.Router();
var i18n = require('../../libs/i18n');
var i18n = require('../../libs/api-v2/i18n');
var unsubscription = require('../../controllers/api-v2/unsubscription');
router.get('/unsubscribe', i18n.getUserLanguage, unsubscription.unsubscribe);
+1 -1
View File
@@ -3,7 +3,7 @@ var router = new express.Router();
var dataexport = require('../controllers/dataexport');
var auth = require('../controllers/api-v2/auth');
var nconf = require('nconf');
var i18n = require('../libs/i18n');
var i18n = require('../libs/api-v2/i18n');
var locals = require('../middlewares/locals');
/* Data export */
+1 -1
View File
@@ -3,7 +3,7 @@ var express = require('express');
var router = new express.Router();
var _ = require('lodash');
var locals = require('../middlewares/locals');
var i18n = require('../libs/i18n');
var i18n = require('../libs/api-v2/i18n');
// -------- App --------
router.get('/', i18n.getUserLanguage, locals, function(req, res) {
+1 -1
View File
@@ -3,7 +3,7 @@ var express = require('express');
var router = new express.Router();
var auth = require('../controllers/api-v2/auth');
var payments = require('../controllers/payments');
var i18n = require('../libs/i18n');
var i18n = require('../libs/api-v2/i18n');
router.get('/paypal/checkout', auth.authWithUrl, i18n.getUserLanguage, payments.paypalCheckout);
router.get('/paypal/checkout/success', i18n.getUserLanguage, payments.paypalCheckoutSuccess);
+1 -1
View File
@@ -17,7 +17,7 @@ import attachMiddlewares from './middlewares/api-v3/index';
utils.setupConfig();
// Setup translations
// let i18n = require('./libs/i18n');
// let i18n = require('./libs/api-v2/i18n');
const IS_PROD = nconf.get('IS_PROD');
// const IS_DEV = nconf.get('IS_DEV');