@mmstack/translate 20.5.11 → 20.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { computed, inject, InjectionToken, LOCALE_ID, signal, isDevMode, effect, resource, untracked, Injectable, isSignal, input, Renderer2, ElementRef, afterRenderEffect, Directive, ChangeDetectorRef } from '@angular/core';
2
+ import { computed, inject, InjectionToken, LOCALE_ID, signal, untracked, isDevMode, effect, resource, Injectable, isSignal, input, Renderer2, ElementRef, afterRenderEffect, Directive, ChangeDetectorRef } from '@angular/core';
3
3
  import { createIntlCache, createIntl } from '@formatjs/intl';
4
4
  import { toSignal } from '@angular/core/rxjs-interop';
5
5
  import { Router, ActivatedRoute } from '@angular/router';
@@ -55,6 +55,10 @@ function createNamespace(ns, translation) {
55
55
  return namespace;
56
56
  }
57
57
 
58
+ /**
59
+ * Dynamic path parameter signal, that allows resolution even when paramsInheritenceStrategy isnt always
60
+ * This helper depends on an internal
61
+ */
58
62
  function pathParam(key, route = inject(ActivatedRoute)) {
59
63
  const keySignal = typeof key === 'string' ? computed(() => key) : computed(key);
60
64
  const routerOptions = inject(Router)['options'];
@@ -132,9 +136,27 @@ function injectSupportedLocales() {
132
136
  * the actual locale signal used to store the current locale string
133
137
  */
134
138
  const STORE_LOCALE = signal('en-US', ...(ngDevMode ? [{ debugName: "STORE_LOCALE" }] : []));
139
+ /**
140
+ * @internal
141
+ * @deprecated will be removed when ng23 drops
142
+ */
143
+ function readLocaleUnsafe() {
144
+ return STORE_LOCALE();
145
+ }
135
146
  function injectLocaleInternal() {
136
147
  return STORE_LOCALE;
137
148
  }
149
+ function proxyToGlobalSingleton(src) {
150
+ const originalSet = src.set;
151
+ src.set = (next) => {
152
+ originalSet(next);
153
+ STORE_LOCALE.set(next);
154
+ };
155
+ src.update = (updater) => {
156
+ src.set(updater(untracked(src)));
157
+ };
158
+ return src;
159
+ }
138
160
  function isDynamicConfig(cfg) {
139
161
  return !!cfg && 'localeStorage' in cfg && !!cfg.localeStorage;
140
162
  }
@@ -232,7 +254,7 @@ class TranslationStore {
232
254
  messages: this.messages(),
233
255
  }, this.cache), ...(ngDevMode ? [{ debugName: "intl" }] : []));
234
256
  constructor() {
235
- this.locale = initLocale(STORE_LOCALE);
257
+ this.locale = proxyToGlobalSingleton(initLocale(signal('en-US')));
236
258
  const paramName = this.config?.localeParamName;
237
259
  if (paramName) {
238
260
  const param = pathParam(paramName);
@@ -426,6 +448,53 @@ function injectAddTranslations() {
426
448
  };
427
449
  }
428
450
 
451
+ function equalLocale(a, b) {
452
+ return a.locale === b.locale;
453
+ }
454
+ function createFormatterProvider(formatterName, libraryDefaults, nonLocaleEqual) {
455
+ const token = new InjectionToken(`@mmstack/translate:format-${formatterName}-config`, {
456
+ factory: () => {
457
+ const loc = injectDynamicLocale();
458
+ return computed(() => ({
459
+ ...libraryDefaults,
460
+ locale: loc(),
461
+ }), { equal: equalLocale });
462
+ },
463
+ });
464
+ const provider = (valueOrFn) => {
465
+ const fnProvider = typeof valueOrFn === 'function'
466
+ ? valueOrFn
467
+ : () => valueOrFn;
468
+ return {
469
+ provide: token,
470
+ useFactory: () => {
471
+ const loc = injectDynamicLocale();
472
+ const providedDefaultsOrSignal = fnProvider();
473
+ if (isSignal(providedDefaultsOrSignal))
474
+ return computed(() => ({
475
+ ...libraryDefaults,
476
+ ...providedDefaultsOrSignal(),
477
+ locale: loc(),
478
+ }), {
479
+ equal: (a, b) => equalLocale(a, b) && nonLocaleEqual(a, b),
480
+ });
481
+ const defaults = {
482
+ ...libraryDefaults,
483
+ ...providedDefaultsOrSignal,
484
+ };
485
+ return computed(() => ({
486
+ ...defaults,
487
+ locale: loc(),
488
+ }), {
489
+ equal: equalLocale,
490
+ });
491
+ },
492
+ };
493
+ };
494
+ const injectFn = () => inject(token);
495
+ return [provider, injectFn];
496
+ }
497
+
429
498
  function unwrap(value) {
430
499
  return isSignal(value) ? value() : value;
431
500
  }
@@ -452,32 +521,68 @@ function validDateOrNull(date) {
452
521
  }
453
522
  const cache$4 = new Map();
454
523
  function getFormatter$4(locale, format, timeZone) {
455
- const cacheKey = `${locale}|${format}|${timeZone ?? ''}`;
524
+ const cacheKey = `${locale}|${typeof format === 'string' ? format : JSON.stringify(format)}|${timeZone ?? ''}`;
456
525
  let formatter = cache$4.get(cacheKey);
457
526
  if (!formatter) {
458
527
  formatter = new Intl.DateTimeFormat(locale, {
459
- ...FORMAT_PRESETS[format],
528
+ ...(typeof format === 'string' ? FORMAT_PRESETS[format] : format),
460
529
  timeZone,
461
530
  });
462
531
  cache$4.set(cacheKey, formatter);
463
532
  }
464
533
  return formatter;
465
534
  }
466
- /**
467
- * Format a date using the current or provided locale & timezone
468
- * 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
469
- *
470
- * @param date - Date to format
471
- * @param opt - Options for formatting
472
- * @returns Formatted date string
473
- */
474
- function formatDate(date, opt) {
535
+ function formatDate(date, optOrLocale) {
475
536
  const validDate = validDateOrNull(unwrap(date));
476
537
  if (validDate === null)
477
538
  return '';
478
- const unwrappedOpt = unwrap(opt);
479
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
480
- return getFormatter$4(loc, unwrappedOpt?.format ?? 'medium', unwrappedOpt?.tz).format(validDate);
539
+ const unwrappedArgs = unwrap(optOrLocale);
540
+ let locale;
541
+ let format = 'medium';
542
+ let tz;
543
+ if (typeof unwrappedArgs === 'string') {
544
+ locale = unwrappedArgs;
545
+ }
546
+ else if (unwrappedArgs && typeof unwrappedArgs === 'object') {
547
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
548
+ format = unwrappedArgs.format ?? 'medium';
549
+ tz = unwrappedArgs.tz;
550
+ }
551
+ else {
552
+ locale = readLocaleUnsafe();
553
+ }
554
+ return getFormatter$4(locale, format, tz).format(validDate);
555
+ }
556
+ const [provideFormatDateDefaults, injectFormatDateOptions] = createFormatterProvider('date', {
557
+ format: 'medium',
558
+ }, (a, b) => {
559
+ if (a.tz !== b.tz)
560
+ return false;
561
+ if (a.format === b.format)
562
+ return true;
563
+ return JSON.stringify(a.format) === JSON.stringify(b.format);
564
+ });
565
+ /**
566
+ * Inject a context-safe date formatting function tied to the current injector.
567
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
568
+ * @example
569
+ * const formatDate = injectFormatDate();
570
+ * readonly displayDate = computed(() => formatDate(this.date()));
571
+ */
572
+ function injectFormatDate() {
573
+ const defaults = injectFormatDateOptions();
574
+ return (date, optOrLocale) => {
575
+ if (!optOrLocale)
576
+ return formatDate(date, defaults());
577
+ const unwrapped = unwrap(optOrLocale);
578
+ const opt = typeof unwrapped === 'object'
579
+ ? { ...defaults(), ...unwrapped }
580
+ : {
581
+ ...defaults(),
582
+ locale: unwrapped,
583
+ };
584
+ return formatDate(date, opt);
585
+ };
481
586
  }
482
587
 
483
588
  const cache$3 = new Map();
@@ -495,21 +600,48 @@ function getFormatter$3(locale, type, style) {
495
600
  }
496
601
  /**
497
602
  * Format a display name using the current or provided locale
498
- * 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
499
603
  *
500
604
  * @param value - The code to format
501
605
  * @param type - The type of display name to format
502
606
  * @param opt - Options for formatting
503
607
  * @returns Formatted display name string
504
608
  */
505
- function formatDisplayName(value, type, opt) {
506
- const unwrapped = unwrap(value);
507
- if (!unwrapped?.trim())
609
+ function formatDisplayName(value, type, localeOrOpt) {
610
+ const unwrappedValue = unwrap(value);
611
+ if (!unwrappedValue?.trim())
508
612
  return '';
509
613
  const unwrappedType = unwrap(type);
510
- const unwrappedOpt = unwrap(opt);
511
- const locale = unwrappedOpt?.locale ?? injectLocaleInternal()();
512
- return (getFormatter$3(locale, unwrappedType, unwrappedOpt?.style ?? 'long').of(unwrapped) ?? '');
614
+ const unwrapped = unwrap(localeOrOpt);
615
+ const locale = typeof unwrapped === 'string'
616
+ ? unwrapped
617
+ : (unwrapped?.locale ?? readLocaleUnsafe());
618
+ const opt = typeof unwrapped === 'object' ? unwrapped : undefined;
619
+ return (getFormatter$3(locale, unwrappedType, opt?.style ?? 'long').of(unwrappedValue) ?? '');
620
+ }
621
+ const [provideFormatDisplayNameDefaults, injectFormatDisplayNameDefaults] = createFormatterProvider('displayName', {
622
+ style: 'long',
623
+ }, (a, b) => a.style === b.style);
624
+ /**
625
+ * Inject a context-safe date formatting function tied to the current injector.
626
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
627
+ * @example
628
+ * const formatDisplayName = injectFormatDisplayName();
629
+ * readonly region = computed(() => formatDisplayName('US', 'region'));
630
+ */
631
+ function injectFormatDisplayName() {
632
+ const defaults = injectFormatDisplayNameDefaults();
633
+ return (value, type, localeOrOpt) => {
634
+ if (!localeOrOpt)
635
+ return formatDisplayName(value, type, defaults());
636
+ const unwrapped = unwrap(localeOrOpt);
637
+ const opt = typeof unwrapped === 'object'
638
+ ? { ...defaults(), ...unwrapped }
639
+ : {
640
+ ...defaults(),
641
+ locale: unwrapped,
642
+ };
643
+ return formatDisplayName(value, type, opt);
644
+ };
513
645
  }
514
646
 
515
647
  const cache$2 = new Map();
@@ -527,21 +659,52 @@ function getFormatter$2(locale, type, style) {
527
659
  }
528
660
  return formatter;
529
661
  }
662
+ function formatList(value, optOrLocale) {
663
+ const unwrappedValue = unwrapList(value);
664
+ if (unwrappedValue.length === 0)
665
+ return '';
666
+ const unwrappedArgs = unwrap(optOrLocale);
667
+ let locale;
668
+ let type = 'conjunction';
669
+ let style = 'long';
670
+ if (typeof unwrappedArgs === 'string') {
671
+ locale = unwrappedArgs;
672
+ }
673
+ else if (unwrappedArgs && typeof unwrappedArgs === 'object') {
674
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
675
+ type = unwrappedArgs.type ?? 'conjunction';
676
+ style = unwrappedArgs.style ?? 'long';
677
+ }
678
+ else {
679
+ locale = readLocaleUnsafe();
680
+ }
681
+ return getFormatter$2(locale, type, style).format(unwrappedValue);
682
+ }
683
+ const [provideFormatListDefaults, injectFormatListOptions] = createFormatterProvider('list', {
684
+ type: 'conjunction',
685
+ style: 'long',
686
+ }, (a, b) => a.type === b.type && a.style === b.style);
530
687
  /**
531
- * Format a list using the current or provided locale
532
- * 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
533
- *
534
- * @param value - The list to format
535
- * @param opt - Options for formatting
536
- * @returns Formatted list string
688
+ * Inject a context-safe list formatting function tied to the current injector.
689
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
690
+ * @example
691
+ * const formatList = injectFormatList();
692
+ * readonly displayList = computed(() => formatList(this.items()));
537
693
  */
538
- function formatList(value, opt) {
539
- const unwrapped = unwrapList(value);
540
- if (unwrapped.length === 0)
541
- return '';
542
- const unwrappedOpt = unwrap(opt);
543
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
544
- return getFormatter$2(loc, unwrappedOpt?.type ?? 'conjunction', unwrappedOpt?.style ?? 'long').format(unwrapped);
694
+ function injectFormatList() {
695
+ const defaults = injectFormatListOptions();
696
+ return (value, optOrLocale) => {
697
+ if (!optOrLocale)
698
+ return formatList(value, defaults());
699
+ const unwrapped = unwrap(optOrLocale);
700
+ const opt = typeof unwrapped === 'object'
701
+ ? { ...defaults(), ...unwrapped }
702
+ : {
703
+ ...defaults(),
704
+ locale: unwrapped,
705
+ };
706
+ return formatList(value, opt);
707
+ };
545
708
  }
546
709
 
547
710
  const cache$1 = new Map();
@@ -568,45 +731,151 @@ function getFormatter$1(locale, minFractionDigits, maxFractionDigits, useGroupin
568
731
  }
569
732
  return formatter;
570
733
  }
571
- /**
572
- * Format a number using the current or provided locale
573
- * 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
574
- *
575
- * @param number - Number to format
576
- * @param opt - Options for formatting
577
- * @returns Formatted number string
578
- */
579
- function formatNumber(value, opt) {
580
- const unwrappedOpt = unwrap(opt);
581
- const unwrappedNumber = unwrapValue(value, unwrappedOpt?.fallbackToZero);
734
+ function formatNumber(value, optOrLocale) {
735
+ const unwrappedArgs = unwrap(optOrLocale);
736
+ const isOpt = unwrappedArgs != null && typeof unwrappedArgs === 'object';
737
+ const fallbackToZero = isOpt ? unwrappedArgs.fallbackToZero : undefined;
738
+ const unwrappedNumber = unwrapValue(value, fallbackToZero);
582
739
  if (unwrappedNumber === null)
583
740
  return '';
584
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
585
- return getFormatter$1(loc, unwrappedOpt?.minFractionDigits, unwrappedOpt?.maxFractionDigits, unwrappedOpt?.useGrouping ?? true, unwrappedOpt?.notation ?? 'standard').format(unwrappedNumber);
741
+ let locale;
742
+ let notation;
743
+ let minFractionDigits;
744
+ let maxFractionDigits;
745
+ let useGrouping = true;
746
+ if (typeof unwrappedArgs === 'string') {
747
+ locale = unwrappedArgs;
748
+ }
749
+ else if (isOpt) {
750
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
751
+ notation = unwrappedArgs.notation ?? 'standard';
752
+ minFractionDigits = unwrappedArgs.minFractionDigits;
753
+ maxFractionDigits = unwrappedArgs.maxFractionDigits;
754
+ useGrouping = unwrappedArgs.useGrouping ?? true;
755
+ }
756
+ else {
757
+ locale = readLocaleUnsafe();
758
+ notation = 'standard';
759
+ }
760
+ return getFormatter$1(locale, minFractionDigits, maxFractionDigits, useGrouping, notation).format(unwrappedNumber);
586
761
  }
762
+ const [provideFormatNumberDefaults, injectFormatNumberOptions] = createFormatterProvider('number', {
763
+ notation: 'standard',
764
+ useGrouping: true,
765
+ }, (a, b) => a.notation === b.notation &&
766
+ a.minFractionDigits === b.minFractionDigits &&
767
+ a.maxFractionDigits === b.maxFractionDigits &&
768
+ a.useGrouping === b.useGrouping &&
769
+ a.fallbackToZero === b.fallbackToZero);
587
770
  /**
588
- * Format a percentage using the current or provided locale
589
- * 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
590
- *
591
- * @param number - Number to format
592
- * @param opt - Options for formatting
593
- * @returns Formatted percentage string
771
+ * Inject a context-safe number formatting function tied to the current injector.
772
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
773
+ * @example
774
+ * const formatNumber = injectFormatNumber();
775
+ * readonly display = computed(() => formatNumber(this.value()));
594
776
  */
595
- function formatPercent(value, opt) {
596
- const unwrappedOpt = unwrap(opt);
597
- const unwrappedNumber = unwrapValue(value, unwrappedOpt?.fallbackToZero);
777
+ function injectFormatNumber() {
778
+ const defaults = injectFormatNumberOptions();
779
+ return (value, optOrLocale) => {
780
+ if (!optOrLocale)
781
+ return formatNumber(value, defaults());
782
+ const unwrapped = unwrap(optOrLocale);
783
+ const opt = typeof unwrapped === 'object'
784
+ ? { ...defaults(), ...unwrapped }
785
+ : {
786
+ ...defaults(),
787
+ locale: unwrapped,
788
+ };
789
+ return formatNumber(value, opt);
790
+ };
791
+ }
792
+ function formatPercent(value, optOrLocale) {
793
+ const unwrappedArgs = unwrap(optOrLocale);
794
+ const isOpt = unwrappedArgs != null && typeof unwrappedArgs === 'object';
795
+ const fallbackToZero = isOpt ? unwrappedArgs.fallbackToZero : undefined;
796
+ const unwrappedNumber = unwrapValue(value, fallbackToZero);
598
797
  if (unwrappedNumber === null)
599
798
  return '';
600
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
601
- return getFormatter$1(loc, unwrappedOpt?.minFractionDigits, unwrappedOpt?.maxFractionDigits, undefined, undefined, undefined, undefined, 'percent').format(unwrappedNumber);
799
+ let locale;
800
+ let minFractionDigits;
801
+ let maxFractionDigits;
802
+ if (typeof unwrappedArgs === 'string') {
803
+ locale = unwrappedArgs;
804
+ }
805
+ else if (isOpt) {
806
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
807
+ minFractionDigits = unwrappedArgs.minFractionDigits;
808
+ maxFractionDigits = unwrappedArgs.maxFractionDigits;
809
+ }
810
+ else {
811
+ locale = readLocaleUnsafe();
812
+ }
813
+ return getFormatter$1(locale, minFractionDigits, maxFractionDigits, undefined, undefined, undefined, undefined, 'percent').format(unwrappedNumber);
602
814
  }
603
- function formatCurrency(value, currency, opt) {
604
- const unwrappedOpt = unwrap(opt);
605
- const unwrappedValue = unwrapValue(value, unwrappedOpt?.fallbackToZero);
815
+ const [provideFormatPercentDefaults, injectFormatPercentOptions] = createFormatterProvider('percent', {}, (a, b) => a.minFractionDigits === b.minFractionDigits &&
816
+ a.maxFractionDigits === b.maxFractionDigits &&
817
+ a.fallbackToZero === b.fallbackToZero);
818
+ /**
819
+ * Inject a context-safe percent formatting function tied to the current injector.
820
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
821
+ */
822
+ function injectFormatPercent() {
823
+ const defaults = injectFormatPercentOptions();
824
+ return (value, optOrLocale) => {
825
+ if (!optOrLocale)
826
+ return formatPercent(value, defaults());
827
+ const unwrapped = unwrap(optOrLocale);
828
+ const opt = typeof unwrapped === 'object'
829
+ ? { ...defaults(), ...unwrapped }
830
+ : {
831
+ ...defaults(),
832
+ locale: unwrapped,
833
+ };
834
+ return formatPercent(value, opt);
835
+ };
836
+ }
837
+ function formatCurrency(value, currency, optOrLocale) {
838
+ const unwrappedArgs = unwrap(optOrLocale);
839
+ const isOpt = unwrappedArgs != null && typeof unwrappedArgs === 'object';
840
+ const fallbackToZero = isOpt ? unwrappedArgs.fallbackToZero : undefined;
841
+ const unwrappedValue = unwrapValue(value, fallbackToZero);
606
842
  if (unwrappedValue === null)
607
843
  return '';
608
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
609
- return getFormatter$1(loc, undefined, undefined, undefined, undefined, unwrap(currency), unwrappedOpt?.display ?? 'symbol', 'currency').format(unwrappedValue);
844
+ let locale;
845
+ let display = 'symbol';
846
+ if (typeof unwrappedArgs === 'string') {
847
+ locale = unwrappedArgs;
848
+ }
849
+ else if (isOpt) {
850
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
851
+ display = unwrappedArgs.display ?? 'symbol';
852
+ }
853
+ else {
854
+ locale = readLocaleUnsafe();
855
+ }
856
+ return getFormatter$1(locale, undefined, undefined, undefined, undefined, unwrap(currency), display, 'currency').format(unwrappedValue);
857
+ }
858
+ const [provideFormatCurrencyDefaults, injectFormatCurrencyOptions] = createFormatterProvider('currency', {
859
+ display: 'symbol',
860
+ }, (a, b) => a.display === b.display && a.fallbackToZero === b.fallbackToZero);
861
+ /**
862
+ * Inject a context-safe currency formatting function tied to the current injector.
863
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
864
+ */
865
+ function injectFormatCurrency() {
866
+ const defaults = injectFormatCurrencyOptions();
867
+ return (value, currency, optOrLocale) => {
868
+ if (!optOrLocale)
869
+ return formatCurrency(value, currency, defaults());
870
+ const unwrapped = unwrap(optOrLocale);
871
+ const opt = typeof unwrapped === 'object'
872
+ ? { ...defaults(), ...unwrapped }
873
+ : {
874
+ ...defaults(),
875
+ locale: unwrapped,
876
+ };
877
+ return formatCurrency(value, currency, opt);
878
+ };
610
879
  }
611
880
 
612
881
  const cache = new Map();
@@ -619,16 +888,7 @@ function getFormatter(locale, style, numeric) {
619
888
  }
620
889
  return formatter;
621
890
  }
622
- /**
623
- * Format a relative time using the current or provided locale
624
- * 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
625
- *
626
- * @param value - The numeric value to use in the relative time internationalization message
627
- * @param unit - The unit to use in the relative time internationalization message
628
- * @param opt - Options for formatting
629
- * @returns Formatted relative time string
630
- */
631
- function formatRelativeTime(value, unit, opt) {
891
+ function formatRelativeTime(value, unit, optOrLocale) {
632
892
  const unwrappedValue = unwrap(value);
633
893
  if (unwrappedValue === null ||
634
894
  unwrappedValue === undefined ||
@@ -637,11 +897,83 @@ function formatRelativeTime(value, unit, opt) {
637
897
  const unwrappedUnit = unwrap(unit);
638
898
  if (!unwrappedUnit)
639
899
  return '';
640
- const unwrappedOpt = unwrap(opt);
641
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
642
- return getFormatter(loc, unwrappedOpt?.style ?? 'long', unwrappedOpt?.numeric ?? 'always').format(unwrappedValue, unwrappedUnit);
900
+ const unwrappedArgs = unwrap(optOrLocale);
901
+ let locale;
902
+ let style = 'long';
903
+ let numeric = 'always';
904
+ if (typeof unwrappedArgs === 'string') {
905
+ locale = unwrappedArgs;
906
+ }
907
+ else if (unwrappedArgs && typeof unwrappedArgs === 'object') {
908
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
909
+ style = unwrappedArgs.style ?? 'long';
910
+ numeric = unwrappedArgs.numeric ?? 'always';
911
+ }
912
+ else {
913
+ locale = readLocaleUnsafe();
914
+ }
915
+ return getFormatter(locale, style, numeric).format(unwrappedValue, unwrappedUnit);
916
+ }
917
+ const [provideFormatRelativeTimeDefaults, injectFormatRelativeTimeOptions] = createFormatterProvider('relativeTime', {
918
+ style: 'long',
919
+ numeric: 'always',
920
+ }, (a, b) => a.style === b.style && a.numeric === b.numeric);
921
+ /**
922
+ * Inject a context-safe relative time formatting function tied to the current injector.
923
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
924
+ * @example
925
+ * const formatRelativeTime = injectFormatRelativeTime();
926
+ * readonly relativeAge = computed(() => formatRelativeTime(this.delta(), 'day'));
927
+ */
928
+ function injectFormatRelativeTime() {
929
+ const defaults = injectFormatRelativeTimeOptions();
930
+ return (value, unit, optOrLocale) => {
931
+ if (!optOrLocale)
932
+ return formatRelativeTime(value, unit, defaults());
933
+ const unwrapped = unwrap(optOrLocale);
934
+ const opt = typeof unwrapped === 'object'
935
+ ? { ...defaults(), ...unwrapped }
936
+ : {
937
+ ...defaults(),
938
+ locale: unwrapped,
939
+ };
940
+ return formatRelativeTime(value, unit, opt);
941
+ };
643
942
  }
644
943
 
944
+ function provideFormatDefaults(cfg) {
945
+ const providers = [];
946
+ if (cfg.date)
947
+ providers.push(provideFormatDateDefaults(cfg.date));
948
+ if (cfg.displayName)
949
+ providers.push(provideFormatDisplayNameDefaults(cfg.displayName));
950
+ if (cfg.list)
951
+ providers.push(provideFormatListDefaults(cfg.list));
952
+ if (cfg.relativeTime)
953
+ providers.push(provideFormatRelativeTimeDefaults(cfg.relativeTime));
954
+ if (cfg.number)
955
+ providers.push(provideFormatNumberDefaults(cfg.number));
956
+ if (cfg.percent)
957
+ providers.push(provideFormatPercentDefaults(cfg.percent));
958
+ if (cfg.currency)
959
+ providers.push(provideFormatCurrencyDefaults(cfg.currency));
960
+ return providers;
961
+ }
962
+ function injectFormatters() {
963
+ return {
964
+ date: injectFormatDate(),
965
+ displayName: injectFormatDisplayName(),
966
+ list: injectFormatList(),
967
+ relativeTime: injectFormatRelativeTime(),
968
+ number: injectFormatNumber(),
969
+ percent: injectFormatPercent(),
970
+ currency: injectFormatCurrency(),
971
+ };
972
+ }
973
+
974
+ /**
975
+ * @internal
976
+ */
645
977
  function injectResolveParamLocale(snapshot) {
646
978
  let locale = null;
647
979
  const paramName = injectIntlConfig()?.localeParamName;
@@ -654,7 +986,7 @@ function injectResolveParamLocale(snapshot) {
654
986
  if (!locale && !alwaysInheritParams) {
655
987
  let currentRoute = snapshot;
656
988
  while (currentRoute && !locale) {
657
- locale = currentRoute.paramMap.get('locale');
989
+ locale = currentRoute.paramMap.get(paramName);
658
990
  currentRoute = currentRoute.parent;
659
991
  }
660
992
  }
@@ -721,44 +1053,85 @@ function createT(store, keyMap = new Map()) {
721
1053
  };
722
1054
  return fn;
723
1055
  }
1056
+ function isCompiledTranslation(value) {
1057
+ return (!!value &&
1058
+ typeof value === 'object' &&
1059
+ 'flat' in value &&
1060
+ 'namespace' in value);
1061
+ }
1062
+ /**
1063
+ * @internal exported for unit testing
1064
+ *
1065
+ * Unwraps a loader result to a `CompiledTranslation`. Detection order:
1066
+ * 1. value is already a `CompiledTranslation` (has `flat` + `namespace`)
1067
+ * 2. value has a `default` export holding a `CompiledTranslation` (ESM default)
1068
+ * 3. value has a `translation` export holding a `CompiledTranslation` (named export)
1069
+ */
1070
+ function resolveTranslationModule(loaded) {
1071
+ if (isCompiledTranslation(loaded))
1072
+ return loaded;
1073
+ if (loaded && typeof loaded === 'object') {
1074
+ const def = loaded.default;
1075
+ if (isCompiledTranslation(def))
1076
+ return def;
1077
+ const tr = loaded.translation;
1078
+ if (isCompiledTranslation(tr))
1079
+ return tr;
1080
+ }
1081
+ throw new Error('[@mmstack/translate] Loader did not return a CompiledTranslation. Expected the value from `createNamespace` / `createTranslation`, or a module exporting one as `default` or `translation`.');
1082
+ }
724
1083
  function registerNamespace(defaultTranslation, other) {
725
1084
  const keyMap = new Map();
1085
+ // Pre-wrap loaders once so the rest of the pipeline — including the
1086
+ // dynamic-locale loader in `TranslationStore`, which reads `.namespace`
1087
+ // and `.flat` directly off the result — always sees a `CompiledTranslation`
1088
+ // regardless of which loader shape the caller used.
1089
+ const unwrappedDefault = () => defaultTranslation().then(resolveTranslationModule);
1090
+ const unwrappedOther = Object.fromEntries(Object.entries(other).map(([loc, loader]) => [
1091
+ loc,
1092
+ () => loader().then(resolveTranslationModule),
1093
+ ]));
726
1094
  const injectT = () => {
727
1095
  const store = inject(TranslationStore);
728
1096
  return addSignalFn(createT(store, keyMap), store, keyMap);
729
1097
  };
1098
+ // Tracks whether the default locale's translation has been loaded by any prior
1099
+ // resolver call. Captured in closure across navigations on purpose — concurrent
1100
+ // resolves may each see `false` and both queue a default preload; that's harmless
1101
+ // because `store.register` is idempotent (same payload overwrites with the same
1102
+ // value). Do not add locking here.
730
1103
  let defaultTranslationLoaded = false;
731
1104
  const resolver = async (snapshot) => {
732
1105
  const store = inject(TranslationStore);
733
1106
  const locale = injectResolveParamLocale(snapshot);
734
1107
  const defaultLocale = injectDefaultLocale();
735
1108
  const shouldPreloadDefault = injectIntlConfig()?.preloadDefaultLocale ?? false;
736
- const tPromise = other[locale];
737
- const promise = tPromise ?? defaultTranslation;
1109
+ const tPromise = unwrappedOther[locale];
1110
+ const promise = tPromise ?? unwrappedDefault;
738
1111
  if (!promise && isDevMode()) {
739
1112
  return console.warn(`No translation found for locale: ${locale}`);
740
1113
  }
741
- if (promise === defaultTranslation && defaultTranslationLoaded)
1114
+ if (promise === unwrappedDefault && defaultTranslationLoaded)
742
1115
  return;
743
1116
  try {
744
1117
  const promises = [promise()];
745
1118
  if (shouldPreloadDefault &&
746
1119
  !defaultTranslationLoaded &&
747
- promise !== defaultTranslation)
748
- promises.push(defaultTranslation());
1120
+ promise !== unwrappedDefault)
1121
+ promises.push(unwrappedDefault());
749
1122
  const translations = await Promise.allSettled(promises);
750
- const fullfilled = translations.map((t) => t.status === 'fulfilled' ? t.value : null);
751
- if (fullfilled.at(0) === null && fullfilled.at(1) === null)
1123
+ const fulfilled = translations.map((t) => t.status === 'fulfilled' ? t.value : null);
1124
+ if (fulfilled.at(0) === null && fulfilled.at(1) === null)
752
1125
  throw new Error('Failed to load translations');
753
- const [t, defaultT] = fullfilled;
1126
+ const [t, defaultT] = fulfilled;
754
1127
  const ns = t?.namespace ?? defaultT?.namespace;
755
1128
  if (!ns)
756
1129
  throw new Error('No namespace found in translation');
757
1130
  if (isDevMode() && t && t.locale !== locale && t.locale)
758
1131
  console.warn(`Expected locale to be ${locale} but got ${t.locale}`);
759
1132
  store.registerOnDemandLoaders(ns, {
760
- ...other,
761
- [defaultLocale]: defaultTranslation,
1133
+ ...unwrappedOther,
1134
+ [defaultLocale]: unwrappedDefault,
762
1135
  });
763
1136
  const toRegister = {};
764
1137
  if (t)
@@ -766,7 +1139,7 @@ function registerNamespace(defaultTranslation, other) {
766
1139
  if (defaultT)
767
1140
  toRegister[defaultLocale] = defaultT.flat;
768
1141
  store.register(ns, toRegister);
769
- if (promise === defaultTranslation || defaultT)
1142
+ if (promise === unwrappedDefault || defaultT)
770
1143
  defaultTranslationLoaded = true;
771
1144
  }
772
1145
  catch {
@@ -790,10 +1163,21 @@ function registerNamespace(defaultTranslation, other) {
790
1163
  */
791
1164
  function registerRemoteNamespace(ns, defaultTranslation, other) {
792
1165
  const keyMap = new Map();
1166
+ // The dynamic-locale loader in TranslationStore reads `.namespace` and `.flat`
1167
+ // off the loader result, so on-demand loaders must return CompiledTranslation.
1168
+ // Wrap the raw remote fetchers once here at registration time rather than every
1169
+ // resolver call.
1170
+ const compileLoader = (loader, locale) => () => loader().then((raw) => compileTranslation(raw, ns, locale));
1171
+ const compiledOther = Object.fromEntries(Object.entries(other).map(([loc, loader]) => [
1172
+ loc,
1173
+ compileLoader(loader, loc),
1174
+ ]));
793
1175
  const injectT = () => {
794
1176
  const store = inject(TranslationStore);
795
1177
  return addSignalFn(createT(store, keyMap), store, keyMap);
796
1178
  };
1179
+ // See `defaultTranslationLoaded` in `registerNamespace` for rationale —
1180
+ // intentionally racy, safe via idempotent `store.register`.
797
1181
  let defaultTranslationLoaded = false;
798
1182
  const resolver = async (snapshot) => {
799
1183
  const store = inject(TranslationStore);
@@ -814,10 +1198,10 @@ function registerRemoteNamespace(ns, defaultTranslation, other) {
814
1198
  promise !== defaultTranslation)
815
1199
  promises.push(defaultTranslation());
816
1200
  const translations = await Promise.allSettled(promises);
817
- const fullfilled = translations.map((t) => t.status === 'fulfilled' ? t.value : null);
818
- if (fullfilled.at(0) === null && fullfilled.at(1) === null)
1201
+ const fulfilled = translations.map((t) => t.status === 'fulfilled' ? t.value : null);
1202
+ if (fulfilled.at(0) === null && fulfilled.at(1) === null)
819
1203
  throw new Error('Failed to load translations');
820
- const [baseT, baseDefaultT] = fullfilled;
1204
+ const [baseT, baseDefaultT] = fulfilled;
821
1205
  const t = baseT ? compileTranslation(baseT, ns, locale) : null;
822
1206
  const defaultT = baseDefaultT
823
1207
  ? compileTranslation(baseDefaultT, ns, defaultLocale)
@@ -825,8 +1209,8 @@ function registerRemoteNamespace(ns, defaultTranslation, other) {
825
1209
  if (isDevMode() && t && t.locale !== locale && t.locale)
826
1210
  console.warn(`Expected locale to be ${locale} but got ${t.locale}`);
827
1211
  store.registerOnDemandLoaders(ns, {
828
- ...other,
829
- [defaultLocale]: defaultTranslation,
1212
+ ...compiledOther,
1213
+ [defaultLocale]: compileLoader(defaultTranslation, defaultLocale),
830
1214
  });
831
1215
  const toRegister = {};
832
1216
  if (t)
@@ -1059,6 +1443,10 @@ function createTransformFn() {
1059
1443
  const store = inject(TranslationStore);
1060
1444
  const t = createT(store);
1061
1445
  const fn = (key, variablesOrLocale,
1446
+ // The locale argument is unused at runtime — the active locale comes from
1447
+ // `TranslationStore`. It exists on the signature so consumers can pass
1448
+ // `locale()` as a pipe argument (`'key' | translate : vars : locale()`)
1449
+ // to bust Angular's pure-pipe memoization on locale changes.
1062
1450
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1063
1451
  _) => {
1064
1452
  const vars = typeof variablesOrLocale === 'string' ? undefined : variablesOrLocale;
@@ -1109,5 +1497,5 @@ function withParams(message) {
1109
1497
  * Generated bundle index. Do not edit.
1110
1498
  */
1111
1499
 
1112
- export { Translate, Translator, canMatchLocale, compileTranslation, createNamespace, formatCurrency, formatDate, formatDisplayName, formatList, formatNumber, formatPercent, formatRelativeTime, injectAddTranslations, injectDynamicLocale, injectIntl, injectResolveParamLocale, injectSupportedLocales, injectUnsafeT, provideIntlConfig, provideMockTranslations, registerNamespace, registerRemoteNamespace, withParams };
1500
+ export { Translate, Translator, canMatchLocale, compileTranslation, createNamespace, formatCurrency, formatDate, formatDisplayName, formatList, formatNumber, formatPercent, formatRelativeTime, injectAddTranslations, injectDefaultLocale, injectDynamicLocale, injectFormatCurrency, injectFormatDate, injectFormatDisplayName, injectFormatList, injectFormatNumber, injectFormatPercent, injectFormatRelativeTime, injectFormatters, injectIntl, injectResolveParamLocale, injectSupportedLocales, injectUnsafeT, provideFormatCurrencyDefaults, provideFormatDateDefaults, provideFormatDefaults, provideFormatDisplayNameDefaults, provideFormatListDefaults, provideFormatNumberDefaults, provideFormatPercentDefaults, provideFormatRelativeTimeDefaults, provideIntlConfig, provideMockTranslations, registerNamespace, registerRemoteNamespace, withParams };
1113
1501
  //# sourceMappingURL=mmstack-translate.mjs.map