(function(global) { 'use strict'; global.mozIntl = { /** * Format an Array of strings using locale specific separators. * Currently it only uses middle separator resulting in a local * equivalent of 'X, Y, Z'. * * In the Intl API it will support start/end separators allowing for * things like 'X, Y and Z'. * * @param {Array} An array of strings to be formatted * @returns {Promise} A promise of a string */ formatList: function(list) { return navigator.mozL10n.formatValue('listSeparator_middle').then( sep => list.join(sep) ); }, /** * Overload of Intl.DateTimeFormat that extends with future features. * * 1) dayperiod token * * `dayperiod` token allows to decide whether the formatted time should * contain dayperiod token. The value may be true or false. * * Example: * * mozIntl.DateTimeFormat(navigator.languages, { * dayperiod: true, * hour12: navigator.mozHour12, * hour: 'numeric', * minute: 'numeric' * }); * * 2) Token formatting * * This feature allows for formatting of tokens in the result string. * * Example: * * var f = mozIntl.DateTimeFormat(navigator.languages, { * weekday: 'long', * day: 'numeric', * month: 'long' * }); * * f.format(date, { * day: '$&', * }); * * Warning: Current implementation is very fragile. Be very careful when * choosing tokens to be formatted. The formatter will replace the * first occurrence of the returned string, so if you try to format * dayperiod ('am') in a string that also contains weekday that has this * string, you may end up with bad formatting. Example: * * French for Saturday is "samedi". So formatting weekday in the same string * as dayperiod will result in "samedi, 8:53 am". * * It's best to make sure that your formatted token is the only number or * the only string. Examples: * * * dayperiod in Hour:Minute am/pm * * day in "Saturday, July 23" * */ DateTimeFormat: function(locales, options) { const resolvedOptions = Object.assign({}, options); if (resolvedOptions.dayperiod) { if (resolvedOptions.hour === undefined) { resolvedOptions.hour = 'numeric'; } } if (resolvedOptions.hour !== undefined && resolvedOptions.hour12 === undefined) { resolvedOptions.hour12 = navigator.mozHour12; } if (resolvedOptions.dayperiod === undefined && resolvedOptions.hour12 === true) { resolvedOptions.dayperiod = true; } var intlFormat = new Intl.DateTimeFormat(locales, resolvedOptions); resolvedOptions.locale = intlFormat.resolvedOptions().locale; resolvedOptions.hour12 = intlFormat.resolvedOptions().hour12; // This is needed for a workaround for bug 1208808 // Remove when that bug is fixed var hourFormatter; if (resolvedOptions.dayperiod !== undefined && resolvedOptions.hour12 === true) { hourFormatter = Intl.DateTimeFormat(locales, { hour: 'numeric', hour12: false }); } return { resolvedOptions() { return resolvedOptions; }, format: function(date, tokenFormats) { var dayPeriod; var string = intlFormat.format(date); if (resolvedOptions.dayperiod === false && resolvedOptions.hour12 === true) { dayPeriod = getDayPeriodTokenForDate(date, hourFormatter); string = string.replace(dayPeriod, '').trim(); } else if (resolvedOptions.dayperiod === true && options.hour === undefined) { dayPeriod = getDayPeriodTokenForDate(date, hourFormatter); const hour = date.toLocaleString(navigator.languages, { hour12: true, hour: 'numeric' }).replace(dayPeriod, '').trim(); string = string.replace(hour, '').trim(); } for (var token in tokenFormats) { if (token === 'dayperiod' && resolvedOptions.hour12 === false) { continue; } const localOptions = { [token]: resolvedOptions[token], }; var formatter = global.mozIntl.DateTimeFormat( navigator.languages, localOptions); var tokenString = formatter.format(date); string = string.replace(tokenString, tokenFormats[token]); } return string; }, }; }, /** * Return locale specific infromation about calendar system. * * Currently supports: * * firstDayOfTheWeek: 0 - Sunday, 1 - Monday, etc. * * @param {String} Identifier of a token to be retrieved * @returns {Promise} A promise of value corresponding to a token */ calendarInfo: function(token) { switch (token) { case 'firstDayOfTheWeek': return navigator.mozL10n.formatValue('firstDayOfTheWeek').then( firstDayOfTheWeek => parseInt(firstDayOfTheWeek) % 7); default: throw new Error('Unknown token: ' + token); } }, /** * Duration formatter. * Formats an integer with milliseconds into locale specific duration string. * * The shim differs from Intl API formatters in that it returns a Promise * because it relies on L20n so it has to be asynchronous. * Intl API will probably be synchronous. * * Currently accepted options: * - maxUnit * - minUnit * both can take values hour | minute | second | millisecond * * Examples: * * mozIntl.DurationFormat(navigator.languages, { * minUnit: 'second', * maxUnit: 'hour' * }).then(formatter => * formatter.format(milliseconds); // 02:12:34 in en-US * ); * * mozIntl.DurationFormat(navigator.languages, { * minUnit: 'millisecond', * maxUnit: 'minute' * }).then(formatter => * formatter.format(milliseconds); // 12:34.80 in en-US * ); * * @param {Array} An array of languages * @param {Array} Options object with `minUnit` and `maxUnit` * @returns {Promise} A promise of a formatter */ DurationFormat: function(locales = navigator.languages, options = {}) { const resolvedOptions = Object.assign({ locale: locales[0], maxUnit: 'hour', minUnit: 'second', }, options); const numFormatter = Intl.NumberFormat(locales, { style: 'decimal', useGrouping: false, minimumIntegerDigits: 2 }); const maxUnitIdx = getDurationUnitIdx(resolvedOptions.maxUnit, 0); const minUnitIdx = getDurationUnitIdx(resolvedOptions.minUnit, durationFormatOrder.length - 1); return navigator.mozL10n.formatValue('durationPattern').then(fmt => ({ resolvedOptions: function() { return resolvedOptions; }, format: function(input) { // Rounding minUnit to closest visible unit const minValue = durationFormatElements[resolvedOptions.minUnit].value; input = Math.round(input / minValue) * minValue; const duration = splitIntoTimeUnits(input, maxUnitIdx, minUnitIdx); var string = trimDurationPattern(fmt, resolvedOptions.maxUnit, resolvedOptions.minUnit); for (var unit in duration) { const token = durationFormatElements[unit].token; string = string.replace(token, numFormatter.format(duration[unit])); } if (input < 0) { return '-' + string; } return string; } })); }, /** * RelativeTime formatter. * Formats an integer with milliseconds into locale specific relative time * string. * * Currently accepted options: * * * style - long | short * * Defines whether the string will be long "1 minute ago" or short "1 min. * ago" * * * minUnit - (default) millisecond * * maxUnit - (default) year * * unit - second | minute | hour | day | week | month | year * (default) bestFit * * Example: * * const formatter = new Intl.RelativeTimeFormat(navigator.languages, { * unit: 'bestFit', * style: long' * }); * * var ms = Date.now() - 2 * 1000; * * formatter.format(ms); // "2 seconds ago" * * @param {Array} An array of languages * @param {Array} Options object * @returns An object with a formatter that returns Promises of strings */ RelativeTimeFormat: function(locales, options) { return { resolvedOptions: function() { return options; }, /* * ECMA 402 rev 3., 1.3.4, FormatRelativeTime * * Notes: This is a modified version of the function to use mozL10n * and simplified to match current data set in data.properties */ format: function(x) { const {unit, value} = relativeTimeFormatId(x, options); return navigator.mozL10n.formatValue(unit, { value }); }, }; }, UnitFormat: function(locales, options) { const unitGroup = getUnitFormatGroupName(options.unit); if (unitGroup === undefined) { throw new RangeError(`invalid value ${options.unit} for option unit`); } if (!unitFormatGroups[unitGroup].styles.includes(options.style)) { throw new RangeError(`invalid value ${options.style} for option style`); } const unit = `${unitGroup}-${options.unit}-${options.style}`; return { format: function(x) { return document.l10n.formatValue(unit, { value: x }); }, }; }, _gaia: { // This is an internal Firefox OS function, not part of the future standard relativePart: function(milliseconds) { const units = computeTimeUnits(milliseconds); const unit = getBestMatchUnit(units); return { unit: unit + 's', value: Math.abs(units[unit]) }; }, // This is an internal Firefox OS function, not part of the future standard RelativeDate: function(locales, options) { const style = options && options.style || 'long'; const maxFormatter = Intl.DateTimeFormat(locales, { year: 'numeric', month: 'numeric', day: 'numeric' }); const relativeFmtOptions = { unit: 'bestFit', style: style, minUnit: 'minute', }; return { format: function(time, maxDiff) { maxDiff = maxDiff || 86400 * 10; // default = 10 days const secDiff = (Date.now() - time) / 1000; if (isNaN(secDiff)) { return navigator.mozL10n.formatValue('incorrectDate'); } if (secDiff > maxDiff) { return Promise.resolve(maxFormatter.format(time)); } const {unit, value} = relativeTimeFormatId(time, relativeFmtOptions); return navigator.mozL10n.formatValue(unit, { value }); }, formatElement: function(element, time, maxDiff) { maxDiff = maxDiff || 86400 * 10; // default = 10 days const secDiff = (Date.now() - time) / 1000; if (isNaN(secDiff)) { element.setAttribute('data-l10n-id', 'incorrectDate'); } element.removeAttribute('data-l10n-id'); if (secDiff > maxDiff) { element.textContent = maxFormatter.format(time); } const {unit, value} = relativeTimeFormatId(time, relativeFmtOptions); navigator.mozL10n.setAttributes(element, unit, { value }); }, }; }, getFormattedUnit: function(type, style, v) { if (!unitFormatData.hasOwnProperty(type)) { throw new RangeError(`invalid type ${type}`); } if (!unitFormatGroups[type].styles.includes(style)) { throw new RangeError(`invalid style ${style} for type ${type}`); } var units = unitFormatData[type]; var scale = 0; for (let i = 1; i < units.length; i++) { if (v < units[i].value * unitFormatGroups[type].rounding) { scale = i - 1; break; } else if (i === units.length - 1) { scale = i; } } var value = Math.round(v / units[scale].value * 100) / 100; return global.mozIntl.UnitFormat(navigator.languages, { unit: units[scale].name, style: style }).format(value); }, } }; /* * This data is used by DurationFormat */ /*jshint unused:false*/ const durationFormatOrder = ['hour', 'minute', 'second', 'millisecond']; const durationFormatElements = { 'hour': {value: 3600000, token: 'hh'}, 'minute': {value: 60000, token: 'mm'}, 'second': {value: 1000, token: 'ss'}, // rounding milliseconds to tens 'millisecond': {value: 10, token: 'SS'} }; const unitFormatData = { 'duration': [ {'name': 'second', 'value': 1}, {'name': 'minute', 'value': 60}, {'name': 'hour', 'value': 60 * 60}, {'name': 'day', 'value': 24 * 60 * 60}, {'name': 'month', 'value': 30 * 24 * 60 * 60}, ], 'digital': [ {'name': 'byte', 'value': 1}, {'name': 'kilobyte', 'value': 1024}, {'name': 'megabyte', 'value': 1024 * 1024}, {'name': 'gigabyte', 'value': 1024 * 1024 * 1024}, {'name': 'terabyte', 'value': 1024 * 1024 * 1024 * 1024}, ], }; const unitFormatGroups = { 'duration': { 'units': ['second', 'minute', 'hour', 'day', 'month'], 'styles': ['narrow'], 'rounding': 1 }, 'digital': { 'units': ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'], 'styles': ['short'], 'rounding': 0.8 } }; /* * This helper function is used by splitIntoTimeUnits */ function getDurationUnitIdx(name, defaultValue) { if (!name) { return defaultValue; } const pos = durationFormatOrder.indexOf(name); if (pos === -1) { throw new Error('Unknown unit type: ' + name); } return pos; } /* * This helper function is used by DurationFormat */ function splitIntoTimeUnits(v, maxUnitIdx, minUnitIdx) { const units = {}; var input = Math.abs(v); for (var i = maxUnitIdx; i <= minUnitIdx; i++) { const key = durationFormatOrder[i]; const {value} = durationFormatElements[key]; units[key] = i == minUnitIdx ? Math.round(input / value) : Math.floor(input / value); input -= units[key] * value; } return units; } function trimDurationPattern(string, maxUnit, minUnit) { const maxToken = durationFormatElements[maxUnit].token; const minToken = durationFormatElements[minUnit].token; // We currently know of no format that would require reverse order // Even RTL languages use LTR duration formatting, so all we care // are separators. string = string.substring( string.indexOf(maxToken), string.indexOf(minToken) + minToken.length); return string; } /** * This helper function is used by mozIntl.DateTimeFormat * * This is necessary because sometimes toLocaleFormat * uses different timezone than Intl API * which leads to it resolving %p to 'PM' while Intl is in 'AM' * * So what we do here, is we force the same hour in toLocaleFormat API * as we use in Intl API, to enforce the same dayperiod to remove it. * Remove once bug 1208808 is fixed */ function getDayPeriodTokenForDate(date, hourFormatter) { const hourToken = hourFormatter.format(date); const newDate = new Date(date); newDate.setHours(parseInt(hourToken)); return new Intl.DateTimeFormat(navigator.language, {hour:'numeric', hour12:true}).formatToParts(newDate).find(part => part.type === 'dayPeriod').value; } /* * ECMA 402 rev 3., 1.3.4, ComputeTimeUnits */ function computeTimeUnits(v) { const units = {}; const millisecond = Math.round(v); const second = Math.round(millisecond / 1000); const minute = Math.round(second / 60); const hour = Math.round(minute / 60); const day = Math.round(hour / 24); const rawYear = day * 400 / 146097; units.millisecond = millisecond; units.second = second; units.minute = minute; units.hour = hour; units.day = day; units.week = Math.round(day / 7); units.month = Math.round(rawYear * 12); units.quarter = Math.round(rawYear * 4); units.year = Math.round(rawYear); return units; } /* * ECMA 402 rev 3., 1.3.4, GetBestMatchUnit */ function getBestMatchUnit(units) { //if (Math.abs(units.second) < 45) { return 'second'; } if (Math.abs(units.minute) < 45) { return 'minute'; } if (Math.abs(units.hour) < 22) { return 'hour'; } // Intl uses 26 days here if (Math.abs(units.day) < 7) { return 'day'; } if (Math.abs(units.week) < 4) { return 'week'; } if (Math.abs(units.month) < 11) { return 'month'; } //if (Math.abs(units.quarter) < 4) { return 'quarter'; } return 'year'; } function relativeTimeFormatId(x, options) { const ms = x - Date.now(); const units = computeTimeUnits(ms); const unit = options.unit === 'bestFit' ? getBestMatchUnit(units) : options.unit; const v = units[unit]; // CLDR uses past || future const tl = v < 0 ? '-ago' : '-until'; const style = options.style || 'long'; const entry = unit + 's' + tl + '-' + style; return { unit: entry, value: Math.abs(v) }; } function getUnitFormatGroupName(unitName) { for (let groupName in unitFormatGroups) { if (unitFormatGroups[groupName].units.includes(unitName)) { return groupName; } } return undefined; } })(this);