gilsdav / ngx-translate-router

Translate routes using ngx-translate
132 stars 43 forks source link

ngx-translate-router implementation with Angular Universal SSR #89

Closed vbonne closed 4 years ago

vbonne commented 4 years ago

I am implementing ngx-translate-router in an Angular Universal app already working fine with SRR. After adding ngx-translate-router with http loader it works correctly with ng serve so it means that the integration without SSR is working fine.

But when running in SSR mode : npm run serve:ssr i get this error :

NetworkError
at XMLHttpRequest.send (F:\GitaLab\vyv-angular\dist\server\main.js:1:819512)
at Observable_Observable._subscribe (F:\GitaLab\vyv-angular\dist\server\main.js:1:3285565)
at Observable_Observable._trySubscribe (F:\GitaLab\vyv-angular\dist\server\main.js:1:576303)
at Observable_Observable.subscribe (F:\GitaLab\vyv-angular\dist\server\main.js:1:576085)
at CatchOperator.call (F:\GitaLab\vyv-angular\dist\server\main.js:1:3994238)
at Observable_Observable.subscribe (F:\GitaLab\vyv-angular\dist\server\main.js:1:575939)
at DoOperator.call (F:\GitaLab\vyv-angular\dist\server\main.js:1:3343772)
at Observable_Observable.subscribe (F:\GitaLab\vyv-angular\dist\server\main.js:1:575939)
at F:\GitaLab\vyv-angular\dist\server\main.js:1:3315893
at Observable_Observable._subscribe (F:\GitaLab\vyv-angular\dist\server\main.js:1:3316238)

I implemented the SSR part based your instructions so what i did additional for the SSR part are the following :

1 - implemented an interceptor in the app.server.module.ts to be able to access the translations within the server part. Here is the interceptor :

    import { REQUEST } from '@nguniversal/express-engine/tokens';
    import * as express from 'express';
    import {Inject, Injectable} from '@angular/core';
    import {HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';

    @Injectable()
    export class TranslateInterceptor implements HttpInterceptor {
      private readonly DEFAULT_PORT = 4200;
      private readonly PORT = process.env.PORT || this.DEFAULT_PORT;

      constructor(@Inject(REQUEST) private request: express.Request) {}

      getBaseUrl(req: express.Request) {
        const { protocol, hostname } = req;
        return this.PORT ?
          `${protocol}://${hostname}:${this.PORT}` :
          `${protocol}://${hostname}`;
      }

      intercept(request: HttpRequest<any>, next: HttpHandler) {
        if (request.url.startsWith('./assets')) {
          const baseUrl = this.getBaseUrl(this.request);
          request = request.clone({
            url: `${baseUrl}/${request.url.replace('./assets', 'assets')}`
          });
        }
        return next.handle(request);
      }
    }

2 - I modified the server.ts to access the different locales and added the routes for them, but I think the issues lies there. I think I incorrectly added the routes listening in the server.ts but i did not find help about this anywhere and the instructions on github are a little short for my level with angular SSR...

here is the server.ts


// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/browser');
  const fs = require('fs');
  const data: any = JSON.parse(fs.readFileSync(`src/assets/locales.json`, 'utf8'));
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
  server.use(cookieParser());
  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));
  server.set('view engine', 'html');
  server.set('views', distFolder);
  server.get('/', (req, res) => {
    const defaultLang = 'en';
    const lang = req.acceptsLanguages('en', 'de', 'fr', 'es', 'pt');
    let cookieLang = req.cookies.lang;
    if (!cookieLang) {
      cookieLang = req.cookies.LOCALIZE_DEFAULT_LANGUAGE; // This is the default name of cookie
    }
    const definedLang = cookieLang || lang || defaultLang;
    console.log('domain requested without language');
    res.redirect(301, `/${definedLang}/`);
  });
  // Example Express Rest API endpoints
  // server.get('/api/**', (req, res) => { });
  // Serve static files from /browser
  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));
  console.log('routes for the locales:');
  console.log(data);
  data.locales.forEach(route => {
    server.get(`/${route}`, (req: express.Request, res: express.Response) => {
      console.log('domain requested with language' + req.originalUrl);
      res.render(indexHtml, {
        req, providers: [
          { provide: REQUEST, useValue: req }
        ]
      });
    });
    server.get(`/${route}/*`, (req: express.Request, res: express.Response) => {
      console.log('page requested with language ' + req.originalUrl);
      res.render(indexHtml, {
        req, providers: [
          { provide: REQUEST, useValue: req }
        ]
      });
    });
  });
  return server;
}

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

  // Start up the Node server
  const server = app();
  server.use(compression());
  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';

when I start the SSR server and then request the page http://localhost I can see the redirect working to the default language and the console logs the "domain requested with language /en/" before the error reported above.

I think the issue is that the server.ts does not manage to map the requested url to something within the routes declared in the app-routing.module.ts but i don't know how to do that. In the GitHub repository your say :

// let node server knows about the new routes:

let fs = require('fs');
let data: any = JSON.parse(fs.readFileSync(`src/assets/locales.json`, 'utf8'));

app.get('/', ngApp);
data.locales.forEach(route => {
  app.get(`/${route}`, ngApp);
  app.get(`/${route}/*`, ngApp);
});

but you don't describe what "ngApp" is so i just extrapolated it base on how the server.ts was before integrating this plugin:

  // All regular routes use the Universal engine
  server.get('*', (req: express.Request, res: express.Response) => {
    res.render(indexHtml, {
      req, providers: [
        { provide: REQUEST, useValue: req }
      ]
    });
  });

So my question is double. Do you think I am right to keep searching on the direction that I dentified ? (server.ts implementation is wrong). If yes do you have an idea how to correct it ? If no, any other direction to look for ?

vbonne commented 4 years ago

i have been 10h on this... I am not good at debugging node... the error was not in the server.ts but in the interceptor. This interceptor, i orginally designed it for ngx-translate and I thought it could be shared just like that to retrieve the data from ./assets/locales.json as it was working fine in SSR for retrieving the ./assets/i18n/en.json, but no luck. ngx-translate and ngx-translate-router cannot share the same interceptor, I don't have the exact reason but that's it. So I had to create a second interceptor (code below) and this solved my issue.

I know I did not inspire a lot of responses but maby one day this post will help someone.

    import { REQUEST } from '@nguniversal/express-engine/tokens';
    import * as express from 'express';
    import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
    import {HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
    import {isPlatformServer} from '@angular/common';

    @Injectable()
    export class LocalizeInterceptor implements HttpInterceptor {
      constructor(@Inject(REQUEST) private request: express.Request, @Inject(PLATFORM_ID) private platformId: any) {}
      intercept(request: HttpRequest<any>, next: HttpHandler) {
        if (request.url.startsWith('assets') && isPlatformServer(this.platformId)) {
          const req = this.request;
          const url = req.protocol + '://' + req.get('host') + '/' + request.url;
          request = request.clone({
            url
          });
        }
        return next.handle(request);
      }
    }

I hope I did not polute your repo, you can close the issue, sorry about this, your tutorial is clear enough, my fault for being lazy and trying to share interceptors between modules, where i should not have...

gilsdav commented 4 years ago

Hello @vbonne,

Relative paths doesn't work on SSR so you have the choice between three things:

  1. Using absolute path (with host included)
  2. Creating an interceptor or specific loader to load files using NodeJS FS.
  3. A mixture of the first two points: Interceptor to translate relative to absolute path.

This providers can be added to app.server.module.ts to use it only on SSR part.