diff --git a/Oqtane.Client/AssemblyInfo.cs b/Oqtane.Client/AssemblyInfo.cs
new file mode 100644
index 00000000..d598bfb9
--- /dev/null
+++ b/Oqtane.Client/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using Microsoft.Extensions.Localization;
+
+[assembly: RootNamespace("Oqtane")]
diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj
index bff51215..515bf8f5 100644
--- a/Oqtane.Client/Oqtane.Client.csproj
+++ b/Oqtane.Client/Oqtane.Client.csproj
@@ -32,6 +32,7 @@
+
diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs
index 8a1fd2b8..912720d9 100644
--- a/Oqtane.Client/Program.cs
+++ b/Oqtane.Client/Program.cs
@@ -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();
@@ -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 dlls = new Dictionary();
- Dictionary pdbs = new Dictionary();
+ var dlls = new Dictionary();
+ var pdbs = new Dictionary();
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);
diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs
index 50b45710..d3529a98 100644
--- a/Oqtane.Server/Controllers/InstallationController.cs
+++ b/Oqtane.Server/Controllers/InstallationController.cs
@@ -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/
@@ -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);
diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs
index d1c77301..67676d16 100644
--- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs
+++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs
@@ -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();
+ 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;
+ }
}
}
diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
index f49d189a..4090be8d 100644
--- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
+++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
@@ -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();
+ 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";
diff --git a/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs b/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs
new file mode 100644
index 00000000..fa20eef4
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs
@@ -0,0 +1,9 @@
+namespace Oqtane.Infrastructure
+{
+ public interface ILocalizationManager
+ {
+ string GetDefaultCulture();
+
+ string[] GetSupportedCultures();
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Localization/LocalizationOptions.cs b/Oqtane.Server/Infrastructure/Localization/LocalizationOptions.cs
new file mode 100644
index 00000000..6330130e
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Localization/LocalizationOptions.cs
@@ -0,0 +1,9 @@
+namespace Oqtane.Infrastructure
+{
+ public class LocalizationOptions
+ {
+ public string DefaultCulture { get; set; }
+
+ public string[] SupportedCultures { get; set; }
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs
new file mode 100644
index 00000000..22b4ca79
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs
@@ -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.Value;
+ }
+
+ public string GetDefaultCulture()
+ => string.IsNullOrEmpty(_localizationOptions.DefaultCulture)
+ ? DefaultCulture
+ : _localizationOptions.DefaultCulture;
+
+ public string[] GetSupportedCultures()
+ => _localizationOptions.SupportedCultures.IsNullOrEmpty()
+ ? SupportedCultures
+ : _localizationOptions.SupportedCultures;
+ }
+}
diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj
index 44237543..31523442 100644
--- a/Oqtane.Server/Oqtane.Server.csproj
+++ b/Oqtane.Server/Oqtane.Server.csproj
@@ -43,6 +43,7 @@
+
diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml
index 0fe00957..e4b6e5a7 100644
--- a/Oqtane.Server/Pages/_Host.cshtml
+++ b/Oqtane.Server/Pages/_Host.cshtml
@@ -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);
+}
diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs
index ee53f340..98d43bcd 100644
--- a/Oqtane.Server/Startup.cs
+++ b/Oqtane.Server/Startup.cs
@@ -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(Configuration.GetSection("Localization"));
+
services.Configure(options =>
{
// Password settings
@@ -187,6 +192,7 @@ namespace Oqtane
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -196,8 +202,11 @@ namespace Oqtane
services.AddTransient();
services.AddTransient();
+ // 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();
diff --git a/Oqtane.Server/appsettings.json b/Oqtane.Server/appsettings.json
index 0ab499f0..1af87b4c 100644
--- a/Oqtane.Server/appsettings.json
+++ b/Oqtane.Server/appsettings.json
@@ -11,5 +11,9 @@
"DefaultTheme": "",
"DefaultLayout": "",
"DefaultContainer": ""
+ },
+ "Localization": {
+ "DefaultCulture": "",
+ "SupportedCultures": []
}
}
\ No newline at end of file
diff --git a/Oqtane.Shared/Extensions/EnumerableExtensions.cs b/Oqtane.Shared/Extensions/EnumerableExtensions.cs
new file mode 100644
index 00000000..a4bdde7c
--- /dev/null
+++ b/Oqtane.Shared/Extensions/EnumerableExtensions.cs
@@ -0,0 +1,8 @@
+namespace System.Collections
+{
+ public static class EnumerableExtensions
+ {
+ public static bool IsNullOrEmpty(this IEnumerable source)
+ => source == null || source.GetEnumerator().MoveNext() == false;
+ }
+}
diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs
index c237857d..db62f416 100644
--- a/Oqtane.Shared/Shared/Constants.cs
+++ b/Oqtane.Shared/Shared/Constants.cs
@@ -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;
}
}
diff --git a/Oqtane.Shared/Shared/ServiceActivator.cs b/Oqtane.Shared/Shared/ServiceActivator.cs
new file mode 100644
index 00000000..5718d174
--- /dev/null
+++ b/Oqtane.Shared/Shared/ServiceActivator.cs
@@ -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().CreateScope();
+ }
+ }
+}