번역문은 자연스럽지만 {name}이 {user}로 바뀌어 앱에 이름이 나오지 않습니다. 같은 변수가 두 번 필요한데 한 번 빠지거나 중괄호가 닫히지 않는 문제도 문장만 읽으면 놓치기 쉽습니다. 날짜 문자열을 지키는 번역 검수와 달리 여기서는 런타임이 치환할 자리표시자의 이름·횟수·문법을 별도 계약으로 다룹니다. 제한 문법 여섯 결과를 실제로 검사하되 번역의 의미나 문체를 자동 평가하지는 않습니다.
번역 전에 문장이 아니라 메시지 형식을 전달합니다.
원문은 “{name}님, {count}개 파일을 {date}에 보냈습니다. {name}님 확인해 주세요.”입니다. 이름 변수는 두 번, 개수와 날짜는 한 번씩 나옵니다. 번역자가 이름을 일반 단어라고 생각해 바꾸거나 반복을 줄이면 화면 치환과 다른 결과가 됩니다. 변수 목록과 각 값의 뜻, 바꿔도 되는 문맥을 함께 제공해야 합니다.
변수 이름을 보존하라는 요구는 원문 어순을 그대로 유지하라는 뜻은 아닙니다. 언어에 맞게 문장 순서를 바꾸되 치환 계약이 유지되는지 봅니다. 이번 실험에서는 반복 횟수까지 의미 있다고 정했습니다. 실제 제품에서는 재구성이 허용될 수 있으므로 그 경우의 승인 기준을 별도로 적어야 합니다. 한 제품의 규칙을 모든 번역문에 기계적으로 적용하지 않습니다.
이번 파서는 단순한 ASCII 이름 필드만 받습니다.
지원하는 형태는 {name}, {count}, {date} 같은 이름 필드입니다. 실제 중괄호를 글자로 쓰는 경우에는 {{와 }}를 이스케이프로 정했습니다. 숫자 위치 인자, 점으로 접근하는 필드, !r 변환, : 형식 지정, ICU 복수형·select·중첩 문법은 지원하지 않습니다. 같은 중괄호를 쓴다고 같은 메시지 언어인 것은 아닙니다.
코드는 왼쪽부터 읽으며 필드 밖의 이중 중괄호만 일반 문자로 소비합니다. 필드 안은 ASCII 이름 문법에 맞는지 확인하고 토큰을 Counter로 비교합니다. 중괄호를 먼저 지워 떨어진 글자를 새 이름으로 합치지 않습니다. 정규식 하나가 Python format이나 ICU의 모든 문법을 안전하게 이해하는 것처럼 소개하지 않습니다. 실제 서비스에서는 사용하는 메시지 엔진의 파서·빌드 검사·런타임 테스트를 우선 사용해야 합니다.
정상 번역은 순서가 달라도 이름과 횟수가 같습니다.
첫 번역은 “Hello {name}, sent {count} files on {date}. Please check, {name}.”입니다. 문장은 영어식으로 바뀌었지만 변수 멀티셋은 name 2, count 1, date 1로 같습니다. 미지원 중괄호도 없어 contractPass가 참입니다. 번역이 자연스럽다는 점수를 매긴 결과는 아닙니다.
변수만 맞았다고 의미 검수가 끝나는 것도 아닙니다. 발송했다는 문장이 취소했다는 뜻으로 뒤집혀도 이 검사는 통과할 수 있습니다. 누가 무엇을 언제 했는지, 부정과 조건이 유지되는지는 사람이 원문과 대조해야 합니다. 자동 구조 검사는 사람이 놓치기 쉬운 치환 오류를 먼저 찾는 한 층입니다.
이름 변경과 반복 누락은 서로 다른 실패입니다.
둘째 결과는 {name} 두 개를 {user}로 바꿨습니다. 필요한 name이 없어지고 모르는 user가 생겼으므로 실패합니다. 셋째는 마지막 {name}을 삭제해 고유한 이름 목록은 같지만 횟수가 다릅니다. 단순 set 비교였다면 반복 누락을 발견하지 못했을 것입니다.
실패 메시지는 이름이 다르다는 사실과 몇 번 필요한지를 함께 보여 주는 편이 좋습니다. “변수 오류” 한 줄만 남기면 번역자는 무엇을 복구해야 하는지 다시 찾게 됩니다. 원문과 번역의 토큰 목록, 누락·추가·횟수 차이를 보여 주되 치환되는 실제 고객 이름이나 주문 정보를 검사 로그에 넣지 않도록 합니다.
입력별 결과를 편집 판단에 연결하기
다음 표의 결과는 제공 실행 파일에서 확인한 값이며, 해석과 다음 확인은 그 결과를 이용하는 편집 예시입니다. 추가 시험을 수행한 결과로 세지 않습니다.
| 입력·관찰 | 실행 결과 또는 선택 | 판단 근거 | 다음 확인 |
|---|---|---|---|
| valid | name 2회·count 1회·date 1회로 계약이 같습니다. | 어순은 달라도 변수 멀티셋을 유지했습니다. 구조 통과와 번역 의미의 동등성은 별도 판정입니다. | 실제 치환값을 넣은 제품 화면은 다음 단계에서 확인합니다. |
| renamed | name이 사라지고 user 2회가 생깁니다. | 원문 변수 이름을 자연어처럼 번역한 실패입니다. 번역문만 보면 같은 사람을 부르는 뜻이어도 호출 코드가 user 값을 제공하지 않을 수 있습니다. | 변수명을 복구하고 번역 파일의 다른 키에서 같은 변경이 반복됐는지 찾습니다. |
| lost-repeat | 고유 이름은 같지만 name이 1회뿐입니다. | 단순 set으로 비교하면 이 사례를 놓칩니다. 이번 계약에서는 두 번째 호명이 필요하다고 정했으므로 횟수 차이를 실패로 기록했습니다. | 의도적인 문장 재구성이면 번역자·제품 담당자가 보존 정책을 다시 정하고 예제를 갱신합니다. |
| syntax | 필드를 순서대로 읽었을 때 잘못된 중괄호가 발견됩니다. | 다른 변수들이 정상으로 보여도 문장 전체의 제한 문법은 통과하지 못합니다. 토큰 몇 개를 찾았다는 결과를 실행 가능성으로 바꾸지 않습니다. | 실제 엔진의 오류 위치를 확인하고 수정 후 같은 입력과 경계 예제를 다시 실행합니다. |
| nested-escape-in-field | 두 name 자리를 {na{{me}로 바꾼 입력은 거부합니다. | 중괄호 쌍을 먼저 지우면 떨어진 na와 me가 합쳐져 정상 이름처럼 보일 수 있습니다. 필드 안의 중첩은 지원하지 않고 오류 위치를 남깁니다. | 문자열 삭제로 새 토큰을 만들어 내지 않는지 회귀 예제로 유지합니다. |
| escaped-literal-valid | 필드 밖에 Literal {{label}}을 추가해도 정상입니다. | 일반 글자인 label을 새로운 변수로 세지 않습니다. name 2회와 count·date 각1회가 유지된 것을 확인합니다. | 실제 일반 중괄호와 치환 필드를 구분하는 정책을 사용하는 메시지 엔진과 맞춥니다. |
중괄호가 깨진 문장은 이름 목록만으로 통과시키지 않습니다.
넷째 결과는 Hello {name,처럼 이름 필드의 닫는 중괄호가 빠졌습니다. 실제 파서는 name의 시작부터 count 뒤의 닫는 중괄호까지를 한 필드로 읽어 거부했고, 정상 토큰은 date 한 개만 남았습니다. count까지 정상 추출됐다고 해석하면 결과 JSON과 맞지 않습니다. 토큰을 일부 찾았다는 사실과 메시지가 실행 가능한 형식이라는 사실은 다릅니다. 출력 JSON에는 inspection과 contractPass를 나눠 두었습니다.
괄호 수만 같아도 올바른 중첩을 보장할 수 없습니다. 또 어떤 시스템은 작은따옴표와 중괄호의 이스케이프 규칙이 다릅니다. 잘못된 입력을 자동 보정해 버리면 번역자가 의도한 literal 문자를 지울 수 있으므로 기본 동작은 오류 위치와 지원 범위를 보여 주고 원문을 보존하는 쪽으로 정하는 것이 좋습니다.
복수형은 count라는 이름이 있다는 것보다 복잡합니다.
개수가 1일 때와 여러 개일 때 문장이 달라지는 언어가 있습니다. ICU 메시지는 plural과 select를 이용해 다른 문장을 고를 수 있고, gettext의 ngettext도 복수형 선택을 다룹니다. count라는 토큰이 남아 있다는 사실만으로 각 분기의 문장이 올바르게 존재한다고 볼 수는 없습니다.
실제 제품에서는 0·1·여러 개, 긴 이름, 날짜 형식, 숫자 형식 같은 대표 값을 넣어 메시지 엔진으로 렌더링합니다. 그다음 UI에서 잘림과 줄바꿈, 버튼의 의미를 확인합니다. 이번 실험은 치환을 실행하지 않은 제한 토큰 비교이므로 이 분기와 화면 문제를 통과한 테스트로 합산하지 않습니다.
변수를 문장 조각으로 나누면 번역의 선택권이 줄어듭니다.
“보냈습니다” 앞뒤를 서로 다른 문자열로 조립하면 언어마다 어순이나 문법이 맞지 않을 수 있습니다. 번역 단위를 정할 때는 치환될 값과 문장 전체를 함께 보여 줍니다. 단순한 공백 하나나 문장부호도 어떤 조각에 속하는지 계약을 정하지 않으면 두 번 나오거나 사라질 수 있습니다.
용어집에는 변수 이름을 번역하지 않는다는 규칙과 값 자체의 현지화 책임을 적습니다. date 변수에 이미 현지화된 문자열이 들어오는지 메시지 엔진이 날짜 객체를 형식화하는지도 구분하세요. 잘못된 책임 분담은 같은 날짜를 이중 변환하거나 원시 기계 값을 그대로 노출하는 원인이 될 수 있습니다.
번역 인계표에는 메시지 키, 원문 전체, 변수 이름과 뜻, 예제 값, 이스케이프 문법, 반복 보존 정책을 둡니다. 예제 값은 실제 고객 대신 “담당자 01”, 개수 2, 가상 날짜처럼 작성합니다. 긴 이름이나 0개처럼 화면과 문법을 흔드는 값도 실제 렌더링 단계의 후보로 준비하되 이번 결과에서 그 시험을 했다고 적지는 않습니다.
검수 결과는 구조·의미·화면 세 단계로 인계합니다.
첫 단계에서 변수 이름·횟수·지원 문법을 검사합니다. 둘째에서 번역자가 의미·부정·조건·용어를 원문과 대조합니다. 셋째에서 개발자가 실제 메시지 엔진과 UI에 대표 값을 넣어 확인합니다. 어느 단계가 끝났는지 기록해야 “검사 통과”라는 말이 다른 사람에게 과한 보증으로 전달되지 않습니다.
수정한 번역문은 해당 언어만 다시 보는 데서 끝내지 말고 공통 키와 다른 언어 파일이 영향을 받았는지 확인합니다. 변수 이름을 제품에서 실제로 바꾸는 작업이라면 원문과 번역 파일, 호출 코드의 버전을 함께 전환해야 합니다. 번역자가 한 언어에서만 임의로 이름을 바꾸는 일과는 다른 변경입니다.
같은 키의 원문이 수정되면 번역 파일의 변수 계약도 다시 비교해야 합니다. count가 total로 바뀌었는데 기존 번역을 그대로 두면 문장이 자연스러워도 화면에는 빈 칸이 나올 수 있습니다. 변경 이력에 변수 추가·삭제·이름 변경을 별도 표시하고, 단순 문체 수정과 다른 검수 경로로 보냅니다. 원문 변경이 번역 품질에 미치는 영향은 사람의 언어 검토가 필요합니다.
여섯 결과가 보여 주는 것과 보여 주지 않는 것
정상과 이스케이프된 일반 괄호를 추가한 정상 2개는 통과했고 이름 변경·반복 누락·닫는 괄호 누락·필드 안 이스케이프 혼입 4개는 실패했습니다. 입력과 결과, 기대 판정을 제공 파일에서 볼 수 있습니다. Python 표준 라이브러리로 실행하며 여섯 결과의 contractPass 배열을 assert로 대조했습니다. 번역 품질·사용자 이해·앱의 실제 치환 결과는 평가하지 않았습니다.
자리표시자 검수는 번역을 기계적으로 만들기 위한 규칙이 아닙니다. 자연스러운 문장을 선택할 여지를 남기면서 프로그램이 요구하는 값을 잃지 않도록 경계를 정하는 일입니다. 문법 종류와 보존 정책을 명확히 전달하면 정답처럼 보이는 번역문이 실제 화면에서 깨지는 문제를 더 구체적으로 검토할 수 있습니다.
한편 반복을 줄이는 것이 더 자연스러운 언어도 있습니다. 실제 메시지 엔진과 문맥에서 반복 횟수가 달라도 동등한 의미를 유지할 수 있다면 정책을 바꿀 수 있습니다. 그 경우 이 예제의 Counter 완전 일치를 절대 규칙으로 강요하지 않습니다. 어떤 것을 지킬지 먼저 명시하고 그 계약에 맞는 실패 입력을 만드는 것이 목적입니다. 언어를 획일화하는 규칙이 아니라 프로그램과 편집 사이의 합의로 사용하세요.
이 글의 입력·실행 자료
합성 자료의 실제 검증일은 2026-10-15입니다. 글의 예약 발행일과 구분합니다. 코드의 fixtureCreatedOn은 예제 작성일이고 executedAt은 재실행할 때의 실제 UTC 시각입니다. Python 3 표준 라이브러리를 사용합니다. 실제 고객정보나 새 Gemini 응답을 입력하지 않았습니다.
python3 translation-placeholder-contract-lab.py확인한 공식 원문
2026-10-15에 열어 확인했습니다. 제공 예제의 규칙과 공식 문서의 일반 기능 설명을 구분해 읽습니다.



