javascript-time-ago
Version:
Localized relative date/time formatting
597 lines (557 loc) • 24.8 kB
JavaScript
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