@jsenv/humanize 1.7.7 → 1.8.0

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.
package/README.md CHANGED
@@ -48,3 +48,116 @@ import { humanizeDuration } from "@jsenv/humanize";
48
48
 
49
49
  humanizeDuration(61_421); // "1 minute and 1 second"
50
50
  ```
51
+
52
+ ## format\* — text for the reader of an app
53
+
54
+ Everything above writes English for a developer reading a terminal. The
55
+ `format*` family writes for the person using the app, in their language,
56
+ through `Intl`.
57
+
58
+ | what you are writing | reach for |
59
+ | --------------------------------------------- | -------------------------------------------------- |
60
+ | a message in a terminal, a log, a test report | `humanize`, `humanizeDuration`, `humanizeFileSize` |
61
+ | a date, a delay or a number someone will read | `format*` |
62
+
63
+ It lives in this package, which has no frontend of its own, so that a server
64
+ and a browser word the same instant identically: these are the functions
65
+ behind `@jsenv/navi`'s `<Time>` components, so a notification row written by a
66
+ backend and the card it points at read the same date the same way — and a REST
67
+ service does not install a frontend framework to print a month name. Nothing
68
+ here touches the DOM.
69
+
70
+ ```js
71
+ import {
72
+ formatDatetime,
73
+ formatDay,
74
+ formatDuration,
75
+ formatHourDuration,
76
+ formatMinuteDuration,
77
+ formatMonth,
78
+ formatNumber,
79
+ formatSecondDuration,
80
+ formatTimeOfDay,
81
+ formatTimeRange,
82
+ formatTimeRelative,
83
+ } from "@jsenv/humanize";
84
+
85
+ const start = new Date("2026-05-11T14:05:00");
86
+ const end = new Date("2026-05-11T16:00:00");
87
+ const lang = "fr";
88
+
89
+ // a date
90
+ formatDay(start, { lang }); // "lundi 11 mai"
91
+ formatDay(start, { lang, format: "numeric" }); // "11/05/2026"
92
+ formatMonth(start, { lang }); // "mai 2026"
93
+ formatDatetime(start, { lang }); // "lun. 11 mai à 14:05"
94
+
95
+ // a time of day, a span
96
+ formatTimeOfDay(start, { lang, format: "compact" }); // "14h05"
97
+ formatTimeRange(start, end, { lang, format: "compact" }); // "14h05–16h00"
98
+
99
+ // a duration, from the unit you happen to hold
100
+ formatMinuteDuration(90, { lang, format: "compact" }); // "1h30"
101
+ formatHourDuration(2.5, { lang }); // "2 heures et 30 minutes"
102
+ formatSecondDuration(90, { lang }); // "1 minute et 30 secondes"
103
+ formatDuration("PT1H30M", { lang }); // "1 heure et 30 minutes"
104
+
105
+ // a moment, relative to now (second argument is how long it lasts)
106
+ formatTimeRelative(start, 30 * 60_000, { lang });
107
+ // "dans 1 heure et 30 minutes", "En cours" or "il y a 2 heures"
108
+
109
+ // a number
110
+ formatNumber(1_234_567.5, { lang }); // "1 234 567,5"
111
+ ```
112
+
113
+ Each takes more than `lang` — how a part is spelled, precision, padding, time
114
+ zone, whether the year is written. Those options live in the function's own
115
+ JSDoc, which the published bundle keeps: hover the import in an editor rather
116
+ than looking for a page listing them.
117
+
118
+ Two more, for a date field with nothing in it yet: `formatDatePlaceholder()`
119
+ writes `"jj/mm/aaaa"`, and `formatMonthPlaceholder()`, `formatWeekPlaceholder()`
120
+ and `formatDatetimePlaceholder()` do the same for their own shape.
121
+
122
+ ### Which language
123
+
124
+ With no `lang`, a call uses the runtime language source: by default the
125
+ runtime's own locale, exactly what `Intl` would pick on its own. A frontend
126
+ replaces that source once, with a live one:
127
+
128
+ ```js
129
+ import { setRuntimeLangSource } from "@jsenv/humanize";
130
+
131
+ setRuntimeLangSource(() => languagesSignal.value);
132
+ ```
133
+
134
+ `@jsenv/navi` already does this, pointing it at the user's language
135
+ preference; because the source is read on every call, a component formatting a
136
+ date re-renders when that preference changes.
137
+
138
+ On a server the default is the process locale, which is nobody's in
139
+ particular — **pass `lang` explicitly whenever the text is for someone else**,
140
+ and `timeZone` too when the instant must be worded in the reader's zone rather
141
+ than the machine's.
142
+
143
+ ### Changing a word
144
+
145
+ The words around the numbers — `"time.ongoing"`, the compact unit symbols, the
146
+ placeholder tokens — live in `humanizeI18n`, and every one of them can be
147
+ replaced or translated into a language that is not shipped:
148
+
149
+ ```js
150
+ import { humanizeI18n } from "@jsenv/humanize";
151
+
152
+ humanizeI18n.add("time.ongoing", { fr: "En cours…" });
153
+ humanizeI18n.addLangKeys("ja", { "time.midnight": "真夜中" });
154
+ ```
155
+
156
+ `src/i18n/humanize_i18n.js` is the exhaustive list of keys and defaults, and is
157
+ meant to be read. It is the same registry `@jsenv/navi` exposes as `naviI18n`,
158
+ so one call reaches both.
159
+
160
+ `createI18n()` (an app's own text registry) and `interpolateText()` (one
161
+ sentence with values in it) are exported here too; `@jsenv/navi`'s
162
+ [docs/i18n.md](../../frontend/navi/docs/i18n.md) explains when to use which,
163
+ and why a library's keys are opaque while an app's are not.