본문으로 건너뛰기
김신건의 로그

[Django] i18n: 다국어와 시간대

· 수정 · 📖 약 1분 · 509자/단어 #django #i18n #localization #translation
django i18n, gettext, ugettext, trans tag, LocaleMiddleware, django translations

정의

Django는 gettext 기반 국제화(i18n)와 지역화(l10n) 표준 시스템을 제공한다. 코드/템플릿의 문자열을 추출해 .po 파일로 번역, 컴파일된 .mo 파일을 런타임에 사용. 시간대 인식(timezone-aware datetime)도 핵심.

활성화

# settings.py
USE_I18N = True
USE_L10N = True       # 4.0+ 항상 True로 deprecated
USE_TZ = True         # 시간대 인식 datetime
LANGUAGE_CODE = "ko-kr"
TIME_ZONE = "Asia/Seoul"

LANGUAGES = [
    ("ko", "한국어"),
    ("en", "English"),
    ("ja", "日本語"),
]

LOCALE_PATHS = [BASE_DIR / "locale"]

MIDDLEWARE = [
    ...,
    "django.middleware.locale.LocaleMiddleware",   # SessionMiddleware 다음, CommonMiddleware 전
    ...,
]

LocaleMiddleware가 사용자의 언어를 자동 감지(쿠키, Accept-Language 헤더).

문자열 마킹

Python 코드

from django.utils.translation import gettext as _, gettext_lazy as _l

def hello(request):
    msg = _("Hello, world")
    return HttpResponse(msg)

# 모델 등 import 시 평가는 안 됨 → lazy
class Profile(models.Model):
    name = models.CharField(_l("Name"), max_length=100)

    class Meta:
        verbose_name = _l("Profile")
        verbose_name_plural = _l("Profiles")

gettext (즉시) vs gettext_lazy (지연). 모델 필드, 클래스 변수, fn 인수 기본값엔 lazy 필요.

보간

_("Welcome, %(name)s") % {"name": user.name}
_("You have %(count)d messages") % {"count": n}

직접 f"Welcome, {name}"을 _()로 감싸면 추출 단계에서 동적 부분이 포함됨 → 안 됨.

복수형

from django.utils.translation import ngettext

ngettext("%(count)d item", "%(count)d items", count) % {"count": count}

언어별 복수형 규칙이 다르므로 ngettext.

템플릿

{% load i18n %}

<h1>{% translate "Welcome" %}</h1>
{# 또는 짧은 별칭 #}
<h1>{% trans "Welcome" %}</h1>

{# blocktrans (긴 문자열 + 보간) #}
{% blocktranslate with name=user.name %}
    Hello, {{ name }}!
{% endblocktranslate %}

{# 복수형 #}
{% blocktranslate count counter=count %}
    {{ counter }} item
{% plural %}
    {{ counter }} items
{% endblocktranslate %}

{# 변수 자체 #}
{% get_current_language as LANGUAGE_CODE %}
<p>현재 언어: {{ LANGUAGE_CODE }}</p>

메시지 파일 생성

# 코드에서 _()/trans 찾아 .po 생성
python manage.py makemessages -l ko -l en

# 결과: locale/ko/LC_MESSAGES/django.po

django.po 예시:

msgid "Hello, world"
msgstr "안녕, 세상"

msgid "Welcome, %(name)s"
msgstr "%(name)s 님 환영합니다"

컴파일

python manage.py compilemessages
# .po → .mo (런타임용 바이너리)

배포 시 잊지 말 것 (Docker entrypoint 등).

언어 전환

URL prefix

from django.conf.urls.i18n import i18n_patterns

urlpatterns = [
    path("admin/", admin.site.urls),
]

urlpatterns += i18n_patterns(
    path("", include("blog.urls")),
    prefix_default_language=False,    # 기본 언어는 prefix 없이
)

/ (한국어), /en/ (영어), /ja/ (일본어).

사용자 선택

# urls.py
path("i18n/", include("django.conf.urls.i18n")),

# 템플릿: 폼으로 set_language view에 POST
<form action="{% url 'set_language' %}" method="post">
    {% csrf_token %}
    <input name="next" value="{{ redirect_to }}" type="hidden">
    <select name="language">
        {% get_available_languages as LANGUAGES %}
        {% for lang in LANGUAGES %}
            <option value="{{ lang.0 }}">{{ lang.1 }}</option>
        {% endfor %}
    </select>
    <button>변경</button>
</form>

쿠키 django_language에 저장.

시간대 (timezone)

USE_TZ = True이면 DB는 모두 UTC로 저장. 표시 시 사용자 시간대로 변환.

from django.utils import timezone

now = timezone.now()              # tz-aware UTC datetime
local_now = timezone.localtime()  # 현재 TZ

템플릿:

{% load tz %}

{# 활성 TZ로 표시 #}
{{ event.start_at }}

{# 특정 TZ로 #}
{% timezone "America/New_York" %}
    {{ event.start_at }}
{% endtimezone %}

{# 명시적으로 UTC #}
{% localtime off %}
    {{ event.start_at }}
{% endlocaltime %}

사용자별 TZ 저장 + 미들웨어로 활성화:

class TimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.user.is_authenticated:
            tz = request.user.profile.timezone
            if tz:
                timezone.activate(tz)
        else:
            timezone.deactivate()
        return self.get_response(request)

숫자/날짜 형식

USE_L10N = True    # 사용자 로케일에 맞춤 (deprecated, 항상 True)
USE_THOUSAND_SEPARATOR = True
{{ value|date:"Y-m-d" }}                    # 명시 포맷
{{ value|date }}                            # 로케일 기본
{{ value|date:"SHORT_DATETIME_FORMAT" }}    # 명명 포맷
{{ amount|floatformat:2 }}                  # 1234.57
{{ amount|intcomma }}                       # 1,234,567 (humanize)

humanize 활용:

INSTALLED_APPS += ["django.contrib.humanize"]
{% load humanize %}
{{ value|intcomma }}          # 1,234,567
{{ value|naturaltime }}        # 3 hours ago
{{ value|apnumber }}           # one, two, ..., nine, 10
{{ value|naturalday }}         # today, yesterday, ...

자주 보는 패턴

컨텍스트별 다른 번역

from django.utils.translation import pgettext, pgettext_lazy

pgettext("month", "May")    # 5월
pgettext("name", "May")     # 인명 May

같은 영문이 컨텍스트별 다르게 번역됨.

Markdown / Rich text 번역

from django.utils.safestring import mark_safe
content = mark_safe(gettext("**bold** text"))

또는 별도 CMS 모델로 다국어 콘텐츠.

모델 필드 다국어

별도 라이브러리: django-modeltranslation, django-parler.

# django-parler
class Post(TranslatableModel):
    translations = TranslatedFields(
        title=models.CharField(max_length=200),
        body=models.TextField(),
    )

각 언어별로 별도 row.

함정

1. lazy를 즉시 평가하는 곳

class Profile(models.Model):
    role = models.CharField(_("Role"), max_length=10)    # 정상

choices = [("a", _("A"))]    # OK
choices = [("a", _("A").upper())]    # 추출 단계에 깨질 수 있음

문자열 메서드 호출은 마킹 후가 좋음.

2. f-string으로 마킹

_(f"Hello, {name}")    # 매번 다른 문자열로 보임 → 추출 안 됨
_("Hello, %(name)s") % {"name": name}

3. makemessages 누락

새 _() 추가했는데 makemessages 안 돌리면 번역 빈 채로 영어 표시 (msgstr 없으면 msgid 그대로).

4. compilemessages 누락

.po 번역했는데 .mo 컴파일 안 함 → 런타임에 적용 안 됨. CI/배포 단계에 포함.

5. JSON content with HTML

{"label": _("<b>Important</b>")}

번역가가 HTML 깨뜨릴 위험. 마크업 분리:

{"label": _("Important")}    # "<b>Important</b>"는 템플릿에서

대안: django-rosetta, django-modeltranslation

  • django-rosetta: 웹에서 .po 편집
  • django-modeltranslation: 모델 필드 자동 다국어
  • wagtail-localize: Wagtail CMS와 통합

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기