Локализация простой pygtk программы c glade формой в Linux

    Сразу оговорюсь, что python и gtk у меня 2й версии.

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

    Чего нет в этой статье:
    — как делать перевод формы в процессе работы. Этого я не смог найти, а хотелось бы знать…
    — как делать перевод текста в .py коде в процессе работы.
    — пива, блэкджека и остального тут тоже конечно нет.

    Перевод будет осуществляться с помощью указания локали при старте (или локали по умолчанию).


    Первое что понадобится — готовая glade форма. Т.к. я специально делал простенький тест, чтобы разобраться, то и формочка у меня простая, с меткой, кнопкой и чекбоксом.

    xml код формы
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
    <!--Generated with glade3 3.4.5 on Tue Nov 13 12:44:47 2012 -->
    <glade-interface>
      <widget class="GtkWindow" id="window1">
        <child>
          <widget class="GtkVBox" id="vbox1">
            <property name="visible">True</property>
            <child>
              <widget class="GtkLabel" id="label1">
                <property name="visible">True</property>
                <property name="label" translatable="yes">label text</property>
              </widget>
            </child>
            <child>
              <widget class="GtkButton" id="button1">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="receives_default">True</property>
                <property name="label" translatable="yes">button text</property>
                <property name="response_id">0</property>
              </widget>
              <packing>
                <property name="position">1</property>
              </packing>
            </child>
            <child>
              <widget class="GtkCheckButton" id="checkbutton1">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="label" translatable="yes">checkbutton text</property>
                <property name="response_id">0</property>
                <property name="draw_indicator">True</property>
              </widget>
              <packing>
                <property name="position">2</property>
              </packing>
            </child>
          </widget>
        </child>
      </widget>
    </glade-interface>
    
    


    Также понадобится программа на питоне (раз уж я о нём пишу), которая эту форму показывает:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import pygtk, gtk, gtk.glade
    
    print "hello to me"
    wTree = gtk.glade.XML("localize.glade", "window1")
    window = wTree.get_widget("window1")
    window.connect("delete_event", gtk.main_quit)
    window.show_all()
    gtk.main()
    

    Заодно она будет выводить строку в консоль, чтобы показать, что и в консоли язык меняется.

    Локализовывать можно по разному. Например, самому писать код обработки каждой строки/виджета и обновлять строки в зависимости от какой-то управляющей команды. В этом есть крошечный плюс — все действия можно производить в любой момент работы программы, но остальное — минусы. И выглядеть и работать это будет ужасно.

    Сам собой напрашиватся вопрос — как не изобретать велосипед с квадратными колёсами? Оказалось, что всё уже есть, нужно только уметь воспользоваться (как и в большинстве случаев).

    Для начала нужно сбросить настройки локалей в дефолтовые для пользователя (обычно определены в переменной окружения LANG). Это поможет избавиться от возможных проблем в многонитевой программе. Для подобного действия понадобится подключить модуль locale.

    	locale.setlocale(locale.LC_ALL, '')
    

    Далее будем пользоваться возможностями модуля gettext (потому его тоже придётся подключить). Глянув на его документацию, можно заметить, что ему необходимы некие «binary .mo files».

    .mo файлы — это файлы со списком всех переводимых строк программы.

    Как их получить:

    Сначала нужно выдрать все строки из glade формы (надписи на виджетах), но делать это вручную, конечно же, не стоит. Для этого воспользуемся набором команд intltool:

    	intltool-extract --type=gettext/glade localize.glade
    

    при желании дополнительные параметры можно посмотреть в man`е. Последний входной параметр — файл glade формы, откуда нужно выдирать текст. Эта команда создаст файл localize.glade.h:

    	char *s = N_("label text");
    	char *s = N_("button text");
    	char *s = N_("checkbutton text");
    

    где, как видно, перечислены все текстовые строки из виджетов формы.

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

    	print _("hello to me")
    

    т.е. взять в _()

    Как можно увидеть выше, в localize.glade.h строки обёрнуты в N_(). Это также своего рода маркер.

    Итак, весь необходимый текст помечен и теперь его надо собрать в одном месте. В этом нам поможет команда:

    	xgettext --language=Python --keyword=_ --keyword=N_ --output=show_form.pot show_form.py localize.glade.h
    

    Опция --keyword показывает программе на какие метки обращать внимание при сборе, потому их тут две "_" и «N_». --output задаёт имя выходного файла, а дальше идёт список всех файлов, где нужно искать метки (можно сделать вывод, что метки могут быть и другими, но я с ними не возился, т.к. это не особо важно).

    В результате получился файл следующего содержания:
    # SOME DESCRIPTIVE TITLE.
    # Copyright YEAR THE PACKAGE'S COPYRIGHT HOLDER
    # This file is distributed under the same license as the PACKAGE package.
    # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
    #
    #, fuzzy
    msgid ""
    msgstr ""
    «Project-Id-Version: PACKAGE VERSION\n»
    «Report-Msgid-Bugs-To: \n»
    «POT-Creation-Date: 2012-11-14 13:54+0300\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»
    «Language: \n»
    «MIME-Version: 1.0\n»
    «Content-Type: text/plain; charset=CHARSET\n»
    «Content-Transfer-Encoding: 8bit\n»

    #: show_form.py:14
    msgid «hello to me»
    msgstr ""

    #: localize.glade.h:1
    msgid «label text»
    msgstr ""

    #: localize.glade.h:2
    msgid «button text»
    msgstr ""

    #: localize.glade.h:3
    msgid «checkbutton text»
    msgstr ""

    Это шаблон для всех будущих файлов с переводами. Его редактировать не надо. Теперь настало время определиться с языками. Я использовал английский (en_US), русский (ru) и немецкий (de_DE) (на самом деле из немецкого я знаю только «гитлер капут» и «хандехох» и то не письменно, но до кучи пусть будет). Для каждого из языков нужно из шаблона создать локализованный файл. Это делается командами:

    	msginit --locale=ru --input=show_form.pot
    	msginit --locale=en_US --input=show_form.pot
    	msginit --locale=de_DE --input=show_form.pot
    

    В результате появляется три файла ru.po, de.po и en_US.po. Внутри они почти такие же пустые как и шаблон, но заполнена шапка, правда не совсем теми данными, что мне бы хотелось (возможно чего-то не указал в ключах) и без перевода строк на другие языки (естесственно). Перевод придётся вбивать руками в поля msgstr. Также я поправил charset на utf-8, размер символа на 16 бит и e-mail.

    В итоге получилось:
    ru.po
    # Russian translations for PACKAGE package.
    # Copyright 2012 THE PACKAGE'S COPYRIGHT HOLDER
    # This file is distributed under the same license as the PACKAGE package.
    # <aaa@bbb>, 2012.
    #
    msgid ""
    msgstr ""
    «Project-Id-Version: PACKAGE VERSION\n»
    «Report-Msgid-Bugs-To: \n»
    «POT-Creation-Date: 2012-11-14 13:54+0300\n»
    «PO-Revision-Date: 2012-11-14 13:58+0300\n»
    «Last-Translator: <aaa@bbb>\n»
    «Language-Team: Russian\n»
    «Language: ru\n»
    «MIME-Version: 1.0\n»
    «Content-Type: text/plain; charset=utf-8\n»
    «Content-Transfer-Encoding: 16bit\n»
    «Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11? 0: n%10>=2 && n»
    "%10<=4 && (n%100<10 || n%100>=20)? 1: 2);\n"

    #: show_form.py:14
    msgid «hello to me»
    msgstr «привет мне»

    #: localize.glade.h:1
    msgid «label text»
    msgstr «метка»

    #: localize.glade.h:2
    msgid «button text»
    msgstr «кнопка»

    #: localize.glade.h:3
    msgid «checkbutton text»
    msgstr «галочка»

    de.po
    (да, там не немецкий язык, но это вобщем-то не важно):
    # German translations for PACKAGE package.
    # Copyright 2012 THE PACKAGE'S COPYRIGHT HOLDER
    # This file is distributed under the same license as the PACKAGE package.
    # <aaa@bbb>, 2012.
    #
    msgid ""
    msgstr ""
    «Project-Id-Version: PACKAGE VERSION\n»
    «Report-Msgid-Bugs-To: \n»
    «POT-Creation-Date: 2012-11-14 13:54+0300\n»
    «PO-Revision-Date: 2012-11-14 14:14+0300\n»
    «Last-Translator: <aaa@bbb>\n»
    «Language-Team: German\n»
    «Language: de\n»
    «MIME-Version: 1.0\n»
    «Content-Type: text/plain; charset=utf-8\n»
    «Content-Transfer-Encoding: 16bit\n»
    «Plural-Forms: nplurals=2; plural=(n != 1);\n»

    #: show_form.py:14
    msgid «hello to me»
    msgstr «f»

    #: localize.glade.h:1
    msgid «label text»
    msgstr «d»

    #: localize.glade.h:2
    msgid «button text»
    msgstr «g»

    #: localize.glade.h:3
    msgid «checkbutton text»
    msgstr «e»

    en_US.po
    # English translations for PACKAGE package.
    # Copyright 2012 THE PACKAGE'S COPYRIGHT HOLDER
    # This file is distributed under the same license as the PACKAGE package.
    # <aaa@bbb>, 2012.
    #
    msgid ""
    msgstr ""
    «Project-Id-Version: PACKAGE VERSION\n»
    «Report-Msgid-Bugs-To: \n»
    «POT-Creation-Date: 2012-11-14 13:54+0300\n»
    «PO-Revision-Date: 2012-11-14 13:58+0300\n»
    «Last-Translator: <aaa@bbb>\n»
    «Language-Team: English\n»
    «Language: en_US\n»
    «MIME-Version: 1.0\n»
    «Content-Type: text/plain; charset=utf-8\n»
    «Content-Transfer-Encoding: 16bit\n»
    «Plural-Forms: nplurals=2; plural=(n != 1);\n»

    #: show_form.py:14
    msgid «hello to me»
    msgstr «hello to me»

    #: localize.glade.h:1
    msgid «label text»
    msgstr «label text»

    #: localize.glade.h:2
    msgid «button text»
    msgstr «button text»

    #: localize.glade.h:3
    msgid «checkbutton text»
    msgstr «checkbutton text»

    Казалось бы — зачем делать родную версию (у меня по умолчанию стоит en_US). Но ведь у другого человека родной может быть, например, de_DE, а он захочет увидеть перевод на английский. Да и разработчики gettext рекомендуют таки создавать.

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

    Итак, есть всё для создания .mo файлов. Это делается командами:

    	msgfmt ru.po -o locale/ru/LC_MESSAGES/show_form.mo
    	msgfmt en_US.po -o locale/en_US/LC_MESSAGES/show_form.mo
    	msgfmt de.po -o locale/de/LC_MESSAGES/show_form.mo
    

    Опция -o (вполне очевидно) указывает каталог, в котором готовый файл будет лежать, причём стоит заметить, что верхний каталог (тут «locale») должен быть один и тот же для всех файлов .mo, а далее должен идти каталог с именем локали (ru, de, en_US, de_DE, ru_RU — т.к. последние два без диалектов, то программы их сокращают до первых букв, но можно использовать и полные имена). Называться он должен так же как указываемый в питоновской программе домен, только с ".mo". Также LC_MESSAGES является одним из нескольких возможных вариантов имени внутреннего каталога (тоже, думаю, лучше использовать одни и те же имена).
    Вот что говорит официальная документация по этому поводу:


    localedir/language/LC_MESSAGES/domain.mo, where languages is searched for in the environment variables LANGUAGE, LC_ALL, LC_MESSAGES, and LANG respectively.

    В итоге получились файлы с переводами строк, которые уже можно использовать в программe (такие манипуляции производятся не только для python/glade).

    Вернёмся к программе на python`е.

    Сперва настроим gettext. После сброса настроек локалей нужно подсказать ему где брать файлы переводов и какие именно файлы. Для этого у меня введены две переменные:

    	APP="show_form"
    	DIR="locale"
    

    То, что APP совпадает с названием программы — это осталось от документации, но, думаю, там может быть любое имя. Хотя если смотреть на .mo файлы с названием программы, к которой они относятся, то гораздо проще понимать что к чему.

    APP — это имя .mo файлов, DIR — общий каталог с языками. Объяснение этого факта gettext`у производится строками:

    	gettext.bindtextdomain(APP, DIR)
    	gettext.textdomain(APP)
    

    Теперь надо объяснить питону, что делать со строками вида _(). Для этого "_" присваивается функция взятия перевода из указанного файла. Записать это можно двумя путями:

    	lang = gettext.translation(APP, DIR)
    	_ = lang.gettext
    

    или

    	_ = gettext.gettext
    

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

    Этого достаточно, чтобы текст в .py файлах выводился на нужном языке. А для локализации glade формы требуется объяснить gtk, где брать перевод и какой:

    	gtk.glade.bindtextdomain(APP, DIR)
    	wTree = gtk.glade.XML("localize.glade", "window1", APP)
    

    Итоговый код:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import pygtk, gtk, gtk.glade
    import locale, gettext
    
    APP="show_form"
    DIR="locale"
    
    locale.setlocale(locale.LC_ALL, '')
    gettext.bindtextdomain(APP, DIR)
    gettext.textdomain(APP)
    _ = gettext.gettext
    print _("hello to me")
    gtk.glade.bindtextdomain(APP, DIR)
    wTree = gtk.glade.XML("localize.glade", "window1", APP)
    window = wTree.get_widget("window1")
    window.connect("delete_event", gtk.main_quit)
    window.show_all()
    gtk.main()
    

    Запуск:

    LANG=en_US.utf-8 ./show_form.py
    

    image

    LANG=ru_RU.utf-8 ./show_form.py
    

    image

    LANG=de_DE.utf-8 ./show_form.py
    

    А тут честно вылетает ошибка, т.к. немецкая локаль у меня не подключена. Таким образом, для использования локали, её нужно добавить в настройках графической оболочки.

    На этом всё.

    Upd:

    Про Builder. Если форма для libglade уже есть, то можно попытаться либо сконвертировать с помощью libglade-convert (у меня выдало ошибку), либо нарисовать новую для builder.

    Код будет выглядеть так:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import gtk, gtk.glade
    import locale, gettext
    
    APP="show_form"
    DIR="locale"
    
    locale.setlocale(locale.LC_ALL, '')
    gettext.bindtextdomain(APP, DIR)
    gettext.textdomain(APP)
    _ = gettext.gettext
    
    print _("hello to me")
    
    builder = gtk.Builder()
    gtk.glade.bindtextdomain(APP, DIR)
    builder.set_translation_domain(APP)
    builder.add_from_file("localize.xml")
    window = builder.get_object("window1")
    window.connect("delete_event", gtk.main_quit)
    window.show_all()
    gtk.main()
    


    И, как верно заметил Moonrise, Content-Transfer-Encoding официально рекомендуется устанавливать в 8 бит (т.е. не изменять).
    • +10
    • 4,4k
    • 3
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 3
    • 0
      Вроде как libglade устарел ещё со времён Gtk 2.12 и разработчики советуют использовать GtkBuilder.

      #!/usr/bin/env python
      # -*- coding: utf-8 -*-
      
      import pygtk, gtk
      
      print "hello to me"
      wTree = gtk.Builder()
      wTree.add_from_file("localize.glade")
      window = wTree.get_object("window1")
      window.connect("delete_event", gtk.main_quit)
      window.show_all()
      gtk.main()
      
      • 0
        Правда надо будет localize.glade пересохранить в формате GtkBuilder.
      • 0
        А в чём смысл устанавливать Content-Transfer-Encoding в 16 бит?

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