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à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.

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 supportedLanguagesdefaultLanguage.
  • Hàm getPreferredLanguage nhận header Accept-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ành en). Lưu ý: Việc phân tích Accept-Language header đầy đủ theo chuẩn RFC có thể phức tạp hơn, có tính đến q 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 header Accept-Language (bao gồm q value).
  • Hàm getBestSupportedLanguage sử dụng thư viện i18n-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ừ object translations.
  • 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ính lang đượ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ơn Accept-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.
  • 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

There are no comments at the moment.