Интернационализация приложения на Elixir
Одна из основных возможностей приложения Forza Football — отправка подписчикам push-уведомлений о ключевых событиях футбольных матчей (подробнее об этих уведомлениях
Gettext для Elixir
Основной инструмент, который мы используем в Elixir для переводов, — Gettext. Он представляет собой реализацию
Данные уведомлений для отправки пользователям формируются следующим кодом:
import Pushboy.Gettext, only: [gettext: 2]
gettext "%{minute}′ Red Card - %{player_name} (%{team_name})",
minute: minute,
player_name: player_name,
team_name: team_name
Приложение, посылающее push-уведомления, мы назвали Pushboy. gettext/2
— макрос, автоматически определяемый в модуле Pushboy.Gettext
с помощью Gettext:
defmodule Pushboy.Gettext do
use Gettext, otp_app: :pushboy
end
Переводы будут идентифицироваться по строке ("%{minute}′ Red Card ..."
); она же поможет Gettext во время выполнения приложения отыскать перевод для указанного языка.
Извлечение переводов
Разобравшись в тонкостях процесса интернационализации, можно приступить к извлечению из исходного кода строк, перевод которых необходимо выполнить. Для этого в Gettext имеется специальная mix-задача:
$ mix gettext.extract
Extracted priv/gettext/default.pot
Задача считывает все вызовы к макросу Gettext (gettext/2
в примере выше) во время компиляции и записывает их в файлы с расширением .pot, которые выглядят следующим образом:
# This would go in priv/gettext/default.pot
#: lib/pushboy/event/red_card.ex:86
msgid "%{minute}′ Red Card - %{player_name} (%{team_name})"
msgstr ""
POT-файлы — это шаблоны, в которых хранится список строк для перевода. В приведённом примере msgid
— идентификатор строки для перевода, а msgstr
— сам перевод. Так как POT-файлы не создаются отдельно для каждого языка, то переводы хранятся не в них, а в файлах с расширением .po
, располагающихся в директории определённого языка. Если перевести эту строку, например, на итальянский, то файл будет выглядеть так:
# This would go in priv/gettext/it/LC_MESSAGES/default.po
#: lib/pushboy/event/red_card.ex:86
msgid "%{minute}′ Red Card - %{player_name} (%{team_name})"
msgstr "%{minute}′ Cartellino rosso - %{player_name} (%{team_name})"
На этапе компиляции Gettext сканирует такие PO-файлы, пытаясь как можно быстрее отыскать перевод нужных строк. Всё, что находится между %{
и }
, — интерполяционные переменные: они не подлежат переводу и во время работы приложения заменяются на динамические значения.
После создания POT- и PO-файлов перевод программно будет выглядеть так:
iex> import Pushboy.Gettext, only: [gettext: 2]
iex> Gettext.put_locale(Pushboy.Gettext, "it")
iex> gettext "%{minute}′ Red Card - %{player_name} (%{team_name})",
...> minute: 38,
...> player_name: "Cristiano Ronaldo",
...> team_name: "Real Madrid"
"38′ Cartellino rosso - Cristiano Ronaldo (Real Madrid)"
Перевод на несколько языков
С интернационализацией приложения мы разобрались, но каким образом осуществляется перевод? Приложение поддерживает множество различных языков, но члены нашей команды владеют только некоторыми из них, в связи с чем вариант выполнения перевода штатными сотрудниками отпадает.
Поэтому мы пользуемся
В большинстве случаев мы прибегаем к использованию внешних сервисов для перевода, однако некоторые сотрудники нашей компании являются носителями различных языков. С помощью Transifex они тоже могут работать над переводами, ведь интерфейс сайта прост и интуитивно понятен даже несведущему в компьютерной технике пользователю.

Интеграция с Gettext
Transifex взаимодействует со многими платформами для локализации приложений, в том числе и с Gettext. Загрузив PO/POT-файлы на Transifex, можно потом скачать их переводы в формате .po
. В Transifex существует понятие ресурсов, то есть различных областей перевода (например, уведомления — это один ресурс, сообщения об ошибках — другой и т. п.). Ресурсы Transifex удачно соотносятся с областями в Gettext: для извлечения переведённого текста из определённой области в Gettext можно использовать макрос dgettext/3
(gettext/2
использует область default
), после чего каждый домен помещается в отдельный PO(T)-файл (как default.pot в примере выше.
В общих чертах, порядок действий таков. Как было показано выше, сначала из исходного кода в POT-файл извлекаем необходимые для перевода строки с помощью mix-задачи mix gettext.extract
. Затем загружаем POT-файл на Transifex и подготавливаем его содержимое к переводу: добавляем и удаляем строки или обновляем файл целиком.

Ждём, когда перевод будет готов. Как только перевод будет готов, скачиваем с Transifex PO-файлы для всех необходимых языков.
Инструменты командной строки
Описанная выше схема справляется со своими задачами, но пользоваться ей не так удобно: многие действия приходится осуществлять вручную, чем больше количество языков локализации, тем медленнее она будет работать. К счастью, в Transifex имеется замечательный инструмент командной строки под названием tx
, позволяющий оправлять строки на перевод сразу же после запуска команды mix gettext.extract
и помещать переведённые строки в PO-файлы, как только те будут доступны.
Настроим tx
, отредактировав файл .tx/config
, расположенный в корневом каталоге приложения. Получаем следующее:
[main]
host = https://www.transifex.com
[pushboy.default]
type = PO
source_file = priv/gettext/default.pot
source_lang = en
file_filter = priv/gettext/<lang>/LC_MESSAGES/default.po
В этом файле мы создаём конфигурации ресурса pushboy.default
(который соотносится с областью default
из Gettext) и передаём инструменту tx
следующую информацию:
-
форматы используемых файлов (PO и POT)
-
местонахождение исходного текста (
priv/gettext/default.pot
) -
язык оригинала (English)
-
загруженные переводы должны находиться по адресу
priv/gettext
в каталоге с названием языка перевода (<lang>
заменяется черезtx
)
После приведения файла настроек к такому виду, строки можно будет отправлять на перевод командой $tx push --source
(--source
проследит, чтобы действия производились только с обновлённым POT-файлом), а получать переведённые строки — командой tx pull
.
Редактирование PO-файлов, загруженных с Transifex
В нашей компании существуют свои требования к оформлению кода, которым не удовлетворяют PO-файлы, созданные платформой Transifex. Для таких случаев в Gettext имеются специальные инструменты для парсинга и редактирования PO/POT-файлов. Достаточно выполнить mix-задачу mix translations.pull
, и она прекратит взаимодействие с tx
.
Сначала вызовется tx pull
, затем выполнится перебор всех PO-файлов и их форматирование согласно указанным правилам:
defp reformat_po_file(path) do
reformatted_po =
path
|> Gettext.PO.parse_file!()
|> reformat_headers()
|> remove_top_of_the_file_comments()
File.write!(path, Gettext.PO.dump(reformatted_po))
end
# We get rid of the Last-Translator header
defp reformat_headers(%Gettext.PO{headers: headers} = po) do
new_headers = Enum.reject(headers, &String.starts_with?(&1, "Last-Translator"))
%Gettext.PO{po | headers: new_headers}
end
# We get rid of comments that Transifex leaves at the top of the PO file
defp remove_top_of_the_file_comments(%Gettext.PO{} = po) do
%Gettext.PO{po | top_of_the_file_comments: []}
end
Окончательная схема работы приложения
Схема состоит из следующих действий:
-
закончив работу над исходным кодом, запускаем
mix gettext.extract
, чтобы получить набор обновлённых POT-файлов -
запускаем
tx push --source
, чтобы загрузить в Transifex обновлённые строки для перевода -
ждём, когда будет выполнен перевод обновлённых строк
-
запускаем
mix translations.pull
и получаем обновлённые переводы
Такая схема работы очень удобна, ведь она позволяет обновлять, отправлять и извлекать переводы программно, а также легко масштабировать приложение после добавления новых языков (схема остаётся прежней, нужно лишь заказать переводы для добавленных языков в Transifex).
Заключение
Теперь вы знаете удобную и быстро работающую схему осуществления перевода на несколько языков push-уведомлений для своего приложения, обеспечивающую хорошую масштабируемость при изменении количества языков перевода и объёма текста.