@reforgium/presentia 2.0.0 → 2.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.
- package/CHANGELOG.md +19 -0
- package/fesm2022/reforgium-presentia.mjs +539 -493
- package/package.json +2 -2
- package/types/reforgium-presentia.d.ts +18 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [2.1.0]: 05.04.2026
|
|
2
|
+
### Refactor:
|
|
3
|
+
- replaced presentia-specific persistence adapter behavior with storage contracts and strategies backed by `@reforgium/internal`
|
|
4
|
+
- `LangService` and `ThemeService` now persist through storage interface bindings instead of direct localStorage-specific adapter semantics
|
|
5
|
+
|
|
6
|
+
### Feat:
|
|
7
|
+
- added grouped `storage` / `persistence` options for `providePresentia(...)` theme and lang config
|
|
8
|
+
- kept `persistenceAdapter` as a compatibility alias during the transition to storage-based persistence
|
|
9
|
+
|
|
10
|
+
### Fix:
|
|
11
|
+
- aligned `persistence: 'none'` with a real no-op storage instead of pseudo-persistent in-memory behavior
|
|
12
|
+
- preserved raw `lang` / `theme` browser storage keys while moving persistence onto shared storage contracts
|
|
13
|
+
|
|
14
|
+
### Test:
|
|
15
|
+
- updated provider and theme tests for the storage-based persistence contract
|
|
16
|
+
- verified `presentia` typecheck, Vitest suite, and Angular package build after the persistence migration
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
1
20
|
## [2.0.0]: 04.04.2026
|
|
2
21
|
### Feat:
|
|
3
22
|
- added grouped `providePresentia(...)` v2 entry point and marked `provideReInit(...)` as legacy in dev mode
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, signal, computed, inject, DestroyRef, PLATFORM_ID, afterRenderEffect, Injectable, TemplateRef, ViewContainerRef, effect, Input, Directive, input, ElementRef, Renderer2, Injector, afterNextRender, runInInjectionContext, Pipe,
|
|
3
|
-
import {
|
|
2
|
+
import { InjectionToken, signal, computed, inject, DestroyRef, PLATFORM_ID, afterRenderEffect, Injectable, TemplateRef, ViewContainerRef, effect, Input, Directive, makeEnvironmentProviders, provideAppInitializer, input, ElementRef, Renderer2, Injector, afterNextRender, runInInjectionContext, Pipe, LOCALE_ID } from '@angular/core';
|
|
3
|
+
import { LruCache, MemoryStorage, deepEqual, compareRoutes, getChainedValue, TRANSLATION, SELECTED_LANG, CHANGE_LANG, SELECTED_THEME, CHANGE_THEME, CURRENT_DEVICE } from '@reforgium/internal';
|
|
4
4
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
|
5
5
|
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
6
6
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
@@ -280,6 +280,61 @@ const innerLangVal = Symbol('reInnerLangVal');
|
|
|
280
280
|
|
|
281
281
|
const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER');
|
|
282
282
|
|
|
283
|
+
function createPresentiaStorage(strategy, storage) {
|
|
284
|
+
if (storage) {
|
|
285
|
+
return storage;
|
|
286
|
+
}
|
|
287
|
+
switch (strategy ?? 'persist') {
|
|
288
|
+
case 'none':
|
|
289
|
+
return createNoopStorage();
|
|
290
|
+
case 'memory':
|
|
291
|
+
return new MemoryStorage();
|
|
292
|
+
case 'session':
|
|
293
|
+
return createBrowserStringStorage('session');
|
|
294
|
+
case 'lru':
|
|
295
|
+
return new LruCache(1);
|
|
296
|
+
case 'persist':
|
|
297
|
+
default:
|
|
298
|
+
return createBrowserStringStorage('local');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function createBrowserStringStorage(kind) {
|
|
302
|
+
const getStorage = () => {
|
|
303
|
+
if (typeof globalThis === 'undefined') {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return kind === 'local' ? (globalThis.localStorage ?? null) : (globalThis.sessionStorage ?? null);
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
get length() {
|
|
310
|
+
return getStorage()?.length ?? 0;
|
|
311
|
+
},
|
|
312
|
+
get(key) {
|
|
313
|
+
return getStorage()?.getItem(key) ?? null;
|
|
314
|
+
},
|
|
315
|
+
set(key, value) {
|
|
316
|
+
getStorage()?.setItem(key, value);
|
|
317
|
+
},
|
|
318
|
+
remove(key) {
|
|
319
|
+
getStorage()?.removeItem(key);
|
|
320
|
+
},
|
|
321
|
+
clear() {
|
|
322
|
+
getStorage()?.clear();
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function createNoopStorage() {
|
|
327
|
+
return {
|
|
328
|
+
get length() {
|
|
329
|
+
return 0;
|
|
330
|
+
},
|
|
331
|
+
get: () => null,
|
|
332
|
+
set: () => undefined,
|
|
333
|
+
remove: () => undefined,
|
|
334
|
+
clear: () => undefined,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
283
338
|
/**
|
|
284
339
|
* Optional DI token for a custom persistence adapter used by `LangService`
|
|
285
340
|
* to store and retrieve the selected language (default key: `'lang'`).
|
|
@@ -291,7 +346,10 @@ const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER
|
|
|
291
346
|
* { provide: LANG_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
|
|
292
347
|
* ```
|
|
293
348
|
*/
|
|
294
|
-
const
|
|
349
|
+
const defaultLangPersistenceAdapter = createPresentiaStorage('persist');
|
|
350
|
+
const LANG_PERSISTENCE_ADAPTER = new InjectionToken('RE_LANG_PERSISTENCE_ADAPTER', {
|
|
351
|
+
factory: () => defaultLangPersistenceAdapter,
|
|
352
|
+
});
|
|
295
353
|
|
|
296
354
|
/**
|
|
297
355
|
* Injection token for providing locale configuration to the language module.
|
|
@@ -313,6 +371,160 @@ const LANG_PERSISTENCE_ADAPTER = new InjectionToken('RE_LANG_PERSISTENCE_ADAPTER
|
|
|
313
371
|
*/
|
|
314
372
|
const LANG_CONFIG = new InjectionToken('RE_LANG_CONFIG');
|
|
315
373
|
|
|
374
|
+
function deepestActivatedRoute(route) {
|
|
375
|
+
let current = route;
|
|
376
|
+
while (current.firstChild) {
|
|
377
|
+
current = current.firstChild;
|
|
378
|
+
}
|
|
379
|
+
return current;
|
|
380
|
+
}
|
|
381
|
+
function deepestRouteSnapshot(snapshot) {
|
|
382
|
+
let current = snapshot;
|
|
383
|
+
while (current.firstChild) {
|
|
384
|
+
current = current.firstChild;
|
|
385
|
+
}
|
|
386
|
+
return current;
|
|
387
|
+
}
|
|
388
|
+
function joinUrl(segments) {
|
|
389
|
+
return segments.length ? segments.map((segment) => segment.path).join('/') : '';
|
|
390
|
+
}
|
|
391
|
+
function snapshotFullPath(snapshot) {
|
|
392
|
+
return snapshot.pathFromRoot
|
|
393
|
+
.map((route) => joinUrl(route.url))
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.join('/');
|
|
396
|
+
}
|
|
397
|
+
function snapshotRoutePattern(snapshot) {
|
|
398
|
+
return snapshot.pathFromRoot
|
|
399
|
+
.map((route) => route.routeConfig?.path ?? '')
|
|
400
|
+
.filter(Boolean)
|
|
401
|
+
.join('/');
|
|
402
|
+
}
|
|
403
|
+
function snapshotMergedParams(snapshot) {
|
|
404
|
+
return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
|
|
405
|
+
}
|
|
406
|
+
function snapshotDeepestParams(snapshot) {
|
|
407
|
+
const mergedParams = snapshotMergedParams(snapshot);
|
|
408
|
+
const keys = extractRouteParamKeys(snapshot.routeConfig?.path ?? '');
|
|
409
|
+
if (!keys.length) {
|
|
410
|
+
return {};
|
|
411
|
+
}
|
|
412
|
+
return keys.reduce((acc, key) => {
|
|
413
|
+
const value = mergedParams[key];
|
|
414
|
+
if (value !== undefined) {
|
|
415
|
+
acc[key] = value;
|
|
416
|
+
}
|
|
417
|
+
return acc;
|
|
418
|
+
}, {});
|
|
419
|
+
}
|
|
420
|
+
function snapshotMergedData(snapshot) {
|
|
421
|
+
return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.data }), {});
|
|
422
|
+
}
|
|
423
|
+
function extractRouteParamKeys(path) {
|
|
424
|
+
return path
|
|
425
|
+
.split('/')
|
|
426
|
+
.filter((segment) => segment.startsWith(':'))
|
|
427
|
+
.map((segment) => segment.slice(1))
|
|
428
|
+
.filter(Boolean);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Reactive snapshot of the current route (the deepest active route).
|
|
433
|
+
* Updates on every `NavigationEnd` event.
|
|
434
|
+
*/
|
|
435
|
+
class RouteWatcher {
|
|
436
|
+
router = inject(Router);
|
|
437
|
+
destroyRef = inject(DestroyRef);
|
|
438
|
+
#params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : /* istanbul ignore next */ []));
|
|
439
|
+
#deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : /* istanbul ignore next */ []));
|
|
440
|
+
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
|
|
441
|
+
#data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
|
|
442
|
+
#mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : /* istanbul ignore next */ []));
|
|
443
|
+
#url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : /* istanbul ignore next */ []));
|
|
444
|
+
#routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : /* istanbul ignore next */ []));
|
|
445
|
+
#fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : /* istanbul ignore next */ []));
|
|
446
|
+
/** Params merged from root to deepest route. */
|
|
447
|
+
params = this.#params.asReadonly();
|
|
448
|
+
/** Params declared on the deepest route only. */
|
|
449
|
+
deepestParams = this.#deepestParams.asReadonly();
|
|
450
|
+
/** Query params from the current navigation. */
|
|
451
|
+
query = this.#query.asReadonly();
|
|
452
|
+
/** Deepest route data only. */
|
|
453
|
+
data = this.#data.asReadonly();
|
|
454
|
+
/** Route data merged from root to deepest route. */
|
|
455
|
+
mergedData = this.#mergedData.asReadonly();
|
|
456
|
+
/** Full current url path assembled from root to deepest route. */
|
|
457
|
+
url = this.#url.asReadonly();
|
|
458
|
+
/** Current route config pattern, e.g. `orgs/:orgId/users/:id`. */
|
|
459
|
+
routePattern = this.#routePattern.asReadonly();
|
|
460
|
+
/** Current url fragment without `#`. */
|
|
461
|
+
fragment = this.#fragment.asReadonly();
|
|
462
|
+
/** Combined computed snapshot. */
|
|
463
|
+
state = computed(() => ({
|
|
464
|
+
params: this.#params(),
|
|
465
|
+
deepestParams: this.#deepestParams(),
|
|
466
|
+
query: this.#query(),
|
|
467
|
+
data: this.#data(),
|
|
468
|
+
mergedData: this.#mergedData(),
|
|
469
|
+
url: this.#url(),
|
|
470
|
+
routePattern: this.#routePattern(),
|
|
471
|
+
fragment: this.#fragment(),
|
|
472
|
+
}), ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
473
|
+
constructor() {
|
|
474
|
+
const read = () => {
|
|
475
|
+
const snapshot = this.deepestSnapshot();
|
|
476
|
+
const nextUrl = snapshotFullPath(snapshot);
|
|
477
|
+
const nextRoutePattern = snapshotRoutePattern(snapshot);
|
|
478
|
+
const nextParams = snapshotMergedParams(snapshot);
|
|
479
|
+
const nextDeepestParams = snapshotDeepestParams(snapshot);
|
|
480
|
+
const nextMergedData = snapshotMergedData(snapshot);
|
|
481
|
+
const nextQuery = snapshot.queryParams;
|
|
482
|
+
const nextData = snapshot.data;
|
|
483
|
+
const nextFragment = snapshot.fragment ?? null;
|
|
484
|
+
!deepEqual(nextParams, this.#params()) && this.#params.set(nextParams);
|
|
485
|
+
!deepEqual(nextDeepestParams, this.#deepestParams()) && this.#deepestParams.set(nextDeepestParams);
|
|
486
|
+
!deepEqual(nextQuery, this.#query()) && this.#query.set(nextQuery);
|
|
487
|
+
!deepEqual(nextData, this.#data()) && this.#data.set(nextData);
|
|
488
|
+
!deepEqual(nextMergedData, this.#mergedData()) && this.#mergedData.set(nextMergedData);
|
|
489
|
+
this.#url() !== nextUrl && this.#url.set(nextUrl);
|
|
490
|
+
this.#routePattern() !== nextRoutePattern && this.#routePattern.set(nextRoutePattern);
|
|
491
|
+
this.#fragment() !== nextFragment && this.#fragment.set(nextFragment);
|
|
492
|
+
};
|
|
493
|
+
read();
|
|
494
|
+
const subscription = this.router.events.subscribe((event) => {
|
|
495
|
+
if (event instanceof NavigationEnd) {
|
|
496
|
+
read();
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
500
|
+
}
|
|
501
|
+
selectData(key, strategy = 'deepest') {
|
|
502
|
+
return computed(() => {
|
|
503
|
+
const source = strategy === 'merged' ? this.#mergedData() : this.#data();
|
|
504
|
+
return source[key];
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
selectParam(key, strategy = 'merged') {
|
|
508
|
+
return computed(() => {
|
|
509
|
+
const source = strategy === 'deepest' ? this.#deepestParams() : this.#params();
|
|
510
|
+
return source[key];
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
matchesPath(path) {
|
|
514
|
+
const current = this.#url();
|
|
515
|
+
return typeof path === 'string' ? current === path : path.test(current);
|
|
516
|
+
}
|
|
517
|
+
deepestSnapshot() {
|
|
518
|
+
return deepestRouteSnapshot(this.router.routerState.snapshot.root);
|
|
519
|
+
}
|
|
520
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
521
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
|
|
522
|
+
}
|
|
523
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, decorators: [{
|
|
524
|
+
type: Injectable,
|
|
525
|
+
args: [{ providedIn: 'root' }]
|
|
526
|
+
}], ctorParameters: () => [] });
|
|
527
|
+
|
|
316
528
|
/**
|
|
317
529
|
* @deprecated Diagnostics are usually enabled through route preload config, not by consuming this service directly.
|
|
318
530
|
*/
|
|
@@ -374,72 +586,193 @@ function normalizeUrlPath$1(url) {
|
|
|
374
586
|
return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
|
|
375
587
|
}
|
|
376
588
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
589
|
+
const PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY = '__rePresentiaRouteNamespacePreload';
|
|
590
|
+
const PRESENTIA_ROUTE_NAMESPACES_DATA_KEY = 'presentiaNamespaces';
|
|
591
|
+
function providePresentiaRouteNamespacePreload() {
|
|
592
|
+
return makeEnvironmentProviders([
|
|
593
|
+
provideAppInitializer(() => {
|
|
594
|
+
const router = inject(Router, { optional: true });
|
|
595
|
+
const langConfig = inject(LANG_CONFIG);
|
|
596
|
+
const normalized = normalizeRouteNamespacePreloadConfig(langConfig.routeNamespacePreload);
|
|
597
|
+
if (!router || !normalized) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (typeof ngDevMode !== 'undefined' && normalized.manifest && Object.keys(normalized.manifest).length === 0) {
|
|
601
|
+
// eslint-disable-next-line no-console
|
|
602
|
+
console.warn('[presentia] routeNamespacePreload.manifest is empty. Route-data preload still works, but manifest mode is effectively disabled.');
|
|
603
|
+
}
|
|
604
|
+
router.resetConfig(patchRoutesWithNamespacePreload(router.config, normalized));
|
|
605
|
+
}),
|
|
394
606
|
]);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
#pendingBatchLoads = new Map();
|
|
400
|
-
#namespaceLoadedAt = new Map();
|
|
401
|
-
#missingKeyFallbacks = new Map();
|
|
402
|
-
/**
|
|
403
|
-
* Computed property determining the current language setting.
|
|
404
|
-
*
|
|
405
|
-
* - If private method `#lang` returns 'kg', `config.kgValue` is checked:
|
|
406
|
-
* - If `config.kgValue` is defined, the property will return its value.
|
|
407
|
-
* - If `config.kgValue` is not defined, the property will return the default value 'kg'.
|
|
408
|
-
* - For other languages (e.g. `ru`, `en`) returns source language as-is.
|
|
409
|
-
*/
|
|
410
|
-
currentLang = computed(() => {
|
|
411
|
-
const lang = this.#lang();
|
|
412
|
-
return lang === 'kg' ? (this.config?.kgValue ?? 'kg') : lang;
|
|
413
|
-
}, ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
|
|
414
|
-
/**
|
|
415
|
-
* Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
|
|
416
|
-
* Expected that property `#lang` has `asReadonly` method that returns immutable representation.
|
|
417
|
-
*/
|
|
418
|
-
[innerLangVal] = this.#lang.asReadonly();
|
|
419
|
-
constructor() {
|
|
420
|
-
const preload = this.config.preloadNamespaces ?? [];
|
|
421
|
-
if (preload.length) {
|
|
422
|
-
queueMicrotask(() => {
|
|
423
|
-
void this.loadNamespaces(preload);
|
|
424
|
-
});
|
|
425
|
-
}
|
|
607
|
+
}
|
|
608
|
+
function normalizeRouteNamespacePreloadConfig(config) {
|
|
609
|
+
if (!config) {
|
|
610
|
+
return null;
|
|
426
611
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
612
|
+
return {
|
|
613
|
+
mode: config.mode ?? 'blocking',
|
|
614
|
+
dataKey: config.dataKey?.trim() || PRESENTIA_ROUTE_NAMESPACES_DATA_KEY,
|
|
615
|
+
manifest: config.manifest,
|
|
616
|
+
mergeStrategy: config.mergeStrategy ?? 'append',
|
|
617
|
+
onError: config.onError ?? 'continue',
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function patchRoutesWithNamespacePreload(routes, config) {
|
|
621
|
+
return routes.map((route) => patchRouteWithNamespacePreload(route, config));
|
|
622
|
+
}
|
|
623
|
+
function patchRouteWithNamespacePreload(route, config) {
|
|
624
|
+
const nextChildren = route.children ? patchRoutesWithNamespacePreload(route.children, config) : route.children;
|
|
625
|
+
if (route.redirectTo) {
|
|
626
|
+
return nextChildren === route.children ? route : { ...route, children: nextChildren };
|
|
627
|
+
}
|
|
628
|
+
const nextResolve = {
|
|
629
|
+
...(route.resolve ?? {}),
|
|
630
|
+
[PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY]: makeRouteNamespacePreloadResolver(config),
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
...route,
|
|
634
|
+
...(nextChildren ? { children: nextChildren } : {}),
|
|
635
|
+
resolve: nextResolve,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function makeRouteNamespacePreloadResolver(config) {
|
|
639
|
+
return async (route, state) => {
|
|
640
|
+
const lang = inject(LangService);
|
|
641
|
+
const diagnostics = inject(RouteNamespaceDiagnosticsService);
|
|
642
|
+
const namespaces = resolveRouteNamespaces(route, state, config);
|
|
643
|
+
diagnostics.registerRouteNamespaces(state.url, namespaces);
|
|
644
|
+
if (!namespaces.length) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
if (config.mode === 'lazy') {
|
|
648
|
+
queueMicrotask(() => {
|
|
649
|
+
void lang.loadNamespaces(namespaces);
|
|
650
|
+
});
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
await lang.loadNamespaces(namespaces);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
if (config.onError === 'throw') {
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
// Keep navigation alive; runtime lazy loading remains a fallback.
|
|
661
|
+
}
|
|
662
|
+
return true;
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function resolveRouteNamespaces(route, state, config) {
|
|
666
|
+
const dataNamespaces = readNamespacesFromRouteData(route, config.dataKey);
|
|
667
|
+
const manifestNamespaces = readNamespacesFromManifest(route, state, config.manifest);
|
|
668
|
+
if (config.mergeStrategy === 'replace' && dataNamespaces.length) {
|
|
669
|
+
return dataNamespaces;
|
|
670
|
+
}
|
|
671
|
+
return uniqueNamespaces([...manifestNamespaces, ...dataNamespaces]);
|
|
672
|
+
}
|
|
673
|
+
function readNamespacesFromRouteData(route, dataKey) {
|
|
674
|
+
return uniqueNamespaces(route.pathFromRoot.flatMap((snapshot) => {
|
|
675
|
+
const value = snapshot.data?.[dataKey];
|
|
676
|
+
return Array.isArray(value) ? value : [];
|
|
677
|
+
}));
|
|
678
|
+
}
|
|
679
|
+
function readNamespacesFromManifest(route, state, manifest) {
|
|
680
|
+
if (!manifest) {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
const actualUrl = normalizeUrlPath(state.url);
|
|
684
|
+
const routePath = snapshotRouteConfigPath(route);
|
|
685
|
+
return uniqueNamespaces(Object.entries(manifest)
|
|
686
|
+
.filter(([key]) => matchesManifestKey(actualUrl, routePath, key))
|
|
687
|
+
.flatMap(([, namespaces]) => namespaces));
|
|
688
|
+
}
|
|
689
|
+
function matchesManifestKey(actualUrl, routePath, manifestKey) {
|
|
690
|
+
const normalizedKey = normalizeUrlPath(manifestKey);
|
|
691
|
+
return compareRoutes(actualUrl, normalizedKey) || (!!routePath && routePath === normalizedKey);
|
|
692
|
+
}
|
|
693
|
+
function snapshotRouteConfigPath(route) {
|
|
694
|
+
const templatePath = route.pathFromRoot
|
|
695
|
+
.map((item) => item.routeConfig?.path ?? '')
|
|
696
|
+
.filter(Boolean)
|
|
697
|
+
.join('/');
|
|
698
|
+
return normalizeUrlPath(templatePath);
|
|
699
|
+
}
|
|
700
|
+
function normalizeUrlPath(url) {
|
|
701
|
+
const [path] = url.split(/[?#]/, 1);
|
|
702
|
+
const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
|
|
703
|
+
return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
|
|
704
|
+
}
|
|
705
|
+
function uniqueNamespaces(namespaces) {
|
|
706
|
+
return Array.from(new Set(namespaces.map((ns) => ns.trim()).filter(Boolean)));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* LangService provides functionality for managing and tracking language settings
|
|
711
|
+
* and translations in the application. It is designed to handle localization needs,
|
|
712
|
+
* loading language resources, caching translations, and dynamically applying translations
|
|
713
|
+
* throughout the application.
|
|
714
|
+
*/
|
|
715
|
+
class LangService {
|
|
716
|
+
static BUILTIN_LANGS = ['ru', 'kg', 'en'];
|
|
717
|
+
static LANG_CODE_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})?$/;
|
|
718
|
+
config = inject(LANG_CONFIG);
|
|
719
|
+
http = inject(HttpClient);
|
|
720
|
+
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
721
|
+
persistence = inject(LANG_PERSISTENCE_ADAPTER);
|
|
722
|
+
missingKeyHandler = inject(LANG_MISSING_KEY_HANDLER, { optional: true });
|
|
723
|
+
routeNamespaceDiagnostics = inject(RouteNamespaceDiagnosticsService, { optional: true });
|
|
724
|
+
supportedLangSet = new Set([
|
|
725
|
+
...LangService.BUILTIN_LANGS,
|
|
726
|
+
...this.normalizeSupportedLangs(this.config.supportedLangs ?? []),
|
|
727
|
+
]);
|
|
728
|
+
#lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : /* istanbul ignore next */ []));
|
|
729
|
+
#cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : /* istanbul ignore next */ []));
|
|
730
|
+
#loadedNamespaces = new Set();
|
|
731
|
+
#pendingLoads = new Map();
|
|
732
|
+
#pendingBatchLoads = new Map();
|
|
733
|
+
#namespaceLoadedAt = new Map();
|
|
734
|
+
#missingKeyFallbacks = new Map();
|
|
735
|
+
/**
|
|
736
|
+
* Computed property determining the current language setting.
|
|
737
|
+
*
|
|
738
|
+
* - If private method `#lang` returns 'kg', `config.kgValue` is checked:
|
|
739
|
+
* - If `config.kgValue` is defined, the property will return its value.
|
|
740
|
+
* - If `config.kgValue` is not defined, the property will return the default value 'kg'.
|
|
741
|
+
* - For other languages (e.g. `ru`, `en`) returns source language as-is.
|
|
742
|
+
*/
|
|
743
|
+
currentLang = computed(() => {
|
|
744
|
+
const lang = this.#lang();
|
|
745
|
+
return lang === 'kg' ? (this.config?.kgValue ?? 'kg') : lang;
|
|
746
|
+
}, ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
|
|
747
|
+
/**
|
|
748
|
+
* Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
|
|
749
|
+
* Expected that property `#lang` has `asReadonly` method that returns immutable representation.
|
|
750
|
+
*/
|
|
751
|
+
[innerLangVal] = this.#lang.asReadonly();
|
|
752
|
+
constructor() {
|
|
753
|
+
const preload = this.config.preloadNamespaces ?? [];
|
|
754
|
+
if (preload.length) {
|
|
755
|
+
queueMicrotask(() => {
|
|
756
|
+
void this.loadNamespaces(preload);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Sets the current language for the application.
|
|
762
|
+
*
|
|
763
|
+
* @param {Langs | 'ky'} lang - The language to set.
|
|
764
|
+
* Accepts predefined type `Langs` or 'ky' to set Kyrgyz language.
|
|
765
|
+
* @return {void} Returns no value.
|
|
766
|
+
*/
|
|
767
|
+
setLang(lang) {
|
|
768
|
+
const langVal = this.normalizeLang(lang);
|
|
769
|
+
if (!langVal || !this.supportedLangSet.has(langVal)) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (langVal !== this.#lang()) {
|
|
773
|
+
this.#lang.set(langVal);
|
|
441
774
|
if (this.isBrowser) {
|
|
442
|
-
|
|
775
|
+
this.persistence.set('lang', langVal);
|
|
443
776
|
}
|
|
444
777
|
const namespaces = Array.from(this.#loadedNamespaces.values()).map((key) => this.namespaceFromKey(key));
|
|
445
778
|
this.#loadedNamespaces.clear();
|
|
@@ -660,7 +993,7 @@ class LangService {
|
|
|
660
993
|
if (!this.isBrowser) {
|
|
661
994
|
return defaultLang;
|
|
662
995
|
}
|
|
663
|
-
return normalize(
|
|
996
|
+
return normalize(this.persistence.get('lang')) ?? defaultLang;
|
|
664
997
|
}
|
|
665
998
|
makeUrl(ns, lang) {
|
|
666
999
|
if (this.config.requestBuilder) {
|
|
@@ -1034,425 +1367,151 @@ class LangDirective {
|
|
|
1034
1367
|
mode = raw.mode;
|
|
1035
1368
|
textKey = raw.textKey;
|
|
1036
1369
|
attrs = raw.attrs;
|
|
1037
|
-
}
|
|
1038
|
-
return {
|
|
1039
|
-
mode,
|
|
1040
|
-
textKey: explicitText ?? textKey,
|
|
1041
|
-
attrs: explicitAttrs ?? attrs,
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
isMode(value) {
|
|
1045
|
-
return (value === 'only-content' ||
|
|
1046
|
-
value === 'only-placeholder' ||
|
|
1047
|
-
value === 'only-label' ||
|
|
1048
|
-
value === 'only-title' ||
|
|
1049
|
-
value === 'all');
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Asynchronously loads namespace and retrieves localized value by key.
|
|
1053
|
-
*
|
|
1054
|
-
* @param key localization key in format `namespace.path.to.key`
|
|
1055
|
-
* @returns localized string
|
|
1056
|
-
*/
|
|
1057
|
-
async getLangValue(key) {
|
|
1058
|
-
const [ns] = key.split('.', 1);
|
|
1059
|
-
if (!this.service.isNamespaceLoaded(ns)) {
|
|
1060
|
-
await this.service.loadNamespace(ns);
|
|
1061
|
-
}
|
|
1062
|
-
return this.service.get(key);
|
|
1063
|
-
}
|
|
1064
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1065
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.4", type: LangDirective, isStandalone: true, selector: "[reLang]", inputs: { lang: { classPropertyName: "lang", publicName: "reLang", isSignal: true, isRequired: false, transformFunction: null }, langForAttr: { classPropertyName: "langForAttr", publicName: "langForAttr", isSignal: true, isRequired: false, transformFunction: null }, reLangKey: { classPropertyName: "reLangKey", publicName: "reLangKey", isSignal: false, isRequired: false, transformFunction: null }, reLangAttrs: { classPropertyName: "reLangAttrs", publicName: "reLangAttrs", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1066
|
-
}
|
|
1067
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, decorators: [{
|
|
1068
|
-
type: Directive,
|
|
1069
|
-
args: [{ selector: '[reLang]', standalone: true }]
|
|
1070
|
-
}], ctorParameters: () => [], propDecorators: { lang: [{ type: i0.Input, args: [{ isSignal: true, alias: "reLang", required: false }] }], langForAttr: [{ type: i0.Input, args: [{ isSignal: true, alias: "langForAttr", required: false }] }], reLangKey: [{
|
|
1071
|
-
type: Input
|
|
1072
|
-
}], reLangAttrs: [{
|
|
1073
|
-
type: Input
|
|
1074
|
-
}] } });
|
|
1075
|
-
|
|
1076
|
-
const LANG_PIPE_CONFIG = new InjectionToken('RE_LANG_PIPE_CONFIG');
|
|
1077
|
-
|
|
1078
|
-
/**
|
|
1079
|
-
* Custom Angular pipe that transforms a language key and additional parameters into a localized string.
|
|
1080
|
-
*
|
|
1081
|
-
* The pipe is declared as standalone and impure — meaning it can be used without importing a module
|
|
1082
|
-
* and will be recalculated when the state changes (for example, when the language is switched).
|
|
1083
|
-
*
|
|
1084
|
-
* In its operation, the pipe observes and caches language keys to improve performance,
|
|
1085
|
-
* using LangService to retrieve translated strings based on the current application language.
|
|
1086
|
-
*
|
|
1087
|
-
* The transformation involves accepting a key string and optional parameters,
|
|
1088
|
-
* forming a cache key, and returning the localized value corresponding to that request.
|
|
1089
|
-
*
|
|
1090
|
-
* @implements {PipeTransform}
|
|
1091
|
-
*/
|
|
1092
|
-
class LangPipe {
|
|
1093
|
-
cache = new Map();
|
|
1094
|
-
warnedUnresolved = new Set();
|
|
1095
|
-
lang = inject(LangService);
|
|
1096
|
-
injector = inject(Injector);
|
|
1097
|
-
config;
|
|
1098
|
-
ttlMs;
|
|
1099
|
-
maxCacheSize;
|
|
1100
|
-
constructor() {
|
|
1101
|
-
const resolved = inject(LANG_PIPE_CONFIG, { optional: true }) ?? {};
|
|
1102
|
-
this.config = resolved;
|
|
1103
|
-
this.ttlMs = resolved.ttlMs ?? 5 * 60 * 1000;
|
|
1104
|
-
this.maxCacheSize = resolved.maxCacheSize ?? 500;
|
|
1105
|
-
runInInjectionContext(this.injector, () => {
|
|
1106
|
-
effect(() => {
|
|
1107
|
-
// Clear cache on language change to avoid stale signals and unbounded growth.
|
|
1108
|
-
this.lang.currentLang();
|
|
1109
|
-
this.cache.clear();
|
|
1110
|
-
});
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
transform(query, params) {
|
|
1114
|
-
if (!query) {
|
|
1115
|
-
return '';
|
|
1116
|
-
}
|
|
1117
|
-
const key = this.makeKey(query, params);
|
|
1118
|
-
const now = Date.now();
|
|
1119
|
-
const existing = this.cache.get(key);
|
|
1120
|
-
if (existing) {
|
|
1121
|
-
if (now - existing.ts < this.ttlMs) {
|
|
1122
|
-
return existing.value();
|
|
1123
|
-
}
|
|
1124
|
-
this.cache.delete(key);
|
|
1125
|
-
}
|
|
1126
|
-
if (!this.cache.has(key)) {
|
|
1127
|
-
this.cache.set(key, { value: this.lang.observe(query, params ?? undefined), ts: now });
|
|
1128
|
-
this.evictIfNeeded();
|
|
1129
|
-
}
|
|
1130
|
-
const value = this.cache.get(key).value();
|
|
1131
|
-
const ns = query.split('.', 1)[0];
|
|
1132
|
-
if (ns && !this.lang.isNamespaceLoaded(ns)) {
|
|
1133
|
-
const placeholder = this.config.placeholder;
|
|
1134
|
-
if (typeof placeholder === 'function') {
|
|
1135
|
-
return placeholder(query);
|
|
1136
|
-
}
|
|
1137
|
-
if (typeof placeholder === 'string') {
|
|
1138
|
-
return placeholder;
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
if (ns && this.lang.isNamespaceLoaded(ns) && !this.lang.has(query)) {
|
|
1142
|
-
this.warnUnresolvedKey(query);
|
|
1143
|
-
}
|
|
1144
|
-
return value;
|
|
1145
|
-
}
|
|
1146
|
-
makeKey(query, params) {
|
|
1147
|
-
if (!params) {
|
|
1148
|
-
return query;
|
|
1149
|
-
}
|
|
1150
|
-
const entries = Object.keys(params)
|
|
1151
|
-
.sort()
|
|
1152
|
-
.map((k) => `${k}:${params[k]}`);
|
|
1153
|
-
return `${query}::${entries.join('|')}`;
|
|
1154
|
-
}
|
|
1155
|
-
evictIfNeeded() {
|
|
1156
|
-
while (this.cache.size > this.maxCacheSize) {
|
|
1157
|
-
const firstKey = this.cache.keys().next().value;
|
|
1158
|
-
if (!firstKey) {
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
this.cache.delete(firstKey);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
warnUnresolvedKey(query) {
|
|
1165
|
-
if (typeof ngDevMode === 'undefined') {
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
if (this.warnedUnresolved.has(query)) {
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
this.warnedUnresolved.add(query);
|
|
1172
|
-
// eslint-disable-next-line no-console
|
|
1173
|
-
console.warn(`LangPipe: namespace loaded but key "${query}" is unresolved`);
|
|
1174
|
-
}
|
|
1175
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1176
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
|
|
1177
|
-
}
|
|
1178
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, decorators: [{
|
|
1179
|
-
type: Pipe,
|
|
1180
|
-
args: [{ name: 'lang', standalone: true, pure: false }]
|
|
1181
|
-
}], ctorParameters: () => [] });
|
|
1182
|
-
|
|
1183
|
-
function deepestActivatedRoute(route) {
|
|
1184
|
-
let current = route;
|
|
1185
|
-
while (current.firstChild) {
|
|
1186
|
-
current = current.firstChild;
|
|
1187
|
-
}
|
|
1188
|
-
return current;
|
|
1189
|
-
}
|
|
1190
|
-
function deepestRouteSnapshot(snapshot) {
|
|
1191
|
-
let current = snapshot;
|
|
1192
|
-
while (current.firstChild) {
|
|
1193
|
-
current = current.firstChild;
|
|
1194
|
-
}
|
|
1195
|
-
return current;
|
|
1196
|
-
}
|
|
1197
|
-
function joinUrl(segments) {
|
|
1198
|
-
return segments.length ? segments.map((segment) => segment.path).join('/') : '';
|
|
1199
|
-
}
|
|
1200
|
-
function snapshotFullPath(snapshot) {
|
|
1201
|
-
return snapshot.pathFromRoot
|
|
1202
|
-
.map((route) => joinUrl(route.url))
|
|
1203
|
-
.filter(Boolean)
|
|
1204
|
-
.join('/');
|
|
1205
|
-
}
|
|
1206
|
-
function snapshotRoutePattern(snapshot) {
|
|
1207
|
-
return snapshot.pathFromRoot
|
|
1208
|
-
.map((route) => route.routeConfig?.path ?? '')
|
|
1209
|
-
.filter(Boolean)
|
|
1210
|
-
.join('/');
|
|
1211
|
-
}
|
|
1212
|
-
function snapshotMergedParams(snapshot) {
|
|
1213
|
-
return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
|
|
1214
|
-
}
|
|
1215
|
-
function snapshotDeepestParams(snapshot) {
|
|
1216
|
-
const mergedParams = snapshotMergedParams(snapshot);
|
|
1217
|
-
const keys = extractRouteParamKeys(snapshot.routeConfig?.path ?? '');
|
|
1218
|
-
if (!keys.length) {
|
|
1219
|
-
return {};
|
|
1220
|
-
}
|
|
1221
|
-
return keys.reduce((acc, key) => {
|
|
1222
|
-
const value = mergedParams[key];
|
|
1223
|
-
if (value !== undefined) {
|
|
1224
|
-
acc[key] = value;
|
|
1225
|
-
}
|
|
1226
|
-
return acc;
|
|
1227
|
-
}, {});
|
|
1228
|
-
}
|
|
1229
|
-
function snapshotMergedData(snapshot) {
|
|
1230
|
-
return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.data }), {});
|
|
1231
|
-
}
|
|
1232
|
-
function extractRouteParamKeys(path) {
|
|
1233
|
-
return path
|
|
1234
|
-
.split('/')
|
|
1235
|
-
.filter((segment) => segment.startsWith(':'))
|
|
1236
|
-
.map((segment) => segment.slice(1))
|
|
1237
|
-
.filter(Boolean);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
/**
|
|
1241
|
-
* Reactive snapshot of the current route (the deepest active route).
|
|
1242
|
-
* Updates on every `NavigationEnd` event.
|
|
1243
|
-
*/
|
|
1244
|
-
class RouteWatcher {
|
|
1245
|
-
router = inject(Router);
|
|
1246
|
-
destroyRef = inject(DestroyRef);
|
|
1247
|
-
#params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : /* istanbul ignore next */ []));
|
|
1248
|
-
#deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : /* istanbul ignore next */ []));
|
|
1249
|
-
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
|
|
1250
|
-
#data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
|
|
1251
|
-
#mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : /* istanbul ignore next */ []));
|
|
1252
|
-
#url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : /* istanbul ignore next */ []));
|
|
1253
|
-
#routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : /* istanbul ignore next */ []));
|
|
1254
|
-
#fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : /* istanbul ignore next */ []));
|
|
1255
|
-
/** Params merged from root to deepest route. */
|
|
1256
|
-
params = this.#params.asReadonly();
|
|
1257
|
-
/** Params declared on the deepest route only. */
|
|
1258
|
-
deepestParams = this.#deepestParams.asReadonly();
|
|
1259
|
-
/** Query params from the current navigation. */
|
|
1260
|
-
query = this.#query.asReadonly();
|
|
1261
|
-
/** Deepest route data only. */
|
|
1262
|
-
data = this.#data.asReadonly();
|
|
1263
|
-
/** Route data merged from root to deepest route. */
|
|
1264
|
-
mergedData = this.#mergedData.asReadonly();
|
|
1265
|
-
/** Full current url path assembled from root to deepest route. */
|
|
1266
|
-
url = this.#url.asReadonly();
|
|
1267
|
-
/** Current route config pattern, e.g. `orgs/:orgId/users/:id`. */
|
|
1268
|
-
routePattern = this.#routePattern.asReadonly();
|
|
1269
|
-
/** Current url fragment without `#`. */
|
|
1270
|
-
fragment = this.#fragment.asReadonly();
|
|
1271
|
-
/** Combined computed snapshot. */
|
|
1272
|
-
state = computed(() => ({
|
|
1273
|
-
params: this.#params(),
|
|
1274
|
-
deepestParams: this.#deepestParams(),
|
|
1275
|
-
query: this.#query(),
|
|
1276
|
-
data: this.#data(),
|
|
1277
|
-
mergedData: this.#mergedData(),
|
|
1278
|
-
url: this.#url(),
|
|
1279
|
-
routePattern: this.#routePattern(),
|
|
1280
|
-
fragment: this.#fragment(),
|
|
1281
|
-
}), ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
1282
|
-
constructor() {
|
|
1283
|
-
const read = () => {
|
|
1284
|
-
const snapshot = this.deepestSnapshot();
|
|
1285
|
-
const nextUrl = snapshotFullPath(snapshot);
|
|
1286
|
-
const nextRoutePattern = snapshotRoutePattern(snapshot);
|
|
1287
|
-
const nextParams = snapshotMergedParams(snapshot);
|
|
1288
|
-
const nextDeepestParams = snapshotDeepestParams(snapshot);
|
|
1289
|
-
const nextMergedData = snapshotMergedData(snapshot);
|
|
1290
|
-
const nextQuery = snapshot.queryParams;
|
|
1291
|
-
const nextData = snapshot.data;
|
|
1292
|
-
const nextFragment = snapshot.fragment ?? null;
|
|
1293
|
-
!deepEqual(nextParams, this.#params()) && this.#params.set(nextParams);
|
|
1294
|
-
!deepEqual(nextDeepestParams, this.#deepestParams()) && this.#deepestParams.set(nextDeepestParams);
|
|
1295
|
-
!deepEqual(nextQuery, this.#query()) && this.#query.set(nextQuery);
|
|
1296
|
-
!deepEqual(nextData, this.#data()) && this.#data.set(nextData);
|
|
1297
|
-
!deepEqual(nextMergedData, this.#mergedData()) && this.#mergedData.set(nextMergedData);
|
|
1298
|
-
this.#url() !== nextUrl && this.#url.set(nextUrl);
|
|
1299
|
-
this.#routePattern() !== nextRoutePattern && this.#routePattern.set(nextRoutePattern);
|
|
1300
|
-
this.#fragment() !== nextFragment && this.#fragment.set(nextFragment);
|
|
1301
|
-
};
|
|
1302
|
-
read();
|
|
1303
|
-
const subscription = this.router.events.subscribe((event) => {
|
|
1304
|
-
if (event instanceof NavigationEnd) {
|
|
1305
|
-
read();
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1308
|
-
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
1309
|
-
}
|
|
1310
|
-
selectData(key, strategy = 'deepest') {
|
|
1311
|
-
return computed(() => {
|
|
1312
|
-
const source = strategy === 'merged' ? this.#mergedData() : this.#data();
|
|
1313
|
-
return source[key];
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
selectParam(key, strategy = 'merged') {
|
|
1317
|
-
return computed(() => {
|
|
1318
|
-
const source = strategy === 'deepest' ? this.#deepestParams() : this.#params();
|
|
1319
|
-
return source[key];
|
|
1320
|
-
});
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
mode,
|
|
1373
|
+
textKey: explicitText ?? textKey,
|
|
1374
|
+
attrs: explicitAttrs ?? attrs,
|
|
1375
|
+
};
|
|
1321
1376
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1377
|
+
isMode(value) {
|
|
1378
|
+
return (value === 'only-content' ||
|
|
1379
|
+
value === 'only-placeholder' ||
|
|
1380
|
+
value === 'only-label' ||
|
|
1381
|
+
value === 'only-title' ||
|
|
1382
|
+
value === 'all');
|
|
1325
1383
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1384
|
+
/**
|
|
1385
|
+
* Asynchronously loads namespace and retrieves localized value by key.
|
|
1386
|
+
*
|
|
1387
|
+
* @param key localization key in format `namespace.path.to.key`
|
|
1388
|
+
* @returns localized string
|
|
1389
|
+
*/
|
|
1390
|
+
async getLangValue(key) {
|
|
1391
|
+
const [ns] = key.split('.', 1);
|
|
1392
|
+
if (!this.service.isNamespaceLoaded(ns)) {
|
|
1393
|
+
await this.service.loadNamespace(ns);
|
|
1394
|
+
}
|
|
1395
|
+
return this.service.get(key);
|
|
1328
1396
|
}
|
|
1329
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type:
|
|
1330
|
-
static
|
|
1397
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1398
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.4", type: LangDirective, isStandalone: true, selector: "[reLang]", inputs: { lang: { classPropertyName: "lang", publicName: "reLang", isSignal: true, isRequired: false, transformFunction: null }, langForAttr: { classPropertyName: "langForAttr", publicName: "langForAttr", isSignal: true, isRequired: false, transformFunction: null }, reLangKey: { classPropertyName: "reLangKey", publicName: "reLangKey", isSignal: false, isRequired: false, transformFunction: null }, reLangAttrs: { classPropertyName: "reLangAttrs", publicName: "reLangAttrs", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1331
1399
|
}
|
|
1332
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type:
|
|
1333
|
-
type:
|
|
1334
|
-
args: [{
|
|
1335
|
-
}], ctorParameters: () => [] }
|
|
1400
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, decorators: [{
|
|
1401
|
+
type: Directive,
|
|
1402
|
+
args: [{ selector: '[reLang]', standalone: true }]
|
|
1403
|
+
}], ctorParameters: () => [], propDecorators: { lang: [{ type: i0.Input, args: [{ isSignal: true, alias: "reLang", required: false }] }], langForAttr: [{ type: i0.Input, args: [{ isSignal: true, alias: "langForAttr", required: false }] }], reLangKey: [{
|
|
1404
|
+
type: Input
|
|
1405
|
+
}], reLangAttrs: [{
|
|
1406
|
+
type: Input
|
|
1407
|
+
}] } });
|
|
1336
1408
|
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1409
|
+
const LANG_PIPE_CONFIG = new InjectionToken('RE_LANG_PIPE_CONFIG');
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Custom Angular pipe that transforms a language key and additional parameters into a localized string.
|
|
1413
|
+
*
|
|
1414
|
+
* The pipe is declared as standalone and impure — meaning it can be used without importing a module
|
|
1415
|
+
* and will be recalculated when the state changes (for example, when the language is switched).
|
|
1416
|
+
*
|
|
1417
|
+
* In its operation, the pipe observes and caches language keys to improve performance,
|
|
1418
|
+
* using LangService to retrieve translated strings based on the current application language.
|
|
1419
|
+
*
|
|
1420
|
+
* The transformation involves accepting a key string and optional parameters,
|
|
1421
|
+
* forming a cache key, and returning the localized value corresponding to that request.
|
|
1422
|
+
*
|
|
1423
|
+
* @implements {PipeTransform}
|
|
1424
|
+
*/
|
|
1425
|
+
class LangPipe {
|
|
1426
|
+
cache = new Map();
|
|
1427
|
+
warnedUnresolved = new Set();
|
|
1428
|
+
lang = inject(LangService);
|
|
1429
|
+
injector = inject(Injector);
|
|
1430
|
+
config;
|
|
1431
|
+
ttlMs;
|
|
1432
|
+
maxCacheSize;
|
|
1433
|
+
constructor() {
|
|
1434
|
+
const resolved = inject(LANG_PIPE_CONFIG, { optional: true }) ?? {};
|
|
1435
|
+
this.config = resolved;
|
|
1436
|
+
this.ttlMs = resolved.ttlMs ?? 5 * 60 * 1000;
|
|
1437
|
+
this.maxCacheSize = resolved.maxCacheSize ?? 500;
|
|
1438
|
+
runInInjectionContext(this.injector, () => {
|
|
1439
|
+
effect(() => {
|
|
1440
|
+
// Clear cache on language change to avoid stale signals and unbounded growth.
|
|
1441
|
+
this.lang.currentLang();
|
|
1442
|
+
this.cache.clear();
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
transform(query, params) {
|
|
1447
|
+
if (!query) {
|
|
1448
|
+
return '';
|
|
1449
|
+
}
|
|
1450
|
+
const key = this.makeKey(query, params);
|
|
1451
|
+
const now = Date.now();
|
|
1452
|
+
const existing = this.cache.get(key);
|
|
1453
|
+
if (existing) {
|
|
1454
|
+
if (now - existing.ts < this.ttlMs) {
|
|
1455
|
+
return existing.value();
|
|
1347
1456
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1457
|
+
this.cache.delete(key);
|
|
1458
|
+
}
|
|
1459
|
+
if (!this.cache.has(key)) {
|
|
1460
|
+
this.cache.set(key, { value: this.lang.observe(query, params ?? undefined), ts: now });
|
|
1461
|
+
this.evictIfNeeded();
|
|
1462
|
+
}
|
|
1463
|
+
const value = this.cache.get(key).value();
|
|
1464
|
+
const ns = query.split('.', 1)[0];
|
|
1465
|
+
if (ns && !this.lang.isNamespaceLoaded(ns)) {
|
|
1466
|
+
const placeholder = this.config.placeholder;
|
|
1467
|
+
if (typeof placeholder === 'function') {
|
|
1468
|
+
return placeholder(query);
|
|
1469
|
+
}
|
|
1470
|
+
if (typeof placeholder === 'string') {
|
|
1471
|
+
return placeholder;
|
|
1351
1472
|
}
|
|
1352
|
-
router.resetConfig(patchRoutesWithNamespacePreload(router.config, normalized));
|
|
1353
|
-
}),
|
|
1354
|
-
]);
|
|
1355
|
-
}
|
|
1356
|
-
function normalizeRouteNamespacePreloadConfig(config) {
|
|
1357
|
-
if (!config) {
|
|
1358
|
-
return null;
|
|
1359
|
-
}
|
|
1360
|
-
return {
|
|
1361
|
-
mode: config.mode ?? 'blocking',
|
|
1362
|
-
dataKey: config.dataKey?.trim() || PRESENTIA_ROUTE_NAMESPACES_DATA_KEY,
|
|
1363
|
-
manifest: config.manifest,
|
|
1364
|
-
mergeStrategy: config.mergeStrategy ?? 'append',
|
|
1365
|
-
onError: config.onError ?? 'continue',
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
|
-
function patchRoutesWithNamespacePreload(routes, config) {
|
|
1369
|
-
return routes.map((route) => patchRouteWithNamespacePreload(route, config));
|
|
1370
|
-
}
|
|
1371
|
-
function patchRouteWithNamespacePreload(route, config) {
|
|
1372
|
-
const nextChildren = route.children ? patchRoutesWithNamespacePreload(route.children, config) : route.children;
|
|
1373
|
-
if (route.redirectTo) {
|
|
1374
|
-
return nextChildren === route.children ? route : { ...route, children: nextChildren };
|
|
1375
|
-
}
|
|
1376
|
-
const nextResolve = {
|
|
1377
|
-
...(route.resolve ?? {}),
|
|
1378
|
-
[PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY]: makeRouteNamespacePreloadResolver(config),
|
|
1379
|
-
};
|
|
1380
|
-
return {
|
|
1381
|
-
...route,
|
|
1382
|
-
...(nextChildren ? { children: nextChildren } : {}),
|
|
1383
|
-
resolve: nextResolve,
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
function makeRouteNamespacePreloadResolver(config) {
|
|
1387
|
-
return async (route, state) => {
|
|
1388
|
-
const lang = inject(LangService);
|
|
1389
|
-
const diagnostics = inject(RouteNamespaceDiagnosticsService);
|
|
1390
|
-
const namespaces = resolveRouteNamespaces(route, state, config);
|
|
1391
|
-
diagnostics.registerRouteNamespaces(state.url, namespaces);
|
|
1392
|
-
if (!namespaces.length) {
|
|
1393
|
-
return true;
|
|
1394
1473
|
}
|
|
1395
|
-
if (
|
|
1396
|
-
|
|
1397
|
-
void lang.loadNamespaces(namespaces);
|
|
1398
|
-
});
|
|
1399
|
-
return true;
|
|
1474
|
+
if (ns && this.lang.isNamespaceLoaded(ns) && !this.lang.has(query)) {
|
|
1475
|
+
this.warnUnresolvedKey(query);
|
|
1400
1476
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1477
|
+
return value;
|
|
1478
|
+
}
|
|
1479
|
+
makeKey(query, params) {
|
|
1480
|
+
if (!params) {
|
|
1481
|
+
return query;
|
|
1403
1482
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1483
|
+
const entries = Object.keys(params)
|
|
1484
|
+
.sort()
|
|
1485
|
+
.map((k) => `${k}:${params[k]}`);
|
|
1486
|
+
return `${query}::${entries.join('|')}`;
|
|
1487
|
+
}
|
|
1488
|
+
evictIfNeeded() {
|
|
1489
|
+
while (this.cache.size > this.maxCacheSize) {
|
|
1490
|
+
const firstKey = this.cache.keys().next().value;
|
|
1491
|
+
if (!firstKey) {
|
|
1492
|
+
return;
|
|
1407
1493
|
}
|
|
1408
|
-
|
|
1494
|
+
this.cache.delete(firstKey);
|
|
1409
1495
|
}
|
|
1410
|
-
return true;
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1413
|
-
function resolveRouteNamespaces(route, state, config) {
|
|
1414
|
-
const dataNamespaces = readNamespacesFromRouteData(route, config.dataKey);
|
|
1415
|
-
const manifestNamespaces = readNamespacesFromManifest(route, state, config.manifest);
|
|
1416
|
-
if (config.mergeStrategy === 'replace' && dataNamespaces.length) {
|
|
1417
|
-
return dataNamespaces;
|
|
1418
1496
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
return [];
|
|
1497
|
+
warnUnresolvedKey(query) {
|
|
1498
|
+
if (typeof ngDevMode === 'undefined') {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (this.warnedUnresolved.has(query)) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
this.warnedUnresolved.add(query);
|
|
1505
|
+
// eslint-disable-next-line no-console
|
|
1506
|
+
console.warn(`LangPipe: namespace loaded but key "${query}" is unresolved`);
|
|
1430
1507
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
return uniqueNamespaces(Object.entries(manifest)
|
|
1434
|
-
.filter(([key]) => matchesManifestKey(actualUrl, routePath, key))
|
|
1435
|
-
.flatMap(([, namespaces]) => namespaces));
|
|
1436
|
-
}
|
|
1437
|
-
function matchesManifestKey(actualUrl, routePath, manifestKey) {
|
|
1438
|
-
const normalizedKey = normalizeUrlPath(manifestKey);
|
|
1439
|
-
return compareRoutes(actualUrl, normalizedKey) || (!!routePath && routePath === normalizedKey);
|
|
1440
|
-
}
|
|
1441
|
-
function snapshotRouteConfigPath(route) {
|
|
1442
|
-
const templatePath = route.pathFromRoot
|
|
1443
|
-
.map((item) => item.routeConfig?.path ?? '')
|
|
1444
|
-
.filter(Boolean)
|
|
1445
|
-
.join('/');
|
|
1446
|
-
return normalizeUrlPath(templatePath);
|
|
1447
|
-
}
|
|
1448
|
-
function normalizeUrlPath(url) {
|
|
1449
|
-
const [path] = url.split(/[?#]/, 1);
|
|
1450
|
-
const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
|
|
1451
|
-
return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
|
|
1452
|
-
}
|
|
1453
|
-
function uniqueNamespaces(namespaces) {
|
|
1454
|
-
return Array.from(new Set(namespaces.map((ns) => ns.trim()).filter(Boolean)));
|
|
1508
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1509
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
|
|
1455
1510
|
}
|
|
1511
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, decorators: [{
|
|
1512
|
+
type: Pipe,
|
|
1513
|
+
args: [{ name: 'lang', standalone: true, pure: false }]
|
|
1514
|
+
}], ctorParameters: () => [] });
|
|
1456
1515
|
|
|
1457
1516
|
/**
|
|
1458
1517
|
* Type-safe mapping of available theme names.
|
|
@@ -1507,10 +1566,7 @@ const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
|
|
|
1507
1566
|
/**
|
|
1508
1567
|
* @deprecated Prefer configuring theme persistence through `providePresentia({ theme: { persistence... } })`.
|
|
1509
1568
|
*/
|
|
1510
|
-
const defaultThemePersistenceAdapter =
|
|
1511
|
-
getItem: (key) => localStorage.getItem(key),
|
|
1512
|
-
setItem: (key, value) => localStorage.setItem(key, value),
|
|
1513
|
-
};
|
|
1569
|
+
const defaultThemePersistenceAdapter = createPresentiaStorage('persist');
|
|
1514
1570
|
/**
|
|
1515
1571
|
* DI token for the persistence adapter used by `ThemeService`
|
|
1516
1572
|
* to store and retrieve the selected theme (default key: `'theme'`).
|
|
@@ -1614,10 +1670,10 @@ class ThemeService {
|
|
|
1614
1670
|
return this.registry.includes(next) ? next : (this.registry[0] ?? themes.light);
|
|
1615
1671
|
}
|
|
1616
1672
|
getStoredTheme() {
|
|
1617
|
-
return this.persistence.
|
|
1673
|
+
return this.persistence.get('theme');
|
|
1618
1674
|
}
|
|
1619
1675
|
persistTheme(theme) {
|
|
1620
|
-
this.persistence.
|
|
1676
|
+
this.persistence.set('theme', theme);
|
|
1621
1677
|
}
|
|
1622
1678
|
applyThemeToDom(theme) {
|
|
1623
1679
|
const root = this.resolveRootElement();
|
|
@@ -1713,6 +1769,7 @@ function buildPresentiaProviders(config) {
|
|
|
1713
1769
|
{ provide: DEVICE_BREAKPOINTS, useValue: config.breakpoints || defaultBreakpoints },
|
|
1714
1770
|
{ provide: THEME_CONFIG, useValue: config.theme || defaultThemeConfig },
|
|
1715
1771
|
{ provide: THEME_PERSISTENCE_ADAPTER, useValue: defaultThemePersistenceAdapter },
|
|
1772
|
+
{ provide: LANG_PERSISTENCE_ADAPTER, useValue: defaultLangPersistenceAdapter },
|
|
1716
1773
|
{ provide: LANG_CONFIG, useValue: config.locale || { defaultValue: '--------' } },
|
|
1717
1774
|
{ provide: LANG_PIPE_CONFIG, useValue: config.langPipe || {} },
|
|
1718
1775
|
{ provide: LANG_MISSING_KEY_HANDLER, useValue: config.langMissingKeyHandler ?? null },
|
|
@@ -1728,21 +1785,10 @@ function providePresentia(config) {
|
|
|
1728
1785
|
? { ...routesConfig, diagnostics: diagnosticsEnabled ?? routesConfig?.diagnostics }
|
|
1729
1786
|
: undefined;
|
|
1730
1787
|
const extraProviders = [];
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
getItem: () => null,
|
|
1736
|
-
setItem: () => undefined,
|
|
1737
|
-
},
|
|
1738
|
-
});
|
|
1739
|
-
}
|
|
1740
|
-
else if (config.theme?.persistenceAdapter) {
|
|
1741
|
-
extraProviders.push({
|
|
1742
|
-
provide: THEME_PERSISTENCE_ADAPTER,
|
|
1743
|
-
useValue: config.theme.persistenceAdapter,
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1788
|
+
const langStorage = createPresentiaStorage(config.lang.persistence, config.lang.storage ?? config.lang.persistenceAdapter);
|
|
1789
|
+
const themeStorage = createPresentiaStorage(config.theme?.persistence, config.theme?.storage ?? config.theme?.persistenceAdapter);
|
|
1790
|
+
extraProviders.push({ provide: LANG_PERSISTENCE_ADAPTER, useValue: langStorage });
|
|
1791
|
+
extraProviders.push({ provide: THEME_PERSISTENCE_ADAPTER, useValue: themeStorage });
|
|
1746
1792
|
return makeEnvironmentProviders([
|
|
1747
1793
|
buildPresentiaProviders({
|
|
1748
1794
|
locale: {
|
|
@@ -2019,5 +2065,5 @@ function joinBaseUrl(baseUrl, path) {
|
|
|
2019
2065
|
* Generated bundle index. Do not edit.
|
|
2020
2066
|
*/
|
|
2021
2067
|
|
|
2022
|
-
export { AdaptiveService, DEVICE_BREAKPOINTS, IfDeviceDirective, LANG_CONFIG, LANG_MISSING_KEY_HANDLER, LANG_PERSISTENCE_ADAPTER, LANG_PIPE_CONFIG, LangDirective, LangPipe, LangService, PRESENTIA_ROUTE_NAMESPACES_DATA_KEY, RouteNamespaceDiagnosticsService, RouteWatcher, SeoRouteListener, SeoService, THEME_CONFIG, THEME_PERSISTENCE_ADAPTER, ThemeService, darkThemePrefix, defaultBreakpoints, defaultThemeConfig, defaultThemePersistenceAdapter, innerLangVal, providePresentia, providePresentiaRouteNamespacePreload, provideReInit, themes };
|
|
2068
|
+
export { AdaptiveService, DEVICE_BREAKPOINTS, IfDeviceDirective, LANG_CONFIG, LANG_MISSING_KEY_HANDLER, LANG_PERSISTENCE_ADAPTER, LANG_PIPE_CONFIG, LangDirective, LangPipe, LangService, PRESENTIA_ROUTE_NAMESPACES_DATA_KEY, RouteNamespaceDiagnosticsService, RouteWatcher, SeoRouteListener, SeoService, THEME_CONFIG, THEME_PERSISTENCE_ADAPTER, ThemeService, darkThemePrefix, defaultBreakpoints, defaultLangPersistenceAdapter, defaultThemeConfig, defaultThemePersistenceAdapter, innerLangVal, providePresentia, providePresentiaRouteNamespacePreload, provideReInit, themes };
|
|
2023
2069
|
//# sourceMappingURL=reforgium-presentia.mjs.map
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.1.0",
|
|
3
3
|
"name": "@reforgium/presentia",
|
|
4
4
|
"description": "Angular infrastructure library for i18n, route-aware namespace preload, theming, adaptive breakpoints, route state, and SEO",
|
|
5
5
|
"author": "rtommievich",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@angular/core": ">=18.0.0",
|
|
66
66
|
"@angular/platform-browser": ">=18.0.0",
|
|
67
67
|
"@angular/router": ">=18.0.0",
|
|
68
|
-
"@reforgium/internal": ">=
|
|
68
|
+
"@reforgium/internal": ">=2.0.0",
|
|
69
69
|
"rxjs": ">=7.0.0"
|
|
70
70
|
},
|
|
71
71
|
"module": "fesm2022/reforgium-presentia.mjs",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as _angular_core from '@angular/core';
|
|
2
2
|
import { InjectionToken, Signal, PipeTransform, EnvironmentProviders } from '@angular/core';
|
|
3
3
|
import * as _reforgium_internal from '@reforgium/internal';
|
|
4
|
-
import { Devices, Langs, Themes as Themes$1 } from '@reforgium/internal';
|
|
4
|
+
import { Devices, Langs, StorageInterface, Themes as Themes$1, StorageStrategy } from '@reforgium/internal';
|
|
5
5
|
import { HttpHeaders, HttpParams, HttpContext } from '@angular/common/http';
|
|
6
6
|
|
|
7
7
|
/** Breakpoints for different device types */
|
|
@@ -250,10 +250,7 @@ type LangRouteNamespacePreloadConfig = {
|
|
|
250
250
|
* Adapter for custom persistence of language/theme selection.
|
|
251
251
|
* Implement this interface to replace the default `localStorage` storage.
|
|
252
252
|
*/
|
|
253
|
-
type PersistenceAdapter =
|
|
254
|
-
getItem(key: string): string | null;
|
|
255
|
-
setItem(key: string, value: string): void;
|
|
256
|
-
};
|
|
253
|
+
type PersistenceAdapter = StorageInterface<string, string>;
|
|
257
254
|
/**
|
|
258
255
|
* Describes a failed namespace load attempt.
|
|
259
256
|
* Exposed via `LangService.errors()` signal.
|
|
@@ -328,6 +325,7 @@ declare const LANG_MISSING_KEY_HANDLER: InjectionToken<LangMissingKeyHandler>;
|
|
|
328
325
|
* { provide: LANG_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
|
|
329
326
|
* ```
|
|
330
327
|
*/
|
|
328
|
+
declare const defaultLangPersistenceAdapter: StorageInterface<string, string>;
|
|
331
329
|
declare const LANG_PERSISTENCE_ADAPTER: InjectionToken<PersistenceAdapter>;
|
|
332
330
|
|
|
333
331
|
/**
|
|
@@ -378,6 +376,7 @@ declare class LangService {
|
|
|
378
376
|
private readonly config;
|
|
379
377
|
private readonly http;
|
|
380
378
|
private readonly isBrowser;
|
|
379
|
+
private readonly persistence;
|
|
381
380
|
private readonly missingKeyHandler;
|
|
382
381
|
private readonly routeNamespaceDiagnostics;
|
|
383
382
|
private readonly supportedLangSet;
|
|
@@ -389,7 +388,7 @@ declare class LangService {
|
|
|
389
388
|
* - If `config.kgValue` is not defined, the property will return the default value 'kg'.
|
|
390
389
|
* - For other languages (e.g. `ru`, `en`) returns source language as-is.
|
|
391
390
|
*/
|
|
392
|
-
readonly currentLang: Signal<
|
|
391
|
+
readonly currentLang: Signal<"ru" | "kg" | "en" | (string & {}) | "ky">;
|
|
393
392
|
/**
|
|
394
393
|
* Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
|
|
395
394
|
* Expected that property `#lang` has `asReadonly` method that returns immutable representation.
|
|
@@ -718,7 +717,7 @@ declare const THEME_CONFIG: InjectionToken<ThemeConfig>;
|
|
|
718
717
|
/**
|
|
719
718
|
* @deprecated Prefer configuring theme persistence through `providePresentia({ theme: { persistence... } })`.
|
|
720
719
|
*/
|
|
721
|
-
declare const defaultThemePersistenceAdapter:
|
|
720
|
+
declare const defaultThemePersistenceAdapter: StorageInterface<string, string>;
|
|
722
721
|
/**
|
|
723
722
|
* DI token for the persistence adapter used by `ThemeService`
|
|
724
723
|
* to store and retrieve the selected theme (default key: `'theme'`).
|
|
@@ -841,6 +840,12 @@ type PresentiaLangConfig = {
|
|
|
841
840
|
maxBatchSize?: number;
|
|
842
841
|
};
|
|
843
842
|
cache?: LangNamespaceCacheConfig;
|
|
843
|
+
persistence?: StorageStrategy | 'none';
|
|
844
|
+
storage?: StorageInterface<string, string>;
|
|
845
|
+
/**
|
|
846
|
+
* @deprecated Prefer `storage`.
|
|
847
|
+
*/
|
|
848
|
+
persistenceAdapter?: PersistenceAdapter;
|
|
844
849
|
diagnostics?: {
|
|
845
850
|
lateNamespaceLoads?: boolean;
|
|
846
851
|
};
|
|
@@ -849,7 +854,11 @@ type PresentiaLangConfig = {
|
|
|
849
854
|
type PresentiaThemeConfig = {
|
|
850
855
|
registry?: readonly Theme[];
|
|
851
856
|
defaultTheme?: ThemeConfig['defaultTheme'];
|
|
852
|
-
persistence?:
|
|
857
|
+
persistence?: StorageStrategy | 'none';
|
|
858
|
+
storage?: StorageInterface<string, string>;
|
|
859
|
+
/**
|
|
860
|
+
* @deprecated Prefer `storage`.
|
|
861
|
+
*/
|
|
853
862
|
persistenceAdapter?: PersistenceAdapter;
|
|
854
863
|
dom?: {
|
|
855
864
|
strategy?: 'root-class' | 'data-attribute';
|
|
@@ -1067,6 +1076,6 @@ declare class RouteNamespaceDiagnosticsService {
|
|
|
1067
1076
|
static ɵprov: _angular_core.ɵɵInjectableDeclaration<RouteNamespaceDiagnosticsService>;
|
|
1068
1077
|
}
|
|
1069
1078
|
|
|
1070
|
-
export { AdaptiveService, DEVICE_BREAKPOINTS, IfDeviceDirective, LANG_CONFIG, LANG_MISSING_KEY_HANDLER, LANG_PERSISTENCE_ADAPTER, LANG_PIPE_CONFIG, LangDirective, LangPipe, LangService, PRESENTIA_ROUTE_NAMESPACES_DATA_KEY, RouteNamespaceDiagnosticsService, RouteWatcher, SeoRouteListener, SeoService, THEME_CONFIG, THEME_PERSISTENCE_ADAPTER, ThemeService, darkThemePrefix, defaultBreakpoints, defaultThemeConfig, defaultThemePersistenceAdapter, innerLangVal, providePresentia, providePresentiaRouteNamespacePreload, provideReInit, themes };
|
|
1079
|
+
export { AdaptiveService, DEVICE_BREAKPOINTS, IfDeviceDirective, LANG_CONFIG, LANG_MISSING_KEY_HANDLER, LANG_PERSISTENCE_ADAPTER, LANG_PIPE_CONFIG, LangDirective, LangPipe, LangService, PRESENTIA_ROUTE_NAMESPACES_DATA_KEY, RouteNamespaceDiagnosticsService, RouteWatcher, SeoRouteListener, SeoService, THEME_CONFIG, THEME_PERSISTENCE_ADAPTER, ThemeService, darkThemePrefix, defaultBreakpoints, defaultLangPersistenceAdapter, defaultThemeConfig, defaultThemePersistenceAdapter, innerLangVal, providePresentia, providePresentiaRouteNamespacePreload, provideReInit, themes };
|
|
1071
1080
|
export type { AppConfig, BaseTheme, DeviceBreakpoints, LangBatchRequestBuilder, LangBatchRequestContext, LangBatchResponseAdapter, LangBatchResponseContext, LangDirectiveConfig, LangDirectiveMode, LangDto, LangHttpRequestOptions, LangKey, LangKeyRegistry, LangMissingKeyHandler, LangModel, LangNamespaceCacheConfig, LangParamValue, LangParams, LangPipeConfig, LangRequestBuilder, LangRequestContext, LangRequestOptionsFactory, LangResponseAdapter, LangResponseContext, LangRouteNamespaceManifest, LangRouteNamespacePreloadConfig, LangRouteNamespacePreloadMergeStrategy, LangRouteNamespacePreloadMode, LoadNamespaceError, LocaleConfig, OgType, PersistenceAdapter, PresentiaAdaptiveConfig, PresentiaConfig, PresentiaLangConfig, PresentiaThemeConfig, RouteWatcherReadStrategy, RouteWatcherState, Theme, ThemeConfig, Themes, TwitterCardType };
|
|
1072
1081
|
//# sourceMappingURL=reforgium-presentia.d.ts.map
|