UNPKG

google-closure-library

Version:
439 lines (404 loc) 14.3 kB
/** * @license * Copyright The Closure Library Authors. * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview RelativeDateTimeFormat provides methods to format simple * relative dates and times into a string in a user friendly way and a locale * sensitive manner. Numeric quantities are supported with negative values * indicating the past, zero meaning now, and positive for the future. Specific * named times such as "tomorrow" are returned and correctly pluralized results * are given for relative times without specific names such as "in 5 days" or "3 * weeks ago". The result is localized according to current locale value. * * Similar to the ICU4J class com/ibm/icu/text/RelativeDateTimeFormatter: * http://icu-project.org/apiref/icu4j/com/ibm/icu/text/RelativeDateTimeFormatter.html */ goog.module('goog.i18n.RelativeDateTimeFormat'); // For referencing goog.i18n.USE_ECMASCRIPT_I18N_RDTF to determine compile-time // choice of ECMAScript vs. JavaScript implementation and data. const LocaleFeature = goog.require('goog.i18n.LocaleFeature'); const MessageFormat = goog.require('goog.i18n.MessageFormat'); const asserts = goog.require('goog.asserts'); const relativeDateTimeSymbols = goog.require('goog.i18n.relativeDateTimeSymbols'); /** * @constructor * @param {!RelativeDateTimeFormat.NumericOption=} opt_numeric * This optional string determines if formatted output is always * the numeric formatting rather than available relative strings. * ALWAYS (default) forces numeric results in all uses of this instance. * AUTO mode uses available relative strings such as "tomorrow", falling * back to numeric. * @param {!RelativeDateTimeFormat.Style=} opt_style * This optional value determines the style of the relative time output. * Values include LONG, SHORT, NARROW. Default is LONG. * as part of the resulting formatted string * @param {!relativeDateTimeSymbols.RelativeDateTimeSymbols=} * opt_relativeDateTimeSymbols This optional value can be used to set the * data for to use for this instance rather than obtaining from * relativedatetimesymbols. * @final */ let RelativeDateTimeFormat = function( opt_numeric, opt_style, opt_relativeDateTimeSymbols) { /** * Records if the implementation is ECMAScript * @private @type {boolean} */ this.nativeMode_ = false; if (!LocaleFeature.USE_ECMASCRIPT_I18N_RDTF) { asserts.assert( opt_relativeDateTimeSymbols || relativeDateTimeSymbols.getRelativeDateTimeSymbols(), 'goog.i18n.RelativeDateTimeSymbols requires symbols ECMAScript mode'); /** * RelativeDateTimeSymbols object for locale data required by the formatter. * @private @const {?relativeDateTimeSymbols.RelativeDateTimeSymbols} */ this.rdtfSymbols_ = !LocaleFeature.USE_ECMASCRIPT_I18N_RDTF ? (opt_relativeDateTimeSymbols || relativeDateTimeSymbols.getRelativeDateTimeSymbols()) : null; } if (!this.rdtfSymbols_) { this.nativeMode_ = true; } /** * Flag to force numeric mode in all cases. Normally true. * @private @type {boolean} */ this.alwaysNumeric_ = true; if (opt_numeric) { asserts.assert( opt_numeric == RelativeDateTimeFormat.NumericOption.ALWAYS || opt_numeric == RelativeDateTimeFormat.NumericOption.AUTO, 'Invalid opt_numeric value'); if (opt_numeric == RelativeDateTimeFormat.NumericOption.ALWAYS) { this.alwaysNumeric_ = true; } else if (opt_numeric == RelativeDateTimeFormat.NumericOption.AUTO) { this.alwaysNumeric_ = false; } } /** @private @type {!RelativeDateTimeFormat.Style} */ this.style_ = RelativeDateTimeFormat.Style.LONG; if (opt_style) { asserts.assert( opt_style >= RelativeDateTimeFormat.Style.LONG && opt_style <= RelativeDateTimeFormat.Style.NARROW, 'Style must be LONG, SHORT, or NARROW'); this.style_ = opt_style; } }; /** * Values for setting the numeric mode in the constructor. * @enum {string} */ RelativeDateTimeFormat.NumericOption = { ALWAYS: 'always', AUTO: 'auto', }; /** * Collection of public style symbols. * @enum {number} */ RelativeDateTimeFormat.Style = { LONG: 0, SHORT: 1, NARROW: 2 }; /** * Relative unit constants for public use. * @enum {number} */ RelativeDateTimeFormat.Unit = { YEAR: 0, QUARTER: 1, MONTH: 2, WEEK: 3, DAY: 4, HOUR: 5, MINUTE: 6, SECOND: 7 }; /** * Formats a string with the amount and relative unit. If data for the quantity * is not available in the requested style, then it falls back to next style. If * not available in any style, then it reverts to formatNumeric for the same * unit. * @param {number} quantity A desired offset from current time, negative * for past, 0 for now, positive for future. * @param {!RelativeDateTimeFormat.Unit} relativeUnit Type such as HOUR, YEAR, * QUARTER. * @return {string} The formatted result. May be empty string for an * unsupported locale. */ RelativeDateTimeFormat.prototype.format = function(quantity, relativeUnit) { asserts.assertNumber(quantity, 'Quantity must be a number'); asserts.assert( relativeUnit >= RelativeDateTimeFormat.Unit.YEAR && relativeUnit <= RelativeDateTimeFormat.Unit.SECOND, 'Unit must be one of the supported values'); /** * Special cases to force numeric units, in order * to match ICU4J as described in * http://unicode.org/cldr/trac/ticket/9165 * http://bugs.icu-project.org/trac/ticket/12171 */ if (LocaleFeature.USE_ECMASCRIPT_I18N_RDTF) { return this.formatNative_(quantity, relativeUnit, this.alwaysNumeric_); } else { return this.formatPolyfill_(quantity, relativeUnit, this.alwaysNumeric_); } }; /** * Format using pure JavaScript * @param {number} quantity Desired offset from current date/time. * @param {!RelativeDateTimeFormat.Unit} relativeUnit Type such as HOUR, YEAR, * QUARTER. * @param {boolean} useNumeric True if numeric output is forced. * @return {string} The formatted result. May be empty string for an * unsupported locale. * @private */ RelativeDateTimeFormat.prototype.formatPolyfill_ = function( quantity, relativeUnit, useNumeric) { /** * Find the right data based on Unit, quantity, and plural. */ const rdtfUnitPattern = this.getUnitStylePattern_(relativeUnit); // Formats using Closure Javascript. Check for forcing numeric and having // relative value with the given quantity. if (!useNumeric && rdtfUnitPattern && rdtfUnitPattern.R && rdtfUnitPattern.R['' + quantity]) { return rdtfUnitPattern.R['' + quantity]; } else { // Direction data doesn't exist. Fallback to format numeric. return this.formatNumericInternal_(quantity, rdtfUnitPattern); } }; /** * Format using ECMAScript Intl class RelativeTimeFormat * @param {number} quantity Desired offset from current date/time. * @param {!RelativeDateTimeFormat.Unit} relativeUnit Type such as HOUR, YEAR, * QUARTER. * @param {boolean} useNumeric True if numeric output is forced. * @return {string} The formatted result. May be empty string for an * unsupported locale. * @private */ RelativeDateTimeFormat.prototype.formatNative_ = function( quantity, relativeUnit, useNumeric) { // Use built-in ECMAScript Intl object. let options = { 'numeric': useNumeric ? 'always' : 'auto', }; switch (this.style_) { case RelativeDateTimeFormat.Style.NARROW: options['style'] = 'narrow'; break; case RelativeDateTimeFormat.Style.SHORT: options['style'] = 'short'; break; case RelativeDateTimeFormat.Style.LONG: options['style'] = 'long'; default: break; } // Use built-in ECMAScript Intl object. const intl = goog.global.Intl; let intlFormatter; try { // Fix "_" to "-" to correspond to BCP-47. intlFormatter = new intl.RelativeTimeFormat(goog.LOCALE.replace(/_/g, '-'), options); } catch (err) { // An empty string is returned for an unsupported LOCALE. return ''; } let unit = 'year'; switch (relativeUnit) { case RelativeDateTimeFormat.Unit.YEAR: unit = 'year'; break; case RelativeDateTimeFormat.Unit.QUARTER: unit = 'quarter'; break; case RelativeDateTimeFormat.Unit.MONTH: unit = 'month'; break; case RelativeDateTimeFormat.Unit.WEEK: unit = 'week'; break; case RelativeDateTimeFormat.Unit.DAY: unit = 'day'; break; case RelativeDateTimeFormat.Unit.HOUR: unit = 'hour'; break; case RelativeDateTimeFormat.Unit.MINUTE: unit = 'minute'; break; case RelativeDateTimeFormat.Unit.SECOND: unit = 'second'; break; } return intlFormatter.format(quantity, unit); }; /** * Format with forced numeric value and relative unit. * @param {number} quantity The number of units. * Negative zero will use PAST, while unsiged or positive indicates FUTURE. * @param {!relativeDateTimeSymbols.StyleElement|undefined} unitStylePattern Has * PAST and FUTURE fields. * @return {string} The formatted result. * @private */ RelativeDateTimeFormat.prototype.formatNumericInternal_ = function( quantity, unitStylePattern) { if (!unitStylePattern) return ''; /** * Stores the plural formatting string. * @type {string} */ let relTimeString; const absQuantity = Math.abs(quantity); // Apply MessageFormat to the unit with FUTURE or PAST quantity, with test for // signed zero value. if (quantity > 0 || (quantity == 0 && (1 / quantity) == Infinity)) { relTimeString = unitStylePattern.F; } else { // Negative zero is interpreted as the past. relTimeString = unitStylePattern.P; } /** * Formatter for the messages requiring units. Plural formatting needed. * @type {?MessageFormat} */ // Take basic message and wrap with plural message type. const msgFormatter = new MessageFormat('{N,plural,' + relTimeString + '}'); return msgFormatter.format({'N': absQuantity}); }; /** * From the data, return the information for the given unit and style. * @param {number} relativeUnit * @return {!relativeDateTimeSymbols.StyleElement|undefined} RelativeUnitStyle * @private */ RelativeDateTimeFormat.prototype.getUnitStylePattern_ = function(relativeUnit) { const unitInfo = this.getUnitPattern_(relativeUnit); asserts.assertObject(unitInfo); return this.getStylePattern_(unitInfo); }; /** * Use public unit symbol to retrieve data for that unit, given the style. * @param{!relativeDateTimeSymbols.RelativeDateTimeFormatStyles} unit * @return {!relativeDateTimeSymbols.StyleElement|undefined} * @private */ RelativeDateTimeFormat.prototype.getStylePattern_ = function(unit) { // Fall back from NARROW to SHORT to LONG as needed. switch (this.style_) { case RelativeDateTimeFormat.Style.NARROW: if (unit.NARROW != undefined) { return unit.NARROW; } case RelativeDateTimeFormat.Style.SHORT: if (unit.SHORT != undefined) { return unit.SHORT; } case RelativeDateTimeFormat.Style.LONG: default: return unit.LONG; } }; /** * Returns the style set for this formatter. * @return {number} One of LONG, SHORT, NARROW, */ RelativeDateTimeFormat.prototype.getFormatStyle = function() { return this.style_; }; /** * Returns the status of the alwaysNumeric field. * @return {!RelativeDateTimeFormat.NumericOption} */ RelativeDateTimeFormat.prototype.getNumericMode = function() { if (this.alwaysNumeric_) { return RelativeDateTimeFormat.NumericOption.ALWAYS; } else { return RelativeDateTimeFormat.NumericOption.AUTO; } }; /** * Use public unit symbol to retrieve data for that unit. * @param {number|!relativeDateTimeSymbols.RelativeDateTimeFormatStyles} unit * @return {!relativeDateTimeSymbols.RelativeDateTimeFormatStyles} * @private */ RelativeDateTimeFormat.prototype.getUnitPattern_ = function(unit) { switch (unit) { default: case RelativeDateTimeFormat.Unit.YEAR: return this.rdtfSymbols_.YEAR; case RelativeDateTimeFormat.Unit.QUARTER: return this.rdtfSymbols_.QUARTER; case RelativeDateTimeFormat.Unit.MONTH: return this.rdtfSymbols_.MONTH; case RelativeDateTimeFormat.Unit.WEEK: return this.rdtfSymbols_.WEEK; case RelativeDateTimeFormat.Unit.DAY: return this.rdtfSymbols_.DAY; case RelativeDateTimeFormat.Unit.HOUR: return this.rdtfSymbols_.HOUR; case RelativeDateTimeFormat.Unit.MINUTE: return this.rdtfSymbols_.MINUTE; case RelativeDateTimeFormat.Unit.SECOND: return this.rdtfSymbols_.SECOND; } }; /** * Returns relative field for an offset of a given value unit * if it is defined for the current style. * If the value does not exist, return undefined. * For example, is there a -2 offset for DAY in the current locale and style. * Note: This data is not available in an ECMAScript implementation. * @param{!RelativeDateTimeFormat.Unit} unit * @param{string|number} offset * @return{string|undefined} * @deprecated */ RelativeDateTimeFormat.prototype.isOffsetDefinedForUnit = function( unit, offset) { if (this.rdtfSymbols_ == undefined) { return undefined; } const rdtfUnitPattern = this.getUnitStylePattern_(unit); // Check for force numeric and requested unit and offset. if (typeof (offset) == 'string') { offset = Number(offset); } if (rdtfUnitPattern && rdtfUnitPattern.R && rdtfUnitPattern.R['' + offset]) { return rdtfUnitPattern.R['' + offset]; } else { return undefined; } }; /** * Returns the implementation used for this formatter. * @return {boolean} True iff native mode. False if polyfill. * @package */ RelativeDateTimeFormat.prototype.isNativeMode = function() { return this.nativeMode_; }; /** * Returns true if a ECMAScript formatter is available in the browser. * @return {boolean} Whether the ECMAScript implementation available. * @package */ RelativeDateTimeFormat.prototype.hasNativeRdtf = function() { const intl = goog.global.Intl; return (Boolean(intl && intl.RelativeTimeFormat)); }; exports = RelativeDateTimeFormat;