ngx-translate / core

The internationalization (i18n) library for Angular
MIT License
4.51k stars 577 forks source link

Calling `get()` right before json is loaded is returning the key itself. #641

Open iget-master opened 7 years ago

iget-master commented 7 years ago

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 this.translateService.get('MY.KEY.NAME').subscribe(console.log); // Console logs: "MY.KEY.NAME" instead of "The translated key value"

Expected/desired behavior

Since it's an observable, the expected result is that the service wait for the translations load before resolving the promise.

Reproduction of the problem

I'm using it on Ionic 3, so I have to override the loader to make things work. I'm overriding it with this:

Module import:

    TranslateModule.forRoot({
        loader: {
            provide: TranslateLoader,
            useFactory: (createTranslateLoader),
            deps: [Http]
        }
    }),

The loader:

export function createTranslateLoader(http: Http) {
    return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

Then try to use translateService.get() on the very first line of code you have, like App component's constructor.

Please tell us about your environment:

mgamsjager commented 7 years ago

Came across a similar issue in our Anguar 4 project. I eventually mitigated it by wrapping the get() in setTimeout with a timeout of 0

patapron commented 7 years ago

Same

seangwright commented 7 years ago

I'm running into a similar issue, though wrapping in a setTimeout didn't solve my problem.

I'm putting all my calls to translate.get('Content.Id') in an array and passing that to Observable.forkJoin so that I get all my translations in one call instead of subscribing to get all of them.

The data being emitted by forkJoin is an array of all my Content.Id values passed to my get calls.

Observable.forkJoin([translate.get('Content.Id1'), translates.get('Content.Id2')])
    .subscribe(data => console.log(data)); //=> ['Content.Id1', 'Content.Id2'];

Edit ---

I see - you need to put your calls to get in a setTimeout, not the subscribe calls to the Observables that get returns.

As I'm debugging what's going on inside translate.service.js I can see in the call to get this.pending is false but this.translations is an empty object, which is caused by the call to get executing before the call to getTranslation is made.

Where can I make my calls to this.translate.setDefaultLang('en-US') and this.translate.use(this.localeId); so that the translation is loaded into the app before any calls to get are made?

iget-master commented 7 years ago

@sgwatgit In my case, the problem is that I use the translation service inside other services, that was injected on App component, and I configure the translation service default language only on App component's constructor, so the service that depends on it was already loaded and trying to use it.

To solve it, I've extended the translation service and added the configuration to it constructor:

That's my TranslateService:

import { Inject, Injectable }                                                                                                      from '@angular/core';
import { MissingTranslationHandler, TranslateCompiler, TranslateLoader, TranslateParser, TranslateService as NgxTranslateService } from '@ngx-translate/core';
import { TranslateStore }                                                                                                          from '@ngx-translate/core/src/translate.store';
import 'rxjs/add/operator/map';

import { AppConfig, appConfig }                                                                                                    from '../app/app.config';

@Injectable()

export class TranslateService extends NgxTranslateService {
    constructor(
        store: TranslateStore,
        currentLoader: TranslateLoader,
        compiler: TranslateCompiler,
        parser: TranslateParser,
        missingTranslationHandler: MissingTranslationHandler,
        @Inject(appConfig) private appConfig: AppConfig,
    ) {
        super(store, currentLoader, compiler, parser, missingTranslationHandler, true, false);
        this.setDefaultLang(this.appConfig.defaultAppLanguage);
        this.use(navigator.language);
    }
}

Then added this to the providers on my App module:

{provide: NgxTranslateService, useClass: TranslateService},

This replaces the NgxTranslateService with my own Translate service that extends it with configurations.

seangwright commented 7 years ago

@iget-master That's a really nice pattern!

I ended up going a different direction. Since my project is using template i18n directives from @angular and the cli to do .xlf file generation, we have separate builds for each language. I removed all calls to get and replaced them with instant.

I then set the translations by importing the json file (via ES module) to my AppModule and loading them with my own custom loader class. I also set the locale there (in my APP_INITIALIZER fn) by importing the value from a file.

// AppModule.ts
import { locale } from '../i18n/locale';
import * as translations from '../i18n/runtime-translations.json';

export function appInitializer(
    translate: TranslateService) {
    return () => new Promise(resolve => {
        translate.setDefaultLang('en-US');
        translate.use(locale);

        // ... other init code
    });
}

export class StaticLoader implements TranslateLoader {
    getTranslation(lang: string): Observable<any> {
        return Observable.of(translations);
    }
}

My import of the TranslateModule looks like this

TranslateModule.forRoot({
    loader: {
        provide: TranslateLoader,
        useClass: StaticLoader
    }
})

I convert my .po to .json using a build process so that its available at the above path for each build of the app I perform for each language.

philip-firstorder commented 5 years ago

Got this bug, today, any status on this?

llwt commented 5 years ago

☝️

rodrigograca31 commented 5 years ago

:point_up:

rodrigograca31 commented 5 years ago

Solved my issue using this.translateService.stream instead of this.translateService.get

Code:

this.translateService.stream('LOGIN_ERROR').subscribe((value) => {
    this.loginErrorString = value;
})

this might also help others: this.translateService.instant('LOGIN_ERROR')

Aiganymus commented 5 years ago

Solved my issue using this.translateService.stream instead of this.translateService.get

Code:

this.translateService.stream('LOGIN_ERROR').subscribe((value) => {
  this.loginErrorString = value;
})

this might also help others: this.translateService.instant('LOGIN_ERROR')

What about this issue with memory leak? #727

rxseb commented 4 years ago

Dirty workaround:

translate(key: string): Promise<string> {
    const message$ = defer(async () => this.translateService.instant(key));
    return message$
      .pipe(
        delay(10),
        expand(value => value !== key ? EMPTY : message$),
        filter(value => value !== key)
      )
      .toPromise();
}