gettext — Услуги по многоязычной интернационализации

Источник: Lib/gettext.py


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

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

GNU gettext API

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

gettext.bindtextdomain(domain, localedir=None)

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

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

gettext.textdomain(domain=None)

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

gettext.gettext(message)

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

gettext.dgettext(domain, message)

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

gettext.ngettext(singular, plural, n)

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

Формула множественного числа берется из заголовка каталога. Она представляет собой выражение на языке C или Python со свободной переменной n; выражение оценивается как индекс множественного числа в каталоге. Точный синтаксис для использования в файлах .po и формулы для различных языков см. в the GNU gettext documentation.

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

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

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()), но перевод ограничивается заданным контекстом сообщения.

Added in version 3.8.

Обратите внимание, что в 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. API. Это рекомендуемый способ локализации приложений и модулей Python. gettext определяет класс GNUTranslations, который реализует разбор файлов формата GNU .mo и имеет методы для возврата строк. Экземпляры этого класса могут также устанавливаться во встроенное пространство имен в виде функции _().

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

Эта функция реализует стандартный алгоритм поиска файлов .mo. Она принимает домен, идентичный тому, что принимает 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)

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

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

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

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

Изменено в версии 3.11: Параметр codeset удален.

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

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

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

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

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

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

Изменено в версии 3.11: names теперь является параметром только для ключевых слов.

Класс NullTranslations

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

class gettext.NullTranslations(fp=None)

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

_parse(fp)

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

add_fallback(fallback)

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

gettext(message)

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

ngettext(singular, plural, n)

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

pgettext(context, message)

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

Added in version 3.8.

npgettext(context, singular, plural, n)

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

Added in version 3.8.

info()

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

charset()

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

install(names=None)

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

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

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

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 используется для инициализации переменной экземпляра «protected» _charset, а если ключ не найден, то по умолчанию используется None. Если указана кодировка charset, то все идентификаторы сообщений и строки сообщений, считанные из каталога, преобразуются в Unicode с использованием этой кодировки, в противном случае предполагается ASCII.

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

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

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

class gettext.GNUTranslations

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

gettext(message)

Находит в каталоге идентификатор message и возвращает соответствующую строку сообщения в виде строки Unicode. Если в каталоге нет записи для идентификатора message и установлен fallback, поиск передается методу fallback gettext(). В противном случае возвращается идентификатор сообщения.

ngettext(singular, plural, n)

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

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

Вот пример:

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 и возвращает соответствующую строку сообщения в виде строки Unicode. Если в каталоге нет записей для идентификатора сообщения и контекста, и был установлен fallback, поиск передается методу fallback pgettext(). В противном случае возвращается идентификатор сообщения.

Added in version 3.8.

npgettext(context, singular, plural, n)

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

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

Added in version 3.8.

Поддержка каталога сообщений 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 package.

(В состав 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 в своем коде, зависит от того, интернационализируете ли вы один модуль или все приложение. В следующих двух разделах мы рассмотрим каждый из этих случаев.

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

Если вы локализуете свой модуль, вы должны следить за тем, чтобы не вносить глобальных изменений, например, во встроенное пространство имен. Вы должны использовать не GNU gettext API, а использовать 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'])

# start by using language1
lang1.install()

# ... time goes by, user selects language 2
lang2.install()

# ... more time goes by, user selects language 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().

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

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

  • Питер Функ

  • Джеймс Хенстридж

  • Хуан Давид Ибаньес Паломар

  • Марк-Андре Лембург

  • Мартин фон Лёвис

  • Франсуа Пинар

  • Барри Варшава

  • Густаво Нимейер

Сноски