Last active
October 21, 2022 01:16
-
-
Save GeorgDangl/70c01f40b2a2d49bbee58c3dffd7bad7 to your computer and use it in GitHub Desktop.
Serving localized Angular SPA apps from Asp.Net Core, see https://blog.dangl.me/archive/serving-localized-angular-single-page-applications-with-aspnet-core/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"configurations": { | |
"production": { | |
"optimization": true, | |
"outputHashing": "all", | |
"sourceMap": false, | |
"extractCss": true, | |
"namedChunks": false, | |
"aot": true, | |
"extractLicenses": true, | |
"vendorChunk": false, | |
"buildOptimizer": true, | |
"fileReplacements": [{ | |
"replace": "src/environments/environment.ts", | |
"with": "src/environments/environment.prod.ts" | |
}], | |
"deployUrl": "/dist/en/" | |
}, | |
"production-de": { | |
"optimization": true, | |
"outputHashing": "all", | |
"sourceMap": false, | |
"extractCss": true, | |
"namedChunks": false, | |
"aot": true, | |
"extractLicenses": true, | |
"vendorChunk": false, | |
"buildOptimizer": true, | |
"fileReplacements": [{ | |
"replace": "src/environments/environment.ts", | |
"with": "src/environments/environment.prod.ts" | |
}], | |
"i18nFile": "src/locale/messages.de.xlf", | |
"i18nFormat": "xlf", | |
"i18nLocale": "de", | |
"deployUrl": "/dist/de/" | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Project.SpaExtensions | |
{ | |
public interface IUserLanguageService | |
{ | |
string GetUserLocale(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Project.SpaExtensions | |
{ | |
public class LocalizedSpaStaticFilePathProvider | |
{ | |
private readonly IUserLanguageService _userLanguageService; | |
private readonly string _distFolder; | |
public LocalizedSpaStaticFilePathProvider(IUserLanguageService userLanguageService, | |
string distFolder) | |
{ | |
_userLanguageService = userLanguageService; | |
_distFolder = distFolder; | |
} | |
public string GetRequestPath(string subpath) | |
{ | |
var userLocale = _userLanguageService.GetUserLocale(); | |
var spaFilePath = "/" + _distFolder + "/" + userLocale + subpath; | |
return spaFilePath; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.AspNetCore.Builder; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.AspNetCore.Http; | |
namespace Project.SpaExtensions | |
{ | |
public static class LocalizedSpaStaticFilesExtensions | |
{ | |
public static void AddLocalizedSpaStaticFiles(this IServiceCollection services, | |
string[] availableLocales, | |
string spaRootPath) | |
{ | |
services.AddTransient<IUserLanguageService>(serviceProvider => | |
{ | |
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); | |
return new UserLanguageService(httpContextAccessor, availableLocales); | |
}); | |
services.AddTransient<LocalizedSpaStaticFilePathProvider>(serviceProvider => | |
{ | |
var userLanguageService = serviceProvider.GetRequiredService<IUserLanguageService>(); | |
return new LocalizedSpaStaticFilePathProvider(userLanguageService, spaRootPath); | |
}); | |
} | |
public static void UseLocalizedSpaStaticFiles(this IApplicationBuilder applicationBuilder, string defaultFile) | |
{ | |
applicationBuilder.Use((context, next) => | |
{ | |
// In this part of the pipeline, the request path is altered to point to a localized SPA asset | |
var spaFilePathProvider = context.RequestServices.GetRequiredService<LocalizedSpaStaticFilePathProvider>(); | |
context.Request.Path = spaFilePathProvider.GetRequestPath("/" + defaultFile.TrimStart('/')); | |
return next(); | |
}); | |
applicationBuilder.UseStaticFiles(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"scripts": { | |
"build:production": "ng build --prod --aot --output-path ../Project/wwwroot/dist/en", | |
"build:production-de": "ng build --prod --configuration=production-de --aot --output-path ../Project/wwwroot/dist/de" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Project | |
{ | |
public class Startup | |
{ | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddLocalizedSpaStaticFiles(availableLocales: new[] { "de", "en" }, spaRootPath: "dist"); | |
} | |
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) | |
{ | |
app.UseStaticFiles(); | |
app.UseMvc(); | |
// This serves localized SPA files from disk, | |
// e.g. from wwwroot/dist/en | |
app.UseLocalizedSpaStaticFiles("index.html"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net.Http.Headers; | |
using Microsoft.AspNetCore.Http; | |
namespace Project.SpaExtensions | |
{ | |
public class UserLanguageService : IUserLanguageService | |
{ | |
private readonly IHttpContextAccessor _httpContextAccessor; | |
private readonly List<string> _availableLanguages; | |
public UserLanguageService(IHttpContextAccessor httpContextAccessor, | |
IEnumerable<string> availableLanguages) | |
{ | |
_httpContextAccessor = httpContextAccessor; | |
_availableLanguages = availableLanguages | |
.Select(l => l.ToLowerInvariant()) | |
.ToList(); | |
} | |
public string GetUserLocale() | |
{ | |
var request = _httpContextAccessor.HttpContext?.Request; | |
if (request == null) | |
{ | |
return _availableLanguages[0]; | |
} | |
var cookieLocale = request.Cookies[".User.Locale"]; | |
if (!string.IsNullOrWhiteSpace(cookieLocale)) | |
{ | |
return cookieLocale; | |
} | |
var userLocales = request.Headers["Accept-Language"].ToString(); | |
var userAcceptLanguage = GetAcceptLanguageFromHeaderOrNull(userLocales); | |
if (!string.IsNullOrWhiteSpace(userAcceptLanguage)) | |
{ | |
return userAcceptLanguage; | |
} | |
return _availableLanguages[0]; | |
} | |
public string GetAcceptLanguageFromHeaderOrNull(string headerValue) | |
{ | |
if (headerValue == null) | |
{ | |
return null; | |
} | |
try | |
{ | |
var clientLanguages = (headerValue) | |
.Split(',') | |
.Select(StringWithQualityHeaderValue.Parse) | |
.OrderByDescending(language => language.Quality.GetValueOrDefault(1)) | |
.Select(language => language.Value) | |
.Select(languageCode => | |
{ | |
if (languageCode.Contains("-")) | |
{ | |
return languageCode.Split('-').First(); | |
} | |
return languageCode; | |
}) | |
.Select(languageCode => languageCode.ToLowerInvariant()) | |
.Distinct() | |
.Where(languageCode => !string.IsNullOrWhiteSpace(languageCode) && languageCode.Trim() != "*"); | |
return clientLanguages | |
.FirstOrDefault(clientLanguage => _availableLanguages.Contains(clientLanguage)); | |
} | |
catch | |
{ | |
return null; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi thanks for gist. very helpful i am trying to implement same logic in .NET 6 but unable to get it working. UserLanguageService never gets called am i missing something? Any suggestion will be helpful thanks.