Transloco: SSR - Template loader still visible when serving the view

Created on 3 Dec 2019  路  10Comments  路  Source: ngneat/transloco

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

When serving a page with a Node server, the result contains the template loaders instead of the translations.

Expected behavior

When serving a page with a Node server, the result should contains the translations instead of the template loaders.

Minimal reproduction of the problem with instructions

Creating a repository with SSR seems very complicated to me nevertheless I am willing to give you as much information as possible to find out where is the problem.

What is the motivation / use case for changing the behavior?

I was using ngx-translate and despite the documentation saying say the SSR support is not available, the SSR was operating as expected with the translations being served.
The only difference was that the translations were already set on build time instead of fetched from a JSON file but only on the server-side.

I migrated from ngx-translate to transloco to extend the possibilities due to the nice features that transloco was able to bring me.
Now, the last piece of my road to transloco is to have the translation working on the server-side for SEO purpose.

Environment


Angular version: 8.2.14
@ngneat/transloco: 2.9.0
@ngneat/transloco-messageformat: 1.1.3
@ngneat/transloco-persist-lang: 1.0.1
@ngneat/transloco-persist-translations: 1.0.0
@nguniversal/express-engine: 8.1.1
@nguniversal/module-map-ngfactory-loader: 8.1.1

Browser:
- [x] 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: 12.3.1
- Platform: Windows

More information

Rendering result

<ng-component class="ng-star-inserted">
    <div class="transloco-loader-template">
        <span>Loading...</span>
    </div>
</ng-component>

Transloco loader

  public getTranslation(lang: string): Observable<Translation> {
    const isProduction: boolean = this.ptlEnvironmentService.isProduction();
    const externalAssetsUrl: string = this.ptlEnvironmentService.getExternalAssetsUrl();
    let i18nUrl: string;

    // The logs on the Node server are pointing on a valid location here
    if (isProduction) {
      i18nUrl = `${externalAssetsUrl}/extassets/customer-portal/i18n/${lang}.json?v=${cacheBusting[ lang ]}`;
    } else {
      i18nUrl = `./assets/i18n/${lang}.json?v=${cacheBusting[ lang ]}`;
    }

    return this.httpClient.get<Translation>(i18nUrl);
  }

Transloco app module config

    TranslocoModule,
    TranslocoPersistTranslationsModule.init({
      loader: TranslocoHttpLoader,
      storage: {
        provide: PERSIST_TRANSLATIONS_STORAGE,
        useValue: i18nLocalForage
      },
      storageKey: 'i18n',
      ttl: ONE_HOUR
    }),
    TranslocoPersistLangModule.init({
      storage: {
        provide: TRANSLOCO_PERSIST_LANG_STORAGE,
        useValue: langLocalForage
      }
    }),
    TranslocoMessageFormatModule.init({
      locales: 'fr-FR'
    }),

Transloco config provider

export const TRANSLOCO_CONFIG_PROVIDER: ValueProvider = {
  provide: TRANSLOCO_CONFIG,
  useValue: <TranslocoConfig>{
    availableLangs: [
      'fr',
      'en'
    ],
    defaultLang: 'fr',
    failedRetries: 3,
    fallbackLang: 'en',
    flatten: {
      aot: ENVIRONMENT.production
    },
    missingHandler: {
      allowEmpty: ENVIRONMENT.production,
      logMissingKey: !ENVIRONMENT.production,
      useFallbackTranslation: false
    },
    prodMode: ENVIRONMENT.production,
    reRenderOnLangChange: true
  }
};

Again, I am not sure at all that transloco is the problem here but I have no idea in this case on how to fix my problem so any help would be very much appreciable.

Thanks !

Most helpful comment

Use the PLATFORM_ID in factory to get the localForage only on the browser:

export function getStorage(platformId: PLATFORM_ID) {
  return isPlatformBrowser(platformId) ? localForage : dummyStorage
}
@NgModule({
imports: [
    TranslocoPersistLangModule.init({
      storage: {
        provide: TRANSLOCO_PERSIST_LANG_STORAGE,
        deps: [PLATFORM_ID],
        useFactory: localStorage
      }
    }),
...

All 10 comments

Why do you see:

<ng-component class="ng-star-inserted">
    <div class="transloco-loader-template">
        <span>Loading...</span>
    </div>
</ng-component>

Did you define a template somewhere?

@C0ZEN could you add also your html?

Hello @ArielGueta and @itayod.
Thank you for taking the time here to find out the culprit.

Let me bring more information of my application.

How do I see the problem

By checking the first network request on Chrome from my application, the index.html file which is merged with the page component contains a bunch of translations.
To be specific, there are 47 translations on this page and all of them are displayed with my custom transloco loader template instead of being translated as expected.

My custom transloco loading template

This provider is declared inside the app.module.ts.

transloco-loading-template.provider.ts

export const TRANSLOCO_LOADING_TEMPLATE_PROVIDER: ValueProvider = {
  provide: TRANSLOCO_LOADING_TEMPLATE,
  useValue: '<span>Loading...</span>'
};

The page on error (home-seo)

The page is lazy loaded.

home-seo-route.ts

{
    loadChildren: () => import('./home-seo/home-seo.module').then(m => m.HomeSeoModule),
    path: 'home-seo'
}

Inside the HomeSeoModule, I have a provider to create the home-seo scope.

transloco-home-seo-scope.provider.ts

export const TRANSLOCO_HOME_SEO_SCOPE_PROVIDER: ValueProvider = {
  provide: TRANSLOCO_SCOPE,
  useValue: 'home-seo'
};

You can checkout a sample of the home seo component.

home-seo.component.html

<div class="cptl-home-seo-component">
  <ptl-home class="cptl-home-seo-component--home">
    <ptl-home-section-manager class="cptl-home-seo-component--home--section-manager">
      <ng-container *transloco="let translation; scope: 'home-seo'">
        <h2 class="cptl-home-seo-component--home--section-manager--title">
          {{ translation('homeSeo.sectionManager.title') }}
        </h2>
      </ng-container>
      <ng-container *transloco="let translation; scope: 'home-seo'">
        <h3 class="cptl-home-seo-component--home--section-manager--description">
          {{ translation('homeSeo.sectionManager.description') }}
        </h3>
      </ng-container>
    </ptl-home-section-manager>
  </ptl-home>
</div>

And here is the culprit with the homeSeo.sectionManager.description translation being displayed as a transloco loading template like if the transloco library was not yet ready to translate it.

I have almost no idea on how the SSR detects that a page is ready.
Nevertheless, I read that this is linked to zone.js and obviously the Angular http client with the get XHR call should force the node server to wait for the end of the request and then serve the page.

If you want once again more details about my app, I am available.
Thank you !

Edit 1

I added some logs inside the getTranslation method and it seems that the method is never called on the node server when running the SSR on local.
Also, there are a bunch of "Unable to load translation and all the fallback languages" errors.

Edit 2

I removed all the scope providers.
They were useless because I use the scope on the template side but on SSR and on client side it just change nothing at all.

I found out the problem !!
Using the TranslocoPersistTranslationsModule does not create the loader or at least it does not works the same than on the client-side.

Using instead the common way to include a loader with a provider fix the SSR bug.

Could you guys still check that out please, since it is really a bug ?

Thank you !

Edit 1

I found out a temporary solution to keep the SSR feature working and the TranslocoPersistTranslationsModule on the client-side by adding the provider only inside the app.server.module.ts file.
This is only possible because the provider for the loader is taken over the one from the TranslocoPersistTranslationsModule when both are registers.

@C0ZEN nice catch, it makes sense to me that it is related to TranslocoPersistTranslationsModule since I am not sure that localForage supports ssr.

Could you try to use a dummy service instead just to see if it works?

You could use this one from the spec file: https://github.com/ngneat/transloco/blob/master/projects/ngneat/transloco-persist-translations/src/lib/transloco-persist-translations.service.spec.ts#L19

@itayod yeah sure I can help and try this tonight.

By the way, this is the config that I used for the local storage.

const i18nLocalForage: LocalForage = localForage.createInstance({
  description: 'storage for the translation files',
  driver: [
    localForage.INDEXEDDB,
    localForage.LOCALSTORAGE,
    localForage.WEBSQL
  ],
  name: 'cptl_i18n',
  size: 4980736,
  storeName: 'cptl_i18n',
  version: 1.0
});

Hello @itayod. I have news for you.

You can check below what I changed for the tests.

app.module.ts

export function createStorageMock(): any {
  return new (class Storage implements MaybeAsyncStorage {
    private _storage = {};

    public getItem(key: string): any {
      return this._storage[ key ];
    }

    public setItem(key: string, value: any): void {
      this._storage[ key ] = value + '';
    }

    public removeItem(key: string): void {
      delete this._storage[ key ];
    }
  })();
}

TranslocoPersistTranslationsModule.init({
      loader: TranslocoHttpLoader,
      storage: {
        provide: PERSIST_TRANSLATIONS_STORAGE,
        useValue: createStorageMock()
      },
      storageKey: 'i18n',
      ttl: ONE_HOUR
    }),

The result is fine. The problem is fixed.

Obviously, this is a problem between localForage and transloco.
I hope you will be able to find a way to fix this.

However I do not think I can do more to help here (no PR) but let me know if you need more tests from me.

Thanks @C0ZEN.

The problem is not between localForage and transloco.
I guess it didn't work for you because you tried to use localForage in SSR environment.

I suggest you to check if you run in Browser or not and provide the storage accordingly.

I suggest you to check if you run in Browser or not and provide the storage accordingly.

I am not sure that this is possible because checking weither or not the app is in a browser (with isPlatformBrowser) require the platform token (PLATFORM_ID) and I do not think that this is possible to use the injector inside the app module.

Use the PLATFORM_ID in factory to get the localForage only on the browser:

export function getStorage(platformId: PLATFORM_ID) {
  return isPlatformBrowser(platformId) ? localForage : dummyStorage
}
@NgModule({
imports: [
    TranslocoPersistLangModule.init({
      storage: {
        provide: TRANSLOCO_PERSIST_LANG_STORAGE,
        deps: [PLATFORM_ID],
        useFactory: localStorage
      }
    }),
...
Was this page helpful?
0 / 5 - 0 ratings

Related issues

mickz18 picture mickz18  路  5Comments

inorganik picture inorganik  路  8Comments

NetanelBasal picture NetanelBasal  路  6Comments

karmasakshi picture karmasakshi  路  6Comments

maxime1992 picture maxime1992  路  4Comments