Serving a different index.html in an Angular build for different languages
Ayyash
Posted on July 26, 2022
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
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.
// 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&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.
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.
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.
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 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.
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.
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
Posted on July 26, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 25, 2024