diff options
author | Ross Smith II <ross@smithii.com> | 2024-06-05 03:05:51 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-05 06:05:51 -0400 |
commit | 23a900e09609dba6c3254eaffff3ae088e87dd87 (patch) | |
tree | c1f5429df0c34a12d062ecf2078165c2cdf8dcc0 | |
parent | 5a304cf2958435aca91438ee8b1b1265363ac7b4 (diff) |
gui: Use localised time in duration (#9552)
https://github.com/syncthing/syncthing/pull/8291 inpired me to develop
this. I tested it with all the languages Syncthing currently supports,
and they all work.
The only issue is that when you change the language in the GUI, you have
to either refresh the page, or wait a few seconds for the page to
refresh by itself, before the duration is translated into the new
language.
### Screenshots
![2024-05-20_21-47-58](https://github.com/syncthing/syncthing/assets/220772/7e3b371e-3495-4e3e-853a-b5a41215e6c7)
### Documentation
The documentation for the translation widget is at
https://github.com/EvanHahn/HumanizeDuration.js/blob/main/README.md
-rw-r--r-- | gui/default/index.html | 1 | ||||
-rw-r--r-- | gui/default/syncthing/core/durationFilter.js | 51 | ||||
-rw-r--r-- | gui/default/vendor/HumanizeDuration.js/LICENSE.txt | 56 | ||||
-rw-r--r-- | gui/default/vendor/HumanizeDuration.js/Makefile | 7 | ||||
-rw-r--r-- | gui/default/vendor/HumanizeDuration.js/humanize-duration.js | 748 |
5 files changed, 861 insertions, 2 deletions
diff --git a/gui/default/index.html b/gui/default/index.html index d0aee77d1e..5765a5f7bf 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -1050,6 +1050,7 @@ <script type="text/javascript" src="vendor/bootstrap/js/bootstrap.js"></script> <script type="text/javascript" src="vendor/daterangepicker/daterangepicker.js"></script> <script type="text/javascript" src="vendor/fancytree/jquery.fancytree-all-deps.js"></script> + <script type="text/javascript" src="vendor/HumanizeDuration.js/humanize-duration.js"></script> <!-- / vendor scripts --> <!-- gui application code --> diff --git a/gui/default/syncthing/core/durationFilter.js b/gui/default/syncthing/core/durationFilter.js index dc73d3b58a..f423e851fd 100644 --- a/gui/default/syncthing/core/durationFilter.js +++ b/gui/default/syncthing/core/durationFilter.js @@ -7,16 +7,63 @@ * {{1|duration:"h"}} --> <1h **/ angular.module('syncthing.core') - .filter('duration', function () { + .filter('duration', function ($translate) { 'use strict'; var SECONDS_IN = { "d": 86400, "h": 3600, "m": 60, "s": 1 }; return function (input, precision) { - var result = ""; if (!precision) { precision = "s"; } input = parseInt(input, 10); + var language_cc = $translate.use(); + if (language_cc != null) { + language_cc = language_cc.replace("-", "_"); + var fallbacks = []; + var language = language_cc.substr(0, 2); + switch (language) { + case "zh": + // Use zh_TW for zh_HK + fallbacks.push("zh_TW"); + break + } + if (language != language_cc) { + fallbacks.push(language); + } + // Fallback to english, if the language isn't found + fallbacks.push("en"); + + var units = ["d", "h", "m", "s"]; + switch (precision) { + case "d": + units.pop(); + // fallthrough + case "h": + units.pop(); + // fallthrough + case "m": + units.pop(); + // fallthrough + case "s": + break + default: + return "[Error: precision must be d, h, m or s, it's " + precision + "]"; + } + + try { + // humanizeDuration accepts only milliseconds + return humanizeDuration(input * 1000, { + language: language_cc, + maxDecimalPoints: 0, + units: units, + fallbacks: fallbacks + }); + } catch(err) { + console.log(err.message + ": language_cc=" + language_cc) + // if we crash, fallthrough to english + } + } + var result = ""; for (var k in SECONDS_IN) { var t = (input / SECONDS_IN[k] | 0); // Math.floor diff --git a/gui/default/vendor/HumanizeDuration.js/LICENSE.txt b/gui/default/vendor/HumanizeDuration.js/LICENSE.txt new file mode 100644 index 0000000000..960366f61f --- /dev/null +++ b/gui/default/vendor/HumanizeDuration.js/LICENSE.txt @@ -0,0 +1,56 @@ +The MIT License (MIT) + +Copyright (c) 2013-2024 Evan Hahn (@EvanHahn) +Portions copyright (c) 2024 Ross Smith II (@rasa) +Other portions copyright their respective authors, see +https://github.com/EvanHahn/HumanizeDuration.js/graphs/contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +This project is also licensed under the Unlicense license, at your option: + +The Unlicense +----------- +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to <https://unlicense.org/> + +----------- +SPDX-License-Identifier: MIT or UNLICENSE diff --git a/gui/default/vendor/HumanizeDuration.js/Makefile b/gui/default/vendor/HumanizeDuration.js/Makefile new file mode 100644 index 0000000000..b967b0ce61 --- /dev/null +++ b/gui/default/vendor/HumanizeDuration.js/Makefile @@ -0,0 +1,7 @@ + +help: + @echo To update this package, type: make update + +update: + wget -O LICENSE.txt https://raw.githubusercontent.com/rasa/HumanizeDuration.js/main/LICENSE.txt + wget -O humanize-duration.js https://raw.githubusercontent.com/rasa/HumanizeDuration.js/main/humanize-duration.js diff --git a/gui/default/vendor/HumanizeDuration.js/humanize-duration.js b/gui/default/vendor/HumanizeDuration.js/humanize-duration.js new file mode 100644 index 0000000000..bbe129e3c6 --- /dev/null +++ b/gui/default/vendor/HumanizeDuration.js/humanize-duration.js @@ -0,0 +1,748 @@ +// HumanizeDuration.js - https://git.io/j0HgmQ + +// @ts-check + +/** + * @typedef {string | ((unitCount: number) => string)} Unit + */ + +/** + * @typedef {("y" | "mo" | "w" | "d" | "h" | "m" | "s" | "ms")} UnitName + */ + +/** + * @typedef {Object} UnitMeasures + * @prop {number} y + * @prop {number} mo + * @prop {number} w + * @prop {number} d + * @prop {number} h + * @prop {number} m + * @prop {number} s + * @prop {number} ms + */ + +/** + * @internal + * @typedef {[string, string, string, string, string, string, string, string, string, string]} DigitReplacements + */ + +/** + * @typedef {Object} Language + * @prop {Unit} y + * @prop {Unit} mo + * @prop {Unit} w + * @prop {Unit} d + * @prop {Unit} h + * @prop {Unit} m + * @prop {Unit} s + * @prop {Unit} ms + * @prop {string} [decimal] + * @prop {string} [delimiter] + * @prop {DigitReplacements} [_digitReplacements] + * @prop {boolean} [_numberFirst] + */ + +/** + * @typedef {Object} Options + * @prop {string} [language] + * @prop {Record<string, Language>} [languages] + * @prop {string[]} [fallbacks] + * @prop {string} [delimiter] + * @prop {string} [spacer] + * @prop {boolean} [round] + * @prop {number} [largest] + * @prop {UnitName[]} [units] + * @prop {string} [decimal] + * @prop {string} [conjunction] + * @prop {number} [maxDecimalPoints] + * @prop {UnitMeasures} [unitMeasures] + * @prop {boolean} [serialComma] + * @prop {DigitReplacements} [digitReplacements] + */ + +/** + * @internal + * @typedef {Required<Options>} NormalizedOptions + */ + +(function () { + // Fallback for `Object.assign` if relevant. + var assign = + Object.assign || + /** @param {...any} destination */ + function (destination) { + var source; + for (var i = 1; i < arguments.length; i++) { + source = arguments[i]; + for (var prop in source) { + if (has(source, prop)) { + destination[prop] = source[prop]; + } + } + } + return destination; + }; + + // Fallback for `Array.isArray` if relevant. + var isArray = + Array.isArray || + function (arg) { + return Object.prototype.toString.call(arg) === "[object Array]"; + }; + + // This has to be defined separately because of a bug: we want to alias + // `gr` and `el` for backwards-compatiblity. In a breaking change, we can + // remove `gr` entirely. + // See https://github.com/EvanHahn/HumanizeDuration.js/issues/143 for more. + var GREEK = language("έ", "μ", "ε", "η", "ώ", "λ", "δ", "χδ", ","); // + + /** + * @internal + * @type {Record<string, Language>} + */ + var LANGUAGES = { + // Afrikaans (Afrikaans) + af: language("j", "mnd", "w", "d", "u", "m", "s", "ms", ","), + // አማርኛ (Amharic) + am: language("ዓ", "ወ", "ሳ", "ቀ", "ሰ", "ደ", "ሰከ", "ሳ", "ሚሊ"), + //العربية (Arabic) (RTL) + // https://github.com/EvanHahn/HumanizeDuration.js/issues/221#issuecomment-2119762498 + // year -> ع stands for "عام" or س stands for "سنة" + // month -> ش stands for "شهر" + // week -> أ stands for "أسبوع" + // day -> ي stands for "يوم" + // hour -> س stands for "ساعة" + // minute -> د stands for "دقيقة" + // second -> ث stands for "ثانية" + ar: assign(language("س", "ش", "أ", "ي", "س", "د", "ث", "م ث", ","), { + _digitReplacements: ["۰", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"] + }), + // български (Bulgarian) + bg: language("г", "мес", "с", "д", "ч", "м", "сек", "мс", ","), + // বাংলা (Bengali) + bn: language("ব", "ম", "সপ্তা", "দ", "ঘ", "মি", "স", "মি.স"), + // català (Catalan) + ca: language("a", "mes", "set", "d", "h", "m", "s", "ms", ","), + //کوردیی ناوەڕاست (Central Kurdish) (RTL) + ckb: language("م چ", "چ", "خ", "ک", "ڕ", "ه", "م", "س", "."), + // čeština (Czech) + cs: language("r", "měs", "t", "d", "h", "m", "s", "ms", ","), + // Cymraeg (Welsh) + cy: language("b", "mis", "wth", "d", "awr", "mun", "eil", "ms"), + // dansk (Danish) + da: language("å", "md", "u", "d", "t", "m", "s", "ms", ","), + // Deutsch (German) + de: language("J", "mo", "w", "t", "std", "m", "s", "ms", ","), + // Ελληνικά (Greek) + el: GREEK, + // English (English) + en: language("y", "mo", "w", "d", "h", "m", "s", "ms"), + // Esperanto (Esperanto) + eo: language("j", "mo", "se", "t", "h", "m", "s", "ms", ","), + // español (Spanish) + es: language("a", "me", "se", "d", "h", "m", "s", "ms", ","), + // eesti keel (Estonian) + et: language("a", "k", "n", "p", "t", "m", "s", "ms", ","), + // euskara (Basque) + eu: language("u", "h", "a", "e", "o", "m", "s", "ms", ","), + //فارسی (Farsi/Persian) (RTL) + fa: language("س", "ما", "ه", "ر", "سا", "دقی", "ثانی", "میلیثانیه"), + // suomi (Finnish) + fi: language("v", "kk", "vk", "pv", "t", "m", "s", "ms", ","), + // føroyskt (Faroese) + fo: language("á", "má", "v", "d", "t", "m", "s", "ms", ","), + // français (French) + fr: language("a", "m", "sem", "j", "h", "m", "s", "ms", ","), + // Ελληνικά (Greek) (el) + gr: GREEK, + //עברית (Hebrew) (RTL) + he: language("ש׳", "ח׳", "שב׳", "י׳", "שע׳", "ד׳", "שנ׳", "מל׳"), + // hrvatski (Croatian) + hr: language("g", "mj", "t", "d", "h", "m", "s", "ms", ","), + // हिंदी (Hindi) + hi: language("व", "म", "स", "द", "घ", "मि", "से", "मि.से"), + // magyar (Hungarian) + hu: language("é", "h", "hét", "n", "ó", "p", "mp", "ms", ","), + // Indonesia (Indonesian) + id: language("t", "b", "mgg", "h", "j", "m", "d", "md"), + // íslenska (Icelandic) + is: language("ár", "mán", "v", "d", "k", "m", "s", "ms"), + // italiano (Italian) + it: language("a", "me", "se", "g", "h", "m", "s", "ms", ","), + // 日本語 (Japanese) + ja: language("年", "月", "週", "日", "時", "分", "秒", "ミリ秒"), + // ភាសាខ្មែរ (Khmer) + km: language("ឆ", "ខ", "សប្តា", "ថ", "ម", "ន", "វ", "មវ"), + // ಕನ್ನಡ (Kannada) + kn: language("ವ", "ತ", "ವ", "ದ", "ಗಂ", "ನಿ", "ಸೆ", "ಮಿಸೆ"), + // 한국어 (Korean) + ko: language("년", "월", "주", "일", "시", "분", "초", "밀리초"), + // Kurdî (Kurdish) + ku: language("sal", "m", "h", "r", "s", "d", "ç", "ms", ","), + // ລາວ (Lao) + lo: language("ປ", "ເດ", "ອ", "ວ", "ຊ", "ນທ", "ວິນ", "ມິລິວິນາທີ", ","), + // lietuvių (Lithuanian) + lt: language("met", "mėn", "sav", "d", "v", "m", "s", "ms", ","), + // latviešu (Latvian) + lv: language("g", "mēn", "n", "d", "st", "m", "s", "ms", ","), + // македонски (Macedonian) + mk: language("г", "мес", "н", "д", "ч", "м", "с", "мс", ","), + // монгол (Mongolian) + mn: language("ж", "с", "дх", "ө", "ц", "м", "с", "мс"), + // मराठी (Marathi) + mr: language("व", "म", "आ", "दि", "त", "मि", "से", "मि.से"), + // Melayu (Malay) + ms: language("thn", "bln", "mgg", "hr", "j", "m", "s", "ms"), + // Nederlands (Dutch) + nl: language("j", "mnd", "w", "d", "u", "m", "s", "ms", ","), + // norsk (Norwegian) + no: language("år", "mnd", "u", "d", "t", "m", "s", "ms", ","), + // polski (Polish) + pl: language("r", "mi", "t", "d", "g", "m", "s", "ms", ","), + // português (Portuguese) + pt: language("a", "mês", "sem", "d", "h", "m", "s", "ms", ","), + // română (Romanian) săpt? + ro: language("a", "l", "să", "z", "h", "m", "s", "ms", ","), + // русский (Russian) + ru: language("г", "мес", "н", "д", "ч", "м", "с", "мс", ","), + // shqip (Albanian) orë? muaj? + sq: language("v", "mu", "j", "d", "o", "m", "s", "ms", ","), + // српски (Serbian) + sr: language("г", "мес", "н", "д", "ч", "м", "с", "мс", ","), + // தமிழ் (Tamil) + ta: language("ஆ", "மா", "வ", "நா", "ம", "நி", "வி", "மி.வி"), + // తెలుగు (Telugu) + te: language("సం", "నె", "వ", "రో", "గం", "ని", "సె", "మి.సె"), // + // українська (Ukrainian) + uk: language("р", "м", "т", "д", "г", "хв", "с", "мс", ","), + //اردو (Urdu) (RTL) + ur: language("س", "م", "ہ", "د", "گ", "م", "س", "م س"), + // slovenčina (Slovak) + sk: language("r", "mes", "t", "d", "h", "m", "s", "ms", ","), + // slovenščina (Slovenian) + sl: language("l", "mes", "t", "d", "ur", "m", "s", "ms", ","), + // svenska (Swedish) + sv: language("å", "mån", "v", "d", "h", "m", "s", "ms", ","), + // Kiswahili (Swahili) + sw: assign(language("mw", "m", "w", "s", "h", "dk", "s", "ms"), { + _numberFirst: true + }), + // Türkçe (Turkish) + tr: language("y", "a", "h", "g", "sa", "d", "s", "ms", ","), + // ไทย (Thai) + th: language("ปี", "ด", "ส", "ว", "ชม", "น", "วิ", "มิลลิวินาที"), + // o'zbek (Uzbek) + uz: language("y", "o", "h", "k", "soa", "m", "s", "ms"), + // Ўзбек (Кирилл) (Uzbek (Cyrillic)) + uz_CYR: language("й", "о", "х", "к", "соа", "д", "с", "мс"), + // Tiếng Việt (Vietnamese) + vi: language("n", "th", "t", "ng", "gi", "p", "g", "ms", ","), + // 中文 (简体) (Chinese, simplified) + zh_CN: language("年", "月", "周", "天", "时", "分", "秒", "毫秒"), + // 中文 (繁體) (Chinese, traditional) + zh_TW: language("年", "月", "週", "天", "時", "分", "秒", "毫秒") + }; + + /** + * Helper function for creating language definitions. + * + * @internal + * @param {Unit} y + * @param {Unit} mo + * @param {Unit} w + * @param {Unit} d + * @param {Unit} h + * @param {Unit} m + * @param {Unit} s + * @param {Unit} ms + * @param {string} [decimal] + * @returns {Language} + */ + function language(y, mo, w, d, h, m, s, ms, decimal) { + /** @type {Language} */ + var result = { y: y, mo: mo, w: w, d: d, h: h, m: m, s: s, ms: ms }; + if (typeof decimal !== "undefined") { + result.decimal = decimal; + } + return result; + } + + /** + * Helper function for Arabic. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2} + */ + // function getArabicForm(c) { + // if (c === 2) { + // return 1; + // } + // if (c > 2 && c < 11) { + // return 2; + // } + // return 0; + // } + + /** + * Helper function for Polish. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2 | 3} + */ + // function getPolishForm(c) { + // if (c === 1) { + // return 0; + // } + // if (Math.floor(c) !== c) { + // return 1; + // } + // if (c % 10 >= 2 && c % 10 <= 4 && !(c % 100 > 10 && c % 100 < 20)) { + // return 2; + // } + // return 3; + // } + + /** + * Helper function for Slavic languages. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2 | 3} + */ + // function getSlavicForm(c) { + // if (Math.floor(c) !== c) { + // return 2; + // } + // if ( + // (c % 100 >= 5 && c % 100 <= 20) || + // (c % 10 >= 5 && c % 10 <= 9) || + // c % 10 === 0 + // ) { + // return 0; + // } + // if (c % 10 === 1) { + // return 1; + // } + // if (c > 1) { + // return 2; + // } + // return 0; + // } + + /** + * Helper function for Czech or Slovak. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2 | 3} + */ + // function getCzechOrSlovakForm(c) { + // if (c === 1) { + // return 0; + // } + // if (Math.floor(c) !== c) { + // return 1; + // } + // if (c % 10 >= 2 && c % 10 <= 4 && c % 100 < 10) { + // return 2; + // } + // return 3; + // } + + /** + * Helper function for Lithuanian. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2} + */ + // function getLithuanianForm(c) { + // if (c === 1 || (c % 10 === 1 && c % 100 > 20)) { + // return 0; + // } + // if ( + // Math.floor(c) !== c || + // (c % 10 >= 2 && c % 100 > 20) || + // (c % 10 >= 2 && c % 100 < 10) + // ) { + // return 1; + // } + // return 2; + // } + + /** + * Helper function for Latvian. + * + * @internal + * @param {number} c + * @returns {boolean} + */ + // function getLatvianForm(c) { + // return c % 10 === 1 && c % 100 !== 11; + // } + + /** + * @internal + * @template T + * @param {T} obj + * @param {keyof T} key + * @returns {boolean} + */ + function has(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + } + + /** + * @internal + * @param {Pick<Required<Options>, "language" | "fallbacks" | "languages">} options + * @throws {Error} Throws an error if language is not found. + * @returns {Language} + */ + function getLanguage(options) { + var possibleLanguages = [options.language]; + + if (has(options, "fallbacks")) { + if (isArray(options.fallbacks) && options.fallbacks.length) { + possibleLanguages = possibleLanguages.concat(options.fallbacks); + } else { + throw new Error("fallbacks must be an array with at least one element"); + } + } + + for (var i = 0; i < possibleLanguages.length; i++) { + var languageToTry = possibleLanguages[i]; + if (has(options.languages, languageToTry)) { + return options.languages[languageToTry]; + } + if (has(LANGUAGES, languageToTry)) { + return LANGUAGES[languageToTry]; + } + } + + throw new Error("No language found."); + } + + /** + * @internal + * @param {Piece} piece + * @param {Language} language + * @param {Pick<Required<Options>, "decimal" | "spacer" | "maxDecimalPoints" | "digitReplacements">} options + */ + function renderPiece(piece, language, options) { + var unitName = piece.unitName; + var unitCount = piece.unitCount; + + var spacer = options.spacer; + var maxDecimalPoints = options.maxDecimalPoints; + + /** @type {string} */ + var decimal; + if (has(options, "decimal")) { + decimal = options.decimal; + } else if (has(language, "decimal")) { + decimal = language.decimal; + } else { + decimal = "."; + } + + /** @type {undefined | DigitReplacements} */ + var digitReplacements; + if ("digitReplacements" in options) { + digitReplacements = options.digitReplacements; + } else if ("_digitReplacements" in language) { + digitReplacements = language._digitReplacements; + } + + /** @type {string} */ + var formattedCount; + var normalizedUnitCount = + maxDecimalPoints === void 0 + ? unitCount + : Math.floor(unitCount * Math.pow(10, maxDecimalPoints)) / + Math.pow(10, maxDecimalPoints); + var countStr = normalizedUnitCount.toString(); + if (digitReplacements) { + formattedCount = ""; + for (var i = 0; i < countStr.length; i++) { + var char = countStr[i]; + if (char === ".") { + formattedCount += decimal; + } else { + // @ts-ignore because `char` should always be 0-9 at this point. + formattedCount += digitReplacements[char]; + } + } + } else { + formattedCount = countStr.replace(".", decimal); + } + + var languageWord = language[unitName]; + var word; + if (typeof languageWord === "function") { + word = languageWord(unitCount); + } else { + word = languageWord; + } + + if (language._numberFirst) { + return word + spacer + formattedCount; + } + return formattedCount + spacer + word; + } + + /** + * @internal + * @typedef {Object} Piece + * @prop {UnitName} unitName + * @prop {number} unitCount + */ + + /** + * @internal + * @param {number} ms + * @param {Pick<Required<Options>, "units" | "unitMeasures" | "largest" | "round">} options + * @returns {Piece[]} + */ + function getPieces(ms, options) { + /** @type {UnitName} */ + var unitName; + + /** @type {number} */ + var i; + + /** @type {number} */ + var unitCount; + + /** @type {number} */ + var msRemaining; + + var units = options.units; + var unitMeasures = options.unitMeasures; + var largest = "largest" in options ? options.largest : Infinity; + + if (!units.length) return []; + + // Get the counts for each unit. Doesn't round or truncate anything. + // For example, might create an object like `{ y: 7, m: 6, w: 0, d: 5, h: 23.99 }`. + /** @type {Partial<Record<UnitName, number>>} */ + var unitCounts = {}; + msRemaining = ms; + for (i = 0; i < units.length; i++) { + unitName = units[i]; + var unitMs = unitMeasures[unitName]; + + var isLast = i === units.length - 1; + unitCount = isLast + ? msRemaining / unitMs + : Math.floor(msRemaining / unitMs); + unitCounts[unitName] = unitCount; + + msRemaining -= unitCount * unitMs; + } + + if (options.round) { + // Update counts based on the `largest` option. + // For example, if `largest === 2` and `unitCount` is `{ y: 7, m: 6, w: 0, d: 5, h: 23.99 }`, + // updates to something like `{ y: 7, m: 6.2 }`. + var unitsRemainingBeforeRound = largest; + for (i = 0; i < units.length; i++) { + unitName = units[i]; + unitCount = unitCounts[unitName]; + + if (unitCount === 0) continue; + + unitsRemainingBeforeRound--; + + // "Take" the rest of the units into this one. + if (unitsRemainingBeforeRound === 0) { + for (var j = i + 1; j < units.length; j++) { + var smallerUnitName = units[j]; + var smallerUnitCount = unitCounts[smallerUnitName]; + unitCounts[unitName] += + (smallerUnitCount * unitMeasures[smallerUnitName]) / + unitMeasures[unitName]; + unitCounts[smallerUnitName] = 0; + } + break; + } + } + + // Round the last piece (which should be the only non-integer). + // + // This can be a little tricky if the last piece "bubbles up" to a larger + // unit. For example, "3 days, 23.99 hours" should be rounded to "4 days". + // It can also require multiple passes. For example, "6 days, 23.99 hours" + // should become "1 week". + for (i = units.length - 1; i >= 0; i--) { + unitName = units[i]; + unitCount = unitCounts[unitName]; + + if (unitCount === 0) continue; + + var rounded = Math.round(unitCount); + unitCounts[unitName] = rounded; + + if (i === 0) break; + + var previousUnitName = units[i - 1]; + var previousUnitMs = unitMeasures[previousUnitName]; + var amountOfPreviousUnit = Math.floor( + (rounded * unitMeasures[unitName]) / previousUnitMs + ); + if (amountOfPreviousUnit) { + unitCounts[previousUnitName] += amountOfPreviousUnit; + unitCounts[unitName] = 0; + } else { + break; + } + } + } + + /** @type {Piece[]} */ + var result = []; + for (i = 0; i < units.length && result.length < largest; i++) { + unitName = units[i]; + unitCount = unitCounts[unitName]; + if (unitCount) { + result.push({ unitName: unitName, unitCount: unitCount }); + } + } + return result; + } + + /** + * @internal + * @param {Piece[]} pieces + * @param {Pick<Required<Options>, "units" | "language" | "languages" | "fallbacks" | "delimiter" | "spacer" | "decimal" | "conjunction" | "maxDecimalPoints" | "serialComma" | "digitReplacements">} options + * @returns {string} + */ + function formatPieces(pieces, options) { + var language = getLanguage(options); + + if (!pieces.length) { + var units = options.units; + var smallestUnitName = units[units.length - 1]; + return renderPiece( + { unitName: smallestUnitName, unitCount: 0 }, + language, + options + ); + } + + var conjunction = options.conjunction; + var serialComma = options.serialComma; + + var delimiter; + if (has(options, "delimiter")) { + delimiter = options.delimiter; + } else if (has(language, "delimiter")) { + delimiter = language.delimiter; + } else { + delimiter = " "; + } + + /** @type {string[]} */ + var renderedPieces = []; + for (var i = 0; i < pieces.length; i++) { + renderedPieces.push(renderPiece(pieces[i], language, options)); + } + + if (!conjunction || pieces.length === 1) { + return renderedPieces.join(delimiter); + } + + if (pieces.length === 2) { + return renderedPieces.join(conjunction); + } + + return ( + renderedPieces.slice(0, -1).join(delimiter) + + (serialComma ? "," : "") + + conjunction + + renderedPieces.slice(-1) + ); + } + + /** + * Create a humanizer, which lets you change the default options. + * + * @param {Options} [passedOptions] + */ + function humanizer(passedOptions) { + /** + * @param {number} ms + * @param {Options} [humanizerOptions] + * @returns {string} + */ + var result = function humanizer(ms, humanizerOptions) { + // Make sure we have a positive number. + // + // Has the nice side-effect of converting things to numbers. For example, + // converts `"123"` and `Number(123)` to `123`. + ms = Math.abs(ms); + + var options = assign({}, result, humanizerOptions || {}); + + var pieces = getPieces(ms, options); + + return formatPieces(pieces, options); + }; + + return assign( + result, + { + language: "en", + spacer: "", + conjunction: "", + serialComma: true, + units: ["y", "mo", "w", "d", "h", "m", "s"], + languages: {}, + round: false, + unitMeasures: { + y: 31557600000, + mo: 2629800000, + w: 604800000, + d: 86400000, + h: 3600000, + m: 60000, + s: 1000, + ms: 1 + } + }, + passedOptions + ); + } + + /** + * Humanize a duration. + * + * This is a wrapper around the default humanizer. + */ + var humanizeDuration = assign(humanizer({}), { + getSupportedLanguages: function getSupportedLanguages() { + var result = []; + for (var language in LANGUAGES) { + if (has(LANGUAGES, language) && language !== "gr") { + result.push(language); + } + } + return result; + }, + humanizer: humanizer + }); + + // @ts-ignore + if (typeof define === "function" && define.amd) { + // |