@reforgium/presentia 2.0.0 → 2.1.1
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 +169 -144
- package/README.md +1 -1
- package/bin/presentia-gen-namespaces.mjs +1248 -1248
- package/fesm2022/reforgium-presentia.mjs +571 -525
- package/package.json +2 -2
- package/types/reforgium-presentia.d.ts +18 -9
|
@@ -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';
|
|
@@ -73,10 +73,10 @@ const DEVICE_BREAKPOINTS = new InjectionToken('RE_DEVICE_BREAKPOINTS', {
|
|
|
73
73
|
*/
|
|
74
74
|
class AdaptiveService {
|
|
75
75
|
/** @internal Signal of the current device type. */
|
|
76
|
-
#device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] :
|
|
76
|
+
#device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : []));
|
|
77
77
|
/** @internal Signals of the current window width and height. */
|
|
78
|
-
#width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] :
|
|
79
|
-
#height = signal(0, ...(ngDevMode ? [{ debugName: "#height" }] :
|
|
78
|
+
#width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : []));
|
|
79
|
+
#height = signal(0, ...(ngDevMode ? [{ debugName: "#height" }] : []));
|
|
80
80
|
/**
|
|
81
81
|
* Current device type (reactive signal).
|
|
82
82
|
* Possible values: `'desktop' | 'tablet' | 'mobile'`.
|
|
@@ -99,15 +99,15 @@ class AdaptiveService {
|
|
|
99
99
|
* Computed signal indicating whether the current device is a desktop.
|
|
100
100
|
* Used for conditional rendering or layout configuration.
|
|
101
101
|
*/
|
|
102
|
-
isDesktop = computed(() => this.#device() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] :
|
|
103
|
-
isMobile = computed(() => this.#device() === 'mobile', ...(ngDevMode ? [{ debugName: "isMobile" }] :
|
|
104
|
-
isTablet = computed(() => this.#device() === 'tablet', ...(ngDevMode ? [{ debugName: "isTablet" }] :
|
|
105
|
-
isDesktopSmall = computed(() => this.#device() === 'desktop-s', ...(ngDevMode ? [{ debugName: "isDesktopSmall" }] :
|
|
102
|
+
isDesktop = computed(() => this.#device() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] : []));
|
|
103
|
+
isMobile = computed(() => this.#device() === 'mobile', ...(ngDevMode ? [{ debugName: "isMobile" }] : []));
|
|
104
|
+
isTablet = computed(() => this.#device() === 'tablet', ...(ngDevMode ? [{ debugName: "isTablet" }] : []));
|
|
105
|
+
isDesktopSmall = computed(() => this.#device() === 'desktop-s', ...(ngDevMode ? [{ debugName: "isDesktopSmall" }] : []));
|
|
106
106
|
/**
|
|
107
107
|
* Computed signal determining whether the screen is in portrait orientation.
|
|
108
108
|
* Returns `true` if window height is greater than width.
|
|
109
109
|
*/
|
|
110
|
-
isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] :
|
|
110
|
+
isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : []));
|
|
111
111
|
deviceBreakpoints = inject(DEVICE_BREAKPOINTS);
|
|
112
112
|
devicePriority = Object.keys(this.deviceBreakpoints);
|
|
113
113
|
destroyRef = inject(DestroyRef);
|
|
@@ -200,10 +200,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
200
200
|
* the template is automatically added or removed from the DOM.
|
|
201
201
|
*/
|
|
202
202
|
class IfDeviceDirective {
|
|
203
|
-
deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] :
|
|
204
|
-
atLeastInput = signal(undefined, ...(ngDevMode ? [{ debugName: "atLeastInput" }] :
|
|
205
|
-
betweenInput = signal(undefined, ...(ngDevMode ? [{ debugName: "betweenInput" }] :
|
|
206
|
-
inverseInput = signal(false, ...(ngDevMode ? [{ debugName: "inverseInput" }] :
|
|
203
|
+
deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : []));
|
|
204
|
+
atLeastInput = signal(undefined, ...(ngDevMode ? [{ debugName: "atLeastInput" }] : []));
|
|
205
|
+
betweenInput = signal(undefined, ...(ngDevMode ? [{ debugName: "betweenInput" }] : []));
|
|
206
|
+
inverseInput = signal(false, ...(ngDevMode ? [{ debugName: "inverseInput" }] : []));
|
|
207
207
|
tpl = inject(TemplateRef);
|
|
208
208
|
vcr = inject(ViewContainerRef);
|
|
209
209
|
adaptive = inject(AdaptiveService);
|
|
@@ -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" }] : []));
|
|
439
|
+
#deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : []));
|
|
440
|
+
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
|
|
441
|
+
#data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : []));
|
|
442
|
+
#mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : []));
|
|
443
|
+
#url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : []));
|
|
444
|
+
#routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : []));
|
|
445
|
+
#fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : []));
|
|
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" }] : []));
|
|
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,83 +586,204 @@ 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
|
-
http = inject(HttpClient);
|
|
388
|
-
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
389
|
-
missingKeyHandler = inject(LANG_MISSING_KEY_HANDLER, { optional: true });
|
|
390
|
-
routeNamespaceDiagnostics = inject(RouteNamespaceDiagnosticsService, { optional: true });
|
|
391
|
-
supportedLangSet = new Set([
|
|
392
|
-
...LangService.BUILTIN_LANGS,
|
|
393
|
-
...this.normalizeSupportedLangs(this.config.supportedLangs ?? []),
|
|
394
|
-
]);
|
|
395
|
-
#lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : /* istanbul ignore next */ []));
|
|
396
|
-
#cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : /* istanbul ignore next */ []));
|
|
397
|
-
#loadedNamespaces = new Set();
|
|
398
|
-
#pendingLoads = new Map();
|
|
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
|
-
}
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Sets the current language for the application.
|
|
429
|
-
*
|
|
430
|
-
* @param {Langs | 'ky'} lang - The language to set.
|
|
431
|
-
* Accepts predefined type `Langs` or 'ky' to set Kyrgyz language.
|
|
432
|
-
* @return {void} Returns no value.
|
|
433
|
-
*/
|
|
434
|
-
setLang(lang) {
|
|
435
|
-
const langVal = this.normalizeLang(lang);
|
|
436
|
-
if (!langVal || !this.supportedLangSet.has(langVal)) {
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
if (langVal !== this.#lang()) {
|
|
440
|
-
this.#lang.set(langVal);
|
|
441
|
-
if (this.isBrowser) {
|
|
442
|
-
localStorage.setItem('lang', langVal);
|
|
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;
|
|
443
599
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
}),
|
|
606
|
+
]);
|
|
607
|
+
}
|
|
608
|
+
function normalizeRouteNamespacePreloadConfig(config) {
|
|
609
|
+
if (!config) {
|
|
610
|
+
return null;
|
|
449
611
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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" }] : []));
|
|
729
|
+
#cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : []));
|
|
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" }] : []));
|
|
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);
|
|
774
|
+
if (this.isBrowser) {
|
|
775
|
+
this.persistence.set('lang', langVal);
|
|
776
|
+
}
|
|
777
|
+
const namespaces = Array.from(this.#loadedNamespaces.values()).map((key) => this.namespaceFromKey(key));
|
|
778
|
+
this.#loadedNamespaces.clear();
|
|
779
|
+
this.#namespaceLoadedAt.clear();
|
|
780
|
+
void this.loadNamespaces(namespaces);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
get(query, params) {
|
|
784
|
+
const value = this.getChainedValue(query);
|
|
785
|
+
const baseValue = value ?? this.resolveMissingValue(query);
|
|
786
|
+
if (params) {
|
|
454
787
|
return this.applyParams(baseValue, params);
|
|
455
788
|
}
|
|
456
789
|
return baseValue;
|
|
@@ -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) {
|
|
@@ -853,19 +1186,19 @@ class LangDirective {
|
|
|
853
1186
|
* Localization mode: defines which parts of the element will be translated.
|
|
854
1187
|
* @default 'all'
|
|
855
1188
|
*/
|
|
856
|
-
lang = input('all', { ...(ngDevMode ? { debugName: "lang" } :
|
|
1189
|
+
lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : {}), alias: 'reLang' });
|
|
857
1190
|
/**
|
|
858
1191
|
* Explicit key for text content translation.
|
|
859
1192
|
*/
|
|
860
|
-
reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] :
|
|
1193
|
+
reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : []));
|
|
861
1194
|
/**
|
|
862
1195
|
* Explicit attribute-to-key map for translation.
|
|
863
1196
|
*/
|
|
864
|
-
reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] :
|
|
1197
|
+
reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : []));
|
|
865
1198
|
/**
|
|
866
1199
|
* Name of an additional attribute to localize (besides standard `title`, `label`, `placeholder`).
|
|
867
1200
|
*/
|
|
868
|
-
langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] :
|
|
1201
|
+
langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : []));
|
|
869
1202
|
el = inject(ElementRef);
|
|
870
1203
|
renderer = inject(Renderer2);
|
|
871
1204
|
service = inject(LangService);
|
|
@@ -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'`).
|
|
@@ -1552,7 +1608,7 @@ class ThemeService {
|
|
|
1552
1608
|
persistence = inject(THEME_PERSISTENCE_ADAPTER);
|
|
1553
1609
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
1554
1610
|
document = inject(DOCUMENT);
|
|
1555
|
-
#theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] :
|
|
1611
|
+
#theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : []));
|
|
1556
1612
|
/**
|
|
1557
1613
|
* Current active theme (`light` or `dark`).
|
|
1558
1614
|
*
|
|
@@ -1562,13 +1618,13 @@ class ThemeService {
|
|
|
1562
1618
|
* <div [class]="themeService.theme()"></div>
|
|
1563
1619
|
* ```
|
|
1564
1620
|
*/
|
|
1565
|
-
theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] :
|
|
1621
|
+
theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
|
|
1566
1622
|
/**
|
|
1567
1623
|
* Convenient flag returning `true` if the light theme is active.
|
|
1568
1624
|
* Suitable for conditional style application or resource selection.
|
|
1569
1625
|
*/
|
|
1570
|
-
isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] :
|
|
1571
|
-
isDark = computed(() => this.#theme() === themes.dark, ...(ngDevMode ? [{ debugName: "isDark" }] :
|
|
1626
|
+
isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : []));
|
|
1627
|
+
isDark = computed(() => this.#theme() === themes.dark, ...(ngDevMode ? [{ debugName: "isDark" }] : []));
|
|
1572
1628
|
constructor() {
|
|
1573
1629
|
effect(() => {
|
|
1574
1630
|
if (!this.isBrowser) {
|
|
@@ -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
|