Bỏ qua tới nội dung
WordPress· ·11 phút đọc

Dịch theme WordPress đa ngôn ngữ từ A đến Z

Nguyen Hien
Dịch theme WordPress đa ngôn ngữ từ A đến Z
Cỡ chữ

Một theme dù đẹp đến đâu mà chữ cứng tiếng Anh nằm rải rác trong PHP thì vẫn không thể đổi ngôn ngữ. Muốn theme nói được tiếng Việt, tiếng Nhật hay bất kỳ tiếng nào, bạn phải làm hai việc tách bạch: chuẩn bị mã nguồn cho dịch thuật (i18n), rồi mới dịch (l10n). Bài này đi qua đủ cả hai theo đúng chuẩn của WordPress.org năm 2026, kèm code chạy thật.

i18n và l10n khác nhau thế nào

i18n (internationalization — quốc tế hoá) là việc lập trình viên bọc mọi chuỗi hiển thị vào hàm dịch để chúng có thể được dịch. l10n (localization — bản địa hoá) là việc người dịch tạo ra bản dịch cụ thể cho một ngôn ngữ. Nói gọn: i18n là bạn viết theme, l10n là người dùng dịch theme. Nếu bạn bỏ qua i18n thì không ai dịch nổi, vì chữ đã đóng đinh trong code.

WordPress dùng khung gettext của GNU để làm việc này — cùng cơ chế mà chính lõi WordPress dùng. Nhờ vậy mọi theme tuân chuẩn đều dịch được bằng cùng một bộ công cụ.

Text domain — định danh cho theme của bạn

Text domain (miền văn bản) là một chuỗi định danh để WordPress biết bản dịch nào thuộc về theme nào. Quy tắc: viết thường, dùng dấu gạch ngang chứ không gạch dưới, và trùng với slug thư mục theme. Theme nằm trong thư mục web22-theme thì text domain là web22-theme.

Khai báo nó ngay trong phần header của style.css:

/*
Theme Name: Web22 Theme
Author: Web22
Text Domain: web22-theme
Domain Path: /languages
*/

Domain Path trỏ tới thư mục chứa file dịch, theo quy ước đặt tên /languages. Một nguyên tắc dễ quên: text domain phải là chuỗi văn bản trực tiếp, tuyệt đối không truyền bằng biến. Viết __( 'Đọc thêm', $domain ) sẽ khiến công cụ quét chuỗi không nhận ra và bỏ sót.

Bọc chuỗi bằng các hàm gettext

Đây là phần cốt lõi của i18n. Mỗi chuỗi tiếng người dùng đọc phải nằm trong một hàm dịch. Các hàm hay dùng nhất:

  • __( 'Chuỗi', 'text-domain' )trả về chuỗi đã dịch (dùng khi ghép biến, return).
  • _e( 'Chuỗi', 'text-domain' )in thẳng chuỗi đã dịch ra màn hình.
  • esc_html__()esc_html_e() — như trên nhưng escape HTML, dùng cho chữ nằm giữa các thẻ.
  • esc_attr__()esc_attr_e() — escape cho thuộc tính như title, alt, placeholder.
  • _x( 'Chuỗi', 'ngữ cảnh', 'text-domain' ) — khi một từ có nhiều nghĩa tùy ngữ cảnh (ví dụ “Post” là danh từ bài viết hay động từ đăng).
  • _n( 'số ít', 'số nhiều', $count, 'text-domain' ) — xử lý số ít/số nhiều.

Quy tắc an toàn của Web22 khi tự code theme và plugin web22-core: luôn ưu tiên bản có escape. Chữ in ra cho người đọc nên dùng esc_html_e(), chữ trong thuộc tính dùng esc_attr_e(). Vừa dịch được, vừa chặn lỗi bảo mật một công đôi việc.

Ví dụ một đoạn template trước và sau khi i18n hoá:

<?php // Chưa i18n — không dịch được ?>
<a href="<?php the_permalink(); ?>" title="Xem chi tiết bài viết">
  Đọc tiếp
</a>

<?php // Đã i18n — dịch được, có escape ?>
<a href="<?php the_permalink(); ?>"
   title="<?php esc_attr_e( 'Xem chi tiết bài viết', 'web22-theme' ); ?>">
  <?php esc_html_e( 'Đọc tiếp', 'web22-theme' ); ?>
</a>

Với chuỗi có biến, dùng printf kết hợp __() thay vì nối chuỗi, để người dịch nắm được trật tự từ:

<?php
printf(
  /* translators: %s là tên tác giả */
  esc_html__( 'Viết bởi %s', 'web22-theme' ),
  esc_html( get_the_author() )
);

Dòng /* translators: ... */ không bắt buộc nhưng rất nên có: nó hiện ra cho người dịch hiểu %s đại diện cho gì.

Sơ đồ bộ ba file pot po mo trong quy trình dịch theme WordPress
Hành trình một chuỗi từ lúc bọc hàm tới lúc thành bản dịch hiển thị.

Nạp file dịch — lưu ý quan trọng từ WordPress 6.7

Sau khi bọc chuỗi, bạn phải bảo theme nạp bản dịch bằng load_theme_textdomain(). Trước đây nhiều người gắn nó vào hook after_setup_theme. Nhưng từ WordPress 6.7, nếu bản dịch bị kích hoạt quá sớm, WordPress sẽ phun cảnh báo _load_textdomain_just_in_time was called incorrectly. Khuyến nghị chính thức hiện nay: nạp tại hook init hoặc muộn hơn.

<?php
function web22_theme_load_textdomain() {
    load_theme_textdomain(
        'web22-theme',
        get_template_directory() . '/languages'
    );
}
add_action( 'init', 'web22_theme_load_textdomain' );

Thực tế, với theme có text domain trùng slug và đặt file đúng chỗ, WordPress 4.6 trở lên đã tự nạp bản dịch đúng lúc cần (just-in-time — nạp khi cần), nên đôi khi bạn không cần gọi hàm này. Nhưng giữ nó ở init vẫn là cách rõ ràng và an toàn nhất khi bạn nạp file từ thư mục riêng của theme.

Bộ ba file .pot, .po, .mo

Khi chuỗi đã được bọc, đến lượt tạo file dịch:

FileVai trò
.potKhuôn mẫu (template) — danh sách toàn bộ chuỗi gốc, chưa dịch. Bạn xuất file này một lần.
.poBản dịch cho một ngôn ngữ, vẫn đọc được bằng mắt. Ví dụ vi.po.
.moFile biên dịch máy đọc, sinh tự động từ .po. Đây là file WordPress thực sự nạp.

Quy tắc đặt tên trong thư mục /languages của theme: dùng đúng mã locale, ví dụ vi.povi.mo cho tiếng Việt, ja.mo cho tiếng Nhật. (Nếu đặt trong thư mục ngôn ngữ chung của WordPress thì có thêm tiền tố text domain: web22-theme-vi.mo.)

Dịch bằng Loco Translate — không cần FTP

Cách nhanh nhất để dịch ngay trong wp-admin là plugin Loco Translate. Nó cho phép quét chuỗi, tạo file .pot, rồi dịch từng ngôn ngữ qua giao diện, và tự sinh cả .po lẫn .mo khi bạn bấm lưu — không cần đụng tới FTP hay dòng lệnh.

  1. Cài và kích hoạt Loco Translate, vào Loco Translate → Themes, chọn theme.
  2. Bấm Create template để quét chuỗi và sinh file .pot.
  3. Bấm New language, chọn ngôn ngữ (ví dụ Vietnamese), chọn nơi lưu System (để bản dịch không mất khi cập nhật theme).
  4. Dịch từng dòng rồi Save — Loco tự tạo .po.mo.

Một lưu ý của Web22 khi bàn giao web cho khách: nên lưu bản dịch ở vị trí System hoặc tách ra child theme, tránh lưu thẳng trong thư mục theme gốc vì bản cập nhật theme có thể ghi đè. Nếu bạn chưa quen tách lớp, bài tạo child theme đúng cách năm 2026 giải thích kỹ chỗ này.

Chuẩn bị theme đa ngôn ngữ thực thụ

Cần phân biệt hai mức. Dịch theme (chữ giao diện như “Đọc tiếp”, “Tìm kiếm”) là việc của gettext và file .po/.mo ở trên. Còn website đa ngôn ngữ — nơi nội dung bài viết, sản phẩm hiển thị theo nhiều thứ tiếng — lại cần plugin như Polylang hoặc WPML quản lý. Hai việc bổ trợ nhau: bạn i18n hoá theme để phần khung dịch được, rồi dùng plugin đa ngôn ngữ cho phần nội dung.

Vì vậy, khi nhận yêu cầu “làm web song ngữ”, việc đầu tiên vẫn là rà lại theme đã bọc chuỗi đầy đủ chưa. Một theme i18n cẩu thả sẽ để lộ chữ tiếng Anh ngay cả khi đã cài plugin dịch. Đây cũng là lý do khi đặt code theme riêng theo nhu cầu, chuẩn i18n nên là yêu cầu bắt buộc ngay từ đầu chứ không vá sau.

Bảng phân biệt i18n l10n và text domain khi dịch theme WordPress
Ba khái niệm nền tảng cần phân biệt trước khi bắt tay dịch theme.

Câu hỏi thường gặp

Theme tải về không có file .pot thì dịch kiểu gì?

Dùng Loco Translate bấm Create template để tự quét chuỗi và sinh .pot, miễn là theme đã bọc chuỗi trong hàm gettext. Nếu theme cứng chữ không bọc hàm thì không quét được — phải sửa code.

Vì sao tôi gặp cảnh báo _load_textdomain_just_in_time?

Do bản dịch bị nạp trước hook init (từ WordPress 6.7). Chuyển lời gọi load_theme_textdomain() sang hook init là hết.

File .mo khác gì .po mà cần cả hai?

.po để người đọc và chỉnh sửa, .mo là bản biên dịch nhị phân để WordPress chạy nhanh. Bạn sửa .po, công cụ tự sinh lại .mo.

Nếu bạn muốn một theme được code chuẩn i18n từ gốc để về sau dịch sang bất kỳ ngôn ngữ nào mà không phải đập đi làm lại, có thể tham khảo cách Web22 làm website WordPress.

Đọc tiếp

Bài viết
cùng chủ đề.

Tất cả bài viết