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, пересоберите каталоги — и подсказки будут готовы к переводу без клиентских костылей.