UNPKG

javascript-time-ago

Version:
652 lines (530 loc) 23.8 kB
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } import RelativeTimeFormatPolyfill from 'relative-time-format'; import Cache from './cache'; import chooseLocale from './locale'; import getStep from './steps/getStep'; import getStepDenominator from './steps/getStepDenominator'; import getTimeToNextUpdate from './steps/getTimeToNextUpdate'; import { addLocaleData, getLocaleData } from './LocaleDataStore'; import defaultStyle from './style/roundMinute'; import getStyleByName from './style/getStyleByName'; import { getRoundFunction } from './round'; // 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} [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 = labelsType, _isArray = Array.isArray(_iterator), _i2 = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { var _ref5; if (_isArray) { if (_i2 >= _iterator.length) break; _ref5 = _iterator[_i2++]; } else { _i2 = _iterator.next(); if (_i2.done) break; _ref5 = _i2.value; } var _labelsType = _ref5; 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. * @param {Object} localeData */ TimeAgo.addDefaultLocale = function (localeData) { if (defaultLocaleHasBeenSpecified) { throw new 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; } } var OBJECT_CONSTRUCTOR = {}.constructor; function isObject(object) { return _typeof(object) !== undefined && object !== null && object.constructor === OBJECT_CONSTRUCTOR; } function isStyle(variable) { return typeof variable === 'string' || isStyleObject(variable); } export function isStyleObject(object) { return isObject(object) && (Array.isArray(object.steps) || // `gradation` property is deprecated: it has been renamed to `steps`. Array.isArray(object.gradation) || // `flavour` property is deprecated: it has been renamed to `labels`. Array.isArray(object.flavour) || typeof object.flavour === 'string' || Array.isArray(object.labels) || typeof object.labels === 'string' || // `units` property is deprecated. Array.isArray(object.units) || // `custom` property is deprecated. typeof object.custom === 'function'); } //# sourceMappingURL=TimeAgo.js.map