Skip to main content

i18n Flask: создание многоязычного приложения с Flask-Babel

От основ gettext до развёртывания: интернационализируйте приложение Flask с помощью Flask-Babel, файлов PO и автоматического перевода на основе ИИ.

1

Установить Flask-Babel

Flask-Babel — стандартное расширение интернационализации для Flask. Оно интегрирует GNU gettext с шаблонами Flask и Jinja2, сразу предоставляя функции перевода, выбор локали и поддержку часовых поясов.

Flask-Babel оборачивает Babel, библиотеку i18n для Python, и интегрирует её с жизненным циклом запросов Flask. Вы получаете функции gettext(), ngettext() и lazy_gettext(), а также автоматическое определение локали по заголовкам браузера.
Terminal
pip install Flask-Babel
2

Настроить Babel

Создайте файл babel.cfg, чтобы указать pybabel, где искать переводимые строки, а затем инициализируйте Flask-Babel с функцией выбора локали, определяющей язык для каждого запроса.

babel.cfg
# babel.cfg — tells pybabel where to find translatable strings
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
app.py
from flask import Flask, request
from flask_babel import Babel

app = Flask(__name__)
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_DEFAULT_TIMEZONE'] = 'UTC'
# Directory where translations live (default: "translations")
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'

def get_locale():
    # 1. Check URL parameter or session
    # 2. Fall back to browser Accept-Language header
    return request.accept_languages.best_match(['en', 'de', 'ja', 'es', 'fr'])

babel = Babel(app, locale_selector=get_locale)
Функция locale_selector вызывается при каждом запросе. Если она возвращает локаль без скомпилированного файла .mo, Flask-Babel без уведомления переходит на локаль по умолчанию. Ошибка не создаётся, строки просто остаются непереведёнными.
3

Пометить строки для перевода

Оберните каждую обращённую к пользователю строку в gettext() в коде Python и _() в шаблонах Jinja2. Используйте lazy_gettext() для строк, определённых при загрузке модуля, например названий полей форм и конфигурации, которые нужно перевести позже во время запроса.

app.py
from flask_babel import gettext, ngettext, lazy_gettext

# In views — gettext() for immediate translation
@app.route('/')
def index():
    flash(gettext('Your profile has been updated.'))
    return render_template('index.html',
        title=gettext('Home'))

# In forms/config — lazy_gettext() for deferred translation
class LoginForm(FlaskForm):
    username = StringField(lazy_gettext('Username'))
    password = PasswordField(lazy_gettext('Password'))
    submit = SubmitField(lazy_gettext('Sign In'))
templates/index.html
{# In Jinja2 templates, use _() shorthand for gettext #}
<h1>{{ _('Welcome to our app') }}</h1>

<p>{{ _('Hello, %(name)s!', name=user.name) }}</p>

<footer>
  {{ _('Copyright %(year)s Example Corp.', year=2026) }}
</footer>
Используйте _() в шаблонах Jinja2 вместо gettext(): это стандартное сокращение gettext, сохраняющее шаблоны понятными. Flask-Babel автоматически регистрирует _() как глобальную функцию Jinja2.
4

Извлечь сообщения

Запустите pybabel extract для сканирования исходного кода и шаблонов на переводимые строки. Будет создан файл .pot (Portable Object Template). Затем инициализируйте каталоги для каждого целевого языка или обновите существующие после изменения исходных строк.

Terminal
# Extract translatable strings from source code
pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .

# Initialize a new language (first time only)
pybabel init -i messages.pot -d translations -l de
pybabel init -i messages.pot -d translations -l ja
pybabel init -i messages.pot -d translations -l es

# Update existing catalogs when source strings change
pybabel update -i messages.pot -d translations
После первого извлечения всегда запускайте pybabel update, а не init. Запуск init для существующего каталога языка перезаписывает все переводы. Команда update объединяет новые строки, сохраняя существующие переводы.
5

Перевести файлы PO

Откройте созданные файлы .po и заполните значения msgstr для каждого msgid. Файлы PO являются обычным текстом: их можно редактировать напрямую, в редакторе PO вроде Poedit или переводить автоматически с помощью инструментов ИИ.

translations/de/LC_MESSAGES/messages.po
# translations/de/LC_MESSAGES/messages.po
msgid "Welcome to our app"
msgstr "Willkommen in unserer App"

msgid "Hello, %(name)s!"
msgstr "Hallo, %(name)s!"

msgid "Your profile has been updated."
msgstr "Ihr Profil wurde aktualisiert."

msgid "Username"
msgstr "Benutzername"

msgid "Password"
msgstr "Passwort"

msgid "Sign In"
msgstr "Anmelden"
Файлы PO содержат комментарии контекста — строки, начинающиеся с #:, — которые показывают, где каждая строка используется в исходном коде. Переводчики используют их для понимания контекста. Сохраняйте эти автоматически созданные pybabel extract комментарии.
6

Скомпилировать переводы

Скомпилируйте файлы .po в двоичные .mo с помощью pybabel compile. Во время выполнения Flask-Babel считывает файлы .mo и не может читать .po напрямую. После каждого обновления переводов необходимо повторять компиляцию.

Terminal
# Compile .po files to binary .mo files (required at runtime)
pybabel compile -d translations

# Flask-Babel reads .mo files, not .po files.
# You MUST compile after every translation update.
Если после редактирования файла .po переводы не появляются, Вы почти наверняка забыли запустить pybabel compile. Это самая распространённая проблема Flask-Babel. Добавьте этап компиляции в сценарий развёртывания, чтобы избежать её в рабочей среде.
7

Обработать формы множественного числа и переменные

Используйте ngettext() для строк, зависящих от количества. Функция принимает форму единственного числа, форму множественного числа и количество. Babel автоматически применяет правильное правило каждого языка: в английском 2 формы, в русском — 3, в арабском — 6, в японском — 1.

app.py
from flask_babel import ngettext

@app.route('/cart')
def cart():
    count = len(session.get('cart_items', []))
    message = ngettext(
        '%(num)d item in your cart',     # singular
        '%(num)d items in your cart',    # plural
        count                             # determines which form
    )
    return render_template('cart.html', message=message)
Plural forms in .po files
# English: 2 forms (nplurals=2)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d item in your cart"
msgstr[1] "%(num)d items in your cart"

# German: 2 forms (nplurals=2)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d Artikel in Ihrem Warenkorb"
msgstr[1] "%(num)d Artikel in Ihrem Warenkorb"

# Japanese: 1 form (nplurals=1)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "カートに%(num)d個の商品があります"

# Russian: 3 forms (nplurals=3)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d товар в вашей корзине"
msgstr[1] "%(num)d товара в вашей корзине"
msgstr[2] "%(num)d товаров в вашей корзине"
Никогда не используйте условие if count == 1 для логики множественного числа. Во французском 0 считается единственным числом. В русском и арабском есть формы, отсутствующие в английском. Поручите выбор ngettext() и правилам множественного числа CLDR в Babel.
8

Добавить переключение локали

Создайте переключатель языка, сохраняющий выбор пользователя в сеансе Flask. Обновите функцию locale_selector, чтобы она сначала проверяла сеанс, а затем переходила на определение браузера.

app.py
from flask import session, redirect, url_for, request
from flask_babel import refresh

@app.route('/set-language/<lang>')
def set_language(lang):
    session['lang'] = lang
    refresh()  # Force Flask-Babel to re-read the locale
    return redirect(request.referrer or url_for('index'))

# Update get_locale to check session first
def get_locale():
    # 1. Explicit user choice (stored in session)
    if 'lang' in session:
        return session['lang']
    # 2. Browser Accept-Language header
    return request.accept_languages.best_match(
        ['en', 'de', 'ja', 'es', 'fr']
    )
templates/components/language_switcher.html
{# Language switcher component #}
<nav class="language-switcher">
  {% for lang, name in [('en','English'),('de','Deutsch'),
                         ('ja','日本語'),('es','Español'),
                         ('fr','Français')] %}
    <a href="{{ url_for('set_language', lang=lang) }}"
       class="{{ 'active' if get_locale() == lang }}">
      {{ name }}
    </a>
  {% endfor %}
</nav>
После изменения локали сеанса вызовите flask_babel.refresh(), чтобы Flask-Babel повторно считал локаль для текущего запроса. Без refresh() старая локаль сохраняется до следующего запроса.
9

Автоматизировать перевод

После завершения настройки Flask-Babel переведите файлы PO с помощью ИИ. Автоматизируйте цикл извлечения, перевода и компиляции в конвейере CI/CD, чтобы переводы оставались синхронизированными с исходным кодом.

Terminal
# Translate your PO files with AI directly from your IDE
# or use the CLI in CI/CD:
npx i18n-agent translate translations/de/LC_MESSAGES/messages.po \
  --source-lang en --target-lang de

# Bulk translate all languages at once:
npx i18n-agent translate messages.pot --lang de,ja,es,fr

# Then compile:
pybabel compile -d translations
Переводите постепенно: добавив новые строки и выполнив pybabel update, переведите только новые непереведённые записи с пустыми значениями msgstr, а не создавайте всё заново. Это сохранит переводы, проверенные людьми.

Дополнение: умная резервная локаль с flask-babel-locale-chain

По умолчанию Flask-Babel сразу переходит на локаль по умолчанию, если предпочитаемая локаль пользователя недоступна. Пользователь pt-BR при наличии только переводов pt-PT видит английский вместо португальского. flask-babel-locale-chain добавляет настраиваемые цепочки, чтобы родственные локали переходили друг к другу естественным образом.

app.py
# pip install flask-babel-locale-chain
from flask_babel_locale_chain import LocaleChain

# Define fallback chains: pt-BR falls back to pt before en
locale_chain = LocaleChain({
    'pt-BR': ['pt-BR', 'pt', 'en'],
    'pt-PT': ['pt-PT', 'pt', 'en'],
    'zh-Hant': ['zh-Hant', 'zh-Hans', 'en'],
    'en-GB': ['en-GB', 'en', 'en-US'],
})

def get_locale():
    requested = request.accept_languages.best_match(
        ['en', 'pt-BR', 'pt', 'zh-Hant', 'zh-Hans']
    )
    # Returns the best available locale from the chain
    return locale_chain.resolve(requested)
flask-babel-locale-chain — пакет Python с открытым исходным кодом. Он доступен на GitHub: github.com/i18n-agent/flask-babel-locale-chain.

Автоматизировать контроль качества перевода

Выявляйте отсутствующие ключи и нарушенные заполнители до выпуска с помощью i18n-validate. Тестируйте интерфейс с псевдопереводами через i18n-pseudo, пока настоящие переводы ещё не готовы.

Распространённые ошибки

Забыли скомпилировать .po в .mo

Flask-Babel считывает скомпилированные файлы .mo, а не .po. Если после редактирования .po переводы не появляются, выполните pybabel compile -d translations. Добавьте этот этап в сценарий развёртывания.

Использование gettext() на уровне модуля

gettext() требует контекста запроса. Если переведённые строки определяются на уровне модуля, например в атрибутах классов или константах, используйте lazy_gettext(). Он откладывает перевод до фактической отрисовки строки в запросе.

При извлечении пропускаются строки

pybabel extract сканирует только файлы, соответствующие шаблонам в babel.cfg. Если строки не извлекаются, убедитесь, что шаблоны babel.cfg соответствуют структуре файлов, добавьте -k lazy_gettext для отложенных вызовов и проверьте расширение шаблонов Jinja2 (.html, .jinja2).

Ошибки кодировки файлов PO

Файлы PO должны использовать кодировку UTF-8. При UnicodeDecodeError проверьте заголовок Content-Type в файле .po: в нём должно быть указано charset=UTF-8. Некоторые редакторы сохраняют данные в других кодировках, поэтому всегда проверяйте после редактирования.

Рекомендуемая структура файлов

Project Structure
my-flask-app/
├── app.py                          # Flask app with Babel config
├── babel.cfg                       # Extraction config
├── messages.pot                    # Template (extracted strings)
├── translations/
│   ├── de/
│   │   └── LC_MESSAGES/
│   │       ├── messages.po         # German translations (editable)
│   │       └── messages.mo         # Compiled binary (generated)
│   ├── ja/
│   │   └── LC_MESSAGES/
│   │       ├── messages.po
│   │       └── messages.mo
│   └── es/
│       └── LC_MESSAGES/
│           ├── messages.po
│           └── messages.mo
├── templates/
│   ├── base.html
│   ├── index.html
│   └── components/
│       └── language_switcher.html
├── requirements.txt
└── venv/

Попробовать i18n Agent

Перетащите сюда файл перевода

JSON, YAML, PO, XML, CSV, Markdown, Properties

или нажмите, чтобы выбрать

Целевые языки

Регистрация не требуетсяМгновенный расчёт

Частые вопросы об i18n Flask