블로그로 돌아가기

Django 번역 워크플로우를 자동화하는 방법

2026-07-11 7분 읽기

Django 번역 시스템을 어느 정도 사용해 보았다면 이 과정이 익숙할 것입니다. 문자열에 _()를 표시하고, makemessages를 실행하고, .po 파일을 열어 수백 개의 빈 msgstr 필드를 마주합니다. 그리고 앱이 지원하는 모든 언어에 대해 같은 작업을 반복합니다.

이 방식은 동작합니다. 하지만 기능 개발에 써야 할 시간을 상당히 소모합니다. 번역 가능한 문자열이 200개이고 지원 언어가 5개인 프로젝트라면 괸리해야 할 번역이 1,000개입니다. 버튼 라벨 하나를 변겅하면 다음 배포 전에 5개의 .po 파일을 모두 업데이트해야 하며, 그렇지 않으면 일부만 번역된 페이지가 배포됩니다.

수동 워크플로의 구조와 한계, 그리고 품질을 유지하면서 자동화하는 실용적인 방법을 정리했습니다.

수동 워크플로

Django에 내장된 번역 파이프라인은 네 단계로 구성됩니다:

  1. 코드와 템플릿에서 gettext 또는 {% trans %}로 문자열을 표시합니다
  2. makemessages를 실행하여 .po 파일로 추출합니다
  3. .po 파일의 msgstr 한목을 번역합니다
  4. compilemessages를 실행하여 Django가 읽는 바이너리 .mo 파일을 생성합니다

1단계와 2단계는 빠릅니다. 4단계는 명령어 하나면 끝납니다. 모든 것이 느려지는 지점은 3단계입니다.

3단계의 실제 모습

에디터에서 locale/de/LC_MESSAGES/django.po를 열면 다음과 같은 내용이 보입니다:

#: myapp/views.py:42
msgid "Your subscription has been renewed."
msgstr ""

#: myapp/templates/dashboard.html:18
msgid "Welcome back, %(username)s"
msgstr ""

#: myapp/forms.py:15
msgid "This field is required."
msgstr ""

각 항목에 대해 %(username)s{count} 같은 플레이스홀더 변수를 보존하는 정확한 번역이 필요합니다. 플레이스홀더가 하나라도 깨지면 Django는 런타임에 포매팅 오류를 발생시킵니다.

5개 언어에 새 문자열이 100개라면, 번역 서비스 사이에서 복사 붙여넣기를 하고, 플레이스홀더 혀식을 수정하고, 빠뜨린 것이 없기를 바라면서 오후를 통째로 보내게 됩니다. 그리고 다음 스프린트에서 같은 작업을 반복합니다.

수동 방식의 한계

소규모 프로젝트는 수동 번역으로 충분할 수 있습니다. 프로젝트가 몇 가지 임계점을 넘으면 워크플로에 문재가 생기기 시작합니다.

증가하는 문자열 수

템플릿이 십여 개, 폼이 몇 개, 이메일 알림이 포함된 Django 앱은 번역 가능한 문자열이 300개에 쉽게 도달합니다. 지원 언어 수를 곱하면 총 수는 빠르게 늘어납니다. 어느 시점이 되면 아무도 그 .po 파일을 열고 싶어하지 않습니다.

오래된 번역

개발자가 기존 문자열을 변경하면 makemessages는 기존 번역을 fuzzy로 표시합니다. fuzzy 항목은 수동 검토가 필요하며, 사용자가 독일어 폐이지에서 영어가 표시되는 것을 신고할 때까지 아무도 눈치채지 못하는 경우가 많습니다. fuzzy 백로그는 릴리스 사이에서 조용히 쌓여 갑니다.

플레이스홀더 오류

Django의 .po 형식은 여러 플레이스홀더 스타일을 사용합니다. Python의 % 포매팅용 %(name)s, .format()과 f-string용 {name}, 템플릿 변수용 {{ name }}이 있습니다. 번역자(사람이든 기계든)가 이를 잘못 변환하는 겅우가 있으며, 오류는 런타임에서만 나타납니다. 독일어 번역에서 %(username)s가 잘못되면 영어에서는 정상 작동하는 뷰가 오류를 발생시킬 수 있습니다.

배포 차단

번역이 배포 파이프라인에서 수동 단계라면 병목이 됩니다. 번역이 완료될 때까지 배포를 기다리거나(느림), 번역이 누락된 상태로 배포하거나(나쁜 사용자 경험), 둘 중 하나를 선택해야 합니다. 주 단위로 배포하는 환경에서는 어느 쪽도 좋은 선택이 아닙니다.

자동화의 구조

Django 번역 자동화란 3단계(수동 번역)를 프로그래밍 방식의 프로세스로 대체하면서 나머지는 그대로 유지하는 것입니다. makemessagescompilemessages 웨크플로는 변하지 않습니다. .po 파일을 직접 번역하는 작업만 중단하면 됩니다.

옵션 1: 직접 스크립트 작성

.po 파일을 읽고, 미번역 문자열을 번역 API로 전송하고, 결과를 다시 기록하는 스크립트를 직접 작성할 수 있습니다. 기본 버전은 의외로 짧습니다:

import polib
from openai import OpenAI

client = OpenAI()
po = polib.POFile("locale/de/LC_MESSAGES/django.po")

for entry in po.untranslated_entries():
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "Translate to German. Preserve all "
             "placeholder variables like %(name)s exactly as they appear."},
            {"role": "user", "content": entry.msgid},
        ],
    )
    entry.msgstr = response.choices[0].message.content.strip()

po.save()

이 코드로 80% 정도는 해결됩니다. 나머지 20%에서 복잡해집니다:

  • 복수형(msgid_plural과 여러 msgstr[n] 항목)은 별도의 쳐리가 필요합니다. 언어마다 복수형 규칙이 다르기 때문입니다
  • 컨텍스트 문자열(msgctxt)을 API에 전달해야 번역이 용도에 맞게 됩니다
  • 플레이스홀더가 번역 과정에서 온전히 유지되었는지 검증이 필요합니다
  • 속도 제한과 API 오류에 대한 재시도 로직이 필요합니다
  • 스크립트가 .po 파일 인코딩(특정 헤더 필드가 포함된 UTF-8)을 올바르게 처리해야 합니다
  • fuzzy 항목은 미번역 항목과 다르게 처리해야 합니다

이 모든 것을 직접 구축하는 것은 가능하지만, 며칠의 작업과 지속적인 유지보수가 필요합니다. 번역 API의 동작이나 가격이 변경될 때마다 스크립트를 업데이트해야 합니다.

옵션 2: 번역 관리 플랫폼 사용

Crowdin, Transifex, Lokalise 같은 서비스는 버전 관리 시스템과 연동되며 번역자를 위한 웹 인터페이스를 제공합니다. .po 파일을 기본 지원하고, 복수형을 처리하며, 기계 번역을 1차 작업으로 활용하는 자동화 워크플로를 설정할 수 있습니다.

단점은 비용과 복잡성입니다. 이러한 플랫폼은 전담 번역자가 있는 팀을 위해 설계되었으며, 가격도 그에 맞춰져 있습니다. .po 파일 번역만 필요한 개인 개발자나 소규모 팀에게는 불필요한 인프라가 추가됩니다.

옵션 3: 전용 도쿠

Django의 .po 파일 워크플로에 특화된 도구는 형식의 특수성을 존중하면서 번역을 자동화할 수 있습니다. 복수형을 처리하고, 플레이스홀더를 보존하고, fuzzy 항목을 관리하며, 기존 makemessages/compilemessages 파이프라인과 연동됩니다.

장점은 이 용도에 정확히 맞게 설계되어 있어 작성하고 유지해야 할 접착 코드가 적다는 것입니다. 단점은 번역 워크플로에 의존성이 하나 추가된다는 것입니다.

CI/CD에서 자동 번역 설정

어떤 도구를 선택하든 배포 파이프라인과의 연동은 동일한 패턴을 따릅니다:

# .github/workflows/translate.yml
name: Update translations

on:
  push:
    branches: [main]

jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: pip install django

      - name: Extract new strings
        run: python manage.py makemessages -a --no-obsolete

      - name: Translate .po files
        run: python scripts/translate.py  # your translation script

      - name: Compile messages
        run: python manage.py compilemessages

      - name: Commit and push
        run: |
          git config user.name "github-actions"
          git config user.email "actions@github.com"
          git add locale/
          git diff --staged --quiet || git commit -m "chore: update translations"
          git push

워크플로가 새 문자열을 추출하고, 번역하고, 결과를 컴파일한 뒤, 업데이트된 파일을 커밋합니다. git diff --staged --quiet 체크는 변경 사항이 없을 때 빈 커밋이 생성되는 것을 방지합니다.

pre-commit 훅이나 예약된 작업으로 실행할 수도 있습니다. 핵심은 동일합니다. 번역이 별도의 수동 단계가 아니라 일반적인 개발 프로세스의 일부로 자동 수행되는 것입니다.

번역 검증

자동 번역에는 검증이 필요합니다. 최고 수준의 AI 모델도 플레이스홀더 형식을 깨뜨리거나 UI 맥락에 맞지 않는 번역을 생성하는 경우가 있습니다.

플레이스홀더 검증

msgidmsgstr 사이의 플레이스홀더를 비교하는 검사를 작성합니다:

import re
import polib

PLACEHOLDER_RE = re.compile(r"%\(\w+\)[sd]|\{\w+\}|\{\{.*?\}\}")

def validate_po(path):
    po = polib.POFile(path)
    errors = []
    for entry in po.translated_entries():
        src = set(PLACEHOLDER_RE.findall(entry.msgid))
        dst = set(PLACEHOLDER_RE.findall(entry.msgstr))
        if src != dst:
            errors.append(f"Mismatch: {entry.msgid!r}")
    return errors

CI에서 실행하여 플레이스홀더가 깨진 경우 빌드를 실패시킵니다. 실행에 몇 초밖에 걸리지 않으며, 가장 위험한 유형의 번역 오류를 프로덕션에 도달하기 전에 잡아냅니다.

길이 검사

일부 번역은 원본 문자열보다 상당히 깁니다. 독일어 텍스트는 일반적으로 영어보다 20-30% 길며, 고정 너비 UI 요소에서 레이아웃이 깨질 수 있습니다. 간단한 길이 비율 검사로 레이아웃 문제를 일으킬 수 있는 번역을 사전에 감지할 수 있습니다.

fuzzy 항목 감사

자동 번역 후 fuzzy로 남아 있는 항목이 없는지 확인합니다. fuzzy 항목이 있으면 Django가 해당 번역을 사용하지 않고 영어로 폴백합니다. CI 파이프라인에서 fuzzy 항목을 실패하는 테스트와 동일하게 취급해야 합니다.

번역 동기화 유지

번역 유지보수에서 가장 어려운 부분은 초기 번역이 아닙니다. 코드베이스가 발전하면서 번역을 최신 상태로 유지하는 것입니다.

문자열 변경

기존 번역 가능 문자열을 변경하면 makemessages는 미묘한 동작을 합니다. .po 파일에서 유사한 문자열을 찾아 fuzzy 일치를 생성합니다. 기존 번역은 보존되지만 검토가 필요하다는 표시가 붙습니다. 사람 번역자가 변경 사항을 검토할 때는 유용하지만, 자동화된 워크플로에서는 fuzzy 항목을 별도로 처리해야 합니다.

실용적인 접근법은 fuzzy 플래그를 지우고 처음부터 다시 번역하는 것입니다. fuzzy 매칭은 사람 번역자의 작업을 절약하지만, 자동화 도구는 새 문자열을 직접 번역하면 됩니다. 기존 번역을 새 원본 문자열에 맞추려고 수정하는 것보다 보통 더 나은 결과를 얻을 수 있습니다.

새 문자열

새 문자열은 makemessages 실행 후 미번역 항목으로 나타납니다. 자동화된 워크플로에서 이를 번역하고 결과를 커밋해야 합니다. CI에서 makemessages를 실행하면 새 문자열은 병합 시 자동으로 번역됩니다.

삭제된 문자열

makemessages --no-obsolete를 실행하면 코드에 더 이상 존재하지 않는 문자열의 항목이 제거됩니다. 이렇게 하면 .po 파일이 깨끗하게 유지되고 시간이 지나면서 쌓이는 불필요한 번역을 방지할 수 있습니다.

자동화 전 확인 사항

자동 번역을 설정하기 전에 몇 가지를 확인합니다:

  • 모든 번역 가능 문자열이 _() 또는 {% trans %}로 올바르게 표시되어 있어야 합니다. makemessages를 실행하고 누락된 문자열이 없는지 확인합니다.
  • LOCALE_PATHS 설정이 올바르고 locale/ 디렉토리 구조가 갖춰져 있어야 합니다.
  • 기존 .po 파일에 대해 compilemessages가 오류 없이 실행되어야 합니다.
  • 자동 번역이 배포되기 전에 검토할 수 있는 방법이 있어야 합니다. 스테이징 환경에서 각 언어로 앱을 클릭해 보는 것만으로도 충분합니다.

번역 자동화는 나머지 i18n 설정이 견고할 때 가장 잘 동작합니다. 1단계와 2단계가 올바르지 않으면 3단계를 자동화해도 도움이 되지 않습니다.

실용적인 팁

Django 자동 번역 작업에서 배운 몇 가지 사항입니다:

  • makemessages--no-obsolete 플래그를 사용하여 .po 파일을 깨끗하게 유지합니다. 불필요한 항목은 사람 검토자와 자동화 도구 모두를 혼란스럽게 합니다.
  • 같은 영어 문자열이 다른 맥락에서 다른 번역이 필요한 경우 msgctxt를 사용합니다. "Post"는 블로그 게시물을 의미할 수도 있고 게시 행위를 의미할 수도 있으며, 컨텍스트 없이는 번역자(사람이든 자동이든)가 추측해야 합니다.
  • 영어와 구조적으로 다른 언어로 테스트합니다. 독일어의 긴 단어와 일본어의 문자 체계는 유럽 언어에서는 발견되지 않는 레이아웃 버그를 잡아냅니다.
  • 프로젝트별 용어 및 톤 가이드라인이 담긴 TRANSLATING.md 파일을 유지합니다. 프로젝트의 UI에서 "workspace"를 "project"로 부르는 경우 이를 문서화합니다. 일부 자동 번역 도구는 이 파일을 읽고 설정된 기준을 일관되게 적용할 수 있습니다.
  • .po.mo 파일을 함께 커밋합니다. CI에서 메시지를 컴파일하는 경우 .mo 파일이 배포 아티팩트에 포함되어 있는지 확인합니다. Django는 .po 파일이 아닌 .mo 파일을 읽습니다.

접근 방식 비교

3-5개 언어를 지원하는 Django 프로젝트를 운영하는 개인 개발자 또는 소규모 팀의 경우:

커스텀 스크립트는 유지보수를 즐기고 문자열 수가 200개 미만인 경우에 적합합니다. 그 이상이 되면 복수형, 플레이스홀더, fuzzy 항목과 관련된 예외 상황이 절약한 시간을 잠식하기 시작합니다.

번역 관리 플랫폼은 여러 프로젝트에 걸쳐 작업하는 번역팀이 있는 경우에 적합합니다. 단일 Django 앱에는 보통 과도합니다.

전용 도구는 중간 지점에 해당합니다. .po 파일의 특수성을 처리하면서도 직접 통합을 구축하고 유지할 필요가 없습니다. 월 비용은 보통 커스텀 솔루션에 투입되는 엔지니어링 시간보다 적습니다.

팀 규모와 번역 인프라 유지 의향에 맞는 접근 방식을 선택하면 됩니다. 중요한 것은 번역이 수동 병목이 아닌 일반적인 개발 주기의 일부가 되는 것입니다.

.po 파일 수동 편집 중단

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