Serving multilingual Angular application with ExpressJS
Tags: angular, javascript, tutorial, webdev
Published: 2022-07-18 04:25:01

Previously we derived the locales file that contains all the language resources in preparation to isolate them. Our focus today is serving through **NodeJs and ExpressJS** server. We will serve different languages using cookies, and later relying on the URL. But before we dig in, one last benefit of our resources class.

Accessing resources from anywhere
---------------------------------

Out of the box, Angular provides [$localize](https://angular.io/api/localize/init/$localize) adapter, but it is limited to i18n uses. Our `res class` can be used even if no locale is targeted, and `language.ts` is used directly. We have already made use of it in [Error catching and toast messages](https://garage.sekrab.com/posts/catching-and-displaying-ui-errors-with-toast-messages-in-angular). Here is a snippet of how it can be freely used:

```typescript
// using the res class for any linguistic content

// extreme case of a warning when an upload file is too large
const size = Config.Upload.MaximumSize;
this.toast.ShowWarning(
  // empty code to fallback
  '',
  // fallback to a dynamically created message
  { text: Res.Get('FILE_LARGE').replace('$0', size)}
);

// where FILE_LARGE in locale/language is:
// FILE_LARGE: 'The size of the file is larger than the specified limit ($0 KB)'
```

Note: The source files are in [StackBlitz](https://stackblitz.com/edit/angular-runtime-translation?file=host%2Fserver.js), but they don't necessarily work in StackBlitz, because the environment is too strict.

Language JavaScript file
------------------------

We covered in a previous article the basics of how to inject an [external configuration file into Angular](https://garage.sekrab.com/posts/loading-external-configurations-inline-and-in-ssr-in-angular) and came to the conclusion that the best way is to **place the javascript file in the index header**. At this stage, we have no clear model that we need to cast to, so let's start with a simple script tag in `index.html`:

`<script src="locale/language.js" defer></script>`

For that to work in development, we'll add an asset to `angular.json`

```json
// angular.json options/assets
{
    "glob": "*.js",
    "input": "src/locale",
    "output": "/locale"
}
```

To make use of the JavaScript keys collection, we **declare** in our `typescript`. The `res class` is the only place that uses the keys, and `app.module` is the only place that uses the locale id. So let's **place everything in** `res class`:

```typescript
// in res class, we declare the keys and locale_id
declare const keys: any;
declare const EXTERNAL_LOCALE_ID: string;

export class Res {
  // export the locale id somehow, a property shall do
  public static LOCALE_ID = EXTERNAL_LOCALE_ID;

  // using them directly: keys
  public static Get(key: string, fallback?: string): string {
    if (keys[key]) {
        return keys[key];
    }
    return fallback || keys.NoRes;
  }

// ...
}

// in app.module, we import the locale id
// ...
providers: [{provide: LOCALE_ID, useValue: Res.LOCALE_ID }]
```

### Angular Locale Package

But how do we import the locale from Angular packages? The easiest, most straightforward way is to do exactly the same as above. Add a script, and reference in `angular.json`. Assuming we want to have multiple locales available, then we include them all in **assets:**

```json
{
  // initially, add them all
  "glob": "*.js",
  "input": "node_modules/@angular/common/locales/global",
  "output": "/locale"
}
```

This means that the locales' files are copied to the host when we build, which is ideal, because this way we know we always have the latest version of the locale. One way is this:

`<script src="locale/ar-JO.js" defer></script>`

The other is to **let the language file create the tag**. Remember though, this file will eventually be called on server platform, so we want to be at least ready for that.

```javascript
// in browser platform
const script = document.createElement('script');
script.type = 'text/javascript';
script.defer = true;
script.src = 'locale/ar-JO.js';
document.head.appendChild(script);

// in server platform, we'll add this later
// require('./ar-JO.js');
```

Let's do one **refactor** before we jump into serving the files. Create a single JavaScript key, and namespace it, so that the 10xers don't troll us, not that it matters.

```javascript
// the locales/language.js file

const keys = {
  NoRes: '',
  // ...
};
// combine and namespace
// window will later be global
window.cr = window.cr || {};
window.cr.resources = {
  language: 'en',
  keys,
  localeId: 'en-US'
};
```

cr is short for cricket. Our project code name.

In our `res class`:

```typescript
// in res class remove imported keys from /locales/language.ts

declare const cr: {
  resources: {
    keys: any;
    language: string;
    localeId: string;
  };
};

export class Res {
  // to use in app.module
  public static get LocaleId(): string {
    return cr?.resources.localeId;
  }

  // add a private getter for keys
  private static get keys(): any {
    return cr?.resources.keys;
  }
  // use it like this this
  public static Get(key: string, fallback?: string): string {
    const keys = Res.keys;
    // ...
  }
  // ...
}
```

### Language specific files

We shall now create two files in locale folder ready to be shipped: [cr-en](https://stackblitz.com/edit/angular-runtime-translation?file=src%2Flocale%2Fen-cr.js&view=editor), and [cr-ar](https://stackblitz.com/edit/angular-runtime-translation?file=src%2Flocale%2Far-cr.js&view=editor). The `cr-ar` contains the added **ar-JO** locale script, while the `cr-en` has nothing special. **We prefix not to clash with Angular packages, since ar.js and en.js already exist.**

(the en-AE mentioned below is for example only, we are not going to use it.)

We are building now with the following `angular.json` settings:

```json
"projects": {
    "cr": {
      "architect": {
        "build": {
          "options": {
            "resourcesOutputPath": "assets/",
            "index": "src/index.html",
            "assets": [
              // ...
              // add all locales in dev
              {
                "glob": "*.js",
                "input": "src/locale",
                "output": "/locale"
              },
              {
                // add angular packages in dev, be selective
                // en-AE is an example
                "glob": "*(ar-JO|en-AE).js",
                "input": "node_modules/@angular/common/locales/global",
                "output": "/locale"
              }
            ]
          },
          "configurations": {
            "production": {
              // place in client folder
              "outputPath": "./host/client/",
              // ...
              // overwrite assets
              "assets": [
                // add only locales needed
                // names clash with Angular packages, prefix them
                {
                  "glob": "*(cr-en|cr-ar).js",
                  "input": "src/locale",
                  "output": "/locale"
                },
                {
                  // add angular packages needed
                  "glob": "*(ar-JO|en-AE).js",
                  "input": "node_modules/@angular/common/locales/global",
                  "output": "/locale"
                }
              ]
            }
          }
        },
        // server build
        "server": {
          "options": {
            // place in host server
            "outputPath": "./host/server",
            "main": "server.ts"
            // ...
          },
          // ...
        }
      }
    }
```

Let's build.

Browser only application
------------------------

Starting with the Angular builder:

`ng build --configuration=production`

This generates the output file **host/client**. Inside that folder, we have **locale** folder that contains all javascript files we included in assets:

-   `/host/client/locale/cr-en.js`
-   `/host/client/locale/cr-ar.js`
-   `/host/client/locale/ar-JO.js`

The index file contains a reference for **locale/language.js**, now it's our job to **rewrite that URL to the right language file**. Creating multiple index files is by far the most extreme, and the best solution. But today, we'll just rewrite using **ExpressJS routing**.

In our main [server.js,](https://stackblitz.com/edit/angular-runtime-translation?file=host%2Fserver.js) we need to create a **middleware to detect language**, for now, from a cookie. The cookie name can easily be lost around, so first, I want to **create a config file** where I will place all my movable parts, this is a personal preference, backend developers probably have a different solution.

```javascript
// server/config.js
const path = require('path');
const rootPath = path.normalize(__dirname + '/../');

module.exports = {
  env: process.env.Node_ENV || 'local',
  rootPath,
  // we'll use this for cookie name
  langCookieName: 'cr-lang',
  // and this for prefix of the language file
  projectPrefix: 'cr-'
};
```

The language middleware:

```javascript
// a middleware to detect language

module.exports = function (config) {
  return function (req, res, next) {
    // check cookies for language, for html request only
    res.locals.lang = req.cookies[config.langCookieName] || 'en';

    // exclude non html sources, for now exclude all resources with extension
    if (req.path.indexOf('.') > 1) {
      next();
      return;
    }

    // set cookie for a year
    res.cookie(config.langCookieName, res.locals.lang, {
      expires: new Date(Date.now() + 31622444360),
    });

    next();
  };
};
```

This middleware simply detects the language cookie, sets it to [response locals property](https://expressjs.com/en/4x/api.html#res.locals), and then saves the language in cookies.

The basic server:

```javascript
const express = require('express');

// get the config
const config = require('./server/config');

// express app
const app = express();

// setup express
require('./server/express')(app);

// language middleware
var language = require('./server/language');
app.use(language(config));

// routes
require('./server/routes')(app, config);

const port = process.env.PORT || 1212;
// listen
app.listen(port, function (err) {
  if (err) {
    return;
  }
});
```

The routes for our application:

```javascript
// build routes for browser only solution
const express = require('express');

// multilingual, non url driven, client side only
module.exports = function (app, config) {

  // reroute according to lang, don't forget the prefix cr-
  app.get('/locale/language.js', function (req, res) {
    res.sendFile(config.rootPath +
        `client/locale/${config.projectPrefix}${res.locals.lang}.js`
    );
    // let's move the path to config, this becomes
    // res.sendFile(config.getLangPath(res.locals.lang));
  });

  // open up client folder, including index.html
  app.use(express.static(config.rootPath + '/client'));

  // serve index file for all other urls
  app.get('/*', (req, res) => {
    res.sendFile(config.rootPath + `client/index.html`);
  });
};
```

Running the server, I can see the cookie saved in Chrome Dev tools, changing it, reloading, it works as expected.

Let's move the language path to server **config** because I will reuse it later.

```javascript
module.exports = {
  // ...
  getLangPath: function (lang) {
    return `${rootPath}client/locale/${this.projectPrefix}${lang}.js`;
  }
};
```

Server platform
---------------

Going back to a previous article: [Loading external configurations in Angular Universal](https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal), we **isolated the server**, and I specifically mentioned one of the benefits is serving a multilingual app using the same build. Today, we shall make use of it. When building for SSR, using:

`ng run cr:server:production`

The file generated in **host/server folder is main.js.** The following is the routes done with SSR in mind (in StackBlitz it's **host/server/routes-ssr.js**)

```javascript
const express = require('express');

// ngExpressEngine from compiled main.js
const ssr = require('./main');

// setup the routes
module.exports = function (app, config) {
  // set engine, we called it AppEngine in server.ts
  app.engine('html', ssr.AppEngine);
  app.set('view engine', 'html');
  app.set('views', config.rootPath + 'client');

  app.get('/locale/language.js', function (req, res) {
    // reroute according to lang
    res.sendFile(config.getLangPath(res.locals.lang));
  });

  // open up client folder
  app.use(express.static(config.rootPath + '/client', {index: false}));

  app.get('/*', (req, res) => {
    // render our index.html
    res.render(config.rootPath + `client/index.html`, {
      req,
      res
    });
  });
};
```

Exclude `index.html` file in the static middleware, in order to force the root URL to pass through the Angular engine.

Previously we used a trick to differentiate between server and browser platforms to include the same JavaScript on both platforms:

```javascript
// in javascript, an old trick we used to make use of the same script on both platforms
if (window == null){
    exports.cr = cr;
}
```

Looking at [Angular Locale scripts](https://github.com/angular/angular/blob/ecffc3557fe1bff9718c01277498e877ca44588d/packages/common/locales/global/en.js), they are wrapped like this:

```javascript
// a better trick
(function(global) {
  global.something = 'something';
})(typeof globalThis !== 'undefined' && globalThis || typeof global !== 'undefined' && global ||
   typeof window !== 'undefined' && window);
```

This is better. Why didn't I think of that earlier? Oh well. Let's rewrite our language files to be wrapped by a function call:

```javascript
// locale/language.js (cr-en and cr-ar) make it run on both platforms
(function (global) {
  // for other than en
  if (window != null) {
    // in browser platform
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.defer = true;
    script.src = 'locale/ar-JO.js';
    document.head.appendChild(script);
  } else {
    // in server platform
    require('./ar-JO.js');
  }

  const keys = {
    NoRes: '',
    // ...
  };

  global.cr = global.cr || {};
  global.cr.resources = {
    language: 'ar',
    keys,
    localeId: 'ar-JO',
  };
})(
  (typeof globalThis !== 'undefined' && globalThis) ||
    (typeof global !== 'undefined' && global) ||
    (typeof window !== 'undefined' && window)
);
```

In **language middleware**, require the file.

```javascript
module.exports = function (config) {
  return function (req, res, next) {
    // ... get cookie

    // if ssr is used
    require(config.getLangPath(res.locals.lang));

    // ... save cookie
  };
};
```

Running the server. We are faced with two problems:

-   `app.module` is loading immediately, before any routing occurs. It looks for `LOCAL_ID` in `global.cr.resources`, which has not been loaded anywhere yet.
-   Defining a default one, the locale does not change on the server, dynamically, since `app.module` has already run with the first locale.

To dynamically change the LOCALE_ID on the server---without restarting the server, Googled and found a [simple answer](https://github.com/angular/angular/issues/15039#issuecomment-456345066). Implementing `useClass` for the provider in `app.module`. Looking into the code generated via SSR, this change eliminated the direct referencing of `LocalId`, and turned it into a void 0 statement.

`exports.Res = exports.LocaleId = void 0;`

This is a recurring problem in SSR, whenever you define root level static elements. Note that once the application hydrates (turns into Browser platform), it no longer matters, browser platform is magic!

```typescript
// in Res class, extend the String class and override its default toString
export class LocaleId extends String {
    toString() {
        return cr.resources.localeId || 'en-US';
    }
}

// and in app.module, useClass instead of useValue
@NgModule({
  // ...
  providers: [{ provide: LOCALE_ID, useClass: LocaleId }]
})
export class AppModule {}
```

This takes care of the first problem. It also partially takes care of the second one. The new problem we're facing now is:

-   **NodeJS requires files once**. If required again, the file will be pulled out the cache, and it will not run the function within. Thus on server platform, switching the language works the first time, but switching back to a previously loaded language, will not update the locale.

To fix that, we need to save the different `global.cr` collections in explicit keys, and in the language middleware assign our NodeJS `global.cr.resources` to the right collection. In our language JavaScript files, let's **add the explicit assignment**:

```javascript
// in cr-en cr-ar, etc,
(function (global) {

  // ...
  // for nodejs, add explicit references
  // global.cr[language] = global.cr.resources
  global.cr.en = global.cr.resources;

})(typeof globalThis !== 'undefined' && globalThis || typeof global !== 'undefined' && global ||
    typeof window !== 'undefined' && window);
```

In our **language middleware**, whenever a new language is requested, it is added to the global collection. Then we pull out the one we want:

```javascript
// language middleware
module.exports = function (config) {
  return function (req, res, next) {
    // ...
    require(config.getLangPath(res.locals.lang));

    // reassign global.cr.resources
    global.cr.resources = global.cr[res.locals.lang];

    // ...
  };
};
```

Running the server, I get no errors. Browsing with JavaScript disabled, it loads the default language. Changing the cookie in the browser multiple times, it works as expected.

That wasn't so hard was it? Let's move on to URL-based language.

URL-based application
---------------------

For content-based and public websites, deciding the language by the URL is crucial. To turn our server to capture selected language from URL instead of a cookie, come back next week. 😴

Thanks for reading through another episode. Let me know if I raised an eyebrow.


### RESOURCES

-   [Dynamically changing the LocaleId in Angular](https://github.com/angular/angular/issues/15039#issuecomment-456345066)
-   [StackBlitz project](https://stackblitz.com/edit/angular-runtime-translation)
-   [Angular $localize](https://angular.io/api/localize/init/$localize)
-   [ExpressJS response locals](https://expressjs.com/en/4x/api.html#res.locals)

### RELATED POSTS

[Loading external configurations in Angular Universal](https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal)

[Catching and displaying UI errors with toast messages in Angular](https://garage.sekrab.com/posts/catching-and-displaying-ui-errors-with-toast-messages-in-angular)

{% embed https://garage.sekrab.com/posts/serving-multilingual-angular-application-with-expressjs %}