在 Razor + Blazor 组件中使用 URL 路径进行本地化

Tea*_*man 1 localization razor asp.net-core blazor

我想构建一个包含 razor 页面和一些 Blazor 组件的 ASP.NET Razor 应用程序,并根据 URL 中的语言对网站内容进行本地化。

例如,/en/home并且/fr/home将有一个基于语言呈现内容的支持页面。

有什么方法可以实现这个目的呢?

Tea*_*man 8

AspNetCore.Mvc.Localization 有我们需要的。

在里面_ViewImports.cshtml,我们可以注入一个IViewLocalizer它将抓取.resx相应页面的文件。

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
Run Code Online (Sandbox Code Playgroud)

现在Localizer我们所有的页面都可以使用它。

例如,Index.cshtml

@page
@model IndexModel

@{
    ViewData["Title"] = @Localizer["Title"];
}

<h1>@Localizer["Header"]</h1>
<section>
    <p>@Localizer["Welcome", User.Identity.Name]</p>
    @Localizer["Learn"]
    <a asp-page="Page1">@Localizer["SomePage"]</a>
    <a asp-page="Dogs/Index">@Localizer["LinkDogs"]</a>
</section>
Run Code Online (Sandbox Code Playgroud)

现在,一旦创建了 resx 文件,页面标题、页眉和内容就会被本地化。

Resources/Pages/Index.resxResources/Pages/Index.fr.resx需要被创建。有一个 VSCode 扩展可用于此,因为这些文件只是丑陋的 XML。

字符串可以参数化。在Index.cshtml示例中,"Welcome"="Howdy {0}"gets 被引用@Localizer["Welcome", User.Identity.Name],用户名将被替换为{0}

在里面Startup.cs,我们还需要添加一些设置。

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
Run Code Online (Sandbox Code Playgroud)

但这只能访问Localizer我们的.cshtml文件内部。我们的页面看起来仍然像/home而不是/en/home.

为了解决这个问题,我们将添加一个IPageRouteModelConvention来修改我们的页面模板,并将其添加{culture}到所有页面的前面。

在里面Startup.cs,我们需要在 razor 配置期间添加约定。

@page
@model IndexModel

@{
    ViewData["Title"] = @Localizer["Title"];
}

<h1>@Localizer["Header"]</h1>
<section>
    <p>@Localizer["Welcome", User.Identity.Name]</p>
    @Localizer["Learn"]
    <a asp-page="Page1">@Localizer["SomePage"]</a>
    <a asp-page="Dogs/Index">@Localizer["LinkDogs"]</a>
</section>
Run Code Online (Sandbox Code Playgroud)

CultureTemplatePageRouteModelConvention.cs在一个Middleware/文件夹下创建了它,但您可以将其放在任何地方(不确定它是否是“技术上”中间件?)。

            services.AddLocalization(options =>
            {
                options.ResourcesPath = "Resources";
            }); // new
            services.AddRazorPages()
                .AddRazorRuntimeCompilation()
                .AddViewLocalization(); // new
            services.AddServerSideBlazor();
Run Code Online (Sandbox Code Playgroud)

现在要解决的/en/home是应该解决的,还是/home不应该解决的。但如果你去查看,/fr/home你会发现它仍然使用英文 resx 文件。这是因为区域性没有根据 URL 进行更新。

要解决此问题,需要进行更多修改Startup.cs

在该Configure方法中,我们将添加

            services.AddRazorPages(options =>
            {
                options.Conventions.Add(new CultureTemplatePageRouteModelConvention());
            })
Run Code Online (Sandbox Code Playgroud)

在 下ConfigureServices,我们将配置请求本地化选项。这将包括添加一个RequestCultureProvider用于确定Culture每个请求的 。

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.Logging;

namespace app.Middleware
{
    public class CultureTemplatePageRouteModelConvention : IPageRouteModelConvention
    {
        public void Apply(PageRouteModel model)
        {
            // For each page Razor has detected
            foreach (var selector in model.Selectors)
            {
                // Grab the template string
                var template = selector.AttributeRouteModel.Template;

                // Skip the MicrosoftIdentity pages
                if (template.StartsWith("MicrosoftIdentity")) continue;

                // Prepend the /{culture?}/ route value to allow for route-based localization
                selector.AttributeRouteModel.Template = AttributeRouteModel.CombineTemplates("{culture?}", template);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这使用扩展方法来删除默认的接受语言标头文化提供程序

            app.UseRequestLocalization();
Run Code Online (Sandbox Code Playgroud)

更重要的是,我们需要创建RouteDataRequestCultureProvider我们刚刚添加到列表中的。

Middleware/RouteDataRequestCultureProvider.cs

            services.Configure<RequestLocalizationOptions>(options =>
            {
                options.SetDefaultCulture("en");
                options.AddSupportedCultures("en", "fr");
                options.AddSupportedUICultures("en", "fr");
                options.FallBackToParentCultures = true;
                options.RequestCultureProviders.Remove(typeof(AcceptLanguageHeaderRequestCultureProvider));
                options.RequestCultureProviders.Insert(0, new Middleware.RouteDataRequestCultureProvider() { Options = options });
            });
Run Code Online (Sandbox Code Playgroud)

RouteValues["culture"]请注意,当该值实际上尚不存在时,我们会在此提供程序中进行检查。这是因为我们需要另一块中间件才能使 Blazor 正常工作。但目前,至少我们的页面将从 URL 应用正确的区域性,这将允许/fr/使用正确的Index.fr.resx而不是Index.resx.

另一个问题是,除非您还指定了用户当前的区域性,asp-page否则标记帮助器将不起作用。asp-route-culture这很糟糕,所以我们将用每次只复制区域性的标签助手来覆盖标签助手。

里面_ViewImports.cshtml

@* Override anchor tag helpers with our own to ensure URL culture is persisted *@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, app
Run Code Online (Sandbox Code Playgroud)

在下面TagHelpders/CultureAnchorTagHelper.cs我们将添加

using System;
using System.Collections.Generic;
using System.Linq;

namespace app.Extensions
{
    public static class ListExtensions {
        public static void Remove<T>(this IList<T> list, Type type)
        {
            var items = list.Where(x => x.GetType() == type).ToList();
            items.ForEach(x => list.Remove(x));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这使用扩展方法从HttpRequest

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;

namespace app.Middleware
{
    public class RouteDataRequestCultureProvider : RequestCultureProvider
    {
        public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
        {
            string routeCulture = (string)httpContext.Request.RouteValues["culture"];
            string urlCulture = httpContext.Request.Path.Value.Split('/')[1];

            // Culture provided in route values
            if (IsSupportedCulture(routeCulture))
            {
                return Task.FromResult(new ProviderCultureResult(routeCulture));
            }
            // Culture provided in URL
            else if (IsSupportedCulture(urlCulture))
            {
                return Task.FromResult(new ProviderCultureResult(urlCulture));
            }
            else
            // Use default culture
            {
                return Task.FromResult(new ProviderCultureResult(DefaultCulture));
            }
        }

        /**
         * Culture must be in the list of supported cultures
         */
        private bool IsSupportedCulture(string lang) =>
            !string.IsNullOrEmpty(lang)
            && Options.SupportedCultures.Any(x =>
                x.TwoLetterISOLanguageName.Equals(
                    lang,
                    StringComparison.InvariantCultureIgnoreCase
                )
            );

        private string DefaultCulture => Options.DefaultRequestCulture.Culture.TwoLetterISOLanguageName;
    }
}
Run Code Online (Sandbox Code Playgroud)

为了确保当前上下文的依赖注入有效,我们需要修改Startup.cs

@* Override anchor tag helpers with our own to ensure URL culture is persisted *@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, app
Run Code Online (Sandbox Code Playgroud)

现在我们可以使用标签助手而不会造成任何破坏。

例子:

using System;
using app.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;


// https://stackoverflow.com/a/59283426/11141271
// https://stackoverflow.com/questions/60397920/razorpages-anchortaghelper-does-not-remove-index-from-href
// https://talagozis.com/en/asp-net-core/razor-pages-localisation-seo-friendly-urls
namespace app.TagHelpers
{
    [HtmlTargetElement("a", Attributes = ActionAttributeName)]
    [HtmlTargetElement("a", Attributes = ControllerAttributeName)]
    [HtmlTargetElement("a", Attributes = AreaAttributeName)]
    [HtmlTargetElement("a", Attributes = PageAttributeName)]
    [HtmlTargetElement("a", Attributes = PageHandlerAttributeName)]
    [HtmlTargetElement("a", Attributes = FragmentAttributeName)]
    [HtmlTargetElement("a", Attributes = HostAttributeName)]
    [HtmlTargetElement("a", Attributes = ProtocolAttributeName)]
    [HtmlTargetElement("a", Attributes = RouteAttributeName)]
    [HtmlTargetElement("a", Attributes = RouteValuesDictionaryName)]
    [HtmlTargetElement("a", Attributes = RouteValuesPrefix + "*")]
    public class CultureAnchorTagHelper : AnchorTagHelper
    {
        private const string ActionAttributeName = "asp-action";
        private const string ControllerAttributeName = "asp-controller";
        private const string AreaAttributeName = "asp-area";
        private const string PageAttributeName = "asp-page";
        private const string PageHandlerAttributeName = "asp-page-handler";
        private const string FragmentAttributeName = "asp-fragment";
        private const string HostAttributeName = "asp-host";
        private const string ProtocolAttributeName = "asp-protocol";
        private const string RouteAttributeName = "asp-route";
        private const string RouteValuesDictionaryName = "asp-all-route-data";
        private const string RouteValuesPrefix = "asp-route-";
        private readonly IHttpContextAccessor _contextAccessor;

        public CultureAnchorTagHelper(IHttpContextAccessor contextAccessor, IHtmlGenerator generator) :
            base(generator)
        {
            this._contextAccessor = contextAccessor;
        }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var culture = _contextAccessor.HttpContext.Request.GetCulture();
            RouteValues["culture"] = culture;
            base.Process(context, output);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

正常页面正常工作后,现在我们可以翻译 Blazor 组件了。

在里面_Imports.razor,我们将添加

@using Microsoft.Extensions.Localization
Run Code Online (Sandbox Code Playgroud)

在我们的内部myComponent.razor,我们将添加

@inject IStringLocalizer<myComponent> Localizer
Run Code Online (Sandbox Code Playgroud)

<h1>@Localizer["Header"]</h1>现在我们可以像在普通页面中一样使用。但现在还有另一个问题:我们的 Blazor 组件未正确设置其文化。组件将其视为/_blazor它们的 URL,而不是页面的 URL。注释掉<base href="~/">您的<head>元素中的 ,_Layout.cshtml使 Blazor 尝试点击/en/_blazor而不是/_blazor。这将得到 404,但我们会解决这个问题。

在里面Startup.cs,我们将注册另一个中间件。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;

namespace app.Extensions
{
    public static class HttpRequestExtensions
    {
        public static string GetCulture(this HttpRequest request)
        {
            return request.HttpContext.Features.Get<IRequestCultureFeature>()
            .RequestCulture.Culture.TwoLetterISOLanguageName;
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

该调用应该在app.UseEndpointsandapp.UseRequestLocalization()调用之前。

Middleware/BlazorCultureExtractor.cs

            // Used by the culture anchor tag helper
            services.AddHttpContextAccessor();
Run Code Online (Sandbox Code Playgroud)

中间件将检查路由是否尝试命中/en/_blazor,将RouteValues["culture"]值设置为en,并在进一步处理之前重写路径/_blazor。这会将 lang 放入路由值中供我们RequestCultureProvider使用,同时还修复了 blazor 尝试访问本地化路由时出现的 404 错误。

里面_Layout.cshtml我也用

    <script src="~/_framework/blazor.server.js"></script>"
Run Code Online (Sandbox Code Playgroud)

确保 blazor 脚本的请求到达正确的路径而不是/en/_framework/.... 请注意~/src属性的前面内容。

结束语

如果您想要纯粹的基于 URL 的本地化,而不是 MS 推广的奇怪的 cookie 内容,那么需要做很多工作。

我还没有费心考虑使用 Blazor页面执行此操作,我现在只是坚持使用组件。

例如,

<component>
    @(await Html.RenderComponentAsync<MyCounterComponent>(RenderMode.Server))
</component>
Run Code Online (Sandbox Code Playgroud)