Sekrab Garage

Twisting Angular localization

Serving a different index.html in an Angular build for different languages

Angular July 26, 22
Subscribe to Sekrab Parts newsletter.

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

anchorRTL 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.

anchorReplacing 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.

// 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:

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:

// 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:

<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

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

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

// 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:

<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:

/* 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:

// 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.

anchorThe 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 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!

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
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

<!-- 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.

// 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. 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.

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 regeneratingindex.html all together, is now we can do even more replacements. Let’s change the HTML language attribute as well. In the process function:

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.

anchorEnhancements 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.

  1. two years ago
    Alternative way to localize in Angular
  2. one year ago
    Serving multilingual Angular application with ExpressJS
  3. one year ago
    Serving the same Angular build with different URLs
  4. one year ago
    Serving a different index.html in an Angular build for different languages
  5. one year ago
    Currency Angular pipe, UI language switch, and a verdict
  6. one year ago
    Pre-generating multiple index files using Angular Builders and Gulp tasks to serve a multilingual Angular app
  7. one year ago
    Using Angular APP_BASE_HREF token to serve multilingual apps, and hosting on Netlify
  8. one year ago
    Multilingual Angular App hosted on Firebase and Surge with the same build
  9. one year ago
    Alternative way to localized in Angular: Conclusion and final enhancements