@nanahoshi/mona-ui 0.23.1 → 0.23.2
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.
|
@@ -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,6 +82,9 @@ function mergeMessages(fallback, localeMessages, overrideMessages) {
|
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
class MonaI18nService {
|
|
85
|
+
#config;
|
|
86
|
+
#locale;
|
|
87
|
+
#overrides;
|
|
46
88
|
constructor() {
|
|
47
89
|
this.#config = inject(MONA_I18N_CONFIG);
|
|
48
90
|
this.#locale = signal(this.#config.locale ?? MONA_DEFAULT_LOCALE, /* @ts-ignore */
|
|
@@ -52,12 +94,14 @@ class MonaI18nService {
|
|
|
52
94
|
this.direction = computed(() => this.#locale().direction, /* @ts-ignore */
|
|
53
95
|
...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
|
|
54
96
|
this.locale = this.#locale.asReadonly();
|
|
55
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Canonical BCP 47 id of the active locale. POSIX ids such as `en_US` are converted to `en-US`,
|
|
99
|
+
* and invalid ids resolve to `MONA_FALLBACK_LOCALE_ID` so formatters always receive a valid tag.
|
|
100
|
+
*/
|
|
101
|
+
this.localeId = computed(() => canonicalizeLocaleId(this.#locale().id), /* @ts-ignore */
|
|
56
102
|
...(ngDevMode ? [{ debugName: "localeId" }] : /* istanbul ignore next */ []));
|
|
103
|
+
warnOnInvalidLocaleId(this.#locale().id);
|
|
57
104
|
}
|
|
58
|
-
#config;
|
|
59
|
-
#locale;
|
|
60
|
-
#overrides;
|
|
61
105
|
clearMessages() {
|
|
62
106
|
this.#overrides.set({});
|
|
63
107
|
}
|
|
@@ -75,6 +119,7 @@ class MonaI18nService {
|
|
|
75
119
|
this.#overrides.set(messages);
|
|
76
120
|
}
|
|
77
121
|
use(locale) {
|
|
122
|
+
warnOnInvalidLocaleId(locale.id);
|
|
78
123
|
this.#locale.set(locale);
|
|
79
124
|
}
|
|
80
125
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MonaI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -85,7 +130,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
85
130
|
args: [{
|
|
86
131
|
providedIn: "root"
|
|
87
132
|
}]
|
|
88
|
-
}] });
|
|
133
|
+
}], ctorParameters: () => [] });
|
|
134
|
+
function warnOnInvalidLocaleId(localeId) {
|
|
135
|
+
if (isDevMode() && tryCanonicalizeLocaleId(localeId) == null) {
|
|
136
|
+
console.warn(`[mona-ui] "${localeId}" is not a valid BCP 47 locale id. Falling back to "${MONA_FALLBACK_LOCALE_ID}".`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
89
139
|
|
|
90
140
|
/**
|
|
91
141
|
* Reads semantic document direction for a connected browser element using `:dir(rtl)` and `:dir(ltr)`.
|
|
@@ -366,6 +416,12 @@ function provideMonaI18n(config = {}) {
|
|
|
366
416
|
]);
|
|
367
417
|
}
|
|
368
418
|
|
|
419
|
+
/** Full-width forms of printable ASCII ("!" to "~"), offset from ASCII by U+FEE0. */
|
|
420
|
+
const FULLWIDTH_ASCII_START = 0xff01;
|
|
421
|
+
const FULLWIDTH_ASCII_END = 0xff5e;
|
|
422
|
+
const FULLWIDTH_ASCII_OFFSET = 0xfee0;
|
|
423
|
+
const DECIMAL_DIGIT_REGEX = /^\p{Nd}$/u;
|
|
424
|
+
const asciiDigitCache = new Map();
|
|
369
425
|
const numberSymbolsCache = new Map();
|
|
370
426
|
const numberFormatterCache = new Map();
|
|
371
427
|
const numberGroupingCache = new Map();
|
|
@@ -376,7 +432,8 @@ function serializeOptions(options) {
|
|
|
376
432
|
const keys = Object.keys(options).sort();
|
|
377
433
|
return keys.map(k => `${k}:${String(options[k])}`).join(";");
|
|
378
434
|
}
|
|
379
|
-
function getNumberGroupingPattern(
|
|
435
|
+
function getNumberGroupingPattern(locale) {
|
|
436
|
+
const localeId = canonicalizeLocaleId(locale);
|
|
380
437
|
let pattern = numberGroupingCache.get(localeId);
|
|
381
438
|
if (!pattern) {
|
|
382
439
|
try {
|
|
@@ -439,7 +496,8 @@ function getNumberGroupingPattern(localeId) {
|
|
|
439
496
|
}
|
|
440
497
|
return pattern;
|
|
441
498
|
}
|
|
442
|
-
function getNumberSymbols(
|
|
499
|
+
function getNumberSymbols(locale) {
|
|
500
|
+
const localeId = canonicalizeLocaleId(locale);
|
|
443
501
|
let symbols = numberSymbolsCache.get(localeId);
|
|
444
502
|
if (!symbols) {
|
|
445
503
|
const digitMap = new Map();
|
|
@@ -482,7 +540,7 @@ function getNumberSymbols(localeId) {
|
|
|
482
540
|
return symbols;
|
|
483
541
|
}
|
|
484
542
|
function getNumberFormatter(locale, options) {
|
|
485
|
-
const cleanLocale = (locale
|
|
543
|
+
const cleanLocale = canonicalizeLocaleId(locale);
|
|
486
544
|
const key = `${cleanLocale}:${serializeOptions(options)}`;
|
|
487
545
|
let formatter = numberFormatterCache.get(key);
|
|
488
546
|
if (!formatter) {
|
|
@@ -499,6 +557,30 @@ function getNumberFormatter(locale, options) {
|
|
|
499
557
|
function formatNumber(value, locale, options) {
|
|
500
558
|
return getNumberFormatter(locale, options).format(value);
|
|
501
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* ASCII value of a Unicode decimal digit (\p{Nd}), or null for any other character. Unicode encodes every
|
|
562
|
+
* decimal digit set as a contiguous 0-9 run, so the value is the distance from the start of the run.
|
|
563
|
+
*/
|
|
564
|
+
function toAsciiDigit(char) {
|
|
565
|
+
let digit = asciiDigitCache.get(char);
|
|
566
|
+
if (digit === undefined) {
|
|
567
|
+
digit = null;
|
|
568
|
+
if (DECIMAL_DIGIT_REGEX.test(char)) {
|
|
569
|
+
const code = char.codePointAt(0);
|
|
570
|
+
let runStart = code;
|
|
571
|
+
while (DECIMAL_DIGIT_REGEX.test(String.fromCodePoint(runStart - 1))) {
|
|
572
|
+
runStart--;
|
|
573
|
+
}
|
|
574
|
+
digit = String((code - runStart) % 10);
|
|
575
|
+
}
|
|
576
|
+
asciiDigitCache.set(char, digit);
|
|
577
|
+
}
|
|
578
|
+
return digit;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Converts digits of any script to ASCII digits. Full-width forms typed by CJK input methods
|
|
582
|
+
* ("12.5", "-3", "1,000") are folded to ASCII first.
|
|
583
|
+
*/
|
|
502
584
|
function normalizeLocalizedDigits(text, localeId) {
|
|
503
585
|
if (!text) {
|
|
504
586
|
return text;
|
|
@@ -506,27 +588,11 @@ function normalizeLocalizedDigits(text, localeId) {
|
|
|
506
588
|
const symbols = localeId ? getNumberSymbols(localeId) : null;
|
|
507
589
|
let result = "";
|
|
508
590
|
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
|
-
}
|
|
591
|
+
const code = char.codePointAt(0);
|
|
592
|
+
const folded = code >= FULLWIDTH_ASCII_START && code <= FULLWIDTH_ASCII_END
|
|
593
|
+
? String.fromCharCode(code - FULLWIDTH_ASCII_OFFSET)
|
|
594
|
+
: char;
|
|
595
|
+
result += symbols?.digits.get(folded) ?? toAsciiDigit(folded) ?? folded;
|
|
530
596
|
}
|
|
531
597
|
return result;
|
|
532
598
|
}
|
|
@@ -548,6 +614,21 @@ function normalizeLocalizedInput(text, localeId) {
|
|
|
548
614
|
cleaned = normalizeLocalizedDigits(cleaned, localeId);
|
|
549
615
|
return cleaned;
|
|
550
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Decimal separators accepted while editing: the locale's own, plus "." and "," so a separator typed on a
|
|
619
|
+
* keyboard layout that does not match the locale still works. An alternate is dropped when it is the locale's
|
|
620
|
+
* group separator; otherwise "1,234" typed in en-US (or "1.234" in de-DE) would be read as 1.234.
|
|
621
|
+
*/
|
|
622
|
+
function getEditDecimalSeparators(symbols) {
|
|
623
|
+
const primary = symbols.decimal || ".";
|
|
624
|
+
const separators = new Set([primary]);
|
|
625
|
+
for (const alternate of [".", ","]) {
|
|
626
|
+
if (alternate !== symbols.group) {
|
|
627
|
+
separators.add(alternate);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return separators;
|
|
631
|
+
}
|
|
551
632
|
function parseLocalizedNumber(text, localeId, options) {
|
|
552
633
|
if (text == null) {
|
|
553
634
|
return null;
|
|
@@ -699,27 +780,7 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
699
780
|
if (/[\s\u00A0\u202F]/.test(cleaned)) {
|
|
700
781
|
return null;
|
|
701
782
|
}
|
|
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
|
-
}
|
|
783
|
+
const candidateSeparators = getEditDecimalSeparators(symbols);
|
|
723
784
|
let foundSep = null;
|
|
724
785
|
let sepIndex = -1;
|
|
725
786
|
for (let i = 0; i < cleaned.length; i++) {
|
|
@@ -878,25 +939,7 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
878
939
|
if (normalized === "") {
|
|
879
940
|
return { valid: true, complete: false, value: null };
|
|
880
941
|
}
|
|
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
|
-
}
|
|
942
|
+
const candidateSeparators = getEditDecimalSeparators(symbols);
|
|
900
943
|
// Count how many separators from the candidate set appear
|
|
901
944
|
let foundSep = null;
|
|
902
945
|
let sepIndex = -1;
|
|
@@ -957,22 +1000,20 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
957
1000
|
/**
|
|
958
1001
|
* Normalizes a locale string to ensure it uses the Gregorian calendar system
|
|
959
1002
|
* and doesn't contain a conflicting -u-ca-* extension.
|
|
1003
|
+
* Empty or malformed ids resolve to `MONA_FALLBACK_LOCALE_ID` (en-US).
|
|
960
1004
|
*/
|
|
961
1005
|
function normalizeGregorianLocale(localeId) {
|
|
962
|
-
const
|
|
963
|
-
if (!trimmed) {
|
|
964
|
-
return trimmed;
|
|
965
|
-
}
|
|
1006
|
+
const canonical = canonicalizeLocaleId(localeId);
|
|
966
1007
|
try {
|
|
967
1008
|
if (typeof Intl !== "undefined" && typeof Intl.Locale === "function") {
|
|
968
|
-
const loc = new Intl.Locale(
|
|
1009
|
+
const loc = new Intl.Locale(canonical, { calendar: "gregory" });
|
|
969
1010
|
return loc.toString();
|
|
970
1011
|
}
|
|
971
1012
|
}
|
|
972
1013
|
catch {
|
|
973
|
-
//
|
|
1014
|
+
// Canonical tags are always accepted; keep the canonical id if Intl.Locale still refuses it
|
|
974
1015
|
}
|
|
975
|
-
return
|
|
1016
|
+
return canonical;
|
|
976
1017
|
}
|
|
977
1018
|
/**
|
|
978
1019
|
* Creates a Luxon DateTime from a Date instance, reconfigured to the specified locale
|
|
@@ -1023,17 +1064,29 @@ function stripBidiControls(value) {
|
|
|
1023
1064
|
function sanitizeDateText(value) {
|
|
1024
1065
|
return stripBidiControls(value).replace(/[\u00A0\u2009\u202F]/g, " ");
|
|
1025
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Luxon throws for formats that mix incompatible fields (e.g. ordinal and month/day tokens). Callers treat
|
|
1069
|
+
* such input as unparsable, so the error becomes an invalid DateTime.
|
|
1070
|
+
*/
|
|
1071
|
+
function fromFormatSafe(text, format, options) {
|
|
1072
|
+
try {
|
|
1073
|
+
return DateTime.fromFormat(text, format, options);
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
return DateTime.invalid("unparsable", error instanceof Error ? error.message : String(error));
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1026
1079
|
/**
|
|
1027
1080
|
* Parses a date string using the specified format and locale, forcing Gregorian calendar.
|
|
1028
1081
|
* Localized digits are normalized first to support locales that use native numbering systems.
|
|
1029
1082
|
*/
|
|
1030
1083
|
function parseGregorianDate(text, format, localeId) {
|
|
1031
1084
|
const locale = normalizeGregorianLocale(localeId);
|
|
1032
|
-
let dt =
|
|
1085
|
+
let dt = fromFormatSafe(text, format, { locale, outputCalendar: "gregory" });
|
|
1033
1086
|
if (!dt.isValid) {
|
|
1034
1087
|
const sanitizedText = sanitizeDateText(text);
|
|
1035
1088
|
const sanitizedFormat = sanitizeDateText(format);
|
|
1036
|
-
dt =
|
|
1089
|
+
dt = fromFormatSafe(sanitizedText, sanitizedFormat, { locale, outputCalendar: "gregory" });
|
|
1037
1090
|
if (!dt.isValid) {
|
|
1038
1091
|
let normalizedText = normalizeLocalizedDigits(sanitizedText, locale);
|
|
1039
1092
|
let latnLocale = locale;
|
|
@@ -1045,20 +1098,20 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1045
1098
|
catch {
|
|
1046
1099
|
// fallback
|
|
1047
1100
|
}
|
|
1048
|
-
dt =
|
|
1101
|
+
dt = fromFormatSafe(normalizedText, sanitizedFormat, { locale: latnLocale, outputCalendar: "gregory" });
|
|
1049
1102
|
if (!dt.isValid) {
|
|
1050
|
-
dt =
|
|
1103
|
+
dt = fromFormatSafe(normalizedText, sanitizedFormat, { locale, outputCalendar: "gregory" });
|
|
1051
1104
|
}
|
|
1052
1105
|
if (!dt.isValid) {
|
|
1053
1106
|
const effectiveLocale = latnLocale !== locale ? latnLocale : locale;
|
|
1054
1107
|
if (sanitizedFormat.includes(". ") && !normalizedText.includes(". ")) {
|
|
1055
1108
|
const textWithDotSpaces = normalizedText.replace(/\.(?!\s|$)/g, ". ");
|
|
1056
|
-
dt =
|
|
1109
|
+
dt = fromFormatSafe(textWithDotSpaces, sanitizedFormat, {
|
|
1057
1110
|
locale: effectiveLocale,
|
|
1058
1111
|
outputCalendar: "gregory"
|
|
1059
1112
|
});
|
|
1060
1113
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1061
|
-
dt =
|
|
1114
|
+
dt = fromFormatSafe(textWithDotSpaces, sanitizedFormat, {
|
|
1062
1115
|
locale,
|
|
1063
1116
|
outputCalendar: "gregory"
|
|
1064
1117
|
});
|
|
@@ -1071,12 +1124,12 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1071
1124
|
if (sanitizedFormat.includes("dd. ") &&
|
|
1072
1125
|
/(\b\d{4}\.\s*\d{1,2}\.\s*\d{1,2})\.?\s+/.test(normalizedText)) {
|
|
1073
1126
|
const withDotBeforeTime = normalizedText.replace(/(\b\d{4}\.\s*\d{1,2}\.\s*\d{1,2})\.?\s+/, (_match, datePart) => `${datePart}. `);
|
|
1074
|
-
dt =
|
|
1127
|
+
dt = fromFormatSafe(withDotBeforeTime, sanitizedFormat, {
|
|
1075
1128
|
locale: effectiveLocale,
|
|
1076
1129
|
outputCalendar: "gregory"
|
|
1077
1130
|
});
|
|
1078
1131
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1079
|
-
dt =
|
|
1132
|
+
dt = fromFormatSafe(withDotBeforeTime, sanitizedFormat, {
|
|
1080
1133
|
locale,
|
|
1081
1134
|
outputCalendar: "gregory"
|
|
1082
1135
|
});
|
|
@@ -1089,12 +1142,12 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1089
1142
|
const trimmedText = normalizedText.trim();
|
|
1090
1143
|
if (trimmedFormat.endsWith(".")) {
|
|
1091
1144
|
const textWithDot = trimmedText.endsWith(".") ? trimmedText : trimmedText + ".";
|
|
1092
|
-
dt =
|
|
1145
|
+
dt = fromFormatSafe(textWithDot, trimmedFormat, {
|
|
1093
1146
|
locale: effectiveLocale,
|
|
1094
1147
|
outputCalendar: "gregory"
|
|
1095
1148
|
});
|
|
1096
1149
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1097
|
-
dt =
|
|
1150
|
+
dt = fromFormatSafe(textWithDot, trimmedFormat, {
|
|
1098
1151
|
locale,
|
|
1099
1152
|
outputCalendar: "gregory"
|
|
1100
1153
|
});
|
|
@@ -4560,8 +4613,15 @@ const dateFormatCache = new Map();
|
|
|
4560
4613
|
const timeFormatCache = new Map();
|
|
4561
4614
|
const dateTimeFormatCache = new Map();
|
|
4562
4615
|
const firstDayCache = new Map();
|
|
4616
|
+
/**
|
|
4617
|
+
* Turns an Intl literal part into Luxon format text. ASCII letters would otherwise be read as format tokens
|
|
4618
|
+
* (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
|
|
4619
|
+
* apostrophes are doubled.
|
|
4620
|
+
*/
|
|
4563
4621
|
function normalizeEditableLiteral(value) {
|
|
4564
|
-
return sanitizeDateText(value)
|
|
4622
|
+
return sanitizeDateText(value)
|
|
4623
|
+
.replace(/'/g, "''")
|
|
4624
|
+
.replace(/[A-Za-z]+/g, run => `'${run}'`);
|
|
4565
4625
|
}
|
|
4566
4626
|
/**
|
|
4567
4627
|
* Derives a localized numeric date format (e.g. "yyyy/MM/dd" or "dd/MM/yyyy") for the given locale.
|
|
@@ -4721,7 +4781,7 @@ function getLocaleDateTimeInputFormat(localeId, options) {
|
|
|
4721
4781
|
* Intl.Locale.getWeekInfo(), or CLDR fallbacks.
|
|
4722
4782
|
*/
|
|
4723
4783
|
function getLocaleFirstDayOfWeek(localeId) {
|
|
4724
|
-
const normalizedLocaleId = (localeId
|
|
4784
|
+
const normalizedLocaleId = canonicalizeLocaleId(localeId);
|
|
4725
4785
|
let firstDay = firstDayCache.get(normalizedLocaleId);
|
|
4726
4786
|
if (!firstDay) {
|
|
4727
4787
|
firstDay = resolveLocaleFirstDayOfWeek(normalizedLocaleId);
|
|
@@ -4870,5 +4930,5 @@ function generatePseudoLocale(catalogs, options) {
|
|
|
4870
4930
|
* Generated bundle index. Do not edit.
|
|
4871
4931
|
*/
|
|
4872
4932
|
|
|
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 };
|
|
4933
|
+
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
4934
|
//# sourceMappingURL=nanahoshi-mona-ui-i18n.mjs.map
|
|
@@ -10,7 +10,7 @@ import { ButtonDirective } from '@nanahoshi/mona-ui/button';
|
|
|
10
10
|
import { themeControlSurfaceClasses, rxTimeout } from '@nanahoshi/mona-ui/internal';
|
|
11
11
|
import { Subject, filter, switchMap, of, tap, delay, concatMap, timer, takeUntil, distinctUntilChanged, map } from 'rxjs';
|
|
12
12
|
import { twMerge } from 'tailwind-merge';
|
|
13
|
-
import { MonaI18nService, formatNumber, validateLocalizedNumber, validateLocalizedNumberEdit } from '@nanahoshi/mona-ui/i18n';
|
|
13
|
+
import { MonaI18nService, formatNumber, validateLocalizedNumber, getNumberSymbols, validateLocalizedNumberEdit } from '@nanahoshi/mona-ui/i18n';
|
|
14
14
|
import { TextBoxDirective } from '@nanahoshi/mona-ui/text-box';
|
|
15
15
|
import { cva } from 'class-variance-authority';
|
|
16
16
|
|
|
@@ -521,6 +521,28 @@ class NumericTextBoxComponent {
|
|
|
521
521
|
}
|
|
522
522
|
this.#pendingPaste = true;
|
|
523
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* The numpad decimal key types "." or "," depending on the keyboard layout, not the locale. Insert the
|
|
526
|
+
* locale's decimal separator instead, so a de-DE layout used with an en-US locale still types a decimal.
|
|
527
|
+
*/
|
|
528
|
+
insertLocaleDecimal(event) {
|
|
529
|
+
const localeId = this.#i18n.localeId();
|
|
530
|
+
const decimal = getNumberSymbols(localeId).decimal;
|
|
531
|
+
if (event.key.length !== 1 || event.key === decimal || event.ctrlKey || event.altKey || event.metaKey) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
event.preventDefault();
|
|
535
|
+
const inputElement = this.valueTextBoxRef().nativeElement;
|
|
536
|
+
const { value, selectionStart, selectionEnd } = inputElement;
|
|
537
|
+
const start = selectionStart ?? value.length;
|
|
538
|
+
const end = selectionEnd ?? value.length;
|
|
539
|
+
const proposedValue = value.slice(0, start) + decimal + value.slice(end);
|
|
540
|
+
if (!validateLocalizedNumberEdit(proposedValue, localeId, { decimals: this.decimals() }).valid) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
inputElement.setRangeText(decimal, start, end, "end");
|
|
544
|
+
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
|
|
545
|
+
}
|
|
524
546
|
parseValue(value) {
|
|
525
547
|
const raw = value == null ? "" : value;
|
|
526
548
|
this.rawInputValue.set(raw);
|
|
@@ -611,6 +633,10 @@ class NumericTextBoxComponent {
|
|
|
611
633
|
if (event.key === "ArrowDown") {
|
|
612
634
|
event.preventDefault();
|
|
613
635
|
this.decrease();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (event.code === "NumpadDecimal") {
|
|
639
|
+
this.insertLocaleDecimal(event);
|
|
614
640
|
}
|
|
615
641
|
});
|
|
616
642
|
}
|
package/package.json
CHANGED
|
@@ -493,7 +493,12 @@ declare class MonaI18nService {
|
|
|
493
493
|
#private;
|
|
494
494
|
readonly direction: Signal<_nanahoshi_mona_ui_i18n.MonaTextDirection>;
|
|
495
495
|
readonly locale: Signal<MonaLocale>;
|
|
496
|
+
/**
|
|
497
|
+
* Canonical BCP 47 id of the active locale. POSIX ids such as `en_US` are converted to `en-US`,
|
|
498
|
+
* and invalid ids resolve to `MONA_FALLBACK_LOCALE_ID` so formatters always receive a valid tag.
|
|
499
|
+
*/
|
|
496
500
|
readonly localeId: Signal<string>;
|
|
501
|
+
constructor();
|
|
497
502
|
clearMessages(): void;
|
|
498
503
|
componentMessages<K extends keyof MonaLocaleMessages>(namespace: K, fallback: MonaLocaleMessages[K]): Signal<MonaLocaleMessages[K]>;
|
|
499
504
|
patchMessages(messages: DeepPartial<MonaLocaleMessages>): void;
|
|
@@ -537,13 +542,23 @@ interface NumberGroupingPattern {
|
|
|
537
542
|
readonly groupSeparator: string;
|
|
538
543
|
readonly minimumGroupedIntegerDigits: number;
|
|
539
544
|
}
|
|
540
|
-
declare function getNumberGroupingPattern(
|
|
541
|
-
declare function getNumberSymbols(
|
|
545
|
+
declare function getNumberGroupingPattern(locale: string): NumberGroupingPattern;
|
|
546
|
+
declare function getNumberSymbols(locale: string): NumberSymbols;
|
|
542
547
|
declare function getNumberFormatter(locale: string, options?: Intl.NumberFormatOptions): Intl.NumberFormat;
|
|
543
548
|
declare function formatNumber(value: number, locale: string, options?: Intl.NumberFormatOptions): string;
|
|
549
|
+
/**
|
|
550
|
+
* Converts digits of any script to ASCII digits. Full-width forms typed by CJK input methods
|
|
551
|
+
* ("12.5", "-3", "1,000") are folded to ASCII first.
|
|
552
|
+
*/
|
|
544
553
|
declare function normalizeLocalizedDigits(text: string, localeId?: string): string;
|
|
545
554
|
declare function normalizeLocalizedMinus(text: string, symbols?: NumberSymbols): string;
|
|
546
555
|
declare function normalizeLocalizedInput(text: string, localeId: string): string;
|
|
556
|
+
/**
|
|
557
|
+
* Decimal separators accepted while editing: the locale's own, plus "." and "," so a separator typed on a
|
|
558
|
+
* keyboard layout that does not match the locale still works. An alternate is dropped when it is the locale's
|
|
559
|
+
* group separator; otherwise "1,234" typed in en-US (or "1.234" in de-DE) would be read as 1.234.
|
|
560
|
+
*/
|
|
561
|
+
declare function getEditDecimalSeparators(symbols: NumberSymbols): ReadonlySet<string>;
|
|
547
562
|
type LocalizedNumberParseMode = "edit" | "locale";
|
|
548
563
|
interface ParseLocalizedNumberOptions {
|
|
549
564
|
/**
|
|
@@ -551,7 +566,7 @@ interface ParseLocalizedNumberOptions {
|
|
|
551
566
|
* Only the locale's canonical decimal separator is treated as decimal, and only
|
|
552
567
|
* canonical grouping separators are treated as grouping.
|
|
553
568
|
* When "edit", allows alternate decimal separators (such as '.' in comma-decimal locales)
|
|
554
|
-
* during interactive input entry.
|
|
569
|
+
* during interactive input entry, unless the alternate is the locale's group separator.
|
|
555
570
|
*/
|
|
556
571
|
mode?: LocalizedNumberParseMode;
|
|
557
572
|
/**
|
|
@@ -583,6 +598,7 @@ declare function validateLocalizedNumberEdit(text: string | null | undefined, lo
|
|
|
583
598
|
/**
|
|
584
599
|
* Normalizes a locale string to ensure it uses the Gregorian calendar system
|
|
585
600
|
* and doesn't contain a conflicting -u-ca-* extension.
|
|
601
|
+
* Empty or malformed ids resolve to `MONA_FALLBACK_LOCALE_ID` (en-US).
|
|
586
602
|
*/
|
|
587
603
|
declare function normalizeGregorianLocale(localeId: string): string;
|
|
588
604
|
/**
|
|
@@ -678,5 +694,5 @@ declare function pseudoLocalizeCatalog<T extends object>(catalog: T, options?: P
|
|
|
678
694
|
*/
|
|
679
695
|
declare function generatePseudoLocale(catalogs: DeepPartial<MonaLocaleMessages>, options?: GeneratePseudoLocaleOptions): MonaLocale;
|
|
680
696
|
|
|
681
|
-
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 };
|
|
697
|
+
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 };
|
|
682
698
|
export type { CalendarMessages, DatePickerMessages, DateTimePickerMessages, DeepPartial, DialogMessages, DirectionObserverHandle, FilterMessages, GeneratePseudoLocaleOptions, LocaleFirstDayOfWeek, LocaleTimeFormatOptions, LocalizedNumberEditValidationResult, LocalizedNumberParseMode, LocalizedNumberValidationResult, MonaAutoCompleteMessages, MonaBreadcrumbMessages, MonaButtonGroupMessages, MonaCalendarMessages, MonaCardMessages, MonaChartMessages, MonaChipMessages, MonaColorGradientMessages, MonaColorPaletteMessages, MonaColorPickerMessages, MonaComboBoxMessages, MonaDatePickerMessages, MonaDateTimePickerMessages, MonaDialogMessages, MonaDropdownListMessages, MonaDropdownsMessages, MonaEditorMessages, MonaFilterMessages, MonaGridMessages, MonaI18nConfig, MonaListBoxMessages, MonaListMessages, MonaLocale, MonaLocaleMessages, MonaMessageNamespace, MonaMultiSelectMessages, MonaNotificationMessages, MonaNumericTextBoxMessages, MonaOtpInputMessages, MonaPagerMessages, MonaRatingMessages, MonaScrollViewMessages, MonaSheetMessages, MonaSliderMessages, MonaSpinnerMessages, MonaSplitButtonMessages, MonaSplitterMessages, MonaStepperMessages, MonaTabsMessages, MonaTextBoxMessages, MonaTextDirection, MonaTimePickerMessages, MonaTimeSelectorMessages, MonaTreeViewMessages, MonaWindowMessages, NotificationMessages, NumberGroupingPattern, NumberSymbols, ParseLocalizedNumberOptions, PseudoLocalizationOptions, SheetMessages, TimePickerMessages, TimeSelectorMessages, ValidateLocalizedNumberEditOptions, WindowMessages };
|
|
@@ -146,6 +146,11 @@ declare class NumericTextBoxComponent implements NumericTextboxVariantInputs, Fo
|
|
|
146
146
|
private formatEditValue;
|
|
147
147
|
private formatValueForDisplay;
|
|
148
148
|
protected onPaste(event: ClipboardEvent): void;
|
|
149
|
+
/**
|
|
150
|
+
* The numpad decimal key types "." or "," depending on the keyboard layout, not the locale. Insert the
|
|
151
|
+
* locale's decimal separator instead, so a de-DE layout used with an en-US locale still types a decimal.
|
|
152
|
+
*/
|
|
153
|
+
private insertLocaleDecimal;
|
|
149
154
|
private parseValue;
|
|
150
155
|
private setBeforeInputSubscription;
|
|
151
156
|
private setInputFocusSubscription;
|