Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Localization (i18n)

Overview Questions

  • Localization trong ASP.NET Core hoạt động như thế nào?
  • Resource files là gì và cách sử dụng?
  • Làm sao để localize controllers và views?
  • Culture providers hoạt động ra sao?
  • Localize data annotations và validation messages?

Localization Setup

Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddControllersWithViews()
    .AddViewLocalization(LanguageViewLocationFormatSuffixFormat.Suffix)
    .AddDataAnnotationsLocalization();

var app = builder.Build();

// Supported cultures
var supportedCultures = new[] { "en-US", "vi-VN", "fr-FR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);

app.Run();

Resource Files

File Structure

Project/
├── Resources/
│   ├── Controllers/
│   │   ├── HomeController.en-US.resx
│   │   ├── HomeController.vi-VN.resx
│   │   └── HomeController.fr-FR.resx
│   ├── Views/
│   │   ├── Home/
│   │   │   ├── Index.en-US.resx
│   │   │   └── Index.vi-VN.resx
│   │   └── Shared/
│   │       └── _Layout.en-US.resx
│   ├── SharedResource.en-US.resx
│   └── SharedResource.vi-VN.resx
└── Program.cs

Resource File Content

<!-- SharedResource.en-US.resx -->
<data name="Hello" xml:space="preserve">
  <value>Hello</value>
</data>
<data name="Welcome" xml:space="preserve">
  <value>Welcome to our application</value>
</data>

<!-- SharedResource.vi-VN.resx -->
<data name="Hello" xml:space="preserve">
  <value>Xin chào</value>
</data>
<data name="Welcome" xml:space="preserve">
  <value>Chào mừng bạn đến với ứng dụng</value>
</data>

Using IStringLocalizer

Basic Usage

public class HomeController : Controller
{
    private readonly IStringLocalizer<SharedResource> _localizer;

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

    public IActionResult Index()
    {
        ViewData["Message"] = _localizer["Hello"];
        ViewData["Welcome"] = _localizer["Welcome"];
        return View();
    }
}

With Parameters

// Resource file
// "Greeting" = "Hello {0}, welcome to {1}"

public IActionResult Index(string name, string city)
{
    var message = _localizer["Greeting", name, city];
    // Output: "Hello John, welcome to New York"
    return View();
}

IStringLocalizer vs IHtmlLocalizer

// IStringLocalizer - plain text
public class MyController : Controller
{
    private readonly IStringLocalizer<MyController> _localizer;

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

// IHtmlLocalizer - HTML content (không encode HTML)
public class MyViewComponent : ViewComponent
{
    private readonly IHtmlLocalizer<SharedResource> _localizer;

    public MyViewComponent(IHtmlLocalizer<SharedResource> localizer)
    {
        _localizer = localizer;
    }

    public IViewComponentResult Invoke()
    {
        // Resource: "Bold" = "<strong>Bold text</strong>"
        var html = _localizer["Bold"]; // Không encode HTML
        return View(html);
    }
}

View Localization

In Razor Views

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer

<h1>@Localizer["Welcome"]</h1>
<p>@Localizer["Hello"]</p>

<!-- Hoặc dùng IHtmlLocalizer -->
@inject IHtmlLocalizer<SharedResource> HtmlLocalizer

<p>@HtmlLocalizer["Description"]</p>

View with Model

@model Product
@inject IViewLocalizer Localizer

<h1>@Localizer["ProductDetails"]</h1>
<div>
    <label>@Localizer["Name"]</label>
    <span>@Model.Name</span>
</div>
<div>
    <label>@Localizer["Price"]</label>
    <span>@Model.Price</span>
</div>

Data Annotations Localization

Localized Validation Messages

public class Product
{
    [Required(ErrorMessage = "NameRequired")]
    [Display(Name = "ProductName")]
    public string Name { get; set; } = string.Empty;

    [Range(0.01, 9999.99, ErrorMessage = "PriceRange")]
    [Display(Name = "ProductPrice")]
    public decimal Price { get; set; }
}

// Resource file
// "NameRequired" = "Tên sản phẩm là bắt buộc"
// "ProductName" = "Tên sản phẩm"
// "PriceRange" = "Giá phải từ 0.01 đến 9999.99"
// "ProductPrice" = "Giá sản phẩm"

Configuration

builder.Services.AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = 
            (type, factory) => factory.Create(typeof(SharedResource));
    });

Culture Providers

Request Culture Providers

┌─────────────────────────────────────────────────────────────────┐
│                  CULTURE PROVIDERS                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Thứ tự kiểm tra (theo mặc định):                               │
│  1. QueryStringRequestCultureProvider                           │
│     ?culture=vi-VN&ui-culture=vi-VN                            │
│                                                                 │
│  2. CookieRequestCultureProvider                                │
│     Cookie: .AspNetCore.Culture=c=vi-VN\|uic=vi-VN             │
│                                                                 │
│  3. AcceptLanguageHeaderRequestCultureProvider                  │
│     Accept-Language: vi-VN, en-US;q=0.9                        │
│                                                                 │
│  Custom Provider:                                               │
│  - Route data (/vi/products, /en/products)                      │
│  - Subdomain (vi.example.com, en.example.com)                   │
│  - Custom header (X-Culture: vi-VN)                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Custom Culture Provider

public class RouteDataRequestCultureProvider : RequestCultureProvider
{
    public int RouteDataIndex = 0;

    public override Task<ProviderCultureResult> DetermineProviderCultureResult(
        HttpContext httpContext)
    {
        var culture = httpContext.Request.RouteValues["culture"]?.ToString();
        
        if (string.IsNullOrEmpty(culture))
        {
            return NullProviderCultureResult;
        }

        var result = new ProviderCultureResult(culture);
        return Task.FromResult(result);
    }
}

// Đăng ký
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture("en-US")
    .AddSupportedCultures("en-US", "vi-VN");

// Thêm custom provider vào đầu danh sách
localizationOptions.RequestCultureProviders.Insert(
    0, new RouteDataRequestCultureProvider());

app.UseRequestLocalization(localizationOptions);
// Set culture cookie
app.MapGet("/set-culture/{culture}", (string culture, HttpContext context) =>
{
    context.Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(
            new RequestCulture(culture)));
    
    return Results.Redirect("/");
});

Best Practices

1. Use Shared Resources

// ✅ Good - centralized resources
public class SharedResource { } // Empty class

// Inject shared localizer
public class HomeController : Controller
{
    private readonly IStringLocalizer<SharedResource> _localizer;
    
    public HomeController(IStringLocalizer<SharedResource> localizer)
    {
        _localizer = localizer;
    }
}

2. Fallback Culture

var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture("en-US")
    .AddSupportedCultures("en-US", "vi-VN")
    .AddSupportedUICultures("en-US", "vi-VN")
    .SetFallbackCulture("en-US"); // Fallback nếu không tìm thấy culture

3. Culture in URLs

// Route với culture
app.MapControllerRoute(
    name: "localized",
    pattern: "{culture=en-US}/{controller=Home}/{action=Index}");

// URLs:
// /en-US/Home/Index
// /vi-VN/Home/Index