Current File : /home/opti/workflow/node_modules/i18n/i18n.js
/**
* @author Created by Marcus Spiegel <marcus.spiegel@gmail.com> on 2011-03-25.
* @link https://github.com/mashpie/i18n-node
* @license http://opensource.org/licenses/MIT
*
* @version 0.8.3
*/
'use strict';
// dependencies
var vsprintf = require('sprintf-js').vsprintf,
fs = require('fs'),
url = require('url'),
path = require('path'),
debug = require('debug')('i18n:debug'),
warn = require('debug')('i18n:warn'),
error = require('debug')('i18n:error'),
Mustache = require('mustache'),
Messageformat = require('messageformat'),
MakePlural = require('make-plural'),
parseInterval = require('math-interval-parser').default;
// exports an instance
module.exports = (function() {
var MessageformatInstanceForLocale = {},
PluralsForLocale = {},
locales = {},
api = {
'__': '__',
'__n': '__n',
'__l': '__l',
'__h': '__h',
'__mf': '__mf',
'getLocale': 'getLocale',
'setLocale': 'setLocale',
'getCatalog': 'getCatalog',
'getLocales': 'getLocales',
'addLocale': 'addLocale',
'removeLocale': 'removeLocale'
},
pathsep = path.sep, // ---> means win support will be available in node 0.8.x and above
autoReload,
cookiename,
defaultLocale,
directory,
directoryPermissions,
extension,
fallbacks,
indent,
logDebugFn,
logErrorFn,
logWarnFn,
preserveLegacyCase,
objectNotation,
prefix,
queryParameter,
register,
updateFiles,
syncFiles;
// public exports
var i18n = {};
i18n.version = '0.8.3';
i18n.configure = function i18nConfigure(opt) {
// reset locales
locales = {};
// Provide custom API method aliases if desired
// This needs to be processed before the first call to applyAPItoObject()
if (opt.api && typeof opt.api === 'object') {
for (var method in opt.api) {
if (opt.api.hasOwnProperty(method)) {
var alias = opt.api[method];
if (typeof api[method] !== 'undefined') {
api[method] = alias;
}
}
}
}
// you may register i18n in global scope, up to you
if (typeof opt.register === 'object') {
register = opt.register;
// or give an array objects to register to
if (Array.isArray(opt.register)) {
register = opt.register;
register.forEach(function(r) {
applyAPItoObject(r);
});
} else {
applyAPItoObject(opt.register);
}
}
// sets a custom cookie name to parse locale settings from
cookiename = (typeof opt.cookie === 'string') ? opt.cookie : null;
// query-string parameter to be watched - @todo: add test & doc
queryParameter = (typeof opt.queryParameter === 'string') ? opt.queryParameter : null;
// where to store json files
directory = (typeof opt.directory === 'string') ?
opt.directory : path.join(__dirname, 'locales');
// permissions when creating new directories
directoryPermissions = (typeof opt.directoryPermissions === 'string') ?
parseInt(opt.directoryPermissions, 8) : null;
// write new locale information to disk
updateFiles = (typeof opt.updateFiles === 'boolean') ? opt.updateFiles : true;
// sync locale information accros all files
syncFiles = (typeof opt.syncFiles === 'boolean') ? opt.syncFiles : false;
// what to use as the indentation unit (ex: "\t", " ")
indent = (typeof opt.indent === 'string') ? opt.indent : '\t';
// json files prefix
prefix = (typeof opt.prefix === 'string') ? opt.prefix : '';
// where to store json files
extension = (typeof opt.extension === 'string') ? opt.extension : '.json';
// setting defaultLocale
defaultLocale = (typeof opt.defaultLocale === 'string') ? opt.defaultLocale : 'en';
// auto reload locale files when changed
autoReload = (typeof opt.autoReload === 'boolean') ? opt.autoReload : false;
// enable object notation?
objectNotation = (typeof opt.objectNotation !== 'undefined') ? opt.objectNotation : false;
if (objectNotation === true) objectNotation = '.';
// read language fallback map
fallbacks = (typeof opt.fallbacks === 'object') ? opt.fallbacks : {};
// setting custom logger functions
logDebugFn = (typeof opt.logDebugFn === 'function') ? opt.logDebugFn : debug;
logWarnFn = (typeof opt.logWarnFn === 'function') ? opt.logWarnFn : warn;
logErrorFn = (typeof opt.logErrorFn === 'function') ? opt.logErrorFn : error;
preserveLegacyCase = (typeof opt.preserveLegacyCase === 'undefined') ?
true : opt.preserveLegacyCase;
// when missing locales we try to guess that from directory
opt.locales = opt.locales || guessLocales(directory);
// implicitly read all locales
if (Array.isArray(opt.locales)) {
opt.locales.forEach(function(l) {
read(l);
});
// auto reload locale files when changed
if (autoReload) {
// watch changes of locale files (it's called twice because fs.watch is still unstable)
fs.watch(directory, function(event, filename) {
var localeFromFile = guessLocaleFromFile(filename);
if (localeFromFile && opt.locales.indexOf(localeFromFile) > -1) {
logDebug('Auto reloading locale file "' + filename + '".');
read(localeFromFile);
}
});
}
}
};
i18n.init = function i18nInit(request, response, next) {
if (typeof request === 'object') {
// guess requested language/locale
guessLanguage(request);
// bind api to req
applyAPItoObject(request);
// looks double but will ensure schema on api refactor
i18n.setLocale(request, request.locale);
} else {
return logError('i18n.init must be called with one parameter minimum, ie. i18n.init(req)');
}
if (typeof response === 'object') {
applyAPItoObject(response);
// and set that locale to response too
i18n.setLocale(response, request.locale);
}
// head over to next callback when bound as middleware
if (typeof next === 'function') {
return next();
}
};
i18n.__ = function i18nTranslate(phrase) {
var msg;
var argv = parseArgv(arguments);
var namedValues = argv[0];
var args = argv[1];
// called like __({phrase: "Hello", locale: "en"})
if (typeof phrase === 'object') {
if (typeof phrase.locale === 'string' && typeof phrase.phrase === 'string') {
msg = translate(phrase.locale, phrase.phrase);
}
}
// called like __("Hello")
else {
// get translated message with locale from scope (deprecated) or object
msg = translate(getLocaleFromObject(this), phrase);
}
// postprocess to get compatible to plurals
if (typeof msg === 'object' && msg.one) {
msg = msg.one;
}
// in case there is no 'one' but an 'other' rule
if (typeof msg === 'object' && msg.other) {
msg = msg.other;
}
// head over to postProcessing
return postProcess(msg, namedValues, args);
};
i18n.__mf = function i18nMessageformat(phrase) {
var msg, mf, f;
var targetLocale = defaultLocale;
var argv = parseArgv(arguments);
var namedValues = argv[0];
var args = argv[1];
// called like __({phrase: "Hello", locale: "en"})
if (typeof phrase === 'object') {
if (typeof phrase.locale === 'string' && typeof phrase.phrase === 'string') {
msg = phrase.phrase;
targetLocale = phrase.locale;
}
}
// called like __("Hello")
else {
// get translated message with locale from scope (deprecated) or object
msg = phrase;
targetLocale = getLocaleFromObject(this);
}
msg = translate(targetLocale, msg);
// --- end get msg
// now head over to Messageformat
// and try to cache instance
if (MessageformatInstanceForLocale[targetLocale]) {
mf = MessageformatInstanceForLocale[targetLocale];
} else {
mf = new Messageformat(targetLocale);
// @see https://messageformat.github.io/messageformat/MessageFormat#disablePluralKeyChecks__anchor
mf.disablePluralKeyChecks();
mf.compiledFunctions = {};
MessageformatInstanceForLocale[targetLocale] = mf;
}
// let's try to cache that function
if (mf.compiledFunctions[msg]) {
f = mf.compiledFunctions[msg];
} else {
f = mf.compile(msg);
mf.compiledFunctions[msg] = f;
}
return postProcess(f(namedValues), namedValues, args);
};
i18n.__l = function i18nTranslationList(phrase) {
var translations = [];
Object.keys(locales).sort().forEach(function(l) {
translations.push(i18n.__({ phrase: phrase, locale: l }));
});
return translations;
};
i18n.__h = function i18nTranslationHash(phrase) {
var translations = [];
Object.keys(locales).sort().forEach(function(l) {
var hash = {};
hash[l] = i18n.__({ phrase: phrase, locale: l });
translations.push(hash);
});
return translations;
};
i18n.__n = function i18nTranslatePlural(singular, plural, count) {
var msg, namedValues, targetLocale, args = [];
// Accept an object with named values as the last parameter
if (argsEndWithNamedObject(arguments)) {
namedValues = arguments[arguments.length - 1];
args = arguments.length >= 5 ? Array.prototype.slice.call(arguments, 3, -1) : [];
} else {
namedValues = {};
args = arguments.length >= 4 ? Array.prototype.slice.call(arguments, 3) : [];
}
// called like __n({singular: "%s cat", plural: "%s cats", locale: "en"}, 3)
if (typeof singular === 'object') {
if (
typeof singular.locale === 'string' &&
typeof singular.singular === 'string' &&
typeof singular.plural === 'string'
) {
targetLocale = singular.locale;
msg = translate(singular.locale, singular.singular, singular.plural);
}
args.unshift(count);
// some template engines pass all values as strings -> so we try to convert them to numbers
if (typeof plural === 'number' || parseInt(plural, 10) + '' === plural) {
count = plural;
}
// called like __n({singular: "%s cat", plural: "%s cats", locale: "en", count: 3})
if (typeof singular.count === 'number' || typeof singular.count === 'string') {
count = singular.count;
args.unshift(plural);
}
} else {
// called like __n('cat', 3)
if (typeof plural === 'number' || parseInt(plural, 10) + '' === plural) {
count = plural;
// we add same string as default
// which efectivly copies the key to the plural.value
// this is for initialization of new empty translations
plural = singular;
args.unshift(count);
args.unshift(plural);
}
// called like __n('%s cat', '%s cats', 3)
// get translated message with locale from scope (deprecated) or object
msg = translate(getLocaleFromObject(this), singular, plural);
targetLocale = getLocaleFromObject(this);
}
if (count === null) count = namedValues.count;
// enforce number
count = parseInt(count, 10);
// find the correct plural rule for given locale
if (typeof msg === 'object') {
var p;
// create a new Plural for locale
// and try to cache instance
if (PluralsForLocale[targetLocale]) {
p = PluralsForLocale[targetLocale];
} else {
// split locales with a region code
var lc = targetLocale.toLowerCase().split(/[_-\s]+/)
.filter(function(el){ return true && el; });
// take the first part of locale, fallback to full locale
p = MakePlural[lc[0] || targetLocale];
PluralsForLocale[targetLocale] = p;
}
// fallback to 'other' on case of missing translations
msg = msg[p(count)] || msg.other;
}
// head over to postProcessing
return postProcess(msg, namedValues, args, count);
};
i18n.setLocale = function i18nSetLocale(object, locale, skipImplicitObjects) {
// when given an array of objects => setLocale on each
if (Array.isArray(object) && typeof locale === 'string') {
for (var i = object.length - 1; i >= 0; i--) {
i18n.setLocale(object[i], locale, true);
}
return i18n.getLocale(object[0]);
}
// defaults to called like i18n.setLocale(req, 'en')
var targetObject = object;
var targetLocale = locale;
// called like req.setLocale('en') or i18n.setLocale('en')
if (locale === undefined && typeof object === 'string') {
targetObject = this;
targetLocale = object;
}
// consider a fallback
if (!locales[targetLocale] && fallbacks[targetLocale]) {
targetLocale = fallbacks[targetLocale];
}
// now set locale on object
targetObject.locale = locales[targetLocale] ? targetLocale : defaultLocale;
// consider any extra registered objects
if (typeof register === 'object') {
if (Array.isArray(register) && !skipImplicitObjects) {
register.forEach(function(r) {
r.locale = targetObject.locale;
});
} else {
register.locale = targetObject.locale;
}
}
// consider res
if (targetObject.res && !skipImplicitObjects) {
// escape recursion
// @see - https://github.com/balderdashy/sails/pull/3631
// - https://github.com/mashpie/i18n-node/pull/218
if (targetObject.res.locals) {
i18n.setLocale(targetObject.res, targetObject.locale, true);
i18n.setLocale(targetObject.res.locals, targetObject.locale, true);
} else {
i18n.setLocale(targetObject.res, targetObject.locale);
}
}
// consider locals
if (targetObject.locals && !skipImplicitObjects) {
// escape recursion
// @see - https://github.com/balderdashy/sails/pull/3631
// - https://github.com/mashpie/i18n-node/pull/218
if (targetObject.locals.res) {
i18n.setLocale(targetObject.locals, targetObject.locale, true);
i18n.setLocale(targetObject.locals.res, targetObject.locale, true);
} else {
i18n.setLocale(targetObject.locals, targetObject.locale);
}
}
return i18n.getLocale(targetObject);
};
i18n.getLocale = function i18nGetLocale(request) {
// called like i18n.getLocale(req)
if (request && request.locale) {
return request.locale;
}
// called like req.getLocale()
return this.locale || defaultLocale;
};
i18n.getCatalog = function i18nGetCatalog(object, locale) {
var targetLocale;
// called like i18n.getCatalog(req)
if (typeof object === 'object' && typeof object.locale === 'string' && locale === undefined) {
targetLocale = object.locale;
}
// called like i18n.getCatalog(req, 'en')
if (!targetLocale && typeof object === 'object' && typeof locale === 'string') {
targetLocale = locale;
}
// called like req.getCatalog('en')
if (!targetLocale && locale === undefined && typeof object === 'string') {
targetLocale = object;
}
// called like req.getCatalog()
if (!targetLocale &&
object === undefined &&
locale === undefined &&
typeof this.locale === 'string'
) {
if (register && register.global) {
targetLocale = '';
} else {
targetLocale = this.locale;
}
}
// called like i18n.getCatalog()
if (targetLocale === undefined || targetLocale === '') {
return locales;
}
if (!locales[targetLocale] && fallbacks[targetLocale]) {
targetLocale = fallbacks[targetLocale];
}
if (locales[targetLocale]) {
return locales[targetLocale];
} else {
logWarn('No catalog found for "' + targetLocale + '"');
return false;
}
};
i18n.getLocales = function i18nGetLocales() {
return Object.keys(locales);
};
i18n.addLocale = function i18nAddLocale(locale) {
read(locale);
};
i18n.removeLocale = function i18nRemoveLocale(locale) {
delete locales[locale];
};
// ===================
// = private methods =
// ===================
var postProcess = function(msg, namedValues, args, count) {
// test for parsable interval string
if ((/\|/).test(msg)) {
msg = parsePluralInterval(msg, count);
}
// replace the counter
if (typeof count === 'number') {
msg = vsprintf(msg, [parseInt(count, 10)]);
}
// if the msg string contains {{Mustache}} patterns we render it as a mini tempalate
if ((/{{.*}}/).test(msg)) {
msg = Mustache.render(msg, namedValues);
}
// if we have extra arguments with values to get replaced,
// an additional substition injects those strings afterwards
if ((/%/).test(msg) && args && args.length > 0) {
msg = vsprintf(msg, args);
}
return msg;
};
var argsEndWithNamedObject = function(args) {
return (args.length > 1 &&
args[args.length - 1] !== null &&
typeof args[args.length - 1] === 'object');
};
var parseArgv = function(args) {
var namedValues, returnArgs;
if (argsEndWithNamedObject(args)) {
namedValues = args[args.length - 1];
returnArgs = Array.prototype.slice.call(args, 1, -1);
} else {
namedValues = {};
returnArgs = args.length >= 2 ? Array.prototype.slice.call(args, 1) : [];
}
return [namedValues, returnArgs];
};
/**
* registers all public API methods to a given response object when not already declared
*/
var applyAPItoObject = function(object) {
var alreadySetted = true;
// attach to itself if not provided
for (var method in api) {
if (api.hasOwnProperty(method)) {
var alias = api[method];
// be kind rewind, or better not touch anything already existing
if (!object[alias]) {
alreadySetted = false;
object[alias] = i18n[method].bind(object);
}
}
}
// set initial locale if not set
if (!object.locale) {
object.locale = defaultLocale;
}
// escape recursion
if (alreadySetted) {
return;
}
// attach to response if present (ie. in express)
if (object.res) {
applyAPItoObject(object.res);
}
// attach to locals if present (ie. in express)
if (object.locals) {
applyAPItoObject(object.locals);
}
};
/**
* tries to guess locales by scanning the given directory
*/
var guessLocales = function(directory) {
var entries = fs.readdirSync(directory);
var localesFound = [];
for (var i = entries.length - 1; i >= 0; i--) {
if (entries[i].match(/^\./)) continue;
var localeFromFile = guessLocaleFromFile(entries[i]);
if (localeFromFile) localesFound.push(localeFromFile);
}
return localesFound.sort();
};
/**
* tries to guess locales from a given filename
*/
var guessLocaleFromFile = function(filename) {
var extensionRegex = new RegExp(extension + '$', 'g');
var prefixRegex = new RegExp('^' + prefix, 'g');
if (prefix && !filename.match(prefixRegex)) return false;
if (extension && !filename.match(extensionRegex)) return false;
return filename.replace(prefix, '').replace(extensionRegex, '');
};
/**
* guess language setting based on http headers
*/
var guessLanguage = function(request) {
if (typeof request === 'object') {
var languageHeader = request.headers? request.headers['accept-language'] : undefined,
languages = [],
regions = [];
request.languages = [defaultLocale];
request.regions = [defaultLocale];
request.language = defaultLocale;
request.region = defaultLocale;
// a query parameter overwrites all
if (queryParameter && request.url) {
var urlObj = url.parse(request.url, true);
if (urlObj.query[queryParameter]) {
logDebug('Overriding locale from query: ' + urlObj.query[queryParameter]);
request.language = urlObj.query[queryParameter];
if (preserveLegacyCase) {
request.language = request.language.toLowerCase();
}
return i18n.setLocale(request, request.language);
}
}
// a cookie overwrites headers
if (cookiename && request.cookies && request.cookies[cookiename]) {
request.language = request.cookies[cookiename];
return i18n.setLocale(request, request.language);
}
// 'accept-language' is the most common source
if (languageHeader) {
var acceptedLanguages = getAcceptedLanguagesFromHeader(languageHeader),
match, fallbackMatch, fallback;
for (var i = 0; i < acceptedLanguages.length; i++) {
var lang = acceptedLanguages[i],
lr = lang.split('-', 2),
parentLang = lr[0],
region = lr[1];
// Check if we have a configured fallback set for this language.
if (fallbacks && fallbacks[lang]) {
fallback = fallbacks[lang];
// Fallbacks for languages should be inserted
// where the original, unsupported language existed.
var acceptedLanguageIndex = acceptedLanguages.indexOf(lang);
var fallbackIndex = acceptedLanguages.indexOf(fallback);
if(fallbackIndex > -1) {
acceptedLanguages.splice(fallbackIndex, 1);
}
acceptedLanguages.splice(acceptedLanguageIndex + 1, 0, fallback);
}
// Check if we have a configured fallback set for the parent language of the locale.
if (fallbacks && fallbacks[parentLang]) {
fallback = fallbacks[parentLang];
// Fallbacks for a parent language should be inserted
// to the end of the list, so they're only picked
// if there is no better match.
if (acceptedLanguages.indexOf(fallback) < 0) {
acceptedLanguages.push(fallback);
}
}
if (languages.indexOf(parentLang) < 0) {
languages.push(parentLang.toLowerCase());
}
if (region) {
regions.push(region.toLowerCase());
}
if (!match && locales[lang]) {
match = lang;
break;
}
if (!fallbackMatch && locales[parentLang]) {
fallbackMatch = parentLang;
}
}
request.language = match || fallbackMatch || request.language;
request.region = regions[0] || request.region;
return i18n.setLocale(request, request.language);
}
}
// last resort: defaultLocale
return i18n.setLocale(request, defaultLocale);
};
/**
* Get a sorted list of accepted languages from the HTTP Accept-Language header
*/
var getAcceptedLanguagesFromHeader = function(header) {
var languages = header.split(','),
preferences = {};
return languages.map(function parseLanguagePreference(item) {
var preferenceParts = item.trim().split(';q=');
if (preferenceParts.length < 2) {
preferenceParts[1] = 1.0;
} else {
var quality = parseFloat(preferenceParts[1]);
preferenceParts[1] = quality ? quality : 0.0;
}
preferences[preferenceParts[0]] = preferenceParts[1];
return preferenceParts[0];
}).filter(function(lang) {
return preferences[lang] > 0;
}).sort(function sortLanguages(a, b) {
return preferences[b] - preferences[a];
});
};
/**
* searches for locale in given object
*/
var getLocaleFromObject = function(obj) {
var locale;
if (obj && obj.scope) {
locale = obj.scope.locale;
}
if (obj && obj.locale) {
locale = obj.locale;
}
return locale;
};
/**
* splits and parses a phrase for mathematical interval expressions
*/
var parsePluralInterval = function(phrase, count) {
var returnPhrase = phrase;
var phrases = phrase.split(/\|/);
// some() breaks on 1st true
phrases.some(function(p) {
var matches = p.match(/^\s*([\(\)\[\]\d,]+)?\s*(.*)$/);
// not the same as in combined condition
if (matches[1]) {
if (matchInterval(count, matches[1]) === true) {
returnPhrase = matches[2];
return true;
}
} else {
returnPhrase = p;
}
});
return returnPhrase;
};
/**
* test a number to match mathematical interval expressions
* [0,2] - 0 to 2 (including, matches: 0, 1, 2)
* ]0,3[ - 0 to 3 (excluding, matches: 1, 2)
* [1] - 1 (matches: 1)
* [20,] - all numbers ≥20 (matches: 20, 21, 22, ...)
* [,20] - all numbers ≤20 (matches: 20, 21, 22, ...)
*/
var matchInterval = function(number, interval) {
interval = parseInterval(interval);
if (interval && typeof number === 'number') {
if (interval.from.value === number) {
return interval.from.included;
}
if (interval.to.value === number) {
return interval.to.included;
}
return (Math.min(interval.from.value, number) === interval.from.value &&
Math.max(interval.to.value, number) === interval.to.value);
}
return false;
};
/**
* read locale file, translate a msg and write to fs if new
*/
var translate = function(locale, singular, plural, skipSyncToAllFiles) {
// add same key to all translations
if (!skipSyncToAllFiles && syncFiles) {
syncToAllFiles(singular, plural);
}
if (locale === undefined) {
logWarn('WARN: No locale found - check the context of the call to __(). Using ' +
defaultLocale + ' as current locale');
locale = defaultLocale;
}
if (!locales[locale] && fallbacks[locale]) {
locale = fallbacks[locale];
}
// attempt to read when defined as valid locale
if (!locales[locale]) {
read(locale);
}
// fallback to default when missed
if (!locales[locale]) {
logWarn('WARN: Locale ' + locale +
' couldn\'t be read - check the context of the call to $__. Using ' +
defaultLocale + ' (default) as current locale');
locale = defaultLocale;
read(locale);
}
// dotnotaction add on, @todo: factor out
var defaultSingular = singular;
var defaultPlural = plural;
if (objectNotation) {
var indexOfColon = singular.indexOf(':');
// We compare against 0 instead of -1 because
// we don't really expect the string to start with ':'.
if (0 < indexOfColon) {
defaultSingular = singular.substring(indexOfColon + 1);
singular = singular.substring(0, indexOfColon);
}
if (plural && typeof plural !== 'number') {
indexOfColon = plural.indexOf(':');
if (0 < indexOfColon) {
defaultPlural = plural.substring(indexOfColon + 1);
plural = plural.substring(0, indexOfColon);
}
}
}
var accessor = localeAccessor(locale, singular);
var mutator = localeMutator(locale, singular);
if (plural) {
if (accessor() == null) {
mutator({
'one': defaultSingular || singular,
'other': defaultPlural || plural
});
write(locale);
}
}
if (accessor() == null) {
mutator(defaultSingular || singular);
write(locale);
}
return accessor();
};
/**
* initialize the same key in all locales
* when not already existing, checked via translate
*/
var syncToAllFiles = function(singular, plural) {
// iterate over locales and translate again
// this will implicitly write/sync missing keys
// to the rest of locales
for (var l in locales) {
translate(l, singular, plural, true);
}
};
/**
* Allows delayed access to translations nested inside objects.
* @param {String} locale The locale to use.
* @param {String} singular The singular term to look up.
* @param {Boolean} [allowDelayedTraversal=true] Is delayed traversal of the tree allowed?
* This parameter is used internally. It allows to signal the accessor that
* a translation was not found in the initial lookup and that an invocation
* of the accessor may trigger another traversal of the tree.
* @returns {Function} A function that, when invoked, returns the current value stored
* in the object at the requested location.
*/
var localeAccessor = function(locale, singular, allowDelayedTraversal) {
// Bail out on non-existent locales to defend against internal errors.
if (!locales[locale]) return Function.prototype;
// Handle object lookup notation
var indexOfDot = objectNotation && singular.lastIndexOf(objectNotation);
if (objectNotation && (0 < indexOfDot && indexOfDot < singular.length - 1)) {
// If delayed traversal wasn't specifically forbidden, it is allowed.
if (typeof allowDelayedTraversal === 'undefined') allowDelayedTraversal = true;
// The accessor we're trying to find and which we want to return.
var accessor = null;
// An accessor that returns null.
var nullAccessor = function() {
return null;
};
// Do we need to re-traverse the tree upon invocation of the accessor?
var reTraverse = false;
// Split the provided term and run the callback for each subterm.
singular.split(objectNotation).reduce(function(object, index) {
// Make the accessor return null.
accessor = nullAccessor;
// If our current target object (in the locale tree) doesn't exist or
// it doesn't have the next subterm as a member...
if (null === object || !object.hasOwnProperty(index)) {
// ...remember that we need retraversal (because we didn't find our target).
reTraverse = allowDelayedTraversal;
// Return null to avoid deeper iterations.
return null;
}
// We can traverse deeper, so we generate an accessor for this current level.
accessor = function() {
return object[index];
};
// Return a reference to the next deeper level in the locale tree.
return object[index];
}, locales[locale]);
// Return the requested accessor.
return function() {
// If we need to re-traverse (because we didn't find our target term)
// traverse again and return the new result (but don't allow further iterations)
// or return the previously found accessor if it was already valid.
return (reTraverse) ? localeAccessor(locale, singular, false)() : accessor();
};
} else {
// No object notation, just return an accessor that performs array lookup.
return function() {
return locales[locale][singular];
};
}
};
/**
* Allows delayed mutation of a translation nested inside objects.
* @description Construction of the mutator will attempt to locate the requested term
* inside the object, but if part of the branch does not exist yet, it will not be
* created until the mutator is actually invoked. At that point, re-traversal of the
* tree is performed and missing parts along the branch will be created.
* @param {String} locale The locale to use.
* @param {String} singular The singular term to look up.
* @param [Boolean} [allowBranching=false] Is the mutator allowed to create previously
* non-existent branches along the requested locale path?
* @returns {Function} A function that takes one argument. When the function is
* invoked, the targeted translation term will be set to the given value inside the locale table.
*/
var localeMutator = function(locale, singular, allowBranching) {
// Bail out on non-existent locales to defend against internal errors.
if (!locales[locale]) return Function.prototype;
// Handle object lookup notation
var indexOfDot = objectNotation && singular.lastIndexOf(objectNotation);
if (objectNotation && (0 < indexOfDot && indexOfDot < singular.length - 1)) {
// If branching wasn't specifically allowed, disable it.
if (typeof allowBranching === 'undefined') allowBranching = false;
// This will become the function we want to return.
var accessor = null;
// An accessor that takes one argument and returns null.
var nullAccessor = function() {
return null;
};
// Fix object path.
var fixObject = function() {
return {};
};
// Are we going to need to re-traverse the tree when the mutator is invoked?
var reTraverse = false;
// Split the provided term and run the callback for each subterm.
singular.split(objectNotation).reduce(function(object, index) {
// Make the mutator do nothing.
accessor = nullAccessor;
// If our current target object (in the locale tree) doesn't exist or
// it doesn't have the next subterm as a member...
if (null === object || !object.hasOwnProperty(index)) {
// ...check if we're allowed to create new branches.
if (allowBranching) {
// Fix `object` if `object` is not Object.
if (null === object || typeof object !== 'object') {
object = fixObject();
}
// If we are allowed to, create a new object along the path.
object[index] = {};
} else {
// If we aren't allowed, remember that we need to re-traverse later on and...
reTraverse = true;
// ...return null to make the next iteration bail our early on.
return null;
}
}
// Generate a mutator for the current level.
accessor = function(value) {
object[index] = value;
return value;
};
// Generate a fixer for the current level.
fixObject = function() {
object[index] = {};
return object[index];
};
// Return a reference to the next deeper level in the locale tree.
return object[index];
}, locales[locale]);
// Return the final mutator.
return function(value) {
// If we need to re-traverse the tree
// invoke the search again, but allow branching
// this time (because here the mutator is being invoked)
// otherwise, just change the value directly.
return (reTraverse) ? localeMutator(locale, singular, true)(value) : accessor(value);
};
} else {
// No object notation, just return a mutator that performs array lookup and changes the value.
return function(value) {
locales[locale][singular] = value;
return value;
};
}
};
/**
* try reading a file
*/
var read = function(locale) {
var localeFile = {},
file = getStorageFilePath(locale);
try {
logDebug('read ' + file + ' for locale: ' + locale);
localeFile = fs.readFileSync(file);
try {
// parsing filecontents to locales[locale]
locales[locale] = JSON.parse(localeFile);
} catch (parseError) {
logError('unable to parse locales from file (maybe ' +
file + ' is empty or invalid json?): ', parseError);
}
} catch (readError) {
// unable to read, so intialize that file
// locales[locale] are already set in memory, so no extra read required
// or locales[locale] are empty, which initializes an empty locale.json file
// since the current invalid locale could exist, we should back it up
if (fs.existsSync(file)) {
logDebug('backing up invalid locale ' + locale + ' to ' + file + '.invalid');
fs.renameSync(file, file + '.invalid');
}
logDebug('initializing ' + file);
write(locale);
}
};
/**
* try writing a file in a created directory
*/
var write = function(locale) {
var stats, target, tmp;
// don't write new locale information to disk if updateFiles isn't true
if (!updateFiles) {
return;
}
// creating directory if necessary
try {
stats = fs.lstatSync(directory);
} catch (e) {
logDebug('creating locales dir in: ' + directory);
fs.mkdirSync(directory, directoryPermissions);
}
// first time init has an empty file
if (!locales[locale]) {
locales[locale] = {};
}
// writing to tmp and rename on success
try {
target = getStorageFilePath(locale);
tmp = target + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(locales[locale], null, indent), 'utf8');
stats = fs.statSync(tmp);
if (stats.isFile()) {
fs.renameSync(tmp, target);
} else {
logError('unable to write locales to file (either ' +
tmp + ' or ' + target + ' are not writeable?): ');
}
} catch (e) {
logError('unexpected error writing files (either ' +
tmp + ' or ' + target + ' are not writeable?): ', e);
}
};
/**
* basic normalization of filepath
*/
var getStorageFilePath = function(locale) {
// changed API to use .json as default, #16
var ext = extension || '.json',
filepath = path.normalize(directory + pathsep + prefix + locale + ext),
filepathJS = path.normalize(directory + pathsep + prefix + locale + '.js');
// use .js as fallback if already existing
try {
if (fs.statSync(filepathJS)) {
logDebug('using existing file ' + filepathJS);
extension = '.js';
return filepathJS;
}
} catch (e) {
logDebug('will use ' + filepath);
}
return filepath;
};
/**
* Logging proxies
*/
function logDebug(msg) {
logDebugFn(msg);
}
function logWarn(msg) {
logWarnFn(msg);
}
function logError(msg) {
logErrorFn(msg);
}
return i18n;
}());