/* Copyright (c) 2014, Yahoo! Inc. All rights reserved. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /* jslint esnext: true */ import {extend, hop} from './utils'; import {defineProperty, objCreate} from './es5'; import Compiler from './compiler'; import parser from 'intl-messageformat-parser'; export default MessageFormat; // -- MessageFormat -------------------------------------------------------- function MessageFormat(message, locales, formats) { // Parse string messages into an AST. var ast = typeof message === 'string' ? MessageFormat.__parse(message) : message; if (!(ast && ast.type === 'messageFormatPattern')) { throw new TypeError('A message must be provided as a String or AST.'); } // Creates a new object with the specified `formats` merged with the default // formats. formats = this._mergeFormats(MessageFormat.formats, formats); // Defined first because it's used to build the format pattern. defineProperty(this, '_locale', {value: this._resolveLocale(locales)}); // Compile the `ast` to a pattern that is highly optimized for repeated // `format()` invocations. **Note:** This passes the `locales` set provided // to the constructor instead of just the resolved locale. var pluralFn = this._findPluralRuleFunction(this._locale); var pattern = this._compilePattern(ast, locales, formats, pluralFn); // "Bind" `format()` method to `this` so it can be passed by reference like // the other `Intl` APIs. var messageFormat = this; this.format = function (values) { try { return messageFormat._format(pattern, values); } catch (e) { if (e.variableId) { throw new Error( 'The intl string context variable \'' + e.variableId + '\'' + ' was not provided to the string \'' + message + '\'' ); } else { throw e; } } }; } // Default format options used as the prototype of the `formats` provided to the // constructor. These are used when constructing the internal Intl.NumberFormat // and Intl.DateTimeFormat instances. defineProperty(MessageFormat, 'formats', { enumerable: true, value: { number: { 'currency': { style: 'currency' }, 'percent': { style: 'percent' } }, date: { 'short': { month: 'numeric', day : 'numeric', year : '2-digit' }, 'medium': { month: 'short', day : 'numeric', year : 'numeric' }, 'long': { month: 'long', day : 'numeric', year : 'numeric' }, 'full': { weekday: 'long', month : 'long', day : 'numeric', year : 'numeric' } }, time: { 'short': { hour : 'numeric', minute: 'numeric' }, 'medium': { hour : 'numeric', minute: 'numeric', second: 'numeric' }, 'long': { hour : 'numeric', minute : 'numeric', second : 'numeric', timeZoneName: 'short' }, 'full': { hour : 'numeric', minute : 'numeric', second : 'numeric', timeZoneName: 'short' } } } }); // Define internal private properties for dealing with locale data. defineProperty(MessageFormat, '__localeData__', {value: objCreate(null)}); defineProperty(MessageFormat, '__addLocaleData', {value: function (data) { if (!(data && data.locale)) { throw new Error( 'Locale data provided to IntlMessageFormat is missing a ' + '`locale` property' ); } MessageFormat.__localeData__[data.locale.toLowerCase()] = data; }}); // Defines `__parse()` static method as an exposed private. defineProperty(MessageFormat, '__parse', {value: parser.parse}); // Define public `defaultLocale` property which defaults to English, but can be // set by the developer. defineProperty(MessageFormat, 'defaultLocale', { enumerable: true, writable : true, value : undefined }); MessageFormat.prototype.resolvedOptions = function () { // TODO: Provide anything else? return { locale: this._locale }; }; MessageFormat.prototype._compilePattern = function (ast, locales, formats, pluralFn) { var compiler = new Compiler(locales, formats, pluralFn); return compiler.compile(ast); }; MessageFormat.prototype._findPluralRuleFunction = function (locale) { var localeData = MessageFormat.__localeData__; var data = localeData[locale.toLowerCase()]; // The locale data is de-duplicated, so we have to traverse the locale's // hierarchy until we find a `pluralRuleFunction` to return. while (data) { if (data.pluralRuleFunction) { return data.pluralRuleFunction; } data = data.parentLocale && localeData[data.parentLocale.toLowerCase()]; } throw new Error( 'Locale data added to IntlMessageFormat is missing a ' + '`pluralRuleFunction` for :' + locale ); }; MessageFormat.prototype._format = function (pattern, values) { var result = '', i, len, part, id, value, err; for (i = 0, len = pattern.length; i < len; i += 1) { part = pattern[i]; // Exist early for string parts. if (typeof part === 'string') { result += part; continue; } id = part.id; // Enforce that all required values are provided by the caller. if (!(values && hop.call(values, id))) { err = new Error('A value must be provided for: ' + id); err.variableId = id; throw err; } value = values[id]; // Recursively format plural and select parts' option — which can be a // nested pattern structure. The choosing of the option to use is // abstracted-by and delegated-to the part helper object. if (part.options) { result += this._format(part.getOption(value), values); } else { result += part.format(value); } } return result; }; MessageFormat.prototype._mergeFormats = function (defaults, formats) { var mergedFormats = {}, type, mergedType; for (type in defaults) { if (!hop.call(defaults, type)) { continue; } mergedFormats[type] = mergedType = objCreate(defaults[type]); if (formats && hop.call(formats, type)) { extend(mergedType, formats[type]); } } return mergedFormats; }; MessageFormat.prototype._resolveLocale = function (locales) { if (typeof locales === 'string') { locales = [locales]; } // Create a copy of the array so we can push on the default locale. locales = (locales || []).concat(MessageFormat.defaultLocale); var localeData = MessageFormat.__localeData__; var i, len, localeParts, data; // Using the set of locales + the default locale, we look for the first one // which that has been registered. When data does not exist for a locale, we // traverse its ancestors to find something that's been registered within // its hierarchy of locales. Since we lack the proper `parentLocale` data // here, we must take a naive approach to traversal. for (i = 0, len = locales.length; i < len; i += 1) { localeParts = locales[i].toLowerCase().split('-'); while (localeParts.length) { data = localeData[localeParts.join('-')]; if (data) { // Return the normalized locale string; e.g., we return "en-US", // instead of "en-us". return data.locale; } localeParts.pop(); } } var defaultLocale = locales.pop(); throw new Error( 'No locale data has been added to IntlMessageFormat for: ' + locales.join(', ') + ', or the default locale: ' + defaultLocale ); };