@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.
Files changed (23) hide show
  1. package/fesm2022/nanahoshi-mona-ui-calendar.mjs +7 -4
  2. package/fesm2022/nanahoshi-mona-ui-color-gradient.mjs +5 -2
  3. package/fesm2022/nanahoshi-mona-ui-dropdowns.mjs +6 -3
  4. package/fesm2022/nanahoshi-mona-ui-editor.mjs +5 -2
  5. package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-row.component-CIMXz60d.mjs → nanahoshi-mona-ui-grid-grid-footer-row.component-BSVlcB9_.mjs} +2 -2
  6. package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-CpeheGXU.mjs → nanahoshi-mona-ui-grid-grid-list.component-B2pBOuLa.mjs} +4 -4
  7. package/fesm2022/{nanahoshi-mona-ui-grid-grid-row.component-B_5ay_Nz.mjs → nanahoshi-mona-ui-grid-grid-row.component-Buf4evf5.mjs} +2 -2
  8. package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-BzKywU5b.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-1RdqLEa9.mjs} +4 -4
  9. 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
  10. package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
  11. package/fesm2022/nanahoshi-mona-ui-i18n.mjs +211 -102
  12. package/fesm2022/nanahoshi-mona-ui-locales.mjs +352 -279
  13. package/fesm2022/nanahoshi-mona-ui-multi-select.mjs +5 -2
  14. package/fesm2022/nanahoshi-mona-ui-notification.mjs +17 -2
  15. package/fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs +27 -1
  16. package/fesm2022/nanahoshi-mona-ui-pager.mjs +10 -7
  17. package/fesm2022/nanahoshi-mona-ui-popup.mjs +15 -1
  18. package/fesm2022/nanahoshi-mona-ui-rating.mjs +5 -2
  19. package/fesm2022/nanahoshi-mona-ui-scroll-view.mjs +6 -3
  20. package/package.json +1 -1
  21. package/types/nanahoshi-mona-ui-i18n.d.ts +52 -8
  22. package/types/nanahoshi-mona-ui-notification.d.ts +1 -0
  23. 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
- this.localeId = computed(() => this.#locale().id, /* @ts-ignore */
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
- #config;
59
- #locale;
60
- #overrides;
61
- clearMessages() {
62
- this.#overrides.set({});
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
- return mergeMessages(fallback, localeMessages, overrideMessages);
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
- patchMessages(messages) {
72
- this.#overrides.update(current => mergeTwo(current, messages));
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
- setMessages(messages) {
75
- this.#overrides.set(messages);
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(localeId) {
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(localeId) {
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 ?? "").trim() || "en-US";
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
- if (symbols?.digits.has(char)) {
510
- result += symbols.digits.get(char);
511
- }
512
- else {
513
- const code = char.charCodeAt(0);
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
- // Canonical strict check: must meet locale's minimum grouping threshold
660
- const totalGroupedDigits = intGroups.join("").length;
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 primaryDec = symbols.decimal || ".";
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
- // Identify allowed decimal separators
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 trimmed = (localeId ?? "").trim();
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(trimmed, { calendar: "gregory" });
1058
+ const loc = new Intl.Locale(canonical, { calendar: "gregory" });
969
1059
  return loc.toString();
970
1060
  }
971
1061
  }
972
1062
  catch {
973
- // Fallback safely if localeId is malformed
1063
+ // Canonical tags are always accepted; keep the canonical id if Intl.Locale still refuses it
974
1064
  }
975
- return trimmed;
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 = DateTime.fromFormat(text, format, { locale, outputCalendar: "gregory" });
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 = DateTime.fromFormat(sanitizedText, sanitizedFormat, { locale, outputCalendar: "gregory" });
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 = DateTime.fromFormat(normalizedText, sanitizedFormat, { locale: latnLocale, outputCalendar: "gregory" });
1150
+ dt = fromFormatSafe(normalizedText, sanitizedFormat, { locale: latnLocale, outputCalendar: "gregory" });
1049
1151
  if (!dt.isValid) {
1050
- dt = DateTime.fromFormat(normalizedText, sanitizedFormat, { locale, outputCalendar: "gregory" });
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 = DateTime.fromFormat(textWithDotSpaces, sanitizedFormat, {
1158
+ dt = fromFormatSafe(textWithDotSpaces, sanitizedFormat, {
1057
1159
  locale: effectiveLocale,
1058
1160
  outputCalendar: "gregory"
1059
1161
  });
1060
1162
  if (!dt.isValid && effectiveLocale !== locale) {
1061
- dt = DateTime.fromFormat(textWithDotSpaces, sanitizedFormat, {
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 = DateTime.fromFormat(withDotBeforeTime, sanitizedFormat, {
1176
+ dt = fromFormatSafe(withDotBeforeTime, sanitizedFormat, {
1075
1177
  locale: effectiveLocale,
1076
1178
  outputCalendar: "gregory"
1077
1179
  });
1078
1180
  if (!dt.isValid && effectiveLocale !== locale) {
1079
- dt = DateTime.fromFormat(withDotBeforeTime, sanitizedFormat, {
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 = DateTime.fromFormat(textWithDot, trimmedFormat, {
1194
+ dt = fromFormatSafe(textWithDot, trimmedFormat, {
1093
1195
  locale: effectiveLocale,
1094
1196
  outputCalendar: "gregory"
1095
1197
  });
1096
1198
  if (!dt.isValid && effectiveLocale !== locale) {
1097
- dt = DateTime.fromFormat(textWithDot, trimmedFormat, {
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 ?? "").trim();
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