
TMS 없는 지속적 현지화
코드를 지속적으로 배포한다면 번역도 지속적으로 배포해야 해요. 개발 워크플로에서 현지화를 자동화하는 방법을 알아보세요.
지속적 현지화란?
지속적 현지화는 번역을 정기 릴리스마다 일괄 처리하는 대신 개발 워크플로의 일부로 새 문자열을 자동 번역하는 방식이에요. 개발자가 UI 문자열이 있는 새 기능을 추가하면 기능을 배포하기 전에 해당 문자열을 번역해요.
기존 방식
대부분의 팀은 새 문자열을 모아 TMS로 내보내고, 번역가를 기다린 뒤 결과를 가져와 배포하는 방식으로 번역을 일괄 처리해요. 이 과정에서 현지화가 병목이 되어 해외 릴리스가 며칠 또는 몇 주씩 늦어져요.
개발자가 원문 로케일 파일에 새 문자열 추가
문자열을 내보내 TMS 플랫폼에 업로드
번역가에게 알림을 보내고 작업 시작
번역 검토 및 승인
번역된 파일 다운로드 및 다시 병합
번역과 함께 기능 배포 (며칠에서 몇 주 후)
IDE에 바로 통합되는 방식
IDE에 바로 통합된 번역 방식에서는 개발자가 평소 코딩 워크플로의 일부로 문자열을 번역해요. 내보내기, 업로드, 대기 없이 개발과 같은 세션에서 번역이 이루어져요.
개발자가 원문 로케일 파일에 새 문자열 추가
개발자가 AI 어시스턴트에게 파일 번역 요청
번역된 파일을 프로젝트에 바로 저장
같은 커밋에서 번역과 함께 기능 배포
기존 방식
6 steps
Multiple tools and platforms
Days to weeks per cycle
IDE에 바로 통합되는 방식
4 steps
Inside your editor
Same commit
번역 파이프라인 설정
IDE를 넘어 번역을 자동화하려는 팀은 main에 푸시할 때마다 새 문자열이나 변경된 문자열을 번역하는 CI/CD 단계를 추가하세요. GitHub Actions, GitLab CI 또는 모든 CI/CD 시스템에서 사용할 수 있어요.
소규모 팀은 IDE에 바로 통합되는 번역부터 시작하세요. 팀의 개발자가 3-4명을 넘거나 main에 병합할 때마다 번역을 반드시 포함해야 한다면 CI/CD 자동화를 추가하세요.
# .github/workflows/translate.yml
name: Translate
on:
push:
branches: [main]
paths: ['locales/en/**']
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Translate changed strings
run: |
npx i18n-agent translate locales/en.json \
--to de,ja,es,fr,ko,zh-Hans \
--api-key ${{ secrets.I18N_AGENT_API_KEY }}
- name: Commit translations
run: |
git config user.name "github-actions"
git config user.email "[email protected]"
git add locales/
git diff --cached --quiet || git commit -m "chore: update translations"
git push# .gitlab-ci.yml
translate:
stage: deploy
only:
changes:
- locales/en/**
script:
- npx i18n-agent translate locales/en.json
--to de,ja,es,fr --api-key $I18N_AGENT_API_KEY
- git add locales/ && git commit -m "chore: translations" && git pushCI에 번역 QA 추가
CI 파이프라인의 자동 검사는 누락된 번역, 손상된 플레이스홀더, 잘못된 ICU 메시지 형식이 실제 서비스에 반영되기 전에 찾아내요. 원문 로케일 파일을 모든 대상 로케일과 비교하는 간단한 스크립트로 구현할 수 있어요.
#!/bin/bash
# check-translations.sh — Run in CI to catch missing translations
SOURCE="locales/en.json"
LANGS=("de" "ja" "es" "fr")
EXIT_CODE=0
# Extract all keys from source
SOURCE_KEYS=$(jq -r '[paths(scalars)] | map(join(".")) | .[]' "$SOURCE" | sort)
SOURCE_COUNT=$(echo "$SOURCE_KEYS" | wc -l)
for lang in "${LANGS[@]}"; do
TARGET="locales/$lang.json"
if [ ! -f "$TARGET" ]; then
echo "❌ Missing: $TARGET"
EXIT_CODE=1
continue
fi
TARGET_KEYS=$(jq -r '[paths(scalars)] | map(join(".")) | .[]' "$TARGET" | sort)
MISSING=$(comm -23 <(echo "$SOURCE_KEYS") <(echo "$TARGET_KEYS"))
if [ -n "$MISSING" ]; then
COUNT=$(echo "$MISSING" | wc -l)
echo "❌ $lang: $COUNT missing keys"
echo "$MISSING" | head -5
EXIT_CODE=1
else
echo "✅ $lang: all $SOURCE_COUNT keys present"
fi
done
exit $EXIT_CODE#!/bin/bash
# check-placeholders.sh — Validate placeholder consistency
SOURCE="locales/en.json"
LANGS=("de" "ja" "es")
for lang in "${LANGS[@]}"; do
TARGET="locales/$lang.json"
# Compare placeholders like {{name}}, {count}, %s, %d
jq -r 'paths(scalars) as $p | [($p | join(".")), (getpath($p))]
| @tsv' "$SOURCE" | while IFS=$'\t' read -r key value; do
SOURCE_PH=$(echo "$value" | grep -oE '\{\{[^}]+\}\}|%[sd@]' | sort)
TARGET_VAL=$(jq -r "getpath($(echo $key | jq -R 'split(".")'))" "$TARGET")
TARGET_PH=$(echo "$TARGET_VAL" | grep -oE '\{\{[^}]+\}\}|%[sd@]' | sort)
if [ "$SOURCE_PH" != "$TARGET_PH" ]; then
echo "❌ $lang/$key: placeholder mismatch"
echo " Source: $SOURCE_PH"
echo " Target: $TARGET_PH"
fi
done
doneCI에서 번역 검증
번역 드리프트 감지 및 처리
번역 드리프트는 원문 문자열이 바뀌어도 번역이 업데이트되지 않는 현상이에요. 영어는 "Save changes"로 바뀌었는데 독일어에는 이전 문구가 남아 있는 경우예요. 드리프트 감지는 원문과 번역 파일의 타임스탬프 또는 콘텐츠 해시를 비교해요.
#!/bin/bash
# detect-drift.sh — Find stale translations
SOURCE="locales/en.json"
SOURCE_HASH=$(md5sum "$SOURCE" | cut -d' ' -f1)
HASH_FILE=".translation-hashes"
# Compare current source hash with stored hash
if [ -f "$HASH_FILE" ]; then
STORED_HASH=$(grep "^en:" "$HASH_FILE" | cut -d: -f2)
if [ "$SOURCE_HASH" != "$STORED_HASH" ]; then
echo "⚠️ Source strings changed since last translation"
echo " Run translations to update all locales"
# Show which keys changed
git diff HEAD~1 "$SOURCE" | grep '^[+-]' | grep -v '^[+-][+-]'
fi
fi
# Update stored hash
echo "en:$SOURCE_HASH" > "$HASH_FILE"번역 방식 선택
모든 콘텐츠에 같은 번역 방식이 필요한 것은 아니에요. UI 문자열과 레이블은 AI 번역에 잘 맞아요. 마케팅 문구는 AI 번역과 사람의 검토를 결합하면 좋아요. 법률 및 규정 준수 문서는 항상 전문 번역가가 번역해야 해요.
Translation Approach Comparison:
Content Type | Approach | Cost/word | Quality
─────────────────────┼───────────────────┼────────────┼──────────
UI strings, labels | AI/LLM | $0.001-01 | Production
Tooltips, help text | AI/LLM | $0.001-01 | Production
Marketing copy | AI + human review | $0.05-0.15 | High
Legal / compliance | Human translator | $0.15-0.40 | Certified
Brand voice content | Human translator | $0.20-0.50 | Premium번역용 Git 워크플로
병합 후가 아니라 병합 전에 기능 브랜치에서 번역하세요. 그러면 main에 병합할 때마다 빠짐없는 번역을 포함할 수 있어요. 지원하는 모든 로케일에서 번역이 빠짐없는지 확인하는 병합 전 검사를 추가하세요.
여러 브랜치가 대형 JSON 로케일 파일을 동시에 수정하면 병합 충돌이 자주 발생해요. 키를 알파벳순으로 정렬하고 한 줄에 키 하나만 사용해 변경 내역을 최소화하고 충돌을 쉽게 해결하세요.
# .gitattributes — reduce merge conflicts in locale files
locales/*.json merge=union
# Sort keys alphabetically to minimize diffs:
# package.json script:
"sort-locales": "node -e \"
const fs = require('fs');
const f = process.argv[1];
const d = JSON.parse(fs.readFileSync(f));
const s = (o) => Object.keys(o).sort().reduce((r,k) =>
({...r, [k]: typeof o[k]==='object' ? s(o[k]) : o[k]}), {});
fs.writeFileSync(f, JSON.stringify(s(d), null, 2)+'\\n');
\""흔한 실수
현지화를 개발 후 작업으로 취급
CI 수준 번역 검증 누락
TMS로 불필요하게 복잡한 시스템 구축
번역의 최신 상태 미추적
지금 i18n Agent 사용해 보기
번역 파일을 여기에 드롭
JSON, YAML, PO, XML, CSV, Markdown, Properties
또는 클릭하여 파일 선택
대상 언어