Интернационализация приложения на Elixir

Одна из основных возможностей приложения Forza Football — отправка подписчикам push-уведомлений о ключевых событиях футбольных матчей (подробнее об этих уведомлениях здесь). Поскольку приложением пользуются люди по всему миру, возникает задача перевода push-уведомлений на множество различных языков. В данной статье я расскажу, как решить эту проблему в Elixir (именно на нём написана push-система нашего приложения) и о том, какие для этого понадобятся инструменты.

Gettext для Elixir

Основной инструмент, который мы используем в Elixir для переводов, — Gettext. Он представляет собой реализацию библиотеки Gettext операционной системы GNU для приложений, написанных на 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 — веб-сайтом с удобным интерфейсом для осуществления перевода и редактирования текста и поддержкой аутсорсинга лингвистических услуг. Платформа взаимодействует с несколькими переводческими сервисами, которые позволяют заказать переводы у носителей языка.

В большинстве случаев мы прибегаем к использованию внешних сервисов для перевода, однако некоторые сотрудники нашей компании являются носителями различных языков. С помощью Transifex они тоже могут работать над переводами, ведь интерфейс сайта прост и интуитивно понятен даже несведущему в компьютерной технике пользователю.

Интерфейс 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 и подготавливаем его содержимое к переводу: добавляем и удаляем строки или обновляем файл целиком.

Загрузка 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-уведомлений для своего приложения, обеспечивающую хорошую масштабируемость при изменении количества языков перевода и объёма текста.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.