UNPKG

23.8 kBJavaScriptView Raw
1function _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); }
2
3function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
4
5function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
6
7function _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; }
8
9function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
10
11function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
12
13function _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); } }
14
15function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
16
17import RelativeTimeFormatPolyfill from 'relative-time-format';
18import Cache from './cache';
19import chooseLocale from './locale';
20import getStep from './steps/getStep';
21import getStepDenominator from './steps/getStepDenominator';
22import getTimeToNextUpdate from './steps/getTimeToNextUpdate';
23import { addLocaleData, getLocaleData } from './LocaleDataStore';
24import defaultStyle from './style/roundMinute';
25import getStyleByName from './style/getStyleByName';
26import { getRoundFunction } from './round'; // Valid time units.
27
28var UNITS = ['now', // The rest are the same as in `Intl.RelativeTimeFormat`.
29'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'];
30
31var TimeAgo =
32/*#__PURE__*/
33function () {
34 /**
35 * @param {(string|string[])} locales=[] - Preferred locales (or locale).
36 * @param {boolean} [polyfill] — Pass `false` to use native `Intl.RelativeTimeFormat` and `Intl.PluralRules` instead of the polyfills.
37 */
38 function TimeAgo() {
39 var locales = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
40
41 var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
42 polyfill = _ref.polyfill;
43
44 _classCallCheck(this, TimeAgo);
45
46 // Convert `locales` to an array.
47 if (typeof locales === 'string') {
48 locales = [locales];
49 } // Choose the most appropriate locale
50 // from the list of `locales` added by the user.
51 // For example, new TimeAgo("en-US") -> "en".
52
53
54 this.locale = chooseLocale(locales.concat(TimeAgo.getDefaultLocale()), getLocaleData);
55
56 if (typeof Intl !== 'undefined') {
57 // Use `Intl.NumberFormat` for formatting numbers (when available).
58 if (Intl.NumberFormat) {
59 this.numberFormat = new Intl.NumberFormat(this.locale);
60 }
61 } // Some people have requested the ability to use native
62 // `Intl.RelativeTimeFormat` and `Intl.PluralRules`
63 // instead of the polyfills.
64 // https://github.com/catamphetamine/javascript-time-ago/issues/21
65
66
67 if (polyfill === false) {
68 this.IntlRelativeTimeFormat = Intl.RelativeTimeFormat;
69 this.IntlPluralRules = Intl.PluralRules;
70 } else {
71 this.IntlRelativeTimeFormat = RelativeTimeFormatPolyfill;
72 this.IntlPluralRules = RelativeTimeFormatPolyfill.PluralRules;
73 } // Cache `Intl.RelativeTimeFormat` instance.
74
75
76 this.relativeTimeFormatCache = new Cache(); // Cache `Intl.PluralRules` instance.
77
78 this.pluralRulesCache = new Cache();
79 }
80 /**
81 * Formats relative date/time.
82 *
83 * @param {number} [options.now] - Sets the current date timestamp.
84 *
85 * @param {boolean} [options.future] — Tells how to format value `0`:
86 * as "future" (`true`) or "past" (`false`).
87 * Is `false` by default, but should have been `true` actually,
88 * in order to correspond to `Intl.RelativeTimeFormat`
89 * that uses `future` formatting for `0` unless `-0` is passed.
90 *
91 * @param {string} [options.round] — Rounding method. Overrides the style's one.
92 *
93 * @param {boolean} [options.getTimeToNextUpdate] — Pass `true` to return `[formattedDate, timeToNextUpdate]` instead of just `formattedDate`.
94 *
95 * @return {string} The formatted relative date/time. If no eligible `step` is found, then an empty string is returned.
96 */
97
98
99 _createClass(TimeAgo, [{
100 key: "format",
101 value: function format(input, style, options) {
102 if (!options) {
103 if (style && !isStyle(style)) {
104 options = style;
105 style = undefined;
106 } else {
107 options = {};
108 }
109 }
110
111 if (!style) {
112 style = defaultStyle;
113 }
114
115 if (typeof style === 'string') {
116 style = getStyleByName(style);
117 }
118
119 var timestamp = getTimestamp(input); // Get locale messages for this type of labels.
120 // "flavour" is a legacy name for "labels".
121
122 var _this$getLabels = this.getLabels(style.flavour || style.labels),
123 labels = _this$getLabels.labels,
124 labelsType = _this$getLabels.labelsType;
125
126 var now; // Can pass a custom `now`, e.g. for testing purposes.
127 //
128 // Legacy way was passing `now` in `style`.
129 // That way is deprecated.
130
131 if (style.now !== undefined) {
132 now = style.now;
133 } // The new way is passing `now` option to `.format()`.
134
135
136 if (now === undefined && options.now !== undefined) {
137 now = options.now;
138 }
139
140 if (now === undefined) {
141 now = Date.now();
142 } // how much time has passed (in seconds)
143
144
145 var secondsPassed = (now - timestamp) / 1000; // in seconds
146
147 var future = options.future || secondsPassed < 0;
148 var nowLabel = getNowLabel(labels, getLocaleData(this.locale).now, getLocaleData(this.locale).long, future); // `custom` – A function of `{ elapsed, time, date, now, locale }`.
149 //
150 // Looks like `custom` function is deprecated and will be removed
151 // in the next major version.
152 //
153 // If this function returns a value, then the `.format()` call will return that value.
154 // Otherwise the relative date/time is formatted as usual.
155 // This feature is currently not used anywhere and is here
156 // just for providing the ultimate customization point
157 // in case anyone would ever need that. Prefer using
158 // `steps[step].format(value, locale)` instead.
159 //
160
161 if (style.custom) {
162 var custom = style.custom({
163 now: now,
164 date: new Date(timestamp),
165 time: timestamp,
166 elapsed: secondsPassed,
167 locale: this.locale
168 });
169
170 if (custom !== undefined) {
171 // Won't return `timeToNextUpdate` here
172 // because `custom()` seems deprecated.
173 return custom;
174 }
175 } // Get the list of available time interval units.
176
177
178 var units = getTimeIntervalMeasurementUnits( // Controlling `style.steps` through `style.units` seems to be deprecated:
179 // create a new custom `style` instead.
180 style.units, labels, nowLabel); // // If no available time unit is suitable, just output an empty string.
181 // if (units.length === 0) {
182 // console.error(`None of the "${units.join(', ')}" time units have been found in "${labelsType}" labels for "${this.locale}" locale.`)
183 // return ''
184 // }
185
186 var round = options.round || style.round; // Choose the appropriate time measurement unit
187 // and get the corresponding rounded time amount.
188
189 var _getStep = getStep( // "gradation" is a legacy name for "steps".
190 // For historical reasons, "approximate" steps are used by default.
191 // In the next major version, there'll be no default for `steps`.
192 style.gradation || style.steps || defaultStyle.steps, secondsPassed, {
193 now: now,
194 units: units,
195 round: round,
196 future: future,
197 getNextStep: true
198 }),
199 _getStep2 = _slicedToArray(_getStep, 3),
200 prevStep = _getStep2[0],
201 step = _getStep2[1],
202 nextStep = _getStep2[2];
203
204 var formattedDate = this.formatDateForStep(timestamp, step, secondsPassed, {
205 labels: labels,
206 labelsType: labelsType,
207 nowLabel: nowLabel,
208 now: now,
209 future: future,
210 round: round
211 }) || '';
212
213 if (options.getTimeToNextUpdate) {
214 var timeToNextUpdate = getTimeToNextUpdate(timestamp, step, {
215 nextStep: nextStep,
216 prevStep: prevStep,
217 now: now,
218 future: future,
219 round: round
220 });
221 return [formattedDate, timeToNextUpdate];
222 }
223
224 return formattedDate;
225 }
226 }, {
227 key: "formatDateForStep",
228 value: function formatDateForStep(timestamp, step, secondsPassed, _ref2) {
229 var _this = this;
230
231 var labels = _ref2.labels,
232 labelsType = _ref2.labelsType,
233 nowLabel = _ref2.nowLabel,
234 now = _ref2.now,
235 future = _ref2.future,
236 round = _ref2.round;
237
238 // If no step matches, then output an empty string.
239 if (!step) {
240 return;
241 }
242
243 if (step.format) {
244 return step.format(timestamp, this.locale, {
245 formatAs: function formatAs(unit, value) {
246 // Mimicks `Intl.RelativeTimeFormat.format()`.
247 return _this.formatValue(value, unit, {
248 labels: labels,
249 future: future
250 });
251 },
252 now: now,
253 future: future
254 });
255 } // "unit" is now called "formatAs".
256
257
258 var unit = step.unit || step.formatAs;
259
260 if (!unit) {
261 throw new Error("[javascript-time-ago] Each step must define either `formatAs` or `format()`. Step: ".concat(JSON.stringify(step)));
262 } // `Intl.RelativeTimeFormat` doesn't operate in "now" units.
263 // Therefore, threat "now" as a special case.
264
265
266 if (unit === 'now') {
267 return nowLabel;
268 } // Amount in units.
269
270
271 var amount = Math.abs(secondsPassed) / getStepDenominator(step); // Apply granularity to the time amount
272 // (and fallback to the previous step
273 // if the first level of granularity
274 // isn't met by this amount)
275 //
276 // `granularity` — (advanced) Time interval value "granularity".
277 // For example, it could be set to `5` for minutes to allow only 5-minute increments
278 // when formatting time intervals: `0 minutes`, `5 minutes`, `10 minutes`, etc.
279 // Perhaps this feature will be removed because there seem to be no use cases
280 // of it in the real world.
281 //
282
283 if (step.granularity) {
284 // Recalculate the amount of seconds passed based on granularity
285 amount = getRoundFunction(round)(amount / step.granularity) * step.granularity;
286 }
287
288 var valueForFormatting = -1 * Math.sign(secondsPassed) * getRoundFunction(round)(amount); // By default, this library formats a `0` in "past" mode,
289 // unless `future: true` option is passed.
290 // This is different to `relative-time-format`'s behavior
291 // which formats a `0` in "future" mode by default, unless it's a `-0`.
292 // So, convert `0` to `-0` if `future: true` option wasn't passed.
293 // `=== 0` matches both `0` and `-0`.
294
295 if (valueForFormatting === 0) {
296 if (future) {
297 valueForFormatting = 0;
298 } else {
299 valueForFormatting = -0;
300 }
301 }
302
303 switch (labelsType) {
304 case 'long':
305 case 'short':
306 case 'narrow':
307 // Format the amount using `Intl.RelativeTimeFormat`.
308 return this.getFormatter(labelsType).format(valueForFormatting, unit);
309
310 default:
311 // Format the amount.
312 // (mimicks `Intl.RelativeTimeFormat` behavior for other time label styles)
313 return this.formatValue(valueForFormatting, unit, {
314 labels: labels,
315 future: future
316 });
317 }
318 }
319 /**
320 * Mimicks what `Intl.RelativeTimeFormat` does for additional locale styles.
321 * @param {number} value
322 * @param {string} unit
323 * @param {object} options.labels — Relative time labels.
324 * @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.
325 * @return {string}
326 */
327
328 }, {
329 key: "formatValue",
330 value: function formatValue(value, unit, _ref3) {
331 var labels = _ref3.labels,
332 future = _ref3.future;
333 return this.getFormattingRule(labels, unit, value, {
334 future: future
335 }).replace('{0}', this.formatNumber(Math.abs(value)));
336 }
337 /**
338 * Returns formatting rule for `value` in `units` (either in past or in future).
339 * @param {object} formattingRules — Relative time labels for different units.
340 * @param {string} unit - Time interval measurement unit.
341 * @param {number} value - Time interval value.
342 * @param {boolean} [options.future] — Tells how to format value `0`: as "future" (`true`) or "past" (`false`). Is `false` by default.
343 * @return {string}
344 * @example
345 * // Returns "{0} days ago"
346 * getFormattingRule(en.long, "day", -2, 'en')
347 */
348
349 }, {
350 key: "getFormattingRule",
351 value: function getFormattingRule(formattingRules, unit, value, _ref4) {
352 var future = _ref4.future;
353 // Passing the language is required in order to
354 // be able to correctly classify the `value` as a number.
355 var locale = this.locale;
356 formattingRules = formattingRules[unit]; // Check for a special "compacted" rules case:
357 // if formatting rules are the same for "past" and "future",
358 // and also for all possible `value`s, then those rules are
359 // stored as a single string.
360
361 if (typeof formattingRules === 'string') {
362 return formattingRules;
363 } // Choose either "past" or "future" based on time `value` sign.
364 // If "past" is same as "future" then they're stored as "other".
365 // If there's only "other" then it's being collapsed.
366
367
368 var pastOrFuture = value === 0 ? future ? 'future' : 'past' : value < 0 ? 'past' : 'future';
369 var quantifierRules = formattingRules[pastOrFuture] || formattingRules; // Bundle size optimization technique.
370
371 if (typeof quantifierRules === 'string') {
372 return quantifierRules;
373 } // Quantify `value`.
374
375
376 var quantifier = this.getPluralRules().select(Math.abs(value)); // "other" rule is supposed to always be present.
377 // If only "other" rule is present then "rules" is not an object and is a string.
378
379 return quantifierRules[quantifier] || quantifierRules.other;
380 }
381 /**
382 * Formats a number into a string.
383 * Uses `Intl.NumberFormat` when available.
384 * @param {number} number
385 * @return {string}
386 */
387
388 }, {
389 key: "formatNumber",
390 value: function formatNumber(number) {
391 return this.numberFormat ? this.numberFormat.format(number) : String(number);
392 }
393 /**
394 * Returns an `Intl.RelativeTimeFormat` for a given `labelsType`.
395 * @param {string} labelsType
396 * @return {object} `Intl.RelativeTimeFormat` instance
397 */
398
399 }, {
400 key: "getFormatter",
401 value: function getFormatter(labelsType) {
402 // `Intl.RelativeTimeFormat` instance creation is (hypothetically) assumed
403 // a lengthy operation so the instances are cached and reused.
404 return this.relativeTimeFormatCache.get(this.locale, labelsType) || this.relativeTimeFormatCache.put(this.locale, labelsType, new this.IntlRelativeTimeFormat(this.locale, {
405 style: labelsType
406 }));
407 }
408 /**
409 * Returns an `Intl.PluralRules` instance.
410 * @return {object} `Intl.PluralRules` instance
411 */
412
413 }, {
414 key: "getPluralRules",
415 value: function getPluralRules() {
416 // `Intl.PluralRules` instance creation is (hypothetically) assumed
417 // a lengthy operation so the instances are cached and reused.
418 return this.pluralRulesCache.get(this.locale) || this.pluralRulesCache.put(this.locale, new this.IntlPluralRules(this.locale));
419 }
420 /**
421 * Gets localized labels for this type of labels.
422 *
423 * @param {(string|string[])} labelsType - Relative date/time labels type.
424 * If it's an array then all label types are tried
425 * until a suitable one is found.
426 *
427 * @returns {Object} Returns an object of shape { labelsType, labels }
428 */
429
430 }, {
431 key: "getLabels",
432 value: function getLabels() {
433 var labelsType = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
434
435 // Convert `labels` to an array.
436 if (typeof labelsType === 'string') {
437 labelsType = [labelsType];
438 } // Supports legacy "tiny" and "mini-time" label styles.
439
440
441 labelsType = labelsType.map(function (labelsType) {
442 switch (labelsType) {
443 case 'tiny':
444 case 'mini-time':
445 return 'mini';
446
447 default:
448 return labelsType;
449 }
450 }); // "long" labels type is the default one.
451 // (it's always present for all languages)
452
453 labelsType = labelsType.concat('long'); // Find a suitable labels type.
454
455 var localeData = getLocaleData(this.locale);
456
457 for (var _iterator = labelsType, _isArray = Array.isArray(_iterator), _i2 = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) {
458 var _ref5;
459
460 if (_isArray) {
461 if (_i2 >= _iterator.length) break;
462 _ref5 = _iterator[_i2++];
463 } else {
464 _i2 = _iterator.next();
465 if (_i2.done) break;
466 _ref5 = _i2.value;
467 }
468
469 var _labelsType = _ref5;
470
471 if (localeData[_labelsType]) {
472 return {
473 labelsType: _labelsType,
474 labels: localeData[_labelsType]
475 };
476 }
477 }
478 }
479 }]);
480
481 return TimeAgo;
482}();
483/**
484 * Default locale global variable.
485 */
486
487
488export { TimeAgo as default };
489var defaultLocale = 'en';
490/**
491 * Gets default locale.
492 * @return {string} locale
493 */
494
495TimeAgo.getDefaultLocale = function () {
496 return defaultLocale;
497};
498/**
499 * Sets default locale.
500 * @param {string} locale
501 */
502
503
504TimeAgo.setDefaultLocale = function (locale) {
505 return defaultLocale = locale;
506};
507/**
508 * Adds locale data for a specific locale.
509 * @param {Object} localeData
510 */
511
512
513TimeAgo.addDefaultLocale = function (localeData) {
514 if (defaultLocaleHasBeenSpecified) {
515 return console.error('[javascript-time-ago] `TimeAgo.addDefaultLocale()` can only be called once. To add other locales, use `TimeAgo.addLocale()`.');
516 }
517
518 defaultLocaleHasBeenSpecified = true;
519 TimeAgo.setDefaultLocale(localeData.locale);
520 TimeAgo.addLocale(localeData);
521};
522
523var defaultLocaleHasBeenSpecified;
524/**
525 * Adds locale data for a specific locale.
526 * @param {Object} localeData
527 */
528
529TimeAgo.addLocale = function (localeData) {
530 addLocaleData(localeData);
531 RelativeTimeFormatPolyfill.addLocale(localeData);
532};
533/**
534 * (legacy alias)
535 * Adds locale data for a specific locale.
536 * @param {Object} localeData
537 * @deprecated
538 */
539
540
541TimeAgo.locale = TimeAgo.addLocale;
542/**
543 * Adds custom labels to locale data.
544 * @param {string} locale
545 * @param {string} name
546 * @param {object} labels
547 */
548
549TimeAgo.addLabels = function (locale, name, labels) {
550 var localeData = getLocaleData(locale);
551
552 if (!localeData) {
553 addLocaleData({
554 locale: locale
555 });
556 localeData = getLocaleData(locale); // throw new Error(`[javascript-time-ago] No data for locale "${locale}"`)
557 }
558
559 localeData[name] = labels;
560}; // Normalizes `.format()` `time` argument.
561
562
563function getTimestamp(input) {
564 if (input.constructor === Date || isMockedDate(input)) {
565 return input.getTime();
566 }
567
568 if (typeof input === 'number') {
569 return input;
570 } // For some weird reason istanbul doesn't see this `throw` covered.
571
572 /* istanbul ignore next */
573
574
575 throw new Error("Unsupported relative time formatter input: ".concat(_typeof(input), ", ").concat(input));
576} // During testing via some testing libraries `Date`s aren't actually `Date`s.
577// https://github.com/catamphetamine/javascript-time-ago/issues/22
578
579
580function isMockedDate(object) {
581 return _typeof(object) === 'object' && typeof object.getTime === 'function';
582} // Get available time interval measurement units.
583
584
585function getTimeIntervalMeasurementUnits(allowedUnits, labels, nowLabel) {
586 // Get all time interval measurement units that're available
587 // in locale data for a given time labels style.
588 var units = Object.keys(labels); // `now` unit is handled separately and is shipped in its own `now.json` file.
589 // `now.json` isn't present for all locales, so it could be substituted with
590 // ".second.current".
591 // Add `now` unit if it's available in locale data.
592
593 if (nowLabel) {
594 units.push('now');
595 } // If only a specific set of available time measurement units can be used
596 // then only those units are allowed (if they're present in locale data).
597
598
599 if (allowedUnits) {
600 units = allowedUnits.filter(function (unit) {
601 return unit === 'now' || units.indexOf(unit) >= 0;
602 });
603 }
604
605 return units;
606}
607
608function getNowLabel(labels, nowLabels, longLabels, future) {
609 var nowLabel = labels.now || nowLabels && nowLabels.now; // Specific "now" message form extended locale data (if present).
610
611 if (nowLabel) {
612 // Bundle size optimization technique.
613 if (typeof nowLabel === 'string') {
614 return nowLabel;
615 } // Not handling `value === 0` as `localeData.now.current` here
616 // because it wouldn't make sense: "now" is a moment,
617 // so one can't possibly differentiate between a
618 // "previous" moment, a "current" moment and a "next moment".
619 // It can only be differentiated between "past" and "future".
620
621
622 if (future) {
623 return nowLabel.future;
624 } else {
625 return nowLabel.past;
626 }
627 } // Use ".second.current" as "now" message.
628
629
630 if (longLabels && longLabels.second && longLabels.second.current) {
631 return longLabels.second.current;
632 }
633}
634
635var OBJECT_CONSTRUCTOR = {}.constructor;
636
637function isObject(object) {
638 return _typeof(object) !== undefined && object !== null && object.constructor === OBJECT_CONSTRUCTOR;
639}
640
641function isStyle(variable) {
642 return typeof variable === 'string' || isStyleObject(variable);
643}
644
645export function isStyleObject(object) {
646 return isObject(object) && (Array.isArray(object.steps) || // `gradation` property is deprecated: it has been renamed to `steps`.
647 Array.isArray(object.gradation) || // `flavour` property is deprecated: it has been renamed to `labels`.
648 Array.isArray(object.flavour) || typeof object.flavour === 'string' || Array.isArray(object.labels) || typeof object.labels === 'string' || // `units` property is deprecated.
649 Array.isArray(object.units) || // `custom` property is deprecated.
650 typeof object.custom === 'function');
651}
652//# sourceMappingURL=TimeAgo.js.map
\No newline at end of file