skip to Main Content

I am using i18n with Angular Universal SSR. The issue is that the client received the text in source locale and after a few seconds are replaced with the correcty locale.

For example, client load http://localhost:4000/en-US/ in the first display shows in es locale and after a few seconds the text is replaced with en-US locale texts.

The build folders are created correctly and the proxy works perfect for each locale. I want the server to return the html with the correct translation so the SEO crawlers can find the content correctly in each locale.

It seems that the problem is in the build, is not generated with the correct locale.

The proyect config in angular.json file:

...
"i18n": {
   "sourceLocale": "es",
   "locales": {
     "en-US": {
      "translation":"src/i18n/messages.en.xlf",
      "baseHref": "en-US/"
     }
 }
},
...
"architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "outputPath": "dist/web/browser",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "localize": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [], 
            "customWebpackConfig":{
              "path": "./webpack.config.js"
            }
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ]
            }
          },
         ...
    }

The command to build the proyect:

ng build --prod --configuration=production && ng run web:server:production

Build directories result in path dist/web/browser:

en-US/
es/
                    

server.ts file:

export function app(lang: string): express.Express {
  const server = express();
  server.use(compression());
  const distFolder = join(process.cwd(), `dist/web/browser/${lang}`);
  const indexHtml = 'index.html';


  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
    extraProviders: [{ provide: LOCALE_ID, useValue: lang }],
  } as any));

  server.set('view engine', 'html');
  server.set('views', distFolder);

  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));

  server.get('*', (req, res) => {
    console.log(`LOADING DIST FOLDER: ${distFolder}`)
    console.log(`LOADING INDEX: ${indexHtml}`)
    res.render(`${indexHtml}`, {
      req,
      res,
      providers: [
        { provide: APP_BASE_HREF, useValue: req.baseUrl },
        { provide: NgxRequest, useValue: req },
        { provide: NgxResponse, useValue: res }
        ]
    });
  });

  return server;
}

function run(): void {
  const port = process.env.PORT || 4000;

  // Start up the Node server
  const server = express();
  const appEs = app('es');
  const appEn = app('en-US');
  server.use('/en-US', appEn);
  server.use('/es', appEs);
  server.use('', appEs);

  server.use(compression());
  server.use(cookieparser());
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
  run();
}

export * from './src/main.server';

2

Answers


  1. Chosen as BEST ANSWER

    After a few research I found the best solution. In order to made the server send the view with the proper translate you need to add a proxy file and create servers for each locale.

    Enable i18n for servers with localize flag in true:

    "server": {
              "builder": "@angular-devkit/build-angular:server",
              "options": {
                "outputPath": "dist/web/server",
                "main": "server.ts",
                "tsConfig": "tsconfig.server.json"
              },
              "configurations": {
                "production": {
                  "outputHashing": "media",
                  "fileReplacements": [
                    {
                      "replace": "src/environments/environment.ts",
                      "with": "src/environments/environment.prod.ts"
                    }
                  ],
                  "sourceMap": false,
                  "optimization": true,
                  "localize": true
                }
            },
    

    Create proxy-server.js:

    const express = require("express");
    const path = require("path");
    
    const getTranslatedServer = (lang) => {
      const distFolder = path.join(
        process.cwd(),
        `dist/web/server/${lang}`
      );
      const server = require(`${distFolder}/main.js`);
      return server.app(lang);
    };
    
    function run() {
      const port = 4000;
    
      // Start up the Node server
      const appEs = getTranslatedServer("/es");
      const appEn = getTranslatedServer("/en");
    
      const server = express();
    
      server.use("/es", appEs);
      server.use("/en", appEn);
      server.use("", appEs);
    
      server.listen(port, () => {
        console.log(`Node Express server listening on http://localhost:${port}`);
      });
    }
    
    run();
    

    server.ts:

    import '@angular/localize/init';
    import 'zone.js/dist/zone-node';
    
    import { ngExpressEngine } from '@nguniversal/express-engine';
    
    import * as express from 'express';
    import { join } from 'path';
    import * as cookieparser from 'cookie-parser';
    
    const path = require('path');
    const fs = require('fs');
    const domino = require('domino');
    const templateA = fs.readFileSync(path.join('dist/web/browser/en', 'index.html')).toString();
    
    const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader');
    
    const compression = require('compression');
    
    const win = domino.createWindow(templateA);
    win.Object = Object;
    win.Math = Math;
    
    global.window = win;
    global.document = win.document;
    global.Event = win.Event;
    global.navigator = win.navigator;
    console.log('declared Global Vars....');
    
    
    
    import { AppServerModule } from './src/main.server';
    import { NgxRequest, NgxResponse } from 'ngxc-universal';
    import { environment } from 'src/environments/environment';
    import { LOCALE_ID } from '@angular/core';
    
    const PORT = process.env.PORT || 4000;
    const DIST_FOLDER = join(process.cwd(), 'dist/web');
    
    const {LAZY_MODULE_MAP} = require('./src/main.server');
    
    
    
    
    
    export function app(lang: string){
      const localePath =  'browser' + lang;
    
      const server = express();
      server.use(compression());
      server.use(cookieparser());
      server.set('view engine', 'html');
      server.set('views', join(DIST_FOLDER, 'browser' + lang));
    
      server.get('*.*', express.static(join(DIST_FOLDER, localePath), {
        maxAge: '1y'
      }));
    
      server.engine('html', ngExpressEngine({
        bootstrap: AppServerModule,
        providers: [
          provideModuleMap(LAZY_MODULE_MAP),
          {provide: LOCALE_ID, useValue: lang}
        ]
      }));
    
      server.get('*', (req, res) => {
        res.render(`index`, {
          req,
          res,
          providers: [
            { provide: NgxRequest, useValue: req },
            { provide: NgxResponse, useValue: res }
            ]
        });
      });
    
    
      return server;
    
    }
    
    
    function run() {
      const server = express();
      const appEn = app('/en');
      const appES = app('/es');
    
      server.use('/en', appEn);
      server.use('/', appES);
    
    
    
      server.listen(PORT, () => {
          console.log(`Node Express server listening on http://localhost:${PORT}`);
      });
    }
    declare const __non_webpack_require__: NodeRequire;
    const mainModule = __non_webpack_require__.main;
    const moduleFilename = (mainModule && mainModule.filename) || '';
    if ( (!environment.production && moduleFilename === __filename) ||
      moduleFilename.includes('iisnode')
    ) {
      run();
    }
    
    export * from './src/main.server';
    

  2. The server is likely serving the static file first by default. To get around that you should change the name of index.html to index.origial.html in the dist folder.

    In your server.ts file

      // check for the correct version
      const indexHtml = existsSync(join(distFolder, 'index.original.html'))
        ? 'index.original.html'
        : 'index.html';
    

    So the result will be that your ‘es’ version is then called index.original.html and the secondary version will become the new static version in index.html will be served first.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search