
Flask i18n: Flask-Babel로 다국어 앱 만들기
gettext 기초부터 프로덕션 배포까지: Flask-Babel, PO 파일, 자동화된 AI 번역으로 Flask 앱을 국제화해요.
Flask-Babel 설치
Flask-Babel은 Flask의 표준 국제화 확장 프로그램이에요. GNU gettext를 Flask 및 Jinja2 템플릿과 통합하며 번역 함수, 로케일 선택, 시간대 지원을 기본으로 제공해요.
pip install Flask-BabelBabel 구성
babel.cfg 파일을 만들어 pybabel이 번역할 문자열을 검색할 위치를 지정한 다음, 요청마다 제공할 언어를 결정하는 로케일 선택기 함수로 Flask-Babel을 초기화해요.
# babel.cfg — tells pybabel where to find translatable strings
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_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)번역할 문자열 표시
사용자에게 표시되는 모든 문자열을 Python 코드에서는 gettext()로, Jinja2 템플릿에서는 _()로 감싸요. 양식 레이블이나 구성처럼 모듈을 불러올 때 정의되고 나중에 요청 시 번역해야 하는 문자열에는 lazy_gettext()를 사용해요.
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')){# 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>메시지 추출
pybabel extract를 실행하여 원본 코드와 템플릿에서 번역할 문자열을 검색해요. 그러면 .pot(Portable Object Template) 파일이 생성돼요. 이후 대상 언어별 카탈로그를 초기화하거나 원본 문자열이 바뀌었을 때 기존 카탈로그를 업데이트해요.
# 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 translationsPO 파일 번역
생성된 .po 파일을 열고 각 msgid에 해당하는 msgstr 값을 입력해요. PO 파일은 일반 텍스트이므로 직접 편집하거나 Poedit 같은 PO 편집기를 사용하거나 AI 도구로 번역을 자동화할 수 있어요.
# 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"번역 컴파일
pybabel compile을 사용해 .po 파일을 바이너리 .mo 파일로 컴파일해요. Flask-Babel은 런타임에 .mo 파일을 읽으며 .po 파일은 직접 읽을 수 없어요. 번역을 업데이트할 때마다 다시 컴파일해야 해요.
# 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.복수형 및 변수 처리
복수형에 따라 달라지는 문자열에는 ngettext()를 사용해요. 단수형, 복수형, 개수를 인수로 받아요. Babel은 언어별로 올바른 복수형 규칙을 자동 적용해요. 영어에는 2가지, 러시아어에는 3가지, 아랍어에는 6가지, 일본어에는 1가지 형식이 있어요.
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)# 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 товаров в вашей корзине"로케일 전환 추가
사용자의 선택을 Flask 세션에 저장하는 언어 선택기를 만들어요. locale_selector 함수가 세션을 먼저 확인한 후 브라우저 감지로 폴백하도록 업데이트해요.
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']
){# 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 설정을 마쳤다면 AI로 PO 파일을 번역해요. 원본 코드와 번역을 동기화할 수 있도록 CI/CD 파이프라인에서 추출-번역-컴파일 주기를 자동화해요.
# 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보너스: flask-babel-locale-chain을 활용한 지능형 로케일 폴백
기본적으로 Flask-Babel은 사용자가 선호하는 로케일을 사용할 수 없을 때 기본 로케일로 바로 폴백해요. pt-PT 번역만 있으면 pt-BR 사용자에게 포르투갈어 대신 영어가 표시돼요. flask-babel-locale-chain은 구성 가능한 폴백 체인을 추가하여 관련 로케일이 자연스럽게 이어지도록 해요.
# 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)번역 품질 자동화
흔히 발생하는 문제
.po를 .mo로 컴파일하지 않음
모듈 수준에서 gettext() 사용
추출에서 문자열 누락
PO 파일 인코딩 오류
권장 파일 구조
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
또는 클릭하여 파일 선택
대상 언어