tháng 6 3, 2021

Cài đặt đa ngôn ngữ trong Phoenix bằng Gettext (Phần 1)

Cài đặt đa ngôn ngữ trong Phoenix bằng Gettext (Phần 1)

Có cần hay không việc đa ngôn ngữ hoá ứng dụng web phụ thuộc vào nhiều yếu tố. Tuy nhiên theo mình nghĩ thì dưới góc độ kỹ thuật, dù hiện tại chỉ cần hỗ trợ một ngôn ngữ nhưng ta vẫn nên chuẩn bị sẵn bộ cơ chế cho việc đa ngôn ngữ hoá vì 1) việc này cũng không tốn thêm nhiều công sức; 2) việc hỗ trợ thêm ngôn ngữ khác trong tương lai sẽ dễ dàng hơn rất nhiều.

Và công việc đa ngôn ngữ hoá trong Phoenix càng dễ dàng hơn với Gettext. Qua 2 bài viết, mình xin tóm lược lại cách cài đặt đa ngôn ngữ trong Phoenix bằng Gettext

Gettext là gì?

Theo wikipedia, "Gettext là một hệ thống quốc tế hóa (i18n) và bản địa hóa (i10n) thường dùng cho việc viết các ứng dụng đa ngôn ngữ trên các hệ điều hànhtương tự Unix. Việc triển khai gettext thường được sử dụng phổ biến nhất là GNU gettext, phát hành bởi GNU Project năm 1995."

Ý tưởng đằng sau Gettext là nó dịch các message dựa vào chính message đó chứ không sử dụng các key. Ví dụ khi cần dịch message "Welcome to my blog" từ tiếng Anh sang tiếng Việt, thay vì sử dụng key messages.welcome, ta chỉ cần sử dụng trực tiếp gettext "Welcome to my blog". Cách tiếp cận như vậy mang lại 2 lợi ích.

  • Nếu sử dụng key, đầu tiên ta phải cần phải thêm key trong template sau đó thêm nội dung cho key đó vào file config. Nếu như key chưa được định nghĩa trong file config, hệ thống sẽ báo lỗi. Với Gettext, ta đơn giản chỉ cần sử dụng trực tiếp nó trong template, nếu không tìm thấy bản dịch, thì nội dung mặc định truyền cho gettext sẽ được sử dụng.
  • Thêm vào đó, khi sử dụng key, ta cần phải mất thêm công sức đặt tên cho key đó và nếu gặp phải key tối nghĩa ta không thể hiểu ngay được nội dung của message là gì nếu không xem trong file config. Điều này thường xuyên xảy ra khi phải làm việc với code của người khác. Còn với Gettext, nội dung message đơn giản đã nằm ngay tại nơi sử dụng.

Cấu trúc file của Gettext

Gettext có 2 loại file là *.pot*.po mặc định nằm trong thư mục priv/gettext. Thư mục này có thể được thay đổi thông qua tham số thiết lập.

File POT (Portable Object Template), đúng như tên gọi, là file liệt kê tất cả các string được sử dụng với gettext trong app. Phoenix mặc định hỗ trợ tiếng Anh và ban đầu chỉ có file errors.pot được tạo sẵn cho Ecto với các thông báo lỗi khi validate changeset.

Một project mới khởi tạo, file templates/page/index.html.eex có nội dung

<h2><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h2>

Khi ta chạy câu lệnh

$ mix gettext.extract

sẽ sinh ra (hay cập nhật) file default.pot với nội dung

## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here as no
## effect: edit them in PO (.po) files instead.
msgid ""
msgstr ""

#, elixir-format
#: lib/localization_demo_web/templates/page/index.html.eex:2
msgid "Welcome to %{name}!"
msgstr ""

Định dạng của một bản ghi

  • #: lib/localization_demo_web/templates/page/index.html.eex:2 là vị trí của string trong file code, nếu string xuất hiện ở nhiều vị trí khác nhau thì sẽ được gộp lại thành một object.
  • msgid là id của string, là string xuất hiện trong file code.
  • msgstr là bản dịch của string đó.

Và như trong đoạn comment đầu file có mô tả, đây chỉ là file template, ta không nên trực tiếp bản dịch vào msgstr, mà thay vào đó hãy điền vào *.po file.

Trong mỗi ngôn ngữ được hỗ trợ, mỗi file POT sẽ tương ứng với một PO file.

Để sinh ra (hay cập nhật) file PO cho tiếng Anh, ta chạy câu lệnh

$ mix gettext.merge priv/gettext

Đối với các ngôn ngữ khác, ta sử dụng option --locale LOCALE_CODE, ví dụ với tiếng Việt

$ mix gettext.merge priv/gettext --locale vi

Khi đó file vi/LC_MESSAGES/default.po có nội dung tương tự default.pot được sinh ra.

## "msgid"s in this file come from POT (.pot) files.
##
## Do not add, change, or remove "msgid"s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: vi\n"
"Plural-Forms: nplurals=1\n"

#, elixir-format
#: lib/localization_demo_web/templates/page/index.html.eex:2
msgid "Welcome to %{name}!"
msgstr ""
Thêm bản dịch tiếng Việt vào msgstr

Phân chia domain

Mặc định khi sử dụng hàm gettext, tất cả string sẽ được gộp vào một domain duy nhất là defaut (gồm có default.potdefault.po). Điều này hoàn toàn ổn với những ứng dụng web nhỏ nhưng đối với những ứng dụng lớn thì việc quản lý sẽ dễ dàng hơn khi chia ra thành nhiều domain nhỏ hơn với hàm dgettext. So với gettext, tên domain được thêm làm tham số đầu tiên của hàm. Ví dụ ta thêm vào file template

<%= dgettext "messages", "Invalid email address or password" %>

Sau đó chạy câu lệnh

$ mix gettext.extract --merge

sẽ tạo ra file POT mới messages.pot và các file messages.po tương ứng.

Thể số nhiều

Một số ngôn ngữ như tiếng Anh phân biệt giữa thể đơn (1) và thể số nhiều (> 1) và Gettext hỗ trợ tính năng này với hàm ngettext

<%= ngettext "There is one person", "There are %{count} people", n %>

Tham số đầu tiên là thể đơn, tham số thứ 2 là thể số nhiều và tham số thứ 3 là số tự nhiên lớn hơn 1 được thay vào %{count}trong tham số thứ 2. Khi chạy lệnh

$ mix gettext.extract --merge

Bản ghi mới được thêm vào file default.pot và các file default.po

msgid "There is one person"
msgid_plural "There are %{count} people"
msgstr[0] ""
msgstr[1] ""
  • msgstr[0]: bản dịch cho thể đơn
  • msgstr[1]: bản dịch cho thể số nhiều

Phân chia ngữ cảnh (context)

Gettext còn hỗ trợ tính năng khác là phân chia theo ngữ cảnh bằng hàm pgettext. Đó là những câu, những từ có thể hiểu theo nghĩa khác nhau trong những ngữ cảnh khác nhau.

Ví dụ từ invalid xuất hiện trong thông báo lỗi kiểm tra định dạng email có thể dịch là không hợp lệ, còn khi xuất hiện trong thông báo lỗi ở màn đăng nhập có thể dịch là không tồn tại.

<%= pgettext "user-interface", "invalid" %>
<%= pgettext "alert-user", "invalid" %>

Sau khi chạy lệnh

$ mix gettext.extract --merge

Hai bản ghi mới sẽ được thêm vào file default.pot và các file default.po.

#, elixir-format
#: lib/localization_demo_web/templates/page/index.html.eex:4
msgctxt "alert-user"
msgid "invalid"
msgstr ""

#, elixir-format
#: lib/localization_demo_web/templates/page/index.html.eex:3
msgctxt "user-interface"
msgid "invalid"
msgstr ""

Khác với bản ghi của các hàm khác, trường msgctxt được thêm vào với giá trị là tham số đầu tiên của hàm. msgctxt sẽ giúp phân biệt các bản ghi tuy có msgid trùng nhau nhưng ở các ngữ cảnh khác nhau.

Như vậy sau phần 1, mình đã giới thiệu về Gettext, cấu trúc file của Gettext trong Phoenix cùng một số tính năng nổi bật của nó. Sang phần 2, mình sẽ trình bày các cách cài đặt Gettext trong Phoenix.

Neit.