@lukfel/ng-scaffold 22.0.6 → 22.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/README.md +48 -1
- package/fesm2022/lukfel-ng-scaffold.mjs +178 -4
- package/fesm2022/lukfel-ng-scaffold.mjs.map +1 -1
- package/package.json +1 -1
- package/types/lukfel-ng-scaffold.d.ts +105 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Documentation
|
|
2
2
|
## Introduction
|
|
3
3
|
|
|
4
|
-
This Angular library provides a foundational scaffold for modern web and mobile applications. It includes essential UI elements such as a header, sidebar, drawer, footer, floating button, and built-in services for theme switching, snackbar notifications, dialog management, and breakpoint detection. Simply wrap your `router-outlet` with the `lf-scaffold` element and configure the `ScaffoldConfig` within the `ScaffoldService`.
|
|
4
|
+
This Angular library provides a foundational scaffold for modern web and mobile applications. It includes essential UI elements such as a header, sidebar, drawer, footer, floating button, and built-in services for theme switching, language switching, snackbar notifications, dialog management, and breakpoint detection. Simply wrap your `router-outlet` with the `lf-scaffold` element and configure the `ScaffoldConfig` within the `ScaffoldService`.
|
|
5
5
|
|
|
6
6
|
- **NPM**: [@lukfel/ng-scaffold](https://www.npmjs.com/package/@lukfel/ng-scaffold)
|
|
7
7
|
- **Demo**: [lukfel.github.io/ng-scaffold](https://lukfel.github.io/ng-scaffold)
|
|
@@ -278,6 +278,7 @@ This library includes several utility services:
|
|
|
278
278
|
- **`RouterService`** – Track route changes and retrieve route history
|
|
279
279
|
- **`SeoService`** – Manage meta tags
|
|
280
280
|
- **`LocalStorageService`** – Handle local storage
|
|
281
|
+
- **`TranslationService`** – Switch languages at runtime with per-locale JSON files
|
|
281
282
|
|
|
282
283
|
### Logger
|
|
283
284
|
Logs information during development and hides all logs during production.
|
|
@@ -394,6 +395,52 @@ export class AppComponent {
|
|
|
394
395
|
}
|
|
395
396
|
```
|
|
396
397
|
|
|
398
|
+
### TranslationService
|
|
399
|
+
Switches the application language at runtime by loading a separate JSON file per locale (e.g. `de.json`, `en.json`) that share the same keys. Keys missing from a locale are automatically resolved from the `fallbackLocale`.
|
|
400
|
+
|
|
401
|
+
* **Note:** Configure the available locales in the `provideScaffold` provider and serve the `<locale>.json` files from the configured `path` (default `assets/i18n/`)
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { provideScaffold } from '@lukfel/ng-scaffold';
|
|
405
|
+
|
|
406
|
+
...
|
|
407
|
+
providers: [
|
|
408
|
+
provideScaffold({
|
|
409
|
+
language: {
|
|
410
|
+
locales: ['de', 'en'], // available locales (also drives a language picker)
|
|
411
|
+
fallbackLocale: 'en', // used when a locale is unknown or a key is missing
|
|
412
|
+
path: 'assets/i18n/', // (Optional) base path of the <locale>.json files
|
|
413
|
+
persist: true // (Optional) store the active locale in local storage
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
]
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Inject the `TranslationService` to read the translations and switch the locale. Each app keeps its own strongly-typed `TransMap` interface and reads the labels through the typed accessors.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import { TranslationService } from '@lukfel/ng-scaffold';
|
|
423
|
+
|
|
424
|
+
export class AppComponent {
|
|
425
|
+
|
|
426
|
+
private translationService = inject(TranslationService);
|
|
427
|
+
|
|
428
|
+
// Typed signal of the active translations (empty until the first load)
|
|
429
|
+
public transMap = this.translationService.transMapAs<TransMap>();
|
|
430
|
+
|
|
431
|
+
// Active locale and all configured locales (e.g. for a language picker)
|
|
432
|
+
public locale = this.translationService.locale;
|
|
433
|
+
public locales = this.translationService.locales;
|
|
434
|
+
|
|
435
|
+
// Switch the active language (persisted according to the config)
|
|
436
|
+
public changeLanguage(locale: string): void {
|
|
437
|
+
this.translationService.setLocale(locale);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
* **Note:** For observable consumers use `locale$` and `transMap$` (or the typed `transMapStream<TransMap>()`, which only emits once translations are loaded). Track the loading state with the `loading` signal or `loading$`.
|
|
443
|
+
|
|
397
444
|
|
|
398
445
|
|
|
399
446
|
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, PLATFORM_ID, DOCUMENT, input, output, ChangeDetectionStrategy, Component,
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, PLATFORM_ID, DOCUMENT, signal, input, output, ChangeDetectionStrategy, Component, effect, DestroyRef, viewChild, computed, model, afterNextRender, linkedSignal, TemplateRef, Directive, contentChild } from '@angular/core';
|
|
3
3
|
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
|
4
4
|
import { isPlatformBrowser, NgClass, NgTemplateOutlet, KeyValuePipe } from '@angular/common';
|
|
5
|
-
import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { toObservable, toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
6
|
import * as i1$3 from '@angular/router';
|
|
7
7
|
import { Router, RouteConfigLoadStart, RouteConfigLoadEnd, NavigationEnd, RouterModule, ActivatedRoute } from '@angular/router';
|
|
8
|
-
import { distinctUntilChanged, firstValueFrom, BehaviorSubject, take, tap, map, fromEvent, debounceTime, finalize } from 'rxjs';
|
|
8
|
+
import { distinctUntilChanged, firstValueFrom, BehaviorSubject, take, filter, tap, map, fromEvent, debounceTime, finalize } from 'rxjs';
|
|
9
9
|
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
10
10
|
import { Overlay } from '@angular/cdk/overlay';
|
|
11
11
|
import * as i1$2 from '@angular/cdk/portal';
|
|
12
12
|
import { ComponentPortal, PortalModule } from '@angular/cdk/portal';
|
|
13
13
|
import { Title, Meta } from '@angular/platform-browser';
|
|
14
14
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
15
|
+
import { HttpClient } from '@angular/common/http';
|
|
15
16
|
import * as i1 from '@angular/material/button';
|
|
16
17
|
import { MatButtonModule } from '@angular/material/button';
|
|
17
18
|
import * as i2 from '@angular/material/icon';
|
|
@@ -683,6 +684,179 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
683
684
|
}]
|
|
684
685
|
}], ctorParameters: () => [] });
|
|
685
686
|
|
|
687
|
+
const DEFAULT_CONFIG = {
|
|
688
|
+
path: 'assets/i18n/',
|
|
689
|
+
persist: true,
|
|
690
|
+
};
|
|
691
|
+
/**
|
|
692
|
+
* Runtime translation service that switches between per-locale JSON files.
|
|
693
|
+
*
|
|
694
|
+
* Configure it through `provideScaffold({ language: { ... } })`. Each app keeps
|
|
695
|
+
* its own strongly-typed `TransMap` interface and reads the labels via
|
|
696
|
+
* {@link TranslationService.transMapAs}.
|
|
697
|
+
*/
|
|
698
|
+
class TranslationService {
|
|
699
|
+
constructor() {
|
|
700
|
+
this.http = inject(HttpClient);
|
|
701
|
+
this.document = inject(DOCUMENT);
|
|
702
|
+
this.platformId = inject(PLATFORM_ID);
|
|
703
|
+
this.storage = inject(LocalStorageService);
|
|
704
|
+
this.logger = inject(Logger);
|
|
705
|
+
this.config = this.resolveConfig(inject(CONFIG, { optional: true })?.language);
|
|
706
|
+
this.STORAGE_KEY = 'LANGUAGE';
|
|
707
|
+
/** In-memory cache of raw (un-merged) locale files, keyed by locale. */
|
|
708
|
+
this.cache = new Map();
|
|
709
|
+
/** Distinct empty seed so `transMap$` can suppress the pre-load emission. */
|
|
710
|
+
this.EMPTY = {};
|
|
711
|
+
this._locale = signal(this.config.fallbackLocale, /* @ts-ignore */
|
|
712
|
+
...(ngDevMode ? [{ debugName: "_locale" }] : /* istanbul ignore next */ []));
|
|
713
|
+
this._transMap = signal(this.EMPTY, /* @ts-ignore */
|
|
714
|
+
...(ngDevMode ? [{ debugName: "_transMap" }] : /* istanbul ignore next */ []));
|
|
715
|
+
this._loading = signal(false, /* @ts-ignore */
|
|
716
|
+
...(ngDevMode ? [{ debugName: "_loading" }] : /* istanbul ignore next */ []));
|
|
717
|
+
/** The active locale, e.g. `'de'`. */
|
|
718
|
+
this.locale = this._locale.asReadonly();
|
|
719
|
+
/** Labels for the active locale (with fallback keys merged in). */
|
|
720
|
+
this.transMap = this._transMap.asReadonly();
|
|
721
|
+
/** `true` while a locale file is being fetched. */
|
|
722
|
+
this.loading = this._loading.asReadonly();
|
|
723
|
+
/** All configured locales — drives a language picker. */
|
|
724
|
+
this.locales = this.config.locales;
|
|
725
|
+
/** Observable interop for `| async` templates and rxjs consumers. */
|
|
726
|
+
this.locale$ = toObservable(this._locale);
|
|
727
|
+
// Suppress the empty seed so subscribers only see loaded translations —
|
|
728
|
+
// consumers that guard on a falsy `transMap` (null/undefined) keep working.
|
|
729
|
+
this.transMap$ = toObservable(this._transMap).pipe(filter((transMap) => transMap !== this.EMPTY));
|
|
730
|
+
this.loading$ = toObservable(this._loading);
|
|
731
|
+
this.init();
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Typed signal accessor: reads the active labels as the app's own `TransMap`
|
|
735
|
+
* shape, preserving compile-time key checking. Emits `{}` until the first load.
|
|
736
|
+
*/
|
|
737
|
+
transMapAs() {
|
|
738
|
+
return this._transMap;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Typed observable accessor: like {@link transMapAs} but as a stream that only
|
|
742
|
+
* emits once translations are loaded (never the empty seed) — matching a
|
|
743
|
+
* `null`/`undefined`-until-loaded consumption pattern.
|
|
744
|
+
*/
|
|
745
|
+
transMapStream() {
|
|
746
|
+
return this.transMap$;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Switch the active locale. Unknown locales fall back to `fallbackLocale`.
|
|
750
|
+
* Persistence is governed by the `persist` config option.
|
|
751
|
+
*
|
|
752
|
+
* @param locale target locale (must be one of the configured `locales`)
|
|
753
|
+
*/
|
|
754
|
+
async setLocale(locale) {
|
|
755
|
+
await this.applyLocale(locale, this.config.persist);
|
|
756
|
+
}
|
|
757
|
+
async applyLocale(locale, persist) {
|
|
758
|
+
const target = this.config.locales.includes(locale)
|
|
759
|
+
? locale
|
|
760
|
+
: this.config.fallbackLocale;
|
|
761
|
+
this._locale.set(target);
|
|
762
|
+
this.setHtmlLangAttribute(target);
|
|
763
|
+
if (persist) {
|
|
764
|
+
this.storage.setItem(this.STORAGE_KEY, target);
|
|
765
|
+
}
|
|
766
|
+
await this.loadTranslations(target);
|
|
767
|
+
}
|
|
768
|
+
init() {
|
|
769
|
+
// No locales configured — stay inert (no stray request to `<fallback>.json`).
|
|
770
|
+
if (!this.config.locales.length) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const stored = this.config.persist
|
|
774
|
+
? this.storage.getItem(this.STORAGE_KEY)
|
|
775
|
+
: null;
|
|
776
|
+
const initial = stored && this.config.locales.includes(stored) ? stored : this.detectLocale();
|
|
777
|
+
// Already persisted (or intentionally not) — don't rewrite storage on startup.
|
|
778
|
+
this.applyLocale(initial, false);
|
|
779
|
+
}
|
|
780
|
+
detectLocale() {
|
|
781
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
782
|
+
return this.config.fallbackLocale;
|
|
783
|
+
}
|
|
784
|
+
const browserLocale = navigator.language.slice(0, 2);
|
|
785
|
+
return this.config.locales.includes(browserLocale) ? browserLocale : this.config.fallbackLocale;
|
|
786
|
+
}
|
|
787
|
+
async loadTranslations(locale) {
|
|
788
|
+
this._loading.set(true);
|
|
789
|
+
try {
|
|
790
|
+
const active = await this.fetchLocale(locale);
|
|
791
|
+
let merged = active;
|
|
792
|
+
if (locale !== this.config.fallbackLocale) {
|
|
793
|
+
try {
|
|
794
|
+
const fallback = await this.fetchLocale(this.config.fallbackLocale);
|
|
795
|
+
merged = deepMerge(fallback, active);
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
// Fallback file unavailable — keep the loaded active locale rather than
|
|
799
|
+
// discarding it and rendering blank.
|
|
800
|
+
this.logger.warn(`[TRANSLATION] Fallback locale '${this.config.fallbackLocale}' failed to load`, error);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// Ignore stale responses if the locale changed while loading.
|
|
804
|
+
if (this._locale() === locale) {
|
|
805
|
+
this._transMap.set(merged);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
this.logger.error(`[TRANSLATION] Failed to load locale '${locale}'`, error);
|
|
810
|
+
}
|
|
811
|
+
finally {
|
|
812
|
+
this._loading.set(false);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async fetchLocale(locale) {
|
|
816
|
+
const cached = this.cache.get(locale);
|
|
817
|
+
if (cached) {
|
|
818
|
+
return cached;
|
|
819
|
+
}
|
|
820
|
+
const data = await firstValueFrom(this.http.get(`${this.config.path}${locale}.json`));
|
|
821
|
+
this.cache.set(locale, data);
|
|
822
|
+
return data;
|
|
823
|
+
}
|
|
824
|
+
setHtmlLangAttribute(locale) {
|
|
825
|
+
this.document?.documentElement?.setAttribute('lang', locale);
|
|
826
|
+
}
|
|
827
|
+
resolveConfig(config) {
|
|
828
|
+
const path = config?.path ?? DEFAULT_CONFIG.path;
|
|
829
|
+
return {
|
|
830
|
+
locales: config?.locales ?? [],
|
|
831
|
+
fallbackLocale: config?.fallbackLocale ?? config?.locales?.[0] ?? 'en',
|
|
832
|
+
path: path.endsWith('/') ? path : `${path}/`,
|
|
833
|
+
persist: config?.persist ?? DEFAULT_CONFIG.persist,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: TranslationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
837
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: TranslationService, providedIn: 'root' }); }
|
|
838
|
+
}
|
|
839
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: TranslationService, decorators: [{
|
|
840
|
+
type: Injectable,
|
|
841
|
+
args: [{ providedIn: 'root' }]
|
|
842
|
+
}], ctorParameters: () => [] });
|
|
843
|
+
/** Deep-merges `override` onto `base`, returning a new object (pure). */
|
|
844
|
+
function deepMerge(base, override) {
|
|
845
|
+
const result = { ...base };
|
|
846
|
+
for (const key of Object.keys(override)) {
|
|
847
|
+
const overrideValue = override[key];
|
|
848
|
+
const baseValue = result[key];
|
|
849
|
+
result[key] =
|
|
850
|
+
isTransMap(baseValue) && isTransMap(overrideValue)
|
|
851
|
+
? deepMerge(baseValue, overrideValue)
|
|
852
|
+
: overrideValue;
|
|
853
|
+
}
|
|
854
|
+
return result;
|
|
855
|
+
}
|
|
856
|
+
function isTransMap(value) {
|
|
857
|
+
return typeof value === 'object' && value !== null;
|
|
858
|
+
}
|
|
859
|
+
|
|
686
860
|
class BottomBarComponent {
|
|
687
861
|
constructor() {
|
|
688
862
|
this.libraryConfig = input(null, /* @ts-ignore */
|
|
@@ -1669,5 +1843,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
1669
1843
|
* Generated bundle index. Do not edit.
|
|
1670
1844
|
*/
|
|
1671
1845
|
|
|
1672
|
-
export { BreakpointService, CONFIG, ColorPickerComponent, DialogService, FileUploadComponent, ListComponent, ListItemAvatarDirective, ListItemButtonsDirective, ListItemSubtitleDirective, ListItemTitleDirective, LocalStorageService, Logger, NotificationComponent, OverlayService, PlaceholderComponent, RouterService, ScaffoldComponent, ScaffoldLoadingInterceptor, ScaffoldService, SeoService, SnackbarService, ThemeService, provideScaffold };
|
|
1846
|
+
export { BreakpointService, CONFIG, ColorPickerComponent, DialogService, FileUploadComponent, ListComponent, ListItemAvatarDirective, ListItemButtonsDirective, ListItemSubtitleDirective, ListItemTitleDirective, LocalStorageService, Logger, NotificationComponent, OverlayService, PlaceholderComponent, RouterService, ScaffoldComponent, ScaffoldLoadingInterceptor, ScaffoldService, SeoService, SnackbarService, ThemeService, TranslationService, provideScaffold };
|
|
1673
1847
|
//# sourceMappingURL=lukfel-ng-scaffold.mjs.map
|