core
core copied to clipboard
Lazy-loaded modules dont get their translations when "isolate" is false.
Current behavior
-
Alternate i18n files are not loaded when using lazy-loaded modules if "isolate" param is false. So the module can access the main file translations but not theirs.
-
When "isolate" is true the file is correctly loaded but the module doesn't have access to previously loaded translations.
Expected behavior
Lazy loaded modules should be able to load their own translation files and at the same time being able to access previously loaded translation files as stated in the docs.
How do you think that we should fix this?
Minimal reproduction of the problem with instructions
For reproduction please follow the steps of the ngx-translate docs in a freshly angular created application with one or more lazy-loaded modules and one shared module exporting TranslateModule.
Environment
ngx-translate version: 12.1.2
Angular version: 9.1.0
Browser:
- [ ] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
For Tooling issues:
- Node version: 10.15.2
- Platform: Linux
Others:
Found a workaround to this issue thanks to a @ye3i comment in a PR. translateService.currentLang = ''; translateService.use ('en');
The trick seams that ngx-translate wont load anything if the language doesn't change one way or another. So when isolate is false the currentLang inherits from parent and if it is the same as the one in "use" it wont make the neccesary http request.
app.module.ts
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/app/', '.json');
}
@NgModule({
declarations: [AppComponent],
imports: [
HttpClientModule,
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useFactory: createTranslateLoader, deps: [HttpClient] }
})
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
app/en.json
{
"action": "Create"
}
Lazy loaded
order.module.ts
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/order/', '.json');
}
@NgModule({
declarations: [OrderComponent],
imports: [
TranslateModule.forChild({
loader: { provide: TranslateLoader, useFactory: createTranslateLoader, deps: [HttpClient] },
isolate: true
})
]
})
export class OrderModule {}
order/en.json
{
"title": "Order"
}
order.component.html
<div>{{'title' | translate}}</div>
<div>{{'action' | translate}}</div>
app.component.html
<div>{{'action' | translate}}</div>
<div>-----Order Component-----</div>
<app-order></app-order>
Result
Create
-----Order Component-----
Order
action
How to access "action" key in order component?
I've got the same issue here, even if I try every combinaison with extend boolean, it doesn't change anything.
RootModule : isolate: false ChildModule (lazy loaded): isolate :false => I access root translations but I don't have my child's translations isolate: true => I access the child's translations but not the root ones
Is there anything I didn't understand ? The documentation in README states
To make a child module extend translations from parent modules use extend: true. This will cause the service to also use translations from its parent module.
Does extend not combine with isolate ? Then, how do you limit propagation of child translations but profit from parent's ones ?
Thanks
I have the same problem. Do you have a solution?
Nop, didn't get any movement here. Have no solution.
My workaround until this issue has been resolved is following:
- Ensure configs
isolate: false
andextend: true
are set in both root and child modules - Set current language in the root component (AppComponent or similar):
this.translateService.use(lang);
- In the lazy loaded module reset the current language to make sure the translations get retrieved:
const currentLang = translateService.currentLang;
translateService.currentLang = '';
translateService.use(currentLang);
did anyone found a solution yet? I tried all of the above
Have a look at this. https://www.youtube.com/watch?v=NJctHJzy5vo
@ocombe are you planning to fix this issue in the immediate future?
works for me with @Juusmann solution , thanks!
@Juusmann's solution works for me as well. To be extremely specific and to add clarity to anyone still wondering how it all fits together, I have the following setup (using abbreviated module definitions):
AppModule
...
// AoT requires an exported function for factories.
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
...
imports: [
...
// NOTE: Normally we'd stick TranslateModule in `CoreModule` but the ability to lazy load
// module translations and extend the main one only works if you set it up in the root `AppModule`.
// Use the TranslateModule's config param "isolate: false" to allow child, lazy loaded modules to
// extend the parent or root module's loaded translations.
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
},
isolate: false
}),
]
SharedModule
@NgModule({
declarations: DECLARATIONS,
imports: [
...MODULES,
...TranslateModule
],
exports: [
...MODULES,
...TranslateModule
...DECLARATIONS,
]
})
export class SharedModule {
}
LazyLoadedModule
...
// AoT requires an exported function for factories.
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, './assets/i18n/'lazy-load, '.json');
}
...
imports: [
...
// Use the TranslateModule's config param "extend: true" to extend the parent or root module's
// loaded translations.
TranslateModule.forChild({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
},
extend: true
}),
]
export class LazyLoadedModule {
constructor(protected translateService: TranslateService) {
const currentLang = translateService.currentLang;
translateService.currentLang = '';
translateService.use(currentLang);
}
}
Key Points
- Use the
TranslateModule's
methodforRoot()
with config paramisolate: false
inAppModule
to allow child, lazy loaded modules to extend the parent or root module's loaded translations. - Use the
TranslateModule's
methodforChild()
with config paramextend: true
inLazyLoadedModule
to extend the parent or root module's loaded translations. -
DO NOT attempt to move the
TranslateModule
setup and configuration fromAppModule
to aCoreModule
as it won't allow the root translations and only works when setup directly inAppModule
. - Force
TranslateModule
to load theLazyLoadedModule's
child translations by setting the locale on theTranslateService
in its constructor.
In addition to @brianmriley solution I also had to do the following in app.component.ts constructor before this would work. Only issue I can see is that it now loads all lazy feature modules .json files upfront and not when the lazy route is hit.
// this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang('en-GB');
// the lang to use, if the lang isn't available, it will use the current loader to get them
translate.use('en-GB')
For lazy-loaded modules with different translation loaders (loading .json
from different files) it seems to be either (in the case of the lazy-loaded):
- (LazyModule
isolate: false
,extend: true
) React to parent module translation events automatically without having to connect anything, just as they say, but cannot load the lazy loaded specific files. - (LazyModule
isolate: true
,extend: true
) We have to propagate changes to parent's translation event changes to the lazy child ourselves, and we can have our specific translations working! But the parent's translation won't work.
It's like I can't blend the two.
I got pretty close though maybe you could have a look and play within StackBlitz
: https://stackblitz.com/edit/translations-and-lazy-loading?file=README.md
@docwhite did you solve it? I also configured the translateService again to set the currentLang. But didn't work
@docwhite did you solve it? I also configured the translateService again to set the currentLang. But didn't work
My company needed something as soon as possible, so I sadly decided to go with another tool called transloco.
I'm a bit scared about ngx-translate getting a little bit left behind (by looking at the last release being like 1 year ago) yet being still a standard. I heard the main developer moved to another job working with the Angular team and this project has been a bit left to the community which doesn't know as much as the actual creator.
I'm happy to come back to it and keep cracking this issue with modular translations. We work with the monorepo workflow as Nx recommend and this is a must for me, and since I saw a Nx example with scopes in transloco I decided to give it a whirl.
You know you can't stay for too long trying to solve an issue when you work for a company :(
I left this example StackBlitz to see if someone can crack the problem and come up with a solution, I couldn't. I am subscribed to this issue so I would be very happy to see someone solve it and then I would get back to ngx-translate.
We are currently facing the same issue. Is there any progress in this?
@KissBalazs No progress on my side. That was a blocker for me. With transloco it's working well. Maybe taking his idea of scopes and the injection system they use and bring it to ngx-translate could help.
I ran into this issue when using TranslateModule.forRoot() outside of app.module. Make sure that you provide forRoot only inside app.module.
For every one still stuck with this issue: So this is my solution to load both lazy loaded module json files and app Module json files : app module
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (http: HttpClient) => (new TranslateHttpLoader(http, './assets/i18n/app/', '.json')),
deps: [HttpClient]
},
}),
app component
this.translate.currentLang = '';
// retrieve the lang from url or user preferences
this.translate.use(lang);
// dispatch lang change to store to update other modules (its just a way of doing)
lazy module
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: (http: HttpClient) => (new TranslateHttpLoader(http, './assets/i18n/lazy/', '.json')),
deps: [HttpClient]
},
extend: true,
}),
lazy module component
this.translateS.currentLang = '';
// listen for changes from store and set the lang or set it explicitly..
translate.use(lang)
So the key points are
- no need to add : isolate true in lazy module
- unsure to add : translateService.currentLang = ''; to every module entry component
Tip to avoid adding this
this.translate.currentLang = '';
translate.use(lang)
to every component in a module, my solution is to create a container component that contain this logic and set all other component as children of that component Tip example
- before
const routes: Routes = [{
{ path: 'path1', component: Component1 },
{ path: 'path2', component: Component2 },
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
- After
const routes: Routes = [{
path: '', component: ContainerComponent, children: [
{ path: 'path1', component: Component1 },
{ path: 'path2', component: Component2 },
]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Of cours don't forget to add <router-outlet></router-outlet>
in the container component HTML
any news on this? I think this is related to my issue, I'm trying to translate a specific module which is loaded on lazy-loaded module, with no success.
So my CustomModule is imported in LazyModuleA < import CustomModule LazyModuleA has the forChild() configuration of ngx-translate pointing to specific /lazy-a/en.json file But when I'm trying to use a specific translation for CustomModule /custom/en.json isn't working at all.
Thanks @brianmriley it works, so now on use translateService to get Key values dont works this._translate.get(['home', 'lazyModule']).subscribe()
this only send me translate keys from the lazyModule i fix this using this._translate.stream(['home', 'lazyModule']).subscribe()
For lazy-loaded modules with different translation loaders (loading
.json
from different files) it seems to be either (in the case of the lazy-loaded):
- (LazyModule
isolate: false
,extend: true
) React to parent module translation events automatically without having to connect anything, just as they say, but cannot load the lazy loaded specific files.- (LazyModule
isolate: true
,extend: true
) We have to propagate changes to parent's translation event changes to the lazy child ourselves, and we can have our specific translations working! But the parent's translation won't work.It's like I can't blend the two.
I got pretty close though maybe you could have a look and play within
StackBlitz
: https://stackblitz.com/edit/translations-and-lazy-loading?file=README.md
Thanks for the valuable replay, i did same but nothing worked for me, i got it working now this is what i did.... Working Demo - https://translations-and-lazy-loading-rfa5v2.stackblitz.io
- In eager loaded module simplay
TranslateModule.forChild()
nothing need to be added in component constructor - In lazy loaded module we need to update module as well as component // in lazy and root module extend should be true
// in lazy module
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
extend: true,
}),
// in lazy component ( without it it's not working )
// this.translate.getDefaultLang() <- use any lang you wish here 'en', 'jp' etc
ngOnInit() {
this.translate.use(this.translate.getDefaultLang());
}
- In root module (app.module ) simply put
extend: true,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: ModuleHttpLoaderFactory,
deps: [HttpClient],
},
extend: true,
}),
Note Although it works for lazy module , eager module and root app module but still have big problem when we move to different lazy module its taking translation from previously visited lazy route that's not what we want.
Finally Got Perfect Solution ( what i wanted)
- want to get translation from root and lazy module
- i18n files should be fetched once
- should have no code duplicy of createTranslateLoader across all lazy modules
Here I got the solution to above problem, Demo - https://hs2504785.github.io/ngdemos/i18napp2 Source Code - https://github.com/hs2504785/ngdemos
Thanks to Chatgpt, almost i gave up, and was thinking to stop thinking about it :), was in position to say bye bye to Chatgpt but finally it gave me something that worked like charm, here is our conversation with it
https://github.com/hs2504785/ngdemos/blob/master/docs/images/i18n.png
my work around
import {
Inject,
Injectable,
InjectionToken,
ModuleWithProviders,
NgModule,
Provider,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { languagesList } from './translations.helper';
import { removeConstStringValues } from './translations.helper';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import {
Observable,
catchError,
firstValueFrom,
forkJoin,
from,
map,
of,
skip,
throwError,
} from 'rxjs';
import { AppTranslateService } from './translate.service';
@Injectable()
export class TranslateModuleLoader implements TranslateLoader {
constructor(
@Inject(TRANSLATE_MODULE_CONFIG)
private configs?: TranslateModuleConfig<any>[]
) {}
getTranslation(lang: languagesList): Observable<any> {
const emptyTranslate = () => firstValueFrom(of({ default: {} }));
// console.log('TranslateModuleConfig getTranslation:', this.configs);
const lazyTranslations = (
config: TranslateModuleConfig<any>
): Promise<{
default: removeConstStringValues<translationsObject>;
}> => {
switch (lang) {
case 'none': {
return emptyTranslate();
break;
}
case 'he':
case 'en': {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-extra-non-null-assertion
return config?.translationsChunks?.[lang]!?.();
break;
}
default: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-extra-non-null-assertion
return config?.translationsChunks?.['he']!?.();
break;
}
}
};
return forkJoin([
...this.configs.map((config) =>
from(lazyTranslations(config) || emptyTranslate()).pipe(
map((x) => x?.default || {}),
catchError(() =>
throwError(
() => new Error(`Please check language ${lang} is supported`)
)
)
)
),
]).pipe(
// tap((x) => {
// debugger;
// }),
map((x) => Object.assign({}, ...x))
// tap((x) => {
// debugger;
// })
);
}
}
export const TRANSLATE_MODULE_CONFIG: InjectionToken<
TranslateModuleConfig<any>
> = new InjectionToken<TranslateModuleConfig<any>>('TranslateModuleConfig');
export const TranslateModuleConfigDefault: Partial<TranslateModuleConfig<any>> =
{};
export const TranslateModuleConfigProvider = (
config: TranslateModuleConfig<any>
): Provider => {
const mergedConfig = { ...TranslateModuleConfigDefault, ...config };
return {
provide: TRANSLATE_MODULE_CONFIG,
useValue: mergedConfig,
multi: true,
};
};
type TranslateModuleConfigTranslations<
defaultTranslations extends translationsObject,
T extends languagesList = languagesList
> = {
// defaultLanguage: T;
defaultLanguage?: T;
supportedLanguages?: T[];
moduleType: 'root' | 'child' | 'lazyChild';
translationsChunks: {
[P in Exclude<T, 'none'>]: P extends 'he'
? () => Promise<{ default: defaultTranslations }>
: () => Promise<{
default: removeConstStringValues<defaultTranslations>;
}>;
};
};
type StringsJSON = { [k: string]: string | StringsJSON };
type translationsObject = {
[k: `${'LIBS' | 'APPS'}_${string}_${string}`]: StringsJSON;
};
type TranslateModuleConfig<
defaultTranslations extends translationsObject
// T extends languagesList = languagesList
> =
// {
// [P in T]:
TranslateModuleConfigTranslations<defaultTranslations>;
// }
type TranslateModuleConfigForRoot<
defaultTranslations extends translationsObject
// T extends languagesList = languagesList
> = Omit<Required<TranslateModuleConfig<defaultTranslations>>, 'moduleType'>;
type TranslateModuleConfigForChild<
defaultTranslations extends translationsObject
// T extends languagesList = languagesList
> = Omit<
TranslateModuleConfig<defaultTranslations>,
'moduleType' | 'defaultLanguage' | 'supportedLanguages'
> & {
isLazy: boolean;
};
/**
please import only using forRoot or forChild
```ts
AppTranslateModule.forRoot({
defaultLanguage: 'he',
supportedLanguages: ['he'],
translationsChunks: {
he: () => firstValueFrom(of({ default: he })),
en: () => import('./i18n/en'),
},
});
AppTranslateModule.forChild({
isLazy: true,
translationsChunks: {
he: () => firstValueFrom(of({ default: he })),
en: () => import('./i18n/en'),
},
});
* ```
* @author Mor Bargig <[email protected]>
*/
@NgModule({
declarations: [],
imports: [CommonModule, TranslateModule],
providers: [AppTranslateService, TranslateModuleLoader],
exports: [TranslateModule],
})
export class AppTranslateModule {
constructor(
appTranslateService: AppTranslateService,
translateModuleLoader: TranslateModuleLoader,
@Inject(TRANSLATE_MODULE_CONFIG)
configs?: TranslateModuleConfig<any>[]
) {
if (!configs?.length) {
throw new Error(
'Please use module AppTranslateModule only with forRoot or forChild'
);
return;
}
const rootConfig = configs?.find((config) => config?.moduleType === 'root');
if (rootConfig) {
appTranslateService.init(
rootConfig?.defaultLanguage,
rootConfig?.supportedLanguages
);
} else {
const lazyChildConfig = configs?.find(
(config) => config?.moduleType === 'lazyChild'
);
if (lazyChildConfig) {
const currentLang: languagesList =
appTranslateService.currentLang || appTranslateService?.defaultLang;
appTranslateService.currentLang = '' as any;
appTranslateService.use(currentLang);
}
}
appTranslateService.onLangChange
.pipe(skip(configs?.length))
.subscribe((event) => {
firstValueFrom(translateModuleLoader.getTranslation(event.lang)).then(
(res) => {
appTranslateService.setTranslation(event.lang, res, true);
}
);
});
}
static forRoot<defaultTranslations extends translationsObject>(
config: TranslateModuleConfigForRoot<defaultTranslations>
): ModuleWithProviders<AppTranslateModule> {
// TODO: add environment configuration
const forRoot = TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateModuleLoader,
},
defaultLanguage: config?.defaultLanguage,
});
return {
ngModule: AppTranslateModule,
providers: [
TranslateModuleConfigProvider({ ...config, moduleType: 'root' }),
...forRoot.providers,
],
};
}
static forChild<defaultTranslations extends translationsObject>(
config: TranslateModuleConfigForChild<defaultTranslations>
): ModuleWithProviders<AppTranslateModule> {
const forChild = TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useClass: TranslateModuleLoader,
},
extend: config?.isLazy,
});
return {
ngModule: AppTranslateModule,
providers: [
TranslateModuleConfigProvider({
...config,
moduleType: config?.isLazy ? 'lazyChild' : 'child',
}),
...forChild.providers,
],
};
}
}
my shortest workaround
in AppModule or in CoreModule when you call forRoot for TranslateModule 'isolate' is false
each lazyModule TranslateModule 'isolate' is false , 'extend' is true
and MAIN
point - custom not singleton service from lazy module or in lazy module itself should have
FYI: example in shared core module
export class SomeCoreModule implements OnDestroy {
private destroySubject = new Subject<void>();
constructor(translateService: TranslateService) {
const setLocaleForChildModule = (locale) => {
translateService.currentLoader
.getTranslation(locale)
.pipe(takeUntil(translateService.onLangChange), takeUntil(this.destroySubject))
.subscribe((translations: { [key: string]: string }) => {
translateService.setTranslation(locale, translations);
translateService.onTranslationChange.next({ lang: locale, translations: translations });
});
};
translateService.onLangChange
.pipe(
filter((x) => !!x.lang?.length),
debounceTime(1),
takeUntil(this.destroySubject)
)
.subscribe((event) => setLocaleForChildModule(event.lang));
}
static forRoot(): ModuleWithProviders<SomeCoreModule> {
return {
ngModule: SomeCoreModule,
providers: [
TranslateModule.forRoot({
isolate: false,
loader: {
provide: TranslateLoader,
useClass: CoreTranslateLoader,// for localize core module components and common cases
deps: [HttpRepositoryService],
},
}).providers,
],
};
}
ngOnDestroy(): void {
this.destroySubject.next();
this.destroySubject.complete();
}
}
Profit
I was also facing a similar issue and after some debugging, I found out(perhaps assumption) that for lazy-loaded modules the translations are not provided quickly enough to be made available in HTML. The proof of it is that if you get translations in TS or call a function to get through a TS function or if you add a condition in HTML that will be true after a few seconds then translations will work fine. I figured out the following two solutions to it.
- The first is to load the TranslateModule as forChild in the relevant lazy-loaded module.
- The second is to add TranslateService(@ngx-translate/core) into the providers array of lazy loaded module. I hope this will be helpful to someone facing a similar issue.
https://github.com/ngx-translate/core/issues/1193#issuecomment-735040662
Works for me to. Easy and short. Thanks dude.