@cocoar/localization 0.1.0-beta.114
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.
- package/README.md +238 -0
- package/fesm2022/cocoar-localization.mjs +1752 -0
- package/fesm2022/cocoar-localization.mjs.map +1 -0
- package/package.json +49 -0
- package/types/cocoar-localization.d.ts +1194 -0
|
@@ -0,0 +1,1752 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, InjectionToken, makeEnvironmentProviders, APP_INITIALIZER, inject, Injectable, Pipe, ChangeDetectorRef, computed, effect, untracked } from '@angular/core';
|
|
3
|
+
import { Subject, lastValueFrom, of, startWith, map, distinctUntilChanged, tap, switchMap, catchError } from 'rxjs';
|
|
4
|
+
import { HttpClient } from '@angular/common/http';
|
|
5
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
6
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
7
|
+
import { BehaviorSubjectProxy } from '@cocoar/ts-utils';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Signal-based storage for locale data.
|
|
11
|
+
* Provides reactive access to loaded locale formatting rules.
|
|
12
|
+
*/
|
|
13
|
+
class CoarLocalizationDataStore {
|
|
14
|
+
store = signal(new Map(), ...(ngDevMode ? [{ debugName: "store" }] : []));
|
|
15
|
+
/**
|
|
16
|
+
* Set locale data for a specific locale.
|
|
17
|
+
* @param locale Locale code (e.g., 'en', 'de', 'en-US')
|
|
18
|
+
* @param data Locale data
|
|
19
|
+
*/
|
|
20
|
+
setLocaleData(locale, data) {
|
|
21
|
+
const current = this.store();
|
|
22
|
+
const updated = new Map(current);
|
|
23
|
+
updated.set(locale, data);
|
|
24
|
+
this.store.set(updated);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get locale data for a specific locale.
|
|
28
|
+
* @param locale Locale code (e.g., 'en', 'de', 'en-US')
|
|
29
|
+
* @returns Locale data or undefined if not loaded
|
|
30
|
+
*/
|
|
31
|
+
getLocaleData(locale) {
|
|
32
|
+
return this.store().get(locale);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if locale data is loaded for a specific locale.
|
|
36
|
+
* @param locale Locale code (e.g., 'en', 'de', 'en-US')
|
|
37
|
+
* @returns True if locale data is loaded
|
|
38
|
+
*/
|
|
39
|
+
hasLocaleData(locale) {
|
|
40
|
+
return this.store().has(locale);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Remove locale data for a specific locale.
|
|
44
|
+
* @param locale Locale code (e.g., 'en', 'de', 'en-US')
|
|
45
|
+
*/
|
|
46
|
+
removeLocaleData(locale) {
|
|
47
|
+
const current = this.store();
|
|
48
|
+
const updated = new Map(current);
|
|
49
|
+
updated.delete(locale);
|
|
50
|
+
this.store.set(updated);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clear all loaded locale data.
|
|
54
|
+
*/
|
|
55
|
+
clear() {
|
|
56
|
+
this.store.set(new Map());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Injection token for locale configuration.
|
|
62
|
+
*/
|
|
63
|
+
const COAR_LOCALIZATION_CONFIG = new InjectionToken('COAR_LOCALIZATION_CONFIG');
|
|
64
|
+
/**
|
|
65
|
+
* Injection token for locale data loaders (multi-provider).
|
|
66
|
+
* Loaders are executed in order and results are deep-merged.
|
|
67
|
+
* Intl loader is always first (provides complete defaults).
|
|
68
|
+
*/
|
|
69
|
+
const COAR_LOCALIZATION_DATA_LOADERS = new InjectionToken('COAR_LOCALIZATION_DATA_LOADERS');
|
|
70
|
+
/**
|
|
71
|
+
* Provides the core locale system (language management).
|
|
72
|
+
*
|
|
73
|
+
* This only provides the language service - no locale data sources.
|
|
74
|
+
* Add locale data sources separately with:
|
|
75
|
+
* - `provideCoarIntlLocalizationSource()` - Browser Intl API (recommended as first source)
|
|
76
|
+
* - `provideCoarHttpLocalizationSource()` - Load from JSON files
|
|
77
|
+
* - Custom sources via `COAR_LOCALIZATION_DATA_LOADERS` multi-provider
|
|
78
|
+
*
|
|
79
|
+
* Sources are executed in registration order and deep-merged.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* // Minimal setup (no formatting data)
|
|
84
|
+
* provideCoarLocalization({
|
|
85
|
+
* availableLanguages: ['en', 'de'],
|
|
86
|
+
* defaultLanguage: 'en',
|
|
87
|
+
* })
|
|
88
|
+
*
|
|
89
|
+
* // With Intl defaults
|
|
90
|
+
* provideCoarLocalization({...}),
|
|
91
|
+
* provideCoarIntlLocalizationSource(),
|
|
92
|
+
*
|
|
93
|
+
* // With Intl + HTTP overrides
|
|
94
|
+
* provideCoarLocalization({...}),
|
|
95
|
+
* provideCoarIntlLocalizationSource(),
|
|
96
|
+
* provideCoarHttpLocalizationSource({
|
|
97
|
+
* url: (lang) => `/locales/${lang}.json`
|
|
98
|
+
* }),
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
function provideCoarLocalization(config) {
|
|
102
|
+
return makeEnvironmentProviders([
|
|
103
|
+
{
|
|
104
|
+
provide: COAR_LOCALIZATION_CONFIG,
|
|
105
|
+
useValue: config,
|
|
106
|
+
},
|
|
107
|
+
CoarLocalizationService,
|
|
108
|
+
CoarLocalizationDataStore,
|
|
109
|
+
{
|
|
110
|
+
provide: APP_INITIALIZER,
|
|
111
|
+
multi: true,
|
|
112
|
+
useFactory: (service) => {
|
|
113
|
+
return async () => {
|
|
114
|
+
// Preload default language from all sources
|
|
115
|
+
try {
|
|
116
|
+
await service.setLanguage(config.defaultLanguage);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.warn(`[CoarLocale] Failed to preload locale data for '${config.defaultLanguage}':`, error);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
deps: [CoarLocalizationService],
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Deep merge utility for locale data.
|
|
130
|
+
* Later sources override earlier sources at the field level.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const intl = { date: { pattern: 'dd.mm.yyyy', firstDayOfWeek: 1 }, number: {...} };
|
|
135
|
+
* const http = { date: { firstDayOfWeek: 0 } }; // Only override firstDayOfWeek
|
|
136
|
+
*
|
|
137
|
+
* const result = mergeLocalizationData([intl, http]);
|
|
138
|
+
* // Result: { date: { pattern: 'dd.mm.yyyy', firstDayOfWeek: 0 }, number: {...} }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
function mergeLocalizationData(sources) {
|
|
142
|
+
if (sources.length === 0)
|
|
143
|
+
return null;
|
|
144
|
+
const result = {};
|
|
145
|
+
for (const source of sources) {
|
|
146
|
+
if (!source)
|
|
147
|
+
continue;
|
|
148
|
+
// Merge code
|
|
149
|
+
if (source.code) {
|
|
150
|
+
result.code = source.code;
|
|
151
|
+
}
|
|
152
|
+
// Deep merge date
|
|
153
|
+
if (source.date) {
|
|
154
|
+
result.date = { ...result.date, ...source.date };
|
|
155
|
+
}
|
|
156
|
+
// Deep merge number
|
|
157
|
+
if (source.number) {
|
|
158
|
+
result.number = { ...result.number, ...source.number };
|
|
159
|
+
}
|
|
160
|
+
// Deep merge currency
|
|
161
|
+
if (source.currency) {
|
|
162
|
+
result.currency = {
|
|
163
|
+
...result.currency,
|
|
164
|
+
...source.currency,
|
|
165
|
+
symbols: { ...result.currency?.symbols, ...source.currency.symbols },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Deep merge percent
|
|
169
|
+
if (source.percent) {
|
|
170
|
+
result.percent = { ...result.percent, ...source.percent };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Ensure we have at least a code
|
|
174
|
+
if (!result.code)
|
|
175
|
+
return null;
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Core locale service responsible for language management.
|
|
181
|
+
*
|
|
182
|
+
* This service manages the current language state and notifies consumers
|
|
183
|
+
* about language changes. It serves as the single source of truth for
|
|
184
|
+
* the application's current language.
|
|
185
|
+
*
|
|
186
|
+
* Other systems (i18n, localization/formatting) can subscribe to language
|
|
187
|
+
* changes and react accordingly.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* import { inject } from '@angular/core';
|
|
192
|
+
* import { CoarLocalizationService } from '@cocoar/localization';
|
|
193
|
+
*
|
|
194
|
+
* export class MyComponent {
|
|
195
|
+
* private readonly locale = inject(CoarLocalizationService);
|
|
196
|
+
*
|
|
197
|
+
* constructor() {
|
|
198
|
+
* // Get current language
|
|
199
|
+
* console.log(this.locale.getCurrentLanguage());
|
|
200
|
+
*
|
|
201
|
+
* // React to changes via Signal
|
|
202
|
+
* effect(() => {
|
|
203
|
+
* console.log('Language changed:', this.locale.language());
|
|
204
|
+
* });
|
|
205
|
+
*
|
|
206
|
+
* // React to changes via Observable
|
|
207
|
+
* this.locale.languageChanged$.subscribe(lang => {
|
|
208
|
+
* console.log('Language changed:', lang);
|
|
209
|
+
* });
|
|
210
|
+
* }
|
|
211
|
+
*
|
|
212
|
+
* changeLanguage() {
|
|
213
|
+
* this.locale.setLanguage('de');
|
|
214
|
+
* }
|
|
215
|
+
* }
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
class CoarLocalizationService {
|
|
219
|
+
config = inject(COAR_LOCALIZATION_CONFIG, { optional: true });
|
|
220
|
+
localeDataStore = inject(CoarLocalizationDataStore);
|
|
221
|
+
localeDataLoaders = inject(COAR_LOCALIZATION_DATA_LOADERS, { optional: true }) ?? [];
|
|
222
|
+
languageSignal = signal(this.config?.defaultLanguage ?? 'en', ...(ngDevMode ? [{ debugName: "languageSignal" }] : []));
|
|
223
|
+
languageChangedSubject = new Subject();
|
|
224
|
+
/**
|
|
225
|
+
* Signal containing the current language.
|
|
226
|
+
* Updates automatically when the language changes.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* const locale = inject(CoarLocalizationService);
|
|
231
|
+
*
|
|
232
|
+
* // Use in computed
|
|
233
|
+
* const greeting = computed(() =>
|
|
234
|
+
* locale.language() === 'de' ? 'Hallo' : 'Hello'
|
|
235
|
+
* );
|
|
236
|
+
*
|
|
237
|
+
* // Use in effect
|
|
238
|
+
* effect(() => {
|
|
239
|
+
* console.log('Current language:', locale.language());
|
|
240
|
+
* });
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
language = this.languageSignal.asReadonly();
|
|
244
|
+
/**
|
|
245
|
+
* Observable that emits when the language changes.
|
|
246
|
+
* Emits the new language code immediately after the change.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* locale.languageChanged$.subscribe(newLang => {
|
|
251
|
+
* console.log('Language changed to:', newLang);
|
|
252
|
+
* // Reload translations, update formatting rules, etc.
|
|
253
|
+
* });
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
languageChanged$ = this.languageChangedSubject.asObservable();
|
|
257
|
+
/**
|
|
258
|
+
* Get the current language code.
|
|
259
|
+
*
|
|
260
|
+
* @returns The current language code (e.g., 'en', 'de', 'en-US')
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* const currentLang = locale.getCurrentLanguage();
|
|
265
|
+
* console.log(currentLang); // 'en'
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
getCurrentLanguage() {
|
|
269
|
+
return this.languageSignal();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Set the current language.
|
|
273
|
+
*
|
|
274
|
+
* Automatically loads locale data from all configured sources (Intl, HTTP, etc.)
|
|
275
|
+
* and deep-merges them in order. Later sources override earlier ones.
|
|
276
|
+
*
|
|
277
|
+
* Cached data is not reloaded, so switching back to a previous language is instant.
|
|
278
|
+
*
|
|
279
|
+
* @param language - The new language code (e.g., 'en', 'de', 'en-US')
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* // Change to German (loads from Intl + HTTP, merges overrides)
|
|
284
|
+
* await locale.setLanguage('de');
|
|
285
|
+
*
|
|
286
|
+
* // Change back to English (uses cached data, instant)
|
|
287
|
+
* await locale.setLanguage('en');
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
async setLanguage(language) {
|
|
291
|
+
const current = this.languageSignal();
|
|
292
|
+
// Load locale data if not already loaded (cached)
|
|
293
|
+
if (!this.localeDataStore.hasLocaleData(language)) {
|
|
294
|
+
try {
|
|
295
|
+
await this.loadAndmergeLocalizationData(language);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.warn(`[CoarLocalizationService] Failed to load locale data for '${language}':`, error);
|
|
299
|
+
// Continue with language switch even if locale data fails to load
|
|
300
|
+
// Formatting pipes will use fallback values
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Only notify if language actually changed
|
|
304
|
+
if (current !== language) {
|
|
305
|
+
this.languageSignal.set(language);
|
|
306
|
+
this.languageChangedSubject.next(language);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Load locale data from all sources and deep-merge them.
|
|
311
|
+
*
|
|
312
|
+
* Sources are executed in order:
|
|
313
|
+
* 1. Intl (always first, provides complete defaults)
|
|
314
|
+
* 2. HTTP (if configured, merges business overrides)
|
|
315
|
+
* 3. Custom sources (if provided, can add dynamic updates)
|
|
316
|
+
*
|
|
317
|
+
* @param language - Language code to load
|
|
318
|
+
*/
|
|
319
|
+
async loadAndmergeLocalizationData(language) {
|
|
320
|
+
if (this.localeDataLoaders.length === 0) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Load from all sources in parallel
|
|
324
|
+
const loadPromises = this.localeDataLoaders.map((loader) => lastValueFrom(loader.loadLocaleData(language)).catch((err) => {
|
|
325
|
+
console.warn(`[CoarLocale] Source failed to load '${language}':`, err);
|
|
326
|
+
return null; // Return null for failed sources
|
|
327
|
+
}));
|
|
328
|
+
const results = await Promise.all(loadPromises);
|
|
329
|
+
// Deep merge all sources (nulls are ignored)
|
|
330
|
+
const merged = mergeLocalizationData(results.filter((r) => r !== null));
|
|
331
|
+
if (merged) {
|
|
332
|
+
this.localeDataStore.setLocaleData(language, merged);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Preload locale data for a language without switching to it.
|
|
337
|
+
* Useful for avoiding UI flicker when switching languages.
|
|
338
|
+
*
|
|
339
|
+
* @param language - The language code to preload
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```typescript
|
|
343
|
+
* // Preload German locale data before switching
|
|
344
|
+
* await locale.preloadLocaleData('de');
|
|
345
|
+
* await locale.setLanguage('de'); // Instant switch, no loading delay
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
async preloadLocaleData(language) {
|
|
349
|
+
// Skip if already loaded
|
|
350
|
+
if (this.localeDataStore.hasLocaleData(language)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
await this.loadAndmergeLocalizationData(language);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.warn(`[CoarLocalizationService] Failed to preload locale data for '${language}':`, error);
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Get the list of available languages configured for the application.
|
|
363
|
+
*
|
|
364
|
+
* @returns Array of available language codes, or empty array if not configured
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const langs = locale.getAvailableLanguages(); // ['en', 'de', 'fr']
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
getAvailableLanguages() {
|
|
372
|
+
return this.config?.availableLanguages ?? [];
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get the default language configured for the application.
|
|
376
|
+
*
|
|
377
|
+
* @returns The default language code
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```typescript
|
|
381
|
+
* const defaultLang = locale.getDefaultLanguage(); // 'en'
|
|
382
|
+
* ```
|
|
383
|
+
*/
|
|
384
|
+
getDefaultLanguage() {
|
|
385
|
+
return this.config?.defaultLanguage ?? 'en';
|
|
386
|
+
}
|
|
387
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
388
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationService, providedIn: 'root' });
|
|
389
|
+
}
|
|
390
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationService, decorators: [{
|
|
391
|
+
type: Injectable,
|
|
392
|
+
args: [{
|
|
393
|
+
providedIn: 'root',
|
|
394
|
+
}]
|
|
395
|
+
}] });
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Locale data structure for date and number formatting.
|
|
399
|
+
*
|
|
400
|
+
* This data can be loaded from various sources:
|
|
401
|
+
* - Browser Intl API (CoarIntlLocaleDataLoader) - default
|
|
402
|
+
* - JSON files (CoarHttpLocaleDataLoader) - for overrides
|
|
403
|
+
* - SignalR (future) - for dynamic backend-controlled formatting
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Abstract loader for locale data.
|
|
408
|
+
* Implement this interface to load locale data from any source (HTTP, memory, database, etc.).
|
|
409
|
+
*/
|
|
410
|
+
class CoarLocalizationDataLoader {
|
|
411
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationDataLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
412
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationDataLoader, providedIn: 'root' });
|
|
413
|
+
}
|
|
414
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarLocalizationDataLoader, decorators: [{
|
|
415
|
+
type: Injectable,
|
|
416
|
+
args: [{ providedIn: 'root' }]
|
|
417
|
+
}] });
|
|
418
|
+
/**
|
|
419
|
+
* HTTP-based locale data loader.
|
|
420
|
+
* Loads locale data from JSON files via HTTP.
|
|
421
|
+
*
|
|
422
|
+
* Supports custom URL patterns and headers for authentication/configuration.
|
|
423
|
+
* Typically used as a second source (after Intl) to provide business-specific overrides.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* // Simple base path
|
|
428
|
+
* new CoarHttpLocaleDataLoader(httpClient, (lang) => `/locales/${lang}.json`)
|
|
429
|
+
*
|
|
430
|
+
* // Custom URL pattern
|
|
431
|
+
* new CoarHttpLocaleDataLoader(httpClient, (lang) => `/api/config/intl-${lang}.json`)
|
|
432
|
+
*
|
|
433
|
+
* // With headers
|
|
434
|
+
* new CoarHttpLocaleDataLoader(
|
|
435
|
+
* httpClient,
|
|
436
|
+
* (lang) => `/api/locales/${lang}.json`,
|
|
437
|
+
* { 'Authorization': 'Bearer token' }
|
|
438
|
+
* )
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
class CoarHttpLocaleDataLoader extends CoarLocalizationDataLoader {
|
|
442
|
+
httpClient;
|
|
443
|
+
urlFn;
|
|
444
|
+
headers;
|
|
445
|
+
constructor(httpClient, urlFn, headers) {
|
|
446
|
+
super();
|
|
447
|
+
this.httpClient = httpClient;
|
|
448
|
+
this.urlFn = urlFn;
|
|
449
|
+
this.headers = headers;
|
|
450
|
+
}
|
|
451
|
+
loadLocaleData(locale) {
|
|
452
|
+
const url = this.urlFn(locale);
|
|
453
|
+
return this.httpClient.get(url, {
|
|
454
|
+
headers: this.headers,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Locale data loader that detects formatting from browser's Intl API.
|
|
461
|
+
*
|
|
462
|
+
* This is the default loader and works for all locales without requiring JSON files.
|
|
463
|
+
* Use this when you want formatting to match the user's browser locale automatically.
|
|
464
|
+
*
|
|
465
|
+
* For business-specific overrides (e.g., force Monday as first day of week),
|
|
466
|
+
* use CoarHttpLocaleDataLoader to load custom JSON files.
|
|
467
|
+
*/
|
|
468
|
+
class CoarIntlLocaleDataLoader extends CoarLocalizationDataLoader {
|
|
469
|
+
loadLocaleData(locale) {
|
|
470
|
+
return of(this.detectFromIntl(locale));
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Detect all locale formatting from browser Intl API.
|
|
474
|
+
*/
|
|
475
|
+
detectFromIntl(locale) {
|
|
476
|
+
return {
|
|
477
|
+
code: locale,
|
|
478
|
+
date: this.detectDateFormat(locale),
|
|
479
|
+
number: this.detectNumberFormat(locale),
|
|
480
|
+
currency: this.detectCurrencyFormat(locale),
|
|
481
|
+
percent: this.detectPercentFormat(locale),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Detect date formatting from Intl.DateTimeFormat.
|
|
486
|
+
*/
|
|
487
|
+
detectDateFormat(locale) {
|
|
488
|
+
// Detect pattern and separator
|
|
489
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
490
|
+
day: '2-digit',
|
|
491
|
+
month: '2-digit',
|
|
492
|
+
year: 'numeric',
|
|
493
|
+
});
|
|
494
|
+
const parts = formatter.formatToParts(new Date(2024, 11, 25)); // Dec 25, 2024
|
|
495
|
+
const order = [];
|
|
496
|
+
for (const part of parts) {
|
|
497
|
+
if (part.type === 'day')
|
|
498
|
+
order.push('d');
|
|
499
|
+
else if (part.type === 'month')
|
|
500
|
+
order.push('m');
|
|
501
|
+
else if (part.type === 'year')
|
|
502
|
+
order.push('y');
|
|
503
|
+
}
|
|
504
|
+
const orderStr = order.join('');
|
|
505
|
+
const formatted = formatter.format(new Date(2024, 11, 25));
|
|
506
|
+
const separator = formatted.match(/[.\-/]/)?.[0] ?? '.';
|
|
507
|
+
let pattern;
|
|
508
|
+
if (orderStr === 'mdy') {
|
|
509
|
+
pattern = 'mm/dd/yyyy';
|
|
510
|
+
}
|
|
511
|
+
else if (orderStr === 'ymd') {
|
|
512
|
+
pattern = 'yyyy-mm-dd';
|
|
513
|
+
}
|
|
514
|
+
else if (separator === '/') {
|
|
515
|
+
pattern = 'dd/mm/yyyy';
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
pattern = 'dd.mm.yyyy';
|
|
519
|
+
}
|
|
520
|
+
// Detect first day of week
|
|
521
|
+
const sundayFirstLocales = ['en-US', 'en-CA', 'ja-JP', 'ko-KR', 'zh-TW', 'he-IL'];
|
|
522
|
+
const baseLocale = locale.split('-')[0];
|
|
523
|
+
const firstDayOfWeek = sundayFirstLocales.some((l) => locale.startsWith(l) || (baseLocale === 'en' && locale.includes('US')))
|
|
524
|
+
? 0 // Sunday
|
|
525
|
+
: 1; // Monday
|
|
526
|
+
// Generate month names
|
|
527
|
+
const monthFormatter = new Intl.DateTimeFormat(locale, { month: 'long' });
|
|
528
|
+
const monthFormatterShort = new Intl.DateTimeFormat(locale, { month: 'short' });
|
|
529
|
+
const monthNames = [];
|
|
530
|
+
const monthNamesShort = [];
|
|
531
|
+
for (let m = 0; m < 12; m++) {
|
|
532
|
+
const date = new Date(2024, m, 1);
|
|
533
|
+
monthNames.push(monthFormatter.format(date));
|
|
534
|
+
monthNamesShort.push(monthFormatterShort.format(date));
|
|
535
|
+
}
|
|
536
|
+
// Generate day names (start from Monday)
|
|
537
|
+
const dayFormatter = new Intl.DateTimeFormat(locale, { weekday: 'long' });
|
|
538
|
+
const dayFormatterShort = new Intl.DateTimeFormat(locale, { weekday: 'short' });
|
|
539
|
+
const dayNames = [];
|
|
540
|
+
const dayNamesShort = [];
|
|
541
|
+
for (let d = 0; d < 7; d++) {
|
|
542
|
+
// Jan 1, 2024 is Monday
|
|
543
|
+
const date = new Date(2024, 0, 1 + d);
|
|
544
|
+
dayNames.push(dayFormatter.format(date));
|
|
545
|
+
dayNamesShort.push(dayFormatterShort.format(date));
|
|
546
|
+
}
|
|
547
|
+
// Detect AM/PM
|
|
548
|
+
const amPmFormatter = new Intl.DateTimeFormat(locale, { hour: 'numeric', hour12: true });
|
|
549
|
+
const amDate = new Date(2024, 0, 1, 9, 0);
|
|
550
|
+
const pmDate = new Date(2024, 0, 1, 21, 0);
|
|
551
|
+
const amFormatted = amPmFormatter.format(amDate);
|
|
552
|
+
const pmFormatted = amPmFormatter.format(pmDate);
|
|
553
|
+
const amPm = [
|
|
554
|
+
amFormatted.replace(/\d+/g, '').trim() || 'AM',
|
|
555
|
+
pmFormatted.replace(/\d+/g, '').trim() || 'PM',
|
|
556
|
+
];
|
|
557
|
+
return {
|
|
558
|
+
pattern,
|
|
559
|
+
firstDayOfWeek,
|
|
560
|
+
monthNames,
|
|
561
|
+
monthNamesShort,
|
|
562
|
+
dayNames,
|
|
563
|
+
dayNamesShort,
|
|
564
|
+
amPm,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Detect number formatting from Intl.NumberFormat.
|
|
569
|
+
*/
|
|
570
|
+
detectNumberFormat(locale) {
|
|
571
|
+
const formatter = new Intl.NumberFormat(locale);
|
|
572
|
+
const formatted = formatter.format(1234.56);
|
|
573
|
+
// Extract decimal separator
|
|
574
|
+
const decimal = formatted.match(/[.,]/)?.[0] ?? '.';
|
|
575
|
+
// Extract group separator (find character between thousands)
|
|
576
|
+
const formattedThousands = formatter.format(1234567);
|
|
577
|
+
const groupMatch = formattedThousands.match(/1(.)234/);
|
|
578
|
+
const group = groupMatch ? groupMatch[1] : ',';
|
|
579
|
+
return {
|
|
580
|
+
decimal,
|
|
581
|
+
group,
|
|
582
|
+
grouping: [3], // Standard grouping by 3 digits
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Detect currency formatting from Intl.NumberFormat.
|
|
587
|
+
*/
|
|
588
|
+
detectCurrencyFormat(locale) {
|
|
589
|
+
// Try to get default currency for the locale
|
|
590
|
+
let defaultCurrency = 'USD';
|
|
591
|
+
try {
|
|
592
|
+
// Extract region from locale (e.g., 'US' from 'en-US')
|
|
593
|
+
const region = locale.split('-')[1];
|
|
594
|
+
if (region) {
|
|
595
|
+
// Common currency mappings
|
|
596
|
+
const currencyMap = {
|
|
597
|
+
US: 'USD',
|
|
598
|
+
GB: 'GBP',
|
|
599
|
+
DE: 'EUR',
|
|
600
|
+
FR: 'EUR',
|
|
601
|
+
AT: 'EUR',
|
|
602
|
+
JP: 'JPY',
|
|
603
|
+
CN: 'CNY',
|
|
604
|
+
IN: 'INR',
|
|
605
|
+
};
|
|
606
|
+
defaultCurrency = currencyMap[region] || 'USD';
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
// Fallback
|
|
611
|
+
}
|
|
612
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
613
|
+
style: 'currency',
|
|
614
|
+
currency: defaultCurrency,
|
|
615
|
+
});
|
|
616
|
+
const formatted = formatter.format(1234.56);
|
|
617
|
+
// Detect symbol position
|
|
618
|
+
const numberPart = formatted.match(/\d/)?.[0];
|
|
619
|
+
const symbolIndex = formatted.indexOf(defaultCurrency.substring(0, 1));
|
|
620
|
+
const numberIndex = formatted.indexOf(numberPart || '1');
|
|
621
|
+
const position = symbolIndex < numberIndex ? 'before' : 'after';
|
|
622
|
+
// Detect spacing
|
|
623
|
+
const spacing = /\s/.test(formatted);
|
|
624
|
+
// Get currency symbols
|
|
625
|
+
const symbols = {
|
|
626
|
+
USD: '$',
|
|
627
|
+
EUR: '€',
|
|
628
|
+
GBP: '£',
|
|
629
|
+
JPY: '¥',
|
|
630
|
+
CNY: '¥',
|
|
631
|
+
INR: '₹',
|
|
632
|
+
};
|
|
633
|
+
return {
|
|
634
|
+
default: defaultCurrency,
|
|
635
|
+
symbols,
|
|
636
|
+
position,
|
|
637
|
+
spacing,
|
|
638
|
+
decimals: 2,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Detect percent formatting from Intl.NumberFormat.
|
|
643
|
+
*/
|
|
644
|
+
detectPercentFormat(locale) {
|
|
645
|
+
const formatter = new Intl.NumberFormat(locale, { style: 'percent' });
|
|
646
|
+
const formatted = formatter.format(0.25);
|
|
647
|
+
// Detect spacing
|
|
648
|
+
const spacing = /\d\s%/.test(formatted);
|
|
649
|
+
return {
|
|
650
|
+
symbol: '%',
|
|
651
|
+
spacing,
|
|
652
|
+
decimals: 0,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Provides browser Intl API as a locale data source.
|
|
659
|
+
*
|
|
660
|
+
* This loader detects date/number formatting from the browser's Intl API
|
|
661
|
+
* and provides complete locale data for any language without requiring JSON files.
|
|
662
|
+
*
|
|
663
|
+
* **Recommended as the first source** to provide complete defaults that other
|
|
664
|
+
* sources can override.
|
|
665
|
+
*
|
|
666
|
+
* @example
|
|
667
|
+
* ```ts
|
|
668
|
+
* // Intl only (pure browser defaults)
|
|
669
|
+
* provideCoarLocalization({ defaultLanguage: 'en', availableLanguages: ['en', 'de'] }),
|
|
670
|
+
* provideCoarIntlLocalizationSource(),
|
|
671
|
+
*
|
|
672
|
+
* // Intl + HTTP overrides (recommended)
|
|
673
|
+
* provideCoarLocalization({...}),
|
|
674
|
+
* provideCoarIntlLocalizationSource(), // First source (complete defaults)
|
|
675
|
+
* provideCoarHttpLocalizationSource({ // Second source (business overrides)
|
|
676
|
+
* url: (lang) => `/locales/${lang}.json`
|
|
677
|
+
* }),
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
function provideCoarIntlLocalizationSource() {
|
|
681
|
+
return makeEnvironmentProviders([
|
|
682
|
+
{
|
|
683
|
+
provide: COAR_LOCALIZATION_DATA_LOADERS,
|
|
684
|
+
multi: true,
|
|
685
|
+
useClass: CoarIntlLocaleDataLoader,
|
|
686
|
+
},
|
|
687
|
+
]);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Provides HTTP as a locale data source.
|
|
692
|
+
*
|
|
693
|
+
* This loader fetches locale data from JSON files via HTTP.
|
|
694
|
+
* Typically used as a second source (after Intl) to provide business-specific
|
|
695
|
+
* overrides like forced firstDayOfWeek, custom separators, etc.
|
|
696
|
+
*
|
|
697
|
+
* @example
|
|
698
|
+
* ```ts
|
|
699
|
+
* // Basic usage (default URL pattern)
|
|
700
|
+
* provideCoarHttpLocalizationSource({
|
|
701
|
+
* url: (lang) => `/locales/${lang}.json`
|
|
702
|
+
* })
|
|
703
|
+
*
|
|
704
|
+
* // Custom URL pattern
|
|
705
|
+
* provideCoarHttpLocalizationSource({
|
|
706
|
+
* url: (lang) => `/api/config/intl-${lang}.json`
|
|
707
|
+
* })
|
|
708
|
+
*
|
|
709
|
+
* // With authentication
|
|
710
|
+
* provideCoarHttpLocalizationSource({
|
|
711
|
+
* url: (lang) => `/api/locales/${lang}.json`,
|
|
712
|
+
* headers: {
|
|
713
|
+
* 'Authorization': 'Bearer ' + getToken()
|
|
714
|
+
* }
|
|
715
|
+
* })
|
|
716
|
+
* ```
|
|
717
|
+
*
|
|
718
|
+
* Expected file structure:
|
|
719
|
+
* ```
|
|
720
|
+
* public/locales/
|
|
721
|
+
* en.json - English overrides
|
|
722
|
+
* de.json - German overrides
|
|
723
|
+
* ```
|
|
724
|
+
*
|
|
725
|
+
* JSON files should contain partial CoarLocalizationData:
|
|
726
|
+
* ```json
|
|
727
|
+
* {
|
|
728
|
+
* "code": "de",
|
|
729
|
+
* "date": {
|
|
730
|
+
* "firstDayOfWeek": 1
|
|
731
|
+
* }
|
|
732
|
+
* }
|
|
733
|
+
* ```
|
|
734
|
+
*/
|
|
735
|
+
function provideCoarHttpLocalizationSource(config) {
|
|
736
|
+
return makeEnvironmentProviders([
|
|
737
|
+
{
|
|
738
|
+
provide: COAR_LOCALIZATION_DATA_LOADERS,
|
|
739
|
+
multi: true,
|
|
740
|
+
useFactory: (http) => {
|
|
741
|
+
return new CoarHttpLocaleDataLoader(http, config.url, config.headers);
|
|
742
|
+
},
|
|
743
|
+
deps: [HttpClient],
|
|
744
|
+
},
|
|
745
|
+
]);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Pipe for formatting dates using locale-specific configuration.
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* ```html
|
|
753
|
+
* <span>{{ myDate | coarDate }}</span>
|
|
754
|
+
* <span>{{ myDate | coarDate:'de' }}</span>
|
|
755
|
+
* ```
|
|
756
|
+
*/
|
|
757
|
+
class CoarDatePipe {
|
|
758
|
+
localeDataStore = inject(CoarLocalizationDataStore);
|
|
759
|
+
localeService = inject(CoarLocalizationService);
|
|
760
|
+
transform(value, locale) {
|
|
761
|
+
if (!value)
|
|
762
|
+
return '';
|
|
763
|
+
// Convert to Temporal.PlainDate
|
|
764
|
+
let date;
|
|
765
|
+
if (value instanceof Date) {
|
|
766
|
+
date = Temporal.PlainDate.from({
|
|
767
|
+
year: value.getFullYear(),
|
|
768
|
+
month: value.getMonth() + 1,
|
|
769
|
+
day: value.getDate(),
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
else if (typeof value === 'string') {
|
|
773
|
+
try {
|
|
774
|
+
date = Temporal.PlainDate.from(value);
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
return '';
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
date = value;
|
|
782
|
+
}
|
|
783
|
+
const effectiveLocale = locale ?? this.localeService.getCurrentLanguage();
|
|
784
|
+
const localeData = this.localeDataStore.getLocaleData(effectiveLocale);
|
|
785
|
+
if (!localeData) {
|
|
786
|
+
// Fallback to ISO format
|
|
787
|
+
return date.toString();
|
|
788
|
+
}
|
|
789
|
+
const { pattern } = localeData.date;
|
|
790
|
+
const sep = pattern.includes('.') ? '.' : pattern.includes('/') ? '/' : '-';
|
|
791
|
+
const day = String(date.day).padStart(2, '0');
|
|
792
|
+
const month = String(date.month).padStart(2, '0');
|
|
793
|
+
const year = String(date.year);
|
|
794
|
+
switch (pattern) {
|
|
795
|
+
case 'dd.mm.yyyy':
|
|
796
|
+
case 'dd/mm/yyyy':
|
|
797
|
+
return `${day}${sep}${month}${sep}${year}`;
|
|
798
|
+
case 'mm/dd/yyyy':
|
|
799
|
+
return `${month}${sep}${day}${sep}${year}`;
|
|
800
|
+
case 'yyyy-mm-dd':
|
|
801
|
+
return `${year}${sep}${month}${sep}${day}`;
|
|
802
|
+
default:
|
|
803
|
+
return `${day}${sep}${month}${sep}${year}`;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
807
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: CoarDatePipe, isStandalone: true, name: "coarDate", pure: false });
|
|
808
|
+
}
|
|
809
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDatePipe, decorators: [{
|
|
810
|
+
type: Pipe,
|
|
811
|
+
args: [{
|
|
812
|
+
name: 'coarDate',
|
|
813
|
+
standalone: true,
|
|
814
|
+
pure: false, // Impure to react to language changes
|
|
815
|
+
}]
|
|
816
|
+
}] });
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Pipe for formatting numbers using locale-specific configuration.
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```html
|
|
823
|
+
* <span>{{ 1234.56 | coarNumber }}</span>
|
|
824
|
+
* <span>{{ 1234.56 | coarNumber:'de':2 }}</span>
|
|
825
|
+
* ```
|
|
826
|
+
*/
|
|
827
|
+
class CoarNumberPipe {
|
|
828
|
+
localeDataStore = inject(CoarLocalizationDataStore);
|
|
829
|
+
localeService = inject(CoarLocalizationService);
|
|
830
|
+
transform(value, locale, decimals = 2) {
|
|
831
|
+
if (value == null || isNaN(value))
|
|
832
|
+
return '';
|
|
833
|
+
const effectiveLocale = locale ?? this.localeService.getCurrentLanguage();
|
|
834
|
+
const localeData = this.localeDataStore.getLocaleData(effectiveLocale);
|
|
835
|
+
if (!localeData) {
|
|
836
|
+
// Fallback to standard formatting
|
|
837
|
+
return value.toFixed(decimals);
|
|
838
|
+
}
|
|
839
|
+
const { decimal, group } = localeData.number;
|
|
840
|
+
// Split into integer and decimal parts
|
|
841
|
+
const [integerPart, decimalPart = ''] = value.toFixed(decimals).split('.');
|
|
842
|
+
// Add thousand separators
|
|
843
|
+
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, group);
|
|
844
|
+
// Combine with decimal separator
|
|
845
|
+
if (decimals > 0 && decimalPart) {
|
|
846
|
+
return `${formattedInteger}${decimal}${decimalPart}`;
|
|
847
|
+
}
|
|
848
|
+
return formattedInteger;
|
|
849
|
+
}
|
|
850
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarNumberPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
851
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: CoarNumberPipe, isStandalone: true, name: "coarNumber", pure: false });
|
|
852
|
+
}
|
|
853
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarNumberPipe, decorators: [{
|
|
854
|
+
type: Pipe,
|
|
855
|
+
args: [{
|
|
856
|
+
name: 'coarNumber',
|
|
857
|
+
standalone: true,
|
|
858
|
+
pure: false, // Impure to react to language changes
|
|
859
|
+
}]
|
|
860
|
+
}] });
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Pipe for formatting currency using locale-specific configuration.
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* ```html
|
|
867
|
+
* <span>{{ 1234.56 | coarCurrency }}</span>
|
|
868
|
+
* <span>{{ 1234.56 | coarCurrency:'de':'EUR' }}</span>
|
|
869
|
+
* ```
|
|
870
|
+
*/
|
|
871
|
+
class CoarCurrencyPipe {
|
|
872
|
+
localeDataStore = inject(CoarLocalizationDataStore);
|
|
873
|
+
localeService = inject(CoarLocalizationService);
|
|
874
|
+
transform(value, locale, currency) {
|
|
875
|
+
if (value == null || isNaN(value))
|
|
876
|
+
return '';
|
|
877
|
+
const effectiveLocale = locale ?? this.localeService.getCurrentLanguage();
|
|
878
|
+
const localeData = this.localeDataStore.getLocaleData(effectiveLocale);
|
|
879
|
+
if (!localeData) {
|
|
880
|
+
// Fallback to standard formatting
|
|
881
|
+
return `${currency ?? 'USD'} ${value.toFixed(2)}`;
|
|
882
|
+
}
|
|
883
|
+
const { decimal, group } = localeData.number;
|
|
884
|
+
const currencyConfig = localeData.currency;
|
|
885
|
+
const effectiveCurrency = currency ?? currencyConfig.default;
|
|
886
|
+
const symbol = currencyConfig.symbols[effectiveCurrency] ?? effectiveCurrency;
|
|
887
|
+
const decimals = currencyConfig.decimals;
|
|
888
|
+
// Format number part
|
|
889
|
+
const [integerPart, decimalPart = ''] = value.toFixed(decimals).split('.');
|
|
890
|
+
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, group);
|
|
891
|
+
const formattedNumber = decimals > 0 && decimalPart
|
|
892
|
+
? `${formattedInteger}${decimal}${decimalPart}`
|
|
893
|
+
: formattedInteger;
|
|
894
|
+
// Add currency symbol
|
|
895
|
+
const space = currencyConfig.spacing ? ' ' : '';
|
|
896
|
+
if (currencyConfig.position === 'before') {
|
|
897
|
+
return `${symbol}${space}${formattedNumber}`;
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
return `${formattedNumber}${space}${symbol}`;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarCurrencyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
904
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: CoarCurrencyPipe, isStandalone: true, name: "coarCurrency", pure: false });
|
|
905
|
+
}
|
|
906
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarCurrencyPipe, decorators: [{
|
|
907
|
+
type: Pipe,
|
|
908
|
+
args: [{
|
|
909
|
+
name: 'coarCurrency',
|
|
910
|
+
standalone: true,
|
|
911
|
+
pure: false, // Impure to react to language changes
|
|
912
|
+
}]
|
|
913
|
+
}] });
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Pipe for formatting percentages using locale-specific configuration.
|
|
917
|
+
*
|
|
918
|
+
* Expects a decimal value (0.25 = 25%).
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```html
|
|
922
|
+
* <span>{{ 0.25 | coarPercent }}</span>
|
|
923
|
+
* <span>{{ 0.25 | coarPercent:'de':1 }}</span>
|
|
924
|
+
* ```
|
|
925
|
+
*/
|
|
926
|
+
class CoarPercentPipe {
|
|
927
|
+
localeDataStore = inject(CoarLocalizationDataStore);
|
|
928
|
+
localeService = inject(CoarLocalizationService);
|
|
929
|
+
transform(value, locale, decimals) {
|
|
930
|
+
if (value == null || isNaN(value))
|
|
931
|
+
return '';
|
|
932
|
+
const effectiveLocale = locale ?? this.localeService.getCurrentLanguage();
|
|
933
|
+
const localeData = this.localeDataStore.getLocaleData(effectiveLocale);
|
|
934
|
+
if (!localeData) {
|
|
935
|
+
// Fallback to standard formatting
|
|
936
|
+
return `${(value * 100).toFixed(decimals ?? 0)}%`;
|
|
937
|
+
}
|
|
938
|
+
const { decimal, group } = localeData.number;
|
|
939
|
+
const percentConfig = localeData.percent;
|
|
940
|
+
const effectiveDecimals = decimals ?? percentConfig.decimals;
|
|
941
|
+
// Convert to percentage (0.25 -> 25)
|
|
942
|
+
const percentValue = value * 100;
|
|
943
|
+
// Format number part
|
|
944
|
+
const [integerPart, decimalPart = ''] = percentValue.toFixed(effectiveDecimals).split('.');
|
|
945
|
+
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, group);
|
|
946
|
+
const formattedNumber = effectiveDecimals > 0 && decimalPart
|
|
947
|
+
? `${formattedInteger}${decimal}${decimalPart}`
|
|
948
|
+
: formattedInteger;
|
|
949
|
+
// Add percent symbol
|
|
950
|
+
const space = percentConfig.spacing ? ' ' : '';
|
|
951
|
+
return `${formattedNumber}${space}${percentConfig.symbol}`;
|
|
952
|
+
}
|
|
953
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarPercentPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
954
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: CoarPercentPipe, isStandalone: true, name: "coarPercent", pure: false });
|
|
955
|
+
}
|
|
956
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarPercentPipe, decorators: [{
|
|
957
|
+
type: Pipe,
|
|
958
|
+
args: [{
|
|
959
|
+
name: 'coarPercent',
|
|
960
|
+
standalone: true,
|
|
961
|
+
pure: false, // Impure to react to language changes
|
|
962
|
+
}]
|
|
963
|
+
}] });
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Injection token for the i18n provider implementation.
|
|
967
|
+
*
|
|
968
|
+
* Apps should provide this token with their chosen i18n backend (Transloco, custom, etc.).
|
|
969
|
+
*/
|
|
970
|
+
const COAR_I18N_PROVIDER = new InjectionToken('COAR_I18N_PROVIDER');
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Determines if a translation result should be considered "missing".
|
|
974
|
+
*
|
|
975
|
+
* A translation is considered missing if it is:
|
|
976
|
+
* - null or undefined
|
|
977
|
+
* - an empty string (after trim)
|
|
978
|
+
* - exactly equal to the requested key
|
|
979
|
+
*
|
|
980
|
+
* This provides unified missing-key semantics across different i18n engines.
|
|
981
|
+
*
|
|
982
|
+
* @param key - The translation key that was requested
|
|
983
|
+
* @param result - The result returned by the i18n service
|
|
984
|
+
* @returns true if the translation is missing, false otherwise
|
|
985
|
+
*
|
|
986
|
+
* @example
|
|
987
|
+
* ```ts
|
|
988
|
+
* coarIsMissingTranslation('coar.button.save', null) // true
|
|
989
|
+
* coarIsMissingTranslation('coar.button.save', '') // true
|
|
990
|
+
* coarIsMissingTranslation('coar.button.save', 'coar.button.save') // true
|
|
991
|
+
* coarIsMissingTranslation('coar.button.save', 'Save') // false
|
|
992
|
+
* ```
|
|
993
|
+
*/
|
|
994
|
+
function coarIsMissingTranslation(key, result) {
|
|
995
|
+
if (result == null) {
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
const trimmed = result.trim();
|
|
999
|
+
if (!trimmed) {
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
return trimmed === key;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Interpolates {placeholders} in a template string using the given params.
|
|
1007
|
+
*
|
|
1008
|
+
* This is the official Cocoar placeholder syntax for i18n strings.
|
|
1009
|
+
* Placeholders use curly braces: {name}, {count}, etc.
|
|
1010
|
+
*
|
|
1011
|
+
* @param template - The template string containing {placeholder} patterns
|
|
1012
|
+
* @param params - Optional key-value pairs for interpolation
|
|
1013
|
+
* @returns The interpolated string
|
|
1014
|
+
*
|
|
1015
|
+
* @example
|
|
1016
|
+
* ```ts
|
|
1017
|
+
* coarInterpolate("Hello {name}, you have {count} items.", { name: 'Alice', count: 3 })
|
|
1018
|
+
* // => "Hello Alice, you have 3 items."
|
|
1019
|
+
* ```
|
|
1020
|
+
*/
|
|
1021
|
+
function coarInterpolate(template, params) {
|
|
1022
|
+
if (!params) {
|
|
1023
|
+
return template;
|
|
1024
|
+
}
|
|
1025
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
1026
|
+
const value = params[key];
|
|
1027
|
+
return value == null ? '' : String(value);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
class CoarI18n {
|
|
1032
|
+
provider = inject(COAR_I18N_PROVIDER);
|
|
1033
|
+
locale = inject(CoarLocalizationService);
|
|
1034
|
+
t(key, fallbackOrParams, maybeParams) {
|
|
1035
|
+
let fallback;
|
|
1036
|
+
let params;
|
|
1037
|
+
if (typeof fallbackOrParams === 'string') {
|
|
1038
|
+
// t(key, 'Fallback') or t(key, 'Fallback', params)
|
|
1039
|
+
fallback = fallbackOrParams;
|
|
1040
|
+
params = maybeParams;
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
// t(key) or t(key, params)
|
|
1044
|
+
params = fallbackOrParams;
|
|
1045
|
+
fallback = undefined;
|
|
1046
|
+
}
|
|
1047
|
+
const raw = this.provider.t(key, params);
|
|
1048
|
+
const base = coarIsMissingTranslation(key, raw) ? (fallback ?? key) : (raw ?? '');
|
|
1049
|
+
// Important: the fallback (and even the key) may contain {placeholders}
|
|
1050
|
+
return coarInterpolate(base, params);
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Reactive variant that updates when the language changes.
|
|
1054
|
+
* Uses CoarLocalizationService to detect language changes.
|
|
1055
|
+
*/
|
|
1056
|
+
t$(key, params, fallback) {
|
|
1057
|
+
// Re-evaluate on every language change from CoarLocalizationService
|
|
1058
|
+
// startWith ensures initial emission
|
|
1059
|
+
return this.locale.languageChanged$.pipe(startWith(undefined), map(() => this.callT(key, params, fallback)), distinctUntilChanged());
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Signal variant that updates when the language changes.
|
|
1063
|
+
* Uses CoarLocalizationService to detect language changes.
|
|
1064
|
+
*/
|
|
1065
|
+
tSignal(key, params, fallback) {
|
|
1066
|
+
const obs$ = this.t$(key, params, fallback);
|
|
1067
|
+
return toSignal(obs$, {
|
|
1068
|
+
initialValue: this.callT(key, params, fallback),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
// Small helper so t$ doesn't need to replicate the overload matrix.
|
|
1072
|
+
callT(key, params, fallback) {
|
|
1073
|
+
if (params && fallback !== undefined) {
|
|
1074
|
+
// t(key, fallback, params)
|
|
1075
|
+
return this.t(key, fallback, params);
|
|
1076
|
+
}
|
|
1077
|
+
if (params) {
|
|
1078
|
+
// t(key, params)
|
|
1079
|
+
return this.t(key, params);
|
|
1080
|
+
}
|
|
1081
|
+
if (fallback !== undefined) {
|
|
1082
|
+
// t(key, fallback)
|
|
1083
|
+
return this.t(key, fallback);
|
|
1084
|
+
}
|
|
1085
|
+
// t(key)
|
|
1086
|
+
return this.t(key);
|
|
1087
|
+
}
|
|
1088
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1089
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n, providedIn: 'root' });
|
|
1090
|
+
}
|
|
1091
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18n, decorators: [{
|
|
1092
|
+
type: Injectable,
|
|
1093
|
+
args: [{ providedIn: 'root' }]
|
|
1094
|
+
}] });
|
|
1095
|
+
|
|
1096
|
+
class CoarI18nPipe {
|
|
1097
|
+
i18n = inject(CoarI18n);
|
|
1098
|
+
cdr = inject(ChangeDetectorRef);
|
|
1099
|
+
subject = new BehaviorSubjectProxy('');
|
|
1100
|
+
lastKey;
|
|
1101
|
+
lastParamsJson;
|
|
1102
|
+
lastFallback;
|
|
1103
|
+
transform(key, paramsOrFallback, maybeFallbackOrParams) {
|
|
1104
|
+
if (!key) {
|
|
1105
|
+
// If there's no key, return fallback if one exists
|
|
1106
|
+
if (typeof paramsOrFallback === 'string') {
|
|
1107
|
+
return paramsOrFallback;
|
|
1108
|
+
}
|
|
1109
|
+
if (typeof maybeFallbackOrParams === 'string') {
|
|
1110
|
+
return maybeFallbackOrParams;
|
|
1111
|
+
}
|
|
1112
|
+
return '';
|
|
1113
|
+
}
|
|
1114
|
+
// Normal param/fallback dispatching
|
|
1115
|
+
let params;
|
|
1116
|
+
let fallback;
|
|
1117
|
+
// Case A — first argument is fallback (string)
|
|
1118
|
+
if (typeof paramsOrFallback === 'string') {
|
|
1119
|
+
fallback = paramsOrFallback;
|
|
1120
|
+
if (maybeFallbackOrParams && typeof maybeFallbackOrParams === 'object') {
|
|
1121
|
+
params = maybeFallbackOrParams;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
// Case B — first argument is params (object)
|
|
1126
|
+
params = paramsOrFallback ?? undefined;
|
|
1127
|
+
if (typeof maybeFallbackOrParams === 'string') {
|
|
1128
|
+
fallback = maybeFallbackOrParams;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// Compare inputs to determine if a new subscription is needed
|
|
1132
|
+
const paramsJson = params ? JSON.stringify(params) : undefined;
|
|
1133
|
+
if (key !== this.lastKey ||
|
|
1134
|
+
fallback !== this.lastFallback ||
|
|
1135
|
+
paramsJson !== this.lastParamsJson) {
|
|
1136
|
+
this.lastKey = key;
|
|
1137
|
+
this.lastFallback = fallback;
|
|
1138
|
+
this.lastParamsJson = paramsJson;
|
|
1139
|
+
// Switch Observable completely using BehaviorSubjectProxy
|
|
1140
|
+
this.subject.next(this.i18n.t$(key, params, fallback));
|
|
1141
|
+
// Update the view when new translated values arrive
|
|
1142
|
+
this.subject.subscribe(() => {
|
|
1143
|
+
this.cdr.markForCheck();
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
return this.subject.value;
|
|
1147
|
+
}
|
|
1148
|
+
ngOnDestroy() {
|
|
1149
|
+
this.subject.unsubscribe();
|
|
1150
|
+
}
|
|
1151
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1152
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nPipe, isStandalone: true, name: "coarI18n", pure: false });
|
|
1153
|
+
}
|
|
1154
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nPipe, decorators: [{
|
|
1155
|
+
type: Pipe,
|
|
1156
|
+
args: [{
|
|
1157
|
+
name: 'coarI18n',
|
|
1158
|
+
standalone: true,
|
|
1159
|
+
pure: false, // Required because translations can change at runtime
|
|
1160
|
+
}]
|
|
1161
|
+
}] });
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Context service providing i18n-related information to translation providers.
|
|
1165
|
+
*
|
|
1166
|
+
* This service acts as a coordination layer between the core locale system
|
|
1167
|
+
* and translation provider implementations (e.g., Transloco, custom providers).
|
|
1168
|
+
*
|
|
1169
|
+
* Providers should inject this service to:
|
|
1170
|
+
* - Get the current language
|
|
1171
|
+
* - Subscribe to language changes
|
|
1172
|
+
* - Access future i18n-related features (caching, preloading, etc.)
|
|
1173
|
+
*
|
|
1174
|
+
* This abstraction decouples providers from direct CoarLocalizationService dependency,
|
|
1175
|
+
* making the system more maintainable and testable.
|
|
1176
|
+
*
|
|
1177
|
+
* @example
|
|
1178
|
+
* ```typescript
|
|
1179
|
+
* // In a translation provider implementation
|
|
1180
|
+
* export function provideMyI18nBackend(): Provider {
|
|
1181
|
+
* return {
|
|
1182
|
+
* provide: COAR_I18N_PROVIDER,
|
|
1183
|
+
* useFactory: (context: CoarI18nContext, backend: MyBackend) => {
|
|
1184
|
+
* // Subscribe to language changes
|
|
1185
|
+
* context.language$.subscribe(lang => {
|
|
1186
|
+
* backend.switchLanguage(lang);
|
|
1187
|
+
* });
|
|
1188
|
+
*
|
|
1189
|
+
* return {
|
|
1190
|
+
* t(key: string): string {
|
|
1191
|
+
* return backend.translate(key);
|
|
1192
|
+
* }
|
|
1193
|
+
* };
|
|
1194
|
+
* },
|
|
1195
|
+
* deps: [CoarI18nContext, MyBackend],
|
|
1196
|
+
* };
|
|
1197
|
+
* }
|
|
1198
|
+
* ```
|
|
1199
|
+
*/
|
|
1200
|
+
class CoarI18nContext {
|
|
1201
|
+
localeService = inject(CoarLocalizationService);
|
|
1202
|
+
/**
|
|
1203
|
+
* Observable that emits whenever the language changes.
|
|
1204
|
+
*
|
|
1205
|
+
* Translation providers should subscribe to this observable to stay
|
|
1206
|
+
* synchronized with the current language state.
|
|
1207
|
+
*
|
|
1208
|
+
* @example
|
|
1209
|
+
* ```typescript
|
|
1210
|
+
* context.language$.subscribe(newLang => {
|
|
1211
|
+
* console.log('Language changed to:', newLang);
|
|
1212
|
+
* translationBackend.loadLanguage(newLang);
|
|
1213
|
+
* });
|
|
1214
|
+
* ```
|
|
1215
|
+
*/
|
|
1216
|
+
language$ = this.localeService.languageChanged$;
|
|
1217
|
+
/**
|
|
1218
|
+
* Get the current language code.
|
|
1219
|
+
*
|
|
1220
|
+
* @returns The current language code (e.g., 'en', 'de', 'en-US')
|
|
1221
|
+
*
|
|
1222
|
+
* @example
|
|
1223
|
+
* ```typescript
|
|
1224
|
+
* const currentLang = context.getCurrentLanguage();
|
|
1225
|
+
* console.log(currentLang); // 'en'
|
|
1226
|
+
* ```
|
|
1227
|
+
*/
|
|
1228
|
+
getCurrentLanguage() {
|
|
1229
|
+
return this.localeService.getCurrentLanguage();
|
|
1230
|
+
}
|
|
1231
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1232
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nContext, providedIn: 'root' });
|
|
1233
|
+
}
|
|
1234
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nContext, decorators: [{
|
|
1235
|
+
type: Injectable,
|
|
1236
|
+
args: [{ providedIn: 'root' }]
|
|
1237
|
+
}] });
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Abstract loader for translation data.
|
|
1241
|
+
*
|
|
1242
|
+
* Implement this interface to load translations from any source:
|
|
1243
|
+
* - HTTP endpoints
|
|
1244
|
+
* - SignalR real-time updates
|
|
1245
|
+
* - Static imports
|
|
1246
|
+
* - IndexedDB
|
|
1247
|
+
* - etc.
|
|
1248
|
+
*
|
|
1249
|
+
* @example
|
|
1250
|
+
* ```ts
|
|
1251
|
+
* @Injectable()
|
|
1252
|
+
* export class MyCustomLoader implements CoarTranslationLoader {
|
|
1253
|
+
* loadTranslations(language: string): Observable<CoarTranslations> {
|
|
1254
|
+
* // Load from your custom source
|
|
1255
|
+
* return this.myService.getTranslations(language);
|
|
1256
|
+
* }
|
|
1257
|
+
* }
|
|
1258
|
+
* ```
|
|
1259
|
+
*/
|
|
1260
|
+
class CoarTranslationLoader {
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* HTTP-based translation loader.
|
|
1264
|
+
*
|
|
1265
|
+
* Loads translation JSON files from a configurable base path.
|
|
1266
|
+
*
|
|
1267
|
+
* ## Default behavior
|
|
1268
|
+
* - Base path: `/i18n/`
|
|
1269
|
+
* - File pattern: `{language}.json`
|
|
1270
|
+
* - Example: `/i18n/en.json`, `/i18n/de.json`
|
|
1271
|
+
*
|
|
1272
|
+
* ## Custom path
|
|
1273
|
+
* ```ts
|
|
1274
|
+
* const loader = new CoarHttpTranslationLoader();
|
|
1275
|
+
* loader.basePath = '/assets/translations/';
|
|
1276
|
+
* // Loads: /assets/translations/en.json
|
|
1277
|
+
* ```
|
|
1278
|
+
*
|
|
1279
|
+
* ## Expected JSON format
|
|
1280
|
+
* ```json
|
|
1281
|
+
* {
|
|
1282
|
+
* "hello": "Hello",
|
|
1283
|
+
* "goodbye": "Goodbye",
|
|
1284
|
+
* "welcome": "Welcome, {{name}}!"
|
|
1285
|
+
* }
|
|
1286
|
+
* ```
|
|
1287
|
+
*/
|
|
1288
|
+
class CoarHttpTranslationLoader {
|
|
1289
|
+
http = inject(HttpClient);
|
|
1290
|
+
/** Base path for translation files */
|
|
1291
|
+
basePath = '/i18n/';
|
|
1292
|
+
loadTranslations(language) {
|
|
1293
|
+
const url = `${this.basePath}${language}.json`;
|
|
1294
|
+
return this.http.get(url);
|
|
1295
|
+
}
|
|
1296
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarHttpTranslationLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1297
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarHttpTranslationLoader });
|
|
1298
|
+
}
|
|
1299
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarHttpTranslationLoader, decorators: [{
|
|
1300
|
+
type: Injectable
|
|
1301
|
+
}] });
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Reactive store for translation data.
|
|
1305
|
+
*
|
|
1306
|
+
* Stores translations for multiple languages in memory and provides
|
|
1307
|
+
* Signal-based API for reactive updates.
|
|
1308
|
+
*
|
|
1309
|
+
* ## Features
|
|
1310
|
+
* - Signal-based reactive API
|
|
1311
|
+
* - Language-scoped storage
|
|
1312
|
+
* - Simple Map-based implementation
|
|
1313
|
+
* - Zero dependencies beyond Angular core
|
|
1314
|
+
*
|
|
1315
|
+
* @example
|
|
1316
|
+
* ```ts
|
|
1317
|
+
* const store = inject(CoarTranslationStore);
|
|
1318
|
+
*
|
|
1319
|
+
* // Load translations
|
|
1320
|
+
* store.setTranslations('en', { 'hello': 'Hello' });
|
|
1321
|
+
* store.setTranslations('de', { 'hello': 'Hallo' });
|
|
1322
|
+
*
|
|
1323
|
+
* // Check if language is loaded
|
|
1324
|
+
* if (store.hasLanguage('en')) {
|
|
1325
|
+
* const translations = store.getTranslations('en');
|
|
1326
|
+
* }
|
|
1327
|
+
*
|
|
1328
|
+
* // Get specific translation
|
|
1329
|
+
* const value = store.getTranslation('en', 'hello'); // 'Hello'
|
|
1330
|
+
*
|
|
1331
|
+
* // Check for missing keys
|
|
1332
|
+
* const missing = store.getTranslation('en', 'unknown'); // undefined
|
|
1333
|
+
* ```
|
|
1334
|
+
*/
|
|
1335
|
+
class CoarTranslationStore {
|
|
1336
|
+
/**
|
|
1337
|
+
* Internal storage: Map<language, Map<key, translation>>
|
|
1338
|
+
*
|
|
1339
|
+
* Why nested Maps instead of Record:
|
|
1340
|
+
* - Faster lookups for large translation sets
|
|
1341
|
+
* - No prototype chain pollution
|
|
1342
|
+
* - Clear separation between languages
|
|
1343
|
+
*/
|
|
1344
|
+
storage = signal(new Map(), ...(ngDevMode ? [{ debugName: "storage" }] : []));
|
|
1345
|
+
/**
|
|
1346
|
+
* Set of all loaded languages.
|
|
1347
|
+
* Used for quick existence checks.
|
|
1348
|
+
*/
|
|
1349
|
+
loadedLanguages = computed(() => {
|
|
1350
|
+
const map = this.storage();
|
|
1351
|
+
return new Set(map.keys());
|
|
1352
|
+
}, ...(ngDevMode ? [{ debugName: "loadedLanguages" }] : []));
|
|
1353
|
+
/**
|
|
1354
|
+
* Stores all translations for a specific language.
|
|
1355
|
+
*
|
|
1356
|
+
* Replaces any existing translations for that language.
|
|
1357
|
+
*
|
|
1358
|
+
* @param language - Language code (e.g., 'en', 'de')
|
|
1359
|
+
* @param translations - Translation key-value pairs
|
|
1360
|
+
*
|
|
1361
|
+
* @example
|
|
1362
|
+
* ```ts
|
|
1363
|
+
* // HTTP: Load entire language at once
|
|
1364
|
+
* store.setTranslations('en', { 'hello': 'Hello', 'goodbye': 'Goodbye' });
|
|
1365
|
+
* ```
|
|
1366
|
+
*/
|
|
1367
|
+
setTranslations(language, translations) {
|
|
1368
|
+
const translationMap = new Map(Object.entries(translations));
|
|
1369
|
+
this.storage.update((current) => {
|
|
1370
|
+
const next = new Map(current);
|
|
1371
|
+
next.set(language, translationMap);
|
|
1372
|
+
return next;
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Updates a single translation key for a specific language.
|
|
1377
|
+
*
|
|
1378
|
+
* Creates the language if it doesn't exist.
|
|
1379
|
+
* Useful for real-time updates via SignalR.
|
|
1380
|
+
*
|
|
1381
|
+
* @param language - Language code
|
|
1382
|
+
* @param key - Translation key
|
|
1383
|
+
* @param value - Translation value
|
|
1384
|
+
*
|
|
1385
|
+
* @example
|
|
1386
|
+
* ```ts
|
|
1387
|
+
* // SignalR: Update single key in real-time
|
|
1388
|
+
* signalR.on('TranslationUpdated', ({ lang, key, value }) => {
|
|
1389
|
+
* store.setTranslation(lang, key, value);
|
|
1390
|
+
* });
|
|
1391
|
+
* ```
|
|
1392
|
+
*/
|
|
1393
|
+
setTranslation(language, key, value) {
|
|
1394
|
+
this.storage.update((current) => {
|
|
1395
|
+
const next = new Map(current);
|
|
1396
|
+
const langMap = next.get(language) ?? new Map();
|
|
1397
|
+
const updatedLangMap = new Map(langMap);
|
|
1398
|
+
updatedLangMap.set(key, value);
|
|
1399
|
+
next.set(language, updatedLangMap);
|
|
1400
|
+
return next;
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Merges partial translations into an existing language.
|
|
1405
|
+
*
|
|
1406
|
+
* Only updates/adds the provided keys, keeps existing keys intact.
|
|
1407
|
+
* Creates the language if it doesn't exist.
|
|
1408
|
+
*
|
|
1409
|
+
* @param language - Language code
|
|
1410
|
+
* @param partialTranslations - Partial translation key-value pairs to merge
|
|
1411
|
+
*
|
|
1412
|
+
* @example
|
|
1413
|
+
* ```ts
|
|
1414
|
+
* // Existing: { 'hello': 'Hello', 'goodbye': 'Goodbye' }
|
|
1415
|
+
* store.updateTranslations('en', { 'hello': 'Hi' });
|
|
1416
|
+
* // Result: { 'hello': 'Hi', 'goodbye': 'Goodbye' }
|
|
1417
|
+
* ```
|
|
1418
|
+
*
|
|
1419
|
+
* @example
|
|
1420
|
+
* ```ts
|
|
1421
|
+
* // SignalR: Batch update multiple keys
|
|
1422
|
+
* signalR.on('TranslationsBatchUpdated', ({ lang, updates }) => {
|
|
1423
|
+
* store.updateTranslations(lang, updates);
|
|
1424
|
+
* });
|
|
1425
|
+
* ```
|
|
1426
|
+
*/
|
|
1427
|
+
updateTranslations(language, partialTranslations) {
|
|
1428
|
+
this.storage.update((current) => {
|
|
1429
|
+
const next = new Map(current);
|
|
1430
|
+
const langMap = next.get(language) ?? new Map();
|
|
1431
|
+
const updatedLangMap = new Map(langMap);
|
|
1432
|
+
// Merge partial updates into existing translations
|
|
1433
|
+
for (const [key, value] of Object.entries(partialTranslations)) {
|
|
1434
|
+
updatedLangMap.set(key, value);
|
|
1435
|
+
}
|
|
1436
|
+
next.set(language, updatedLangMap);
|
|
1437
|
+
return next;
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Checks if translations for a language are loaded.
|
|
1442
|
+
*
|
|
1443
|
+
* @param language - Language code to check
|
|
1444
|
+
* @returns True if language is loaded
|
|
1445
|
+
*/
|
|
1446
|
+
hasLanguage(language) {
|
|
1447
|
+
return this.storage().has(language);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Gets all translations for a specific language.
|
|
1451
|
+
*
|
|
1452
|
+
* @param language - Language code
|
|
1453
|
+
* @returns Translation map, or undefined if language not loaded
|
|
1454
|
+
*/
|
|
1455
|
+
getTranslations(language) {
|
|
1456
|
+
return this.storage().get(language);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Gets a specific translation value.
|
|
1460
|
+
*
|
|
1461
|
+
* @param language - Language code
|
|
1462
|
+
* @param key - Translation key
|
|
1463
|
+
* @returns Translation value, or undefined if not found
|
|
1464
|
+
*/
|
|
1465
|
+
getTranslation(language, key) {
|
|
1466
|
+
return this.storage().get(language)?.get(key);
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Clears all translations from the store.
|
|
1470
|
+
*
|
|
1471
|
+
* Used primarily for testing.
|
|
1472
|
+
*/
|
|
1473
|
+
clear() {
|
|
1474
|
+
this.storage.set(new Map());
|
|
1475
|
+
}
|
|
1476
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarTranslationStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1477
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarTranslationStore, providedIn: 'root' });
|
|
1478
|
+
}
|
|
1479
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarTranslationStore, decorators: [{
|
|
1480
|
+
type: Injectable,
|
|
1481
|
+
args: [{ providedIn: 'root' }]
|
|
1482
|
+
}] });
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Core i18n service implementation.
|
|
1486
|
+
*
|
|
1487
|
+
* Integrates:
|
|
1488
|
+
* - `CoarLocalizationService` - Language state management
|
|
1489
|
+
* - `CoarTranslationStore` - Translation storage
|
|
1490
|
+
* - `CoarTranslationLoader` - Translation loading
|
|
1491
|
+
* - `CoarI18nContext` - Coordination layer
|
|
1492
|
+
*
|
|
1493
|
+
* ## Features
|
|
1494
|
+
* - Automatic translation loading when language changes
|
|
1495
|
+
* - Missing translation detection
|
|
1496
|
+
* - Parameter interpolation
|
|
1497
|
+
* - Reactive Signal-based API
|
|
1498
|
+
*
|
|
1499
|
+
* ## Usage
|
|
1500
|
+
* Use `provideCoarI18n()` to configure - don't instantiate directly.
|
|
1501
|
+
*
|
|
1502
|
+
* @example
|
|
1503
|
+
* ```ts
|
|
1504
|
+
* export const appConfig: ApplicationConfig = {
|
|
1505
|
+
* providers: [
|
|
1506
|
+
* provideCoarLocalization({
|
|
1507
|
+
* availableLanguages: ['en', 'de'],
|
|
1508
|
+
* defaultLanguage: 'en',
|
|
1509
|
+
* }),
|
|
1510
|
+
* provideCoarI18n({
|
|
1511
|
+
* loader: CoarHttpTranslationLoader,
|
|
1512
|
+
* }),
|
|
1513
|
+
* ],
|
|
1514
|
+
* };
|
|
1515
|
+
* ```
|
|
1516
|
+
*/
|
|
1517
|
+
class CoarI18nService {
|
|
1518
|
+
locale = inject(CoarLocalizationService);
|
|
1519
|
+
store = inject(CoarTranslationStore);
|
|
1520
|
+
loader = inject(CoarTranslationLoader);
|
|
1521
|
+
/**
|
|
1522
|
+
* Signal containing all translations for the current language.
|
|
1523
|
+
*
|
|
1524
|
+
* Returns undefined if language not yet loaded.
|
|
1525
|
+
*/
|
|
1526
|
+
currentTranslations = computed(() => {
|
|
1527
|
+
const lang = this.locale.language();
|
|
1528
|
+
return this.store.getTranslations(lang);
|
|
1529
|
+
}, ...(ngDevMode ? [{ debugName: "currentTranslations" }] : []));
|
|
1530
|
+
constructor() {
|
|
1531
|
+
// Auto-load translations when language changes
|
|
1532
|
+
effect(() => {
|
|
1533
|
+
const lang = this.locale.language();
|
|
1534
|
+
// If language already loaded, do nothing
|
|
1535
|
+
if (untracked(() => this.store.hasLanguage(lang))) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
// Load translations for new language
|
|
1539
|
+
this.loadLanguage(lang).subscribe();
|
|
1540
|
+
}, { allowSignalWrites: true });
|
|
1541
|
+
}
|
|
1542
|
+
t(key, params) {
|
|
1543
|
+
const translations = this.currentTranslations();
|
|
1544
|
+
// Language not loaded yet - return key
|
|
1545
|
+
if (!translations) {
|
|
1546
|
+
return key;
|
|
1547
|
+
}
|
|
1548
|
+
const value = translations.get(key);
|
|
1549
|
+
// Missing translation - return key
|
|
1550
|
+
if (value === undefined) {
|
|
1551
|
+
return key;
|
|
1552
|
+
}
|
|
1553
|
+
// No params - return value as-is
|
|
1554
|
+
if (!params) {
|
|
1555
|
+
return value;
|
|
1556
|
+
}
|
|
1557
|
+
// Interpolate params
|
|
1558
|
+
return coarInterpolate(value, params);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Loads translations for a specific language.
|
|
1562
|
+
*
|
|
1563
|
+
* Called automatically when language changes via effect.
|
|
1564
|
+
* Can also be called manually to preload languages.
|
|
1565
|
+
*
|
|
1566
|
+
* @param language - Language code to load
|
|
1567
|
+
* @returns Observable that completes when loading finishes
|
|
1568
|
+
*/
|
|
1569
|
+
loadLanguage(language) {
|
|
1570
|
+
return this.loader.loadTranslations(language).pipe(tap((translations) => {
|
|
1571
|
+
this.store.setTranslations(language, translations);
|
|
1572
|
+
}), switchMap(() => of(void 0)), catchError((error) => {
|
|
1573
|
+
console.error(`Failed to load translations for language: ${language}`, error);
|
|
1574
|
+
// Store empty translations to prevent repeated load attempts
|
|
1575
|
+
this.store.setTranslations(language, {});
|
|
1576
|
+
return of(void 0);
|
|
1577
|
+
}));
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Preloads translations for a specific language.
|
|
1581
|
+
*
|
|
1582
|
+
* Used by APP_INITIALIZER to prevent flash of untranslated content.
|
|
1583
|
+
*
|
|
1584
|
+
* @param language - Language code to preload
|
|
1585
|
+
* @returns Promise that resolves when loading completes
|
|
1586
|
+
*/
|
|
1587
|
+
preloadLanguage(language) {
|
|
1588
|
+
// Already loaded - return immediately
|
|
1589
|
+
if (this.store.hasLanguage(language)) {
|
|
1590
|
+
return Promise.resolve();
|
|
1591
|
+
}
|
|
1592
|
+
return new Promise((resolve) => {
|
|
1593
|
+
this.loadLanguage(language).subscribe(() => resolve());
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1597
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nService });
|
|
1598
|
+
}
|
|
1599
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarI18nService, decorators: [{
|
|
1600
|
+
type: Injectable
|
|
1601
|
+
}], ctorParameters: () => [] });
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Default/fallback implementation of CoarI18nProvider.
|
|
1605
|
+
*
|
|
1606
|
+
* This is a minimal passthrough implementation that returns the key unchanged,
|
|
1607
|
+
* with placeholder interpolation applied if params are provided.
|
|
1608
|
+
*
|
|
1609
|
+
* Used when the app does not provide a custom i18n provider.
|
|
1610
|
+
* The CoarI18n service wraps this and adds the tWithDefault method.
|
|
1611
|
+
*
|
|
1612
|
+
* @example
|
|
1613
|
+
* ```ts
|
|
1614
|
+
* import { provideCoarDefaultI18n } from '@cocoar/localization';
|
|
1615
|
+
*
|
|
1616
|
+
* export const appConfig: ApplicationConfig = {
|
|
1617
|
+
* providers: [provideCoarDefaultI18n()],
|
|
1618
|
+
* };
|
|
1619
|
+
* ```
|
|
1620
|
+
*/
|
|
1621
|
+
class CoarDefaultI18n {
|
|
1622
|
+
t(key, params) {
|
|
1623
|
+
// Return the key as-is with interpolation applied
|
|
1624
|
+
return coarInterpolate(key, params);
|
|
1625
|
+
}
|
|
1626
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDefaultI18n, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1627
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDefaultI18n });
|
|
1628
|
+
}
|
|
1629
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDefaultI18n, decorators: [{
|
|
1630
|
+
type: Injectable
|
|
1631
|
+
}] });
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Provides complete Cocoar i18n system.
|
|
1635
|
+
*
|
|
1636
|
+
* Sets up translation loading, storage, and automatic language synchronization.
|
|
1637
|
+
*
|
|
1638
|
+
* ## Features
|
|
1639
|
+
* - Automatic translation loading when language changes
|
|
1640
|
+
* - Preloads initial language (prevents flash of untranslated content)
|
|
1641
|
+
* - Signal-based reactive updates
|
|
1642
|
+
* - Customizable loader (HTTP, SignalR, static, etc.)
|
|
1643
|
+
*
|
|
1644
|
+
* ## Basic usage
|
|
1645
|
+
* ```ts
|
|
1646
|
+
* import { provideCoarLocalization } from '@cocoar/localization';
|
|
1647
|
+
* import { provideCoarI18n } from '@cocoar/localization';
|
|
1648
|
+
*
|
|
1649
|
+
* export const appConfig: ApplicationConfig = {
|
|
1650
|
+
* providers: [
|
|
1651
|
+
* provideHttpClient(),
|
|
1652
|
+
* provideCoarLocalization({
|
|
1653
|
+
* availableLanguages: ['en', 'de', 'fr'],
|
|
1654
|
+
* defaultLanguage: 'en',
|
|
1655
|
+
* }),
|
|
1656
|
+
* provideCoarI18n(),
|
|
1657
|
+
* ],
|
|
1658
|
+
* };
|
|
1659
|
+
* ```
|
|
1660
|
+
*
|
|
1661
|
+
* ## Custom HTTP path
|
|
1662
|
+
* ```ts
|
|
1663
|
+
* provideCoarI18n({
|
|
1664
|
+
* basePath: '/assets/translations/',
|
|
1665
|
+
* })
|
|
1666
|
+
* ```
|
|
1667
|
+
*
|
|
1668
|
+
* ## Custom loader (e.g., SignalR)
|
|
1669
|
+
* ```ts
|
|
1670
|
+
* @Injectable()
|
|
1671
|
+
* class SignalRTranslationLoader implements CoarTranslationLoader {
|
|
1672
|
+
* loadTranslations(lang: string): Observable<CoarTranslations> {
|
|
1673
|
+
* return this.signalR.getTranslations(lang);
|
|
1674
|
+
* }
|
|
1675
|
+
* }
|
|
1676
|
+
*
|
|
1677
|
+
* provideCoarI18n({
|
|
1678
|
+
* loader: SignalRTranslationLoader,
|
|
1679
|
+
* })
|
|
1680
|
+
* ```
|
|
1681
|
+
*
|
|
1682
|
+
* @param config - Optional configuration
|
|
1683
|
+
* @returns Environment providers for the i18n system
|
|
1684
|
+
*/
|
|
1685
|
+
function provideCoarI18n(config) {
|
|
1686
|
+
const loaderClass = config?.loader ?? CoarHttpTranslationLoader;
|
|
1687
|
+
const basePath = config?.basePath ?? '/i18n/';
|
|
1688
|
+
const providers = [
|
|
1689
|
+
// Provide the loader
|
|
1690
|
+
{
|
|
1691
|
+
provide: CoarTranslationLoader,
|
|
1692
|
+
useFactory: () => {
|
|
1693
|
+
const instance = new loaderClass();
|
|
1694
|
+
// For HTTP loader, set basePath
|
|
1695
|
+
if (instance instanceof CoarHttpTranslationLoader) {
|
|
1696
|
+
instance.basePath = basePath;
|
|
1697
|
+
}
|
|
1698
|
+
return instance;
|
|
1699
|
+
},
|
|
1700
|
+
},
|
|
1701
|
+
// Provide the i18n service as COAR_I18N_PROVIDER
|
|
1702
|
+
{
|
|
1703
|
+
provide: COAR_I18N_PROVIDER,
|
|
1704
|
+
useClass: CoarI18nService,
|
|
1705
|
+
},
|
|
1706
|
+
// Preload initial language to prevent flash of untranslated content
|
|
1707
|
+
{
|
|
1708
|
+
provide: APP_INITIALIZER,
|
|
1709
|
+
multi: true,
|
|
1710
|
+
useFactory: () => {
|
|
1711
|
+
const service = inject(COAR_I18N_PROVIDER);
|
|
1712
|
+
const locale = inject(CoarLocalizationService);
|
|
1713
|
+
return async () => {
|
|
1714
|
+
const initialLanguage = locale.getCurrentLanguage();
|
|
1715
|
+
await service.preloadLanguage(initialLanguage);
|
|
1716
|
+
};
|
|
1717
|
+
},
|
|
1718
|
+
},
|
|
1719
|
+
];
|
|
1720
|
+
return makeEnvironmentProviders(providers);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* Provides the default i18n provider.
|
|
1725
|
+
*
|
|
1726
|
+
* This minimal provider returns keys unchanged with interpolation applied.
|
|
1727
|
+
* Use this as a fallback when no translation engine is configured.
|
|
1728
|
+
*
|
|
1729
|
+
* @example
|
|
1730
|
+
* ```ts
|
|
1731
|
+
* import { provideCoarDefaultI18n } from '@cocoar/localization';
|
|
1732
|
+
*
|
|
1733
|
+
* export const appConfig: ApplicationConfig = {
|
|
1734
|
+
* providers: [provideCoarDefaultI18n()],
|
|
1735
|
+
* };
|
|
1736
|
+
* ```
|
|
1737
|
+
*/
|
|
1738
|
+
function provideCoarDefaultI18n() {
|
|
1739
|
+
return {
|
|
1740
|
+
provide: COAR_I18N_PROVIDER,
|
|
1741
|
+
useClass: CoarDefaultI18n,
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// Locale (language management)
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* Generated bundle index. Do not edit.
|
|
1749
|
+
*/
|
|
1750
|
+
|
|
1751
|
+
export { COAR_I18N_PROVIDER, COAR_LOCALIZATION_CONFIG, COAR_LOCALIZATION_DATA_LOADERS, CoarCurrencyPipe, CoarDatePipe, CoarDefaultI18n, CoarHttpLocaleDataLoader, CoarHttpTranslationLoader, CoarI18n, CoarI18nContext, CoarI18nPipe, CoarI18nService, CoarIntlLocaleDataLoader, CoarLocalizationDataLoader, CoarLocalizationDataStore, CoarLocalizationService, CoarNumberPipe, CoarPercentPipe, CoarTranslationLoader, CoarTranslationStore, coarInterpolate, coarIsMissingTranslation, mergeLocalizationData, provideCoarDefaultI18n, provideCoarHttpLocalizationSource, provideCoarI18n, provideCoarIntlLocalizationSource, provideCoarLocalization };
|
|
1752
|
+
//# sourceMappingURL=cocoar-localization.mjs.map
|