I'm doing most of my web development with an ASP.NET Core backend and an Angular frontend these days, and I'm quite happy with how these two work together.
ASP.NET Core serves both my API endpoints as well as the static files, so all I have to deploy is just a single website. Angular files are in this case served from the wwwroot/dist folder. This works fine as long as you only have to support a single language, but causes a bit of trouble as soon as you need internationalization, or i18n for short.
With Angular, there's no possibility to switch languages at runtime. That's because so much of the templates is compiled internally when you do a production build. So when you need a multilanguage app, you're actually deploying two independent Angular apps and need some way to serve different language files at runtime. For a lot of sites, going with different domains, e.g. .com for English and .de for German, is a viable approach. But many projects require that the user can select a language, without having to change domains or paths. And who wants to see /de in their urls, anyways?
As always, there's lots of solutions for how to solve this. The two most prominent ones are probably using a dedicated SPA controller that would return the correct entry point to your SPA, or simple re-route all not served requests to the SPA entries index.html at the end of your pipeline. I've chosen to do the latter.
Configuring Angular for Multilanguage Builds
This post isn't going to detail how to set up i18n in Angular, but here's a short recap: At first, you have to configure multiple configurations in your angular.json, one for each locale. Make sure that each one has a distinct deployUrl parameter that maps to where the static files will be put later, because that's going to be used as path for the injected references in index.html.
Then, make sure to have build scripts that you run before publishing the app in project.json:
Adding a Localized SPA Middleware to ASP.NET Core
To be able to serve different files based on a users locale, you have to implement something like IUserLanguageService:
It's up to you how you implement it, but I've decided to either use a stored Cookie set by the Angular frontend (so I can have a language switcher in the app) or default to the Accept-Language http header. Make sure your UserLanguageService implements some default language as fallback, since not all clients will send the required header or have Cookies set.
Next, there needs to be some provider that can generate the different paths on disk to the localized files, like dist/en and dist/de:
The LocalizedSpaStaticFilePathProvider simply builds the path to where each localized Angular app is stored.
Now you have to wire this together, for example in an extension class:
The LocalizedSpaStaticFilesExtensions configures the services required for our implementation of a localized SPA middleware. We're just providing it the list of availableLocales, so that the UserLanguageService won't return a locale we don't have a translation for. The spaRootPath parameter just indicates where, relative to wwwroot, we put all the localized builds.
For the application builder, we define a middleware that reroutes all requests to the localized defaultFile, e.g. index.html, for the current user and then have the regular ASP.NET Core StaticFiles middleware handle it. It's important to note that the applicationBuilder.UseStaticFiles(); call does not replace the regular static files middleware, it's just being called a second time. You must put UseLocalizedSpaStaticFiles() at the end of your request pipeline, because it alters all requests to your SPA entry point. That's required because your SPA most likely has routes of its own defined that your backend doesn't know, so you want to just return your SPA and let it handle client side routing.
Finally, configure everything in your Startup class:
That's how you get a localized app!