UNPKG

indigojs

Version:

IndigoJS is an open source, JavaScript/NodeJS localization framework

310 lines (279 loc) 8.49 kB
'use strict'; const indigo = global.__indigo, fs = require('fs'), rules = require('./locales/accept-rules.json'), cjson = require('cjson'), cookieName = 'localeCode', reCookie = RegExp(`(?:^|;\\s*)${cookieName}=([^;]*)`), defaultLocale = 'en-us'; /** * This module exposes the locale-determination logic for the implementation of resource * bundles that need to produce localized messages. * * @version 1.0 * * @module * @mixin libs/locales */ const locales = () => { /** @lends libs/locales.prototype */ return { /** * Collection of localization objects where key points to locale code and value map * of key and value of localization messages. * @memberof libs/locales * @alias localeMap * @type {Object} */ localeMap: {}, /** * Collection of expections during parsering the locale files, where key is file * name and value is error object. * @memberof libs/locales * @alias errorFiles * @type {Object} */ errorFiles: {}, /** * Default language code * @memberof libs/locales * @alias defaultLocale * @type {String} */ defaultLocale, /** * Map of locale files modified dates * @memberof libs/locales * @alias lastModified * @type {Object} */ lastModified: {}, /** * This method is executed once reading <code>locales</code> setting defined in appconf.json * and building tree of locale messages <code>localeMap</code> at start time. * * @memberof libs/locales * @alias config * * @example * conf/app.json *{ * ... * "locales": { * "default": "en-us", * "path": "/src/locales" * } * ... *} * @param {Object} appconf JSON object represents application configuration. */ config(appconf) { this.defaultLocale = appconf.get('locales:default') || defaultLocale; this.lastModified = {}; this.errorFiles = {}; this.localeMap = {}; this.localeMap[this.defaultLocale] = { __lookup__: [], __localName__: this.defaultLocale }; const localeDir = __appDir + (appconf.get('server:moduleDir') || '') + appconf.get('locales:path'); if (fs.existsSync(localeDir)) { const dirs = fs.readdirSync(localeDir); for (let d in dirs) { let localeName = dirs[d]; if (fs.lstatSync(`${localeDir}/${localeName}`).isDirectory()) { this.localeMap[localeName] = { __lookup__: [], __localName__:localeName }; this.parse(`${localeDir}/${localeName}`, this.localeMap[localeName]); } } } localelookup(localeDir, this); }, /** * Loads locale files and builds a locale object. * * @memberof libs/locales * @alias parse * * @param {String} dirName Absolute path to json files. * @param {Object} parent Locales properties. */ parse(dirName, parent) { const files = fs.readdirSync(dirName); for (let f in files) { let file = files[f], arr = file.split('.'); if (arr.length > 1 && arr[1] === 'json') { file = `${dirName}/${file}`; try { parent[arr[0]] = cjson.load(file); this.lastModified[file] = fs.lstatSync(file).mtime; } catch (e) { this.errorFiles[file] = e; indigo.logger.error('FILE: %s, ERROR: %s', file, e.toString()); } } else if (fs.lstatSync(`${dirName}/${file}`).isDirectory()) { parent[file] = {}; this.parse(`${dirName}/${file}`, parent[file]); } } }, /** * Initializing user locale by using request header. * * @memberof libs/locales * @alias headerLocale * * @param {express.Request} req Defines an object to provide client request information. * @return {String} locale Return the router locale name. */ headerLocale(req) { if (req.headers) { if (req.headers.cookie) { const match = req.headers.cookie.match(reCookie); if (match) { return setLocality(req, match[1], this); //set from cookies } } if (req.headers['accept-language']) { const split = req.headers['accept-language'].split(';'); // en-us,en-au;q=0.8,en;q=0.5,ru;q=0.3 for (let value in split) { const languages = split[value].split(','); for (let name in languages) { let locale = languages[name].toLowerCase(); if (!locale.includes('q=') && this.localeMap[locale]) { return setLocality(req, locale, this); } } } } } return setLocality(req, this.defaultLocale, this); }, /** * Initializes the current user locale and returns a map of localized messages. * * @memberof libs/locales * @alias headerLocale * * @param {express.Request} req Defines an object to provide client request information. * @param {String} [locale] User language code. * @return {Object} locale Collection of localization messages. */ routeLocale(req, locale) { req.model.locality = req.model.locality || {}; return setLocality(req, locale || req.model.locality.locale, this); }, /** * Monitor changes in locale file. * * @memberof libs/locales * @alias headerLocale * * @param {Object} appconf JSON object represents application configuration. */ monitor(appconf) { const seconds = appconf.get('locales:monitor'); if (!seconds) { return; } clearInterval(this.lastModifiedInterval); this.lastModifiedInterval = setInterval(() => { for (let file in this.lastModified) { if (this.lastModified[file].getTime() !== fs.lstatSync(file).mtime.getTime()) { indigo.logger.info('File updated: ' + file); this.config(appconf); return; } } }, seconds * 1000); } }; }; /** * Save current language information to <code>req.model.locality</code>. * @memberof libs/locales.prototype * @param {express.Request} req Defines an object to provide client request information. * @param {String} locale User language code. * @param {libs/locales} locales instance class. * @access protected */ const setLocality = (req, locale, locales) => { locale = locales.localeMap[locale] ? locale : locales.defaultLocale; if (req.res && req.res.cookie) { req.res.cookie(cookieName, locale); } const userLocale = locales.localeMap[locale]; req.model.locality = { localeLookup: userLocale.__lookup__, langugage: userLocale.__localName__, locale: locale }; req.model.locales = userLocale; return userLocale; }; /** * Traverse all locales files under locale directory. * @memberof libs/locales.prototype * @param {String} localeDir Absolute path to <code>locale</code> directory. * @access protected */ const localelookup = (localeDir, locales) => { const file = `${localeDir}/accept-rules.json`; if (fs.existsSync(file)) { const customRules = cjson.load(file); for (let code in customRules) { rules[code] = customRules[code]; } } const traverse = code => { const target = locales.localeMap[code] = locales.localeMap[code] || { __lookup__: [], __localName__:code }; let locale, lookup = rules[code]; for (let index in lookup) { locale = lookup[index]; if (target.__lookup__.indexOf(locale) === -1) { target.__lookup__.push(locale); } } for (let index in lookup) { locale = lookup[index]; let source = locales.localeMap[locale], sourceLocale = true; if (!source) { source = traverse(locale); } for (let i in source.__lookup__) { locale = source.__lookup__[i]; if (locale && target.__lookup__.indexOf(locale) === -1) { target.__lookup__.push(locale); } } for (let name in source) { if (name !== '__localName__' && name !== '__lookup__') { if (!target[name]) { target[name] = source[name]; } else { //copy only missing key/values sourceLocale = false; for (let key in source[name]) { if (!target[name][key]) { target[name][key] = source[name][key]; } } } } } if (sourceLocale) { target.__localName__ = source.__localName__; } } if (target.__lookup__.indexOf('default') === -1) { target.__lookup__.push('default'); } return target; }; for (let code in rules) { traverse(code); } }; /** * @module locales * @see {@link libs/locales} */ module.exports = locales;