@nanahoshi/mona-ui 0.23.1 → 0.23.3
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/fesm2022/nanahoshi-mona-ui-calendar.mjs +7 -4
- package/fesm2022/nanahoshi-mona-ui-color-gradient.mjs +5 -2
- package/fesm2022/nanahoshi-mona-ui-dropdowns.mjs +6 -3
- package/fesm2022/nanahoshi-mona-ui-editor.mjs +5 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-row.component-CIMXz60d.mjs → nanahoshi-mona-ui-grid-grid-footer-row.component-BSVlcB9_.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-CpeheGXU.mjs → nanahoshi-mona-ui-grid-grid-list.component-B2pBOuLa.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-row.component-B_5ay_Nz.mjs → nanahoshi-mona-ui-grid-grid-row.component-Buf4evf5.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-BzKywU5b.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-1RdqLEa9.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-IewqeT4m.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-DeRc_QcP.mjs} +15 -12
- package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-i18n.mjs +211 -102
- package/fesm2022/nanahoshi-mona-ui-locales.mjs +352 -279
- package/fesm2022/nanahoshi-mona-ui-multi-select.mjs +5 -2
- package/fesm2022/nanahoshi-mona-ui-notification.mjs +17 -2
- package/fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs +27 -1
- package/fesm2022/nanahoshi-mona-ui-pager.mjs +10 -7
- package/fesm2022/nanahoshi-mona-ui-popup.mjs +15 -1
- package/fesm2022/nanahoshi-mona-ui-rating.mjs +5 -2
- package/fesm2022/nanahoshi-mona-ui-scroll-view.mjs +6 -3
- package/package.json +1 -1
- package/types/nanahoshi-mona-ui-i18n.d.ts +52 -8
- package/types/nanahoshi-mona-ui-notification.d.ts +1 -0
- package/types/nanahoshi-mona-ui-numeric-text-box.d.ts +5 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, signal, computed, Injectable, ElementRef, DestroyRef, makeEnvironmentProviders } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, signal, computed, Injectable, isDevMode, ElementRef, DestroyRef, makeEnvironmentProviders } from '@angular/core';
|
|
3
3
|
import { Directionality } from '@angular/cdk/bidi';
|
|
4
4
|
import { DateTime } from 'luxon';
|
|
5
5
|
|
|
@@ -13,6 +13,45 @@ const MONA_I18N_CONFIG = new InjectionToken("MONA_I18N_CONFIG", {
|
|
|
13
13
|
factory: () => ({})
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Locale id used whenever a supplied id cannot be turned into a valid BCP 47 tag.
|
|
18
|
+
* Matches the id of `MONA_DEFAULT_LOCALE`.
|
|
19
|
+
*/
|
|
20
|
+
const MONA_FALLBACK_LOCALE_ID = "en-US";
|
|
21
|
+
const canonicalLocaleCache = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Converts a locale id to a canonical BCP 47 tag, or returns `null` when it is not a valid tag.
|
|
24
|
+
*
|
|
25
|
+
* POSIX/Java style ids are accepted: `en_US` becomes `en-US`, and an encoding or modifier suffix
|
|
26
|
+
* (`de_DE.UTF-8`, `de_DE@euro`) is dropped.
|
|
27
|
+
*/
|
|
28
|
+
function tryCanonicalizeLocaleId(localeId) {
|
|
29
|
+
const raw = (localeId ?? "").trim();
|
|
30
|
+
if (!raw) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (canonicalLocaleCache.has(raw)) {
|
|
34
|
+
return canonicalLocaleCache.get(raw) ?? null;
|
|
35
|
+
}
|
|
36
|
+
let canonical = null;
|
|
37
|
+
try {
|
|
38
|
+
const tag = raw.replace(/[.@].*$/, "").replace(/_/g, "-");
|
|
39
|
+
canonical = Intl.getCanonicalLocales(tag)[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
canonical = null;
|
|
43
|
+
}
|
|
44
|
+
canonicalLocaleCache.set(raw, canonical);
|
|
45
|
+
return canonical;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Converts a locale id to a canonical BCP 47 tag. Ids that are empty or not valid tags resolve to
|
|
49
|
+
* {@link MONA_FALLBACK_LOCALE_ID}, so every formatter receives the same, valid locale.
|
|
50
|
+
*/
|
|
51
|
+
function canonicalizeLocaleId(localeId) {
|
|
52
|
+
return tryCanonicalizeLocaleId(localeId) ?? MONA_FALLBACK_LOCALE_ID;
|
|
53
|
+
}
|
|
54
|
+
|
|
16
55
|
function isPlainObject(value) {
|
|
17
56
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
57
|
}
|
|
@@ -43,38 +82,80 @@ function mergeMessages(fallback, localeMessages, overrideMessages) {
|
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
class MonaI18nService {
|
|
85
|
+
#config;
|
|
86
|
+
#locale;
|
|
87
|
+
#localeOverrides;
|
|
88
|
+
#overrides;
|
|
46
89
|
constructor() {
|
|
47
90
|
this.#config = inject(MONA_I18N_CONFIG);
|
|
48
91
|
this.#locale = signal(this.#config.locale ?? MONA_DEFAULT_LOCALE, /* @ts-ignore */
|
|
49
92
|
...(ngDevMode ? [{ debugName: "#locale" }] : /* istanbul ignore next */ []));
|
|
93
|
+
this.#localeOverrides = signal(createLocaleOverrides(this.#config.localeMessages), /* @ts-ignore */
|
|
94
|
+
...(ngDevMode ? [{ debugName: "#localeOverrides" }] : /* istanbul ignore next */ []));
|
|
50
95
|
this.#overrides = signal(this.#config.messages ?? {}, /* @ts-ignore */
|
|
51
96
|
...(ngDevMode ? [{ debugName: "#overrides" }] : /* istanbul ignore next */ []));
|
|
52
97
|
this.direction = computed(() => this.#locale().direction, /* @ts-ignore */
|
|
53
98
|
...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
|
|
54
99
|
this.locale = this.#locale.asReadonly();
|
|
55
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Canonical BCP 47 id of the active locale. POSIX ids such as `en_US` are converted to `en-US`,
|
|
102
|
+
* and invalid ids resolve to `MONA_FALLBACK_LOCALE_ID` so formatters always receive a valid tag.
|
|
103
|
+
*/
|
|
104
|
+
this.localeId = computed(() => canonicalizeLocaleId(this.#locale().id), /* @ts-ignore */
|
|
56
105
|
...(ngDevMode ? [{ debugName: "localeId" }] : /* istanbul ignore next */ []));
|
|
106
|
+
warnOnInvalidLocaleId(this.#locale().id);
|
|
57
107
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Removes overrides. With `options.locale`, only that locale's overrides are removed; without it, every
|
|
110
|
+
* override is removed, global and per locale.
|
|
111
|
+
*/
|
|
112
|
+
clearMessages(options) {
|
|
113
|
+
if (options?.locale == null) {
|
|
114
|
+
this.#overrides.set({});
|
|
115
|
+
this.#localeOverrides.set(new Map());
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const localeId = canonicalizeLocaleId(options.locale);
|
|
119
|
+
this.#localeOverrides.update(current => {
|
|
120
|
+
const next = new Map(current);
|
|
121
|
+
next.delete(localeId);
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
63
124
|
}
|
|
64
125
|
componentMessages(namespace, fallback) {
|
|
65
126
|
return computed(() => {
|
|
66
127
|
const localeMessages = this.#locale().messages[namespace];
|
|
67
128
|
const overrideMessages = this.#overrides()[namespace];
|
|
68
|
-
|
|
129
|
+
const localeOverrideMessages = this.#localeOverrides().get(this.localeId())?.[namespace];
|
|
130
|
+
const merged = mergeMessages(fallback, localeMessages, overrideMessages);
|
|
131
|
+
return mergeTwo(merged, localeOverrideMessages);
|
|
69
132
|
});
|
|
70
133
|
}
|
|
71
|
-
|
|
72
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Merges overrides into the existing ones. With `options.locale`, they apply only while that locale is
|
|
136
|
+
* active, so overrides patched for German do not show after switching to English.
|
|
137
|
+
*/
|
|
138
|
+
patchMessages(messages, options) {
|
|
139
|
+
if (options?.locale == null) {
|
|
140
|
+
this.#overrides.update(current => mergeTwo(current, messages));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const localeId = canonicalizeLocaleId(options.locale);
|
|
144
|
+
this.#localeOverrides.update(current => new Map(current).set(localeId, mergeTwo(current.get(localeId) ?? {}, messages)));
|
|
73
145
|
}
|
|
74
|
-
|
|
75
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Replaces the existing overrides. With `options.locale`, only that locale's overrides are replaced.
|
|
148
|
+
*/
|
|
149
|
+
setMessages(messages, options) {
|
|
150
|
+
if (options?.locale == null) {
|
|
151
|
+
this.#overrides.set(messages);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const localeId = canonicalizeLocaleId(options.locale);
|
|
155
|
+
this.#localeOverrides.update(current => new Map(current).set(localeId, messages));
|
|
76
156
|
}
|
|
77
157
|
use(locale) {
|
|
158
|
+
warnOnInvalidLocaleId(locale.id);
|
|
78
159
|
this.#locale.set(locale);
|
|
79
160
|
}
|
|
80
161
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MonaI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -85,7 +166,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
85
166
|
args: [{
|
|
86
167
|
providedIn: "root"
|
|
87
168
|
}]
|
|
88
|
-
}] });
|
|
169
|
+
}], ctorParameters: () => [] });
|
|
170
|
+
function createLocaleOverrides(localeMessages) {
|
|
171
|
+
const overrides = new Map();
|
|
172
|
+
for (const [localeId, messages] of Object.entries(localeMessages ?? {})) {
|
|
173
|
+
const canonicalId = canonicalizeLocaleId(localeId);
|
|
174
|
+
overrides.set(canonicalId, mergeTwo(overrides.get(canonicalId) ?? {}, messages));
|
|
175
|
+
}
|
|
176
|
+
return overrides;
|
|
177
|
+
}
|
|
178
|
+
function warnOnInvalidLocaleId(localeId) {
|
|
179
|
+
if (isDevMode() && tryCanonicalizeLocaleId(localeId) == null) {
|
|
180
|
+
console.warn(`[mona-ui] "${localeId}" is not a valid BCP 47 locale id. Falling back to "${MONA_FALLBACK_LOCALE_ID}".`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
89
183
|
|
|
90
184
|
/**
|
|
91
185
|
* Reads semantic document direction for a connected browser element using `:dir(rtl)` and `:dir(ltr)`.
|
|
@@ -366,6 +460,15 @@ function provideMonaI18n(config = {}) {
|
|
|
366
460
|
]);
|
|
367
461
|
}
|
|
368
462
|
|
|
463
|
+
/** Full-width forms of printable ASCII ("!" to "~"), offset from ASCII by U+FEE0. */
|
|
464
|
+
const FULLWIDTH_ASCII_START = 0xff01;
|
|
465
|
+
const FULLWIDTH_ASCII_END = 0xff5e;
|
|
466
|
+
const FULLWIDTH_ASCII_OFFSET = 0xfee0;
|
|
467
|
+
const DECIMAL_DIGIT_REGEX = /^\p{Nd}$/u;
|
|
468
|
+
/** Apostrophe, right single quotation mark and modifier letter apostrophe. */
|
|
469
|
+
const APOSTROPHE_REGEX = /^['\u2019\u02BC]$/;
|
|
470
|
+
const APOSTROPHE_REGEX_GLOBAL = /['\u2019\u02BC]/g;
|
|
471
|
+
const asciiDigitCache = new Map();
|
|
369
472
|
const numberSymbolsCache = new Map();
|
|
370
473
|
const numberFormatterCache = new Map();
|
|
371
474
|
const numberGroupingCache = new Map();
|
|
@@ -376,7 +479,8 @@ function serializeOptions(options) {
|
|
|
376
479
|
const keys = Object.keys(options).sort();
|
|
377
480
|
return keys.map(k => `${k}:${String(options[k])}`).join(";");
|
|
378
481
|
}
|
|
379
|
-
function getNumberGroupingPattern(
|
|
482
|
+
function getNumberGroupingPattern(locale) {
|
|
483
|
+
const localeId = canonicalizeLocaleId(locale);
|
|
380
484
|
let pattern = numberGroupingCache.get(localeId);
|
|
381
485
|
if (!pattern) {
|
|
382
486
|
try {
|
|
@@ -439,7 +543,8 @@ function getNumberGroupingPattern(localeId) {
|
|
|
439
543
|
}
|
|
440
544
|
return pattern;
|
|
441
545
|
}
|
|
442
|
-
function getNumberSymbols(
|
|
546
|
+
function getNumberSymbols(locale) {
|
|
547
|
+
const localeId = canonicalizeLocaleId(locale);
|
|
443
548
|
let symbols = numberSymbolsCache.get(localeId);
|
|
444
549
|
if (!symbols) {
|
|
445
550
|
const digitMap = new Map();
|
|
@@ -482,7 +587,7 @@ function getNumberSymbols(localeId) {
|
|
|
482
587
|
return symbols;
|
|
483
588
|
}
|
|
484
589
|
function getNumberFormatter(locale, options) {
|
|
485
|
-
const cleanLocale = (locale
|
|
590
|
+
const cleanLocale = canonicalizeLocaleId(locale);
|
|
486
591
|
const key = `${cleanLocale}:${serializeOptions(options)}`;
|
|
487
592
|
let formatter = numberFormatterCache.get(key);
|
|
488
593
|
if (!formatter) {
|
|
@@ -499,6 +604,30 @@ function getNumberFormatter(locale, options) {
|
|
|
499
604
|
function formatNumber(value, locale, options) {
|
|
500
605
|
return getNumberFormatter(locale, options).format(value);
|
|
501
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* ASCII value of a Unicode decimal digit (\p{Nd}), or null for any other character. Unicode encodes every
|
|
609
|
+
* decimal digit set as a contiguous 0-9 run, so the value is the distance from the start of the run.
|
|
610
|
+
*/
|
|
611
|
+
function toAsciiDigit(char) {
|
|
612
|
+
let digit = asciiDigitCache.get(char);
|
|
613
|
+
if (digit === undefined) {
|
|
614
|
+
digit = null;
|
|
615
|
+
if (DECIMAL_DIGIT_REGEX.test(char)) {
|
|
616
|
+
const code = char.codePointAt(0);
|
|
617
|
+
let runStart = code;
|
|
618
|
+
while (DECIMAL_DIGIT_REGEX.test(String.fromCodePoint(runStart - 1))) {
|
|
619
|
+
runStart--;
|
|
620
|
+
}
|
|
621
|
+
digit = String((code - runStart) % 10);
|
|
622
|
+
}
|
|
623
|
+
asciiDigitCache.set(char, digit);
|
|
624
|
+
}
|
|
625
|
+
return digit;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Converts digits of any script to ASCII digits. Full-width forms typed by CJK input methods
|
|
629
|
+
* ("12.5", "-3", "1,000") are folded to ASCII first.
|
|
630
|
+
*/
|
|
502
631
|
function normalizeLocalizedDigits(text, localeId) {
|
|
503
632
|
if (!text) {
|
|
504
633
|
return text;
|
|
@@ -506,27 +635,11 @@ function normalizeLocalizedDigits(text, localeId) {
|
|
|
506
635
|
const symbols = localeId ? getNumberSymbols(localeId) : null;
|
|
507
636
|
let result = "";
|
|
508
637
|
for (const char of text) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (code >= 0x0660 && code <= 0x0669) {
|
|
515
|
-
result += String(code - 0x0660);
|
|
516
|
-
}
|
|
517
|
-
else if (code >= 0x06f0 && code <= 0x06f9) {
|
|
518
|
-
result += String(code - 0x06f0);
|
|
519
|
-
}
|
|
520
|
-
else if (code >= 0x09e6 && code <= 0x09ef) {
|
|
521
|
-
result += String(code - 0x09e6);
|
|
522
|
-
}
|
|
523
|
-
else if (code >= 0x0966 && code <= 0x096f) {
|
|
524
|
-
result += String(code - 0x0966);
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
result += char;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
638
|
+
const code = char.codePointAt(0);
|
|
639
|
+
const folded = code >= FULLWIDTH_ASCII_START && code <= FULLWIDTH_ASCII_END
|
|
640
|
+
? String.fromCharCode(code - FULLWIDTH_ASCII_OFFSET)
|
|
641
|
+
: char;
|
|
642
|
+
result += symbols?.digits.get(folded) ?? toAsciiDigit(folded) ?? folded;
|
|
530
643
|
}
|
|
531
644
|
return result;
|
|
532
645
|
}
|
|
@@ -548,6 +661,21 @@ function normalizeLocalizedInput(text, localeId) {
|
|
|
548
661
|
cleaned = normalizeLocalizedDigits(cleaned, localeId);
|
|
549
662
|
return cleaned;
|
|
550
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Decimal separators accepted while editing: the locale's own, plus "." and "," so a separator typed on a
|
|
666
|
+
* keyboard layout that does not match the locale still works. An alternate is dropped when it is the locale's
|
|
667
|
+
* group separator; otherwise "1,234" typed in en-US (or "1.234" in de-DE) would be read as 1.234.
|
|
668
|
+
*/
|
|
669
|
+
function getEditDecimalSeparators(symbols) {
|
|
670
|
+
const primary = symbols.decimal || ".";
|
|
671
|
+
const separators = new Set([primary]);
|
|
672
|
+
for (const alternate of [".", ","]) {
|
|
673
|
+
if (alternate !== symbols.group) {
|
|
674
|
+
separators.add(alternate);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return separators;
|
|
678
|
+
}
|
|
551
679
|
function parseLocalizedNumber(text, localeId, options) {
|
|
552
680
|
if (text == null) {
|
|
553
681
|
return null;
|
|
@@ -581,6 +709,11 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
581
709
|
const groupSep = grouping.groupSeparator;
|
|
582
710
|
const isSpaceGroup = /[ \u00A0\u202F]/.test(groupSep);
|
|
583
711
|
const decimalSep = symbols.decimal;
|
|
712
|
+
// Engines disagree on the apostrophe a locale groups with (de-CH: Node ICU "'", Chrome "’"),
|
|
713
|
+
// and keyboards type "'", so every apostrophe variant counts as that locale's group separator.
|
|
714
|
+
if (APOSTROPHE_REGEX.test(groupSep)) {
|
|
715
|
+
cleaned = cleaned.replace(APOSTROPHE_REGEX_GLOBAL, groupSep);
|
|
716
|
+
}
|
|
584
717
|
// Reject non-accepted control or whitespace characters (tabs, newlines, formfeeds)
|
|
585
718
|
if (/[^\S\u0020\u00A0\u202F]/.test(cleaned)) {
|
|
586
719
|
return null;
|
|
@@ -656,11 +789,8 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
656
789
|
if (intGroups.length < 2) {
|
|
657
790
|
return null;
|
|
658
791
|
}
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
if (totalGroupedDigits < grouping.minimumGroupedIntegerDigits) {
|
|
662
|
-
return null;
|
|
663
|
-
}
|
|
792
|
+
// Grouping below the locale's minimum (es-ES formats 1000, not 1.000) is still accepted:
|
|
793
|
+
// users type and paste it, and it is unambiguous because the group sizes are checked below.
|
|
664
794
|
// Last (rightmost) group: must match primaryGroupSize digits
|
|
665
795
|
const lastGroup = intGroups[intGroups.length - 1];
|
|
666
796
|
if (lastGroup.length !== grouping.primaryGroupSize || !/^\d+$/.test(lastGroup)) {
|
|
@@ -699,27 +829,7 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
699
829
|
if (/[\s\u00A0\u202F]/.test(cleaned)) {
|
|
700
830
|
return null;
|
|
701
831
|
}
|
|
702
|
-
const
|
|
703
|
-
const candidateSeparators = new Set();
|
|
704
|
-
candidateSeparators.add(primaryDec);
|
|
705
|
-
if (primaryDec === ".") {
|
|
706
|
-
candidateSeparators.add(",");
|
|
707
|
-
}
|
|
708
|
-
else if (primaryDec === ",") {
|
|
709
|
-
candidateSeparators.add(".");
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
candidateSeparators.add(".");
|
|
713
|
-
candidateSeparators.add(",");
|
|
714
|
-
}
|
|
715
|
-
if (primaryDec === "\u066B") {
|
|
716
|
-
candidateSeparators.add(".");
|
|
717
|
-
candidateSeparators.add(",");
|
|
718
|
-
}
|
|
719
|
-
if (primaryDec === "/") {
|
|
720
|
-
candidateSeparators.add(".");
|
|
721
|
-
candidateSeparators.add(",");
|
|
722
|
-
}
|
|
832
|
+
const candidateSeparators = getEditDecimalSeparators(symbols);
|
|
723
833
|
let foundSep = null;
|
|
724
834
|
let sepIndex = -1;
|
|
725
835
|
for (let i = 0; i < cleaned.length; i++) {
|
|
@@ -878,25 +988,7 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
878
988
|
if (normalized === "") {
|
|
879
989
|
return { valid: true, complete: false, value: null };
|
|
880
990
|
}
|
|
881
|
-
|
|
882
|
-
const primaryDec = symbols.decimal || ".";
|
|
883
|
-
// Candidate separators: primary decimal, plus alternate '.' or ','
|
|
884
|
-
const candidateSeparators = new Set();
|
|
885
|
-
candidateSeparators.add(primaryDec);
|
|
886
|
-
if (primaryDec === ".") {
|
|
887
|
-
candidateSeparators.add(",");
|
|
888
|
-
}
|
|
889
|
-
else if (primaryDec === ",") {
|
|
890
|
-
candidateSeparators.add(".");
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
candidateSeparators.add(".");
|
|
894
|
-
candidateSeparators.add(",");
|
|
895
|
-
}
|
|
896
|
-
if (primaryDec === "\u066B") {
|
|
897
|
-
candidateSeparators.add(".");
|
|
898
|
-
candidateSeparators.add(",");
|
|
899
|
-
}
|
|
991
|
+
const candidateSeparators = getEditDecimalSeparators(symbols);
|
|
900
992
|
// Count how many separators from the candidate set appear
|
|
901
993
|
let foundSep = null;
|
|
902
994
|
let sepIndex = -1;
|
|
@@ -957,22 +1049,20 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
957
1049
|
/**
|
|
958
1050
|
* Normalizes a locale string to ensure it uses the Gregorian calendar system
|
|
959
1051
|
* and doesn't contain a conflicting -u-ca-* extension.
|
|
1052
|
+
* Empty or malformed ids resolve to `MONA_FALLBACK_LOCALE_ID` (en-US).
|
|
960
1053
|
*/
|
|
961
1054
|
function normalizeGregorianLocale(localeId) {
|
|
962
|
-
const
|
|
963
|
-
if (!trimmed) {
|
|
964
|
-
return trimmed;
|
|
965
|
-
}
|
|
1055
|
+
const canonical = canonicalizeLocaleId(localeId);
|
|
966
1056
|
try {
|
|
967
1057
|
if (typeof Intl !== "undefined" && typeof Intl.Locale === "function") {
|
|
968
|
-
const loc = new Intl.Locale(
|
|
1058
|
+
const loc = new Intl.Locale(canonical, { calendar: "gregory" });
|
|
969
1059
|
return loc.toString();
|
|
970
1060
|
}
|
|
971
1061
|
}
|
|
972
1062
|
catch {
|
|
973
|
-
//
|
|
1063
|
+
// Canonical tags are always accepted; keep the canonical id if Intl.Locale still refuses it
|
|
974
1064
|
}
|
|
975
|
-
return
|
|
1065
|
+
return canonical;
|
|
976
1066
|
}
|
|
977
1067
|
/**
|
|
978
1068
|
* Creates a Luxon DateTime from a Date instance, reconfigured to the specified locale
|
|
@@ -1023,17 +1113,29 @@ function stripBidiControls(value) {
|
|
|
1023
1113
|
function sanitizeDateText(value) {
|
|
1024
1114
|
return stripBidiControls(value).replace(/[\u00A0\u2009\u202F]/g, " ");
|
|
1025
1115
|
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Luxon throws for formats that mix incompatible fields (e.g. ordinal and month/day tokens). Callers treat
|
|
1118
|
+
* such input as unparsable, so the error becomes an invalid DateTime.
|
|
1119
|
+
*/
|
|
1120
|
+
function fromFormatSafe(text, format, options) {
|
|
1121
|
+
try {
|
|
1122
|
+
return DateTime.fromFormat(text, format, options);
|
|
1123
|
+
}
|
|
1124
|
+
catch (error) {
|
|
1125
|
+
return DateTime.invalid("unparsable", error instanceof Error ? error.message : String(error));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1026
1128
|
/**
|
|
1027
1129
|
* Parses a date string using the specified format and locale, forcing Gregorian calendar.
|
|
1028
1130
|
* Localized digits are normalized first to support locales that use native numbering systems.
|
|
1029
1131
|
*/
|
|
1030
1132
|
function parseGregorianDate(text, format, localeId) {
|
|
1031
1133
|
const locale = normalizeGregorianLocale(localeId);
|
|
1032
|
-
let dt =
|
|
1134
|
+
let dt = fromFormatSafe(text, format, { locale, outputCalendar: "gregory" });
|
|
1033
1135
|
if (!dt.isValid) {
|
|
1034
1136
|
const sanitizedText = sanitizeDateText(text);
|
|
1035
1137
|
const sanitizedFormat = sanitizeDateText(format);
|
|
1036
|
-
dt =
|
|
1138
|
+
dt = fromFormatSafe(sanitizedText, sanitizedFormat, { locale, outputCalendar: "gregory" });
|
|
1037
1139
|
if (!dt.isValid) {
|
|
1038
1140
|
let normalizedText = normalizeLocalizedDigits(sanitizedText, locale);
|
|
1039
1141
|
let latnLocale = locale;
|
|
@@ -1045,20 +1147,20 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1045
1147
|
catch {
|
|
1046
1148
|
// fallback
|
|
1047
1149
|
}
|
|
1048
|
-
dt =
|
|
1150
|
+
dt = fromFormatSafe(normalizedText, sanitizedFormat, { locale: latnLocale, outputCalendar: "gregory" });
|
|
1049
1151
|
if (!dt.isValid) {
|
|
1050
|
-
dt =
|
|
1152
|
+
dt = fromFormatSafe(normalizedText, sanitizedFormat, { locale, outputCalendar: "gregory" });
|
|
1051
1153
|
}
|
|
1052
1154
|
if (!dt.isValid) {
|
|
1053
1155
|
const effectiveLocale = latnLocale !== locale ? latnLocale : locale;
|
|
1054
1156
|
if (sanitizedFormat.includes(". ") && !normalizedText.includes(". ")) {
|
|
1055
1157
|
const textWithDotSpaces = normalizedText.replace(/\.(?!\s|$)/g, ". ");
|
|
1056
|
-
dt =
|
|
1158
|
+
dt = fromFormatSafe(textWithDotSpaces, sanitizedFormat, {
|
|
1057
1159
|
locale: effectiveLocale,
|
|
1058
1160
|
outputCalendar: "gregory"
|
|
1059
1161
|
});
|
|
1060
1162
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1061
|
-
dt =
|
|
1163
|
+
dt = fromFormatSafe(textWithDotSpaces, sanitizedFormat, {
|
|
1062
1164
|
locale,
|
|
1063
1165
|
outputCalendar: "gregory"
|
|
1064
1166
|
});
|
|
@@ -1071,12 +1173,12 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1071
1173
|
if (sanitizedFormat.includes("dd. ") &&
|
|
1072
1174
|
/(\b\d{4}\.\s*\d{1,2}\.\s*\d{1,2})\.?\s+/.test(normalizedText)) {
|
|
1073
1175
|
const withDotBeforeTime = normalizedText.replace(/(\b\d{4}\.\s*\d{1,2}\.\s*\d{1,2})\.?\s+/, (_match, datePart) => `${datePart}. `);
|
|
1074
|
-
dt =
|
|
1176
|
+
dt = fromFormatSafe(withDotBeforeTime, sanitizedFormat, {
|
|
1075
1177
|
locale: effectiveLocale,
|
|
1076
1178
|
outputCalendar: "gregory"
|
|
1077
1179
|
});
|
|
1078
1180
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1079
|
-
dt =
|
|
1181
|
+
dt = fromFormatSafe(withDotBeforeTime, sanitizedFormat, {
|
|
1080
1182
|
locale,
|
|
1081
1183
|
outputCalendar: "gregory"
|
|
1082
1184
|
});
|
|
@@ -1089,12 +1191,12 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1089
1191
|
const trimmedText = normalizedText.trim();
|
|
1090
1192
|
if (trimmedFormat.endsWith(".")) {
|
|
1091
1193
|
const textWithDot = trimmedText.endsWith(".") ? trimmedText : trimmedText + ".";
|
|
1092
|
-
dt =
|
|
1194
|
+
dt = fromFormatSafe(textWithDot, trimmedFormat, {
|
|
1093
1195
|
locale: effectiveLocale,
|
|
1094
1196
|
outputCalendar: "gregory"
|
|
1095
1197
|
});
|
|
1096
1198
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1097
|
-
dt =
|
|
1199
|
+
dt = fromFormatSafe(textWithDot, trimmedFormat, {
|
|
1098
1200
|
locale,
|
|
1099
1201
|
outputCalendar: "gregory"
|
|
1100
1202
|
});
|
|
@@ -4560,8 +4662,15 @@ const dateFormatCache = new Map();
|
|
|
4560
4662
|
const timeFormatCache = new Map();
|
|
4561
4663
|
const dateTimeFormatCache = new Map();
|
|
4562
4664
|
const firstDayCache = new Map();
|
|
4665
|
+
/**
|
|
4666
|
+
* Turns an Intl literal part into Luxon format text. ASCII letters would otherwise be read as format tokens
|
|
4667
|
+
* (the "h" in fr-CA "21 h 30", the "klo" in fi-FI "15.09.2026 klo 21.30"), so letter runs are quoted and
|
|
4668
|
+
* apostrophes are doubled.
|
|
4669
|
+
*/
|
|
4563
4670
|
function normalizeEditableLiteral(value) {
|
|
4564
|
-
return sanitizeDateText(value)
|
|
4671
|
+
return sanitizeDateText(value)
|
|
4672
|
+
.replace(/'/g, "''")
|
|
4673
|
+
.replace(/[A-Za-z]+/g, run => `'${run}'`);
|
|
4565
4674
|
}
|
|
4566
4675
|
/**
|
|
4567
4676
|
* Derives a localized numeric date format (e.g. "yyyy/MM/dd" or "dd/MM/yyyy") for the given locale.
|
|
@@ -4721,7 +4830,7 @@ function getLocaleDateTimeInputFormat(localeId, options) {
|
|
|
4721
4830
|
* Intl.Locale.getWeekInfo(), or CLDR fallbacks.
|
|
4722
4831
|
*/
|
|
4723
4832
|
function getLocaleFirstDayOfWeek(localeId) {
|
|
4724
|
-
const normalizedLocaleId = (localeId
|
|
4833
|
+
const normalizedLocaleId = canonicalizeLocaleId(localeId);
|
|
4725
4834
|
let firstDay = firstDayCache.get(normalizedLocaleId);
|
|
4726
4835
|
if (!firstDay) {
|
|
4727
4836
|
firstDay = resolveLocaleFirstDayOfWeek(normalizedLocaleId);
|
|
@@ -4870,5 +4979,5 @@ function generatePseudoLocale(catalogs, options) {
|
|
|
4870
4979
|
* Generated bundle index. Do not edit.
|
|
4871
4980
|
*/
|
|
4872
4981
|
|
|
4873
|
-
export { BIDI_CONTROL_REGEX, MONA_DEFAULT_LOCALE, MONA_I18N_CONFIG, MonaDirectionService, MonaI18nService, formatGregorianDateToLocaleString, formatNumber, generatePseudoLocale, getLocaleDateInputFormat, getLocaleDateTimeInputFormat, getLocaleFirstDayOfWeek, getLocaleTimeInputFormat, getNumberFormatter, getNumberGroupingPattern, getNumberSymbols, gregorianDateTime, gregorianDateTimeFromObject, injectComponentDirection, mergeMessages, mergeTwo, normalizeGregorianLocale, normalizeLocalizedDigits, normalizeLocalizedInput, normalizeLocalizedMinus, observeComponentDirection, parseGregorianDate, parseLocalizedNumber, provideMonaI18n, pseudoLocalize, pseudoLocalizeCatalog, resolveComponentDirection, sanitizeDateText, stripBidiControls, validateLocalizedNumber, validateLocalizedNumberEdit };
|
|
4982
|
+
export { BIDI_CONTROL_REGEX, MONA_DEFAULT_LOCALE, MONA_I18N_CONFIG, MonaDirectionService, MonaI18nService, formatGregorianDateToLocaleString, formatNumber, generatePseudoLocale, getEditDecimalSeparators, getLocaleDateInputFormat, getLocaleDateTimeInputFormat, getLocaleFirstDayOfWeek, getLocaleTimeInputFormat, getNumberFormatter, getNumberGroupingPattern, getNumberSymbols, gregorianDateTime, gregorianDateTimeFromObject, injectComponentDirection, mergeMessages, mergeTwo, normalizeGregorianLocale, normalizeLocalizedDigits, normalizeLocalizedInput, normalizeLocalizedMinus, observeComponentDirection, parseGregorianDate, parseLocalizedNumber, provideMonaI18n, pseudoLocalize, pseudoLocalizeCatalog, resolveComponentDirection, sanitizeDateText, stripBidiControls, validateLocalizedNumber, validateLocalizedNumberEdit };
|
|
4874
4983
|
//# sourceMappingURL=nanahoshi-mona-ui-i18n.mjs.map
|