Red Hot Chili Python

Интернационализация большого проекта при помощи Python + gettext

«  Клавиатурное   ::   Contents   ::   Заголовок окна в емаксе  »

Интернационализация большого проекта при помощи Python + gettext

Note

Пользуясь случаем, напомню, что мы ищем очень умных (сениор) и просто умных разработчиков на питоне (Киев). Также имеется вакансия frontend-UI-монстра. Резюме шлите на k.bx@ya.ru.

В свое время мы (Prom.ua) решили, что будем запускаться на различных языках (отличных от русского). В основном этим занимался я, потому в этой заметке буду постепенно дописывать что я и как делал.

Gettext

Для тех, кто вдруг не знает, что это такое – идея очень простая. Вместо того, чтобы просто писать:

print u"Переведи меня!"

стоит писать:

print _(u"Переведи меня!")

То есть оборачивать в специальную функцию “нижнее подчёркивание”. При выполнении эта функция (она же gettext.gettext) будет смотреть в словарь (вида ключ-значение) и искать фразу “Переведи меня!” и, когда найдёт, подставит перевод.

Но откуда берется фраза и перевод?

Po-, pot-, mo-файлы

Специальный сборщик (в основном, это Babel) проходится по вашему проекту и собирает из всех файлов (ну, точнее, указанные вами) все фразы, заключенные в функцию _(), gettext() и еще некоторые.

Bebel собирает все эти фразы в pot-файл. Pot-файл – это большой файл, в котором хранятся только фразы из вашего проекты (формат такой же, как и у po-файла, в котором еще и переводы, но здесь их нет).

Затем, для каждого языка у вас есть .po-файл. В нем содержатся переводы фраз (с ними работают переводчики). Кроме того, .mo-файл – скомпилированный .po-файл, с которым ваше приложение и работает.

Вот кусок po-файла:

#: uaprom/templates/portlets_cabinet.mako:558
msgid "Служба поддержки "
msgstr "Destek Merkezi"

#: uaprom/templates/portlets_cabinet.mako:560
#: uaprom/templates/cabinet/cabinet_base.mako:170
msgid "Задайте вопрос"
msgstr "Soru sorun"

Более подробно вы можете почитать на страничке проекта Babel, еще где-то (о gettext и po-файлах – поищите статьи на Хабре, и в Гугле наверняка найдется немало информации), а я же хотел сосредоточиться на практической составляющей именно нашего проекта.

Перефигачить весь проект (именно это слово точнее опишет несколько дней вашей работы)

Так вот, проект писали сразу на всякий случай оборачивая все фразы в _(), но естественно иногда об этом забывали. А еще часто новички делают вот такую ошибку:

# Неправильно!!
print _(u"Здравствуй, %(username)s" % dict(username="Саша"))

А надо вот так:

# Правильно!!
print _(u"Здравствуй, %(username)s") % dict(username="Саша")

Проблема еще в том, что ваш Babel эту фразу распарсит (при сборе фраз из проекта, python setup.py extract_messages), но при этом перевод подставляться не будет, потому что на вход функции _() придет не u"Здравствуй, %(username)s", а u"Здравствуй, Саша". Очень важно это понимать, чтоб в дальнейшем не ошибаться.

Emacs

Вообще, большим помощником во всей этой эпопее с переводом мне послужил любимый инструмент редактирования всего и всея – Emacs.

Неожиданно полезной оказалась функция highlight-regexp. При её помощи вы можете ярким желтым цветом подсветить все русские буквы в файле и не пропустить ни одну из них.

../_images/emacs_highlight_regexp.png

Также понадобились неплохие умения по поиску по проекту (я пользуюсь ack-grep + ack.el). Ну и многое другое. В поиске по проекту тоже понравилась именно емаксовая искалка, которая в одном буфере открывает результаты, клацая на каждый – тот открывается в соседнем буфере. Ну и в этом же буфере результатов можно всё подсветить (русские буквы) и так далее. Давно это было, но помню что делал что-то более-менее умное, чтоб отыскать большинство фраз, не попавших в перевод, ошибок, как описана выше и т.д.

На этом этапе мне понадобилось много кофе (и знаний регулярок).

Выкинуть админку из перевода

Мы решили, что переводить админку не стоит, потому надо было её выкинуть из перевода. Это существенно упростило работу переводчиков. Но не всё так просто. Проект большой, компоненты тесно связаны друг с другом, потому, думаю, процентов 20 админки таки перевелось (ну и ничего страшного, вполне реально что пригодится).

Это я к тому, чтоб вы думали об этом. И, к примеру, если у вас есть файл каких-то общих функций – обязательно разделяйте функции админки, функции сайтов компаний и так далее.

Множественные формы

В gettext есть возможность использовать множественные формы фраз (ngettext):

count = i.get_apples_count(datetime.datetime.now())
print (ngettext(u"I have %(count)s apple",
                u"I have %(count)s apples",
                count) % dict(count=count))

Работает это примерно так:

  1. В перевод попадает фраза I have %(count)s apple (помечается, как и остальные, msgid), её множественная форма I have %(count)s apples (помечается как msg_plural).

  2. Умный переводчик (babel) в зависимости от вашего языка вставит в переводе нужное количество форм (для русского языка - 3). В переводе у вас будет (примерно):

    msgid "I have %(count)s apple"
    msgid_plural "I have %(count)s apples"
    msgstr[0] "У меня есть %(count)s яблоко"
    msgstr[1] "У меня есть %(count)s яблока"
    msgstr[2] "У меня есть %(count)s яблок"
  3. В зависимости от переменной count вы получите либо нужную фразу из msgstr[], а затем над ней сделаете % dict(). Такие вот чудеса.

Русский язык по-умолчанию

Но! В нашем проекте по-умолчанию используется русский язык, и это нормально (мне сложно поверить, что проект бы в принципе выжил, если бы делался на английском, а потом переводился). Но авторы gettext с нами не согласны, потому предполагают, что все проекты пишутся изначально на английском. Отсюда и начинается косяк с вышеописанным ngettext.

Заключается он в том, что (поскольку дефолт-язый – английский) – ngettext принимает два аргумента (две формы). А в русском языке их три. Как выйти из этой ситуации?

Наше решение таково, что мы пишем две формы (для единственного и множественного числа), а затем переводим с русского на русский. То есть если вы не перевели – у вас будет косяк типа:

У меня есть 3 яблок

Но это не так уж и страшно, и все что вам надо сделать – перевести эту фразу (на три формы).

Редакторы po-файлов

Самым приятным для десктопа показался poedit. В нем есть поиск (правда есть и пару неприятных багов), он быстр и так далее. Но проблема с десктопными редакторами в том, что вам (переводчикам) сложно работать с системами контроля версий. Потому стоит искать веб-редактор (по-хорошему).

Взаимодействие (совместная работа) с переводчиками и контроль версий

Переводчику очень сложно понять что такое svn (ну это еще ладно, там тебе коммит да апдейт), mercurial (здесь вообще сложно), и самое ужасное – что делать когда произошел конфликт (одновременно я подредактировал po-файл и переводчик, а потом обновил).

Розетта

Потому мы взяли систему django-rosetta. Это такое джанго-приложение для редактирования po-файлов. Довольно приятное, но со своими косяками.

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

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

Также я написал небольшой скрипт (с использованием inotify), который следит за po-файлами, и в случае изменения – компилирует po-файл в mo-файл и делает коммит. Таким образом переводчик уже через 10 секунд после изменения в розетте может посмотреть на результат на внутренней версии проекта (специально для этого языка).

В розетте присутствует проблема с одновременным переводом (не умеет она), причем если вы что-то долго переводили и нажимаете “сохранить”, а файл изменился, она просто скажет “простите, файл уже изменился, ничего не сохраню”. Переводчик негодует)

Также какие-то баги с множественными формами, но пока я особо не копал.

Интернационализация изображений (картинки с русским текстом)

Отдельная тема – перевод картинок. Для начала стоит сказать, что лучше вообще избегать картинок с русскоязычным текстом как таковых. А если есть – посмотреть, может удастся их просто сверстать (текст поверх картинки). К примеру, вот так:

../_images/images_translation.png

Здесь текст на картинке – действительно текст, и он в po-файле.

Но иногда таки надо делать разные картинки на разных сайтах, и тут уже либо дизайнеру приходится потеть, либо переводчикам. У нас же (в коде) просто функция, которая сама подставляет нужный каталог.

Note

Следует отметить, что надо различать картинки для языка и картинки для страны (домена). То есть у нас есть в картинках папки i18n и i18n-domain. Соответственно в i18n папка es для испанского, а в i18n-domain много папок для стран Чили (cl), Аргентина (ar), Мексика (mx) и так далее.

Перевод больших текстов

Перевод больших текстов – при работе с po-файлами это боль. Не потому что их сложнее переводить, чем маленькие, а потому, что когда текст даже слегка поменяется – переводчик не увидит, что конкретно поменялось.

Это одна из проблем, которую я собираюсь решить в своем толстом редакторе po-файлов (пока на ранней стадии разработки).

Перевод словарей/регионов

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

Но! Есть украинский язык. И в его случае – у нас две версии одного сайта. И делать две таблички с курсами валют или списком стран/регионов просто глупо, потому был написан скрипт вытягивания этого списка в po-файл, и отдельный скрипт, который объединяет (делает merge) несколько po-файлов в mo-файл (эта проблема также будет решена в толстом po-редакторе).

Всегда писать %(something)s

Лучше не используйте %s вовсе. Переводчику очень сложно каждый раз догадываться, что это за загадочный %s (переводчику вообще сложно понять что эти странные символы означают). Более того, ему ведь надо будет еще найти в переводе фразу, которая будет подставлена (благо обычно она где-то рядом, так как babel вытягивает фразы поочередно сверху вниз). Потому вместо:

print _(u"Hello, %s") % user.name

Пишите:

print _(u"Hello, %(username)s") % dict(username=user.name)

Note

В принципе, этого часто все равно не достаточно, потому что переводчику сложно даже при наличии %(link_1)s понять, каков контекст. Как вариант и идея в моей голове – сделать свои собственные строки (ну, надстройку), которые бы имели примерно такой формат:

print _(u"Перейдите по %(link|ссылке)s") % (
    dict(
        link=lambda link_1: h.link_to(link_1, h.url_for('home'))))

То есть чтоб вы могли писать слово прямо в этой же фразе. Посмотрим, если найдётся время еще раз сильно над этим подумаю.

Турецкий и плохо составленные фразы

Турецкий язык таков, что я даже не понял каков он. Но главное я уяснил – практически всегда следует делать так, чтоб фраза попала в перевод полностью. То есть:

# Плохо!
print _(u"Акция действительна с ") + date_from + _(" по ") + date_to

Казалось бы, ничего сложного, чтоб запихнуть всю строку и подставить через % dict(). Но все будет гораздо сложнее, если у вас имеются формы (а там выбор даты и так далее), потому просто будьте бдительны.

Перевод JavaScript

С этим у нас проблем не было, просто JavaScript генерируется прямо в Mako, а библиотечный JavaScript не содержит русскоязычных фраз.

Ошибки переводчиков

Переводчикам свойственно ошибаться, особенно во всяких %s / %(username)s‘ах. И это нормально (попробуйте вы не ошибаться). С этим почти удачно справляется babel, когда вы делаете python setup.py compile_catalog -l tr – он выведет на какой строке, к примеру, у вас в оригинале и переводе несоответствие %(something)s. Но babel работает не всегда (для чего-то я даже патч слал чтоб улучшить, не знаю, влили ли его).

Розетта тоже подсвечивает ошибки, но опыт показал что тоже ошибки таки есть.

Так вот эти ошибки ловить сложнее всего, точнее главное помнить о том, что они вполне возможны, тогда и ловить их будет проще. Учитывая то, что у нас фреймворк pylons, у которого веб-обработчик ошибок не понимает юникод (когда вы видите красивую страничку с ошибками, русский текст превращен в какую-то левую кодировку, уже не помню какую).

Потому даже понять что это за фраза и почему в ней ошибка – надо проявить немного смекалки (ну и с опытом приходит).

Фразы, которые не нужно переводить

Есть много фраз, которые переводить не следует. К примеру, попало много фраз из админки, но фразы эти находятся в каком-то общем файле с функциями, а разнести у вас нету времени (да и каждый раз перегенерировать файлы перевода – долго очень это всё).

Потому переводчики не знают что с этими фразами делать.

Пробелами их не переведёшь (потому что несоответствие %s будет), оригинал не скопируешь (переводы обновляются, придется еще и оригиналы копировать). В общем, это проблема (и её тоже буду решать, надо придумать обязательно какие-то флажки, чтоб переводчик складывал ненужные фразы по группам).

Мердж между ветками

В один прекрасный момент вы решаете, что пора обновить версию (точнее начать перевод) интернационального проекта на более свежую ветку. В этом случае необходимо просто скопировать переводы в новую ветку, и накатить на них python setup.py update_catalog. Но, все же, хотелось бы более умного мерджа и постепенного. Бывает, что фраза просто немного поменялась, а бабель не поймёт, что это она же, потому что она уже на другой линии, потому просто забудет ваш перевод и т.д. Как обычно, поле для работы :-)

Русский фильтр

Ну вот, всё же, мы добрались до продакшна. Что делать с фразами, которые не переведены? Как их отслеживать? И тут то, что русский язык для нас первоначален, является плюсом.

Мы просто сделали обертку над выводом странички (и посылкой писем), который еще грепает по русскоязычным символам, и в случае нахождения такового просто шлёт красивое письмо:

../_images/russian_filter.png

Обезьяна намекает, что этого здесь быть не должно. Из минусов этого решения – есть компании (к примеру, обучение русскому языку), у которых русские буквы на сайте – нормально. Ну что ж, 50-60 лишних писем в день я таки получаю. Если будет очень много – как-то по-умному буду не в письма а в БД (к примеру, MongoDB) запихивать и клеить (чтоб не повторялись), но пока это лишние трудозатраты.

Сбор фраз (где встречаются)

Из неудачных идей – когда-то давно, когда мне надоели вечные вопросы переводчиков: “где на сайте встречается эта фраза”, я сделал штуку, которая (в дебаг-версии) оборачивает _() в специальную штуку и запоминает (в MongoDB), на какой страничке эта фраза встречалась. Переводчики и так лазят по локальной версии своего сайта, потому большинство фраз можно отловить таким образом.

Практика показала, что идея скорее ненужная (но мне было интересно её реализовать, все равно это не отняло очень много времени).

Отслеживание изменений

И самое главное, чего не хватает (и я буду это делать в своем переводчике) – отслеживание изменений. Благодаря ему я могу смотреть, что когда переводил тот или иной человек, что добавилось при обновлении (сборке фраз со всего проекта), а главное – что добавилось при обыкновенных коммитах тех или иных людей. Именно так можно сразу ловить программиста “на горячем” и спросить: “и как, по-твоему, переводчик будет работать с фразой “а также вы”? Как он поймёт контекст?”.

Выводы

Я понял, что больших проектов, которые переведены на несколько языков не так уж и много. То есть именно таких, в которых куча текстов, разнообразных “тонких” фич и в которых очень быстрый этап разработки.

Ну, либо они есть, но инструменты никакие открытые они не допиливают. Придется немного самому еще поработать чтоб сделать инструменты удобнее.

Ах да, вот, собственно, некоторые сайты (возможно пополнение):

п.с.

Очень непривычно сейчас писать этот текст, глаз сам реагирует на то, что он не обернут в _(u"") :-)

«  Клавиатурное   ::   Contents   ::   Заголовок окна в емаксе  »

blog comments powered by Disqus