UNPKG

counterpart

Version:

A translation and localization library for Node.js and the browser

424 lines (328 loc) 11.7 kB
'use strict'; var extend = require('extend'); var isArray = require('util').isArray; var isDate = require('util').isDate; var sprintf = require("sprintf-js").sprintf; var events = require('events'); var except = require('except'); var strftime = require('./strftime'); var translationScope = 'counterpart'; function isString(val) { return typeof val === 'string' || Object.prototype.toString.call(val) === '[object String]'; } function isFunction(val) { return typeof val === 'function' || Object.prototype.toString.call(val) === '[object Function]'; } function isPlainObject(val) { //Deal with older browsers (IE8) that don't return [object Null] in this case. if (val === null) { return false; } return Object.prototype.toString.call(val) === '[object Object]'; } function isSymbol(key) { return isString(key) && key[0] === ':'; } function hasOwnProp(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } function getEntry(translations, keys) { return keys.reduce(function(result, key) { if (isPlainObject(result) && hasOwnProp(result, key)) { return result[key]; } else { return null; } }, translations); } function Counterpart() { events.EventEmitter.apply(this); this._registry = { locale: 'en', interpolate: true, fallbackLocales: [], scope: null, translations: {}, interpolations: {}, normalizedKeys: {}, separator: '.', keepTrailingDot: false, keyTransformer: function(key) { return key; }, generateMissingEntry: function(key) { return 'missing translation: ' + key; } }; this.registerTranslations('en', require('./locales/en')); this.setMaxListeners(0); } Counterpart.prototype = events.EventEmitter.prototype; Counterpart.prototype.constructor = events.EventEmitter; Counterpart.prototype.getLocale = function() { return this._registry.locale; }; Counterpart.prototype.setLocale = function(value) { var previous = this._registry.locale; if (previous != value) { this._registry.locale = value; this.emit('localechange', value, previous); } return previous; }; Counterpart.prototype.getFallbackLocale = function() { return this._registry.fallbackLocales; }; Counterpart.prototype.setFallbackLocale = function(value) { var previous = this._registry.fallbackLocales; this._registry.fallbackLocales = [].concat(value || []); return previous; }; Counterpart.prototype.getAvailableLocales = function() { return this._registry.availableLocales || Object.keys(this._registry.translations); }; Counterpart.prototype.setAvailableLocales = function(value) { var previous = this.getAvailableLocales(); this._registry.availableLocales = value; return previous; }; Counterpart.prototype.getSeparator = function() { return this._registry.separator; }; Counterpart.prototype.setSeparator = function(value) { var previous = this._registry.separator; this._registry.separator = value; return previous; }; Counterpart.prototype.setInterpolate = function(value) { var previous = this._registry.interpolate; this._registry.interpolate = value; return previous; }; Counterpart.prototype.getInterpolate = function() { return this._registry.interpolate; }; Counterpart.prototype.setKeyTransformer = function(value) { var previous = this._registry.keyTransformer; this._registry.keyTransformer = value; return previous; }; Counterpart.prototype.getKeyTransformer = function() { return this._registry.keyTransformer; }; Counterpart.prototype.setMissingEntryGenerator = function(value) { var previous = this._registry.generateMissingEntry; this._registry.generateMissingEntry = value; return previous; }; Counterpart.prototype.getMissingEntryGenerator = function() { return this._registry.generateMissingEntry; }; Counterpart.prototype.registerTranslations = function(locale, data) { var translations = {}; translations[locale] = data; extend(true, this._registry.translations, translations); return translations; }; Counterpart.prototype.registerInterpolations = function(data) { return extend(true, this._registry.interpolations, data); }; Counterpart.prototype.onLocaleChange = Counterpart.prototype.addLocaleChangeListener = function(callback) { this.addListener('localechange', callback); }; Counterpart.prototype.offLocaleChange = Counterpart.prototype.removeLocaleChangeListener = function(callback) { this.removeListener('localechange', callback); }; Counterpart.prototype.onTranslationNotFound = Counterpart.prototype.addTranslationNotFoundListener = function(callback) { this.addListener('translationnotfound', callback); }; Counterpart.prototype.offTranslationNotFound = Counterpart.prototype.removeTranslationNotFoundListener = function(callback) { this.removeListener('translationnotfound', callback); }; Counterpart.prototype.onError = Counterpart.prototype.addErrorListener = function(callback) { this.addListener('error', callback); }; Counterpart.prototype.offError = Counterpart.prototype.removeErrorListener = function(callback) { this.removeListener('error', callback); }; Counterpart.prototype.translate = function(key, options) { if (!isArray(key) && !isString(key) || !key.length) { throw new Error('invalid argument: key'); } if (isSymbol(key)) { key = key.substr(1); } key = this._registry.keyTransformer(key, options); options = extend(true, {}, options); var locale = options.locale || this._registry.locale; delete options.locale; var scope = options.scope || this._registry.scope; delete options.scope; var separator = options.separator || this._registry.separator; delete options.separator; var fallbackLocales = [].concat(options.fallbackLocale || this._registry.fallbackLocales); delete options.fallbackLocale; var keys = this._normalizeKeys(locale, scope, key, separator); var entry = getEntry(this._registry.translations, keys); if (entry === null) { this.emit('translationnotfound', locale, key, options.fallback, scope); if (options.fallback) { entry = this._fallback(locale, scope, key, options.fallback, options); } } if (entry === null && fallbackLocales.length > 0 && fallbackLocales.indexOf(locale) === -1) { for (var i = 0, ii = fallbackLocales.length; i < ii; i++) { var fallbackLocale = fallbackLocales[i]; var fallbackKeys = this._normalizeKeys(fallbackLocale, scope, key, separator); entry = getEntry(this._registry.translations, fallbackKeys); if (entry) { locale = fallbackLocale; break; } } } if (entry === null) { entry = this._registry.generateMissingEntry(keys.join(separator)); } entry = this._pluralize(locale, entry, options.count); if (this._registry.interpolate !== false && options.interpolate !== false) { entry = this._interpolate(entry, options); } return entry; }; Counterpart.prototype.localize = function(object, options) { if (!isDate(object)) { throw new Error('invalid argument: object must be a date'); } options = extend(true, {}, options); var locale = options.locale || this._registry.locale; var scope = options.scope || translationScope; var type = options.type || 'datetime'; var format = options.format || 'default'; options = { locale: locale, scope: scope, interpolate: false }; format = this.translate(['formats', type, format], extend(true, {}, options)); return strftime(object, format, this.translate('names', options)); }; Counterpart.prototype._pluralize = function(locale, entry, count) { if (typeof entry !== 'object' || entry === null || typeof count !== 'number') { return entry; } var pluralizeFunc = this.translate('pluralize', { locale: locale, scope: translationScope }); if (Object.prototype.toString.call(pluralizeFunc) !== '[object Function]') { return pluralizeFunc; } return pluralizeFunc(entry, count); }; Counterpart.prototype.withLocale = function(locale, callback, context) { var previous = this._registry.locale; this._registry.locale = locale; var result = callback.call(context); this._registry.locale = previous; return result; }; Counterpart.prototype.withScope = function(scope, callback, context) { var previous = this._registry.scope; this._registry.scope = scope; var result = callback.call(context); this._registry.scope = previous; return result; }; Counterpart.prototype.withSeparator = function(separator, callback, context) { var previous = this.setSeparator(separator); var result = callback.call(context); this.setSeparator(previous); return result; }; Counterpart.prototype._normalizeKeys = function(locale, scope, key, separator) { var keys = []; keys = keys.concat(this._normalizeKey(locale, separator)); keys = keys.concat(this._normalizeKey(scope, separator)); keys = keys.concat(this._normalizeKey(key, separator)); return keys; }; Counterpart.prototype._normalizeKey = function(key, separator) { this._registry.normalizedKeys[separator] = this._registry.normalizedKeys[separator] || {}; this._registry.normalizedKeys[separator][key] = this._registry.normalizedKeys[separator][key] || (function(key) { if (isArray(key)) { var normalizedKeyArray = key.map(function(k) { return this._normalizeKey(k, separator); }.bind(this)); return [].concat.apply([], normalizedKeyArray); } else { if (typeof key === 'undefined' || key === null) { return []; } var keys = key.split(separator); for (var i = keys.length - 1; i >= 0; i--) { if (keys[i] === '') { keys.splice(i, 1); if (this._registry.keepTrailingDot === true && i == keys.length) { keys[keys.length - 1] += '' + separator; } } } return keys; } }.bind(this))(key); return this._registry.normalizedKeys[separator][key]; }; Counterpart.prototype._interpolate = function(entry, values) { if (typeof entry !== 'string') { return entry; } try { return sprintf(entry, extend({}, this._registry.interpolations, values)); } catch (err) { if (this.listenerCount('error') > 0) { this.emit('error', err, entry, values); } else { throw err; } return null; } }; Counterpart.prototype._resolve = function(locale, scope, object, subject, options) { options = options || {}; if (options.resolve === false) { return subject; } var result; if (isSymbol(subject)) { result = this.translate(subject, extend({}, options, { locale: locale, scope: scope })); } else if (isFunction(subject)) { var dateOrTime; if (options.object) { dateOrTime = options.object; delete options.object; } else { dateOrTime = object; } result = this._resolve(locale, scope, object, subject(dateOrTime, options)); } else { result = subject; } return /^missing translation:/.test(result) ? null : result; }; Counterpart.prototype._fallback = function(locale, scope, object, subject, options) { options = except(options, 'fallback'); if (isArray(subject)) { for (var i = 0, ii = subject.length; i < ii; i++) { var result = this._resolve(locale, scope, object, subject[i], options); if (result) { return result; } } return null; } else { return this._resolve(locale, scope, object, subject, options); } }; var instance = new Counterpart(); function translate() { return instance.translate.apply(instance, arguments); } extend(translate, instance, { Instance: Counterpart, Translator: Counterpart }); module.exports = translate;