%PDF- %PDF-
Direktori : /home/opti/workflow/node_modules/i18n/ |
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; }());