Зачем нужны t-строки¶
Четыре способа подставить значение в переводимое сообщение, сравнённые на одном и том же предложении. Все четыре дают заполнителям имена и позволяют переводчику их переставлять; различаются они тем, что происходит при неверном переводе, тем, до какой части вашей программы дотягивается каталог, и тем, во что обходится их внедрение.
Сначала идут таблицы — найдите интересующую вас строку и читайте только тот раздел, который за ней стоит.
К каждому переведённому сообщению причастны три стороны
Каталог — это файл переводов: .po, пока его редактируют люди, и
скомпилированный .mo, который загружает приложение —
учебник проходит оба шага. К каждому сообщению причастны
три стороны: разработчик пишет исходную строку, переводчик
редактирует каталог — часто на внешней платформе, вдали от какого-либо
code review, — а приложение соединяет их во время выполнения. Каждый
стиль форматирования ниже по-своему отвечает на один и тот же вопрос:
какую часть языка форматирования контролирует каталог? В примерах _ —
традиционное имя функции перевода, а tr — имя из этой библиотеки.
Сравнение¶
Когда переводчик ошибается. Каталог проходит через множество рук, и бо́льшая часть того, что в нём портится, портится случайно:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Перевод удаляет заполнитель — что выводится? | значение молча исчезает | значение молча исчезает | значение молча исчезает | исходное сообщение с предупреждением (по умолчанию) |
| Перевод добавляет неизвестный заполнитель — что выводится? | исключение | исключение | заполнитель остаётся видимым как текст | исходное сообщение с предупреждением (по умолчанию) |
| Перевод переформатирует заполнитель — что выводится? | то, что запросил каталог, либо исключение, если буква типа больше не подходит значению | то, что запросил каталог | в $-строках невыразимо |
исходное сообщение с предупреждением |
| Проверяются ли заполнители при рендеринге? | нет | нет | нет | да (см. ниже) |
Какой властью обладает каталог. Перевод — это данные, приходящие извне вашего репозитория, и каждый стиль отдаёт им разный объём полномочий:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Откуда берутся значения? | явное отображение | явные аргументы | локальные и глобальные переменные вызывающей стороны плюс необязательный extras |
значения, захваченные внутри t-строки |
| Может ли каталог изменить форматирование значения? | да | да | нет | нет |
| Может ли каталог заглянуть внутрь объектов (доступ к атрибутам)? | нет | да | да, с именами через точку | нет |
| Где живёт «текущий язык»? | там, куда его положит приложение | там, куда его положит приложение | стек кодов языков в общем объекте приложения | ContextVar, для каждой задачи или запроса |
Во что обходится внедрение. Всё вышеперечисленное достаётся бесплатно, если инструменты подходят; вот там, где они могут не подойти:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Минимальный Python | любой | любой | 3.10 | 3.14 |
| Зрелость | стандартная библиотека | стандартная библиотека | стабильный выпуск | alpha |
| Использует ли обычные каталоги PO/MO? | да | да | да | да |
| Нужен ли специальный экстрактор исходного кода? | нет | нет | нет | да, сейчас |
| Какой флаг PO выводит Babel для проверки существующими инструментами? | python-format |
python-brace-format |
отсутствует | python-brace-format |
О проверке при рендеринге: для сообщений в единственном числе проверяется точное совпадение набора заполнителей. Сообщения с множественным числом тоже проверяются — по правилу объединения и пересечения, которое позволяет формам множественного числа целевого языка отличаться от исходных; более строгая проверка каждой формы выполняется при компиляции каталогов (Извлечение).
Строка о флаге формата относится к проверке с учётом заполнителей, а не к
совместимости каталога. отсутствует означает, что стандартные инструменты
gettext по-прежнему читают и компилируют сообщение, но у
msgfmt --check-format нет грамматики $-заполнителей для применения.
Совместимость и зрелость¶
Первые две строки последней таблицы и решают вопрос о внедрении, так что их стоит проговорить словами, а не ячейками.
%-формат и .format() встроены в Python и вообще не требуют зависимостей.
flufl.i18n — зрелый пакет, выпущенный и используемый в
продакшене, который работает на Python 3.10 и новее. gettext-tstrings
находится на стадии alpha и требует Python 3.14 или новее, потому что
t-строки — новый синтаксис в 3.14, обратного портирования нет и быть не может.
Его спецификация — стабильная часть; Python API до 1.0 ещё может
измениться.
Чего не стоит ни один из них, так это совместимости каталогов. Все четыре дают обычные файлы POT/PO/MO, которые уже читают любой PO-редактор, любая платформа перевода и любой инструмент GNU gettext, — поэтому выбор ниже обратим так, как не была бы обратима смена формата каталога. Миграция описывает переход существующего проекта.
Разделы ниже разбирают каждый компромисс подробно, по одному методу за раз.
%-форматирование¶
Что может пойти не так: повреждённая подстановка оборачивается исключением во время выполнения, если только проверка каталога не поймает её раньше.
В каталоге находится синтаксис printf, в том числе завершающая буква типа —
s в %(name)s, — которую легко не заметить и легко повредить:
>>> "Hello %(name)" % {"name": "Ada"} # the trailing "s" was deleted
Traceback (most recent call last):
...
ValueError: incomplete format
Правка одного символа в PO-редакторе становится трейсбэком в продакшене. GNU
msgfmt --check-format это обнаружит, но лишь для сообщений с флагом
python-format и только если каталог действительно проходит через msgfmt по
пути к вашему приложению.
str.format¶
Завершающая буква типа исчезает, при этом заполнитель остаётся именованным и может свободно переставляться. Что может пойти не так — перемещается на другую сторону обмена: перевод получает власть над вашими объектами.
str.format — небольшой язык выражений, и его вызов для строки даёт этой
строке право использовать этот язык:
>>> "{name.__class__.__mro__}".format(name="Ada")
"(<class 'str'>, <class 'object'>)"
>>> settings.api_key = "sk-live-…"
>>> "{conf.api_key}".format(conf=settings)
'sk-live-…'
Теперь замените эти литеральные строки на то, что возвращает _(). Если
перевод Hello {name} вернулся как {conf.api_key}, его рендеринг напечатает
ваш API-ключ — что именно будет прочитано, решил каталог, а не ваш код.
Каталог — не код, но путешествует он как данные: на платформу перевода, через
несколько рук, обратно в виде .po, компилируется в .mo, а иногда целиком
поставляется извне проекта. .format() даёт каждому шагу этого пути доступ к
атрибутам передаваемых объектов.
$-строки и flufl.i18n¶
from flufl.i18n import initialize
_ = initialize("example")
name = "Ada"
print(_("Hello $name")) # Hello Ada — the value came from the caller's locals
Стандартная библиотека предоставляет язык интерполяции $name через
string.Template, но сам по себе это не API перевода.
flufl.i18n сочетает этот стиль с поиском в каталогах gettext.
Обратите внимание: значение никуда не передаётся — flufl.i18n строит
пространство имён подстановки из глобальных и локальных переменных вызывающей
стороны, так что сообщению доступны любые переменные, существующие в точке
вызова. Необязательное отображение extras имеет приоритет над ними. В
синтаксисе для переводчика нет завершающей буквы типа или спецификатора
формата, а заполнители можно свободно переставлять.
Недоступная подстановка не вызывает исключения. При name = "Ada" и отсутствии
nombre в пространстве имён вызывающей стороны перевод каталога
Hello $nombre отображается как Hello $nombre: неразрешённый заполнитель
остаётся видимым. Это документированное поведение сохраняет остальную часть
переведённого сообщения вместо сбоя вызова. Исключения при разрешении атрибута
или преобразовании значения всё же могут распространяться дальше.
В одном важном отношении flufl.i18n способен на большее, чем обычный
string.Template. Его собственный Template принимает заполнители с точками,
например $settings.api_key, а его переводчик разрешает такие пути по
значениям вызывающей стороны. Заполнитель перевода может назвать любую
доступную локальную или глобальную переменную вызывающей стороны и с помощью
точечной записи пройти по её атрибутам. Это удобно, когда сообщению нужен
атрибут, но одновременно делает фрейм вызывающей стороны частью пространства
имён подстановки каталога. Здесь описывается flufl.i18n 6.0.0, а не все
возможные способы использования string.Template.
Он отвечает ещё на один вопрос, который два других стиля форматирования
целиком оставляют приложению: какой язык сейчас активен и как его сменить.
Объект приложения держит стек языков, _.push(code) и
_.pop() двигают его, with _.using(code): вкладывает блоки, а
стратегия находит каталог по коду языка, так что самому приложению
никогда не приходится иметь дело с объектами каталогов. Сервер, которому в
пределах одной единицы работы нужно выдать текст более чем на одном языке —
страницу для читателя, уведомление тому, у кого в аккаунте выбран другой
язык, — это и есть тот случай, ради которого всё это существует.
Стек живёт в этом объекте приложения, а его разделяет весь процесс. Поэтому два перекрывающихся запроса делят один стек, и блоки, вложенность которых не строгая во времени, передают друг другу не тот язык:
async def greet(code, delay):
with _.using(code):
await asyncio.sleep(delay)
return _("Hello $name")
async def main():
return await asyncio.gather(greet("fr", 0.01), greet("ja", 0.02))
>>> asyncio.run(main()) # "fr" entered first and left first, so it read "ja" off the top
['こんにちは Ada', 'Bonjour Ada']
Эта библиотека сохраняет ту же возможность — привязки так же вкладываются и
разворачиваются, — но в ContextVar, а не в общем стеке, поэтому чередование
выше разрешается для каждой задачи отдельно. Эквиваленты — на странице
Несколько языков сразу. Чего она не даёт,
так это поиска каталога по коду языка: вы передаёте объект переводов, который в
обычном случае сводится к одному вызову gettext.translation(), а стандартная
библиотека кеширует разобранный каталог.
t-строки¶
Каталог по-прежнему видит Hello {name} и остаётся обычным каталогом PO/MO.
Разница в том, что переводу разрешено сказать — и кто это проверяет.
Эта библиотека сверяет каждый перевод с заполнителями исходного сообщения
перед рендерингом и принимает простые имена — и ничего больше. Для
t"Hello {name}":
| Перевод содержит | Причина отклонения |
|---|---|
{name.__class__.__mro__} |
placeholder {name.__class__.__mro__} must be a plain name, copied from the source message unchanged |
{name!r} |
placeholder {name} adds formatting; write {name} on its own, because the source message decides how the value is formatted |
{0} |
placeholder {0} must be a plain name, copied from the source message unchanged |
{nombre} |
translation does not match the source placeholders: {name} is missing; {nombre} is not in the source message |
Отклонено не значит упало: по умолчанию библиотека записывает предупреждение в журнал и выводит исходное сообщение, поэтому плохой каталог никогда не останавливает приложение — тот же контракт, которого придерживается сам gettext.
Форматирование остаётся там, где было написано, — в коде:
:,.2f никогда не попадает в каталог, поэтому никакой перевод не может его
изменить, и ни одному переводчику не приходится на него смотреть. При этом
формат здесь фиксированный, а не локализованный: выбирать цифры и
разделители под каждый язык —
работа Babel, выполняемая до вызова.
Ещё одно отличие — инструменты: t-строки — новый синтаксис, поэтому для их
извлечения в .pot сейчас нужен экстрактор с поддержкой t-строк, например
тот, который этот пакет предоставляет для Babel.
Цена ограничения¶
Помимо требования к версии Python, за всё это платят одним правилом: каждая интерполяция должна быть простым именем.
Это реальное ограничение — и то самое, из которого рождаются все гарантии выше. Вместе с привязкой значений на стороне исходного кода и проверкой заполнителей во время выполнения оно не позволяет строкам каталога вычислять выражения и сохраняет имена заполнителей осмысленными для того, кто их переводит.
f-строку вообще нельзя использовать таким образом: к моменту, когда её видит любая библиотека, это уже готовая строка, поэтому её перевод означает перевод фрагмента. t-строки (PEP 750) хранят статический текст и значения раздельно, сохраняя похожий на f-строки синтаксис и явную привязку значений.
Как Python пришёл к этому — две PEP с разницей в десять лет и обсуждение в stdlib, закрытое без ответа, — рассказано со ссылками на источники на странице Предыстория.