ngx-atomic-i18n 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.
@@ -0,0 +1,1099 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, Injector, runInInjectionContext, effect, signal, computed, Injectable, Inject, Pipe, ElementRef, input, Directive, makeStateKey, isDevMode, PLATFORM_ID, Optional, TransferState, APP_INITIALIZER } from '@angular/core';
3
+ import { Observable, firstValueFrom } from 'rxjs';
4
+ import { HttpClient } from '@angular/common/http';
5
+ import { isPlatformServer } from '@angular/common';
6
+
7
+ /** Scoped namespace for translating a component tree (string or array). */
8
+ const TRANSLATION_NAMESPACE = new InjectionToken('TRANSLATION_NAMESPACE');
9
+ /** Root configuration describing language support and loader behavior. */
10
+ const TRANSLATION_CONFIG = new InjectionToken('TRANSLATION_CONFIG');
11
+ /** Factory used to retrieve translation JSON for a namespace/language tuple. */
12
+ const TRANSLATION_LOADER = new InjectionToken('TRANSLATION_LOADER');
13
+ /** Optional build fingerprint appended to namespace cache keys. */
14
+ const BUILD_VERSION = new InjectionToken('BUILD_VERSION');
15
+ /** Custom ICU formatter constructor injected by the host app when available. */
16
+ const ICU_FORMATTER_TOKEN = new InjectionToken('ICU_FORMATTER_TOKEN');
17
+ const PAGE_TRANSLATION_ROOT = new InjectionToken('PAGE_TRANSLATION_ROOT');
18
+ /** Per-request language captured during SSR and replayed on CSR. */
19
+ const CLIENT_REQUEST_LANG = new InjectionToken('CLIENT_REQUEST_LANG');
20
+
21
+ /** Normalizes a language code against the configured supported languages. */
22
+ function normalizeLangCode(lang, supportedLangs) {
23
+ if (!lang)
24
+ return null;
25
+ const variants = new Set();
26
+ variants.add(lang);
27
+ variants.add(lang.replace(/_/g, '-'));
28
+ variants.add(lang.replace(/-/g, '_'));
29
+ variants.add(lang.toLowerCase());
30
+ variants.add(lang.replace(/_/g, '-').toLowerCase());
31
+ for (const candidate of variants) {
32
+ const match = supportedLangs.find(supported => supported.toLowerCase() === candidate.toLowerCase());
33
+ if (match)
34
+ return match;
35
+ }
36
+ return null;
37
+ }
38
+ /** Determines the most appropriate language using the configured detection order. */
39
+ function detectPreferredLang(config) {
40
+ const { supportedLangs, fallbackLang, langDetectionOrder } = config;
41
+ for (const source of langDetectionOrder) {
42
+ let lang;
43
+ switch (source) {
44
+ case 'url':
45
+ lang = typeof window !== 'undefined' ? window.location.pathname.split('/')[1] : null;
46
+ break;
47
+ case 'clientRequest':
48
+ lang = config.clientRequestLang ?? null;
49
+ break;
50
+ case 'localStorage':
51
+ lang = typeof window !== 'undefined' ? localStorage.getItem('lang') : null;
52
+ break;
53
+ case 'browser':
54
+ const langTag = globalThis?.navigator?.language ?? '';
55
+ lang = supportedLangs.find(s => langTag.startsWith(s)) ?? null;
56
+ break;
57
+ case 'customLang':
58
+ lang = typeof config.customLang === 'function' ? config.customLang() : config.customLang ?? null;
59
+ break;
60
+ case 'fallback':
61
+ lang = fallbackLang;
62
+ break;
63
+ }
64
+ const normalized = normalizeLangCode(lang, supportedLangs);
65
+ if (normalized) {
66
+ return normalized;
67
+ }
68
+ ;
69
+ }
70
+ return normalizeLangCode(fallbackLang, supportedLangs) ?? fallbackLang;
71
+ }
72
+ /** Lightweight ICU parser that supports nested select/plural structures. */
73
+ function parseICU(templateText, params) {
74
+ if (typeof params === 'object' ? !Object.keys(params).length : true)
75
+ return templateText;
76
+ const paramMap = {};
77
+ for (const [key, val] of Object.entries(params)) {
78
+ paramMap[key] = String(val);
79
+ }
80
+ function extractBlock(text, startIndex) {
81
+ let depth = 0;
82
+ let i = startIndex;
83
+ while (i < text.length) {
84
+ if (text[i] === '{') {
85
+ if (depth === 0)
86
+ startIndex = i;
87
+ depth++;
88
+ }
89
+ else if (text[i] === '}') {
90
+ depth--;
91
+ if (depth === 0)
92
+ return [text.slice(startIndex, i + 1), i + 1];
93
+ }
94
+ i++;
95
+ }
96
+ return [text.slice(startIndex), text.length];
97
+ }
98
+ function extractOptions(body) {
99
+ const options = {};
100
+ let i = 0;
101
+ while (i < body.length) {
102
+ while (body[i] === ' ')
103
+ i++;
104
+ let key = '';
105
+ while (i < body.length && body[i] !== '{' && body[i] !== ' ') {
106
+ key += body[i++];
107
+ }
108
+ while (i < body.length && body[i] === ' ')
109
+ i++;
110
+ if (body[i] !== '{') {
111
+ // Option must have a nested block; otherwise skip it.
112
+ i++;
113
+ continue;
114
+ }
115
+ const [block, next] = extractBlock(body, i);
116
+ options[key] = block.slice(1, -1);
117
+ i = next;
118
+ }
119
+ return options;
120
+ }
121
+ function resolveICU(text) {
122
+ text = text.replace(/\{\{(\w+)\}\}/g, (_, key) => paramMap[key] ?? '');
123
+ let result = '';
124
+ let cursor = 0;
125
+ while (cursor < text.length) {
126
+ const start = text.indexOf('{', cursor);
127
+ if (start === -1) {
128
+ result += text.slice(cursor);
129
+ break;
130
+ }
131
+ result += text.slice(cursor, start);
132
+ const [block, nextIndex] = extractBlock(text, start);
133
+ if (!block || block === '' || block === '{}') {
134
+ cursor = nextIndex;
135
+ continue;
136
+ }
137
+ const match = block.match(/^\{(\w+),\s*(plural|select),/);
138
+ if (!match) {
139
+ result += block;
140
+ cursor = nextIndex;
141
+ continue;
142
+ }
143
+ const [, varName, type] = match;
144
+ const rawVal = paramMap[varName] ?? '';
145
+ const numVal = Number(rawVal);
146
+ const body = block.slice(match[0].length, -1);
147
+ const options = extractOptions(body);
148
+ const selected = options[`=${rawVal}`] ||
149
+ (type === 'plural' && numVal === 1 && options['one']) ||
150
+ options[rawVal] ||
151
+ options['other'] ||
152
+ '';
153
+ if (!selected) {
154
+ result += block;
155
+ cursor = nextIndex;
156
+ continue;
157
+ }
158
+ let resolved = resolveICU(selected);
159
+ if (type === 'plural') {
160
+ resolved = resolved.replace(/#/g, rawVal);
161
+ }
162
+ resolved = resolved.replace(/{{(\w+)}}|\{(\w+)\}/g, (_, k1, k2) => {
163
+ const k = k1 || k2;
164
+ return paramMap[k] ?? '';
165
+ });
166
+ result += resolved;
167
+ cursor = nextIndex;
168
+ }
169
+ return result;
170
+ }
171
+ return resolveICU(templateText);
172
+ }
173
+ /** Flattens a nested translation object using dot notation keys. */
174
+ function flattenTranslations(obj, prefix = '') {
175
+ const result = {};
176
+ for (const [key, value] of Object.entries(obj)) {
177
+ const newKey = prefix ? `${prefix}.${key}` : key;
178
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
179
+ Object.assign(result, flattenTranslations(value, newKey));
180
+ }
181
+ else {
182
+ result[newKey] = String(value);
183
+ }
184
+ }
185
+ return result;
186
+ }
187
+ /** Converts a signal to an observable while preserving injection context. */
188
+ function toObservable(signal) {
189
+ const injector = inject(Injector);
190
+ return new Observable(subscribe => {
191
+ subscribe.next(signal());
192
+ const stop = runInInjectionContext(injector, () => effect(() => subscribe.next(signal()), { allowSignalWrites: true }));
193
+ return () => stop.destroy();
194
+ });
195
+ }
196
+ /** Deeply merges plain objects, replacing non-object values by assignment. */
197
+ function deepMerge(target, source) {
198
+ const output = { ...target };
199
+ for (const key in source) {
200
+ const targetValue = target[key];
201
+ if (source.hasOwnProperty(key) &&
202
+ typeof source[key] === 'object' &&
203
+ source[key] !== null &&
204
+ !Array.isArray(source[key]) &&
205
+ typeof targetValue === 'object' &&
206
+ targetValue !== null &&
207
+ !Array.isArray(targetValue)) {
208
+ output[key] = deepMerge(targetValue, source[key]);
209
+ }
210
+ else {
211
+ output[key] = source[key];
212
+ }
213
+ }
214
+ return output;
215
+ }
216
+ /** Recursively retains only keys that are not already present in the existing object. */
217
+ function filterNewKeysDeep(bundle, existing) {
218
+ const result = {};
219
+ for (const key in bundle) {
220
+ const existValue = existing[key];
221
+ if (typeof bundle[key] === 'object' &&
222
+ bundle[key] !== null &&
223
+ !Array.isArray(bundle[key]) &&
224
+ typeof existValue === 'object' &&
225
+ existValue !== null &&
226
+ !Array.isArray(existValue)) {
227
+ result[key] = filterNewKeysDeep(bundle[key], existValue);
228
+ }
229
+ else if (!(key in existing)) {
230
+ result[key] = bundle[key];
231
+ }
232
+ }
233
+ return result;
234
+ }
235
+ /** Safely reads a dotted path from a nested object. */
236
+ function getNested(obj, path) {
237
+ return path.split('.').reduce((res, key) => res?.[key], obj);
238
+ }
239
+ /** Removes any leading slashes from path-like strings. */
240
+ const stripLeadingSep = (s) => s.replace(/^[\\/]+/, '');
241
+ /** Normalises the path template configuration to an array form. */
242
+ const tempToArray = (template) => Array.isArray(template) ? template : (template ? [template] : undefined);
243
+ /**
244
+ * Detect current build version from injected script names (CSR only).
245
+ * Matches filenames like: main.<hash>.js, runtime.<hash>.js, polyfills.<hash>.js
246
+ */
247
+ function detectBuildVersion() {
248
+ try {
249
+ if (typeof document === 'undefined' || !document?.scripts)
250
+ return null;
251
+ const regex = /\/(?:main|runtime|polyfills)\.([a-f0-9]{8,})\.[^\/]*\.js(?:\?|$)/i;
252
+ for (const s of Array.from(document.scripts)) {
253
+ const version = regex.exec(s.src);
254
+ if (version?.[1])
255
+ return version[1];
256
+ }
257
+ return null;
258
+ }
259
+ catch {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ /** Combined FIFO/LRU cache used to store compiled formatters. */
265
+ class FIFOCache {
266
+ max;
267
+ map = new Map();
268
+ get size() {
269
+ return this.map.size;
270
+ }
271
+ constructor(max) {
272
+ this.max = max;
273
+ }
274
+ set(key, value) {
275
+ if (this.map.has(key)) {
276
+ this.map.delete(key);
277
+ }
278
+ this.map.set(key, value);
279
+ if (this.map.size > this.max) {
280
+ const first = this.map.keys().next().value;
281
+ if (first) {
282
+ this.map.delete(first);
283
+ }
284
+ }
285
+ }
286
+ get(key) {
287
+ const val = this.map.get(key);
288
+ // Refresh recency on hit so frequently used entries stay resident.
289
+ if (val !== undefined) {
290
+ this.map.delete(key);
291
+ this.map.set(key, val);
292
+ }
293
+ return val;
294
+ }
295
+ has(key) {
296
+ return this.map.has(key);
297
+ }
298
+ delete(k) {
299
+ this.map.delete(k);
300
+ }
301
+ clear() {
302
+ this.map.clear();
303
+ }
304
+ /** Utility helper for controlled external iteration/manipulation. */
305
+ keys() {
306
+ return this.map.keys();
307
+ }
308
+ /** Iterates cached values without exposing the backing Map. */
309
+ forEach(cb) {
310
+ this.map.forEach((v, k) => cb(v, k));
311
+ }
312
+ /**
313
+ * Delete all entries that match the predicate.
314
+ * Returns the number of deleted entries.
315
+ */
316
+ deleteWhere(predicate) {
317
+ let count = 0;
318
+ for (const [k, v] of this.map) {
319
+ if (predicate(k, v)) {
320
+ this.map.delete(k);
321
+ count++;
322
+ }
323
+ }
324
+ return count;
325
+ }
326
+ }
327
+
328
+ /** Maximum compiled formatter entries retained before evicting older ones. */
329
+ const MAX_CACHE_SIZE = 30;
330
+ /**
331
+ * Central cache and orchestration layer for loading translation namespaces,
332
+ * compiling ICU messages, and serving formatted strings to the higher-level API.
333
+ */
334
+ class TranslationCoreService {
335
+ /** Immutable runtime configuration injected from the library bootstrap. */
336
+ _config = inject(TRANSLATION_CONFIG);
337
+ /** Active loader used to fetch namespace JSON payloads. */
338
+ _loader = inject(TRANSLATION_LOADER);
339
+ /** Optional custom ICU formatter constructor supplied by the host application. */
340
+ _ICU = inject(ICU_FORMATTER_TOKEN, { optional: true });
341
+ debugEnabled = !!this._config.debug;
342
+ /** Reactive language state tracking the current locale. */
343
+ _lang = signal(detectPreferredLang(this._config));
344
+ lang = this._lang.asReadonly();
345
+ /** Translation documents keyed by language then namespace. */
346
+ _jsonCache = signal(new Map()); // lang => namespace => key
347
+ /** Version fingerprints captured alongside each namespace entry. */
348
+ _versionMap = signal(new Map()); // lang => namespace => version
349
+ /** Least-recently-used cache of compiled ICU formatters. */
350
+ _formatterCache = new FIFOCache(MAX_CACHE_SIZE);
351
+ _missingKeyCache = new Set();
352
+ _inflight = new Map();
353
+ /** Caches compiled ICU expressions (with or without custom formatters). */
354
+ _icuCompiledCache = new Map();
355
+ onLangChange = toObservable(this._lang);
356
+ fallbackLang = this._config.fallbackLang ?? 'en';
357
+ get currentLang() {
358
+ return this._lang.asReadonly()();
359
+ }
360
+ readySignal(namespace, version) {
361
+ return computed(() => this.hasJsonCacheValue(this.lang(), namespace, version));
362
+ }
363
+ setLang(lang) {
364
+ const attempted = lang ?? detectPreferredLang(this._config);
365
+ const currentLang = normalizeLangCode(attempted, this._config.supportedLangs);
366
+ if (!currentLang) {
367
+ this.warn(`Unsupported language requested: ${attempted}`);
368
+ return;
369
+ }
370
+ if (this._lang() !== currentLang) {
371
+ this._lang.set(currentLang);
372
+ this.log(`Switched active language to "${currentLang}".`);
373
+ this._icuCompiledCache.clear();
374
+ const isBroswer = typeof window !== 'undefined';
375
+ if (isBroswer) {
376
+ try {
377
+ localStorage.setItem('lang', this._lang());
378
+ }
379
+ catch (err) {
380
+ this.warn('Failed to persist language to localStorage.', err);
381
+ }
382
+ }
383
+ }
384
+ }
385
+ async load(nsKey, fetchFn) {
386
+ const parts = nsKey.split(':');
387
+ const lang = parts[0];
388
+ const namespace = parts[1];
389
+ const version = parts[2];
390
+ if (this.hasJsonCacheValue(lang, namespace, version)) {
391
+ this.log(`Namespace "${namespace}" for "${lang}" is already cached. Skip loader.`);
392
+ return;
393
+ }
394
+ // coalesce concurrent loads for the same nsKey
395
+ if (this._inflight.has(nsKey)) {
396
+ this.log(`Namespace "${namespace}" for "${lang}" is already loading. Reusing in-flight request.`);
397
+ await this._inflight.get(nsKey);
398
+ return;
399
+ }
400
+ this.log(`Loading namespace "${namespace}" for "${lang}"${version ? ` (version ${version})` : ''}.`);
401
+ const p = (async () => {
402
+ try {
403
+ const json = await fetchFn();
404
+ this.handleNewTranslations(json, lang, namespace, version);
405
+ this.log(`Namespace "${namespace}" for "${lang}" loaded successfully.`);
406
+ }
407
+ catch (error) {
408
+ this.error(`Failed to load namespace "${namespace}" for "${lang}".`, error);
409
+ throw error;
410
+ }
411
+ finally {
412
+ this._inflight.delete(nsKey);
413
+ }
414
+ })();
415
+ this._inflight.set(nsKey, p);
416
+ await p;
417
+ }
418
+ getAndCreateFormatter(nsKey, key) {
419
+ const cacheKey = `${nsKey}:${key}`;
420
+ if (this._formatterCache.has(cacheKey))
421
+ return this._formatterCache.get(cacheKey);
422
+ const [lang, namespace] = nsKey.split(':');
423
+ const raw = getNested(this._jsonCache().get(lang)?.get(namespace), key);
424
+ if (raw === undefined)
425
+ return;
426
+ let result;
427
+ if (this._ICU) {
428
+ const k = `${raw}|${this.lang()}`;
429
+ const exist = this._icuCompiledCache.get(k);
430
+ if (exist) {
431
+ result = exist;
432
+ }
433
+ else {
434
+ result = new this._ICU(raw, this.lang());
435
+ this._icuCompiledCache.set(k, result);
436
+ }
437
+ }
438
+ else {
439
+ const k = raw;
440
+ const exist = this._icuCompiledCache.get(k);
441
+ if (exist) {
442
+ result = exist;
443
+ }
444
+ else {
445
+ result = { format: (p) => parseICU(raw, p) };
446
+ this._icuCompiledCache.set(k, result);
447
+ }
448
+ }
449
+ this._formatterCache.set(cacheKey, result);
450
+ return result;
451
+ }
452
+ findFallbackFormatter(key, exclude, version) {
453
+ const namespaces = Array.isArray(this._config.fallbackNamespace)
454
+ ? this._config.fallbackNamespace
455
+ : [this._config.fallbackNamespace ?? ''];
456
+ for (const namespace of namespaces) {
457
+ const nsKey = version ? `${this.currentLang}:${namespace}:${version}` : `${this.currentLang}:${namespace}`;
458
+ if (exclude.includes(nsKey))
459
+ continue;
460
+ const missKey = version
461
+ ? `${this.currentLang}:${namespace}:${version}:${key}`
462
+ : `${this.currentLang}:${namespace}:${key}`;
463
+ if (this._missingKeyCache.has(missKey))
464
+ continue;
465
+ const result = this.getAndCreateFormatter(nsKey, key);
466
+ if (result)
467
+ return result;
468
+ this._missingKeyCache.add(missKey);
469
+ }
470
+ return undefined;
471
+ }
472
+ async preloadNamespaces(namespaces, lang) {
473
+ const roots = Array.isArray(this._config.i18nRoots) ? this._config.i18nRoots : [this._config.i18nRoots];
474
+ const loadList = namespaces.filter(namespace => !this.hasJsonCacheValue(lang, namespace));
475
+ const jsonArray = await Promise.all(loadList.map(namespace => this._loader.load(roots, namespace, lang)));
476
+ jsonArray.forEach((json, index) => this.handleNewTranslations(json, lang, loadList[index], undefined));
477
+ }
478
+ handleNewTranslations(json, lang, namespace, version) {
479
+ const map = new Map(this._jsonCache());
480
+ const langMap = new Map(this._jsonCache().get(lang));
481
+ langMap.set(namespace, json);
482
+ map.set(lang, langMap);
483
+ this._jsonCache.set(map);
484
+ const vMap = new Map(this._versionMap());
485
+ const vLangMap = new Map(this._versionMap().get(lang));
486
+ vLangMap.set(namespace, version);
487
+ vMap.set(lang, vLangMap);
488
+ this._versionMap.set(vMap);
489
+ // Invalidate formatter cache for this namespace (covers with/without version)
490
+ const nsKey = `${lang}:${namespace}:`;
491
+ this._formatterCache.deleteWhere((k) => k.startsWith(nsKey));
492
+ this._missingKeyCache.clear();
493
+ if (this.debugEnabled) {
494
+ const keyCount = json && typeof json === 'object' ? Object.keys(json).length : 0;
495
+ this.log(`Cached namespace "${namespace}" for "${lang}" (${keyCount} top-level keys).`);
496
+ }
497
+ }
498
+ hasJsonCacheValue(lang, namespace, version) {
499
+ const exists = this._jsonCache().get(lang)?.get(namespace) !== undefined;
500
+ if (!exists)
501
+ return false;
502
+ if (version !== undefined) {
503
+ const stored = this._versionMap().get(lang)?.get(namespace);
504
+ return stored === version;
505
+ }
506
+ return true;
507
+ }
508
+ addResourceBundle(lang, namespace, bundle, deep = true, overwrite = true) {
509
+ const map = new Map(this._jsonCache());
510
+ const oldLangMap = map.get(lang);
511
+ const langMap = oldLangMap ? new Map(map.get(lang)) : new Map();
512
+ const existTranslations = langMap.get(namespace) ?? {};
513
+ let merged;
514
+ if (deep) {
515
+ merged = overwrite
516
+ ? deepMerge(existTranslations, bundle)
517
+ : deepMerge(existTranslations, filterNewKeysDeep(bundle, existTranslations));
518
+ }
519
+ else {
520
+ merged = overwrite
521
+ ? { ...existTranslations, ...bundle }
522
+ : { ...bundle };
523
+ if (!overwrite) {
524
+ for (const key in existTranslations) {
525
+ if (!(key in merged)) {
526
+ merged[key] = existTranslations[key];
527
+ }
528
+ }
529
+ }
530
+ }
531
+ langMap.set(namespace, merged);
532
+ map.set(lang, langMap);
533
+ this._jsonCache.set(map);
534
+ }
535
+ addResources(lang, namespace, obj, overwrite = true) {
536
+ this.addResourceBundle(lang, namespace, obj, false, overwrite);
537
+ }
538
+ addResource(lang, namespace, key, val, overwrite = true) {
539
+ this.addResources(lang, namespace, { [key]: val }, overwrite);
540
+ }
541
+ getAllBundle() {
542
+ return this._jsonCache();
543
+ }
544
+ hasResourceBundle(lang, namespace) {
545
+ return !!this._jsonCache().get(lang)?.has(namespace);
546
+ }
547
+ getResourceBundle(lang, namespace) {
548
+ return this._jsonCache().get(lang)?.get(namespace);
549
+ }
550
+ removeResourceBundle(lang, namespace) {
551
+ const map = new Map(this._jsonCache());
552
+ const langMap = new Map(map.get(lang));
553
+ langMap.delete(namespace);
554
+ map.set(lang, langMap);
555
+ this._jsonCache.set(map);
556
+ // Evict all formatter cache entries that belong to this lang:namespace (with or without version)
557
+ // Keys are in form `${nsKey}:${key}` where nsKey may include version
558
+ const prefix = `${lang}:${namespace}:`;
559
+ this._formatterCache.deleteWhere((k) => k.startsWith(prefix));
560
+ // also clear missing-key entries for this namespace
561
+ for (const k of Array.from(this._missingKeyCache)) {
562
+ if (k.startsWith(prefix))
563
+ this._missingKeyCache.delete(k);
564
+ }
565
+ }
566
+ /** Clear everything: data, versions, formatters, missing keys, inflight */
567
+ clearAll() {
568
+ this._jsonCache.set(new Map());
569
+ this._versionMap.set(new Map());
570
+ this._formatterCache.clear();
571
+ this._missingKeyCache.clear();
572
+ this._inflight.clear();
573
+ this._icuCompiledCache.clear();
574
+ }
575
+ /** Clear all resources for a language */
576
+ clearLang(lang) {
577
+ const map = new Map(this._jsonCache());
578
+ map.delete(lang);
579
+ this._jsonCache.set(map);
580
+ const vmap = new Map(this._versionMap());
581
+ vmap.delete(lang);
582
+ this._versionMap.set(vmap);
583
+ this._formatterCache.deleteWhere((k) => k.startsWith(`${lang}:`));
584
+ for (const k of Array.from(this._missingKeyCache)) {
585
+ if (k.startsWith(`${lang}:`))
586
+ this._missingKeyCache.delete(k);
587
+ }
588
+ for (const k of Array.from(this._icuCompiledCache.keys())) {
589
+ if (k.endsWith(`|${lang}`))
590
+ this._icuCompiledCache.delete(k);
591
+ }
592
+ }
593
+ /** Clear a specific namespace for a language */
594
+ clearNamespace(lang, namespace) {
595
+ this.removeResourceBundle(lang, namespace);
596
+ }
597
+ getResource(lang, namespace, key) {
598
+ return getNested(this._jsonCache().get(lang)?.get(namespace), key);
599
+ }
600
+ log(message, ...details) {
601
+ if (!this.debugEnabled)
602
+ return;
603
+ if (details.length) {
604
+ console.info(`[ngx-atomic-i18n] ${message}`, ...details);
605
+ }
606
+ else {
607
+ console.info(`[ngx-atomic-i18n] ${message}`);
608
+ }
609
+ }
610
+ error(message, error) {
611
+ if (!this.debugEnabled)
612
+ return;
613
+ console.error(`[ngx-atomic-i18n] ${message}`, error);
614
+ }
615
+ warn(message, ...details) {
616
+ if (!this.debugEnabled)
617
+ return;
618
+ if (details.length) {
619
+ console.warn(`[ngx-atomic-i18n] ${message}`, ...details);
620
+ }
621
+ else {
622
+ console.warn(`[ngx-atomic-i18n] ${message}`);
623
+ }
624
+ }
625
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationCoreService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
626
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationCoreService, providedIn: 'root' });
627
+ }
628
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationCoreService, decorators: [{
629
+ type: Injectable,
630
+ args: [{
631
+ providedIn: 'root'
632
+ }]
633
+ }] });
634
+
635
+ class TranslationService {
636
+ namespaceInput;
637
+ /** Shared translation configuration resolved from the host application. */
638
+ config = inject(TRANSLATION_CONFIG);
639
+ parent = inject(TranslationService, { skipSelf: true, optional: true });
640
+ isPageRoot = inject(PAGE_TRANSLATION_ROOT, { self: true, optional: true }) ?? false;
641
+ /** Core translation engine that handles lookups and formatter lifecycle. */
642
+ core = inject(TranslationCoreService);
643
+ /** Loader implementation responsible for fetching translation payloads. */
644
+ loader = inject(TRANSLATION_LOADER);
645
+ /** Optional build fingerprint for cache busting injected at runtime. */
646
+ injectedVersion = inject(BUILD_VERSION, { optional: true });
647
+ /** Flag used to guard debug logging output. */
648
+ debugEnabled = !!this.config.debug;
649
+ /** Observable mirror of the language signal for consumers outside of signals. */
650
+ onLangChange = toObservable(this.lang);
651
+ /** Namespace currently owned by this service instance. */
652
+ namespace = '';
653
+ get lang() {
654
+ return computed(() => this.core.lang());
655
+ }
656
+ get currentLang() {
657
+ return this.core.currentLang;
658
+ }
659
+ get supportedLangs() {
660
+ return this.config.supportedLangs;
661
+ }
662
+ /** Build version identifier used to scope namespace caches. */
663
+ buildVersion = this.injectedVersion ?? detectBuildVersion();
664
+ /** Composes the unique key used to store namespace resources per lang and build. */
665
+ get getNskey() {
666
+ const version = this.buildVersion;
667
+ return version ? `${this.lang()}:${this.namespace}:${version}` : `${this.lang()}:${this.namespace}`;
668
+ }
669
+ /** Signal that flips to true when the namespace resources are available. */
670
+ get readySignal() {
671
+ return this.core.readySignal(this.namespace, this.buildVersion ?? undefined);
672
+ }
673
+ /** Convenience boolean wrapper around the readiness signal. */
674
+ get ready() {
675
+ return this.core.readySignal(this.namespace, this.buildVersion ?? undefined)();
676
+ }
677
+ constructor(namespaceInput) {
678
+ this.namespaceInput = namespaceInput;
679
+ this.namespace = namespaceInput;
680
+ effect(() => {
681
+ const nsKey = this.getNskey;
682
+ if (!this.ready) {
683
+ this.info(`Namespace "${this.namespace}" is not ready. Loading for "${this.lang()}" using key "${nsKey}".`);
684
+ this.core.load(nsKey, () => this.loader.load(this.config.i18nRoots, this.namespace, this.lang()));
685
+ }
686
+ });
687
+ }
688
+ /** Switches the active language and triggers downstream refresh logic. */
689
+ setLang(lang) {
690
+ this.info(`setLang called with "${lang}".`);
691
+ this.core.setLang(lang);
692
+ }
693
+ /**
694
+ * Resolves the translation for `key`, formatting with the provided params
695
+ * and falling back to configured behaviors when a translation is missing.
696
+ */
697
+ t(key, params = {}) {
698
+ const nsKey = this.getNskey;
699
+ const missingResult = this.getMissingTranslation(key);
700
+ if (!this.ready) {
701
+ this.warn(`Namespace "${this.namespace}" is not ready.`);
702
+ return '';
703
+ }
704
+ const formatResult = this.core.getAndCreateFormatter(nsKey, key);
705
+ if (formatResult)
706
+ return formatResult.format(params);
707
+ if (this.config.enablePageFallback && !this.isPageRoot && this.parent) {
708
+ return this.parent.t(key, params);
709
+ }
710
+ const fallback = this.core.findFallbackFormatter(key, [], this.buildVersion ?? undefined);
711
+ if (fallback) {
712
+ this.info(`Resolved key "${key}" via fallback namespace while rendering "${this.namespace}".`);
713
+ return fallback.format(params);
714
+ }
715
+ this.warn(`Missing translation for key "${key}" in namespace "${this.namespace}". Returning fallback value.`);
716
+ return missingResult;
717
+ }
718
+ /** Determines the fallback string or error when a translation entry is missing. */
719
+ getMissingTranslation(key) {
720
+ const forceMode = this.config.missingTranslationBehavior ?? 'show-key';
721
+ switch (forceMode) {
722
+ case 'throw-error':
723
+ throw new Error(`[i18n] Missing translation: ${key} in ${this.namespace}`);
724
+ case 'empty':
725
+ this.warn(`Missing translation returned an empty string for key "${key}" in namespace "${this.namespace}".`);
726
+ return '';
727
+ case 'show-key':
728
+ if (key) {
729
+ this.warn(`Showing key "${key}" because no translation was found in namespace "${this.namespace}".`);
730
+ }
731
+ return key ?? '';
732
+ default: return forceMode;
733
+ }
734
+ }
735
+ /** Pass-through helpers that delegate resource management to the core service. */
736
+ addResourceBundle(...p) {
737
+ return this.core.addResourceBundle(...p);
738
+ }
739
+ addResources(...p) {
740
+ return this.core.addResources(...p);
741
+ }
742
+ addResource(...p) {
743
+ return this.core.addResource(...p);
744
+ }
745
+ hasResourceBundle(...p) {
746
+ return this.core.hasResourceBundle(...p);
747
+ }
748
+ getResource(...p) {
749
+ return this.core.getResource(...p);
750
+ }
751
+ getResourceBundle(...p) {
752
+ return this.core.getResourceBundle(...p);
753
+ }
754
+ getAllBundle() {
755
+ return this.core.getAllBundle();
756
+ }
757
+ removeResourceBundle(...p) {
758
+ return this.core.removeResourceBundle(...p);
759
+ }
760
+ preloadNamespaces(...p) {
761
+ return this.core.preloadNamespaces(...p);
762
+ }
763
+ /** Emits debug info when verbose logging is enabled. */
764
+ info(message, ...details) {
765
+ if (!this.debugEnabled)
766
+ return;
767
+ if (details.length) {
768
+ console.info(`[ngx-atomic-i18n] ${message}`, ...details);
769
+ }
770
+ else {
771
+ console.info(`[ngx-atomic-i18n] ${message}`);
772
+ }
773
+ }
774
+ /** Emits debug warnings when verbose logging is enabled. */
775
+ warn(message, ...details) {
776
+ if (!this.debugEnabled)
777
+ return;
778
+ if (details.length) {
779
+ console.warn(`[ngx-atomic-i18n] ${message}`, ...details);
780
+ }
781
+ else {
782
+ console.warn(`[ngx-atomic-i18n] ${message}`);
783
+ }
784
+ }
785
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationService, deps: [{ token: TRANSLATION_NAMESPACE }], target: i0.ɵɵFactoryTarget.Injectable });
786
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationService });
787
+ }
788
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationService, decorators: [{
789
+ type: Injectable
790
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
791
+ type: Inject,
792
+ args: [TRANSLATION_NAMESPACE]
793
+ }] }] });
794
+
795
+ /** Template helper that proxies lookups to `TranslationService.t`. */
796
+ class TranslationPipe {
797
+ service = inject(TranslationService);
798
+ /** Formats the translation identified by `key` using the optional params. */
799
+ transform(key, params) {
800
+ return this.service.t(key, params);
801
+ }
802
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
803
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: TranslationPipe, isStandalone: true, name: "t", pure: false });
804
+ }
805
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationPipe, decorators: [{
806
+ type: Pipe,
807
+ args: [{ name: 't', standalone: true, pure: false }]
808
+ }] });
809
+
810
+ /** Binds translation keys to an element, updating text or attributes reactively. */
811
+ class TranslationDirective {
812
+ selfElm = inject(ElementRef).nativeElement;
813
+ service = inject(TranslationService);
814
+ /** Translation key resolved for the host element. */
815
+ t = input('');
816
+ /** Optional interpolation parameters passed to the translation formatter. */
817
+ tParams = input(undefined);
818
+ /** Attribute name to receive the translated value instead of textContent. */
819
+ tAttr = input('');
820
+ constructor() {
821
+ effect(() => {
822
+ const value = this.service.t(this.t(), this.tParams());
823
+ if (this.tAttr()) {
824
+ this.selfElm.setAttribute(this.tAttr(), value);
825
+ }
826
+ else {
827
+ this.selfElm.textContent = value;
828
+ }
829
+ });
830
+ }
831
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
832
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.13", type: TranslationDirective, isStandalone: true, selector: "[t]", inputs: { t: { classPropertyName: "t", publicName: "t", isSignal: true, isRequired: false, transformFunction: null }, tParams: { classPropertyName: "tParams", publicName: "tParams", isSignal: true, isRequired: false, transformFunction: null }, tAttr: { classPropertyName: "tAttr", publicName: "tAttr", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
833
+ }
834
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslationDirective, decorators: [{
835
+ type: Directive,
836
+ args: [{
837
+ selector: '[t]',
838
+ standalone: true
839
+ }]
840
+ }], ctorParameters: () => [] });
841
+
842
+ var TempToken;
843
+ (function (TempToken) {
844
+ /** Placeholder for the language code inside loader path templates. */
845
+ TempToken["Lang"] = "{{lang}}";
846
+ /** Placeholder for the namespace inside loader path templates. */
847
+ TempToken["Namespace"] = "{{namespace}}";
848
+ /** Placeholder for the root folder inside loader path templates. */
849
+ TempToken["Root"] = "{{root}}";
850
+ })(TempToken || (TempToken = {}));
851
+
852
+ class HttpTranslationLoader {
853
+ http;
854
+ option;
855
+ pathTemplates;
856
+ pathTemplateCache;
857
+ constructor(http, option = {}, pathTemplates) {
858
+ this.http = http;
859
+ this.option = option;
860
+ this.pathTemplates = pathTemplates;
861
+ }
862
+ async load(i18nRoots, namespace, lang) {
863
+ const roots = tempToArray(i18nRoots) ?? [];
864
+ const baseUrl = this.option.baseUrl ?? '/assets';
865
+ const safeBaseUrl = (/^https?:\/\//i.test(baseUrl)
866
+ ? baseUrl
867
+ : (baseUrl.startsWith('/') ? baseUrl : '/' + baseUrl)).replace(/[\\/]+$/, '');
868
+ const tempArray = tempToArray(this.pathTemplates);
869
+ const pathTemps = this.pathTemplateCache ? [this.pathTemplateCache, ...(tempArray ?? []).filter(t => t !== this.pathTemplateCache)] : (tempArray ?? defaultConfig.pathTemplates);
870
+ for (const root of roots) {
871
+ for (const temp of pathTemps) {
872
+ let url = `${safeBaseUrl}/${temp.replace(TempToken.Root, root).replace(TempToken.Lang, lang).replace(TempToken.Namespace, namespace)}`;
873
+ const v = detectBuildVersion();
874
+ if (v) {
875
+ url += (url.includes('?') ? '&' : '?') + `v=${encodeURIComponent(v)}`;
876
+ }
877
+ try {
878
+ const json = await firstValueFrom(this.http.get(url));
879
+ if (!this.pathTemplateCache)
880
+ this.pathTemplateCache = temp;
881
+ return json;
882
+ }
883
+ catch {
884
+ /* ignore */
885
+ }
886
+ }
887
+ }
888
+ throw new Error(`[i18n] ${namespace}.json for ${lang} not found in any i18nRoot`);
889
+ }
890
+ }
891
+
892
+ /** File-system backed loader used during SSR to read translation JSON from disk. */
893
+ class FsTranslationLoader {
894
+ fsOptions;
895
+ pathTemplates;
896
+ customFs;
897
+ cache = new Map();
898
+ /**
899
+ * @param customFs Optional fs-like abstraction injected explicitly (tests or adapters).
900
+ */
901
+ constructor(fsOptions = {}, pathTemplates, customFs) {
902
+ this.fsOptions = fsOptions;
903
+ this.pathTemplates = pathTemplates;
904
+ this.customFs = customFs;
905
+ }
906
+ async load(i18nRoots, namespace, lang) {
907
+ const roots = (Array.isArray(i18nRoots) ? i18nRoots : [i18nRoots]).map(stripLeadingSep);
908
+ const pathMod = await this.importSafely('node:path');
909
+ const fsImported = await this.importSafely('node:fs');
910
+ const fsLike = this.pickFs(this.customFs) ?? this.pickFs(this.fsOptions.fsModule) ?? this.pickFs(fsImported);
911
+ const nodeProcess = globalThis.process;
912
+ const baseDir = this.fsOptions.baseDir ?? (nodeProcess?.cwd?.() ?? '/');
913
+ const assetPathRaw = this.fsOptions.assetPath ?? 'dist/browser/assets';
914
+ const assetPath = stripLeadingSep(assetPathRaw);
915
+ const templates = tempToArray(this.pathTemplates) ??
916
+ tempToArray(defaultConfig.pathTemplates);
917
+ for (const root of roots) {
918
+ const candidatePaths = this.fsOptions.resolvePaths?.({
919
+ baseDir,
920
+ assetPath,
921
+ root,
922
+ lang,
923
+ namespace,
924
+ }) ??
925
+ templates.map((temp) => this.safeJoin(pathMod, baseDir, assetPath, temp.replace(TempToken.Root, root).replace(TempToken.Lang, lang).replace(TempToken.Namespace, namespace)));
926
+ for (const absolutePath of candidatePaths) {
927
+ try {
928
+ if (!fsLike?.statSync || !fsLike?.readFileSync)
929
+ continue;
930
+ const stat = fsLike.statSync(absolutePath);
931
+ const mtimeMs = typeof stat.mtimeMs === 'number'
932
+ ? stat.mtimeMs
933
+ : stat.mtime?.getTime?.() ?? 0;
934
+ const size = typeof stat.size === 'number' ? stat.size : 0;
935
+ const sign = (mtimeMs | 0) * 1000003 + (size | 0);
936
+ const cached = this.cache.get(absolutePath);
937
+ if (cached && cached.mtimeMs === sign) {
938
+ return cached.data;
939
+ }
940
+ const raw = fsLike.readFileSync(absolutePath, 'utf8');
941
+ const json = JSON.parse(raw);
942
+ this.cache.set(absolutePath, { mtimeMs: sign, data: json });
943
+ return json;
944
+ }
945
+ catch {
946
+ // Continue probing other candidate files until a match is found.
947
+ }
948
+ }
949
+ }
950
+ throw new Error(`[SSR i18n] ${namespace}.json for ${lang} not found in any i18nRoot`);
951
+ }
952
+ /** Attempts to import a Node built-in without throwing when unavailable (e.g. CSR). */
953
+ async importSafely(specifier) {
954
+ const nodeProcess = globalThis.process;
955
+ const isNode = !!nodeProcess?.versions?.node;
956
+ if (!isNode)
957
+ return undefined;
958
+ try {
959
+ const importer = new Function('s', 'return import(s)');
960
+ return await importer(specifier);
961
+ }
962
+ catch {
963
+ return undefined;
964
+ }
965
+ }
966
+ pickFs(x) {
967
+ return x && typeof x.readFileSync === 'function' && typeof x.statSync === 'function'
968
+ ? x
969
+ : undefined;
970
+ }
971
+ safeJoin(pathMod, ...parts) {
972
+ return (pathMod?.join ??
973
+ pathMod?.default?.join ??
974
+ ((...parts) => parts.join('/')))(...parts);
975
+ }
976
+ }
977
+
978
+ const defaultConfig = {
979
+ supportedLangs: ['en'],
980
+ fallbackNamespace: 'common',
981
+ fallbackLang: '',
982
+ i18nRoots: ['i18n'],
983
+ pathTemplates: [`${TempToken.Root}/${TempToken.Namespace}/${TempToken.Lang}.json`],
984
+ enablePageFallback: false,
985
+ missingTranslationBehavior: 'show-key',
986
+ langDetectionOrder: ['url', 'clientRequest', 'localStorage', 'browser', 'customLang', 'fallback'],
987
+ clientRequestLang: null,
988
+ };
989
+ const CLIENT_REQUEST_LANG_STATE_KEY = makeStateKey('NGX_I18N_CLIENT_REQUEST_LANG');
990
+ function resolveClientRequestLang(platformId, transferState, providedLang) {
991
+ const stored = transferState?.get(CLIENT_REQUEST_LANG_STATE_KEY, null);
992
+ if (stored)
993
+ return stored;
994
+ const resolved = providedLang ?? null;
995
+ if (resolved && transferState && isPlatformServer(platformId)) {
996
+ transferState.set(CLIENT_REQUEST_LANG_STATE_KEY, resolved);
997
+ }
998
+ return resolved;
999
+ }
1000
+ /** Bootstraps the entire translation infrastructure for an application. */
1001
+ function provideTranslationInit(userConfig) {
1002
+ const debugEnabled = userConfig?.debug ?? isDevMode();
1003
+ const baseConfig = { ...defaultConfig, ...(userConfig ?? {}), fallbackLang: defaultConfig.supportedLangs[0], debug: debugEnabled };
1004
+ if (debugEnabled) {
1005
+ console.info('[ngx-atomic-i18n] Debug logging is enabled.');
1006
+ }
1007
+ return [
1008
+ {
1009
+ provide: TRANSLATION_CONFIG,
1010
+ useFactory: (platformId, transferState, clientRequestLang) => {
1011
+ const requestLang = resolveClientRequestLang(platformId, transferState, clientRequestLang ?? baseConfig.clientRequestLang ?? null);
1012
+ const finalConfig = { ...baseConfig, clientRequestLang: requestLang };
1013
+ const preferredLang = detectPreferredLang(finalConfig);
1014
+ return { ...finalConfig, customLang: preferredLang };
1015
+ },
1016
+ deps: [PLATFORM_ID, [new Optional(), TransferState], [new Optional(), CLIENT_REQUEST_LANG]],
1017
+ },
1018
+ {
1019
+ provide: BUILD_VERSION,
1020
+ useValue: userConfig?.buildVersion ?? null,
1021
+ },
1022
+ ...provideTranslationLoader(baseConfig),
1023
+ ...provideTranslation(baseConfig.fallbackNamespace),
1024
+ {
1025
+ provide: APP_INITIALIZER,
1026
+ useFactory: (ts) => {
1027
+ return async () => {
1028
+ const preload = baseConfig.preloadNamespaces;
1029
+ if (preload?.length) {
1030
+ await ts.preloadNamespaces(preload, ts.currentLang);
1031
+ }
1032
+ ts.setLang(ts.currentLang);
1033
+ };
1034
+ },
1035
+ deps: [TranslationService],
1036
+ multi: true,
1037
+ },
1038
+ ];
1039
+ }
1040
+ /** Provides the component-scoped namespace injection for component-registered service.
1041
+ * @param namespace The namespace owned by the component.
1042
+ * @param isPage Whether the component is a top-level page (defaults to false).
1043
+ */
1044
+ function provideTranslation(namespace, isPage = false) {
1045
+ return [
1046
+ {
1047
+ provide: TRANSLATION_NAMESPACE,
1048
+ useValue: namespace,
1049
+ },
1050
+ TranslationService,
1051
+ ...(isPage ? [{
1052
+ provide: PAGE_TRANSLATION_ROOT,
1053
+ useValue: true,
1054
+ }] : []),
1055
+ ];
1056
+ }
1057
+ /** Configures the runtime translation loader for CSR or SSR environments. */
1058
+ function provideTranslationLoader(config) {
1059
+ return [
1060
+ {
1061
+ provide: TRANSLATION_LOADER,
1062
+ useFactory: (platformId) => {
1063
+ const options = config.loader ?? {};
1064
+ const finalPathTemplates = config.pathTemplates ?? defaultConfig.pathTemplates;
1065
+ const isSSR = options.forceMode === 'ssr' || (options.forceMode !== 'csr' && isPlatformServer(platformId));
1066
+ if (isSSR) {
1067
+ const nodeProcess = globalThis.process;
1068
+ const baseDir = options.fsOptions?.baseDir ??
1069
+ (typeof nodeProcess?.cwd === 'function' ? nodeProcess.cwd() : '');
1070
+ const assetPath = options.fsOptions?.assetPath ?? (isDevMode() ? 'src/assets' : 'dist/browser/assets');
1071
+ return options.ssrLoader?.()
1072
+ ?? new FsTranslationLoader({
1073
+ baseDir,
1074
+ assetPath,
1075
+ resolvePaths: options.fsOptions?.resolvePaths,
1076
+ fsModule: options.fsOptions?.fsModule
1077
+ }, finalPathTemplates);
1078
+ }
1079
+ const http = inject(HttpClient);
1080
+ return options.csrLoader?.(http)
1081
+ ?? new HttpTranslationLoader(http, {
1082
+ baseUrl: options.httpOptions?.baseUrl ?? '/assets',
1083
+ }, finalPathTemplates);
1084
+ },
1085
+ deps: [PLATFORM_ID],
1086
+ }
1087
+ ];
1088
+ }
1089
+
1090
+ /*
1091
+ * Public API Surface of ngx-atomic-i18n
1092
+ */
1093
+
1094
+ /**
1095
+ * Generated bundle index. Do not edit.
1096
+ */
1097
+
1098
+ export { BUILD_VERSION, CLIENT_REQUEST_LANG, FIFOCache, FsTranslationLoader, HttpTranslationLoader, ICU_FORMATTER_TOKEN, PAGE_TRANSLATION_ROOT, TRANSLATION_CONFIG, TRANSLATION_LOADER, TRANSLATION_NAMESPACE, TempToken, TranslationCoreService, TranslationDirective, TranslationPipe, TranslationService, deepMerge, defaultConfig, detectBuildVersion, detectPreferredLang, filterNewKeysDeep, flattenTranslations, getNested, normalizeLangCode, parseICU, provideTranslation, provideTranslationInit, provideTranslationLoader, stripLeadingSep, tempToArray, toObservable };
1099
+ //# sourceMappingURL=ngx-atomic-i18n.mjs.map