목차
누락된 번역은 파이프라인의 모든 단계를 통과하는 종류의 버그입니다. 단위 테스트는 번역 파일을 확인하지 않으므로 통과합니다. 통합 테스트는 기본 언어로 실행되므로 통과합니다. QA는 수동 테스트가 12개 언어 모두를 거의 다루지 않으므로 통과합니다. 그리고 브라질의 사용자가 버튼 레이블이 있어야 할 곳에 checkout.confirm_button을 보게 되고, 팀이 부주의해 보이는 버그 리포트를 받게 됩니다.
문제는 팀이 번역을 잊는 것이 아닙니다. 타입 검사기가 타입 오류를 잡거나 린터가 코드 스타일 문제를 잡는 것처럼 번역 격차를 잡는 자동화된 검사가 없다는 것입니다. 코드에는 ESLint, Prettier, TypeScript, 그리고 전체 CI 파이프라인이 있습니다. 번역에는... 누군가가 업데이트하는 것을 기억했으면 하는 JSON 파일이 있습니다.
이 글에서는 프로덕션에 도달하기 전에 누락된 번역, 플레이스홀더 불일치, 고아 키, 하드코딩된 문자열을 잡는 자동화된 i18n 상태 검사를 구현하는 방법을 다룹니다.
i18n 상태 검사가 실제로 무엇을 확인하나요?
포괄적인 i18n 상태 검사는 번역 설정의 네 가지 차원을 평가합니다:
1. 커버리지: 모든 키가 번역되었나요?
커버리지는 가장 간단하고 가장 영향력 있는 검사입니다. 소스 코드에서 사용되는 모든 번역 키에 대해 모든 대상 언어에 번역이 존재하나요?
소스 코드 참조: 1,247개 키 영어 (소스): 1,247/1,247 (100%) 스페인어: 1,235/1,247 (99%) 프랑스어: 1,198/1,247 (96%) 일본어: 1,150/1,247 (92%) 한국어: 1,089/1,247 (87%)
커버리지 검사는 가장 일반적인 시나리오를 잡습니다: 개발자가 새 기능을 추가하고, 영어 문자열을 작성하고, 다음 작업으로 넘어갑니다. 키는 영어 JSON 파일에 들어가지만 번역을 위해 전송되지 않습니다. 커버리지 검사 없이는 사용자가 만날 때까지 격차가 보이지 않습니다.
커버리지 검사는 네임스페이스 불일치도 잡습니다. 코드가 t('checkout.confirm')을 참조하지만 checkout 네임스페이스가 한국어에 존재하지 않는 경우, 이는 한국어 사용자에게 원시 키를 표시할 커버리지 격차입니다.
2. 품질: 번역이 구조적으로 올바른가요?
커버리지는 번역이 존재하는지만 알려줍니다. 품질은 런타임에 올바르게 작동할지를 알려줍니다.
가장 중요한 품질 검사는 플레이스홀더 유효성 검사입니다. 영어 문자열이 다음과 같다면:
"장바구니에 {count}개의 상품이 있습니다, {name}."
해당 문자열의 모든 번역에는 정확히 {count}와 {name}이 포함되어야 합니다. {count} 대신 {nombre}을 작성한 프랑스어 번역가는 런타임 버그를 만듭니다 — 보간 엔진이 {nombre}에 대한 값을 찾지 못하고 원시 플레이스홀더를 표시하거나 오류를 발생시킵니다.
다른 품질 검사에는 다음이 포함됩니다:
- 빈 값: 언어 파일에 존재하지만 빈 문자열을 가진 키. 이는 보통 실제 번역 없이 프로그래밍 방식으로 키를 생성한 것을 나타냅니다.
- 소스와 동일한 문자열: 소스 언어와 문자 대 문자로 동일한 번역. 일부 문자열(브랜드 이름, URL)은 합법적으로 동일하지만, 높은 수는 보통 번역되지 않은 콘텐츠를 의미합니다.
- 과도한 길이: 소스보다 상당히 긴 번역으로 UI 컨테이너를 넘칠 수 있습니다. 독일어 번역은 영어보다 30-40% 더 길다고 알려져 있습니다.
3. 구조: 번역 파일이 깔끔한가요?
구조 검사는 번역 파일의 구성 및 위생을 평가합니다:
- 고아 키: 번역 파일에 존재하지만 소스 코드에서 참조되지 않는 키. 기능이 제거되어도 번역 파일이 정리되지 않을 때 누적됩니다. 번역가의 노력을 낭비하고 혼란을 야기합니다.
- 중복 키: 단일 파일에서 동일한 키가 두 번 정의됩니다. JSON은 중복 키에 오류를 발생시키지 않습니다 — 마지막 것을 자동으로 사용하여 혼란스러운 동작을 초래할 수 있습니다.
- 명명 불일치: 키의 90%가
snake_case를 사용하지만 일부가camelCase를 사용하는 경우, 불일치로 인해 키를 찾고 유지하기가 더 어려워집니다.
4. 코드: 문자열이 올바르게 국제화되어 있나요?
코드 분석은 AST 파싱을 사용하여 번역 함수로 래핑되어야 하는 소스 파일의 하드코딩된 문자열을 찾습니다.
// 표시됨: 하드코딩된 사용자 대면 문자열
<h1>우리 앱에 오신 것을 환영합니다</h1>
// 표시 안 됨: 올바르게 국제화됨
<h1>{t('home.welcome_title')}</h1>
// 표시 안 됨: 사용자 대면이 아님 (CSS 클래스, 데이터 속성)
<div className="container" data-testid="home">
이 검사는 소스에서 i18n 부채를 잡습니다. i18n 설정에 익숙하지 않은 새 개발자들은 하드코딩된 문자열을 작성합니다. 자동화된 검사 없이 이러한 문자열은 번역 감사 중에 누군가가 발견할 때까지 지속됩니다.
상태 점수: 복잡성을 숫자로 줄이기
개별 검사는 상세한 보고서를 생성하지만, CI 통합 및 추세 추적을 위해서는 단일 숫자가 필요합니다: 상태 점수.
잘 설계된 상태 점수는 사용자 영향에 따라 카테고리에 가중치를 부여합니다:
| 카테고리 | 가중치 | 근거 |
|---|---|---|
| 커버리지 | 40% | 누락된 번역은 사용자에게 직접 영향을 미침 |
| 품질 | 30% | 플레이스홀더 버그는 런타임 오류 발생 |
| 구조 | 20% | 고아 키는 노력을 낭비하지만 UX를 깨지 않음 |
| 코드 | 10% | 하드코딩된 문자열은 부채이지만 즉각적인 오류는 아님 |
87/100을 받은 프로젝트는 다음과 같이 분류될 수 있습니다:
전체: 87/100 통과 커버리지 92/100 ████████████████████ 3개 누락 키 품질 85/100 █████████████████░░░ 2개 플레이스홀더 불일치 구조 78/100 ███████████████░░░░░ 12개 고아 키 코드 90/100 ██████████████████░░ 4개 하드코딩된 문자열
통과/실패 임계값은 구성 가능합니다. 80의 임계값은 대부분의 팀에게 실용적입니다 — 실제 문제를 잡을 만큼 엄격하지만, 사소한 경고가 배포를 차단하지 않을 만큼 관대합니다.
CI/CD에서 i18n 상태 검사 설정
상태 검사의 실제 가치는 모든 풀 리퀘스트에서 자동으로 실행하는 것에서 옵니다. GitHub Actions 워크플로우를 설정하는 방법입니다:
# .github/workflows/i18n-doctor.yml
name: i18n 상태 검사
on:
pull_request:
paths:
- "locales/**"
- "src/**"
- "messages/**"
jobs:
doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Bun 설정
uses: oven-sh/setup-bun@v2
- name: 의존성 설치
run: bun install
- name: i18n Doctor 실행
run: bunx @better-i18n/cli doctor --ci --threshold 80
env:
BETTER_I18N_API_KEY: ${{ secrets.BETTER_I18N_API_KEY }}
--ci 플래그가 하는 것
--ci 플래그는 CI 환경을 위한 Doctor의 동작을 변경합니다:
- 종료 코드: 스캔이 실패하면 종료 코드 1을 반환하여 GitHub Actions 작업을 실패시킵니다
- GitHub 주석: GitHub Actions 주석 형식으로 문제를 출력하여 PR diff에 인라인 주석으로 나타납니다
- 요약: GitHub Actions 검사 출력을 위한 구조화된 요약을 생성합니다
- 비대화형: 진행 표시줄과 색상 출력을 억제합니다
경로 필터가 중요합니다
워크플로우 구성의 paths 필터는 성능에 중요합니다. 없으면 번역 영향 없이 문서나 백엔드 코드만 변경하는 PR을 포함하여 모든 PR에서 상태 검사가 실행됩니다. 번역 파일 디렉토리와 소스 코드 디렉토리로 필터링하십시오.
플랫폼에 결과 보고
번역 관리 플랫폼에 결과를 제출하려면 --report 플래그를 추가하십시오:
- name: i18n Doctor 실행
run: bunx @better-i18n/cli doctor --ci --report --threshold 80
env:
BETTER_I18N_API_KEY: ${{ secrets.BETTER_I18N_API_KEY }}
보고서에는 커밋 SHA, 브랜치 이름, 파일 수, 키 수가 포함됩니다. 시간이 지남에 따라 개선을 추적하고, 회귀를 잡고, 팀 목표를 설정하는 데 사용할 수 있는 i18n 상태 기록을 구축합니다.
자동 생성된 워크플로우
GitHub Actions를 수동으로 구성하는 것이 불필요한 마찰처럼 느껴진다면, 일부 번역 플랫폼(Better i18n 포함)은 워크플로우 파일을 만들어드릴 수 있습니다. 플랫폼은 GitHub API를 사용하여 사전 구성된 워크플로우 파일이 있는 리포지토리에 PR을 엽니다. 검토하고, 병합하면 상태 검사가 활성화됩니다.
검사가 실패하면 어떻게 되나요
실패한 상태 검사는 단순히 빨간 X가 아니라 실행 가능한 정보를 제공해야 합니다. 유용한 실패의 모습입니다:
누락된 번역 키
오류: 대상 언어에서 12개 키 누락
checkout.confirm_order
누락됨: fr, de, ja, ko
추가된 커밋: abc1234 (2일 전)
파일: src/pages/Checkout.tsx:45
checkout.payment_method
누락됨: fr, de, ja, ko
추가된 커밋: abc1234 (2일 전)
파일: src/pages/Checkout.tsx:52
개발자는 어떤 키가 누락되었는지, 어떤 언어에서, 언제 추가되었는지, 어디서 사용되는지를 정확히 봅니다. 해결책이 명확합니다: 병합 전에 이 키에 대한 번역을 요청하십시오.
플레이스홀더 불일치
오류: notifications.new_messages (de)에서 플레이스홀더 불일치
소스: "You have {count} new messages from {sender}"
대상: "Sie haben {anzahl} neue Nachrichten von {sender}"
누락: {count}
추가: {anzahl}
개발자 또는 번역가는 정확한 불일치를 보고 {anzahl} 대신 {count}를 사용하도록 독일어 번역을 수정할 수 있습니다.
하드코딩된 문자열
경고: JSX에서 하드코딩된 문자열 (src/components/Header.tsx:23)
<h1>환영합니다!</h1>
제안: <h1>{t('header.welcome_back')}</h1>
이것은 오류가 아닌 경고입니다 — 기본적으로 PR을 차단하지 않습니다. 하지만 보고서에 나타나고 코드 분석 점수에 기여합니다.
실제 영향: 전후 비교
이전: 수동 프로세스
- 개발자가 30개의 새 키로 새 기능 추가
- 개발자가 영어 번역 추가
- 개발자가 PR 열고, 검토 및 병합됨
- 2주 후 QA가 프랑스어로 기능 테스트 — 30개의 원시 키 발견
- QA가 버그 리포트 제출
- 개발자가 번역 티켓 생성
- 번역가가 프랑스어 번역 제공
- 개발자가 번역 파일 커밋, 새 PR 열기
- 독일어, 일본어, 한국어 등에 대해 반복
코드 병합에서 완전히 번역된 기능까지의 시간: 3-6주.
이후: 자동화된 상태 검사
- 개발자가 30개의 새 키로 새 기능 추가
- 개발자가 영어 번역 추가
- 개발자가 PR 열기
- CI가 i18n Doctor 실행 — "fr, de, ja, ko에서 30개 키 누락"으로 실패
- 개발자가 플랫폼을 통해 번역 요청
- 번역 도착 (AI 생성은 몇 분, 사람 검토는 몇 시간)
- 개발자가 PR에 번역 추가
- CI 재실행 — 통과
- 모든 언어가 완성된 PR 병합
코드 병합에서 완전히 번역된 기능까지의 시간: 당일.
차이는 단순히 속도가 아닙니다 — 올바른 장소에서 문제를 잡는 것에 관한 것입니다. CI 검사는 키가 추가된 동일한 PR에서 누락된 번역을 잡으므로, 개발자가 여전히 기능에 대한 완전한 컨텍스트를 가지고 있을 때입니다. 3주 후의 버그 리포트는 개발자가 이미 넘어간 기능으로 다시 컨텍스트를 전환해야 합니다.
시간에 걸쳐 상태 추적
단일 상태 점수는 통과/실패 게이팅에 유용합니다. 상태 점수의 기록은 추세를 이해하는 데 유용합니다.
Doctor 보고서를 플랫폼 대시보드에 제출하면 다음을 추적할 수 있습니다:
- 점수 추이: i18n 상태가 개선, 안정 또는 저하되고 있나요?
- 카테고리 추세: 커버리지는 훌륭하지만 고아 키가 누적되고 있을 수 있습니다. 카테고리 분류는 정리 노력을 어디에 집중할지 보여줍니다.
- 브랜치별 비교: 기능 브랜치는 종종 낮은 점수를 가집니다 (번역이 없는 새 키). 메인 브랜치는 지속적으로 높은 점수를 유지해야 합니다.
- 프로젝트 간 비교: 여러 제품을 가진 조직의 경우, 프로젝트 전반에 걸쳐 i18n 상태를 비교하여 어느 것이 주의가 필요한지 파악합니다.
팀 목표 설정
상태 점수는 측정 가능한 i18n 목표를 설정할 수 있게 합니다:
- "메인 브랜치에서 90+ 상태 점수 유지" — 품질 기준
- "분기 말까지 고아 키를 200개에서 50개로 줄이기" — 정리 이니셔티브
- "플레이스홀더 불일치 제로" — 가장 중요한 검사에 대한 제로 결함 목표
일반적인 반론 및 답변
"두 가지 언어밖에 없어서 필요 없습니다." 두 가지 언어는 커버리지 격차와 플레이스홀더 불일치가 사용자에게 보이는 버그를 일으키기에 충분합니다. 상태 검사는 가볍습니다 — CI 파이프라인에 분이 아닌 초를 추가합니다.
"번역가들이 품질을 처리합니다." 번역가는 언어적 품질을 보장합니다. 상태 검사는 기술적 품질을 보장합니다 — 플레이스홀더 정확성, 키 커버리지, 파일 구조. 이것들은 서로 다른 관심사입니다. 번역가는 키가 소스 코드에서 참조되는지 알 수 없습니다.
"더 많은 언어가 생기면 나중에 추가하겠습니다." i18n 부채는 복잡해집니다. 두 가지 언어로 축적하는 고아 키, 하드코딩된 문자열, 불일치 명명은 세 번째, 네 번째, 다섯 번째 언어를 추가할 때 훨씬 수정하기 어려워집니다. 초기에 상태 검사를 시작하는 것이 소급 적용하는 것보다 저렴합니다.
"CI가 이미 느립니다."
8개 언어가 있는 10,000개 키 프로젝트의 Doctor 스캔은 10초 미만이 소요됩니다. --skip-code를 사용하여 AST 분석을 제거하고 3초 미만으로 줄이십시오. GitHub Actions 구성의 경로 필터는 번역 관련 파일을 건드리는 PR에서만 검사가 실행되도록 합니다.
시작하기
Better i18n을 사용 중이라면 Doctor 명령이 CLI에 내장되어 있습니다:
# CLI 설치 bun add -g @better-i18n/cli # 첫 번째 상태 검사 실행 bi18n doctor # 보고와 함께 CI 모드로 실행 bi18n doctor --ci --report --threshold 80
Better i18n을 사용하지 않는다면, 이 글의 원칙은 모든 번역 설정에 적용됩니다. 다음을 수행하는 사용자 지정 스크립트로 유사한 검사를 구축할 수 있습니다:
- 번역 키 참조를 위해 소스 코드 파싱
- 참조된 키를 번역 파일과 비교
- 플레이스홀더 일관성 유효성 검사
- CI 시스템의 주석 형식으로 결과 출력
중요한 것은 어떤 도구를 사용하느냐가 아닙니다. 누락된 번역이 사용자가 발견하는 놀라움이 아니라 개발자가 발견하는 CI 검사가 되는 것입니다.
누락된 번역은 예방 가능한 버그입니다. CI에서 잡기 시작하십시오 — Better i18n Doctor 설정하고 오늘 첫 번째 상태 검사를 실행하십시오.