import Compiler from './compiler'; import { getAllPlurals, getPlural, hasPlural } from './plurals'; export default class MessageFormat { /** * Used by the constructor when no `locale` argument is given. * * @memberof MessageFormat * @default 'en' */ static defaultLocale = 'en'; /** * Escape special characaters by surrounding the characters `{` and `}` in the * input string with 'quotes'. This will allow those characters to not be * considered as MessageFormat control characters. * * @memberof MessageFormat * @param {string} str - The input string * @param {boolean} [octothorpe=false] - Also escape `#` * @returns {string} The escaped string */ static escape(str, octothorpe) { const esc = octothorpe ? /[#{}]/g : /[{}]/g; return String(str).replace(esc, "'$&'"); } /** * Returns a subset of `locales` consisting of those for which MessageFormat * has built-in plural category support. * * @memberof MessageFormat * @param {(string|string[])} locales * @returns {string[]} */ static supportedLocalesOf(locales) { const la = Array.isArray(locales) ? locales : [locales]; return la.filter(hasPlural); } /** * @typedef {Object} MessageFormat~Options - The shape of the options object * that may be used as the second argument of the constructor. * @property {boolean} [biDiSupport=false] - Add Unicode control characters to * all input parts to preserve the integrity of the output when mixing LTR * and RTL text * @property {string} [currency='USD'] - The currency to use when formatting * `{V, number, currency}` * @property {Object} [customFormatters] - Map of custom formatting functions * to include. See the {@page guide} for more details. * @property {boolean} [requireAllArguments=false] - Require all message * arguments to be set with a defined value * @property {('string'|'values')} [returnType='string'] - Return type of * compiled functions; either a concatenated string or an array (possibly * hierarchical) of values * @property {boolean} [strictNumberSign=false] - Allow `#` only directly * within a plural or selectordinal case, rather than in any inner select * case as well. */ /** * Create a new MessageFormat compiler * * ``` * import MessageFormat from 'messageformat' * ``` * * @class MessageFormat * @classdesc MessageFormat-to-JavaScript compiler * @param {string|Array} [locale] * Define the locale or locales supported by this MessageFormat instance. If * given multiple valid locales, the first will be the default. If `locale` * is empty, it will fall back to `MessageFormat.defaultLocale`. * * String `locale` values will be matched to plural categorisation functions * provided by the Unicode CLDR. If defining your own instead, use named * functions instead, optionally providing them with the properties: * `cardinals: string[]`, `ordinals: string[]`, `module: string` (to import * the formatter as a runtime dependency, rather than inlining its source). * * If `locale` has the special value `'*'`, it will match *all* available * locales. This may be useful if you want your messages to be completely * determined by your data, but may provide surprising results if your input * message object includes any 2-3 character keys that are not locale * identifiers. * @param {MessageFormat~Options} [options] - Compiler options */ constructor(locale, options) { this.options = Object.assign( { biDiSupport: false, currency: 'USD', customFormatters: {}, returnType: 'string', strictNumberSign: false }, options ); if (locale === '*') { this.plurals = getAllPlurals(MessageFormat.defaultLocale); } else if (Array.isArray(locale)) { this.plurals = locale.map(getPlural).filter(Boolean); } else if (locale) { const pl = getPlural(locale); if (pl) this.plurals = [pl]; } if (!this.plurals || this.plurals.length === 0) { const pl = getPlural(MessageFormat.defaultLocale); this.plurals = [pl]; } } /** * @typedef {Object} MessageFormat~ResolvedOptions * @property {boolean} biDiSupport - Whether Unicode control characters be * added to all input parts to preserve the integrity of the output when * mixing LTR and RTL text * @property {object} customFormatters - Map of custom formatting functions * @property {string} locale - The default locale * @property {object[]} plurals - All of the supported plurals * @property {boolean} strictNumberSign - Is `#` only allowed directly within * a plural or selectordinal case */ /** * Returns a new object with properties reflecting the default locale, * plurals, and other options computed during initialization. * * @memberof MessageFormat * @instance * @returns {MessageFormat~ResolvedOptions} */ resolvedOptions() { return { ...this.options, locale: this.plurals[0].locale, plurals: this.plurals }; } /** * Compile a message into a function * * Given a string `message` with ICU MessageFormat declarations, the result is * a function taking a single Object parameter representing each of the * input's defined variables, using the first valid locale. * * @memberof MessageFormat * @instance * @param {string} message - The input message to be compiled, in ICU MessageFormat * @returns {function} - The compiled function * * @example * const mf = new MessageFormat('en') * const msg = mf.compile('A {TYPE} example.') * * msg({ TYPE: 'simple' }) // 'A simple example.' */ compile(message) { const compiler = new Compiler(this.options); const fnBody = 'return ' + compiler.compile(message, this.plurals[0]); const nfArgs = []; const fnArgs = []; for (const [key, fmt] of Object.entries(compiler.runtime)) { nfArgs.push(key); fnArgs.push(fmt); } const fn = new Function(...nfArgs, fnBody); return fn.apply(null, fnArgs); } }