angular / angular-cli

CLI tool for Angular
https://cli.angular.io
MIT License
26.72k stars 11.99k forks source link

SSR with i18n with Angular 9 not working #25726

Open joshribb opened 4 years ago

joshribb commented 4 years ago

🐞 Bug report

What modules are related to this issue?

Is this a regression?

No, localize is new to Angular 9.

Description

The distFolder is hardcoded in ./server.ts. When the browser server assets are built with localize: true, the assets are placed in a subfolder with the locale name (eg: dist/{appName}/browser/{locale}/ and dist/{appName}/server/{locale}). Now the server can no longer find the correct directory for the browser assets and fails to render.

Is there any way server.ts can know location of the browser assets without hardcoding the path?

Thanks.

πŸ”¬ Minimal Reproduction

ng new ng-sample
ng add @angular/localize@next
ng add @nguniversal/express-engine@next

add localize: true to the options of the build and server architect

ng build --prod
ng run ng-sample:server:production
node dist/ng-sample/server/en-US/main.js

browser to http://localhost:4000

πŸ”₯ Exception or Error

Error: Failed to lookup view "index" in views directory "/code/ng-sample/dist/ng-sample/browser"
    at Function.render (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1122933)
    at ServerResponse.render (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1398756)
    at server.get (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:2259271)
    at Layer.handle [as handle_request] (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1144375)
    at next (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1131913)
    at Route.dispatch (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1131942)
    at Layer.handle [as handle_request] (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:1144375)
    at /code/ng-sample/dist/ng-sample/server/en-US/main.js:1:2473361
    at param (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:2474870)
    at param (/code/ng-sample/dist/ng-sample/server/en-US/main.js:1:2475277)

🌍 Your Environment

Angular CLI: 9.0.0-rc.9
Node: 10.16.0
OS: darwin x64

Angular: 9.0.0-rc.9
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, localize, platform-browser
... platform-browser-dynamic, platform-server, router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.900.0-rc.9
@angular-devkit/build-angular     0.900.0-rc.9
@angular-devkit/build-optimizer   0.900.0-rc.9
@angular-devkit/build-webpack     0.900.0-rc.9
@angular-devkit/core              9.0.0-rc.9
@angular-devkit/schematics        9.0.0-rc.9
@ngtools/webpack                  9.0.0-rc.9
@nguniversal/builders             9.0.0-rc.0
@nguniversal/common               9.0.0-rc.0
@nguniversal/express-engine       9.0.0-rc.0
@schematics/angular               9.0.0-rc.9
@schematics/update                0.900.0-rc.9
rxjs                              6.5.4
typescript                        3.6.4
webpack                           4.41.2
keserwan commented 4 years ago

I updated my answer on https://github.com/angular/universal/issues/1497

keserwan commented 4 years ago

I made it work with 2 node apps working on the server. If anybody can come up with 1 node app serving all locales that would be perfect as we were doing it with webpack.server.config previously.

MarcusRiemer commented 4 years ago

I am in the same boat: Previously I could import multiple bundles and load them dynamically based on the URL that was requested. Now I need to run one server for each language, this is quite tedious.

ymlsam commented 4 years ago

Same here. I adopted same setup for my production site as mentioned by @keserwan in angular/universal#1497, which is now broken.

Besides, look like this block of code is wrong. "req.baseUrl" causes app routing on server side to fail:

server.get('*', (req, res) => {
    res.render(indexHtml, {req, providers: [{provide: APP_BASE_HREF, useValue: req.baseUrl}]});
\});

I changed it as follows, and the routing works again:

const baseHref = '/en/';
server.get(baseHref + '*', (req, res) => {
    res.render(indexHtml, {req, providers: [{provide: APP_BASE_HREF, useValue: baseHref}]});
});
squelix commented 4 years ago

Any update about this ?

ftanrisevdi commented 4 years ago

Hi, Is there any progress about this issue?

piotrbrzuska commented 4 years ago

I add args to my server.ts : server.ts:

var args = process.argv.splice(process.execArgv.length + 2);
var local = args[0];
const distFolder = join(__dirname, '../../browser', local);

and run it with:

    "serve:ssr:pl": "node dist/app/server/pl/main.js pl",

but I have a problem with build configuration in angular.json. What is a proper way to build ssr with many configurations at once - production and some locale ?

piotrbrzuska commented 4 years ago

Ok, I spend about a one work day, but I'm do it.

a idea: to have files like ~/dist/app-name/server/{locale}/main.js and one main ~/server.js, which start all locales servers as modules:

var express = require('express')
const server = express();

var locals = ["en", "pl", "fr", "de"]; 
for (let idx = 0; idx < locals.length; idx++) {
    const local = locals[idx];
    var localModule = require(`./dist/app-name/server/${local}/main.js`);
    server.use('/'+local, localModule.app(local));
    idx == 0 && server.use('/', localModule.app(local)); // use first locale as main locale
}
const port = process.env.PORT || 4000;
server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
});

other things i must change is pass Locale_ID to APP_BASE_HREF in my (Browser) AppModule.

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
 // ...
  ],
  providers: [
// ....
    { provide: APP_BASE_HREF, useFactory: (locale: string) => locale, deps: [LOCALE_ID] },
// ...
  ],
  bootstrap: [AppComponent]
})
export class AppModule { 
Fafnur commented 4 years ago

@piotrbrzuska

For server localize build, you can use parameter localize on server task on angular.json:

"server": {
    "builder": "@angular-devkit/build-angular:server",
     "options": {
         ...
            "localize": ["en", "ru"]
         ...  
     }
}
squelix commented 4 years ago

Ok, I spend about a one work day, but I'm do it.

a idea: to have files like ~/dist/app-name/server/{locale}/main.js and one main ~/server.js, which start all locales servers as modules:

var express = require('express')
const server = express();

var locals = ["en", "pl", "fr", "de"]; 
for (let idx = 0; idx < locals.length; idx++) {
    const local = locals[idx];
    var localModule = require(`./dist/app-name/server/${local}/main.js`);
    server.use('/'+local, localModule.app(local));
    idx == 0 && server.use('/', localModule.app(local)); // use first locale as main locale
}
const port = process.env.PORT || 4000;
server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
});

other things i must change is pass Locale_ID to APP_BASE_HREF in my (Browser) AppModule.

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
 // ...
  ],
  providers: [
// ....
    { provide: APP_BASE_HREF, useFactory: (locale: string) => locale, deps: [LOCALE_ID] },
// ...
  ],
  bootstrap: [AppComponent]
})
export class AppModule { 

Can i have your full config please ?

baj9032 commented 4 years ago

any solution to run application (with multiple language) on single express port?????

marcmarcet commented 4 years ago

@piotrbrzuska solution worked for me.

Basically, I did the following:

server.ts:

export function app(locale) {
    const server = express();

    server.engine(
        'html',
        ngExpressEngine({
            bootstrap: AppServerModule,
        })
    );

    const distPath = join(process.cwd(), `dist/my-app/browser/${locale}`);

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

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

    server.get('*', (req, res) => {
        res.render(join(distPath, 'index.html'), {
            req,
            providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
        });
    });

    return server;
}

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

Then I created a separate server.run.js with this:


function app() {
    const server = express();

    ['ca', 'en', 'en-gb', 'es'].forEach((locale) => {
        const appServerModule = require(path.join(__dirname, 'dist', 'my-app', 'server', locale, 'main.js'));
        server.use(`/${locale}`, appServerModule.app(locale));
    });

    return server;
}

function run() {
    app().listen(4200, () => {
        console.log(`Node Express server listening on http://localhost:4200`);
    });
}

run();
p3x-robot commented 4 years ago

@piotrbrzuska solution worked for me.

Basically, I did the following:

server.ts:

export function app(locale) {
    const server = express();

    server.engine(
        'html',
        ngExpressEngine({
            bootstrap: AppServerModule,
        })
    );

    const distPath = join(process.cwd(), `dist/my-app/browser/${locale}`);

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

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

    server.get('*', (req, res) => {
        res.render(join(distPath, 'index.html'), {
            req,
            providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
        });
    });

    return server;
}

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

Then I created a separate server.run.js with this:


function app() {
    const server = express();

    ['ca', 'en', 'en-gb', 'es'].forEach((locale) => {
        const appServerModule = require(path.join(__dirname, 'dist', 'my-app', 'server', locale, 'main.js'));
        server.use(`/${locale}`, appServerModule.app(locale));
    });

    return server;
}

function run() {
    app().listen(4200, () => {
        console.log(`Node Express server listening on http://localhost:4200`);
    });
}

run();

wow this is awesome!!! perfect solution! thanks!

panki commented 4 years ago

@marcmarcet-codinghumans thanks for the solution, but how you build your server.run.ts?

If I build it the same way as the angular server webpack warns me: Critical dependency: the request of a dependency is an expression.

I think it's because of using dynamic require.

p3x-robot commented 4 years ago

@marcmarcet-codinghumans thanks for the solution, but how you build your server.run.ts?

If I build it the same way as the angular server webpack warns me: Critical dependency: the request of a dependency is an expression.

I think it's because of using dynamic require.

nope, for me it works, and i am using multiple languages with 1 express app.

panki commented 4 years ago

@marcmarcet-codinghumans thanks for the solution, but how you build your server.run.ts? If I build it the same way as the angular server webpack warns me: Critical dependency: the request of a dependency is an expression. I think it's because of using dynamic require.

nope, for me it works, and i am using multiple languages with 1 express app.

do you use custom webpack config to build server.run.ts?

p3x-robot commented 4 years ago

@marcmarcet-codinghumans thanks for the solution, but how you build your server.run.ts? If I build it the same way as the angular server webpack warns me: Critical dependency: the request of a dependency is an expression. I think it's because of using dynamic require.

nope, for me it works, and i am using multiple languages with 1 express app.

do you use custom webpack config to build server.run.ts?

server.ts

/***************************************************************************************************
 * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
 */
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 { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
//import { existsSync } from 'fs';

// The Express app is exported so that it can be used by serverless Functions.
export function app(locale) {
  const server = express();

//  const distFolder = join(process.cwd(), 'dist/backwash-ai/browser');

  //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,
  }));

  const distPath = join(process.cwd(), `dist/backwash-ai/browser/${locale}`);

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

  // Example Express Rest API endpoints
  // server.get('/api/**', (req, res) => { });
  // Serve static files from /browser
  server.get('*.*', express.static(distPath, {
    maxAge: '1y'
  }));

  // All regular routes use the Universal engine
  server.get('*', (req, res) => {
    res.render(join(distPath, 'index.html'),  { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  });

  return server;
}

/*
function run() {
  const port = process.env.PORT || 1978;

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

server.run.js

const path = require('path')
const apiService = require('./api/src/service/boot')

function runApps(appWithOptions) {

  const server = appWithOptions.express.app

  const locales = appWithOptions.config.locales

  locales.forEach((locale) => {
    const appServerModule = require(path.join(__dirname, 'dist', 'backwash-ai', 'server', locale, 'main.js'));
    server.use(`/${locale}`, appServerModule.app(locale));
  });

}

function run() {
  process.env.NODE_ENV = process.env.NODE_ENV || 'development'
  const appWithOptions = apiService({
    express: {
      cors: false
    }
  })

  const app = appWithOptions.express.app

  if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'ssr') {
    runApps(appWithOptions)

    const findOutLocale = (cookieLocale) => {
      if (appWithOptions.config.locales.includes(cookieLocale)) {
        return cookieLocale
      }
      return appWithOptions.config.defaultLocale
    }

    app.get('/', function(req, res) {
      res.redirect(`/${findOutLocale(req.cookies['bw-lang'])}`);
    });

    app.get('*', function(req, res) {
      res.redirect(`/${findOutLocale(req.cookies['bw-lang'])}` + req.url);
    });
  }

  const port = process.env.PORT || appWithOptions.config.port;

  appWithOptions.express.app.listen(port, () => {
    console.log(`backwash-ai listening on http://localhost:${port} on ${process.env.NODE_ENV}`);
  });
}

run();

i have a hack that is described here: https://github.com/angular/universal/issues/1689

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "backwash-ai": {
      "i18n": {
        "sourceLocale": "en",
        "locales": {
          "hu": "messages.hu.xlf"
        }
      },
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "skipTests": true,
          "style": "scss"
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "bw",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "localize": ["en", "hu"],
            "outputPath": "dist/backwash-ai/browser",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          },
          "configurations": {
            "en": {
              "localize": ["en"],
              "baseHref": "/en/"
            },
            "production": {
              "i18nMissingTranslation": "error",
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "backwash-ai:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "backwash-ai:build:production"
            },
            "en": {
              "browserTarget": "backwash-ai:build:en"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "backwash-ai:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.spec.json",
            "karmaConfig": "karma.conf.js",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "tsconfig.app.json",
              "tsconfig.spec.json",
              "e2e/tsconfig.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        },
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "backwash-ai:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "backwash-ai:serve:production"
            }
          }
        },
        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/backwash-ai/server",
            "main": "server.ts",
            "localize": ["en", "hu"],
            "tsConfig": "tsconfig.server.json"
          },
          "configurations": {
            "production": {
              "outputHashing": "media",
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "sourceMap": false,
              "optimization": true
            }
          }
        },
        "serve-ssr": {
          "builder": "@nguniversal/builders:ssr-dev-server",
          "options": {
            "browserTarget": "backwash-ai:build",
            "serverTarget": "backwash-ai:server"
          },
          "configurations": {
            "production": {
              "browserTarget": "backwash-ai:build:production",
              "serverTarget": "backwash-ai:server:production"
            }
          }
        },
        "prerender": {
          "builder": "@nguniversal/builders:prerender",
          "options": {
            "browserTarget": "backwash-ai:build:production",
            "serverTarget": "backwash-ai:server:production",
            "routes": [
              "/"
            ]
          },
          "configurations": {
            "production": {}
          }
        }
      }
    }},
  "defaultProject": "backwash-ai"
}
panki commented 4 years ago

@p3x-robot could you also show your tsconfig.server.json?

panki commented 4 years ago

@p3x-robot I don't see a target to build server.run.ts in your angular.json', but looks like you somehow managed to build both server.ts and server.run.ts by running ng run backwash-ai:server:production, did I miss something?

p3x-robot commented 4 years ago

tsconfig.server.json

{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app-server",
    "module": "commonjs",
    "types": [
      "node"
    ]
  },
  "files": [
    "src/main.server.ts",
    "server.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "./src/app/app.server.module#AppServerModule"
  }
}

as i said, it is a hack an is written here: https://github.com/angular/universal/issues/1689

spock123 commented 4 years ago

Maybe this can help for inspiration as well:

https://medium.com/@marcozuccaroli/a-multilanguage-application-with-angular-universal-6e5fe4c2d81c

adamgasiorek commented 4 years ago

Any update of this, at incoming Angular 10 ?

adamgasiorek commented 4 years ago

I am sharing ready working solution for Angular 10 on one port based on your answers πŸš€πŸš€

Repositorium

Angular documentation is so deprecated, maybe this gonna helps someone ;)

kana7 commented 4 years ago

I tried to apply @marcmarcet-codinghumans solution for my project but I keep getting a server timeout. I am hosting my project using Firebase. Does anybody have successfully served an angular universal app with i18n and firebase/cloud function ? Here's my question on stackoverflow for the details.

spock123 commented 4 years ago

I tried to apply @marcmarcet-codinghumans solution for my project but I keep getting a server timeout. I am hosting my project using Firebase. Does anybody have successfully served an angular universal app with i18n and firebase/cloud function ? Here's my question on stackoverflow for the details.

You're using US-1 region, right? Cloud Functions only work in that region afaik

kana7 commented 4 years ago

You're using US-1 region, right? Cloud Functions only work in that region afaik

Yes, I do. The cloud function get executed, but it doesn't render anything when I do:

server.get('*', (req, res) => {
   // this log shows up in my firebase console
   console.log(`serving request, with locale ${locale}, base url: ${req.baseUrl}, accept-language: ${req.headers["accept-language"]}`);
    res.render('index.html', {
      req,
      providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
    });
  });
kana7 commented 4 years ago

It turns out that the problem comes from angularFire (firebase/firestore). Any data query using a rxjs pipe with take(1) in the application, cause angular universal to get stuck in a infinite loop until the server timeout.. https://github.com/angular/angularfire/issues/2420

ranibb commented 4 years ago

why it is always a nightmare deploying SSR with i18n to firebase?

PowerKiKi commented 3 years ago

Here is the entire commit (minus my app very specific changes) that I used to add SSR on an Angular 10 app that already used i18n: https://gist.github.com/PowerKiKi/b8ecd4bdfb3f4d694a1370e3c57a5062

It is based on the server.run.js solution. But it automatically gets locales from angular.json (so no duplicated config). And it automatically use the proxy config that you might need for your local API.

server.ts still has its run() function in order to run yarn dev-ssr, although the app still fails because of incorrect baseHref. And it has a full configuration for pm2 where you can see that server.run.js is the main entry point (and not server.ts anymore).

And to be extra complete here is the relevant nginx configuration to proxy bots, but no humans, to the SSR.

boban100janovski commented 3 years ago

I followed the guide in this blog post, helped me alot. https://medium.com/@pierre.machaux/angular-universal-and-i18n-working-together-8828423e8a68

seliceantitus commented 3 years ago

I followed the guide in this blog post, helped me alot. https://medium.com/@pierre.machaux/angular-universal-and-i18n-working-together-8828423e8a68

That guide helped me a lot too. Might be off-topic, but just a note, if you're using Angular 10, when installing the dependencies with ng add it might put it in devDependencies. This happened to me with the @angular/localize package and the language code URL prefix was not working at all, throwing cannot match route errors.

Took me a while to figure it out and I fixed it by moving @angular/localize to dependencies and now redirects, routerLink and app URLs are working flawlessly.

mferuscomelo commented 3 years ago

From what I understand, it's just easier to use ngx-translate?

PowerKiKi commented 3 years ago

The difficulty experienced here is because i18n produce one build for each locale, whereas ngx-translate can only produce a single build. So you may save a few line of code with ngx-translate for SSR, but you would lose in performance for non-SSR because of the extra cost to manage locale in a single build (or gain in flexibility depending on your point of view...)

mferuscomelo commented 3 years ago

The difficulty experienced here is because i18n produce one build for each locale, whereas ngx-translate can only produce a single build. So you may save a few line of code with ngx-translate for SSR, but you would lose in performance for non-SSR because of the extra cost to manage locale in a single build (or gain in flexibility depending on your point of view...)

Got it. I managed to get it working with i18n using the server.run.js file. I'd like to reroute users to their respective localized url as well as reroute non-localized urls to the default, english. What's the best way to do this? I'm using Firebase for hosting if it helps.

PowerKiKi commented 3 years ago

Can't help you with Firebase. Best I can do is direct you to https://angular.io/guide/i18n#configuring-servers where you'll find suggestions for the mechanism you describe for nginx and Apache

ahmedelshantaly commented 3 years ago

Is there a way to change server-run.js to typescript and bundle it to avoid installing express on docker environment ?

douglasward commented 3 years ago

Did anyone have any luck running the above solutions with webpack 5? I just upgraded my working solution to angular 12.0.1 and now the compiling of server.run.js just hangs after outputting <s> [webpack.Progress] 100%.

piotrbrzuska commented 3 years ago

Did anyone have any luck running the above solutions with webpack 5? I just upgraded my working solution to angular 12.0.1 and now the compiling of server.run.js just hangs after outputting <s> [webpack.Progress] 100%.

I'm quitting, I'm going to migrate to i18next

PowerKiKi commented 3 years ago

I'd like to report that I migrated two projects with my solution from Angular 11 to 12 without any problems. In fact I did not change a single thing and the builds kept working.

itea-dev commented 3 years ago

Did anyone have any luck running the above solutions with webpack 5? I just upgraded my working solution to angular 12.0.1 and now the compiling of server.run.js just hangs after outputting <s> [webpack.Progress] 100%.

I'm quitting, I'm going to migrate to i18next

I'd suggest transloco library unless it's missing some feature you need.

https://github.com/ngneat/transloco

PowerKiKi commented 2 years ago

@alan-agius4 you closed this issue via angular/universal#2567, but I don't see how that PR, that only remove stuff, could fix support for i18n.

Did you mean to close this issue as wontfix ? or was it a mistake to close this issue ? or did i18n indeed got support in a way that I could not figure out yet ?

nikxco commented 2 years ago

Angular CLI: 13.3.3 Node: 14.18.3 Package Manager: npm 6.14.15 OS: darwin x64

Angular: 13.3.4 ... animations, cdk, common, compiler, compiler-cli, core, forms ... localize, material, platform-browser ... platform-browser-dynamic, platform-server, router ... service-worker

Package Version

@angular-devkit/architect 0.1303.3 @angular-devkit/build-angular 13.3.3 @angular-devkit/core 13.3.3 @angular-devkit/schematics 13.3.3 @angular/cli 13.3.3 @angular/flex-layout 13.0.0-beta.36 @nguniversal/builders 13.1.0 @nguniversal/express-engine 13.1.0 @schematics/angular 13.3.3 rxjs 7.5.5 typescript 4.6.3


Any update on this? I'm facing the same issue.

ciriousjoker commented 2 years ago

To add onto @marcmarcet 's workaround:

1) It works (thank you so much!) 2) It shouldn't be necessary, SSR with i18n is not an exotic use case. 3) When specifying multiple locales in angular.json, make sure to end the baseHref with a /, otherwise view-source will have the prerendered texts in the correct language, but as soon as the main.js bundle is loaded from /, the texts are replaced with the default language again. 4) Here's a version that hosts the default locale (e.g. en-US) on /:

const defaultLocale = "en-US";

for (const locale of ["de", defaultLocale]) {
  const appServerModule = require(path.join(__dirname, "dist", "frontend", "server", locale, "main.js"));
  server.use(locale == defaultLocale ? "/" : `/${locale}`, appServerModule.app(locale));
}
"i18n": {
    "sourceLocale": {
      "code": "en-US",
      "baseHref": "/" // <--
    },
    ...

I know this was a little bit off-topic, but I can't be the only one struggling with this, perhaps it helps someone.

nschipperbrainsmith commented 1 year ago

The approach listed within this topic works for the build followed by run. The thing that so far isn't working is having a reliable approach for the dev:ssr there was another issue about this (https://github.com/angular/universal/issues/1689) which got closed but I would also like to voice my support.

It should be possible to somehow tell serve-ssr that it should modify it's delivery path or to specify a custom js file for the main.js file to be ran as the primary express server file. Since this is the only reason it isn't working right now. It tries to grab the main.js file from the dist/<<project>>/server root instead of the localized sub-path.

If i override this manually in the lib file, everything works which is such a shame.

anisabboud commented 1 year ago

Overall however the approach highlighted above in this comment #1454 (comment) is the recommended approach for build time i18n.

@alan-agius4 from my understanding, the recommended approach includes a hack from https://github.com/angular/universal/issues/1689 inside node_modules. That sounds very problematic, as the hack will get erased on npm install (unless we add more hacks).

Angular Universal and Angular i18n are two major Angular features, and I was expecting them to work together seamlessly by v15. For instance, when you run ng add @nguniversal/express-engine, the schematics should check that you're using i18n and update the server.ts code accordingly. npm run prerender isn't working for me either.

Could you guys please revisit this issue or recommend a workaround for Angular 15 prerendering with i18n?


A bit more context on my use-case:


Update: npm run prerender worked after adding "localize": ... to angular.json under "server" -> "configurations" -> "production" (see comment below).

PowerKiKi commented 1 year ago

My solution requires no hacks and it still in use in production today with Angular 15. You might want to try that if the rest is not working for you.

nschipperbrainsmith commented 1 year ago

Overall however the approach highlighted above in this comment #1454 (comment) is the recommended approach for build time i18n.

@alan-agius4 from my understanding, the recommended approach includes a hack from angular/universal#1689 inside node_modules. That sounds very problematic, as the hack will get erased on npm install (unless we add more hacks).

Angular Universal and Angular i18n are two major Angular features, and I was expecting them to work together seamlessly by v15. For instance, when you run ng add @nguniversal/express-engine, the schematics should check that you're using i18n and update the server.ts code accordingly. npm run prerender isn't working for me either.

Could you guys please revisit this issue or recommend a workaround for Angular 15 prerendering with i18n?

A bit more context on my use-case:

  • App in 3 languages, ran ng add @nguniversal/express-engine
  • Then tried npm run prerender => error in node_modules/@nguniversal/builders/src/prerender/index.js: An unhandled exception occurred: Could not find the main bundle: dist/my-project/server/en/main.js
  • Indeed there is no such file because there isn't an /en/ folder under server/:

    $ ls dist/my-project/server/
    269.js  426.js  562.js  793.js  875.js  50.js   623.js  868.js  main.js  3rdpartylicenses.txt  (files)
    
    $ ls dist/my-project/browser/
    en  ab  cd  (folder per locale)
  • The error comes from this line: https://github.com/angular/universal/blob/b5b9c1761fc647f6dc64187d443a72fcee306cf2/modules/builders/src/prerender/index.ts#L116

I have also not went the route of a hack this as we work in a larger team and such a thing would result in a various amount of problems. Instead I copied the entire builder and changed it. This is now part of the project and I use it within the angular.json as a drop in replacement for the regular serve:ssr (dev version). The only caveat it has is that it no longer supports multiple languages when doing the dev serve ssr but given that this is a minor thing for development purposes we can deal with that. As it now gives us a view that very closely matches the production view with the rest of the containerized dev environment.

anisabboud commented 1 year ago

@PowerKiKi and @schippie - thanks for the tips!

Indeed I managed to get npm run prerender to work without any hacks and with a single change in angular.json - only had to add "localize": ["en", "ab", "cd"] under "server" -> "configurations" -> "production"` (to match the regular "build" "production" configuration).

It seems that the ng add @nguniversal/express-engine schematics don't copy over the localize value when generating the "server" configuration. I believe this can be fixed in the schematics to improve developer experience.

(Clarification: For now I'm only doing prerendering, which worked well without hacks - just by adding one localize line to angular.json like I mentioned. I haven't fully tried SSR yet, but npm run dev:ssr seems to work too.)

PowerKiKi commented 1 year ago

You can probably use the simpler "localize": true, instead of repeating every locale. That's what we do.

Here we never prerender, we only render live. Glad it could also work for prerendering then :+1:

aleesaan commented 1 year ago

In our app we don't keep the language in the URL (i.e. we use "baseHref": "" for every locale), so I need to serve the localized bundles based on the request's headers or cookie. So instead of having server.use(`/${locale}`, appServerModule.app(locale)) for each locale, I'd like to have something like server.use('', (req) => appServerModule.app(getLocaleFromReq(req))), but I can't get it to work...
Is there a way I can achieve this adapting the recommended approach? @PowerKiKi do you have suggestions maybe?