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

- Django는 기본적으로 같은
msgid에 하나의 번역만 매핑합니다. - 그래서
"Polish","May","Book"처럼 동음이의어가 번역에서 자주 꼬입니다. -
이때 소스 문자열을 억지로 바꾸지 말고:
-
템플릿에서는
{% translate "…" context "…" %} - 파이썬 코드에서는
pgettext/pgettext_lazy/npgettext를 사용해 문맥(context)을 추가하세요. -
그러면
.po파일에msgctxt가 추가되어 -
같은 원문이라도 서로 다른 번역
- 번역 도구에서도 별도 항목으로 관리 가 가능해집니다.
결과적으로:
- 코드 가독성은 유지하고
- 번역 품질과 유지보수성은 크게 올릴 수 있습니다.
다국어 지원이 점점 필수가 되는 시대에, Contextual Markers는 Django 개발자가 꼭 한 번 숙지해 둘 만한 i18n 도구입니다.
함께보면 좋은 글
댓글이 없습니다.