@mmstack/translate 21.1.11 → 21.1.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,8 +1,9 @@
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';
6
+ import { inject as inject$1 } from '@angular/core/primitives/di';
6
7
 
7
8
  const KEY_DELIM = '::MMT_DELIM::';
8
9
  function prependDelim(prefix, key) {
@@ -132,9 +133,27 @@ function injectSupportedLocales() {
132
133
  * the actual locale signal used to store the current locale string
133
134
  */
134
135
  const STORE_LOCALE = signal('en-US', ...(ngDevMode ? [{ debugName: "STORE_LOCALE" }] : /* istanbul ignore next */ []));
136
+ /**
137
+ * @internal
138
+ * @deprecated will be removed when ng23 drops
139
+ */
140
+ function readLocaleUnsafe() {
141
+ return STORE_LOCALE();
142
+ }
135
143
  function injectLocaleInternal() {
136
144
  return STORE_LOCALE;
137
145
  }
146
+ function proxyToGlobalSignleton(src) {
147
+ const originalSet = src.set;
148
+ src.set = (next) => {
149
+ originalSet(next);
150
+ STORE_LOCALE.set(next);
151
+ };
152
+ src.update = (updater) => {
153
+ src.set(updater(untracked(src)));
154
+ };
155
+ return src;
156
+ }
138
157
  function isDynamicConfig(cfg) {
139
158
  return !!cfg && 'localeStorage' in cfg && !!cfg.localeStorage;
140
159
  }
@@ -230,7 +249,7 @@ class TranslationStore {
230
249
  messages: this.messages(),
231
250
  }, this.cache), ...(ngDevMode ? [{ debugName: "intl" }] : /* istanbul ignore next */ []));
232
251
  constructor() {
233
- this.locale = initLocale(STORE_LOCALE);
252
+ this.locale = proxyToGlobalSignleton(initLocale(signal('en-US')));
234
253
  const paramName = this.config?.localeParamName;
235
254
  if (paramName) {
236
255
  const param = pathParam(paramName);
@@ -320,10 +339,10 @@ class TranslationStore {
320
339
  hasLocaleLoaders(locale) {
321
340
  return Array.from(this.onDemandLoaders.values()).some((loaders) => loaders[locale]);
322
341
  }
323
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TranslationStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
324
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TranslationStore, providedIn: 'root' });
342
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TranslationStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
343
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TranslationStore, providedIn: 'root' });
325
344
  }
326
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TranslationStore, decorators: [{
345
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TranslationStore, decorators: [{
327
346
  type: Injectable,
328
347
  args: [{
329
348
  providedIn: 'root',
@@ -380,6 +399,96 @@ function injectDynamicLocale() {
380
399
  source.isLoading = store.dynamicLocaleLoader.isLoading;
381
400
  return source;
382
401
  }
402
+ /**
403
+ * Power-user escape hatch for adding translations imperatively (e.g. content
404
+ * loaded from a remote API after bootstrap). Returns a function that registers
405
+ * a flat per-locale map of keys under a given namespace
406
+ *
407
+ * Pair with {@link injectUnsafeT} to read the added keys without compile-time
408
+ * constraints.
409
+ *
410
+ * @example
411
+ * ```ts
412
+ * const addTranslations = injectAddTranslations();
413
+ * addTranslations('remote', {
414
+ * 'en-US': { greeting: 'Hi {name}' },
415
+ * 'sl-SI': { greeting: 'Zdravo {name}' },
416
+ * });
417
+ * ```
418
+ */
419
+ function injectAddTranslations() {
420
+ const store = inject(TranslationStore);
421
+ const supportedLocales = injectIntlConfig()?.supportedLocales;
422
+ const supportedLocalesSet = supportedLocales
423
+ ? new Set(supportedLocales)
424
+ : null;
425
+ const validate = supportedLocalesSet
426
+ ? (translations) => {
427
+ const clean = {};
428
+ const invalidLocales = [];
429
+ for (const [locale, translation] of Object.entries(translations)) {
430
+ if (!supportedLocalesSet.has(locale)) {
431
+ invalidLocales.push(locale);
432
+ continue;
433
+ }
434
+ clean[locale] = translation;
435
+ }
436
+ if (isDevMode() && invalidLocales.length > 0)
437
+ console.warn(`[Translate] Attempted to add translations for unsupported locales: ${invalidLocales.join(', ')}. These translations were ignored. Supported locales are: ${(supportedLocales ?? []).join(', ')}.`);
438
+ return clean;
439
+ }
440
+ : (translations) => translations;
441
+ return (ns, translations) => {
442
+ store.register(ns, validate(translations));
443
+ };
444
+ }
445
+
446
+ function equalLocale(a, b) {
447
+ return a.locale === b.locale;
448
+ }
449
+ function createFormatterProvider(formatterName, libraryDefaults, nonLocaleEqual) {
450
+ const token = new InjectionToken(`@mmstack/translate:format-${formatterName}-config`, {
451
+ factory: () => {
452
+ const loc = injectDynamicLocale();
453
+ return computed(() => ({
454
+ ...libraryDefaults,
455
+ locale: loc(),
456
+ }), { equal: equalLocale });
457
+ },
458
+ });
459
+ const provider = (valueOrFn) => {
460
+ const fnProvider = typeof valueOrFn === 'function'
461
+ ? valueOrFn
462
+ : () => valueOrFn;
463
+ return {
464
+ provide: token,
465
+ useFactory: () => {
466
+ const loc = injectDynamicLocale();
467
+ const providedDefaultsOrSignal = fnProvider();
468
+ if (isSignal(providedDefaultsOrSignal))
469
+ return computed(() => ({
470
+ ...libraryDefaults,
471
+ ...providedDefaultsOrSignal(),
472
+ locale: loc(),
473
+ }), {
474
+ equal: (a, b) => equalLocale(a, b) && nonLocaleEqual(a, b),
475
+ });
476
+ const defaults = {
477
+ ...libraryDefaults,
478
+ ...providedDefaultsOrSignal,
479
+ };
480
+ return computed(() => ({
481
+ ...defaults,
482
+ locale: loc(),
483
+ }), {
484
+ equal: equalLocale,
485
+ });
486
+ },
487
+ };
488
+ };
489
+ const injectFn = () => inject$1(token);
490
+ return [provider, injectFn];
491
+ }
383
492
 
384
493
  function unwrap(value) {
385
494
  return isSignal(value) ? value() : value;
@@ -407,32 +516,68 @@ function validDateOrNull(date) {
407
516
  }
408
517
  const cache$4 = new Map();
409
518
  function getFormatter$4(locale, format, timeZone) {
410
- const cacheKey = `${locale}|${format}|${timeZone ?? ''}`;
519
+ const cacheKey = `${locale}|${typeof format === 'string' ? format : JSON.stringify(format)}|${timeZone ?? ''}`;
411
520
  let formatter = cache$4.get(cacheKey);
412
521
  if (!formatter) {
413
522
  formatter = new Intl.DateTimeFormat(locale, {
414
- ...FORMAT_PRESETS[format],
523
+ ...(typeof format === 'string' ? FORMAT_PRESETS[format] : format),
415
524
  timeZone,
416
525
  });
417
526
  cache$4.set(cacheKey, formatter);
418
527
  }
419
528
  return formatter;
420
529
  }
421
- /**
422
- * Format a date using the current or provided locale & timezone
423
- * 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
424
- *
425
- * @param date - Date to format
426
- * @param opt - Options for formatting
427
- * @returns Formatted date string
428
- */
429
- function formatDate(date, opt) {
530
+ function formatDate(date, optOrLocale) {
430
531
  const validDate = validDateOrNull(unwrap(date));
431
532
  if (validDate === null)
432
533
  return '';
433
- const unwrappedOpt = unwrap(opt);
434
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
435
- return getFormatter$4(loc, unwrappedOpt?.format ?? 'medium', unwrappedOpt?.tz).format(validDate);
534
+ const unwrappedArgs = unwrap(optOrLocale);
535
+ let locale;
536
+ let format = 'medium';
537
+ let tz;
538
+ if (typeof unwrappedArgs === 'string') {
539
+ locale = unwrappedArgs;
540
+ }
541
+ else if (unwrappedArgs && typeof unwrappedArgs === 'object') {
542
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
543
+ format = unwrappedArgs.format ?? 'medium';
544
+ tz = unwrappedArgs.tz;
545
+ }
546
+ else {
547
+ locale = readLocaleUnsafe();
548
+ }
549
+ return getFormatter$4(locale, format, tz).format(validDate);
550
+ }
551
+ const [provideFormatDateDefaults, injectFormatDateOptions] = createFormatterProvider('date', {
552
+ format: 'medium',
553
+ }, (a, b) => {
554
+ if (a.tz !== b.tz)
555
+ return false;
556
+ if (a.format === b.format)
557
+ return true;
558
+ return JSON.stringify(a.format) === JSON.stringify(b.format);
559
+ });
560
+ /**
561
+ * Inject a context-safe date formatting function tied to the current injector.
562
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
563
+ * @example
564
+ * const formatDate = injectFormatDate();
565
+ * readonly displayDate = computed(() => formatDate(this.date()));
566
+ */
567
+ function injectFormatDate() {
568
+ const defaults = injectFormatDateOptions();
569
+ return (date, optOrLocale) => {
570
+ if (!optOrLocale)
571
+ return formatDate(date, defaults());
572
+ const unwrapped = unwrap(optOrLocale);
573
+ const opt = typeof unwrapped === 'object'
574
+ ? { ...defaults(), ...unwrapped }
575
+ : {
576
+ ...defaults(),
577
+ locale: unwrapped,
578
+ };
579
+ return formatDate(date, opt);
580
+ };
436
581
  }
437
582
 
438
583
  const cache$3 = new Map();
@@ -450,21 +595,48 @@ function getFormatter$3(locale, type, style) {
450
595
  }
451
596
  /**
452
597
  * Format a display name using the current or provided locale
453
- * 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
454
598
  *
455
599
  * @param value - The code to format
456
600
  * @param type - The type of display name to format
457
601
  * @param opt - Options for formatting
458
602
  * @returns Formatted display name string
459
603
  */
460
- function formatDisplayName(value, type, opt) {
461
- const unwrapped = unwrap(value);
462
- if (!unwrapped?.trim())
604
+ function formatDisplayName(value, type, localeOrOpt) {
605
+ const unwrappedValue = unwrap(value);
606
+ if (!unwrappedValue?.trim())
463
607
  return '';
464
608
  const unwrappedType = unwrap(type);
465
- const unwrappedOpt = unwrap(opt);
466
- const locale = unwrappedOpt?.locale ?? injectLocaleInternal()();
467
- return (getFormatter$3(locale, unwrappedType, unwrappedOpt?.style ?? 'long').of(unwrapped) ?? '');
609
+ const unwrapped = unwrap(localeOrOpt);
610
+ const locale = typeof unwrapped === 'string'
611
+ ? unwrapped
612
+ : (unwrapped?.locale ?? readLocaleUnsafe());
613
+ const opt = typeof unwrapped === 'object' ? unwrapped : undefined;
614
+ return (getFormatter$3(locale, unwrappedType, opt?.style ?? 'long').of(unwrappedValue) ?? '');
615
+ }
616
+ const [provideFormatDisplayNameDefaults, injectFormatDisplayNameDefaults] = createFormatterProvider('displayName', {
617
+ style: 'long',
618
+ }, (a, b) => a.style === b.style);
619
+ /**
620
+ * Inject a context-safe date formatting function tied to the current injector.
621
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
622
+ * @example
623
+ * const formatDisplayName = injectFormatDisplayName();
624
+ * readonly region = computed(() => formatDisplayName('US', 'region'));
625
+ */
626
+ function injectFormatDisplayName() {
627
+ const defaults = injectFormatDisplayNameDefaults();
628
+ return (value, type, localeOrOpt) => {
629
+ if (!localeOrOpt)
630
+ return formatDisplayName(value, type, defaults());
631
+ const unwrapped = unwrap(localeOrOpt);
632
+ const opt = typeof unwrapped === 'object'
633
+ ? { ...defaults, ...unwrapped }
634
+ : {
635
+ ...defaults,
636
+ locale: unwrapped,
637
+ };
638
+ return formatDisplayName(value, type, opt);
639
+ };
468
640
  }
469
641
 
470
642
  const cache$2 = new Map();
@@ -482,21 +654,52 @@ function getFormatter$2(locale, type, style) {
482
654
  }
483
655
  return formatter;
484
656
  }
657
+ function formatList(value, optOrLocale) {
658
+ const unwrappedValue = unwrapList(value);
659
+ if (unwrappedValue.length === 0)
660
+ return '';
661
+ const unwrappedArgs = unwrap(optOrLocale);
662
+ let locale;
663
+ let type = 'conjunction';
664
+ let style = 'long';
665
+ if (typeof unwrappedArgs === 'string') {
666
+ locale = unwrappedArgs;
667
+ }
668
+ else if (unwrappedArgs && typeof unwrappedArgs === 'object') {
669
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
670
+ type = unwrappedArgs.type ?? 'conjunction';
671
+ style = unwrappedArgs.style ?? 'long';
672
+ }
673
+ else {
674
+ locale = readLocaleUnsafe();
675
+ }
676
+ return getFormatter$2(locale, type, style).format(unwrappedValue);
677
+ }
678
+ const [provideFormatListDefaults, injectFormatListOptions] = createFormatterProvider('list', {
679
+ type: 'conjunction',
680
+ style: 'long',
681
+ }, (a, b) => a.type === b.type && a.style === b.style);
485
682
  /**
486
- * Format a list using the current or provided locale
487
- * 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
488
- *
489
- * @param value - The list to format
490
- * @param opt - Options for formatting
491
- * @returns Formatted list string
683
+ * Inject a context-safe list formatting function tied to the current injector.
684
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
685
+ * @example
686
+ * const formatList = injectFormatList();
687
+ * readonly displayList = computed(() => formatList(this.items()));
492
688
  */
493
- function formatList(value, opt) {
494
- const unwrapped = unwrapList(value);
495
- if (unwrapped.length === 0)
496
- return '';
497
- const unwrappedOpt = unwrap(opt);
498
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
499
- return getFormatter$2(loc, unwrappedOpt?.type ?? 'conjunction', unwrappedOpt?.style ?? 'long').format(unwrapped);
689
+ function injectFormatList() {
690
+ const defaults = injectFormatListOptions();
691
+ return (value, optOrLocale) => {
692
+ if (!optOrLocale)
693
+ return formatList(value, defaults());
694
+ const unwrapped = unwrap(optOrLocale);
695
+ const opt = typeof unwrapped === 'object'
696
+ ? { ...defaults(), ...unwrapped }
697
+ : {
698
+ ...defaults(),
699
+ locale: unwrapped,
700
+ };
701
+ return formatList(value, opt);
702
+ };
500
703
  }
501
704
 
502
705
  const cache$1 = new Map();
@@ -523,45 +726,151 @@ function getFormatter$1(locale, minFractionDigits, maxFractionDigits, useGroupin
523
726
  }
524
727
  return formatter;
525
728
  }
526
- /**
527
- * Format a number using the current or provided locale
528
- * 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
529
- *
530
- * @param number - Number to format
531
- * @param opt - Options for formatting
532
- * @returns Formatted number string
533
- */
534
- function formatNumber(value, opt) {
535
- const unwrappedOpt = unwrap(opt);
536
- const unwrappedNumber = unwrapValue(value, unwrappedOpt?.fallbackToZero);
729
+ function formatNumber(value, optOrLocale) {
730
+ const unwrappedArgs = unwrap(optOrLocale);
731
+ const isOpt = unwrappedArgs != null && typeof unwrappedArgs === 'object';
732
+ const fallbackToZero = isOpt ? unwrappedArgs.fallbackToZero : undefined;
733
+ const unwrappedNumber = unwrapValue(value, fallbackToZero);
537
734
  if (unwrappedNumber === null)
538
735
  return '';
539
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
540
- return getFormatter$1(loc, unwrappedOpt?.minFractionDigits, unwrappedOpt?.maxFractionDigits, unwrappedOpt?.useGrouping ?? true, unwrappedOpt?.notation ?? 'standard').format(unwrappedNumber);
736
+ let locale;
737
+ let notation;
738
+ let minFractionDigits;
739
+ let maxFractionDigits;
740
+ let useGrouping = true;
741
+ if (typeof unwrappedArgs === 'string') {
742
+ locale = unwrappedArgs;
743
+ }
744
+ else if (isOpt) {
745
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
746
+ notation = unwrappedArgs.notation ?? 'standard';
747
+ minFractionDigits = unwrappedArgs.minFractionDigits;
748
+ maxFractionDigits = unwrappedArgs.maxFractionDigits;
749
+ useGrouping = unwrappedArgs.useGrouping ?? true;
750
+ }
751
+ else {
752
+ locale = readLocaleUnsafe();
753
+ notation = 'standard';
754
+ }
755
+ return getFormatter$1(locale, minFractionDigits, maxFractionDigits, useGrouping, notation).format(unwrappedNumber);
541
756
  }
757
+ const [provideFormatNumberDefaults, injectFormatNumberOptions] = createFormatterProvider('number', {
758
+ notation: 'standard',
759
+ useGrouping: true,
760
+ }, (a, b) => a.notation === b.notation &&
761
+ a.minFractionDigits === b.minFractionDigits &&
762
+ a.maxFractionDigits === b.maxFractionDigits &&
763
+ a.useGrouping === b.useGrouping &&
764
+ a.fallbackToZero === b.fallbackToZero);
542
765
  /**
543
- * Format a percentage using the current or provided locale
544
- * 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
545
- *
546
- * @param number - Number to format
547
- * @param opt - Options for formatting
548
- * @returns Formatted percentage string
766
+ * Inject a context-safe number formatting function tied to the current injector.
767
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
768
+ * @example
769
+ * const formatNumber = injectFormatNumber();
770
+ * readonly display = computed(() => formatNumber(this.value()));
549
771
  */
550
- function formatPercent(value, opt) {
551
- const unwrappedOpt = unwrap(opt);
552
- const unwrappedNumber = unwrapValue(value, unwrappedOpt?.fallbackToZero);
772
+ function injectFormatNumber() {
773
+ const defaults = injectFormatNumberOptions();
774
+ return (value, optOrLocale) => {
775
+ if (!optOrLocale)
776
+ return formatNumber(value, defaults());
777
+ const unwrapped = unwrap(optOrLocale);
778
+ const opt = typeof unwrapped === 'object'
779
+ ? { ...defaults(), ...unwrapped }
780
+ : {
781
+ ...defaults(),
782
+ locale: unwrapped,
783
+ };
784
+ return formatNumber(value, opt);
785
+ };
786
+ }
787
+ function formatPercent(value, optOrLocale) {
788
+ const unwrappedArgs = unwrap(optOrLocale);
789
+ const isOpt = unwrappedArgs != null && typeof unwrappedArgs === 'object';
790
+ const fallbackToZero = isOpt ? unwrappedArgs.fallbackToZero : undefined;
791
+ const unwrappedNumber = unwrapValue(value, fallbackToZero);
553
792
  if (unwrappedNumber === null)
554
793
  return '';
555
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
556
- return getFormatter$1(loc, unwrappedOpt?.minFractionDigits, unwrappedOpt?.maxFractionDigits, undefined, undefined, undefined, undefined, 'percent').format(unwrappedNumber);
794
+ let locale;
795
+ let minFractionDigits;
796
+ let maxFractionDigits;
797
+ if (typeof unwrappedArgs === 'string') {
798
+ locale = unwrappedArgs;
799
+ }
800
+ else if (isOpt) {
801
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
802
+ minFractionDigits = unwrappedArgs.minFractionDigits;
803
+ maxFractionDigits = unwrappedArgs.maxFractionDigits;
804
+ }
805
+ else {
806
+ locale = readLocaleUnsafe();
807
+ }
808
+ return getFormatter$1(locale, minFractionDigits, maxFractionDigits, undefined, undefined, undefined, undefined, 'percent').format(unwrappedNumber);
557
809
  }
558
- function formatCurrency(value, currency, opt) {
559
- const unwrappedOpt = unwrap(opt);
560
- const unwrappedValue = unwrapValue(value, unwrappedOpt?.fallbackToZero);
810
+ const [provideFormatPercentDefaults, injectFormatPercentOptions] = createFormatterProvider('percent', {}, (a, b) => a.minFractionDigits === b.minFractionDigits &&
811
+ a.maxFractionDigits === b.maxFractionDigits &&
812
+ a.fallbackToZero === b.fallbackToZero);
813
+ /**
814
+ * Inject a context-safe percent formatting function tied to the current injector.
815
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
816
+ */
817
+ function injectFormatPercent() {
818
+ const defaults = injectFormatPercentOptions();
819
+ return (value, optOrLocale) => {
820
+ if (!optOrLocale)
821
+ return formatPercent(value, defaults());
822
+ const unwrapped = unwrap(optOrLocale);
823
+ const opt = typeof unwrapped === 'object'
824
+ ? { ...defaults(), ...unwrapped }
825
+ : {
826
+ ...defaults(),
827
+ locale: unwrapped,
828
+ };
829
+ return formatPercent(value, opt);
830
+ };
831
+ }
832
+ function formatCurrency(value, currency, optOrLocale) {
833
+ const unwrappedArgs = unwrap(optOrLocale);
834
+ const isOpt = unwrappedArgs != null && typeof unwrappedArgs === 'object';
835
+ const fallbackToZero = isOpt ? unwrappedArgs.fallbackToZero : undefined;
836
+ const unwrappedValue = unwrapValue(value, fallbackToZero);
561
837
  if (unwrappedValue === null)
562
838
  return '';
563
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
564
- return getFormatter$1(loc, undefined, undefined, undefined, undefined, unwrap(currency), unwrappedOpt?.display ?? 'symbol', 'currency').format(unwrappedValue);
839
+ let locale;
840
+ let display = 'symbol';
841
+ if (typeof unwrappedArgs === 'string') {
842
+ locale = unwrappedArgs;
843
+ }
844
+ else if (isOpt) {
845
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
846
+ display = unwrappedArgs.display ?? 'symbol';
847
+ }
848
+ else {
849
+ locale = readLocaleUnsafe();
850
+ }
851
+ return getFormatter$1(locale, undefined, undefined, undefined, undefined, unwrap(currency), display, 'currency').format(unwrappedValue);
852
+ }
853
+ const [provideFormatCurrencyDefaults, injectFormatCurrencyOptions] = createFormatterProvider('currency', {
854
+ display: 'symbol',
855
+ }, (a, b) => a.display === b.display && a.fallbackToZero === b.fallbackToZero);
856
+ /**
857
+ * Inject a context-safe currency formatting function tied to the current injector.
858
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
859
+ */
860
+ function injectFormatCurrency() {
861
+ const defaults = injectFormatCurrencyOptions();
862
+ return (value, currency, optOrLocale) => {
863
+ if (!optOrLocale)
864
+ return formatCurrency(value, currency, defaults());
865
+ const unwrapped = unwrap(optOrLocale);
866
+ const opt = typeof unwrapped === 'object'
867
+ ? { ...defaults(), ...unwrapped }
868
+ : {
869
+ ...defaults(),
870
+ locale: unwrapped,
871
+ };
872
+ return formatCurrency(value, currency, opt);
873
+ };
565
874
  }
566
875
 
567
876
  const cache = new Map();
@@ -574,16 +883,7 @@ function getFormatter(locale, style, numeric) {
574
883
  }
575
884
  return formatter;
576
885
  }
577
- /**
578
- * Format a relative time using the current or provided locale
579
- * 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
580
- *
581
- * @param value - The numeric value to use in the relative time internationalization message
582
- * @param unit - The unit to use in the relative time internationalization message
583
- * @param opt - Options for formatting
584
- * @returns Formatted relative time string
585
- */
586
- function formatRelativeTime(value, unit, opt) {
886
+ function formatRelativeTime(value, unit, optOrLocale) {
587
887
  const unwrappedValue = unwrap(value);
588
888
  if (unwrappedValue === null ||
589
889
  unwrappedValue === undefined ||
@@ -592,9 +892,78 @@ function formatRelativeTime(value, unit, opt) {
592
892
  const unwrappedUnit = unwrap(unit);
593
893
  if (!unwrappedUnit)
594
894
  return '';
595
- const unwrappedOpt = unwrap(opt);
596
- const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
597
- return getFormatter(loc, unwrappedOpt?.style ?? 'long', unwrappedOpt?.numeric ?? 'always').format(unwrappedValue, unwrappedUnit);
895
+ const unwrappedArgs = unwrap(optOrLocale);
896
+ let locale;
897
+ let style = 'long';
898
+ let numeric = 'always';
899
+ if (typeof unwrappedArgs === 'string') {
900
+ locale = unwrappedArgs;
901
+ }
902
+ else if (unwrappedArgs && typeof unwrappedArgs === 'object') {
903
+ locale = unwrappedArgs.locale ?? readLocaleUnsafe();
904
+ style = unwrappedArgs.style ?? 'long';
905
+ numeric = unwrappedArgs.numeric ?? 'always';
906
+ }
907
+ else {
908
+ locale = readLocaleUnsafe();
909
+ }
910
+ return getFormatter(locale, style, numeric).format(unwrappedValue, unwrappedUnit);
911
+ }
912
+ const [provideFormatRelativeTimeDefaults, injectFormatRelativeTimeOptions] = createFormatterProvider('relativeTime', {
913
+ style: 'long',
914
+ numeric: 'always',
915
+ }, (a, b) => a.style === b.style && a.numeric === b.numeric);
916
+ /**
917
+ * Inject a context-safe relative time formatting function tied to the current injector.
918
+ * Uses the libraries locale signal & provided default configuration to react to locale/config changes
919
+ * @example
920
+ * const formatRelativeTime = injectFormatRelativeTime();
921
+ * readonly relativeAge = computed(() => formatRelativeTime(this.delta(), 'day'));
922
+ */
923
+ function injectFormatRelativeTime() {
924
+ const defaults = injectFormatRelativeTimeOptions();
925
+ return (value, unit, optOrLocale) => {
926
+ if (!optOrLocale)
927
+ return formatRelativeTime(value, unit, defaults());
928
+ const unwrapped = unwrap(optOrLocale);
929
+ const opt = typeof unwrapped === 'object'
930
+ ? { ...defaults(), ...unwrapped }
931
+ : {
932
+ ...defaults(),
933
+ locale: unwrapped,
934
+ };
935
+ return formatRelativeTime(value, unit, opt);
936
+ };
937
+ }
938
+
939
+ function provideFormatDefaults(cfg) {
940
+ const providers = [];
941
+ if (cfg.date)
942
+ providers.push(provideFormatDateDefaults(cfg.date));
943
+ if (cfg.displayName)
944
+ providers.push(provideFormatDisplayNameDefaults(cfg.displayName));
945
+ if (cfg.list)
946
+ providers.push(provideFormatListDefaults(cfg.list));
947
+ if (cfg.relativeTime)
948
+ providers.push(provideFormatRelativeTimeDefaults(cfg.relativeTime));
949
+ if (cfg.number)
950
+ providers.push(provideFormatNumberDefaults(cfg.number));
951
+ if (cfg.percent)
952
+ providers.push(provideFormatPercentDefaults(cfg.percent));
953
+ if (cfg.currency)
954
+ providers.push(provideFormatCurrencyDefaults(cfg.currency));
955
+ return providers;
956
+ }
957
+ function injectFormatters() {
958
+ return {
959
+ date: injectFormatDate(),
960
+ displayName: injectFormatDisplayName(),
961
+ list: injectFormatList(),
962
+ relativeTime: injectFormatRelativeTime(),
963
+ number: injectFormatNumber(),
964
+ percent: injectFormatPercent(),
965
+ currency: injectFormatCurrency(),
966
+ };
598
967
  }
599
968
 
600
969
  function injectResolveParamLocale(snapshot) {
@@ -807,6 +1176,58 @@ function registerRemoteNamespace(ns, defaultTranslation, other) {
807
1176
  resolveNamespaceTranslation: resolver,
808
1177
  };
809
1178
  }
1179
+ class UnsafeTKeyMap {
1180
+ map = new Map();
1181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UnsafeTKeyMap, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1182
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UnsafeTKeyMap, providedIn: 'root' });
1183
+ }
1184
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UnsafeTKeyMap, decorators: [{
1185
+ type: Injectable,
1186
+ args: [{
1187
+ providedIn: 'root',
1188
+ }]
1189
+ }] });
1190
+ /**
1191
+ * Power-user escape hatch that returns a fully untyped translation function.
1192
+ * Intended for use alongside {@link injectAddTranslations} when translations
1193
+ * are added imperatively (e.g. from a remote API), or for cross-namespace
1194
+ * lookups where the typed API would be impractical.
1195
+ *
1196
+ * @example
1197
+ * ```ts
1198
+ * const t = injectUnsafeT();
1199
+ * t('any.namespace.key', { name: 'Alice', count: 3 });
1200
+ * const sig = t.asSignal('any.namespace.key', () => ({ name: name() }));
1201
+ * ```
1202
+ */
1203
+ function injectUnsafeT() {
1204
+ const store = inject(TranslationStore);
1205
+ const map = inject(UnsafeTKeyMap).map;
1206
+ const fn = (key, params) => {
1207
+ let k = map.get(key);
1208
+ if (k === undefined) {
1209
+ k = replaceWithDelim(key);
1210
+ map.set(key, k);
1211
+ }
1212
+ return store.formatMessage(k, params);
1213
+ };
1214
+ fn.asSignal = (key, params) => {
1215
+ let k = map.get(key);
1216
+ if (k === undefined) {
1217
+ k = replaceWithDelim(key);
1218
+ map.set(key, k);
1219
+ }
1220
+ if (!params)
1221
+ return store.buildSimpleKeySignal(k);
1222
+ const paramsSignal = isSignal(params)
1223
+ ? params
1224
+ : computed(() => params(), {
1225
+ equal: createEqualsRecord(Object.keys(params())),
1226
+ });
1227
+ return computed(() => store.formatMessage(k, paramsSignal()));
1228
+ };
1229
+ return fn;
1230
+ }
810
1231
 
811
1232
  /**
812
1233
  * Guard that validates the locale parameter against supported locales.
@@ -837,6 +1258,72 @@ function canMatchLocale(prefixSegments = []) {
837
1258
  };
838
1259
  }
839
1260
 
1261
+ /**
1262
+ * Provides an isolated mock `TranslationStore` usable across testing modules that use components
1263
+ * depending on `@mmstack/translate` APIs (like `Translate` directive, `Translator` pipe, or `injectNamespaceT`).
1264
+ *
1265
+ * This provider intercepts all translation logic, bypassing chunk loaders and Intl.
1266
+ * When a custom configuration isn't provided, formatMessage simply echoes the translation key, using dots `.`.
1267
+ *
1268
+ * ### Usage
1269
+ * ```typescript
1270
+ * TestBed.configureTestingModule({
1271
+ * providers: [provideMockTranslations()]
1272
+ * });
1273
+ * ```
1274
+ */
1275
+ function provideMockTranslations(options) {
1276
+ // We compile the mock strings to flat delimiters just like the internal compile module.
1277
+ const mappedMocks = {};
1278
+ if (options?.translations) {
1279
+ for (const [namespace, translationObj] of Object.entries(options.translations)) {
1280
+ const compiled = compileTranslation(translationObj, namespace);
1281
+ for (const [key, val] of Object.entries(compiled.flat)) {
1282
+ // e.g. from 'home::MMT_DELIM::title'
1283
+ const fullKey = `${namespace}::MMT_DELIM::${key}`;
1284
+ mappedMocks[fullKey] = val;
1285
+ }
1286
+ }
1287
+ }
1288
+ const locale = options?.locale ?? 'en-US';
1289
+ let intl;
1290
+ if (options?.formatValues) {
1291
+ intl = createIntl({ locale, messages: mappedMocks }, createIntlCache());
1292
+ }
1293
+ return [
1294
+ {
1295
+ provide: TranslationStore,
1296
+ useValue: {
1297
+ locale: signal(locale),
1298
+ formatMessage: (key, values) => {
1299
+ const message = mappedMocks[key];
1300
+ if (!message) {
1301
+ // Fallback to echoing the key back in dot notation (more readable for unit assertions).
1302
+ return key.replaceAll('::MMT_DELIM::', '.');
1303
+ }
1304
+ if (intl) {
1305
+ return intl.formatMessage({ id: key, defaultMessage: message }, values);
1306
+ }
1307
+ return message;
1308
+ },
1309
+ hasLocaleLoaders: () => false,
1310
+ register: () => {
1311
+ // noop
1312
+ },
1313
+ registerOnDemandLoaders: () => {
1314
+ // noop
1315
+ },
1316
+ dynamicLocaleLoader: {
1317
+ isLoading: signal(false),
1318
+ value: signal(null),
1319
+ error: signal(null),
1320
+ },
1321
+ loadQueue: signal([]),
1322
+ },
1323
+ },
1324
+ ];
1325
+ }
1326
+
840
1327
  function compareObjects(a, b) {
841
1328
  if (!a && !b)
842
1329
  return true;
@@ -875,10 +1362,10 @@ class Translate {
875
1362
  },
876
1363
  });
877
1364
  }
878
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: Translate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
879
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.8", type: Translate, isStandalone: true, inputs: { translate: { classPropertyName: "translate", publicName: "translate", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
1365
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: Translate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1366
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.12", type: Translate, isStandalone: true, inputs: { translate: { classPropertyName: "translate", publicName: "translate", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
880
1367
  }
881
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: Translate, decorators: [{
1368
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: Translate, decorators: [{
882
1369
  type: Directive
883
1370
  }], ctorParameters: () => [], propDecorators: { translate: [{ type: i0.Input, args: [{ isSignal: true, alias: "translate", required: true }] }] } });
884
1371
 
@@ -906,74 +1393,35 @@ class Translator {
906
1393
  }
907
1394
 
908
1395
  /**
909
- * Provides an isolated mock `TranslationStore` usable across testing modules that use components
910
- * depending on `@mmstack/translate` APIs (like `Translate` directive, `Translator` pipe, or `injectNamespaceT`).
1396
+ * Power-user escape hatch for ICU messages whose parameters can't be inferred
1397
+ * from the message string typically variables nested inside `plural` /
1398
+ * `select` / `selectordinal` arms, which the type-level extractor skips.
911
1399
  *
912
- * This provider intercepts all translation logic, bypassing chunk loaders and Intl.
913
- * When a custom configuration isn't provided, formatMessage simply echoes the translation key, using dots `.`.
1400
+ * Declared params are merged with auto-extracted ones; on key conflict, the
1401
+ * declared params win. Non-default locales for a key wrapped with `withParams`
1402
+ * may be plain strings — they don't need to repeat the wrapper.
914
1403
  *
915
- * ### Usage
916
- * ```typescript
917
- * TestBed.configureTestingModule({
918
- * providers: [provideMockTranslations()]
1404
+ * @example
1405
+ * ```ts
1406
+ * const ns = createNamespace('quote', {
1407
+ * // auto-extracts `count`; `name` is declared explicitly because it
1408
+ * // lives inside the plural arms and can't be inferred
1409
+ * stats: withParams<{ name: string }>(
1410
+ * '{count, plural, one {1 quote from {name}} other {# quotes from {name}}}',
1411
+ * ),
919
1412
  * });
1413
+ *
1414
+ * // t inferred as: (key, { count: number; name: string }) => string
1415
+ * t('quote.stats', { count: 3, name: 'Alice' });
920
1416
  * ```
921
1417
  */
922
- function provideMockTranslations(options) {
923
- // We compile the mock strings to flat delimiters just like the internal compile module.
924
- const mappedMocks = {};
925
- if (options?.translations) {
926
- for (const [namespace, translationObj] of Object.entries(options.translations)) {
927
- const compiled = compileTranslation(translationObj, namespace);
928
- for (const [key, val] of Object.entries(compiled.flat)) {
929
- // e.g. from 'home::MMT_DELIM::title'
930
- const fullKey = `${namespace}::MMT_DELIM::${key}`;
931
- mappedMocks[fullKey] = val;
932
- }
933
- }
934
- }
935
- const locale = options?.locale ?? 'en-US';
936
- let intl;
937
- if (options?.formatValues) {
938
- intl = createIntl({ locale, messages: mappedMocks }, createIntlCache());
939
- }
940
- return [
941
- {
942
- provide: TranslationStore,
943
- useValue: {
944
- locale: signal(locale),
945
- formatMessage: (key, values) => {
946
- const message = mappedMocks[key];
947
- if (!message) {
948
- // Fallback to echoing the key back in dot notation (more readable for unit assertions).
949
- return key.replaceAll('::MMT_DELIM::', '.');
950
- }
951
- if (intl) {
952
- return intl.formatMessage({ id: key, defaultMessage: message }, values);
953
- }
954
- return message;
955
- },
956
- hasLocaleLoaders: () => false,
957
- register: () => {
958
- // noop
959
- },
960
- registerOnDemandLoaders: () => {
961
- // noop
962
- },
963
- dynamicLocaleLoader: {
964
- isLoading: signal(false),
965
- value: signal(null),
966
- error: signal(null),
967
- },
968
- loadQueue: signal([]),
969
- },
970
- },
971
- ];
1418
+ function withParams(message) {
1419
+ return message;
972
1420
  }
973
1421
 
974
1422
  /**
975
1423
  * Generated bundle index. Do not edit.
976
1424
  */
977
1425
 
978
- export { Translate, Translator, canMatchLocale, compileTranslation, createNamespace, formatCurrency, formatDate, formatDisplayName, formatList, formatNumber, formatPercent, formatRelativeTime, injectDynamicLocale, injectIntl, injectResolveParamLocale, injectSupportedLocales, provideIntlConfig, provideMockTranslations, registerNamespace, registerRemoteNamespace };
1426
+ export { Translate, Translator, canMatchLocale, compileTranslation, createNamespace, formatCurrency, formatDate, formatDisplayName, formatList, formatNumber, formatPercent, formatRelativeTime, injectAddTranslations, 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 };
979
1427
  //# sourceMappingURL=mmstack-translate.mjs.map