Merge pull request #762 from hishamco/localization-support

Localization support
This commit is contained in:
Shaun Walker 2020-10-01 10:06:01 -04:00 committed by GitHub
commit 666721bf1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 237 additions and 30 deletions

View File

@ -0,0 +1,3 @@
using Microsoft.Extensions.Localization;
[assembly: RootNamespace("Oqtane")]

View File

@ -32,6 +32,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.3" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
</ItemGroup>

View File

@ -1,19 +1,18 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using Oqtane.Services;
using System.Reflection;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using Oqtane.Modules;
using Oqtane.Shared;
using Oqtane.Providers;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
using System.IO.Compression;
using System.IO;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Modules;
using Oqtane.Providers;
using Oqtane.Shared;
using Oqtane.Services;
namespace Oqtane.Client
{
@ -28,6 +27,9 @@ namespace Oqtane.Client
builder.Services.AddSingleton(httpClient);
builder.Services.AddOptions();
// Register localization services
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
// register auth services
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<IdentityAuthenticationStateProvider>();
@ -101,8 +103,8 @@ namespace Oqtane.Client
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
{
Dictionary<string, byte[]> dlls = new Dictionary<string, byte[]>();
Dictionary<string, byte[]> pdbs = new Dictionary<string, byte[]>();
var dlls = new Dictionary<string, byte[]>();
var pdbs = new Dictionary<string, byte[]>();
foreach (ZipArchiveEntry entry in archive.Entries)
{
@ -115,7 +117,15 @@ namespace Oqtane.Client
switch (Path.GetExtension(entry.Name))
{
case ".dll":
dlls.Add(entry.Name, file);
// Loads the stallite assemblies early
if (entry.Name.EndsWith(Constants.StalliteAssemblyExtension))
{
Assembly.Load(file);
}
else
{
dlls.Add(entry.Name, file);
}
break;
case ".pdb":
pdbs.Add(entry.Name, file);

View File

@ -1,17 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Oqtane.Models;
using Oqtane.Shared;
using Oqtane.Infrastructure;
using System;
using System;
using System.IO;
using System.Reflection;
using System.Linq;
using System.IO.Compression;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Shared;
using Oqtane.Themes;
using System.Diagnostics;
namespace Oqtane.Controllers
{
@ -21,12 +20,14 @@ namespace Oqtane.Controllers
private readonly IConfigurationRoot _config;
private readonly IInstallationManager _installationManager;
private readonly IDatabaseManager _databaseManager;
private readonly ILocalizationManager _localizationManager;
public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, IDatabaseManager databaseManager)
public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager)
{
_config = config;
_installationManager = installationManager;
_databaseManager = databaseManager;
_localizationManager = localizationManager;
}
// POST api/<controller>
@ -73,6 +74,21 @@ namespace Oqtane.Controllers
// get list of assemblies which should be downloaded to browser
var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies();
var list = assemblies.Select(a => a.GetName().Name).ToList();
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// Get the satellite assemblies
foreach (var culture in _localizationManager.GetSupportedCultures())
{
if (culture == Constants.DefaultCulture)
{
continue;
}
foreach (var resourceFile in Directory.EnumerateFiles(Path.Combine(binFolder, culture)))
{
list.Add(Path.Combine(culture, Path.GetFileNameWithoutExtension(resourceFile)));
}
}
// get module and theme dependencies
foreach (var assembly in assemblies)
@ -96,7 +112,6 @@ namespace Oqtane.Controllers
}
// create zip file containing assemblies and debug symbols
string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
byte[] zipfile;
using (var memoryStream = new MemoryStream())
{
@ -106,17 +121,17 @@ namespace Oqtane.Controllers
foreach (string file in list)
{
entry = archive.CreateEntry(file + ".dll");
using (var filestream = new FileStream(Path.Combine(binfolder, file + ".dll"), FileMode.Open, FileAccess.Read))
using (var filestream = new FileStream(Path.Combine(binFolder, file + ".dll"), FileMode.Open, FileAccess.Read))
using (var entrystream = entry.Open())
{
filestream.CopyTo(entrystream);
}
// include debug symbols ( we may want to consider restricting this to only host users or when running in debug mode for performance )
if (System.IO.File.Exists(Path.Combine(binfolder, file + ".pdb")))
if (System.IO.File.Exists(Path.Combine(binFolder, file + ".pdb")))
{
entry = archive.CreateEntry(file + ".pdb");
using (var filestream = new FileStream(Path.Combine(binfolder, file + ".pdb"), FileMode.Open, FileAccess.Read))
using (var filestream = new FileStream(Path.Combine(binFolder, file + ".pdb"), FileMode.Open, FileAccess.Read))
using (var entrystream = entry.Open())
{
filestream.CopyTo(entrystream);

View File

@ -1,8 +1,10 @@
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
namespace Oqtane.Extensions
@ -22,5 +24,22 @@ namespace Oqtane.Extensions
return app;
}
public static IApplicationBuilder UseOqtaneLocalization(this IApplicationBuilder app)
{
var localizationManager = app.ApplicationServices.GetService<ILocalizationManager>();
var defaultCulture = localizationManager.GetDefaultCulture();
var supportedCultures = localizationManager.GetSupportedCultures();
CultureInfo.CurrentUICulture = new CultureInfo(defaultCulture);
app.UseRequestLocalization(options => {
options.SetDefaultCulture(defaultCulture)
.AddSupportedUICultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
});
return app;
}
}
}

View File

@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
using Oqtane.Infrastructure;
using Oqtane.Modules;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
// ReSharper disable once CheckNamespace
@ -14,10 +15,12 @@ namespace Microsoft.Extensions.DependencyInjection
{
public static class OqtaneServiceCollectionExtensions
{
public static IServiceCollection AddOqtaneParts(this IServiceCollection services, Runtime runtime)
public static IServiceCollection AddOqtane(this IServiceCollection services, Runtime runtime)
{
LoadAssemblies();
LoadSatelliteAssemblies();
services.AddOqtaneServices(runtime);
return services;
}
@ -119,6 +122,55 @@ namespace Microsoft.Extensions.DependencyInjection
}
}
private static void LoadSatelliteAssemblies()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
if (assemblyPath == null)
{
return;
}
AssemblyLoadContext.Default.Resolving += ResolveDependencies;
using (var serviceScope = ServiceActivator.GetScope())
{
var localizationManager = serviceScope.ServiceProvider.GetService<ILocalizationManager>();
foreach (var culture in localizationManager.GetSupportedCultures())
{
if (culture == Constants.DefaultCulture)
{
continue;
}
var assembliesFolder = new DirectoryInfo(Path.Combine(assemblyPath, culture));
foreach (var assemblyFile in assembliesFolder.EnumerateFiles(Constants.StalliteAssemblyExtension))
{
AssemblyName assemblyName;
try
{
assemblyName = AssemblyName.GetAssemblyName(assemblyFile.FullName);
}
catch
{
Console.WriteLine($"Not Satellite Assembly : {assemblyFile.Name}");
continue;
}
try
{
Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyFile.FullName)));
Console.WriteLine($"Loaded : {assemblyName}");
}
catch (Exception e)
{
Console.WriteLine($"Failed : {assemblyName}\n{e}");
}
}
}
}
}
private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name)
{
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + "\\" + name.Name + ".dll";

View File

@ -0,0 +1,9 @@
namespace Oqtane.Infrastructure
{
public interface ILocalizationManager
{
string GetDefaultCulture();
string[] GetSupportedCultures();
}
}

View File

@ -0,0 +1,9 @@
namespace Oqtane.Infrastructure
{
public class LocalizationOptions
{
public string DefaultCulture { get; set; }
public string[] SupportedCultures { get; set; }
}
}

View File

@ -0,0 +1,29 @@
using System.Collections;
using Microsoft.Extensions.Options;
using Oqtane.Shared;
namespace Oqtane.Infrastructure
{
public class LocalizationManager : ILocalizationManager
{
private static readonly string DefaultCulture = Constants.DefaultCulture;
private static readonly string[] SupportedCultures = new[] { DefaultCulture };
private readonly LocalizationOptions _localizationOptions;
public LocalizationManager(IOptions<LocalizationOptions> localizationOptions)
{
_localizationOptions = localizationOptions.Value;
}
public string GetDefaultCulture()
=> string.IsNullOrEmpty(_localizationOptions.DefaultCulture)
? DefaultCulture
: _localizationOptions.DefaultCulture;
public string[] GetSupportedCultures()
=> _localizationOptions.SupportedCultures.IsNullOrEmpty()
? SupportedCultures
: _localizationOptions.SupportedCultures;
}
}

View File

@ -43,6 +43,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" />
<PackageReference Include="System.Drawing.Common" Version="4.7.0" />
</ItemGroup>

View File

@ -1,10 +1,17 @@
@page "/"
@namespace Oqtane.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using System.Globalization
@using Microsoft.AspNetCore.Localization
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@model Oqtane.Pages.HostModel
@{
// Set localization cookie
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture));
HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue);
}
<!DOCTYPE html>
<html>
<head>

View File

@ -51,6 +51,9 @@ namespace Oqtane
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
// Register localization services
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddServerSideBlazor();
// setup HttpClient for server side in a client side compatible fashion ( with auth cookie )
@ -125,6 +128,8 @@ namespace Oqtane
.AddSignInManager()
.AddDefaultTokenProviders();
services.Configure<LocalizationOptions>(Configuration.GetSection("Localization"));
services.Configure<IdentityOptions>(options =>
{
// Password settings
@ -187,6 +192,7 @@ namespace Oqtane
services.AddTransient<ISettingRepository, SettingRepository>();
services.AddTransient<ILogRepository, LogRepository>();
services.AddTransient<ILogManager, LogManager>();
services.AddTransient<ILocalizationManager, LocalizationManager>();
services.AddTransient<IJobRepository, JobRepository>();
services.AddTransient<IJobLogRepository, JobLogRepository>();
services.AddTransient<INotificationRepository, NotificationRepository>();
@ -196,8 +202,11 @@ namespace Oqtane
services.AddTransient<ISqlRepository, SqlRepository>();
services.AddTransient<IUpgradeManager, UpgradeManager>();
// TODO: Check if there's a better way instead of building service provider
ServiceActivator.Configure(services.BuildServiceProvider());
// load the external assemblies into the app domain, install services
services.AddOqtaneParts(_runtime);
services.AddOqtane(_runtime);
services.AddMvc()
.AddNewtonsoftJson()
@ -225,6 +234,10 @@ namespace Oqtane
}
// to allow install middleware it should be moved up
app.ConfigureOqtaneAssemblies(env);
// Allow oqtane localization middleware
app.UseOqtaneLocalization();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseBlazorFrameworkFiles();

View File

@ -11,5 +11,9 @@
"DefaultTheme": "",
"DefaultLayout": "",
"DefaultContainer": ""
},
"Localization": {
"DefaultCulture": "",
"SupportedCultures": []
}
}

View File

@ -0,0 +1,8 @@
namespace System.Collections
{
public static class EnumerableExtensions
{
public static bool IsNullOrEmpty(this IEnumerable source)
=> source == null || source.GetEnumerator().MoveNext() == false;
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Globalization;
namespace Oqtane.Shared
{
@ -57,5 +58,9 @@ namespace Oqtane.Shared
(Char) 28, (Char) 29, (Char) 30, (Char) 31, ':', '*', '?', '\\', '/'
};
public static readonly string[] InvalidFileNameEndingChars = { ".", " " };
public static readonly string StalliteAssemblyExtension = ".resources.dll";
public static readonly string DefaultCulture = CultureInfo.InstalledUICulture.Name;
}
}

View File

@ -0,0 +1,22 @@
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Oqtane.Shared
{
public static class ServiceActivator
{
private static IServiceProvider _serviceProvider = null;
public static void Configure(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public static IServiceScope GetScope(IServiceProvider serviceProvider = null)
{
var provider = serviceProvider ?? _serviceProvider;
return provider?.GetRequiredService<IServiceScopeFactory>().CreateScope();
}
}
}