@softure-ai/core 0.1.11 → 0.2.1
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/CHANGELOG.md +50 -0
- package/README.md +122 -19
- package/dist/calendar-period.d.ts +18 -0
- package/dist/calendar-period.d.ts.map +1 -0
- package/dist/calendar-period.js +44 -0
- package/dist/calendar-period.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/format.d.ts +35 -14
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +30 -16
- package/dist/format.js.map +1 -1
- package/dist/http-json.d.ts +32 -0
- package/dist/http-json.d.ts.map +1 -0
- package/dist/http-json.js +83 -0
- package/dist/http-json.js.map +1 -0
- package/dist/i18n.d.ts +34 -11
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +40 -10
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/local-date-time.d.ts +10 -0
- package/dist/local-date-time.d.ts.map +1 -0
- package/dist/local-date-time.js +25 -0
- package/dist/local-date-time.js.map +1 -0
- package/dist/messages/en.d.ts +5 -15
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +5 -5
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +1 -1
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/index.js +2 -3
- package/dist/messages/index.js.map +1 -1
- package/package.json +1 -1
- package/src/calendar-period.ts +54 -0
- package/src/config.ts +2 -2
- package/src/format.ts +61 -25
- package/src/http-json.ts +130 -0
- package/src/i18n.ts +53 -19
- package/src/index.ts +24 -1
- package/src/local-date-time.ts +30 -0
- package/src/messages/en.ts +5 -5
- package/src/messages/index.ts +2 -3
- package/dist/messages/pl.d.ts +0 -3
- package/dist/messages/pl.d.ts.map +0 -1
- package/dist/messages/pl.js +0 -12
- package/dist/messages/pl.js.map +0 -1
- package/src/messages/pl.ts +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,56 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
|
|
|
4
4
|
production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
|
|
5
5
|
"Definition of done"). Versions before the first one below are described in their GitHub Releases (`core@x.y.z`).
|
|
6
6
|
|
|
7
|
+
## 0.2.1
|
|
8
|
+
|
|
9
|
+
- New (#599): calendar periods and local wall-clock date-times next to calendar days.
|
|
10
|
+
- `CalendarPeriod` (`{ from, to }`, both ends included) and `calendarPeriodSchema`, which refuses `to` before
|
|
11
|
+
`from` on the path `["to"]` with `must not come before from`.
|
|
12
|
+
- `isDayInPeriod(day, period)`, `doPeriodsOverlap(a, b)` (one shared day overlaps, adjacent periods do not),
|
|
13
|
+
`countPeriodDays(period)` (1 for a single day).
|
|
14
|
+
- `LocalDateTime`, a branded `YYYY-MM-DDTHH:mm` string with no zone, made by `localDateTimeSchema` or
|
|
15
|
+
`isLocalDateTime(value)`; its day part goes through `isCalendarDay`, so `2026-02-30T10:00` is refused.
|
|
16
|
+
`getLocalDay(value)` returns the day.
|
|
17
|
+
- Migrate: an app that defined its own period or local date-time drops its schema, type and helpers and imports
|
|
18
|
+
these. Check two differences: overlap counts a single shared day, and a period's error sits on `to`. An app
|
|
19
|
+
that typed a local date-time as `string` passes values through `localDateTimeSchema` (or `isLocalDateTime`)
|
|
20
|
+
before using them as `LocalDateTime`.
|
|
21
|
+
- `requestJson({ url, fetch?, headers?, timeoutMs?, schema? })`: a JSON GET for third-party APIs (#598). A timeout
|
|
22
|
+
(default 10 000 ms), no redirect followed, `Accept: application/json` by default, and every failure returned as an
|
|
23
|
+
`HttpJsonFailure` with a code and the status, never the URL or the error text: `core.http_unreachable`,
|
|
24
|
+
`core.http_refused` (3xx, 401, 403), `core.http_rate_limited` (429), `core.http_unavailable` (5xx),
|
|
25
|
+
`core.http_unexpected_answer` (other status, not JSON, fails the schema). The body is `unknown` without a zod
|
|
26
|
+
`schema` and the schema's output with one. `toQueryString(record)` builds a query string, skipping `null` and
|
|
27
|
+
`undefined`. English copy for the five codes in `coreMessages` (`errors.http_*`).
|
|
28
|
+
- Migrate (optional): an app with its own request helper replaces it with `requestJson`, passes the same headers
|
|
29
|
+
and timeout, moves its response check into `schema`, and maps the five codes to its own codes and messages
|
|
30
|
+
(`result.error`, `result.status`). The helper's own timeout, redirect and error-scrubbing code can go.
|
|
31
|
+
|
|
32
|
+
## 0.2.0
|
|
33
|
+
|
|
34
|
+
- **Breaking (#446):** English is the only built-in language.
|
|
35
|
+
- `LOCALES` is removed; `Locale` is any BCP 47 tag, `isLocale(value)` says whether a value is a well-formed one,
|
|
36
|
+
`DEFAULT_LOCALE` is `"en"`. `defineSoftureConfig` takes any tag as `locale` and refuses one that is not.
|
|
37
|
+
- `coreMessages` and every module ship `en` only. `Dictionaries<T>` is a complete `en` plus any other locales;
|
|
38
|
+
`MessageOverrides<T>` is keyed by any tag, and `mergeMessages` builds a locale without its own defaults on `en`.
|
|
39
|
+
- New `selectMessages(dictionaries, locale)` (the locale, then its language, then `en`), `pickLocalizedText` and
|
|
40
|
+
`LocalizedText` for an app's per-locale option texts.
|
|
41
|
+
- Migrate: an app that shows Polish keeps its own translation and passes it per module as the overrides for its
|
|
42
|
+
locale, `auth({ messages: { pl: authPl } })`; a key it leaves out reads English. Read a module's copy with
|
|
43
|
+
`selectMessages(module.messages, config.locale)` instead of `module.messages[config.locale]`.
|
|
44
|
+
- **Breaking (#438):** core's formatters build in no language besides English.
|
|
45
|
+
- `formatCalendarMonth` drops the `"genitive"` style; `CalendarMonthStyle` is `"long" | "roman"`. A full date's
|
|
46
|
+
month form stays available through `formatCalendarDay`.
|
|
47
|
+
- `formatDuration(months, options?)` takes `{ locale?, messages? }` instead of a locale. Its words default to
|
|
48
|
+
English (`DEFAULT_DURATION_MESSAGES`); an app plugs in its own translation with `messages: DurationMessages` and
|
|
49
|
+
the `locale` whose plural rules pick a form. Migrate `formatDuration(n, "en")` to `formatDuration(n)`.
|
|
50
|
+
- `coreMessages` no longer has the `duration` group.
|
|
51
|
+
- `formatCalendarDay`, `formatCalendarMonth`, `formatMoney`, `formatPercent` and `selectPlural` take any BCP 47 locale
|
|
52
|
+
tag, not only `LOCALES` (#438). Exported: `DurationMessages`, `FormatDurationOptions`, `DEFAULT_DURATION_MESSAGES`.
|
|
53
|
+
- `formatPercent(basisPoints, locale, { fractionDigits })` prints exactly that many fraction digits (`19.00%`,
|
|
54
|
+
`+3.0%`), an integer from 0 to 20 (otherwise a `RangeError`). A value that rounds to zero at that precision stays
|
|
55
|
+
unsigned (`0.0%`). Without it, nothing changes: at most two digits (#455).
|
|
56
|
+
|
|
7
57
|
## 0.1.11
|
|
8
58
|
|
|
9
59
|
- Formatting (#410): `formatCalendarMonth(month, locale, "long" | "genitive" | "roman")` for `YYYY-MM` months
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ The contract every SOFTURE module stands on. Standard:
|
|
|
6
6
|
## 1. What it provides
|
|
7
7
|
|
|
8
8
|
The app configuration (`defineSoftureConfig`), the module contract (`defineModule`), `Result`
|
|
9
|
-
with namespaced error codes, an injectable `Clock`,
|
|
9
|
+
with namespaced error codes, an injectable `Clock`, English messages with partial overrides and app-supplied translations,
|
|
10
10
|
and `safeError` with `PublicError`. `@softure-ai/core/cli` loads the app's config for the modules' command-line tools.
|
|
11
11
|
`@softure-ai/core/og` (server only) is the OG card font loader the modules share (`createOgFontLoader`,
|
|
12
12
|
`loadOgFonts`); an app takes it from `@softure-ai/seo/server`, where it is documented.
|
|
@@ -27,7 +27,7 @@ interface SoftureConfigInput {
|
|
|
27
27
|
url: string;
|
|
28
28
|
handle?: () => DatabaseHandle | Promise<DatabaseHandle>; // the app's own handle, see below
|
|
29
29
|
} | null;
|
|
30
|
-
locale: "en"
|
|
30
|
+
locale: string; // a BCP 47 tag, e.g. "en" or "de-AT"
|
|
31
31
|
timezone: string; // IANA zone, e.g. "Europe/Warsaw"
|
|
32
32
|
appOrigin: string; // http(s) origin without a path
|
|
33
33
|
landingOrigin?: string; // the public landing site beside the app (default: appOrigin)
|
|
@@ -47,7 +47,7 @@ import { notes } from "@softure-ai/notes";
|
|
|
47
47
|
|
|
48
48
|
const config = defineSoftureConfig({
|
|
49
49
|
database: { url: process.env.DATABASE_URL ?? "" },
|
|
50
|
-
locale: "
|
|
50
|
+
locale: "en",
|
|
51
51
|
timezone: "Europe/Warsaw",
|
|
52
52
|
appOrigin: process.env.APP_ORIGIN ?? "http://localhost:3000",
|
|
53
53
|
modules: [notes({ limit: 10, routes: { list: "/my-notes" }, messages: { en: { list: { title: "My notes" } } } })],
|
|
@@ -249,17 +249,35 @@ None. Tokens, slots and styles live in `@softure-ai/ui`.
|
|
|
249
249
|
|
|
250
250
|
## 9. Copy
|
|
251
251
|
|
|
252
|
-
-
|
|
253
|
-
`
|
|
254
|
-
-
|
|
255
|
-
|
|
252
|
+
- English is the only built-in language: every module ships one complete `en` dictionary, and
|
|
253
|
+
`DEFAULT_LOCALE` is `"en"`. `Locale` is any BCP 47 tag; `isLocale(value)` says whether a value is a
|
|
254
|
+
well-formed one, and `defineSoftureConfig` refuses a `locale` that is not.
|
|
255
|
+
- `Dictionaries<T>` is a complete `en` plus any other locales; `MessageOverrides<T>` partial dictionaries keyed
|
|
256
|
+
by locale. `mergeMessages(defaults, overrides)` returns `en` and every locale the defaults or the overrides
|
|
257
|
+
name; a locale without its own defaults is built on `en`. Unknown keys and a string in place of a group are
|
|
258
|
+
ignored.
|
|
259
|
+
- `selectMessages(dictionaries, locale)` picks the exact locale, then its language (`de-AT` reads `de`), then
|
|
260
|
+
`en`. `pickLocalizedText(text, locale, fallback?)` does the same for an app's `LocalizedText`
|
|
261
|
+
(`{ en: "Monthly", de: "Monatlich" }`).
|
|
262
|
+
- **Another language is the app's own translation.** The app sets `locale` and passes its dictionary as the
|
|
263
|
+
overrides for that locale, per module; a key it leaves out reads English:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
defineSoftureConfig({
|
|
267
|
+
locale: "de",
|
|
268
|
+
// ...
|
|
269
|
+
modules: [auth({ messages: { de: authDe } })], // authDe: DeepPartial<AuthMessages>, kept in the app
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The ui and charts components take the same translation through their `messages` prop (or the ui provider).
|
|
256
274
|
- `formatMessage("{count} of {total}", { count, total })` fills placeholders; a missing value
|
|
257
275
|
stays visible as `{name}`.
|
|
258
276
|
- `selectPlural(locale, count, { one, few, many, other })` uses `Intl.PluralRules`, built once per
|
|
259
|
-
locale and reused.
|
|
277
|
+
locale and reused. `locale` is any BCP 47 tag; a category the forms leave out falls back to `other`.
|
|
260
278
|
- `getMessage(dictionary, "errors.unexpected")` reads a dotted path.
|
|
261
279
|
- Core's own keys (`coreMessages`): `errors.database_failed`, `errors.unexpected`, the copy for
|
|
262
|
-
the `core.database_failed` and `core.unexpected` codes.
|
|
280
|
+
the `core.database_failed` and `core.unexpected` codes, and `errors.http_*` for `requestJson`'s codes.
|
|
263
281
|
|
|
264
282
|
**Errors as values.** `Result<T, E>` is `{ ok: true, value } | { ok: false, error }`, built with
|
|
265
283
|
`ok(value)` / `ok()` and `err("module.code")`. Codes are namespaced by module id
|
|
@@ -287,6 +305,45 @@ if (plan.endedAt !== null) throw new PublicError("This plan has ended; pick anot
|
|
|
287
305
|
`isPublicError` recognises one from another copy of core too (a registry symbol, not `instanceof`).
|
|
288
306
|
`safeError` itself still answers `core.unexpected` for it, since callers switch on its two codes.
|
|
289
307
|
|
|
308
|
+
**External JSON APIs.** `requestJson({ url, fetch?, headers?, timeoutMs?, schema? })` GETs a third-party JSON API
|
|
309
|
+
and returns `Ok<value>` or an `HttpJsonFailure` (`{ ok: false, error, status }`). It sets the details a hand-written
|
|
310
|
+
helper tends to miss:
|
|
311
|
+
|
|
312
|
+
- a timeout, `AbortSignal.timeout(timeoutMs)` (default 10 000 ms), body included;
|
|
313
|
+
- `redirect: "manual"`, so a key in a header or the URL never follows a redirect to another host;
|
|
314
|
+
- `Accept: application/json` unless `headers` sets its own;
|
|
315
|
+
- every failure is a code plus the status (`null` when no response arrived), never the URL, a header or the caught
|
|
316
|
+
error's text (some APIs put the key in the URL, and a fetch error can quote it).
|
|
317
|
+
|
|
318
|
+
| Code | When |
|
|
319
|
+
|---|---|
|
|
320
|
+
| `core.http_unreachable` | network error, timeout, a body that breaks off |
|
|
321
|
+
| `core.http_refused` | 3xx, an opaque redirect, 401, 403 |
|
|
322
|
+
| `core.http_rate_limited` | 429 |
|
|
323
|
+
| `core.http_unavailable` | 5xx |
|
|
324
|
+
| `core.http_unexpected_answer` | any other non-2xx status, a body that is not JSON or fails the schema |
|
|
325
|
+
|
|
326
|
+
Without `schema` the value is `unknown`, to be narrowed at the boundary; with a zod schema it is the schema's
|
|
327
|
+
output. `fetch` defaults to the global one; tests pass a fake. `toQueryString(record)` builds the query part
|
|
328
|
+
(without `?`) from strings, numbers and booleans, leaving out `null` and `undefined`:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import { err, requestJson, toQueryString } from "@softure-ai/core";
|
|
332
|
+
import { z } from "zod";
|
|
333
|
+
|
|
334
|
+
const rateSchema = z.object({ rates: z.array(z.object({ mid: z.number() })) });
|
|
335
|
+
|
|
336
|
+
const result = await requestJson({
|
|
337
|
+
url: `https://api.example.com/rates?${toQueryString({ currency, date, page: undefined })}`,
|
|
338
|
+
headers: { Authorization: `Bearer ${env.RATES_API_KEY}` },
|
|
339
|
+
timeoutMs: 5_000,
|
|
340
|
+
schema: rateSchema,
|
|
341
|
+
});
|
|
342
|
+
if (!result.ok) {
|
|
343
|
+
return err(result.error === "core.http_rate_limited" ? "rates.busy" : "rates.unavailable"); // the app's own codes
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
290
347
|
**Time.** Server code receives a `Clock` (`ModuleContext.clock`) instead of calling `new Date()`.
|
|
291
348
|
`systemClock` is the real one; `createTestClock(start)` has `advance(ms)` and `set(date)`.
|
|
292
349
|
A whole test run can also move to another day without code changes: `TEST_TODAY=YYYY-MM-DD` with the
|
|
@@ -317,31 +374,77 @@ or a fractional count.
|
|
|
317
374
|
|
|
318
375
|
`calendarDaySchema` is `isCalendarDay` as a zod schema, for tool and form inputs that take a day.
|
|
319
376
|
|
|
377
|
+
**Calendar periods.** A `CalendarPeriod` is `{ from, to }`, two calendar days with **both ends included**:
|
|
378
|
+
one day is `{ from: d, to: d }`. `calendarPeriodSchema` parses one from input and refuses `to` before `from`
|
|
379
|
+
with the message `must not come before from` on the path `["to"]`, so a form shows it next to the end field;
|
|
380
|
+
a malformed day gets only its own `calendarDaySchema` issue.
|
|
381
|
+
|
|
382
|
+
| Function | Semantics |
|
|
383
|
+
| --- | --- |
|
|
384
|
+
| `isDayInPeriod(day, period)` | `day` lies in `period`, either end included. |
|
|
385
|
+
| `doPeriodsOverlap(a, b)` | The periods share at least one day: `12-01..12-31` and `12-31..01-05` overlap, `12-01..12-31` and `01-01..01-31` do not. |
|
|
386
|
+
| `countPeriodDays(period)` | Days in the period, both ends counted: 1 for a single day, 31 for December. |
|
|
387
|
+
|
|
388
|
+
They throw a `RangeError` for a malformed day or a period that ends before it starts.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const input = z.object({ stay: calendarPeriodSchema }).parse(body);
|
|
392
|
+
countPeriodDays(input.stay); // 7
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Local date-times.** A `LocalDateTime` is `YYYY-MM-DDTHH:mm` with no time zone: a time that is local at a
|
|
396
|
+
place, such as a departure or a booking slot, read the same whatever the server's zone. The day part must be a
|
|
397
|
+
real calendar day (`2026-02-30T10:00` is refused), the hour 00-23 and the minute 00-59; no seconds, no offset.
|
|
398
|
+
It is a branded string, so only `localDateTimeSchema` (input) or `isLocalDateTime(value)` (a guard) make one,
|
|
399
|
+
and it sorts as text in time order. `getLocalDay(value)` returns its `YYYY-MM-DD` day.
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const departure = localDateTimeSchema.parse("2026-12-01T06:45"); // LocalDateTime
|
|
403
|
+
isDayInPeriod(getLocalDay(departure), trip); // true when the departure falls on a day of the trip
|
|
404
|
+
```
|
|
405
|
+
|
|
320
406
|
**Display.** One way to show days, money and percentages in every module and app, in `config.locale`:
|
|
321
407
|
|
|
322
408
|
```ts
|
|
323
409
|
formatCalendarDay("2026-10-04", "en"); // "October 4, 2026"; "medium": "Oct 4, 2026"; "numeric": "10/04/2026"
|
|
324
410
|
formatCalendarDay(toCalendarDay(instant, config.timezone), config.locale); // an instant's day in the app's zone
|
|
325
|
-
formatMoney(123456, "PLN", "en"); // "PLN 1,234.56";
|
|
411
|
+
formatMoney(123456, "PLN", "en"); // "PLN 1,234.56"; thousands always grouped, also where a locale would not
|
|
326
412
|
formatMoney(-123456, "PLN", "en", { rounded: true, signed: true }); // "-PLN 1,235"; { compact: true }: "PLN 1.2K"
|
|
327
413
|
formatPercent(1250, "en"); // "12.5%" (basis points, at most two fraction digits)
|
|
328
414
|
formatPercent(-50, "en", { signed: true, tight: true }); // "-0.5%"; 1250 signed: "+12.5%"
|
|
329
|
-
|
|
330
|
-
|
|
415
|
+
formatPercent(1900, "en", { fractionDigits: 2 }); // "19.00%"; 300 at one digit, signed: "+3.0%"
|
|
416
|
+
formatCalendarMonth("2026-10", "en"); // "October 2026"; "roman": "X 2026"
|
|
417
|
+
formatDuration(38); // "3 years and 2 months" (whole months, as wholeMonthsBetween returns them)
|
|
331
418
|
```
|
|
332
419
|
|
|
333
|
-
|
|
334
|
-
|
|
420
|
+
Every formatter takes any BCP 47 locale tag (an invalid tag throws `Intl`'s `RangeError`), and
|
|
421
|
+
writes it as `Intl` does. A day is formatted at UTC midnight in UTC, so no zone moves it. `formatMoney` takes the amount in the currency's minor unit, read from the pinned ISO 4217
|
|
335
422
|
table `CURRENCY_MINOR_UNIT_DIGITS` (a code outside it gets `Intl`'s digits), and always groups thousands.
|
|
336
423
|
Zero never carries a sign: a negative zero (`Math.round(-0.4)`) or an amount that rounds to zero shows as `PLN 0.00`,
|
|
337
424
|
and a percentage that rounds to zero as `0%`. `signed` puts a `+` before a positive amount or percentage; the minus is
|
|
338
|
-
the one `Intl` writes (a hyphen-minus in en
|
|
425
|
+
the one `Intl` writes (a hyphen-minus in en). `tight` drops any space a locale's pattern puts before `%`.
|
|
426
|
+
`fractionDigits` fixes the number of fraction digits (an integer from 0 to 20) so a column reads equally precise; a
|
|
427
|
+
value that rounds to zero at that precision is still unsigned (`0.0%`).
|
|
339
428
|
|
|
340
429
|
`formatCalendarMonth(month, locale, style)` takes `YYYY-MM` (`isCalendarMonth`) or a `YYYY-MM-DD` day. `long` is the
|
|
341
|
-
month as written alone
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
itself is "0 months".
|
|
430
|
+
month as written alone, `roman` the month in Roman numerals for compact tables.
|
|
431
|
+
|
|
432
|
+
`formatDuration(months, { locale, messages })` writes years and months; a part that is zero is left out, and zero
|
|
433
|
+
itself is "0 months". Its only built-in words are English (`DEFAULT_DURATION_MESSAGES`). An app that shows another
|
|
434
|
+
language passes its own `DurationMessages` and the locale whose plural rules pick among them; a locale alone keeps the
|
|
435
|
+
English words:
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
const durationDe: DurationMessages = {
|
|
439
|
+
years: { one: "{count} Jahr", other: "{count} Jahre" },
|
|
440
|
+
months: { one: "{count} Monat", other: "{count} Monate" },
|
|
441
|
+
both: "{years} und {months}",
|
|
442
|
+
};
|
|
443
|
+
formatDuration(13, { locale: "de", messages: durationDe }); // "1 Jahr und 1 Monat"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Give every plural category the locale uses (`Intl.PluralRules`: `one`, `few`, `many`, `other`, ...); a missing one
|
|
447
|
+
falls back to `other`.
|
|
345
448
|
|
|
346
449
|
**Memoization.** `memoizeByContent(fn, { max })` caches a pure function of plain data by the content of its arguments:
|
|
347
450
|
two equal objects built apart (key order aside) hit the same entry. The least recently used entry goes beyond `max`;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Calendar days from `from` to `to`, both included; `to` never comes before `from`. */
|
|
3
|
+
export interface CalendarPeriod {
|
|
4
|
+
readonly from: string;
|
|
5
|
+
readonly to: string;
|
|
6
|
+
}
|
|
7
|
+
/** A `CalendarPeriod` in tool and form input: two calendar days, `to` refused (path `["to"]`) when it comes first. */
|
|
8
|
+
export declare const calendarPeriodSchema: z.ZodObject<{
|
|
9
|
+
from: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
10
|
+
to: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
/** Whether `day` lies in `period`, both ends included. Throws a `RangeError` for a malformed day or period. */
|
|
13
|
+
export declare function isDayInPeriod(day: string, period: CalendarPeriod): boolean;
|
|
14
|
+
/** Whether `a` and `b` share at least one day; adjacent periods do not. Throws a `RangeError` for a malformed period. */
|
|
15
|
+
export declare function doPeriodsOverlap(a: CalendarPeriod, b: CalendarPeriod): boolean;
|
|
16
|
+
/** The number of days in `period`, both ends included: 1 for a single day. Throws a `RangeError` for a malformed period. */
|
|
17
|
+
export declare function countPeriodDays(period: CalendarPeriod): number;
|
|
18
|
+
//# sourceMappingURL=calendar-period.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar-period.d.ts","sourceRoot":"","sources":["../src/calendar-period.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,sHAAsH;AACtH,eAAO,MAAM,oBAAoB;;;iBAO7B,CAAC;AAEL,+GAA+G;AAC/G,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAK1E;AAED,yHAAyH;AACzH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,OAAO,CAK9E;AAED,4HAA4H;AAC5H,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAG9D"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// A calendar period: two calendar days (`YYYY-MM-DD`) with both ends included, so one day is
|
|
2
|
+
// `{ from: d, to: d }`. Zero-padded days sort as text in calendar order, so comparisons work on the strings.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { calendarDaysBetween, calendarDaySchema, isCalendarDay } from "./calendar-day.js";
|
|
5
|
+
/** A `CalendarPeriod` in tool and form input: two calendar days, `to` refused (path `["to"]`) when it comes first. */
|
|
6
|
+
export const calendarPeriodSchema = z
|
|
7
|
+
.object({ from: calendarDaySchema, to: calendarDaySchema })
|
|
8
|
+
// A malformed end already has its own issue, and zod still runs the refinement then: skip it, so the
|
|
9
|
+
// order is checked only between two real days.
|
|
10
|
+
.refine((period) => !isCalendarDay(period.from) || !isCalendarDay(period.to) || period.from <= period.to, {
|
|
11
|
+
message: "must not come before from",
|
|
12
|
+
path: ["to"],
|
|
13
|
+
});
|
|
14
|
+
/** Whether `day` lies in `period`, both ends included. Throws a `RangeError` for a malformed day or period. */
|
|
15
|
+
export function isDayInPeriod(day, period) {
|
|
16
|
+
const operation = "isDayInPeriod";
|
|
17
|
+
assertCalendarDay(operation, day);
|
|
18
|
+
assertPeriod(operation, period);
|
|
19
|
+
return period.from <= day && day <= period.to;
|
|
20
|
+
}
|
|
21
|
+
/** Whether `a` and `b` share at least one day; adjacent periods do not. Throws a `RangeError` for a malformed period. */
|
|
22
|
+
export function doPeriodsOverlap(a, b) {
|
|
23
|
+
const operation = "doPeriodsOverlap";
|
|
24
|
+
assertPeriod(operation, a);
|
|
25
|
+
assertPeriod(operation, b);
|
|
26
|
+
return a.from <= b.to && b.from <= a.to;
|
|
27
|
+
}
|
|
28
|
+
/** The number of days in `period`, both ends included: 1 for a single day. Throws a `RangeError` for a malformed period. */
|
|
29
|
+
export function countPeriodDays(period) {
|
|
30
|
+
assertPeriod("countPeriodDays", period);
|
|
31
|
+
return calendarDaysBetween(period.from, period.to) + 1;
|
|
32
|
+
}
|
|
33
|
+
function assertPeriod(operation, period) {
|
|
34
|
+
assertCalendarDay(operation, period.from);
|
|
35
|
+
assertCalendarDay(operation, period.to);
|
|
36
|
+
if (period.to < period.from) {
|
|
37
|
+
throw new RangeError(`${operation}: the period ends ("${period.to}") before it starts ("${period.from}")`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function assertCalendarDay(operation, day) {
|
|
41
|
+
if (!isCalendarDay(day))
|
|
42
|
+
throw new RangeError(`${operation}: "${String(day)}" is not a YYYY-MM-DD calendar day`);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=calendar-period.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar-period.js","sourceRoot":"","sources":["../src/calendar-period.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,6GAA6G;AAC7G,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAQ1F,sHAAsH;AACtH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,iBAAiB,EAAE,CAAC;IAC3D,qGAAqG;IACrG,+CAA+C;KAC9C,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,EAAE,EAAE;IACxG,OAAO,EAAE,2BAA2B;IACpC,IAAI,EAAE,CAAC,IAAI,CAAC;CACb,CAAC,CAAC;AAEL,+GAA+G;AAC/G,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,MAAsB;IAC/D,MAAM,SAAS,GAAG,eAAe,CAAC;IAClC,iBAAiB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAClC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC,IAAI,IAAI,GAAG,IAAI,GAAG,IAAI,MAAM,CAAC,EAAE,CAAC;AAChD,CAAC;AAED,yHAAyH;AACzH,MAAM,UAAU,gBAAgB,CAAC,CAAiB,EAAE,CAAiB;IACnE,MAAM,SAAS,GAAG,kBAAkB,CAAC;IACrC,YAAY,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAC3B,YAAY,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAC3B,OAAO,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC;AAC1C,CAAC;AAED,4HAA4H;AAC5H,MAAM,UAAU,eAAe,CAAC,MAAsB;IACpD,YAAY,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,mBAAmB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,YAAY,CAAC,SAAiB,EAAE,MAAsB;IAC7D,iBAAiB,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IAC1C,iBAAiB,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACxC,IAAI,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,IAAI,UAAU,CAAC,GAAG,SAAS,uBAAuB,MAAM,CAAC,EAAE,yBAAyB,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC;IAC7G,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,SAAiB,EAAE,GAAW;IACvD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,SAAS,MAAM,MAAM,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AACnH,CAAC"}
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAGnD;;;GAGG;AAEH,MAAM,WAAW,0BAA0B;CAAG;AAE9C,iHAAiH;AACjH,MAAM,MAAM,qBAAqB,GAAG,0BAA0B,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5H,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,YAAY,CAAC,qBAAqB,CAAC,CAAC;CAC7D;AAED,qHAAqH;AACrH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AAED,kDAAkD;AAClD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AA+BD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAoB5E;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEzF;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GACnC,MAAM,CAAC,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAGrD"}
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { formatIssues, SoftureConfigError } from "./config-error.js";
|
|
5
5
|
import { isDatabaseOptional } from "./database-requirement.js";
|
|
6
|
-
import {
|
|
6
|
+
import { isLocale } from "./i18n.js";
|
|
7
7
|
import { err, ok } from "./result.js";
|
|
8
8
|
import { parseVersionRange, satisfiesRange } from "./version-range.js";
|
|
9
9
|
const CONFIG_SUBJECT = "softure.config (defineSoftureConfig)";
|
|
@@ -20,7 +20,7 @@ const configSchema = z.object({
|
|
|
20
20
|
})
|
|
21
21
|
.nullable()
|
|
22
22
|
.default(null),
|
|
23
|
-
locale: z.
|
|
23
|
+
locale: z.string().refine(isLocale, "must be a BCP 47 language tag, e.g. en or de-AT"),
|
|
24
24
|
timezone: z.string().refine(isTimeZone, "must be an IANA time zone, e.g. Europe/Warsaw"),
|
|
25
25
|
appOrigin: z.string().refine(isOrigin, ORIGIN_MESSAGE),
|
|
26
26
|
landingOrigin: z.string().refine(isOrigin, ORIGIN_MESSAGE).optional(),
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,gEAAgE;AAChE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/format.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
/** `long`: October 4, 2026
|
|
1
|
+
import { type PluralForms } from "./i18n.js";
|
|
2
|
+
/** `long`: October 4, 2026; `medium`: Oct 4, 2026; `numeric`: 10/04/2026 (each locale writes its own order and separators). */
|
|
3
3
|
export type CalendarDayStyle = "long" | "medium" | "numeric";
|
|
4
4
|
export interface FormatMoneyOptions {
|
|
5
5
|
/** Whole units only, rounded half away from zero: PLN 1,235 for 1234.56. */
|
|
@@ -9,39 +9,60 @@ export interface FormatMoneyOptions {
|
|
|
9
9
|
/** The locale's short notation for large amounts: PLN 1.2M. */
|
|
10
10
|
readonly compact?: boolean;
|
|
11
11
|
}
|
|
12
|
-
/** `long`: October 2026
|
|
13
|
-
export type CalendarMonthStyle = "long" | "
|
|
12
|
+
/** `long`: October 2026; `roman`: X 2026. */
|
|
13
|
+
export type CalendarMonthStyle = "long" | "roman";
|
|
14
|
+
/** The words `formatDuration` writes: plural forms with `{count}`, and `both` joining `{years}` and `{months}`. */
|
|
15
|
+
export interface DurationMessages {
|
|
16
|
+
readonly years: PluralForms;
|
|
17
|
+
readonly months: PluralForms;
|
|
18
|
+
readonly both: string;
|
|
19
|
+
}
|
|
20
|
+
export interface FormatDurationOptions {
|
|
21
|
+
/** The BCP 47 tag whose plural rules pick a form (default `"en"`). */
|
|
22
|
+
readonly locale?: string;
|
|
23
|
+
/** The app's own words (default `DEFAULT_DURATION_MESSAGES`, English). */
|
|
24
|
+
readonly messages?: DurationMessages;
|
|
25
|
+
}
|
|
14
26
|
export interface FormatPercentOptions {
|
|
15
27
|
/** A `+` before a positive value (a `-` before a negative one is always there). Zero has neither in any mode. */
|
|
16
28
|
readonly signed?: boolean;
|
|
17
29
|
/** No space between the number and `%`, whatever the locale's pattern: `12,5%`, not `12,5 %`. */
|
|
18
30
|
readonly tight?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Exactly this many fraction digits, an integer from 0 to 20: `{ fractionDigits: 2 }` writes 1900 as `19.00%`.
|
|
33
|
+
* Without it a value has at most two fraction digits and no trailing zeros.
|
|
34
|
+
*/
|
|
35
|
+
readonly fractionDigits?: number;
|
|
19
36
|
}
|
|
37
|
+
/** The built-in, English words of `formatDuration`; any other category falls back to `other`. */
|
|
38
|
+
export declare const DEFAULT_DURATION_MESSAGES: DurationMessages;
|
|
20
39
|
/** A `YYYY-MM-DD` day as `locale` writes it. Throws a `RangeError` for a malformed day. */
|
|
21
|
-
export declare function formatCalendarDay(day: string, locale:
|
|
40
|
+
export declare function formatCalendarDay(day: string, locale: string, style?: CalendarDayStyle): string;
|
|
22
41
|
/** Whether `value` is a real calendar month written `YYYY-MM` (`2026-13` and `2026-1` are not). */
|
|
23
42
|
export declare function isCalendarMonth(value: unknown): value is string;
|
|
24
43
|
/**
|
|
25
44
|
* The month of a `YYYY-MM` month or a `YYYY-MM-DD` day, with its year, as `locale` writes it alone. Throws a
|
|
26
45
|
* `RangeError` for anything else.
|
|
27
46
|
*/
|
|
28
|
-
export declare function formatCalendarMonth(month: string, locale:
|
|
47
|
+
export declare function formatCalendarMonth(month: string, locale: string, style?: CalendarMonthStyle): string;
|
|
29
48
|
/**
|
|
30
|
-
* Whole months as years and months
|
|
31
|
-
*
|
|
49
|
+
* Whole months as years and months: 38 is "3 years and 2 months"; a part that is zero is left out, and zero itself is
|
|
50
|
+
* "0 months". English by default; an app passes its own `messages` and the `locale` whose plural rules pick among
|
|
51
|
+
* them. Throws a `RangeError` for a negative or fractional count, or an invalid locale tag.
|
|
32
52
|
*/
|
|
33
|
-
export declare function formatDuration(months: number,
|
|
53
|
+
export declare function formatDuration(months: number, options?: FormatDurationOptions): string;
|
|
34
54
|
/**
|
|
35
55
|
* An amount in the currency's minor unit (2900 PLN = PLN 29.00) in `locale`'s notation, thousands always
|
|
36
|
-
* grouped (
|
|
56
|
+
* grouped (even where a locale's default would leave four digits together), and zero is never signed (`-0`
|
|
37
57
|
* and an amount that rounds to zero show as zero). The minor unit is the pinned ISO
|
|
38
58
|
* 4217 one (`CURRENCY_MINOR_UNIT_DIGITS`); a code outside the table gets `Intl`'s digits. Throws a
|
|
39
59
|
* `RangeError` for an amount that is not a whole number of minor units, or a code `Intl` refuses.
|
|
40
60
|
*/
|
|
41
|
-
export declare function formatMoney(minor: number, currency: string, locale:
|
|
61
|
+
export declare function formatMoney(minor: number, currency: string, locale: string, options?: FormatMoneyOptions): string;
|
|
42
62
|
/**
|
|
43
|
-
* Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits
|
|
44
|
-
* and a value that rounds to zero show as `0%`). Throws a
|
|
63
|
+
* Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits unless `fractionDigits` fixes
|
|
64
|
+
* them, and zero is never signed (`-0` and a value that rounds to zero show as `0%`, or `0.0%` at one digit). Throws a
|
|
65
|
+
* `RangeError` for a non-finite value or an invalid `fractionDigits`.
|
|
45
66
|
*/
|
|
46
|
-
export declare function formatPercent(basisPoints: number, locale:
|
|
67
|
+
export declare function formatPercent(basisPoints: number, locale: string, options?: FormatPercentOptions): string;
|
|
47
68
|
//# sourceMappingURL=format.d.ts.map
|
package/dist/format.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAMA,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAE1E,+HAA+H;AAC/H,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,yKAAyK;IACzK,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,6CAA6C;AAC7C,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,OAAO,CAAC;AAElD,mHAAmH;AACnH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,iHAAiH;IACjH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,iGAAiG;IACjG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAMD,iGAAiG;AACjG,eAAO,MAAM,yBAAyB,EAAE,gBAIvC,CAAC;AAeF,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,gBAAyB,GAAG,MAAM,CAGvG;AAED,mGAAmG;AACnG,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAM/D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,kBAA2B,GAAG,MAAM,CAO7G;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAa1F;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,CAarH;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAc7G"}
|
package/dist/format.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
// Days, money and percentages as people read them, one way for every module and app. A calendar day
|
|
2
|
-
// is formatted at UTC midnight in UTC, so neither the server's nor the reader's zone moves it.
|
|
2
|
+
// is formatted at UTC midnight in UTC, so neither the server's nor the reader's zone moves it. Every
|
|
3
|
+
// formatter takes any BCP 47 locale tag (an invalid one throws Intl's RangeError); the only built-in
|
|
4
|
+
// copy is English, and an app passes its own words where a formatter writes any.
|
|
3
5
|
import { isCalendarDay } from "./calendar-day.js";
|
|
4
6
|
import { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
|
|
5
7
|
import { formatMessage, selectPlural } from "./i18n.js";
|
|
6
|
-
import { coreMessages } from "./messages/index.js";
|
|
7
8
|
const MONTHS_PER_YEAR = 12;
|
|
8
9
|
const MONTH_PATTERN = /^(\d{4})-(\d{2})$/;
|
|
9
10
|
const ROMAN_MONTHS = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
|
|
11
|
+
/** The built-in, English words of `formatDuration`; any other category falls back to `other`. */
|
|
12
|
+
export const DEFAULT_DURATION_MESSAGES = {
|
|
13
|
+
years: { one: "{count} year", other: "{count} years" },
|
|
14
|
+
months: { one: "{count} month", other: "{count} months" },
|
|
15
|
+
both: "{years} and {months}",
|
|
16
|
+
};
|
|
10
17
|
const BASIS_POINTS_PER_UNIT = 10_000;
|
|
11
18
|
const MAX_PERCENT_FRACTION_DIGITS = 2;
|
|
19
|
+
const FRACTION_DIGITS_LIMIT = 20;
|
|
12
20
|
const dayFormatters = new Map();
|
|
13
21
|
const numberFormatters = new Map();
|
|
14
22
|
const DAY_STYLE_OPTIONS = {
|
|
@@ -44,22 +52,19 @@ export function formatCalendarMonth(month, locale, style = "long") {
|
|
|
44
52
|
const year = firstDay.slice(0, 4);
|
|
45
53
|
if (style === "roman")
|
|
46
54
|
return `${ROMAN_MONTHS[date.getUTCMonth()] ?? ""} ${year}`;
|
|
47
|
-
|
|
48
|
-
return getDayFormatter(`${locale}:month`, locale, { month: "long", year: "numeric" }).format(date);
|
|
49
|
-
// The genitive is the form a full date uses, so it is read from a day-month-year date's month part.
|
|
50
|
-
const parts = getDayFormatter(`${locale}:day-month`, locale, { day: "numeric", month: "long", year: "numeric" }).formatToParts(date);
|
|
51
|
-
const name = parts.find((part) => part.type === "month")?.value ?? "";
|
|
52
|
-
return `${name} ${year}`;
|
|
55
|
+
return getDayFormatter(`${locale}:month`, locale, { month: "long", year: "numeric" }).format(date);
|
|
53
56
|
}
|
|
54
57
|
/**
|
|
55
|
-
* Whole months as years and months
|
|
56
|
-
*
|
|
58
|
+
* Whole months as years and months: 38 is "3 years and 2 months"; a part that is zero is left out, and zero itself is
|
|
59
|
+
* "0 months". English by default; an app passes its own `messages` and the `locale` whose plural rules pick among
|
|
60
|
+
* them. Throws a `RangeError` for a negative or fractional count, or an invalid locale tag.
|
|
57
61
|
*/
|
|
58
|
-
export function formatDuration(months,
|
|
62
|
+
export function formatDuration(months, options = {}) {
|
|
59
63
|
if (!Number.isSafeInteger(months) || months < 0) {
|
|
60
64
|
throw new RangeError(`formatDuration: ${String(months)} is not a whole, non-negative number of months`);
|
|
61
65
|
}
|
|
62
|
-
const
|
|
66
|
+
const locale = options.locale ?? "en";
|
|
67
|
+
const copy = options.messages ?? DEFAULT_DURATION_MESSAGES;
|
|
63
68
|
const years = Math.floor(months / MONTHS_PER_YEAR);
|
|
64
69
|
const rest = months % MONTHS_PER_YEAR;
|
|
65
70
|
const yearsText = formatMessage(selectPlural(locale, years, copy.years), { count: years });
|
|
@@ -72,7 +77,7 @@ export function formatDuration(months, locale) {
|
|
|
72
77
|
}
|
|
73
78
|
/**
|
|
74
79
|
* An amount in the currency's minor unit (2900 PLN = PLN 29.00) in `locale`'s notation, thousands always
|
|
75
|
-
* grouped (
|
|
80
|
+
* grouped (even where a locale's default would leave four digits together), and zero is never signed (`-0`
|
|
76
81
|
* and an amount that rounds to zero show as zero). The minor unit is the pinned ISO
|
|
77
82
|
* 4217 one (`CURRENCY_MINOR_UNIT_DIGITS`); a code outside the table gets `Intl`'s digits. Throws a
|
|
78
83
|
* `RangeError` for an amount that is not a whole number of minor units, or a code `Intl` refuses.
|
|
@@ -93,15 +98,16 @@ export function formatMoney(minor, currency, locale, options = {}) {
|
|
|
93
98
|
return format.format(minor / 10 ** digits);
|
|
94
99
|
}
|
|
95
100
|
/**
|
|
96
|
-
* Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits
|
|
97
|
-
* and a value that rounds to zero show as `0%`). Throws a
|
|
101
|
+
* Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits unless `fractionDigits` fixes
|
|
102
|
+
* them, and zero is never signed (`-0` and a value that rounds to zero show as `0%`, or `0.0%` at one digit). Throws a
|
|
103
|
+
* `RangeError` for a non-finite value or an invalid `fractionDigits`.
|
|
98
104
|
*/
|
|
99
105
|
export function formatPercent(basisPoints, locale, options = {}) {
|
|
100
106
|
if (!Number.isFinite(basisPoints))
|
|
101
107
|
throw new RangeError(`formatPercent: ${String(basisPoints)} is not a finite number of basis points`);
|
|
102
108
|
const format = getNumberFormatter(locale, {
|
|
103
109
|
style: "percent",
|
|
104
|
-
|
|
110
|
+
...getPercentFractionDigits(options.fractionDigits),
|
|
105
111
|
signDisplay: options.signed === true ? "exceptZero" : "negative",
|
|
106
112
|
});
|
|
107
113
|
const value = basisPoints / BASIS_POINTS_PER_UNIT;
|
|
@@ -113,6 +119,14 @@ export function formatPercent(basisPoints, locale, options = {}) {
|
|
|
113
119
|
.map((part) => part.value)
|
|
114
120
|
.join("");
|
|
115
121
|
}
|
|
122
|
+
function getPercentFractionDigits(fractionDigits) {
|
|
123
|
+
if (fractionDigits === undefined)
|
|
124
|
+
return { maximumFractionDigits: MAX_PERCENT_FRACTION_DIGITS };
|
|
125
|
+
if (!Number.isInteger(fractionDigits) || fractionDigits < 0 || fractionDigits > FRACTION_DIGITS_LIMIT) {
|
|
126
|
+
throw new RangeError(`formatPercent: fractionDigits ${String(fractionDigits)} is not an integer from 0 to ${String(FRACTION_DIGITS_LIMIT)}`);
|
|
127
|
+
}
|
|
128
|
+
return { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits };
|
|
129
|
+
}
|
|
116
130
|
function isNextToPercentSign(parts, index) {
|
|
117
131
|
return parts[index - 1]?.type === "percentSign" || parts[index + 1]?.type === "percentSign";
|
|
118
132
|
}
|