Browse Source

feat: Implement URL-based culture handling in Blazor and MVC requests

pull/25174/head
maliming 6 months ago
parent
commit
420c860b08
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 4
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider.cs
  2. 47
      framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestCultureCookieHelper.cs
  3. 14
      framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs
  4. 46
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider_Tests.cs
  5. 4
      modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor.cs

4
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider.cs

@ -54,10 +54,10 @@ public class AbpCultureMenuItemUrlProvider : IMenuItemUrlProvider, ITransientDep
var httpContext = HttpContextAccessor.HttpContext;
if (httpContext != null)
{
return httpContext.GetRouteValue("culture")?.ToString();
return AbpRequestCultureCookieHelper.GetRouteCulture(httpContext);
}
// No HttpContext: Blazor interactive circuit or WASM client.
// No HttpContext: fallback to CurrentCulture.
var currentCulture = CultureInfo.CurrentCulture.Name;
var isKnownCulture = AbpLocalizationOptions.Value.Languages
.Any(l => string.Equals(l.CultureName, currentCulture, StringComparison.OrdinalIgnoreCase));

47
framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestCultureCookieHelper.cs

@ -1,11 +1,41 @@
using System;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.RequestLocalization;
public static class AbpRequestCultureCookieHelper
{
public const string HasRouteCultureCookieName = "Abp.HasRouteCulture";
/// <summary>
/// Gets the current route culture from the request. First checks route values,
/// then falls back to the HasRouteCulture cookie (set during Blazor SSR) with CurrentCulture.
/// Returns null if the request has no route-based culture.
/// </summary>
public static string? GetRouteCulture(HttpContext? httpContext)
{
if (httpContext == null)
{
return null;
}
var routeCulture = httpContext.GetRouteValue("culture")?.ToString();
if (!string.IsNullOrEmpty(routeCulture))
{
return routeCulture;
}
if (httpContext.Request.Cookies.ContainsKey(HasRouteCultureCookieName))
{
return CultureInfo.CurrentCulture.Name;
}
return null;
}
public static void SetCultureCookie(
HttpContext httpContext,
RequestCulture requestCulture)
@ -20,4 +50,21 @@ public static class AbpRequestCultureCookieHelper
}
);
}
public static void SetHasRouteCultureCookie(HttpContext httpContext, bool hasRouteCulture)
{
if (hasRouteCulture)
{
httpContext.Response.Cookies.Append(
HasRouteCultureCookieName, "1",
new CookieOptions
{
IsEssential = true
});
}
else
{
httpContext.Response.Cookies.Delete(HasRouteCultureCookieName);
}
}
}

14
framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
@ -48,6 +49,17 @@ public class AbpRequestLocalizationMiddleware : AbpMiddlewareBase, ITransientDep
requestCultureFeature.RequestCulture
);
}
// Only manage HasRouteCulture cookie for Blazor component page requests.
// This cookie is used by AbpCultureMenuItemUrlProvider to determine if the
// initial SSR page had a culture prefix, since the Blazor interactive circuit
// (/_blazor) does not carry the original route values.
var endpoint = context.GetEndpoint();
if (endpoint?.Metadata.Any(m => m.GetType().Name == "ComponentTypeMetadata") == true)
{
AbpRequestCultureCookieHelper.SetHasRouteCultureCookie(
context, requestCultureFeature?.Provider is RouteDataRequestCultureProvider);
}
}
return Task.CompletedTask;

46
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider_Tests.cs

@ -1,3 +1,4 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -38,33 +39,51 @@ public class AbpCultureMenuItemUrlProvider_Tests
}
[Fact]
public async Task Should_Not_Add_Prefix_When_HttpContext_Has_No_Culture_Route()
public async Task Should_Not_Add_Prefix_For_Mvc_Request_Without_Culture()
{
// HttpContext exists but has no {culture} route value (e.g. MVC request to /about).
// No prefix should be added to keep URL style consistent with the current page.
var provider = CreateProvider(useRouteBasedCulture: true, cultureName: null);
// MVC request to /about (no culture, no HasRouteCulture cookie).
var httpContext = new DefaultHttpContext();
httpContext.Request.RouteValues["controller"] = "Home";
httpContext.Request.RouteValues["action"] = "About";
var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext };
var localizationOptions = MsOptions.Create(
new AbpRequestLocalizationOptions { UseRouteBasedCulture = true });
var abpLocOptions = new AbpLocalizationOptions();
abpLocOptions.Languages.Add(new LanguageInfo("en"));
abpLocOptions.Languages.Add(new LanguageInfo("zh-Hans"));
var provider = new AbpCultureMenuItemUrlProvider(
httpContextAccessor, localizationOptions, MsOptions.Create(abpLocOptions));
var menu = CreateMenuWithItems("/home", "/about");
await provider.HandleAsync(new MenuItemUrlProviderContext(menu));
var previousCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("zh-Hans");
await provider.HandleAsync(new MenuItemUrlProviderContext(menu));
}
finally
{
CultureInfo.CurrentCulture = previousCulture;
}
menu.Items[0].Url.ShouldBe("/home");
menu.Items[1].Url.ShouldBe("/about");
}
[Fact]
public async Task Should_Not_Add_Prefix_When_HttpContext_Has_No_Culture_Route_Even_With_Known_Languages()
public async Task Should_Fallback_To_CurrentCulture_In_Blazor_Circuit()
{
// HttpContext exists but has no {culture} route value, even though CurrentCulture
// matches a known language. No prefix should be added because the current request
// does not have a culture segment in the URL.
var httpContext = new DefaultHttpContext(); // no culture route value
// Blazor Server interactive circuit: HttpContext exists (SignalR) but has
// no route culture. Cookie was set during SSR indicating route culture was used.
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers["Cookie"] = $"{AbpRequestCultureCookieHelper.HasRouteCultureCookieName}=1";
var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext };
var localizationOptions = MsOptions.Create(
new AbpRequestLocalizationOptions { UseRouteBasedCulture = true });
var abpLocOptions = new AbpLocalizationOptions();
abpLocOptions.Languages.Add(new LanguageInfo("en"));
abpLocOptions.Languages.Add(new LanguageInfo("zh-Hans"));
abpLocOptions.Languages.Add(new LanguageInfo("tr"));
var provider = new AbpCultureMenuItemUrlProvider(
httpContextAccessor, localizationOptions, MsOptions.Create(abpLocOptions));
@ -81,8 +100,8 @@ public class AbpCultureMenuItemUrlProvider_Tests
CultureInfo.CurrentCulture = previousCulture;
}
menu.Items[0].Url.ShouldBe("/home");
menu.Items[1].Url.ShouldBe("/about");
menu.Items[0].Url.ShouldBe("/zh-Hans/home");
menu.Items[1].Url.ShouldBe("/zh-Hans/about");
}
[Fact]
@ -257,4 +276,5 @@ public class AbpCultureMenuItemUrlProvider_Tests
}
return menu;
}
}

4
modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor.cs

@ -3,7 +3,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.RequestLocalization;
using Volo.Abp.UI.Navigation;
namespace Volo.Abp.AspNetCore.Components.Server.BasicTheme.Themes.Basic;
@ -27,7 +27,7 @@ public partial class LoginDisplay : IDisposable
protected string GetLoginUrl()
{
var culture = HttpContextAccessor.HttpContext?.GetRouteValue("culture")?.ToString();
var culture = AbpRequestCultureCookieHelper.GetRouteCulture(HttpContextAccessor.HttpContext);
return string.IsNullOrEmpty(culture) ? "Account/Login" : $"{culture}/Account/Login";
}

Loading…
Cancel
Save