loc
Version:
lightweight simple translation module with dynamic json storage
260 lines (225 loc) • 7.62 kB
JavaScript
/**
* @author Andi Neck <andi.neck@intesso.com>
* @author John Resig <jeresig@gmail.com>
* @author Originally by Marcus Spiegel <marcus.spiegel@gmail.com>
* @link https://github.com/intesso/loc
* @license http://opensource.org/licenses/MIT
*
*/
/**
* dependencies
*/
var vsprintf = require('sprintf').vsprintf;
var utils = require('./utils');
var debug = utils.debug;
/**
* Expose `Translation`.
*/
exports = module.exports = Translation;
/**
* Translation constructor function
* @param {Object} options
*/
function Translation(i18n, req, res) {
var self = this;
this.i18n = i18n;
if(!req || !res) return this;
this.applyLocaleStrategy(req, res);
if (res.locals) {
this.registerMethods(res.locals, req);
}
return this;
};
exports.pathRewrite = function(i18n) {
var path = i18n.strategies['path'];
// problematic are static assets and url's that start with one to three letters in the path like /p/, /js/ or /img/
this.excludeList = this.excludeList || ['.css', '.js', '.ico', '/img/', '/css/', '/js/', '/rss/', '/api/'];
return function pathRewrite(req, res, next) {
debug('pathRewriteMiddleware', req.url, res.locals.getLocale());
var url = req.url
if (!url || !res.locals || !res.locals.getLocale) return next();
if (!utils.acceptsHtmlExplicit(req) || utils.isExcluded(url, i18n.excludeList)) return next();
debug('pathRewriteMiddleware pattern', path.pattern, url, path.pattern.test(url))
// extract the locale segment from the url if it is stored in the path
if (path.pattern.test(url)) {
req.url = url.replace(RegExp.$1 + '/', '');
debug('locale: ' + res.locals.getLocale());
}
debug('pathRewriteMiddleware AFTER', url, req.url)
next();
}
};
Translation.prototype.applyLocaleStrategy = function(req, res) {
// Set the locale to the prefered locale (acceptedLanguage) of the user
this.setPreferredLocale(req);
if (req && req.accepts('html') && this.i18n.getLocaleFrom) {
var locale;
// getLocaleFrom
var from = this.i18n.getLocaleFrom;
for (var i = 0; i < from.length; i++) {
var strategy = this.getStrategy(from[i]);
if (!strategy.getLocaleFrom) {
debug('strategy not valid, function getLocaleFrom is missing.', strategy);
continue;
}
locale = strategy.getLocaleFrom(req);
if (locale) break;
}
// storeLocaleTo
if (locale) {
this.setLocale(locale);
var to = this.i18n.storeLocaleTo;
for (var i = 0; i < to.length; i++) {
var strategy = this.getStrategy(to[i]);
if (!strategy.storeLocaleTo) {
debug('strategy not valid, function storeLocaleTo is missing.', strategy);
continue;
}
locale = strategy.storeLocaleTo(req, res, locale);
if (locale) break;
}
}
}
if (!this.locale) this.setLocale(this.preferredLocale);
};
Translation.prototype.registerMethods = function(helpers, req) {
var self = this;
this.i18n.resMethods.forEach(function(method) {
helpers[method] = self[method].bind(self);
});
return helpers;
};
Translation.prototype.t =
Translation.prototype.__ = function() {
var msg = this.translate(this.locale, arguments[0], undefined, true);
if (arguments.length > 1) {
msg = vsprintf(msg, Array.prototype.slice.call(arguments, 1));
}
return msg;
};
Translation.prototype.tn =
Translation.prototype.__n = function(singular, plural, count) {
var msg = this.translate(this.locale, singular, plural, true);
msg = vsprintf(parseInt(count, 10) > 1 ? msg.other : msg.one, [count]);
if (arguments.length > 3) {
msg = vsprintf(msg, Array.prototype.slice.call(arguments, 3));
}
return msg;
};
// find exact locale
Translation.prototype.findLocale = function(locale) {
if (!locale) return false;
locale = locale.toLowerCase();
if (this.i18n.localeCache[locale]) {
return locale;
}
return false;
};
// find locale or the sublocale e.g. en-GB -> finds en-GB but also en if available (incremental fallback)
Translation.prototype.findSubLocale = function(locale) {
if (!locale) return false;
var primary = this.findLocale(locale);
if (primary) return primary;
return this.getSubLocale(locale);
};
// find the sublocale e.g. en-GB -> finds en if available (incremental fallback)
Translation.prototype.getSubLocale = function(locale) {
if (!locale) return false;
var foundLocale = false;
while (~locale.indexOf('-')) {
var index = locale.lastIndexOf('-');
locale = locale.substring(0, index);
foundLocale = this.findLocale(locale);
if (foundLocale) break;
}
return foundLocale;
};
// find the closest locale.
// first try the exact one,
// then try the sub locale,
// otherwise do the same for the preferredLocales (acceptedLanguages) of the user.
Translation.prototype.findClosestLocale = function(locale) {
if (!locale) return false;
var translation = this.findSubLocale(locale);
if (translation) return translation;
for (var i = 0; this.preferredLocales.length; i++) {
var closest = this.preferredLocales[i];
var translation = this.findSubLocale(closest);
if (translation) return translation;
}
return this.i18n.defaultLocale;
};
Translation.prototype.setLocale = function(locale) {
if (!locale) return false;
this.locale = this.findClosestLocale(locale);
debug('setLocale', this.locale, locale);
};
Translation.prototype.getLocale = function() {
return this.locale;
};
Translation.prototype.setPreferredLocale = function(req) {
if (!req) return;
var acceptedLanguages = this.preferredLocales = (req.acceptedLanguages) ? req.acceptedLanguages : req.acceptsLanguages();
if (!acceptedLanguages || !acceptedLanguages.length || acceptedLanguages.length < 1) return;
this.preferredLocale = acceptedLanguages[0];
};
Translation.prototype.isPreferredLocale = function() {
return !this.preferredLocale || this.preferredLocale === this.getLocale();
};
// read locale file, translate a msg and write to fs if new
Translation.prototype.translate = function(locale, singular, plural, insert) {
var self = this;
var localeCache = this.i18n.localeCache;
if (!locale || !localeCache[locale]) {
locale = this.findClosestLocale(locale);
//localeCache[locale] = localeCache[locale] || {};
debug('WARN: No locale definition found. Using the locale (' + locale + ') as current locale');
}
var context = utils.extractContext(singular);
var key = utils.extractKey(singular);
var sublocale = locale;
var keyPath, obj;
// find the translation with locale fallback
while (true) {
var keyPath = utils.getKeyPath(sublocale, singular);
var obj = utils.getObj(localeCache, keyPath);
if (obj) break;
sublocale = this.getSubLocale(sublocale);
if (!sublocale) break;
}
if (!obj && insert) {
var val = plural ? {
one: key,
other: plural
} : key;
keyPath = utils.getKeyPath(locale, singular);
utils.setObj(localeCache, keyPath, val);
// write locale due to new locale entry
if (this.i18n.devMode) {
debug("Mr Write", locale, context, val);
this.i18n.write(locale, context, val);
}
debug('INFO: added new locale definition entry', locale, keyPath, obj);
}
return obj;
};
Translation.prototype.getStrategy = function(strategy) {
var defaultStrategy = {
name: 'default',
getLocaleFrom: function(req) {
return false;
},
storeLocaleTo: function(req, locale) {
debug('I18n default strategy. function storeLocaleTo not implemented');
}
}
// try loading the strategy
try {
var loaded = utils.getDependency(this.i18n.strategies, strategy);
if (loaded.fieldname && this.i18n.fieldname) loaded.fieldname = this.i18n.fieldname;
return loaded;
} catch (err) {
debug('strategy could not be loaded', strategy);
return defaultStrategy;
}
};