With more than 400.000 weekly downloads, ngx-translate might just be the most used internationalization library for Angular. It works out-of-the-box wonderfully on the client-side, however, it needs some tweaks to work with Angular Universal's SSR.

NGX-Translate is an internationalization library for Angular. It lets you define translations for your content in different languages and switch between them easily.
- NGX-Translate: The i18n library for Angular 2+

If you're not familiar with the library, I suggest reading their documentation on GitHub.

The issue with SSR

The library provides a simple way to load the translation files on the client-side through their HTTP loader. However, it does not work too well with the SSR solution the Angular team provides: Angular Universal.

In a previous post, Caching server-side requests - Angular Universal, I described an issue similar to the one that occurs when using ngx-translate. When using Angular Universal, I noticed that the content would "blink" because duplicate requests were being made, once on the server-side, and then again on the client-side.

The issue with ngx-translate is also related to the request of translation files. The files could not be retrieved over HTTP on the server-side, and as such the page would first appear in the browser with the translation keys, and only after the client receives the translations the translation keys would change to the actual translations.

Loading the translation files on the server-side

We need the translation files on the server-side. Working on the server-side, we can get the files through the file system.

First, I created a custom loader, translate-server.loader.ts, for the server module. It uses TransferState in order to later retrieve the translations on the client-side.

// shared/loaders/translate-server.loader.ts
import { join } from 'path';
import { Observable } from 'rxjs';
import { TranslateLoader } from '@ngx-translate/core';
import {
  makeStateKey,
  StateKey,
  TransferState
} from '@angular/platform-browser';
import * as fs from 'fs';

export class TranslateServerLoader implements TranslateLoader {
  constructor(
    private transferState: TransferState,
    private prefix: string = 'i18n',
    private suffix: string = '.json'
  ) {}

  public getTranslation(lang: string): Observable<any> {
    return new Observable((observer) => {
      const assets_folder = join(
        process.cwd(),
        'dist',
        'project-name', // Your project name here
        'browser',
        'assets',
        this.prefix
      );

      const jsonData = JSON.parse(
        fs.readFileSync(`${assets_folder}/${lang}${this.suffix}`, 'utf8')
      );

      // Here we save the translations in the transfer-state
      const key: StateKey<number> = makeStateKey<number>(
        'transfer-translate-' + lang
      );
      this.transferState.set(key, jsonData);

      observer.next(jsonData);
      observer.complete();
    });
  }
}

export function translateServerLoaderFactory(transferState: TransferState) {
  return new TranslateServerLoader(transferState);
}

Then, I set the TranslateModule's settings on the app.server.module.ts.

// app.server.module.ts
import { NgModule } from '@angular/core';
import {
  ServerModule,
  ServerTransferStateModule
} from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { translateServerLoaderFactory } from './shared/loaders/translate-server.loader';
import { TransferState } from '@angular/platform-browser';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ServerTransferStateModule,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: translateServerLoaderFactory,
        deps: [TransferState]
      }
    })
  ],
  bootstrap: [AppComponent]
})
export class AppServerModule {}

To take advantage of loading the translation files on the server-side, I created another loader, translate-browser.loader.ts, to fetch the previously loaded data from TransferState. If it does not find the translations in the cache, it uses the HTTP loader to get them.

// shared/loaders/translate-browser.loader.ts
import { Observable } from 'rxjs';
import { TranslateLoader } from '@ngx-translate/core';

import {
  makeStateKey,
  StateKey,
  TransferState
} from '@angular/platform-browser';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';

export class TranslateBrowserLoader implements TranslateLoader {
  constructor(private http: HttpClient, private transferState: TransferState) {}

  public getTranslation(lang: string): Observable<any> {
    const key: StateKey<number> = makeStateKey<number>(
      'transfer-translate-' + lang
    );
    const data = this.transferState.get(key, null);

    // First we are looking for the translations in transfer-state, 
	// if none found, http load as fallback
    if (data) {
      return new Observable((observer) => {
        observer.next(data);
        observer.complete();
      });
    } else {
      return new TranslateHttpLoader(this.http).getTranslation(lang);
    }
  }
}

export function translateBrowserLoaderFactory(
  httpClient: HttpClient,
  transferState: TransferState
) {
  return new TranslateBrowserLoader(httpClient, transferState);
}

Finally, declare it in your app.module.ts.

// app.module.ts
import { BrowserModule, TransferState } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClient, HttpClientModule } from '@angular/common/http';

import { TransferHttpCacheModule } from '@nguniversal/common';
import { translateBrowserLoaderFactory } from './shared/loaders/translate-browser.loader';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    TransferHttpCacheModule,
    HttpClientModule,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: translateBrowserLoaderFactory,
        deps: [HttpClient, TransferState]
      }
    }),
    AppRoutingModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Final thoughts

It may be helpful to load the translations on the server-side, if working with SSR, to deliver a page with the correct content faster. It is way more elegant to show the page with its true information instead of with the translation keys, even for a split second.