Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Lazy-loaded modules dont get their translations when "isolate" is false. #1193

Open
thessoro opened this issue Apr 3, 2020 · 26 comments
Open

Comments

@thessoro
Copy link

thessoro commented Apr 3, 2020

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:

@thessoro
Copy link
Author

thessoro commented Apr 3, 2020

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.

@phongca22
Copy link

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?

@GuillaumeSpera
Copy link

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

@Totot0
Copy link

Totot0 commented Nov 14, 2020

I have the same problem. Do you have a solution?

@GuillaumeSpera
Copy link

Nop, didn't get any movement here. Have no solution.

@Juusmann
Copy link

Juusmann commented Nov 28, 2020

My workaround until this issue has been resolved is following:

  1. Ensure configs isolate: false and extend: true are set in both root and child modules
  2. Set current language in the root component (AppComponent or similar): this.translateService.use(lang);
  3. 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);

@ali1996hass
Copy link

did anyone found a solution yet?
I tried all of the above

@Stusaw
Copy link

Stusaw commented Jan 28, 2021

Have a look at this. https://www.youtube.com/watch?v=NJctHJzy5vo

@BrandoCaserotti
Copy link

@ocombe are you planning to fix this issue in the immediate future?

@petogriac
Copy link

works for me with @Juusmann solution , thanks!

@brianmriley
Copy link

brianmriley commented May 10, 2021

@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 method forRoot() with config param isolate: false in AppModule to allow child, lazy loaded modules to extend the parent or root module's loaded translations.
  • Use the TranslateModule's method forChild() with config param extend: true in LazyLoadedModule to extend the parent or root module's loaded translations.
  • DO NOT attempt to move the TranslateModule setup and configuration from AppModule to a CoreModule as it won't allow the root translations and only works when setup directly in AppModule.
  • Force TranslateModule to load the LazyLoadedModule's child translations by setting the locale on the TranslateService in its constructor.

@Stusaw
Copy link

Stusaw commented Jun 9, 2021

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

@eulersson
Copy link

eulersson commented Jul 15, 2021

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

@niroshank
Copy link

@docwhite did you solve it?
I also configured the translateService again to set the currentLang. But didn't work

@eulersson
Copy link

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

@KissBalazs
Copy link

We are currently facing the same issue. Is there any progress in this?

@eulersson
Copy link

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

@DartWelder
Copy link

I ran into this issue when using TranslateModule.forRoot() outside of app.module. Make sure that you provide forRoot only inside app.module.

@rabiedadi
Copy link

rabiedadi commented Apr 29, 2022

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

  1. no need to add : isolate true in lazy module
  2. 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

@BruneXX
Copy link

BruneXX commented Jun 21, 2022

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.

@Darcksody
Copy link

Darcksody commented Feb 9, 2023

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

@hs2504785
Copy link

hs2504785 commented Feb 18, 2023

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

  1. In eager loaded module simplay TranslateModule.forChild() nothing need to be added in component constructor
  2. 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());
  }
  1. 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

@morbargig
Copy link

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 <morb4@fnx.co.il>
 */
@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,
      ],
    };
  }
}

@Khokhlachov
Copy link

Khokhlachov commented Jul 24, 2023

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

@Muzummil
Copy link

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.

  1. The first is to load the TranslateModule as forChild in the relevant
    lazy-loaded module.
  2. 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.

@nugo-cell
Copy link

#1193 (comment)

Works for me to. Easy and short. Thanks dude.

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

No branches or pull requests