chronolizer 0.2.0 → 0.4.0
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 +170 -167
- package/dist/ast/constructors.d.mts +3 -8
- package/dist/ast/fold.d.mts +2 -1
- package/dist/ast/fold.mjs +38 -31
- package/dist/ast/normalize.mjs +4 -2
- package/dist/ast/schemas.d.mts +18 -25
- package/dist/ast/schemas.mjs +8 -17
- package/dist/filter/expression.mjs +27 -38
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/language/model.d.mts +33 -33
- package/dist/language/model.mjs +5 -8
- package/dist/language/registry.mjs +35 -33
- package/dist/locales/cs.mjs +253 -31
- package/dist/locales/de.mjs +366 -42
- package/dist/locales/en.mjs +215 -51
- package/dist/locales/es.mjs +498 -91
- package/dist/locales/fr.mjs +360 -65
- package/dist/locales/nl.mjs +302 -64
- package/dist/locales/pl.mjs +447 -135
- package/dist/locales/shared.mjs +192 -23
- package/dist/locales/tr.mjs +220 -41
- package/dist/natural/correction.d.mts +1 -1
- package/dist/natural/correction.mjs +31 -5
- package/dist/natural/format.mjs +1 -1
- package/dist/natural/parse.d.mts +1 -1
- package/dist/natural/parse.mjs +5 -4
- package/dist/natural/policy.mjs +2 -2
- package/dist/natural/suggest.mjs +19 -3
- package/dist/natural/suggestion.mjs +11 -5
- package/dist/resolve/resolve.mjs +2 -10
- package/package.json +1 -1
package/dist/locales/fr.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
|
|
|
4
4
|
import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
|
|
5
5
|
import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
|
|
6
6
|
import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
|
|
7
|
-
import { calendarPeriodOffset, candidate, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
|
|
7
|
+
import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decomposeShiftedPeriodRange, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodDay, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
|
|
8
8
|
import { Effect, Option, String as String$1 } from "effect";
|
|
9
9
|
//#region src/locales/fr.ts
|
|
10
10
|
const months = [
|
|
@@ -21,6 +21,89 @@ const months = [
|
|
|
21
21
|
"novembre",
|
|
22
22
|
"décembre"
|
|
23
23
|
];
|
|
24
|
+
const weekdays = [
|
|
25
|
+
"lundi",
|
|
26
|
+
"mardi",
|
|
27
|
+
"mercredi",
|
|
28
|
+
"jeudi",
|
|
29
|
+
"vendredi",
|
|
30
|
+
"samedi",
|
|
31
|
+
"dimanche"
|
|
32
|
+
];
|
|
33
|
+
const nextWeekdayPhrases = weekdays.map((weekday) => `${weekday} prochain`);
|
|
34
|
+
const frenchCountWords = [
|
|
35
|
+
["deux"],
|
|
36
|
+
["trois"],
|
|
37
|
+
["quatre"],
|
|
38
|
+
["cinq"],
|
|
39
|
+
["six"],
|
|
40
|
+
["sept"],
|
|
41
|
+
["huit"],
|
|
42
|
+
["neuf"],
|
|
43
|
+
["dix"],
|
|
44
|
+
["onze"],
|
|
45
|
+
["douze"],
|
|
46
|
+
["treize"],
|
|
47
|
+
["quatorze"],
|
|
48
|
+
["quinze"],
|
|
49
|
+
["seize"],
|
|
50
|
+
["dix-sept"],
|
|
51
|
+
["dix-huit"],
|
|
52
|
+
["dix-neuf"],
|
|
53
|
+
["vingt"]
|
|
54
|
+
];
|
|
55
|
+
const frenchCompoundCountAliases = () => {
|
|
56
|
+
const words = [
|
|
57
|
+
"",
|
|
58
|
+
"un",
|
|
59
|
+
"deux",
|
|
60
|
+
"trois",
|
|
61
|
+
"quatre",
|
|
62
|
+
"cinq",
|
|
63
|
+
"six",
|
|
64
|
+
"sept",
|
|
65
|
+
"huit",
|
|
66
|
+
"neuf",
|
|
67
|
+
"dix",
|
|
68
|
+
"onze",
|
|
69
|
+
"douze",
|
|
70
|
+
"treize",
|
|
71
|
+
"quatorze",
|
|
72
|
+
"quinze",
|
|
73
|
+
"seize",
|
|
74
|
+
"dix-sept",
|
|
75
|
+
"dix-huit",
|
|
76
|
+
"dix-neuf"
|
|
77
|
+
];
|
|
78
|
+
const aliases = [];
|
|
79
|
+
for (let amount = 21; amount <= 99; amount += 1) {
|
|
80
|
+
const tens = Math.floor(amount / 10);
|
|
81
|
+
const remainder = amount % 10;
|
|
82
|
+
let phrase = "";
|
|
83
|
+
if (tens <= 6) {
|
|
84
|
+
const tensWord = [
|
|
85
|
+
"",
|
|
86
|
+
"",
|
|
87
|
+
"vingt",
|
|
88
|
+
"trente",
|
|
89
|
+
"quarante",
|
|
90
|
+
"cinquante",
|
|
91
|
+
"soixante"
|
|
92
|
+
][tens];
|
|
93
|
+
if (tensWord === void 0) continue;
|
|
94
|
+
phrase = remainder === 1 ? `${tensWord} et un` : `${tensWord}-${words[remainder] ?? ""}`;
|
|
95
|
+
} else if (tens === 7) phrase = remainder === 1 ? "soixante et onze" : `soixante-${words[10 + remainder] ?? ""}`;
|
|
96
|
+
else if (tens === 8) phrase = remainder === 0 ? "quatre-vingts" : `quatre-vingt-${words[remainder] ?? ""}`;
|
|
97
|
+
else phrase = `quatre-vingt-${words[10 + remainder] ?? ""}`;
|
|
98
|
+
aliases.push([phrase, amount], [phrase.replaceAll("-", " "), amount]);
|
|
99
|
+
if (amount % 10 === 1) aliases.push([phrase.replace(/un$/u, "une"), amount]);
|
|
100
|
+
}
|
|
101
|
+
return aliases;
|
|
102
|
+
};
|
|
103
|
+
const frenchCountAliases = [...sequentialCountAliases(frenchCountWords, 2), ...frenchCompoundCountAliases()];
|
|
104
|
+
const normalizeFrenchCounts = compileCountAliasNormalizer(frenchCountAliases);
|
|
105
|
+
const frenchCountVocabulary = new Set(countAliasVocabulary(frenchCountAliases));
|
|
106
|
+
const correctFrench = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, frenchCountVocabulary);
|
|
24
107
|
const monthAbbreviations = [
|
|
25
108
|
["jan", "janv"],
|
|
26
109
|
[
|
|
@@ -50,6 +133,9 @@ const units = [
|
|
|
50
133
|
unit: "day",
|
|
51
134
|
singular: "jour",
|
|
52
135
|
plural: "jours",
|
|
136
|
+
rollingSingular: "jour",
|
|
137
|
+
rollingPlural: "jours",
|
|
138
|
+
rollingGender: "masculine",
|
|
53
139
|
current: "aujourd'hui",
|
|
54
140
|
previous: "hier",
|
|
55
141
|
next: "demain",
|
|
@@ -60,6 +146,9 @@ const units = [
|
|
|
60
146
|
unit: "week",
|
|
61
147
|
singular: "semaine",
|
|
62
148
|
plural: "semaines",
|
|
149
|
+
rollingSingular: "semaine",
|
|
150
|
+
rollingPlural: "semaines",
|
|
151
|
+
rollingGender: "feminine",
|
|
63
152
|
current: "cette semaine",
|
|
64
153
|
previous: "la semaine dernière",
|
|
65
154
|
next: "la semaine prochaine",
|
|
@@ -70,6 +159,9 @@ const units = [
|
|
|
70
159
|
unit: "month",
|
|
71
160
|
singular: "mois",
|
|
72
161
|
plural: "mois",
|
|
162
|
+
rollingSingular: "mois",
|
|
163
|
+
rollingPlural: "mois",
|
|
164
|
+
rollingGender: "masculine",
|
|
73
165
|
current: "ce mois-ci",
|
|
74
166
|
previous: "le mois dernier",
|
|
75
167
|
next: "le mois prochain",
|
|
@@ -80,6 +172,9 @@ const units = [
|
|
|
80
172
|
unit: "quarter",
|
|
81
173
|
singular: "trimestre",
|
|
82
174
|
plural: "trimestres",
|
|
175
|
+
rollingSingular: "trimestre",
|
|
176
|
+
rollingPlural: "trimestres",
|
|
177
|
+
rollingGender: "masculine",
|
|
83
178
|
current: "ce trimestre",
|
|
84
179
|
previous: "le trimestre dernier",
|
|
85
180
|
next: "le trimestre prochain",
|
|
@@ -90,6 +185,9 @@ const units = [
|
|
|
90
185
|
unit: "year",
|
|
91
186
|
singular: "an",
|
|
92
187
|
plural: "ans",
|
|
188
|
+
rollingSingular: "année",
|
|
189
|
+
rollingPlural: "années",
|
|
190
|
+
rollingGender: "feminine",
|
|
93
191
|
current: "cette année",
|
|
94
192
|
previous: "l'année dernière",
|
|
95
193
|
next: "l'année prochaine",
|
|
@@ -97,7 +195,41 @@ const units = [
|
|
|
97
195
|
remaining: "reste de l'année"
|
|
98
196
|
}
|
|
99
197
|
];
|
|
100
|
-
const
|
|
198
|
+
const rollingArticle = (entry) => entry.rollingGender === "feminine" ? "la" : "le";
|
|
199
|
+
const indefiniteArticle = (entry) => entry.unit === "week" ? "une" : "un";
|
|
200
|
+
const rollingModifier = (entry, future, plural) => {
|
|
201
|
+
if (future) {
|
|
202
|
+
if (entry.rollingGender === "feminine") return plural ? "prochaines" : "prochaine";
|
|
203
|
+
return plural ? "prochains" : "prochain";
|
|
204
|
+
}
|
|
205
|
+
if (entry.rollingGender === "feminine") return plural ? "dernières" : "dernière";
|
|
206
|
+
return plural ? "derniers" : "dernier";
|
|
207
|
+
};
|
|
208
|
+
const withDe = (period) => {
|
|
209
|
+
if (period.startsWith("le ")) return `du ${period.slice(3)}`;
|
|
210
|
+
if (period.startsWith("les ")) return `des ${period.slice(4)}`;
|
|
211
|
+
if (period.startsWith("la ")) return `de la ${period.slice(3)}`;
|
|
212
|
+
if (period.startsWith("l'")) return `de ${period}`;
|
|
213
|
+
return `de ${period}`;
|
|
214
|
+
};
|
|
215
|
+
const withA = (period) => {
|
|
216
|
+
if (period.startsWith("le ")) return `au ${period.slice(3)}`;
|
|
217
|
+
if (period.startsWith("les ")) return `aux ${period.slice(4)}`;
|
|
218
|
+
if (period.startsWith("la ")) return `à la ${period.slice(3)}`;
|
|
219
|
+
if (period.startsWith("l'")) return `à ${period}`;
|
|
220
|
+
return `à ${period}`;
|
|
221
|
+
};
|
|
222
|
+
const afterDe = (period) => {
|
|
223
|
+
if (period.startsWith("du ")) return `le ${period.slice(3)}`;
|
|
224
|
+
if (period.startsWith("de la ")) return `la ${period.slice(6)}`;
|
|
225
|
+
if (period.startsWith("de l'")) return `l'${period.slice(5)}`;
|
|
226
|
+
return period.startsWith("de ") ? period.slice(3) : period;
|
|
227
|
+
};
|
|
228
|
+
const startsWithDay = (period) => period[0] !== void 0 && period[0] >= "0" && period[0] <= "9";
|
|
229
|
+
const rangeLabel = (lower, upper) => {
|
|
230
|
+
return `${startsWithDay(lower) ? `du ${lower}` : withDe(lower)} ${startsWithDay(upper) ? `au ${upper}` : withA(upper)}`;
|
|
231
|
+
};
|
|
232
|
+
const untilLabel = (period) => `jusqu'${withA(period)}`;
|
|
101
233
|
const unitAliases = [
|
|
102
234
|
["jour", "day"],
|
|
103
235
|
["jours", "day"],
|
|
@@ -362,17 +494,30 @@ const monthNumber = (value) => {
|
|
|
362
494
|
return short === -1 ? void 0 : short + 1;
|
|
363
495
|
};
|
|
364
496
|
const dateLabel = (day, month, year) => `${day === 1 ? "1er" : day} ${textAt(months, month - 1)} ${year}`;
|
|
497
|
+
const currentDateLabel = (day, month) => `${day === 1 ? "1er" : day} ${textAt(months, month - 1)}`;
|
|
365
498
|
const parseNamedDate = (input) => {
|
|
366
|
-
const named = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?)(?: de)? (\d{4})$/u)(input);
|
|
499
|
+
const named = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?),?(?: de)? (\d{4})$/u)(input);
|
|
367
500
|
if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1).replace(/er$/u, ""), monthNumber, dateLabel);
|
|
368
501
|
const numeric = String$1.match(/^([0-3]?\d)[./-]([01]?\d)[./-](\d{4})$/u)(input);
|
|
369
|
-
if (Option.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
502
|
+
if (Option.isSome(numeric)) {
|
|
503
|
+
const year = validYear(textAt(numeric.value, 3));
|
|
504
|
+
const month = Number(textAt(numeric.value, 2));
|
|
505
|
+
const day = Number(textAt(numeric.value, 1));
|
|
506
|
+
if (year !== void 0 && month >= 1 && month <= 12) {
|
|
507
|
+
const value = isoDate(year, month, day);
|
|
508
|
+
if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, dateLabel(day, month, year)));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const current = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?)$/u)(input);
|
|
512
|
+
if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1).replace(/er$/u, ""), monthNumber, currentDateLabel);
|
|
513
|
+
const relative = String$1.match(/^(?:le )?(1er|[0-3]?\d) (de .+|du .+)$/u)(input);
|
|
514
|
+
if (Option.isNone(relative)) return Option.none();
|
|
515
|
+
const periodText = afterDe(textAt(relative.value, 2));
|
|
516
|
+
const alias = periodAliases.find((entry) => entry[0] === periodText && entry[1] === "month");
|
|
517
|
+
if (alias === void 0) return Option.none();
|
|
518
|
+
const day = Number(textAt(relative.value, 1).replace(/er$/u, ""));
|
|
519
|
+
const month = relativePeriod(alias[1], alias[2], alias[3]);
|
|
520
|
+
return periodDay(month, day, `${day === 1 ? "1er" : day} ${withDe(alias[3])}`);
|
|
376
521
|
};
|
|
377
522
|
const quarterNumber = (value) => {
|
|
378
523
|
if ((value.startsWith("q") || value.startsWith("t")) && value.length === 2) return Number(value.slice(1));
|
|
@@ -399,12 +544,13 @@ const parseQuarter = (input) => {
|
|
|
399
544
|
const quarter = quarterNumber(textAt(standalone.value, 1));
|
|
400
545
|
return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
|
|
401
546
|
};
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
547
|
+
const parseDatedPeriod = (input) => {
|
|
548
|
+
const knownPeriod = Option.firstSomeOf([
|
|
549
|
+
absoluteDatePeriod(input, "fr"),
|
|
550
|
+
parseNamedDate(input),
|
|
551
|
+
parseQuarter(input)
|
|
552
|
+
]);
|
|
553
|
+
if (Option.isSome(knownPeriod)) return knownPeriod;
|
|
408
554
|
const yearMatch = String$1.match(/^(?:(?:l')?année |annee )?(\d{4})$/u)(input);
|
|
409
555
|
if (Option.isSome(yearMatch)) {
|
|
410
556
|
const year = validYear(textAt(yearMatch.value, 1));
|
|
@@ -414,17 +560,31 @@ const parseBasePeriod = (input) => {
|
|
|
414
560
|
if (Option.isSome(monthYear)) {
|
|
415
561
|
const month = monthNumber(textAt(monthYear.value, 1));
|
|
416
562
|
const year = validYear(textAt(monthYear.value, 2));
|
|
417
|
-
if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${
|
|
563
|
+
if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
|
|
564
|
+
}
|
|
565
|
+
return Option.none();
|
|
566
|
+
};
|
|
567
|
+
const parseBasePeriod = (input) => {
|
|
568
|
+
const datedPeriod = parseDatedPeriod(input);
|
|
569
|
+
if (Option.isSome(datedPeriod)) return datedPeriod;
|
|
570
|
+
const suffixedRelativeMonth = String$1.match(/^([a-zàâçéèêëîïôûùüÿœ]+\.?) (dernier|prochain)$/u)(input);
|
|
571
|
+
if (Option.isSome(suffixedRelativeMonth)) {
|
|
572
|
+
const month = monthNumber(textAt(suffixedRelativeMonth.value, 1));
|
|
573
|
+
if (month !== void 0) {
|
|
574
|
+
const modifier = textAt(suffixedRelativeMonth.value, 2);
|
|
575
|
+
const direction = modifier === "dernier" ? -1 : 1;
|
|
576
|
+
return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} ${modifier}`));
|
|
577
|
+
}
|
|
418
578
|
}
|
|
419
579
|
const relativeMonth = String$1.match(/^([a-zàâçéèêëîïôûùüÿœ]+\.?) de (l'année dernière|l'annee derniere|l'année prochaine|l'annee prochaine|cette année|cette annee)$/u)(input);
|
|
420
580
|
if (Option.isSome(relativeMonth)) {
|
|
421
581
|
const month = monthNumber(textAt(relativeMonth.value, 1));
|
|
422
582
|
const yearText = textAt(relativeMonth.value, 2);
|
|
423
583
|
const direction = relativeYearDirection(yearText);
|
|
424
|
-
if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${
|
|
584
|
+
if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} de ${relativeYearName(direction)}`));
|
|
425
585
|
}
|
|
426
586
|
const standaloneMonth = monthNumber(input);
|
|
427
|
-
if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0,
|
|
587
|
+
if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, textAt(months, standaloneMonth - 1)));
|
|
428
588
|
const alias = periodAliases.find((entry) => entry[0] === input);
|
|
429
589
|
if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], alias[3]));
|
|
430
590
|
if ([
|
|
@@ -446,16 +606,35 @@ const parseBasePeriod = (input) => {
|
|
|
446
606
|
].includes(input)) return Option.some(relativeWeekend(1, "le week-end prochain"));
|
|
447
607
|
if (input === "l'avant-dernier week-end") return Option.some(relativeWeekend(-2, input));
|
|
448
608
|
if (input === "le week-end après le prochain") return Option.some(relativeWeekend(2, input));
|
|
449
|
-
|
|
609
|
+
const weekday = nextWeekdayPhrases.indexOf(input);
|
|
610
|
+
return weekday === -1 ? Option.none() : Option.some(relativeWeekday(weekday, 1, nextWeekdayPhrases[weekday] ?? input));
|
|
450
611
|
};
|
|
451
612
|
const parsePeriod = (input) => {
|
|
613
|
+
const shifted = String$1.match(/^(.+) (il y a|dans) ([1-9]\d*) (jour|jours|semaine|semaines|mois|trimestre|trimestres|an|ans|année|annee|années|annees)$/u)(input);
|
|
614
|
+
if (Option.isSome(shifted)) {
|
|
615
|
+
const amount = parseTrailingCount(textAt(shifted.value, 3));
|
|
616
|
+
const alias = unitAliases.find((unit) => unit[0] === textAt(shifted.value, 4));
|
|
617
|
+
const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
|
|
618
|
+
const period = parsePeriod(textAt(shifted.value, 1));
|
|
619
|
+
if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
|
|
620
|
+
const past = textAt(shifted.value, 2) === "il y a";
|
|
621
|
+
const direction = past ? -amount.value : amount.value;
|
|
622
|
+
const noun = amount.value === 1 ? entry.singular : entry.plural;
|
|
623
|
+
const canonical = `${period.value.canonical} ${past ? "il y a" : "dans"} ${amount.value} ${noun}`;
|
|
624
|
+
return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
452
627
|
const edge = String$1.match(/^(?:le |la |l')?(début|debut|commencement|fin)(?: du | de la | de l'| de )(.+)$/u)(input);
|
|
453
628
|
if (Option.isSome(edge)) {
|
|
454
629
|
const edgeName = textAt(edge.value, 1);
|
|
455
|
-
const
|
|
630
|
+
const periodText = textAt(edge.value, 2);
|
|
631
|
+
const basePeriod = parseBasePeriod(periodText);
|
|
632
|
+
const implicitUnit = unitAliases.find((entry) => entry[0] === periodText)?.[1];
|
|
633
|
+
const implicit = units.find((entry) => entry.unit === implicitUnit);
|
|
634
|
+
const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
|
|
456
635
|
if (Option.isSome(period)) {
|
|
457
636
|
const isEnd = edgeName === "fin";
|
|
458
|
-
const canonical = `${isEnd ? "fin" : "début"}
|
|
637
|
+
const canonical = `${isEnd ? "fin" : "début"} ${withDe(period.value.canonical)}`;
|
|
459
638
|
return Option.some(isEnd ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
|
|
460
639
|
}
|
|
461
640
|
}
|
|
@@ -466,9 +645,30 @@ const parsePeriod = (input) => {
|
|
|
466
645
|
"tout le ",
|
|
467
646
|
"toute la "
|
|
468
647
|
].find((prefix) => input.startsWith(prefix));
|
|
469
|
-
|
|
648
|
+
const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
|
|
649
|
+
return Option.isSome(base) ? base : parseCalendarOffset(input);
|
|
650
|
+
};
|
|
651
|
+
const countedUnit = (value, amount) => {
|
|
652
|
+
const plural = value === "mois" || value.endsWith("s");
|
|
653
|
+
if (value !== "mois" && plural === (amount === 1)) return void 0;
|
|
654
|
+
const unit = unitAliases.find((entry) => entry[0] === value)?.[1];
|
|
655
|
+
return unit === void 0 ? void 0 : units.find((entry) => entry.unit === unit);
|
|
656
|
+
};
|
|
657
|
+
const modifierAgreesWithNoun = (input, noun) => {
|
|
658
|
+
const match = String$1.match(/(?:^| )(derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes|prochains|prochaines|suivants|suivantes)(?: |$)/u)(input);
|
|
659
|
+
if (Option.isNone(match)) return true;
|
|
660
|
+
const modifier = textAt(match.value, 1);
|
|
661
|
+
return (noun.startsWith("semaine") || noun.startsWith("année") || noun.startsWith("annee")) === [
|
|
662
|
+
"dernières",
|
|
663
|
+
"dernieres",
|
|
664
|
+
"passées",
|
|
665
|
+
"passes",
|
|
666
|
+
"précédentes",
|
|
667
|
+
"precedentes",
|
|
668
|
+
"prochaines",
|
|
669
|
+
"suivantes"
|
|
670
|
+
].includes(modifier);
|
|
470
671
|
};
|
|
471
|
-
const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
|
|
472
672
|
const countedUnitPattern = "jour|jours|semaine|semaines|mois|trimestre|trimestres|an|ans|année|annee|années|annees";
|
|
473
673
|
const compileCountedPattern = (source) => new RegExp(source.replace("UNIT", countedUnitPattern), "u");
|
|
474
674
|
const calendarPastPattern = compileCountedPattern("^il y a ([1-9]\\d*) (UNIT)$");
|
|
@@ -477,38 +677,89 @@ const rollingSincePattern = compileCountedPattern("^depuis ([1-9]\\d*) (UNIT)$")
|
|
|
477
677
|
const rollingBarePattern = compileCountedPattern("^([1-9]\\d*) (UNIT)$");
|
|
478
678
|
const rollingPastPatterns = [
|
|
479
679
|
compileCountedPattern("^(?:(?:les|la) )?(?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes) ([1-9]\\d*) (UNIT)$"),
|
|
480
|
-
compileCountedPattern("^(?:(?:les|la) )?([1-9]\\d*) (?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes) (UNIT)$"),
|
|
680
|
+
compileCountedPattern("^(?:(?:les|la|au cours des|pendant les) )?([1-9]\\d*) (?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes) (UNIT)$"),
|
|
481
681
|
compileCountedPattern("^([1-9]\\d*) (UNIT) (?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes)$")
|
|
482
682
|
];
|
|
483
683
|
const rollingFuturePatterns = [
|
|
484
684
|
compileCountedPattern("^(?:(?:les|la) )?(?:prochains|prochaines|suivants|suivantes) ([1-9]\\d*) (UNIT)$"),
|
|
485
|
-
compileCountedPattern("^(?:(?:les|la) )?([1-9]\\d*) (?:prochains|prochaines|suivants|suivantes) (UNIT)$"),
|
|
685
|
+
compileCountedPattern("^(?:(?:les|la|au cours des|pendant les) )?([1-9]\\d*) (?:prochains|prochaines|suivants|suivantes) (UNIT)$"),
|
|
486
686
|
compileCountedPattern("^([1-9]\\d*) (UNIT) (?:prochains|prochaines|suivants|suivantes|à venir)$")
|
|
487
687
|
];
|
|
488
688
|
const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
|
|
489
|
-
const isFeminineUnit = (unit) => unit === "week";
|
|
490
689
|
const relativeCountPhrase = (amount, entry, future) => {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (isFeminineUnit(entry.unit)) modifier = future ? "prochaines" : "dernières";
|
|
494
|
-
return `les ${amount} ${modifier} ${noun}`;
|
|
690
|
+
if (amount === 1) return singularRollingCanonical(entry, future);
|
|
691
|
+
return `les ${amount} ${rollingModifier(entry, future, true)} ${entry.rollingPlural}`;
|
|
495
692
|
};
|
|
693
|
+
const singularRollingCanonical = (entry, future) => {
|
|
694
|
+
const article = indefiniteArticle(entry);
|
|
695
|
+
return future ? `à partir de maintenant pendant ${article} ${entry.singular}` : `depuis ${article} ${entry.singular}`;
|
|
696
|
+
};
|
|
697
|
+
const singularRollingPhrases = units.flatMap((entry) => {
|
|
698
|
+
const period = `${rollingArticle(entry)} ${rollingModifier(entry, false, false)} ${entry.rollingSingular}`;
|
|
699
|
+
const futurePeriod = `${rollingArticle(entry)} ${rollingModifier(entry, true, false)} ${entry.rollingSingular}`;
|
|
700
|
+
const article = indefiniteArticle(entry);
|
|
701
|
+
return [
|
|
702
|
+
{
|
|
703
|
+
phrase: period,
|
|
704
|
+
entry,
|
|
705
|
+
future: false
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
phrase: `au cours ${withDe(period)}`,
|
|
709
|
+
entry,
|
|
710
|
+
future: false
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
phrase: `depuis ${article} ${entry.singular}`,
|
|
714
|
+
entry,
|
|
715
|
+
future: false
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
phrase: `pendant ${futurePeriod}`,
|
|
719
|
+
entry,
|
|
720
|
+
future: true
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
phrase: `à partir de maintenant pendant ${article} ${entry.singular}`,
|
|
724
|
+
entry,
|
|
725
|
+
future: true
|
|
726
|
+
}
|
|
727
|
+
];
|
|
728
|
+
});
|
|
729
|
+
const singularCalendarOffsets = units.flatMap((entry) => {
|
|
730
|
+
const quantity = `${indefiniteArticle(entry)} ${entry.singular}`;
|
|
731
|
+
return [{
|
|
732
|
+
phrase: `il y a ${quantity}`,
|
|
733
|
+
entry,
|
|
734
|
+
direction: -1
|
|
735
|
+
}, {
|
|
736
|
+
phrase: `dans ${quantity}`,
|
|
737
|
+
entry,
|
|
738
|
+
direction: 1
|
|
739
|
+
}];
|
|
740
|
+
});
|
|
496
741
|
const parseCalendarOffset = (input) => {
|
|
742
|
+
const singular = singularCalendarOffsets.find((entry) => entry.phrase === input);
|
|
743
|
+
if (singular !== void 0) return Option.some(relativePeriod(singular.entry.unit, singular.direction, singular.phrase));
|
|
497
744
|
const past = String$1.match(calendarPastPattern)(input);
|
|
498
745
|
const future = String$1.match(calendarFuturePattern)(input);
|
|
499
746
|
const match = Option.firstSomeOf([past, future]);
|
|
500
747
|
if (Option.isNone(match)) return Option.none();
|
|
501
748
|
const amount = parseTrailingCount(textAt(match.value, 1));
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const entry = units.find((item) => item.unit === unit);
|
|
749
|
+
if (Option.isNone(amount)) return Option.none();
|
|
750
|
+
const entry = countedUnit(textAt(match.value, 2), amount.value);
|
|
505
751
|
if (entry === void 0) return Option.none();
|
|
506
752
|
const direction = Option.isSome(past) ? -amount.value : amount.value;
|
|
507
753
|
const noun = amount.value === 1 ? entry.singular : entry.plural;
|
|
508
754
|
const canonical = direction < 0 ? `il y a ${amount.value} ${noun}` : `dans ${amount.value} ${noun}`;
|
|
509
|
-
return Option.some(
|
|
755
|
+
return Option.some(relativePeriod(entry.unit, direction, canonical));
|
|
510
756
|
};
|
|
511
757
|
const parseRollingPeriod = (input) => {
|
|
758
|
+
const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
|
|
759
|
+
if (singular !== void 0) {
|
|
760
|
+
const range = singular.future ? futureRange(1, singular.entry.unit) : trailingRange(1, singular.entry.unit);
|
|
761
|
+
return Option.some(candidate(range, singularRollingCanonical(singular.entry, singular.future)));
|
|
762
|
+
}
|
|
512
763
|
const since = String$1.match(rollingSincePattern)(input);
|
|
513
764
|
const past = firstPatternMatch(input, rollingPastPatterns);
|
|
514
765
|
const bare = String$1.match(rollingBarePattern)(input);
|
|
@@ -521,19 +772,31 @@ const parseRollingPeriod = (input) => {
|
|
|
521
772
|
]);
|
|
522
773
|
if (Option.isNone(match)) return Option.none();
|
|
523
774
|
const amount = parseTrailingCount(textAt(match.value, 1));
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const entry =
|
|
527
|
-
if (entry === void 0) return Option.none();
|
|
775
|
+
if (Option.isNone(amount)) return Option.none();
|
|
776
|
+
const noun = textAt(match.value, 2);
|
|
777
|
+
const entry = countedUnit(noun, amount.value);
|
|
778
|
+
if (entry === void 0 || !modifierAgreesWithNoun(input, noun)) return Option.none();
|
|
528
779
|
const isFuture = Option.isSome(future);
|
|
529
|
-
const range = isFuture ? futureRange(amount.value, unit) : trailingRange(amount.value, unit);
|
|
780
|
+
const range = isFuture ? futureRange(amount.value, entry.unit) : trailingRange(amount.value, entry.unit);
|
|
530
781
|
return Option.some(candidate(range, relativeCountPhrase(amount.value, entry, isFuture)));
|
|
531
782
|
};
|
|
783
|
+
const parseElidedDateRange = (input) => {
|
|
784
|
+
const joined = String$1.match(/^(?:du (1er|[0-3]?\d) au|entre le (1er|[0-3]?\d) et le) (1er|[0-3]?\d) (.+)$/u)(input);
|
|
785
|
+
const dashed = String$1.match(/^(1er|[0-3]?\d)[–—-](1er|[0-3]?\d) (.+)$/u)(input);
|
|
786
|
+
const match = Option.firstSomeOf([joined, dashed]);
|
|
787
|
+
if (Option.isNone(match)) return Option.none();
|
|
788
|
+
const isJoined = Option.isSome(joined);
|
|
789
|
+
const lowerDay = textAt(match.value, 1) || textAt(match.value, 2);
|
|
790
|
+
const upperDay = textAt(match.value, isJoined ? 3 : 2);
|
|
791
|
+
const period = afterDe(textAt(match.value, isJoined ? 4 : 3));
|
|
792
|
+
const datePeriod = periodAliases.some((entry) => entry[0] === period && entry[1] === "month") ? withDe(period) : period;
|
|
793
|
+
return joinedPeriodCandidate(`du ${lowerDay} ${datePeriod} au ${upperDay} ${datePeriod}`, [["du ", " au "]], parsePeriod, rangeLabel);
|
|
794
|
+
};
|
|
532
795
|
const boundaryCandidate = (input) => {
|
|
533
796
|
const included = String$1.match(/^jusqu'à (.+) (?:inclus|incluse|inclusivement)$/u)(input);
|
|
534
797
|
if (Option.isSome(included)) {
|
|
535
798
|
const period = parsePeriod(textAt(included.value, 1));
|
|
536
|
-
if (Option.isSome(period)) return Option.some(periodBoundaryCandidate(period.value, "through",
|
|
799
|
+
if (Option.isSome(period)) return Option.some(periodBoundaryCandidate(period.value, "through", untilLabel(period.value.canonical)));
|
|
537
800
|
}
|
|
538
801
|
return openBoundaryCandidate(input, [
|
|
539
802
|
["jusqu'avant ", "before"],
|
|
@@ -562,12 +825,12 @@ const parseFrench = (input) => {
|
|
|
562
825
|
"à partir de maintenant",
|
|
563
826
|
"désormais"
|
|
564
827
|
].includes(input)) return Option.some(candidate(fromNowRange(), "depuis maintenant"));
|
|
565
|
-
const offset = parseCalendarOffset(input);
|
|
566
|
-
if (Option.isSome(offset)) return offset;
|
|
567
828
|
const rolling = parseRollingPeriod(input);
|
|
568
829
|
if (Option.isSome(rolling)) return rolling;
|
|
569
830
|
const toDate = toDatePhrases.find((entry) => entry.phrase === input);
|
|
570
831
|
if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), toDate.entry.toDate));
|
|
832
|
+
const elided = parseElidedDateRange(input);
|
|
833
|
+
if (Option.isSome(elided)) return elided;
|
|
571
834
|
const nowBounded = joinedNowCandidate(input, [
|
|
572
835
|
["depuis ", " jusqu'à maintenant"],
|
|
573
836
|
["depuis ", " jusqu'à aujourd'hui"],
|
|
@@ -575,7 +838,8 @@ const parseFrench = (input) => {
|
|
|
575
838
|
], [
|
|
576
839
|
"d'aujourd'hui à ",
|
|
577
840
|
"de maintenant à ",
|
|
578
|
-
"entre aujourd'hui et "
|
|
841
|
+
"entre aujourd'hui et ",
|
|
842
|
+
"maintenant à "
|
|
579
843
|
], parsePeriod, (period) => `depuis ${period} jusqu'à maintenant`, (period) => `de maintenant à ${period}`);
|
|
580
844
|
if (Option.isSome(nowBounded)) return nowBounded;
|
|
581
845
|
const bounded = joinedPeriodCandidate(input, [
|
|
@@ -588,20 +852,32 @@ const parseFrench = (input) => {
|
|
|
588
852
|
["", " — "],
|
|
589
853
|
["", " jusqu'à "],
|
|
590
854
|
["", " au "]
|
|
591
|
-
], parsePeriod,
|
|
855
|
+
], parsePeriod, rangeLabel);
|
|
592
856
|
if (Option.isSome(bounded)) return bounded;
|
|
593
857
|
const boundary = boundaryCandidate(input);
|
|
594
858
|
if (Option.isSome(boundary)) return boundary;
|
|
595
|
-
return Option.map(
|
|
859
|
+
return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
|
|
596
860
|
};
|
|
597
|
-
const
|
|
598
|
-
...periodAliases.map((entry) => entry[0]),
|
|
861
|
+
const weekendPhrases = [
|
|
599
862
|
"ce week-end",
|
|
600
863
|
"le week-end dernier",
|
|
601
864
|
"le week-end prochain",
|
|
602
865
|
"l'avant-dernier week-end",
|
|
603
|
-
"le week-end après le prochain"
|
|
604
|
-
|
|
866
|
+
"le week-end après le prochain"
|
|
867
|
+
];
|
|
868
|
+
const edgePeriodPhrases = [
|
|
869
|
+
...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
|
|
870
|
+
entry.current,
|
|
871
|
+
entry.previous,
|
|
872
|
+
entry.next
|
|
873
|
+
]),
|
|
874
|
+
...weekendPhrases,
|
|
875
|
+
...months
|
|
876
|
+
].flatMap((period) => [`début ${withDe(period)}`, `fin ${withDe(period)}`]);
|
|
877
|
+
const staticPeriodPhrases = [
|
|
878
|
+
...periodAliases.map((entry) => entry[0]),
|
|
879
|
+
...weekendPhrases,
|
|
880
|
+
...edgePeriodPhrases,
|
|
605
881
|
...[
|
|
606
882
|
1,
|
|
607
883
|
2,
|
|
@@ -615,9 +891,12 @@ const staticPeriodPhrases = [
|
|
|
615
891
|
]),
|
|
616
892
|
...months.flatMap((month) => [
|
|
617
893
|
month,
|
|
894
|
+
`${month} dernier`,
|
|
895
|
+
`${month} prochain`,
|
|
618
896
|
`${month} de l'année dernière`,
|
|
619
897
|
`${month} de l'année prochaine`
|
|
620
|
-
])
|
|
898
|
+
]),
|
|
899
|
+
...nextWeekdayPhrases
|
|
621
900
|
];
|
|
622
901
|
const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
|
|
623
902
|
const boundaryPrefixes = [
|
|
@@ -631,15 +910,16 @@ const countedSuggestions = (input) => {
|
|
|
631
910
|
if (amount === void 0) return [];
|
|
632
911
|
return units.flatMap((entry) => {
|
|
633
912
|
const noun = amount === 1 ? entry.singular : entry.plural;
|
|
634
|
-
const
|
|
635
|
-
const
|
|
913
|
+
const rollingNoun = amount === 1 ? entry.rollingSingular : entry.rollingPlural;
|
|
914
|
+
const past = rollingModifier(entry, false, amount !== 1);
|
|
915
|
+
const future = rollingModifier(entry, true, amount !== 1);
|
|
636
916
|
return [
|
|
637
917
|
relativeCountPhrase(amount, entry, false),
|
|
638
|
-
`${amount} ${past} ${
|
|
918
|
+
`${amount} ${past} ${rollingNoun}`,
|
|
639
919
|
`${amount} ${noun}`,
|
|
640
920
|
relativeCountPhrase(amount, entry, true),
|
|
641
|
-
`${amount} ${future} ${
|
|
642
|
-
`${amount} ${
|
|
921
|
+
`${amount} ${future} ${rollingNoun}`,
|
|
922
|
+
`${amount} ${rollingNoun} à venir`,
|
|
643
923
|
`il y a ${amount} ${noun}`,
|
|
644
924
|
`dans ${amount} ${noun}`
|
|
645
925
|
];
|
|
@@ -648,6 +928,8 @@ const countedSuggestions = (input) => {
|
|
|
648
928
|
const frenchSuggestionPhrases = [
|
|
649
929
|
...units.map((entry) => entry.toDate),
|
|
650
930
|
...units.map((entry) => entry.remaining),
|
|
931
|
+
...singularRollingPhrases.map((entry) => entry.phrase),
|
|
932
|
+
...singularCalendarOffsets.map((entry) => entry.phrase),
|
|
651
933
|
...staticPeriodPhrases,
|
|
652
934
|
...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
|
|
653
935
|
"jusqu'à maintenant",
|
|
@@ -663,6 +945,17 @@ const suggestFrench = (input, limit) => {
|
|
|
663
945
|
], limit);
|
|
664
946
|
};
|
|
665
947
|
const renderFrench = (range) => {
|
|
948
|
+
const shifted = decomposeShiftedPeriodRange(range);
|
|
949
|
+
if (Option.isSome(shifted)) {
|
|
950
|
+
const base = renderFrench(shifted.value.baseRange);
|
|
951
|
+
const entry = units.find((unit) => unit.unit === shifted.value.unit);
|
|
952
|
+
if (Option.isSome(base) && entry !== void 0) {
|
|
953
|
+
const amount = Math.abs(shifted.value.amount);
|
|
954
|
+
const noun = amount === 1 ? entry.singular : entry.plural;
|
|
955
|
+
const direction = shifted.value.amount < 0 ? "il y a" : "dans";
|
|
956
|
+
return Option.some(`${base.value} ${direction} ${amount} ${noun}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
666
959
|
const offset = calendarPeriodOffset(range);
|
|
667
960
|
if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
|
|
668
961
|
const entry = units.find((unit) => unit.unit === offset.value.unit);
|
|
@@ -675,24 +968,26 @@ const renderFrench = (range) => {
|
|
|
675
968
|
const future = futurePeriod(range);
|
|
676
969
|
if (Option.isSome(future)) {
|
|
677
970
|
const entry = units.find((unit) => unit.unit === future.value.unit);
|
|
678
|
-
if (entry !== void 0)
|
|
679
|
-
const noun = future.value.amount === 1 ? entry.singular : entry.plural;
|
|
680
|
-
return Option.some(`${future.value.amount} ${noun} à venir`);
|
|
681
|
-
}
|
|
971
|
+
if (entry !== void 0) return Option.some(future.value.amount === 1 ? singularRollingCanonical(entry, true) : relativeCountPhrase(future.value.amount, entry, true));
|
|
682
972
|
}
|
|
683
973
|
const trailing = trailingPeriod(range);
|
|
684
974
|
if (Option.isSome(trailing)) {
|
|
685
975
|
const entry = units.find((unit) => unit.unit === trailing.value.unit);
|
|
686
|
-
if (entry !== void 0) return Option.some(relativeCountPhrase(trailing.value.amount, entry, false));
|
|
976
|
+
if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? singularRollingCanonical(entry, false) : relativeCountPhrase(trailing.value.amount, entry, false));
|
|
687
977
|
}
|
|
688
|
-
const periods = [
|
|
689
|
-
|
|
978
|
+
const periods = [
|
|
979
|
+
...staticPeriods,
|
|
980
|
+
...currentYearDatePeriods(range, currentDateLabel),
|
|
981
|
+
...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)
|
|
982
|
+
];
|
|
983
|
+
return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry.unit), entry.toDate)), ...units.map((entry) => candidate(remainingPeriodRange(entry.unit), entry.remaining))], periods, (period) => `depuis ${period}`, (period) => `avant ${period}`, untilLabel, (period) => `après ${period}`, rangeLabel, (period) => `depuis ${period} jusqu'à maintenant`, (period) => `de maintenant à ${period}`, () => "jusqu'à maintenant", () => "depuis maintenant");
|
|
690
984
|
};
|
|
691
|
-
const normalizeFrench = (input, locale) => normalizeNaturalText(input, locale).replaceAll("’", "'");
|
|
985
|
+
const normalizeFrench = (input, locale) => normalizeFrenchCounts(normalizeNaturalText(input, locale).replaceAll("’", "'"));
|
|
692
986
|
const FrenchContribution = new BaseLanguageContribution({
|
|
693
987
|
locale: "fr",
|
|
694
988
|
vocabulary: [
|
|
695
989
|
...months,
|
|
990
|
+
...weekdays,
|
|
696
991
|
...monthAbbreviations.flatMap((aliases) => aliases),
|
|
697
992
|
...units.flatMap((entry) => [
|
|
698
993
|
entry.singular,
|
|
@@ -723,7 +1018,7 @@ const FrenchContribution = new BaseLanguageContribution({
|
|
|
723
1018
|
"jusqu'à"
|
|
724
1019
|
],
|
|
725
1020
|
normalize: normalizeFrench,
|
|
726
|
-
correct:
|
|
1021
|
+
correct: correctFrench,
|
|
727
1022
|
parseExact: parseFrench,
|
|
728
1023
|
suggest: suggestFrench,
|
|
729
1024
|
render: renderFrench
|