Bài 32.4: Server-side language detection

Bài 32.4: Server-side language detection
Chào mừng bạn quay trở lại với series Lập trình Web! Trong chặng đường xây dựng các ứng dụng web hiện đại, đặc biệt khi hướng tới người dùng toàn cầu, việc hỗ trợ đa ngôn ngữ (Internationalization - i18n và Localization - l10n) là vô cùng quan trọng. Hôm nay, chúng ta sẽ cùng đi sâu vào một kỹ thuật nền tảng giúp trải nghiệm đa ngôn ngữ của người dùng trở nên mượt mà và hiệu quả hơn: Server-side language detection.
Tại sao Server-side Language Detection lại "Đỉnh Cao"?
Trong thế giới web, có hai cách chính để phát hiện ngôn ngữ ưu tiên của người dùng: Client-side (trên trình duyệt) và Server-side (trên máy chủ).
Client-side detection: Thường dựa vào JavaScript để đọc cài đặt ngôn ngữ của trình duyệt (
navigator.language
). Sau đó, JavaScript sẽ tải nội dung hoặc chuyển hướng người dùng đến phiên bản ngôn ngữ phù hợp.- Ưu điểm: Dễ triển khai với các thư viện front-end.
- Nhược điểm:
- Trải nghiệm ban đầu không tốt: Trang có thể hiển thị bằng ngôn ngữ mặc định trước, rồi đột ngột chuyển sang ngôn ngữ của người dùng khi JavaScript chạy ("Flash of Unstyled Content" - FOUC).
- SEO kém: Các công cụ tìm kiếm có thể khó crawl và index các phiên bản ngôn ngữ khác nhau nếu chúng chỉ được tải sau khi JavaScript chạy.
- Phụ thuộc JavaScript: Sẽ không hoạt động nếu người dùng tắt JavaScript.
Server-side detection: Phát hiện ngôn ngữ ngay khi máy chủ nhận được yêu cầu từ trình duyệt. Dựa vào ngôn ngữ phát hiện được, máy chủ sẽ trả về trực tiếp nội dung HTML bằng ngôn ngữ phù hợp.
- Ưu điểm:
- Tốc độ và Trải nghiệm người dùng tốt hơn: Người dùng nhận được nội dung bằng ngôn ngữ của họ ngay lập tức từ lần tải trang đầu tiên. Không có FOUC hay chờ đợi JavaScript.
- Tối ưu cho SEO: Công cụ tìm kiếm sẽ dễ dàng index các phiên bản ngôn ngữ khác nhau, vì mỗi URL (hoặc phản hồi từ server) đã chứa nội dung ngôn ngữ cụ thể.
- Không phụ thuộc JavaScript: Hoạt động ngay cả khi JavaScript bị tắt.
- Nhược điểm: Cần cấu hình và xử lý ở phía server.
- Ưu điểm:
Rõ ràng, để mang lại trải nghiệm tốt nhất và tối ưu cho công cụ tìm kiếm, server-side language detection là một kỹ thuật không thể thiếu cho các ứng dụng web quốc tế hoá chuyên nghiệp.
Bí Mật Nằm Ở HTTP Header: Accept-Language
Vậy làm thế nào mà máy chủ biết được ngôn ngữ ưu tiên của người dùng? Bí mật nằm trong các thông tin mà trình duyệt gửi kèm mỗi khi thực hiện yêu cầu HTTP tới server. Một trong những thông tin quan trọng đó chính là HTTP Header Accept-Language
.
Header này chứa danh sách các ngôn ngữ mà trình duyệt của người dùng (dựa trên cài đặt hệ điều hành hoặc trình duyệt) mong muốn nhận nội dung, được sắp xếp theo mức độ ưu tiên.
Ví dụ về header Accept-Language
:
Accept-Language: en-US,en;q=0.9,fr;q=0.8,vi;q=0.7
Hãy "giải mã" chuỗi này:
en-US
: Tiếng Anh (Mỹ) - Đây là ngôn ngữ được ưu tiên nhất (mức ưu tiên mặc định làq=1.0
nếu không ghi rõ).en;q=0.9
: Tiếng Anh (bất kỳ vùng miền nào) - Ưu tiên thứ hai, với "chất lượng" (quality value) là 0.9.fr;q=0.8
: Tiếng Pháp - Ưu tiên thứ ba, với chất lượng 0.8.vi;q=0.7
: Tiếng Việt - Ưu tiên thứ tư, với chất lượng 0.7.
Trình duyệt gửi danh sách này, và nhiệm vụ của server là đọc, phân tích, so sánh với danh sách ngôn ngữ mà server hỗ trợ, và chọn ra ngôn ngữ phù hợp nhất để phản hồi.
Thực Hành: Bắt và Xử lý Accept-Language
trên Server
Cách tiếp cận header Accept-Language
phụ thuộc vào ngôn ngữ và framework bạn sử dụng ở backend. Tuy nhiên, nguyên lý chung là giống nhau: truy cập đối tượng request và đọc giá trị của header Accept-Language
.
Chúng ta hãy xem xét một ví dụ đơn giản với Node.js và framework Express, một lựa chọn phổ biến trong thế giới phát triển web hiện đại và tương thích tốt với các công nghệ front-end như React/NextJS.
Giả sử ứng dụng của bạn hỗ trợ các ngôn ngữ: Tiếng Anh (en
) và Tiếng Việt (vi
). Ngôn ngữ mặc định là Tiếng Anh.
1. Đọc Header trong Express:
Trong Express, thông tin về request được chứa trong đối tượng req
. Các header được truy cập thông qua req.headers
.
const express = require('express');
const app = express();
app.get('/', (req, res) => {
// Lấy giá trị của header Accept-Language
const acceptLanguageHeader = req.headers['accept-language'];
console.log("Accept-Language Header:", acceptLanguageHeader);
// Bây giờ bạn cần xử lý chuỗi này để chọn ngôn ngữ
res.send('Hello World (Language depends on header)');
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Khi chạy đoạn code này và truy cập từ trình duyệt, bạn sẽ thấy giá trị Accept-Language
in ra trong console của server.
2. Phân tích và Chọn Ngôn ngữ Phù hợp:
Đây là phần "cốt lõi". Bạn cần phân tích chuỗi Accept-Language
(thường là chuỗi phức tạp có dấu phẩy, chấm phẩy và q
value) và so sánh với danh sách ngôn ngữ bạn hỗ trợ (supportedLanguages
).
Một cách tiếp cận đơn giản (bỏ qua q
value phức tạp) là lấy danh sách các ngôn ngữ được ưu tiên nhất và kiểm tra xem ngôn ngữ nào có trong danh sách hỗ trợ của bạn.
const express = require('express');
const app = express();
const supportedLanguages = ['vi', 'en']; // Ngôn ngữ hỗ trợ, ưu tiên theo thứ tự (có thể sắp xếp lại sau)
const defaultLanguage = 'en'; // Ngôn ngữ mặc định nếu không tìm thấy sự phù hợp
function getPreferredLanguage(acceptLanguageHeader, supportedLangs, defaultLang) {
if (!acceptLanguageHeader) {
return defaultLang; // Không có header, dùng mặc định
}
// Phân tích header: Tách theo dấu phẩy, rồi tách theo chấm phẩy để lấy mã ngôn ngữ
// Ví dụ: "en-US,en;q=0.9,fr;q=0.8" -> ["en-US", "en;q=0.9", "fr;q=0.8"]
// Lấy phần trước dấu chấm phẩy: "en-US", "en", "fr"
const requestedLanguages = acceptLanguageHeader
.split(',')
.map(lang => lang.split(';')[0].trim()) // Lấy phần mã ngôn ngữ, bỏ qua q-value
.map(lang => lang.split('-')[0].toLowerCase()); // Lấy mã ngôn ngữ gốc (ví dụ: en-US -> en)
console.log("Requested Languages (simple parse):", requestedLanguages);
// Tìm ngôn ngữ phù hợp đầu tiên trong danh sách ưu tiên của người dùng
for (const lang of requestedLanguages) {
if (supportedLangs.includes(lang)) {
return lang; // Tìm thấy ngôn ngữ hỗ trợ
}
}
// Không tìm thấy ngôn ngữ phù hợp, dùng mặc định
return defaultLang;
}
app.get('/', (req, res) => {
const acceptLanguageHeader = req.headers['accept-language'];
const userLanguage = getPreferredLanguage(acceptLanguageHeader, supportedLanguages, defaultLanguage);
console.log("Selected Language:", userLanguage);
// Dựa vào userLanguage, trả về nội dung tương ứng
if (userLanguage === 'vi') {
res.send('Chào mừng bạn đến với trang web (Tiếng Việt)!');
} else { // Mặc định là en
res.send('Welcome to the website (English)!');
}
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Giải thích code:
- Chúng ta định nghĩa mảng
supportedLanguages
vàdefaultLanguage
. - Hàm
getPreferredLanguage
nhận headerAccept-Language
, danh sách hỗ trợ và ngôn ngữ mặc định. - Nó phân tích chuỗi header, tách các ngôn ngữ, loại bỏ phần q-value và chỉ lấy mã ngôn ngữ gốc (ví dụ:
en-US
thànhen
). Lưu ý: Việc phân tíchAccept-Language
header đầy đủ theo chuẩn RFC có thể phức tạp hơn, có tính đếnq
value và ký tự đại diện ``. Các thư viện chuyên dụng thường được sử dụng cho mục đích này trong thực tế.* - Nó lặp qua danh sách ngôn ngữ mà người dùng yêu cầu (đã được phân tích) và kiểm tra xem ngôn ngữ nào tồn tại trong
supportedLanguages
. Ngôn ngữ đầu tiên tìm thấy là ngôn ngữ được chọn. - Nếu không có ngôn ngữ nào phù hợp, nó trả về
defaultLanguage
. - Trong route handler (
app.get('/')
), chúng ta gọi hàm này để xác định ngôn ngữ và sau đó trả về nội dung tương ứng.
Ví dụ phức tạp hơn một chút (gần hơn với thực tế):
Trong ứng dụng thực tế, bạn sẽ không res.send
các chuỗi tĩnh như vậy. Bạn sẽ sử dụng templating engine (như EJS, Pug, Handlebars...) hoặc framework (như Next.js, nơi bạn có thể tạo các trang theo cấu trúc thư mục ngôn ngữ) để render nội dung động theo ngôn ngữ đã chọn.
Hoặc, bạn có thể tải các tệp ngôn ngữ (JSON object chứa các chuỗi dịch) dựa trên userLanguage
và truyền dữ liệu đó vào template hoặc gửi về cho front-end (ví dụ: trong trường hợp của API hoặc các ứng dụng SPA render server-side).
const express = require('express');
const app = express();
const supportedLanguages = ['vi', 'en'];
const defaultLanguage = 'en';
// Giả định bạn có các tệp chứa nội dung dịch
const translations = {
en: {
title: "Welcome",
greeting: "Hello, World!",
description: "This is an English page."
},
vi: {
title: "Chào mừng",
greeting: "Xin chào thế giới!",
description: "Đây là trang Tiếng Việt."
}
};
// Hàm xử lý Accept-Language (sử dụng thư viện i18n-detect để đơn giản hóa parsing)
// Cần cài đặt: npm install i18n-detect
const detectLanguage = require('i18n-detect');
function getBestSupportedLanguage(acceptLanguageHeader, supportedLangs, defaultLang) {
const detected = detectLanguage(acceptLanguageHeader, supportedLangs);
return detected || defaultLang; // detectLanguage trả về ngôn ngữ tốt nhất hoặc null
}
app.get('/', (req, res) => {
const acceptLanguageHeader = req.headers['accept-language'];
// Sử dụng hàm detectLanguage của thư viện
const userLanguage = getBestSupportedLanguage(acceptLanguageHeader, supportedLanguages, defaultLanguage);
console.log("Selected Language:", userLanguage);
// Lấy nội dung dịch cho ngôn ngữ đã chọn
const content = translations[userLanguage];
// Render HTML (ví dụ đơn giản)
res.send(`
<!DOCTYPE html>
<html lang="${userLanguage}">
<head>
<meta charset="UTF-8">
<title>${content.title}</title>
</head>
<body>
<h1>${content.greeting}</h1>
<p>${content.description}</p>
</body>
</html>
`);
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Giải thích:
- Chúng ta sử dụng một object
translations
giả định chứa nội dung dịch cho các ngôn ngữ. - Thay vì tự phân tích
Accept-Language
một cách đơn giản, chúng ta giới thiệu việc sử dụng một thư viện nhưi18n-detect
(npm install i18n-detect
). Các thư viện này được thiết kế để xử lý chính xác các trường hợp phức tạp của headerAccept-Language
(bao gồmq
value). - Hàm
getBestSupportedLanguage
sử dụng thư việni18n-detect
để tìm ngôn ngữ phù hợp nhất từ header so với danh sách hỗ trợ. - Trong route handler, sau khi xác định được
userLanguage
, chúng ta lấy nội dung tương ứng từ objecttranslations
. - Cuối cùng, chúng ta render một trang HTML đơn giản, chèn nội dung dịch vào các vị trí tương ứng. Điều quan trọng là thẻ
<html>
có thuộc tínhlang
được thiết lập đúng ngôn ngữ.
Ví dụ này minh họa cách server có thể trực tiếp trả về HTML đã được dịch dựa trên header Accept-Language
, mang lại lợi ích về tốc độ và SEO so với việc dựa hoàn toàn vào client-side JavaScript.
Các Yếu tố Cần Quan tâm Thêm
- Quản lý Ngôn ngữ Hỗ trợ: Duy trì danh sách
supportedLanguages
và cấu trúc tệp dịch (ví dụ:locales/en.json
,locales/vi.json
). - User Overrides: Người dùng có thể muốn tự chọn ngôn ngữ khác với cài đặt trình duyệt của họ. Cơ chế phổ biến là sử dụng:
- URL Parameters:
/en/about
,/vi/about
. Đây là cách phổ biến nhất và thân thiện với SEO. Server sẽ ưu tiên tham số URL hơnAccept-Language
header. - Cookies: Lưu lựa chọn ngôn ngữ của người dùng vào cookie. Server kiểm tra cookie trước khi xem
Accept-Language
.
- URL Parameters:
- Fallback: Luôn có một ngôn ngữ mặc định (
defaultLanguage
) để sử dụng khi không thể xác định ngôn ngữ hoặc không có ngôn ngữ phù hợp được hỗ trợ. - Caching: Nếu bạn cache các trang đã render, hãy đảm bảo cache được phân biệt theo ngôn ngữ.
Kết nối với Front-end (Next.js)
Trong các framework hiện đại như Next.js, server-side language detection và i18n được hỗ trợ mạnh mẽ và cấu hình sẵn. Next.js cho phép bạn định nghĩa các locales (ngôn ngữ) được hỗ trợ và xử lý định tuyến dựa trên ngôn ngữ (ví dụ: /en
, /vi
). Nó tự động đọc Accept-Language
header và chuyển hướng/render trang phù hợp. Hiểu được nguyên tắc hoạt động của Accept-Language
header sẽ giúp bạn cấu hình và tận dụng tối đa các tính năng i18n của Next.js.
Comments