SEO//14 최소 읽기 시간

Python i18n: gettext부터 모던 번역 워크플로우까지

Eray Gündoğmuş
공유

Python i18n: gettext부터 모던 번역 워크플로우까지

Python은 모든 프로그래밍 언어 중에서 가장 풍부한 i18n 에코시스템을 보유하고 있습니다. 이는 수십 년에 걸친 대규모 다국어 애플리케이션의 투자——특히 전 세계에 배포된 Django 웹 프레임워크와 국제 커뮤니티를 지원해야 하는 과학 계산 및 데이터 도구에서의 Python 광범위한 활용——덕분입니다.

이 가이드는 Python i18n의 전체 영역을 다룹니다. 표준 라이브러리에 내장된 클래식한 gettext 방식부터 Babel의 포괄적인 로케일 데이터 라이브러리, Django와 Flask를 위한 프레임워크별 솔루션, 그리고 Fluent를 활용한 모던한 접근 방식까지 설명합니다.

Python i18n 옵션 한눈에 보기

라이브러리 / 방식최적 용도
gettext (stdlib)간단한 스크립트와 애플리케이션
Babel숫자, 날짜, 통화 포맷팅; PO 파일 관리
Django i18nDjango 웹 애플리케이션
Flask-BabelFlask 웹 애플리케이션
fluent.runtimeMozilla Fluent 포맷을 사용하는 애플리케이션
Babel + gettext대부분의 프로덕션 Python 웹 서비스

gettext: Python의 내장 i18n

Python 표준 라이브러리에는 GNU gettext 국제화 API를 구현하는 gettext가 포함되어 있습니다. 이것은 많은 상위 라이브러리들이 구축되는 기반입니다.

gettext 동작 원리

gettext는 번역 저장을 위해 .po(Portable Object) 파일을 사용하고, 런타임 로딩을 위해 바이너리 .mo(Machine Object) 파일로 컴파일합니다.

워크플로우:

  1. 소스 코드의 문자열을 _()또는 gettext()로 표시합니다
  2. xgettext 또는 pybabel로 표시된 문자열을 .pot 템플릿으로 추출합니다
  3. 템플릿에서 로케일별 .po 파일을 생성합니다
  4. 번역가가 번역을 채워 넣습니다
  5. .po 파일을 .mo 파일로 컴파일합니다
  6. 사용자 로케일에 따라 런타임에 로드합니다

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 배포를 하나의 개발자 중심 플랫폼으로 통합합니다. 스프레드시트 관리를 그만하고 모든 언어로 출시를 시작하세요.

무료로 시작하기 → · 기능 탐색하기 · 문서 읽기

Comments

Loading comments...