jsverse / transloco

🚀 😍 The internationalization (i18n) library for Angular
https://jsverse.github.io/transloco/
MIT License
1.98k stars 190 forks source link

Transloco with Module Federation #608

Open hila opened 1 year ago

hila commented 1 year ago

I've tried to use Transloco in a remote MFE that is being hosted with 'Module Federation'. The MFE - is my original project that so far worked grate with transloco, we have different translations files for each module, currently supporting only 'en'. The Host - a new empty project I just created that hosts a module from the MFE described above.

I've initially tried to share the ngneat/transloco, then I tried to delete all references of transloco from the host, and leave it only in the remote - none of those options works.

The HttpLoader is not being loaded and the function getTranslation is not called.

I also tried deleting the providedIn: root and switching to static forChild... - still no luck

What can be the cause of the translations not being loaded in a remote application in module federation?

NetanelBasal commented 1 year ago

Can you prepare a basic reproduction and upload it to Github, please?

jdiemke commented 1 year ago

Seems like transloco does not allow translations in a lazy loaded module. This is necessary in order to support micro frontends.

ViktorMedvedchuk commented 1 year ago

the same problem, I am using Module federation in my micro frontend, and when I try to inject TranslocoLocaleService or TranslocoService into my child app component, I can't, see NullInjectorError: No provider for InjectionToken TRANSLOCO_TRANSPILER! It works in the parent Shell but doesn't work in the child module that I try to upload lazily to the parent

jdiemke commented 1 year ago

You can setup transloco as shared dependency and then use a scope with custom loader for your lazy loaded micro frontend. This worked for me but requires the shell app to provide transloco as root and each lazy loaded module to have scope with custom loader and transloco module imported. Let me know if you need more details.

ghaschel commented 1 year ago

@jdiemke that would be nice, because I am trying to setup this and I am having a lot of trouble

jdiemke commented 1 year ago

A word of warning taken from Building Micro-Frontends by Luca Mezzalira:

Never use the application shell as a layer to interact constantly with micro-frontends during a user session. The application shell should only be used for edge cases or initialization. Using it as a shared layer for micro-frontends risks having a logical coupling between micro-frontends and the application shell, forcing testing and/or redeployment of all micro-frontends available in an application. This situation is also called a distributed monolith and is a developer’s worst nightmare.

Having that said, lets see how integrating Transloco into micro frontends using module federation could look like.

How to use Transloco in the Context of Module Federation

The standard way Transloco uses to load translations totally makes sense in case of a single monolithic SPA. In case of micro frontends the root module of Angular is provided by the shell app and should by design know nothing about the remotes. Hence, the translations of the remotes are not known to the shell app and therefor can not be specified in the i18n folder of the shell app.

One might think, that the remote should include its own transloco instance and take care of translations itself. This approach does not work. Transloco allows only one instance at the root module. Hence, all lazy loaded remote modules share the same global Transloco instance. The only way to make Transloco work in a module federated application is therefor to define transloco as a shared dependency in the partial webpack config of your host and remote.

shared: share({
  ...
  "@ngneat/transloco": {singleton: true},
}),

This solves the problem of having just a single instance at the root level. Still this does not solve the problem of self-contained remotes that bring their own translations. In order to solve this problem each lazy loaded remote module has to provide their own translations.

To make this approach work, two steps are necessary:

  1. The lazy loadable module of the remote (not its root module!) has to include the TranslocoModul. These modules can also access the global transloco scope to use its translations.
  2. In order for the lazy loadable remote module to load its own translations we need to create a so called Transloco scope that includes a custom loader.

Scopes usually are just used to partition translations into multiple separate files. Since scopes also allow to define a custom loader this is the only practical way to let the remote module specify its own loading routine to fetch the modules translations. Hence, Transloco scopes allow deferred loading of translations at the time the lazy loaded module is fetched from the remote. The implementation of the custom loader is up to the developer. There are multiple options:

  1. The loader can load translations that are already bundled into the remote module as part of webpack's bundling step (the example code at the end uses this approach)
  2. The loader can make a remote call to a REST API to fetch the translations over HTTP

It is important to notice, that custom loaders always require a custom scope and that a scope always requires to define a namespace for this scope. This namespsace then needs to be prefixed to the actual key of the translation.

Convenience Utils

In order to simplify the code necessary to create a scope with a custom loader I created the TranslocoLazyModuleUtils:

import {TranslocoLazyModuleUtils} from "./transloco/transloco-lazy-module-utils";

@NgModule({
  imports: [
    ...
    TranslocoModule
  ],
  providers: [
    ...
    TranslocoLazyModuleUtils.getScopeProvider('my-microfrontend-ui', ['en', 'de'])
  ]

The code shown creates a new scope of name my-microfrontend-ui and loads the translations for the languages defined in the array. The scopes name must be unique and should not collide with the name of the scope of other remotes.

By default, if you had a translation key named hello in the translation definitions you needed to prefix your messages.

{{ my-microfrontend-ui.hello' | transloco }}

This default behaviour is overridden by the TranslocoLazyModuleUtils. Instead, it always defines an alias named message for your scope:

{{ 'message.hello' | transloco }}

This has the advantage of not having to adjust the scope prefix in templates when the scope name is adjusted in the scope definition of the module.

In case you do not want to prefix your keys with the scope name you can use the structural directive version of Transloco and define the default scope using:

<div *transloco="let t; read: 'message'">
  {{ t('hello') }}
</div>

Complete implementation of the transloco-lazy-module-utils.ts:

import {ValueProvider} from "@angular/core";
import {TRANSLOCO_SCOPE} from "@ngneat/transloco";

interface TranslocoInlineLoader {
  [key: string]: Function
}

export class TranslocoLazyModuleUtils {

  private static createInlineLoader(languages: Array<string>): TranslocoInlineLoader {
    const translocoInlineLoader: TranslocoInlineLoader = {};

    languages.forEach(language => {
      translocoInlineLoader[language] = () => import(`../i18n/${language}.json`);
    });

    return translocoInlineLoader;
  }

  public static getScopeProvider(scope: string, languages: Array<string>): ValueProvider {
    return {
      provide: TRANSLOCO_SCOPE,
      useValue: {
        scope: scope,
        alias: 'message',
        loader: this.createInlineLoader(languages)
      }
    };
  }
}

Further Reading

You can read more about scopes and inline loaders here (actually I borrowed a lot of code from there but added proper types and encapsulated the code in a reusable class):

Let me know if this approach works out for you. :)

Avanti-Jundare commented 9 months ago

Hey is the issue resolved ... currently i am also facing the issue of transloco pipe not found when the component of remote MFE is lazily loaded from shell app .

emanuelegaleotti commented 7 months ago

I also get an error when the module is loaded from the main application. [TranslocoService -> TranslocoService -> InjectionToken TRANSLOCO_TRANSPILER -> InjectionToken TRANSLOCO_TRANSPILER -> InjectionToken TRANSLOCO_TRANSPILER]: NullInjectorError: No provider for InjectionToken TRANSLOCO_TRANSPILER! this is the personal project I'm trying on: https://github.com/emanuelegaleotti/Wormhole

marco24690 commented 3 weeks ago

@emanuelegaleotti same problem for me, how did you solve it?

emanuelegaleotti commented 3 weeks ago

@marco24690 I solved the problem some time ago, try looking at this example project of mine. If I remember correctly, it was enough to replicate the translateModule also in the microfrontends https://github.com/emanuelegaleotti/Wormhole/tree/main

S-Furman commented 2 weeks ago

@marco24690 did you deal with it?