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

    Несмотря на то, что мировая культура в лице Википедии и Пола Маккартни уверяет нас, что 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 (C) 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
    メアリーの子羊いた
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 30

      +1
      Большое спасибо за статью, сразу в избранное и на дальнейшее более глубокое изучение на практике.
        –13
        На мой взгляд бесполезная вещь, для интернационализации можно все слова и фразы интерфейса пользователя вынести в отдельный файл (к примеру, так сделано в punbb).
          +5
          Зачем изобретать свою интернационализацию, если есть общепринятый формат, который будет удобнее и разработчику, и переводчику? Кроме того, скомпилированный файл .mo будет читаться куда быстрее просто текстового файла.
            –3
            Спорный вопрос. Ведь если переводчик не будет видеть результат своего труда (вряд ли сам переводчик будет компилировать файл), то результат перевода будет удручающий (смотрите об отзывах перевода каких-нибудь пиратских игр). Решением может быть проверка корректности перевода в приложении.
            Языки питон, РНР, 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
              Просто в этом случае придётся изобретать свой велосипед по хранению данных, переводу на разные языки, определению локалей… да даже по поиску в исходниках строк, которые надо переводить.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                          Думаете, подобное руководство будет востребовано?
                            0
                            Думаю, да, раз уж эта статья с самыми основами попала на главную :) Подробностей про gettext в виде статей не слишком много, больше крупинки информации по форумам, отсылки в гугл и документацию. Вот тут говорят — надо переводить фразами, но есть ведь меню и заголовки таблиц, где без падежей ещё кое-как, но без родов бывает тяжко. Есть, правда, вариант делать исходники на языке со сложной грамматикой (падежами, родами и т.д.), а потом уже переводить на английский. Но, например, в опенсорсе это не прокатит 100%. Есть возможность использовать «контекст», как выше предлагает siasia, но если есть более красивое решение — это очень интересно.
                              0
                              Было бы очень полезно и интересно.
                          • UFO just landed and posted this here
                            0
                            сталкивался с проблемой, в веб приложениях (или когда один процесс обрабатывает несколько запросов параллельно) gettext бесполезен, так как переключение локали идет через переменную окружения LANG и
                            переключение в одном потоке, переключает локаль во всех :-(
                              +2
                              Видимо что-то было не так со структурой вашего приложения, а скорее всего вы что-то путаете. Gettext успешно используется во многих web-фреймворках как основной инструмент реализации i18n.
                              +4
                              Еще можно добавить, что следующие варианты равнозначны:

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

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

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

                              Only users with full accounts can post comments. Log in, please.