Pull to refresh

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

Reading time 9 min
Views 5.8K
Сразу оговорюсь, что 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 бит (т.е. не изменять).
Tags:
Hubs:
+10
Comments 3
Comments Comments 3

Articles