Thực tế khắc nghiệt khi tiến ra toàn cầu
Hardcode các chuỗi giao diện (UI) bằng một ngôn ngữ duy nhất có thể ổn với các dự án nhỏ. Nhưng ngay khi ứng dụng của bạn thu hút người dùng tại các thị trường như Nhật Bản hay Đức, cột name cứng nhắc đó sẽ trở thành nút thắt cổ chai. Quốc tế hóa (i18n) không chỉ là dịch văn bản; đó là việc thiết kế kiến trúc dữ liệu sao cho hệ thống không bị lỗi khi đạt mốc 100.000 người dùng trên khắp năm múi giờ.
Tôi từng quản lý việc di chuyển dữ liệu cho các nền tảng chuyển từ MySQL sang PostgreSQL chuyên biệt để xử lý nội dung bản địa hóa hiệu quả hơn. Trong một trường hợp, một schema thiết kế kém đã làm tăng gấp ba lần độ trễ truy vấn (query latency) ngay khi chúng tôi thêm ngôn ngữ thứ tư. Việc chọn đúng cấu trúc ngay từ đầu giúp ngăn chặn cơn ác mộng refactor một bảng production có 5 triệu dòng trong khi vẫn phải duy trì uptime 99,9%.
Hướng dẫn này sẽ bỏ qua những chi tiết thừa để chỉ cho bạn ba chiến lược đã được kiểm chứng thực tế để xử lý dữ liệu đa ngôn ngữ ở quy mô lớn.
Thiết lập môi trường thử nghiệm
Bạn sẽ cần một instance PostgreSQL hoặc MySQL đang hoạt động để thử nghiệm các pattern này. Mặc dù logic cốt lõi áp dụng cho cả hai, PostgreSQL cung cấp các công cụ chuyên dụng như JSONB giúp nó có lợi thế hơn một chút trong việc xử lý bản địa hóa phi cấu trúc.
Hãy khởi tạo một môi trường demo. Chạy các lệnh sau trong terminal của bạn để bắt đầu:
-- Cho PostgreSQL
CREATE DATABASE i18n_lab;
\c i18n_lab;
-- Cho MySQL
CREATE DATABASE i18n_lab;
USE i18n_lab;
Chúng ta sẽ mô phỏng một bảng “Sản phẩm” (Products) của thương mại điện tử. Mỗi mặt hàng cần có tên và mô tả bằng tiếng Anh, tiếng Việt và tiếng Pháp.
Lựa chọn kiến trúc của bạn
Không có một bản thiết kế vạn năng nào cả. Phương pháp “tốt nhất” phụ thuộc vào việc bạn đang hỗ trợ hai hay hai mươi ngôn ngữ.
1. Phương pháp Cột Tĩnh
Đây là phương pháp “Nhanh và Gọn”. Bạn chỉ cần thêm một cột mới cho mỗi ngôn ngữ trực tiếp vào bảng chính.
CREATE TABLE products_simple (
id SERIAL PRIMARY KEY,
sku VARCHAR(50) UNIQUE,
name_en TEXT,
name_vi TEXT,
name_fr TEXT
);
Khi nào nên dùng: Nếu yêu cầu của bạn đã cố định và bạn chỉ hỗ trợ 2 hoặc 3 ngôn ngữ, cách này cực kỳ nhanh. Việc đọc dữ liệu không yêu cầu JOIN. Dữ liệu được truy xuất trực tiếp.
Rủi ro: Việc thêm một ngôn ngữ mới yêu cầu lệnh ALTER TABLE. Trên một bảng có 10 triệu dòng, việc này có thể khóa cơ sở dữ liệu của bạn trong nhiều phút. Nó cũng buộc các lập trình viên backend phải viết logic điều kiện lộn xộn chỉ để chọn đúng tên cột.
2. Bảng Dịch (Tiêu chuẩn Ngành)
Đây là cách tiếp cận quan hệ cổ điển để giải quyết vấn đề. Bạn tách biệt dữ liệu tĩnh (như giá cả hoặc SKU) khỏi nội dung được dịch.
-- Dữ liệu sản phẩm cốt lõi
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price DECIMAL(10, 2),
created_at TIMESTAMP DEFAULT NOW()
);
-- Lưu trữ bản dịch
CREATE TABLE product_translations (
id SERIAL PRIMARY KEY,
product_id INT REFERENCES products(id) ON DELETE CASCADE,
lang_code VARCHAR(5), -- 'en', 'vi', 'ja'
name TEXT,
description TEXT,
UNIQUE(product_id, lang_code)
);
Khi nào nên dùng: Đây là lựa chọn linh hoạt nhất cho người dùng MySQL. Bạn có thể thêm 50 ngôn ngữ vào ngày mai mà không cần chạm vào schema. Nó giữ cho bảng chính của bạn gọn nhẹ và ngăn nắp.
Đánh đổi: Bạn phải trả “thuế JOIN”. Việc lấy danh sách 100 sản phẩm yêu cầu join 100 dòng bản dịch. Nếu không có composite index trên (product_id, lang_code), hiệu suất truy vấn của bạn sẽ giảm sút khi bảng phình to.
3. Sức mạnh JSONB (Chỉ dành cho PostgreSQL)
Nếu bạn đang dùng PostgreSQL, đây thường là lựa chọn ưu việt. Bạn lưu trữ tất cả bản dịch trong một JSON nhị phân duy nhất.
CREATE TABLE products_json (
id SERIAL PRIMARY KEY,
sku VARCHAR(50),
translations JSONB
);
Chèn dữ liệu rất sạch sẽ và dễ đọc:
INSERT INTO products_json (sku, translations) VALUES
('MACBOOK-PRO', '{"en": "MacBook Pro", "vi": "Máy tính MacBook Pro"}');
Khi nào nên dùng: Hãy sử dụng cách này khi bạn muốn tốc độ của một bảng duy nhất nhưng có sự linh hoạt của việc tách bảng. Nó hoàn hảo cho việc làm prototype nhanh và các môi trường có tần suất đọc cao.
Tối ưu hiệu suất & Kiểm tra
Một chiến lược chỉ tốt khi nó được thực hiện hiệu quả. Hãy xem cách đảm bảo các truy vấn này luôn nhanh dưới tải trọng lớn.
Tối ưu hóa Bảng Dịch
Để lấy một sản phẩm bằng tiếng Việt, bạn sẽ sử dụng một join tiêu chuẩn. Để giữ thời gian phản hồi dưới 10ms, bạn phải đánh index cho các foreign key:
SELECT p.id, t.name, p.price
FROM products p
JOIN product_translations t ON p.id = t.product_id
WHERE t.lang_code = 'vi';
Chạy EXPLAIN ANALYZE trên truy vấn này. Nếu bạn thấy “Sequential Scan” trên một bảng lớn, nghĩa là các index của bạn không hoạt động. Hãy thêm một composite index để khắc phục.
Đánh Index JSONB để tăng tốc
Đừng để định dạng JSON đánh lừa; nó có thể được đánh index. Trong PostgreSQL, bạn có thể sử dụng GIN index để giúp việc tra cứu nhanh gần như các cột thông thường:
CREATE INDEX idx_products_json_translations ON products_json USING GIN (translations);
Sau đó, bạn có thể truy vấn các key cụ thể một cách hiệu quả:
SELECT id, translations->>'en' as title
FROM products_json
WHERE translations ? 'en';
Xác định các nội dung còn thiếu
Một trong những vấn đề đau đầu nhất trong i18n là thiếu bản dịch. Bạn có thể dễ dàng kiểm tra cơ sở dữ liệu để tìm các bản dịch tiếng Pháp còn thiếu bằng cách sử dụng LEFT JOIN:
SELECT p.id, p.sku
FROM products p
LEFT JOIN product_translations t ON p.id = t.product_id AND t.lang_code = 'fr'
WHERE t.name IS NULL;
Cách tiếp cận Bảng Dịch giúp việc kiểm tra này trở nên đơn giản. Trong khi JSONB giúp phát triển nhanh hơn, nó đòi hỏi nhiều logic kiểm tra (validation) hơn trong code ứng dụng để đảm bảo các key tồn tại. Hãy chọn con đường phù hợp với quy mô đội ngũ của bạn—và thế mạnh của cơ sở dữ liệu bạn đang dùng.

