lostium / ssr-i18n-angular17

This project is a basic example of how to use Server Side Rendering with multi-language (i18n) websites using Angular 17.
23 stars 6 forks source link

SsrI18nAngular17

Introduction

This project is a basic example of how to use Server Side Rendering with multi-language (i18n) websites using Angular 17. You can read the article in lostium.com.

The project has been created using the Angular CLI with Server-Side Rendering (SSR) enabled by default.

Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? (y/N)

In this example, internationalization has been done in English, Spanish, German, and French. Each language is accessed from the corresponding path: /en/, /es/, /de/, and /fr/.

angular.json

The angular.json file has been modified as follows:

Language Configuration

"i18n": {
    "sourceLocale": {
        "code": "en"
    },
    "locales": {
        "es": {
            "translation": "src/locale/messages.es.xlf",
            "baseHref": "/es/"
        },
        "fr": {
            "translation": "src/locale/messages.fr.xlf",
            "baseHref": "/fr/"
        },
        "de": {
            "translation": "src/locale/messages.de.xlf",
            "baseHref": "/de/"
        }
    }
},

Development Server Configuration

The development server only supports one language. In this case, we configure it to use English and set the baseHref to /en/. If you change the default language, make sure to modify both attributes accordingly.

"development": {
    "optimization": false,
    "extractLicenses": false,
    "sourceMap": true,
    "localize": [
        "en"
    ],
    "baseHref": "/en/"
}

ng-extract-i18n-merge

Additionally, we use ng-extract-i18n-merge, a tool that synchronizes translations between languages. Here is our configuration:

  "extract-i18n": {
    "builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
    "options": {
    "browserTarget": "ssr-i18n-angular17:build",
    "format": "xlf2",
    "outputPath": "src/locale",
    "targetFiles": [
        "messages.es.xlf",
        "messages.fr.xlf",
        "messages.de.xlf"
        ]
    }
},

server.ts

Angular provides a server for Server-Side Rendering (SSR) in server.ts, which we need to edit to serve each language at the appropriate path.

The most relevant changes are:

 /**
   * Get the language from the corresponding folder
   */
  const lang = basename(serverDistFolder);
  /**
   * Set the route for static content and APP_BASE_HREF
   */
  const langPath = `/${lang}/`;
  /**
   * Note that the 'browser' folder is located two directories above 'server/{lang}/'
   */
  const browserDistFolder = resolve(serverDistFolder, `../../browser/${lang}`);
 // Example Express Rest API endpoints
  // server.get('/api/**', (req, res) => { });
  // Serve static files from /browser
  // Complete the route for static content by concatenating the language.
  server.get(
    '*.*',
    express.static(browserDistFolder, {
      maxAge: '1y',
    })
  );
 // All regular routes use the Angular engine
  server.get('*', (req, res, next) => {
    /**
     * Discard baseUrl as we will provide it with langPath
     */
    const { protocol, originalUrl, headers } = req;
    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: resolve(serverDistFolder, `../../browser/`), // publicPath does not need to concatenate the language.
        providers: [
          { provide: APP_BASE_HREF, useValue: langPath },
          { provide: LOCALE_ID, useValue: lang },
          { provide: RESPONSE, useValue: res },
          { provide: REQUEST, useValue: req },
        ],
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
  });

proxy-server.mjs

The server Express is started by proxy-server.mjs. It simply imports each of the app() functions and associates them with the corresponding path.

In our case, we will copy the proxy to the dist/ssr-i18n-angular17 folder. If you modify its destination, you will also need to modify the import paths.

import { app as serverEn } from './server/en/server.mjs';
import { app as serverEs } from './server/es/server.mjs';
import { app as serverFr } from './server/fr/server.mjs';
import { app as serverDe } from './server/de/server.mjs';

const express = require('express');

function run() {
  const port = process.env.PORT || 4000;
  const server = express();

  server.use('/fr', serverFr());
  server.use('/de', serverDe());
  server.use('/es', serverEs());
  server.use('/en', serverEn());
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

run();

Build and Production Execution

At the end of the build, we will copy proxy-server.mjs to the destination folder. To do this, we have added a node script to the project copy-proxy-server.js, which you can adapt or copy in a different way.

Additionally, we modify package.json to include the copy of the proxy in the build script. We also adapt the serve:ssr:ssr-i18n-angular17 script to run the proxy-server.

  "scripts": {
    "ng": "ng",
    "start":

 "ng serve",
    "build": "ng build && node copy-proxy-server.js",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "serve:ssr:ssr-i18n-angular17": "node dist/ssr-i18n-angular17/proxy-server.mjs"
  },

To compile and run (we use pnpm):

pnpm build
pnpm serve:ssr:ssr-i18n-angular17