UNPKG

javascript-time-ago

Version:

Localized relative date/time formatting

597 lines (557 loc) 24.8 kB
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _createForOfIteratorHelperLoose(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (t) return (t = t.call(r)).next.bind(t); if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var o = 0; return function () { return o >= r.length ? { done: !0 } : { done: !1, value: r[o++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) { n[e] = r[e]; } return n; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0) { ; } } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } import RelativeTimeFormatPolyfill from 'relative-time-format'; import Cache from './cache.js'; import chooseLocale from './locale.js'; import isStyleObject from './isStyleObject.js'; import getStep from './steps/getStep.js'; import getStepDenominator from './steps/getStepDenominator.js'; import getTimeToNextUpdate from './steps/getTimeToNextUpdate.js'; import { addLocaleData, getLocaleData } from './LocaleDataStore.js'; import defaultStyle from './style/roundMinute.js'; import getStyleByName from './style/getStyleByName.js'; import { getRoundFunction } from './round.js'; // Valid time units. var UNITS = ['now', // The rest are the same as in `Intl.RelativeTimeFormat`. 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year']; var TimeAgo = /*#__PURE__*/function () { /** * @param {(string|string[])} locales=[] - Preferred locales (or locale). * @param {boolean} [polyfill] — Pass `false` to use native `Intl.RelativeTimeFormat` and `Intl.PluralRules` instead of the polyfills. */ function TimeAgo() { var locales = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, polyfill = _ref.polyfill; _classCallCheck(this, TimeAgo); // Convert `locales` to an array. if (typeof locales === 'string') { locales = [locales]; } // Choose the most appropriate locale // from the list of `locales` added by the user. // For example, new TimeAgo("en-US") -> "en". this.locale = chooseLocale(locales.concat(TimeAgo.getDefaultLocale()), getLocaleData); if (typeof Intl !== 'undefined') { // Use `Intl.NumberFormat` for formatting numbers (when available). if (Intl.NumberFormat) { this.numberFormat = new Intl.NumberFormat(this.locale); } } // Some people have requested the ability to use native // `Intl.RelativeTimeFormat` and `Intl.PluralRules` // instead of the polyfills. // https://github.com/catamphetamine/javascript-time-ago/issues/21 if (polyfill === false) { this.IntlRelativeTimeFormat = Intl.RelativeTimeFormat; this.IntlPluralRules = Intl.PluralRules; } else { this.IntlRelativeTimeFormat = RelativeTimeFormatPolyfill; this.IntlPluralRules = RelativeTimeFormatPolyfill.PluralRules; } // Cache `Intl.RelativeTimeFormat` instance. this.relativeTimeFormatCache = new Cache(); // Cache `Intl.PluralRules` instance. this.pluralRulesCache = new Cache(); } /** * Formats relative date/time. * * @param {(number|Date)} input — A `Date` or a javascript timestamp. * * @param {(string|object)} style — Date/time formatting style. Either one of the built-in style names or a "custom" style definition object having `steps: object[]` and `labels: string[]`. * * @param {number} [options.now] - Sets the current date timestamp. * * @param {boolean} [options.future] — Tells how to format value `0`: * as "future" (`true`) or "past" (`false`). * Is `false` by default, but should have been `true` actually, * in order to correspond to `Intl.RelativeTimeFormat` * that uses `future` formatting for `0` unless `-0` is passed. * * @param {string} [options.round] — Rounding method. Overrides the style's one. * * @param {boolean} [options.getTimeToNextUpdate] — Pass `true` to return `[formattedDate, timeToNextUpdate]` instead of just `formattedDate`. * * @return {string} The formatted relative date/time. If no eligible `step` is found, then an empty string is returned. */ _createClass(TimeAgo, [{ key: "format", value: function format(input, style, options) { if (!options) { if (style && !isStyle(style)) { options = style; style = undefined; } else { options = {}; } } if (!style) { style = defaultStyle; } if (typeof style === 'string') { style = getStyleByName(style); } var timestamp = getTimestamp(input); // Get locale messages for this type of labels. // "flavour" is a legacy name for "labels". var _this$getLabels = this.getLabels(style.flavour || style.labels), labels = _this$getLabels.labels, labelsType = _this$getLabels.labelsType; var now; // Can pass a custom `now`, e.g. for testing purposes. // // Legacy way was passing `now` in `style`. // That way is deprecated. if (style.now !== undefined) { now = style.now; } // The new way is passing `now` option to `.format()`. if (now === undefined && options.now !== undefined) { now = options.now; } if (now === undefined) { now = Date.now(); } // how much time has passed (in seconds) var secondsPassed = (now - timestamp) / 1000; // in seconds var future = options.future || secondsPassed < 0; var nowLabel = getNowLabel(labels, getLocaleData(this.locale).now, getLocaleData(this.locale)["long"], future); // `custom` – A function of `{ elapsed, time, date, now, locale }`. // // Looks like `custom` function is deprecated and will be removed // in the next major version. // // If this function returns a value, then the `.format()` call will return that value. // Otherwise the relative date/time is formatted as usual. // This feature is currently not used anywhere and is here // just for providing the ultimate customization point // in case anyone would ever need that. Prefer using // `steps[step].format(value, locale)` instead. // if (style.custom) { var custom = style.custom({ now: now, date: new Date(timestamp), time: timestamp, elapsed: secondsPassed, locale: this.locale }); if (custom !== undefined) { // Won't return `timeToNextUpdate` here // because `custom()` seems deprecated. return custom; } } // Get the list of available time interval units. var units = getTimeIntervalMeasurementUnits( // Controlling `style.steps` through `style.units` seems to be deprecated: // create a new custom `style` instead. style.units, labels, nowLabel); // // If no available time unit is suitable, just output an empty string. // if (units.length === 0) { // console.error(`None of the "${units.join(', ')}" time units have been found in "${labelsType}" labels for "${this.locale}" locale.`) // return '' // } var round = options.round || style.round; // Choose the appropriate time measurement unit // and get the corresponding rounded time amount. var _getStep = getStep( // "gradation" is a legacy name for "steps". // For historical reasons, "approximate" steps are used by default. // In the next major version, there'll be no default for `steps`. style.gradation || style.steps || defaultStyle.steps, secondsPassed, { now: now, units: units, round: round, future: future, getNextStep: true }), _getStep2 = _slicedToArray(_getStep, 3), prevStep = _getStep2[0], step = _getStep2[1], nextStep = _getStep2[2]; var formattedDate = this.formatDateForStep(timestamp, step, secondsPassed, { labels: labels, labelsType: labelsType, nowLabel: nowLabel, now: now, future: future, round: round }) || ''; if (options.getTimeToNextUpdate) { var timeToNextUpdate = getTimeToNextUpdate(timestamp, step, { nextStep: nextStep, prevStep: prevStep, now: now, future: future, round: round }); return [formattedDate, timeToNextUpdate]; } return formattedDate; } }, { key: "formatDateForStep", value: function formatDateForStep(timestamp, step, secondsPassed, _ref2) { var _this = this; var labels = _ref2.labels, labelsType = _ref2.labelsType, nowLabel = _ref2.nowLabel, now = _ref2.now, future = _ref2.future, round = _ref2.round; // If no step matches, then output an empty string. if (!step) { return; } if (step.format) { return step.format(timestamp, this.locale, { formatAs: function formatAs(unit, value) { // Mimicks `Intl.RelativeTimeFormat.format()`. return _this.formatValue(value, unit, { labels: labels, future: future }); }, now: now, future: future }); } // "unit" is now called "formatAs". var unit = step.unit || step.formatAs; if (!unit) { throw new Error("[javascript-time-ago] Each step must define either `formatAs` or `format()`. Step: ".concat(JSON.stringify(step))); } // `Intl.RelativeTimeFormat` doesn't operate in "now" units. // Therefore, threat "now" as a special case. if (unit === 'now') { return nowLabel; } // Amount in units. var amount = Math.abs(secondsPassed) / getStepDenominator(step); // Apply granularity to the time amount // (and fallback to the previous step // if the first level of granularity // isn't met by this amount) // // `granularity` — (advanced) Time interval value "granularity". // For example, it could be set to `5` for minutes to allow only 5-minute increments // when formatting time intervals: `0 minutes`, `5 minutes`, `10 minutes`, etc. // Perhaps this feature will be removed because there seem to be no use cases // of it in the real world. // if (step.granularity) { // Recalculate the amount of seconds passed based on granularity amount = getRoundFunction(round)(amount / step.granularity) * step.granularity; } var valueForFormatting = -1 * Math.sign(secondsPassed) * getRoundFunction(round)(amount); // By default, this library formats a `0` in "past" mode, // unless `future: true` option is passed. // This is different to `relative-time-format`'s behavior // which formats a `0` in "future" mode by default, unless it's a `-0`. // So, convert `0` to `-0` if `future: true` option wasn't passed. // `=== 0` matches both `0` and `-0`. if (valueForFormatting === 0) { if (future) { valueForFormatting = 0; } else { valueForFormatting = -0; } } switch (labelsType) { case 'long': case 'short': case 'narrow': // Format the amount using `Intl.RelativeTimeFormat`. return this.getFormatter(labelsType).format(valueForFormatting, unit); default: // Format the amount. // (mimicks `Intl.RelativeTimeFormat` behavior for other time label styles) return this.formatValue(valueForFormatting, unit, { labels: labels, future: future }); } } /** * Mimicks what `Intl.RelativeTimeFormat` does for additional locale styles. * @param {number} value * @param {string} unit * @param {object} options.labels — Relative time labels. * @param {boolean} [options.future] — Tells how to format value `0`: as "future" (`true`) or "past" (`false`). Is `false` by default, but should have been `true` actually. * @return {string} */ }, { key: "formatValue", value: function formatValue(value, unit, _ref3) { var labels = _ref3.labels, future = _ref3.future; return this.getFormattingRule(labels, unit, value, { future: future }).replace('{0}', this.formatNumber(Math.abs(value))); } /** * Returns formatting rule for `value` in `units` (either in past or in future). * @param {object} formattingRules — Relative time labels for different units. * @param {string} unit - Time interval measurement unit. * @param {number} value - Time interval value. * @param {boolean} [options.future] — Tells how to format value `0`: as "future" (`true`) or "past" (`false`). Is `false` by default. * @return {string} * @example * // Returns "{0} days ago" * getFormattingRule(en.long, "day", -2, 'en') */ }, { key: "getFormattingRule", value: function getFormattingRule(formattingRules, unit, value, _ref4) { var future = _ref4.future; // Passing the language is required in order to // be able to correctly classify the `value` as a number. var locale = this.locale; formattingRules = formattingRules[unit]; // Check for a special "compacted" rules case: // if formatting rules are the same for "past" and "future", // and also for all possible `value`s, then those rules are // stored as a single string. if (typeof formattingRules === 'string') { return formattingRules; } // Choose either "past" or "future" based on time `value` sign. // If "past" is same as "future" then they're stored as "other". // If there's only "other" then it's being collapsed. var pastOrFuture = value === 0 ? future ? 'future' : 'past' : value < 0 ? 'past' : 'future'; var quantifierRules = formattingRules[pastOrFuture] || formattingRules; // Bundle size optimization technique. if (typeof quantifierRules === 'string') { return quantifierRules; } // Quantify `value`. var quantifier = this.getPluralRules().select(Math.abs(value)); // "other" rule is supposed to always be present. // If only "other" rule is present then "rules" is not an object and is a string. return quantifierRules[quantifier] || quantifierRules.other; } /** * Formats a number into a string. * Uses `Intl.NumberFormat` when available. * @param {number} number * @return {string} */ }, { key: "formatNumber", value: function formatNumber(number) { return this.numberFormat ? this.numberFormat.format(number) : String(number); } /** * Returns an `Intl.RelativeTimeFormat` for a given `labelsType`. * @param {string} labelsType * @return {object} `Intl.RelativeTimeFormat` instance */ }, { key: "getFormatter", value: function getFormatter(labelsType) { // `Intl.RelativeTimeFormat` instance creation is (hypothetically) assumed // a lengthy operation so the instances are cached and reused. return this.relativeTimeFormatCache.get(this.locale, labelsType) || this.relativeTimeFormatCache.put(this.locale, labelsType, new this.IntlRelativeTimeFormat(this.locale, { style: labelsType })); } /** * Returns an `Intl.PluralRules` instance. * @return {object} `Intl.PluralRules` instance */ }, { key: "getPluralRules", value: function getPluralRules() { // `Intl.PluralRules` instance creation is (hypothetically) assumed // a lengthy operation so the instances are cached and reused. return this.pluralRulesCache.get(this.locale) || this.pluralRulesCache.put(this.locale, new this.IntlPluralRules(this.locale)); } /** * Gets localized labels for this type of labels. * * @param {(string|string[])} labelsType - Relative date/time labels type. * If it's an array then all label types are tried * until a suitable one is found. * * @returns {Object} Returns an object of shape { labelsType, labels } */ }, { key: "getLabels", value: function getLabels() { var labelsType = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; // Convert `labels` to an array. if (typeof labelsType === 'string') { labelsType = [labelsType]; } // Supports legacy "tiny" and "mini-time" label styles. labelsType = labelsType.map(function (labelsType) { switch (labelsType) { case 'tiny': case 'mini-time': return 'mini'; default: return labelsType; } }); // "long" labels type is the default one. // (it's always present for all languages) labelsType = labelsType.concat('long'); // Find a suitable labels type. var localeData = getLocaleData(this.locale); for (var _iterator = _createForOfIteratorHelperLoose(labelsType), _step; !(_step = _iterator()).done;) { var _labelsType = _step.value; if (localeData[_labelsType]) { return { labelsType: _labelsType, labels: localeData[_labelsType] }; } } } }]); return TimeAgo; }(); /** * Default locale global variable. */ export { TimeAgo as default }; var defaultLocale = 'en'; /** * Gets default locale. * @return {string} locale */ TimeAgo.getDefaultLocale = function () { return defaultLocale; }; /** * Sets default locale. * @param {string} locale */ TimeAgo.setDefaultLocale = function (locale) { return defaultLocale = locale; }; /** * Adds locale data for a specific locale and marks the locale as default. * @param {Object} localeData */ TimeAgo.addDefaultLocale = function (localeData) { if (defaultLocaleHasBeenSpecified) { return console.error('[javascript-time-ago] `TimeAgo.addDefaultLocale()` can only be called once. To add other locales, use `TimeAgo.addLocale()`.'); } defaultLocaleHasBeenSpecified = true; TimeAgo.setDefaultLocale(localeData.locale); TimeAgo.addLocale(localeData); }; var defaultLocaleHasBeenSpecified; /** * Adds locale data for a specific locale. * @param {Object} localeData */ TimeAgo.addLocale = function (localeData) { addLocaleData(localeData); RelativeTimeFormatPolyfill.addLocale(localeData); }; /** * (legacy alias) * Adds locale data for a specific locale. * @param {Object} localeData * @deprecated */ TimeAgo.locale = TimeAgo.addLocale; /** * Adds custom labels to locale data. * @param {string} locale * @param {string} name * @param {object} labels */ TimeAgo.addLabels = function (locale, name, labels) { var localeData = getLocaleData(locale); if (!localeData) { addLocaleData({ locale: locale }); localeData = getLocaleData(locale); // throw new Error(`[javascript-time-ago] No data for locale "${locale}"`) } localeData[name] = labels; }; // Normalizes `.format()` `time` argument. function getTimestamp(input) { if (input.constructor === Date || isMockedDate(input)) { return input.getTime(); } if (typeof input === 'number') { return input; } // For some weird reason istanbul doesn't see this `throw` covered. /* istanbul ignore next */ throw new Error("Unsupported relative time formatter input: ".concat(_typeof(input), ", ").concat(input)); } // During testing via some testing libraries `Date`s aren't actually `Date`s. // https://github.com/catamphetamine/javascript-time-ago/issues/22 function isMockedDate(object) { return _typeof(object) === 'object' && typeof object.getTime === 'function'; } // Get available time interval measurement units. function getTimeIntervalMeasurementUnits(allowedUnits, labels, nowLabel) { // Get all time interval measurement units that're available // in locale data for a given time labels style. var units = Object.keys(labels); // `now` unit is handled separately and is shipped in its own `now.json` file. // `now.json` isn't present for all locales, so it could be substituted with // ".second.current". // Add `now` unit if it's available in locale data. if (nowLabel) { units.push('now'); } // If only a specific set of available time measurement units can be used // then only those units are allowed (if they're present in locale data). if (allowedUnits) { units = allowedUnits.filter(function (unit) { return unit === 'now' || units.indexOf(unit) >= 0; }); } return units; } function getNowLabel(labels, nowLabels, longLabels, future) { var nowLabel = labels.now || nowLabels && nowLabels.now; // Specific "now" message form extended locale data (if present). if (nowLabel) { // Bundle size optimization technique. if (typeof nowLabel === 'string') { return nowLabel; } // Not handling `value === 0` as `localeData.now.current` here // because it wouldn't make sense: "now" is a moment, // so one can't possibly differentiate between a // "previous" moment, a "current" moment and a "next moment". // It can only be differentiated between "past" and "future". if (future) { return nowLabel.future; } else { return nowLabel.past; } } // Use ".second.current" as "now" message. if (longLabels && longLabels.second && longLabels.second.current) { return longLabels.second.current; } } function isStyle(variable) { return typeof variable === 'string' || isStyleObject(variable); } //# sourceMappingURL=TimeAgo.js.map