Автоматический переводчик gettext po-файлов

Наиболее распространённый способ переводов программного обеспечения и веб-сайтов на разные языки – с помощью библиотеки gettext.
Для тех, кто не в курсе, поясню как она работает:

  1. В коде программы/сайта, везде где вам нужно выводить некий текст, меняющийся в зависимости от языка, вы пишете не просто строковые константы, типа “Да” и “Нет”, а используете некую специальную функцию вроде _(“Да”), _(“Нет”), или t(“Да”), t(“Нет”), или даже [`Да`], [`Нет`].
  2. Вы запускаете специальный скрипт, который сканирует весь ваш исходный код, находит такие вот специфические вызовы функции, и составляет из них *.po – файл.
  3. Вы с помощью некоего редактора, например poedit добавляете в файл *.po переводы всех строк которые там есть на некий язык, и сохраняете файл как, например, en_US.po, после чего генерируете соответствующий *.mo-файл, в данном случае en_US.mo.
  4. Вы указываете вашей программе/сайту путь к файлу перевода (например, берёте из сессии или из данных браузера язык, который выбран у пользователя, и исходя из этого, выбираете ru_RU.mo или en_US.mo
  5. Программа/сайт автоматически заменяет ваши t(“Да”) и t(“Нет”) на слова написанные на том языке, который был выбран.

Как же перевести po-файл автоматически, если качество перевода для вас не важно, но нужно чтобы сайт/приложение начало выглядеть как многоязычное для вашего заказчика?

Для демонстрации многоязычного интерфейса на языках, которых я не знаю (финский, немецкий), но также их не знает и заказчик, мне понадобилось создать файлы переводов для этих языков. В самом файле было около 1500 различных строк, и, начав переводить файл с помощью google translate (каждое слово вручную), я довольно быстро понял, что эта процедура затянется на несколько дней.
А если я всё равно перевожу строки через google translate, и, при этом, не вдумываюсь в их смысл, почему бы не сделать этот процесс автоматическим?

Инструменты

Для решения задачи был выбран Python3, библиотека для работы с *.po и *.mo файлами polib, и библиотека yandex.translate для работы с яндекс-переводчиком. Яндекс-переводчик был выбран в связи с тем, что его API гораздо проще в использовании нежели API гугл-транслейта. Также, для создания консольного скрипта, я использовал библиотеку click

Принцип действия

Для работы модулю требуется указать исходный файл *.po из которого нужно считать какой-то уже существующий перевод (то есть фразы и слова в нём должны иметь значения, пусть даже на том же самом языке). Также, нужно указать языки – с какого и на какой переводить. После чего можно, запустив перевод, и дождавшись его окончания, сохранить результат в виде *.po-файла, а также в *.mo-файл.

Код:

Основной класс для работы с переводом:

import polib
from yandex_translate import YandexTranslate
 
class Translator:
    """
    Класс для перевода po-файлов
    """
    def __init__(self, yandex_key, src_lang=None, dest_lang=None, src_po_file=None):
        """
        Конструктор
        :param yandex_key: ключ для доступа к API яндекс-переводчика
        :param src_lang: исходный язык
        :param dest_lang: язык назначения
        :param src_po_file: исходный po-файл
        :return:
        """
        self.yandex_key = yandex_key
        self.src_lang = src_lang
        self.dest_lang = dest_lang
        self.yandex_translate = YandexTranslate(self.yandex_key)
        if src_po_file is not None:
            self.open_po_fle(src_po_file)
 
    def open_po_fle(self, po_filename):
        """
        Открыть po-файл
        :param po_filename:
        :return:
        """
        self.po = polib.pofile(po_filename)
 
    def _translate_str(self, text, src_lang, dest_lang, return_src_if_empty_result=True, need_print=False):
        """
        Перевести текстовую строку (приватный метод)
        :param text: текст, который нужно перевести
        :param src_lang: исходный язык
        :param dest_lang: конечный язык
        :param return_src_if_empty_result: вернуть исходную строку, если перевод не найден?
        :param need_print: Выводить на экран информацию о результате перевода (для отладки)
        :return: переведённая строка (или исходная, если перевод не найден, и return_src_if_empty_result==True)
        """
        tr = self.yandex_translate.translate(text, "{}-{}".format(self.src_lang, self.dest_lang))
        if tr['code'] == 200 and len(tr['text']) and tr['text'][0]:
            tr_text = tr['text'][0]
        else:
            tr_text = ""
        if need_print:
            print(text + " => " + tr_text)
        if not tr_text and return_src_if_empty_result:
            tr_text = text
        return tr_text
 
    def go_translate(self, src_lang=None, dest_lang=None, break_on=0):
        """
        Запустить перевод po-файла
        :param src_lang: исходный язык
        :param dest_lang: конечный язык
        :param break_on: номер строки, на которой нужно остановить процесс, используется для отладки
        :return:
        """
        if src_lang is None:
            src_lang = self.src_lang
        if dest_lang is None:
            dest_lang = self.dest_lang
        count = len(self.po)
        pos = 0
        for item in self.po:
            if break_on and break_on == pos:
                break
            pos += 1
            percent = int(pos * 100 / count)
            print("Percent:" + str(percent) + "%")
            if item.msgstr:
                item.msgstr = self._translate_str(item.msgstr, src_lang, dest_lang, True, True)
            if item.msgstr_plural:
                for num in item.msgstr_plural:
                    item.msgstr_plural[num] = self._translate_str(item.msgstr_plural[num], True, True)
 
    def save_po_file(self, dest_po_file=None):
        """
        Сохранить результирующий po-файл
        :param dest_po_file: Имя файла
        :return:
        """
        if dest_po_file is None:
            dest_po_file = self.dest_po_file
        self.po.save(dest_po_file)
 
    def save_mo_file(self, dest_mo_file=None):
        """
        Сохранить mo-файл
        :param dest_mo_file: имя файла
        :return:
        """
        if dest_mo_file is None:
            dest_mo_file = self.dest_mo_file
        self.po.save_as_mofile(dest_mo_file)

Для работы классу необходим API-ключ для яндекс-переводчика. Получить его можно здесь: https://tech.yandex.ru/keys/get/?service=trnsl

Установка питона, если у вас его ещё нет (от рута):

apt-get install python3 python3-pip

Установка скрипта:

pip3 install PoTrans

Сам код можно найти по адресу https://github.com/MihanEntalpo/PoTrans

После установки вам станет доступен консольный скрипт potrans

Использование в качестве консольного инструмента:

С помощью скрипта можно сделать 2 вещи:
1) Перевод:
Перевести файл ./ru_RU.po с русского на итальянский и сохранить результат в ./it_IT.po:
(длинная хрень после ключа –key – это ключ API яндекс)

potrans translate -i ./ru_RU.po -il ru -ol it -o ./it_IT.po --key trnsl.1.1.20160516T111755Z.a0378f3552843fb4.a5ac4af82a585d1e3517f32c4c953f3f791ddb2a

2) Перевод не указывая ключ:
Сначала положим в конфигурационный файл наш ключ:

echo "trnsl.1.1.20160516T111755Z.a0378f3552843fb4.a5ac4af82a585d1e3517f32c4c953f3f791ddb2a" > ~/.config/potrans.key

И запускаем перевод уже без ключа key:

potrans translate -i ./ru_RU.po -il ru -ol it -o ./it_IT.po

3) Перевод и сохранение результатов в *.po и *.mo файлы:

potrans translate -i ./ru_RU.po -il ru -ol it -o ./it_IT.po -om ./it.IT.mo

4) Перевод файла с выводом всех обрабатываемых фраз и их переводов:

potrans translate --debug -i ./ru_RU.po -il ru -ol it -o ./it_IT.po

5) Перевод файла с использование msgid если msgstr пуст (можно использовать если в исходном файле нет перевода):

potrans translate --usemsgid -i ./ru_RU.po -il ru -ol it -o ./it_IT.po

6) Простая конвертация файла из *.po в *.mo без перевода
Сконвертировать файл ./ru_RU.po в ./ru_RU.mo

potrans convert -i ./ru_RU.po -o ./ru_RU.mo

Использование в качестве библиотеки:

from PoTrans import Translator
 
# Ключ API переводчика яндекса
yandex_key = "trnsl.1.1.20160516T111755Z.a0378f3552843fb4.a5ac4af82a585d3e3517f32c4c953f3f791dfb2a"
 
# Создаём объект переводчика
t = Translator(yandex_key, src_lang="ru",dest_lang="de")
# Загружаем po-файл
t.open_po_file("./ru_ruMessages.po")
# Запускаем перевод
t.go_translate()
# Сохраняем результат
t.save_po_file("./de_deMessages.po")

Другие библиотеки и инструменты

Справедливости ради, надо сказать, что я нашел несколько аналогичных модулей в репозитории (вот например: https://pypi.python.org/pypi/potranslate), но, все они меня не удовлетворили по одной из причин:
1) Скрипты в основном используют google-переводчик, для которого относительно сложно получить API-ключ (в сравнеии с процедурой получения ключа для яндекс-переводчика)
2) Некоторые были написаны давно, и API google-переводчика уже успело измениться, поэтому плагины теперь не работают.
3) Как показал анализ, многие из них не умеют работать с plural-ами, то есть переводить слова, у которых есть разные формы для разных количеств (например, единственное или множественное для английского языка, или 1,2,5 для русского)
4) Написаны они все поголовно на Python 2.7, а мне нужен был скрипт, написанный на Python 3
5) Перевод во всех этих модулях осуществляется из msgid в msgstr, то есть берётся исходная строка из msgid, переводится, и результат записывается в msgstr. В моём же случае берётся msgstr одного файла, а перевод записывается в msgstr другого, а также есть возможность, при отсутствии значений в msgstr брать их из msgid.

Для интересующихся, исходный код находится здесь


So, what do you think ?