Django 다국어 처리: "Polish"가 "폴란드어"가 되는 비극 막기 (Contextual Markers)

다국어(i18n)를 제대로 지원하다 보면 한 번쯤 이런 경험을 합니다.

  • 버튼에 “Polish”(UI를 다듬다)라고 적어 두었는데 번역 결과가 “폴란드어”
  • 날짜 선택 컴포넌트의 “May”가 어떤 언어에서는 사람 이름처럼 번역
  • 메뉴의 “Book”이 “책”으로만 번역되고, “예약하다”로는 번역이 안 됨

문제의 원인은 간단합니다.

컴퓨터는 “문맥(context)”을 모릅니다. 문자열이 같으면 전부 같은 것으로 취급합니다.

이 글에서는 Django의 Contextual Markers를 활용해서, 소스 코드는 그대로 유지하면서 동일한 문자열에 서로 다른 번역을 붙이는 방법을 정리합니다.


왜 이런 일이 생길까? gettext의 기본 동작



Django의 번역 시스템은 내부적으로 GNU gettext를 사용합니다. gettext는 아주 단순한 규칙으로 동작합니다.

  • 원문 문자열 → msgid
  • 번역 문자열 → msgstr
  • msgid가 같으면 무조건 같은 msgstr 을 사용
# django.po

msgid "Polish"
msgstr "폴란드어"

위와 같이 .po 파일에 한 번 매핑이 생기면, 어디에서 “Polish”를 쓰든 항상 같은 번역이 쓰입니다.

그래서 다음 두 UI는 구분이 되지 않습니다.

  • “Polish” = UI를 다듬는 동사
  • “Polish” = 언어/국가 이름

이걸 피하려고 소스 코드를 이렇게 바꾸는 경우가 있습니다.

<!-- 지양할 코드 -->
{% trans "Polish (verb)" %}
{% trans "Polish (language)" %}

이러면 번역은 잘 되더라도, 사용자에게 보이는 문자열 자체가 이상해지거나 다른 곳에서 재사용하기 어려워지는 문제가 생깁니다.

우리가 원하는 것은:

  • 소스 문자열은 여전히 "Polish" 그대로 두고
  • “지금은 어떤 의미로 쓰인 것인지”를 따로 설명하는 것

바로 이때 쓰는 것이 Contextual Marker(문맥 정보)입니다.


템플릿에서 해결하기: {% translate %} + context

Django 템플릿에서는 {% translate %} (또는 예전 스타일 {% trans %}) 태그에 context 옵션을 붙여서 같은 문자열을 문맥별로 구분할 수 있습니다.

1) 기존 코드 (충돌 발생)

{% load i18n %}

<button>{% translate "Polish" %}</button>
<span>{% translate "Polish" %}</span>

.po 파일 입장에서는 둘 다 msgid "Polish"이므로 둘 중 하나의 의미로만 번역할 수 있습니다.

2) 개선된 코드 (context 사용)

{% load i18n %}

<button>
  {% translate "Polish" context "verb: to refine UI" %}
</button>

<span>
  {% translate "Polish" context "language name" %}
</span>

여기서 중요한 점:

  • context 뒤의 문자열은 사용자에게 보이지 않습니다.
  • 오직 번역 시스템과 번역가를 위한 메타 정보입니다.
  • 의미가 분명하게 드러나도록 짧게 설명하는 것이 좋습니다.

  • "verb: to refine UI"

  • "language name"
  • "menu label"
  • "button text"

3) {% blocktranslate %}에서도 사용 가능

문장이 길어서 {% blocktranslate %} (또는 {% blocktrans %})를 쓸 때도 context를 붙일 수 있습니다.

{% load i18n %}

{% blocktranslate context "greeting message" with username=user.username %}
  Hello {{ username }}
{% endblocktranslate %}

이렇게 하면 같은 "Hello %(username)s" 스타일의 문장도 다른 문맥으로 여러 번 사용할 수 있습니다.


파이썬 코드에서 해결하기: pgettext



템플릿이 아니라 views, models, forms 같은 파이썬 코드 안에서는 gettext 대신 pgettext 계열 함수를 사용합니다.

대표적으로 다음 3가지가 있습니다.

  • pgettext(context, message)
  • pgettext_lazy(context, message) – 지연 평가용 (모델 필드, 모듈 레벨 등)
  • npgettext(context, singular, plural, number) – 복수형 + 문맥 동시 처리

1) 기본 예제

from django.utils.translation import pgettext

def my_view(request):
    # 1. 달(month) 이름 "May"
    month = pgettext("month name", "May")

    # 2. 사람 이름 "May"
    person = pgettext("person name", "May")

    # 3. 조동사 "may" (~일지도 모른다)
    verb = pgettext("auxiliary verb", "may")

이렇게 하면 세 경우 모두 msgid"May"/"may"이지만, 서로 다른 context 덕분에 각기 다른 번역을 가질 수 있습니다.

2) 모델에서 pgettext_lazy 사용하기

모델 필드의 verbose_name이나 help_text, 혹은 choices에 들어가는 라벨도 종종 동음이의어 문제가 발생합니다.

from django.db import models
from django.utils.translation import pgettext_lazy

class Order(models.Model):
    # "Order" = 주문
    type = models.CharField(
        verbose_name=pgettext_lazy("order model field", "Order type"),
        max_length=20,
    )

    STATUS_CHOICES = [
        # "Open" = 상태
        ("open",  pgettext_lazy("order status", "Open")),
        # "Open" = 동작(열다)
        ("opened", pgettext_lazy("log action", "Open")),
    ]

    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
    )

위 예시처럼:

  • 같은 "Open"이라도
  • “상태(status)”“행동(action)”을 명확히 분리해 둘 수 있습니다.

3) 복수형까지 함께 처리: npgettext

동일한 단어가 복수형일 때 의미가 달라지는 경우에는 npgettext를 사용해 문맥 + 복수형을 동시에 처리할 수 있습니다.

from django.utils.translation import npgettext

def get_notification(count):
    # "Message"라는 단어를 알림 용도로 사용한다고 가정
    return npgettext(
        "user notification",        # context
        "You have %(count)d message",   # singular
        "You have %(count)d messages",  # plural
        count
    ) % {"count": count}

.po 파일에서는 어떻게 보일까?

위와 같이 context를 사용한 뒤:

python manage.py makemessages -l ko

같은 명령으로 메시지를 추출하면, .po 파일에는 msgctxt라는 필드가 추가됩니다.

# django.po

# 1) "Polish" = UI를 다듬다 (동사)
msgctxt "verb: to refine UI"
msgid "Polish"
msgstr "다듬기"

# 2) "Polish" = 언어/국가 이름
msgctxt "language name"
msgid "Polish"
msgstr "폴란드어"

msgid는 동일하지만:

  • msgctxt가 다르기 때문에
  • 번역 도구(Poedit, Weblate, Crowdin 등)에서도 서로 다른 항목으로 보입니다.
  • 번역가는 실제 UI에서 어떻게 쓰일지 쉽게 이해할 수 있습니다.

중요한 점은:

  • 소스 코드의 문자열은 전혀 바뀌지 않았고 ("Polish" 그대로)
  • 오직 .po 파일 구조만 더 똑똑해졌다는 것입니다.

좋은 Context 작성 팁

문맥 문자열은 사용자에게 보이지 않지만, 번역가에게는 거의 유일한 힌트입니다. 다음 기준을 추천합니다.

1) “역할”을 설명하듯이 쓰기

  • "button label" – 버튼에 쓰이는 텍스트
  • "menu item" – 네비게이션 메뉴
  • "tooltip" – 마우스 오버 텍스트
  • "error message" – 에러 메시지
  • "form field label" – 폼 라벨

문자열의 UI 역할을 설명하면, 거의 모든 언어에서 자동 번역을 거쳐도 의미가 유지됩니다.

2) “어떤 개념인지”를 한 번 더 적기

동일한 단어가 여러 개념에 쓰일 때:

  • "File"

  • "file menu item"

  • "uploaded file object"

  • "Order"

  • "e-commerce order"

  • "sorting order"

이렇게 도메인 개념을 함께 적어 주면, 다국어 번역에서도 의미가 훨씬 안정적으로 유지됩니다.

3) 번역 문자열을 context에 넣지 않기

context는 설명 영역입니다. 여기에 번역 결과(다른 언어)를 직접 써 넣으면:

  • 언어가 추가될수록 관리가 매우 어려워지고
  • 자동 번역 시스템이 혼란을 겪을 수 있습니다.

항상 원문(보통 영어) 기준으로, 개념/역할을 짧게 설명하는 것이 좋습니다.


언제 Contextual Markers를 써야 할까?

다음 중 하나에 해당하면 거의 무조건 context/pgettext를 고려해 볼 만합니다.

  1. 길이가 짧은 문자열 (1–2단어)
  • 버튼 텍스트
  • 탭 이름
  • 메뉴 항목
  1. 실제 UI에서 서로 다른 의미로 재사용되는 문자열
  • "Open", "Close", "Save", "Apply", "Reset" 같은 공통 액션
  • "Book", "Order", "Post"처럼 명사/동사로 모두 쓰이는 단어
  1. 번역가가 화면을 보지 못하고 번역하는 경우가 많을 때
  • 협업 번역 플랫폼 사용 시
  • 외부 번역 업체에 .po 파일만 전달하는 경우

정리

동음이의어 번역에 당혹해하는 개발자의 모습

  • Django는 기본적으로 같은 msgid에 하나의 번역만 매핑합니다.
  • 그래서 "Polish", "May", "Book"처럼 동음이의어가 번역에서 자주 꼬입니다.
  • 이때 소스 문자열을 억지로 바꾸지 말고:

  • 템플릿에서는 {% translate "…" context "…" %}

  • 파이썬 코드에서는 pgettext / pgettext_lazy / npgettext 를 사용해 문맥(context)을 추가하세요.
  • 그러면 .po 파일에 msgctxt가 추가되어

  • 같은 원문이라도 서로 다른 번역

  • 번역 도구에서도 별도 항목으로 관리 가 가능해집니다.

결과적으로:

  • 코드 가독성은 유지하고
  • 번역 품질과 유지보수성은 크게 올릴 수 있습니다.

다국어 지원이 점점 필수가 되는 시대에, Contextual Markers는 Django 개발자가 꼭 한 번 숙지해 둘 만한 i18n 도구입니다.


함께보면 좋은 글