2025, Sep 22 14:00
Как перевести подсказки в атрибуте title в Flask/Jinja с помощью pybabel
Разбираем, почему строки в атрибуте title не попадают в каталоги перевода Flask/Jinja, и как исправить с pybabel: используем ключ --no-wrap для extract и init.
Перевод всплывающих подсказок в интерфейсе на основе шаблонов Flask/Jinja выглядит обманчиво простым — пока строки, привязанные к атрибутам title, упрямо не хотят появляться в ваших каталогах. Вы оборачиваете их в вызов gettext, запускаете пайплайн извлечения — и всё равно ничего не появляется — или, что ещё хуже, итоговый .pot оказывается испорченным и непригодным к переводу. Исправление находится не в шаблоне, а в том, как вызывается pybabel.
Проблема
Строки, встроенные в HTML-атрибуты, например в title у подсказок Bootstrap, не подхватываются для перевода. Даже если текст обёрнут в вызов gettext внутри Jinja-шаблона, извлечение не создаёт рабочей записи в файле .pot.
Пример
Ниже — минимальная иллюстрация с подсказкой, чей title нужно локализовать. Разметка использует функцию перевода в многострочном атрибуте — это типично для более длинного текста подсказок.
<div class="col col-lg-3">
<label for="machineIrrNew">
{{ _l('Ideal Run Rate (unit/min)') }}
<button type="button" id="irrHint" class="fa-solid fa-circle-info"
data-bs-toggle="tooltip" data-bs-placement="top" data-bs-delay="0"
title="{{ _l('Used to
specify an optional default ideal run rate for continuous products. It has no effect on
batch products. Setting a default does not prevent you from specifiying custom run rates
for particular products') }}">
</button>
</label>
</div>
Почему так происходит
Проблема возникает не из-за шаблона. Корень в стандартном поведении pybabel: инструмент вставляет переносы в длинные строки и на этапе extract, и на этапе init. Даже если вы не разбиваете строки в шаблонах, pybabel всё равно сделает перенос, и в результате получится «битый» .pot, который нельзя корректно переводить. Когда каталог повреждён, строки в атрибутах — например, в title подсказки — распознаются неправильно.
Решение
Отключите переносы на уровне инструмента. Добавьте флаг --no-wrap и к pybabel extract, и к pybabel init в вашем пайплайне автоматизации. Как только переносы полностью отключены, строки в атрибуте title корректно распознаются и извлекаются.
import subprocess
subprocess.run(
[
"pybabel",
"extract",
"--no-wrap",
"-F",
"babel.cfg",
"-k",
"_l",
"-o",
"messages.pot",
".",
],
check=True,
text=True,
)
import subprocess
locale_tag = lang # предполагается, что значение приходит извне
subprocess.run(
[
"pybabel",
"init",
"--no-wrap",
"-i",
"messages.pot",
"-d",
"src/pro_machina/translations",
"-l",
locale_tag,
],
check=True,
text=True,
)
С отключённой обёрткой и там, и там pybabel корректно подхватывает текст, помещённый в атрибут title.
Зачем это важно
Содержимое подсказок и другие строки в атрибутах обычно длиннее и нередко разбиваются на несколько строк ради читаемости. Если на этапе извлечения они тихо «переносятся» и повреждаются, вы получаете каталоги, которые нельзя надёжно переводить. Исправление пайплайна избавляет от хрупких обходных решений в шаблонах и делает процесс i18n предсказуемым.
Итоги
Если строки из атрибутов не попадают в каталоги, не боритесь с шаблоном. Запускайте pybabel с --no-wrap и для extract, и для init. Оставьте вызовы gettext в HTML, пересоберите каталоги — и подсказки будут готовы к переводу без клиентских костылей.