목차
Python i18n: gettext부터 모던 번역 워크플로우까지
Python은 모든 프로그래밍 언어 중에서 가장 풍부한 i18n 에코시스템을 보유하고 있습니다. 이는 수십 년에 걸친 대규모 다국어 애플리케이션의 투자——특히 전 세계에 배포된 Django 웹 프레임워크와 국제 커뮤니티를 지원해야 하는 과학 계산 및 데이터 도구에서의 Python 광범위한 활용——덕분입니다.
이 가이드는 Python i18n의 전체 영역을 다룹니다. 표준 라이브러리에 내장된 클래식한 gettext 방식부터 Babel의 포괄적인 로케일 데이터 라이브러리, Django와 Flask를 위한 프레임워크별 솔루션, 그리고 Fluent를 활용한 모던한 접근 방식까지 설명합니다.
Python i18n 옵션 한눈에 보기
| 라이브러리 / 방식 | 최적 용도 |
|---|---|
| gettext (stdlib) | 간단한 스크립트와 애플리케이션 |
| Babel | 숫자, 날짜, 통화 포맷팅; PO 파일 관리 |
| Django i18n | Django 웹 애플리케이션 |
| Flask-Babel | Flask 웹 애플리케이션 |
| fluent.runtime | Mozilla Fluent 포맷을 사용하는 애플리케이션 |
| Babel + gettext | 대부분의 프로덕션 Python 웹 서비스 |
gettext: Python의 내장 i18n
Python 표준 라이브러리에는 GNU gettext 국제화 API를 구현하는 gettext가 포함되어 있습니다. 이것은 많은 상위 라이브러리들이 구축되는 기반입니다.
gettext 동작 원리
gettext는 번역 저장을 위해 .po(Portable Object) 파일을 사용하고, 런타임 로딩을 위해 바이너리 .mo(Machine Object) 파일로 컴파일합니다.
워크플로우:
- 소스 코드의 문자열을
_()또는gettext()로 표시합니다 xgettext또는pybabel로 표시된 문자열을.pot템플릿으로 추출합니다- 템플릿에서 로케일별
.po파일을 생성합니다 - 번역가가 번역을 채워 넣습니다
.po파일을.mo파일로 컴파일합니다- 사용자 로케일에 따라 런타임에 로드합니다
gettext 기본 사용법
import gettext
import locale
def setup_i18n(lang: str) -> gettext.GNUTranslations:
"""지정된 언어의 번역을 로드합니다."""
translation = gettext.translation(
domain='messages',
localedir='locales',
languages=[lang],
fallback=True # 찾지 못하면 msgid(소스 문자열)로 폴백
)
return translation
# 애플리케이션 설정
trans = setup_i18n('fr')
_ = trans.gettext
ngettext = trans.ngettext # 복수형 지원 번역
# 사용 예시
print(_("Hello, world!"))
print(_("Welcome, %(name)s!") % {"name": "Alice"})
# 복수형
count = 3
print(ngettext(
"%(count)d item", # 단수형
"%(count)d items", # 복수형
count # 형식을 결정하는 숫자
) % {"count": count})
추출을 위한 문자열 마킹
# _()로 직접 마킹
title = _("Dashboard")
error = _("An error occurred: %(message)s") % {"message": str(e)}
# i18n이 설정되기 전에 정의해야 하는 문자열에는
# 지연 번역 패턴을 사용합니다:
def _(s):
return s # 정의 시 아무것도 하지 않음
# 이 문자열들은 추출되지만 런타임까지 번역되지 않습니다
ERROR_MESSAGES = {
"not_found": _("Resource not found"),
"unauthorized": _("You are not authorized to perform this action"),
}
# 런타임에 실제 _ 함수로 번역합니다:
def get_error_message(key: str, translation_func) -> str:
return translation_func(ERROR_MESSAGES[key])
Babel: Python의 포괄적인 i18n 라이브러리
Babel은 숫자, 통화, 날짜 등을 위한 완전한 CLDR 기반 로케일 데이터로 gettext를 확장합니다. Python에서 가장 포괄적인 i18n 라이브러리입니다.
pip install Babel
숫자와 통화 포맷팅
from babel.numbers import format_number, format_currency, format_percent
from babel import Locale
# Locale 객체
en_us = Locale('en', 'US')
de_de = Locale('de', 'DE')
ja_jp = Locale('ja', 'JP')
# 숫자 포맷팅
amount = 1234567.89
print(format_number(amount, locale='en_US')) # 1,234,567.89
print(format_number(amount, locale='de_DE')) # 1.234.567,89
print(format_number(amount, locale='en_IN')) # 12,34,567.89
# 통화 포맷팅
print(format_currency(1234.56, 'USD', locale='en_US')) # $1,234.56
print(format_currency(1234.56, 'EUR', locale='de_DE')) # 1.234,56 €
print(format_currency(1234.56, 'JPY', locale='ja_JP')) # ¥1,235 (소수점 없음)
# 퍼센트
print(format_percent(0.8527, locale='en_US')) # 85%
print(format_percent(0.8527, '#.##%', locale='de_DE')) # 85,27%
날짜와 시간 포맷팅
from babel.dates import format_date, format_datetime, format_time, get_timezone
from datetime import datetime, date
dt = datetime(2024, 3, 15, 14, 30, 0)
d = date(2024, 3, 15)
# 날짜 포맷
print(format_date(d, locale='en_US')) # Mar 15, 2024
print(format_date(d, locale='de_DE')) # 15.03.2024
print(format_date(d, locale='ja_JP')) # 2024/03/15
print(format_date(d, format='full', locale='fr_FR')) # vendredi 15 mars 2024
# 타임존이 있는 날짜시간
tz = get_timezone('Europe/Berlin')
print(format_datetime(dt, locale='de_DE', tzinfo=tz))
# 상대적 시간 (예: "3시간 전")
from babel.dates import format_timedelta
from datetime import timedelta
delta = timedelta(hours=-3)
print(format_timedelta(delta, locale='en_US', add_direction=True)) # 3 hours ago
print(format_timedelta(delta, locale='fr_FR', add_direction=True)) # il y a 3 heures
Babel로 PO 파일 관리하기
Babel은 번역 파일을 관리하는 커맨드라인 도구 pybabel을 제공합니다:
# 1. Python 소스에서 번역 가능한 문자열 추출 pybabel extract -F babel.cfg -o messages.pot . # babel.cfg는 추출할 파일을 설정합니다: # [python: **.py] # [jinja2: **/templates/**.html] # 2. 새 언어 초기화 (locales/fr/LC_MESSAGES/messages.po 생성) pybabel init -i messages.pot -d locales -l fr # 3. 소스 변경 후 기존 .po 파일 업데이트 pybabel update -i messages.pot -d locales # 4. 런타임 사용을 위해 .po 파일을 .mo 파일로 컴파일 pybabel compile -d locales
Babel/gettext의 복수형
# .po 파일 내 (프랑스어):
# msgid "%(count)d item"
# msgid_plural "%(count)d items"
# msgstr[0] "%(count)d élément"
# msgstr[1] "%(count)d éléments"
# Python에서:
from babel.support import Translations
translations = Translations.load('locales', ['fr'])
ngettext = translations.ngettext
for count in [0, 1, 2, 10]:
msg = ngettext("%(count)d item", "%(count)d items", count) % {"count": count}
print(msg)
Django i18n
Django는 템플릿, 모델, ORM에 깊이 통합된 포괄적인 내장 i18n 지원을 제공합니다.
Django i18n 설정
# settings.py
LANGUAGE_CODE = 'en-us'
LANGUAGES = [
('en', 'English'),
('fr', 'Français'),
('de', 'Deutsch'),
('ja', '日本語'),
('ar', 'العربية'),
]
USE_I18N = True
USE_L10N = True # 로케일에 맞는 숫자/날짜 포맷팅
USE_TZ = True
LOCALE_PATHS = [BASE_DIR / 'locale']
MIDDLEWARE = [
'django.middleware.locale.LocaleMiddleware', # 요청에서 언어 설정
# ... 다른 미들웨어
]
TEMPLATES = [{
'OPTIONS': {
'context_processors': [
'django.template.context_processors.i18n',
# ...
],
},
}]
Python 코드에서 Django 번역
from django.utils.translation import gettext as _, ngettext, gettext_lazy as _lazy
# 즉시 번역 (바로 평가됨)
message = _("Welcome!")
# 지연 번역 (문자열 접근 시 평가됨 - 모델 필드와 클래스 속성에 사용)
class Article(models.Model):
title = models.CharField(max_length=200)
class Meta:
verbose_name = _lazy("article")
verbose_name_plural = _lazy("articles")
# 복수형
def item_message(count: int) -> str:
return ngettext(
"You have %(count)d item",
"You have %(count)d items",
count
) % {"count": count}
# 문자열 포맷팅 - 번역 가능성을 위해 명명된 매개변수 사용
def welcome(name: str) -> str:
return _("Welcome, %(name)s!") % {"name": name}
# 컨텍스트에 민감한 번역 (같은 문자열, 다른 의미)
from django.utils.translation import pgettext
month = pgettext("month name", "May") # 동사로서의 "May"와 구별
Django 템플릿 번역
{% load i18n %}
{# 단순 번역 #}
<h1>{% trans "Dashboard" %}</h1>
{# 변수가 있는 번역 #}
{% blocktrans with name=user.first_name %}
Welcome, {{ name }}!
{% endblocktrans %}
{# 템플릿의 복수형 #}
{% blocktrans count count=items|length %}
You have {{ count }} item.
{% plural %}
You have {{ count }} items.
{% endblocktrans %}
{# 언어 전환기 #}
{% get_available_languages as LANGUAGES %}
<ul>
{% for lang_code, lang_name in LANGUAGES %}
<li>
<a href="/{{ lang_code }}/">{{ lang_name }}</a>
</li>
{% endfor %}
</ul>
Django URL i18n 패턴
# urls.py
from django.conf.urls.i18n import i18n_patterns
from django.urls import path, include
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), # 언어 전환기 엔드포인트
]
urlpatterns += i18n_patterns(
# 이 URL들은 언어 접두사를 가짐: /en/about/, /fr/about/
path('about/', views.about, name='about'),
path('products/', include('products.urls')),
prefix_default_language=False, # /about/는 /en/about/로 리다이렉트
)
Flask-Babel을 활용한 Flask i18n
Flask-Babel은 Babel의 강력함을 Flask 애플리케이션에 제공합니다:
pip install Flask-Babel
# app.py
from flask import Flask, g, request
from flask_babel import Babel, _, ngettext, format_currency, format_datetime
app = Flask(__name__)
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_DEFAULT_TIMEZONE'] = 'UTC'
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
def get_locale():
# 1. URL 파라미터 확인
lang = request.args.get('lang')
if lang:
return lang
# 2. 세션/데이터베이스의 사용자 설정 확인
if hasattr(g, 'current_user') and g.current_user.language:
return g.current_user.language
# 3. Accept-Language 헤더 사용
return request.accept_languages.best_match(['en', 'fr', 'de', 'ja'])
babel = Babel(app, locale_selector=get_locale)
@app.route('/products/<int:product_id>')
def product_detail(product_id):
product = Product.query.get_or_404(product_id)
# 현재 로케일을 자동으로 사용합니다
price = format_currency(product.price, 'USD')
created = format_datetime(product.created_at, format='medium')
return render_template('product.html',
product=product,
price=price,
created=created,
title=_("Product: %(name)s") % {"name": product.name}
)
모던한 접근 방식: Python용 Fluent
Mozilla의 Fluent는 fluent.runtime을 통해 Python에서 사용 가능합니다:
pip install fluent.runtime
from fluent.runtime import FluentBundle, FluentResource
# Fluent 파일 로드 및 사용
def create_bundle(locale: str, ftl_content: str) -> FluentBundle:
bundle = FluentBundle([locale])
resource = FluentResource(ftl_content)
errors = bundle.add_resource(resource)
if errors:
raise ValueError(f"FTL errors: {errors}")
return bundle
# FTL 파일 내용
en_ftl = """
welcome = Welcome to our app!
greeting = Hello, { $name }!
items =
{ $count ->
[0] No items
[one] { $count } item
*[other] { $count } items
}
"""
bundle = create_bundle("en-US", en_ftl)
def translate(bundle: FluentBundle, message_id: str, **kwargs) -> str:
msg = bundle.get_message(message_id)
if not msg or not msg.value:
return message_id
value, errors = bundle.format_pattern(msg.value, kwargs)
return value
print(translate(bundle, "welcome"))
print(translate(bundle, "greeting", name="Alice"))
print(translate(bundle, "items", count=0))
print(translate(bundle, "items", count=1))
print(translate(bundle, "items", count=5))
CI/CD와의 통합
Python i18n 워크플로우는 지속적인 로컬라이제이션 파이프라인과 자연스럽게 통합됩니다:
# .github/workflows/i18n.yml
name: i18n
on:
push:
paths:
- '**.py'
- '**/templates/**.html'
jobs:
extract-and-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Babel
run: pip install Babel
- name: Extract strings
run: pybabel extract -F babel.cfg -o messages.pot .
- name: Update translation files
run: pybabel update -i messages.pot -d locales
- name: Push to translation platform
run: |
# messages.pot을 TMS에 업로드
curl -X POST https://api.better-i18n.com/upload \
-F "file=@messages.pot" \
-H "Authorization: Bearer ${{ secrets.BETTER_I18N_API_KEY }}"
포괄적인 CI/CD i18n 패턴은 i18n CI/CD 파이프라인 자동화를 참조하세요.
로케일 감지 모범 사례
from babel import Locale, UnknownLocaleError
from typing import Optional
SUPPORTED_LOCALES = ['en', 'fr', 'de', 'ja', 'ar', 'pt-BR']
def parse_accept_language(header: str) -> list[str]:
"""Accept-Language 헤더를 언어 코드의 정렬된 목록으로 파싱합니다."""
locales = []
for part in header.split(','):
parts = part.strip().split(';')
lang = parts[0].strip()
# 품질(q=0.9)을 추출하거나 기본값 1.0 사용
q = 1.0
for param in parts[1:]:
if param.strip().startswith('q='):
try:
q = float(param.strip()[2:])
except ValueError:
pass
locales.append((lang, q))
# 품질로 정렬, 가장 높은 것 먼저
locales.sort(key=lambda x: x[1], reverse=True)
return [lang for lang, _ in locales]
def negotiate_locale(accept_language: str, supported: list[str] = SUPPORTED_LOCALES) -> str:
"""주어진 Accept-Language 헤더에 대한 가장 적합한 지원 로케일을 찾습니다."""
requested = parse_accept_language(accept_language)
for requested_lang in requested:
# 정확한 일치 먼저 확인
if requested_lang in supported:
return requested_lang
# 언어만 일치 (en-US → en)
base = requested_lang.split('-')[0]
if base in supported:
return base
# 같은 베이스를 가진 지원 로케일 찾기
for supported_locale in supported:
if supported_locale.startswith(base + '-'):
return supported_locale
return 'en' # 기본 폴백
여러 언어의 복수형 규칙에 대한 심층적인 내용은 언어별 복수형 규칙을 참조하세요. 번역 관리 에코시스템 개요는 번역 관리 시스템을 참조하세요.
better-i18n으로 앱을 글로벌하게 만드세요
better-i18n은 AI 기반 번역, git 네이티브 워크플로우, 글로벌 CDN 배포를 하나의 개발자 중심 플랫폼으로 통합합니다. 스프레드시트 관리를 그만하고 모든 언어로 출시를 시작하세요.
무료로 시작하기 → · 기능 탐색하기 · 문서 읽기