Module của bạn hiển thị "Mã thẻ", "Điểm tích luỹ", "Hạng thẻ". Giờ khách hàng Nhật muốn xem bằng tiếng Anh, khách khác muốn tiếng Pháp. Bạn không sửa code, không nhân bản module — Odoo có sẵn cơ chế đa ngôn ngữ (i18n) dựa trên chuẩn gettext: tách toàn bộ chuỗi hiển thị ra một file .pot (mẫu), rồi dịch sang file .po cho từng ngôn ngữ. Bài này chạy thật quy trình đó trên module quan_ca_phe và mổ xẻ file .pot sinh ra.
Chuỗi nào được dịch, và đánh dấu thế nào
Odoo trích chuỗi từ hai nguồn:
- Tự động: nhãn field (
string=),help=, nhãnSelection, và mọi text trong file view XML. Bạn không cần làm gì — Odoo tự nhận. - Thủ công trong Python: chuỗi sinh lúc chạy (thông báo lỗi, tên động) phải bọc bằng hàm
_()để Odoo biết đó là chuỗi cần dịch.

Hình 1: Phần 1 — from odoo import _, rồi bọc chuỗi runtime: raise UserError(_("Thẻ đã huỷ, không thể gia hạn.")). Field label thì tự động, không cần _(). Phần 2 — một entry trong file .po: msgid là chuỗi gốc (khoá tra cứu), msgstr là bản dịch. Dịch "Mã thẻ" → "Card code" mà không đụng tới model.
Xuất file .pot thật
Odoo 19 có lệnh con odoo i18n export. Mình chạy nó thật trên quan_ca_phe và đọc file sinh ra:

Hình 2: Thật, đọc trực tiếp từ file. Lệnh export sinh 1029 dòng, 183 msgid — toàn bộ trích tự động. Để ý các dòng chú thích #. và #:: chúng ghi chuỗi này đến từ đâu — msgid "Mã thẻ" được trích từ cả field name (field_description) lẫn view (arch_db:...view_the_form). Trong file .pot, mọi msgstr đều rỗng — đó là bản mẫu chưa dịch.
.pot và .po khác nhau ở đâu
.pot(Portable Object Template): bản mẫu, chứa mọimsgidvớimsgstrrỗng. Đây là "danh sách những gì cần dịch". Không gắn với ngôn ngữ nào..po(Portable Object): bản dịch cho một ngôn ngữ, ví dụfr.po,ja.po. Sao chép từ.potrồi điềnmsgstr.
Đặt file .po vào thư mục i18n/ của module (i18n/fr.po). Khi cài/nâng cấp module với ngôn ngữ đó đã kích hoạt, Odoo tự nạp bản dịch.
quan_ca_phe/
├── i18n/
│ ├── quan_ca_phe.pot # mẫu, msgstr rỗng
│ ├── fr.po # bản tiếng Pháp
│ └── ja.po # bản tiếng Nhật
Nạp và cập nhật bản dịch
Quy trình thực tế:
- Xuất
.pot:odoo i18n export -o i18n/quan_ca_phe.pot quan_ca_phe. - Dịch: copy
.potthànhfr.po, điềnmsgstr(bằng tay hoặc công cụ như Poedit/Weblate). - Cài ngôn ngữ: bật ngôn ngữ trong Cài đặt → Ngôn ngữ, hoặc
odoo i18n loadlang -l fr_FR. - Nạp: nâng cấp module (
-u quan_ca_phe) hoặcodoo i18n import— Odoo đọc.povà ghi bản dịch vào bảngir.translation.
Khi bạn thêm chuỗi mới vào module, xuất lại .pot và hợp nhất vào .po cũ (công cụ msgmerge) — những chuỗi đã dịch được giữ, chỉ chuỗi mới cần dịch thêm.
Vài lưu ý
- Đừng nối chuỗi động vào
_()._("Xin chào " + ten)làmmsgidchứa cả tên → không dịch được. Dùng_("Xin chào %s") % ten— placeholder nằm trong chuỗi gốc. _()chỉ trích được khi chuỗi là literal._(bien)vớibienlà biến sẽ không được trích ra.pot(công cụ quét mã tĩnh, không chạy code).- Đừng dịch dữ liệu người dùng nhập. i18n dành cho giao diện/nhãn cố định, không phải nội dung động trong CSDL.
Ba ý mang về
- Odoo trích chuỗi ra file
.pot(mẫu,msgstrrỗng), bạn dịch sang.potừng ngôn ngữ đặt trongi18n/. Đã xuất thật 183msgidtừquan_ca_phechỉ bằng một lệnh, không sửa code. - Field/view/Selection tự động được trích; chuỗi runtime trong Python phải bọc
_()(from odoo import _) với chuỗi literal và placeholder%s. msgidlà chuỗi gốc (khoá),msgstrlà bản dịch; đặt.povàoi18n/, kích hoạt ngôn ngữ rồi-u/i18n importđể nạp. Thêm chuỗi mới thì xuất lại.potvàmsgmergeđể giữ bản dịch cũ.
Module đã dịch được, phần cuối sê-ri quay lại thứ quyết định một hệ thống dùng được hay không khi dữ liệu lớn lên: hiệu năng. Phần sau mổ xẻ bẫy N+1 trong ORM — vì sao một vòng lặp tưởng vô hại có thể bắn hàng nghìn câu SQL, và cách gộp chúng lại.