Cài đặt đa ngôn ngữ trong Phoenix bằng Gettext (Phần 2)
Ở 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 xin trình bày các cách cài đặt Gettext trong Phoenix.
- Phần 1: Giới thiệu Gettext trong Phoenix
- Phần 2: Cài đặt Gettext trong Phoenix
Mỗi module có use Gettext được gọi là một Gettext backend. Và một process có thể có nhiều Gettext backend. Khi các hàm hay macro *gettext (như gettext, dgettext, pgettext, ngettext đã giới thiệu ở phần 1) của một Gettext backend được thực thi, để xác định bản dịch của ngôn ngữ nào sẽ được sử dụng, chúng sẽ xác định giá trị locale theo thứ tự ưu tiên sau.
- Locale được cài đặt cho backend đó trong process (bằng
put_locale/2), nếu có - Locale được cài đặt cho process (bằng
put_locale/1), nếu có - Default locale của backend đó, nếu có
- Default locale của Gettext, nếu có.
- Locale mặc định của Gettext là tiếng Anh -
"en"
Bước 1 - Cài đặt default locale
Default locale cho Gettext
Default locale cho Gettext được cài đặt thông qua atom :default_locale trong config.
config :gettext, :default_locale, "vi"Default locale cho Gettext backend
Như đã trình bày ở trên một module khi sử dụng use Gettext. Một project mới tạo, trong file gettext.ex phần web có định nghĩa sẵn một backend.
defmodule LocalizationDemoWeb.Gettext do
use Gettext, otp_app: :localization_demo
endNếu cần cài đặt riêng locale cho một backend thay vì sử dụng locale chung của Gettext, ta có thể cài đặt bằng 2 cách
Cách 1: Sử dụng option default_locale trong use Gettext
defmodule LocalizationDemoWeb.Gettext do
use Gettext,
otp_app: :localization_demo,
default_locale: "vi",
other_options
endNgoài default_locale, có một số option khác như
:priv: cài đặt thư mục lưu bản dịch. Mặc định là"priv/gettext":allowed_locales: mảng các locale được cho phép. Mặc định là tất cả các locale trong thư mụcpriv/gettext
Cách 2: Sử dụng option default_locale trong config
config :localization_demo, LocalizationDemoWeb.Gettext,
default_locale: "vi",
locales: ~w(en vi)
# Option locales tương tự như :allowed_locales trong cách 1Như vậy với file priv/gettext/vi/LC_MESSAGE/default.po sau khi thêm bản dịch
#, elixir-format
#: lib/localization_demo_web/templates/page/index.html.eex:2
msgid "Welcome to %{name}!"
msgstr "Chào mừng tới %{name}"
#, elixir-format
#: lib/localization_demo_web/templates/page/index.html.eex:3
msgid "Peace of mind from prototype to production"
msgstr "Yên tâm từ nguyên mẫu tới thành phẩm"
#, elixir-format
#: lib/localization_demo_web/templates/layout/app.html.eex:18
msgid "LiveDashboard"
msgstr "Bảng điều khiển"
#, elixir-format
#: lib/localization_demo_web/templates/layout/app.html.eex:16
msgid "Get Started"
msgstr "Bắt đầu!"Sẽ hiển thị giao diện tiếng Việt như sau.

Nếu app chỉ cần hỗ trợ một ngôn ngữ duy nhất, ta có thể dừng lại ở bước này. Còn nếu hỗ trợ từ hai ngôn ngữ trở lên, sẽ cần tới cơ chế cho phép ta chuyển đổi giữa các ngôn ngữ với nhau.
Bước 2: Lựa chọn locale cho app
Có 2 hướng cho vấn đề này: 1) sử dụng thư viện đã có sẵn hoặc 2) tự làm từ đầu. Ta sẽ lần lượt đi theo từng hướng để giải quyết.
Hướng 1: Sử dụng thư viện có sẵn.
Ta chọn plug set_locale, hiện tại hỗ trợ lấy locale theo thứ tự ưu tiên từ URL, cookie, tham số accept-language của request header và default locale trong config.
Bước 1: Cài đặt set_locale
Thêm set_locale vào mix.exs như sau và chạy lệnh mix deps.get
def deps do
[
# ...
{:set_locale, "~> 0.2.1"}
]
enddef application do
[
mod: {LocalizationDemo.Application, []},
extra_applications: [
:logger,
:runtime_tools,
:set_locale
]
]
endBước 2: Thêm plug vào pipeline :browser
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug(SetLocale,
gettext: LocalizationDemoWeb.Gettext,
default_locale: "vi",
cookie_key: "_localization_demo_locale",
additional_locales: []
)
endBước 3: Thêm /:locale vào routes
scope "/", LocalizationDemoWeb do
pipe_through :browser
get "/", PageController, :dummy
end
scope "/:locale", LocalizationDemoWeb do
pipe_through :browser
get "/", PageController, :index
end

Hướng 2: Thực hiện từ đầu
Bước 1: Tạo một module có thể plug vào pipeline :browser
Một plug-able module cần cài đặt hai callback:
init/1: khởi tạo các options truyền chocall/2. Ta có thể truyềndefault_localeđể sử dụng trong trường hợp không tìm thấy locale tại các nơi chỉ định. Tuy nhiên, để tránh việc định nghĩa default_locale ở nhiều nơi gây ra tình trạng không đồng nhất, ta chỉ trả vềnil.call/2: nhậnconnlàm đầu vào, thêm locale vàoconn, trả vềconn.
defmodule LocalizationDemoWeb.Plugs.SetLocale do
import Plug.Conn
@supported_locales Gettext.known_locales(LocalizationDemoWeb.Gettext)
@max_age 60*60*24*30 # 30 ngày
def init(_opts), do: nil
def call(conn, _options) do
case fetch_locale_from(conn) do
nil ->
conn
locale ->
Gettext.put_locale(LocalizationDemoWeb.Gettext, locale)
conn |> put_resp_cookie("locale", locale, max_age: @max_age)
end
end
defp fetch_locale_from(conn) do
(conn.params["locale"] || conn.cookies["locale"]) |> check_locale
end
defp check_locale(locale) when locale in @supported_locales, do: locale
defp check_locale(_), do: nil
endGettext.known_locales: trả về danh sách các locale được cho phép. Nếu danh sách này chưa được định nghĩa trong config, mặc định sẽ lấy các locale trong thư mụcpriv/gettext.- Hàm
fetch_locale_fromtìm locale trong URL, nếu không có nó sẽ tìm trong cookie. Sau đó kiểm tra locale được tìm thấy có hợp lệ (nằm trong danh sách được cho phép) hay không. - Nếu tìm thấy locale thì sử dụng hàm
Gettext.put_localeđể cài đặt locale cho backendLocalizationDemoWeb.Gettextvà sử dụng hàmput_resp_cookieđể lưu locale vào cookie.
Bước 2: Plug module SetLocale vào pipeline :browser
# lib/router.ex
# ...
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug LocalizationDemoWeb.Plugs.SetLocale
endBước 3 (tuỳ chọn): Thêm tuỳ chọn ngôn ngữ vào giao diện
Ta sẽ thêm 2 đường dẫn lựa chọn ngôn ngữ vào header trong layout.
Đầu tiên, ta thêm vào module LayoutView, hàm new_locale. Hàm helper này sẽ render đường dẫn tới "/?locale=LOCALE_CODE"
defmodule LocalizationDemoWeb.LayoutView do
use LocalizationDemoWeb, :view
def new_locale(conn, locale, language_title) do
"<a href='#{Routes.page_path(conn, :index, locale: locale)}'>#{language_title}</a>" |> raw
end
endSau đó, sử dụng new_locale trong file layout app.html.eex để tạo 2 đường dẫn tới 2 ngôn ngữ tiếng Anh và tiếng Việt
...
<nav role="navigation">
<ul>
<li><a href="https://hexdocs.pm/phoenix/overview.html"><%= gettext "Get Started" %></a></li>
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
<li><%= link gettext("LiveDashboard"), to: Routes.live_dashboard_path(@conn, :home) %></li>
<li><%= new_locale @conn, :en, gettext("English") %></li>
<li><%= new_locale @conn, :vi, gettext("Vietnamese") %></li>
<% end %>
</ul>
</nav>Cuối cùng chạy lệnh mix gettext.extract --merge để cập nhật lại file default.pot, default.po, thêm bản dịch tiếng Việt.
#, elixir-format
#: lib/localization_demo_web/templates/layout/app.html.eex:19
msgid "English"
msgstr "Tiếng Anh"
#, elixir-format
#: lib/localization_demo_web/templates/layout/app.html.eex:20
msgid "Vietnamese"
msgstr "Tiếng Việt"Kết quả


Từ các kiến thức lượm lặt trên web, mình hệ thống lại thành hai bài viết giới thiệu về Gettext, trước hết là giúp mình hệ thống lại kiến thức, sau đó hy vọng giúp ích được gì đó cho ai đó vô tình đọc được bài viết này.
Code của app LocalizationDemo, mình đẩy lên Github để có thể tham khảo nếu cần.
Neit.