Интернационализация приложения на Elixir
Одна из основных возможностей приложения Forza Football — отправка подписчикам push-уведомлений о ключевых событиях футбольных матчей (подробнее об этих уведомлениях ). Поскольку приложением пользуются люди по всему миру, возникает задача перевода push-уведомлений на множество различных языков. В данной статье я расскажу, как решить эту проблему в Elixir (именно на нём написана push-система нашего приложения) и о том, какие для этого понадобятся инструменты.
Gettext для Elixir
Основной инструмент, который мы используем в Elixir для переводов, — Gettext. Он представляет собой реализацию для приложений, написанных на Elixir. Если говорить коротко, то Gettext — это система интернационализации программного обеспечения, в основе которой лежит процесс извлечения строк из исходного кода и помещение их в файлы перевода. Все подробности об особенностях реализации Gettext для Elixir можно узнать из файла README.
Данные уведомлений для отправки пользователям формируются следующим кодом:
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-уведомлений для своего приложения, обеспечивающую хорошую масштабируемость при изменении количества языков перевода и объёма текста.