express-tongue
Version:
Localization library for Express. It works with every template engine and exposes a json endpoint to consume on client side.
277 lines (255 loc) • 9.57 kB
JavaScript
/**
* Express middleware to handle automatic localization.
*
* @author: Carlos Luis Castro Márquez
*/
;
// Environment configuration
const env = process.env.NODE_ENV || "development";
const DEBUG = env === "development";
const DEBUG_SIGNATURE = "i18n";
// Module requires
const _ = require("lodash");
const fs = require("fs");
const path = require("path");
const express = require("express");
const debug = require("debug")(DEBUG_SIGNATURE);
// Module exports
exports.localize = localizeMiddleware;
/**
* Generates an Express middleware to handle sites in multiple languages.
*
* This middleware injects the variable i18n inside res.locals, for usage inside template engines. Also is you are
* planning to use localization on client side; for example in SPA, then you have several endpoints which helps
* you to develop a fully localized page.
*
* ### Options:
*
* - `defaultLang` {String} Default language abbreviation. Defaults to `en`.
* - `path` {String} Directory where localization files are located. Defaults to `WORKING_DIR/i18n`.
* - `languages` {Array} Replace available languages for specified. This is useful when you want
* to deactivate some language
* - `endpointEnabled` {Boolean} Enables API endpoint for usage in client apps. Defaults to `false`
* - `endpointPath` {String} Route in which endpoint will be mounted. Defaults to `/i18n`
* - `queryStringEnabled` {Boolean} Allows language replacement by setting key in querystring.
* - `queryStringKey` {String} Setup key used in querystring to define current language. Defaults to `hl`.
* - `langCookie` {String} Cookie name for storing current locale.
*
* @param {Object} options
* @returns {Function} Express middleware function.
*/
/* jshint maxcomplexity: 12 */
/* jshint -W071 */
function localizeMiddleware(options) {
// Set default options if not present
options = options || {};
options.defaultLang = options.defaultLang || "en";
options.path = options.path || path.join(process.cwd(), "i18n");
options.languages = options.languages || mapDirectoryFiles();
options.endpointEnabled = options.endpointEnabled || false;
options.endpointPath = options.endpointPath || "/i18n";
options.queryStringEnabled = options.queryStringEnabled || false;
options.queryStringKey = options.queryStringKey || "hl";
options.langCookie = options.langCookie || "xp_i18n_lang";
debug("Configuration options:", options);
// Private fields
var languagesConfigured = options.languages;
var defaultLangAbbr = options.defaultLang;
var languagesPath = options.path;
var resources = {};
var defaultLang = {};
var languages = [];
var cookieTTL = 365 * 24 * 3600000; // 1 year
/**
* Maps language files in i18n directory
*
* @returns {Array} Array of available languages in directory.
* */
function mapDirectoryFiles() {
let available = [];
fs.readdirSync(options.path)
.filter(function (file) {
return (
file.indexOf(".") !== 0 &&
file.indexOf(".json") === file.length - 5
);
})
.forEach(function (file) {
available.push(file.substring(0, file.indexOf(".")));
});
debug("Languages in directory:", available);
return available;
}
/**
* Initializes localization engine.
* */
function initialize() {
// Build default lang
debug("Building default language:", defaultLangAbbr);
defaultLang = buildLang(defaultLangAbbr);
// Build all these languages
languagesConfigured.forEach(function (lang) {
if (lang !== defaultLangAbbr) {
debug("Building lang:", lang);
buildLang(lang);
}
});
languages = _.map(resources, "lang");
}
/**
* Builds path for a language.
*
* @param {String} lang Language abbreviation.
* @returns {String} Path to language file.
* */
function buildLangPath(lang) {
// Secure with realpath to avoid wrong absolute uri
return fs.realpathSync(path.join(languagesPath, lang + ".json"));
}
/**
* Builds localization data for language.
*
* @param {String} lang Language abbreviation.
* @returns {Object} Language object.
* */
function buildLang(lang) {
// Checks if lang is already build
if (resources[lang] !== undefined) {
return false;
}
// Get lang path
var langPath = buildLangPath(lang);
// Get key values from file, and overrides an empty object for security
var keyValues = _.extend({}, require(langPath));
// Set strings from default language if does not exists this key in language selected.
keyValues.strings = _.defaults(keyValues.strings, defaultLang.strings);
// Set language data in cache.
resources[lang] = keyValues;
// Return lang dictionary
return keyValues;
}
/**
* Gets localization data for specified language
*
* @param {String} langString Language abbreviation.
* @returns {Object} Localization data object.
* */
function getLocaleData(langString) {
if (!_.isString(langString)) {
throw new TypeError("Wrong method input!");
}
// If string get language data
return getLang(langString);
}
/**
* Get resources for a given language
*
* @param {String} lang Language abbreviation.
* @returns {Object} Localization resources.
* */
function getLang(lang) {
// If there are available resources for the language given returns it
if (_.has(resources, lang)) {
return resources[lang];
}
// Otherwise return default resources
return defaultLang;
}
/**
* Get languages available from strings.
*
* @returns {Array} Array with information referred to loaded languages.
* */
function getLanguages() {
return _.map(resources, function (item) {
return { name: item.language, value: item.lang };
});
}
/**
* Serves locale data in res.locals.i18n.
*
* @param {Object} req Express request.
* @param {Object} res Express response.
* @param {Function} next Next call in express pipeline.
* */
function serveLocaleData(req, res, next) {
res.locals.i18n = {};
const i18nQueryKey = options.queryStringKey;
const queryValue = req.query[i18nQueryKey];
if (req.user && req.user.language) {
debug("Setting according to user language.");
res.locals.i18n = getLocaleData(req.user.language);
} else if (req.query && queryValue && queryValue.length === 2) {
res.locals.i18n = getLocaleData(queryValue);
res.cookie(options.langCookie, queryValue, {
maxAge: cookieTTL,
});
} else if (req.cookies && req.cookies[options.langCookie]) {
debug("Setting according to cookie language.");
res.locals.i18n = getLocaleData(req.cookies[options.langCookie]);
} else {
debug("Setting according to User-Agent.");
// Ask express for language accepted according to request
let accepted = req.acceptsLanguages(languages);
// If there is no one accepted then select default
if (!accepted) {
debug(
"User-Agent does not accept an language, setting default instead."
);
accepted = defaultLangAbbr;
} else {
debug("User-Agent accepts:", accepted);
}
res.locals.i18n = getLocaleData(accepted);
}
next();
}
/**
* Serves all i18n resources.
*
* @param {Object} req Express request.
* @param {Object} res Express response.
* @param {Function} next Next call in express pipeline.
* */
function serveResources(req, res, next) {
return res.json(res.locals.i18n);
}
/**
* Serves i18n string resources.
*
* @param {Object} req Express request.
* @param {Object} res Express response.
* @param {Function} next Next call in express pipeline.
* */
function serveStrings(req, res, next) {
return res.json(res.locals.i18n.strings);
}
/**
* Serves i18n available languages.
*
* @param {Object} req Express request.
* @param {Object} res Express response.
* @param {Function} next Next call in express pipeline.
* */
function serveLanguages(req, res, next) {
return res.json(getLanguages());
}
// Initialize engine
initialize();
// Builds sub router and returns as middleware
var router = express.Router();
router.all("*", serveLocaleData);
if (options.endpointEnabled) {
debug("Setting i18n endpoint!");
const endpointPath = options.endpointPath;
const lastSlash = endpointPath.lastIndexOf("/");
if (lastSlash === endpointPath.length - 1) {
endpointPath.substring(0, lastSlash);
}
router.get(endpointPath, serveResources);
router.get(endpointPath + "/strings", serveStrings);
router.get(endpointPath + "/languages", serveLanguages);
}
return router;
}
(function (i18n) {})(module.exports);