# Django 다국어 처리: "Polish"가 "폴란드어"가 되는 비극 막기 (Contextual Markers)
다국어(i18n)를 제대로 지원하다 보면 한 번쯤 이런 경험을 합니다.
* 버튼에 **“Polish”**(UI를 다듬다)라고 적어 두었는데 번역 결과가 **“폴란드어”**
* 날짜 선택 컴포넌트의 **“May”**가 어떤 언어에서는 **사람 이름**처럼 번역
* 메뉴의 **“Book”**이 “책”으로만 번역되고, “예약하다”로는 번역이 안 됨
문제의 원인은 간단합니다.
> 컴퓨터는 “문맥(context)”을 모릅니다.
> 문자열이 같으면 전부 같은 것으로 취급합니다.
이 글에서는 Django의 **Contextual Markers**를 활용해서,
소스 코드는 그대로 유지하면서 **동일한 문자열에 서로 다른 번역**을 붙이는 방법을 정리합니다.
---
## 왜 이런 일이 생길까? gettext의 기본 동작 {#sec-aff43a618865}
Django의 번역 시스템은 내부적으로 **GNU gettext**를 사용합니다.
gettext는 아주 단순한 규칙으로 동작합니다.
* 원문 문자열 → `msgid`
* 번역 문자열 → `msgstr`
* **`msgid`가 같으면 무조건 같은 `msgstr`** 을 사용
```po
# django.po
msgid "Polish"
msgstr "폴란드어"
```
위와 같이 `.po` 파일에 한 번 매핑이 생기면,
어디에서 “Polish”를 쓰든 **항상 같은 번역**이 쓰입니다.
그래서 다음 두 UI는 구분이 되지 않습니다.
* “Polish” = UI를 다듬는 동사
* “Polish” = 언어/국가 이름
이걸 피하려고 소스 코드를 이렇게 바꾸는 경우가 있습니다.
```html
{% trans "Polish (verb)" %}
{% trans "Polish (language)" %}
```
이러면 번역은 잘 되더라도,
**사용자에게 보이는 문자열 자체가 이상해지거나**
다른 곳에서 재사용하기 어려워지는 문제가 생깁니다.
우리가 원하는 것은:
* 소스 문자열은 여전히 `"Polish"` 그대로 두고
* **“지금은 어떤 의미로 쓰인 것인지”를 따로 설명**하는 것
바로 이때 쓰는 것이 **Contextual Marker(문맥 정보)**입니다.
---
## 템플릿에서 해결하기: `{% translate %}` + `context` {#sec-e28151952ba2}
Django 템플릿에서는 `{% translate %}` (또는 예전 스타일 `{% trans %}`) 태그에
`context` 옵션을 붙여서 같은 문자열을 문맥별로 구분할 수 있습니다.
### 1) 기존 코드 (충돌 발생) {#sec-40afc992a96b}
```html
{% load i18n %}
{% translate "Polish" %}
```
`.po` 파일 입장에서는 둘 다 `msgid "Polish"`이므로
**둘 중 하나의 의미로만 번역**할 수 있습니다.
### 2) 개선된 코드 (context 사용) {#sec-4fa05f04b9d3}
```html
{% load i18n %}
{% translate "Polish" context "language name" %}
```
여기서 중요한 점:
* `context` 뒤의 문자열은 **사용자에게 보이지 않습니다.**
* 오직 번역 시스템과 번역가를 위한 **메타 정보**입니다.
* 의미가 분명하게 드러나도록 짧게 설명하는 것이 좋습니다.
* `"verb: to refine UI"`
* `"language name"`
* `"menu label"`
* `"button text"` 등
### 3) `{% blocktranslate %}`에서도 사용 가능 {#sec-c2a9f27dea01}
문장이 길어서 `{% blocktranslate %}` (또는 `{% blocktrans %}`)를 쓸 때도 `context`를 붙일 수 있습니다.
```html
{% load i18n %}
{% blocktranslate context "greeting message" with username=user.username %}
Hello {{ username }}
{% endblocktranslate %}
```
이렇게 하면 같은 `"Hello %(username)s"` 스타일의 문장도
다른 문맥으로 여러 번 사용할 수 있습니다.
---
## 파이썬 코드에서 해결하기: `pgettext` {#sec-3a9a008d3f99}
템플릿이 아니라 **views, models, forms** 같은 파이썬 코드 안에서는
`gettext` 대신 **`pgettext`** 계열 함수를 사용합니다.
대표적으로 다음 3가지가 있습니다.
* `pgettext(context, message)`
* `pgettext_lazy(context, message)` – 지연 평가용 (모델 필드, 모듈 레벨 등)
* `npgettext(context, singular, plural, number)` – 복수형 + 문맥 동시 처리
### 1) 기본 예제 {#sec-27f89a09cea2}
```python
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` 사용하기 {#sec-11832a96275e}
모델 필드의 `verbose_name`이나 `help_text`,
혹은 `choices`에 들어가는 라벨도 종종 동음이의어 문제가 발생합니다.
```python
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` {#sec-ed84f3cf8d15}
동일한 단어가 복수형일 때 의미가 달라지는 경우에는
`npgettext`를 사용해 문맥 + 복수형을 동시에 처리할 수 있습니다.
```python
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` 파일에서는 어떻게 보일까? {#sec-fb610023c81a}
위와 같이 `context`를 사용한 뒤:
```bash
python manage.py makemessages -l ko
```
같은 명령으로 메시지를 추출하면,
`.po` 파일에는 **`msgctxt`**라는 필드가 추가됩니다.
```po
# 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 작성 팁 {#sec-df37b35ab408}
문맥 문자열은 사용자에게 보이지 않지만,
**번역가에게는 거의 유일한 힌트**입니다. 다음 기준을 추천합니다.
### 1) “역할”을 설명하듯이 쓰기 {#sec-9dfe29490c86}
* `"button label"` – 버튼에 쓰이는 텍스트
* `"menu item"` – 네비게이션 메뉴
* `"tooltip"` – 마우스 오버 텍스트
* `"error message"` – 에러 메시지
* `"form field label"` – 폼 라벨
문자열의 **UI 역할**을 설명하면,
거의 모든 언어에서 자동 번역을 거쳐도 의미가 유지됩니다.
### 2) “어떤 개념인지”를 한 번 더 적기 {#sec-135dce4b280b}
동일한 단어가 여러 개념에 쓰일 때:
* `"File"`
* `"file menu item"`
* `"uploaded file object"`
* `"Order"`
* `"e-commerce order"`
* `"sorting order"`
이렇게 **도메인 개념**을 함께 적어 주면,
다국어 번역에서도 의미가 훨씬 안정적으로 유지됩니다.
### 3) 번역 문자열을 context에 넣지 않기 {#sec-9197b1a38f27}
`context`는 설명 영역입니다.
여기에 번역 결과(다른 언어)를 직접 써 넣으면:
* 언어가 추가될수록 관리가 매우 어려워지고
* 자동 번역 시스템이 혼란을 겪을 수 있습니다.
**항상 원문(보통 영어) 기준으로, 개념/역할을 짧게 설명**하는 것이 좋습니다.
---
## 언제 Contextual Markers를 써야 할까? {#sec-58e84d2a2b4c}
다음 중 하나에 해당하면 거의 무조건 `context`/`pgettext`를 고려해 볼 만합니다.
1. **길이가 짧은 문자열 (1–2단어)**
* 버튼 텍스트
* 탭 이름
* 메뉴 항목
2. **실제 UI에서 서로 다른 의미로 재사용되는 문자열**
* `"Open"`, `"Close"`, `"Save"`, `"Apply"`, `"Reset"` 같은 공통 액션
* `"Book"`, `"Order"`, `"Post"`처럼 명사/동사로 모두 쓰이는 단어
3. **번역가가 화면을 보지 못하고 번역하는 경우가 많을 때**
* 협업 번역 플랫폼 사용 시
* 외부 번역 업체에 `.po` 파일만 전달하는 경우
---
## 정리 {#sec-8cc5759d0812}

* Django는 기본적으로 같은 `msgid`에 하나의 번역만 매핑합니다.
* 그래서 `"Polish"`, `"May"`, `"Book"`처럼 **동음이의어**가 번역에서 자주 꼬입니다.
* 이때 **소스 문자열을 억지로 바꾸지 말고**:
* 템플릿에서는 `{% translate "…" context "…" %}`
* 파이썬 코드에서는 `pgettext` / `pgettext_lazy` / `npgettext`
를 사용해 **문맥(context)**을 추가하세요.
* 그러면 `.po` 파일에 `msgctxt`가 추가되어
* 같은 원문이라도 서로 다른 번역
* 번역 도구에서도 별도 항목으로 관리
가 가능해집니다.
결과적으로:
* 코드 가독성은 유지하고
* 번역 품질과 유지보수성은 크게 올릴 수 있습니다.
다국어 지원이 점점 필수가 되는 시대에,
Contextual Markers는 Django 개발자가 꼭 한 번 숙지해 둘 만한 i18n 도구입니다.
---
**함께보면 좋은 글**
- [Django에서 gettext vs gettext_lazy 헷갈림 끝내기: 평가 시점으로 이해하기](/ko/whitedec/2026/1/5/django-gettext-vs-gettext-lazy/)
- [gettext_lazy를 JSON 키에 쓰면 생기는 문제와 해결 방법](/ko/whitedec/2025/4/26/gettext-lazy-json-key-problem-solution/)