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ãn Selection, 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.

Ảnh chụp mã Python nền tối hai phần. Phần 1 đánh dấu chuỗi dịch trong Python bằng gạch dưới. Import models fields và dấu gạch dưới từ odoo, import UserError. Class TheThanhVien inherit quan the thanh vien, method action_gia_han nếu trang_thai bằng huy thì raise UserError bọc chuỗi Thẻ đã huỷ không thể gia hạn trong hàm gạch dưới để được trích ra pot, chú thích field label Mã thẻ Điểm tích luỹ tự động trích không cần hàm gạch dưới. Phần 2 bản dịch trong i18n en.po msgid gốc msgstr bản dịch. Có chú thích module quan_ca_phe và dòng model ir model fields field_description name. msgid Mã thẻ chú thích chuỗi gốc là khoá, msgstr Card code chú thích bản dịch sang tiếng Anh. msgid Điểm tích luỹ msgstr Loyalty points. msgid Thẻ đã huỷ không thể gia hạn msgstr Card cancelled cannot renew

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:

Ảnh chụp terminal nền tối chạy odoo i18n export thật đọc file pot của quan_ca_phe. Dòng lệnh odoo i18n export d blog19 o quan_ca_phe.pot quan_ca_phe. Log INFO odoo cli i18n Exporting quan_ca_phe pot to quan_ca_phe.pot. Lệnh wc l cho 1029 dòng chú thích 183 msgid được trích tự động. Lệnh cat trích nội dung. Header Translation of Odoo Server sao quan_ca_phe, msgstr rỗng, Project-Id-Version Odoo Server 19.0, Content-Type text plain charset UTF-8. Một entry chú thích module quan_ca_phe, dòng model ir model fields field_description field quan the thanh vien name, dòng model_terms ir ui view arch_db view_the_form, msgid Mã thẻ chú thích trích từ field name và từ view, msgstr rỗng chú thích pot msgstr rỗng chưa dịch. Entry tiếp field quan the thanh vien diem msgid Điểm tích luỹ msgstr rỗng

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ọi msgid với msgstr rỗ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ừ .pot rồi điền msgstr.

Đặ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ế:

  1. Xuất .pot: odoo i18n export -o i18n/quan_ca_phe.pot quan_ca_phe.
  2. Dịch: copy .pot thành fr.po, điền msgstr (bằng tay hoặc công cụ như Poedit/Weblate).
  3. 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.
  4. Nạp: nâng cấp module (-u quan_ca_phe) hoặc odoo i18n import — Odoo đọc .po và ghi bản dịch vào bảng ir.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àm msgid chứ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ới bien là 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ề

  1. Odoo trích chuỗi ra file .pot (mẫu, msgstr rỗng), bạn dịch sang .po từng ngôn ngữ đặt trong i18n/. Đã xuất thật 183 msgid từ quan_ca_phe chỉ bằng một lệnh, không sửa code.
  2. 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.
  3. msgid là chuỗi gốc (khoá), msgstr là bản dịch; đặt .po vào i18n/, 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 .pot và 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.