ngx-translate / core

The internationalization (i18n) library for Angular
MIT License
4.5k stars 572 forks source link

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

Open thessoro opened 4 years ago

thessoro commented 4 years ago

Current behavior

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

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

GuillaumeSpera commented 3 years ago

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

Juusmann commented 3 years ago

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 commented 3 years ago

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

Stusaw commented 3 years ago

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

BrandoCaserotti commented 3 years ago

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

petogriac commented 3 years ago

works for me with @Juusmann solution , thanks!

brianmriley commented 3 years ago

@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

Stusaw commented 3 years ago

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 commented 3 years ago

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

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 commented 3 years ago

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

eulersson commented 3 years ago

@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 commented 2 years ago

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

eulersson commented 2 years ago

@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 commented 2 years ago

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

rabiedadi commented 2 years ago

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

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 commented 2 years ago

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 commented 1 year ago

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 commented 1 year ago

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)

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 commented 1 year ago

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 commented 1 year ago

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 commented 11 months ago

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 commented 6 months ago

https://github.com/ngx-translate/core/issues/1193#issuecomment-735040662

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