Python

индекс
250,37

Международные ягнята

Несмотря на то, что мировая культура в лице Википедии и Пола Маккартни уверяет нас, что Mary had a little lamb, на территории одной восьмой части суши продолжают считать, что на самом деле «У Мэри был ягнёнок». Кто же на самом деле был у Мэри, и как записать это на разных языках мира? Попробуем выяснить это (а также понять, что думают по этому поводу японцы) вместе с нашим любимым Python-ом и встроенным в него модулем поддержки многоязычных переводов gettext.

Приступим


Начнём с того, что напомним, что библиотека gettext используется для перевода не только программ на Python, а на многих различных языках. Он позволяет использовать в нашей программе шаблоны фраз, которые можно переводить с помощью отдельных и независимых файлов перевода. В самой программе мы, как и прежде, выводим текст сразу на экран, на диск, в логи или ещё куда-нибудь, всего лишь пометив переводимые строки особым образом; библиотека gettext же позволяет взять эти переводимые строки, наборы файлов перевода, и, при наличии подходящего для текущего языка файла перевода, подставить нужную строку.

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

Напишем для начала базовую программу (назовём её mary.py), которую мы и будем пытаться перевести на различные языки:
#!/usr/bin/python

name = _("Mary")
animal = _("lamb")

print _("%s had a little %s") % (name, animal)


При использовании модуля gettext принято помечать переводимые строки вызовом функции _(). Пока эта функция не определена (впрочем, никто не мешает нам временно определить что-нибудь наподобие_ = lambda x: x), поэтому программа даже наверняка не сможет запуститься… но нам пока и не надо.

Вы уже, наверное, подумали, что сейчас мы будем создавать новый текстовый файл с ассоциациями, в котором надо будет не забыть указать все переводимые строки из программы? В нашем случае таких строк всего 3, но в серьёзной программе их может быть намного больше…

Шаблон перевода: .pot


… вы почти угадали. Создавать файл мы будем. Но при этом воспользуемся приятной возможностью системы gettext — анализом файлов с исходниками на предмет переводимых строк. Поскольку мы их благоразумно пометили вызовом функции _() ещё до того, как этот вызов стал всерьёз использовать gettext, теперь синтаксический анализатор может их быстро собрать.

Поскольку система gettext ориентирована для использования в любых языках программирования, в её состав входит программа xgettext, способная сформировать шаблонный файл для перевода из исходников на достаточно большом количестве языков — C, C++, ObjectiveC, C#, Java, Perl, Python, PHP, Lisp… Но это в том случае, если вы не поленились поставить сам комплект программ gettext («aptitude install gettext», или как там в вашем дистрибутиве). Но мы пишем программу на Python-е, который для перевода программ самодостаточен; поэтому мы воспользуемся входящим в состав Python-а скриптом pygettext.py (или pygettext под юниксами).

Запускаем pygettext: pygettext mary.py. В одном каталоге с нашей программой появился файл messages.pot, содержащий следующее:
# SOME DEscriptIVE TITLE.
# Copyright © YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2009-10-28 01:12+MSK\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"

#: mary.py:6
msgid "Mary"
msgstr ""

#: mary.py:7
msgid "lamb"
msgstr ""

#: mary.py:10
msgid "%s had a little %s"
msgstr ""


Что это такое? Это шаблон для перевода всей нашей программы. Если у нас есть большая команда переводчиков, то мы можем дать этот шаблон каждому переводчику для каждого целевого языка, и он должен нам будет вернуть заполненный шаблон для его языка. Обычно шаблоны имеют расширение .pot, а заполненные файлы имеют расширение .po.

Синтаксис у файла достаточно прозрачный. Комментарии, пометки авторских прав на перевод, пары из оригинальных строк и переводов. Выкинем из файла всё лишнее, кроме строчки с «Content-Type:» и необходимых для перевода строк, укажем кодировку UTF-8 и напишем переводы:

Файл перевода: .po


msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"

msgid "Mary"
msgstr "Мэри"

msgid "lamb"
msgstr "ягнёнок"

msgid "%s had a little %s"
msgstr "У %s был маленький %s"


В нашем случае файл достаточно маленький и простой; будь он посложнее, было бы удобнее использовать специализированные редакторы .po-файлов, наподобие Poedit, или «специализированного редактора всего» Emacs.

Скомпилированный файл перевода: .mo


Итак, строки в нашей программе мы перевели. Зря, кстати. gettext направлен исключительно на перевод законченных готовых предложений, и перевод отдельных слов и шаблонов предложений в нём делать опасно… (например, gettext совершенно не поддерживает падежи и рода и кое-как поддерживает разве что различение единственного и множественного числа; так что, чтобы подставить вместо Мэри «Таню» или «Свету», придётся учитывать падеж для каждого возможного употребления исходного имени.) Ну да ладно — в нашем случае это не принципиально. Сейчас у нас задача в другом: подготовить файл перевода к использованию.

Использовать исходный текстовый файл было бы неудобно по соображениям производительности (для программ, в которых много переводимого текста), поэтому система gettext использует скомпилированные в специальный формат файлы. Для компиляции мы можем воспользоваться либо тулой msgfmt из комплекта gettext, либо msgfmt.py из комплекта Python (в дебианоподобных дистрибутивах она входит в состав пакета python2.5-examples). Воспользуемся второй:

msgfmt.py mary.po

Ага, видим файл mary.mo. В отличие от mary.po он уже явно не предназначен для ручного редактирования.

Структура каталогов и запуск программы


Если бы мы подготавливали программу к инсталляции в служебные директории, то мы бы создали примерно такую иерархию (в случае Debian linux): системный каталог /usr/share/locale, в нём подкаталоги для разных языков — ru, en и т.п.; в них — по каталогу LC_MESSAGES, а там уже — файл наподобие mary.mo (с максимально уникальным именем, чтобы не пересечься с другими программами). Но в нашем учебном случае мы просто сделаем подкаталог locale в нашем каталоге, создадим в нём подкаталоги ru/LC_MESSAGES, а в последний уже положим mary.mo.

Теперь наконец добавим в нашу программу поддержку gettext:

#!/usr/bin/python
import gettext

gettext.install('mary', './locale', unicode=True)

name = _("Mary")
animal = _("lamb")

print _("%s had a little %s") % (name, animal)


Что изменилось? Мы проимпортировали модуль gettext (ну, это очевидно). А ещё мы проинсталлировали в глобальное пространство программы функцию _(), которая для перевода строк в подкаталоге ./locale (второй аргумент) найдёт каталог с нашей текущей локалью (тот самый подкаталог ru), а в его подкаталоге LC_MESSAGES будет искать юникодный (третий аргумент) файл mary.mo перевода программы mary (первый аргумент).

Что имеется в виду под словом «проинсталлировали»? А то, что, после этого действия, мы можем импортировать другие модули нашей программы, и функция _() в них будет уже определена.

Запускаем нашу программу…

1:/tmp/mary> ./mary.py
У Мэри был маленький ягнёнок


Ага. Как-то так.

Бонус


Согласно Google Translate, .po-файл для японского языка будет выглядеть примерно так:
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"

msgid "Mary"
msgstr "メアリー"

msgid "lamb"
msgstr "子羊"

msgid "%s had a little %s"
msgstr "%sの%sいた"


И для нормальной поддержки японского языка (помимо русского) нам придётся поменять последнюю строку кода на
print (_("%s had a little %s") % (name, animal)).encode('UTF-8')

Проверим в работе:
1:/tmp/mary> LANG=ja_JP.UTF-8 ./mary.py
メアリーの子羊いた
+32
28 октября 2009, 02:19
41

комментарии (30)

+1
yurtaev #
Большое спасибо за статью, сразу в избранное и на дальнейшее более глубокое изучение на практике.
–13
dicos #
На мой взгляд бесполезная вещь, для интернационализации можно все слова и фразы интерфейса пользователя вынести в отдельный файл (к примеру, так сделано в punbb).
+5
crtman #
Зачем изобретать свою интернационализацию, если есть общепринятый формат, который будет удобнее и разработчику, и переводчику? Кроме того, скомпилированный файл .mo будет читаться куда быстрее просто текстового файла.
–3
dicos #
Спорный вопрос. Ведь если переводчик не будет видеть результат своего труда (вряд ли сам переводчик будет компилировать файл), то результат перевода будет удручающий (смотрите об отзывах перевода каких-нибудь пиратских игр). Решением может быть проверка корректности перевода в приложении.
Языки питон, РНР, ruby, perl имеют свои форматы словарей или хеш-массивов. Например кусок кода на РНР:
$lang_delete = array(
'Delete post'			=>	'Delete post',
'Warning'				=>	'Warning! If this is the first post in the topic, the whole topic will be deleted.',
'Delete'				=>	'Delete',	// The submit button
'Post del redirect'		=>	'Post deleted. Redirecting …',
'Topic del redirect'		=>	'Topic deleted. Redirecting …'

Переводчики настолько глупы, что после объяснения что переводить они впадут в ступор?
P.S. Если говорить про питон, то неужели скомпелированный файл .mo будет потреблять меньше ресурсов по сравнению с .pyc?
+1
Honeyman #
Просто в этом случае придётся изобретать свой велосипед по хранению данных, переводу на разные языки, определению локалей… да даже по поиску в исходниках строк, которые надо переводить.

Придётся изобретать свой формат/процедуру обмена данными с переводчиком — при том что для .po существует тот же Poedit, а в нём, например, существует Translation memory (которым, в том или ином виде, будет вынужден пользоваться любой переводчик для значительных объёмов переводимых данных).

В случае escapable данных для .po-файлов этим займётся специально обученный редактор; при переводе же «прямо в исходниках» переводчику, возможно, придётся объяснять совершенно не нужные ему правила эскейпинга для используемого языка программирования.

Не, я нисколько не против — можно изобрести и реализовать любой формат и инфраструктуру перевода — хоть в SQL-е хранить XML-документы с соответствиями фраз переводам.
А можно взять уже готовый фреймворк и сэкономленное время потратить на улучшение бизнес-логики.
0
Honeyman #
Кстати, обратите внимание, как делаются переводы в опенсорс-проектах (в том же Debian). Есть команды переводчиков на разные языки, и они переводят все программы; они вполне получают feedback, ибо способны запускать эти программы и видеть, в какой момент там появляется соответствующее значение; но требовать от них знания особенностей синтаксиса C, C++, Perl, Python, TCL, Lua, bash, C#, Java, Ruby, PHP… было бы неразумно. У них есть более важные дела. Программы переводить.
0
dicos #
Соглашусь с вами, видимо до таких проектов еще не дорос, чтобы о них говорить.
0
crtman #
Немного опоздал с ответом, но все же напишу.

Да, действительно можно хранить строки в словаре и написать функцию, которая будет получать переведенную строку по ключу, можно добавить поддержку загрузки разных файлов для разных локалей, но зачем, если все это уже умеет gettext?:)

Да и, как упоминалось выше, переводчик не обязан знать язык, на котором программа, да и вообще он может вообще не быть программистом. Кроме того, потенциальный переводчик наверняка пользуется Poedit или каким-нибудь текстовым редактором с бандлом или плагином для работы с gettext, а тут ему придется работать с неизвестным форматом и возможно даже в другой программе.

Получается, что велосипед не выгоден и программисту (время на написание/отловку багов/дописывание забытых фич), и переводчику (непривычный синтаксис и среда)
0
alexey_uzhva #
Подобный подход таит в себе несколько подводных камней:

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

Во-вторых, когда строки вынесены, идентификаторы придуманы, а вы вдруг хотите поменять то или иное сообщение, то вам необходимо проследить все использования этого идентификатора так, чтобы изменение этого сообщения не повлекло негативного side-effect-а в других частях программы. Это, помимо дополнительной работы, снижает гибкость подсистемы перевода и заставляет программиста заниматься дополнительной рутиной, отвлекаясь от основной задачи и тратя драгоценное время и внимание впустую.

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

В итоге программист занимается программированием, а переводчик — переводом, не мешая друг другу.
+1
lanabel #
Gettext действительно полезная вещь. Сейчас как раз занимаюсь интернационализацией одной своей программки и пока столкнулась только с одной неприятной вещью, как раз указанное автором «перевод отдельных слов и шаблонов предложений в нём делать опасно». Когда одно и то же английское слово в разных местах интерфейса надо по разному перевести на русский (например, name — имя и название) возникает проблема. Пока выкручиваюсь, добавляя в текст програмы какие-либо различия для этих слов (например name1 и name2) и создавая отдельный файл переводов для английского, где они снова сводятся в одно.
+1
siasia #
Gettext достаточно гибок, чтобы можно было обработать и этот случай.

В таких случаях используется контекстный перевод. Реализуется дополнительный метод(например _с()), который принимает фразы в формате "|" разделитель для контекста можно выбрать естественно любой. Затем, если вы пользуетесь xgettext неполохо написать небольшой скрипт, который перед передачей питоньих файлов в xgettext заменяет все вызовы _с() на _() и обратно после прохода xgettext(я для этого использую sed), можно просто бэкапить файл рядом и переписывать потом его назад.

В результате в pot и po файлы попадают строки с контекстом, которые вы уже переводите в соответствующем контексте. Например вот строки для имени личности и имени какой-либо другой сущности: «name|person» «name|enitity», которые можно перевести на русский по-разному.

Опять же использовать дополнительный метод или нет это уже дело вкуса. Как правило он нужен только для того, чтобы вырезать контекст из непереведённых фраз так, чтобы он не попадал в юзер интерфейс. Опять же это накладывает оверхэд из-за того, что требуется препроцессить каждую строку помеченную для перевода таким образом. Как вариант можно не реализовывать этот метод, а просто переводить английские фразы с контекстом в те же самые фразы без контекста.
0
siasia #
извините забыл, что тэги обрабатываются:
… который принимает фразы в формате "<phrase>|<context>" разделитель…
0
lanabel #
Ну, в принципе ваш вариант «без реализации метода» я и использую, только не догадалась до такого красивого синтаксиса :) Пожалуй, можно и переделать, пока не поздно — так выглядит логичнее и удобнее.
0
siasia #
Кстати как бы я отрицательно я не относился к Wordpress, но это решение я нашёл именно в его коде.
0
alexey_uzhva #
gettext еще более гибок, чем вы считаете. У него есть готовая функция на этот счет:
gettext.dgettext(domain, message)
Like gettext(), but look the message up in the specified domain.

Естесственно, ее можно забиндить на то же _d(), и это займет ровно одну строчку кода.
+3
cblp #
Так что же всё-таки делать с падежами?
+2
el777 #
… и с числами. Знаю, есть поддержка, но если бы вы привели пример, было бы совсем хорошо.
+2
Webchemist #
Есть возможность указания вида фразы в единственном и множественном числе для одного варианта перевода. Поддержки падежей нет, gettext не знает про грамматику и синтаксис русского или любого другого языка. Переводимой единицей для gettext является не слово, а фраза целиком. Частично можно реализовать, используя дополнительно pytils.
0
el777 #
Спасибо за pytils — интересная вещь.
Не в курсе, в ней можно так же в отдельный файл вынести переводы?
0
Webchemist #
На сколько я помню — нет. Документация в репозитории проекта — посмотрите.
+2
alexey_uzhva #
Сейчас некогда, но если тема актуальна, то ближе к выходным могу сделать статью о том, как при помощи того же gettext управляться с падежами, не привлекая для этого сторонние утилиты.

Думаете, подобное руководство будет востребовано?
0
lanabel #
Думаю, да, раз уж эта статья с самыми основами попала на главную :) Подробностей про gettext в виде статей не слишком много, больше крупинки информации по форумам, отсылки в гугл и документацию. Вот тут говорят — надо переводить фразами, но есть ведь меню и заголовки таблиц, где без падежей ещё кое-как, но без родов бывает тяжко. Есть, правда, вариант делать исходники на языке со сложной грамматикой (падежами, родами и т.д.), а потом уже переводить на английский. Но, например, в опенсорсе это не прокатит 100%. Есть возможность использовать «контекст», как выше предлагает siasia, но если есть более красивое решение — это очень интересно.
0
kmike #
Было бы очень полезно и интересно.
0
catsmile #
Как-то странно гуглтранслейт воспринимает Mary. По идее, каной это писалось бы как メリー, без ア.
0
Chikiro #
japanesetranslator.co.uk/your-name-in-japanese/?forename=Mary&style=0

Возможно, потому что в английском произношении там дифтонг, превратившийся в японском в сочетание двух символов катаканы.
(транскрипция в лингво)
0
agorlov #
сталкивался с проблемой, в веб приложениях (или когда один процесс обрабатывает несколько запросов параллельно) gettext бесполезен, так как переключение локали идет через переменную окружения LANG и
переключение в одном потоке, переключает локаль во всех :-(
+2
Webchemist #
Видимо что-то было не так со структурой вашего приложения, а скорее всего вы что-то путаете. Gettext успешно используется во многих web-фреймворках как основной инструмент реализации i18n.
+4
neithere #
Еще можно добавить, что следующие варианты равнозначны:

print _("%s had a little %s") % (name, animal)
print _("%(name)s had a little %(animal)s") % dict(name=name, animal=animal)

При этом первый вариант сломается в некоторых языках, если порядок слов меняется из-за особенностей грамматики; второй же вариант в худшем случае будет выглядеть коряво, но смысл сохранится.
0
Honeyman #
Угу. Но, разумеется, переводить уже надо будет строку "%(name)s had a little %(animal)s".

Вообще, это начинает быть полезно с того момента, как появляются те самые ситуации, «если порядок слов меняется из-за особенностей грамматики». Поэтому к абсолютному разделению труда программиста и переводчика стремиться особо не стоит — для взаимного удобства будет лучше, если они сотрудничают и способны влиять на решения друг друга. Чтобы переводчик мог сообщить программисту о проблеме с порядком слов, падежами, ещё какими особенностями языка (возможно, совершенно неизвестными программисту).
0
neithere #
В идеале — да, надо сотрудничать. На практике часто сначала пишется программа (на английском и, возможно, с переводом на второй язык, родной для автора), а затем она постепенно обрастает переводами. Чтобы из-за очередного перевода не пришлось править код программы, разумно просто использовать словари во всех случаях, когда переменных несколько.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.