chronolizer 0.3.0 → 0.4.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 +50 -47
- package/dist/ast/fold.mjs +8 -10
- package/dist/ast/normalize.mjs +7 -16
- package/dist/filter/codec.mjs +5 -6
- package/dist/filter/expression.mjs +26 -21
- package/dist/language/model.mjs +1 -1
- package/dist/language/registry.mjs +34 -12
- package/dist/locales/cs.mjs +194 -36
- package/dist/locales/de.mjs +307 -490
- package/dist/locales/en.mjs +162 -46
- package/dist/locales/es.mjs +187 -37
- package/dist/locales/fr.mjs +168 -52
- package/dist/locales/nl.mjs +125 -37
- package/dist/locales/pl.mjs +163 -36
- package/dist/locales/shared.mjs +68 -5
- package/dist/locales/tr.mjs +144 -42
- package/dist/natural/correction.d.mts +1 -1
- package/dist/natural/correction.mjs +97 -28
- package/dist/natural/format.mjs +1 -1
- package/dist/natural/parse.mjs +6 -5
- package/dist/natural/suggest.mjs +27 -3
- package/dist/natural/suggestion.mjs +50 -26
- package/dist/resolve/resolve.mjs +2 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Chronolizer
|
|
2
2
|
|
|
3
|
-
Chronolizer is an Effect 4 library that converts natural-language date ranges to compact date
|
|
3
|
+
Chronolizer is an Effect 4 library that converts natural-language date ranges to compact date filters and back. It supports ranges without a start or end, autocomplete, spelling correction, and eight languages.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
year to date → { gte: "now/y", lte: "now" }
|
|
@@ -9,11 +9,11 @@ since January 2025 → { gte: "2025-01-01" }
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
> [!NOTE]
|
|
12
|
-
> Chronolizer currently uses `effect@4.0.0-rc.112`.
|
|
12
|
+
> Chronolizer currently uses `effect@4.0.0-rc.112`. Effect 4 is not stable yet, so its API can change.
|
|
13
13
|
|
|
14
14
|
## Install Chronolizer
|
|
15
15
|
|
|
16
|
-
Chronolizer requires Effect 4 and
|
|
16
|
+
Chronolizer requires Effect 4 and uses ES modules. Install both packages:
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
pnpm add chronolizer effect@4.0.0-rc.112
|
|
@@ -21,7 +21,7 @@ pnpm add chronolizer effect@4.0.0-rc.112
|
|
|
21
21
|
|
|
22
22
|
## Parse your first date range
|
|
23
23
|
|
|
24
|
-
English is available from the main package
|
|
24
|
+
English is available from the main package.
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
import { EnglishLanguageLayer, formatFilter, parseNatural } from "chronolizer";
|
|
@@ -43,7 +43,7 @@ Effect.runPromise(program);
|
|
|
43
43
|
|
|
44
44
|
### Parse other languages
|
|
45
45
|
|
|
46
|
-
Import each non-English language
|
|
46
|
+
Import each non-English language separately. This keeps unused language code out of your build.
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
49
|
import { formatFilter, parseNatural } from "chronolizer";
|
|
@@ -60,7 +60,7 @@ Effect.runPromise(program);
|
|
|
60
60
|
// { gte: "now/y", lte: "now" }
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Use `languagePluginsLayer`
|
|
63
|
+
Use `languagePluginsLayer` to combine languages:
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
66
|
import { EnglishLanguage, languagePluginsLayer, parseNatural } from "chronolizer";
|
|
@@ -77,7 +77,7 @@ Effect.runPromise(program);
|
|
|
77
77
|
|
|
78
78
|
### Format a range as natural language
|
|
79
79
|
|
|
80
|
-
`formatNatural` returns
|
|
80
|
+
`formatNatural` returns one standard phrase for a range. It preserves the meaning, but it does not reproduce the original wording. Absolute days use the language's numeric `Intl.DateTimeFormat` form.
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
83
|
import { formatNatural, parseFilter } from "chronolizer";
|
|
@@ -96,7 +96,7 @@ Effect.runPromise(program);
|
|
|
96
96
|
|
|
97
97
|
### Add autocomplete
|
|
98
98
|
|
|
99
|
-
`suggestNatural` returns valid
|
|
99
|
+
`suggestNatural` returns valid standard phrases and their date ranges.
|
|
100
100
|
|
|
101
101
|
```ts
|
|
102
102
|
import { EnglishLanguageLayer, suggestNatural } from "chronolizer";
|
|
@@ -115,7 +115,7 @@ The default limit is 10. The maximum limit is 100. A nonpositive or invalid limi
|
|
|
115
115
|
|
|
116
116
|
### Accept spelling errors
|
|
117
117
|
|
|
118
|
-
Set `typoMode` to `"tolerant"`
|
|
118
|
+
Set `typoMode` to `"tolerant"` to correct close spelling errors:
|
|
119
119
|
|
|
120
120
|
```ts
|
|
121
121
|
const program = parseNatural("januray of last yaer", {
|
|
@@ -124,11 +124,11 @@ const program = parseNatural("januray of last yaer", {
|
|
|
124
124
|
});
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
The
|
|
127
|
+
The result contains its `quality`, applied `corrections`, and any `alternatives` with a different meaning. Strict mode is the default and does not correct the input.
|
|
128
128
|
|
|
129
|
-
### Exclude future
|
|
129
|
+
### Exclude future ranges
|
|
130
130
|
|
|
131
|
-
Set `allowFuture` to `false` to reject
|
|
131
|
+
Set `allowFuture` to `false` to reject periods after the current period:
|
|
132
132
|
|
|
133
133
|
```ts
|
|
134
134
|
const program = parseNatural("next 3 months", {
|
|
@@ -137,7 +137,7 @@ const program = parseNatural("next 3 months", {
|
|
|
137
137
|
});
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
This option rejects forms such as `next month`, `next 3 weeks`, and `in 3 years`. It keeps complete current calendar periods such as `today`, `this week`, and `this month`. It does not change them to
|
|
140
|
+
This option rejects forms such as `next month`, `next 3 weeks`, and `in 3 years`. It keeps complete current calendar periods such as `today`, `this week`, and `this month`. It does not change them to ranges that end at the current time.
|
|
141
141
|
|
|
142
142
|
The option does not compare fixed dates, such as `January 2099`, with the current date. Parsing does not read the clock.
|
|
143
143
|
|
|
@@ -145,7 +145,7 @@ The same option is available in `suggestNatural`.
|
|
|
145
145
|
|
|
146
146
|
### Resolve a range to dates
|
|
147
147
|
|
|
148
|
-
Provide an Effect time zone when you
|
|
148
|
+
Provide an Effect time zone when you calculate ranges based on the current date:
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
151
|
import { parseFilter, resolve } from "chronolizer";
|
|
@@ -159,7 +159,7 @@ const program = parseFilter({ gte: "now/y", lte: "now" }).pipe(
|
|
|
159
159
|
Effect.runPromise(program);
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
`resolve` uses the Effect clock and `DateTime.CurrentTimeZone`. It
|
|
162
|
+
`resolve` uses the Effect clock and `DateTime.CurrentTimeZone`. It uses only the time zone that you provide.
|
|
163
163
|
|
|
164
164
|
### Validate an external filter
|
|
165
165
|
|
|
@@ -176,18 +176,19 @@ const program = Schema.decodeUnknownEffect(DateFilter)(externalInput).pipe(
|
|
|
176
176
|
Effect.runPromise(program);
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
## Supported
|
|
179
|
+
## Supported input
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
You can parse:
|
|
182
182
|
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
- period
|
|
187
|
-
- fixed months, quarters, years, and
|
|
183
|
+
- days, weeks, months, quarters, and years;
|
|
184
|
+
- ranges with a length, such as `last 3 months`;
|
|
185
|
+
- single periods in the past or future, such as `30 months ago`;
|
|
186
|
+
- ranges from a period start to now, such as `year to date`;
|
|
187
|
+
- fixed months, quarters, years, and date ranges;
|
|
188
188
|
- named days with or without a year, such as `January 12` and `12th of January`;
|
|
189
|
-
-
|
|
190
|
-
-
|
|
189
|
+
- ranges without one end, such as `since January 2025` and `from January 12`;
|
|
190
|
+
- written counts from one through ninety-nine, such as `two weeks ago` and `twenty-one weeks ago`;
|
|
191
|
+
- combined phrases, such as `the day before January 12` and `yesterday two weeks ago`;
|
|
191
192
|
- period starts, period ends, and weekends;
|
|
192
193
|
- abbreviated month names and common equivalent phrases.
|
|
193
194
|
|
|
@@ -206,16 +207,16 @@ Examples:
|
|
|
206
207
|
|
|
207
208
|
## Supported languages
|
|
208
209
|
|
|
209
|
-
| Language |
|
|
210
|
-
| -------- |
|
|
211
|
-
| English | `en`
|
|
212
|
-
| German | `de`
|
|
213
|
-
| Spanish | `es`
|
|
214
|
-
| French | `fr`
|
|
215
|
-
| Dutch | `nl`
|
|
216
|
-
| Turkish | `tr`
|
|
217
|
-
| Czech | `cs`
|
|
218
|
-
| Polish | `pl`
|
|
210
|
+
| Language | Code | Import |
|
|
211
|
+
| -------- | ---- | ----------------------------------------- |
|
|
212
|
+
| English | `en` | `chronolizer` or `chronolizer/locales/en` |
|
|
213
|
+
| German | `de` | `chronolizer/locales/de` |
|
|
214
|
+
| Spanish | `es` | `chronolizer/locales/es` |
|
|
215
|
+
| French | `fr` | `chronolizer/locales/fr` |
|
|
216
|
+
| Dutch | `nl` | `chronolizer/locales/nl` |
|
|
217
|
+
| Turkish | `tr` | `chronolizer/locales/tr` |
|
|
218
|
+
| Czech | `cs` | `chronolizer/locales/cs` |
|
|
219
|
+
| Polish | `pl` | `chronolizer/locales/pl` |
|
|
219
220
|
|
|
220
221
|
## Date filter reference
|
|
221
222
|
|
|
@@ -229,32 +230,34 @@ A date filter has at least one bound. It can have one lower bound and one upper
|
|
|
229
230
|
| `lte` | On or before the value |
|
|
230
231
|
|
|
231
232
|
An expression starts with `now` or an ISO date. Operations run from left to right.
|
|
233
|
+
Calendar shifts are not combined or canceled. For example, `2025-01-31||+1M+1M`
|
|
234
|
+
resolves to March 28, not March 31. Each month shift clamps the day to the last
|
|
235
|
+
valid day of that month. Time-zone gaps can also affect intermediate dates.
|
|
232
236
|
|
|
233
237
|
```text
|
|
234
|
-
expression :=
|
|
235
|
-
anchor := "now" | YYYY-MM-DD | YYYY-MM-DD "||"
|
|
238
|
+
expression := "now" operation* | YYYY-MM-DD | YYYY-MM-DD "||" operation+
|
|
236
239
|
operation := ("+" | "-") positiveInteger unit | "/" unit
|
|
237
240
|
unit := "d" | "w" | "M" | "q" | "y"
|
|
238
241
|
```
|
|
239
242
|
|
|
240
243
|
`/unit` moves a value to the start of its calendar unit. Add `||` before operations on a fixed date, for example `2025-01-01||+1M`.
|
|
241
244
|
|
|
242
|
-
|
|
245
|
+
A complete calendar period includes its start but excludes the start of the next period. Weeks start on Monday.
|
|
243
246
|
|
|
244
247
|
A named day without a year uses the current calendar year. Write the year for February 29 because parsing does not read the clock.
|
|
245
248
|
|
|
246
249
|
## Main API
|
|
247
250
|
|
|
248
|
-
| Export | Purpose
|
|
249
|
-
| --------------------- |
|
|
250
|
-
| `parseNatural` | Parse complete natural-language input to a
|
|
251
|
-
| `formatNatural` |
|
|
252
|
-
| `suggestNatural` | Return autocomplete suggestions and
|
|
253
|
-
| `parseFilter` | Parse a date filter to a
|
|
254
|
-
| `formatFilter` | Format a
|
|
255
|
-
| `resolve` |
|
|
256
|
-
| `DateFilter` | Validate external date-filter data with Effect Schema
|
|
257
|
-
| `DateRangeFromFilter` |
|
|
251
|
+
| Export | Purpose |
|
|
252
|
+
| --------------------- | ---------------------------------------------------------- |
|
|
253
|
+
| `parseNatural` | Parse complete natural-language input to a date range |
|
|
254
|
+
| `formatNatural` | Write a supported range as one standard phrase |
|
|
255
|
+
| `suggestNatural` | Return autocomplete suggestions and date ranges |
|
|
256
|
+
| `parseFilter` | Parse a date filter to a date range |
|
|
257
|
+
| `formatFilter` | Format a date range as a date filter |
|
|
258
|
+
| `resolve` | Calculate dates with an Effect clock and time zone |
|
|
259
|
+
| `DateFilter` | Validate external date-filter data with Effect Schema |
|
|
260
|
+
| `DateRangeFromFilter` | Convert a filter in both directions with one Effect Schema |
|
|
258
261
|
|
|
259
262
|
## License
|
|
260
263
|
|
package/dist/ast/fold.mjs
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Match, Schema
|
|
1
|
+
import { GreaterThanOrEqual, LessThan, Now, Shift, StartOf } from "./schemas.mjs";
|
|
2
|
+
import { Match, Schema } from "effect";
|
|
3
3
|
//#region src/ast/fold.ts
|
|
4
|
-
const isDateLiteral = Schema.is(DateLiteral);
|
|
5
4
|
const isGreaterThanOrEqual = Schema.is(GreaterThanOrEqual);
|
|
6
5
|
const isLessThan = Schema.is(LessThan);
|
|
7
6
|
const isNow = Schema.is(Now);
|
|
8
7
|
const isShift = Schema.is(Shift);
|
|
9
8
|
const isStartOf = Schema.is(StartOf);
|
|
10
|
-
const foldInstant = (expression, algebra) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
9
|
+
const foldInstant = (expression, algebra) => Match.typeTags()({
|
|
10
|
+
Now: () => algebra.now(),
|
|
11
|
+
DateLiteral: (literal) => algebra.dateLiteral(literal.value),
|
|
12
|
+
Shift: (operation) => algebra.shift(foldInstant(operation.base, algebra), operation.amount, operation.unit),
|
|
13
|
+
StartOf: (operation) => algebra.startOf(foldInstant(operation.base, algebra), operation.unit)
|
|
14
|
+
})(expression);
|
|
17
15
|
const shiftOffset = (base, amount, unit) => Match.value(unit).pipe(Match.when("year", () => ({
|
|
18
16
|
...base,
|
|
19
17
|
months: base.months + amount * 12
|
package/dist/ast/normalize.mjs
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
import { Shift } from "./schemas.mjs";
|
|
2
1
|
import { boundedRange, dateLiteral, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, now, shift, startOf, upperOpenRange } from "./constructors.mjs";
|
|
3
|
-
import {
|
|
2
|
+
import { foldInstant } from "./fold.mjs";
|
|
3
|
+
import { Match } from "effect";
|
|
4
4
|
//#region src/ast/normalize.ts
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Shift: (operation) => {
|
|
11
|
-
const base = normalizeInstant(operation.base);
|
|
12
|
-
if (operation.amount === 0) return base;
|
|
13
|
-
if (isShift(base) && base.unit === operation.unit) {
|
|
14
|
-
const amount = base.amount + operation.amount;
|
|
15
|
-
if (Number.isSafeInteger(amount)) return normalizeInstant(shift(base.base, amount, operation.unit));
|
|
16
|
-
}
|
|
17
|
-
return shift(base, operation.amount, operation.unit);
|
|
18
|
-
}
|
|
5
|
+
const normalizeInstant = (expression) => foldInstant(expression, {
|
|
6
|
+
now,
|
|
7
|
+
dateLiteral,
|
|
8
|
+
startOf,
|
|
9
|
+
shift: (base, amount, unit) => amount === 0 ? base : shift(base, amount, unit)
|
|
19
10
|
});
|
|
20
11
|
const normalizeLower = (bound) => Match.valueTags(bound, {
|
|
21
12
|
GreaterThan: (value) => greaterThan(normalizeInstant(value.value)),
|
package/dist/filter/codec.mjs
CHANGED
|
@@ -31,13 +31,12 @@ const formatUpper = Match.typeTags()({
|
|
|
31
31
|
LessThanOrEqual: (bound) => DateFilter.make({ lte: formatInstantExpression(bound.value) })
|
|
32
32
|
});
|
|
33
33
|
const formatFilter = (range) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
...
|
|
37
|
-
...formatUpper(normalized.upper)
|
|
34
|
+
if (range.lower !== void 0 && range.upper !== void 0) return DateFilter.make({
|
|
35
|
+
...formatLower(range.lower),
|
|
36
|
+
...formatUpper(range.upper)
|
|
38
37
|
});
|
|
39
|
-
if (
|
|
40
|
-
return formatUpper(
|
|
38
|
+
if (range.lower !== void 0) return formatLower(range.lower);
|
|
39
|
+
return formatUpper(range.upper);
|
|
41
40
|
};
|
|
42
41
|
const rangeKey = (range) => {
|
|
43
42
|
const filter = formatFilter(range);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isIsoDate } from "../ast/schemas.mjs";
|
|
2
2
|
import { dateLiteral, now, shift, startOf } from "../ast/constructors.mjs";
|
|
3
3
|
import { foldInstant } from "../ast/fold.mjs";
|
|
4
|
-
import { normalizeInstant } from "../ast/normalize.mjs";
|
|
5
4
|
import { FilterExpressionParseError } from "./errors.mjs";
|
|
6
5
|
import { Effect, Match } from "effect";
|
|
7
6
|
//#region src/filter/expression.ts
|
|
@@ -14,25 +13,31 @@ const failAt = (input, offset, expected) => Effect.fail(new FilterExpressionPars
|
|
|
14
13
|
}));
|
|
15
14
|
const isDigit = (value) => value >= "0" && value <= "9";
|
|
16
15
|
const isFirstPositiveDigit = (value) => value >= "1" && value <= "9";
|
|
16
|
+
const parseAnchor = Effect.fn(function* (input) {
|
|
17
|
+
if (input.startsWith("now")) return {
|
|
18
|
+
expression: now(),
|
|
19
|
+
cursor: 3,
|
|
20
|
+
fixed: false
|
|
21
|
+
};
|
|
22
|
+
const candidate = input.slice(0, 10);
|
|
23
|
+
if (!isIsoDate(candidate)) return yield* failAt(input, 0, "\"now\" or an ISO date (YYYY-MM-DD)");
|
|
24
|
+
return {
|
|
25
|
+
expression: dateLiteral(candidate),
|
|
26
|
+
cursor: 10,
|
|
27
|
+
fixed: true
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
const operationStart = Effect.fn(function* (input, anchor) {
|
|
31
|
+
if (!anchor.fixed || anchor.cursor === input.length) return anchor.cursor;
|
|
32
|
+
if (input.slice(anchor.cursor, anchor.cursor + 2) !== "||") return yield* failAt(input, anchor.cursor, "\"||\" before date operations");
|
|
33
|
+
const cursor = anchor.cursor + 2;
|
|
34
|
+
if (cursor === input.length) return yield* failAt(input, cursor, "an operation beginning with \"+\", \"-\", or \"/\"");
|
|
35
|
+
return cursor;
|
|
36
|
+
});
|
|
17
37
|
const parseInstantExpression = Effect.fn(function* (input) {
|
|
18
|
-
|
|
19
|
-
let expression;
|
|
20
|
-
let
|
|
21
|
-
if (input.startsWith("now")) {
|
|
22
|
-
expression = now();
|
|
23
|
-
cursor = 3;
|
|
24
|
-
} else {
|
|
25
|
-
const candidate = input.slice(0, 10);
|
|
26
|
-
if (!isIsoDate(candidate)) return yield* failAt(input, 0, "\"now\" or an ISO date (YYYY-MM-DD)");
|
|
27
|
-
expression = dateLiteral(candidate);
|
|
28
|
-
cursor = 10;
|
|
29
|
-
fixedAnchor = true;
|
|
30
|
-
}
|
|
31
|
-
if (fixedAnchor && cursor < input.length) {
|
|
32
|
-
if (input.slice(cursor, cursor + 2) !== "||") return yield* failAt(input, cursor, "\"||\" before date operations");
|
|
33
|
-
cursor += 2;
|
|
34
|
-
if (cursor === input.length) return yield* failAt(input, cursor, "an operation beginning with \"+\", \"-\", or \"/\"");
|
|
35
|
-
}
|
|
38
|
+
const anchor = yield* parseAnchor(input);
|
|
39
|
+
let expression = anchor.expression;
|
|
40
|
+
let cursor = yield* operationStart(input, anchor);
|
|
36
41
|
while (cursor < input.length) {
|
|
37
42
|
const operator = input[cursor];
|
|
38
43
|
if (operator === "/") {
|
|
@@ -60,7 +65,7 @@ const appendOperation = (base, operation) => ({
|
|
|
60
65
|
text: `${base.text}${base.fixedAnchor ? "||" : ""}${operation}`,
|
|
61
66
|
fixedAnchor: false
|
|
62
67
|
});
|
|
63
|
-
const formatInstantExpression = (expression) => foldInstant(
|
|
68
|
+
const formatInstantExpression = (expression) => foldInstant(expression, {
|
|
64
69
|
now: () => ({
|
|
65
70
|
text: "now",
|
|
66
71
|
fixedAnchor: false
|
|
@@ -69,7 +74,7 @@ const formatInstantExpression = (expression) => foldInstant(normalizeInstant(exp
|
|
|
69
74
|
text: value,
|
|
70
75
|
fixedAnchor: true
|
|
71
76
|
}),
|
|
72
|
-
shift: (base, amount, unit) => appendOperation(base, `${amount < 0 ? "-" : "+"}${Math.abs(amount)}${symbolFromUnit(unit)}`),
|
|
77
|
+
shift: (base, amount, unit) => amount === 0 ? base : appendOperation(base, `${amount < 0 ? "-" : "+"}${Math.abs(amount)}${symbolFromUnit(unit)}`),
|
|
73
78
|
startOf: (base, unit) => appendOperation(base, `/${symbolFromUnit(unit)}`)
|
|
74
79
|
}).text;
|
|
75
80
|
//#endregion
|
package/dist/language/model.mjs
CHANGED
|
@@ -35,7 +35,7 @@ const NaturalSuggestion = Schema.Struct({
|
|
|
35
35
|
var BaseLanguageContribution = class extends Data.TaggedClass("BaseLanguage") {};
|
|
36
36
|
var LanguageExtensionContribution = class extends Data.TaggedClass("LanguageExtension") {};
|
|
37
37
|
const canonicalBaseLocale = (input) => Result.getSuccess(Result.try(() => new Intl.Locale(input).baseName));
|
|
38
|
-
const Locale = Schema.String.check(Schema.makeFilter((value) => Option.contains(canonicalBaseLocale(value), value), { expected: "a
|
|
38
|
+
const Locale = Schema.String.check(Schema.makeFilter((value) => Option.contains(canonicalBaseLocale(value), value), { expected: "a standard BCP 47 language code" })).annotate({ identifier: "Locale" });
|
|
39
39
|
const BaseLanguageMetadata = Schema.TaggedStruct("BaseLanguage", {
|
|
40
40
|
locale: Locale,
|
|
41
41
|
vocabulary: Schema.Array(Schema.String)
|
|
@@ -45,28 +45,34 @@ const compileLanguage = (locale, registered) => {
|
|
|
45
45
|
}));
|
|
46
46
|
};
|
|
47
47
|
const createRegistry = Effect.fn(function* () {
|
|
48
|
-
const
|
|
48
|
+
const state = yield* Ref.make({
|
|
49
|
+
entries: [],
|
|
50
|
+
compiledLanguages: /* @__PURE__ */ new Map()
|
|
51
|
+
});
|
|
49
52
|
const register = Effect.fn(function* (pluginId, contribution) {
|
|
50
53
|
if (pluginId.length === 0 || !Schema.is(LanguageContributionMetadata)(contribution)) return yield* new LanguageRegistrationError({
|
|
51
54
|
pluginId,
|
|
52
55
|
locale: contribution.locale,
|
|
53
|
-
message: "
|
|
56
|
+
message: "The plugin name or language settings are invalid"
|
|
54
57
|
});
|
|
55
58
|
const token = Symbol(pluginId);
|
|
56
|
-
yield* Effect.acquireRelease(Ref.modify(
|
|
59
|
+
yield* Effect.acquireRelease(Ref.modify(state, (current) => {
|
|
57
60
|
const conflictingBase = Match.valueTags(contribution, {
|
|
58
|
-
BaseLanguage: (base) => Array.findFirst(current, (entry) => Match.valueTags(entry.contribution, {
|
|
61
|
+
BaseLanguage: (base) => Array.findFirst(current.entries, (entry) => Match.valueTags(entry.contribution, {
|
|
59
62
|
BaseLanguage: (registeredBase) => registeredBase.locale === base.locale,
|
|
60
63
|
LanguageExtension: () => false
|
|
61
64
|
})),
|
|
62
65
|
LanguageExtension: () => Option.none()
|
|
63
66
|
});
|
|
64
67
|
return Option.match(conflictingBase, {
|
|
65
|
-
onNone: () => [Result.succeed(token),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
onNone: () => [Result.succeed(token), {
|
|
69
|
+
entries: Array.append(current.entries, {
|
|
70
|
+
token,
|
|
71
|
+
pluginId,
|
|
72
|
+
contribution
|
|
73
|
+
}),
|
|
74
|
+
compiledLanguages: /* @__PURE__ */ new Map()
|
|
75
|
+
}],
|
|
70
76
|
onSome: (conflict) => [Result.fail(new LanguageConflictError({
|
|
71
77
|
locale: contribution.locale,
|
|
72
78
|
firstPluginId: conflict.pluginId,
|
|
@@ -74,12 +80,28 @@ const createRegistry = Effect.fn(function* () {
|
|
|
74
80
|
message: "Only one base language can be registered for a locale"
|
|
75
81
|
})), current]
|
|
76
82
|
});
|
|
77
|
-
}).pipe(Effect.flatMap((result) => Effect.fromResult(result))), (registeredToken) => Ref.update(
|
|
83
|
+
}).pipe(Effect.flatMap((result) => Effect.fromResult(result))), (registeredToken) => Ref.update(state, (current) => ({
|
|
84
|
+
entries: Array.filter(current.entries, (entry) => entry.token !== registeredToken),
|
|
85
|
+
compiledLanguages: /* @__PURE__ */ new Map()
|
|
86
|
+
})));
|
|
78
87
|
});
|
|
79
88
|
const resolve = Effect.fn(function* (locale) {
|
|
89
|
+
const cached = (yield* Ref.get(state)).compiledLanguages.get(locale);
|
|
90
|
+
if (cached !== void 0) return cached;
|
|
80
91
|
const canonical = canonicalBaseLocale(locale);
|
|
81
92
|
if (Option.isSome(canonical)) {
|
|
82
|
-
const compiled =
|
|
93
|
+
const compiled = yield* Ref.modify(state, (current) => {
|
|
94
|
+
const canonicalCached = current.compiledLanguages.get(canonical.value);
|
|
95
|
+
if (canonicalCached !== void 0) return [Option.some(canonicalCached), current];
|
|
96
|
+
const language = compileLanguage(canonical.value, current.entries);
|
|
97
|
+
if (Option.isNone(language)) return [language, current];
|
|
98
|
+
const cache = new Map(current.compiledLanguages);
|
|
99
|
+
cache.set(canonical.value, language.value);
|
|
100
|
+
return [language, {
|
|
101
|
+
entries: current.entries,
|
|
102
|
+
compiledLanguages: cache
|
|
103
|
+
}];
|
|
104
|
+
});
|
|
83
105
|
if (Option.isSome(compiled)) return compiled.value;
|
|
84
106
|
}
|
|
85
107
|
return yield* new UnsupportedLocaleError({ locale });
|
|
@@ -102,7 +124,7 @@ const createPluginRegistry = Effect.fn(function* (plugins) {
|
|
|
102
124
|
if (duplicate !== void 0) return yield* new LanguageRegistrationError({
|
|
103
125
|
pluginId: duplicate,
|
|
104
126
|
locale: "*",
|
|
105
|
-
message: "Plugin
|
|
127
|
+
message: "Plugin names must be unique"
|
|
106
128
|
});
|
|
107
129
|
const registry = yield* createRegistry();
|
|
108
130
|
const context = { register: registry.register };
|