ASP.NET Core Web API 多語系(i18n)

建立多語系檔案

在專案中建立 Resources 資料夾,在資料夾上點選滑鼠右鍵 > 加入 > 新增項目:

選擇「資源檔」

這裡的檔名要依照使用的位置來命名。
例如要在 Controllers 資料夾內的 UserController.cs 檔案使用則檔名就要叫做 Controllers.UserController.resx (預設資源檔),語系為 zh-TW 則是 Controllers.UserController.zh-TW.resx

覺得這樣檔名太長還可以使用資料夾的方式,就會變成 Controllers/UserController.resx 和 Controllers/UserController.zh-TW.resx 本文的範例如下:

Controllers/UserController.zh-TW.resx
    
Hello		哈囉
Hello_Name	哈囉 {0}!
    

Controllers/UserController.en-US.resx
    
Hello		Hello
Hello_Name	Hello {0}!
    

在 Program.cs 中加入以下程式碼:
    
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    // 預設語言 (不指定就會使用裝置的預設語系)
    options.DefaultRequestCulture = new RequestCulture("zh-TW");
    
    // 支援的語言
    var supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("zh-TW"),
    };
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
});

var app = builder.Build();

app.UseRequestLocalization();

app.Run();
    

常見錯誤

在存取 IStringLocalizer _localizer 時出現錯誤:
    
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.InvalidOperationException: Unable to resolve service for type 'Microsoft.Extensions.Localization.IStringLocalizerFactory' while attempting to activate 'WebApplicationI18nTest0706.Controllers.MyController'.
         at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ThrowHelperUnableToResolveService(Type type, Type requiredBy)
         at lambda_method3(Closure, IServiceProvider, Object[])
         at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.>>c__DisplayClass6_0.>CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.>InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.>InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.>InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
         at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
         at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
    

代表少了下面的程式碼,補上即可:
    
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
    

依照語系替換內容

這裡最簡單示範單純替換文字和可以帶入參數替換的兩種多語系替換方式:
    
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;


[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
    private readonly IStringLocalizer<UserController> _localizer;

    public UserController(IStringLocalizer<UserController> localizer)
    {
        _localizer = localizer;
    }

    [HttpGet]
    public IActionResult Get()
    {
        // 取得當前 API 的語系
        Console.WriteLine($"CultureInfo: {CultureInfo.CurrentCulture}");

        var hello = _localizer["Hello"].Value;
        Console.WriteLine($"Hello: {hello}");

        string input = "Ruyut";
        var helloName = _localizer["Hello_Name", input].Value;
        Console.WriteLine($"Hello_Name: {helloName}");

        return Ok(helloName);
    }
}
    

API 指定語言

在呼叫 API 時,可以在 QueryString 中帶入 culture 參數來指定語言,下面示範將語言指定為 en-us :
    
http://localhost:5202/User?culture=en-us
    

也可以透過 Header 加入 Accept-Language 參數來達成:
    
curl --location 'http://localhost:5202/User' \
--header 'Accept-Language: en-us'
    

註:在筆者的測試中發現不區分大小寫, en-US 和 en-us 相同,zh-tw 和 zh-TW 也是相同。

輸出範例

zh-TW (預設)
    
Hello: 哈囉
Hello_Name: 哈囉 Ruyut!
    

en-US
    
Hello: Hello
Hello_Name: Hello Ruyut!
    


每個功能都需要一個單獨的資源檔很麻煩怎麼辦?筆者後來研究出來了,可以查看這篇:ASP.NET Core Web API 多語系(i18n) 共用資源檔

參考資料:
Microsoft.Learn - Globalization and localization in ASP.NET Core
Microsoft.Learn - Make an ASP.NET Core app's content localizable
Microsoft.Learn - Troubleshoot ASP.NET Core localization
StackOverflow - ASP.NET Core Request Localization Options

留言