Trong Day 5, chúng ta đã tìm hiểu cách sử dụng Language Variants trong Umbraco 13 để quản lý bài viết Lý Tự Nghiệp (李嗣業) bằng tiếng Việt và tiếng Anh.
Với nội dung CMS, Umbraco đã cung cấp sẵn cơ chế quản lý phiên bản ngôn ngữ. Tuy nhiên, trong một ứng dụng thực tế, chúng ta còn có nhiều dữ liệu nghiệp vụ khác như danh mục sản phẩm, tên sản phẩm, mô tả, cấu hình hoặc thông tin do backend quản lý.
Ví dụ, Bookstore có thể cần các danh mục sau:
- Lịch sử — History.
- Triết học — Philosophy.
Thay vì tạo hai danh mục riêng biệt cho hai ngôn ngữ, chúng ta muốn lưu một danh mục duy nhất và quản lý tên của nó theo từng ngôn ngữ.
Trong bài này, chúng ta sẽ thiết kế một mô hình database đa ngôn ngữ đơn giản bằng SQL Server và ASP.NET Core Web API. Mô hình có bảng Languages để quản lý ngôn ngữ, bảng Categories để lưu dữ liệu nghiệp vụ và bảng CategoryTranslations để lưu nội dung đã dịch.
Chúng ta cũng sẽ bổ sung trạng thái enable/disable cho ngôn ngữ, xây dựng API trả về danh mục theo ngôn ngữ được yêu cầu và xử lý trường hợp ngôn ngữ không được kích hoạt.
Mục tiêu sau bài học:
- Hiểu cách tách dữ liệu nghiệp vụ và dữ liệu dịch thuật.
- Thiết kế các bảng Languages, Categories và CategoryTranslations.
- Sử dụng khóa chính, khóa ngoại và unique constraint để bảo đảm tính toàn vẹn dữ liệu.
- Phân biệt ngôn ngữ mặc định, ngôn ngữ được bật và ngôn ngữ bị tắt.
- Viết truy vấn SQL lấy danh mục theo ngôn ngữ.
- Xây dựng API bằng ASP.NET Core để trả về dữ liệu đã dịch.
1. Khi nào cần thiết kế bảng translation riêng?
Trước tiên, cần phân biệt hai loại dữ liệu.
1.1. Dữ liệu nội dung CMS
Trong Day 5, bài viết Lý Tự Nghiệp được quản lý trong Umbraco Backoffice. Một content item có thể có các phiên bản theo culture, chẳng hạn vi-VN và en-US.
Đối với trường hợp này, chúng ta nên ưu tiên sử dụng Language Variants có sẵn của Umbraco thay vì tự xây dựng cơ chế dịch thuật riêng.
1.2. Dữ liệu nghiệp vụ
Giả sử backend cần quản lý các danh mục sách bằng một bảng SQL Server. Mỗi danh mục có mã định danh ổn định, trạng thái hoạt động và có thể có nhiều tên theo ngôn ngữ.
Chúng ta có thể sử dụng mô hình translation riêng để lưu các giá trị được dịch.
Ví dụ:
| CategoryId | Language | Name |
|---|---|---|
| 1 | vi-VN | Lịch sử |
| 1 | en-US | History |
| 2 | vi-VN | Triết học |
| 2 | en-US | Philosophy |
Nhìn vào bảng trên, chúng ta nhận thấy mỗi danh mục chỉ có một ID, nhưng có thể có nhiều bản dịch.
Đây chính là ý tưởng cốt lõi của mô hình translation: dữ liệu nghiệp vụ được lưu một lần, còn dữ liệu phụ thuộc ngôn ngữ được lưu riêng theo từng culture.
2. Thiết kế database
Trong bài thực hành này, chúng ta sử dụng SQL Server. Mô hình gồm ba bảng:
Languages: danh sách ngôn ngữ được hệ thống hỗ trợ.Categories: thông tin nghiệp vụ của danh mục.CategoryTranslations: tên và mô tả của danh mục theo từng ngôn ngữ.
2.1. Sơ đồ quan hệ
Quan hệ giữa các bảng như sau:
- Một ngôn ngữ có thể được sử dụng trong nhiều bản dịch.
- Một danh mục có thể có nhiều bản dịch.
- Mỗi cặp
CategoryIdvàLanguageIdchỉ có tối đa một bản dịch.
Chúng ta sẽ dùng khóa chính kép trên bảng CategoryTranslations để bảo đảm mỗi danh mục không có hai bản dịch trùng nhau cho cùng một ngôn ngữ.
3. Tạo bảng Languages
Bảng Languages quản lý các ngôn ngữ mà ứng dụng có thể sử dụng.
Chúng ta cần các trường sau:
| Trường | Ý nghĩa |
|---|---|
Id |
Khóa chính của ngôn ngữ. |
Code |
Mã culture, ví dụ vi-VN hoặc en-US. |
Name |
Tên ngôn ngữ hiển thị. |
IsEnabled |
Cho biết ngôn ngữ có được phép sử dụng hay không. |
IsDefault |
Cho biết đây có phải ngôn ngữ mặc định của ứng dụng hay không. |
SQL tạo bảng:
CREATE TABLE Languages
(
Id INT IDENTITY(1, 1) PRIMARY KEY,
Code NVARCHAR(10) NOT NULL,
Name NVARCHAR(100) NOT NULL,
IsEnabled BIT NOT NULL DEFAULT 1,
IsDefault BIT NOT NULL DEFAULT 0,
CONSTRAINT UQ_Languages_Code UNIQUE (Code)
);
Tiếp theo, thêm hai ngôn ngữ:
INSERT INTO Languages
(Code, Name, IsEnabled, IsDefault)
VALUES
(N'vi-VN', N'Tiếng Việt', 1, 1),
(N'en-US', N'English', 1, 0);
Trong dữ liệu ban đầu:
- Tiếng Việt được kích hoạt và là ngôn ngữ mặc định.
- Tiếng Anh được kích hoạt nhưng không phải ngôn ngữ mặc định.
Lưu ý: IsEnabled và IsDefault có ý nghĩa khác nhau. Ngôn ngữ mặc định không nhất thiết là ngôn ngữ duy nhất được sử dụng. Một ứng dụng có thể hỗ trợ nhiều ngôn ngữ nhưng chỉ nên có một ngôn ngữ mặc định.
Trong thiết kế thực tế, cần bảo đảm quy tắc chỉ có một ngôn ngữ mặc định bằng validation ở tầng ứng dụng hoặc cơ chế phù hợp với database. Hai cột trên tự chúng không ngăn được việc có nhiều dòng cùng đặt IsDefault = 1.
4. Tạo bảng Categories
Bảng Categories lưu thông tin chung của danh mục. Những thông tin không phụ thuộc ngôn ngữ nên được đặt tại đây.
Ví dụ:
- Mã danh mục.
- Trạng thái hoạt động.
- ID dùng để liên kết với dữ liệu khác.
SQL tạo bảng:
CREATE TABLE Categories
(
Id INT IDENTITY(1, 1) PRIMARY KEY,
Code NVARCHAR(50) NOT NULL,
IsActive BIT NOT NULL DEFAULT 1,
CONSTRAINT UQ_Categories_Code UNIQUE (Code)
);
Ở đây, chúng ta không lưu tên danh mục trong bảng Categories. Lý do là tên có thể khác nhau theo ngôn ngữ.
Ví dụ, danh mục có mã HISTORY vẫn là cùng một danh mục dù tên hiển thị là Lịch sử hay History.
5. Tạo bảng CategoryTranslations
Bảng CategoryTranslations lưu những trường cần dịch của danh mục.
SQL tạo bảng:
CREATE TABLE CategoryTranslations
(
CategoryId INT NOT NULL,
LanguageId INT NOT NULL,
Name NVARCHAR(200) NOT NULL,
Description NVARCHAR(1000) NULL,
CONSTRAINT PK_CategoryTranslations
PRIMARY KEY (CategoryId, LanguageId),
CONSTRAINT FK_CategoryTranslations_Categories
FOREIGN KEY (CategoryId)
REFERENCES Categories(Id),
CONSTRAINT FK_CategoryTranslations_Languages
FOREIGN KEY (LanguageId)
REFERENCES Languages(Id)
);
Khóa chính kép:
PRIMARY KEY (CategoryId, LanguageId)
có nghĩa là một danh mục không thể có hai bản dịch cho cùng một ngôn ngữ.
Ví dụ, cặp (1, 1) chỉ có thể xuất hiện một lần. Nếu danh mục có ID 1 đã có bản dịch tiếng Việt, chúng ta không thể thêm một bản dịch tiếng Việt thứ hai cho cùng danh mục đó.
Khóa ngoại cũng bảo đảm rằng mỗi bản dịch phải tham chiếu đến một danh mục và một ngôn ngữ tồn tại.
6. Thêm dữ liệu mẫu
Sau khi tạo bảng, chúng ta thêm hai danh mục Lịch sử và Triết học.
INSERT INTO Categories (Code, IsActive)
VALUES
(N'HISTORY', 1),
(N'PHILOSOPHY', 1);
Tiếp theo, thêm các bản dịch. Giả sử ID ngôn ngữ và ID danh mục được tạo theo thứ tự trong ví dụ này:
INSERT INTO CategoryTranslations
(CategoryId, LanguageId, Name, Description)
VALUES
(1, 1, N'Lịch sử', N'Sách và bài viết về lịch sử'),
(1, 2, N'History', N'Books and articles about history'),
(2, 1, N'Triết học', N'Sách và bài viết về triết học'),
(2, 2, N'Philosophy', N'Books and articles about philosophy');
Trong môi trường thực tế, không nên giả định ID luôn bằng 1 hoặc 2. Hãy truy vấn bảng Languages và Categories để lấy ID thật, hoặc sử dụng cơ chế liên kết dữ liệu phù hợp trong ứng dụng.
Để xem các bản dịch đã lưu, chạy:
SELECT
c.Id AS CategoryId,
c.Code,
l.Code AS LanguageCode,
t.Name,
t.Description
FROM Categories c
INNER JOIN CategoryTranslations t
ON t.CategoryId = c.Id
INNER JOIN Languages l
ON l.Id = t.LanguageId
ORDER BY c.Id, l.Code;
Kết quả sẽ cho thấy mỗi danh mục có hai bản dịch tương ứng với hai ngôn ngữ.
7. Truy vấn danh mục theo ngôn ngữ
Giả sử frontend yêu cầu danh sách danh mục bằng tiếng Anh. Backend cần tìm những bản dịch thuộc culture en-US.
Truy vấn SQL:
DECLARE @LanguageCode NVARCHAR(10) = N'en-US';
SELECT
c.Id,
c.Code,
t.Name,
t.Description
FROM Categories c
INNER JOIN CategoryTranslations t
ON t.CategoryId = c.Id
INNER JOIN Languages l
ON l.Id = t.LanguageId
WHERE
l.Code = @LanguageCode
AND l.IsEnabled = 1
AND c.IsActive = 1
ORDER BY t.Name;
Nếu đổi biến thành vi-VN, truy vấn sẽ trả về Lịch sử và Triết học thay vì History và Philosophy.
Chúng ta cần chú ý ba điều kiện:
l.Code = @LanguageCode: chọn đúng ngôn ngữ mà client yêu cầu.l.IsEnabled = 1: chỉ sử dụng ngôn ngữ đang được bật.c.IsActive = 1: chỉ trả về danh mục đang hoạt động.
Việc dùng tham số SQL giúp tránh ghép trực tiếp dữ liệu người dùng vào câu truy vấn. Khi triển khai bằng Entity Framework Core, chúng ta có thể biểu diễn truy vấn này bằng LINQ.
8. Xử lý trạng thái enable/disable của ngôn ngữ
Giả sử sau một thời gian, website tạm thời ngừng cung cấp tiếng Anh. Thay vì xóa dữ liệu tiếng Anh, chúng ta đặt IsEnabled = 0.
UPDATE Languages
SET IsEnabled = 0
WHERE Code = N'en-US';
Khi truy vấn danh mục bằng tiếng Anh, điều kiện l.IsEnabled = 1 sẽ khiến API không trả về bản dịch tiếng Anh.
Điều quan trọng là phải phân biệt ba khái niệm:
| Trạng thái | Ý nghĩa | Xử lý đề xuất |
|---|---|---|
| Enabled | Ngôn ngữ được phép sử dụng | Cho phép truy vấn bản dịch |
| Disabled | Ngôn ngữ tạm thời không được cung cấp | Không phục vụ qua API công khai |
| Missing translation | Ngôn ngữ đang bật nhưng chưa có bản dịch cho một danh mục | Áp dụng chính sách fallback hoặc trả lỗi phù hợp |
Disabled không đồng nghĩa với việc xóa bản dịch. Bằng cách giữ lại dữ liệu, chúng ta có thể bật lại ngôn ngữ trong tương lai mà không cần dịch lại toàn bộ nội dung.
Ngoài ra, nếu tiếng Anh đang là ngôn ngữ mặc định thì không nên cho phép tắt nó mà chưa chuyển ngôn ngữ mặc định sang một ngôn ngữ đang bật khác.
9. Xử lý trường hợp thiếu bản dịch
Một vấn đề thường gặp là ngôn ngữ đã được bật nhưng một số danh mục chưa có bản dịch.
Ví dụ, danh mục mới được tạo và chỉ có tên tiếng Việt. Khi client yêu cầu tiếng Anh, truy vấn INNER JOIN ở phần trước sẽ không trả về danh mục đó.
Có hai cách xử lý phổ biến.
9.1. Chỉ trả về dữ liệu có bản dịch
Đây là cách đơn giản nhất. API chỉ trả về danh mục có bản dịch tương ứng với ngôn ngữ được yêu cầu.
Ưu điểm là dữ liệu luôn đúng ngôn ngữ, không vô tình hiển thị nội dung tiếng Việt trong giao diện tiếng Anh.
9.2. Fallback về ngôn ngữ mặc định
Nếu nghiệp vụ yêu cầu luôn hiển thị danh mục, backend có thể dùng bản dịch của ngôn ngữ mặc định khi không tìm thấy bản dịch được yêu cầu.
Ví dụ, nếu danh mục chưa có tiếng Anh, API có thể hiển thị tên tiếng Việt thay thế.
Tuy nhiên, fallback phải được thiết kế rõ ràng. Không nên để việc thay thế ngôn ngữ xảy ra một cách ngẫu nhiên trong truy vấn.
Trong bài thực hành này, chúng ta chọn chính sách đơn giản: chỉ trả về bản dịch đã tồn tại trong ngôn ngữ được yêu cầu. Phần fallback có thể được bổ sung trong bài nâng cao sau này.
10. Xây dựng Custom API bằng ASP.NET Core Controller và EF Core
Để quản lý các bảng database custom (Languages, Categories, CategoryTranslations) song song với Umbraco 13, chúng ta sẽ sử dụng Entity Framework Core (EF Core) để tương tác với database và xây dựng API bằng ASP.NET Core ApiController. Cách tiếp cận bằng Controller giúp API tự động tương thích 100% với tài liệu Swagger mặc định của Umbraco mà không cần thêm cấu hình phức tạp.
10.1. Cài đặt các gói NuGet cần thiết
Mở Package Manager Console trong Visual Studio hoặc chạy các lệnh CLI sau tại thư mục root của dự án Umbraco:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore.SqlServer: Nhà cung cấp dữ liệu SQL Server cho EF Core.Microsoft.EntityFrameworkCore.Tools: Hỗ trợ chạy các lệnh Migration (Add-Migration,Update-Database).
10.2. Tổ chức cấu trúc thư mục dự án
Toàn bộ code custom của backend nên được gom gọn vào thư mục Custom/ ngay tại root của dự án Umbraco 13. Điều này giúp phân định rõ ràng giữa code hệ thống CMS và code nghiệp vụ tự phát triển.
MyUmbracoProject/ <-- Root dự án Umbraco 13 (.NET 8)
├── Custom/ <-- Thư mục chứa toàn bộ Custom Backend Code
│ ├── Data/ <-- Chứa DbContext & EF Core Migrations
│ │ ├── AppDbContext.cs
│ │ └── Migrations/
│ ├── Entities/ <-- Chứa các Class đại diện cho Bảng Database
│ │ ├── Language.cs
│ │ ├── Category.cs
│ │ └── CategoryTranslation.cs
│ ├── Dtos/ <-- Chứa Data Transfer Objects (Response API)
│ │ └── CategoryDto.cs
│ └── Controllers/ <-- Chứa các ASP.NET Core ApiControllers
│ └── CategoriesController.cs
├── wwwroot/
├── appsettings.json <-- Chuỗi kết nối Database (umbracoDbDSN)
├── Program.cs <-- Đăng ký DbContext và Umbraco Pipeline
└── MyUmbracoProject.csproj
10.3. Tạo các Class Entities (Custom/Entities/)
Tạo các class C# tương ứng với các bảng database đã thiết kế trong thư mục Custom/Entities/.
Language.cs:
namespace MyUmbracoSite.Custom.Entities;
public class Language
{
public int Id { get; set; }
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool IsEnabled { get; set; }
public bool IsDefault { get; set; }
public ICollection<CategoryTranslation> Translations { get; set; } = new List<CategoryTranslation>();
}
Category.cs:
namespace MyUmbracoSite.Custom.Entities;
public class Category
{
public int Id { get; set; }
public string Code { get; set; } = string.Empty;
public bool IsActive { get; set; }
public ICollection<CategoryTranslation> Translations { get; set; } = new List<CategoryTranslation>();
}
CategoryTranslation.cs:
namespace MyUmbracoSite.Custom.Entities;
public class CategoryTranslation
{
public int CategoryId { get; set; }
public int LanguageId { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public Category Category { get; set; } = null!;
public Language Language { get; set; } = null!;
}
10.4. Tạo DbContext (Custom/Data/AppDbContext.cs)
Tạo class AppDbContext.cs trong thư mục Custom/Data/ để cấu hình Fluent API, thiết lập khóa chính kép và các ràng buộc quan hệ:
using Microsoft.EntityFrameworkCore;
using MyUmbracoSite.Custom.Entities;
namespace MyUmbracoSite.Custom.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Language> Languages => Set<Language>();
public DbSet<Category> Categories => Set<Category>();
public DbSet<CategoryTranslation> CategoryTranslations => Set<CategoryTranslation>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Language>()
.HasIndex(x => x.Code)
.IsUnique();
modelBuilder.Entity<Category>()
.HasIndex(x => x.Code)
.IsUnique();
// Khóa chính kép cho bảng Translation
modelBuilder.Entity<CategoryTranslation>()
.HasKey(x => new { x.CategoryId, x.LanguageId });
modelBuilder.Entity<CategoryTranslation>()
.HasOne(x => x.Category)
.WithMany(x => x.Translations)
.HasForeignKey(x => x.CategoryId);
modelBuilder.Entity<CategoryTranslation>()
.HasOne(x => x.Language)
.WithMany(x => x.Translations)
.HasForeignKey(x => x.LanguageId);
}
}
10.5. Tạo DTO cho API Response (Custom/Dtos/CategoryDto.cs)
Tạo DTO trong Custom/Dtos/ để định nghĩa cấu trúc dữ liệu JSON phản hồi ra Frontend:
namespace MyUmbracoSite.Custom.Dtos;
public record CategoryDto(
int Id,
string Code,
string Name,
string? Description
);
10.6. Viết ApiController (Custom/Controllers/CategoriesController.cs)
Tạo class Controller kế thừa từ ControllerBase trong thư mục Custom/Controllers/. Sử dụng [ApiExplorerSettings(GroupName = "Categories")] để giúp Swagger của Umbraco hiển thị nhóm API rõ ràng:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MyUmbracoSite.Custom.Data;
using MyUmbracoSite.Custom.Dtos;
namespace MyUmbracoSite.Custom.Controllers;
[ApiController]
[Route("api/categories")]
[ApiExplorerSettings(GroupName = "Categories")] // Giúp hiện rõ nhóm API trong Swagger
public class CategoriesController : ControllerBase
{
private readonly AppDbContext _db;
public CategoriesController(AppDbContext db)
{
_db = db;
}
[HttpGet]
[ProducesResponseType(typeof(List<CategoryDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> GetCategories(
[FromQuery] string? lang,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(lang))
{
lang = "vi-VN";
}
var language = await _db.Languages
.AsNoTracking()
.SingleOrDefaultAsync(x => x.Code == lang, cancellationToken);
if (language is null)
{
return NotFound(new { message = "Language not found." });
}
if (!language.IsEnabled)
{
return Problem(
statusCode: StatusCodes.Status409Conflict,
title: "Language is disabled.");
}
var categories = await _db.CategoryTranslations
.AsNoTracking()
.Where(x => x.LanguageId == language.Id && x.Category.IsActive)
.OrderBy(x => x.Name)
.Select(x => new CategoryDto(
x.CategoryId,
x.Category.Code,
x.Name,
x.Description))
.ToListAsync(cancellationToken);
return Ok(categories);
}
}
10.7. Đăng ký EF Core trong Program.cs
Cuối cùng, mở file Program.cs tại root dự án Umbraco để đăng ký EndpointsApiExplorer và AppDbContext:
using Microsoft.EntityFrameworkCore;
using MyUmbracoSite.Custom.Data;
using MyUmbracoSite.Middlewares;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// 1. Cấu hình Umbraco Services
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddComposers()
.Build();
// 2. Đăng ký EndpointsApiExplorer để Swagger nhận diện API
builder.Services.AddEndpointsApiExplorer();
// 3. Cấu hình EF Core DbContext (Dùng chung ConnectionString với Umbraco)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("umbracoDbDSN")));
WebApplication app = builder.Build();
// 4. Khởi chạy Umbraco Pipeline
await app.BootUmbracoAsync();
app.UseUmbraco()
.WithMiddleware(u =>
{
// Gọi Custom Debug Middleware
u.AppBuilder.UseUmbracoDebug();
// Kích hoạt các Middleware mặc định của Umbraco
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
await app.RunAsync();
11. Kiểm thử API
Sau khi chạy ASP.NET Core API, hãy thử các request sau bằng Swagger, Postman hoặc curl.
11.1. Lấy danh mục bằng tiếng Việt
GET /api/categories?lang=vi-VN
Kết quả mong đợi gồm Lịch sử và Triết học.
11.2. Lấy danh mục bằng tiếng Anh
GET /api/categories?lang=en-US
Kết quả mong đợi gồm History và Philosophy.
11.3. Yêu cầu một ngôn ngữ không tồn tại
GET /api/categories?lang=fr-FR
Nếu fr-FR chưa được khai báo trong bảng Languages, API trả về 404 theo logic của ví dụ.
11.4. Tắt tiếng Anh
Chạy SQL:
UPDATE Languages
SET IsEnabled = 0
WHERE Code = N'en-US';
Gửi lại request:
GET /api/categories?lang=en-US
API sẽ trả về 409 theo quy ước đã chọn trong ví dụ.
Sau khi kiểm thử xong, có thể bật lại ngôn ngữ:
UPDATE Languages
SET IsEnabled = 1
WHERE Code = N'en-US';
Hãy xác nhận rằng các bản dịch tiếng Anh vẫn còn nguyên vẹn. Đây là một trong những lợi ích của việc dùng trạng thái enable/disable thay vì xóa dữ liệu ngôn ngữ.
12. Một số nguyên tắc thiết kế cần nhớ
12.1. Không đưa mọi trường vào bảng translation
Chỉ những trường phụ thuộc ngôn ngữ mới nên nằm trong bảng translation.
Ví dụ:
Category.Code: dữ liệu dùng chung.Category.IsActive: trạng thái nghiệp vụ dùng chung.CategoryTranslation.Name: dữ liệu cần dịch.CategoryTranslation.Description: dữ liệu cần dịch.
Nếu đặt tất cả trường vào bảng translation, chúng ta có thể vô tình tạo nhiều bản sao của cùng một dữ liệu nghiệp vụ và làm cho việc cập nhật trở nên khó kiểm soát.
12.2. Phân biệt trạng thái ngôn ngữ và trạng thái bản dịch
Languages.IsEnabled cho biết ngôn ngữ có được cung cấp hay không. Nó không cho biết mọi danh mục đã được dịch đầy đủ.
Nếu cần theo dõi tiến độ dịch thuật, bạn có thể bổ sung các trường như TranslationStatus hoặc ReviewedAt vào bảng translation.
Không nên dùng IsEnabled để đại diện cho trạng thái dịch thuật vì hai khái niệm này giải quyết hai vấn đề khác nhau.
12.3. Không xóa ngôn ngữ chỉ vì tạm thời không sử dụng
Việc xóa ngôn ngữ có thể làm ảnh hưởng đến dữ liệu translation đang tham chiếu đến ngôn ngữ đó.
Với hệ thống có dữ liệu lịch sử, thường nên giữ lại ngôn ngữ và chuyển trạng thái sang disabled. Nếu thực sự cần xóa, phải có chính sách xử lý các bản dịch liên quan và kiểm tra ràng buộc khóa ngoại.
12.4. Không nhầm lẫn với Language Variants của Umbraco
Trong Day 5, chúng ta sử dụng Language Variants để quản lý bài viết CMS. Trong Day 6, chúng ta thiết kế bảng translation cho dữ liệu nghiệp vụ do ứng dụng quản lý.
Hai cơ chế này có thể cùng tồn tại trong một hệ thống nhưng không nên được sử dụng thay thế cho nhau một cách tùy tiện.
13. Tổng kết Day 6
Qua bài thực hành này, chúng ta đã xây dựng một mô hình đa ngôn ngữ đơn giản cho backend Bookstore.
Những kiến thức quan trọng cần ghi nhớ:
- Bảng Languages quản lý mã culture, tên ngôn ngữ và trạng thái enable/disable.
- Bảng nghiệp vụ lưu những dữ liệu dùng chung, chẳng hạn mã danh mục và trạng thái hoạt động.
- Bảng translation lưu các trường cần dịch theo từng ngôn ngữ.
- Khóa ngoại và khóa chính kép giúp bảo đảm tính toàn vẹn dữ liệu.
- API cần kiểm tra ngôn ngữ tồn tại và trạng thái kích hoạt trước khi truy vấn bản dịch.
- Chính sách fallback khi thiếu bản dịch cần được thiết kế rõ ràng.
- Language Variants của Umbraco vẫn là lựa chọn phù hợp cho nội dung CMS.
Điểm quan trọng nhất là thiết kế dữ liệu dựa trên bản chất của nghiệp vụ: dữ liệu nào dùng chung thì lưu một lần, dữ liệu nào thay đổi theo ngôn ngữ thì lưu trong bảng translation.
14. Bài tiếp theo: Day 7 – Hoàn thiện API đa ngôn ngữ
Sau khi đã có database và endpoint cơ bản, bước tiếp theo là hoàn thiện API để có thể sử dụng trong ứng dụng thực tế.
Trong Day 7, chúng ta có thể mở rộng project với các chức năng:
- Tách controller hoặc endpoint thành các lớp phù hợp.
- Đưa logic truy vấn vào service hoặc repository khi cần thiết.
- Quản lý ngôn ngữ mặc định trong một dịch vụ dùng chung.
- Thiết kế chính sách fallback và chuẩn hóa response lỗi.
- Viết integration test cho các trường hợp language enabled, disabled và missing translation.
- Tích hợp API với ứng dụng frontend sau khi backend hoạt động ổn định.
Đây sẽ là bước chuyển từ một ví dụ database đơn giản sang một API đa ngôn ngữ có cấu trúc rõ ràng và dễ bảo trì hơn.
Nhận xét