65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET 8 – 多语言应用程序,使用单一 Resx 文件 - 第 4 部分 – 资源管理器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2024 年 3 月 22 日

CPOL

4分钟阅读

viewsIcon

7400

downloadIcon

201

构建多语言 ASP.NET 8 MVC 应用程序的实用指南

1. 资源管理器在 ASP.NET 8 MVC 中仍然可用

对于喜欢传统方法的人来说,好消息是资源管理器在 ASP.NET 8 MVC 中仍然可用。您可以将其与 IStringLocalizer 同时使用,或者如果您喜欢,甚至可以将其作为唯一的本地化机制。

1.1 资源管理器的工作原理

因此,一种典型的方法是在代码中使用表达式,例如 Resources.SharedResource.Wellcome。这实际上是一个求值为 string 的属性。字符串的求值是在运行时动态完成的,并且字符串是从 SharedResource resx 文件中选择的,具体取决于当前线程的区域性。

2. 本系列其他文章

本系列文章包括:

3. 共享资源方法

默认情况下,ASP.NET Core 8 MVC 技术设想为每个控制器和视图使用单独的资源文件 .resx。但大多数人不喜欢。由于应用程序中大多数多语言字符串在不同地方都是相同的,我们希望将其放在同一个地方。文献 [1] 将这种方法称为“共享资源”方法。为了实现这一点,我们将创建一个标记类 SharedResoureces.cs 来 agrup 所有资源。

4. 多语言应用程序步骤

4.1 配置本地化服务和中间件

本地化服务在 Program.cs 中配置

 private static void AddingMultiLanguageSupportServices(WebApplicationBuilder? builder)
 {
     if (builder == null) { throw new Exception("builder==null"); };

     builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
     builder.Services.AddMvc()
             .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
     builder.Services.Configure<RequestLocalizationOptions>(options =>
     {
         var supportedCultures = new[] { "en", "fr", "de", "it" };
         options.SetDefaultCulture(supportedCultures[0])
             .AddSupportedCultures(supportedCultures)
             .AddSupportedUICultures(supportedCultures);
     });
 }

 private static void AddingMultiLanguageSupport(WebApplication? app)
 {
     app?.UseRequestLocalization();
 } 

4.2 创建标记类 SharedResources.cs

这只是一个用于 agrupir 共享资源的虚拟标记类。我们需要它是因为它的名称和类型。

似乎命名空间需要与应用程序的根命名空间相同,而根命名空间需要与程序集名称相同。我在更改命名空间时遇到了一些问题,它不起作用。如果对您不起作用,您可以尝试在 DI 指令中使用完整的类名,例如:IStringLocalizer<SharedResources01.SharedResource> StringLocalizer

SharedResource”这个名字没有什么魔法,你可以将其命名为“MyResources”,然后在代码中更改所有对“MyResources”的引用,一切仍然会正常工作。

似乎位置可以是任何文件夹,尽管一些文章 ([6] 声称它需要是项目根文件夹。在这个示例中我没有看到这样的问题。对我来说,看起来它应该是任何文件夹,只要保持您的命名空间整洁即可。

//SharedResource.cs===================================================
namespace SharedResources04
{
    /*
    * This is just a dummy marker class to group shared resources
    * We need it for its name and type
    * 
    * It seems the namespace needs to be the same as app root namespace
    * which needs to be the same as the assembly name.
    * I had some problems when changing the namespace, it would not work.
    * If it doesn't work for you, you can try to use full class name
    * in your DI instruction, like this one:
    * IStringLocalizer<SharedResources04.SharedResource> StringLocalizer
    * 
    * There is no magic in the name "SharedResource", you can
    * name it "MyResources" and change all references in the code
    * to "MyResources" and all will still work
    * 
    * Location seems can be any folder, although some
    * articles claim it needs to be the root project folder
    * I do not see such problems in this example. 
    * To me looks it can be any folder, just keep your
    * namespace tidy. 
    */

    public class SharedResource
    {
    }
}

4.3 创建语言资源文件

在“Resources”文件夹中,创建你的语言资源文件,并确保将它们命名为 SharedResources.xx.resx

在 Visual Studio 资源编辑器中,您需要为所有资源文件设置访问修饰符为 Public。

4.4 选择语言/区域性

基于 [5],本地化服务有三个默认提供程序

  1. QueryStringRequestCultureProvider
  2. CookieRequestCultureProvider
  3. AcceptLanguageHeaderRequestCultureProvider

由于大多数应用程序通常会提供一种机制来使用 ASP.NET Core 区域性 cookie 来设置区域性,因此在我们的示例中,我们将只关注这种方法。

这是设置 .AspNetCore.Culture cookie 的代码

private void ChangeLanguage_SetCookie(HttpContext myContext, string? culture)
{
    if(culture == null) { throw new Exception("culture == null"); };

    //this code sets .AspNetCore.Culture cookie
    CookieOptions cookieOptions = new CookieOptions();
    cookieOptions.Expires = DateTimeOffset.UtcNow.AddMonths(1);
    cookieOptions.IsEssential = true;

    myContext.Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        cookieOptions
    );
}

可以使用 Chrome DevTools 轻松查看 Cookie

我构建了一个小型应用程序来演示它,这是我可以更改语言的屏幕

请注意,我在页脚添加了一些调试信息,以显示 **请求语言 Cookie** 的值,以查看应用程序是否按预期工作。

4.5 在控制器中使用本地化服务

在这里,我们展示了 IStringLocalizer 和 **资源管理器** 这两种方法如何用于本地化控制器代码中的字符串

这是代码片段:

 public class HomeController : Controller
 {
     private readonly ILogger<HomeController> _logger;
     private readonly IStringLocalizer<SharedResource> _stringLocalizer;

     /* Here is, of course, the Dependency Injection (DI) coming in and filling 
      * all the dependencies. The key thing is we are asking for a specific 
      * type=SharedResource. 
      * If it doesn't work for you, you can try to use full class name
      * in your DI instruction, like this one:
      * IStringLocalizer<SharedResources04.SharedResource> stringLocalizer
      */
     public HomeController(ILogger<HomeController> logger,
         IStringLocalizer<SharedResource> stringLocalizer)
     {
         _logger = logger;
         _stringLocalizer = stringLocalizer;
     }
     
     //============================
     public IActionResult LocalizationExample(LocalizationExampleViewModel model)
{
    string text = "Thread CurrentUICulture is [" + 
                   @Thread.CurrentThread.CurrentUICulture.ToString() + "] ; ";
    text += "Thread CurrentCulture is [" + @Thread.CurrentThread.CurrentCulture.ToString() + "]";
    model.ThreadCultureInController = text;
    //here we test localization by Resource Manager
    model.LocalizedInControllerByResourceManager1 = Resources.SharedResource.Wellcome;
    model.LocalizedInControllerByResourceManager2 = Resources.SharedResource.Hello_World;
    //here we test localization by IStringLocalizer
    model.LocalizedInControllerByIStringLocalizer1 = _stringLocalizer["Wellcome"];
    model.LocalizedInControllerByIStringLocalizer2 = _stringLocalizer["Hello World"];

    return View(model);
}

4.6 在视图中使用本地化服务

在这里,我们展示了 IStringLocalizer 和 **资源管理器** 这两种方法如何用于本地化视图代码中的 string

这是代码片段:

@* LocalizationExample.cshtml ====================================================*@
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@using SharedResources04

@model LocalizationExampleViewModel

@* Here is of course the Dependency Injection (DI) coming in and filling
all the dependencies. The key thing is we are asking for a specific
type=SharedResource.
If it doesn't work for you, you can try to use full class name
in your DI instruction, like this one:
@inject IStringLocalizer<SharedResources01.SharedResource> StringLocalizer
 *@

@inject IStringLocalizer<SharedResource> StringLocalizer

@{
    <div style="width:600px">
        <p class="text-success">
            Controller Thread Culture:  <br />
            @Model.ThreadCultureInController
        </p>

        <p class="text-primary">
            Localized In Controller By ResourceManager: <br />
            @Model.LocalizedInControllerByResourceManager1
        </p>

        <p class="text-primary">
            Localized In Controller By ResourceManager: <br />
            @Model.LocalizedInControllerByResourceManager2
        </p>

        <p class="text-primary">
            Localized In Controller By IStringLocalizer: <br />
            @Model.LocalizedInControllerByIStringLocalizer1
        </p>

        <p class="text-primary">
            Localized In Controller By IStringLocalizer: <br />
            @Model.LocalizedInControllerByIStringLocalizer2
        </p>

        <p class="text-success">
            @{
                string text = "Thread CurrentUICulture is [" +
                @Thread.CurrentThread.CurrentUICulture.ToString() + "] ; ";
                text += "Thread CurrentCulture is [" +
                @Thread.CurrentThread.CurrentCulture.ToString() + "]";
            }
            View Thread Culture:  <br />
            @text
        </p>

        <p class="text-primary">
            Localized In View By ResourceManager: <br />
            @SharedResources04.Resources.SharedResource.Wellcome
        </p>

        <p class="text-primary">
            Localized In View By ResourceManager: <br />
            @SharedResources04.Resources.SharedResource.Hello_World
        </p>

        <p class="text-primary">
            Localized In View By IStringLocalizer: <br />
            @StringLocalizer["Wellcome"]
        </p>

        <p class="text-primary">
            Localized In View By IStringLocalizer: <br />
            @StringLocalizer["Hello World"]
        </p>

    </div>
}

4.7 执行结果

执行结果如下所示

请注意,我在页脚添加了一些调试信息,以显示 **请求语言 Cookie** 的值,以查看应用程序是否按预期工作。

5. 完整代码

由于大多数人都喜欢可以直接复制粘贴的代码,所以这里是应用程序的完整代码。

//Program.cs===========================================================================
namespace SharedResources04
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //=====Middleware and Services=============================================
            var builder = WebApplication.CreateBuilder(args);

            //adding multi-language support
            AddingMultiLanguageSupportServices(builder);

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            //====App===================================================================
            var app = builder.Build();

            //adding multi-language support
            AddingMultiLanguageSupport(app);

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=ChangeLanguage}/{id?}");

            app.Run();
        }

        private static void AddingMultiLanguageSupportServices
                            (WebApplicationBuilder? builder)
        {
            if (builder == null) { throw new Exception("builder==null"); };

            builder.Services.AddLocalization
                    (options => options.ResourcesPath = "Resources");
            builder.Services.AddMvc()
                    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
            builder.Services.Configure<RequestLocalizationOptions>(options =>
            {
                var supportedCultures = new[] { "en", "fr", "de", "it" };
                options.SetDefaultCulture(supportedCultures[0])
                    .AddSupportedCultures(supportedCultures)
                    .AddSupportedUICultures(supportedCultures);
            });
        }

        private static void AddingMultiLanguageSupport(WebApplication? app)
        {
            app?.UseRequestLocalization();
        }
    }
}

//SharedResource.cs===================================================
namespace SharedResources04
{
    /*
    * This is just a dummy marker class to group shared resources
    * We need it for its name and type
    * 
    * It seems the namespace needs to be the same as app root namespace
    * which needs to be the same as the assembly name.
    * I had some problems when changing the namespace, it would not work.
    * If it doesn't work for you, you can try to use full class name
    * in your DI instruction, like this one:
    * IStringLocalizer<SharedResources04.SharedResource> StringLocalizer
    * 
    * There is no magic in the name "SharedResource", you can
    * name it "MyResources" and change all references in the code
    * to "MyResources" and all will still work
    * 
    * Location seems can be any folder, although some
    * articles claim it needs to be the root project folder
    * I do not see such problems in this example. 
    * To me looks it can be any folder, just keep your
    * namespace tidy. 
    */

    public class SharedResource
    {
    }
}

//HomeController.cs================================================================
namespace SharedResources04.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IStringLocalizer<SharedResource> _stringLocalizer;

        /* Here is, of course, the Dependency Injection (DI) coming in and filling 
         * all the dependencies. The key thing is we are asking for a specific 
         * type=SharedResource. 
         * If it doesn't work for you, you can try to use full class name
         * in your DI instruction, like this one:
         * IStringLocalizer<SharedResources04.SharedResource> stringLocalizer
         */
        public HomeController(ILogger<HomeController> logger,
            IStringLocalizer<SharedResource> stringLocalizer)
        {
            _logger = logger;
            _stringLocalizer = stringLocalizer;
        }

        public IActionResult ChangeLanguage(ChangeLanguageViewModel model)
        {
            if (model.IsSubmit)
            {
                HttpContext myContext = this.HttpContext;
                ChangeLanguage_SetCookie(myContext, model.SelectedLanguage);
                //doing funny redirect to get new Request Cookie
                //for presentation
                return LocalRedirect("/Home/ChangeLanguage");
            }

            //prepare presentation
            ChangeLanguage_PreparePresentation(model);
            return View(model);
        }

        private void ChangeLanguage_PreparePresentation(ChangeLanguageViewModel model)
        {
            model.ListOfLanguages = new List<SelectListItem>
                        {
                            new SelectListItem
                            {
                                Text = "English",
                                Value = "en"
                            },

                            new SelectListItem
                            {
                                Text = "German",
                                Value = "de",
                            },

                            new SelectListItem
                            {
                                Text = "French",
                                Value = "fr"
                            },

                            new SelectListItem
                            {
                                Text = "Italian",
                                Value = "it"
                            }
                        };
        }

        private void ChangeLanguage_SetCookie(HttpContext myContext, string? culture)
        {
            if(culture == null) { throw new Exception("culture == null"); };

            //this code sets .AspNetCore.Culture cookie
            CookieOptions cookieOptions = new CookieOptions();
            cookieOptions.Expires = DateTimeOffset.UtcNow.AddMonths(1);
            cookieOptions.IsEssential = true;

            myContext.Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
                cookieOptions
            );
        }

        public IActionResult LocalizationExample(LocalizationExampleViewModel model)
        {
            string text = "Thread CurrentUICulture is [" + 
                           @Thread.CurrentThread.CurrentUICulture.ToString() + "] ; ";
            text += "Thread CurrentCulture is 
                    [" + @Thread.CurrentThread.CurrentCulture.ToString() + "]";
            model.ThreadCultureInController = text;
            //here we test localization by Resource Manager
            model.LocalizedInControllerByResourceManager1 = 
                                       Resources.SharedResource.Wellcome;
            model.LocalizedInControllerByResourceManager2 = 
                                       Resources.SharedResource.Hello_World;
            //here we test localization by IStringLocalizer
            model.LocalizedInControllerByIStringLocalizer1 = 
                                       _stringLocalizer["Wellcome"];
            model.LocalizedInControllerByIStringLocalizer2 = 
                                      _stringLocalizer["Hello World"];

            return View(model);
        }

        public IActionResult Error()
        {
            return View(new ErrorViewModel 
            { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

//ChangeLanguageViewModel.cs=====================================================
namespace SharedResources04.Models.Home
{
    public class ChangeLanguageViewModel
    {
        //model
        public string? SelectedLanguage { get; set; } = "en";

        public bool IsSubmit { get; set; } = false;

        //view model
        public List<SelectListItem>? ListOfLanguages { get; set; }
    }
}

//LocalizationExampleViewModel.cs===============================================
namespace SharedResources04.Models.Home
{
    public class LocalizationExampleViewModel
    {
        public string? LocalizedInControllerByResourceManager1 { get; set; }
        public string? LocalizedInControllerByResourceManager2 { get; set; }

        public string? LocalizedInControllerByIStringLocalizer1 { get; set; }
        public string? LocalizedInControllerByIStringLocalizer2 { get; set; }

        public string? ThreadCultureInController { get; set; }
    }
}
@* ChangeLanguage.cshtml ===================================================*@
@model ChangeLanguageViewModel

@{
    <div style="width:500px">
        <p class="bg-info">
            <partial name="_Debug.AspNetCore.CultureCookie" /><br />
        </p>

        <form id="form1">
            <fieldset class="border rounded-3 p-3">
                <legend class="float-none w-auto px-3">Change Language</legend>
                <div class="form-group">
                    <label asp-for="SelectedLanguage">Select Language</label>
                    <select class="form-select" asp-for="SelectedLanguage"
                            asp-items="@Model.ListOfLanguages">
                    </select>
                    <input type="hidden" name="IsSubmit" value="true">
                    <button type="submit" form="form1" 
                     class="btn btn-primary mt-3 float-end"
                            asp-area="" asp-controller="Home" 
                            asp-action="ChangeLanguage">
                        Submit
                    </button>
                </div>
            </fieldset>
        </form>
    </div>
}

@* LocalizationExample.cshtml ====================================================*@
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@using SharedResources04

@model LocalizationExampleViewModel

@* Here is of course the Dependency Injection (DI) coming in and filling
all the dependencies. The key thing is we are asking for a specific
type=SharedResource.
If it doesn't work for you, you can try to use full class name
in your DI instruction, like this one:
@inject IStringLocalizer<SharedResources04.SharedResource> StringLocalizer
 *@

@inject IStringLocalizer<SharedResource> StringLocalizer

@{
    <div style="width:600px">
        <p class="text-success">
            Controller Thread Culture:  <br />
            @Model.ThreadCultureInController
        </p>

        <p class="text-primary">
            Localized In Controller By ResourceManager: <br />
            @Model.LocalizedInControllerByResourceManager1
        </p>

        <p class="text-primary">
            Localized In Controller By ResourceManager: <br />
            @Model.LocalizedInControllerByResourceManager2
        </p>

        <p class="text-primary">
            Localized In Controller By IStringLocalizer: <br />
            @Model.LocalizedInControllerByIStringLocalizer1
        </p>

        <p class="text-primary">
            Localized In Controller By IStringLocalizer: <br />
            @Model.LocalizedInControllerByIStringLocalizer2
        </p>

        <p class="text-success">
            @{
                string text = "Thread CurrentUICulture is [" +
                @Thread.CurrentThread.CurrentUICulture.ToString() + "] ; ";
                text += "Thread CurrentCulture is [" +
                @Thread.CurrentThread.CurrentCulture.ToString() + "]";
            }
            View Thread Culture:  <br />
            @text
        </p>

        <p class="text-primary">
            Localized In View By ResourceManager: <br />
            @SharedResources04.Resources.SharedResource.Wellcome
        </p>

        <p class="text-primary">
            Localized In View By ResourceManager: <br />
            @SharedResources04.Resources.SharedResource.Hello_World
        </p>

        <p class="text-primary">
            Localized In View By IStringLocalizer: <br />
            @StringLocalizer["Wellcome"]
        </p>

        <p class="text-primary">
            Localized In View By IStringLocalizer: <br />
            @StringLocalizer["Hello World"]
        </p>

    </div>
}

6. 参考文献

7. 历史

  • 2024 年 3 月 22 日:初始版本
© . All rights reserved.