Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 9: Using angular i18n along with server side rendering (angular universal)

I'm in the process of migrating an angular 7 application to angular 9, which uses server side rendering (angular universal) and angular i18n for 2 languages (french and english).

In the old angular 7 process, since I was using AOT I had to do 5 builds for production:

  • 2 x client build (1 for french, 1 for english)
  • 2 x server build (1 for french, 1 for english)
  • build of server.ts

Then, in server.ts I was loading dynamically the correct server bundle

old server.ts

app.engine('html', (_, options: any, callback) => {

  const isFR= options.req.url.indexOf('site-fr') >= 0 ;
  const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = isFR ? require('./dist/server/fr/main') : require('./dist/server/en/main');

// Our index.html we'll use as our template
  const template = readFileSync(join(DIST_FOLDER, 'browser', isFR ? 'fr' : 'en', 'index.html')).toString();
  renderModuleFactory(AppServerModuleNgFactory, {
    // Our index.html
    document: template,

I migrated the app to angular 9, and now from what I understand in the documentation, only one client build is needed.

You can also provide the --localize option to the ng build command with your existing production configuration. In this case, the CLI builds all locales defined under i18n in the project configuration.

This seems to work for the client build, as 2 folders (fr and en are generated).

However, nowhere is there any mention of using SSR with i18n. So I'm ending up with one server.ts.

Here are the scripts I use to build and run the project

angular.json

"serve:ssr": "node dist/myproject/server/main.js",
"build:ssr": "ng build -c production --localize && ng run myproject:server:production"

new server.ts

// The Express app is exported so that it can be used by serverless Functions.
export function app(port) {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/myproject/browser/fr');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

Dist folder structure

- dist
  - myproject
    - browser
        - fr
          - index.html
          - *.js
        - en
          - index.html
          - *.js      
    - server
        - main.js

Note: I did see a closed github issue describing this problem, but the solution is basically to go back the way it was before, i.e. have 2 builds per language and also 2 builds for server.ts.

Surely there is another way?

like image 248
David Avatar asked Mar 11 '20 16:03

David


People also ask

Can Angular be used for server-side rendering?

Angular Universal executes on the server-side by generating static pages and later are sent to the client browser for display. Thus, Angular Universal renders the app more quickly and allows users to view the application's layout.

What is client-side rendering and server-side rendering in Angular?

Client-side rendering manages the routing dynamically without refreshing the page every time a user requests a different route. But server-side rendering is able to display a fully populated page on the first load for any route of the website, whereas client-side rendering displays a blank page first.

What is i18n Angular?

Angular Internationalizationlink Internationalization, sometimes referenced as i18n, is the process of designing and preparing your project for use in different locales around the world. Localization is the process of building versions of your project for different locales.

Should I use Angular universal?

A primary benefit for using Angular Universal is that it improves web crawler support for enhanced Search Engine Optimization (SEO). With traditional client-side rendered SPAs, anything that is not in that shell of an . html is all rendered by the JavaScript.


1 Answers

I found a solution involving just 2 builds. But running 2 instances of the server process is now needed.

Step #1: angular.json

Ensure your locales are correctly defined in angular.json and add a new allLocales target in my-project:server option.

I created a new allLocales target because I did not know how to combine production target with en and fr configurations. The point of doing this is to just have one server build with all language server generated.

For the front bundle, this is natively possible with ng build (i.e. ng build --configuration=production,fr,en or ng build --configuration=production --localize)

angular.json

 "projects": {
    "my-project": {
      "i18n": {
        "locales": {
          "en": {
            "translation": "src/locale/messages.en.xlf",
            "baseHref": ""
          },
          "fr": {
            "translation": "src/locale/messages.fr.xlf",
            "baseHref": ""
          }
        }
      },
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            //...
          },
          "configurations": {
            "production": {
             //...
            },

            "en": {
              "localize": [
                "en"
              ]
            },


            "fr": {
              "localize": [
                "fr"
              ]
            }

          }
        },

        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/my-project/server",
            "main": "server.ts",
            "tsConfig": "tsconfig.server.json"
          },

          "configurations": {
            "production": {
              //...
            },

            "allLocales": {
              "outputHashing": "none",
              "optimization": false,
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "localize": [
                "en", "fr"
              ],
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            }
          }
        },

Step #2: server.ts

Modify server.ts file to accept a language parameter. Each running insteace of the generated server bundle main.js will have its own port and language.

server.ts

//...
export function app(language) { //add language here
  const server = express();
  const distFolder = join(process.cwd(), 'dist/my-project/browser', language); //Use language here


//...

function run() {
  const port = process.env.PORT || 5006;
  const language = process.env.LANGUAGE || 'fr';

  // Start up the Node server
  const server = app(language); //Use the language here
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port} for language ${language}`);
    });

Step #3: Modify package.json

package.json

"build:ssr": "ng build -c production --localize && ng run my-project:server:allLocales"
"serve:ssr-en": "env PORT=5006 LANGUAGE=en node dist/my-project/server/en/main.js",
"serve:ssr-fr": "env PORT=5007 LANGUAGE=fr node dist/my-project/server/fr/main.js",

build:ssr will build the the client bundles for all languages and then build the server bundles for all languages server:ssr-XX will start the nodejs server for the port and language associated to language XX

Here is the structure

- dist
  - myproject
    - browser
        - fr
          - index.html
          - *.js
        - en
          - index.html
          - *.js      
    - server
        - fr
            - main.js
        - en
            - main.js

Step 4: Reverse proxy

If you are using a reverse proxy, do not forget to redirect all requests to the correct main.js instance

Note The build process is now much faster with angular 9, since there is only 2 builds.

like image 99
David Avatar answered Oct 20 '22 12:10

David