@cocoar/localization 0.1.0-beta.155 → 0.1.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,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,
|
|
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 {
|
|
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
|
-
*
|
|
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 ||
|