[ASP.NET Core] 為應用加上多國語系及本地化

為應用加上多國語系及本地化

在.NetCore也是使用 .resx 來存放語系相關設定,用法跟.net Framework有些許差異

使用.NetCore 3.1 MVC範本專案,需要的package似乎都有了,就不贅述

要注意的是預設的Resources會根據各個檔案的namespace及檔名搭配對應的 .resx,後續會提出解決方式

ex:

MyProject.Controller.HomeController 預設對應的資源檔是 MyProject.Controller.HomeController.<lang>.resx


1.調整 Startup的 ConfigureServices

  • AddLocalization,並且指定路徑為 "Resources",

  • 若未指定Path,預設為專案的根目錄(後續會舉例若無設定,會有什麼結果)

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            services.AddLocalization(options =>
            {
                options.ResourcesPath = "Resources";
            });
        }

2.以及 Configure,加入Middleware

  • 在這邊設定支援的語系列表
  • 預設的文化及UI文化(一個是顯示的語言,一個是資料顯示的格式)
  • 語系的選擇預設有三種方式,預設的執行順序也是按照QueryString => Cookie => Header
    • QueryStringRequestCultureProvider - 從QueryString
    • CookieRequestCultureProvider - 從Cookie
    • AcceptLanguageHeaderRequestCultureProvider - 從Header(一般大部分的請求都會有)
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            var supportedCultures = new List<CultureInfo>()
            {
                new CultureInfo("zh"),
                new CultureInfo("en"),
            };
            app.UseRequestLocalization(new RequestLocalizationOptions()
            {
                DefaultRequestCulture = new RequestCulture("zh"),
                SupportedCultures = supportedCultures,
                SupportedUICultures = supportedCultures,
            });

            // anything...

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

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

3.接著在 Controller 或是其他要使用語系的地方,注入 IStringLocalizer<T>

  • 這邊的 <T> 會影響到抓取哪一個語系資源檔
  • 一般會填上當下的Class Name,在這邊就是 IStringLocalizer<HomeController> 
        public HomeController(ILogger<HomeController> logger,IStringLocalizer<HomeController> localizer)
        {
            _logger = logger;
            _localizer = localizer;
        }

4.在Action準備一點程式碼,從資源檔讀出幾筆資料丟到前端呈現

  • Action
            public IActionResult Index()
            {
                ViewBag.Account = _localizer["Account"];
                ViewBag.Password = _localizer["Password"];
                return View();
            }
    
  • View

    <div class="text-center">
        <h1 class="display-4">Welcome</h1>
        <h1 class="display-4">@ViewBag.Account</h1>
        <h1 class="display-4">@ViewBag.Password</h1>
    </div>

5.接著新增資源檔 .resx

  • 路徑及名稱必須匹配相對class
  • 在資源檔加入一點資料

6.開啟應用,理應上就能看到顯示的文字是資源檔的Value了

7.上面提到預設有三種設定語系的方式,在這邊可以試著加入QueryString,將顯示不同的語系內容(ex: culture=zh)

  • 若是不支援的語系,則會顯示default設定的語系

8.再來讓View也能拿到資源檔的語系值,在Startup加上 AddViewLocalization

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews()
                    .AddViewLocalization();

            services.AddLocalization(options =>
            {
                options.ResourcesPath = "Resources";
            });
        }

9.調整View

  • 主要需要加上 @inject IViewLocalizer _localizer,讓我們可以拿到語系值
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer _localizer

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <h1 class="display-4">@ViewBag.Account</h1>
    <h1 class="display-4">----------</h1>
    <h1 class="display-4">@_localizer["Account"]</h1>
</div>

10.把應用Run起來,很不意外的顯示的是語系的Key值,因為View也需要對應路徑的資源檔

11.新增後再次執行,這次顯示的文字是來自資源檔了

12.接著在View上面,也許有個Model,並且透過HtmlHelper取得他的DisplayName顯示在上面

@model MyClass

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <h1 class="display-4">@ViewBag.Account</h1>
    <h1 class="display-4">----------</h1>
    <h1 class="display-4">@_localizer["Account"]</h1>
    <h1 class="display-4">----------</h1>
    <div><label asp-for="Id"></label></div>
    <div><label asp-for="Name"></label></div>
</div>
namespace LocalizationSample.Models
{
    public class MyClass
    {
        [Display(Name = "MyClass.Id")]
        public string Id { get; set; }
        
        [Display(Name = "MyClass.Name")]
        public string Name { get; set; }
    }
}

13.再來要讓顯示的內容也來自資源檔

14.一樣先調整Startup,加上 AddDataAnnotationsLocalization

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews()
                    .AddViewLocalization()
                    .AddDataAnnotationsLocalization();

            services.AddLocalization(options =>
            {
                options.ResourcesPath = "Resources";
            });
        }

15.為Model加上資源檔

16.執行應用,這次連Model的DisplayName也來自資源檔了

17.回過頭來看,每個地方都有自己的資源檔,若有共用的資源,在維護上絕對是個災難

18.再來讓所有的地方都讀取同一個資源檔

  • 新增一個空的class,我選擇放在Resources底下
  • 接著新增對應名稱的 .resx,對應上面新增的class路徑,需要放在Resources下的Resources底下(記得也要新增內容進去)
  • 先從Controller開始,把IStringLocalizer<T>的型別參數更改為 SharedResources
            public HomeController(ILogger<HomeController> logger,
                                  IStringLocalizer<SharedResources> localizer)
            {
                _logger = logger;
                _localizer = localizer;
            }
    
  • 執行應用,Controller這邊的已經改讀 SharedResources 的設定
  • 再來換View
  • 把View上的 IViewLocalizer 更改為 IHtmlLocalizer<SharedResources>,執行應用後,也能拿到語系值了
    @using Microsoft.AspNetCore.Mvc.Localization
    @using LocalizationSample.Resources
    @* @inject IViewLocalizer _localizer *@
    @inject IHtmlLocalizer<SharedResources> _localizer
    
    @model MyClass
    
    <div class="text-center">
        <h1 class="display-4">Welcome</h1>
        <h1 class="display-4">@ViewBag.Account</h1>
        <h1 class="display-4">----------</h1>
        <h1 class="display-4">@_localizer["Account"]</h1>
        <h1 class="display-4">----------</h1>
        <div><label asp-for="Id"></label></div>
        <div><label asp-for="Name"></label></div>
    </div>
  • 最後換Model上的DisplayName
  • 調整Startup的 AddDataAnnotationsLocalization
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllersWithViews()
                        .AddViewLocalization()
                        .AddDataAnnotationsLocalization(options =>
                        {
                            options.DataAnnotationLocalizerProvider = (type, factory) => 
                                factory.Create(typeof(SharedResources));
                        } );
    
                services.AddLocalization(options =>
                {
                    options.ResourcesPath = "Resources";
                });
            }
    

19.目前爲止Controller、View、Model的語系直都來自 SharedResources了,基本操作到此為止


補充

1.若沒有在Startup上指定 ResourcePath,那專案結構就可能會變成這樣

2.CookieRequestCultureProvider

  • 預設的Cookie名稱為 ".AspNetCore.Culture"
  • 內容格式為 "c=<lang>|uic=<lang>"
    • ex: "c=en|uic=en"

3.若要自定Cookie名稱,可參考下面做法(這邊是ExtensionMethod)

        public static void UseLocalization(this IApplicationBuilder app)
        {
            var cultures = new List<CultureInfo>()
            {
                new CultureInfo("zh"),
                new CultureInfo("en"),
            };
            var localizationOptions = new RequestLocalizationOptions()
            {
                DefaultRequestCulture = new RequestCulture("zh"),
                SupportedCultures = cultures,
                SupportedUICultures = cultures
            };
            localizationOptions.RequestCultureProviders
                               .OfType<CookieRequestCultureProvider>()
                               .First()
                               .CookieName = "UserCulture";

            app.UseRequestLocalization(localizationOptions);
        }

4.若要取得語系的所有內容

            IEnumerable<LocalizedString> result = _localizer.GetAllStrings();

剛好工作專案上有用到,研究了一下並筆記一下做法

不得不抱怨微軟的這篇文章實在不好閱讀(不然就是我的問題,總是對不上頻率🤣)


Microsoft Docs https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/localization?view=aspnetcore-3.1

SampleCode https://github.com/ianChen806/LocalizationSample