Problem with setting translocoConfig using httpcall in factory, when TranslocoService as dependency anywhere
See original GitHub issueI’m submitting a…
[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report
[ ] Performance issue
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request
[ ] Other... Please describe:
Current behavior
After migrating from ngx-translate, transloco can’t load translocoConfig
using factory, as the config object needs to be populated by data from ConfigService (ngx-config), which loads the config file loaded by http call, if the TranslocoService is dependency in any module or provided service, e.g. HttpInterceptor.
I know this is very mouthful so please check the minimal reproduction example app below.
Expected behavior
We expect to be able to get values from ConfigService in customTranslateConfigFactory, just as when using ngx-translate, as the configuration of our app has not changed.
Minimal reproduction of the problem with instructions
Please check the code (you can install and serve the app directly, using docker is not mandatory) in minimal reproduction app and let me explain:
The tech stack in this app is Transloco (migrated from ngx-translate mainly to support Ivy) and ngx-config.
There are slovak and english translation files, identified by lang identifier (sk or en) in the name of the file. The lang identifiers are to be loaded from config file and are replaced in url in HttpInterceptor. Both config and translation json files are loaded by http calls, hosted on cdn. All the logic is currently in app.module.ts:
for config file:
...
export function configFactory(http: HttpClient): ConfigLoader {
return new ConfigHttpLoader(http, 'http://cdn.pelikan.sk/app/transloco-minimal-example/market-config.json');
}
...
ConfigModule.forRoot({
provide: ConfigLoader,
useFactory: (configFactory),
deps: [HttpClient]
}),
...
for transaltions:
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {
constructor(private readonly http: HttpClient) {}
private readonly i18nUrl: string = `http://cdn.pelikan.sk/app/transloco-minimal-example/{lang}-translations.json`;
getTranslation(): Observable<any> {
return this.http.get<Translation>(this.i18nUrl);
}
}
...
providers: [
...
{ provide: TRANSLOCO_LOADER, useClass: TranslocoHttpLoader, deps: [HttpClient] },
...
]
...
all good so far, both files are downloaded, also in correct order. We try to create translocoConfig
using factory, as we need values from ConfigService.
export function customTranslateConfigFactory(config: ConfigService): TranslocoConfig {
const marketLangs: ReadonlyArray<any> = config.getSettings<ReadonlyArray<any>>('market.languages', []);
const availableLangs: Array<string> = marketLangs.map((language: any) => language.code);
const defaultLang: string = marketLangs.reduce((prev: string, curr: any) => curr.default ? curr.code : prev, '');
return translocoConfig({
defaultLang,
availableLangs,
failedRetries : 0,
reRenderOnLangChange : true,
fallbackLang : defaultLang,
prodMode : environment.production
});
}
...
providers: [
...
{ provide: TRANSLOCO_CONFIG, useFactory: (customTranslateConfigFactory), deps: [ConfigService] },
...
]
...
Now… This setup works!
But only as far as the TranslocoService is not used as dependency in anywhere.
In this app, it is used as dependency for CustomHttpInterceptor, where we want to set this.translate.getActiveLang()
as X-App-Language request header. Please note that this use-case is only for example of the behavior, in the enterprise app we want to use injected TranslocoService in many places, e.g. in ngrx Effects etc.
So when TranslocoService is used as dependency of HttpModule (i guess it is as it’s used in HttpInterceptor), translocoConfig does not have the ConfigService data (as it needs HttpClient to download data) and the factory returns empty config values. It is a kind of circle dependency problem.
In current state the app works, because the values that have not been able to be set in module are set imperatively in app.component.ts (ConfigService returns correct config data in component):
const marketCode: string = this.config.getSettings('market.code', '');
const languages: ReadonlyArray<string> = this.config.getSettings('market.languages', []).map((language: any) => language.code);
const defaultLang: string = this.config.getSettings('market.languages', [])
.reduce((prev: string, curr: any) => curr.default ? curr.code : prev, '');
const availableLangs = this.config.getSettings('market.languages', [])
.map((language: any) => language.code);
this.translate.setAvailableLangs(availableLangs);
this.translate.setDefaultLang(defaultLang);
this.translate.setActiveLang(defaultLang);
But this is not the behavior we prefer (setting imperatively). The app also works if we comment out imperative setting mentioned above, and we remove TranslocoService from CustomHttpInterceptor deps (app.module.ts — line 85)
What is the motivation / use case for changing the behavior?
Please note that even when this seems to be ngx-config issue at first sight, I really believe it has to do with Transloco too, so please don’t shove it off the table right away.
Before, when using ngx-translate, the whole config/translate initialization process was the same, except the class provided for TranslateModule looks little bit different:
in ngx-translate, we use TranslateHttpLoader class to load the data:
export class BrowserTranslateLoader implements TranslateLoader {
private readonly _browserLoader: TranslateHttpLoader;
constructor(
private readonly http : HttpClient
) {
this._browserLoader = new TranslateHttpLoader(http, http://cdn.pelikan.sk/app/transloco-minimal-example/{lang}-translations.json);
}
getTranslation(languageCode: string): Observable<any> {
return this._browserLoader.getTranslation(languageCode);
}
}
in Transloco, we load the translation file directly with HttpClient:
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {
constructor(private readonly http: HttpClient) {}
private readonly i18nUrl: string = `http://cdn.pelikan.sk/app/transloco-minimal-example/{lang}-translations.json`;
getTranslation(): Observable<any> {
return this.http.get<Translation>(this.i18nUrl);
}
}
and this is the only difference.
Is there any way to set up this whole config & translation loading process in AppModule only, as with ngx-translate? Or is this simply impossible by design and the config values must be set imperatively from component?
Thanks for any suggestions!
Environment
Angular version: 9.04
Browser: any
For Tooling issues:
- Node version: any
- Platform: any
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:11 (1 by maintainers)
You have a race condition.
customTranslateConfigFactory
function isn’t going to wait untilconfigFactory
request is back.Because based on your code, I can’t see any difference with the loaders.
TranslocoHttpLoader
andBrowserTranslateLoader
are both returns the same results.