Serving a different index.html in an Angular build for different languages
Tags: angular, webdev, javascript, tutorial
Published: 2022-07-26 16:32:00

Previously we ended with the notion that reloading the site to change the language is better than changing on client side. There are few problems with not refreshing:

-   User experience issue: If the change language button only changes the text appearing on the screen, the change may be too subtle to be noticed. This can be fixed by creating the illusion of reloading.
-   Other resources: sometimes, changing the language entails changing the direction, the styles, and the fonts. Like from an LTR language to an RTL language. Or from a Latin base alphabet, to Russian alphabet. Changing the fonts means loading a new font on top of the existing one. Changing styles is so unpredictable, it might flash a screen with no styles for a few milliseconds.

Those might not be enough to convince you, in all cases, having the language reload the app is cheap, considering that users won't often do it. Today, we are going to adapt our server to serve different stylesheets based on language.

> The code is on [StackBlitz](https://stackblitz.com/edit/angular-runtime-translation)


RTL vs LTR
----------

When generating localized versions for RTL languages, there are a couple of more changes that need to be done on `index.html`, and since now we have HTML engines, we can replace more than just the `base href`.

### Replacing styles

The stylesheet by default is injected in Angular. To have the server choose a different stylesheet, we must first disable auto injection of the stylesheet.

```json
// angular.json remove injection
{
"projects": {
  "cr": {
    "architect": {
      "build": {
        "options": {
					// ...
					// bundle up styles and styles.rtl
          "styles": [
              {
                  "input": "src/assets/css/styles.css",
                  "bundleName": "styles.ltr",
                  "inject": false
              },
              {
                  "input": "src/assets/css/styles.rtl.css",
                  "bundleName": "styles.rtl",
                  "inject": false
              }
          ],
          // ...
      },
      "configurations": {
        "production": {
          "optimization": {
            "scripts": true,
						// lets also remove auto inlining of fonts and critical css
            "fonts": false,
            "styles": {
              "inlineCritical": false,
              "minify": true
            }
          },
      // ...
}
```

**Optimization** configuration for styles, allows `inlineCritical`. Which will inline the two included `css`, that is not what we want for now, so we switch it off.

**In non URL based app:**

In our express server, the following line in express must be higher than the express static module:

```javascript
app.get('/styles.css', (req, res) => {
  // reroute to either styles.ltr or styles.rtl
  if (res.locals.lang === 'ar') {
     res.sendFile(config.rootPath + 'client/styles.rtl.css');
  } else {
     res.sendFile(config.rootPath + 'client/styles.ltr.css');
  }
});
```

**In URL based app:**

```javascript
// route styles under URL specific language
app.get('/:lang/styles.css', (req, res) => {
  if (res.locals.lang === 'ar') {
     res.sendFile(config.rootPath + 'client/styles.rtl.css');
  } else {
     res.sendFile(config.rootPath + 'client/styles.ltr.css');
  }
});
```

The `index.html` will have a solid link:

`<link rel="stylesheet" href="styles.css">`

As for fonts, we have **two options**, bittersweet. If we use **Google fonts** suggested code snippets:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Signika:wght@300..700&amp;display=swap" rel="stylesheet">
```

#### 1\. Import the font URL directly in main styles.

If we import the fonts directly into the styles, the external stylesheet waits until the stylesheet is ready, making the font itself less available on time. So let's do an alternative, and import the styles in its own stylesheet.

![Network activity with imported font](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/729515083r2pm9rpxsvr.png)

#### 2\. Import in a separate stylesheet (fonts.css)

This option is a bit better since the fonts file will only have one import, and downloading the font will start as soon as the stylesheet is loaded, and the font is requested.

![Network activity with importing in a separate fonts.css](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f7vq1hmrhegwads69qku.png)

Let's create a new `fonts.[dir].css` file, add to `angular.json`, and write express path for the `fonts.css` as well.

```json
// angular.json
"options": {
	// ...
	// bundle up fonts and fonts.rtl
  "styles": [
	 // ...
    {
        "input": "src/assets/css/fonts.css",
        "bundleName": "fonts.ltr",
        "inject": false
    },
    {
        "input": "src/assets/css/fonts.rtl.css",
        "bundleName": "fonts.rtl",
        "inject": false
    }
  ],
```

Add it as is to the `index.html`:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">

<!-- Add the fonts as well well -->
<link rel="stylesheet" href="fonts.css">
<link rel="stylesheet" href="styles.css">
```

The `fonts.ltr` and `fonts.rtl` that you would create in development, and include in build have only a single import statement:

```css
/* import the right google font for each language */
@import url('https://fonts.googleapis.com/css2?family=Signika:wght@300..700&display=swap');
```

Finally the express paths are similar to the styles paths:

```javascript
// in non-url based (routes.js)
app.get('/fonts.css', (req, res) => {
  if (res.locals.lang === 'ar') {
    res.sendFile(config.rootPath + 'client/fonts.rtl.css');
  } else {
    res.sendFile(config.rootPath + 'client/fonts.ltr.css');
  }
});

// in url-based (routes-url.js)
app.get('/:lang/fonts.css', (req, res) => {
  if (res.locals.lang === 'ar') {
    res.sendFile(config.rootPath + 'client/fonts.rtl.css');
  } else {
    res.sendFile(config.rootPath + 'client/fonts.ltr.css');
  }
});
```

As for SSR, everything is the same, but there is a slight issue we need to fix first.

### The curious case of inlineCriticalCSS in Angular SSR:

When implementing the above, and running for ssr, which is rendered via `ngExpressEngine`, the process goes like this:

-   the engine reads the `index.html` code
-   finds the `styles.css` reference
-   opens it
-   and inlines the critical CSS (using [Critters](https://www.npmjs.com/package/critters) plugin).

It is a great option on server side, but the `styles.css` does not exist (nor should it). So we have to do without it. The only problem is the obnoxious 404 message that appears in server logs.

To fix that, after diving a bit deep in the compiled files, inline critical css on the server is an **undocumented option** for the `ngExpressEngine`!

```javascript
res.render('', {
    req,
    res,
    document: rendered,
		// add this line to skip css file processing
    inlineCriticalCss: false
});
```

Before we adopt this undocumented (thus unreliable) feature, and let go of the nice feature of **inlining critical css** on server, let's move on with a different option.

#### Regenerate `index.html` itself by our own template engine

The performance of linking the fonts stylesheet from HTML is better than the two options above. But for that, we need to regenerate `index.html` itself.

![Network activity of linked stylesheet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fqzk7lkc3x39owngdf8a.png)
 

On my small scale app, testing with a slow network, I can only give in to that statement, because I saw no real difference in performance.

There are some good HTML engines out there, like PUG, handlebars, and mustache, but they are an overkill for our needs. Let's create two fonts URL. and since we are at it, let's do the same for styles.

We will start with this in `index.html`

```html
<!-- in production index.html, add all different styles and fonts, and language specific -->
<html lang="$lang">

<!-- #LTR -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Signika:wght@300..700&display=swap">
<link rel="stylesheet" href="styles.ltr.css">
<!-- #ENDLTR -->
<!-- #RTL -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;500&display=swap">
<link rel="stylesheet" href="styles.rtl.css">
<!-- #ENDRTL -->

And in the body we can also specify two loading messages, hey, why not?
<app-root>
 <!-- #LTR -->
  loading
  <!-- #ENDLTR -->
  <!-- #RTL -->
  انتظر
  <!-- #ENDRTL -->
</app-root>
```

Remember: all of our processing is on the production index.

Back to our **express HTML engines**, let's create a function to handle processing of `index.html`.

For simplicity, because the express server is getting out of hand, we shall require the `config` file instead of passing it along.

```javascript
// process the html, remove base href, and styles and fonts
// create the regular expressions to look for in index
// we can have as many languages as we need

const fs = require('fs');
const config = require('./config');

const reLTR = /<!-- #LTR -->([\s\S]*?)<!-- #ENDLTR -->/gim;
const reRTL = /<!-- #RTL -->([\s\S]*?)<!-- #ENDRTL -->/gim;

const process = function (html, lang) {
  let contents = html.toString();
  // if config is with URL, change the base href
  if (config.urlBased) {
    contents = contents.replace('<base href="/">', `<base href="/${lang}/">`);
  }
  // if lang is ar, remove ltr and keep rtl
  if (lang === 'ar') {
    contents = contents.replace(reLTR, '');
  } else {
    contents = contents.replace(reRTL, '');
  }
  return contents;
};
```

In our routes, we can make use of this function whenever we need to. Find renderer functions grouped under [host/server/renderer.js](https://stackblitz.com/edit/angular-runtime-translation?file=host%2Fserver%2Frenderer.js). To make use of the exported functions, we use lines like these:

`renderer.htmlEngine(app);`

`renderer.htmlRender(res);`

`renderer.ngEngine(req, res);`

#### Back to optimization of styles and fonts

Remember at the beginning of this article, we needed to disable Angular styles and fonts optimization in order to control them via express routes. Now that we are regenerating the index, let's put them back in. The output after building still has our tags, and regenerating via our renderer still works as expected.

```html
The fonts and the styles are optimized, our comment tags are preserved
<!-- #LTR -->
<style type="text/css">@font-face{font-family:'Signika';font-style:normal...}</style>
<style>:root{...}</style><link rel="stylesheet" href="styles.ltr.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.ltr.css"></noscript>
<!-- #ENDLTR -->
<!-- #RTL -->
<style type="text/css">@font-face{font-family:'Tajawal';font-style:normal;...}</style>
<style>:root{...}</style><link rel="stylesheet" href="styles.rtl.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.rtl.css"></noscript>
<!-- #ENDRTL -->
```

#### Lang attribute

One good benefit of regenerating`index.html` all together, is now we can do even more replacements. Let's change the **HTML language attribute** as well. In the process function:

```javascript
const reLang = /\$lang/gim;

const process = function(html, lang) {
   // ... replace $lang when found
   contents = contents.replace(reLang, lang);

   // ...
}
```

And in our `index.html`:

`<html lang="$lang">`

Building, serving... working. Moving on.

Enhancements and adjustments
----------------------------

As we found out previously, the currency symbols of a locale are part of the built in library, and if that specific currency is not provided, it rolls back to its code. Is there a way to adjust this behavior? That, in addition to building the UI for language switch, is coming next episode. 😴

Thank you for reading this far, I know it was confusing, let me know if you have questions.

### RESOURCES

-   [Critters plugin](https://www.npmjs.com/package/critters)
-   [StackBlitz Project](https://stackblitz.com/edit/angular-runtime-translation)

{% embed https://garage.sekrab.com/posts/serving-a-different-index-html-in-an-angular-build-for-different-languages %}