gettext — Службы многоязычной интернационализации


Модуль gettext предоставляет службы интернационализации (I18N) и локализации (L10N) для ваших Python модулей и приложений. Он поддерживает как API-интерфейс каталога сообщений GNU gettext, так и API-интерфейс более высокого уровня на основе классов, который больше подходит для Python файлов. Описанный ниже интерфейс позволяет писать сообщения вашего модуля и приложения на одном естественном языке, а также предоставляет каталог переведенных сообщений для работы на разных естественных языках.

Также даны некоторые советы по локализации ваших Python модулей и приложений.

API GNU gettext

Модуль gettext определяет API, очень похожий на API GNU gettext. Если вы используете данный API, вы глобально повлияете на перевод всего вашего приложения. Часто это то, что вам нужно, если ваше приложение является одноязычным, а выбор языка зависит от локали вашего пользователя. Если вы локализуете модуль Python или если вашему приложению необходимо переключать языки на лету, вы, вероятно, захотите использовать API на основе классов.

gettext.bindtextdomain(domain, localedir=None)

Привязка domain к каталогу локали localedir. gettext будет искать двоичные файлы .mo для данного домена, используя путь (в Unix): localedir/language/LC_MESSAGES/domain.mo, где language ищется в переменных среды LANGUAGE, LC_ALL, LC_MESSAGES и LANG соответственно.

Если localedir пропущен или None, то возвращается текущая привязка для domain. [1]

gettext.bind_textdomain_codeset(domain, codeset=None)

Связать domain с codeset, изменив кодировку байтовых строк, возвращаемых функциями lgettext(), ldgettext(), lngettext() и ldngettext(). Если codeset пропущен, возвращается текущая привязка.

Устарело с версии 3.8, будет удалено в 3.10 версии..

gettext.textdomain(domain=None)

Изменяет или запрашивает текущий глобальный домен. Если domain равен None, то возвращается текущий глобальный домен, в противном случае глобальный домен устанавливается на domain, который и возвращается.

gettext.gettext(message)

Возвращает локализованный перевод message на основе текущего глобального домена, языка и каталога локали. Данная функция обычно имеет псевдоним _() в локальном пространстве имён (см. примеры ниже).

gettext.dgettext(domain, message)

Аналогична gettext(), но ищет сообщение в указанном domain.

gettext.ngettext(singular, plural, n)

Как gettext(), но учитывает формы множественного числа. Если перевод найден, применяет формулу множественного числа к n и возвращает полученное сообщение (некоторые языки имеют более двух форм множественного числа). Если перевод не найден, возвращает singular, если n равно 1; возвращает plural в противном случае.

Формула множественного числа взята из заголовка каталога. Это C выражение или Python со свободной переменной n; выражение вычисляется как индекс множественного числа в каталоге. См. документация GNU GetText для точного синтаксиса, который будет использоваться в файлах .po, и формул для различных языков.

gettext.dngettext(domain, singular, plural, n)

Аналогична ngettext(), но ищет сообщение в указанном domain.

gettext.pgettext(context, message)
gettext.dpgettext(domain, context, message)
gettext.npgettext(context, singular, plural, n)
gettext.dnpgettext(domain, context, singular, plural, n)

Аналогична соответствующим функциям без p в префиксе (т. е. gettext(), dgettext(), ngettext(), dngettext()), но перевод ограничен данным сообщением context.

Добавлено в версии 3.8.

gettext.lgettext(message)
gettext.ldgettext(domain, message)
gettext.lngettext(singular, plural, n)
gettext.ldngettext(domain, singular, plural, n)

Эквивалентна соответствующим функциям без префикса l (gettext(), dgettext(), ngettext() и dngettext()), но перевод возвращается в виде строки байтов, закодированной в предпочтительной системной кодировке, если никакая другая кодировка не была явно задана с помощью bind_textdomain_codeset().

Предупреждение

Данных функций следует избегать в Python 3, поскольку они возвращают закодированные байты. Гораздо лучше использовать альтернативы, которые вместо этого возвращают Юникод строки, поскольку большинство приложений Python захотят манипулировать удобочитаемым текстом как строками, а не байтами. Кроме того, возможно, что вы можете получить неожиданные исключения, связанные с Юникодом, если есть проблемы с кодировкой переведённых строк.

Устарело с версии 3.8, будет удалено в 3.10 версии..

Обратите внимание, что GNU gettext также определяет метод dcgettext(), но он был сочтен бесполезным и поэтому в настоящее время не реализован.

Вот пример типичного использования API:

import gettext
gettext.bindtextdomain('myapplication', '/path/to/my/language/directory')
gettext.textdomain('myapplication')
_ = gettext.gettext
# ...
print(_('This is a translatable string.'))

API на основе классов

API-интерфейс модуля gettext на основе классов обеспечивает большую гибкость и удобство, чем API-интерфейс GNU gettext. Это рекомендуемый способ локализации Python приложений и модулей. gettext определяет класс GNUTranslations, который реализует парсинг файлов формата GNU .mo и содержит методы для возврата строк. Экземпляры этого класса также могут устанавливаться во встроенное пространство имён как функция _().

gettext.find(domain, localedir=None, languages=None, all=False)

Данная функция реализует стандартный алгоритм поиска .mo файлов. Требуется domain, идентичный textdomain(). Дополнительный localedir аналогичен bindtextdomain(). Необязательный languages — это список строк, где каждая строка — это код языка.

Если localedir не указан, используется каталог локали системы по умолчанию. [2] Если languages не указан, поиск осуществляется по следующим переменным среды: LANGUAGE, LC_ALL, LC_MESSAGES и LANG. Первый, возвращающий непустое значение, используется для переменной languages. Переменные среды должны содержать список языков, разделенных двоеточием, который будет разделён двоеточием, чтобы получить ожидаемый список строк кода языка.

Затем find() расширяет и нормализует языки, а затем перебирает их, ища существующий файл, созданный из данных компонентов:

localedir/language/LC_MESSAGES/domain.mo

Первое такое имя файла, которое существует, возвращается find(). Если такой файл не найден, возвращается None. Если указано all, возвращается список всех имён файлов в том порядке, в котором они появляются в списке языков или переменных среды.

gettext.translation(domain, localedir=None, languages=None, class_=None, fallback=False, codeset=None)

Возвращает экземпляр *Translations на основе domain, localedir и languages, которые сначала передаются в find(), чтобы получить список связанных путей к файлам .mo. Экземпляры с идентичными именами файлов .mo кэшируются. Фактически созданный экземпляр класса — class_, если он предоставлен, иначе GNUTranslations. Конструктор класса должен принимать один аргумент файловый объект. Если указано, codeset изменит кодировку, используемую для кодирования переведенных строк в методах lgettext() и lngettext().

Если найдено несколько файлов, более поздние файлы используются в качестве запасных для более ранних. Чтобы разрешить настройку резервного варианта, copy.copy() используется для клонирования каждого объекта перевода из кэша; фактические данные экземпляра по-прежнему используются совместно с кешем.

Если файл .mo не найден, данная функция вызывает OSError, если у fallback ложное значение (по умолчанию), и возвращает экземпляр NullTranslations, если у fallback истинное значение.

Изменено в версии 3.3: Раньше IOError вызывался вместо OSError.

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр codeset.

gettext.install(domain, localedir=None, codeset=None, names=None)

Устанавливает функцию _() в встроенном пространстве имён Python на основе domain, localedir и codeset, которые передаются функции translation().

Параметр names см. в описании метода объекта перевода install().

Как показано ниже, вы обычно помечаете в своём приложении строки, которые являются кандидатами на перевод, оборачивая их в вызов функции _(), как здесь:

print(_('This string will be translated.'))

Для удобства вы хотите, чтобы функция _() была установлена в пространстве имён встроенных функций Python, чтобы она была легко доступна во всех модулях вашего приложения.

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр codeset.

Класс NullTranslations

Классы перевода — это то, что на самом деле реализует перевод строк сообщений оригинального исходного файла в переведенные строки сообщений. Базовый класс, используемый всеми классами перевода, — NullTranslations; это обеспечивает базовый интерфейс, который вы можете использовать для написания собственных специализированных классов перевода. Вот методы NullTranslations:

class gettext.NullTranslations(fp=None)

Принимает необязательный файловый объект fp, который игнорируется базовым классом. Инициализирует «защищённые» переменные экземпляра _info и _charset, которые устанавливаются производными классами, а также _fallback, который устанавливается через add_fallback(). Затем он вызывает self._parse(fp), если fp не является None.

_parse(fp)

Без операции в базовом классе данный метод принимает файловый объект fp и считывает данные из файла, инициализируя его каталог сообщений. Если у вас есть неподдерживаемый формат файла каталога сообщений, вы должны переопределить данный метод, чтобы проанализировать ваш формат.

add_fallback(fallback)

Добавляет fallback в качестве резервного объекта для текущего объекта перевода. Объект перевода должен обращаться к запасному варианту, если он не может предоставить перевод для данного сообщения.

gettext(message)

Если был установлен резервный вариант, перенаправляет gettext() на резервный. В противном случае возвращает message. Переопределено в производных классах.

ngettext(singular, plural, n)

Если был установлен резервный вариант, перенаправляет ngettext() на резервный. В противном случае возвращает singular, если n равно 1; возвращает plural в противном случае. Переопределено в производных классах.

pgettext(context, message)

Если был установлен резервный вариант, перенаправляет pgettext() на резервный. В противном случае возвращает переведенное сообщение. Переопределено в производных классах.

Добавлено в версии 3.8.

npgettext(context, singular, plural, n)

Если был установлен резервный вариант, перенаправляет npgettext() на резервный. В противном случае возвращает переведенное сообщение. Переопределено в производных классах.

Добавлено в версии 3.8.

lgettext(message)
lngettext(singular, plural, n)

Эквивалентна gettext() и ngettext(), но перевод возвращается в виде строки байтов, закодированной в предпочтительной системной кодировке, если кодировка не была явно задана с помощью set_output_charset(). Переопределено в производных классах.

Предупреждение

Данных методов следует избегать в Python 3. См. предупреждение для функции lgettext().

Устарело с версии 3.8, будет удалено в 3.10 версии..

info()

Возвращает «защищённую» переменную _info, словарь, содержащий метаданные, найденные в файле каталога сообщений.

charset()

Возвращает кодировку файла каталога сообщений.

output_charset()

Возвращает кодировку, используемую для возврата переведенных сообщений в lgettext() и lngettext().

Устарело с версии 3.8, будет удалено в 3.10 версии..

set_output_charset(charset)

Изменяет кодировку, используемую для возврата переведённых сообщений.

Устарело с версии 3.8, будет удалено в 3.10 версии..

install(names=None)

Данный метод устанавливает gettext() во встроенное пространство имён, привязывая его к _.

Если задан параметр names, это должна быть последовательность, содержащая имена функций, которые вы хотите установить в пространстве имён встроенных модулей, в дополнение к _(). Поддерживаемые имена: 'gettext', 'ngettext', 'pgettext', 'npgettext', 'lgettext' и 'lngettext'.

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

import gettext
t = gettext.translation('mymodule', ...)
_ = t.gettext

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

Изменено в версии 3.8: Добавлены 'pgettext' и 'npgettext'.

Класс GNUTranslations

Модуль gettext предоставляет один дополнительный класс, производный от NullTranslations: GNUTranslations. Данный класс переопределяет _parse(), чтобы разрешить чтение файлов GNU gettext формата .mo как в формате с прямым порядком байтов, так и с обратным порядком байтов.

GNUTranslations анализирует необязательные метаданные из каталога переводов. В GNU gettext принято включать метаданные в качестве перевода пустой строки. Данные метаданные представлены парами RFC 822 в стиле key: value и должны содержать ключ Project-Id-Version. Если ключ Content-Type найден, то свойство charset используется для инициализации «защищённой» переменной экземпляра _charset, по умолчанию используется значение None, если оно не найдено. Если указана кодировка набора символов, то все идентификаторы сообщений и строки сообщений, прочитанные из каталога, преобразуются в Юникод с использованием этой кодировки, в противном случае предполагается ASCII.

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

Весь множество пар ключ/значение помещается в словарь и устанавливается как «защищённая» переменная экземпляра _info.

Если магический номер файла .mo недействителен, основной номер версии является неожиданным или если при чтении файла происходят другие проблемы, создание экземпляра класса GNUTranslations может вызвать OSError.

class gettext.GNUTranslations

Следующие методы переопределены из реализации базового класса:

gettext(message)

Ищет идентификатор message в каталоге и возвращает соответствующую строку сообщения в виде Юникод строки. Если в каталоге нет записи для идентификатора message и задан резервный вариант, поиск перенаправляется на резервный метод gettext(). В противном случае возвращается идентификатор message.

ngettext(singular, plural, n)

Выполняет поиск идентификатора сообщения во множественном числе. singular используется в качестве идентификатора сообщения для поиска в каталоге, а n используется для определения используемой формы множественного числа. Возвращаемая строка сообщения является Юникод строкой.

Если идентификатор сообщения не найден в каталоге и указан резервный вариант, запрос перенаправляется в метод ngettext() резервного варианта. В противном случае, когда n равен 1, возвращается singular, а во всех остальных случаях возвращается plural.

Пример:

n = len(os.listdir('.'))
cat = GNUTranslations(somefile)
message = cat.ngettext(
    'There is %(num)d file in this directory',
    'There are %(num)d files in this directory',
    n) % {'num': n}
pgettext(context, message)

Ищет идентификатор context и message в каталоге и возвращает соответствующую строку сообщения в виде Юникод строки. Если в каталоге нет записи для идентификатора message и context и задан резервный вариант, поиск перенаправляется на резервный метод pgettext(). В противном случае возвращается идентификатор message.

Добавлено в версии 3.8.

npgettext(context, singular, plural, n)

Выполнить поиск идентификатора сообщения во множественном числе. singular используется в качестве идентификатора сообщения для поиска в каталоге, а n используется для определения используемой формы множественного числа.

Если идентификатор сообщения для context не найден в каталоге и указан резервный вариант, запрос перенаправляется в резервный метод npgettext(). В противном случае, когда n равен 1, возвращается singular, а во всех остальных случаях возвращается plural.

Добавлено в версии 3.8.

lgettext(message)
lngettext(singular, plural, n)

Эквивалентен gettext() и ngettext(), но перевод возвращается в виде строки байтов, закодированной в предпочтительной системной кодировке, если кодировка не была явно задана с помощью set_output_charset().

Предупреждение

Данных методов следует избегать в Python 3. См. предупреждение для функции lgettext().

Устарело с версии 3.8, будет удалено в 3.10 версии..

Поддержка каталога сообщений Solaris

Операционная система Solaris определяет собственный двоичный формат файла .mo, но, поскольку документация по этому формату отсутствует, в настоящее время он не поддерживается.

Конструктор каталога

GNOME использует версию модуля gettext Джеймса Хенстриджа, но версия имеет немного другой API. Его задокументированное использование было:

import gettext
cat = gettext.Catalog(domain, localedir)
_ = cat.gettext
print(_('hello world'))

Для совместимости с этим старым модулем функция Catalog() является псевдонимом функции translation(), описанной выше.

Одно различие между этим модулем и модулем Хенстриджа: его объекты каталога поддерживали доступ через картографический API, но он, по-видимому, не используется и поэтому в настоящее время не поддерживается.

Интернационализация ваших программ и модулей

Интернационализация (I18N) относится к операции, посредством которой программа узнает о нескольких языках. Локализация (L10N) означает адаптацию вашей программы после интернационализации к местному языку и культурным обычаям. Чтобы обеспечить многоязычные сообщения для ваших программ Python, вам необходимо выполнить следующие шаги:

  1. подготовьте свою программу или модуль, специально пометив переводимые строки
  2. запустите множество инструментов над отмеченными файлами для создания каталогов необработанных сообщений
  3. создавайте языковые переводы каталогов сообщений
  4. используйте модуль gettext, чтобы строки сообщений были правильно переведены

Чтобы подготовить свой код для I18N, вам нужно просмотреть все строки в ваших файлах. Любая строка, которую необходимо перевести, должна быть помечена обёрткой в _('...'), т. е. вызовом функции _(). Например:

filename = 'mylog.txt'
message = _('writing a log message')
with open(filename, 'w') as fp:
    fp.write(message)

В этом примере строка 'writing a log message' помечена как кандидат на перевод, а строки 'mylog.txt' и 'w' — нет.

Есть несколько инструментов для извлечения строк, предназначенных для перевода. Первоначальный GNU gettext поддерживал только C исходный код или C++, но его расширенная версия xgettext сканирует код, написанный на нескольких языках, включая Python, для поиска строк, помеченных как переводимые. Babel — это библиотека интернационализации Python, которая включает pybabel сценарий для извлечения и компиляции каталогов сообщений. Программа Франсуа Пинара под названием xpot выполняет аналогичную работу и доступна как часть его po-utils пакета.

(Python также включает версии данных программ на чистом Python, называемые pygettext.py и msgfmt.py; некоторые дистрибутивы Python установят их за вас. pygettext.py похож на xgettext, но понимает только исходный код Python и не может работать с другими языками программирования, такими как C или C++. pygettext.py поддерживает интерфейс командной строки, аналогичный xgettext; для получения подробной информации о её использовании выполните pygettext.py --help. msgfmt.py бинарно совместима с GNU msgfmt. С этими двумя программами вам может не понадобиться пакет GNU gettext для интернационализации ваших Python приложений.)

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

Копии данных файлов .po затем передаются отдельным людям-переводчикам, которые пишут переводы для каждого поддерживаемого естественного языка. Они отправляют обратно завершенные языковые версии в виде файла <language-name>.po, который компилируется в машиночитаемый двоичный файл каталога .mo с помощью программы msgfmt. Файлы .mo используются модулем gettext для фактической обработки перевода во время выполнения.

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

Локализация вашего модуля

Если вы локализуете свой модуль, вы должны позаботиться о том, чтобы не вносить глобальные изменения, например. во встроенное пространство имён. Вы не должны использовать API GNU gettext, а вместо этого API на основе классов.

Допустим, ваш модуль называется «spam», а различные файлы перевода модуля на естественный язык .mo находятся в /usr/share/locale в формате GNU gettext. Вот то, что вы бы поместили в верхней части вашего модуля:

import gettext
t = gettext.translation('spam', '/usr/share/locale')
_ = t.gettext

Локализация вашего приложения

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

В простом случае вам нужно только добавить следующий фрагмент кода в основной файл драйвера вашего приложения:

import gettext
gettext.install('myapplication')

Если вам нужно установить каталог локали, вы можете передать его в функцию install():

import gettext
gettext.install('myapplication', '/usr/share/locale')

Смена языков на лету

Если ваша программа должна поддерживать несколько языков одновременно, вы можете создать несколько экземпляров перевода, а затем явно переключаться между ними, например:

import gettext

lang1 = gettext.translation('myapplication', languages=['en'])
lang2 = gettext.translation('myapplication', languages=['fr'])
lang3 = gettext.translation('myapplication', languages=['de'])

# Начало с использования language1
lang1.install()

# ... проходит время, пользователь выбирает язык 2
lang2.install()

# ... проходит ещё больше времени, пользователь выбирает язык 3
lang3.install()

Отложенные переводы

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

animals = ['mollusk',
           'albatross',
           'rat',
           'penguin',
           'python', ]
# ...
for a in animals:
    print(a)

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

Вот один из способов справиться с этой ситуацией:

def _(message): return message

animals = [_('mollusk'),
           _('albatross'),
           _('rat'),
           _('penguin'),
           _('python'), ]

del _

# ...
for a in animals:
    print(_(a))

Это работает, потому что фиктивное определение _() просто возвращает строку без изменений. И это фиктивное определение временно переопределит любое определение _() во встроенном пространстве имён (до команды del). Будьте осторожны, если у вас есть предыдущее определение _() в локальном пространстве имён.

Обратите внимание, что второе использование _() не идентифицирует «a» как переводимое в программу gettext, поскольку параметр не является строковым литералом.

Другой способ справиться с этим — следующий пример:

def N_(message): return message

animals = [N_('mollusk'),
           N_('albatross'),
           N_('rat'),
           N_('penguin'),
           N_('python'), ]

# ...
for a in animals:
    print(_(a))

В этом случае вы помечаете переводимые строки с помощью функции N_(), которая не будет конфликтовать ни с одним определением _(). Однако вам нужно научить свою программу извлечения сообщений искать переводимые строки, помеченные N_(). xgettext, pygettext, pybabel extract и xpot поддерживают это с помощью переключателя командной строки -k. Выбор N_() здесь совершенно произвольный; с таким же успехом это мог быть MarkThisStringForTranslation().

Благодарности

Следующие люди предоставили код, отзывы, предложения по дизайну, предыдущие реализации и ценный опыт для создания данного модуля:

  • Питер Функ
  • Джеймс Хенстридж
  • Хуан Давид Ибаньес Паломар
  • Марк-Андре Лембург
  • Мартин фон Лоуис
  • Франсуа Пинар
  • Барри Варшава
  • Густаво Нимейер

Сноски

[1]Каталог локали по умолчанию зависит от системы; например, на RedHat Linux это /usr/share/locale, но в Solaris это /usr/lib/locale. Модуль gettext не пытается поддерживать данные зависимости от системы значения по умолчанию; вместо этого по умолчанию используется sys.base_prefix/share/locale (см. sys.base_prefix). По этой причине всегда лучше вызывать bindtextdomain() с явным абсолютным путём в начале вашего приложения.
[2]См. сноску для bindtextdomain() выше.