Trong bài viết này, chúng ta sẽ tìm hiểu cách triển khai JSON Based Localization trong ASP.NET Core và kết hợp với Caching để làm cho nó hiệu quả hơn nữa. Sau phương pháp nội địa hóa dựa trên RESX thông thường, hướng dẫn này sử dụng các tệp JSON để lưu trữ các chuỗi đã được nội địa hóa và triển khai middleware để chuyển đổi ngôn ngữ thông qua các khóa ngôn ngữ trong tiêu đề yêu cầu (request header).
Bạn có thể tìm thấy toàn bộ mã nguồn của quá trình triển khai này tại đây.
Chúng ta sẽ xây dựng những gì?
Chúng ta sẽ xây dựng một .NET 5.0 WebAPI đơn giản trả về các thông báo dựa trên Accepted Language của tiêu đề yêu cầu. Ở phía sau, chúng ta cũng sẽ lưu trữ chuỗi vào bộ nhớ đệm thông qua IDistributedCache. Mục tiêu chính của việc triển khai này là đọc các chuỗi ngôn ngữ từ tệp JSON thay vì tệp RESX. Đối với điều này, chúng ta sẽ thêm một triển khai mới cho IStringLocalizer. Đây sẽ là một bài viết khá đơn giản và dễ hiểu nhưng lại rất hữu ích trong các ứng dụng sẵn sàng cho sản xuất. Ai mà không muốn sử dụng các tệp JSON chứ? :D
Nó sẽ đơn giản như việc thêm 3 lớp mới và một vài đăng ký dịch vụ. Hãy bắt đầu nào!
Bắt đầu với JSON Based Localization trong ASP.NET Core
Mở IDE yêu thích của bạn (tôi sử dụng Visual Studio 2019 Community) và tạo một Dự án ASP.NET Core Web API mới. Đảm bảo chọn .NET 5.0 Framework (hoặc phiên bản mới nhất tại thời điểm đọc bài viết này. Lưu ý rằng .NET 6 LTS sắp ra mắt!).
Để giữ cho việc triển khai này đơn giản, tôi sẽ không thêm bất kỳ thư viện lớp bổ sung nào. Tôi đã xóa các bộ điều khiển thời tiết và các tệp liên quan khỏi giải pháp WebAPI để làm gọn dự án.
Như đã đề cập trước đó, chúng ta có hai phần cho việc triển khai này:
Một Middleware có thể xác định mã ngôn ngữ được truyền vào tiêu đề yêu cầu bởi máy khách (trong trường hợp của chúng ta là Postman).
Một triển khai của IStringLocalizer để hỗ trợ các tệp JSON. Tôi dự định lưu trữ tệp JSON theo tên miền địa phương (en-US.json) trong thư mục Resources. Lưu ý rằng chúng ta cũng sẽ sử dụng IDistributedCache để làm cho hệ thống của mình hiệu quả hơn.
Hãy tạo một lớp mới và đặt tên là JsonStringLocalizer.cs
public class JsonStringLocalizer : IStringLocalizer{ private readonly IDistributedCache _cache; private readonly JsonSerializer _serializer = new JsonSerializer(); public JsonStringLocalizer(IDistributedCache cache) { _cache = cache; } public LocalizedString this[string name] { get { string value = GetString(name); return new LocalizedString(name, value ?? name, value == null); } } public LocalizedString this[string name, params object[] arguments] { get { var actualValue = this[name]; return !actualValue.ResourceNotFound ? new LocalizedString(name, string.Format(actualValue.Value, arguments), false) : actualValue; } } public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) { string filePath = $"Resources/{Thread.CurrentThread.CurrentCulture.Name}.json"; using (var str = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var sReader = new StreamReader(str)) using (var reader = new JsonTextReader(sReader)) { while (reader.Read()) { if (reader.TokenType != JsonToken.PropertyName) continue; string key = (string)reader.Value; reader.Read(); string value = _serializer.Deserialize<string>(reader); yield return new LocalizedString(key, value, false); } } } private string GetString(string key) { string relativeFilePath = $"Resources/{Thread.CurrentThread.CurrentCulture.Name}.json"; string fullFilePath = Path.GetFullPath(relativeFilePath); if (File.Exists(fullFilePath)) { string cacheKey = $"locale_{Thread.CurrentThread.CurrentCulture.Name}_{key}"; string cacheValue = _cache.GetString(cacheKey); if (!string.IsNullOrEmpty(cacheValue)) return cacheValue; string result = GetValueFromJSON(key, Path.GetFullPath(relativeFilePath)); if (!string.IsNullOrEmpty(result)) _cache.SetString(cacheKey, result); return result; } return default(string); } private string GetValueFromJSON(string propertyName, string filePath) { if (propertyName == null) return default; if (filePath == null) return default; using (var str = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var sReader = new StreamReader(str)) using (var reader = new JsonTextReader(sReader)) { while (reader.Read()) { if (reader.TokenType == JsonToken.PropertyName && (string)reader.Value == propertyName) { reader.Read(); return _serializer.Deserialize<string>(reader); } } return default; } }}Lưu ý rằng chúng ta đang triển khai giao diện IStringLocalizer.
Dòng #3: chúng ta sử dụng IDistributedCache ở đây.
Dòng #27 - 44: GetAllStrings() tại đây chúng ta cố gắng đọc tên tệp JSON theo CurrentCulture và trả về một danh sách các đối tượng LocalizedString. Lưu ý rằng danh sách này sẽ chứa cả khóa và giá trị của tất cả các mục trong tệp JSON được tìm thấy. Mỗi giá trị JSON được đọc đều được giải mã.
Dòng #45 - 59: GetString() đây là hàm chịu trách nhiệm nội địa hóa các chuỗi. Ở đây, đường dẫn tệp cũng được xác định theo văn hóa hiện tại của yêu cầu. Nếu tệp tồn tại, một khóa bộ nhớ đệm được tạo với một tên khá độc đáo. Về lý tưởng, hệ thống cố gắng kiểm tra xem có giá trị nào tồn tại trong bộ nhớ đệm cho khóa tương ứng hay không. Nếu tìm thấy giá trị trong bộ nhớ đệm, nó sẽ được trả về. Nếu không, ứng dụng sẽ truy cập tệp JSON và cố gắng lấy và trả về chuỗi được tìm thấy.
Dòng #60 - 78: GetValueFromJSON() như tên gọi, phương thức này chấp nhận tên thuộc tính và đường dẫn tệp của tệp JSON, sau đó được mở ở Chế độ Đọc. Nếu thuộc tính tương ứng được tìm thấy trong tệp JSON, nó sẽ được trả về. Nếu không, một giá trị null sẽ được trả về.
Dòng #9 - 16: this[string name] Đây là phương thức nhập mà chúng ta sẽ sử dụng trong bộ điều khiển của mình. Nó chấp nhận một khóa và cố gắng tìm các giá trị tương ứng từ tệp JSON bằng phương pháp đã giải thích trước đó. Điều quan trọng cần lưu ý là phương thức này sẽ trả về cùng một khóa nếu không tìm thấy giá trị nào trong tệp JSON.
Tiếp theo, hãy thêm một lớp Factory chịu trách nhiệm tạo nội bộ phiên bản JsonStringLocalizer. Đặt tên lớp mới là JsonStringLocalizerFactory.
public class JsonStringLocalizerFactory : IStringLocalizerFactory{ private readonly IDistributedCache _cache; public JsonStringLocalizerFactory(IDistributedCache cache) { _cache = cache; } public IStringLocalizer Create(Type resourceSource) => new JsonStringLocalizer(_cache); public IStringLocalizer Create(string baseName, string location) => new JsonStringLocalizer(_cache);}Tiếp theo là phần thú vị, nơi chúng ta tạo một Middleware có thể đọc khóa Accept-Language từ tiêu đề yêu cầu và đặt ngôn ngữ của luồng hiện tại nếu văn hóa đó hợp lệ.
Tạo một lớp mới và đặt tên là LocalizationMiddleware
public class LocalizationMiddleware : IMiddleware{ public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var cultureKey = context.Request.Headers["Accept-Language"]; if (!string.IsNullOrEmpty(cultureKey)) { if (DoesCultureExist(cultureKey)) { var culture = new System.Globalization.CultureInfo(cultureKey); Thread.CurrentThread.CurrentCulture = culture; Thread.CurrentThread.CurrentUICulture = culture; } } await next(context); } private static bool DoesCultureExist(string cultureName) { return CultureInfo.GetCultures(CultureTypes.AllCultures).Any(culture => string.Equals(culture.Name, cultureName,StringComparison.CurrentCultureIgnoreCase)); }}Dòng #5: Tại đây chúng ta đọc Accept-Language từ header yêu cầu của ngữ cảnh HTTP hiện tại.
Dòng #8-13: Nếu tìm thấy một culture hợp lệ, chúng ta thiết lập culture cho luồng hiện tại.
Sau khi hoàn tất, hãy thêm một số tệp ngôn ngữ. Tạo một thư mục mới tên là Resources và thêm vào 2 tệp JSON mới. Chúng ta sẽ đặt tên các tệp JSON này là en-US.json và de-DE.json.
Đây là mẫu en-US.json
{ "hi": "Hello", "welcome": "Welcome {0}, How are you?"}Tiếp theo, de-DE.json. Tái bút, nội dung sau đây đã được dịch qua Google Translate.
{ "hi": "Hallo", "welcome": "Willkommen {0}, wie geht es dir?"}Như bạn có thể thấy, chúng ta có 2 khóa, hi và welcome, các khóa này sẽ được ứng dụng dịch tùy thuộc vào header yêu cầu.
Bây giờ đến phần quan trọng, nơi chúng ta thực hiện đăng ký dịch vụ cho middleware và JsonLocalizer. Mở Startup.cs và thêm đoạn sau vào dưới phương thức ConfigureServices.
services.AddLocalization();services.AddSingleton<LocalizationMiddleware>();services.AddDistributedMemoryCache();services.AddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();Dưới phương thức Configure, hãy thêm đoạn sau. Lưu ý rằng chúng ta đang xác định en-US là culture mặc định của ứng dụng. Bạn cũng có thể dễ dàng làm cho phần này có thể cấu hình được bằng cách chuyển nó vào appsettings.
var options = new RequestLocalizationOptions{ DefaultRequestCulture = new RequestCulture(new CultureInfo("en-US"))};app.UseRequestLocalization(options);app.UseStaticFiles();app.UseMiddleware<LocalizationMiddleware>();Cuối cùng, hãy tạo một API Controller mới để minh họa cách sử dụng bộ bản địa hóa JSON của chúng ta. Thêm một Controller mới và đặt tên là DemoController.
public class DemoController : ControllerBase{ private readonly ILogger<DemoController> _logger; private readonly IStringLocalizer<DemoController> _loc; public DemoController(ILogger<DemoController> logger, IStringLocalizer<DemoController> loc) { _logger = logger; _loc = loc; } [HttpGet] public IActionResult Get() { _logger.LogInformation(_loc["hi"]); var message = _loc["hi"].ToString(); return Ok(message); } [HttpGet("{name}")] public IActionResult Get(string name) { var message = string.Format(_loc["welcome"],name); return Ok(message); } [HttpGet("all")] public IActionResult GetAll() { var message = _loc.GetAllStrings(); return Ok(message); }}Dòng #3-9: Tiêm phụ thuộc (Constructor Injection) cho các instance ILogger và IStringLocalizer.
Dòng #11-16: Đây là một thử nghiệm đơn giản, nơi chúng ta cố gắng in phiên bản đã bản địa hóa của khóa ‘hi’ ra console cũng như trả về nó dưới dạng phản hồi.
Dòng #17-22: Tại đây, chúng ta truyền một tên ngẫu nhiên vào endpoint này và ứng dụng dự kiến sẽ trả về phiên bản đã bản địa hóa của “Welcome xxxx, how are you?”. Đơn giản vậy thôi.
Dòng #23-28: Phương thức này về lý thuyết sẽ trả về tất cả các khóa và giá trị được tìm thấy trong tệp JSON tương ứng.
Kiểm tra với Postman
Sau khi hoàn tất, hãy khởi chạy Postman và thực hiện một số bài kiểm tra cơ bản.
Đây là những gì bạn nhận được dưới dạng phản hồi khi gửi yêu cầu GET đến endpoint /demo với Accept-Language là de-DE

Accept-Language được đặt thành en-US. Đơn giản phải không?

Bây giờ, tôi thử gửi yêu cầu GET đến endpoint /demo cùng với tên của mình. Accept-Language được đặt thành en-US

Accept-Language được đặt thành de-DE.

Cuối cùng, khi bạn gửi yêu cầu GET đến endpoint /demo/all, bạn sẽ thấy tất cả các cặp khóa/giá trị của các chuỗi trong tệp JSON liên quan đến ngôn ngữ chấp nhận (accept language) tương ứng.

Khá tiện lợi để có trong các dự án của bạn, phải không? Đó là phần kết thúc của bài viết.
Tóm tắt
Trong bài viết này, chúng ta đã tìm hiểu một cách khá đơn giản để đạt được Bản địa hóa dựa trên JSON trong các ứng dụng ASP.NET Core. Hãy chia sẻ bài viết này với đồng nghiệp và cộng đồng lập trình viên của bạn nếu bạn thấy nó thú vị. Bạn có thể tìm thấy mã nguồn của các triển khai đã đề cập tại đây. Cảm ơn!