Skip to main content

คู่มือโลคัลไลเซชัน ASP.NET Core อย่างครบถ้วน

ตั้งแต่ IStringLocalizer จนถึงระบบจริง ตั้งค่าโลคัลไลเซชันแบบใช้ทรัพยากรใน ASP.NET Core แล้วทำให้การแปลเป็นอัตโนมัติด้วย AI

1

เปิดใช้บริการโลคัลไลเซชัน

ลงทะเบียนบริการโลคัลไลเซชันใน Program.cs ด้วย AddLocalization() กำหนดค่าวัฒนธรรมที่รองรับ และเพิ่มมิดเดิลแวร์โลคัลไลเซชันคำขอ วิธีนี้เชื่อมไปป์ไลน์โลคัลไลเซชันทั้งหมดสำหรับแอป ASP.NET Core

AddLocalization() ลงทะเบียน IStringLocalizer และ IStringLocalizerFactory ในคอนเทนเนอร์ DI ส่วน ResourcesPath บอกเฟรมเวิร์กว่าจะหาไฟล์ .resx ที่ใด AddViewLocalization() เปิดใช้ IViewLocalizer ในมุมมอง Razor และ AddDataAnnotationsLocalization() เปิดใช้ข้อความตรวจสอบฉบับโลคัลไลซ์
Program.cs
using Microsoft.AspNetCore.Localization;
using System.Globalization;

var builder = WebApplication.CreateBuilder(args);

// 1. Register localization services
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");

// 2. Add MVC with view/data-annotation localization
builder.Services.AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization();

var app = builder.Build();

// 3. Configure supported cultures
var supportedCultures = new[] { "en", "de", "ja", "es", "pt-BR" }
    .Select(c => new CultureInfo(c)).ToArray();

app.UseRequestLocalization(new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures,
});

app.UseStaticFiles();
app.UseRouting();
app.MapControllers();
app.Run();
2

สร้างไฟล์ทรัพยากร RESX

ASP.NET Core ใช้ไฟล์ RESX (ทรัพยากร XML) สำหรับคำแปล สร้างหนึ่งไฟล์ต่อวัฒนธรรมต่อคลาส เช่น HomeController.en.resx, HomeController.de.resx และอื่นๆ เฟรมเวิร์กจะค้นหาไฟล์ที่ถูกต้องตามวัฒนธรรมของคำขอปัจจุบัน

Resources/Controllers/HomeController.{culture}.resx
<!-- Resources/Controllers/HomeController.en.resx -->
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Welcome" xml:space="preserve">
    <value>Welcome to our application</value>
  </data>
  <data name="Greeting" xml:space="preserve">
    <value>Hello, {0}!</value>
  </data>
</root>

<!-- Resources/Controllers/HomeController.de.resx -->
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Welcome" xml:space="preserve">
    <value>Willkommen in unserer Anwendung</value>
  </data>
  <data name="Greeting" xml:space="preserve">
    <value>Hallo, {0}!</value>
  </data>
</root>
ใช้คลาส SharedResource พร้อมไฟล์ RESX ของตัวเองสำหรับข้อความที่ใช้ร่วมกันในคอนโทรลเลอร์และมุมมอง เช่น ป้ายปุ่ม รายการนำทาง และข้อความตรวจสอบทั่วไป วิธีนี้ไม่ต้องทำคีย์ซ้ำในไฟล์ RESX เฉพาะคอนโทรลเลอร์หลายสิบไฟล์
Shared resources for cross-cutting strings
<!-- Resources/SharedResource.en.resx — shared across controllers -->
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="AppName" xml:space="preserve">
    <value>My Application</value>
  </data>
  <data name="Save" xml:space="preserve"><value>Save</value></data>
  <data name="Cancel" xml:space="preserve"><value>Cancel</value></data>
</root>

// Marker class (empty — only used for type lookup)
namespace MyApp;
public class SharedResource { }
3

ใช้ IStringLocalizer ในคอนโทรลเลอร์และบริการ

ฉีด IStringLocalizer&lt;T&gt; ลงในคอนโทรลเลอร์ บริการ หรือมิดเดิลแวร์ผ่านการฉีดการพึ่งพา พารามิเตอร์ชนิดทั่วไป T กำหนดว่าจะโหลดไฟล์ RESX ใด ใช้ไวยากรณ์วงเล็บ localizer["Key"] เพื่อดึงข้อความที่แปลแล้วพร้อมพารามิเตอร์รูปแบบเสริม

Controllers/HomeController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

public class HomeController(
    IStringLocalizer<HomeController> localizer,
    IStringLocalizer<SharedResource> shared) : Controller
{
    public IActionResult Index()
    {
        ViewData["Welcome"] = localizer["Welcome"];
        ViewData["AppName"] = shared["AppName"];

        // String interpolation with format parameters
        var greeting = localizer["Greeting", User.Identity?.Name ?? "Guest"];
        return View(new HomeViewModel { Greeting = greeting });
    }
}
หาก IStringLocalizer คืนชื่อคีย์แทนค่าที่แปลแล้ว ให้ตรวจสามอย่าง : (1) ชื่อไฟล์ RESX ตรงกับเนมสเปซคลาส (2) ResourcesPath ใน AddLocalization() ชี้โฟลเดอร์ที่ถูกต้อง และ (3) Build Action ของไฟล์ RESX ตั้งเป็น Embedded Resource ใน Visual Studio
4

กำหนดค่ามิดเดิลแวร์วัฒนธรรมคำขอ

ASP.NET Core เลือกวัฒนธรรมคำขอจากลำดับผู้ให้บริการ ได้แก่ สตริงคำขอ คุกกี้ และส่วนหัว Accept-Language ตามลำดับ คุณเพิ่มผู้ให้บริการแบบกำหนดเองได้ เช่น อ่านวัฒนธรรมจากเซกเมนต์เส้นทาง URL อย่าง /de/home

Custom RouteDataRequestCultureProvider
// Culture resolved in order: QueryString, Cookie, Accept-Language
// Custom provider: read culture from URL route segment /de/home
public class RouteDataRequestCultureProvider : RequestCultureProvider
{
    public override Task<ProviderCultureResult?> DetermineProviderCultureResult(
        HttpContext httpContext)
    {
        var culture = httpContext.GetRouteValue("culture")?.ToString();
        if (string.IsNullOrEmpty(culture))
            return NullProviderCultureResult;
        return Task.FromResult<ProviderCultureResult?>(
            new ProviderCultureResult(culture));
    }
}

// Register in Program.cs (route provider first = highest priority):
app.UseRequestLocalization(new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures,
    RequestCultureProviders = new List<IRequestCultureProvider>
    {
        new RouteDataRequestCultureProvider(),
        new QueryStringRequestCultureProvider(),
        new CookieRequestCultureProvider(),
        new AcceptLanguageHeaderRequestCultureProvider(),
    }
});
Language Switcher Action
// Language switcher: persist choice in cookie
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
    return LocalRedirect(returnUrl);
}
ลำดับมิดเดิลแวร์สำคัญ ต้องเรียก UseRequestLocalization() หลัง UseRouting() แต่ก่อน UseEndpoints() หรือ MapControllers() หากวางช้าเกินไป ระบบจะยังไม่ตั้งวัฒนธรรมตอนคอนโทรลเลอร์ทำงาน
5

ทำโลคัลไลเซชันคำอธิบายข้อมูล

แอตทริบิวต์ตรวจสอบอย่าง [Required], [StringLength] และ [Display] ทำโลคัลไลเซชันได้ด้วยการตั้งพร็อพเพอร์ตี ErrorMessage หรือ Name เป็นชื่อคีย์ RESX เรียก AddDataAnnotationsLocalization() ใน Program.cs เพื่อเปิดใช้

ViewModels/RegisterViewModel.cs
using System.ComponentModel.DataAnnotations;

public class RegisterViewModel
{
    [Required(ErrorMessage = "NameRequired")]
    [Display(Name = "FullName")]
    [StringLength(100, ErrorMessage = "NameLength", MinimumLength = 2)]
    public string Name { get; set; } = string.Empty;

    [Required(ErrorMessage = "EmailRequired")]
    [EmailAddress(ErrorMessage = "EmailInvalid")]
    [Display(Name = "EmailAddress")]
    public string Email { get; set; } = string.Empty;

    [Required(ErrorMessage = "PasswordRequired")]
    [StringLength(100, ErrorMessage = "PasswordLength", MinimumLength = 8)]
    [Display(Name = "Password")]
    public string Password { get; set; } = string.Empty;
}
// RESX keys map to ErrorMessage/Name values:
// RegisterViewModel.de.resx: NameRequired = "Name ist erforderlich"
// RegisterViewModel.de.resx: FullName = "Vollständiger Name"
โลคัลไลเซชันคำอธิบายข้อมูลใช้ชื่อคลาส ViewModel หาไฟล์ RESX ไม่ใช่คอนโทรลเลอร์ สำหรับ RegisterViewModel เฟรมเวิร์กจะหา Resources/ViewModels/RegisterViewModel.de.resx หากตั้งชื่อไฟล์ RESX ตามคอนโทรลเลอร์ ข้อความตรวจสอบจะไม่ได้รับการโลคัลไลซ์
6

จัดการพหูพจน์และข้อความ ICU

.NET ไม่รองรับกฎพหูพจน์ในตัวอย่าง ICU สำหรับกรณีง่าย ให้ใช้คีย์ RESX แยก (ItemCount_One, ItemCount_Other) พร้อม switch ในโค้ด สำหรับ ICU MessageFormat เต็มรูปแบบในทุกหมวดหมู่พหูพจน์ CLDR ให้ใช้ไลบรารี MessageFormat.NET

Plural handling strategies
// Option 1: Separate RESX keys with code switch
// HomeController.en.resx: ItemCount_One = "You have {0} item"
// HomeController.en.resx: ItemCount_Other = "You have {0} items"
public string GetItemCount(int count)
{
    var key = count == 1 ? "ItemCount_One" : "ItemCount_Other";
    return _localizer[key, count];
}

// Option 2: ICU MessageFormat (dotnet add package MessageFormat.NET)
using Jeffijoe.MessageFormat;
var formatter = new MessageFormatter();

var pattern = "{count, plural, one {# item} other {# items}} in your cart";
var result = formatter.FormatMessage(pattern,
    new Dictionary<string, object> { { "count", 5 } });
// => "5 items in your cart"

// Arabic: 6 plural forms (zero, one, two, few, many, other)
var arPattern = @"{count, plural,
    zero {لا عناصر} one {عنصر واحد} two {عنصران}
    few {# عناصر} many {# عنصرًا} other {# عنصر}}";
อย่าใช้ count == 1 ตรวจหารูปเอกพจน์ ภาษาฝรั่งเศสมอง 0 เป็นเอกพจน์ รัสเซียมีรูป 'few' และ 'many' แยกกัน ส่วนอาหรับมีหกหมวดหมู่พหูพจน์ ให้ใช้กฎพหูพจน์ที่รับรู้ CLDR หรือไลบรารีอย่าง MessageFormat.NET ที่จัดการได้ถูกต้อง
7

ทำโลคัลไลเซชันมุมมอง Razor

ใช้ IViewLocalizer ในมุมมอง Razor ผ่าน @inject โดยจะค้นหาไฟล์ RESX ตามพาธไฟล์ของมุมมอง สำหรับข้อความที่ปลอดภัยต่อ HTML และมีมาร์กอัป ให้ใช้ IHtmlLocalizer ส่วน Tag Helper อย่าง asp-for และ asp-validation-for จะใช้แอตทริบิวต์ Display กับ ErrorMessage ฉบับโลคัลไลซ์โดยอัตโนมัติ

Views/Home/Index.cshtml
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedHtml

<h1>@Localizer["Welcome"]</h1>
<p>@Localizer["Greeting", User.Identity?.Name]</p>

@* IHtmlLocalizer: does NOT escape — use for RESX values with HTML *@
<p>@SharedHtml["TermsNotice"]</p>

@* Tag Helpers auto-localize Display/ErrorMessage attributes *@
<form asp-action="Register">
    <label asp-for="Name"></label>
    <input asp-for="Name" />
    <span asp-validation-for="Name"></span>
    <button type="submit">@Localizer["Submit"]</button>
</form>
IViewLocalizer ค้นหาไฟล์ RESX ตามพาธมุมมอง : Views/Home/Index.cshtml จะหา Resources/Views/Home/Index.de.resx หากต้องการแชร์ข้อความระหว่างมุมมอง ให้ฉีด IStringLocalizer&lt;SharedResource&gt; แยกต่างหาก
8

ทำให้การแปล RESX เป็นอัตโนมัติ

เมื่อตั้งค่าโลคัลไลเซชันเสร็จแล้ว ให้แปลไฟล์ RESX ด้วย AI โดยบอกผู้ช่วย AI ใน IDE ให้แปล RESX ต้นฉบับ หรือใช้ CLI ของ i18n Agent ในไปป์ไลน์ CI/CD เพื่อให้คำแปลซิงค์กัน

Terminal
# In your IDE, ask your AI assistant:
> Translate Resources/Controllers/HomeController.en.resx to German, Japanese, Spanish

# HomeController.de.resx created (1.2s)
# HomeController.ja.resx created (1.5s)
# HomeController.es.resx created (1.1s)

# Or use the CLI in CI/CD:
npx i18n-agent translate Resources/Controllers/HomeController.en.resx --lang de,ja,es
แปลแบบเพิ่มทีละส่วน เมื่อเพิ่มคีย์ใหม่ในไฟล์ RESX ต้นฉบับ ให้แปลเฉพาะส่วนต่างแทนการสร้างทุกไฟล์ใหม่ วิธีนี้ช่วยรักษาคำแปลที่มนุษย์ตรวจทานแล้วและลดการเปลี่ยนแปลงที่ไม่จำเป็น

ทำให้คุณภาพการแปลเป็นอัตโนมัติ

ใช้ i18n-validate จับคีย์ที่หายไปและตัวยึดตำแหน่งเสียหายก่อนส่งขึ้นใช้งาน แล้วทดสอบ UI ด้วยคำแปลจำลองผ่าน i18n-pseudo ก่อนคำแปลจริงจะมาถึง

แก้การใช้ภาษาสำรองด้วย LocaleChain.NET

ลำดับชั้น CultureInfo.Parent ในตัวของ .NET ใช้เฉพาะการตัดทอน BCP 47 โดย pt-BR ถอยไปใช้ pt แล้ว InvariantCulture และข้าม pt-PT LocaleChain.NET มีลำดับการใช้ภาษาสำรองที่กำหนดค่าได้รายภาษาสำหรับระบบ .NET ทั้งหมด

หากไม่มี LocaleChain.NET ผู้ใช้ pt-BR จะเห็นภาษาอังกฤษเมื่อไม่มีข้อความโปรตุเกส แม้คุณมีคำแปล pt-PT ครบถ้วน ปัญหาเดียวกันนี้เกิดกับ es-MX ซึ่งข้าม es-419, zh-Hant ซึ่งข้าม zh-Hans และรูปแบบภาษาตามภูมิภาคอีกหลายสิบรายการ
Terminal
dotnet add package I18nAgent.LocaleChain
Program.cs
using I18nAgent.LocaleChain;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");

// Zero-config: built-in chains (pt-BR -> pt-PT -> pt -> en, etc.)
LocaleChain.Configure();

// Or customize specific chains:
LocaleChain.Configure(new Dictionary<string, string[]>
{
    ["pt-BR"] = new[] { "pt-PT", "pt", "en" },
    ["es-MX"] = new[] { "es-419", "es", "en" },
});

// Register the chain-aware string localizer
builder.Services.AddSingleton(
    typeof(IStringLocalizer<>),
    typeof(LocaleChainStringLocalizer<>));

ข้อผิดพลาดที่พบบ่อย

ไม่ได้ตั้งวัฒนธรรมในคำขอ

คำแปลแสดงภาษาเริ่มต้นเสมอ ตรวจว่าเรียก UseRequestLocalization() ในไปป์ไลน์มิดเดิลแวร์และกำหนดค่าผู้ให้บริการวัฒนธรรมแล้ว ตรวจว่าเบราว์เซอร์ส่งส่วนหัว Accept-Language ทดสอบด้วย ?culture=de ในสตริงคำขอเพื่อยืนยันว่ามิดเดิลแวร์ทำงาน

หาไฟล์ RESX ไม่พบ

IStringLocalizer คืนชื่อคีย์แทนค่าที่แปลแล้ว สาเหตุที่พบบ่อยที่สุดคือชื่อไฟล์ RESX ไม่ตรงกับพาธเนมสเปซเต็มของคลาสเมื่อเทียบกับ ResourcesPath เปิดบันทึกดีบักสำหรับ Microsoft.Extensions.Localization เพื่อดูพาธที่เฟรมเวิร์กค้นหา

ลำดับมิดเดิลแวร์ไม่ถูกต้อง

UseRequestLocalization() ต้องอยู่ก่อน UseEndpoints() และ MapControllers() หากวางหลัง ระบบจะยังไม่ตั้งวัฒนธรรมคำขอตอนคอนโทรลเลอร์ทำงาน ในการโฮสต์ขั้นต่ำของ .NET 6+ ให้เรียกก่อน app.MapControllers()

เธรดเบื้องหลังใช้วัฒนธรรมผิด

CultureInfo.CurrentCulture และ CurrentUICulture แยกตามเธรด งานเบื้องหลัง (Task.Run, บริการโฮสต์) จะรับวัฒนธรรมของพูลเธรด ไม่ใช่วัฒนธรรมคำขอ ให้จับและตั้งวัฒนธรรมอย่างชัดเจนเมื่อส่งงานเบื้องหลัง

โครงสร้างโปรเจกต์ที่แนะนำ

Project Structure
MyAspNetApp/
├── Controllers/
│   └── HomeController.cs
├── ViewModels/
│   └── RegisterViewModel.cs
├── Views/
│   └── Home/
│       └── Index.cshtml
├── Resources/
│   ├── Controllers/
│   │   ├── HomeController.en.resx     # English (source)
│   │   ├── HomeController.de.resx     # German
│   │   └── HomeController.ja.resx     # Japanese
│   ├── ViewModels/
│   │   ├── RegisterViewModel.en.resx
│   │   └── RegisterViewModel.de.resx
│   ├── Views/Home/
│   │   ├── Index.en.resx
│   │   └── Index.de.resx
│   └── SharedResource.en.resx
├── SharedResource.cs                  # Marker class
├── Program.cs
└── MyAspNetApp.csproj

แปลรายการต่อไปนี้ได้ด้วย :

ลองใช้ i18n Agent ตอนนี้

ลากและวางไฟล์แปลของคุณที่นี่

JSON, YAML, PO, XML, CSV, Markdown, Properties

หรือคลิกเพื่อเลือกไฟล์

ภาษาเป้าหมาย

ไม่ต้องลงทะเบียนประเมินราคาได้ทันที

การใช้ภาษาสำรองด้วย I18nAgent.LocaleChain

เมื่อไม่มีคีย์คำแปลในภาษาตามภูมิภาคอย่าง pt-BR .NET จะข้ามไปใช้วัฒนธรรมคงที่ทันทีแทนที่จะตรวจภาษาหลัก pt ก่อน

Terminal
dotnet add package I18nAgent.LocaleChain
Configuration
using I18nAgent.LocaleChain;

LocaleChain.Configure(new Dictionary<string, string[]>
{
    ["pt-BR"] = new[] {"pt", "en"},
    ["zh-Hant-HK"] = new[] {"zh-Hant", "zh", "en"},
});

ดูคู่มือการใช้ภาษาสำรองของเราสำหรับรายการเฟรมเวิร์กที่รองรับทั้งหมดและลำดับสำเร็จรูป 75 รายการ Learn more →

คำถามที่พบบ่อย