@reykjavik/webtools 0.1.28 → 0.1.30

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,12 +4,28 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
- ## 0.1.28
7
+ ## 0.1.30
8
+
9
+ _2024-09-15_
10
+
11
+ - `@reykjavik/webtools/http`:
12
+ - feat: Add `toMs` duration helper
13
+ - `@reykjavik/webtools/fixIcelandicLocale`:
14
+ - feat: Patch `Intl.RelativeTimeFormat`
15
+ - feat: Patch all `supportedLocalesOf` methods to report "is\*" as supported
16
+ - fix: Incorrect `PluralRules` results for negative values
17
+ - fix: Use each `Intl.*` class' `supportedLocalesOf` method to map locales
18
+ - fix: Remove unnecessary `Intl.\*` method instance-bindings
19
+
20
+ ## 0.1.28 – 0.1.29
8
21
 
9
22
  _2024-08-26_
10
23
 
11
24
  - `@reykjavik/webtools/http`:
25
+ - feat: Add `cacheControlHeaders` helper that returns a `HeadersInit` object
12
26
  - feat: `cacheControl` now also accepts `Map<string, string>` for headers
27
+ - fix: `cacheControl` with `maxAge: 'unset'` didn't delete `X-Cache-Control`
28
+ in dev mode
13
29
 
14
30
  ## 0.1.27
15
31
 
package/README.md CHANGED
@@ -20,8 +20,10 @@ bun add @reykjavik/webtools
20
20
  - [HTTP Status Codes](#http-status-codes)
21
21
  - [Types for HTTP Status code groups](#types-for-http-status-code-groups)
22
22
  - [`cacheControl` helper](#cachecontrol-helper)
23
+ - [`cacheControlHeaders` helper](#cachecontrolheaders-helper)
23
24
  - [Type `TTLConfig`](#type-ttlconfig)
24
25
  - [`toSec` TTL helper](#tosec-ttl-helper)
26
+ - [`toMs` duration helper](#toms-duration-helper)
25
27
  - [`@reykjavik/webtools/async`](#reykjavikwebtoolsasync)
26
28
  - [`promiseAllObject`](#promiseallobject)
27
29
  - [`maxWait`](#maxwait)
@@ -131,6 +133,27 @@ The directives `private` and `immutable` are used by by default.
131
133
  Use the optional `eTag` parameter if you intend to
132
134
  [handle conditional requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests).
133
135
 
136
+ ### `cacheControlHeaders` helper
137
+
138
+ **Syntax:**
139
+ `cacheControlHeaders(ttlCfg: TTLConfig, eTag?: string|number): Record<string, string>`
140
+
141
+ Similar to the [`cacheControl` helper](#cachecontrol-helper), but returns an
142
+ plain object with the headers for use in situations where `HeadersInit` object
143
+ are expected.
144
+
145
+ ```js
146
+ import { cacheControlHeaders } from '@reykjavik/webtools/http';
147
+
148
+ const response = new Response('Hello, World!', {
149
+ headers: cacheControlHeaders('4h'),
150
+ });
151
+ ```
152
+
153
+ ```js
154
+
155
+ ```
156
+
134
157
  #### Type `TTLConfig`
135
158
 
136
159
  ```js
@@ -184,7 +207,7 @@ behavior.
184
207
  **Syntax:**
185
208
  `` toSec(ttl: number | `${number}${'s'|'m'|'h'|'d'|'w'}`): number ``
186
209
 
187
- 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
188
211
  negative input values.
189
212
 
190
213
  ```js
@@ -195,6 +218,22 @@ const ttl: TTL = '2h';
195
218
  const ttlSec = toSec(ttl);
196
219
  ```
197
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
+
198
237
  ---
199
238
 
200
239
  ## `@reykjavik/webtools/async`
@@ -266,15 +305,16 @@ This module patches the following methods/classes by substituting the `is`
266
305
  locale with `da` (Danish) and apply a few post-hoc fixes to their return
267
306
  values.
268
307
 
269
- - `Intl.Collator` and `String.prototype.localeCompare`
270
- - `Intl.NumberFormat` and `Number.prototype.toLocaleString`
308
+ - `Intl.Collator` and `String.prototype.localeCompare` (\*)
309
+ - `Intl.NumberFormat` and `Number.prototype.toLocaleString` (\*)
271
310
  - `Intl.DateTimeFormat` and `Date.prototype.toLocaleString`,
272
- `.toLocaleDateString`, and `.toLocaleTimeString`
311
+ `.toLocaleDateString`, and `.toLocaleTimeString` (\*)
312
+ - `Intl.RelativeDateFormat`
273
313
  - `Intl.PluralRules`
274
314
  - `Intl.ListFormat`
275
315
 
276
- This provides usable (but not perfect) results, with a few caveats that are
277
- listed below.
316
+ (\*) The results are quite usable, but not entirely perfect. The
317
+ limitations/caveats are listed below.
278
318
 
279
319
  To apply the patch, simply "side-effect import" this module at the top of your
280
320
  app's entry point:
@@ -316,6 +356,8 @@ detection test.)
316
356
  - The `dayPeriod` option has a couple of slight mismatches, at 5 am and 12
317
357
  noon.
318
358
 
359
+ We eagerly accept bugfixes, additions, etc. to this module!
360
+
319
361
  ---
320
362
 
321
363
  ## `@reykjavik/webtools/SiteImprove`
@@ -551,19 +593,19 @@ export const myClass = vanillaClass(`
551
593
  // more complex styles.
552
594
  export const myOtherClass = vanillaClass(
553
595
  (className) => `
554
- .${className} {
555
- background-color: #ccc;
556
- padding: .5em 1em;
557
- }
558
- .${className} > strong {
559
- color: #c00;
560
- }
561
- @media (min-width: 800px) {
562
- .${className} {
563
- background-color: #eee;
596
+ .${className} {
597
+ background-color: #ccc;
598
+ padding: .5em 1em;
564
599
  }
565
- }
566
- `
600
+ .${className} > strong {
601
+ color: #c00;
602
+ }
603
+ @media (min-width: 800px) {
604
+ .${className} {
605
+ background-color: #eee;
606
+ }
607
+ }
608
+ `
567
609
  );
568
610
 
569
611
  export const humanReadableClass = vanillaClass(
@@ -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
  };
@@ -174,9 +181,17 @@ type ResponseStub = {
174
181
  * Use this function to quickly set the `Cache-Control` header with a `max-age=`
175
182
  * on a HTTP response
176
183
  *
177
- * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#getcssbundleurl
184
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#cachecontrol-helper
178
185
  */
179
186
  export declare const cacheControl: (response: ServerResponseStub | ResponseStub | Map<string, string> | {
180
187
  res: ServerResponseStub | ResponseStub;
181
188
  }, ttlCfg: TTLConfig, eTag?: string | number) => void;
189
+ /**
190
+ * Generates a Record with `Cache-Control` and `ETag` headers, for use in
191
+ * situations requiring a `HeadersInit` compatible object.
192
+ *
193
+ * Accepts the same arguments as `cacheControl()`.
194
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#cachecontrolheaders-helper
195
+ */
196
+ export declare const cacheControlHeaders: (ttlCfg: TTLConfig, eTag?: string | number) => Record<string, string>;
182
197
  export {};
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;
@@ -188,7 +195,7 @@ const setCC = (response, cc) => {
188
195
  * Use this function to quickly set the `Cache-Control` header with a `max-age=`
189
196
  * on a HTTP response
190
197
  *
191
- * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#getcssbundleurl
198
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#cachecontrol-helper
192
199
  */
193
200
  // eslint-disable-next-line complexity
194
201
  export const cacheControl = (response, ttlCfg, eTag) => {
@@ -209,7 +216,7 @@ export const cacheControl = (response, ttlCfg, eTag) => {
209
216
  }
210
217
  }
211
218
  if (maxAge == null) {
212
- toRespnseStubHeaders(response).delete('Cache-Control');
219
+ setCC(response, undefined);
213
220
  return;
214
221
  }
215
222
  maxAge = toSec(maxAge);
@@ -226,3 +233,15 @@ export const cacheControl = (response, ttlCfg, eTag) => {
226
233
  setCC(response, `${scope}, max-age=${maxAge + sWR + sIE + stability}`);
227
234
  eTag != null && toRespnseStubHeaders(response).set('ETag', String(eTag));
228
235
  };
236
+ /**
237
+ * Generates a Record with `Cache-Control` and `ETag` headers, for use in
238
+ * situations requiring a `HeadersInit` compatible object.
239
+ *
240
+ * Accepts the same arguments as `cacheControl()`.
241
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#cachecontrolheaders-helper
242
+ */
243
+ export const cacheControlHeaders = (ttlCfg, eTag) => {
244
+ const headers = new Map();
245
+ cacheControl(headers, ttlCfg, eTag);
246
+ return Object.fromEntries(headers);
247
+ };
@@ -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
  };
@@ -174,9 +181,17 @@ type ResponseStub = {
174
181
  * Use this function to quickly set the `Cache-Control` header with a `max-age=`
175
182
  * on a HTTP response
176
183
  *
177
- * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#getcssbundleurl
184
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#cachecontrol-helper
178
185
  */
179
186
  export declare const cacheControl: (response: ServerResponseStub | ResponseStub | Map<string, string> | {
180
187
  res: ServerResponseStub | ResponseStub;
181
188
  }, ttlCfg: TTLConfig, eTag?: string | number) => void;
189
+ /**
190
+ * Generates a Record with `Cache-Control` and `ETag` headers, for use in
191
+ * situations requiring a `HeadersInit` compatible object.
192
+ *
193
+ * Accepts the same arguments as `cacheControl()`.
194
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#cachecontrolheaders-helper
195
+ */
196
+ export declare const cacheControlHeaders: (ttlCfg: TTLConfig, eTag?: string | number) => Record<string, string>;
182
197
  export {};
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.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;
@@ -193,7 +201,7 @@ const setCC = (response, cc) => {
193
201
  * Use this function to quickly set the `Cache-Control` header with a `max-age=`
194
202
  * on a HTTP response
195
203
  *
196
- * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#getcssbundleurl
204
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#cachecontrol-helper
197
205
  */
198
206
  // eslint-disable-next-line complexity
199
207
  const cacheControl = (response, ttlCfg, eTag) => {
@@ -214,7 +222,7 @@ const cacheControl = (response, ttlCfg, eTag) => {
214
222
  }
215
223
  }
216
224
  if (maxAge == null) {
217
- toRespnseStubHeaders(response).delete('Cache-Control');
225
+ setCC(response, undefined);
218
226
  return;
219
227
  }
220
228
  maxAge = (0, exports.toSec)(maxAge);
@@ -232,3 +240,16 @@ const cacheControl = (response, ttlCfg, eTag) => {
232
240
  eTag != null && toRespnseStubHeaders(response).set('ETag', String(eTag));
233
241
  };
234
242
  exports.cacheControl = cacheControl;
243
+ /**
244
+ * Generates a Record with `Cache-Control` and `ETag` headers, for use in
245
+ * situations requiring a `HeadersInit` compatible object.
246
+ *
247
+ * Accepts the same arguments as `cacheControl()`.
248
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#cachecontrolheaders-helper
249
+ */
250
+ const cacheControlHeaders = (ttlCfg, eTag) => {
251
+ const headers = new Map();
252
+ (0, exports.cacheControl)(headers, ttlCfg, eTag);
253
+ return Object.fromEntries(headers);
254
+ };
255
+ exports.cacheControlHeaders = cacheControlHeaders;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/webtools",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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",