@reykjavik/webtools 0.1.29 → 0.1.31

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 CHANGED
@@ -4,21 +4,37 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
- ## 0.1.29
7
+ ## 0.1.31
8
8
 
9
- _2024-08-26_
9
+ _2024-09-23_
10
+
11
+ - `@reykjavik/webtools/async`:
12
+ - feat: `maxWait` returns full `PromiseSettledResult` objects
13
+ - fix: `maxWait` result objects should remain stable
14
+ - fix: `maxWait` should distinguish between unresolved and rejected promises
15
+
16
+ ## 0.1.30
17
+
18
+ _2024-09-15_
10
19
 
11
20
  - `@reykjavik/webtools/http`:
12
- - feat: Add `cacheControlHeaders` helper that returns a `HeadersInit` object
13
- - fix: `cacheControl` with `maxAge: 'unset'` didn't delete `X-Cache-Control`
14
- in dev mode
21
+ - feat: Add `toMs` duration helper
22
+ - `@reykjavik/webtools/fixIcelandicLocale`:
23
+ - feat: Patch `Intl.RelativeTimeFormat`
24
+ - feat: Patch all `supportedLocalesOf` methods to report "is\*" as supported
25
+ - fix: Incorrect `PluralRules` results for negative values
26
+ - fix: Use each `Intl.*` class' `supportedLocalesOf` method to map locales
27
+ - fix: Remove unnecessary `Intl.\*` method instance-bindings
15
28
 
16
- ## 0.1.28
29
+ ## 0.1.28 – 0.1.29
17
30
 
18
31
  _2024-08-26_
19
32
 
20
33
  - `@reykjavik/webtools/http`:
34
+ - feat: Add `cacheControlHeaders` helper that returns a `HeadersInit` object
21
35
  - feat: `cacheControl` now also accepts `Map<string, string>` for headers
36
+ - fix: `cacheControl` with `maxAge: 'unset'` didn't delete `X-Cache-Control`
37
+ in dev mode
22
38
 
23
39
  ## 0.1.27
24
40
 
package/README.md CHANGED
@@ -23,6 +23,7 @@ bun add @reykjavik/webtools
23
23
  - [`cacheControlHeaders` helper](#cachecontrolheaders-helper)
24
24
  - [Type `TTLConfig`](#type-ttlconfig)
25
25
  - [`toSec` TTL helper](#tosec-ttl-helper)
26
+ - [`toMs` duration helper](#toms-duration-helper)
26
27
  - [`@reykjavik/webtools/async`](#reykjavikwebtoolsasync)
27
28
  - [`promiseAllObject`](#promiseallobject)
28
29
  - [`maxWait`](#maxwait)
@@ -206,7 +207,7 @@ behavior.
206
207
  **Syntax:**
207
208
  `` toSec(ttl: number | `${number}${'s'|'m'|'h'|'d'|'w'}`): number ``
208
209
 
209
- Converts a `TTL` (max-age) value into seconds, and returns `0` for bad and/or
210
+ Converts a `TTL` (max-age) value into seconds. Returns `0` for bad and/or
210
211
  negative input values.
211
212
 
212
213
  ```js
@@ -217,6 +218,22 @@ const ttl: TTL = '2h';
217
218
  const ttlSec = toSec(ttl);
218
219
  ```
219
220
 
221
+ ### `toMs` duration helper
222
+
223
+ **Syntax:**
224
+ `` toSec(duration: number | `${number}${'s'|'m'|'h'|'d'|'w'}`): number ``
225
+
226
+ Converts a `TTL` (duration) value into milliseconds. Returns `0` for bad
227
+ and/or negative input values.
228
+
229
+ ```js
230
+ import type { toMs, TTL } from '@reykjavik/webtools/http';
231
+
232
+ const ttl: TTL = '2h';
233
+
234
+ const ttlSec = toMs(ttl);
235
+ ```
236
+
220
237
  ---
221
238
 
222
239
  ## `@reykjavik/webtools/async`
@@ -248,23 +265,21 @@ const { user, posts } = await promiseAllObject({
248
265
 
249
266
  **Syntax:** `maxWait(timeout: number, promises: Array<any>): Promise<void>`
250
267
  **Syntax:**
251
- `maxWait<T extends PlainObj>(timeout: number, promises: T): Promise<{ [K in keyof T]: { value: Awaited<T[K]> } | undefined }>`
268
+ `maxWait<T extends PlainObj>(timeout: number, promises: T): Promise<{ [K in keyof T]: PromiseSettledResult<T[K]> } | undefined }>`
252
269
 
253
- This somewhat esoteric helper resolves soon as all of the passed `promises`
254
- have resolved, or after `timeout` milliseconds — whichever comes first.
270
+ This somewhat esoteric helper resolves soon when all of the passed `promises`
271
+ have settled (resolved or rejected), OR after `timeout` milliseconds —
272
+ whichever comes first.
255
273
 
256
274
  If an object is passed, the resolved value will be an object with the same
257
- keys, with undefined values for any promises that didn't resolve in time, and
258
- the resolved values in a `value` container object.
259
-
260
- If any of the promises reject, their values become undefined in the returned
261
- object.
275
+ keys, and any settled values in a `PromiseSettledResult` object, and
276
+ `undefined` for any promises that didn't settle in time.
262
277
 
263
278
  ```ts
264
279
  import { maxWait } from '@reykjavik/webtools/async';
265
280
 
266
- const user = fetchUser();
267
- const posts = fetchPosts();
281
+ const user = fetchUser(); // Promise<User>
282
+ const posts = fetchPosts(); // Promise<Array<Post>>
268
283
 
269
284
  // Array of promises resolves to void
270
285
  await maxWait(500, [user, posts]);
@@ -274,6 +289,8 @@ const { user, posts } = await maxWait(500, { user, posts });
274
289
 
275
290
  console.log(user?.value); // undefined | User
276
291
  console.log(posts?.value); // undefined | Array<Post>
292
+ console.log(posts?.status); // 'fulfilled' | 'rejected'
293
+ console.log(posts?.reason); // undefined | unknown
277
294
  ```
278
295
 
279
296
  ---
@@ -288,15 +305,16 @@ This module patches the following methods/classes by substituting the `is`
288
305
  locale with `da` (Danish) and apply a few post-hoc fixes to their return
289
306
  values.
290
307
 
291
- - `Intl.Collator` and `String.prototype.localeCompare`
292
- - `Intl.NumberFormat` and `Number.prototype.toLocaleString`
308
+ - `Intl.Collator` and `String.prototype.localeCompare` (\*)
309
+ - `Intl.NumberFormat` and `Number.prototype.toLocaleString` (\*)
293
310
  - `Intl.DateTimeFormat` and `Date.prototype.toLocaleString`,
294
- `.toLocaleDateString`, and `.toLocaleTimeString`
311
+ `.toLocaleDateString`, and `.toLocaleTimeString` (\*)
312
+ - `Intl.RelativeDateFormat`
295
313
  - `Intl.PluralRules`
296
314
  - `Intl.ListFormat`
297
315
 
298
- This provides usable (but not perfect) results, with a few caveats that are
299
- listed below.
316
+ (\*) The results are quite usable, but not entirely perfect. The
317
+ limitations/caveats are listed below.
300
318
 
301
319
  To apply the patch, simply "side-effect import" this module at the top of your
302
320
  app's entry point:
@@ -315,11 +333,10 @@ detection test.)
315
333
 
316
334
  **`Intl.Collator` and `localeCompare`:**
317
335
 
318
- - It incorrectly treats `ð` and `d` as the same letter (most of the time), and
319
- the acute-accented characters `á`, `é`, `í`, `ó`, `ú` and `ý` get lumped in
320
- with their non-accented counterparts (unless the compared).
321
- We fix this only for the first letter in the string, but not for the rest of
322
- it.
336
+ - It sorts initial letters correctly but in the rest of the string, it
337
+ incorrectly treats `ð` and `d` as the same letter (most of the time), and
338
+ lumps the acute-accented characters `á`, `é`, `í`, `ó`, `ú` and `ý` in with
339
+ their non-accented counterparts.
323
340
 
324
341
  **`Intl.NumberFormat` and `toLocaleString`:**
325
342
 
@@ -338,6 +355,8 @@ detection test.)
338
355
  - The `dayPeriod` option has a couple of slight mismatches, at 5 am and 12
339
356
  noon.
340
357
 
358
+ We eagerly accept bugfixes, additions, etc. to this module!
359
+
341
360
  ---
342
361
 
343
362
  ## `@reykjavik/webtools/SiteImprove`
@@ -573,19 +592,19 @@ export const myClass = vanillaClass(`
573
592
  // more complex styles.
574
593
  export const myOtherClass = vanillaClass(
575
594
  (className) => `
576
- .${className} {
577
- background-color: #ccc;
578
- padding: .5em 1em;
579
- }
580
- .${className} > strong {
581
- color: #c00;
582
- }
583
- @media (min-width: 800px) {
584
- .${className} {
585
- background-color: #eee;
595
+ .${className} {
596
+ background-color: #ccc;
597
+ padding: .5em 1em;
586
598
  }
587
- }
588
- `
599
+ .${className} > strong {
600
+ color: #c00;
601
+ }
602
+ @media (min-width: 800px) {
603
+ .${className} {
604
+ background-color: #eee;
605
+ }
606
+ }
607
+ `
589
608
  );
590
609
 
591
610
  export const humanReadableClass = vanillaClass(
package/async.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EitherObj } from '@reykjavik/hanna-utils';
1
2
  type PlainObj = Record<string, unknown>;
2
3
  /**
3
4
  * Simple sleep function. Returns a promise that resolves after `length`
@@ -5,16 +6,19 @@ type PlainObj = Record<string, unknown>;
5
6
  */
6
7
  export declare const sleep: (length: number) => Promise<void>;
7
8
  /**
8
- * Resolves soon as all of the passed `promises` have resolved, or after
9
- * `timeout` milliseconds whichever comes first.
9
+ * Returns a function that adds lag/delay to a promise chain,
10
+ * passing the promise payload through.
11
+ */
12
+ export declare const addLag: (length: number) => <T>(res: T) => Promise<T>;
13
+ /**
14
+ * Resolves as soon as all of the passed `promises` have resolved/settled,
15
+ * or after `timeout` milliseconds — whichever comes first.
10
16
  *
11
17
  * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#maxwait
12
18
  */
13
19
  export declare function maxWait(timeout: number, promises: Array<unknown>): Promise<void>;
14
20
  export declare function maxWait<PromiseMap extends PlainObj>(timeout: number, promises: PromiseMap): Promise<{
15
- -readonly [K in keyof PromiseMap]: {
16
- value: Awaited<PromiseMap[K]>;
17
- } | undefined;
21
+ -readonly [K in keyof PromiseMap]: EitherObj<PromiseFulfilledResult<Awaited<PromiseMap[K]>>, PromiseRejectedResult> | undefined;
18
22
  }>;
19
23
  /**
20
24
  * A variation of `Promise.all()` that accepts an object with named promises
package/async.js CHANGED
@@ -1,12 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.promiseAllObject = exports.maxWait = exports.sleep = void 0;
3
+ exports.promiseAllObject = exports.maxWait = exports.addLag = exports.sleep = void 0;
4
4
  /**
5
5
  * Simple sleep function. Returns a promise that resolves after `length`
6
6
  * milliseconds.
7
7
  */
8
8
  const sleep = (length) => new Promise((resolve) => setTimeout(resolve, length));
9
9
  exports.sleep = sleep;
10
+ /**
11
+ * Returns a function that adds lag/delay to a promise chain,
12
+ * passing the promise payload through.
13
+ */
14
+ const addLag = (length) => (res) => (0, exports.sleep)(length).then(() => res);
15
+ exports.addLag = addLag;
10
16
  function maxWait(timeout, promises) {
11
17
  if (Array.isArray(promises)) {
12
18
  return Promise.race([
@@ -19,17 +25,17 @@ function maxWait(timeout, promises) {
19
25
  Object.entries(promises).forEach(([key, value]) => {
20
26
  if (value instanceof Promise) {
21
27
  retObj[key] = undefined;
22
- value
23
- .then((value) => {
24
- retObj[key] = { value };
25
- })
26
- .catch(() => undefined);
28
+ value.then((value) => {
29
+ retObj[key] = { status: 'fulfilled', value };
30
+ }, (reason) => {
31
+ retObj[key] = { status: 'rejected', reason };
32
+ });
27
33
  }
28
34
  else {
29
- retObj[key] = { value };
35
+ retObj[key] = { status: 'fulfilled', value };
30
36
  }
31
37
  });
32
- return (0, exports.sleep)(0).then(() => retObj);
38
+ return Promise.resolve().then(() => ({ ...retObj }));
33
39
  });
34
40
  }
35
41
  exports.maxWait = maxWait;
package/esm/async.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EitherObj } from '@reykjavik/hanna-utils';
1
2
  type PlainObj = Record<string, unknown>;
2
3
  /**
3
4
  * Simple sleep function. Returns a promise that resolves after `length`
@@ -5,16 +6,19 @@ type PlainObj = Record<string, unknown>;
5
6
  */
6
7
  export declare const sleep: (length: number) => Promise<void>;
7
8
  /**
8
- * Resolves soon as all of the passed `promises` have resolved, or after
9
- * `timeout` milliseconds whichever comes first.
9
+ * Returns a function that adds lag/delay to a promise chain,
10
+ * passing the promise payload through.
11
+ */
12
+ export declare const addLag: (length: number) => <T>(res: T) => Promise<T>;
13
+ /**
14
+ * Resolves as soon as all of the passed `promises` have resolved/settled,
15
+ * or after `timeout` milliseconds — whichever comes first.
10
16
  *
11
17
  * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#maxwait
12
18
  */
13
19
  export declare function maxWait(timeout: number, promises: Array<unknown>): Promise<void>;
14
20
  export declare function maxWait<PromiseMap extends PlainObj>(timeout: number, promises: PromiseMap): Promise<{
15
- -readonly [K in keyof PromiseMap]: {
16
- value: Awaited<PromiseMap[K]>;
17
- } | undefined;
21
+ -readonly [K in keyof PromiseMap]: EitherObj<PromiseFulfilledResult<Awaited<PromiseMap[K]>>, PromiseRejectedResult> | undefined;
18
22
  }>;
19
23
  /**
20
24
  * A variation of `Promise.all()` that accepts an object with named promises
package/esm/async.js CHANGED
@@ -3,6 +3,11 @@
3
3
  * milliseconds.
4
4
  */
5
5
  export const sleep = (length) => new Promise((resolve) => setTimeout(resolve, length));
6
+ /**
7
+ * Returns a function that adds lag/delay to a promise chain,
8
+ * passing the promise payload through.
9
+ */
10
+ export const addLag = (length) => (res) => sleep(length).then(() => res);
6
11
  export function maxWait(timeout, promises) {
7
12
  if (Array.isArray(promises)) {
8
13
  return Promise.race([
@@ -15,17 +20,17 @@ export function maxWait(timeout, promises) {
15
20
  Object.entries(promises).forEach(([key, value]) => {
16
21
  if (value instanceof Promise) {
17
22
  retObj[key] = undefined;
18
- value
19
- .then((value) => {
20
- retObj[key] = { value };
21
- })
22
- .catch(() => undefined);
23
+ value.then((value) => {
24
+ retObj[key] = { status: 'fulfilled', value };
25
+ }, (reason) => {
26
+ retObj[key] = { status: 'rejected', reason };
27
+ });
23
28
  }
24
29
  else {
25
- retObj[key] = { value };
30
+ retObj[key] = { status: 'fulfilled', value };
26
31
  }
27
32
  });
28
- return sleep(0).then(() => retObj);
33
+ return Promise.resolve().then(() => ({ ...retObj }));
29
34
  });
30
35
  }
31
36
  // ---------------------------------------------------------------------------
@@ -1,10 +1,10 @@
1
- import { _PatchedCollator, _PatchedDateTimeFormat, _patchedDateToLocaleDateString, _patchedDateToLocaleString, _patchedDateToLocaleTimeString, _PatchedListFormat, _PatchedNumberFormat, _patchedNumberToLocaleString, _PatchedPluralRules, _patchedStringLocaleCompare, } from './fixIcelandicLocale.privates.js';
1
+ import { _PatchedCollator, _PatchedDateTimeFormat, _patchedDateToLocaleDateString, _patchedDateToLocaleString, _patchedDateToLocaleTimeString, _PatchedListFormat, _PatchedNumberFormat, _patchedNumberToLocaleString, _PatchedPluralRules, _PatchedRelativeTimeFormat, _patchedStringLocaleCompare, } from './fixIcelandicLocale.privates.js';
2
2
  /*
3
3
  Mantra: Partial Icelandic suppoort is better than none. Partial Icelandic
4
4
  suppoort is better than none. Partial Icelandic suppoort is better than
5
5
  none. Partial Icelandic suppoort is better than none. Partial Icelandic...
6
6
  */
7
- if (Intl.Collator.supportedLocalesOf(['is']).length < 1) {
7
+ if (!Intl.Collator.supportedLocalesOf(['is']).length) {
8
8
  Intl.Collator = _PatchedCollator;
9
9
  String.prototype.localeCompare = _patchedStringLocaleCompare;
10
10
  Intl.NumberFormat = _PatchedNumberFormat;
@@ -15,10 +15,15 @@ if (Intl.Collator.supportedLocalesOf(['is']).length < 1) {
15
15
  Date.prototype.toLocaleTimeString = _patchedDateToLocaleTimeString;
16
16
  }
17
17
  /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-unnecessary-condition */
18
- if (Intl.ListFormat && Intl.ListFormat.supportedLocalesOf(['is']).length < 1) {
18
+ if (Intl.ListFormat && !Intl.ListFormat.supportedLocalesOf(['is']).length) {
19
19
  Intl.ListFormat = _PatchedListFormat;
20
20
  }
21
- if (Intl.PluralRules && Intl.PluralRules.supportedLocalesOf(['is']).length < 1) {
21
+ if (Intl.PluralRules && !Intl.PluralRules.supportedLocalesOf(['is']).length) {
22
22
  Intl.PluralRules = _PatchedPluralRules;
23
23
  }
24
+ if (Intl.RelativeTimeFormat &&
25
+ !Intl.RelativeTimeFormat.supportedLocalesOf(['is']).length) {
26
+ Intl.RelativeTimeFormat =
27
+ _PatchedRelativeTimeFormat;
28
+ }
24
29
  /* eslint-enable @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-unnecessary-condition */
@@ -75,3 +75,9 @@ export declare const _PatchedListFormat: {
75
75
  } & {
76
76
  $original: typeof Intl.ListFormat;
77
77
  };
78
+ export declare const _PatchedRelativeTimeFormat: {
79
+ new (locales?: string | string[] | undefined, options?: Intl.RelativeTimeFormatOptions | undefined): Intl.RelativeTimeFormat;
80
+ supportedLocalesOf(locales?: string | string[] | undefined, options?: Intl.RelativeTimeFormatOptions | undefined): string[];
81
+ } & {
82
+ $original: typeof Intl.RelativeTimeFormat;
83
+ };
@@ -1,10 +1,10 @@
1
- var _a, _b;
2
- const _Collator = Intl.Collator;
3
- const mapLocales = (locales) => {
1
+ var _a, _b, _c;
2
+ const islLocaleRe = /^isl?(?:-|$)/i;
3
+ const mapLocales = (constr, locales) => {
4
4
  locales = typeof locales === 'string' ? [locales] : locales || [];
5
5
  for (let i = 0, loc; (loc = locales[i]); i++) {
6
- const isIsLocale = /^isl?(?:-|$)/i.test(loc);
7
- if (isIsLocale) {
6
+ const isIslLocale = islLocaleRe.test(loc);
7
+ if (isIslLocale) {
8
8
  // Danish feels like a "good enough" substitution for Icelandic.
9
9
  // For alphabetization, it seems to just the internal order of `Ø` and `Ö`
10
10
  // that's different, and when the `sensitivity` option is set to "base"
@@ -15,22 +15,49 @@ const mapLocales = (locales) => {
15
15
  // as fully equal to the base letter.
16
16
  return ['da'];
17
17
  }
18
- if (_Collator.supportedLocalesOf(loc).length) {
18
+ if (constr.supportedLocalesOf(loc).length) {
19
19
  return; // no mapping needed. YOLO!
20
20
  }
21
21
  }
22
22
  };
23
+ const patchSupportedLocalesOf = (constr) => {
24
+ const BASE_CHAR_CODE = 64; // 'A'.charCodeAt(0) - 1; // used for generating unique suffix for fake locales
25
+ const sLO = (locales, options) => {
26
+ let localesArr = typeof locales === 'string' ? [locales] : locales;
27
+ const memoIsl = [];
28
+ localesArr = localesArr.map((locale) => {
29
+ if (islLocaleRe.test(locale)) {
30
+ // Some engines throw a RangeError if the locale is weirdly shaped,
31
+ // so we must use a short, safe, unique fake locale instead,
32
+ // and store the actual locale in `memoIsl` for later reinsertion.
33
+ memoIsl.push(locale);
34
+ return `da-X${String.fromCharCode(BASE_CHAR_CODE + memoIsl.length)}`;
35
+ }
36
+ return locale;
37
+ });
38
+ const supportedLocales = constr.supportedLocalesOf(localesArr, options);
39
+ if (!memoIsl.length) {
40
+ return supportedLocales;
41
+ }
42
+ return supportedLocales.map((locale) => locale.startsWith('da-X') ? memoIsl.shift() : locale);
43
+ };
44
+ return sLO;
45
+ };
23
46
  const combineParts = (parts) => parts.map(({ value }) => value).join('');
47
+ // ===========================================================================
48
+ // Collator
49
+ // ===========================================================================
50
+ const _Collator = Intl.Collator;
24
51
  const PatchedCollator = function Collator(locales, options) {
25
52
  if (!(this instanceof PatchedCollator)) {
26
53
  // @ts-expect-error (YOLO! Can't be arsed)
27
54
  return new PatchedCollator(locales, options);
28
55
  }
29
- const mappedLocales = mapLocales(locales);
30
- const parent = _Collator(mappedLocales || locales, options);
56
+ const mappedLocales = mapLocales(_Collator, locales);
57
+ const super_ = _Collator(mappedLocales || locales, options);
31
58
  const mapped = !!mappedLocales;
32
59
  this.compare = (a, b) => {
33
- const res1 = parent.compare(a, b);
60
+ const res1 = super_.compare(a, b);
34
61
  if (!mapped) {
35
62
  return res1;
36
63
  }
@@ -39,14 +66,13 @@ const PatchedCollator = function Collator(locales, options) {
39
66
  if (/\d/.test(a0 + b0)) {
40
67
  return res1;
41
68
  }
42
- const res2 = parent.compare(a0, b0);
69
+ const res2 = super_.compare(a0, b0);
43
70
  return res2 !== 0 ? res2 : res1;
44
71
  };
45
- this.resolvedOptions = () => parent.resolvedOptions();
72
+ this.resolvedOptions = () => super_.resolvedOptions();
46
73
  };
47
74
  PatchedCollator.prototype = { constructor: PatchedCollator };
48
- // Static methods (not patched since "is" is not ACTUALLY supported.)
49
- PatchedCollator.supportedLocalesOf = _Collator.supportedLocalesOf;
75
+ PatchedCollator.supportedLocalesOf = /*#__PURE__*/ patchSupportedLocalesOf(_Collator);
50
76
  PatchedCollator.$original = _Collator;
51
77
  export const _PatchedCollator = PatchedCollator;
52
78
  // ---------------------------------------------------------------------------
@@ -78,24 +104,26 @@ const PatchedNumberFormat = function NumberFormat(locales, options) {
78
104
  // @ts-expect-error (YOLO! Can't be arsed)
79
105
  return new PatchedNumberFormat(locales, options);
80
106
  }
81
- const mappedLocales = mapLocales(locales);
82
- const parent = _NumberFormat(mappedLocales || locales, options);
107
+ const mappedLocales = mapLocales(_NumberFormat, locales);
108
+ const super_ = _NumberFormat(mappedLocales || locales, options);
83
109
  const mapped = !!mappedLocales;
84
- this.format = (value) => combineParts(this.formatToParts(value));
85
- this.formatRange = (value1, value2) => combineParts(this.formatRangeToParts(value1, value2));
110
+ this.format = (value) => mapped ? combineParts(this.formatToParts(value)) : super_.format(value);
111
+ this.formatRange = (value1, value2) => mapped
112
+ ? combineParts(this.formatRangeToParts(value1, value2))
113
+ : super_.formatRange(value1, value2);
86
114
  this.formatToParts = (value) => {
87
- const parts = parent.formatToParts(value);
88
- return mapped ? reformatNumberParts(parent, parts) : parts;
115
+ const parts = super_.formatToParts(value);
116
+ return mapped ? reformatNumberParts(super_, parts) : parts;
89
117
  };
90
118
  this.formatRangeToParts = (value1, value2) => {
91
- const parts = parent.formatRangeToParts(value1, value2);
92
- return mapped ? reformatNumberParts(parent, parts) : parts;
119
+ const parts = super_.formatRangeToParts(value1, value2);
120
+ return mapped ? reformatNumberParts(super_, parts) : parts;
93
121
  };
94
- this.resolvedOptions = () => parent.resolvedOptions();
122
+ this.resolvedOptions = () => super_.resolvedOptions();
95
123
  };
96
124
  PatchedNumberFormat.prototype = { constructor: PatchedNumberFormat };
97
- // Static methods (not patched since "is" is not ACTUALLY supported.)
98
- PatchedNumberFormat.supportedLocalesOf = _NumberFormat.supportedLocalesOf;
125
+ PatchedNumberFormat.supportedLocalesOf =
126
+ /*#__PURE__*/ patchSupportedLocalesOf(_NumberFormat);
99
127
  PatchedNumberFormat.$original = _NumberFormat;
100
128
  export const _PatchedNumberFormat = PatchedNumberFormat;
101
129
  // ---------------------------------------------------------------------------
@@ -197,7 +225,7 @@ const PatchedDateTimeFormat = function DateTimeFormat(locales, options) {
197
225
  // @ts-expect-error (YOLO! Can't be arsed)
198
226
  return new PatchedDateTimeFormat(locales, options);
199
227
  }
200
- const mappedLocales = mapLocales(locales);
228
+ const mappedLocales = mapLocales(_DateTimeFormat, locales);
201
229
  if (options === null || options === void 0 ? void 0 : options.hour12) {
202
230
  options = {
203
231
  ...options,
@@ -205,23 +233,25 @@ const PatchedDateTimeFormat = function DateTimeFormat(locales, options) {
205
233
  hourCycle: 'h11',
206
234
  };
207
235
  }
208
- const parent = _DateTimeFormat(mappedLocales || locales, options);
236
+ const super_ = _DateTimeFormat(mappedLocales || locales, options);
209
237
  const mapped = !!mappedLocales;
210
- this.format = (value) => combineParts(this.formatToParts(value));
211
- this.formatRange = (value1, value2) => combineParts(this.formatRangeToParts(value1, value2));
238
+ this.format = (value) => mapped ? combineParts(this.formatToParts(value)) : super_.format(value);
239
+ this.formatRange = (value1, value2) => mapped
240
+ ? combineParts(this.formatRangeToParts(value1, value2))
241
+ : super_.formatRange(value1, value2);
212
242
  this.formatToParts = (value) => {
213
- const parts = parent.formatToParts(value);
214
- return mapped ? reformatDateTimeParts(parent, parts) : parts;
243
+ const parts = super_.formatToParts(value);
244
+ return mapped ? reformatDateTimeParts(super_, parts) : parts;
215
245
  };
216
246
  this.formatRangeToParts = (value1, value2) => {
217
- const parts = parent.formatRangeToParts(value1, value2);
218
- return mapped ? reformatDateTimeParts(parent, parts) : parts;
247
+ const parts = super_.formatRangeToParts(value1, value2);
248
+ return mapped ? reformatDateTimeParts(super_, parts) : parts;
219
249
  };
220
- this.resolvedOptions = () => parent.resolvedOptions();
250
+ this.resolvedOptions = () => super_.resolvedOptions();
221
251
  };
222
252
  PatchedDateTimeFormat.prototype = { constructor: PatchedDateTimeFormat };
223
- // Static methods (not patched since "is" is not ACTUALLY supported.)
224
- PatchedDateTimeFormat.supportedLocalesOf = _DateTimeFormat.supportedLocalesOf;
253
+ PatchedDateTimeFormat.supportedLocalesOf =
254
+ /*#__PURE__*/ patchSupportedLocalesOf(_DateTimeFormat);
225
255
  PatchedDateTimeFormat.$original = _DateTimeFormat;
226
256
  export const _PatchedDateTimeFormat = PatchedDateTimeFormat;
227
257
  // ---------------------------------------------------------------------------
@@ -299,15 +329,14 @@ let PatchedPluralRules;
299
329
  if (_PluralRules) {
300
330
  PatchedPluralRules = (_a = class PluralRules extends _PluralRules {
301
331
  pluralIsl(n) {
332
+ n = n < 0 ? -n : n;
302
333
  return this.ord ? 'other' : n % 10 !== 1 || n % 100 === 11 ? 'other' : 'one';
303
334
  }
304
335
  constructor(locales, options) {
305
- const mappedLocales = mapLocales(locales);
336
+ const mappedLocales = mapLocales(_PluralRules, locales);
306
337
  super(mappedLocales || locales, options);
307
338
  this.mapped = !!mappedLocales;
308
339
  this.ord = (options === null || options === void 0 ? void 0 : options.type) === 'ordinal';
309
- this.select = this.select.bind(this);
310
- this.selectRange = this.selectRange.bind(this);
311
340
  }
312
341
  select(n) {
313
342
  if (this.mapped) {
@@ -325,6 +354,7 @@ if (_PluralRules) {
325
354
  return super.selectRange(n, n2);
326
355
  }
327
356
  },
357
+ _a.supportedLocalesOf = patchSupportedLocalesOf(_PluralRules),
328
358
  _a.$original = _PluralRules,
329
359
  _a);
330
360
  }
@@ -338,11 +368,9 @@ let PatchedListFormat;
338
368
  if (_ListFormat) {
339
369
  PatchedListFormat = (_b = class ListFormat extends _ListFormat {
340
370
  constructor(locales, options) {
341
- const mappedLocales = mapLocales(locales);
371
+ const mappedLocales = mapLocales(_ListFormat, locales);
342
372
  super(mappedLocales || locales, options);
343
373
  this.mapped = !!mappedLocales;
344
- this.format = this.format.bind(this);
345
- this.formatToParts = this.formatToParts.bind(this);
346
374
  }
347
375
  format(list) {
348
376
  return this.mapped ? combineParts(this.formatToParts(list)) : super.format(list);
@@ -351,8 +379,8 @@ if (_ListFormat) {
351
379
  const parts = super.formatToParts(list);
352
380
  if (this.mapped) {
353
381
  for (const item of parts) {
354
- const { value } = item;
355
- if (item.type === 'literal' && (value === ' el. ' || value === ' eller ')) {
382
+ const { type, value } = item;
383
+ if (type === 'literal' && (value === ' el. ' || value === ' eller ')) {
356
384
  item.value = ' eða ';
357
385
  }
358
386
  }
@@ -360,7 +388,112 @@ if (_ListFormat) {
360
388
  return parts;
361
389
  }
362
390
  },
391
+ _b.supportedLocalesOf = patchSupportedLocalesOf(_ListFormat),
363
392
  _b.$original = _ListFormat,
364
393
  _b);
365
394
  }
366
395
  export const _PatchedListFormat = PatchedListFormat;
396
+ // ===========================================================================
397
+ // RelativeTimeFormat
398
+ // ===========================================================================
399
+ const _RelativeTimeFormat = Intl.RelativeTimeFormat;
400
+ let PatchedRelativeTimeFormat;
401
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
402
+ if (_RelativeTimeFormat) {
403
+ let pluralIsl;
404
+ let numFormatIsl;
405
+ const islUnits = {
406
+ year: [['ári', 'árum', 'ár', 'ár'], ''],
407
+ quarter: [['ársfjórðungi', 'ársfjórðungum', 'ársfjórðung', 'ársfjórðunga'], 'ársfj.'],
408
+ month: [['mánuði', 'mánuðum', 'mánuð', 'mánuði'], 'mán.'],
409
+ week: [['viku', 'vikum', 'viku', 'vikur'], ''],
410
+ day: [['degi', 'dögum', 'dag', 'daga'], ''],
411
+ hour: [['klukkustund', 'klukkustundum', 'klukkustund', 'klukkustundir'], 'klst.'],
412
+ minute: [['mínútu', 'mínútum', 'mínútu', 'mínútur'], 'mín.'],
413
+ second: [['sekúndu', 'sekúndum', 'sekúndu', 'sekúndur'], 'sek.'],
414
+ };
415
+ const phrases = {
416
+ nu: 'núna',
417
+ 'næste år': 'á næsta ári',
418
+ 'sidste år': 'á síðasta ári',
419
+ 'i år': 'á þessu ári',
420
+ 'sidste kvartal': 'síðasti ársfjórðungur',
421
+ 'dette kvartal': 'þessi ársfjórðungur',
422
+ 'næste kvartal': 'næsti ársfjórðungur',
423
+ 'sidste kvt.': 'síðasti ársfj.',
424
+ 'dette kvt.': 'þessi ársfj.',
425
+ 'næste kvt.': 'næsti ársfj.',
426
+ 'sidste måned': 'í síðasta mánuði',
427
+ 'denne måned': 'í þessum mánuði',
428
+ 'næste måned': 'í næsta mánuði',
429
+ 'sidste md.': 'í síðasta mán.',
430
+ 'denne md.': 'í þessum mán.',
431
+ 'næste md.': 'í næsta mán.',
432
+ 'sidste uge': 'í síðustu viku',
433
+ 'denne uge': 'í þessari viku',
434
+ 'næste uge': 'í næstu viku',
435
+ 'i forgårs': 'í fyrradag',
436
+ 'i går': 'í gær',
437
+ 'i dag': 'í dag',
438
+ 'i morgen': 'á morgun',
439
+ 'i overmorgen': 'eftir tvo daga',
440
+ 'denne time': 'þessa stundina',
441
+ 'dette minut': 'á þessari mínútu',
442
+ };
443
+ PatchedRelativeTimeFormat = (_c = class RelativeTimeFormat extends _RelativeTimeFormat {
444
+ constructor(locales, options) {
445
+ const mappedLocales = mapLocales(_RelativeTimeFormat, locales);
446
+ super(mappedLocales || locales, options);
447
+ this.mapped = !!mappedLocales;
448
+ }
449
+ format(value, unit) {
450
+ return this.mapped
451
+ ? combineParts(this.formatToParts(value, unit))
452
+ : super.format(value, unit);
453
+ }
454
+ // eslint-disable-next-line complexity
455
+ formatToParts(value, unit) {
456
+ const parts = super.formatToParts(value, unit);
457
+ if (!this.mapped) {
458
+ return parts;
459
+ }
460
+ if (!pluralIsl) {
461
+ pluralIsl = new _PatchedPluralRules('is');
462
+ }
463
+ if (!numFormatIsl) {
464
+ numFormatIsl = new _PatchedNumberFormat('is');
465
+ }
466
+ const options = this.resolvedOptions();
467
+ const unitSngl = unit.replace(/s$/, '');
468
+ if (parts.length === 1) {
469
+ const firstPart = parts[0];
470
+ firstPart.value = phrases[firstPart.value] || firstPart.value;
471
+ return parts;
472
+ }
473
+ const [long, short] = islUnits[unitSngl];
474
+ const idx = (value < 0 ? 0 : 2) + (pluralIsl.select(value) === 'one' ? 0 : 1);
475
+ const prefixStr = options.style === 'narrow' &&
476
+ (unitSngl === 'second' || unitSngl === 'minute' || unitSngl === 'hour')
477
+ ? value < 0
478
+ ? '-'
479
+ : '+'
480
+ : value < 0
481
+ ? 'fyrir '
482
+ : 'eftir ';
483
+ const valueStr = (options.style !== 'long' && short) || long[idx];
484
+ const islParts = [
485
+ { type: 'literal', value: prefixStr },
486
+ ...numFormatIsl.formatToParts(Math.abs(value)).map((part) => {
487
+ part.unit =
488
+ unitSngl;
489
+ return part;
490
+ }),
491
+ { type: 'literal', value: ` ${valueStr}` },
492
+ ];
493
+ return islParts;
494
+ }
495
+ },
496
+ _c.$original = _RelativeTimeFormat,
497
+ _c);
498
+ }
499
+ export const _PatchedRelativeTimeFormat = PatchedRelativeTimeFormat;
package/esm/http.d.ts CHANGED
@@ -158,12 +158,19 @@ type TTLObj = {
158
158
  */
159
159
  export type TTLConfig = TTL | TTLKeywords | TTLObj;
160
160
  /**
161
- * Converts a `TTL` (max-age) value into seconds, and returns `0` for bad
162
- * and/or negative input values.
161
+ * Converts a `TTL` (max-age) value into seconds. Returns `0` for bad and/or
162
+ * negative input values.
163
163
  *
164
164
  * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#tosec-ttl-helper
165
165
  */
166
166
  export declare const toSec: (ttl: TTL) => number;
167
+ /**
168
+ * Converts a `TTL` (duration) value into milliseconds. Returns `0` for bad
169
+ * and/or negative input values.
170
+ *
171
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#toms-duration-helper
172
+ */
173
+ export declare const toMs: (ttl: TTL) => number;
167
174
  type ServerResponseStub = Pick<ServerResponse, 'setHeader' | 'getHeader' | 'removeHeader'> & {
168
175
  headers?: Record<string, string | Array<string>>;
169
176
  };
package/esm/http.js CHANGED
@@ -133,8 +133,8 @@ const unitToSeconds = {
133
133
  w: 7 * 24 * 3600,
134
134
  };
135
135
  /**
136
- * Converts a `TTL` (max-age) value into seconds, and returns `0` for bad
137
- * and/or negative input values.
136
+ * Converts a `TTL` (max-age) value into seconds. Returns `0` for bad and/or
137
+ * negative input values.
138
138
  *
139
139
  * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#tosec-ttl-helper
140
140
  */
@@ -146,6 +146,13 @@ export const toSec = (ttl) => {
146
146
  }
147
147
  return Math.max(0, Math.round(ttl)) || 0;
148
148
  };
149
+ /**
150
+ * Converts a `TTL` (duration) value into milliseconds. Returns `0` for bad
151
+ * and/or negative input values.
152
+ *
153
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#toms-duration-helper
154
+ */
155
+ export const toMs = (ttl) => toSec(ttl) * 1000;
149
156
  const toRespnseStubHeaders = (response) => {
150
157
  if (response instanceof Map) {
151
158
  return response;
@@ -6,7 +6,7 @@ const fixIcelandicLocale_privates_js_1 = require("./fixIcelandicLocale.privates.
6
6
  suppoort is better than none. Partial Icelandic suppoort is better than
7
7
  none. Partial Icelandic suppoort is better than none. Partial Icelandic...
8
8
  */
9
- if (Intl.Collator.supportedLocalesOf(['is']).length < 1) {
9
+ if (!Intl.Collator.supportedLocalesOf(['is']).length) {
10
10
  Intl.Collator = fixIcelandicLocale_privates_js_1._PatchedCollator;
11
11
  String.prototype.localeCompare = fixIcelandicLocale_privates_js_1._patchedStringLocaleCompare;
12
12
  Intl.NumberFormat = fixIcelandicLocale_privates_js_1._PatchedNumberFormat;
@@ -17,10 +17,15 @@ if (Intl.Collator.supportedLocalesOf(['is']).length < 1) {
17
17
  Date.prototype.toLocaleTimeString = fixIcelandicLocale_privates_js_1._patchedDateToLocaleTimeString;
18
18
  }
19
19
  /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-unnecessary-condition */
20
- if (Intl.ListFormat && Intl.ListFormat.supportedLocalesOf(['is']).length < 1) {
20
+ if (Intl.ListFormat && !Intl.ListFormat.supportedLocalesOf(['is']).length) {
21
21
  Intl.ListFormat = fixIcelandicLocale_privates_js_1._PatchedListFormat;
22
22
  }
23
- if (Intl.PluralRules && Intl.PluralRules.supportedLocalesOf(['is']).length < 1) {
23
+ if (Intl.PluralRules && !Intl.PluralRules.supportedLocalesOf(['is']).length) {
24
24
  Intl.PluralRules = fixIcelandicLocale_privates_js_1._PatchedPluralRules;
25
25
  }
26
+ if (Intl.RelativeTimeFormat &&
27
+ !Intl.RelativeTimeFormat.supportedLocalesOf(['is']).length) {
28
+ Intl.RelativeTimeFormat =
29
+ fixIcelandicLocale_privates_js_1._PatchedRelativeTimeFormat;
30
+ }
26
31
  /* eslint-enable @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-unnecessary-condition */
@@ -75,3 +75,9 @@ export declare const _PatchedListFormat: {
75
75
  } & {
76
76
  $original: typeof Intl.ListFormat;
77
77
  };
78
+ export declare const _PatchedRelativeTimeFormat: {
79
+ new (locales?: string | string[] | undefined, options?: Intl.RelativeTimeFormatOptions | undefined): Intl.RelativeTimeFormat;
80
+ supportedLocalesOf(locales?: string | string[] | undefined, options?: Intl.RelativeTimeFormatOptions | undefined): string[];
81
+ } & {
82
+ $original: typeof Intl.RelativeTimeFormat;
83
+ };
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
- var _a, _b;
2
+ var _a, _b, _c;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports._PatchedListFormat = exports._PatchedPluralRules = exports._patchedDateToLocaleTimeString = exports._patchedDateToLocaleDateString = exports._patchedDateToLocaleString = exports._PatchedDateTimeFormat = exports._patchedNumberToLocaleString = exports._PatchedNumberFormat = exports._patchedStringLocaleCompare = exports._PatchedCollator = void 0;
5
- const _Collator = Intl.Collator;
6
- const mapLocales = (locales) => {
4
+ exports._PatchedRelativeTimeFormat = exports._PatchedListFormat = exports._PatchedPluralRules = exports._patchedDateToLocaleTimeString = exports._patchedDateToLocaleDateString = exports._patchedDateToLocaleString = exports._PatchedDateTimeFormat = exports._patchedNumberToLocaleString = exports._PatchedNumberFormat = exports._patchedStringLocaleCompare = exports._PatchedCollator = void 0;
5
+ const islLocaleRe = /^isl?(?:-|$)/i;
6
+ const mapLocales = (constr, locales) => {
7
7
  locales = typeof locales === 'string' ? [locales] : locales || [];
8
8
  for (let i = 0, loc; (loc = locales[i]); i++) {
9
- const isIsLocale = /^isl?(?:-|$)/i.test(loc);
10
- if (isIsLocale) {
9
+ const isIslLocale = islLocaleRe.test(loc);
10
+ if (isIslLocale) {
11
11
  // Danish feels like a "good enough" substitution for Icelandic.
12
12
  // For alphabetization, it seems to just the internal order of `Ø` and `Ö`
13
13
  // that's different, and when the `sensitivity` option is set to "base"
@@ -18,22 +18,49 @@ const mapLocales = (locales) => {
18
18
  // as fully equal to the base letter.
19
19
  return ['da'];
20
20
  }
21
- if (_Collator.supportedLocalesOf(loc).length) {
21
+ if (constr.supportedLocalesOf(loc).length) {
22
22
  return; // no mapping needed. YOLO!
23
23
  }
24
24
  }
25
25
  };
26
+ const patchSupportedLocalesOf = (constr) => {
27
+ const BASE_CHAR_CODE = 64; // 'A'.charCodeAt(0) - 1; // used for generating unique suffix for fake locales
28
+ const sLO = (locales, options) => {
29
+ let localesArr = typeof locales === 'string' ? [locales] : locales;
30
+ const memoIsl = [];
31
+ localesArr = localesArr.map((locale) => {
32
+ if (islLocaleRe.test(locale)) {
33
+ // Some engines throw a RangeError if the locale is weirdly shaped,
34
+ // so we must use a short, safe, unique fake locale instead,
35
+ // and store the actual locale in `memoIsl` for later reinsertion.
36
+ memoIsl.push(locale);
37
+ return `da-X${String.fromCharCode(BASE_CHAR_CODE + memoIsl.length)}`;
38
+ }
39
+ return locale;
40
+ });
41
+ const supportedLocales = constr.supportedLocalesOf(localesArr, options);
42
+ if (!memoIsl.length) {
43
+ return supportedLocales;
44
+ }
45
+ return supportedLocales.map((locale) => locale.startsWith('da-X') ? memoIsl.shift() : locale);
46
+ };
47
+ return sLO;
48
+ };
26
49
  const combineParts = (parts) => parts.map(({ value }) => value).join('');
50
+ // ===========================================================================
51
+ // Collator
52
+ // ===========================================================================
53
+ const _Collator = Intl.Collator;
27
54
  const PatchedCollator = function Collator(locales, options) {
28
55
  if (!(this instanceof PatchedCollator)) {
29
56
  // @ts-expect-error (YOLO! Can't be arsed)
30
57
  return new PatchedCollator(locales, options);
31
58
  }
32
- const mappedLocales = mapLocales(locales);
33
- const parent = _Collator(mappedLocales || locales, options);
59
+ const mappedLocales = mapLocales(_Collator, locales);
60
+ const super_ = _Collator(mappedLocales || locales, options);
34
61
  const mapped = !!mappedLocales;
35
62
  this.compare = (a, b) => {
36
- const res1 = parent.compare(a, b);
63
+ const res1 = super_.compare(a, b);
37
64
  if (!mapped) {
38
65
  return res1;
39
66
  }
@@ -42,14 +69,13 @@ const PatchedCollator = function Collator(locales, options) {
42
69
  if (/\d/.test(a0 + b0)) {
43
70
  return res1;
44
71
  }
45
- const res2 = parent.compare(a0, b0);
72
+ const res2 = super_.compare(a0, b0);
46
73
  return res2 !== 0 ? res2 : res1;
47
74
  };
48
- this.resolvedOptions = () => parent.resolvedOptions();
75
+ this.resolvedOptions = () => super_.resolvedOptions();
49
76
  };
50
77
  PatchedCollator.prototype = { constructor: PatchedCollator };
51
- // Static methods (not patched since "is" is not ACTUALLY supported.)
52
- PatchedCollator.supportedLocalesOf = _Collator.supportedLocalesOf;
78
+ PatchedCollator.supportedLocalesOf = /*#__PURE__*/ patchSupportedLocalesOf(_Collator);
53
79
  PatchedCollator.$original = _Collator;
54
80
  exports._PatchedCollator = PatchedCollator;
55
81
  // ---------------------------------------------------------------------------
@@ -82,24 +108,26 @@ const PatchedNumberFormat = function NumberFormat(locales, options) {
82
108
  // @ts-expect-error (YOLO! Can't be arsed)
83
109
  return new PatchedNumberFormat(locales, options);
84
110
  }
85
- const mappedLocales = mapLocales(locales);
86
- const parent = _NumberFormat(mappedLocales || locales, options);
111
+ const mappedLocales = mapLocales(_NumberFormat, locales);
112
+ const super_ = _NumberFormat(mappedLocales || locales, options);
87
113
  const mapped = !!mappedLocales;
88
- this.format = (value) => combineParts(this.formatToParts(value));
89
- this.formatRange = (value1, value2) => combineParts(this.formatRangeToParts(value1, value2));
114
+ this.format = (value) => mapped ? combineParts(this.formatToParts(value)) : super_.format(value);
115
+ this.formatRange = (value1, value2) => mapped
116
+ ? combineParts(this.formatRangeToParts(value1, value2))
117
+ : super_.formatRange(value1, value2);
90
118
  this.formatToParts = (value) => {
91
- const parts = parent.formatToParts(value);
92
- return mapped ? reformatNumberParts(parent, parts) : parts;
119
+ const parts = super_.formatToParts(value);
120
+ return mapped ? reformatNumberParts(super_, parts) : parts;
93
121
  };
94
122
  this.formatRangeToParts = (value1, value2) => {
95
- const parts = parent.formatRangeToParts(value1, value2);
96
- return mapped ? reformatNumberParts(parent, parts) : parts;
123
+ const parts = super_.formatRangeToParts(value1, value2);
124
+ return mapped ? reformatNumberParts(super_, parts) : parts;
97
125
  };
98
- this.resolvedOptions = () => parent.resolvedOptions();
126
+ this.resolvedOptions = () => super_.resolvedOptions();
99
127
  };
100
128
  PatchedNumberFormat.prototype = { constructor: PatchedNumberFormat };
101
- // Static methods (not patched since "is" is not ACTUALLY supported.)
102
- PatchedNumberFormat.supportedLocalesOf = _NumberFormat.supportedLocalesOf;
129
+ PatchedNumberFormat.supportedLocalesOf =
130
+ /*#__PURE__*/ patchSupportedLocalesOf(_NumberFormat);
103
131
  PatchedNumberFormat.$original = _NumberFormat;
104
132
  exports._PatchedNumberFormat = PatchedNumberFormat;
105
133
  // ---------------------------------------------------------------------------
@@ -202,7 +230,7 @@ const PatchedDateTimeFormat = function DateTimeFormat(locales, options) {
202
230
  // @ts-expect-error (YOLO! Can't be arsed)
203
231
  return new PatchedDateTimeFormat(locales, options);
204
232
  }
205
- const mappedLocales = mapLocales(locales);
233
+ const mappedLocales = mapLocales(_DateTimeFormat, locales);
206
234
  if (options === null || options === void 0 ? void 0 : options.hour12) {
207
235
  options = {
208
236
  ...options,
@@ -210,23 +238,25 @@ const PatchedDateTimeFormat = function DateTimeFormat(locales, options) {
210
238
  hourCycle: 'h11',
211
239
  };
212
240
  }
213
- const parent = _DateTimeFormat(mappedLocales || locales, options);
241
+ const super_ = _DateTimeFormat(mappedLocales || locales, options);
214
242
  const mapped = !!mappedLocales;
215
- this.format = (value) => combineParts(this.formatToParts(value));
216
- this.formatRange = (value1, value2) => combineParts(this.formatRangeToParts(value1, value2));
243
+ this.format = (value) => mapped ? combineParts(this.formatToParts(value)) : super_.format(value);
244
+ this.formatRange = (value1, value2) => mapped
245
+ ? combineParts(this.formatRangeToParts(value1, value2))
246
+ : super_.formatRange(value1, value2);
217
247
  this.formatToParts = (value) => {
218
- const parts = parent.formatToParts(value);
219
- return mapped ? reformatDateTimeParts(parent, parts) : parts;
248
+ const parts = super_.formatToParts(value);
249
+ return mapped ? reformatDateTimeParts(super_, parts) : parts;
220
250
  };
221
251
  this.formatRangeToParts = (value1, value2) => {
222
- const parts = parent.formatRangeToParts(value1, value2);
223
- return mapped ? reformatDateTimeParts(parent, parts) : parts;
252
+ const parts = super_.formatRangeToParts(value1, value2);
253
+ return mapped ? reformatDateTimeParts(super_, parts) : parts;
224
254
  };
225
- this.resolvedOptions = () => parent.resolvedOptions();
255
+ this.resolvedOptions = () => super_.resolvedOptions();
226
256
  };
227
257
  PatchedDateTimeFormat.prototype = { constructor: PatchedDateTimeFormat };
228
- // Static methods (not patched since "is" is not ACTUALLY supported.)
229
- PatchedDateTimeFormat.supportedLocalesOf = _DateTimeFormat.supportedLocalesOf;
258
+ PatchedDateTimeFormat.supportedLocalesOf =
259
+ /*#__PURE__*/ patchSupportedLocalesOf(_DateTimeFormat);
230
260
  PatchedDateTimeFormat.$original = _DateTimeFormat;
231
261
  exports._PatchedDateTimeFormat = PatchedDateTimeFormat;
232
262
  // ---------------------------------------------------------------------------
@@ -307,15 +337,14 @@ let PatchedPluralRules;
307
337
  if (_PluralRules) {
308
338
  PatchedPluralRules = (_a = class PluralRules extends _PluralRules {
309
339
  pluralIsl(n) {
340
+ n = n < 0 ? -n : n;
310
341
  return this.ord ? 'other' : n % 10 !== 1 || n % 100 === 11 ? 'other' : 'one';
311
342
  }
312
343
  constructor(locales, options) {
313
- const mappedLocales = mapLocales(locales);
344
+ const mappedLocales = mapLocales(_PluralRules, locales);
314
345
  super(mappedLocales || locales, options);
315
346
  this.mapped = !!mappedLocales;
316
347
  this.ord = (options === null || options === void 0 ? void 0 : options.type) === 'ordinal';
317
- this.select = this.select.bind(this);
318
- this.selectRange = this.selectRange.bind(this);
319
348
  }
320
349
  select(n) {
321
350
  if (this.mapped) {
@@ -333,6 +362,7 @@ if (_PluralRules) {
333
362
  return super.selectRange(n, n2);
334
363
  }
335
364
  },
365
+ _a.supportedLocalesOf = patchSupportedLocalesOf(_PluralRules),
336
366
  _a.$original = _PluralRules,
337
367
  _a);
338
368
  }
@@ -346,11 +376,9 @@ let PatchedListFormat;
346
376
  if (_ListFormat) {
347
377
  PatchedListFormat = (_b = class ListFormat extends _ListFormat {
348
378
  constructor(locales, options) {
349
- const mappedLocales = mapLocales(locales);
379
+ const mappedLocales = mapLocales(_ListFormat, locales);
350
380
  super(mappedLocales || locales, options);
351
381
  this.mapped = !!mappedLocales;
352
- this.format = this.format.bind(this);
353
- this.formatToParts = this.formatToParts.bind(this);
354
382
  }
355
383
  format(list) {
356
384
  return this.mapped ? combineParts(this.formatToParts(list)) : super.format(list);
@@ -359,8 +387,8 @@ if (_ListFormat) {
359
387
  const parts = super.formatToParts(list);
360
388
  if (this.mapped) {
361
389
  for (const item of parts) {
362
- const { value } = item;
363
- if (item.type === 'literal' && (value === ' el. ' || value === ' eller ')) {
390
+ const { type, value } = item;
391
+ if (type === 'literal' && (value === ' el. ' || value === ' eller ')) {
364
392
  item.value = ' eða ';
365
393
  }
366
394
  }
@@ -368,7 +396,112 @@ if (_ListFormat) {
368
396
  return parts;
369
397
  }
370
398
  },
399
+ _b.supportedLocalesOf = patchSupportedLocalesOf(_ListFormat),
371
400
  _b.$original = _ListFormat,
372
401
  _b);
373
402
  }
374
403
  exports._PatchedListFormat = PatchedListFormat;
404
+ // ===========================================================================
405
+ // RelativeTimeFormat
406
+ // ===========================================================================
407
+ const _RelativeTimeFormat = Intl.RelativeTimeFormat;
408
+ let PatchedRelativeTimeFormat;
409
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
410
+ if (_RelativeTimeFormat) {
411
+ let pluralIsl;
412
+ let numFormatIsl;
413
+ const islUnits = {
414
+ year: [['ári', 'árum', 'ár', 'ár'], ''],
415
+ quarter: [['ársfjórðungi', 'ársfjórðungum', 'ársfjórðung', 'ársfjórðunga'], 'ársfj.'],
416
+ month: [['mánuði', 'mánuðum', 'mánuð', 'mánuði'], 'mán.'],
417
+ week: [['viku', 'vikum', 'viku', 'vikur'], ''],
418
+ day: [['degi', 'dögum', 'dag', 'daga'], ''],
419
+ hour: [['klukkustund', 'klukkustundum', 'klukkustund', 'klukkustundir'], 'klst.'],
420
+ minute: [['mínútu', 'mínútum', 'mínútu', 'mínútur'], 'mín.'],
421
+ second: [['sekúndu', 'sekúndum', 'sekúndu', 'sekúndur'], 'sek.'],
422
+ };
423
+ const phrases = {
424
+ nu: 'núna',
425
+ 'næste år': 'á næsta ári',
426
+ 'sidste år': 'á síðasta ári',
427
+ 'i år': 'á þessu ári',
428
+ 'sidste kvartal': 'síðasti ársfjórðungur',
429
+ 'dette kvartal': 'þessi ársfjórðungur',
430
+ 'næste kvartal': 'næsti ársfjórðungur',
431
+ 'sidste kvt.': 'síðasti ársfj.',
432
+ 'dette kvt.': 'þessi ársfj.',
433
+ 'næste kvt.': 'næsti ársfj.',
434
+ 'sidste måned': 'í síðasta mánuði',
435
+ 'denne måned': 'í þessum mánuði',
436
+ 'næste måned': 'í næsta mánuði',
437
+ 'sidste md.': 'í síðasta mán.',
438
+ 'denne md.': 'í þessum mán.',
439
+ 'næste md.': 'í næsta mán.',
440
+ 'sidste uge': 'í síðustu viku',
441
+ 'denne uge': 'í þessari viku',
442
+ 'næste uge': 'í næstu viku',
443
+ 'i forgårs': 'í fyrradag',
444
+ 'i går': 'í gær',
445
+ 'i dag': 'í dag',
446
+ 'i morgen': 'á morgun',
447
+ 'i overmorgen': 'eftir tvo daga',
448
+ 'denne time': 'þessa stundina',
449
+ 'dette minut': 'á þessari mínútu',
450
+ };
451
+ PatchedRelativeTimeFormat = (_c = class RelativeTimeFormat extends _RelativeTimeFormat {
452
+ constructor(locales, options) {
453
+ const mappedLocales = mapLocales(_RelativeTimeFormat, locales);
454
+ super(mappedLocales || locales, options);
455
+ this.mapped = !!mappedLocales;
456
+ }
457
+ format(value, unit) {
458
+ return this.mapped
459
+ ? combineParts(this.formatToParts(value, unit))
460
+ : super.format(value, unit);
461
+ }
462
+ // eslint-disable-next-line complexity
463
+ formatToParts(value, unit) {
464
+ const parts = super.formatToParts(value, unit);
465
+ if (!this.mapped) {
466
+ return parts;
467
+ }
468
+ if (!pluralIsl) {
469
+ pluralIsl = new exports._PatchedPluralRules('is');
470
+ }
471
+ if (!numFormatIsl) {
472
+ numFormatIsl = new exports._PatchedNumberFormat('is');
473
+ }
474
+ const options = this.resolvedOptions();
475
+ const unitSngl = unit.replace(/s$/, '');
476
+ if (parts.length === 1) {
477
+ const firstPart = parts[0];
478
+ firstPart.value = phrases[firstPart.value] || firstPart.value;
479
+ return parts;
480
+ }
481
+ const [long, short] = islUnits[unitSngl];
482
+ const idx = (value < 0 ? 0 : 2) + (pluralIsl.select(value) === 'one' ? 0 : 1);
483
+ const prefixStr = options.style === 'narrow' &&
484
+ (unitSngl === 'second' || unitSngl === 'minute' || unitSngl === 'hour')
485
+ ? value < 0
486
+ ? '-'
487
+ : '+'
488
+ : value < 0
489
+ ? 'fyrir '
490
+ : 'eftir ';
491
+ const valueStr = (options.style !== 'long' && short) || long[idx];
492
+ const islParts = [
493
+ { type: 'literal', value: prefixStr },
494
+ ...numFormatIsl.formatToParts(Math.abs(value)).map((part) => {
495
+ part.unit =
496
+ unitSngl;
497
+ return part;
498
+ }),
499
+ { type: 'literal', value: ` ${valueStr}` },
500
+ ];
501
+ return islParts;
502
+ }
503
+ },
504
+ _c.$original = _RelativeTimeFormat,
505
+ _c);
506
+ }
507
+ exports._PatchedRelativeTimeFormat = PatchedRelativeTimeFormat;
package/http.d.ts CHANGED
@@ -158,12 +158,19 @@ type TTLObj = {
158
158
  */
159
159
  export type TTLConfig = TTL | TTLKeywords | TTLObj;
160
160
  /**
161
- * Converts a `TTL` (max-age) value into seconds, and returns `0` for bad
162
- * and/or negative input values.
161
+ * Converts a `TTL` (max-age) value into seconds. Returns `0` for bad and/or
162
+ * negative input values.
163
163
  *
164
164
  * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#tosec-ttl-helper
165
165
  */
166
166
  export declare const toSec: (ttl: TTL) => number;
167
+ /**
168
+ * Converts a `TTL` (duration) value into milliseconds. Returns `0` for bad
169
+ * and/or negative input values.
170
+ *
171
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#toms-duration-helper
172
+ */
173
+ export declare const toMs: (ttl: TTL) => number;
167
174
  type ServerResponseStub = Pick<ServerResponse, 'setHeader' | 'getHeader' | 'removeHeader'> & {
168
175
  headers?: Record<string, string | Array<string>>;
169
176
  };
package/http.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HTTP_502_BadGateway = exports.HTTP_501_NotImplemented = exports.HTTP_500_InternalServerError = exports.HTTP_451_UnavailableForLegalReasons = exports.HTTP_431_RequestHeaderFieldsTooLarge = exports.HTTP_429_TooManyRequests = exports.HTTP_428_PreconditionRequired = exports.HTTP_426_UpgradeRequired = exports.HTTP_424_FailedDependency = exports.HTTP_423_Locked = exports.HTTP_422_UnprocessableContent = exports.HTTP_421_MisdirectedRequest = exports.HTTP_418_ImATeapot = exports.HTTP_417_ExpectationFailed = exports.HTTP_416_RangeNotSatisfiable = exports.HTTP_415_UnsupportedMediaType = exports.HTTP_414_URITooLong = exports.HTTP_413_PayloadTooLarge = exports.HTTP_412_PreconditionFailed = exports.HTTP_411_LengthRequired = exports.HTTP_410_Gone = exports.HTTP_409_Conflict = exports.HTTP_408_RequestTimeout = exports.HTTP_407_ProxyAuthenticationRequired = exports.HTTP_406_NotAcceptable = exports.HTTP_405_MethodNotAllowed = exports.HTTP_404_NotFound = exports.HTTP_403_Forbidden = exports.HTTP_401_Unauthorized = exports.HTTP_400_BadRequest = exports.HTTP_308_PermanentRedirect = exports.HTTP_307_TemporaryRedirect = exports.HTTP_304_NotModified = exports.HTTP_303_SeeOther = exports.HTTP_302_Found = exports.HTTP_301_MovedPermanently = exports.HTTP_226_IMUsed = exports.HTTP_208_AlreadyReported = exports.HTTP_207_MultiStatus = exports.HTTP_206_PartialContent = exports.HTTP_205_ResetContent = exports.HTTP_204_NoContent = exports.HTTP_203_NonAuthoritativeInformation = exports.HTTP_202_Accepted = exports.HTTP_201_Created = exports.HTTP_200_OK = exports.HTTP_103_EarlyHints = exports.HTTP_102_Processing = exports.HTTP_101_SwitchingProtocols = exports.HTTP_100_Continue = void 0;
4
- exports.cacheControlHeaders = exports.cacheControl = exports.toSec = exports.HTTP_511_NetworkAuthenticationRequired = exports.HTTP_510_NotExtended = exports.HTTP_508_LoopDetected = exports.HTTP_507_InsufficientStorage = exports.HTTP_506_VariantAlsoNegotiates = exports.HTTP_505_HTTPVersionNotSupported = exports.HTTP_504_GatewayTimeout = exports.HTTP_503_ServiceUnavailable = void 0;
4
+ exports.cacheControlHeaders = exports.cacheControl = exports.toMs = exports.toSec = exports.HTTP_511_NetworkAuthenticationRequired = exports.HTTP_510_NotExtended = exports.HTTP_508_LoopDetected = exports.HTTP_507_InsufficientStorage = exports.HTTP_506_VariantAlsoNegotiates = exports.HTTP_505_HTTPVersionNotSupported = exports.HTTP_504_GatewayTimeout = exports.HTTP_503_ServiceUnavailable = void 0;
5
5
  // INFORMATION
6
6
  /** The client should continue the request or ignore the response if the request is already finished. */
7
7
  exports.HTTP_100_Continue = 100;
@@ -137,8 +137,8 @@ const unitToSeconds = {
137
137
  w: 7 * 24 * 3600,
138
138
  };
139
139
  /**
140
- * Converts a `TTL` (max-age) value into seconds, and returns `0` for bad
141
- * and/or negative input values.
140
+ * Converts a `TTL` (max-age) value into seconds. Returns `0` for bad and/or
141
+ * negative input values.
142
142
  *
143
143
  * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#tosec-ttl-helper
144
144
  */
@@ -151,6 +151,14 @@ const toSec = (ttl) => {
151
151
  return Math.max(0, Math.round(ttl)) || 0;
152
152
  };
153
153
  exports.toSec = toSec;
154
+ /**
155
+ * Converts a `TTL` (duration) value into milliseconds. Returns `0` for bad
156
+ * and/or negative input values.
157
+ *
158
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#toms-duration-helper
159
+ */
160
+ const toMs = (ttl) => (0, exports.toSec)(ttl) * 1000;
161
+ exports.toMs = toMs;
154
162
  const toRespnseStubHeaders = (response) => {
155
163
  if (response instanceof Map) {
156
164
  return response;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/webtools",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Misc. JS/TS helpers used by Reykjavík City's web dev teams.",
5
5
  "main": "index.js",
6
6
  "repository": "ssh://git@github.com:reykjavikcity/webtools.git",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "license": "MIT",
13
13
  "dependencies": {
14
- "@reykjavik/hanna-utils": "^0.2.3"
14
+ "@reykjavik/hanna-utils": "^0.2.16"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "@remix-run/react": "^2.6.0",