Skip to content

Loading Translations via APP_INITIALIZER

FilipLeitner edited this page Oct 10, 2024 · 2 revisions

Using APP_INITIALIZER allows us to perform tasks before the app is fully initialized. In this case, we're using it to load both configuration and translations, ensuring they're available as soon as the app starts.

1. Configuration Service

Create a service to handle the loading of configurations and translations. Here's a simplified version based on the actual implementation:

import { Injectable, Injector, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { BehaviorSubject, Observable, forkJoin, of } from "rxjs";
import { catchError, tap, switchMap, filter, take } from "rxjs/operators";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { HsConfig, HsLanguageService, WebpackTranslateLoader } from "hslayers-ng/services/language";

@Injectable({
  providedIn: "root",
})
export class AppConfigService {
  private configSubject = new BehaviorSubject<AppConfig | undefined>(undefined);
  private injector = inject(Injector);
  readonly assetsPath = `assets/`;

  readonly config$ = this.configSubject.asObservable().pipe(
    filter((config): config is AppConfig => !!config)
  );

  constructor(
    private http: HttpClient,
    private hsConfig: HsConfig,
    private hsLanguageService: HsLanguageService
  ) {}

  initializeConfig() {
    const configURL = `${this.assetsPath}config.json`;
    const config$ = this.http.get<AppConfig>(configURL).pipe(
      catchError(() => of({ /* default config */ } as AppConfig))
    );

    const translations$ = this.getConfigFile("translations");

    forkJoin([config$, translations$])
      .pipe(
        tap(([config, translationOverrides]) => {
          // Update config and set translation overrides
          this.updateConfig(config, translationOverrides);
        }),
        switchMap(([config, _]) => this.waitForTranslationsToLoad(config)),
        switchMap(() => this.fetchLayersAndLocations(config))
      )
      .subscribe({
        /* Passing value to configSubject which 'starts' the application */
        next: (finalConfig) => this.configSubject.next(finalConfig),
        error: (error) => {
          console.error("An error occurred while initializing the configuration:", error);
          this.configSubject.next(/* default config */);
        },
      });
  }

  private updateConfig(config: AppConfig, translationOverrides: any) {
    // Update HSLayers config
    this.hsConfig.update({ ...this.baseConfig, ...config.hslayersConfig, translationOverrides });
  }

  private waitForTranslationsToLoad(config: AppConfig): Observable<boolean> {
    const currentLoader = this.hsLanguageService.getTranslator().currentLoader as WebpackTranslateLoader;
    const loadedLanguages$ = toObservable(currentLoader.loadedLanguages);

    return loadedLanguages$.pipe(
      filter((loadedLanguages) => loadedLanguages[config.hslayersConfig.language || "en"]),
      take(1),
      tap((loadedLanguages) => {
        //Mark loaded lanaguages as loaded via the initializer to prevent unnecessary reloads
        //of the translations. Reload may cause the translations to not be available on first compoonent paint
        currentLoader.loadedViaInitializator.push(...Object.keys(loadedLanguages));
        currentLoader.loadedViaInitializator.push(...Object.keys(loadedLanguages));
      })
    );
  }
}

2. APP_INITIALIZER Setup

Set up the APP_INITIALIZER in a module:

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AppConfig} from './aiConfig.service';

@NgModule({
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      useFactory: (config: AppConfigService) => {
        return () => {
          config.initializeConfig();
          return config.config$.pipe(take(1));
        };
      },
      deps: [AppConfigService],
    },
  ],
})
export class InitializerModule {}

How It Works

  1. The initializeConfig method in AppConfigService is called during app initialization.
  2. It uses forkJoin to load both the main configuration and translations simultaneously:
    forkJoin([config$, translations$])
  3. After loading, it updates the app configuration and sets the translation overrides:
    tap(([config, translationOverrides]) => {
      this.updateConfig(config, translationOverrides);
    })
  4. It then waits for the translations to be fully loaded and marks loaded langugages as loadedViaInitializator which is cructial. This step prevents additional reload of the translation which is normally used to add translationOverrides
    switchMap(([config, _]) => this.waitForTranslationsToLoad(config))
  5. Once translations are loaded, it fetches additional configurations (layers and locations):
    switchMap(() => this.fetchLayersAndLocations(config))
  6. Finally, it emits the complete configuration:
    next: (finalConfig) => this.configSubject.next(finalConfig)
Clone this wiki locally