기능플러그인가격리소스
언어 변경
리소스CI/CD 파이프라인에서 .po 번역을 자동화하는 방법

CI/CD 파이프라인에서 .po 번역을 자동화하는 방법

SimplePoTranslate Team2026년 6월 9일
CI/CD 파이프라인에서 .po 번역을 자동화하는 방법

팀은 하루에 14번 main에 병합합니다. 이러한 병합은 각각 새로운 사용자 대면 문자열(버튼 레이블, 오류 메시지, 툴팁)을 추가할 수 있습니다. 그리고 이 모든 문자열은 처음에는 영어로만 존재합니다. 릴리스 프로세스의 어딘가에서, 사람이 새로운 문자열을 인지하고, 내보내고, 번역하고, 다시 붙여넣고, 재컴파일해야 합니다. 이 사람은 병목 현상이며, 빠르게 배포하는 팀에서는 이 병목 현상으로 인해 독일 사용자들이 두 번의 스프린트 동안 영어 플레이스홀더를 보게 됩니다.

해결책은 CI/CD 파이프라인에서 번역을 자동화하여 새로운 문자열이 도입된 동일한 병합에서 번역되도록 하는 것입니다. 일상적인 경우에는 중요한 경로에 사람이 개입할 필요가 없습니다. 이것은 오늘날 완전히 달성 가능하지만, 대부분의 팀이 빠지는 함정, 즉 %s 플레이스홀더와 .po 구조를 조용히 손상시키는 원시 LLM 호출을 직접 작성하는 것을 피해야 합니다. 이 가이드는 현실적인 CI 파이프라인 패턴, 작동하는 GitHub Actions 스켈레톤, 그리고 파이프라인이 도움이 되는 것과 손상된 빌드를 배포하는 것을 구분하는 설계 결정(멱등성, 새 항목만 번역, 검토 게이트)에 대해 설명합니다.

수동 번역이 릴리스 병목 현상인 이유

"각 릴리스 전에 번역하면 안 되는 이유"에 대한 답은 타이밍이 절대 맞지 않기 때문입니다. 문자열은 개발자에 의해 지속적으로 추가되지만, 번역은 다른 사람이 다른 일정으로 일괄적으로 수행됩니다. 이 두 가지 주기의 간극에서 현지화 부채가 축적됩니다.

두 가지 주기 문제

수동 흐름을 상상해 보세요. 개발자가 __( 'Export to CSV', 'mytextdomain' )를 추가하고 병합합니다. 아무도 .pot를 재생성하지 않습니다. 2주 후 누군가가 wp i18n make-pot를 실행하고, 번역되지 않은 새로운 문자열 40개(일부는 휴가 간 개발자로부터 온 것)를 발견하고, 그 중 절반의 의도를 추측한 다음, .po를 번역가에게 보냅니다. 번역본이 돌아오고 붙여넣어지지만, 플레이스홀더가 살아남았을 수도 있고 그렇지 않을 수도 있습니다. 그동안 세 번의 릴리스가 해당 문자열들을 영어로 출시했습니다.

거기 있는 모든 단계는 수동적이고, 일괄 처리되며, 오류가 발생하기 쉽습니다. CI/CD 자동화의 목표는 이를 병합 시 자동으로 실행되고, 실제로 변경된 내용만 번역하며, 뭔가 잘못되었을 때 크게 실패하는 것으로 압축하여, 번역을 주기적인 잡일에서 파이프라인의 보이지 않는 부분으로 만드는 것입니다.

파이프라인 패턴: 추출, 차이점 비교, 번역, 커밋

높은 수준에서, 자동화된 번역 작업은 main으로 병합될 때 네 단계를 실행합니다: 템플릿 재생성, 새로운 내용 감지, 해당 문자열만 번역, 그리고 결과를 다시 커밋. 전체 프로세스는 멱등해야 합니다. 문자열 변경이 없는 병합에서 실행할 경우 차이점도 소음도 없어야 합니다.

추출 및 차이점 비교

첫 번째 단계는 추출입니다. 현재 소스에서 .pot를 재생성하여 코드베이스의 모든 문자열을 반영하도록 합니다:

wp i18n make-pot . languages/myplugin.pot

두 번째 단계는 차이점 비교입니다. 이것은 전체 파이프라인에서 가장 중요한 설계 결정입니다. 모든 병합에서 모든 문자열을 다시 번역하고 싶지 않을 것입니다. 이는 API 호출을 낭비하고, 사람이 이미 수정한 문자열을 다시 번역할 위험이 있으며, 엄청난 양의 검토 차이점을 생성합니다. 대신, 새로 생성된 .pot를 각 기존 .po에 병합하고, 이제 비어있는 항목(진정으로 새로운 문자열)만 번역합니다:

# Merge new template into each locale, preserving existing translations.
# Newly-added strings appear with empty msgstr; --backup=none keeps the tree clean.
for po in languages/*.po; do
    msgmerge --update --backup=none "$po" languages/myplugin.pot
done

msgmerge 후에는 완전히 새로운 문자열만 비어있는 msgstr을 가집니다. 이전에 번역된 모든 것은 그대로 유지됩니다. 이 속성 덕분에 파이프라인은 멱등성을 가지며, 검토 차이점은 작고 검토 가능하게 유지됩니다.

번역 및 커밋

세 번째 단계는 비어있는 항목들만 번역 단계로 보냅니다. 네 번째 단계는 업데이트된 .po를 커밋하고, .mo를 컴파일하며, 모든 JSON을 재생성합니다. 다음으로 이 모든 것을 GitHub Actions에 연결할 것입니다.

GitHub Actions 스켈레톤

"워크플로 파일에서 이것이 실제로 어떻게 보이는가"에 대한 답은 다음과 같습니다: main으로 푸시될 때 트리거되는 작업으로, 추출-차이점 비교-번역-커밋 주기를 실행하고, 결과를 main에 직접 커밋하는 대신 풀 리퀘스트를 엽니다.

워크플로 파일

name: Translate on merge

on:
  push:
    branches: [ main ]

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

      - name: Install WP-CLI and gettext
        run: |
          sudo apt-get update && sudo apt-get install -y gettext
          curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
          sudo mv wp-cli.phar /usr/local/bin/wp && sudo chmod +x /usr/local/bin/wp

      - name: Regenerate template and merge into locales
        run: |
          wp i18n make-pot . languages/myplugin.pot
          for po in languages/*.po; do
            msgmerge --update --backup=none "$po" languages/myplugin.pot
          done

      - name: Translate only new (empty) strings
        env:
          TRANSLATE_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
        run: ./scripts/translate-new-strings.sh languages/

      - name: Compile .mo and JSON
        run: |
          wp i18n make-mo languages/
          wp i18n make-json languages/ --no-purge

      - name: Open pull request with translations
        uses: peter-evans/create-pull-request@v6
        with:
          branch: chore/auto-translations
          title: "chore: auto-translate new strings"
          commit-message: "chore: auto-translate new strings"

실제 작업이 숨어있는 곳

translate-new-strings.sh 단계는 실제 번역이 일어나는 곳입니다. 이 스크립트의 순진한 버전은 각 비어있는 항목을 읽고, LLM API에 한 번에 하나의 문자열을 전송하고, 응답을 다시 붙여넣습니다. 그 순진한 버전이 바로 함정이며, 그 이유를 명확히 설명할 가치가 있습니다.

직접 작성한 LLM 호출이 파일을 손상시키는 이유

간단히 말해: 원시 LLM 호출은 .po 문자열을 산문으로 취급하며, .po 문자열은 산문이 아닙니다. 이들은 플레이스홀더, 복수형, 그리고 챗-완성 호출이 기쁘게 파괴하는 컨텍스트를 가진 구조화된 데이터입니다.

테스트에서 볼 수 없는 플레이스홀더 손상

Deleted %d of %s files를 일반 채팅 엔드포인트로 보내면 Supprimé %d des %s fichiers (괜찮음) 또는 Supprimé %d de % s fichiers (공백이 플레이스홀더에 끼어들어가 sprintf()가 런타임에 예외를 발생시킴)를 돌려받을 수 있습니다. 복수형 항목을 보내면 모델이 두 가지 형태를 하나로 합쳐 두 개 이상의 복수형 범주를 가진 언어를 손상시킬 수 있습니다. <a href="%s">가 포함된 문자열을 보내면 모델이 URL을 번역하거나 태그를 삭제할 수 있습니다. 이러한 실패는 모든 로케일에서 렌더링된 출력을 특별히 테스트하지 않는 한 테스트에서 나타나지 않습니다. 대신 버그 리포트를 읽을 수 없는 사용자를 위한 프로덕션 환경에서 런타임 오류로 나타납니다.

유지보수의 함정

프롬프트 엔지니어링과 정규 표현식 후처리로 이를 방어하려고 시도할 수 있으며, 많은 팀이 그렇게 합니다. 문제는 이제 취약한 번역 엔진을 사이드 프로젝트로 유지보수하고, Gettext 생태계가 이미 해결한 모든 플레이스홀더 엣지 케이스를 다시 발견하는 것입니다. 우리는 코드 변수를 손상시키지 않고 PO 파일 번역하기에서 변수가 손상되는 구체적인 방법과 이를 방지하는 방법을 정리했습니다. 여기에서 얻을 수 있는 교훈은 직접적으로 적용됩니다: 모델 품질은 거의 문제가 되지 않으며, 그 주변의 구조적 처리가 문제입니다.

API 기반 클라우드 번역기를 CI에 통합하기

이것이 바로 API 기반 번역 서비스가 translate-new-strings.sh의 추측성 작업을 대체하는 지점입니다. LLM 호출과 후처리 정규 표현식을 직접 작성하는 대신, CI 단계에서 변경된 .po를 Gettext 구조를 이미 이해하고 깔끔한 출력을 반환하는 서비스에 업로드합니다. 파이프라인 형태는 동일하게 유지됩니다(추출, 차이점 비교, 번역, 커밋). 하지만 취약한 중간 단계가 단일 API 호출이 됩니다.

스크립트를 대체하는 API 호출

SimplePoTranslate는 바로 이러한 용도로 제작되었습니다. 자동화 및 CI에 적합한 클라우드 API를 제공하므로, 워크플로의 번역 단계는 .po를 전달하고 번역된 .po를 다시 받는 요청이 되며, 문자열별 반복 작업이 필요 없습니다. Syntax Locking%s, %1$s, {count}, HTML 및 코드 토큰을 자동으로 제자리에 고정합니다. 이전 섹션의 모든 종류의 버그는 사용자가 유지보수하는 정규 표현식이 아니라 엔진에 의해 처리됩니다. 완벽한 Gettext 복수형 및 msgctxt 지원을 통해 복수형 형식과 컨텍스트는 왕복 후에도 유지되며, 이는 챗-완성 호출이 보장할 수 없는 부분입니다.

멱등성과 검토 게이트는 여전히 사용자 권한입니다

어떤 엔진을 사용하든 두 가지 설계 결정은 여전히 사용자에게 달려 있습니다. 첫째, 멱등성: msgmerge 후에 비어있는 항목만 계속 번역하여, 무의미한 병합이 차이점을 생성하지 않도록 합니다. 둘째, 검토 게이트: 위 스켈레톤과 같이 작업이 main에 직접 커밋하는 대신 풀 리퀘스트를 열도록 합니다. 기계 번역은 문자열을 빠르게 배포하는 데 탁월하지만, 병합 전 사람의 확인은 드물게 발생하는 컨텍스트 누락을 잡아낼 수 있으며, PR은 일반적인 경우를 막지 않으면서 이러한 확인을 가능하게 합니다. 여러 로케일 또는 여러 클라이언트 사이트를 다루는 팀은 에이전시를 위한 이상적인 현지화 워크플로에서 이 형태를 인식할 것입니다. 여기서는 동일한 자동화 후 검토 패턴이 수십 개의 프로젝트에 걸쳐 확장됩니다.

결론

손상된 빌드를 배포하지 않고 CI/CD에서 번역을 자동화하려면 패턴은 일관적입니다: 병합 시 .pot를 재생성하고, 각 로케일에 msgmerge하여 새로운 문자열만 드러내고, 해당 문자열만 번역하며, 풀 리퀘스트 검토 게이트 뒤에 결과를 커밋합니다. 멱등성은 차이점을 깨끗하게 유지하고, 검토 게이트는 드물게 발생하는 잘못된 번역이 프로덕션으로 나가는 것을 막습니다.

정확히 해야 할 부분은 번역 단계 그 자체입니다. 직접 작성한 LLM 호출은 테스트에서 잡히지 않는 방식으로 플레이스홀더와 복수형을 손상시킬 것이며, 기능 코드를 유지보수하는 것보다 번역 연동 코드를 유지보수하는 데 더 많은 시간을 할애하게 될 것입니다. Syntax Locking이 적용된 API 기반 엔진은 이러한 전체 실패 모드를 제거하여, 파이프라인이 새로운 문자열을 도입한 동일한 병합에서 번역하도록 합니다. 그러면 비영어권 사용자들이 더 이상 영어 플레이스홀더를 보지 않게 됩니다.

플레이스홀더를 손상시키지 않고 파이프라인에서 번역을 자동화할 준비가 되셨나요? 신용카드 없이 SimplePoTranslate를 무료로 사용해 보세요 — 무료 티어에서 시작하여 사용자 자신의 .po 파일에 대해 API를 검증하고, 준비가 되면 CI에 연결하세요.