블로그로 돌아가기

Django 번역이 깨지는 이유와 가장 흔한 10가지 원인 해결법

2026-02-04 8분 읽기
Django 번역이 깨지는 이유와 가장 흔한 10가지 원인 해결법

번역을 위해 문자열을 표시하고, .po 파일을 생성하고, compilemessages를 실행했는데 앱이 여전히 영어를 표시합니다. 당신만 그런 것이 아닙니다. Django의 i18n 프레임워크는 강력하지만, 숙련된 개발자도 걸려드는 날카로운 함정이 있습니다.

이 가이드는 Django 번역이 조용히 실패하는 가장 일반적인 10가지 원인을 각각의 정확한 증상과 수정 방법과 함께 다룹니다.

1. .po 파일 편집 후 compilemessages 실행을 잊음

.po 파일을 편집했지만(수동 또는 도구로) 번역된 텍스트가 나타나지 않습니다. 앱은 원래 영어 문자열을 계속 표시합니다.

Django는 런타임에 .po 파일을 읽지 않습니다. 대신 컴파일된 .mo(머신 오브젝트) 바이너리 파일을 읽습니다. .po 파일을 재컴파일하지 않고 편집하면 Django는 변경된 것을 알지 못합니다.

.po 파일을 변경할 때마다 compilemessages를 실행하세요:

python manage.py compilemessages

TranslateBot으로 번역을 자동화하는 경우 워크플로의 마지막 단계로 compilemessages를 추가하세요:

python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages

2. 템플릿에서 {% load i18n %} 누락

템플릿에서 {% trans "Hello" %}를 사용하지만 Django가 TemplateSyntaxError를 발생시킵니다. 또는 더 나쁘게, 템플릿 엔진이 잘못 구성되어 있으면 태그가 조용히 아무것도 하지 않습니다.

{% trans %}{% blocktrans %} 태그는 Django의 i18n 템플릿 태그 라이브러리에 있습니다. 이를 로드하지 않으면 템플릿 엔진이 인식하지 못합니다.

번역 태그를 사용하는 모든 템플릿의 상단에 {% load i18n %}를 추가하세요:

{% load i18n %}

<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>

이것은 템플릿별 요구 사항입니다. 부모 템플릿이 i18n을 로드하더라도 번역 태그를 사용하는 자식 템플릿에는 자체 {% load i18n %} 선언이 필요합니다.

3. LocaleMiddleware가 MIDDLEWARE에 없거나 잘못된 위치에 있음

브라우저의 Accept-Language 헤더, URL 접두사 또는 세션 설정에 관계없이 Django가 항상 기본 언어로 콘텐츠를 제공합니다.

LocaleMiddleware는 각 요청의 활성 언어를 결정합니다. 이것이 없으면 Django는 LANGUAGE_CODE를 기본값으로 사용하고 모든 언어 선택 메커니즘을 무시합니다. 미들웨어 스택에서의 위치도 중요합니다. 세션 데이터와 URL 해석에 접근해야 하기 때문입니다.

MIDDLEWARE 설정에 LocaleMiddleware를 추가하세요. SessionMiddlewareCommonMiddleware 뒤에 배치합니다:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.locale.LocaleMiddleware",  # Must be after SessionMiddleware
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
]

URL 기반 언어 전환을 사용하는 경우 URL 구성에 django.conf.urls.i18n이 포함되어 있는지도 확인하세요:

from django.conf.urls.i18n import i18n_patterns

urlpatterns = i18n_patterns(
    path("", include("myapp.urls")),
)

4. 언어 코드 불일치 (예: pt-br vs pt_BR)

번역이 .po 파일에 존재하고 compilemessages가 성공하지만 Django가 특정 로케일의 번역을 무시합니다.

Django는 로케일 디렉토리가 <language>_<COUNTRY> 형식(밑줄 구분자와 대문자 국가 코드)을 따르도록 기대합니다. 예를 들어, 브라질 포르투갈어는 pt_BR입니다. 디렉토리 이름이 pt-br, pt-BR 또는 ptBR이면 Django가 찾지 못합니다. LANGUAGES 설정에도 동일하게 적용됩니다: 거기의 코드는 하이픈(pt-br)을 사용하지만 파일 시스템은 밑줄(pt_BR)을 사용합니다.

디렉토리 구조가 Django의 기대와 일치하는지 확인하세요:

locale/
    pt_BR/
        LC_MESSAGES/
            django.po
            django.mo

설정에서는 하이픈 형식을 사용하세요:

LANGUAGES = [
    ("en", "English"),
    ("pt-br", "Brazilian Portuguese"),
    ("zh-hans", "Simplified Chinese"),
]

makemessages를 실행할 때 로케일 플래그에 밑줄 형식을 사용하세요:

python manage.py makemessages -l pt_BR

5. 컴파일 중 Fuzzy 항목이 조용히 건너뛰어짐

번역이 .po 파일에 존재하지만 Django가 해당 특정 항목에 대해 런타임에 원래 영어 문자열을 표시합니다. 번역이 파일에 있는데도 그렇기 때문에 특히 답답합니다.

Django의 makemessages가 소스 문자열이 약간 변경되었음을 감지하면 기존 번역을 "fuzzy"(사람의 검토가 필요한 추측이라는 의미)로 표시합니다. compilemessages 명령은 모든 fuzzy 항목을 건너뛰고 미번역으로 처리합니다. 따라서 항목은 .po 파일에서 번역된 것처럼 보이지만 .mo 파일은 완전히 제외합니다.

fuzzy 항목은 다음과 같습니다:

#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"

번역을 검토하고 필요하면 msgstr를 업데이트한 다음 #, fuzzy 플래그를 제거하세요:

msgid "Welcome to our website!"
msgstr "Welkom op onze website!"

그런 다음 재컴파일하세요:

python manage.py compilemessages

큰 프로젝트에서는 fuzzy 항목이 쌓이고 놓치기 쉽습니다. TranslateBot의 check_translations 명령이 자동으로 잡아냅니다:

python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed

CI 파이프라인에 check_translations --makemessages로 추가하면 다시는 fuzzy 항목을 배포하지 않게 됩니다.

6. LOCALE_PATHS가 구성되지 않았거나 잘못된 디렉토리를 가리킴

makemessages가 한 위치에 .po 파일을 생성하지만 Django는 다른 곳에서 찾습니다. 번역이 디스크에 존재하지만 로드되지 않습니다.

Django는 특정 순서로 번역 파일을 검색합니다: 먼저 LOCALE_PATHS 디렉토리, 그다음 각 앱의 locale/ 디렉토리, 마지막으로 프로젝트의 locale/ 디렉토리. LOCALE_PATHS가 설정되지 않았거나 잘못된 경로를 가리키면 Django가 .po 파일을 찾지 못할 수 있습니다.

설정에서 LOCALE_PATHS를 절대 경로로 설정하세요:

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

LOCALE_PATHS = [
    BASE_DIR / "locale",
]

디렉토리가 존재하고 예상 구조를 포함하는지 확인하세요:

locale/
    de/
        LC_MESSAGES/
            django.po
            django.mo
    nl/
        LC_MESSAGES/
            django.po
            django.mo

흔한 실수는 LOCALE_PATHS를 절대 경로 대신 locale/(상대 경로)로 설정하는 것입니다. Django는 프로젝트 루트에서 상대 경로를 해석하지 않습니다. 프로세스의 작업 디렉토리에 의존하며, 이것은 기대와 다른 경우가 많습니다.

7. 캐시가 오래된 번역을 제공

번역을 업데이트하고 컴파일했지만 이전 텍스트가 계속 나타납니다. 서버를 재시작하면 해결됩니다.

Django의 번역 카탈로그는 프로세스당 한 번 로드됩니다. 프로덕션에서 Gunicorn이나 Uvicorn 같은 WSGI/ASGI 서버는 워커 프로세스를 오랜 기간 유지합니다. .mo 파일이 디스크에서 변경되었을 수 있지만 실행 중인 프로세스는 여전히 메모리에 이전 번역을 가지고 있습니다. 또한 Django의 캐시 프레임워크나 Nginx, Cloudflare 같은 리버스 프록시를 사용하면 캐시된 응답이 만료될 때까지 이전 콘텐츠를 제공합니다.

새 번역을 배포한 후 애플리케이션 서버를 재시작하세요:

# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)

# Systemd
sudo systemctl restart myapp

# Docker
docker compose restart web

Django의 캐시 프레임워크의 경우 번역 업데이트 후 캐시를 지우세요:

from django.core.cache import cache
cache.clear()

개발 환경에서 runserver는 Python 파일이 변경되면 자동으로 리로드하지만 .mo 파일은 감시하지 않습니다. compilemessages를 실행한 후 수동으로 재시작해야 합니다.

8. 문자열이 gettext(_() 또는 {% trans %})로 감싸지지 않음

makemessages가 특정 문자열을 추출하지 않아 .po 파일에 나타나지 않고 번역되지 않습니다. 이것은 가장 기본적인 문제이면서 큰 코드베이스에서 가장 간과하기 쉬운 문제입니다.

Django의 makemessages 명령은 xgettext를 사용하여 소스 코드에서 번역 마커를 스캔합니다. 문자열이 gettext()(일반적으로 _()로 별칭), gettext_lazy(), {% trans %} 또는 {% blocktrans %}로 감싸지지 않으면 추출 프로세스에 보이지 않습니다.

모든 사용자 대면 문자열을 감싸세요:

# Python code
from django.utils.translation import gettext_lazy as _

class Article(models.Model):
    class Meta:
        verbose_name = _("article")
        verbose_name_plural = _("articles")

# Views
from django.utils.translation import gettext as _

def my_view(request):
    message = _("Your changes have been saved.")
    return HttpResponse(message)
<!-- Templates -->
{% load i18n %}
<h1>{% trans "Welcome" %}</h1>
<p>{% blocktrans with count=items|length %}You have {{ count }} items.{% endblocktrans %}</p>

TranslateBot의 --dry-run 플래그를 사용하여 현재 미번역 문자열을 미리 보고 추출 중 놓친 문자열을 잡으세요:

python manage.py translate --target-lang de --dry-run

이는 API 호출이나 변경 없이 .po 파일의 모든 미번역 항목을 표시합니다.

9. f-string은 번역할 수 없음 (Django 제한)

_()에 f-string을 감싸면 구문 오류가 발생하거나 makemessages가 번역할 수 없는 깨진/부분 문자열을 추출합니다.

Python f-string은 런타임에 평가됩니다. xgettext 추출 도구는 소스 코드를 정적으로 파싱하므로 {} 중괄호 안의 Python 표현식을 평가할 수 없습니다. 따라서 _(f"Hello, {name}")는 리터럴 {name} 표현식이 포함된 문자열로 추출되거나(또는 완전히 추출에 실패) 결과 .po 항목은 런타임 문자열과 절대 일치하지 않습니다.

대신 Django의 % 포맷팅 또는 이름이 지정된 플레이스홀더가 있는 .format()을 사용하세요:

# Wrong -- f-string cannot be extracted
message = _(f"Hello, {user.name}! You have {count} new messages.")

# Correct -- named placeholders
message = _("Hello, %(name)s! You have %(count)d new messages.") % {
    "name": user.name,
    "count": count,
}

# Also correct -- .format() with positional args
message = _("Hello, {0}! You have {1} new messages.").format(user.name, count)

이것은 TranslateBot이나 도구의 제한이 아닙니다. gettext 작동 방식의 근본적인 특성입니다. 소스 문자열은 런타임에 추출하고 조회할 수 있도록 정적 리터럴이어야 합니다.

TranslateBot은 번역 중 이러한 모든 플레이스홀더 형식(%(name)s, {0}, %s, HTML 태그)을 보존하므로 번역된 문자열이 완전히 기능적으로 유지됩니다.

10. 플레이스홀더 포맷 문자열 오류가 .po 컴파일을 깨뜨림

compilemessages가 오류와 함께 실패하거나 .po 파일에 #, python-format 플래그 불일치가 있고 항목이 조용히 삭제됩니다.

소스 문자열에 %(name)s와 같은 Python 포맷 플레이스홀더가 포함되면 Django는 .po 항목을 #, python-format으로 표시합니다. 번역에 다른 플레이스홀더가 있으면(%(nome)s 같은 오타, 누락된 플레이스홀더 또는 추가 플레이스홀더) gettext 도구가 항목을 거부하거나 compilemessages가 실패할 수 있습니다. 이는 수동 번역이나 플레이스홀더 의미론을 이해하지 못하는 AI 번역 도구에서 흔히 발생합니다.

깨진 항목은 다음과 같습니다:

#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."

여기서 %(naam)s%(name)s여야 합니다. 플레이스홀더는 소스와 정확히 일치해야 합니다.

번역된 문자열이 소스와 정확히 같은 플레이스홀더를 포함하는지 확인하세요. 오타, 누락된 플레이스홀더, 추가 플레이스홀더를 확인하세요.

이것은 TranslateBot이 진정한 이점을 제공하는 영역입니다. 플레이스홀더 보존 로직은 번역 출력의 모든 포맷 문자열(%(name)s, {0}, %s)이 소스와 정확히 일치하도록 보장합니다. 플레이스홀더 처리는 100% 테스트 커버리지로 다루어지므로 번역에서 발생하는 포맷 문자열 오류가 컴파일 시점이 아닌 도구 수준에서 제거됩니다.

수동으로 번역하거나 플레이스홀더를 처리하지 않는 도구를 사용하는 경우 다음으로 .po 파일을 검증하세요:

msgfmt --check-format locale/de/LC_MESSAGES/django.po

이는 gettext의 포맷 문자열 유효성 검사를 실행하고 불일치를 보고합니다.

모든 것을 종합: 방어적 워크플로

이러한 문제의 대부분은 공통 근본 원인을 공유합니다: 잊기 쉬운 수동 단계. 10가지 문제를 모두 방지하는 워크플로입니다:

# 1. Extract strings (catches #8 -- any new gettext-wrapped strings)
python manage.py makemessages -a --no-obsolete

# 2. Translate (catches #1, #5, #8, #9, #10 -- handles untranslated,
#    fuzzy, and placeholder issues automatically)
python manage.py translate

# 3. Compile (catches #1 -- generates .mo files)
python manage.py compilemessages

# 4. Verify in CI (catches everything that slipped through)
python manage.py check_translations --makemessages

4단계를 CI 파이프라인에 추가하면 미번역 문자열, fuzzy 항목, 포맷 오류가 프로덕션에 도달하기 전에 빌드를 실패시킵니다.

빠른 참조 테이블

원인 증상 한 줄 수정
compilemessages 없음 번역이 존재하지만 나타나지 않음 python manage.py compilemessages
{% load i18n %} 누락 {% trans %}에서 TemplateSyntaxError 템플릿에 {% load i18n %} 추가
LocaleMiddleware 누락 언어가 항상 영어 기본값 MIDDLEWAREdjango.middleware.locale.LocaleMiddleware 추가
언어 코드 불일치 로케일 디렉토리를 찾을 수 없음 디렉토리에 pt_BR(밑줄), 설정에 pt-br(하이픈) 사용
Fuzzy 항목 건너뜀 .po에 번역이 있지만 앱에 없음 검토 후 #, fuzzy 플래그 제거
잘못된 LOCALE_PATHS .po 파일이 존재하지만 Django가 무시 LOCALE_PATHS를 절대 경로로 설정
캐시된 번역 업데이트 후 이전 텍스트 표시 애플리케이션 서버 재시작
문자열이 gettext에 없음 문자열이 .po 파일에 없음 _() 또는 {% trans %}로 감싸기
gettext의 f-string 깨진 추출 또는 런타임 불일치 % 또는 .format() 플레이스홀더로 대체
플레이스홀더 불일치 compilemessages 실패 또는 항목 삭제 소스와 번역 간 플레이스홀더 정확히 일치

번역 단계를 자동화하고 CI에서 검사를 시행하면 이러한 문제의 대부분이 사라집니다. TranslateBot의 translate 명령은 플레이스홀더 보존과 증분 번역을 처리하고 check_translations는 프로덕션에 도달하기 전에 빠져나가는 모든 것(미번역 항목, fuzzy 플래그, 포맷 문자열 문제)을 잡아냅니다.

.po 파일 수동 편집 중단

TranslateBot은 AI로 Django 번역을 자동화합니다. 명령 하나로 모든 언어를, 번역당 몇 푼이면 충분합니다.