@jarenjs/locales 0.49.2 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/types/intl-zones.d.ts +41 -0
- package/package.json +6 -2
- package/src/ar.js +2 -0
- package/src/de.js +2 -0
- package/src/es.js +2 -0
- package/src/fr.js +2 -0
- package/src/index.js +5 -1
- package/src/intl-zones.js +271 -0
- package/src/ja.js +2 -0
- package/src/ko.js +2 -0
- package/src/nl.js +2 -0
- package/src/pt.js +2 -0
- package/src/ru.js +2 -0
- package/src/tr.js +2 -0
- package/src/zh-tw.js +2 -0
package/README.md
CHANGED
|
@@ -139,6 +139,59 @@ object, and a bundler drops the provider from a bundle that never calls
|
|
|
139
139
|
it. ICU has no date-format display names, so that member carries the
|
|
140
140
|
repository's English ones unless `options.formatNames` overrides them.
|
|
141
141
|
|
|
142
|
+
### The optional `Intl` zone provider
|
|
143
|
+
|
|
144
|
+
The same idea one seam over. `@jarenjs/core/series` reads its calendar
|
|
145
|
+
boundaries on a clock, and a clock that is a named zone needs a
|
|
146
|
+
`provider` — a tzdb — that the kernel refuses to bundle, because a
|
|
147
|
+
bundled table is megabytes that go stale on a government's timetable.
|
|
148
|
+
`createIntlZoneProvider(options?)` is that provider, over the host's ICU:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { createIntlZoneProvider } from '@jarenjs/locales/intl-zones';
|
|
152
|
+
import { resolveClock, compileBuckets } from '@jarenjs/core/series';
|
|
153
|
+
|
|
154
|
+
const provider = createIntlZoneProvider();
|
|
155
|
+
|
|
156
|
+
const clock = resolveClock({ zone: 'Europe/Amsterdam', provider, disambiguation: 'later' });
|
|
157
|
+
clock.epochOf({ year: 2026, month: 3, day: 29, hours: 2, minutes: 30, seconds: 0 });
|
|
158
|
+
// 2026-03-29T01:30:00Z — 02:30 never happened that morning; 'later' is 03:30 CEST
|
|
159
|
+
|
|
160
|
+
compileBuckets('P1D', { zone: 'Australia/Adelaide', provider }).floor(Date.UTC(2026, 3, 5, 12));
|
|
161
|
+
// 2026-04-04T13:30:00Z — local midnight on the day the clock went back, 25 hours long
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The seam is unchanged: `@jarenjs/core` still bundles no zone data, a
|
|
165
|
+
named zone with no provider is still a refusal, and this module is what
|
|
166
|
+
a host passes to answer it. Three rules hold it honest. **It never reads
|
|
167
|
+
the host's own zone** — every call names its zone, and there is no
|
|
168
|
+
default that asks the process where it is, because that hidden clock is
|
|
169
|
+
exactly what the seam exists to forbid. **An unknown name is a refusal
|
|
170
|
+
naming it**, never a quiet UTC that is right for eight months of the
|
|
171
|
+
year. And **the hard half is answered exactly**: on a spring-forward day
|
|
172
|
+
02:30 never happens and on a fall-back day it happens twice, so
|
|
173
|
+
`toEpoch(parts, zone, disambiguation)` verifies every candidate instant
|
|
174
|
+
by reading its wall clock back. Two that read back are a fold, and
|
|
175
|
+
`'earlier'`/`'later'` are the smaller and the larger; none is a gap, and
|
|
176
|
+
`'earlier'` is the wall clock read with the offset in force after the
|
|
177
|
+
transition (that far *before* the gap), `'later'` the reading with the
|
|
178
|
+
offset before it (that far after) — one transition apart either way.
|
|
179
|
+
`'reject'`, the kernel's default, answers both with `NaN`, which the
|
|
180
|
+
kernel turns into a refusal naming the local time. Offsets are derived
|
|
181
|
+
from the wall clock rather than parsed from a `GMT+02:00` string, so a
|
|
182
|
+
historical `-00:44:30` is exact.
|
|
183
|
+
|
|
184
|
+
Like `createIntlDateLocale`, it is opt-in for the reason the header of
|
|
185
|
+
that module gives: ICU output moves between Node versions and between a
|
|
186
|
+
browser and a server. Here that means the *tzdata* — a zone whose rules
|
|
187
|
+
changed last year answers differently on a host whose ICU predates the
|
|
188
|
+
change — so a consumer that needs the same instant on every host pins
|
|
189
|
+
the tzdata its hosts run. Nothing runs at module load, and a bundler
|
|
190
|
+
drops the provider from a bundle that never calls it. Formatters are
|
|
191
|
+
built once per zone and kept in a bounded, least-recently-used cache of
|
|
192
|
+
`ZONE_CACHE_LIMIT` zones (`options.zones` resizes it), so a generated
|
|
193
|
+
stream of zone names cannot grow it without limit.
|
|
194
|
+
|
|
142
195
|
## Authoring a pack
|
|
143
196
|
|
|
144
197
|
A catalog is a plain flat object; each entry is either a **template
|
|
@@ -212,3 +265,29 @@ Rules of the road:
|
|
|
212
265
|
|
|
213
266
|
The named export is the locale tag camel-cased (`zhTW`), the subpath keeps
|
|
214
267
|
the tag itself (`@jarenjs/locales/zh-tw`).
|
|
268
|
+
|
|
269
|
+
## Exports
|
|
270
|
+
|
|
271
|
+
Every subpath a consumer can import, derived from the manifest by
|
|
272
|
+
`npm run docs:derive` (`npm run docs:check` fails when the two drift):
|
|
273
|
+
|
|
274
|
+
<!--fact:exports.locales-->
|
|
275
|
+
| Import | Kind | Declarations |
|
|
276
|
+
|---|---|---|
|
|
277
|
+
| `@jarenjs/locales` | JavaScript | declared |
|
|
278
|
+
| `@jarenjs/locales/dates` | JavaScript | declared |
|
|
279
|
+
| `@jarenjs/locales/intl-dates` | JavaScript | declared |
|
|
280
|
+
| `@jarenjs/locales/intl-zones` | JavaScript | declared |
|
|
281
|
+
| `@jarenjs/locales/ar` | JavaScript | declared |
|
|
282
|
+
| `@jarenjs/locales/de` | JavaScript | declared |
|
|
283
|
+
| `@jarenjs/locales/es` | JavaScript | declared |
|
|
284
|
+
| `@jarenjs/locales/fr` | JavaScript | declared |
|
|
285
|
+
| `@jarenjs/locales/ja` | JavaScript | declared |
|
|
286
|
+
| `@jarenjs/locales/ko` | JavaScript | declared |
|
|
287
|
+
| `@jarenjs/locales/nl` | JavaScript | declared |
|
|
288
|
+
| `@jarenjs/locales/pt` | JavaScript | declared |
|
|
289
|
+
| `@jarenjs/locales/ru` | JavaScript | declared |
|
|
290
|
+
| `@jarenjs/locales/tr` | JavaScript | declared |
|
|
291
|
+
| `@jarenjs/locales/zh-tw` | JavaScript | declared |
|
|
292
|
+
| `@jarenjs/locales/package.json` | metadata | — |
|
|
293
|
+
<!--/fact-->
|
package/dist/types/index.d.ts
CHANGED
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
* formatter and a UI read; importing that subpath directly costs no
|
|
18
18
|
* `Intl` construction at all, which is what keeps server-rendered output
|
|
19
19
|
* byte-stable. `createIntlDateLocale` (`./intl-dates`) is the opt-in
|
|
20
|
-
* provider for hosts that want the platform's locales instead
|
|
20
|
+
* provider for hosts that want the platform's locales instead, and
|
|
21
|
+
* `createIntlZoneProvider` (`./intl-zones`) is its twin one seam over:
|
|
22
|
+
* the zone provider `@jarenjs/core/series` takes for a named zone, over
|
|
23
|
+
* the host's ICU. It is reached only by its own subpath and an explicit
|
|
24
|
+
* call, which is what keeps the default path free of `Intl` work.
|
|
21
25
|
*/
|
|
22
26
|
export { dateMessagesEn, compileDateLocale, RELATIVE_UNITS } from './dates.js';
|
|
23
27
|
export { createIntlDateLocale } from './intl-dates.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many zones' formatters stay resident at once. A formatter is
|
|
3
|
+
* expensive to build and cheap to reuse, and a host serving a handful of
|
|
4
|
+
* zones wants each built once; the bound is what keeps a generated or
|
|
5
|
+
* hostile stream of zone names from growing the map without limit.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ZONE_CACHE_LIMIT = 32;
|
|
8
|
+
/**
|
|
9
|
+
* Build a zone provider from the host's ICU time-zone data.
|
|
10
|
+
*
|
|
11
|
+
* `toParts(epoch, zone)` is a formatted read. `toEpoch(parts, zone,
|
|
12
|
+
* disambiguation)` inverts a function that is neither injective nor
|
|
13
|
+
* total: it guesses an instant by reading the wall clock as UTC,
|
|
14
|
+
* corrects it by the offset in force there and again by the offset in
|
|
15
|
+
* force at the corrected instant, adds the offsets in force a day either
|
|
16
|
+
* side so both sides of any transition are represented, and then
|
|
17
|
+
* verifies every distinct candidate by reading its wall clock back.
|
|
18
|
+
* Exactly one candidate that reads back as asked is the instant. Two
|
|
19
|
+
* are a fold - the hour that happened twice - and `earlier` and `later`
|
|
20
|
+
* are the smaller and the larger. None is a gap - the hour that never
|
|
21
|
+
* happened - and `earlier` is the wall clock read with the offset in
|
|
22
|
+
* force after the transition (the instant that far before the gap),
|
|
23
|
+
* `later` the reading with the offset before it (that far after); the
|
|
24
|
+
* two are one transition apart. `reject`, the kernel's default, answers
|
|
25
|
+
* either case with `NaN`, which `@jarenjs/core/series` turns into a
|
|
26
|
+
* refusal naming the local time.
|
|
27
|
+
*
|
|
28
|
+
* An unknown zone name is a refusal naming it, never a quiet UTC, and
|
|
29
|
+
* no call reads the host's own zone.
|
|
30
|
+
*
|
|
31
|
+
* @param {{ zones?: number }} [options] - `zones`: how many zones'
|
|
32
|
+
* formatters stay resident (default `ZONE_CACHE_LIMIT`)
|
|
33
|
+
* @returns {import('@jarenjs/core/series').ZoneProvider}
|
|
34
|
+
* @example
|
|
35
|
+
* const provider = createIntlZoneProvider();
|
|
36
|
+
* resolveClock({ zone: 'Europe/Amsterdam', provider, disambiguation: 'later' });
|
|
37
|
+
* compileBuckets('P1M', { zone: 'Australia/Adelaide', provider });
|
|
38
|
+
*/
|
|
39
|
+
export declare function createIntlZoneProvider(options?: {
|
|
40
|
+
zones?: number;
|
|
41
|
+
}): import('@jarenjs/core/series').ZoneProvider;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/locales",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.66.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/types/intl-dates.d.ts",
|
|
20
20
|
"default": "./src/intl-dates.js"
|
|
21
21
|
},
|
|
22
|
+
"./intl-zones": {
|
|
23
|
+
"types": "./dist/types/intl-zones.d.ts",
|
|
24
|
+
"default": "./src/intl-zones.js"
|
|
25
|
+
},
|
|
22
26
|
"./ar": {
|
|
23
27
|
"types": "./dist/types/ar.d.ts",
|
|
24
28
|
"default": "./src/ar.js"
|
|
@@ -98,6 +102,6 @@
|
|
|
98
102
|
"prepack": "npm run build:types"
|
|
99
103
|
},
|
|
100
104
|
"dependencies": {
|
|
101
|
-
"@jarenjs/core": "^0.
|
|
105
|
+
"@jarenjs/core": "^0.66.1"
|
|
102
106
|
}
|
|
103
107
|
}
|
package/src/ar.js
CHANGED
|
@@ -224,6 +224,8 @@ export const ar = {
|
|
|
224
224
|
'contract/seq-regression': 'خالف دفق العملية {op} ترتيب seq الخاص به',
|
|
225
225
|
'contract/stream-error': 'انتهى دفق العملية {op} بخطأ من الخادم ({code})',
|
|
226
226
|
'contract/heartbeat-missed': 'ظل دفق العملية {op} صامتًا لمدة {ms} مللي ثانية',
|
|
227
|
+
'contract/slow-consumer': 'انتهى دفق العملية {op}: تأخر المستهلك عن طابوره المحدود',
|
|
228
|
+
'contract/reconnect-exhausted': 'تعذّر إعادة إنشاء دفق العملية {op} بعد {attempts} محاولات (الأخيرة: {lastCode})',
|
|
227
229
|
//#endregion
|
|
228
230
|
|
|
229
231
|
//#region calendar language (the date names, relative phrasing and
|
package/src/de.js
CHANGED
|
@@ -167,6 +167,8 @@ export const de = {
|
|
|
167
167
|
'contract/seq-regression': 'der Stream der Operation {op} hat seine seq-Reihenfolge verletzt',
|
|
168
168
|
'contract/stream-error': 'der Stream der Operation {op} endete mit einem Serverfehler ({code})',
|
|
169
169
|
'contract/heartbeat-missed': 'der Stream der Operation {op} blieb {ms} ms lang still',
|
|
170
|
+
'contract/slow-consumer': 'der Stream der Operation {op} wurde beendet: der Verbraucher blieb hinter seiner begrenzten Warteschlange zurück',
|
|
171
|
+
'contract/reconnect-exhausted': 'der Stream der Operation {op} konnte nach {attempts} Versuchen nicht wiederhergestellt werden (zuletzt: {lastCode})',
|
|
170
172
|
//#endregion
|
|
171
173
|
|
|
172
174
|
//#region calendar language (the date names, relative phrasing and
|
package/src/es.js
CHANGED
|
@@ -167,6 +167,8 @@ export const es = {
|
|
|
167
167
|
'contract/seq-regression': 'el flujo de la operación {op} violó el orden de sus seq',
|
|
168
168
|
'contract/stream-error': 'el flujo de la operación {op} terminó con un error del servidor ({code})',
|
|
169
169
|
'contract/heartbeat-missed': 'el flujo de la operación {op} quedó en silencio durante {ms} ms',
|
|
170
|
+
'contract/slow-consumer': 'el flujo de la operación {op} terminó: el consumidor se quedó atrás de su cola acotada',
|
|
171
|
+
'contract/reconnect-exhausted': 'el flujo de la operación {op} no pudo restablecerse tras {attempts} intentos (último: {lastCode})',
|
|
170
172
|
//#endregion
|
|
171
173
|
|
|
172
174
|
//#region calendar language (the date names, relative phrasing and
|
package/src/fr.js
CHANGED
|
@@ -167,6 +167,8 @@ export const fr = {
|
|
|
167
167
|
'contract/seq-regression': "le flux de l'opération {op} a violé l'ordre de ses seq",
|
|
168
168
|
'contract/stream-error': "le flux de l'opération {op} s'est terminé par une erreur serveur ({code})",
|
|
169
169
|
'contract/heartbeat-missed': "le flux de l'opération {op} est resté silencieux pendant {ms} ms",
|
|
170
|
+
'contract/slow-consumer': "le flux de l'opération {op} s'est terminé : le consommateur a pris du retard sur sa file bornée",
|
|
171
|
+
'contract/reconnect-exhausted': "le flux de l'opération {op} n'a pas pu être rétabli après {attempts} tentatives (dernière : {lastCode})",
|
|
170
172
|
//#endregion
|
|
171
173
|
|
|
172
174
|
//#region calendar language (the date names, relative phrasing and
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
* formatter and a UI read; importing that subpath directly costs no
|
|
20
20
|
* `Intl` construction at all, which is what keeps server-rendered output
|
|
21
21
|
* byte-stable. `createIntlDateLocale` (`./intl-dates`) is the opt-in
|
|
22
|
-
* provider for hosts that want the platform's locales instead
|
|
22
|
+
* provider for hosts that want the platform's locales instead, and
|
|
23
|
+
* `createIntlZoneProvider` (`./intl-zones`) is its twin one seam over:
|
|
24
|
+
* the zone provider `@jarenjs/core/series` takes for a named zone, over
|
|
25
|
+
* the host's ICU. It is reached only by its own subpath and an explicit
|
|
26
|
+
* call, which is what keeps the default path free of `Intl` work.
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
export { dateMessagesEn, compileDateLocale, RELATIVE_UNITS } from './dates.js';
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The opt-in `Intl` zone provider: the `ZoneProvider` the temporal
|
|
5
|
+
* kernel's clock seam asks for, sourced from the host's ICU time-zone
|
|
6
|
+
* data instead of from a database this repository would have to ship.
|
|
7
|
+
*
|
|
8
|
+
* It is a separate module and an explicit call because it is the one
|
|
9
|
+
* thing the default may not be. A named zone in `@jarenjs/core/series`
|
|
10
|
+
* is a refusal until a provider is passed, and that refusal is the
|
|
11
|
+
* point: a tzdb goes stale on a government's timetable, so the only
|
|
12
|
+
* honest copy is the host's own, and the host must say so by
|
|
13
|
+
* constructing one. ICU output also moves between Node versions and
|
|
14
|
+
* between a browser and a server - a zone whose rules changed last
|
|
15
|
+
* year answers differently on a host whose ICU predates the change -
|
|
16
|
+
* so a consumer that needs the same instant on every host pins the
|
|
17
|
+
* tzdata its hosts run, not this module.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here runs at module load: the `Intl` objects are built inside
|
|
20
|
+
* the factory, so importing the module allocates nothing and a bundle
|
|
21
|
+
* that never calls it carries no ICU work.
|
|
22
|
+
*
|
|
23
|
+
* The provider never asks the host what zone it is in. Every call names
|
|
24
|
+
* its zone, and there is no default that reads the process's - that is
|
|
25
|
+
* the hidden clock the seam exists to forbid, and a provider that read
|
|
26
|
+
* it would smuggle the non-determinism back in invisibly.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { createBoundedCache } from '@jarenjs/core/cache';
|
|
30
|
+
import { daysFromCivil } from '@jarenjs/core/dates/civil';
|
|
31
|
+
import { epochOfRFC3339Parts } from '@jarenjs/core/dates/rfc3339';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How many zones' formatters stay resident at once. A formatter is
|
|
35
|
+
* expensive to build and cheap to reuse, and a host serving a handful of
|
|
36
|
+
* zones wants each built once; the bound is what keeps a generated or
|
|
37
|
+
* hostile stream of zone names from growing the map without limit.
|
|
38
|
+
*/
|
|
39
|
+
export const ZONE_CACHE_LIMIT = 32;
|
|
40
|
+
|
|
41
|
+
/** What a caller may ask for when a local time is ambiguous or absent. */
|
|
42
|
+
const DISAMBIGUATION = Object.freeze(['reject', 'earlier', 'later']);
|
|
43
|
+
|
|
44
|
+
const MINUTE = 60000;
|
|
45
|
+
const DAY = 86400000;
|
|
46
|
+
|
|
47
|
+
/** The instants a `Date` can hold: beyond it ICU has nothing to read. */
|
|
48
|
+
const RANGE = 8.64e15;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Refuse an instant the platform cannot represent by name, rather than
|
|
52
|
+
* letting ICU's own `Invalid time value` — which names nothing — escape.
|
|
53
|
+
* @param {any} epoch
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
function checkInstant(epoch) {
|
|
57
|
+
if (typeof epoch !== 'number' || !Number.isFinite(epoch) || Math.abs(epoch) > RANGE)
|
|
58
|
+
throw new TypeError(`an instant is a finite number of epoch milliseconds within ±${RANGE}, not ${epoch}`);
|
|
59
|
+
return epoch;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The formatter that reads a wall clock in one zone. The locale is
|
|
64
|
+
* pinned to `en-US` with Latin digits because the output is parsed, not
|
|
65
|
+
* shown; the calendar to `gregory` because the kernel's dates are
|
|
66
|
+
* proleptic Gregorian; the hour cycle to `h23` so midnight reads `00`
|
|
67
|
+
* rather than `24`; and the era is read so a year before 1 comes back
|
|
68
|
+
* as the kernel counts it.
|
|
69
|
+
* @param {string} zone
|
|
70
|
+
* @returns {Intl.DateTimeFormat}
|
|
71
|
+
*/
|
|
72
|
+
function createFormatter(zone) {
|
|
73
|
+
try {
|
|
74
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
75
|
+
timeZone: zone,
|
|
76
|
+
calendar: 'gregory',
|
|
77
|
+
numberingSystem: 'latn',
|
|
78
|
+
hourCycle: 'h23',
|
|
79
|
+
era: 'short',
|
|
80
|
+
year: 'numeric',
|
|
81
|
+
month: 'numeric',
|
|
82
|
+
day: 'numeric',
|
|
83
|
+
hour: 'numeric',
|
|
84
|
+
minute: 'numeric',
|
|
85
|
+
second: 'numeric',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// ICU throws for a name it does not know; a name it would quietly
|
|
90
|
+
// map onto something else is exactly what the caller must not get
|
|
91
|
+
throw new TypeError(`'${zone}' is not a time zone this host's ICU knows`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The wall clock at an instant, and the offset that produced it. The
|
|
97
|
+
* offset is not read from a formatted `GMT+02:00` string but derived:
|
|
98
|
+
* the wall clock re-read as UTC minus the instant is the offset, exact
|
|
99
|
+
* to the second, which is what a historical `-00:44:30` needs.
|
|
100
|
+
* @param {Intl.DateTimeFormat} format
|
|
101
|
+
* @param {number} epoch
|
|
102
|
+
* @returns {{ year: number, month: number, day: number, hours: number,
|
|
103
|
+
* minutes: number, seconds: number, offset: number }}
|
|
104
|
+
*/
|
|
105
|
+
function readParts(format, epoch) {
|
|
106
|
+
let year = 0;
|
|
107
|
+
let month = 0;
|
|
108
|
+
let day = 0;
|
|
109
|
+
let hours = 0;
|
|
110
|
+
let minutes = 0;
|
|
111
|
+
let seconds = 0;
|
|
112
|
+
let beforeChrist = false;
|
|
113
|
+
for (const part of format.formatToParts(epoch)) {
|
|
114
|
+
switch (part.type) {
|
|
115
|
+
case 'year': year = Number(part.value); break;
|
|
116
|
+
case 'month': month = Number(part.value); break;
|
|
117
|
+
case 'day': day = Number(part.value); break;
|
|
118
|
+
case 'hour': hours = Number(part.value); break;
|
|
119
|
+
case 'minute': minutes = Number(part.value); break;
|
|
120
|
+
case 'second': seconds = Number(part.value); break;
|
|
121
|
+
case 'era': beforeChrist = part.value.charCodeAt(0) === 0x42; break; // 'B'
|
|
122
|
+
default: break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (beforeChrist)
|
|
126
|
+
year = 1 - year;
|
|
127
|
+
const wholeSecond = Math.floor(epoch / 1000) * 1000;
|
|
128
|
+
// Use the core calendar for every year, including year zero and BCE.
|
|
129
|
+
const wall = daysFromCivil(year, month, day) * DAY
|
|
130
|
+
+ ((hours * 60 + minutes) * 60 + seconds) * 1000;
|
|
131
|
+
const offset = (wall - wholeSecond) / MINUTE;
|
|
132
|
+
return { year, month, day, hours, minutes, seconds: seconds + (epoch - wholeSecond) / 1000, offset };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The wall clock a caller asked for, in the kernel's shape: an absent or
|
|
137
|
+
* negative time member is midnight's, as `epochOfRFC3339Parts` reads a
|
|
138
|
+
* negative one.
|
|
139
|
+
* @param {any} parts
|
|
140
|
+
* @returns {{ year: number, month: number, day: number, hours: number,
|
|
141
|
+
* minutes: number, seconds: number }}
|
|
142
|
+
*/
|
|
143
|
+
function wantedWall(parts) {
|
|
144
|
+
const clamp = (value) => (typeof value === 'number' && value > 0 ? value : 0);
|
|
145
|
+
return {
|
|
146
|
+
year: parts.year,
|
|
147
|
+
month: parts.month,
|
|
148
|
+
day: parts.day,
|
|
149
|
+
hours: clamp(parts.hours),
|
|
150
|
+
minutes: clamp(parts.minutes),
|
|
151
|
+
seconds: clamp(parts.seconds),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether a wall clock the zone reports is the one that was asked for.
|
|
157
|
+
* @param {ReturnType<typeof readParts>} read
|
|
158
|
+
* @param {ReturnType<typeof wantedWall>} wanted
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
function sameWall(read, wanted) {
|
|
162
|
+
return read.year === wanted.year && read.month === wanted.month && read.day === wanted.day
|
|
163
|
+
&& read.hours === wanted.hours && read.minutes === wanted.minutes
|
|
164
|
+
&& Math.floor(read.seconds) === Math.floor(wanted.seconds);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build a zone provider from the host's ICU time-zone data.
|
|
169
|
+
*
|
|
170
|
+
* `toParts(epoch, zone)` is a formatted read. `toEpoch(parts, zone,
|
|
171
|
+
* disambiguation)` inverts a function that is neither injective nor
|
|
172
|
+
* total: it guesses an instant by reading the wall clock as UTC,
|
|
173
|
+
* corrects it by the offset in force there and again by the offset in
|
|
174
|
+
* force at the corrected instant, adds the offsets in force a day either
|
|
175
|
+
* side so both sides of any transition are represented, and then
|
|
176
|
+
* verifies every distinct candidate by reading its wall clock back.
|
|
177
|
+
* Exactly one candidate that reads back as asked is the instant. Two
|
|
178
|
+
* are a fold - the hour that happened twice - and `earlier` and `later`
|
|
179
|
+
* are the smaller and the larger. None is a gap - the hour that never
|
|
180
|
+
* happened - and `earlier` is the wall clock read with the offset in
|
|
181
|
+
* force after the transition (the instant that far before the gap),
|
|
182
|
+
* `later` the reading with the offset before it (that far after); the
|
|
183
|
+
* two are one transition apart. `reject`, the kernel's default, answers
|
|
184
|
+
* either case with `NaN`, which `@jarenjs/core/series` turns into a
|
|
185
|
+
* refusal naming the local time.
|
|
186
|
+
*
|
|
187
|
+
* An unknown zone name is a refusal naming it, never a quiet UTC, and
|
|
188
|
+
* no call reads the host's own zone.
|
|
189
|
+
*
|
|
190
|
+
* @param {{ zones?: number }} [options] - `zones`: how many zones'
|
|
191
|
+
* formatters stay resident (default `ZONE_CACHE_LIMIT`)
|
|
192
|
+
* @returns {import('@jarenjs/core/series').ZoneProvider}
|
|
193
|
+
* @example
|
|
194
|
+
* const provider = createIntlZoneProvider();
|
|
195
|
+
* resolveClock({ zone: 'Europe/Amsterdam', provider, disambiguation: 'later' });
|
|
196
|
+
* compileBuckets('P1M', { zone: 'Australia/Adelaide', provider });
|
|
197
|
+
*/
|
|
198
|
+
export function createIntlZoneProvider(options = undefined) {
|
|
199
|
+
const limit = options?.zones ?? ZONE_CACHE_LIMIT;
|
|
200
|
+
if (typeof limit !== 'number' || !Number.isInteger(limit) || limit <= 0)
|
|
201
|
+
throw new TypeError('zones is a positive whole number of resident zone formatters');
|
|
202
|
+
/** @type {import('@jarenjs/core/cache').BoundedCache<string, Intl.DateTimeFormat>} */
|
|
203
|
+
const formatters = createBoundedCache(limit);
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} zone
|
|
207
|
+
* @returns {Intl.DateTimeFormat}
|
|
208
|
+
*/
|
|
209
|
+
const formatterFor = (zone) => {
|
|
210
|
+
if (typeof zone !== 'string' || zone === '')
|
|
211
|
+
throw new TypeError('a zone is an IANA name, given explicitly');
|
|
212
|
+
return formatters.getOrCreate(zone, createFormatter);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {number} epoch
|
|
217
|
+
* @param {string} zone
|
|
218
|
+
*/
|
|
219
|
+
function toParts(epoch, zone) {
|
|
220
|
+
const format = formatterFor(zone);
|
|
221
|
+
return readParts(format, checkInstant(epoch));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {any} parts
|
|
226
|
+
* @param {string} zone
|
|
227
|
+
* @param {string} [disambiguation]
|
|
228
|
+
* @returns {number}
|
|
229
|
+
*/
|
|
230
|
+
function toEpoch(parts, zone, disambiguation = 'reject') {
|
|
231
|
+
const format = formatterFor(zone);
|
|
232
|
+
if (!DISAMBIGUATION.includes(disambiguation)) {
|
|
233
|
+
throw new TypeError(`disambiguation is ${
|
|
234
|
+
DISAMBIGUATION.map((d) => `'${d}'`).join(', ')}, not '${disambiguation}'`);
|
|
235
|
+
}
|
|
236
|
+
if (parts === null || typeof parts !== 'object')
|
|
237
|
+
throw new TypeError('a wall clock is a parts record');
|
|
238
|
+
const wanted = wantedWall(parts);
|
|
239
|
+
const guess = epochOfRFC3339Parts({ ...wanted, offset: 0 });
|
|
240
|
+
// a wall clock the platform cannot hold, or one so near its edge
|
|
241
|
+
// that a probe a day away would fall off it, names no instant
|
|
242
|
+
if (!Number.isFinite(guess) || Math.abs(guess) + DAY > RANGE)
|
|
243
|
+
return NaN;
|
|
244
|
+
const offsetAt = (at) => readParts(format, at).offset;
|
|
245
|
+
|
|
246
|
+
/** @type {number[]} */
|
|
247
|
+
const candidates = [];
|
|
248
|
+
const consider = (offset) => {
|
|
249
|
+
const at = guess - offset * MINUTE;
|
|
250
|
+
if (!candidates.includes(at))
|
|
251
|
+
candidates.push(at);
|
|
252
|
+
};
|
|
253
|
+
const first = offsetAt(guess);
|
|
254
|
+
consider(first);
|
|
255
|
+
consider(offsetAt(guess - first * MINUTE));
|
|
256
|
+
consider(offsetAt(guess - DAY));
|
|
257
|
+
consider(offsetAt(guess + DAY));
|
|
258
|
+
|
|
259
|
+
const verified = candidates.filter((at) => sameWall(readParts(format, at), wanted));
|
|
260
|
+
if (verified.length === 1)
|
|
261
|
+
return verified[0];
|
|
262
|
+
if (disambiguation === 'reject')
|
|
263
|
+
return NaN;
|
|
264
|
+
const choices = verified.length > 1 ? verified : candidates;
|
|
265
|
+
if (choices.length < 2)
|
|
266
|
+
return NaN;
|
|
267
|
+
return disambiguation === 'earlier' ? Math.min(...choices) : Math.max(...choices);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return Object.freeze({ toParts, toEpoch });
|
|
271
|
+
}
|
package/src/ja.js
CHANGED
|
@@ -162,6 +162,8 @@ export const ja = {
|
|
|
162
162
|
'contract/seq-regression': '操作 {op} のストリームが seq の順序に違反しました',
|
|
163
163
|
'contract/stream-error': '操作 {op} のストリームはサーバーエラーで終了しました ({code})',
|
|
164
164
|
'contract/heartbeat-missed': '操作 {op} のストリームが {ms} ミリ秒間沈黙しました',
|
|
165
|
+
'contract/slow-consumer': '操作 {op} のストリームは終了しました: 消費側が有界キューに追いつけませんでした',
|
|
166
|
+
'contract/reconnect-exhausted': '操作 {op} のストリームは {attempts} 回の試行後も再確立できませんでした(最後: {lastCode})',
|
|
165
167
|
//#endregion
|
|
166
168
|
|
|
167
169
|
//#region calendar language (the date names, relative phrasing and
|
package/src/ko.js
CHANGED
|
@@ -165,6 +165,8 @@ export const ko = {
|
|
|
165
165
|
'contract/seq-regression': '작업 {op}의 스트림이 seq 순서를 위반했습니다',
|
|
166
166
|
'contract/stream-error': '작업 {op}의 스트림이 서버 오류로 종료되었습니다 ({code})',
|
|
167
167
|
'contract/heartbeat-missed': '작업 {op}의 스트림이 {ms}ms 동안 침묵했습니다',
|
|
168
|
+
'contract/slow-consumer': '작업 {op}의 스트림이 종료되었습니다: 소비자가 제한된 큐를 따라가지 못했습니다',
|
|
169
|
+
'contract/reconnect-exhausted': '작업 {op}의 스트림을 {attempts}회 시도 후에도 다시 연결하지 못했습니다 (마지막: {lastCode})',
|
|
168
170
|
//#endregion
|
|
169
171
|
|
|
170
172
|
//#region calendar language (the date names, relative phrasing and
|
package/src/nl.js
CHANGED
|
@@ -166,6 +166,8 @@ export const nl = {
|
|
|
166
166
|
'contract/seq-regression': 'de stream van operatie {op} heeft de seq-volgorde geschonden',
|
|
167
167
|
'contract/stream-error': 'de stream van operatie {op} is geëindigd met een serverfout ({code})',
|
|
168
168
|
'contract/heartbeat-missed': 'de stream van operatie {op} is {ms} ms stil gebleven',
|
|
169
|
+
'contract/slow-consumer': 'de stream van operatie {op} is geëindigd: de afnemer bleef achter bij zijn begrensde wachtrij',
|
|
170
|
+
'contract/reconnect-exhausted': 'de stream van operatie {op} kon na {attempts} pogingen niet worden hersteld (laatste: {lastCode})',
|
|
169
171
|
//#endregion
|
|
170
172
|
|
|
171
173
|
//#region calendar language (the date names, relative phrasing and
|
package/src/pt.js
CHANGED
|
@@ -167,6 +167,8 @@ export const pt = {
|
|
|
167
167
|
'contract/seq-regression': 'o fluxo da operação {op} violou a ordem de seus seq',
|
|
168
168
|
'contract/stream-error': 'o fluxo da operação {op} terminou com um erro do servidor ({code})',
|
|
169
169
|
'contract/heartbeat-missed': 'o fluxo da operação {op} ficou em silêncio por {ms} ms',
|
|
170
|
+
'contract/slow-consumer': 'o fluxo da operação {op} terminou: o consumidor ficou atrás da sua fila limitada',
|
|
171
|
+
'contract/reconnect-exhausted': 'o fluxo da operação {op} não pôde ser restabelecido após {attempts} tentativas (última: {lastCode})',
|
|
170
172
|
//#endregion
|
|
171
173
|
|
|
172
174
|
//#region calendar language (the date names, relative phrasing and
|
package/src/ru.js
CHANGED
|
@@ -185,6 +185,8 @@ export const ru = {
|
|
|
185
185
|
'contract/seq-regression': 'поток операции {op} нарушил порядок своих seq',
|
|
186
186
|
'contract/stream-error': 'поток операции {op} завершился ошибкой сервера ({code})',
|
|
187
187
|
'contract/heartbeat-missed': 'поток операции {op} молчал {ms} мс',
|
|
188
|
+
'contract/slow-consumer': 'поток операции {op} завершён: потребитель отстал от своей ограниченной очереди',
|
|
189
|
+
'contract/reconnect-exhausted': 'поток операции {op} не удалось восстановить после {attempts} попыток (последняя: {lastCode})',
|
|
188
190
|
//#endregion
|
|
189
191
|
|
|
190
192
|
//#region calendar language (the date names, relative phrasing and
|
package/src/tr.js
CHANGED
|
@@ -164,6 +164,8 @@ export const tr = {
|
|
|
164
164
|
'contract/seq-regression': '{op} işleminin akışı seq sırasını ihlal etti',
|
|
165
165
|
'contract/stream-error': '{op} işleminin akışı bir sunucu hatasıyla sona erdi ({code})',
|
|
166
166
|
'contract/heartbeat-missed': '{op} işleminin akışı {ms} ms boyunca sessiz kaldı',
|
|
167
|
+
'contract/slow-consumer': '{op} işleminin akışı sona erdi: tüketici sınırlı kuyruğunun gerisinde kaldı',
|
|
168
|
+
'contract/reconnect-exhausted': '{op} işleminin akışı {attempts} denemeden sonra yeniden kurulamadı (son: {lastCode})',
|
|
167
169
|
//#endregion
|
|
168
170
|
|
|
169
171
|
//#region calendar language (the date names, relative phrasing and
|
package/src/zh-tw.js
CHANGED
|
@@ -165,6 +165,8 @@ export const zhTW = {
|
|
|
165
165
|
'contract/seq-regression': '操作 {op} 的串流違反了其 seq 順序',
|
|
166
166
|
'contract/stream-error': '操作 {op} 的串流以伺服器錯誤結束({code})',
|
|
167
167
|
'contract/heartbeat-missed': '操作 {op} 的串流沉默了 {ms} 毫秒',
|
|
168
|
+
'contract/slow-consumer': '操作 {op} 的串流已結束:消費端落後於其有界佇列',
|
|
169
|
+
'contract/reconnect-exhausted': '操作 {op} 的串流在 {attempts} 次嘗試後仍無法重新建立(最後:{lastCode})',
|
|
168
170
|
//#endregion
|
|
169
171
|
|
|
170
172
|
//#region calendar language (the date names, relative phrasing and
|