Skip to content

Lazy loaded module is not adding the new custom translateLoader #444

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
jlberrocal opened this issue Feb 23, 2017 · 26 comments
Closed

Lazy loaded module is not adding the new custom translateLoader #444

jlberrocal opened this issue Feb 23, 2017 · 26 comments

Comments

@jlberrocal
Copy link
Contributor

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

@dhardtke
Copy link
Contributor

Might be the same issue that I have: #425?

@jlberrocal
Copy link
Contributor Author

Yes is similar, I try your solution but it didn't work for me

@k-schneider
Copy link

k-schneider commented Feb 26, 2017

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.

@jlberrocal
Copy link
Contributor Author

what @k-schneider describes is exactly the same that i reported

@atiertant
Copy link

the problem is that you must reconfigure the service in all lazy loaded modules but if you didn't set isolate to true and the lang has already been loaded by an other module, lang is not reloaded...

but if you set isole to true lazy loaded modules are totaly independant so if you would like use translations from parent modules and new others for this modules and its childs only, this is not working...

@jlberrocal
Copy link
Contributor Author

@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)

@jlberrocal
Copy link
Contributor Author

will be nice to take advantage of lazy loading even with the translations, so we can add the needed translations for the module without lose the main (root) translations. actually i'm trying with this approach

export default class MaintenancesModule {
    constructor(http: Http, translate: TranslateService) {
        http.get(`/metro-apps/maintenances/i18n/${translate.currentLang}.json`)
            .map((resp: Response) => resp.json())
            .subscribe((i18n: Object) => {
                translate.setTranslation(translate.currentLang, i18n, true);
            });
    }
}

but the instruction for set the new translations is being omited

@atiertant
Copy link

@jlberrocal think in isolate mode when store has no translation for the key, it should ask his parent using injector.parent recursivly

@jlberrocal
Copy link
Contributor Author

@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

@atiertant
Copy link

@jlberrocal have a look at #379 @ocombe wouldn't like to change all spec and wanted to have one instance of service by lazy loaded modules to avoid this changes... this is a good idea but not working as expected for now... the problem is service configuration is saved in his instance, so new service instance need reconfigure. the second problem is that store doesn't identify service instance so when the root loader already loaded a lang, new loader isn't called...

@sebelga
Copy link

sebelga commented Mar 28, 2017

I am facing the same problem where the lazy loaded forChild loader never being called (unless I set isolate to true). Is there a way around it?

I tried to call translate.use('en') in my component from the lazy loaded module but it didn't trigger the load.

thanks!

@Hadev-JHH
Copy link

Would be really ace if it would no longer be required to set isolate in order to get lazy loading without having to provide 'common' translations in every lazy loaded module. I don't want to use isolate because I really want only 1 instance of TranslateService (hence using the forChildren).

@avilao
Copy link

avilao commented Jun 1, 2017

I also have the same problem as @sebelga . The translate.use('xx'); does not load the new translations. Any thoughts?

@chris08002
Copy link

@avilao Did you set isolate: true? The seperate translation provider works for me when isolating (and then setting the language again), but it doesn't work when not isolating.

@sebelga
Copy link

sebelga commented Jun 15, 2017

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({});
            });
    }
}

@jlberrocal
Copy link
Contributor Author

@sebelga never do the action of a subscription in a do(), you have to use subscribe()

@sebelga
Copy link

sebelga commented Jun 15, 2017

This is a resolver. It will automatically subscribe. In resolver you don't return 'Subscription'. It's true that it could have been a "map" instead but I don't think it matters.

@aguerot
Copy link

aguerot commented Jul 17, 2017

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
  }
})

@Tuizi
Copy link

Tuizi commented Sep 23, 2017

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

@Ludevik
Copy link

Ludevik commented Sep 25, 2017

Having same issue with lazy loaded translations.
@Tuizi your solution works because you have isolated translation services.

What i was looking for was: load shared translations in root module, add additional translations for each lazy loaded module. Don't make request twice when translations for given module are already loaded (eg. when you navigate from one lazy module to another and back). Actually the same as @sebelga posted.

@jlberrocal
Copy link
Contributor Author

@Ludevik this doesn't work for you?

@Ludevik
Copy link

Ludevik commented Sep 25, 2017

@jlberrocal it works, but it is just a part of what we need. We don't want to make http request each time we navigate between lazy loaded modules. I currently added some additional module which prevents requesting translations for same module twice.

@ratidzidziguri
Copy link

ratidzidziguri commented Jul 7, 2018

@sebelga your sollution is actually really good but how would you handle language switching than? i did something like that with resolver but language switching never works for me :(
Would you please provide any simple repo?

@ardent42c
Copy link

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/" });
    }

@AbdoDabbas
Copy link

AbdoDabbas commented Dec 27, 2020

@ardent42c your answer solved my problem of loading the translation files from lazy loaded modules, but now I'm facing an issue:
It doesn't translate if the translation directly in the view you're loading, so I solved it by modifying AddTranslation function like this:

public AddTranslation(module: LazyModuleData) {
            .... code ....
            this.http.get(path).subscribe(
                res => {
                    console.log("got translation!");
                    console.log(res);
                    translate.setTranslation(l, res, true);
                    // new added line to fix the issue:
                    translate.use(l);
                },
                error => {
                    console.log("error getting translation");
                    console.log(error);
                }
            );
        };

    }

@AbdoDabbas
Copy link

AbdoDabbas commented Dec 27, 2020

P.S:
I didn't add this:

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/"
}

I don't know if it's the reason.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests