@mmstack/translate 19.2.7 → 19.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, signal, LOCALE_ID, computed, Injectable, isSignal, isDevMode, input, Renderer2, ElementRef, effect, Directive } 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,27 +55,97 @@ 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');
135
+ function injectLocaleInternal() {
136
+ return STORE_LOCALE;
68
137
  }
69
138
  class TranslationStore {
70
139
  cache = createIntlCache();
71
140
  config = injectIntlConfig();
72
- locale = signal(inject(LOCALE_ID));
141
+ loadQueue = signal([]);
142
+ locale;
73
143
  defaultLocale = injectDefaultLocale();
74
144
  translations = signal({
75
145
  [this.defaultLocale]: {},
76
146
  });
147
+ attemptedFallbackLoad = false;
148
+ onDemandLoaders = new Map();
77
149
  nonMessageConfig = computed(() => ({
78
150
  ...this.config,
79
151
  locale: this.locale(),
@@ -81,19 +153,107 @@ class TranslationStore {
81
153
  messages = computed(() => this.translations()[this.locale()] ??
82
154
  this.translations()[this.defaultLocale] ??
83
155
  {});
156
+ dynamicLocaleLoader = resource({
157
+ request: computed(() => this.loadQueue().at(0) ?? null),
158
+ loader: async ({ request: newLocale, abortSignal }) => {
159
+ if (!newLocale)
160
+ return;
161
+ const currentTranslations = untracked(this.translations);
162
+ const loadPromises = [];
163
+ for (const [namespace, loaders] of this.onDemandLoaders.entries()) {
164
+ const loader = loaders[newLocale];
165
+ if (loader) {
166
+ const hasNamespaceForLocale = currentTranslations[newLocale] &&
167
+ Object.keys(currentTranslations[newLocale]).some((key) => key.startsWith(`${prependDelim(namespace, '').slice(0, -1)}`));
168
+ if (!hasNamespaceForLocale) {
169
+ loadPromises.push(loader()
170
+ .then((translation) => {
171
+ if (abortSignal.aborted)
172
+ return null;
173
+ return {
174
+ namespace: translation.namespace,
175
+ flat: translation.flat,
176
+ };
177
+ })
178
+ .catch((err) => {
179
+ if (isDevMode()) {
180
+ console.error('[Translate] Failed to load', namespace, newLocale, err);
181
+ }
182
+ return null;
183
+ }));
184
+ }
185
+ }
186
+ }
187
+ return Promise.all(loadPromises)
188
+ .then((res) => res.filter((r) => r !== null))
189
+ .then((res) => ({
190
+ locales: res,
191
+ locale: newLocale,
192
+ }));
193
+ },
194
+ });
84
195
  intl = computed(() => createIntl({
85
196
  ...this.nonMessageConfig(),
86
197
  messages: this.messages(),
87
198
  }, this.cache));
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
+ }
217
+ effect(() => {
218
+ if (
219
+ // should never be in error state, but best to check in case something throws
220
+ this.dynamicLocaleLoader.error() ||
221
+ this.dynamicLocaleLoader.isLoading())
222
+ return;
223
+ const dynamicLocales = this.dynamicLocaleLoader.value();
224
+ if (!dynamicLocales)
225
+ return;
226
+ // Register loaded translations
227
+ for (const locale of dynamicLocales.locales) {
228
+ this.register(locale.namespace, {
229
+ [dynamicLocales.locale]: locale.flat,
230
+ });
231
+ }
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
+ }
238
+ });
239
+ }
88
240
  formatMessage(key, values) {
89
- const message = this.translations()[this.locale()]?.[key] ?? '';
90
- 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
+ });
91
252
  return '';
253
+ }
92
254
  return this.intl().formatMessage({ id: key, defaultMessage: message }, values);
93
255
  }
94
- register(namespace, flat, locale) {
95
- console.log(locale);
96
- this.locale.set(locale);
256
+ register(namespace, flat) {
97
257
  this.translations.update((cur) => {
98
258
  return Object.entries(flat).reduce((acc, [locale, translation]) => {
99
259
  const localeTranslation = acc[locale] ?? {};
@@ -109,18 +269,309 @@ class TranslationStore {
109
269
  }, { ...cur });
110
270
  });
111
271
  }
112
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: TranslationStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
113
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: TranslationStore, providedIn: 'root' });
272
+ registerOnDemandLoaders(namespace, loaders) {
273
+ this.onDemandLoaders.set(namespace, loaders);
274
+ }
275
+ hasLocaleLoaders(locale) {
276
+ return Array.from(this.onDemandLoaders.values()).some((loaders) => loaders[locale]);
277
+ }
278
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: TranslationStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
279
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: TranslationStore, providedIn: 'root' });
114
280
  }
115
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: TranslationStore, decorators: [{
281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: TranslationStore, decorators: [{
116
282
  type: Injectable,
117
283
  args: [{
118
284
  providedIn: 'root',
119
285
  }]
120
- }] });
286
+ }], ctorParameters: () => [] });
121
287
  function injectIntl() {
122
288
  return inject(TranslationStore).intl;
123
289
  }
290
+ /**
291
+ * Inject a dynamic locale signal that supports runtime language switching.
292
+ *
293
+ * @returns A writable signal with the current locale and loading state.
294
+ * Only allows switching to locales that have registered loaders.
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * const locale = injectDynamicLocale();
299
+ *
300
+ * // Switch language (triggers automatic translation loading)
301
+ * locale.set('sl-SI');
302
+ *
303
+ * // Check loading state
304
+ * if (locale.isLoading()) {
305
+ * // Show spinner
306
+ * }
307
+ * ```
308
+ */
309
+ function injectDynamicLocale() {
310
+ const store = inject(TranslationStore);
311
+ const supportedLocales = injectIntlConfig()?.supportedLocales;
312
+ const source = computed(() => store.locale());
313
+ const inSupportedLocales = supportedLocales === undefined
314
+ ? () => true
315
+ : (locale) => supportedLocales.includes(locale);
316
+ const set = (value) => {
317
+ if (value === untracked(source) ||
318
+ untracked(store.loadQueue).includes(value))
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.`);
327
+ store.loadQueue.update((q) => [...q, value]);
328
+ };
329
+ source.set = set;
330
+ source.update = (updater) => {
331
+ const next = updater(untracked(source));
332
+ source.set(next);
333
+ };
334
+ source.asReadonly = () => source;
335
+ source.isLoading = store.dynamicLocaleLoader.isLoading;
336
+ return source;
337
+ }
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
+ });
475
+ cache$1.set(cacheKey, formatter);
476
+ }
477
+ return formatter;
478
+ }
479
+ /**
480
+ * Format a number using the current or provided locale
481
+ * 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
482
+ *
483
+ * @param number - Number to format
484
+ * @param opt - Options for formatting
485
+ * @returns Formatted number string
486
+ */
487
+ function formatNumber(value, opt) {
488
+ const unwrappedOpt = unwrap(opt);
489
+ const unwrappedNumber = unwrapValue(value, unwrappedOpt?.fallbackToZero);
490
+ if (unwrappedNumber === null)
491
+ return '';
492
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
493
+ return getFormatter$1(loc, unwrappedOpt?.minFractionDigits ?? 0, unwrappedOpt?.maxFractionDigits ?? 0, unwrappedOpt?.useGrouping ?? true, unwrappedOpt?.notation ?? 'standard').format(unwrappedNumber);
494
+ }
495
+ /**
496
+ * Format a percentage using the current or provided locale
497
+ * 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
498
+ *
499
+ * @param number - Number to format
500
+ * @param opt - Options for formatting
501
+ * @returns Formatted percentage string
502
+ */
503
+ function formatPercent(value, opt) {
504
+ const unwrappedOpt = unwrap(opt);
505
+ const unwrappedNumber = unwrapValue(value, unwrappedOpt?.fallbackToZero);
506
+ if (unwrappedNumber === null)
507
+ return '';
508
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
509
+ return getFormatter$1(loc, unwrappedOpt?.minFractionDigits ?? 0, unwrappedOpt?.maxFractionDigits ?? 0, undefined, undefined, undefined, undefined, 'percent').format(unwrappedNumber);
510
+ }
511
+ function formatCurrency(value, currency, opt) {
512
+ const unwrappedOpt = unwrap(opt);
513
+ const unwrappedValue = unwrapValue(value, unwrappedOpt?.fallbackToZero);
514
+ if (unwrappedValue === null)
515
+ return '';
516
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
517
+ return getFormatter$1(loc, 0, 0, undefined, undefined, unwrap(currency), unwrappedOpt?.display ?? 'symbol', 'currency').format(unwrappedValue);
518
+ }
519
+
520
+ const cache = new Map();
521
+ function getFormatter(locale, style, numeric) {
522
+ const cacheKey = `${locale}|${style}|${numeric}`;
523
+ let formatter = cache.get(cacheKey);
524
+ if (!formatter) {
525
+ formatter = new Intl.RelativeTimeFormat(locale, { style, numeric });
526
+ cache.set(cacheKey, formatter);
527
+ }
528
+ return formatter;
529
+ }
530
+ /**
531
+ * Format a relative time 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 numeric value to use in the relative time internationalization message
535
+ * @param unit - The unit to use in the relative time internationalization message
536
+ * @param opt - Options for formatting
537
+ * @returns Formatted relative time string
538
+ */
539
+ function formatRelativeTime(value, unit, opt) {
540
+ const unwrappedValue = unwrap(value);
541
+ if (unwrappedValue === null ||
542
+ unwrappedValue === undefined ||
543
+ isNaN(unwrappedValue))
544
+ return '';
545
+ const unwrappedUnit = unwrap(unit);
546
+ if (!unwrappedUnit)
547
+ return '';
548
+ const unwrappedOpt = unwrap(opt);
549
+ const loc = unwrappedOpt?.locale ?? injectLocaleInternal()();
550
+ return getFormatter(loc, unwrappedOpt?.style ?? 'long', unwrappedOpt?.numeric ?? 'always').format(unwrappedValue, unwrappedUnit);
551
+ }
552
+
553
+ function injectResolveParamLocale(snapshot) {
554
+ let locale = null;
555
+ const paramName = injectIntlConfig()?.localeParamName;
556
+ const routerConfig = inject(Router)['options'];
557
+ const alwaysInheritParams = typeof routerConfig === 'object' &&
558
+ !!routerConfig &&
559
+ routerConfig.paramsInheritanceStrategy === 'always';
560
+ if (paramName) {
561
+ locale = snapshot.paramMap.get(paramName);
562
+ if (!locale && !alwaysInheritParams) {
563
+ let currentRoute = snapshot;
564
+ while (currentRoute && !locale) {
565
+ locale = currentRoute.paramMap.get('locale');
566
+ currentRoute = currentRoute.parent;
567
+ }
568
+ }
569
+ }
570
+ if (!locale) {
571
+ locale = untracked(inject(TranslationStore).locale);
572
+ }
573
+ return locale;
574
+ }
124
575
 
125
576
  function createEqualsRecord(keys = []) {
126
577
  let keyMatcher;
@@ -150,7 +601,7 @@ function addSignalFn(fn, store) {
150
601
  const variables = args[0];
151
602
  const stringKey = key;
152
603
  const flatPath = replaceWithDelim(stringKey);
153
- const varsFn = variables === undefined ? () => undefined : variables;
604
+ const varsFn = variables ?? (() => undefined);
154
605
  const varsSignal = isSignal(varsFn)
155
606
  ? varsFn
156
607
  : computed(varsFn, {
@@ -174,9 +625,11 @@ function registerNamespace(defaultTranslation, other) {
174
625
  return addSignalFn(createT(store), store);
175
626
  };
176
627
  let defaultTranslationLoaded = false;
177
- const resolver = async () => {
178
- const locale = inject(LOCALE_ID);
628
+ const resolver = async (snapshot) => {
179
629
  const store = inject(TranslationStore);
630
+ const locale = injectResolveParamLocale(snapshot);
631
+ const defaultLocale = injectDefaultLocale();
632
+ const shouldPreloadDefault = injectIntlConfig()?.preloadDefaultLocale ?? false;
180
633
  const tPromise = other[locale];
181
634
  const promise = tPromise ?? defaultTranslation;
182
635
  if (!promise && isDevMode()) {
@@ -185,25 +638,110 @@ function registerNamespace(defaultTranslation, other) {
185
638
  if (promise === defaultTranslation && defaultTranslationLoaded)
186
639
  return;
187
640
  try {
188
- const translation = await promise();
189
- if (promise !== defaultTranslation &&
190
- translation.locale !== locale &&
191
- isDevMode()) {
192
- console.log('hre');
193
- return console.warn(`Expected locale to be ${locale} but got ${translation.locale}`);
194
- }
195
- store.register(translation.namespace, {
196
- [locale]: translation.flat,
197
- }, locale);
198
- if (promise === defaultTranslation) {
641
+ const promises = [promise()];
642
+ if (shouldPreloadDefault &&
643
+ !defaultTranslationLoaded &&
644
+ promise !== defaultTranslation)
645
+ promises.push(defaultTranslation());
646
+ const translations = await Promise.allSettled(promises);
647
+ const fullfilled = translations.map((t) => t.status === 'fulfilled' ? t.value : null);
648
+ if (fullfilled.at(0) === null && fullfilled.at(1) === null)
649
+ throw new Error('Failed to load translations');
650
+ const [t, defaultT] = fullfilled;
651
+ const ns = t?.namespace ?? defaultT?.namespace;
652
+ if (!ns)
653
+ throw new Error('No namespace found in translation');
654
+ if (isDevMode() && t && t.locale !== locale && t.locale)
655
+ console.warn(`Expected locale to be ${locale} but got ${t.locale}`);
656
+ store.registerOnDemandLoaders(ns, {
657
+ ...other,
658
+ [defaultLocale]: defaultTranslation,
659
+ });
660
+ const toRegister = {};
661
+ if (t)
662
+ toRegister[locale] = t.flat;
663
+ if (defaultT)
664
+ toRegister[defaultLocale] = defaultT.flat;
665
+ store.register(ns, toRegister);
666
+ if (promise === defaultTranslation || defaultT)
199
667
  defaultTranslationLoaded = true;
668
+ }
669
+ catch {
670
+ if (isDevMode()) {
671
+ console.warn(`Failed to load translation for locale: ${locale}`);
200
672
  }
201
673
  }
674
+ finally {
675
+ if (locale !== untracked(store.locale))
676
+ store.locale.set(locale);
677
+ }
678
+ };
679
+ return {
680
+ injectNamespaceT: injectT,
681
+ resolveNamespaceTranslation: resolver,
682
+ };
683
+ }
684
+ /**
685
+ * Registers a type-unsafe namespace, meant for remote loading of unknown key-value pairs using mmstack/translate infrastructure
686
+ * The resolver & t function work the same as they would with typed namespaces, but without type safety
687
+ */
688
+ function registerRemoteNamespace(ns, defaultTranslation, other) {
689
+ const injectT = () => {
690
+ const store = inject(TranslationStore);
691
+ return addSignalFn(createT(store), store);
692
+ };
693
+ let defaultTranslationLoaded = false;
694
+ const resolver = async (snapshot) => {
695
+ const store = inject(TranslationStore);
696
+ const locale = injectResolveParamLocale(snapshot);
697
+ const defaultLocale = injectDefaultLocale();
698
+ const shouldPreloadDefault = injectIntlConfig()?.preloadDefaultLocale ?? false;
699
+ const tPromise = other[locale];
700
+ const promise = tPromise ?? defaultTranslation;
701
+ if (!promise && isDevMode()) {
702
+ return console.warn(`No translation found for locale: ${locale}`);
703
+ }
704
+ if (promise === defaultTranslation && defaultTranslationLoaded)
705
+ return;
706
+ try {
707
+ const promises = [promise()];
708
+ if (shouldPreloadDefault &&
709
+ !defaultTranslationLoaded &&
710
+ promise !== defaultTranslation)
711
+ promises.push(defaultTranslation());
712
+ const translations = await Promise.allSettled(promises);
713
+ const fullfilled = translations.map((t) => t.status === 'fulfilled' ? t.value : null);
714
+ if (fullfilled.at(0) === null && fullfilled.at(1) === null)
715
+ throw new Error('Failed to load translations');
716
+ const [baseT, baseDefaultT] = fullfilled;
717
+ const t = baseT ? compileTranslation(baseT, ns, locale) : null;
718
+ const defaultT = baseDefaultT
719
+ ? compileTranslation(baseDefaultT, ns, defaultLocale)
720
+ : null;
721
+ if (isDevMode() && t && t.locale !== locale && t.locale)
722
+ console.warn(`Expected locale to be ${locale} but got ${t.locale}`);
723
+ store.registerOnDemandLoaders(ns, {
724
+ ...other,
725
+ [defaultLocale]: defaultTranslation,
726
+ });
727
+ const toRegister = {};
728
+ if (t)
729
+ toRegister[locale] = t.flat;
730
+ if (defaultT)
731
+ toRegister[defaultLocale] = defaultT.flat;
732
+ store.register(ns, toRegister);
733
+ if (promise === defaultTranslation || defaultT)
734
+ defaultTranslationLoaded = true;
735
+ }
202
736
  catch {
203
737
  if (isDevMode()) {
204
738
  console.warn(`Failed to load translation for locale: ${locale}`);
205
739
  }
206
740
  }
741
+ finally {
742
+ if (locale !== untracked(store.locale))
743
+ store.locale.set(locale);
744
+ }
207
745
  };
208
746
  return {
209
747
  injectNamespaceT: injectT,
@@ -211,7 +749,36 @@ function registerNamespace(defaultTranslation, other) {
211
749
  };
212
750
  }
213
751
 
214
- class BaseTranslateDirective {
752
+ /**
753
+ * Guard that validates the locale parameter against supported locales.
754
+ * Redirects to default locale if the locale is invalid.
755
+ *
756
+ * @param prefixSegments Optional array of path segments preceding the locale segment.
757
+ * if (you wanted to match /app/:locale/... you would pass ['app'] here) & the function would match the second parameter + redirect accordingly
758
+ *
759
+ * @example
760
+ * ```typescript
761
+ * {
762
+ * path: ':locale',
763
+ * canMatch: [canMatchLocale()],
764
+ * children: [...]
765
+ * }
766
+ * ```
767
+ */
768
+ function canMatchLocale(prefixSegments = []) {
769
+ return (_route, segments) => {
770
+ const supportedLocales = injectSupportedLocales();
771
+ const locale = segments.at(prefixSegments.length)?.path;
772
+ if (!locale || !supportedLocales.includes(locale))
773
+ return inject(Router).createUrlTree([
774
+ ...prefixSegments,
775
+ injectDefaultLocale(),
776
+ ]);
777
+ return true;
778
+ };
779
+ }
780
+
781
+ class Translate {
215
782
  t = createT(inject(TranslationStore));
216
783
  translate = input.required();
217
784
  constructor() {
@@ -239,19 +806,32 @@ class BaseTranslateDirective {
239
806
  const translation = computed(() => this.t(key(), args()));
240
807
  const renderer = inject(Renderer2);
241
808
  const el = inject(ElementRef);
242
- effect(() => renderer.setProperty(el.nativeElement, 'textContent', translation()));
809
+ afterRenderEffect({
810
+ write: () => {
811
+ renderer.setProperty(el.nativeElement, 'textContent', translation());
812
+ },
813
+ });
243
814
  }
244
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: BaseTranslateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
245
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.3", type: BaseTranslateDirective, isStandalone: true, inputs: { translate: { classPropertyName: "translate", publicName: "translate", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
815
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: Translate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
816
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.19", type: Translate, isStandalone: true, inputs: { translate: { classPropertyName: "translate", publicName: "translate", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
246
817
  }
247
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: BaseTranslateDirective, decorators: [{
818
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: Translate, decorators: [{
248
819
  type: Directive
249
820
  }], ctorParameters: () => [] });
250
821
 
251
- class BaseTranslatePipe {
252
- t = createT(inject(TranslationStore));
822
+ class Translator {
823
+ store = inject(TranslationStore);
824
+ t = createT(this.store);
825
+ constructor() {
826
+ const cdr = inject(ChangeDetectorRef);
827
+ effect(() => {
828
+ this.store.locale();
829
+ cdr.markForCheck();
830
+ });
831
+ }
253
832
  transform(key, ...args) {
254
- return this.t(key, ...args);
833
+ const actualArgs = args.filter((a) => typeof a === 'object');
834
+ return this.t(key, ...actualArgs);
255
835
  }
256
836
  }
257
837
 
@@ -259,5 +839,5 @@ class BaseTranslatePipe {
259
839
  * Generated bundle index. Do not edit.
260
840
  */
261
841
 
262
- export { BaseTranslateDirective, BaseTranslatePipe, compileTranslation, createNamespace, injectIntl, provideIntlConfig, registerNamespace };
842
+ export { Translate, Translator, canMatchLocale, compileTranslation, createNamespace, formatCurrency, formatDate, formatDisplayName, formatList, formatNumber, formatPercent, formatRelativeTime, injectDynamicLocale, injectIntl, injectResolveParamLocale, injectSupportedLocales, provideIntlConfig, registerNamespace, registerRemoteNamespace };
263
843
  //# sourceMappingURL=mmstack-translate.mjs.map