diff --git a/website/src/libs/api-v3/i18n.js b/website/src/libs/api-v3/i18n.js new file mode 100644 index 0000000000..96ad15a2e7 --- /dev/null +++ b/website/src/libs/api-v3/i18n.js @@ -0,0 +1,98 @@ +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; +import shared from '../../../../common'; + +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-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'], + zh: ['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); +} \ No newline at end of file diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js new file mode 100644 index 0000000000..9a273b79bf --- /dev/null +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -0,0 +1,85 @@ +import { model as User } from '../../models/user'; +import accepts from 'accepts'; +import _ from 'lodash'; +import { + translations, + defaultLangCodes, + multipleVersionsLanguages, +} from '../../libs/api-v3/i18n'; + +function _getFromBrowser (req) { + let acceptedLanguages = accepts(req).languages(); + + let acceptable = _(acceptedLanguages).map((lang) => { + return lang.slice(0, 2); + }).uniq().value(); + + let matches = _.intersection(acceptable, defaultLangCodes); + + let iAcceptedCompleteLang = matches.length > 0 ? multipleVersionsLanguages.indexOf(matches[0].toLowerCase()) : -1; + + if (iAcceptedCompleteLang !== -1) { + let acceptedCompleteLang = _.find(acceptedLanguages, (accepted) => { + return accepted.slice(0, 2) === multipleVersionsLanguages[iAcceptedCompleteLang]; + }); + + if (acceptedCompleteLang) { + acceptedCompleteLang = acceptedCompleteLang.toLowerCase(); + } else { + return 'en'; + } + + if (matches[0] === 'es') { + // In case of a Latin American version of Spanish use 'es_419' + return multipleVersionsLanguages.es.indexOf(acceptedCompleteLang !== -1) ? 'es_419' : 'es'; + } else if (matches[0] === 'zh') { + let iChinese = multipleVersionsLanguages.zh.indexOf(acceptedCompleteLang.toLowerCase()); + + return iChinese !== -1 ? multipleVersionsLanguages.zh[iChinese] : 'zh'; + } else { + return 'en'; + } + } else if (matches.length > 0) { + return matches[0].toLowerCase(); + } else { + return 'en'; + } +} + +function _getFromUser (user, req) { + let lang; + + if (user && user.preferences.language && translations[user.preferences.language]) { + lang = user.preferences.language; + } else { + let preferred = _getFromBrowser(req); + + lang = translations[preferred] ? preferred : 'en'; + } + + 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(); + } +} \ No newline at end of file