An approach to supporting multiple locales in an Angular 8 Universal SPA using cookies and the Express engine

Stefan Peshikj
Web Factory LLC
Published in
8 min readJan 25, 2021

As most people are already aware, a normal Angular application executes in the browser, rendering pages in the DOM in response to user actions.

The application code is transpiled into JavaScript, so when we open the website, we are mostly waiting for all of those scripts to download so we can start the execution of our application, and here is a moment where our application loads really slow, since long projects can contain large amounts of code which can take some time to load and start-up. When our browser downloads all of the JavaScript files and they are ready to be executed, Angular will start executing our code and start rendering the content defined by the routing options of our application.

Angular Universal, on the other hand, executes on the server, generating static application pages that later get bootstrapped on the client. This means that the application generally renders more quickly, giving users a chance to view the application layout before it becomes fully interactive. This behavior improves our user experience and page score as we will not be stuck on a loading spinner until the application is ready to show the content.

Server-Side Rendering is also important for Search Engines and how our application presents itself to the end-users. Without it, some search engines or applications would not be able to process different metadata like title, description, and image on different routes of our website, and SSR allows us to quickly generate that information before we send the response back to the entity making the request.

Question: How do i know that an Angular application has been rendered on the server?
Well, that is really easy to find out, just open some browser’s network inspector, inspect the first page request to the application, and if you see any HTML tag inside the app-root or some other root selector, that means that most likely the code has already been rendered on the server.

Why use cookies for this task?

Well, cookies are just one of many options. First, it’s really easy to implement and it will get you started really fast. It will allow us to support multiple locales, but search engines and crawlers will only index our primary locale since the other locale options will not be visible to them. Storage options like LocalStorage and SessionStorage do not exist on the server, so we will not be able to transfer locale information when we start rendering the page, and cookies exist both on the client and server, so this comes as a natural choice for us.

If you want to index all of the multiple locales, this may not be the best option, as adding some locale information in the route would be a better way to go forward.

How do we do this?

First, we need to allow our server to start parsing the cookies that are exchanged with the browser. This can be easily done by using a cookie parsing library. For Express, this most likely would be the cookie-parser library.

This example will use TypeScript for the Express code. The frontend application uses Angular 8. The code snippet illustrates some of the changes added in the server.ts of our application.

import * as express from 'express';
import * as cookieparser from 'cookie-parser';
// Express server
const app = express();
const PORT = process.env.PORT || 8080;
const DIST_FOLDER = join(process.cwd(), 'dist/app/browser');
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {
AppServerModuleNgFactory,
LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap, REQUEST, RESPONSE
} = require('./dist/app/server/main');
// Use the cookie parser
app.use(cookieparser());
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', {
req, res, providers: [
/// for cookie-parsing
{ provide: REQUEST, useValue: req },
{ provide: RESPONSE, useValue: res }
]

});
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});

From the example above, you can see that we have imported the cookie parser and let our application use it in the upcoming requests. The other code is really similar to the starting code when we add Angular Universal using a command like ng add.

The only change is that now we are also importing a REQUEST and RESPONSE injection token from the main.server.ts file. We are also providing the values for those Injection Tokens in every request (app.get(‘*’)) and this will be useful in the Server side of the application later on when we manipulate with the cookies. These Injection Tokens are declared in @nguniversal/express-engine/tokens and we just re-export them.

An example of the main.server.ts file.

import { enableProdMode } from '@angular/core';

import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

export { AppServerModule } from './app/app.server.module';
export { ngExpressEngine } from '@nguniversal/express-engine';
export { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
export { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';

Looking at this code, it is apparent that we need to add a couple of more libraries to our application, @nguniversal/common,@nguniversal/express-engine, @nguniversal/module-map-ngfactory-loader.

We are close, the only thing left is for us to actually read cookie information in our application. A good thing to use is a library that supports both Client and Server environments. In our example, we will use ngx-cookie@5.0.0 and ngx-cookie-backend@5.0.0, and it’s pretty simple to implement.

In our app.module.ts we import the Cookie module:

import { LOCALE_ID, NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { CoreModule } from './core/core.module';
import { BrowserModule } from '@angular/platform-browser';
import { TransferHttpCacheModule } from '@nguniversal/common';
import { LocaleService } from './core/locale.service';
import { CookieModule } from 'ngx-cookie';
@NgModule({
declarations: [AppComponent],
imports: [
AppRoutingModule,
CoreModule,
BrowserModule.withServerTransition({ appId: 'your-app-id' }),
TransferHttpCacheModule,
CookieModule.forRoot()
],
providers: [
{
provide: LOCALE_ID,
useFactory: (localeService: LocaleService) => localeService.getLocale(),
deps: [LocaleService]
}
]
})
export class AppModule {
}

and in our app.server.module we import the Cookie Backend module:

import { NgModule } from '@angular/core';
import {
ServerModule,
ServerTransferStateModule
} from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
import { CookieBackendModule } from 'ngx-cookie-backend';
@NgModule({
imports: [
AppModule,
ServerModule,
NoopAnimationsModule,
ModuleMapLoaderModule,
ServerTransferStateModule,
CookieBackendModule.forRoot()
],
bootstrap: [AppComponent]
})
export class AppServerModule {
}

From here on out, we can use the CookieService from ngx-cookiefor any cookie manipulation in our application, not just for the locale.

In order for us to set different locales to our application, we need to provide the LOCALE_ID Injection Token in order to tell Angular to use a specific locale. If none is provided, Angular’s components which use the LOCALE_ID, like the DatePipe will fallback to “en-US”.

A good thing about Providers is that if we want to, we can also return a Promise, and Angular will wait until we resolve the response. So naturally, this will be the moment where we check which locale data to use, and this is depicted in the providers shown in the app.module in the example above. In our example, we will be synchronously checking the cookies so there is no need for providing a Promise for this Injection Token.

providers: [
{
provide: LOCALE_ID,
useFactory: (localeService: LocaleService) => localeService.getLocale(),
deps: [LocaleService]
}
]

Is it time to change my LOCALE information now?

Yes! In order to correctly provide the LOCALE_ID, we also need to import locale data which Angular conveniently provides for us. This can be done by importing the locale data from @angular/common/locale and then registering the actual data. An example of this is:

import localeEn from '@angular/common/locales/en';
import localeFr from '@angular/common/locales/fr';
import localeJa from '@angular/common/locales/ja';
registerLocaleData(localeEn);
registerLocaleData(localeFr);
registerLocaleData(localeJa);

NOTE: We should register all the locales that we want to support. This will be useful when we use common Angular Pipes like DatePipe, CurrencyPipes, DecimalPipe, PercentPipe, and all of these use the LOCALE_ID. We can also later prepare the templates for translations and i18n our application.

NOTE: If we do not want to use pipes, but rather access the transform methods directly from the code, we can import the methods from the @angular/common package and use them without using pipes, for example: import {formatDate} from '@angular/common'.

Once we know which Locales we want to support, we just need to add the code to check the cookies and whether we have a valid LOCALE_ID selection or fallback to our default locale. Once we select our locale, we can write the correct LOCALE_ID into the cookie using a correct domain so our cookie will be actually written.

For this, we can use the DOCUMENT: InjectionToken<Document> that Angular exposes and gets the hostname information on both the Client and Server-side of our application. Here is an example of the LocaleService.

import { Inject, Injectable } from '@angular/core';
import { DOCUMENT, registerLocaleData } from '@angular/common';
import localeEn from '@angular/common/locales/en';
import localeFr from '@angular/common/locales/fr';
import localeJa from '@angular/common/locales/ja';
import { CookieService } from 'ngx-cookie';

registerLocaleData(localeEn);
registerLocaleData(localeFr);
registerLocaleData(localeJa);

@Injectable({
providedIn: 'root'
})
export class LocaleService {
public static SUPPORTED_LOCALES = ['en', 'fr', 'ja'];
private static LOCALE_STORAGE_KEY = 'app_locale';
private locale: string;

constructor(@Inject(DOCUMENT) private dom,
private cookieService: CookieService) {
this.setupInitialLocale();
}
// Check if we have the locale in the cookie. If we don't, or we do but it's not supported, fallback to some initial locale (for simplicity, we will fallback to the first locale in our list)
private setupInitialLocale(): void {
this.locale = this.cookieService.get(LocaleService.LOCALE_STORAGE_KEY);

if (LocaleService.SUPPORTED_LOCALES.indexOf(this.locale) === -1) {
this.locale = LocaleService.SUPPORTED_LOCALES[0];
}

this.setLocale(this.locale);
}
// Store the locale variable, we may need it later, save the locale selection as a cookie with 1 year expiry date (this is custom, we can save it for as long as we want)
setLocale(newLocale: string): void {
this.locale = newLocale;
const localeCookie = this.cookieService.get(LocaleService.LOCALE_STORAGE_KEY);
if (this.locale !== localeCookie) {
const oneYearFromToday = new Date();
oneYearFromToday.setFullYear(oneYearFromToday.getFullYear() + 1);
this.cookieService.put(LocaleService.LOCALE_STORAGE_KEY, this.locale, {
domain: this.dom.location.hostname,
expires: oneYearFromToday
});
}
}
// If there is no locale, check the cookie and select a locale which we can use
getLocale(): string {
if (!this.locale) {
this.setupInitialLocale();
}

return this.locale;
}
}

The code is pretty simple and effectively chooses the correct Locale on the application startup. A good thing to note is that once we set the LOCALE_ID, we need to refresh the page if we want to set it to a different value so all of our application code will pick up that change and start using the different locale.

In this simple example, we have illustrated a way to manipulate locale information on both Client and Server environments, which will correctly be used to render our page and smoothly transition it from an SSR environment, to our own Browser environment, using the correct LOCALE_ID. We can later upgrade our application and make it i18n ready using Angular’s i18n guidelines, or some translation package like ngx-translate, which will also give us phenomenal results.

There are many ways to create this behavior and this example shows only one of the possibilities of handling this use case. In this example, we used Angular 8 and the Express web application framework for Node.js.

Good luck and happy coding!

--

--

Stefan Peshikj
Web Factory LLC

Fullstack Engineer at Web Factory LLC | LinkedIn https://www.linkedin.com/in/peshou/. Passionate Software Developer who wants to know many things :).