@jsenv/humanize 1.8.2 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1969,17 +1969,50 @@ const formatCompactNumber = (value, lang) => {
|
|
|
1969
1969
|
return Number.isFinite(n) ? memoIntl("NumberFormat", lang).format(n) : value;
|
|
1970
1970
|
};
|
|
1971
1971
|
|
|
1972
|
+
// One verbosity vocabulary is shared by everything here, but a word only means
|
|
1973
|
+
// something where the shape it names exists: a clock has no ISO spelling, a
|
|
1974
|
+
// duration no numeric one, a relative sentence neither. Handed to Intl, a word
|
|
1975
|
+
// it does not know answers `Value X out of range for Intl.Y options property
|
|
1976
|
+
// style` — which names neither the caller, nor the option the caller wrote, nor
|
|
1977
|
+
// the value's own vocabulary, and is thrown mid-render, where it blanks the
|
|
1978
|
+
// subtree being drawn. So every formatter says which words it reads, maps the
|
|
1979
|
+
// ones that have a reading of their own, and writes its default wording for the
|
|
1980
|
+
// rest after naming what it dropped.
|
|
1981
|
+
const formatsWarned = new Set();
|
|
1982
|
+
const resolveFormat = (format, { fnName, reads, as }) => {
|
|
1983
|
+
if (reads.includes(format)) {
|
|
1984
|
+
return format;
|
|
1985
|
+
}
|
|
1986
|
+
if (as && Object.hasOwn(as, format)) {
|
|
1987
|
+
return as[format];
|
|
1988
|
+
}
|
|
1989
|
+
// Warn once per (formatter, value): these run in render loops, and a bad
|
|
1990
|
+
// format is a property of the call site, not of the frame.
|
|
1991
|
+
const warnKey = `${fnName}|${String(format)}`;
|
|
1992
|
+
if (!formatsWarned.has(warnKey)) {
|
|
1993
|
+
formatsWarned.add(warnKey);
|
|
1994
|
+
const accepted = [...reads, ...(as ? Object.keys(as) : [])]
|
|
1995
|
+
.map((word) => `"${word}"`)
|
|
1996
|
+
.join(", ");
|
|
1997
|
+
console.warn(
|
|
1998
|
+
`${fnName}: format "${String(format)}" says nothing here — expected ${accepted}; writing the "long" form`,
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
return "long";
|
|
2002
|
+
};
|
|
2003
|
+
|
|
1972
2004
|
/**
|
|
1973
2005
|
* Formats a date as a human-readable day string.
|
|
1974
2006
|
*
|
|
1975
2007
|
* @param {Date} date
|
|
1976
|
-
* @param {{ lang?: string, format?: "long"|"short"|"narrow"|"numeric"|{ weekday?: "long"|"short"|"narrow"|false, day?: boolean, month?: "long"|"short"|"narrow"|"numeric"|false }, year?: boolean|"auto", now?: Date, timeZone?: string }} [options]
|
|
2008
|
+
* @param {{ lang?: string, format?: "long"|"short"|"narrow"|"numeric"|"compact"|{ weekday?: "long"|"short"|"narrow"|false, day?: boolean, month?: "long"|"short"|"narrow"|"numeric"|false }, year?: boolean|"auto", now?: Date, timeZone?: string }} [options]
|
|
1977
2009
|
* A string spells the weekday and the month the same way. An object spells
|
|
1978
2010
|
* them apart, each key defaulting to `"long"`: a narrow card usually wants
|
|
1979
2011
|
* the weekday whole (it is the reading anchor) and the month abbreviated (it
|
|
1980
2012
|
* is where the characters are — "septembre" is 9 of them, "sept." reads the
|
|
1981
2013
|
* same). `"numeric"` stays a string-only spelling: it drops the weekday and
|
|
1982
|
-
* writes the whole date in digits
|
|
2014
|
+
* writes the whole date in digits, and `"compact"` is a name for it — a date
|
|
2015
|
+
* has nothing tighter ("narrow" is the single-letter weekday, "J 17 S").
|
|
1983
2016
|
*
|
|
1984
2017
|
* In the object form, `false` drops a part: `{ day: false, month: false }`
|
|
1985
2018
|
* writes the weekday alone ("mardi"), `{ month: false }` the weekday and
|
|
@@ -2018,6 +2051,16 @@ const formatDay = (
|
|
|
2018
2051
|
timeZone,
|
|
2019
2052
|
} = {},
|
|
2020
2053
|
) => {
|
|
2054
|
+
// The object form spells each part on its own; only a word is resolved.
|
|
2055
|
+
if (typeof format === "string") {
|
|
2056
|
+
format = resolveFormat(format, {
|
|
2057
|
+
fnName: "formatDay",
|
|
2058
|
+
reads: ["long", "short", "narrow", "numeric"],
|
|
2059
|
+
// A date has no notation tighter than its digits: "narrow" is the
|
|
2060
|
+
// single-letter weekday ("J 17 S"), which is not read, it is deciphered.
|
|
2061
|
+
as: { compact: "numeric" },
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2021
2064
|
if (format === "numeric") {
|
|
2022
2065
|
const yearWritten =
|
|
2023
2066
|
year === "auto"
|
|
@@ -2208,20 +2251,31 @@ const formatMonth = (
|
|
|
2208
2251
|
{ lang = getRuntimeLang(), format = "long", timeZone } = {},
|
|
2209
2252
|
) => {
|
|
2210
2253
|
return memoIntl("DateTimeFormat", lang, {
|
|
2211
|
-
month: format,
|
|
2254
|
+
month: resolveFormat(format, {
|
|
2255
|
+
fnName: "formatMonth",
|
|
2256
|
+
reads: ["long", "short", "narrow", "numeric"],
|
|
2257
|
+
as: { compact: "numeric" },
|
|
2258
|
+
}),
|
|
2212
2259
|
year: "numeric",
|
|
2213
2260
|
timeZone,
|
|
2214
2261
|
}).format(date);
|
|
2215
2262
|
};
|
|
2216
2263
|
|
|
2217
2264
|
/**
|
|
2218
|
-
* Formats a date as "lun. 11 mai, 14:30" (long), "11 mai, 14:30" (short),
|
|
2265
|
+
* Formats a date as "lun. 11 mai, 14:30" (long), "11 mai, 14:30" (short),
|
|
2266
|
+
* "11/05, 14:30" (narrow, which `"compact"` is a name for).
|
|
2219
2267
|
* `timeZone` words the instant in that IANA zone instead of the runtime's own.
|
|
2220
2268
|
*/
|
|
2221
2269
|
const formatDatetime = (
|
|
2222
2270
|
date,
|
|
2223
2271
|
{ lang = getRuntimeLang(), format = "long", timeZone } = {},
|
|
2224
2272
|
) => {
|
|
2273
|
+
format = resolveFormat(format, {
|
|
2274
|
+
fnName: "formatDatetime",
|
|
2275
|
+
reads: ["long", "short", "narrow"],
|
|
2276
|
+
// The narrow datetime is already all digits ("17/09 14:30").
|
|
2277
|
+
as: { compact: "narrow" },
|
|
2278
|
+
});
|
|
2225
2279
|
if (format === "long") {
|
|
2226
2280
|
return memoIntl("DateTimeFormat", lang, {
|
|
2227
2281
|
weekday: "short",
|
|
@@ -2313,6 +2367,10 @@ const formatTimeOfDay = (
|
|
|
2313
2367
|
// An "HH:MM" string is a wall-clock reading, not an instant — re-reading
|
|
2314
2368
|
// it in another zone would shift what the caller already spelled out.
|
|
2315
2369
|
const zone = typeof value === "string" ? undefined : timeZone;
|
|
2370
|
+
format = resolveFormat(format, {
|
|
2371
|
+
fnName: "formatTimeOfDay",
|
|
2372
|
+
reads: ["long", "short", "narrow", "compact", "timestring"],
|
|
2373
|
+
});
|
|
2316
2374
|
if (format === "timestring") {
|
|
2317
2375
|
return formatTime(date, { lang, timeZone: zone });
|
|
2318
2376
|
}
|
|
@@ -2517,6 +2575,10 @@ const formatMinuteDuration = (
|
|
|
2517
2575
|
forceUnit = false,
|
|
2518
2576
|
} = {},
|
|
2519
2577
|
) => {
|
|
2578
|
+
format = resolveFormat(format, {
|
|
2579
|
+
fnName: "formatMinuteDuration",
|
|
2580
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2581
|
+
});
|
|
2520
2582
|
if (minutes < 0) {
|
|
2521
2583
|
// the d/h/m split below only holds for a positive value; formatting the
|
|
2522
2584
|
// magnitude and putting the sign back is the only reading that works
|
|
@@ -2608,13 +2670,21 @@ const formatSingleUnit = (value, unit, { lang, format }) => {
|
|
|
2608
2670
|
* formatHourDuration(36, { lang: "fr", forceUnit: true }) // "36 heures"
|
|
2609
2671
|
*/
|
|
2610
2672
|
const formatHourDuration = (hours, options = {}) => {
|
|
2611
|
-
const { lang = getRuntimeLang(),
|
|
2673
|
+
const { lang = getRuntimeLang(), forceUnit } = options;
|
|
2674
|
+
const format = resolveFormat(options.format ?? "long", {
|
|
2675
|
+
fnName: "formatHourDuration",
|
|
2676
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2677
|
+
});
|
|
2612
2678
|
if (hours === 0 || (forceUnit && Number.isInteger(hours))) {
|
|
2613
2679
|
return formatSingleUnit(hours, "hour", { lang, format });
|
|
2614
2680
|
}
|
|
2615
2681
|
// a fractional value has no single-unit spelling, it needs its minutes
|
|
2616
2682
|
const totalMinutes = Math.round(hours * 60);
|
|
2617
|
-
return formatMinuteDuration(totalMinutes, {
|
|
2683
|
+
return formatMinuteDuration(totalMinutes, {
|
|
2684
|
+
...options,
|
|
2685
|
+
format,
|
|
2686
|
+
forceUnit: false,
|
|
2687
|
+
});
|
|
2618
2688
|
};
|
|
2619
2689
|
|
|
2620
2690
|
/**
|
|
@@ -2638,6 +2708,10 @@ const formatSecondDuration = (
|
|
|
2638
2708
|
seconds,
|
|
2639
2709
|
{ lang = getRuntimeLang(), format = "long", forceUnit = false } = {},
|
|
2640
2710
|
) => {
|
|
2711
|
+
format = resolveFormat(format, {
|
|
2712
|
+
fnName: "formatSecondDuration",
|
|
2713
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2714
|
+
});
|
|
2641
2715
|
if (seconds < 0) {
|
|
2642
2716
|
// the d/h/m/s split below only holds for a positive value; formatting the
|
|
2643
2717
|
// magnitude and putting the sign back is the only reading that works
|
|
@@ -2703,6 +2777,10 @@ const formatDuration = (
|
|
|
2703
2777
|
duration,
|
|
2704
2778
|
{ lang = getRuntimeLang(), format = "long" } = {},
|
|
2705
2779
|
) => {
|
|
2780
|
+
format = resolveFormat(format, {
|
|
2781
|
+
fnName: "formatDuration",
|
|
2782
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2783
|
+
});
|
|
2706
2784
|
if (typeof duration === "string") {
|
|
2707
2785
|
duration = parseDuration(duration) ?? {};
|
|
2708
2786
|
} else if (typeof duration === "number") {
|
|
@@ -2848,6 +2926,20 @@ const smallestUnitOf = (duration) => {
|
|
|
2848
2926
|
return null;
|
|
2849
2927
|
};
|
|
2850
2928
|
|
|
2929
|
+
// A relative time is a sentence around a number ("dans 3 heures"), and only
|
|
2930
|
+
// Intl words it — in three styles. "compact" is our own notation, so the
|
|
2931
|
+
// sentence takes the narrowest wording Intl has and the duration inside it
|
|
2932
|
+
// stays compact (see formatFuture).
|
|
2933
|
+
const resolveRelativeFormat = (format) => {
|
|
2934
|
+
return resolveFormat(format, {
|
|
2935
|
+
fnName: "formatTimeRelative",
|
|
2936
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2937
|
+
});
|
|
2938
|
+
};
|
|
2939
|
+
const toRelativeTimeStyle = (relativeFormat) => {
|
|
2940
|
+
return relativeFormat === "compact" ? "narrow" : relativeFormat;
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2851
2943
|
/**
|
|
2852
2944
|
* Formats a date relative to now: "il y a 3 jours", "dans 2 heures", etc.
|
|
2853
2945
|
*/
|
|
@@ -2857,7 +2949,7 @@ const formatTimeAgo = (
|
|
|
2857
2949
|
) => {
|
|
2858
2950
|
const rtf = memoIntl("RelativeTimeFormat", lang, {
|
|
2859
2951
|
numeric: "auto",
|
|
2860
|
-
style: format,
|
|
2952
|
+
style: toRelativeTimeStyle(resolveRelativeFormat(format)),
|
|
2861
2953
|
});
|
|
2862
2954
|
const nowMs = now instanceof Date ? now.getTime() : now;
|
|
2863
2955
|
const diff = date.getTime() - nowMs;
|
|
@@ -2911,7 +3003,12 @@ const formatTimeAgo = (
|
|
|
2911
3003
|
*
|
|
2912
3004
|
* @param {Date|number} start Start of the event (Date or ms timestamp)
|
|
2913
3005
|
* @param {number} durationMs Duration in milliseconds (0 = instant event)
|
|
2914
|
-
* @param {{ lang?: string, now?: Date|number, bare?: boolean, format?: "long"|"short"|"narrow" }} options
|
|
3006
|
+
* @param {{ lang?: string, now?: Date|number, bare?: boolean, format?: "long"|"short"|"narrow"|"compact" }} options
|
|
3007
|
+
* `format` is the verbosity of the wording: `"long"` ("dans 3 heures"),
|
|
3008
|
+
* `"short"` ("dans 3 h"), `"narrow"` ("+3 h") and `"compact"`, which words
|
|
3009
|
+
* the sentence like `"narrow"` and tightens the duration inside it ("dans
|
|
3010
|
+
* 1h30"). The verbosities naming another shape entirely — `"numeric"`,
|
|
3011
|
+
* `"timestring"`, `"iso"` — say nothing here and format as `"long"`.
|
|
2915
3012
|
*
|
|
2916
3013
|
* @example
|
|
2917
3014
|
* // 90 min from now
|
|
@@ -2945,9 +3042,10 @@ const formatTimeRelative = (
|
|
|
2945
3042
|
};
|
|
2946
3043
|
|
|
2947
3044
|
const formatFuture = (date, diff, { lang, now, format = "long" }) => {
|
|
3045
|
+
const relativeFormat = resolveRelativeFormat(format);
|
|
2948
3046
|
const rtf = memoIntl("RelativeTimeFormat", lang, {
|
|
2949
3047
|
numeric: "auto",
|
|
2950
|
-
style:
|
|
3048
|
+
style: toRelativeTimeStyle(relativeFormat),
|
|
2951
3049
|
});
|
|
2952
3050
|
const nowDate = now instanceof Date ? now : new Date(now);
|
|
2953
3051
|
|
|
@@ -2970,7 +3068,7 @@ const formatFuture = (date, diff, { lang, now, format = "long" }) => {
|
|
|
2970
3068
|
}
|
|
2971
3069
|
const duration = formatMinuteDuration(hours * 60 + minutes, {
|
|
2972
3070
|
lang,
|
|
2973
|
-
format,
|
|
3071
|
+
format: relativeFormat,
|
|
2974
3072
|
});
|
|
2975
3073
|
const template = humanizeI18n("time.in_duration", undefined, { lang });
|
|
2976
3074
|
if (template !== "time.in_duration") {
|