diff --git a/src/OrchardCore.Modules/OrchardCore.Localization/Services/LocalizationService.cs b/src/OrchardCore.Modules/OrchardCore.Localization/Services/LocalizationService.cs
index c1c0fc19e8e..7775da8b8ff 100644
--- a/src/OrchardCore.Modules/OrchardCore.Localization/Services/LocalizationService.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Localization/Services/LocalizationService.cs
@@ -1,58 +1,57 @@
using System.Globalization;
using System.Threading.Tasks;
-using OrchardCore.Entities;
using OrchardCore.Localization.Models;
using OrchardCore.Settings;
-namespace OrchardCore.Localization.Services
+namespace OrchardCore.Localization.Services;
+
+///
+/// Represents a localization service.
+///
+public class LocalizationService : ILocalizationService
{
+ private static readonly string _defaultCulture = CultureInfo.InstalledUICulture.Name;
+ private static readonly string[] _supportedCultures = [CultureInfo.InstalledUICulture.Name];
+
+ private readonly ISiteService _siteService;
+
+ private LocalizationSettings _localizationSettings;
+
///
- /// Represents a localization service.
+ /// Creates a new instance of .
///
- public class LocalizationService : ILocalizationService
+ /// The .
+ public LocalizationService(ISiteService siteService)
{
- private static readonly string _defaultCulture = CultureInfo.InstalledUICulture.Name;
- private static readonly string[] _supportedCultures = new[] { CultureInfo.InstalledUICulture.Name };
-
- private readonly ISiteService _siteService;
+ _siteService = siteService;
+ }
- private LocalizationSettings _localizationSettings;
+ ///
+ public async Task GetDefaultCultureAsync()
+ {
+ var settings = await GetLocalizationSettingsAsync();
- ///
- /// Creates a new instance of .
- ///
- /// The .
- public LocalizationService(ISiteService siteService)
- {
- _siteService = siteService;
- }
+ return settings.DefaultCulture ?? _defaultCulture;
+ }
- ///
- public async Task GetDefaultCultureAsync()
- {
- await InitializeLocalizationSettingsAsync();
+ ///
+ public async Task GetSupportedCulturesAsync()
+ {
+ var settings = await GetLocalizationSettingsAsync();
- return _localizationSettings.DefaultCulture ?? _defaultCulture;
- }
+ return settings.SupportedCultures?.Length == 0
+ ? _supportedCultures
+ : settings.SupportedCultures;
+ }
- ///
- public async Task GetSupportedCulturesAsync()
+ private async Task GetLocalizationSettingsAsync()
+ {
+ if (_localizationSettings == null)
{
- await InitializeLocalizationSettingsAsync();
-
- return _localizationSettings.SupportedCultures == null || _localizationSettings.SupportedCultures.Length == 0
- ? _supportedCultures
- : _localizationSettings.SupportedCultures
- ;
+ var siteSettings = await _siteService.GetSiteSettingsAsync();
+ _localizationSettings = siteSettings.As();
}
- private async Task InitializeLocalizationSettingsAsync()
- {
- if (_localizationSettings == null)
- {
- var siteSettings = await _siteService.GetSiteSettingsAsync();
- _localizationSettings = siteSettings.As();
- }
- }
+ return _localizationSettings;
}
}
diff --git a/src/OrchardCore.Modules/OrchardCore.Localization/Services/RequestLocalizationMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.Localization/Services/RequestLocalizationMiddleware.cs
new file mode 100644
index 00000000000..1eb52110183
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Localization/Services/RequestLocalizationMiddleware.cs
@@ -0,0 +1,226 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Localization;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace OrchardCore.Localization.Services;
+///
+/// Enables automatic setting of the culture for s based on information
+/// sent by the client in headers and logic provided by the application.
+/// The middleware is inspired by
+/// and should have the same logic.
+///
+public class RequestLocalizationMiddleware
+{
+ private const int MaxCultureFallbackDepth = 5;
+
+ private readonly RequestDelegate _next;
+ private readonly CultureOptions _cultureOptions;
+ private readonly ILocalizationService _localizationService;
+ private readonly ILogger _logger;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The representing the next middleware in the pipeline.
+ /// The used for logging.
+ /// The used for logging.
+ /// The used for logging.
+ ///
+ public RequestLocalizationMiddleware(
+ RequestDelegate next,
+ ILoggerFactory loggerFactory,
+ IOptions cultureOptions,
+ ILocalizationService localizationService)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ _cultureOptions = cultureOptions.Value;
+ _localizationService = localizationService;
+ _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ ///
+ /// Invokes the logic of the middleware.
+ ///
+ /// The .
+ /// A that completes when the middleware has completed processing.
+ public async Task Invoke(HttpContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ var localizationOptions = new RequestLocalizationOptions();
+ var supportedCultures = await _localizationService.GetSupportedCulturesAsync();
+ new LocalizationOptionsUpdater(localizationOptions, _cultureOptions.IgnoreSystemSettings)
+ .SetDefaultCulture(await _localizationService.GetDefaultCultureAsync())
+ .AddSupportedCultures(supportedCultures)
+ .AddSupportedUICultures(supportedCultures);
+
+ var requestCulture = localizationOptions.DefaultRequestCulture;
+
+ IRequestCultureProvider winningProvider = null;
+
+ if (localizationOptions.RequestCultureProviders != null)
+ {
+ foreach (var provider in localizationOptions.RequestCultureProviders)
+ {
+ var providerResultCulture = await provider.DetermineProviderCultureResult(context);
+ if (providerResultCulture == null)
+ {
+ continue;
+ }
+
+ var cultures = providerResultCulture.Cultures;
+ var uiCultures = providerResultCulture.UICultures;
+
+ CultureInfo cultureInfo = null;
+ CultureInfo uiCultureInfo = null;
+ if (localizationOptions.SupportedCultures != null)
+ {
+ cultureInfo = GetCultureInfo(
+ cultures,
+ localizationOptions.SupportedCultures,
+ localizationOptions.FallBackToParentCultures);
+
+ if (cultureInfo == null)
+ {
+ _logger.UnsupportedCultures(provider.GetType().Name, cultures);
+ }
+ }
+
+ if (localizationOptions.SupportedUICultures != null)
+ {
+ uiCultureInfo = GetCultureInfo(
+ uiCultures,
+ localizationOptions.SupportedUICultures,
+ localizationOptions.FallBackToParentUICultures);
+
+ if (uiCultureInfo == null)
+ {
+ _logger.UnsupportedUICultures(provider.GetType().Name, uiCultures);
+ }
+ }
+
+ if (cultureInfo == null && uiCultureInfo == null)
+ {
+ continue;
+ }
+
+ cultureInfo ??= localizationOptions.DefaultRequestCulture.Culture;
+ uiCultureInfo ??= localizationOptions.DefaultRequestCulture.UICulture;
+
+ var result = new RequestCulture(cultureInfo, uiCultureInfo);
+ requestCulture = result;
+ winningProvider = provider;
+
+ break;
+ }
+ }
+
+ context.Features.Set(new RequestCultureFeature(requestCulture, winningProvider));
+
+ SetCurrentThreadCulture(requestCulture);
+
+ if (localizationOptions.ApplyCurrentCultureToResponseHeaders)
+ {
+ var headers = context.Response.Headers;
+ headers.ContentLanguage = requestCulture.UICulture.Name;
+ }
+
+ await _next(context);
+ }
+
+ private static void SetCurrentThreadCulture(RequestCulture requestCulture)
+ {
+ CultureInfo.CurrentCulture = requestCulture.Culture;
+ CultureInfo.CurrentUICulture = requestCulture.UICulture;
+ }
+
+ private static CultureInfo GetCultureInfo(
+ IList cultureNames,
+ IList supportedCultures,
+ bool fallbackToParentCultures)
+ {
+ foreach (var cultureName in cultureNames)
+ {
+ // Allow empty string values as they map to InvariantCulture, whereas null culture values will throw in
+ // the CultureInfo ctor
+ if (cultureName != null)
+ {
+ var cultureInfo = GetCultureInfo(cultureName, supportedCultures, fallbackToParentCultures, currentDepth: 0);
+ if (cultureInfo != null)
+ {
+ return cultureInfo;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static CultureInfo GetCultureInfo(
+ StringSegment cultureName,
+ IList supportedCultures,
+ bool fallbackToParentCultures,
+ int currentDepth)
+ {
+ // If the cultureName is an empty string there
+ // is no chance we can resolve the culture info.
+ if (cultureName.Equals(string.Empty))
+ {
+ return null;
+ }
+
+ var culture = GetCultureInfo(cultureName, supportedCultures);
+
+ if (culture == null && fallbackToParentCultures && currentDepth < MaxCultureFallbackDepth)
+ {
+ try
+ {
+ culture = CultureInfo.GetCultureInfo(cultureName.ToString());
+
+ culture = GetCultureInfo(culture.Parent.Name, supportedCultures, fallbackToParentCultures, currentDepth + 1);
+ }
+ catch (CultureNotFoundException)
+ {
+ }
+ }
+
+ return culture;
+ }
+
+ private static CultureInfo GetCultureInfo(StringSegment name, IList supportedCultures)
+ {
+ // Allow only known culture names as this API is called with input from users (HTTP requests) and
+ // creating CultureInfo objects is expensive and we don't want it to throw either.
+ if (name == null || supportedCultures == null)
+ {
+ return null;
+ }
+
+ var culture = supportedCultures.FirstOrDefault(
+ supportedCulture => StringSegment.Equals(supportedCulture.Name, name, StringComparison.OrdinalIgnoreCase));
+
+ if (culture == null)
+ {
+ return null;
+ }
+
+ return CultureInfo.ReadOnly(culture);
+ }
+}
+
+internal static partial class RequestCultureProviderLoggerExtensions
+{
+ [LoggerMessage(1, LogLevel.Debug, "{requestCultureProvider} returned the following unsupported cultures '{cultures}'.", EventName = "UnsupportedCulture")]
+ public static partial void UnsupportedCultures(this ILogger logger, string requestCultureProvider, IList cultures);
+
+ [LoggerMessage(2, LogLevel.Debug, "{requestCultureProvider} returned the following unsupported UI Cultures '{uiCultures}'.", EventName = "UnsupportedUICulture")]
+ public static partial void UnsupportedUICultures(this ILogger logger, string requestCultureProvider, IList uiCultures);
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs
index 8c825301c3d..083a3f7c8a0 100644
--- a/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs
@@ -26,13 +26,12 @@ public class Startup : StartupBase
{
public override int ConfigureOrder => -100;
- ///
public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped, LocalizationSettingsDisplayDriver>();
services.AddScoped();
services.AddScoped();
- services.AddScoped();
+ services.AddSingleton();
services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization").
AddDataAnnotationsPortableObjectLocalization();
@@ -40,23 +39,9 @@ public override void ConfigureServices(IServiceCollection services)
services.Replace(ServiceDescriptor.Singleton());
}
- ///
public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
- var localizationService = serviceProvider.GetService();
-
- var defaultCulture = localizationService.GetDefaultCultureAsync().GetAwaiter().GetResult();
- var supportedCultures = localizationService.GetSupportedCulturesAsync().GetAwaiter().GetResult();
-
- var localizationOptions = serviceProvider.GetService>().Value;
- var ignoreSystemSettings = serviceProvider.GetService>().Value.IgnoreSystemSettings;
-
- new LocalizationOptionsUpdater(localizationOptions, ignoreSystemSettings)
- .SetDefaultCulture(defaultCulture)
- .AddSupportedCultures(supportedCultures)
- .AddSupportedUICultures(supportedCultures);
-
- app.UseRequestLocalization(localizationOptions);
+ app.UseMiddleware();
}
}