@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.
|
@@ -1985,17 +1985,50 @@ const formatCompactNumber = (value, lang) => {
|
|
|
1985
1985
|
return Number.isFinite(n) ? memoIntl("NumberFormat", lang).format(n) : value;
|
|
1986
1986
|
};
|
|
1987
1987
|
|
|
1988
|
+
// One verbosity vocabulary is shared by everything here, but a word only means
|
|
1989
|
+
// something where the shape it names exists: a clock has no ISO spelling, a
|
|
1990
|
+
// duration no numeric one, a relative sentence neither. Handed to Intl, a word
|
|
1991
|
+
// it does not know answers `Value X out of range for Intl.Y options property
|
|
1992
|
+
// style` — which names neither the caller, nor the option the caller wrote, nor
|
|
1993
|
+
// the value's own vocabulary, and is thrown mid-render, where it blanks the
|
|
1994
|
+
// subtree being drawn. So every formatter says which words it reads, maps the
|
|
1995
|
+
// ones that have a reading of their own, and writes its default wording for the
|
|
1996
|
+
// rest after naming what it dropped.
|
|
1997
|
+
const formatsWarned = new Set();
|
|
1998
|
+
const resolveFormat = (format, { fnName, reads, as }) => {
|
|
1999
|
+
if (reads.includes(format)) {
|
|
2000
|
+
return format;
|
|
2001
|
+
}
|
|
2002
|
+
if (as && Object.hasOwn(as, format)) {
|
|
2003
|
+
return as[format];
|
|
2004
|
+
}
|
|
2005
|
+
// Warn once per (formatter, value): these run in render loops, and a bad
|
|
2006
|
+
// format is a property of the call site, not of the frame.
|
|
2007
|
+
const warnKey = `${fnName}|${String(format)}`;
|
|
2008
|
+
if (!formatsWarned.has(warnKey)) {
|
|
2009
|
+
formatsWarned.add(warnKey);
|
|
2010
|
+
const accepted = [...reads, ...(as ? Object.keys(as) : [])]
|
|
2011
|
+
.map((word) => `"${word}"`)
|
|
2012
|
+
.join(", ");
|
|
2013
|
+
console.warn(
|
|
2014
|
+
`${fnName}: format "${String(format)}" says nothing here — expected ${accepted}; writing the "long" form`,
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
return "long";
|
|
2018
|
+
};
|
|
2019
|
+
|
|
1988
2020
|
/**
|
|
1989
2021
|
* Formats a date as a human-readable day string.
|
|
1990
2022
|
*
|
|
1991
2023
|
* @param {Date} date
|
|
1992
|
-
* @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]
|
|
2024
|
+
* @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]
|
|
1993
2025
|
* A string spells the weekday and the month the same way. An object spells
|
|
1994
2026
|
* them apart, each key defaulting to `"long"`: a narrow card usually wants
|
|
1995
2027
|
* the weekday whole (it is the reading anchor) and the month abbreviated (it
|
|
1996
2028
|
* is where the characters are — "septembre" is 9 of them, "sept." reads the
|
|
1997
2029
|
* same). `"numeric"` stays a string-only spelling: it drops the weekday and
|
|
1998
|
-
* writes the whole date in digits
|
|
2030
|
+
* writes the whole date in digits, and `"compact"` is a name for it — a date
|
|
2031
|
+
* has nothing tighter ("narrow" is the single-letter weekday, "J 17 S").
|
|
1999
2032
|
*
|
|
2000
2033
|
* In the object form, `false` drops a part: `{ day: false, month: false }`
|
|
2001
2034
|
* writes the weekday alone ("mardi"), `{ month: false }` the weekday and
|
|
@@ -2034,6 +2067,16 @@ const formatDay = (
|
|
|
2034
2067
|
timeZone,
|
|
2035
2068
|
} = {},
|
|
2036
2069
|
) => {
|
|
2070
|
+
// The object form spells each part on its own; only a word is resolved.
|
|
2071
|
+
if (typeof format === "string") {
|
|
2072
|
+
format = resolveFormat(format, {
|
|
2073
|
+
fnName: "formatDay",
|
|
2074
|
+
reads: ["long", "short", "narrow", "numeric"],
|
|
2075
|
+
// A date has no notation tighter than its digits: "narrow" is the
|
|
2076
|
+
// single-letter weekday ("J 17 S"), which is not read, it is deciphered.
|
|
2077
|
+
as: { compact: "numeric" },
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2037
2080
|
if (format === "numeric") {
|
|
2038
2081
|
const yearWritten =
|
|
2039
2082
|
year === "auto"
|
|
@@ -2224,20 +2267,31 @@ const formatMonth = (
|
|
|
2224
2267
|
{ lang = getRuntimeLang(), format = "long", timeZone } = {},
|
|
2225
2268
|
) => {
|
|
2226
2269
|
return memoIntl("DateTimeFormat", lang, {
|
|
2227
|
-
month: format,
|
|
2270
|
+
month: resolveFormat(format, {
|
|
2271
|
+
fnName: "formatMonth",
|
|
2272
|
+
reads: ["long", "short", "narrow", "numeric"],
|
|
2273
|
+
as: { compact: "numeric" },
|
|
2274
|
+
}),
|
|
2228
2275
|
year: "numeric",
|
|
2229
2276
|
timeZone,
|
|
2230
2277
|
}).format(date);
|
|
2231
2278
|
};
|
|
2232
2279
|
|
|
2233
2280
|
/**
|
|
2234
|
-
* Formats a date as "lun. 11 mai, 14:30" (long), "11 mai, 14:30" (short),
|
|
2281
|
+
* Formats a date as "lun. 11 mai, 14:30" (long), "11 mai, 14:30" (short),
|
|
2282
|
+
* "11/05, 14:30" (narrow, which `"compact"` is a name for).
|
|
2235
2283
|
* `timeZone` words the instant in that IANA zone instead of the runtime's own.
|
|
2236
2284
|
*/
|
|
2237
2285
|
const formatDatetime = (
|
|
2238
2286
|
date,
|
|
2239
2287
|
{ lang = getRuntimeLang(), format = "long", timeZone } = {},
|
|
2240
2288
|
) => {
|
|
2289
|
+
format = resolveFormat(format, {
|
|
2290
|
+
fnName: "formatDatetime",
|
|
2291
|
+
reads: ["long", "short", "narrow"],
|
|
2292
|
+
// The narrow datetime is already all digits ("17/09 14:30").
|
|
2293
|
+
as: { compact: "narrow" },
|
|
2294
|
+
});
|
|
2241
2295
|
if (format === "long") {
|
|
2242
2296
|
return memoIntl("DateTimeFormat", lang, {
|
|
2243
2297
|
weekday: "short",
|
|
@@ -2329,6 +2383,10 @@ const formatTimeOfDay = (
|
|
|
2329
2383
|
// An "HH:MM" string is a wall-clock reading, not an instant — re-reading
|
|
2330
2384
|
// it in another zone would shift what the caller already spelled out.
|
|
2331
2385
|
const zone = typeof value === "string" ? undefined : timeZone;
|
|
2386
|
+
format = resolveFormat(format, {
|
|
2387
|
+
fnName: "formatTimeOfDay",
|
|
2388
|
+
reads: ["long", "short", "narrow", "compact", "timestring"],
|
|
2389
|
+
});
|
|
2332
2390
|
if (format === "timestring") {
|
|
2333
2391
|
return formatTime(date, { lang, timeZone: zone });
|
|
2334
2392
|
}
|
|
@@ -2533,6 +2591,10 @@ const formatMinuteDuration = (
|
|
|
2533
2591
|
forceUnit = false,
|
|
2534
2592
|
} = {},
|
|
2535
2593
|
) => {
|
|
2594
|
+
format = resolveFormat(format, {
|
|
2595
|
+
fnName: "formatMinuteDuration",
|
|
2596
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2597
|
+
});
|
|
2536
2598
|
if (minutes < 0) {
|
|
2537
2599
|
// the d/h/m split below only holds for a positive value; formatting the
|
|
2538
2600
|
// magnitude and putting the sign back is the only reading that works
|
|
@@ -2624,13 +2686,21 @@ const formatSingleUnit = (value, unit, { lang, format }) => {
|
|
|
2624
2686
|
* formatHourDuration(36, { lang: "fr", forceUnit: true }) // "36 heures"
|
|
2625
2687
|
*/
|
|
2626
2688
|
const formatHourDuration = (hours, options = {}) => {
|
|
2627
|
-
const { lang = getRuntimeLang(),
|
|
2689
|
+
const { lang = getRuntimeLang(), forceUnit } = options;
|
|
2690
|
+
const format = resolveFormat(options.format ?? "long", {
|
|
2691
|
+
fnName: "formatHourDuration",
|
|
2692
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2693
|
+
});
|
|
2628
2694
|
if (hours === 0 || (forceUnit && Number.isInteger(hours))) {
|
|
2629
2695
|
return formatSingleUnit(hours, "hour", { lang, format });
|
|
2630
2696
|
}
|
|
2631
2697
|
// a fractional value has no single-unit spelling, it needs its minutes
|
|
2632
2698
|
const totalMinutes = Math.round(hours * 60);
|
|
2633
|
-
return formatMinuteDuration(totalMinutes, {
|
|
2699
|
+
return formatMinuteDuration(totalMinutes, {
|
|
2700
|
+
...options,
|
|
2701
|
+
format,
|
|
2702
|
+
forceUnit: false,
|
|
2703
|
+
});
|
|
2634
2704
|
};
|
|
2635
2705
|
|
|
2636
2706
|
/**
|
|
@@ -2654,6 +2724,10 @@ const formatSecondDuration = (
|
|
|
2654
2724
|
seconds,
|
|
2655
2725
|
{ lang = getRuntimeLang(), format = "long", forceUnit = false } = {},
|
|
2656
2726
|
) => {
|
|
2727
|
+
format = resolveFormat(format, {
|
|
2728
|
+
fnName: "formatSecondDuration",
|
|
2729
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2730
|
+
});
|
|
2657
2731
|
if (seconds < 0) {
|
|
2658
2732
|
// the d/h/m/s split below only holds for a positive value; formatting the
|
|
2659
2733
|
// magnitude and putting the sign back is the only reading that works
|
|
@@ -2719,6 +2793,10 @@ const formatDuration = (
|
|
|
2719
2793
|
duration,
|
|
2720
2794
|
{ lang = getRuntimeLang(), format = "long" } = {},
|
|
2721
2795
|
) => {
|
|
2796
|
+
format = resolveFormat(format, {
|
|
2797
|
+
fnName: "formatDuration",
|
|
2798
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2799
|
+
});
|
|
2722
2800
|
if (typeof duration === "string") {
|
|
2723
2801
|
duration = parseDuration(duration) ?? {};
|
|
2724
2802
|
} else if (typeof duration === "number") {
|
|
@@ -2864,6 +2942,20 @@ const smallestUnitOf = (duration) => {
|
|
|
2864
2942
|
return null;
|
|
2865
2943
|
};
|
|
2866
2944
|
|
|
2945
|
+
// A relative time is a sentence around a number ("dans 3 heures"), and only
|
|
2946
|
+
// Intl words it — in three styles. "compact" is our own notation, so the
|
|
2947
|
+
// sentence takes the narrowest wording Intl has and the duration inside it
|
|
2948
|
+
// stays compact (see formatFuture).
|
|
2949
|
+
const resolveRelativeFormat = (format) => {
|
|
2950
|
+
return resolveFormat(format, {
|
|
2951
|
+
fnName: "formatTimeRelative",
|
|
2952
|
+
reads: ["long", "short", "narrow", "compact"],
|
|
2953
|
+
});
|
|
2954
|
+
};
|
|
2955
|
+
const toRelativeTimeStyle = (relativeFormat) => {
|
|
2956
|
+
return relativeFormat === "compact" ? "narrow" : relativeFormat;
|
|
2957
|
+
};
|
|
2958
|
+
|
|
2867
2959
|
/**
|
|
2868
2960
|
* Formats a date relative to now: "il y a 3 jours", "dans 2 heures", etc.
|
|
2869
2961
|
*/
|
|
@@ -2873,7 +2965,7 @@ const formatTimeAgo = (
|
|
|
2873
2965
|
) => {
|
|
2874
2966
|
const rtf = memoIntl("RelativeTimeFormat", lang, {
|
|
2875
2967
|
numeric: "auto",
|
|
2876
|
-
style: format,
|
|
2968
|
+
style: toRelativeTimeStyle(resolveRelativeFormat(format)),
|
|
2877
2969
|
});
|
|
2878
2970
|
const nowMs = now instanceof Date ? now.getTime() : now;
|
|
2879
2971
|
const diff = date.getTime() - nowMs;
|
|
@@ -2927,7 +3019,12 @@ const formatTimeAgo = (
|
|
|
2927
3019
|
*
|
|
2928
3020
|
* @param {Date|number} start Start of the event (Date or ms timestamp)
|
|
2929
3021
|
* @param {number} durationMs Duration in milliseconds (0 = instant event)
|
|
2930
|
-
* @param {{ lang?: string, now?: Date|number, bare?: boolean, format?: "long"|"short"|"narrow" }} options
|
|
3022
|
+
* @param {{ lang?: string, now?: Date|number, bare?: boolean, format?: "long"|"short"|"narrow"|"compact" }} options
|
|
3023
|
+
* `format` is the verbosity of the wording: `"long"` ("dans 3 heures"),
|
|
3024
|
+
* `"short"` ("dans 3 h"), `"narrow"` ("+3 h") and `"compact"`, which words
|
|
3025
|
+
* the sentence like `"narrow"` and tightens the duration inside it ("dans
|
|
3026
|
+
* 1h30"). The verbosities naming another shape entirely — `"numeric"`,
|
|
3027
|
+
* `"timestring"`, `"iso"` — say nothing here and format as `"long"`.
|
|
2931
3028
|
*
|
|
2932
3029
|
* @example
|
|
2933
3030
|
* // 90 min from now
|
|
@@ -2961,9 +3058,10 @@ const formatTimeRelative = (
|
|
|
2961
3058
|
};
|
|
2962
3059
|
|
|
2963
3060
|
const formatFuture = (date, diff, { lang, now, format = "long" }) => {
|
|
3061
|
+
const relativeFormat = resolveRelativeFormat(format);
|
|
2964
3062
|
const rtf = memoIntl("RelativeTimeFormat", lang, {
|
|
2965
3063
|
numeric: "auto",
|
|
2966
|
-
style:
|
|
3064
|
+
style: toRelativeTimeStyle(relativeFormat),
|
|
2967
3065
|
});
|
|
2968
3066
|
const nowDate = now instanceof Date ? now : new Date(now);
|
|
2969
3067
|
|
|
@@ -2986,7 +3084,7 @@ const formatFuture = (date, diff, { lang, now, format = "long" }) => {
|
|
|
2986
3084
|
}
|
|
2987
3085
|
const duration = formatMinuteDuration(hours * 60 + minutes, {
|
|
2988
3086
|
lang,
|
|
2989
|
-
format,
|
|
3087
|
+
format: relativeFormat,
|
|
2990
3088
|
});
|
|
2991
3089
|
const template = humanizeI18n("time.in_duration", undefined, { lang });
|
|
2992
3090
|
if (template !== "time.in_duration") {
|