@mmstack/translate 20.5.4 → 20.5.6

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,6 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, signal, LOCALE_ID, computed, resource, untracked, isDevMode, effect, Injectable, isSignal, input, Renderer2, ElementRef, afterRenderEffect, Directive, ChangeDetectorRef } from '@angular/core';
2
+ import { computed, inject, InjectionToken, LOCALE_ID, signal, resource, untracked, isDevMode, effect, Injectable, isSignal, input, Renderer2, ElementRef, afterRenderEffect, Directive, ChangeDetectorRef } from '@angular/core';
3
3
  import { createIntlCache, createIntl } from '@formatjs/intl';
4
+ import { toSignal } from '@angular/core/rxjs-interop';
5
+ import { Router, ActivatedRoute } from '@angular/router';
4
6
 
5
7
  const KEY_DELIM = '::MMT_DELIM::';
6
8
  function prependDelim(prefix, key) {
@@ -53,28 +55,96 @@ function createNamespace(ns, translation) {
53
55
  return namespace;
54
56
  }
55
57
 
58
+ function pathParam(key, route = inject(ActivatedRoute)) {
59
+ const keySignal = typeof key === 'string' ? computed(() => key) : computed(key);
60
+ const routerOptions = inject(Router)['options'];
61
+ if (routerOptions &&
62
+ typeof routerOptions === 'object' &&
63
+ routerOptions.paramsInheritanceStrategy === 'always') {
64
+ const params = toSignal(route.paramMap, {
65
+ initialValue: route.snapshot.paramMap,
66
+ });
67
+ return computed(() => params().get(keySignal()));
68
+ }
69
+ const paramMapSignals = [];
70
+ let currentRoute = route;
71
+ const isStatic = typeof key === 'string';
72
+ while (currentRoute) {
73
+ const initial = currentRoute.snapshot.paramMap;
74
+ paramMapSignals.push(toSignal(currentRoute.paramMap, {
75
+ initialValue: initial,
76
+ }));
77
+ // For static keys, stop once we find the param, will find first in computed for loop already so basically noop for for loop
78
+ if (isStatic && initial.has(key))
79
+ break;
80
+ currentRoute = currentRoute.parent;
81
+ }
82
+ return computed(() => {
83
+ const paramKey = keySignal();
84
+ for (const map of paramMapSignals) {
85
+ const v = map().get(paramKey);
86
+ if (v)
87
+ return v;
88
+ }
89
+ return null;
90
+ });
91
+ }
92
+
56
93
  const CONFIG_TOKEN = new InjectionToken('mmstack-intl-config');
57
94
  function provideIntlConfig(config) {
58
- return {
59
- useValue: config,
60
- provide: CONFIG_TOKEN,
61
- };
95
+ const providers = [
96
+ {
97
+ useFactory: (localeId) => {
98
+ const next = {
99
+ ...config,
100
+ };
101
+ const defaultLocale = config.defaultLocale ?? config.supportedLocales?.at(0) ?? localeId;
102
+ if (next.supportedLocales &&
103
+ !next.supportedLocales.includes(defaultLocale)) {
104
+ next.supportedLocales = [...next.supportedLocales, defaultLocale];
105
+ }
106
+ return next;
107
+ },
108
+ deps: [LOCALE_ID],
109
+ provide: CONFIG_TOKEN,
110
+ },
111
+ ];
112
+ const defaultLocale = config.defaultLocale ?? config.supportedLocales?.at(0);
113
+ if (!defaultLocale)
114
+ return providers;
115
+ providers.push({
116
+ provide: LOCALE_ID,
117
+ useValue: defaultLocale,
118
+ });
119
+ return providers;
62
120
  }
63
121
  function injectIntlConfig() {
64
122
  return inject(CONFIG_TOKEN, { optional: true }) ?? undefined;
65
123
  }
66
124
  function injectDefaultLocale() {
67
- return injectIntlConfig()?.defaultLocale ?? 'en-US';
125
+ return injectIntlConfig()?.defaultLocale ?? inject(LOCALE_ID) ?? 'en-US';
126
+ }
127
+ function injectSupportedLocales() {
128
+ return injectIntlConfig()?.supportedLocales ?? [injectDefaultLocale()];
129
+ }
130
+ /**
131
+ * @internal
132
+ * the actual locale signal used to store the current locale string
133
+ */
134
+ const STORE_LOCALE = signal('en-US', ...(ngDevMode ? [{ debugName: "STORE_LOCALE" }] : []));
135
+ function injectLocaleInternal() {
136
+ return STORE_LOCALE;
68
137
  }
69
138
  class TranslationStore {
70
139
  cache = createIntlCache();
71
140
  config = injectIntlConfig();
72
141
  loadQueue = signal([], ...(ngDevMode ? [{ debugName: "loadQueue" }] : []));
73
- locale = signal(inject(LOCALE_ID), ...(ngDevMode ? [{ debugName: "locale" }] : []));
142
+ locale;
74
143
  defaultLocale = injectDefaultLocale();
75
144
  translations = signal({
76
145
  [this.defaultLocale]: {},
77
146
  }, ...(ngDevMode ? [{ debugName: "translations" }] : []));
147
+ attemptedFallbackLoad = false;
78
148
  onDemandLoaders = new Map();
79
149
  nonMessageConfig = computed(() => ({
80
150
  ...this.config,
@@ -127,6 +197,23 @@ class TranslationStore {
127
197
  messages: this.messages(),
128
198
  }, this.cache), ...(ngDevMode ? [{ debugName: "intl" }] : []));
129
199
  constructor() {
200
+ this.locale = STORE_LOCALE;
201
+ this.locale.set(injectDefaultLocale());
202
+ const paramName = this.config?.localeParamName;
203
+ if (paramName) {
204
+ const param = pathParam(paramName);
205
+ effect(() => {
206
+ const loc = param();
207
+ if (!loc ||
208
+ loc === untracked(this.locale) ||
209
+ untracked(this.loadQueue).includes(loc))
210
+ return;
211
+ if (this.hasLocaleLoaders(loc))
212
+ this.locale.set(loc);
213
+ else
214
+ this.loadQueue.update((q) => [...q, loc]);
215
+ });
216
+ }
130
217
  effect(() => {
131
218
  if (
132
219
  // should never be in error state, but best to check in case something throws
@@ -136,19 +223,34 @@ class TranslationStore {
136
223
  const dynamicLocales = this.dynamicLocaleLoader.value();
137
224
  if (!dynamicLocales)
138
225
  return;
226
+ // Register loaded translations
139
227
  for (const locale of dynamicLocales.locales) {
140
228
  this.register(locale.namespace, {
141
229
  [dynamicLocales.locale]: locale.flat,
142
230
  });
143
231
  }
144
- this.loadQueue.update((q) => q.filter((l) => l !== dynamicLocales.locale));
145
- this.locale.set(dynamicLocales.locale);
232
+ const hasTranslations = dynamicLocales.locales.length > 0 ||
233
+ this.translations()[dynamicLocales.locale];
234
+ if (hasTranslations) {
235
+ this.loadQueue.update((q) => q.filter((l) => l !== dynamicLocales.locale));
236
+ this.locale.set(dynamicLocales.locale);
237
+ }
146
238
  });
147
239
  }
148
240
  formatMessage(key, values) {
149
- const message = this.translations()[this.locale()]?.[key] ?? '';
150
- if (!message)
241
+ const message = this.translations()[this.locale()]?.[key] ??
242
+ this.translations()[this.defaultLocale]?.[key] ??
243
+ '';
244
+ if (!message) {
245
+ if (this.attemptedFallbackLoad)
246
+ return '';
247
+ this.attemptedFallbackLoad = true;
248
+ untracked(() => {
249
+ if (!this.loadQueue().includes(this.defaultLocale))
250
+ this.loadQueue.update((q) => [...q, this.defaultLocale]);
251
+ });
151
252
  return '';
253
+ }
152
254
  return this.intl().formatMessage({ id: key, defaultMessage: message }, values);
153
255
  }
154
256
  register(namespace, flat) {
@@ -173,10 +275,10 @@ class TranslationStore {
173
275
  hasLocaleLoaders(locale) {
174
276
  return Array.from(this.onDemandLoaders.values()).some((loaders) => loaders[locale]);
175
277
  }
176
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: TranslationStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
177
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: TranslationStore, providedIn: 'root' });
278
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TranslationStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
279
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TranslationStore, providedIn: 'root' });
178
280
  }
179
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: TranslationStore, decorators: [{
281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TranslationStore, decorators: [{
180
282
  type: Injectable,
181
283
  args: [{
182
284
  providedIn: 'root',
@@ -206,12 +308,22 @@ function injectIntl() {
206
308
  */
207
309
  function injectDynamicLocale() {
208
310
  const store = inject(TranslationStore);
311
+ const supportedLocales = injectIntlConfig()?.supportedLocales;
209
312
  const source = computed(() => store.locale());
313
+ const inSupportedLocales = supportedLocales === undefined
314
+ ? () => true
315
+ : (locale) => supportedLocales.includes(locale);
210
316
  const set = (value) => {
211
317
  if (value === untracked(source) ||
212
- !store.hasLocaleLoaders(value) ||
213
318
  untracked(store.loadQueue).includes(value))
214
319
  return;
320
+ if (!inSupportedLocales(value)) {
321
+ if (isDevMode())
322
+ console.warn(`[Translate] Locale "${value}" is not in supportedLocales, switch prevented. Available options are:`, supportedLocales);
323
+ return;
324
+ }
325
+ if (isDevMode() && !store.hasLocaleLoaders(value))
326
+ console.warn(`[Translate] No loaders registered for locale "${value}". Switching to this locale will have no effect.`);
215
327
  store.loadQueue.update((q) => [...q, value]);
216
328
  };
217
329
  source.set = set;
@@ -224,6 +336,245 @@ function injectDynamicLocale() {
224
336
  return source;
225
337
  }
226
338
 
339
+ function unwrap(value) {
340
+ return isSignal(value) ? value() : value;
341
+ }
342
+
343
+ const FORMAT_PRESETS = {
344
+ short: { dateStyle: 'short', timeStyle: 'short' },
345
+ medium: { dateStyle: 'medium', timeStyle: 'medium' },
346
+ long: { dateStyle: 'long', timeStyle: 'long' },
347
+ full: { dateStyle: 'full', timeStyle: 'full' },
348
+ shortDate: { dateStyle: 'short' },
349
+ mediumDate: { dateStyle: 'medium' },
350
+ longDate: { dateStyle: 'long' },
351
+ fullDate: { dateStyle: 'full' },
352
+ shortTime: { timeStyle: 'short' },
353
+ mediumTime: { timeStyle: 'medium' },
354
+ longTime: { timeStyle: 'long' },
355
+ fullTime: { timeStyle: 'full' },
356
+ };
357
+ function validDateOrNull(date) {
358
+ if (date == null)
359
+ return null;
360
+ const d = date instanceof Date ? date : new Date(date);
361
+ return isNaN(d.getTime()) ? null : d;
362
+ }
363
+ const cache$4 = new Map();
364
+ function getFormatter$4(locale, format, timeZone) {
365
+ const cacheKey = `${locale}|${format}|${timeZone ?? ''}`;
366
+ let formatter = cache$4.get(cacheKey);
367
+ if (!formatter) {
368
+ formatter = new Intl.DateTimeFormat(locale, {
369
+ ...FORMAT_PRESETS[format],
370
+ timeZone,
371
+ });
372
+ cache$4.set(cacheKey, formatter);
373
+ }
374
+ return formatter;
375
+ }
376
+ /**
377
+ * Format a date using the current or provided locale & timezone
378
+ * By default it is reactive to the global dynamic locale, works best when wrapped in a computed() if you need to react to locale changes
379
+ *
380
+ * @param date - Date to format
381
+ * @param opt - Options for formatting
382
+ * @returns Formatted date string
383
+ */
384
+ function formatDate(date, opt) {
385
+ const validDate = validDateOrNull(unwrap(date));
386
+ if (validDate === null)
387
+ return '';
388
+ const unwrappedOpt = unwrap(opt);
389
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
390
+ return getFormatter$4(loc, unwrappedOpt?.format ?? 'medium', unwrappedOpt?.tz).format(validDate);
391
+ }
392
+
393
+ const cache$3 = new Map();
394
+ function getFormatter$3(locale, type, style) {
395
+ const cacheKey = `${locale}|${type}|${style}`;
396
+ let formatter = cache$3.get(cacheKey);
397
+ if (!formatter) {
398
+ formatter = new Intl.DisplayNames(locale, {
399
+ type,
400
+ style,
401
+ });
402
+ cache$3.set(cacheKey, formatter);
403
+ }
404
+ return formatter;
405
+ }
406
+ /**
407
+ * Format a display name using the current or provided locale
408
+ * By default it is reactive to the global dynamic locale, works best when wrapped in a computed() if you need to react to locale changes
409
+ *
410
+ * @param value - The code to format
411
+ * @param type - The type of display name to format
412
+ * @param opt - Options for formatting
413
+ * @returns Formatted display name string
414
+ */
415
+ function formatDisplayName(value, type, opt) {
416
+ const unwrapped = unwrap(value);
417
+ if (!unwrapped?.trim())
418
+ return '';
419
+ const unwrappedType = unwrap(type);
420
+ const unwrappedOpt = unwrap(opt);
421
+ const locale = unwrappedOpt?.locale ?? injectLocaleInternal()();
422
+ return (getFormatter$3(locale, unwrappedType, unwrappedOpt?.style ?? 'long').of(unwrapped) ?? '');
423
+ }
424
+
425
+ const cache$2 = new Map();
426
+ const EMPTY_ARRAY = [];
427
+ function unwrapList(value) {
428
+ const unwrapped = unwrap(value);
429
+ return Array.isArray(unwrapped) ? unwrapped : EMPTY_ARRAY;
430
+ }
431
+ function getFormatter$2(locale, type, style) {
432
+ const cacheKey = `${locale}|${type}|${style}`;
433
+ let formatter = cache$2.get(cacheKey);
434
+ if (!formatter) {
435
+ formatter = new Intl.ListFormat(locale, { type, style });
436
+ cache$2.set(cacheKey, formatter);
437
+ }
438
+ return formatter;
439
+ }
440
+ /**
441
+ * Format a list using the current or provided locale
442
+ * By default it is reactive to the global dynamic locale, works best when wrapped in a computed() if you need to react to locale changes
443
+ *
444
+ * @param value - The list to format
445
+ * @param opt - Options for formatting
446
+ * @returns Formatted list string
447
+ */
448
+ function formatList(value, opt) {
449
+ const unwrapped = unwrapList(value);
450
+ if (unwrapped.length === 0)
451
+ return '';
452
+ const unwrappedOpt = unwrap(opt);
453
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
454
+ return getFormatter$2(loc, unwrappedOpt?.type ?? 'conjunction', unwrappedOpt?.style ?? 'long').format(unwrapped);
455
+ }
456
+
457
+ const cache$1 = new Map();
458
+ function unwrapValue(value, fallbackToZero = false) {
459
+ const unwrapped = unwrap(value);
460
+ if (unwrapped === null || unwrapped === undefined || isNaN(unwrapped))
461
+ return fallbackToZero ? 0 : null;
462
+ return unwrapped;
463
+ }
464
+ function getFormatter$1(locale, minFractionDigits, maxFractionDigits, useGrouping, notation, currency, display, style) {
465
+ const cacheKey = `${locale}|${notation ?? 'standard'}|${minFractionDigits}|${maxFractionDigits}|${useGrouping ?? true}|${currency ?? 'none'}|${display ?? 'none'}|${style ?? 'decimal'}`;
466
+ let formatter = cache$1.get(cacheKey);
467
+ if (!formatter) {
468
+ formatter = new Intl.NumberFormat(locale, {
469
+ style,
470
+ notation,
471
+ minimumFractionDigits: minFractionDigits,
472
+ maximumFractionDigits: maxFractionDigits,
473
+ useGrouping,
474
+ currency,
475
+ currencyDisplay: display,
476
+ });
477
+ cache$1.set(cacheKey, formatter);
478
+ }
479
+ return formatter;
480
+ }
481
+ /**
482
+ * Format a number using the current or provided locale
483
+ * By default it is reactive to the global dynamic locale, works best when wrapped in a computed() if you need to react to locale changes
484
+ *
485
+ * @param number - Number to format
486
+ * @param opt - Options for formatting
487
+ * @returns Formatted number string
488
+ */
489
+ function formatNumber(value, opt) {
490
+ const unwrappedOpt = unwrap(opt);
491
+ const unwrappedNumber = unwrapValue(value, unwrappedOpt?.fallbackToZero);
492
+ if (unwrappedNumber === null)
493
+ return '';
494
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
495
+ return getFormatter$1(loc, unwrappedOpt?.minFractionDigits, unwrappedOpt?.maxFractionDigits, unwrappedOpt?.useGrouping ?? true, unwrappedOpt?.notation ?? 'standard').format(unwrappedNumber);
496
+ }
497
+ /**
498
+ * Format a percentage using the current or provided locale
499
+ * By default it is reactive to the global dynamic locale, works best when wrapped in a computed() if you need to react to locale changes
500
+ *
501
+ * @param number - Number to format
502
+ * @param opt - Options for formatting
503
+ * @returns Formatted percentage string
504
+ */
505
+ function formatPercent(value, opt) {
506
+ const unwrappedOpt = unwrap(opt);
507
+ const unwrappedNumber = unwrapValue(value, unwrappedOpt?.fallbackToZero);
508
+ if (unwrappedNumber === null)
509
+ return '';
510
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
511
+ return getFormatter$1(loc, unwrappedOpt?.minFractionDigits, unwrappedOpt?.maxFractionDigits, undefined, undefined, undefined, undefined, 'percent').format(unwrappedNumber);
512
+ }
513
+ function formatCurrency(value, currency, opt) {
514
+ const unwrappedOpt = unwrap(opt);
515
+ const unwrappedValue = unwrapValue(value, unwrappedOpt?.fallbackToZero);
516
+ if (unwrappedValue === null)
517
+ return '';
518
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
519
+ return getFormatter$1(loc, undefined, undefined, undefined, undefined, unwrap(currency), unwrappedOpt?.display ?? 'symbol', 'currency').format(unwrappedValue);
520
+ }
521
+
522
+ const cache = new Map();
523
+ function getFormatter(locale, style, numeric) {
524
+ const cacheKey = `${locale}|${style}|${numeric}`;
525
+ let formatter = cache.get(cacheKey);
526
+ if (!formatter) {
527
+ formatter = new Intl.RelativeTimeFormat(locale, { style, numeric });
528
+ cache.set(cacheKey, formatter);
529
+ }
530
+ return formatter;
531
+ }
532
+ /**
533
+ * Format a relative time using the current or provided locale
534
+ * By default it is reactive to the global dynamic locale, works best when wrapped in a computed() if you need to react to locale changes
535
+ *
536
+ * @param value - The numeric value to use in the relative time internationalization message
537
+ * @param unit - The unit to use in the relative time internationalization message
538
+ * @param opt - Options for formatting
539
+ * @returns Formatted relative time string
540
+ */
541
+ function formatRelativeTime(value, unit, opt) {
542
+ const unwrappedValue = unwrap(value);
543
+ if (unwrappedValue === null ||
544
+ unwrappedValue === undefined ||
545
+ isNaN(unwrappedValue))
546
+ return '';
547
+ const unwrappedUnit = unwrap(unit);
548
+ if (!unwrappedUnit)
549
+ return '';
550
+ const unwrappedOpt = unwrap(opt);
551
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
552
+ return getFormatter(loc, unwrappedOpt?.style ?? 'long', unwrappedOpt?.numeric ?? 'always').format(unwrappedValue, unwrappedUnit);
553
+ }
554
+
555
+ function injectResolveParamLocale(snapshot) {
556
+ let locale = null;
557
+ const paramName = injectIntlConfig()?.localeParamName;
558
+ const routerConfig = inject(Router)['options'];
559
+ const alwaysInheritParams = typeof routerConfig === 'object' &&
560
+ !!routerConfig &&
561
+ routerConfig.paramsInheritanceStrategy === 'always';
562
+ if (paramName) {
563
+ locale = snapshot.paramMap.get(paramName);
564
+ if (!locale && !alwaysInheritParams) {
565
+ let currentRoute = snapshot;
566
+ while (currentRoute && !locale) {
567
+ locale = currentRoute.paramMap.get('locale');
568
+ currentRoute = currentRoute.parent;
569
+ }
570
+ }
571
+ }
572
+ if (!locale) {
573
+ locale = untracked(inject(TranslationStore).locale);
574
+ }
575
+ return locale;
576
+ }
577
+
227
578
  function createEqualsRecord(keys = []) {
228
579
  let keyMatcher;
229
580
  if (keys.length === 0) {
@@ -276,10 +627,11 @@ function registerNamespace(defaultTranslation, other) {
276
627
  return addSignalFn(createT(store), store);
277
628
  };
278
629
  let defaultTranslationLoaded = false;
279
- const resolver = async () => {
630
+ const resolver = async (snapshot) => {
280
631
  const store = inject(TranslationStore);
281
- const locale = untracked(store.locale);
632
+ const locale = injectResolveParamLocale(snapshot);
282
633
  const defaultLocale = injectDefaultLocale();
634
+ const shouldPreloadDefault = injectIntlConfig()?.preloadDefaultLocale ?? false;
283
635
  const tPromise = other[locale];
284
636
  const promise = tPromise ?? defaultTranslation;
285
637
  if (!promise && isDevMode()) {
@@ -288,28 +640,110 @@ function registerNamespace(defaultTranslation, other) {
288
640
  if (promise === defaultTranslation && defaultTranslationLoaded)
289
641
  return;
290
642
  try {
291
- const translation = await promise();
292
- if (promise !== defaultTranslation &&
293
- translation.locale !== locale &&
294
- isDevMode()) {
295
- return console.warn(`Expected locale to be ${locale} but got ${translation.locale}`);
296
- }
297
- store.registerOnDemandLoaders(translation.namespace, {
643
+ const promises = [promise()];
644
+ if (shouldPreloadDefault &&
645
+ !defaultTranslationLoaded &&
646
+ promise !== defaultTranslation)
647
+ promises.push(defaultTranslation());
648
+ const translations = await Promise.allSettled(promises);
649
+ const fullfilled = translations.map((t) => t.status === 'fulfilled' ? t.value : null);
650
+ if (fullfilled.at(0) === null && fullfilled.at(1) === null)
651
+ throw new Error('Failed to load translations');
652
+ const [t, defaultT] = fullfilled;
653
+ const ns = t?.namespace ?? defaultT?.namespace;
654
+ if (!ns)
655
+ throw new Error('No namespace found in translation');
656
+ if (isDevMode() && t && t.locale !== locale && t.locale)
657
+ console.warn(`Expected locale to be ${locale} but got ${t.locale}`);
658
+ store.registerOnDemandLoaders(ns, {
298
659
  ...other,
299
660
  [defaultLocale]: defaultTranslation,
300
661
  });
301
- store.register(translation.namespace, {
302
- [locale]: translation.flat,
303
- });
304
- if (promise === defaultTranslation) {
662
+ const toRegister = {};
663
+ if (t)
664
+ toRegister[locale] = t.flat;
665
+ if (defaultT)
666
+ toRegister[defaultLocale] = defaultT.flat;
667
+ store.register(ns, toRegister);
668
+ if (promise === defaultTranslation || defaultT)
305
669
  defaultTranslationLoaded = true;
670
+ }
671
+ catch {
672
+ if (isDevMode()) {
673
+ console.warn(`Failed to load translation for locale: ${locale}`);
306
674
  }
307
675
  }
676
+ finally {
677
+ if (locale !== untracked(store.locale))
678
+ store.locale.set(locale);
679
+ }
680
+ };
681
+ return {
682
+ injectNamespaceT: injectT,
683
+ resolveNamespaceTranslation: resolver,
684
+ };
685
+ }
686
+ /**
687
+ * Registers a type-unsafe namespace, meant for remote loading of unknown key-value pairs using mmstack/translate infrastructure
688
+ * The resolver & t function work the same as they would with typed namespaces, but without type safety
689
+ */
690
+ function registerRemoteNamespace(ns, defaultTranslation, other) {
691
+ const injectT = () => {
692
+ const store = inject(TranslationStore);
693
+ return addSignalFn(createT(store), store);
694
+ };
695
+ let defaultTranslationLoaded = false;
696
+ const resolver = async (snapshot) => {
697
+ const store = inject(TranslationStore);
698
+ const locale = injectResolveParamLocale(snapshot);
699
+ const defaultLocale = injectDefaultLocale();
700
+ const shouldPreloadDefault = injectIntlConfig()?.preloadDefaultLocale ?? false;
701
+ const tPromise = other[locale];
702
+ const promise = tPromise ?? defaultTranslation;
703
+ if (!promise && isDevMode()) {
704
+ return console.warn(`No translation found for locale: ${locale}`);
705
+ }
706
+ if (promise === defaultTranslation && defaultTranslationLoaded)
707
+ return;
708
+ try {
709
+ const promises = [promise()];
710
+ if (shouldPreloadDefault &&
711
+ !defaultTranslationLoaded &&
712
+ promise !== defaultTranslation)
713
+ promises.push(defaultTranslation());
714
+ const translations = await Promise.allSettled(promises);
715
+ const fullfilled = translations.map((t) => t.status === 'fulfilled' ? t.value : null);
716
+ if (fullfilled.at(0) === null && fullfilled.at(1) === null)
717
+ throw new Error('Failed to load translations');
718
+ const [baseT, baseDefaultT] = fullfilled;
719
+ const t = baseT ? compileTranslation(baseT, ns, locale) : null;
720
+ const defaultT = baseDefaultT
721
+ ? compileTranslation(baseDefaultT, ns, defaultLocale)
722
+ : null;
723
+ if (isDevMode() && t && t.locale !== locale && t.locale)
724
+ console.warn(`Expected locale to be ${locale} but got ${t.locale}`);
725
+ store.registerOnDemandLoaders(ns, {
726
+ ...other,
727
+ [defaultLocale]: defaultTranslation,
728
+ });
729
+ const toRegister = {};
730
+ if (t)
731
+ toRegister[locale] = t.flat;
732
+ if (defaultT)
733
+ toRegister[defaultLocale] = defaultT.flat;
734
+ store.register(ns, toRegister);
735
+ if (promise === defaultTranslation || defaultT)
736
+ defaultTranslationLoaded = true;
737
+ }
308
738
  catch {
309
739
  if (isDevMode()) {
310
740
  console.warn(`Failed to load translation for locale: ${locale}`);
311
741
  }
312
742
  }
743
+ finally {
744
+ if (locale !== untracked(store.locale))
745
+ store.locale.set(locale);
746
+ }
313
747
  };
314
748
  return {
315
749
  injectNamespaceT: injectT,
@@ -317,6 +751,91 @@ function registerNamespace(defaultTranslation, other) {
317
751
  };
318
752
  }
319
753
 
754
+ /**
755
+ * Guard that validates the locale parameter against supported locales.
756
+ * Redirects to default locale if the locale is invalid.
757
+ *
758
+ * @param prefixSegments Optional array of path segments preceding the locale segment.
759
+ * if (you wanted to match /app/:locale/... you would pass ['app'] here) & the function would match the second parameter + redirect accordingly
760
+ *
761
+ * @example
762
+ * ```typescript
763
+ * {
764
+ * path: ':locale',
765
+ * canMatch: [canMatchLocale()],
766
+ * children: [...]
767
+ * }
768
+ * ```
769
+ */
770
+ function canMatchLocale(prefixSegments = []) {
771
+ return (_route, segments) => {
772
+ const supportedLocales = injectSupportedLocales();
773
+ const locale = segments.at(prefixSegments.length)?.path;
774
+ if (!locale || !supportedLocales.includes(locale))
775
+ return inject(Router).createUrlTree([
776
+ ...prefixSegments,
777
+ injectDefaultLocale(),
778
+ ]);
779
+ return true;
780
+ };
781
+ }
782
+
783
+ /**
784
+ * Provides an isolated mock `TranslationStore` usable across testing modules that use components
785
+ * depending on `@mmstack/translate` APIs (like `Translate` directive, `Translator` pipe, or `injectNamespaceT`).
786
+ *
787
+ * This provider intercepts all translation logic, bypassing chunk loaders and Intl.
788
+ * When a custom configuration isn't provided, formatMessage simply echoes the translation key, using dots `.`.
789
+ *
790
+ * ### Usage
791
+ * ```typescript
792
+ * TestBed.configureTestingModule({
793
+ * providers: [provideMockTranslations()]
794
+ * });
795
+ * ```
796
+ */
797
+ function provideMockTranslations(options) {
798
+ // We compile the mock strings to flat delimiters just like the internal compile module.
799
+ const mappedMocks = {};
800
+ if (options?.translations) {
801
+ for (const [namespace, translationObj] of Object.entries(options.translations)) {
802
+ const compiled = compileTranslation(translationObj, namespace);
803
+ for (const [key, val] of Object.entries(compiled.flat)) {
804
+ // e.g. from 'home::MMT_DELIM::title'
805
+ const fullKey = `${namespace}::MMT_DELIM::${key}`;
806
+ mappedMocks[fullKey] = val;
807
+ }
808
+ }
809
+ }
810
+ return [
811
+ {
812
+ provide: TranslationStore,
813
+ useValue: {
814
+ locale: signal('en-US'),
815
+ formatMessage: (key) => {
816
+ if (mappedMocks[key])
817
+ return mappedMocks[key];
818
+ // Fallback to echoing the key back in dot notation (more readable for unit assertions).
819
+ return key.replaceAll('::MMT_DELIM::', '.');
820
+ },
821
+ hasLocaleLoaders: () => false,
822
+ register: () => {
823
+ // noop
824
+ },
825
+ registerOnDemandLoaders: () => {
826
+ // noop
827
+ },
828
+ dynamicLocaleLoader: {
829
+ isLoading: signal(false),
830
+ value: signal(null),
831
+ error: signal(null),
832
+ },
833
+ loadQueue: signal([]),
834
+ },
835
+ },
836
+ ];
837
+ }
838
+
320
839
  class Translate {
321
840
  t = createT(inject(TranslationStore));
322
841
  translate = input.required(...(ngDevMode ? [{ debugName: "translate" }] : []));
@@ -362,12 +881,12 @@ class Translate {
362
881
  },
363
882
  });
364
883
  }
365
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: Translate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
366
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.1", type: Translate, isStandalone: true, inputs: { translate: { classPropertyName: "translate", publicName: "translate", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
884
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: Translate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
885
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.17", type: Translate, isStandalone: true, inputs: { translate: { classPropertyName: "translate", publicName: "translate", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
367
886
  }
368
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: Translate, decorators: [{
887
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: Translate, decorators: [{
369
888
  type: Directive
370
- }], ctorParameters: () => [] });
889
+ }], ctorParameters: () => [], propDecorators: { translate: [{ type: i0.Input, args: [{ isSignal: true, alias: "translate", required: true }] }] } });
371
890
 
372
891
  class Translator {
373
892
  store = inject(TranslationStore);
@@ -389,5 +908,5 @@ class Translator {
389
908
  * Generated bundle index. Do not edit.
390
909
  */
391
910
 
392
- export { Translate, Translator, compileTranslation, createNamespace, injectDynamicLocale, injectIntl, provideIntlConfig, registerNamespace };
911
+ export { Translate, Translator, canMatchLocale, compileTranslation, createNamespace, formatCurrency, formatDate, formatDisplayName, formatList, formatNumber, formatPercent, formatRelativeTime, injectDynamicLocale, injectIntl, injectResolveParamLocale, injectSupportedLocales, provideIntlConfig, provideMockTranslations, registerNamespace, registerRemoteNamespace };
393
912
  //# sourceMappingURL=mmstack-translate.mjs.map