# 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} ![동음이의어 번역에 당혹해하는 개발자의 모습](/media/editor_temp/6/14112b82-d049-4b32-9275-02429e9305c0.png) * 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/)