UNPKG

bazinga-translator

Version:

A pretty nice way to use your translation messages generated by the BazingaJsTranslationBundle in your JavaScript.

645 lines (558 loc) 20.9 kB
/** * @author William DURAND <william.durand1@gmail.com> * @license MIT Licensed */ (function (root, factory) { if (typeof module === 'object' && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(require('intl-messageformat')); } else if (typeof define === 'function' && define.amd) { define(['intl-messageformat'], factory); } else { root.Translator = factory(root.IntlMessageFormat); } }(typeof self !== 'undefined' ? self : this, function (IntlMessageFormat) { "use strict"; var _messages = {}, _fallbackLocale = 'en', _domains = [], _sPluralRegex = new RegExp(/^\w+\: +(.+)$/), _cPluralRegex = new RegExp(/^\s*((\{\s*(\-?\d+[\s*,\s*\-?\d+]*)\s*\})|([\[\]])\s*(-Inf|\-?\d+)\s*,\s*(\+?Inf|\-?\d+)\s*([\[\]]))\s?(.+?)$/), _iPluralRegex = new RegExp(/^\s*(\{\s*(\-?\d+[\s*,\s*\-?\d+]*)\s*\})|([\[\]])\s*(-Inf|\-?\d+)\s*,\s*(\+?Inf|\-?\d+)\s*([\[\]])/), INTL_DOMAIN_SUFFIX = '+intl-icu'; var Translator = { /** * The current locale. * * @type {String} * @api public */ locale: get_current_locale(), /** * Fallback locale. * * @type {String} * @api public */ fallback: _fallbackLocale, /** * Placeholder prefix. * * @type {String} * @api public */ placeHolderPrefix: '%', /** * Placeholder suffix. * * @type {String} * @api public */ placeHolderSuffix: '%', /** * Default domain. * * @type {String} * @api public */ defaultDomain: 'messages', /** * Plural separator. * * @type {String} * @api public */ pluralSeparator: '|', /** * Adds a translation entry. * * @param {String} id The message id * @param {String} message The message to register for the given id * @param {String} [domain] The domain for the message or null to use the default * @param {String} [locale] The locale or null to use the default * @return {Object} Translator * @api public */ add: function(id, message, domain, locale) { var _locale = locale || this.locale || this.fallback, _domain = domain || this.defaultDomain; if (!_messages[_locale]) { _messages[_locale] = {}; } if (!_messages[_locale][_domain]) { _messages[_locale][_domain] = {}; } _messages[_locale][_domain][id] = message; if (false === exists(_domains, _domain)) { _domains.push(_domain); } return this; }, /** * Translates the given message. * * @param {String} id The message id * @param {Object} [parameters] An array of parameters for the message * @param {String} [domain] The domain for the message or null to guess it * @param {String} [locale] The locale or null to use the default * @return {String} The translated string * @api public */ trans: function(id, parameters, domain, locale) { var _additionalReturn = {}; var _message = get_message( id, domain, locale, this.locale, this.fallback, _additionalReturn ); if (_additionalReturn.isICU) { if (typeof IntlMessageFormat === 'undefined') { throw new Error('The dependency "IntlMessageFormat" is required to use ICU MessageFormat but it has not been found. Please read https://github.com/willdurand/BazingaJsTranslationBundle/blob/master/Resources/doc/index.md#using-icu-messageformat') } var mf = new IntlMessageFormat.IntlMessageFormat(_message, undefined, undefined, {ignoreTag: true}); return mf.format(parameters || {}); } return replace_placeholders(_message, parameters || {}); }, /** * Translates the given choice message by choosing a translation according to a number. * * @param {String} id The message id * @param {Number} number The number to use to find the indice of the message * @param {Object} [parameters] An array of parameters for the message * @param {String} [domain] The domain for the message or null to guess it * @param {String} [locale] The locale or null to use the default * @return {String} The translated string * @api public */ transChoice: function(id, number, parameters, domain, locale) { var _message = get_message( id, domain, locale, this.locale, this.fallback ); var _number = parseInt(number, 10); parameters = parameters || {}; if (parameters.count === undefined) { parameters.count = number; } if (typeof _message !== 'undefined' && !isNaN(_number)) { _message = pluralize( _message, _number, locale || this.locale || this.fallback ); } return replace_placeholders(_message, parameters); }, /** * Loads translations from JSON. * * @param {String} data A JSON string or object literal * @return {Object} Translator * @api public */ fromJSON: function(data) { if (typeof data === 'string') { data = JSON.parse(data); } if (data.locale) { this.locale = data.locale; } if (data.fallback) { this.fallback = data.fallback; } if (data.defaultDomain) { this.defaultDomain = data.defaultDomain; } if (data.translations) { for (var locale in data.translations) { for (var domain in data.translations[locale]) { for (var id in data.translations[locale][domain]) { this.add(id, data.translations[locale][domain][id], domain, locale); } } } } return this; }, /** * @api public */ reset: function() { _messages = {}; _domains = []; this.locale = get_current_locale(); } }; /** * Replace placeholders in given message. * * **WARNING:** used placeholders are removed. * * @param {String} message The translated message * @param {Object} placeholders The placeholders to replace * @return {String} A human readable message * @api private */ function replace_placeholders(message, placeholders) { var _i, _prefix = Translator.placeHolderPrefix, _suffix = Translator.placeHolderSuffix; for (_i in placeholders) { var _r = new RegExp(_prefix + _i + _suffix, 'g'); if (_r.test(message)) { var _v = String(placeholders[_i]).replace(new RegExp('\\$', 'g'), '$$$$'); message = message.replace(_r, _v); } } return message; } /** * Get the message based on its id, its domain, and its locale. If domain or * locale are not specified, it will try to find the message using fallbacks. * * @param {String} id The message id * @param {String} domain The domain for the message or null to guess it * @param {String} locale The locale or null to use the default * @param {String} currentLocale The current locale or null to use the default * @param {String} localeFallback The fallback (default) locale * @param {Object} additionalReturn An object passed by reference, used to return more data than the message * @return {String} The right message if found, `undefined` otherwise * @api private */ function get_message(id, domain, locale, currentLocale, localeFallback, additionalReturn) { var _locale = locale || currentLocale || localeFallback, _domain = domain, _additionalReturn = additionalReturn || {}; var nationalLocaleFallback = _locale.split('_')[0]; _additionalReturn.isICU = false; if (!(_locale in _messages)) { if (!(nationalLocaleFallback in _messages)) { if (!(localeFallback in _messages)) { return id; } _locale = localeFallback; } else { _locale = nationalLocaleFallback; } } if (typeof _domain === 'undefined' || null === _domain) { for (var i = 0; i < _domains.length; i++) { if (has_message(_locale, _domains[i], id) || has_message(nationalLocaleFallback, _domains[i], id) || has_message(localeFallback, _domains[i], id)) { _domain = _domains[i].replace(INTL_DOMAIN_SUFFIX, ''); break; } } } if (has_message(_locale, _domain + INTL_DOMAIN_SUFFIX, id)) { _additionalReturn.isICU = true; return _messages[_locale][_domain + INTL_DOMAIN_SUFFIX][id]; } if (has_message(_locale, _domain, id)) { return _messages[_locale][_domain][id]; } var _length, _parts, _last, _lastLength; while (_locale.length > 2) { _length = _locale.length; _parts = _locale.split(/[\s_]+/); _last = _parts[_parts.length - 1]; _lastLength = _last.length; if (1 === _parts.length) { break; } _locale = _locale.substring(0, _length - (_lastLength + 1)); if (has_message(_locale, _domain, id)) { return _messages[_locale][_domain][id]; } } if (has_message(localeFallback, _domain, id)) { return _messages[localeFallback][_domain][id]; } return id; } /** * Just look for a specific locale / domain / id if the message is available, * helpful for message availability validation * * @param {String} locale The locale * @param {String} domain The domain for the message * @param {String} id The message id * @return {Boolean} Return `true` if message is available, * ` false` otherwise * @api private */ function has_message(locale, domain, id) { if (!(locale in _messages)) { return false; } if (!(domain in _messages[locale])) { return false; } if (!(id in _messages[locale][domain])) { return false; } return true; } /** * The logic comes from the Symfony2 PHP Framework. * * Given a message with different plural translations separated by a * pipe (|), this method returns the correct portion of the message based * on the given number, the current locale and the pluralization rules * in the message itself. * * The message supports two different types of pluralization rules: * * interval: {0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples * indexed: There is one apple|There is %count% apples * * The indexed solution can also contain labels (e.g. one: There is one apple). * This is purely for making the translations more clear - it does not * affect the functionality. * * The two methods can also be mixed: * {0} There is no apples|one: There is one apple|more: There is %count% apples * * @param {String} message The message id * @param {Number} number The number to use to find the indice of the message * @param {String} locale The locale * @return {String} The message part to use for translation * @api private */ function pluralize(message, number, locale) { var _p, _e, _explicitRules = [], _standardRules = [], _parts = message.split(Translator.pluralSeparator), _matches = []; for (_p = 0; _p < _parts.length; _p++) { var _part = _parts[_p]; if (_cPluralRegex.test(_part)) { _matches = _part.match(_cPluralRegex); _explicitRules[_matches[0]] = _matches[_matches.length - 1]; } else if (_sPluralRegex.test(_part)) { _matches = _part.match(_sPluralRegex); _standardRules.push(_matches[1]); } else { _standardRules.push(_part); } } for (_e in _explicitRules) { if (_iPluralRegex.test(_e)) { _matches = _e.match(_iPluralRegex); if (_matches[1]) { var _ns = _matches[2].split(','), _n; for (_n in _ns) { if (number == _ns[_n]) { return _explicitRules[_e]; } } } else { var _leftNumber = convert_number(_matches[4]); var _rightNumber = convert_number(_matches[5]); if (('[' === _matches[3] ? number >= _leftNumber : number > _leftNumber) && (']' === _matches[6] ? number <= _rightNumber : number < _rightNumber)) { return _explicitRules[_e]; } } } } return _standardRules[plural_position(number, locale)] || _standardRules[0] || undefined; } /** * The logic comes from the Symfony2 PHP Framework. * * Convert number as String, "Inf" and "-Inf" * values to number values. * * @param {String} number A literal number * @return {Number} The int value of the number * @api private */ function convert_number(number) { if ('-Inf' === number) { return Number.NEGATIVE_INFINITY; } else if ('+Inf' === number || 'Inf' === number) { return Number.POSITIVE_INFINITY; } return parseInt(number, 10); } /** * The logic comes from the Symfony2 PHP Framework. * * Returns the plural position to use for the given locale and number. * * @param {Number} number The number to use to find the indice of the message * @param {String} locale The locale * @return {Number} The plural position * @api private */ function plural_position(number, locale) { var _locale = locale; if ('pt_BR' === _locale) { _locale = 'xbr'; } if (_locale.length > 3) { _locale = _locale.split('_')[0]; } switch (_locale) { case 'bo': case 'dz': case 'id': case 'ja': case 'jv': case 'ka': case 'km': case 'kn': case 'ko': case 'ms': case 'th': case 'tr': case 'vi': case 'zh': return 0; case 'af': case 'az': case 'bn': case 'bg': case 'ca': case 'da': case 'de': case 'el': case 'en': case 'eo': case 'es': case 'et': case 'eu': case 'fa': case 'fi': case 'fo': case 'fur': case 'fy': case 'gl': case 'gu': case 'ha': case 'he': case 'hu': case 'is': case 'it': case 'ku': case 'lb': case 'ml': case 'mn': case 'mr': case 'nah': case 'nb': case 'ne': case 'nl': case 'nn': case 'no': case 'om': case 'or': case 'pa': case 'pap': case 'ps': case 'pt': case 'so': case 'sq': case 'sv': case 'sw': case 'ta': case 'te': case 'tk': case 'ur': case 'zu': return (number == 1) ? 0 : 1; case 'am': case 'bh': case 'fil': case 'fr': case 'gun': case 'hi': case 'ln': case 'mg': case 'nso': case 'xbr': case 'ti': case 'wa': return ((number === 0) || (number == 1)) ? 0 : 1; case 'be': case 'bs': case 'hr': case 'ru': case 'sr': case 'uk': return ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); case 'cs': case 'sk': return (number == 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2); case 'ga': return (number == 1) ? 0 : ((number == 2) ? 1 : 2); case 'lt': return ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); case 'sl': return (number % 100 == 1) ? 0 : ((number % 100 == 2) ? 1 : (((number % 100 == 3) || (number % 100 == 4)) ? 2 : 3)); case 'mk': return (number % 10 == 1) ? 0 : 1; case 'mt': return (number == 1) ? 0 : (((number === 0) || ((number % 100 > 1) && (number % 100 < 11))) ? 1 : (((number % 100 > 10) && (number % 100 < 20)) ? 2 : 3)); case 'lv': return (number === 0) ? 0 : (((number % 10 == 1) && (number % 100 != 11)) ? 1 : 2); case 'pl': return (number == 1) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12) || (number % 100 > 14))) ? 1 : 2); case 'cy': return (number == 1) ? 0 : ((number == 2) ? 1 : (((number == 8) || (number == 11)) ? 2 : 3)); case 'ro': return (number == 1) ? 0 : (((number === 0) || ((number % 100 > 0) && (number % 100 < 20))) ? 1 : 2); case 'ar': return (number === 0) ? 0 : ((number == 1) ? 1 : ((number == 2) ? 2 : (((number >= 3) && (number <= 10)) ? 3 : (((number >= 11) && (number <= 99)) ? 4 : 5)))); default: return 0; } } /** * @type {Array} An array * @type {String} An element to compare * @return {Boolean} Return `true` if `array` contains `element`, * `false` otherwise * @api private */ function exists(array, element) { for (var i = 0; i < array.length; i++) { if (element === array[i]) { return true; } } return false; } /** * Get the current application's locale based on the `lang` attribute * on the `html` tag. * * @return {String} The current application's locale * @api private */ function get_current_locale() { if (typeof document !== 'undefined') { return document.documentElement.lang.replace('-', '_'); } else { return _fallbackLocale; } } return Translator; }));