Theme hardcode tiếng Việt thẳng vào template buộc dev phải tìm thay từng string khi cần đa ngữ — không khả thi với theme 100+ string và quy mô client EN. WordPress i18n API kết hợp workflow .pot/.po/.mo cho phép translate sạch sẽ, không touch code template.
Bài này hướng dẫn từ register text domain, 6 hàm i18n cốt lõi, generate .pot bằng WP-CLI và Loco Translate, plural rule tiếng Việt, đến deploy .mo file production.
Text domain — quy tắc đặt tên và load đúng pattern
Text domain là identifier unique cho theme khi WordPress load translation file. Mỗi theme và plugin có text domain riêng — WordPress dùng text domain để biết string nào thuộc về theme nào khi load file .mo.
Sai text domain đồng nghĩa translation không apply được dù file .mo đúng. Đây là sai lầm tốn nhiều giờ debug nhất khi setup i18n lần đầu.
Ba quy tắc đặt tên text domain
Quy tắc đặt tên cộng đồng WordPress chuẩn hoá để tránh conflict giữa theme và plugin. Web22-2026 dùng web22-2026 làm text domain — match folder name và dễ nhớ.
- Match theme slug (folder name): nếu theme nằm trong
wp-content/themes/web22-2026/, text domain làweb22-2026— không cần đoán. - Chỉ chữ thường và dash: không underscore, không space, không uppercase — chuẩn URL slug để compat với mọi hệ thống.
- Không trùng plugin active: check tên plugin folder trước khi đặt — vd
elementorđã có plugin chính dùng — đừng đặt theme cùng tên.
Declare trong style.css header
Hai field bắt buộc trong style.css header để WordPress nhận diện text domain và path chứa translation file. Submit theme lên WordPress.org Theme Directory yêu cầu cả 2 field này.
/*
Theme Name: Web22 2026
Text Domain: web22-2026
Domain Path: /languages
*/
Load text domain trong setup
Hook after_setup_theme bắt buộc cho load_theme_textdomain. Load sớm hơn không có effect vì path chưa định nghĩa.
Load muộn hơn miss một số string đầu request.
add_action('after_setup_theme', function () {
load_theme_textdomain(
'web22-2026',
get_template_directory() . '/languages'
);
});
Sáu hàm i18n cốt lõi và khi nào dùng
WordPress cung cấp 8 hàm i18n chính cho 8 use case khác nhau. Dùng đúng hàm tránh được lỗi sai escape và lỗi context disambiguation phổ biến trong theme đa ngữ.
Quy tắc tổng quát: __() trả về string, _e() echo trực tiếp, prefix esc_* thêm escape, _n() handle số ít/nhiều, _x() phân biệt context cùng từ.
Bảng decision khi nào dùng hàm nào
Bảng dưới giúp em chọn nhanh hàm phù hợp cho từng context output. Tham khảo trước khi viết code thay vì học thuộc lòng — sai context đồng nghĩa lỗ hổng XSS hoặc string không escape đúng.
| Hàm | Khi dùng | Ví dụ |
|---|---|---|
__() |
Trả về string, dùng trong attribute hoặc concat | $btn = __('Gửi', 'web22-2026'); |
_e() |
Echo trực tiếp trong template | <?php _e('Gửi', 'web22-2026'); ?> |
esc_html__() |
Trả về escape HTML và dịch | <?= esc_html__('Tên', 'web22-2026') ?> |
esc_html_e() |
Echo escape HTML | <?php esc_html_e('Tên', 'web22-2026'); ?> |
esc_attr__() |
Trả về escape attribute | placeholder="<?= esc_attr__('Email', 'web22-2026') ?>" |
_n() |
Số nhiều theo count | xem dưới |
_x() |
Disambiguate context | xem dưới |
_n() — handle số ít và số nhiều
Hàm _n trả về string khác nhau theo count number. Pattern phổ biến nhất là hiển thị “1 comment” vs “5 comments” cho locale English.
Tiếng Việt không có số nhiều nên 2 string giống nhau.
// English: 1 comment / 5 comments
// Vietnamese: 1 bình luận / 5 bình luận (giống nhau)
$count = get_comments_number();
printf(
_n('%s bình luận', '%s bình luận', $count, 'web22-2026'),
number_format_i18n($count)
);
_x() — disambiguate cùng từ khác nghĩa
Một từ tiếng Anh có thể có 2+ nghĩa khi dịch sang ngôn ngữ khác. Hàm _x cho phép translator phân biệt context để dịch đúng.
// "Post" có thể là noun (bài viết) hoặc verb (đăng)
echo _x('Post', 'noun · bài viết', 'web22-2026'); // dịch "Bài viết"
echo _x('Post', 'verb · đăng bài', 'web22-2026'); // dịch "Đăng"
Generate .pot file với WP-CLI hoặc Loco Translate
File .pot là template chứa toàn bộ msgid (string gốc) extract từ source code theme. Translator dịch từng msgid để tạo .po file per language.
Compile .po thành .mo binary để WordPress load runtime.
Có 3 cách generate .pot phổ biến. WP-CLI là cách dev khuyến nghị vì có thể tự động hoá trong CI/CD.
Loco Translate là plugin UI phù hợp dev không thoải mái với command line.
Cách 1 — WP-CLI (recommended cho dev)
WP-CLI có command wp i18n make-pot để scan theme folder và extract mọi string trong hàm i18n. Pattern này nhanh nhất và chính xác nhất cho theme có nhiều file.
# Install WP-CLI nếu chưa có
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
# Generate .pot từ theme folder
cd wp-content/themes/web22-2026
wp i18n make-pot . languages/web22-2026.pot --domain=web22-2026
Output languages/web22-2026.pot chứa khoảng 50-200 msgid tuỳ độ phức tạp theme. File này commit vào Git — translator pull về để dịch.
Cách 2 — Loco Translate plugin
Cài plugin Loco Translate từ WordPress.org plugin directory. Vào Loco → Themes → Web22 2026 → New language.
Loco scan code tự động, generate .po và .mo trực tiếp trong UI.
Loco UX phù hợp non-dev — translator có thể dịch trực tiếp trong admin WordPress mà không cần install Poedit hay edit file binary. Trade-off là phải có quyền admin server.
Cách 3 — Poedit desktop app
Poedit là desktop app cross-platform Windows, macOS, Linux. Mở Poedit → File → New from POT/PO file → chọn file .pot → translate từng msgid → save tự động tạo .po và .mo.
Translator comment để clarify ngữ cảnh
String ngắn dễ ambiguity. Translator không biết context phải đoán nghĩa — dịch sai dẫn tới UX vỡ.
Comment translators: ngay trên dòng __() sẽ được extract vào file .pot dưới dạng comment để translator tham khảo.
/* translators: %s = số bài viết */
printf(__('Đã đăng %s bài', 'web22-2026'), $count);
/* translators: button label trong form contact */
$label = __('Gửi yêu cầu', 'web22-2026');
/* translators: tooltip khi hover icon search */
$tooltip = esc_attr__('Tìm kiếm sản phẩm', 'web22-2026');
Quy tắc viết translator comment
Comment hữu ích cho translator phải cung cấp context cụ thể chứ không phải mô tả chung chung. Ba quy tắc dưới giúp em viết comment translator hiệu quả.
- Giải thích placeholder %s và %d: luôn note
%s = Xhoặc%d = Y— translator biết placeholder thay cho cái gì. - Note context UI cụ thể: “button label trong form X” hoặc “tooltip hover icon Y” — translator chọn từ phù hợp với UI element.
- Phân biệt formal vs informal: note “formal — gửi cho khách doanh nghiệp” hoặc “informal — chat support” — tone dịch khác nhau.
Pluralization tiếng Việt — đặc thù không có số nhiều
Tiếng Việt không phân biệt số ít và số nhiều như English hay Russian. Cấu trúc tiếng Việt dùng số đếm trước danh từ (“1 bài”, “5 bài”) chứ không biến đổi danh từ.
Khai báo plural rule trong header file .po để WordPress biết cách handle. Tiếng Việt dùng nplurals=1 đơn giản nhất trong các locale phổ biến.
# Trong header file web22-2026-vi.po
"Plural-Forms: nplurals=1; plural=0;"
nplurals=1 có nghĩa là chỉ 1 form. Mọi _n() call trả về form duy nhất ở vị trí msgstr[0] trong file .po.
So sánh plural rule giữa các ngôn ngữ
Mỗi ngôn ngữ có rule plural khác nhau. Tiếng Việt đơn giản nhất, Russian phức tạp nhất trong các ngôn ngữ phổ biến.
- English — 2 form:
nplurals=2; plural=(n != 1);— singular cho 1, plural cho mọi số khác. - Russian — 3 form phức tạp:
nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : ...)— 3 form theo modulo 10 và modulo 100. - Vietnamese — 1 form duy nhất:
nplurals=1; plural=0;— không cần phân biệt, 1 form cho mọi count.
Translate Customizer settings và Carbon Fields labels
String trong Customizer và Carbon Fields labels cũng phải wrap trong hàm i18n để translate được. Đây là chỗ dev quên nhiều nhất — focus vào template body mà bỏ qua admin UI.
Customizer settings và controls
Mọi title, description, label của Customizer section và control phải wrap trong __(). Pattern dưới minh hoạ section liên hệ với 1 text control.
$wp_customize->add_section('w22_contact', [
'title' => __('Web22 — Liên hệ', 'web22-2026'),
'priority' => 30,
]);
$wp_customize->add_control('w22_phone', [
'label' => __('Số điện thoại', 'web22-2026'),
'description' => __('Định dạng: 0981 828 781', 'web22-2026'),
'section' => 'w22_contact',
'type' => 'text',
]);
Carbon Fields container và fields
Carbon Fields nhận label làm parameter thứ 3 cho mọi field type. Wrap label và help text trong __() với text domain theme.
Container::make('theme_options', __('Web22 Options', 'web22-2026'))
->add_fields([
Field::make('text', 'tax_code', __('Mã số thuế', 'web22-2026'))
->set_help_text(__('Hiển thị footer và invoice', 'web22-2026')),
Field::make('image', 'og_default', __('OG image mặc định', 'web22-2026'))
->set_help_text(__('Kích thước 1200x630px', 'web22-2026')),
]);
Deploy .mo file production và quản lý cache
File .mo binary phải có mặt trong folder theme/languages/ để WordPress load runtime. Cache nginx hay Apache có thể serve .mo version cũ — translation update không apply tới user.
Filename convention bắt buộc
WordPress load .mo theo naming convention strict. Sai tên file đồng nghĩa WordPress không load được translation dù file đúng nội dung.
- Theme:
{textdomain}-{locale}.mo— vdweb22-2026-vi.mo,web22-2026-en_US.mo. - Plugin:
{textdomain}-{locale}.mo— cùng convention với theme, nằm trong folder plugin riêng. - Core WordPress: chỉ
{locale}.motrongwp-content/languages/— không có text domain prefix.
Clear cache sau khi update .mo
WordPress reload .mo mỗi request nhưng object cache (Redis, Memcached) có thể cache content trong memory. Clear cache để force reload.
# WP-CLI clear translation cache
wp cache flush
wp transient delete --all
# Check loại object cache đang dùng
wp cache type
# Clear OPcache nếu có
wp eval 'opcache_reset();'
Workflow deploy .mo cho production
Deploy .mo production khác với dev — phải có rollback plan vì translation lỗi có thể vỡ UX cho hàng nghìn user cùng lúc. Ba bước an toàn dưới đây.
- Test trên staging trước: upload
.molên staging environment, smoke test 30 phút mọi page chính trước khi push production. - Backup .mo cũ trước khi replace: rename
web22-2026-vi.mothànhweb22-2026-vi.mo.bak— rollback trong 30 giây nếu issue. - Monitor error log 1 giờ sau deploy: tail
wp-content/debug.logđể bắt warning “Translation file not found” — fix ngay nếu xuất hiện.
CI/CD pipeline tự động compile .mo
Manual compile .po sang .mo mỗi lần dịch tốn thời gian và dễ quên bước cuối. Pipeline CI/CD tự động hoá bước này giúp translator chỉ commit .po, hệ thống tự build và deploy.
Pattern phổ biến với GitHub Actions hoặc GitLab CI: webhook trigger khi commit .po mới, container chạy msgfmt compile .mo, deploy file binary qua SFTP hoặc rsync lên production server.
- Tool msgfmt từ gettext: command line tool chuẩn để compile
.posang.mo— có sẵn trong mọi Linux distro, install qua brew trên macOS. - GitHub Actions workflow: trigger trên push branch translation/* — chạy msgfmt và commit
.mobinary vào branch riêng. - Validate .po syntax trước compile:
msgfmt --checkcatch lỗi syntax như placeholder mismatch trước khi compile — fail fast tốt hơn deploy file binary lỗi.
Theo dõi translation coverage qua thời gian
Theme grow theo thời gian, string mới được thêm liên tục. Translation coverage giảm dần nếu translator không theo kịp pace dev — UX vỡ một phần cho user locale EN.
- Đo coverage hàng tháng: chạy
msgfmt --statistics web22-2026-en_US.pora số translated / fuzzy / untranslated — dashboard hoá để team thấy. - Alert khi coverage dưới 95%: Slack notification khi pipeline detect coverage giảm — translator nhận task trong sprint tiếp.
- Audit string mới mỗi release: diff
.potgiữa 2 release để biết string nào mới cần dịch — prioritize string UI critical trước.
Câu hỏi thường gặp
Theme chỉ dùng tiếng Việt — có cần i18n không?
Có nếu plan đa ngữ tương lai hoặc có ý định submit theme lên WordPress.org Theme Directory. Không bắt buộc nếu theme custom 1-1 chỉ dùng riêng cho 1 client và chắc chắn không bao giờ đa ngữ.
Trade-off: i18n đòi wrap mọi string trong __() — ban đầu mất 1-2 giờ cho theme size trung bình. Sau đó maintain free, không thêm cost mỗi feature mới.
load_theme_textdomain vs load_child_theme_textdomain khác gì?
load_child_theme_textdomain dùng cho child theme khi cần override translation của parent. Path mặc định trỏ vào get_stylesheet_directory() (folder child) thay vì get_template_directory() (folder parent).
Child theme thường không cần override translation parent — chỉ cần load translation riêng nếu có string mới trong child. Trường hợp đặc biệt mới phải dùng load_child_theme_textdomain.
Có cần restart server sau khi upload .mo file?
Không. WordPress reload .mo mỗi request — không cần restart PHP-FPM hay nginx.
Chỉ cần clear object cache nếu dùng Redis hoặc Memcached.
Trường hợp duy nhất phải restart là khi dùng OPcache với opcache.validate_timestamps=0. Setup này cache file PHP vĩnh viễn — phải opcache_reset() hoặc restart PHP-FPM để reload.
WPML, Polylang vs i18n native — chọn cái nào?
i18n native chỉ translate UI string của theme (label, button, message). WPML hoặc Polylang translate CONTENT thực sự (post body, taxonomy, menu, custom field).
Nếu cần đa ngữ cho user thực sự thì cần WPML hoặc Polylang. i18n native vẫn là baseline cần có dù dùng WPML — UI string vẫn cần translate qua text domain mechanism.
Bao nhiêu string trong theme là quá nhiều?
Theme web22-2026 có khoảng 80 string. Theme premium thường 200-500 string vì cover nhiều use case.
Trên 500 string thường là dấu hiệu code generic quá hoặc nên tách logic sang plugin riêng.
Quy tắc kinh nghiệm: string nhiều mà toàn label generic (“Submit”, “Cancel”, “Save”) nên dùng esc_html__() trực tiếp từ WordPress core text domain thay vì duplicate trong theme.
Tài nguyên và bước tiếp theo
i18n đúng pattern mở khoá thị trường đa ngữ cho theme và đáp ứng requirement submit WordPress.org Theme Directory. Sau khi nắm workflow .pot/.po/.mo, mở rộng sang các topic kỹ thuật khác.
- functions.php best practice — 7 pattern theme WordPress scale — load_theme_textdomain đúng hook và file structure inc.
- Enqueue script style WordPress đúng cách — 4 hook + conditional load — translate JS string qua wp_localize_script.
- Walker_Nav_Menu tùy biến — build mega menu WordPress — i18n menu title và badge label đa ngữ.
- WooCommerce theme support — 3 cấp độ + override template — translate string shop và checkout cho thị trường EN.
- Dịch vụ thiết kế website WordPress chuyên nghiệp — gói thi công theme custom turnkey kèm i18n.
Cần đội Web22 audit i18n và dịch theme cho thị trường EN/VN của bạn? Dịch vụ dev theme/plugin WordPress custom tại Web22 — quét string thiếu wrap, tạo file .pot, dịch baseline EN, deploy .mo production an toàn.


