Merge pull request #762 from hishamco/localization-support
Localization support
This commit is contained in:
commit
666721bf1a
3
Oqtane.Client/AssemblyInfo.cs
Normal file
3
Oqtane.Client/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
using Microsoft.Extensions.Localization;
|
||||
|
||||
[assembly: RootNamespace("Oqtane")]
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
public interface ILocalizationManager
|
||||
{
|
||||
string GetDefaultCulture();
|
||||
|
||||
string[] GetSupportedCultures();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
public class LocalizationOptions
|
||||
{
|
||||
public string DefaultCulture { get; set; }
|
||||
|
||||
public string[] SupportedCultures { get; set; }
|
||||
}
|
||||
}
|
29
Oqtane.Server/Infrastructure/LocalizationManager.cs
Normal file
29
Oqtane.Server/Infrastructure/LocalizationManager.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -11,5 +11,9 @@
|
|||
"DefaultTheme": "",
|
||||
"DefaultLayout": "",
|
||||
"DefaultContainer": ""
|
||||
},
|
||||
"Localization": {
|
||||
"DefaultCulture": "",
|
||||
"SupportedCultures": []
|
||||
}
|
||||
}
|
8
Oqtane.Shared/Extensions/EnumerableExtensions.cs
Normal file
8
Oqtane.Shared/Extensions/EnumerableExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
22
Oqtane.Shared/Shared/ServiceActivator.cs
Normal file
22
Oqtane.Shared/Shared/ServiceActivator.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user