@cocoar/localization 0.1.0-beta.155 → 0.1.0-beta.186

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,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { signal, computed, Injectable, InjectionToken, inject, DestroyRef, makeEnvironmentProviders, APP_INITIALIZER, Pipe, ChangeDetectorRef } from '@angular/core';
3
- import { of, switchMap, tap, catchError, forkJoin, combineLatest, BehaviorSubject, Subject, lastValueFrom, map as map$1, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
3
+ import { forkJoin, catchError, of, map, distinctUntilChanged, switchMap, tap, combineLatest, BehaviorSubject, Subject, lastValueFrom } from 'rxjs';
4
4
  import { ReadonlyState, BehaviorSubjectProxy } from '@cocoar/ts-utils';
5
- import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
5
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
6
  import { HttpClient } from '@angular/common/http';
7
- import { map, distinctUntilChanged } from 'rxjs/operators';
7
+ import { map as map$1, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs/operators';
8
8
  import { Temporal } from '@js-temporal/polyfill';
9
9
 
10
10
  /**
@@ -64,6 +64,63 @@ class CoarLocalizationDataStore {
64
64
  clear() {
65
65
  this.store.set(new Map());
66
66
  }
67
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationDataStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
68
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationDataStore, providedIn: 'root' });
69
+ }
70
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationDataStore, decorators: [{
71
+ type: Injectable,
72
+ args: [{ providedIn: 'root' }]
73
+ }] });
74
+
75
+ /**
76
+ * Deep merge utility for locale data.
77
+ * Later sources override earlier sources at the field level.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const intl = { date: { pattern: 'dd.mm.yyyy', firstDayOfWeek: 1 }, number: {...} };
82
+ * const http = { date: { firstDayOfWeek: 0 } }; // Only override firstDayOfWeek
83
+ *
84
+ * const result = mergeLocalizationData([intl, http]);
85
+ * // Result: { date: { pattern: 'dd.mm.yyyy', firstDayOfWeek: 0 }, number: {...} }
86
+ * ```
87
+ */
88
+ function mergeLocalizationData(sources) {
89
+ if (sources.length === 0)
90
+ return null;
91
+ const result = {};
92
+ for (const source of sources) {
93
+ if (!source)
94
+ continue;
95
+ // Merge code
96
+ if (source.code) {
97
+ result.code = source.code;
98
+ }
99
+ // Deep merge date
100
+ if (source.date) {
101
+ result.date = { ...result.date, ...source.date };
102
+ }
103
+ // Deep merge number
104
+ if (source.number) {
105
+ result.number = { ...result.number, ...source.number };
106
+ }
107
+ // Deep merge currency
108
+ if (source.currency) {
109
+ result.currency = {
110
+ ...result.currency,
111
+ ...source.currency,
112
+ symbols: { ...result.currency?.symbols, ...source.currency.symbols },
113
+ };
114
+ }
115
+ // Deep merge percent
116
+ if (source.percent) {
117
+ result.percent = { ...result.percent, ...source.percent };
118
+ }
119
+ }
120
+ // Ensure we have at least a code
121
+ if (!result.code)
122
+ return null;
123
+ return result;
67
124
  }
68
125
 
69
126
  /**
@@ -79,12 +136,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
79
136
  args: [{ providedIn: 'root' }]
80
137
  }] });
81
138
  /**
82
- * HTTP-based locale data loader.
83
- * Loads locale data from JSON files via HTTP.
139
+ * HTTP-based locale data loader with BCP 47 fallback.
84
140
  *
85
- * Supports custom URL patterns and headers for authentication/configuration.
141
+ * Loads locale data from JSON files via HTTP.
86
142
  * Typically used as a second source (after Intl) to provide business-specific overrides.
87
143
  *
144
+ * When a full BCP 47 tag is used (e.g., `de-AT`), the loader:
145
+ * 1. Loads the base language file (`de.json`)
146
+ * 2. Tries to load the regional file (`de-AT.json`)
147
+ * 3. Deep-merges them — regional fields override base fields
148
+ *
149
+ * If the regional file doesn't exist, the base file is used as-is.
150
+ * If the base file doesn't exist either, the loader fails (propagated to caller).
151
+ *
88
152
  * @example
89
153
  * ```typescript
90
154
  * // Simple base path
@@ -100,6 +164,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
100
164
  * { 'Authorization': 'Bearer token' }
101
165
  * )
102
166
  * ```
167
+ *
168
+ * ## Expected file structure
169
+ * ```
170
+ * /locales/
171
+ * en.json ← base English overrides
172
+ * en-AT.json ← optional: Austrian-specific overrides on top of en.json
173
+ * de.json ← base German overrides
174
+ * de-AT.json ← optional: Austrian-specific overrides on top of de.json
175
+ * ```
103
176
  */
104
177
  class CoarHttpLocaleDataLoader extends CoarLocalizationDataLoader {
105
178
  httpClient;
@@ -112,12 +185,42 @@ class CoarHttpLocaleDataLoader extends CoarLocalizationDataLoader {
112
185
  this.headers = headers;
113
186
  }
114
187
  loadLocaleData(locale) {
188
+ const baseLanguage = extractBaseLanguage$1(locale);
189
+ if (baseLanguage) {
190
+ // BCP 47 tag with region (e.g., 'de-AT'):
191
+ // Load base ('de') first, then try regional ('de-AT'), deep-merge them.
192
+ return forkJoin([
193
+ this.loadFile(baseLanguage),
194
+ this.loadFile(locale).pipe(catchError(() => of(null))),
195
+ ]).pipe(map(([base, regional]) => {
196
+ if (!regional)
197
+ return base;
198
+ return mergeLocalizationData([base, regional]) ?? base;
199
+ }));
200
+ }
201
+ // Simple language code (e.g., 'de'): load directly.
202
+ return this.loadFile(locale);
203
+ }
204
+ loadFile(locale) {
115
205
  const url = this.urlFn(locale);
116
206
  return this.httpClient.get(url, {
117
207
  headers: this.headers,
118
208
  });
119
209
  }
120
210
  }
211
+ /**
212
+ * Extract the base language from a BCP 47 tag.
213
+ * Returns `null` if the tag has no region component.
214
+ *
215
+ * @example
216
+ * extractBaseLanguage('de-AT') // → 'de'
217
+ * extractBaseLanguage('en-US') // → 'en'
218
+ * extractBaseLanguage('de') // → null
219
+ */
220
+ function extractBaseLanguage$1(language) {
221
+ const hyphenIndex = language.indexOf('-');
222
+ return hyphenIndex > 0 ? language.substring(0, hyphenIndex) : null;
223
+ }
121
224
 
122
225
  /**
123
226
  * Locale data loader that detects formatting from browser's Intl API.
@@ -353,6 +456,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
353
456
  */
354
457
  const COAR_I18N_PROVIDER = new InjectionToken('COAR_I18N_PROVIDER');
355
458
 
459
+ /**
460
+ * Determines if a translation result should be considered "missing".
461
+ *
462
+ * A translation is considered missing if it is:
463
+ * - null or undefined
464
+ * - an empty string (after trim)
465
+ * - exactly equal to the requested key
466
+ *
467
+ * This provides unified missing-key semantics across different i18n engines.
468
+ *
469
+ * @param key - The translation key that was requested
470
+ * @param result - The result returned by the i18n service
471
+ * @returns true if the translation is missing, false otherwise
472
+ *
473
+ * @example
474
+ * ```ts
475
+ * coarIsMissingTranslation('coar.button.save', null) // true
476
+ * coarIsMissingTranslation('coar.button.save', '') // true
477
+ * coarIsMissingTranslation('coar.button.save', 'coar.button.save') // true
478
+ * coarIsMissingTranslation('coar.button.save', 'Save') // false
479
+ * ```
480
+ */
481
+ function coarIsMissingTranslation(key, result) {
482
+ if (result == null) {
483
+ return true;
484
+ }
485
+ const trimmed = result.trim();
486
+ if (!trimmed) {
487
+ return true;
488
+ }
489
+ return trimmed === key;
490
+ }
491
+
356
492
  /**
357
493
  * Interpolates {placeholders} in a template string using the given params.
358
494
  *
@@ -379,6 +515,75 @@ function coarInterpolate(template, params) {
379
515
  });
380
516
  }
381
517
 
518
+ class CoarI18n {
519
+ provider = inject(COAR_I18N_PROVIDER, { optional: true });
520
+ locale = inject(CoarLocalizationService, { optional: true });
521
+ t(key, fallbackOrParams, maybeParams) {
522
+ let fallback;
523
+ let params;
524
+ if (typeof fallbackOrParams === 'string') {
525
+ // t(key, 'Fallback') or t(key, 'Fallback', params)
526
+ fallback = fallbackOrParams;
527
+ params = maybeParams;
528
+ }
529
+ else {
530
+ // t(key) or t(key, params)
531
+ params = fallbackOrParams;
532
+ fallback = undefined;
533
+ }
534
+ if (!this.provider) {
535
+ return coarInterpolate(fallback ?? key, params);
536
+ }
537
+ const raw = this.provider.t(key, params);
538
+ const base = coarIsMissingTranslation(key, raw) ? (fallback ?? key) : (raw ?? '');
539
+ // Important: the fallback (and even the key) may contain {placeholders}
540
+ return coarInterpolate(base, params);
541
+ }
542
+ /**
543
+ * Reactive variant that updates when the language changes.
544
+ * Uses CoarLocalizationService to detect language changes.
545
+ */
546
+ t$(key, params, fallback) {
547
+ if (!this.locale) {
548
+ return of(this.callT(key, params, fallback));
549
+ }
550
+ // Re-evaluate on every language change from CoarLocalizationService
551
+ return this.locale.languageState.value$.pipe(map(() => this.callT(key, params, fallback)), distinctUntilChanged());
552
+ }
553
+ /**
554
+ * Signal variant that updates when the language changes.
555
+ * Uses CoarLocalizationService to detect language changes.
556
+ */
557
+ tSignal(key, params, fallback) {
558
+ const obs$ = this.t$(key, params, fallback);
559
+ return toSignal(obs$, {
560
+ initialValue: this.callT(key, params, fallback),
561
+ });
562
+ }
563
+ // Small helper so t$ doesn't need to replicate the overload matrix.
564
+ callT(key, params, fallback) {
565
+ if (params && fallback !== undefined) {
566
+ // t(key, fallback, params)
567
+ return this.t(key, fallback, params);
568
+ }
569
+ if (params) {
570
+ // t(key, params)
571
+ return this.t(key, params);
572
+ }
573
+ if (fallback !== undefined) {
574
+ // t(key, fallback)
575
+ return this.t(key, fallback);
576
+ }
577
+ // t(key)
578
+ return this.t(key);
579
+ }
580
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
581
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n });
582
+ }
583
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n, decorators: [{
584
+ type: Injectable
585
+ }] });
586
+
382
587
  /**
383
588
  * Injection token for translation loaders (multi-provider).
384
589
  * Loaders are executed in order and results are deep-merged.
@@ -409,10 +614,21 @@ const COAR_TRANSLATION_LOADERS = new InjectionToken('COAR_TRANSLATION_LOADERS');
409
614
  class CoarTranslationLoader {
410
615
  }
411
616
  /**
412
- * HTTP-based translation loader.
617
+ * HTTP-based translation loader with BCP 47 fallback.
413
618
  *
414
619
  * Loads translation JSON files from a configurable URL function.
415
620
  *
621
+ * When a full BCP 47 tag is used (e.g., `de-AT`), the loader:
622
+ * 1. Loads the base language file (`de.json`)
623
+ * 2. Tries to load the regional file (`de-AT.json`)
624
+ * 3. Merges them — regional keys override base keys
625
+ *
626
+ * If the regional file doesn't exist, the base file is used as-is.
627
+ * If the base file doesn't exist either, the loader fails (propagated to caller).
628
+ *
629
+ * This means `en.json` serves all English variants. An optional `en-AT.json`
630
+ * only needs to contain keys that differ from the base.
631
+ *
416
632
  * ## Usage
417
633
  * ```ts
418
634
  * const loader = new CoarHttpTranslationLoader();
@@ -420,6 +636,15 @@ class CoarTranslationLoader {
420
636
  * loader.headers = { 'Authorization': 'Bearer token' };
421
637
  * ```
422
638
  *
639
+ * ## Expected file structure
640
+ * ```
641
+ * /i18n/
642
+ * en.json ← base English (used for en-US, en-GB, en-AT, etc.)
643
+ * en-AT.json ← optional: only keys that differ from en.json
644
+ * de.json ← base German
645
+ * de-AT.json ← optional: Austrian overrides (if any)
646
+ * ```
647
+ *
423
648
  * ## Expected JSON format
424
649
  * ```json
425
650
  * {
@@ -436,6 +661,22 @@ class CoarHttpTranslationLoader {
436
661
  /** Optional HTTP headers */
437
662
  headers;
438
663
  loadTranslations(language) {
664
+ const baseLanguage = extractBaseLanguage(language);
665
+ if (baseLanguage) {
666
+ // BCP 47 tag with region (e.g., 'de-AT'):
667
+ // Load base ('de') first, then try regional ('de-AT'), merge them.
668
+ return forkJoin([
669
+ this.loadFile(baseLanguage),
670
+ this.loadFile(language).pipe(catchError(() => of(null))),
671
+ ]).pipe(map(([base, regional]) => ({
672
+ ...base,
673
+ ...(regional ?? {}),
674
+ })));
675
+ }
676
+ // Simple language code (e.g., 'de'): load directly.
677
+ return this.loadFile(language);
678
+ }
679
+ loadFile(language) {
439
680
  const url = this.urlFn(language);
440
681
  return this.http.get(url, {
441
682
  headers: this.headers,
@@ -447,6 +688,19 @@ class CoarHttpTranslationLoader {
447
688
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarHttpTranslationLoader, decorators: [{
448
689
  type: Injectable
449
690
  }] });
691
+ /**
692
+ * Extract the base language from a BCP 47 tag.
693
+ * Returns `null` if the tag has no region component.
694
+ *
695
+ * @example
696
+ * extractBaseLanguage('de-AT') // → 'de'
697
+ * extractBaseLanguage('en-US') // → 'en'
698
+ * extractBaseLanguage('de') // → null
699
+ */
700
+ function extractBaseLanguage(language) {
701
+ const hyphenIndex = language.indexOf('-');
702
+ return hyphenIndex > 0 ? language.substring(0, hyphenIndex) : null;
703
+ }
450
704
 
451
705
  /**
452
706
  * Flattens nested translation objects into dot notation.
@@ -1020,13 +1274,13 @@ class CoarTimeZoneService {
1020
1274
  const providerStreams = allProviders.map((provider) => provider.timeZone$);
1021
1275
  this.timeZone$ = combineLatest(providerStreams).pipe(
1022
1276
  // Find first non-null value in priority order
1023
- map((timeZones) => {
1277
+ map$1((timeZones) => {
1024
1278
  const resolved = timeZones.find((tz) => tz !== null);
1025
1279
  // UTC as safety net if all providers return null (shouldn't happen)
1026
1280
  return resolved ?? 'UTC';
1027
1281
  }),
1028
1282
  // Prevent unnecessary emissions when value doesn't actually change
1029
- distinctUntilChanged());
1283
+ distinctUntilChanged$1());
1030
1284
  // Initialize signal after timeZone$ is set up
1031
1285
  this.currentTimeZone = toSignal(this.timeZone$, { requireSync: true });
1032
1286
  }
@@ -1107,6 +1361,8 @@ function provideCoarLocalization(config) {
1107
1361
  provide: COAR_I18N_PROVIDER,
1108
1362
  useClass: CoarI18nService,
1109
1363
  },
1364
+ // i18n: Convenience service used by CoarI18nPipe and consumers
1365
+ CoarI18n,
1110
1366
  // i18n: Auto-include Intl source as first translation loader (provides common defaults)
1111
1367
  {
1112
1368
  provide: COAR_TRANSLATION_LOADERS,
@@ -1142,57 +1398,6 @@ function provideCoarLocalization(config) {
1142
1398
  ]);
1143
1399
  }
1144
1400
 
1145
- /**
1146
- * Deep merge utility for locale data.
1147
- * Later sources override earlier sources at the field level.
1148
- *
1149
- * @example
1150
- * ```ts
1151
- * const intl = { date: { pattern: 'dd.mm.yyyy', firstDayOfWeek: 1 }, number: {...} };
1152
- * const http = { date: { firstDayOfWeek: 0 } }; // Only override firstDayOfWeek
1153
- *
1154
- * const result = mergeLocalizationData([intl, http]);
1155
- * // Result: { date: { pattern: 'dd.mm.yyyy', firstDayOfWeek: 0 }, number: {...} }
1156
- * ```
1157
- */
1158
- function mergeLocalizationData(sources) {
1159
- if (sources.length === 0)
1160
- return null;
1161
- const result = {};
1162
- for (const source of sources) {
1163
- if (!source)
1164
- continue;
1165
- // Merge code
1166
- if (source.code) {
1167
- result.code = source.code;
1168
- }
1169
- // Deep merge date
1170
- if (source.date) {
1171
- result.date = { ...result.date, ...source.date };
1172
- }
1173
- // Deep merge number
1174
- if (source.number) {
1175
- result.number = { ...result.number, ...source.number };
1176
- }
1177
- // Deep merge currency
1178
- if (source.currency) {
1179
- result.currency = {
1180
- ...result.currency,
1181
- ...source.currency,
1182
- symbols: { ...result.currency?.symbols, ...source.currency.symbols },
1183
- };
1184
- }
1185
- // Deep merge percent
1186
- if (source.percent) {
1187
- result.percent = { ...result.percent, ...source.percent };
1188
- }
1189
- }
1190
- // Ensure we have at least a code
1191
- if (!result.code)
1192
- return null;
1193
- return result;
1194
- }
1195
-
1196
1401
  /**
1197
1402
  * Core locale service responsible for language management.
1198
1403
  *
@@ -1663,105 +1868,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
1663
1868
  }]
1664
1869
  }] });
1665
1870
 
1666
- /**
1667
- * Determines if a translation result should be considered "missing".
1668
- *
1669
- * A translation is considered missing if it is:
1670
- * - null or undefined
1671
- * - an empty string (after trim)
1672
- * - exactly equal to the requested key
1673
- *
1674
- * This provides unified missing-key semantics across different i18n engines.
1675
- *
1676
- * @param key - The translation key that was requested
1677
- * @param result - The result returned by the i18n service
1678
- * @returns true if the translation is missing, false otherwise
1679
- *
1680
- * @example
1681
- * ```ts
1682
- * coarIsMissingTranslation('coar.button.save', null) // true
1683
- * coarIsMissingTranslation('coar.button.save', '') // true
1684
- * coarIsMissingTranslation('coar.button.save', 'coar.button.save') // true
1685
- * coarIsMissingTranslation('coar.button.save', 'Save') // false
1686
- * ```
1687
- */
1688
- function coarIsMissingTranslation(key, result) {
1689
- if (result == null) {
1690
- return true;
1691
- }
1692
- const trimmed = result.trim();
1693
- if (!trimmed) {
1694
- return true;
1695
- }
1696
- return trimmed === key;
1697
- }
1698
-
1699
- class CoarI18n {
1700
- provider = inject(COAR_I18N_PROVIDER);
1701
- locale = inject(CoarLocalizationService);
1702
- t(key, fallbackOrParams, maybeParams) {
1703
- let fallback;
1704
- let params;
1705
- if (typeof fallbackOrParams === 'string') {
1706
- // t(key, 'Fallback') or t(key, 'Fallback', params)
1707
- fallback = fallbackOrParams;
1708
- params = maybeParams;
1709
- }
1710
- else {
1711
- // t(key) or t(key, params)
1712
- params = fallbackOrParams;
1713
- fallback = undefined;
1714
- }
1715
- const raw = this.provider.t(key, params);
1716
- const base = coarIsMissingTranslation(key, raw) ? (fallback ?? key) : (raw ?? '');
1717
- // Important: the fallback (and even the key) may contain {placeholders}
1718
- return coarInterpolate(base, params);
1719
- }
1720
- /**
1721
- * Reactive variant that updates when the language changes.
1722
- * Uses CoarLocalizationService to detect language changes.
1723
- */
1724
- t$(key, params, fallback) {
1725
- // Re-evaluate on every language change from CoarLocalizationService
1726
- return this.locale.languageState.value$.pipe(map$1(() => this.callT(key, params, fallback)), distinctUntilChanged$1());
1727
- }
1728
- /**
1729
- * Signal variant that updates when the language changes.
1730
- * Uses CoarLocalizationService to detect language changes.
1731
- */
1732
- tSignal(key, params, fallback) {
1733
- const obs$ = this.t$(key, params, fallback);
1734
- return toSignal(obs$, {
1735
- initialValue: this.callT(key, params, fallback),
1736
- });
1737
- }
1738
- // Small helper so t$ doesn't need to replicate the overload matrix.
1739
- callT(key, params, fallback) {
1740
- if (params && fallback !== undefined) {
1741
- // t(key, fallback, params)
1742
- return this.t(key, fallback, params);
1743
- }
1744
- if (params) {
1745
- // t(key, params)
1746
- return this.t(key, params);
1747
- }
1748
- if (fallback !== undefined) {
1749
- // t(key, fallback)
1750
- return this.t(key, fallback);
1751
- }
1752
- // t(key)
1753
- return this.t(key);
1754
- }
1755
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1756
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n, providedIn: 'root' });
1757
- }
1758
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n, decorators: [{
1759
- type: Injectable,
1760
- args: [{ providedIn: 'root' }]
1761
- }] });
1762
-
1763
1871
  class CoarI18nPipe {
1764
- i18n = inject(CoarI18n);
1872
+ i18n = inject(CoarI18n, { optional: true });
1765
1873
  cdr = inject(ChangeDetectorRef);
1766
1874
  subject = new BehaviorSubjectProxy('');
1767
1875
  lastKey;
@@ -1795,6 +1903,10 @@ class CoarI18nPipe {
1795
1903
  fallback = maybeFallbackOrParams;
1796
1904
  }
1797
1905
  }
1906
+ // No i18n service available — return fallback or key
1907
+ if (!this.i18n) {
1908
+ return coarInterpolate(fallback ?? key, params);
1909
+ }
1798
1910
  // Compare inputs to determine if a new subscription is needed
1799
1911
  const paramsJson = params ? JSON.stringify(params) : undefined;
1800
1912
  if (key !== this.lastKey ||