Skip to main content

Flask i18n: Xây dựng ứng dụng đa ngôn ngữ với Flask-Babel

Từ kiến thức gettext cơ bản đến triển khai thực tế: quốc tế hóa ứng dụng Flask bằng Flask-Babel, tệp PO và bản dịch AI tự động.

1

Cài đặt Flask-Babel

Flask-Babel là tiện ích mở rộng quốc tế hóa tiêu chuẩn cho Flask. Nó tích hợp GNU gettext với Flask và mẫu Jinja2, cung cấp sẵn hàm dịch, lựa chọn locale và hỗ trợ múi giờ.

Flask-Babel bọc Babel (thư viện i18n Python) và tích hợp vào vòng đời yêu cầu của Flask. Thư viện cung cấp các hàm gettext(), ngettext() và lazy_gettext(), cùng khả năng tự động phát hiện locale từ tiêu đề trình duyệt.
Terminal
pip install Flask-Babel
2

Cấu hình Babel

Tạo tệp babel.cfg để cho pybabel biết vị trí cần quét chuỗi có thể dịch, rồi khởi tạo Flask-Babel với hàm chọn locale nhằm xác định ngôn ngữ phục vụ từng yêu cầu.

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)
Hàm locale_selector được gọi với mọi yêu cầu. Nếu hàm trả về locale không có tệp .mo đã biên dịch, Flask-Babel sẽ âm thầm chuyển về locale mặc định. Hệ thống không báo lỗi, chuỗi chỉ hiển thị mà chưa dịch.
3

Đánh dấu chuỗi cần dịch

Bọc mọi chuỗi dành cho người dùng bằng gettext() trong mã Python và _() trong mẫu Jinja2. Dùng lazy_gettext() cho chuỗi được xác định lúc tải mô-đun (như nhãn biểu mẫu và cấu hình) nhưng cần dịch sau khi xử lý yêu cầu.

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>
Dùng _() thay cho gettext() trong mẫu Jinja2. Đây là dạng viết tắt gettext tiêu chuẩn, giúp mẫu gọn gàng. Flask-Babel tự động đăng ký _() làm biến toàn cục Jinja2.
4

Trích xuất thông báo

Chạy pybabel extract để quét mã nguồn và mẫu nhằm tìm chuỗi có thể dịch. Lệnh này tạo tệp .pot (Portable Object Template). Sau đó, khởi tạo danh mục cho từng ngôn ngữ đích hoặc cập nhật danh mục hiện có khi chuỗi nguồn thay đổi.

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
Luôn chạy pybabel update (không phải init) sau lần trích xuất đầu tiên. Chạy init trên thư mục ngôn ngữ hiện có sẽ ghi đè toàn bộ bản dịch. Lệnh update hợp nhất chuỗi mới trong khi giữ nguyên bản dịch hiện có.
5

Dịch tệp PO

Mở các tệp .po đã tạo và điền giá trị msgstr cho từng msgid. Tệp PO là văn bản thuần; bạn có thể sửa trực tiếp, dùng trình biên tập PO như Poedit hoặc tự động dịch bằng công cụ AI.

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"
Tệp PO có chú thích ngữ cảnh (dòng bắt đầu bằng #:) cho biết vị trí sử dụng từng chuỗi trong mã nguồn. Người dịch dựa vào đó để hiểu ngữ cảnh. Hãy giữ lại vì pybabel extract tự động tạo các chú thích này.
6

Biên dịch bản dịch

Dùng pybabel compile để biên dịch tệp .po thành tệp .mo nhị phân. Khi chạy, Flask-Babel đọc tệp .mo và không thể đọc trực tiếp tệp .po. Bạn phải biên dịch lại sau mỗi lần cập nhật bản dịch.

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.
Nếu bản dịch không xuất hiện sau khi sửa tệp .po, gần như chắc chắn bạn quên chạy pybabel compile. Đây là lỗi Flask-Babel phổ biến nhất. Hãy thêm bước biên dịch vào tập lệnh triển khai để tránh lỗi trên môi trường thực tế.
7

Xử lý dạng số nhiều và biến

Dùng ngettext() cho chuỗi phụ thuộc vào số lượng. Hàm nhận dạng số ít, dạng số nhiều và số lượng. Babel tự động dùng đúng quy tắc số nhiều cho từng ngôn ngữ: tiếng Anh có 2 dạng, tiếng Nga 3, tiếng Ả Rập 6 và tiếng Nhật 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 товаров в вашей корзине"
Không dùng if count == 1 cho logic số nhiều. Các ngôn ngữ như tiếng Pháp coi 0 là số ít. Tiếng Nga và Ả Rập có những dạng mà tiếng Anh không có. Hãy để ngettext() và quy tắc số nhiều CLDR của Babel lựa chọn.
8

Thêm chức năng chuyển locale

Tạo bộ chọn ngôn ngữ lưu lựa chọn của người dùng trong phiên Flask. Cập nhật hàm locale_selector để kiểm tra phiên trước, rồi dự phòng bằng cơ chế phát hiện của trình duyệt.

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>
Sau khi đổi locale của phiên, gọi flask_babel.refresh() để buộc Flask-Babel đọc lại locale cho yêu cầu hiện tại. Không có refresh(), locale cũ vẫn tồn tại đến yêu cầu tiếp theo.
9

Tự động dịch

Sau khi hoàn tất thiết lập Flask-Babel, hãy dùng AI để dịch tệp PO. Tự động hóa chu trình trích xuất–dịch–biên dịch trong quy trình CI/CD để đồng bộ bản dịch với mã nguồn.

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
Dịch tăng dần: khi thêm chuỗi mới và chạy pybabel update, chỉ dịch các mục mới chưa có bản dịch (giá trị msgstr trống) thay vì tạo lại mọi thứ. Cách này giữ nguyên bản dịch đã được con người rà soát.

Bổ sung: Dự phòng locale thông minh với flask-babel-locale-chain

Theo mặc định, Flask-Babel chuyển thẳng về locale mặc định khi locale ưu tiên của người dùng không khả dụng. Người dùng pt-BR chỉ có bản dịch pt-PT sẽ thấy tiếng Anh thay vì tiếng Bồ Đào Nha. flask-babel-locale-chain bổ sung chuỗi dự phòng có thể cấu hình để các locale liên quan chuyển tiếp tự nhiên.

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 là gói Python nguồn mở. Xem trên GitHub tại github.com/i18n-agent/flask-babel-locale-chain.

Tự động kiểm soát chất lượng bản dịch

Dùng i18n-validate để phát hiện khóa thiếu và chữ giữ chỗ hỏng trước khi phát hành. Thử nghiệm UI bằng bản dịch giả lập qua i18n-pseudo trước khi có bản dịch thật.

Lỗi thường gặp

Quên biên dịch .po thành .mo

Flask-Babel đọc tệp .mo đã biên dịch, không đọc tệp .po. Nếu bản dịch không xuất hiện sau khi sửa tệp .po, hãy chạy pybabel compile -d translations. Thêm bước này vào tập lệnh triển khai.

Dùng gettext() ở cấp mô-đun

gettext() cần ngữ cảnh yêu cầu. Nếu xác định chuỗi đã dịch ở cấp mô-đun (thuộc tính lớp, hằng số), hãy dùng lazy_gettext(). Hàm này trì hoãn việc dịch đến khi chuỗi thực sự được kết xuất trong một yêu cầu.

Trích xuất bỏ sót chuỗi

pybabel extract chỉ quét tệp khớp với mẫu trong babel.cfg. Nếu không trích xuất được chuỗi, hãy kiểm tra mẫu babel.cfg có khớp cấu trúc tệp không, thêm -k lazy_gettext để trích xuất lời gọi lazy và bảo đảm mẫu Jinja2 dùng đúng phần mở rộng (.html, .jinja2).

Lỗi bảng mã tệp PO

Tệp PO phải dùng bảng mã UTF-8. Nếu thấy UnicodeDecodeError, hãy kiểm tra tiêu đề Content-Type trong tệp .po: tiêu đề phải ghi charset=UTF-8. Một số trình biên tập lưu bằng bảng mã khác; hãy luôn xác minh sau khi sửa.

Cấu trúc tệp đề xuất

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/

Dùng thử i18n Agent ngay

Thả tệp bản dịch của bạn vào đây

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

hoặc nhấp để duyệt

Ngôn ngữ đích

Không cần đăng kýBáo giá tức thì

Câu hỏi thường gặp về Flask i18n