Skip to main content

Flask i18n: Flask-Babel로 다국어 앱 만들기

gettext 기초부터 프로덕션 배포까지: Flask-Babel, PO 파일, 자동화된 AI 번역으로 Flask 앱을 국제화해요.

1

Flask-Babel 설치

Flask-Babel은 Flask의 표준 국제화 확장 프로그램이에요. GNU gettext를 Flask 및 Jinja2 템플릿과 통합하며 번역 함수, 로케일 선택, 시간대 지원을 기본으로 제공해요.

Flask-Babel은 Babel(Python i18n 라이브러리)을 감싸 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

번역할 문자열 표시

사용자에게 표시되는 모든 문자열을 Python 코드에서는 gettext()로, 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 init이 아니라 pybabel update를 실행하세요. 기존 언어 디렉터리에 init을 실행하면 기존 번역을 모두 덮어써요. update 명령은 기존 번역을 보존하면서 새 문자열을 병합해요.
5

PO 파일 번역

생성된 .po 파일을 열고 각 msgid에 해당하는 msgstr 값을 입력해요. PO 파일은 일반 텍스트이므로 직접 편집하거나 Poedit 같은 PO 편집기를 사용하거나 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"
PO 파일에는 원본 코드에서 각 문자열이 사용된 위치를 보여 주는 컨텍스트 주석(#:로 시작하는 줄)이 포함돼요. 번역가는 이 정보를 바탕으로 문맥을 파악해요. 이 주석은 pybabel extract가 자동으로 생성하므로 그대로 유지하세요.
6

번역 컴파일

pybabel compile을 사용해 .po 파일을 바이너리 .mo 파일로 컴파일해요. 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()와 Babel의 CLDR 복수형 규칙이 선택을 처리하도록 하세요.
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 설정을 마쳤다면 AI로 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-PT 번역만 있으면 pt-BR 사용자에게 포르투갈어 대신 영어가 표시돼요. 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의 의사 번역으로 UI를 테스트해요.

흔히 발생하는 문제

.po를 .mo로 컴파일하지 않음

Flask-Babel은 .po 파일이 아니라 컴파일된 .mo 파일을 읽어요. .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가 표시되면 .po 파일의 Content-Type 헤더를 확인하세요. 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

또는 클릭하여 파일 선택

대상 언어

가입 불필요즉시 견적

Flask i18n 자주 묻는 질문