Create an Angular app with translated field validations using Transloco, Server-side Rendering (ssr) and Angular Material

A while ago I wanted to start building a new project that I envisioned needed at least to have

Server-side Rendering (SSR), to reduce first load times, important especially for people on mobile phones

I18N, which would also need to work when fetching that first page using SSR

Form Validation with translations

Angular Material, because it just works and looks clean :)

In order to translate a website in different languages using Angular’s I18N though, you will have to compile your source code in every language your website is going to be presented to the user. You will then need to have every different version running behind a proxy, which will route your user to the site with the desired language. This might not fit your development workflow and could also add some additional complexity when deploying your website. On top of that, changing the language in runtime is not yet possible, although Google is already working on it - see the issue here.

There are some very good alternatives to Angular’s I18N. For example, there are a ton of tutorials about ngx-translate out there. However, this lib has a lot of opened issues and the owner of this repository is shifting his efforts (see here) towards helping to improve the official Angular I18N library. Fortunately for us, there is a new kid on the block called Transloco, which is being actively developed and is more powerful than the former. We’ll be using this library for this tutorial.

All this took me a bit more effort than expected, but in the end I got it working and can share it with you all. So let’s get to it!

TL;DR;

This is a very large post :) If you are just interested in a working example of these integrations, head over to my git repository

Initial setup

Angular 9 is finally here! With it, the new Ivy renderer as default, which is faster and generates smaller js compiled code. And also: we can now use Typescript 3.7, which allows us to use Optional Chaining!!! Awesome!!

Creating a new project

First, install @angular/cli if you don’t already have it (make sure it’s up to date and using version 9) to generate a new project called ssr-translate .

1 2 3 4 5 # install @angular/cli npm i -g @angular/cli # generate project ng new ssr-translate cd ssr-translate

Angular CLI will ask if you want to enable routing (for which I answered yes) and what kind of stylesheet you want to use (I chose SCSS).

Enable Server-side Rendering

Enable server-side rendering by adding angular universal

1 ng add @nguniversal/express-engine --clientProject ssr-translate

where ssr-translate is the name of the project you created. You should expect an output similar to

Installing packages for tooling via npm. Installed packages for tooling via npm. CREATE src/main.server.ts (298 bytes) CREATE src/app/app.server.module.ts (318 bytes) CREATE tsconfig.server.json (325 bytes) CREATE server.ts (2013 bytes) UPDATE package.json (1799 bytes) UPDATE angular.json (5188 bytes) UPDATE src/main.ts (432 bytes) UPDATE src/app/app.module.ts (438 bytes) UPDATE src/app/app-routing.module.ts (284 bytes) ✔ Packages installed successfully.

I am not going into much detail here about what files are additionally created after adding SSR support, since the official documentation already covers this in great detail, and I can only recommend you reading the details over there :).

By now you should be able to test that SSR works by building and serving your app

1 npm run build:ssr && npm run serve:ssr

This can take a while, but if everything works you should see something like

[...] chunk {4} styles.0e4338761429b4eb16ac.css (styles) 0 bytes [initial] [rendered] Date: 2020-02-01T21:56:40.381Z - Hash: 588b308cc4f8db79fdb6 - Time: 31561ms Hash: 0458e5a6c22e09044830 Time: 17983ms Built at: 02/01/2020 9:57:08 PM Asset Size Chunks Chunk Names main.js 2.83 MiB 0 [emitted] [big] main Entrypoint main [big] = main.js chunk {0} main.js (main) 5.81 MiB [entry] [rendered] > ssr-translate@0.0.0 serve:ssr /home/martin/how-tos/ssr-translate > node dist/ssr-translate/server/main.js Node Express server listening on http://localhost:4000

If you go to http://localhost:4000, you’ll see that the page loads instantly.

Configure Transloco

First we’ll add transloco by running

1 ng add @ngneat/transloco

When asked, choose the languages you’ll have on your website (comma separated) and also be sure to answer yes when asked if working with server side rendering.

Transloco will add some translations inside assets/i18n folder. For a simple test, we may add a test translation. In my case, I configured Transloco to support pt and en languages. So I added some content in en.json

1 2 3 { "test" : "this is a test" }

and pt.json

1 2 3 { "test" : "isto é um teste" }

Now erase whatever you find inside app.component.html and paste this snippet:

1 2 3 <ng-container *transloco= "let t" > <p> {{ t("test") }} </p> </ng-container>

You will find a more detailed explanation about how to use transloco in the official documentation.

Now if you run the app (without using SSR yet)

1 ng serve

and head over to http://localhost:4200, you will see that the page will always be presented in the first language you listed when adding transloco’s library. If you open transloco-root.module.ts you will see that a default language is set there. What we actually want though is to choose it dinamically, based on which language is set in the user’s browser configuration.

For that, we will need to get the browser’s language configuration and use it to decide in which language to present the page. Since we are also going to do SSR, we will need to get the language from the request headers, since we don’t have access to the user’s browser configuration.

We will use Angular’s dependency injection to properly fetch the preferred user’s language, for the browser part and for the server side.

Fetching Language from browser config

Create a file called locale-lang-config.ts inside src/app . We are going to use this file to list all our supported locales and languages, which we will also reference from transloco-root.module.ts .

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // this configuration will be injectable by app.component.ts export class LocaleConfig { constructor ( public language : string , public locale : string ) {} } // some locale and language configuration for all the available languages our website supports export const DEFAULT_LANG = " pt " ; export const DEFAULT_LOCALE = " pt-BR " ; export const SUPPORTED_LANGUAGES = [ { language : " pt " , locales : [ " pt-BR " , " pt-PT " ] }, { language : " en " , locales : [ " en-US " , " en-GB " ] } ]; // This factory is what will create the LocaleConfig to be used when // the app is running in our browser (so not in our Server-side rendered page) // Notice the reference to window object, which is only available when // we are running in a browser export const browserLocaleFactory : () => LocaleConfig = () => { if ( typeof window === " undefined " || typeof window . navigator === " undefined " ) { throw new Error ( " Fetching locale failed. Are you really in a browser?? " ); } const wn = window . navigator as any ; let locale = wn . languages ? ` ${ wn . languages [ 0 ]} ` : DEFAULT_LOCALE ; locale = locale || wn . language || wn . browserLanguage || wn . userLanguage ; const language = locale . split ( " - " )[ 0 ]; return new LocaleConfig ( language , locale ); };

In transloco-root.module.ts , change availableLangs and defaultLangs values to point to the ones created in locale-lang-config.ts , like so:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [...] @ NgModule ({ exports : [ TranslocoModule ], providers : [ { provide : TRANSLOCO_CONFIG , useValue : translocoConfig ({ availableLangs : SUPPORTED_LANGUAGES . map ( lang => lang . language ), defaultLang : DEFAULT_LANG , reRenderOnLangChange : true , prodMode : environment . production }) }, { provide : TRANSLOCO_LOADER , useClass : TranslocoHttpLoader } ] }) export class TranslocoRootModule {}

Now in you app.module.ts file, add a provider configuration for LocaleConfig using the browserLocaleFactory we defined in locale-lang-config.ts :

1 2 3 4 5 6 7 8 9 10 [...] bootstrap : [ AppComponent ], providers : [ { provide : LocaleConfig , useFactory : browserLocaleFactory } ] }) export class AppModule {}

Finally, to have the app presented using the browser language config, add this constructor to app.component.ts :

1 2 3 constructor ( transloco : TranslocoService , localeConf : LocaleConfig ) { transloco . setActiveLang ( localeConf . language ); }

If everything went according to plan, when opening the page at http://localhost:4200 you should see the page in the language configured in your browser. If you want to test changing the language in firefox:

Fetching Language from request headers (SSR part)

Just as an experiment, let’s try to run what we’ve built so far using Server-side Rendering.

1 npm run build:ssr && npm run serve:ssr

If we go to http://localhost:4000, we will get an internal server error. Looking at the logs, we will see that we get the exception we defined in our browserLocaleFactory :

Node Express server listening on http://localhost:4000 ERROR Error: Fetching locale failed. Are you really in a browser??

This happens because browserLocaleFactory is being used to provide the LocaleConfig . This won’t work because we need to provide a serverLocalFactory that does not rely on a browser window for the SSR part. We will also need to change a bit the way we provide these browser and server factories.

Let’s add a new function called serverLocaleFactory to our locale-lang-config.ts file:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [...] export const serverLocaleFactory : ( locale ?: string ) => () => LocaleConfig = ( reqLocales ?: string ) => () => { if ( ! reqLocales ) { return new LocaleConfig ( DEFAULT_LANG , DEFAULT_LOCALE ); } // try setting locale according to list of locales sent to us. Try finding the first one of the list - since // this will probably the preferred client's language const localeFound : string | undefined = reqLocales . split ( new RegExp ( ' ,|; ' )) . find ( reqLocale => SUPPORTED_LANGUAGES . find ( lang => reqLocale . includes ( lang . language ))); if ( localeFound ) { // From the iteration above we only arrive here if a language was found (that's why the bang and tslint disable) // tslint:disable-next-line: no-non-null-assertion const foundLangauge = SUPPORTED_LANGUAGES . find ( lang => localeFound . includes ( lang . language )) ! ; const supportedLocale = foundLangauge . locales . find ( locale => locale === localeFound ); return new LocaleConfig ( foundLangauge . language , supportedLocale || foundLangauge . locales [ 0 ]); } else { return new LocaleConfig ( DEFAULT_LANG , DEFAULT_LOCALE ); } };

This new factory will have to be provided in the /server.ts express server file - found in the root folder of our project - which was created for us when we added support for SSR. The reason to add it in this file is that we will be able to extract the language from the request headers sent by the user’s browser. Find this code block:

1 2 3 4 5 6 [...] // All regular routes use the Universal engine server . get ( ' * ' , ( req , res ) => { res . render ( indexHtml , { req , providers : [{ provide : APP_BASE_HREF , useValue : req . baseUrl }] }); }); [...]

and substitute it by adding the serverLocaleFactory as a provider for LocaleConfig , like so:

1 2 3 4 5 6 7 8 9 10 11 12 [...] // All regular routes use the Universal engine server . get ( ' * ' , ( req , res ) => { res . render ( indexHtml , { req , providers : [ { provide : APP_BASE_HREF , useValue : req . baseUrl }, { provide : LocaleConfig , useFactory : serverLocaleFactory ( req . headers [ ' accept-language ' ]) } ] }); }); [...]

This still won’t work, though. When we try to access the website using SSR, we will now have 2 distinct providers for LocaleConfig since AppModule also already defines a provider for LocaleConfig - look at AppServerModule : it references AppModule .

So the last step is to separate client and server modules so that each declares a single locale factory. To do so, create an app.client.module.ts file under src/app with the following content:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { NgModule } from " @angular/core " ; import { AppComponent } from " ./app.component " ; import { AppModule } from " ./app.module " ; import { browserLocaleFactory , LocaleConfig } from " ./locale-lang-config " ; @ NgModule ({ imports : [ AppModule ], providers : [ { provide : LocaleConfig , useFactory : browserLocaleFactory } ], bootstrap : [ AppComponent ] }) export class AppClientModule {}

Now in src/main.ts , instead of bootstrapping the AppModule , we will bootstrap AppClientModule , like so:

1 2 3 4 5 6 [...] document . addEventListener ( ' DOMContentLoaded ' , () => { platformBrowserDynamic () . bootstrapModule ( AppClientModule ) . catch ( err => console . error ( err )); });

We may now remove the browserLocaleFactory from our AppModule . It should look like this:

1 2 3 4 5 6 7 8 9 10 11 @ NgModule ({ declarations : [ AppComponent ], imports : [ BrowserModule . withServerTransition ({ appId : " serverApp " }), AppRoutingModule , HttpClientModule , TranslocoRootModule ], bootstrap : [ AppComponent ] }) export class AppModule {}

As a last step, we need to change src/environments/environment.prod.ts . When we added Transloco, the ng add included a baseURL configuration property to both src/environments/environment.ts and src/environments/environment.prod.ts . The configuration for the production environment needs some special attention: when running the website in SSR mode, Transloco will download translation files dinamically from the URL that is set in baseUrl property; for local tests, I had to change mine to

1 2 3 4 export const environment = { baseUrl : " http://localhost:4000 " , production : true };

where express server port is now 4000 (when running the app in SSR mode). Also note that this baseUrl will have to match your domain name once you deploy your app, otherwise Transloco will not be able to retrieve translations!

FINALLY! After much sweat and tears, and if everything went well, you can start the server with npm run build:ssr && npm run serve:ssr and test that all works by navigating to http://localhost:4000. The page should render instantly with the correct language configuration.

Add Angular Material

Add Angular material by running

1 ng add @angular/material

Progress through all questions with your choices (I also chose to have global fonts enabled, since this applies roboto font to the whole page). If you don’t want roboto applied to the body of your page, just stick with the defaults.

Now let’s add a new reactive form in our app.component.html, with some translated fields:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <form class= "example-form" [formGroup]= "form" *transloco= "let t" > <mat-form-field class= "full-width" > <textarea matInput [placeholder]= "t('app.form.address')" formControlName= "address" ></textarea> </mat-form-field> <table class= "full-width" cellspacing= "0" > <tr> <td> <mat-form-field class= "full-width" > <input matInput [placeholder]= "t('app.form.city')" formControlName= "city" /> </mat-form-field> </td> <td> <mat-form-field class= "full-width" > <input matInput [placeholder]= "t('app.form.state')" formControlName= "state" /> </mat-form-field> </td> </tr> </table> <button type= "submit" mat-button > {{ t("app.form.submit") }} </button> </form>

For this to work, we need to add some imports to our AppModule ,

1 2 3 4 5 6 7 imports : [ [...] ReactiveFormsModule , MatFormFieldModule , MatButtonModule , MatInputModule , ]

and we also need to create the translations for pt and en

1 2 3 4 5 6 7 8 9 10 { "app" : { "form" : { "address" : "Endereço" , "city" : "Cidade" , "state" : "Estado" , "submit" : "Enviar" } } }

1 2 3 4 5 6 7 8 9 10 { "app" : { "form" : { "address" : "Address" , "city" : "City" , "state" : "State" , "submit" : "Submit" } } }

Initialize the FormGroup in app.component.ts:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export class AppComponent implements OnInit { form : FormGroup ; constructor ( transloco : TranslocoService , localeConf : LocaleConfig , private fb : FormBuilder ) { transloco . setActiveLang ( localeConf . language ); } ngOnInit (): void { this . form = this . fb . group ({ address : [ "" ], city : [ "" ], state : [ "" ] }); } }

And that’s it, we now have Material fields with translated names.

Translate form field validation errors with Transloco

Now to the final piece of the puzzle: translating field validation errors. To make this work, we will need a pipe operator, to transform the ValidationErrors that are returned from each validator into something that can be used as input for Transloco. Create a new pipe operator by using angular cli:

1 ng g p pipes/translate-error --module app

with the following content

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { Pipe , PipeTransform } from " @angular/core " ; import { ValidationErrors } from " @angular/forms " ; @ Pipe ({ name : " i18nErr " }) export class TranslateErrorPipe implements PipeTransform { transform ( errors : ValidationErrors | null , i18nFunc : ( i18nKey : string , obj ?: { [ key : string ]: any }) => string ): string { if ( ! errors ) { return "" ; } const errorKeys = Object . keys ( errors ); if ( errorKeys . length === 0 ) { return "" ; } const errorKey = errorKeys [ 0 ]; const errorDetails = errors [ errorKey ]; if ( typeof errorDetails === " object " ) { return i18nFunc ( errorKey , errorDetails ); } return i18nFunc ( errorKey ); } }

Our new pipe will take two arguments; the first argument takes all ValidationErrors a field will output when in error. If we look at the definition of Angular’s ValidationErrors interface:

1 2 3 export interface ValidationError { [ key : string ]: any ; }

we can see that each error is represented as a key in this interface. The any type on the value side might contain an arbitrary object detailing the error. For instance, if we define a Validators.maxLength(3) for a field and when the user inputs more than 3 chars, the field will output the following error:

1 2 3 4 5 6 { "maxlength" : { "requiredLength" : 3 , "actualLength" : 6 } }

The second pipe argument is Transloco’s translate function (named i18nFunc in this pipe). This function accepts a translation key and an optional additional object used for interpolating the string we are translating; we can use the additional information we receive in the error object to complete our translation with additional information.

Ultimately, what our pipe will do is to find the first error (if any) from the ValidationErrors argument and extract the first key and the value it finds for that key. The key will be passed as an argument to i18nFunc function, and if there is also a value object for the error key, it will be passed as the optional argument for the i18nFunc function

This sounds confusing right now but it’ll make sense in a minute when we add translations for our validations.

Let’s add some validations in app.component.ts then:

1 2 3 4 5 6 7 ngOnInit (): void { this . form = this . fb . group ({ address : [ '' ], city : [ '' , [ Validators . required ]], state : [ '' , [ Validators . required , Validators . maxLength ( 20 ), Validators . minLength ( 3 )]] }); }

In the template, display the error messages by adding a mat-error right under each input tag

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [...] <mat-form-field class= "full-width" > <input matInput [placeholder]= "t('app.form.city')" formControlName= "city" /> <mat-error *ngIf= "form.controls.city.errors as errors" > {{ errors | i18nErr: t }} </mat-error> </mat-form-field> </td> <td> <mat-form-field class= "full-width" > <input matInput [placeholder]= "t('app.form.state')" formControlName= "state" /> <mat-error *ngIf= "form.controls.state.errors as errors" > {{ errors | i18nErr: t }} </mat-error> </mat-form-field> </td> </tr> </table> [...]

Add translations for required , minlength and maxlength to pt.json and en.json. These are the keys of ValidationErrors we might get when we have these errors present in our form:

1 2 3 4 [ ... ] "required" : "Campo mandatório" , "minlength" : "Deve ter no mínimo {{ requiredLength }} caracteres" , "maxlength" : "Deve ter no máximo {{ requiredLength }} caracteres"

1 2 3 4 [ ... ] "required" : "Value required" , "minlength" : "Must have at least {{ requiredLength }} characters" , "maxlength" : "Cannot have more than {{ requiredLength }} characters"

In the translation files, you can see we are using string interpolation for the requiredLength , which is part of ValidationErrors details when we have a minlength or maxlength validation error.

And that’s it, we are finally done with this huge tutorial!

Wrapping up

Hopefully this tutorial didn’t melt your brain. Having translations and SSR configured properly right at the beginning of a new project is extremely beneficial down the road since having to change all this later can be very frustrating and time consuming!

If you want to have a look at the fully working example, please check it out at github.com/martinreus/ssr-translate.

If you found an issue or typos in this tutorial, please kindly let me know by opening an issue here

Thanks for reading and see you in the next one! Cheers!