Руководство¶
Эта страница — справочник по времени выполнения: всё, что делает с этой библиотекой код приложения, когда каталоги уже существуют. Если вы ещё не видели весь цикл — пометить, извлечь, перевести, скомпилировать, запустить, — учебник проходит его за пять минут; создание и проверка каталогов описаны в разделе Извлечение, а то, как команда поддерживает вращение цикла — циклы обновления, 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 задаётся там, где сообщение написано, а не там, где оно рендерится:
Отложенная строка рендерится там, где её в итоге используют, — внутри шаблона,
формы, строки лога, — и это место редко знает, тестовый ли это прогон или
продакшен. Именно передача 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. Передавайте контекст, а не полагайтесь на значение по умолчанию:
asyncio.to_thread уже делает это за вас.
Значения с учётом локали¶
Эта библиотека решает, где значение окажется в переведённом сообщении. Само
значение она не локализует. {amount:,.2f} — это спецификация формата Python
с фиксированным поведением: запятая через каждые три разряда и точка перед
дробной частью, — и она даёт одни и те же символы, на каком бы языке ни было
сообщение:
По-немецки это число пишется 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
Предупреждение появляется один раз для каждой пары сообщения и шаблона, а не при каждом рендеринге, поэтому неверная запись каталога не заливает журнал.
В тестах и 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.
Безопасность и границы¶
Допустимо:
Намеренно отклоняется:
Сначала явно вычислите значение:
Перевод никогда не вычисляется и не может добавить доступ к атрибутам, вызов, преобразование или формат. Как и с обычным gettext, приложение отвечает за экранирование для места вывода и целостность каталога.