@nanahoshi/mona-ui 0.23.2 → 0.24.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/fesm2022/nanahoshi-mona-ui-auto-complete.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-breadcrumb.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-button-group.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-calendar.mjs +15 -10
- package/fesm2022/nanahoshi-mona-ui-card.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-chart.mjs +4 -2
- package/fesm2022/nanahoshi-mona-ui-chip.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-color-gradient.mjs +8 -3
- package/fesm2022/nanahoshi-mona-ui-color-palette.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-color-picker.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-combo-box.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-date-picker.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-datetime-picker.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-dialog.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-dropdown-list.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-dropdowns.mjs +8 -3
- package/fesm2022/nanahoshi-mona-ui-editor.mjs +8 -3
- package/fesm2022/nanahoshi-mona-ui-filter.mjs +3 -1
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-row.component-CIMXz60d.mjs → nanahoshi-mona-ui-grid-grid-footer-row.component-CnPhVIr4.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-CpeheGXU.mjs → nanahoshi-mona-ui-grid-grid-list.component-DxY6bfTX.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-row.component-B_5ay_Nz.mjs → nanahoshi-mona-ui-grid-grid-row.component-CaO5Lae6.mjs} +3 -3
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-BzKywU5b.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-jNYtqg5B.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-IewqeT4m.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-9FtfCutf.mjs} +18 -13
- package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-i18n-icu.mjs +888 -0
- package/fesm2022/nanahoshi-mona-ui-i18n.mjs +398 -116
- package/fesm2022/nanahoshi-mona-ui-internal-list.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-internal-tree.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-list-box.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-locales-ar-sa.mjs +401 -0
- package/fesm2022/nanahoshi-mona-ui-locales-de-de.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-es-es.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-fr-fr.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-id-id.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-it-it.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-ja-jp.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-ko-kr.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-pt-br.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-ru-ru.mjs +401 -0
- package/fesm2022/nanahoshi-mona-ui-locales-tr-tr.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-zh-cn.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-zh-tw.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales.mjs +77 -4988
- package/fesm2022/nanahoshi-mona-ui-multi-select.mjs +7 -2
- package/fesm2022/nanahoshi-mona-ui-notification.mjs +19 -2
- package/fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs +299 -167
- package/fesm2022/nanahoshi-mona-ui-otp-input.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-pager.mjs +13 -8
- package/fesm2022/nanahoshi-mona-ui-popup.mjs +15 -1
- package/fesm2022/nanahoshi-mona-ui-rating.mjs +7 -2
- package/fesm2022/nanahoshi-mona-ui-scroll-view.mjs +9 -4
- package/fesm2022/nanahoshi-mona-ui-sheet.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-slider.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-spinner.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-split-button.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-splitter.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-stepper.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-tabs.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-text-box.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-time-picker.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-time-selector.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-window.mjs +2 -0
- package/i18n/catalogs/en-US.messages.json +373 -0
- package/i18n/icu/package.json +4 -0
- package/locales/ar-sa/ar-sa.messages.json +373 -0
- package/locales/ar-sa/package.json +4 -0
- package/locales/de-de/de-de.messages.json +373 -0
- package/locales/de-de/package.json +4 -0
- package/locales/es-es/es-es.messages.json +373 -0
- package/locales/es-es/package.json +4 -0
- package/locales/fr-fr/fr-fr.messages.json +373 -0
- package/locales/fr-fr/package.json +4 -0
- package/locales/id-id/id-id.messages.json +373 -0
- package/locales/id-id/package.json +4 -0
- package/locales/it-it/it-it.messages.json +373 -0
- package/locales/it-it/package.json +4 -0
- package/locales/ja-jp/ja-jp.messages.json +373 -0
- package/locales/ja-jp/package.json +4 -0
- package/locales/ko-kr/ko-kr.messages.json +373 -0
- package/locales/ko-kr/package.json +4 -0
- package/locales/pt-br/package.json +4 -0
- package/locales/pt-br/pt-br.messages.json +373 -0
- package/locales/ru-ru/package.json +4 -0
- package/locales/ru-ru/ru-ru.messages.json +373 -0
- package/locales/tr-tr/package.json +4 -0
- package/locales/tr-tr/tr-tr.messages.json +373 -0
- package/locales/zh-cn/package.json +4 -0
- package/locales/zh-cn/zh-cn.messages.json +373 -0
- package/locales/zh-tw/package.json +4 -0
- package/locales/zh-tw/zh-tw.messages.json +373 -0
- package/package.json +111 -53
- package/types/nanahoshi-mona-ui-i18n-icu.d.ts +74 -0
- package/types/nanahoshi-mona-ui-i18n.d.ts +139 -9
- package/types/nanahoshi-mona-ui-locales-ar-sa.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-de-de.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-es-es.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-fr-fr.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-id-id.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-it-it.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-ja-jp.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-ko-kr.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-pt-br.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-ru-ru.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-tr-tr.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-zh-cn.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-zh-tw.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales.d.ts +30 -4915
- package/types/nanahoshi-mona-ui-notification.d.ts +1 -0
- package/types/nanahoshi-mona-ui-numeric-text-box.d.ts +113 -37
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, signal, computed, Injectable, isDevMode, ElementRef, DestroyRef, makeEnvironmentProviders } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, signal, computed, Injectable, isDevMode, ElementRef, DestroyRef, afterNextRender, Injector, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';
|
|
3
3
|
import { Directionality } from '@angular/cdk/bidi';
|
|
4
4
|
import { DateTime } from 'luxon';
|
|
5
5
|
|
|
@@ -83,16 +83,36 @@ function mergeMessages(fallback, localeMessages, overrideMessages) {
|
|
|
83
83
|
|
|
84
84
|
class MonaI18nService {
|
|
85
85
|
#config;
|
|
86
|
+
#loadError;
|
|
87
|
+
#loadedLocales;
|
|
88
|
+
#loadingLocaleId;
|
|
86
89
|
#locale;
|
|
90
|
+
#localeOverrides;
|
|
87
91
|
#overrides;
|
|
92
|
+
/** Incremented by every `load()` and `use()`, so only the latest request changes the locale. */
|
|
93
|
+
#localeRequest;
|
|
88
94
|
constructor() {
|
|
89
95
|
this.#config = inject(MONA_I18N_CONFIG);
|
|
96
|
+
this.#loadError = signal(null, /* @ts-ignore */
|
|
97
|
+
...(ngDevMode ? [{ debugName: "#loadError" }] : /* istanbul ignore next */ []));
|
|
98
|
+
this.#loadedLocales = new Map();
|
|
99
|
+
this.#loadingLocaleId = signal(null, /* @ts-ignore */
|
|
100
|
+
...(ngDevMode ? [{ debugName: "#loadingLocaleId" }] : /* istanbul ignore next */ []));
|
|
90
101
|
this.#locale = signal(this.#config.locale ?? MONA_DEFAULT_LOCALE, /* @ts-ignore */
|
|
91
102
|
...(ngDevMode ? [{ debugName: "#locale" }] : /* istanbul ignore next */ []));
|
|
103
|
+
this.#localeOverrides = signal(createLocaleOverrides(this.#config.localeMessages), /* @ts-ignore */
|
|
104
|
+
...(ngDevMode ? [{ debugName: "#localeOverrides" }] : /* istanbul ignore next */ []));
|
|
92
105
|
this.#overrides = signal(this.#config.messages ?? {}, /* @ts-ignore */
|
|
93
106
|
...(ngDevMode ? [{ debugName: "#overrides" }] : /* istanbul ignore next */ []));
|
|
107
|
+
/** Incremented by every `load()` and `use()`, so only the latest request changes the locale. */
|
|
108
|
+
this.#localeRequest = 0;
|
|
94
109
|
this.direction = computed(() => this.#locale().direction, /* @ts-ignore */
|
|
95
110
|
...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
|
|
111
|
+
/** The error of the latest `load()` when it failed, otherwise `null`. */
|
|
112
|
+
this.loadError = this.#loadError.asReadonly();
|
|
113
|
+
/** Whether a `load()` is in progress and has not been superseded. */
|
|
114
|
+
this.loading = computed(() => this.#loadingLocaleId() != null, /* @ts-ignore */
|
|
115
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
96
116
|
this.locale = this.#locale.asReadonly();
|
|
97
117
|
/**
|
|
98
118
|
* Canonical BCP 47 id of the active locale. POSIX ids such as `en_US` are converted to `en-US`,
|
|
@@ -102,23 +122,108 @@ class MonaI18nService {
|
|
|
102
122
|
...(ngDevMode ? [{ debugName: "localeId" }] : /* istanbul ignore next */ []));
|
|
103
123
|
warnOnInvalidLocaleId(this.#locale().id);
|
|
104
124
|
}
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Removes overrides. With `options.locale`, only that locale's overrides are removed; without it, every
|
|
127
|
+
* override is removed, global and per locale.
|
|
128
|
+
*/
|
|
129
|
+
clearMessages(options) {
|
|
130
|
+
if (options?.locale == null) {
|
|
131
|
+
this.#overrides.set({});
|
|
132
|
+
this.#localeOverrides.set(new Map());
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const localeId = canonicalizeLocaleId(options.locale);
|
|
136
|
+
this.#localeOverrides.update(current => {
|
|
137
|
+
const next = new Map(current);
|
|
138
|
+
next.delete(localeId);
|
|
139
|
+
return next;
|
|
140
|
+
});
|
|
107
141
|
}
|
|
108
142
|
componentMessages(namespace, fallback) {
|
|
109
143
|
return computed(() => {
|
|
110
144
|
const localeMessages = this.#locale().messages[namespace];
|
|
111
145
|
const overrideMessages = this.#overrides()[namespace];
|
|
112
|
-
|
|
146
|
+
const localeOverrideMessages = this.#localeOverrides().get(this.localeId())?.[namespace];
|
|
147
|
+
const merged = mergeMessages(fallback, localeMessages, overrideMessages);
|
|
148
|
+
return mergeTwo(merged, localeOverrideMessages);
|
|
113
149
|
});
|
|
114
150
|
}
|
|
115
|
-
|
|
116
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Loads a locale with the `loadLocale` function configured in `provideMonaI18n` and switches to it.
|
|
153
|
+
*
|
|
154
|
+
* - Ids are canonicalized, and each locale is loaded once: later calls reuse it, and concurrent calls for the
|
|
155
|
+
* same id share one request.
|
|
156
|
+
* - The latest `load()` or `use()` wins. When a newer one starts before this one finishes, this one resolves
|
|
157
|
+
* without switching.
|
|
158
|
+
* - When loading fails, the current locale stays, `loadError` is set and the promise rejects. The failed
|
|
159
|
+
* locale is not cached, so a later call retries it.
|
|
160
|
+
*/
|
|
161
|
+
async load(localeId) {
|
|
162
|
+
const loadLocale = this.#config.loadLocale;
|
|
163
|
+
if (loadLocale == null) {
|
|
164
|
+
throw new Error("[mona-ui] MonaI18nService.load() needs a loader: provideMonaI18n({ loadLocale: loadMonaLocale }).");
|
|
165
|
+
}
|
|
166
|
+
const canonicalId = canonicalizeLocaleId(localeId);
|
|
167
|
+
const request = ++this.#localeRequest;
|
|
168
|
+
this.#loadingLocaleId.set(canonicalId);
|
|
169
|
+
this.#loadError.set(null);
|
|
170
|
+
let pending = this.#loadedLocales.get(canonicalId);
|
|
171
|
+
if (pending == null) {
|
|
172
|
+
pending = Promise.resolve().then(() => loadLocale(canonicalId));
|
|
173
|
+
this.#loadedLocales.set(canonicalId, pending);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const locale = await pending;
|
|
177
|
+
if (request === this.#localeRequest) {
|
|
178
|
+
this.#applyLocale(locale);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (this.#loadedLocales.get(canonicalId) === pending) {
|
|
183
|
+
this.#loadedLocales.delete(canonicalId);
|
|
184
|
+
}
|
|
185
|
+
if (request === this.#localeRequest) {
|
|
186
|
+
this.#loadError.set(error);
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (request === this.#localeRequest) {
|
|
192
|
+
this.#loadingLocaleId.set(null);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Merges overrides into the existing ones. With `options.locale`, they apply only while that locale is
|
|
198
|
+
* active, so overrides patched for German do not show after switching to English.
|
|
199
|
+
*/
|
|
200
|
+
patchMessages(messages, options) {
|
|
201
|
+
if (options?.locale == null) {
|
|
202
|
+
this.#overrides.update(current => mergeTwo(current, messages));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const localeId = canonicalizeLocaleId(options.locale);
|
|
206
|
+
this.#localeOverrides.update(current => new Map(current).set(localeId, mergeTwo(current.get(localeId) ?? {}, messages)));
|
|
117
207
|
}
|
|
118
|
-
|
|
119
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Replaces the existing overrides. With `options.locale`, only that locale's overrides are replaced.
|
|
210
|
+
*/
|
|
211
|
+
setMessages(messages, options) {
|
|
212
|
+
if (options?.locale == null) {
|
|
213
|
+
this.#overrides.set(messages);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const localeId = canonicalizeLocaleId(options.locale);
|
|
217
|
+
this.#localeOverrides.update(current => new Map(current).set(localeId, messages));
|
|
120
218
|
}
|
|
219
|
+
/** Switches to a locale that is already loaded. It supersedes a `load()` still in progress. */
|
|
121
220
|
use(locale) {
|
|
221
|
+
this.#localeRequest++;
|
|
222
|
+
this.#loadingLocaleId.set(null);
|
|
223
|
+
this.#loadError.set(null);
|
|
224
|
+
this.#applyLocale(locale);
|
|
225
|
+
}
|
|
226
|
+
#applyLocale(locale) {
|
|
122
227
|
warnOnInvalidLocaleId(locale.id);
|
|
123
228
|
this.#locale.set(locale);
|
|
124
229
|
}
|
|
@@ -131,6 +236,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
131
236
|
providedIn: "root"
|
|
132
237
|
}]
|
|
133
238
|
}], ctorParameters: () => [] });
|
|
239
|
+
function createLocaleOverrides(localeMessages) {
|
|
240
|
+
const overrides = new Map();
|
|
241
|
+
for (const [localeId, messages] of Object.entries(localeMessages ?? {})) {
|
|
242
|
+
const canonicalId = canonicalizeLocaleId(localeId);
|
|
243
|
+
overrides.set(canonicalId, mergeTwo(overrides.get(canonicalId) ?? {}, messages));
|
|
244
|
+
}
|
|
245
|
+
return overrides;
|
|
246
|
+
}
|
|
134
247
|
function warnOnInvalidLocaleId(localeId) {
|
|
135
248
|
if (isDevMode() && tryCanonicalizeLocaleId(localeId) == null) {
|
|
136
249
|
console.warn(`[mona-ui] "${localeId}" is not a valid BCP 47 locale id. Falling back to "${MONA_FALLBACK_LOCALE_ID}".`);
|
|
@@ -197,39 +310,94 @@ function resolveComponentDirection(hostElement, directionality) {
|
|
|
197
310
|
}
|
|
198
311
|
return "ltr";
|
|
199
312
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
313
|
+
/**
|
|
314
|
+
* One `dir` attribute observer per observed root (the document element, a shadow root, or the top of a detached
|
|
315
|
+
* tree), shared by every direction-aware instance under it. A mutation only invalidates the instances it can affect,
|
|
316
|
+
* so a `dir` written on an overlay pane does not re-resolve every component on the page.
|
|
317
|
+
*/
|
|
318
|
+
const directionRegistries = new WeakMap();
|
|
319
|
+
function isShadowRoot(node) {
|
|
320
|
+
return node.nodeType === 11 && "host" in node;
|
|
321
|
+
}
|
|
322
|
+
function getComposedParent(node) {
|
|
323
|
+
if (node.parentNode) {
|
|
324
|
+
return node.parentNode;
|
|
325
|
+
}
|
|
326
|
+
return isShadowRoot(node) ? node.host : null;
|
|
327
|
+
}
|
|
328
|
+
function composedContains(ancestor, node) {
|
|
329
|
+
for (let current = node; current; current = getComposedParent(current)) {
|
|
330
|
+
if (current === ancestor) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* A `dir` change affects an instance when it lands on the instance's element or one of its ancestors, or anywhere
|
|
338
|
+
* inside the `dir="auto"` root it resolves against (a nested explicit direction changes which text counts).
|
|
339
|
+
*/
|
|
340
|
+
function isAffectedByDirChange(subscriber, targets) {
|
|
341
|
+
for (let current = subscriber.element; current; current = getComposedParent(current)) {
|
|
342
|
+
if (targets.has(current)) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const autoRoot = subscriber.autoRoot;
|
|
347
|
+
if (autoRoot) {
|
|
348
|
+
for (const target of targets) {
|
|
349
|
+
if (composedContains(autoRoot, target)) {
|
|
350
|
+
return true;
|
|
208
351
|
}
|
|
209
|
-
});
|
|
210
|
-
if (doc.documentElement) {
|
|
211
|
-
observer.observe(doc.documentElement, {
|
|
212
|
-
attributes: true,
|
|
213
|
-
attributeFilter: ["dir"],
|
|
214
|
-
subtree: true
|
|
215
|
-
});
|
|
216
352
|
}
|
|
217
|
-
entry = { refCount: 0, observer, callbacks };
|
|
218
|
-
documentObservers.set(doc, entry);
|
|
219
353
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
function getObservedRoots(element) {
|
|
357
|
+
const roots = [];
|
|
358
|
+
let root = element.getRootNode?.() ?? null;
|
|
359
|
+
while (root && isShadowRoot(root)) {
|
|
360
|
+
roots.push(root);
|
|
361
|
+
root = root.host.getRootNode?.() ?? null;
|
|
362
|
+
}
|
|
363
|
+
// The top of a detached tree, so a host that is not attached yet still sees its own subtree change.
|
|
364
|
+
if (root && root !== element && root.nodeType === 1) {
|
|
365
|
+
roots.push(root);
|
|
366
|
+
}
|
|
367
|
+
const documentElement = element.ownerDocument?.documentElement;
|
|
368
|
+
if (documentElement && !roots.includes(documentElement)) {
|
|
369
|
+
roots.push(documentElement);
|
|
370
|
+
}
|
|
371
|
+
return roots;
|
|
372
|
+
}
|
|
373
|
+
function registerDirectionSubscriber(observedRoot, subscriber) {
|
|
374
|
+
let registry = directionRegistries.get(observedRoot);
|
|
375
|
+
if (!registry) {
|
|
376
|
+
const subscribers = new Set();
|
|
377
|
+
const observer = new MutationObserver(records => {
|
|
378
|
+
const targets = new Set();
|
|
379
|
+
for (const record of records) {
|
|
380
|
+
targets.add(record.target);
|
|
381
|
+
}
|
|
382
|
+
for (const candidate of Array.from(subscribers)) {
|
|
383
|
+
if (subscribers.has(candidate) && isAffectedByDirChange(candidate, targets)) {
|
|
384
|
+
candidate.invalidate();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
observer.observe(observedRoot, { attributes: true, attributeFilter: ["dir"], subtree: true });
|
|
389
|
+
registry = { observer, subscribers };
|
|
390
|
+
directionRegistries.set(observedRoot, registry);
|
|
391
|
+
}
|
|
392
|
+
const activeRegistry = registry;
|
|
393
|
+
activeRegistry.subscribers.add(subscriber);
|
|
223
394
|
return () => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (entry.refCount <= 0) {
|
|
231
|
-
entry.observer.disconnect();
|
|
232
|
-
documentObservers.delete(doc);
|
|
395
|
+
activeRegistry.subscribers.delete(subscriber);
|
|
396
|
+
if (activeRegistry.subscribers.size === 0) {
|
|
397
|
+
activeRegistry.observer.disconnect();
|
|
398
|
+
if (directionRegistries.get(observedRoot) === activeRegistry) {
|
|
399
|
+
directionRegistries.delete(observedRoot);
|
|
400
|
+
}
|
|
233
401
|
}
|
|
234
402
|
};
|
|
235
403
|
}
|
|
@@ -276,46 +444,61 @@ function observeAutoDirectionChanges(autoRoot, callback) {
|
|
|
276
444
|
}
|
|
277
445
|
};
|
|
278
446
|
}
|
|
447
|
+
/** The nearest `dir="auto"` ancestor-or-self, unless an explicit `ltr`/`rtl` comes first. */
|
|
448
|
+
function findAutoRoot(element) {
|
|
449
|
+
for (let current = element; current; current = current.parentElement) {
|
|
450
|
+
const rawDir = current.getAttribute?.("dir")?.trim().toLowerCase();
|
|
451
|
+
if (rawDir === "auto") {
|
|
452
|
+
return current;
|
|
453
|
+
}
|
|
454
|
+
if (rawDir === "rtl" || rawDir === "ltr") {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
279
460
|
function observeComponentDirection(hostElement, directionality, onChange) {
|
|
280
461
|
const element = hostElement instanceof ElementRef ? hostElement.nativeElement : hostElement;
|
|
462
|
+
const canObserve = typeof MutationObserver !== "undefined" && !!element;
|
|
281
463
|
let currentDir = resolveComponentDirection(element, directionality);
|
|
282
464
|
let unobserveAuto = null;
|
|
283
|
-
|
|
465
|
+
const rootSubscriptions = new Map();
|
|
466
|
+
const subscriber = element
|
|
467
|
+
? { autoRoot: null, element, invalidate: () => check() }
|
|
468
|
+
: null;
|
|
284
469
|
const updateAutoContentObserver = () => {
|
|
285
|
-
if (
|
|
470
|
+
if (!canObserve || !subscriber) {
|
|
286
471
|
return;
|
|
287
472
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const rawDir = current.getAttribute?.("dir")?.trim().toLowerCase();
|
|
292
|
-
if (rawDir === "auto") {
|
|
293
|
-
autoRoot = current;
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
if (rawDir === "rtl" || rawDir === "ltr") {
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
current = current.parentElement;
|
|
473
|
+
const autoRoot = findAutoRoot(subscriber.element);
|
|
474
|
+
if (autoRoot === subscriber.autoRoot) {
|
|
475
|
+
return;
|
|
300
476
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
477
|
+
unobserveAuto?.();
|
|
478
|
+
unobserveAuto = autoRoot ? observeAutoDirectionChanges(autoRoot, () => check()) : null;
|
|
479
|
+
subscriber.autoRoot = autoRoot;
|
|
480
|
+
};
|
|
481
|
+
// Re-registers with the roots the host is under now: it may have been created in a detached view and inserted
|
|
482
|
+
// into the document or a shadow root later.
|
|
483
|
+
const syncObservedRoots = () => {
|
|
484
|
+
if (!canObserve || !subscriber) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const roots = getObservedRoots(subscriber.element);
|
|
488
|
+
for (const [root, unsubscribe] of rootSubscriptions) {
|
|
489
|
+
if (!roots.includes(root)) {
|
|
490
|
+
unsubscribe();
|
|
491
|
+
rootSubscriptions.delete(root);
|
|
308
492
|
}
|
|
309
493
|
}
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
unobserveAuto = null;
|
|
314
|
-
observedAutoElement = null;
|
|
494
|
+
for (const root of roots) {
|
|
495
|
+
if (!rootSubscriptions.has(root)) {
|
|
496
|
+
rootSubscriptions.set(root, registerDirectionSubscriber(root, subscriber));
|
|
315
497
|
}
|
|
316
498
|
}
|
|
317
499
|
};
|
|
318
500
|
const check = () => {
|
|
501
|
+
syncObservedRoots();
|
|
319
502
|
updateAutoContentObserver();
|
|
320
503
|
const nextDir = resolveComponentDirection(element, directionality);
|
|
321
504
|
if (nextDir !== currentDir) {
|
|
@@ -333,27 +516,24 @@ function observeComponentDirection(hostElement, directionality, onChange) {
|
|
|
333
516
|
});
|
|
334
517
|
cleanupFns.push(() => sub.unsubscribe());
|
|
335
518
|
}
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const root = element.getRootNode?.();
|
|
342
|
-
if (root && root !== element && root !== element.ownerDocument) {
|
|
343
|
-
const rootObserver = new MutationObserver(() => check());
|
|
344
|
-
rootObserver.observe(root, { attributes: true, attributeFilter: ["dir"], subtree: true });
|
|
345
|
-
cleanupFns.push(() => rootObserver.disconnect());
|
|
346
|
-
}
|
|
347
|
-
if (element.ownerDocument) {
|
|
348
|
-
cleanupFns.push(observeDocumentDirChanges(element.ownerDocument, check));
|
|
349
|
-
}
|
|
350
|
-
}
|
|
519
|
+
if (canObserve && element) {
|
|
520
|
+
// The host's own dir, which the shared root observers miss while the host is outside them (detached or moved).
|
|
521
|
+
const hostObserver = new MutationObserver(() => check());
|
|
522
|
+
hostObserver.observe(element, { attributes: true, attributeFilter: ["dir"] });
|
|
523
|
+
cleanupFns.push(() => hostObserver.disconnect());
|
|
351
524
|
}
|
|
525
|
+
syncObservedRoots();
|
|
526
|
+
cleanupFns.push(() => {
|
|
527
|
+
for (const unsubscribe of rootSubscriptions.values()) {
|
|
528
|
+
unsubscribe();
|
|
529
|
+
}
|
|
530
|
+
rootSubscriptions.clear();
|
|
531
|
+
});
|
|
352
532
|
cleanupFns.push(() => {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
533
|
+
unobserveAuto?.();
|
|
534
|
+
unobserveAuto = null;
|
|
535
|
+
if (subscriber) {
|
|
536
|
+
subscriber.autoRoot = null;
|
|
357
537
|
}
|
|
358
538
|
});
|
|
359
539
|
const handle = Object.assign(() => {
|
|
@@ -372,17 +552,17 @@ function injectComponentDirection(hostElementRef) {
|
|
|
372
552
|
const destroyRef = inject(DestroyRef, { optional: true });
|
|
373
553
|
const domVersion = signal(0, /* @ts-ignore */
|
|
374
554
|
...(ngDevMode ? [{ debugName: "domVersion" }] : /* istanbul ignore next */ []));
|
|
375
|
-
let updateAutoObserverFn = () => { };
|
|
376
555
|
if (destroyRef) {
|
|
377
|
-
const
|
|
556
|
+
const handle = observeComponentDirection(hostRef, directionality, () => {
|
|
378
557
|
domVersion.update(v => v + 1);
|
|
379
558
|
});
|
|
380
|
-
|
|
381
|
-
|
|
559
|
+
destroyRef.onDestroy(() => handle());
|
|
560
|
+
// A host can be moved under a different `dir="auto"` root after it is created; re-check once it has rendered.
|
|
561
|
+
afterNextRender(() => handle.check(), { injector: inject(Injector) });
|
|
382
562
|
}
|
|
563
|
+
// Pure: observers are managed by the handle above, never during signal evaluation.
|
|
383
564
|
return computed(() => {
|
|
384
565
|
domVersion();
|
|
385
|
-
updateAutoObserverFn();
|
|
386
566
|
return resolveComponentDirection(hostRef, directionality);
|
|
387
567
|
});
|
|
388
568
|
}
|
|
@@ -408,11 +588,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
408
588
|
}] });
|
|
409
589
|
|
|
410
590
|
function provideMonaI18n(config = {}) {
|
|
591
|
+
const initialLocale = config.initialLocale;
|
|
411
592
|
return makeEnvironmentProviders([
|
|
412
593
|
{
|
|
413
594
|
provide: MONA_I18N_CONFIG,
|
|
414
595
|
useValue: config
|
|
415
|
-
}
|
|
596
|
+
},
|
|
597
|
+
...(initialLocale == null
|
|
598
|
+
? []
|
|
599
|
+
: [
|
|
600
|
+
// A failed load must not stop the application from starting: it keeps the configured locale.
|
|
601
|
+
provideAppInitializer(() => inject(MonaI18nService)
|
|
602
|
+
.load(initialLocale)
|
|
603
|
+
.catch((error) => console.error(`[mona-ui] Could not load the initial locale "${initialLocale}".`, error)))
|
|
604
|
+
])
|
|
416
605
|
]);
|
|
417
606
|
}
|
|
418
607
|
|
|
@@ -421,6 +610,9 @@ const FULLWIDTH_ASCII_START = 0xff01;
|
|
|
421
610
|
const FULLWIDTH_ASCII_END = 0xff5e;
|
|
422
611
|
const FULLWIDTH_ASCII_OFFSET = 0xfee0;
|
|
423
612
|
const DECIMAL_DIGIT_REGEX = /^\p{Nd}$/u;
|
|
613
|
+
/** Apostrophe, right single quotation mark and modifier letter apostrophe. */
|
|
614
|
+
const APOSTROPHE_REGEX = /^['\u2019\u02BC]$/;
|
|
615
|
+
const APOSTROPHE_REGEX_GLOBAL = /['\u2019\u02BC]/g;
|
|
424
616
|
const asciiDigitCache = new Map();
|
|
425
617
|
const numberSymbolsCache = new Map();
|
|
426
618
|
const numberFormatterCache = new Map();
|
|
@@ -554,7 +746,12 @@ function getNumberFormatter(locale, options) {
|
|
|
554
746
|
}
|
|
555
747
|
return formatter;
|
|
556
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* Formats a number for the locale. A decimal string, such as one returned by {@link parseLocalizedDecimal}, is
|
|
751
|
+
* formatted exactly, without converting it to a `number` first.
|
|
752
|
+
*/
|
|
557
753
|
function formatNumber(value, locale, options) {
|
|
754
|
+
// Intl.NumberFormat formats decimal strings exactly (Intl.NumberFormat v3); the ES2022 lib types only declare numbers.
|
|
558
755
|
return getNumberFormatter(locale, options).format(value);
|
|
559
756
|
}
|
|
560
757
|
/**
|
|
@@ -629,7 +826,52 @@ function getEditDecimalSeparators(symbols) {
|
|
|
629
826
|
}
|
|
630
827
|
return separators;
|
|
631
828
|
}
|
|
829
|
+
const MAX_SAFE_INTEGER_DIGITS = String(Number.MAX_SAFE_INTEGER);
|
|
830
|
+
/** The most significant digits a double reproduces exactly when converted from decimal text and back. */
|
|
831
|
+
const MAX_EXACT_SIGNIFICANT_DIGITS = 15;
|
|
832
|
+
/** Whether ASCII integer and fraction digits survive conversion to a `number` unchanged. */
|
|
833
|
+
function isRepresentableAsNumber(intDigits, fracDigits) {
|
|
834
|
+
const integer = intDigits.replace(/^0+/, "");
|
|
835
|
+
const fraction = fracDigits.replace(/0+$/, "");
|
|
836
|
+
if (fraction === "") {
|
|
837
|
+
return (integer.length < MAX_SAFE_INTEGER_DIGITS.length ||
|
|
838
|
+
(integer.length === MAX_SAFE_INTEGER_DIGITS.length && integer <= MAX_SAFE_INTEGER_DIGITS));
|
|
839
|
+
}
|
|
840
|
+
return (integer + fraction).replace(/^0+/, "").length <= MAX_EXACT_SIGNIFICANT_DIGITS;
|
|
841
|
+
}
|
|
842
|
+
function partsToNumber(parts) {
|
|
843
|
+
if (!isRepresentableAsNumber(parts.integer, parts.fraction)) {
|
|
844
|
+
return "precision";
|
|
845
|
+
}
|
|
846
|
+
return Number((parts.negative ? "-" : "") + parts.integer + (parts.fraction ? "." + parts.fraction : ""));
|
|
847
|
+
}
|
|
848
|
+
/** Leading zeros, trailing fraction zeros and the sign of zero are dropped: `"-0012.500"` becomes `"-12.5"`. */
|
|
849
|
+
function partsToDecimal(parts) {
|
|
850
|
+
const integer = parts.integer.replace(/^0+(?=\d)/, "") || "0";
|
|
851
|
+
const fraction = parts.fraction.replace(/0+$/, "");
|
|
852
|
+
const digits = fraction ? `${integer}.${fraction}` : integer;
|
|
853
|
+
return parts.negative && /[1-9]/.test(digits) ? `-${digits}` : digits;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Parses localized text to a number. Returns `null` when the text is not a number in the locale, or when a
|
|
857
|
+
* `number` cannot hold it exactly (see {@link LocalizedNumberInvalidReason}); it never rounds silently.
|
|
858
|
+
* Use {@link parseLocalizedDecimal} for values that need more precision.
|
|
859
|
+
*/
|
|
632
860
|
function parseLocalizedNumber(text, localeId, options) {
|
|
861
|
+
const parts = parseLocalizedNumberParts(text, localeId, options);
|
|
862
|
+
const value = parts === null ? null : partsToNumber(parts);
|
|
863
|
+
return value === "precision" ? null : value;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Parses localized text with the same rules as {@link parseLocalizedNumber}, but returns a canonical decimal string
|
|
867
|
+
* (`"-1234.5"`: ASCII digits, `.` as the decimal separator, no grouping, exponent, leading zeros or trailing
|
|
868
|
+
* fraction zeros) and has no precision limit. Returns `null` when the text is not a number in the locale.
|
|
869
|
+
*/
|
|
870
|
+
function parseLocalizedDecimal(text, localeId, options) {
|
|
871
|
+
const parts = parseLocalizedNumberParts(text, localeId, options);
|
|
872
|
+
return parts === null ? null : partsToDecimal(parts);
|
|
873
|
+
}
|
|
874
|
+
function parseLocalizedNumberParts(text, localeId, options) {
|
|
633
875
|
if (text == null) {
|
|
634
876
|
return null;
|
|
635
877
|
}
|
|
@@ -662,6 +904,11 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
662
904
|
const groupSep = grouping.groupSeparator;
|
|
663
905
|
const isSpaceGroup = /[ \u00A0\u202F]/.test(groupSep);
|
|
664
906
|
const decimalSep = symbols.decimal;
|
|
907
|
+
// Engines disagree on the apostrophe a locale groups with (de-CH: Node ICU "'", Chrome "’"),
|
|
908
|
+
// and keyboards type "'", so every apostrophe variant counts as that locale's group separator.
|
|
909
|
+
if (APOSTROPHE_REGEX.test(groupSep)) {
|
|
910
|
+
cleaned = cleaned.replace(APOSTROPHE_REGEX_GLOBAL, groupSep);
|
|
911
|
+
}
|
|
665
912
|
// Reject non-accepted control or whitespace characters (tabs, newlines, formfeeds)
|
|
666
913
|
if (/[^\S\u0020\u00A0\u202F]/.test(cleaned)) {
|
|
667
914
|
return null;
|
|
@@ -737,11 +984,8 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
737
984
|
if (intGroups.length < 2) {
|
|
738
985
|
return null;
|
|
739
986
|
}
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
if (totalGroupedDigits < grouping.minimumGroupedIntegerDigits) {
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
987
|
+
// Grouping below the locale's minimum (es-ES formats 1000, not 1.000) is still accepted:
|
|
988
|
+
// users type and paste it, and it is unambiguous because the group sizes are checked below.
|
|
745
989
|
// Last (rightmost) group: must match primaryGroupSize digits
|
|
746
990
|
const lastGroup = intGroups[intGroups.length - 1];
|
|
747
991
|
if (lastGroup.length !== grouping.primaryGroupSize || !/^\d+$/.test(lastGroup)) {
|
|
@@ -770,9 +1014,7 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
770
1014
|
}
|
|
771
1015
|
}
|
|
772
1016
|
}
|
|
773
|
-
|
|
774
|
-
const num = Number(normalizedStr);
|
|
775
|
-
return Number.isFinite(num) ? num : null;
|
|
1017
|
+
return { fraction: fracPart ?? "", integer: intPart, negative: sign === "-" };
|
|
776
1018
|
}
|
|
777
1019
|
else {
|
|
778
1020
|
// mode === "edit": editor buffer grammar with exactly one decimal separator (locale primary or alternate)
|
|
@@ -826,11 +1068,7 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
826
1068
|
intPart = cleaned;
|
|
827
1069
|
fracPart = "";
|
|
828
1070
|
}
|
|
829
|
-
|
|
830
|
-
(intPart.length > 0 ? intPart : "0") +
|
|
831
|
-
(fracPart.length > 0 ? "." + fracPart : cleaned.endsWith(foundSep ?? "") ? "" : "");
|
|
832
|
-
const num = Number(normalizedStr);
|
|
833
|
-
return Number.isFinite(num) ? num : null;
|
|
1071
|
+
return { fraction: fracPart, integer: intPart.length > 0 ? intPart : "0", negative: sign === "-" };
|
|
834
1072
|
}
|
|
835
1073
|
}
|
|
836
1074
|
function validateLocalizedNumber(text, localeId, options) {
|
|
@@ -842,9 +1080,13 @@ function validateLocalizedNumber(text, localeId, options) {
|
|
|
842
1080
|
return { value: null, valid: true, fractionDigits: 0 };
|
|
843
1081
|
}
|
|
844
1082
|
const mode = options?.mode ?? "locale";
|
|
845
|
-
const
|
|
1083
|
+
const parts = parseLocalizedNumberParts(trimmed, localeId, { mode });
|
|
1084
|
+
const parsed = parts === null ? null : partsToNumber(parts);
|
|
1085
|
+
if (parsed === "precision") {
|
|
1086
|
+
return { value: null, valid: false, fractionDigits: 0, reason: "precision" };
|
|
1087
|
+
}
|
|
846
1088
|
if (parsed === null || !Number.isFinite(parsed)) {
|
|
847
|
-
return { value: null, valid: false, fractionDigits: 0 };
|
|
1089
|
+
return { value: null, valid: false, fractionDigits: 0, reason: "syntax" };
|
|
848
1090
|
}
|
|
849
1091
|
// Determine the number of fraction digits from the source string
|
|
850
1092
|
const symbols = getNumberSymbols(localeId);
|
|
@@ -903,11 +1145,42 @@ function validateLocalizedNumber(text, localeId, options) {
|
|
|
903
1145
|
}
|
|
904
1146
|
}
|
|
905
1147
|
if (options?.decimals !== undefined && fractionDigits > options.decimals) {
|
|
906
|
-
return { value: parsed, valid: false, fractionDigits };
|
|
1148
|
+
return { value: parsed, valid: false, fractionDigits, reason: "decimals" };
|
|
907
1149
|
}
|
|
908
1150
|
return { value: parsed, valid: true, fractionDigits };
|
|
909
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Validates localized text like {@link validateLocalizedNumber}, but returns a canonical decimal string and has no
|
|
1154
|
+
* precision limit, so `reason` is never `"precision"`.
|
|
1155
|
+
*/
|
|
1156
|
+
function validateLocalizedDecimal(text, localeId, options) {
|
|
1157
|
+
const trimmed = text == null ? "" : String(text).trim();
|
|
1158
|
+
if (trimmed === "" || trimmed === "-" || trimmed === "+") {
|
|
1159
|
+
return { fractionDigits: 0, valid: true, value: null };
|
|
1160
|
+
}
|
|
1161
|
+
const parts = parseLocalizedNumberParts(trimmed, localeId, { mode: options?.mode ?? "locale" });
|
|
1162
|
+
if (parts === null) {
|
|
1163
|
+
return { fractionDigits: 0, reason: "syntax", valid: false, value: null };
|
|
1164
|
+
}
|
|
1165
|
+
const value = partsToDecimal(parts);
|
|
1166
|
+
const fractionDigits = parts.fraction.length;
|
|
1167
|
+
if (options?.decimals !== undefined && fractionDigits > options.decimals) {
|
|
1168
|
+
return { fractionDigits, reason: "decimals", valid: false, value };
|
|
1169
|
+
}
|
|
1170
|
+
return { fractionDigits, valid: true, value };
|
|
1171
|
+
}
|
|
1172
|
+
const EDIT_SYNTAX_ERROR = { complete: false, reason: "syntax", valid: false, value: null };
|
|
910
1173
|
function validateLocalizedNumberEdit(text, localeId, options) {
|
|
1174
|
+
return validateEditInput(text, localeId, options, partsToNumber);
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Validates text while it is typed, like {@link validateLocalizedNumberEdit}, but returns a canonical decimal string
|
|
1178
|
+
* and has no precision limit.
|
|
1179
|
+
*/
|
|
1180
|
+
function validateLocalizedDecimalEdit(text, localeId, options) {
|
|
1181
|
+
return validateEditInput(text, localeId, options, partsToDecimal);
|
|
1182
|
+
}
|
|
1183
|
+
function validateEditInput(text, localeId, options, convert) {
|
|
911
1184
|
if (text == null) {
|
|
912
1185
|
return { valid: true, complete: false, value: null };
|
|
913
1186
|
}
|
|
@@ -917,7 +1190,7 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
917
1190
|
}
|
|
918
1191
|
// Reject control or whitespace characters in interactive typing
|
|
919
1192
|
if (/[\s\u00A0\u202F]/.test(raw)) {
|
|
920
|
-
return
|
|
1193
|
+
return EDIT_SYNTAX_ERROR;
|
|
921
1194
|
}
|
|
922
1195
|
const symbols = getNumberSymbols(localeId);
|
|
923
1196
|
// Normalize minus signs and bidi controls
|
|
@@ -928,12 +1201,12 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
928
1201
|
const allowSign = options?.allowSign ?? true;
|
|
929
1202
|
if (normalized.startsWith("-") || normalized.startsWith("+")) {
|
|
930
1203
|
if (!allowSign) {
|
|
931
|
-
return
|
|
1204
|
+
return EDIT_SYNTAX_ERROR;
|
|
932
1205
|
}
|
|
933
1206
|
normalized = normalized.slice(1);
|
|
934
1207
|
}
|
|
935
1208
|
if (normalized.includes("-") || normalized.includes("+")) {
|
|
936
|
-
return
|
|
1209
|
+
return EDIT_SYNTAX_ERROR;
|
|
937
1210
|
}
|
|
938
1211
|
// If input was just "-" or "+"
|
|
939
1212
|
if (normalized === "") {
|
|
@@ -948,7 +1221,7 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
948
1221
|
if (normalized.startsWith(sep, i)) {
|
|
949
1222
|
if (foundSep !== null) {
|
|
950
1223
|
// More than one separator is rejected in edit mode
|
|
951
|
-
return
|
|
1224
|
+
return EDIT_SYNTAX_ERROR;
|
|
952
1225
|
}
|
|
953
1226
|
foundSep = sep;
|
|
954
1227
|
sepIndex = i;
|
|
@@ -961,23 +1234,28 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
961
1234
|
if (foundSep !== null) {
|
|
962
1235
|
if (decimals === 0) {
|
|
963
1236
|
// No decimal point permitted when decimals === 0
|
|
964
|
-
return { valid: false, complete: false, value: null };
|
|
1237
|
+
return { valid: false, complete: false, value: null, reason: "decimals" };
|
|
965
1238
|
}
|
|
966
1239
|
const intPart = normalized.slice(0, sepIndex);
|
|
967
1240
|
const fracPart = normalized.slice(sepIndex + foundSep.length);
|
|
968
1241
|
// Integer part before separator must consist purely of ASCII digits (or empty, e.g. ".5")
|
|
969
1242
|
if (intPart.length > 0 && !/^\d+$/.test(intPart)) {
|
|
970
|
-
return
|
|
1243
|
+
return EDIT_SYNTAX_ERROR;
|
|
971
1244
|
}
|
|
972
1245
|
// Fractional part after separator must consist purely of ASCII digits (or empty, e.g. "12.")
|
|
973
1246
|
if (fracPart.length > 0 && !/^\d+$/.test(fracPart)) {
|
|
974
|
-
return
|
|
1247
|
+
return EDIT_SYNTAX_ERROR;
|
|
975
1248
|
}
|
|
976
1249
|
// Count fraction digits using normalized ASCII characters (safe against UTF-16 astral splitting)
|
|
977
1250
|
if (fracPart.length > decimals) {
|
|
978
|
-
return { valid: false, complete: false, value: null };
|
|
1251
|
+
return { valid: false, complete: false, value: null, reason: "decimals" };
|
|
1252
|
+
}
|
|
1253
|
+
const parts = parseLocalizedNumberParts(raw, localeId, { mode: "edit" });
|
|
1254
|
+
const parsed = parts === null ? null : convert(parts);
|
|
1255
|
+
// Refuse the keystroke that would make the value too precise for the target type, instead of rounding it.
|
|
1256
|
+
if (parsed === "precision") {
|
|
1257
|
+
return { valid: false, complete: false, value: null, reason: "precision" };
|
|
979
1258
|
}
|
|
980
|
-
const parsed = parseLocalizedNumber(raw, localeId, { mode: "edit" });
|
|
981
1259
|
const complete = fracPart.length > 0 && (intPart.length > 0 || fracPart.length > 0);
|
|
982
1260
|
return {
|
|
983
1261
|
valid: true,
|
|
@@ -987,9 +1265,13 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
987
1265
|
}
|
|
988
1266
|
// No separator found: must be purely ASCII digits
|
|
989
1267
|
if (!/^\d+$/.test(normalized)) {
|
|
990
|
-
return
|
|
1268
|
+
return EDIT_SYNTAX_ERROR;
|
|
1269
|
+
}
|
|
1270
|
+
const parts = parseLocalizedNumberParts(raw, localeId, { mode: "edit" });
|
|
1271
|
+
const parsed = parts === null ? null : convert(parts);
|
|
1272
|
+
if (parsed === "precision") {
|
|
1273
|
+
return { valid: false, complete: false, value: null, reason: "precision" };
|
|
991
1274
|
}
|
|
992
|
-
const parsed = parseLocalizedNumber(raw, localeId, { mode: "edit" });
|
|
993
1275
|
return {
|
|
994
1276
|
valid: true,
|
|
995
1277
|
complete: true,
|
|
@@ -4930,5 +5212,5 @@ function generatePseudoLocale(catalogs, options) {
|
|
|
4930
5212
|
* Generated bundle index. Do not edit.
|
|
4931
5213
|
*/
|
|
4932
5214
|
|
|
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 };
|
|
5215
|
+
export { BIDI_CONTROL_REGEX, MONA_DEFAULT_LOCALE, MONA_FALLBACK_LOCALE_ID, MONA_I18N_CONFIG, MonaDirectionService, MonaI18nService, canonicalizeLocaleId, formatGregorianDateToLocaleString, formatNumber, generatePseudoLocale, getEditDecimalSeparators, getLocaleDateInputFormat, getLocaleDateTimeInputFormat, getLocaleFirstDayOfWeek, getLocaleTimeInputFormat, getNumberFormatter, getNumberGroupingPattern, getNumberSymbols, gregorianDateTime, gregorianDateTimeFromObject, injectComponentDirection, mergeMessages, mergeTwo, normalizeGregorianLocale, normalizeLocalizedDigits, normalizeLocalizedInput, normalizeLocalizedMinus, observeComponentDirection, parseGregorianDate, parseLocalizedDecimal, parseLocalizedNumber, provideMonaI18n, pseudoLocalize, pseudoLocalizeCatalog, resolveComponentDirection, sanitizeDateText, stripBidiControls, tryCanonicalizeLocaleId, validateLocalizedDecimal, validateLocalizedDecimalEdit, validateLocalizedNumber, validateLocalizedNumberEdit };
|
|
4934
5216
|
//# sourceMappingURL=nanahoshi-mona-ui-i18n.mjs.map
|