core: Lazy loaded module is not adding the new custom translateLoader

I’m submitting a … (check one with “x”)

[x] bug report => check the FAQ and search github for a similar issue or PR before submitting
[ ] support request => check the FAQ and search github for a similar issue before submitting
[ ] feature request

Current behavior When a lazy loaded module is being loaded it triggers the factory for create the new TranslateHttpLoader however it is not adding the translations in the file

Expected/desired behavior Load the translations of the lazy loaded module

Reproduction of the problem I can’t provide a plnkr because but this is the code that i’m using

export function  translateFactory(http: Http) {
        return new TranslateHttpLoader(http, '/metro-apps/maintenances/i18n/', '.json');
}
[...]
TranslateModule.forChild({
    loader: {
        provide: TranslateLoader,
        useFactory: translateFactory,
        deps: [Http]
    }
})

Please tell us about your environment: Kubuntu 16.04

  • ngx-translate version: core=6.0.0 loader: 0.0.3

  • Angular version: 2.4.7

  • Browser: all

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 20
  • Comments: 26 (8 by maintainers)

Most upvoted comments

Just bumped into this as well.

I wanted my app module to fetch translations from “/assets/i18n/{lang}.json” and my lazy loaded modules to fetch from “/assets/i18n/{feature}/{lang}.json” but was unable to get this to work.

The custom loader configured by forChild(…) was ignored.

@atiertant i can’t set isolate to true because i’ll need the parent translations, i was expecting to be able to add more translations based on my current module (lazy)

I never managed to do it. So I created my own service to add translations to the root translateModule. I have a resolver for the routes accessing the lazyloaded module calling loadTranslationModule() before resolving.

@Injectable()
export class LocalizationService {
    private modulesTranslation: any = {};

    constructor(private http: Http, private translate: TranslateService) { }

    loadTranslationModule(module: string): Observable<any> {
        const lang = this.translate.currentLang || 'en';

        if (this.modulesTranslation[module] && this.modulesTranslation[module][lang]) {
            return Observable.of(this.modulesTranslation[module][lang]);
        }

        const uri = `assets/${module}/i18n/${lang}.json`;
        return this.http.get(uri)
            .map(res => res.json())
            .do((i18n: any) => {
                if (!this.modulesTranslation[module]) {
                    this.modulesTranslation[module] = {};
                }
                this.modulesTranslation[module][lang] = i18n;
                this.translate.setTranslation(lang, i18n, true); // add translation to global translations
                console.log(`[Localization] lang (${lang}) loaded for module (${module})...`);
            })
            .catch((err: any) => {
                console.log(`[Localization] lang (${lang}) not found for module (${module})`);
                return Observable.of({});
            });
    }
}

@atiertant no exactly, because what about keeping the same instance? what the forChild method needs to do is only insert the new translations keeping the same instance of the TranslateService

I’ve implemented a solution for this issue which seems to work. Being relatively a newbie in Angular, I would appreciate your comments.

The solution loads the translation for lazy loaded modules (once they are loaded) and merges that into a single translation service. It supports switching languages before and after lazy loaded modules are loaded.

I’m using a service (LazyAPIService) which is used to collect data from every loaded module. It can be used to get notification anywhere when any lazy module is loaded, but I primarily use it to load translations.

Each module calls the Add function to add itself in the module constructor, with a unique name and the translation asset path to the list (modules might be loaded more then once when used as child modules of lazy loaded modules, so we do not add a module if the name already exists)

MultiTranslateHttpLoader is used as the translation loader. It loads the translation for each module added to the LazyAPIService. It uses the deepmerge “all” function to merge the loaded JSON translations into a single JSON object.

Since the loader holds the injected singleton LazyAPIService, loading other languages (which will call getTranslation) will load the language from all the added modules so far (including the lazy loaded ones).

The MultiTranslateHttpLoader marks a flag once it finishes it’s initial load on the app.module startup. This means that any module added afterwards is a lazy loaded module. So, any module added after the flag is set, also calls AddTranslation to load and merge just the translation file for the added module (for all languages already loaded. I could not find a way to not load the module translation for languages that are NOT the current, but already loaded, but still make them load if that language is selected later on. Let me know if you solve that. I also tried to reset the other languages, but that did not trigger loading them again when used)

Since LazyAPIService is constructed before the TranslationService, I use the Injector service to manually retrieve the TranslateService when needed in the AddTranslation.

Don’t forget to add the assests in angular.json: “src/assets”, { // add this for every module that has translation “glob”: “**/*”, “input”: “./src/app/packages/admin/assets/”, “output”: “./assets/” }


// LazyModuleData.ts  *******************************************
export interface LazyModuleData {
    name: string;
    translationPath?: string;
}

// lazy-api.service.ts  *******************************************
import { Injectable, Injector } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { LazyModuleData } from '../interfaces/LazyModuleData';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient} from '@angular/common/http';


@Injectable({
    providedIn: 'root'
})
export class LazyAPIService {
    private _moduleLoadedNotify = new Subject<LazyModuleData>();

    get moduleLoadedNotify(): Observable<LazyModuleData> {
        return this._moduleLoadedNotify.asObservable();
    }

    private _modules: Array<LazyModuleData> = [];
    private _translationLoaded = false;
    get translationLoaded() {
        return this._translationLoaded;
    }

    set translationLoaded(value: boolean) {
        this._translationLoaded = value;
    }

    get modules() {
        return this._modules;
    }

    constructor(
        private injector: Injector,
        private http: HttpClient
      ) {
    }

    public Add(module: LazyModuleData) {
        // do not add if already added.
        for (var m of this._modules) {
            if (m.name === module.name)
                return;
        }
        this._modules.push(module);
        if (this._translationLoaded)
            this.AddTranslation(module);
        this.Notify(module);
    }

    public Notify(module: LazyModuleData) {
        this._moduleLoadedNotify.next(module);
    }


    public AddTranslation(module: LazyModuleData) {
        let translate = this.injector.get(TranslateService);
        for (let l of translate.langs) {
            const path = module.translationPath + l + ".json";
            console.log("getting translation from ", path);
            this.http.get(path).subscribe(
                res => {
                    console.log("got translation!");
                    console.log(res);
                    translate.setTranslation(l, res, true);
                },
                error => {
                    console.log("error getting translation");
                    console.log(error);
                }
            );
        };

    }
}

// multi-translate-http-loader.ts  *******************************************
import { HttpClient } from "@angular/common/http";
import { TranslateLoader } from "@ngx-translate/core";
import { Observable, forkJoin, of } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { all } from "deepmerge";
import { LazyAPIService } from './services/lazy-api.service';

export class MultiTranslateHttpLoader implements TranslateLoader {
    constructor(
        private http: HttpClient,
        private lazyAPI: LazyAPIService
    ) { }

    public getTranslation(lang: string): Observable<any> {
        // this makes sure that from now on, we load and merge translation for addition (lazy loaded) modules.
        this.lazyAPI.translationLoaded = true;
        // load translation of all added modules.
        const requests = this.lazyAPI.modules.map(module => {
            const path = module.translationPath + lang + ".json";
        return this.http.get(path).pipe(catchError(res => {
            console.error("Could not find translation file:", path);
            return of({});
        }));
    });
        return forkJoin(requests).pipe(map(response => all(response)));
    }
}

// app.module.ts  *******************************************
// in @NgModule imports:
        TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: HttpLoaderFactory,
                deps: [HttpClient, LazyAPIService]
            }

// http loader factory function
export function HttpLoaderFactory(
    http: HttpClient,
    lazyAPI: LazyAPIService
) {
    lazyAPI.Add({ name: "app", translationPath: "./assets/translate/" });
    return new MultiTranslateHttpLoader(http, lazyAPI);;
}

// lazy loaded (or any other) module contructor *****************************
    constructor(private lazyAPI: LazyAPIService) {
        lazyAPI.Add({ name: "admin", translationPath: "./assets/translate/admin/" });
    }

I wrote a article about how to have 1 json file per lazy loaded module without having to write a new Custom Loader etc… it’s quiet simple, only the documentation is not clear in fact: https://medium.com/@TuiZ/how-to-split-your-i18n-file-per-lazy-loaded-module-with-ngx-translate-3caef57a738f

I worked around this issue with a custom MissingTranslationHandler as the correct currentLoader is set on the lazily loaded module translate service.

In my lazy loaded module

export function HttpLoaderFactory(http: Http) {
  // define custom resolution path for translation
  return new TranslateHttpLoader(http, './assets/i18n/association/', '.json');
}
...
    TranslateModule.forChild({
       loader: {
        provide: TranslateLoader,
        useFactory: (HttpLoaderFactory),
        deps: [ Http ]
      },
      missingTranslationHandler: {
        provide: MissingTranslationHandler,
        useClass: CustomMissingTranslationHandler
      }
    })

and the common missing translation handler:

export class CustomMissingTranslationHandler implements MissingTranslationHandler {
  handle(params: MissingTranslationHandlerParams) {
    return params.translateService.currentLoader.getTranslation(params.translateService.currentLang)
      .map(r => {
        const trad = r[params.key];
        if (trad) {
          return trad;
        } else {
          const prefix = (<any>params.translateService.currentLoader).prefix;
          console.warn(`translation not found for key ${prefix} ${params.key} in ${params.translateService.currentLang}`);
          return `**${params.key}**`;
        }
      });
 }
}```

and the global loader for common translations
```typscript
export function authServiceBuilder(backend: XHRBackend, options: RequestOptions, authService: AuthenticationService) {
  return new HttpService(backend, options, authService);
}
...
TranslateModule.forRoot({
  loader: {
    provide: TranslateLoader,
    useFactory: HttpLoaderFactory,
    deps: [ Http ]
  },
  missingTranslationHandler: {
    provide: MissingTranslationHandler,
    useClass: CustomMissingTranslationHandler
  }
})