Перейти к содержанию

Руководство

Эта страница — справочник по времени выполнения: всё, что делает с этой библиотекой код приложения, когда каталоги уже существуют. Если вы ещё не видели весь цикл — пометить, извлечь, перевести, скомпилировать, запустить, — учебник проходит его за пять минут; создание и проверка каталогов описаны в разделе Извлечение, а то, как команда поддерживает вращение цикла — циклы обновления, CI, платформы перевода, — в разделе В продакшене.

Какую точку входа выбрать?

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

Ваша ситуация Что использовать
Один язык на весь процесс — CLI, настольное приложение, скрипт Translator, вызываемый как _
Свой язык на каждый запрос или async-задачу — веб-приложение use_translations() вокруг работы, затем tr()
Сообщение, определённое при импорте, — метка формы, enum, константа lazy_gettext() или lazy_pgettext()
Формулировку выбирает число ngettext() / npgettext() в любой из форм выше
Рендеринг шаблона вообще без каталога compile_template()

Всё, что ниже, — эти пять, в том же порядке.

Привязка каталога

Рекомендуемая форма повторяет объектный API gettext: один раз привяжите стандартный объект переводов и используйте вызываемый обработчик как _.

import gettext

from gettext_tstrings import Translator

translations = gettext.translation("messages", localedir="locales", languages=["ja"])
_ = Translator(translations)

name = "Ada"
print(_(t"Hello {name}"))  # こんにちは Ada

n = 3
print(_.ngettext(t"One file", t"{n} files", n))  # picks the right plural form for n

filename = "report.txt"
print(_.pgettext("button", t"Open {filename}"))  # "button" disambiguates homonyms

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

from gettext_tstrings import gettext, ngettext, npgettext, pgettext

gettext(t"Hello {name}", translations=translations)
ngettext(t"One file", t"{n} files", n, translations=translations)
pgettext("button", t"Open {filename}", translations=translations)
npgettext("inbox", t"One message", t"{n} messages", n, translations=translations)

tr и ntr — точные псевдонимы gettext и ngettext.

Язык для каждого запроса

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

from gettext_tstrings import tr, use_translations


def handle(request):
    name = request.user.display_name
    translations = load_translations(request.locale)
    with use_translations(translations):
        return render(tr(t"Hello {name}"))

set_translations() привязывает без блока, если жизненным циклом управляет фреймворк; get_translations() читает привязку. Явный translations= имеет приоритет. Без привязки fallback — глобальные функции gettext стандартной библиотеки. Разобранные примеры для Flask и middleware ASGI — на странице В продакшене.

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

t-строка захватывает значения немедленно. Для метки, enum или константы, созданной при импорте, но выводимой на активном языке в момент использования, применяйте отложенную строку.

from gettext_tstrings import lazy_gettext, lazy_pgettext, use_translations

SAVE = lazy_gettext(t"Save changes")  # defined once, at import
OPEN = lazy_pgettext("button", t"Open file")

with use_translations(japanese):
    assert str(SAVE) == "変更を保存"  # rendered here, in this language

LazyString рендерится через str(), format() и f-строки и сравнивается с текстом.

Намеренно не хешируется

Текст зависит от языка. Изменение хеша незаметно повредило бы set или dict. Для ключа сначала вызовите str().

strict задаётся там, где сообщение написано, а не там, где оно рендерится:

SAVE = lazy_gettext(t"Save changes", strict=True)

Отложенная строка рендерится там, где её в итоге используют, — внутри шаблона, формы, строки лога, — и это место редко знает, тестовый ли это прогон или продакшен. Именно передача strict=True при определении позволяет применить к строке, которая рендерится не в месте вызова, тот же выбор «громко в CI, снисходительно в продакшене».

Множественные формы зависят от числа во время выполнения; выводите их сразу через ngettext.

Несколько языков сразу

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

with use_translations(reader):
    page = tr(t"Hello {name}")
    with use_translations(recipient):
        notice = tr(t"Hello {name}")  # the recipient's language
    footer = tr(t"Hello {name}")  # the reader's again

Для списка получателей работу делают отложенные строки: сообщение написано один раз, при импорте, и рендерится по разу на каждый язык.

SUBJECT = lazy_gettext(t"Your order shipped")

for user in users:
    with use_translations(load_translations(user.locale)):
        send(user.email, str(SUBJECT))

Привязка — это ContextVar, а не стек, лежащий в общем объекте, поэтому перекрывающиеся запросы не могут подхватить язык друг друга — в том числе когда они выходят из своих блоков в том же порядке, в каком вошли; именно на таком чередовании стек с проталкиванием ошибается. Загружать каталог на каждый язык дёшево: gettext.translation() разбирает каждый .mo один раз и раздаёт копии, которые пользуются общим разобранным каталогом.

Наследует ли рабочий поток привязку, зависит от сборки

Обычный threading.Thread или ThreadPoolExecutor.submit стартует либо с копии контекста вызывающего кода, либо с пустого, а что именно из этих двух — решает sys.flags.thread_inherit_context: по умолчанию истина в свободнопоточных сборках и ложь во всех остальных. Поэтому один и тот же код отображает привязанный язык на 3.14t и глобальный для процесса каталог на 3.14. Передавайте контекст, а не полагайтесь на значение по умолчанию:

pool.submit(contextvars.copy_context().run, render)

asyncio.to_thread уже делает это за вас.

Значения с учётом локали

Эта библиотека решает, где значение окажется в переведённом сообщении. Само значение она не локализует. {amount:,.2f} — это спецификация формата Python с фиксированным поведением: запятая через каждые три разряда и точка перед дробной частью, — и она даёт одни и те же символы, на каком бы языке ни было сообщение:

>>> f"{1234.5:,.2f}"  # the same in every locale
'1,234.50'

По-немецки это число пишется 1.234,50, по-французски 1 234,50, а хинди группирует 1234567 как 12,34,567, а не 1,234,567. Числа, валюты, даты, время и единицы измерения — вотчина Babel. Сначала отформатируйте значение, затем подставьте готовую строку:

from babel.numbers import format_currency

total = format_currency(amount, "EUR", locale=locale)
tr(t"Your order comes to {total}")

В сообщении со счётом число выполняет две работы — выбирает множественную форму и появляется в тексте, — и локализуется только вторая. Оставьте сырое число для выбора формы, а для показа передайте отформатированную строку:

from babel.numbers import format_decimal

shown = format_decimal(n, locale=locale)
_.ngettext(t"One file", t"{shown} files", n)

Форматирование до вызова — это ещё и то, что удерживает спецификацию формата вне каталога: переводчик видит готовый кусок текста, а не число с инструкцией, как его отрисовать.

Если каталог неверен

Если заполнители перевода не соответствуют источнику — потерянное, неизвестное или переформатированное поле, проскользнувшее мимо проверок из отредактированного вручную .mo, из поставленного извне каталога или из конвейера без checker, — режим по умолчанию выводит исходное сообщение вместо исключения. Это контракт gettext: неверный каталог не должен останавливать приложение.

Если Hello {name} переведено как こんにちは {nombre}, рендеринг завершается, а logger gettext_tstrings получает предупреждение:

WARNING gettext_tstrings: invalid translation for msgid 'Hello {name}'; using
source text: translation does not match the source placeholders: {name} is
missing; {nombre} is not in the source message
>>> _(t"Hello {name}")
'Hello Ada'

Предупреждение появляется один раз для каждой пары сообщения и шаблона, а не при каждом рендеринге, поэтому неверная запись каталога не заливает журнал.

В тестах и CI включайте строгий режим:

strict = Translator(translations, strict=True)
tr(t"Hello {name}", translations=translations, strict=True)

Тогда тот же поиск вызывает исключение:

>>> strict(t"Hello {name}")
Traceback (most recent call last):
  ...
gettext_tstrings.errors.InvalidTranslationError: translation does not match the
source placeholders: {name} is missing; {nombre} is not in the source message

Эти сообщения написаны для того, кто способен что-то с ними сделать, а для проблемы с каталогом это чаще переводчик, чем программист, — поэтому там, где заполнитель выглядит присутствующим, но таковым не является, сообщение объясняет почему, а не повторяет, что его нет. Полноширинные скобки, удвоенное {{name}}, невидимый неразрывный пробел, кириллическая буква среди латинских — у каждого случая своя формулировка, и все они с примерами собраны на странице Переводчикам. Эта страница написана так, чтобы её можно было передать тому, кто правит .po.

Рендеринг шаблона без каталога

compile_template создаёт msgid, связывает значения и рендерит шаблон:

from gettext_tstrings import compile_template

name = "Ada"
compiled = compile_template(t"Hello {name}")

compiled.msgid  # "Hello {name}"
compiled.placeholders  # ("name",)
compiled.render("こんにちは {name}")  # "こんにちは Ada"

render проверяет по тем же правилам и при несоответствии всегда вызывает исключение. Без поиска в каталоге нет fallback.

Безопасность и границы

Допустимо:

tr(t"Hello {name}")

Намеренно отклоняется:

tr(t"Hello {user.name}")  # attribute access
tr(t"Hello {display_name()}")  # a call

Сначала явно вычислите значение:

name = user.display_name()
tr(t"Hello {name}")

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