함정¶
이 사이트는 서른다섯 개 언어로 번역되어 있고, 그 전부가 이 문서가 가르치는 루프를 돌려서 만들어졌습니다. 업계 기준으로는 작은 코퍼스이지만, 그것만으로도 i18n을 보기보다 어렵게 만드는 함정 대부분에 걸리기에는 충분했습니다.
아래의 각 절은 실제로 여기서 잘못된 일이고, 그때 그것이 어떻게 보였는지, 그리고 라이브러리가 대신 검사해 주는 것과 끝까지 당신의 판단으로 남는 것 사이의 경계가 어디에 있는지입니다.
변수 이름을 바꾸면 문장이 다시 번역된다¶
msgid는 카탈로그의 키이고, 보간된 이름은 그 안에 있습니다. 상수 하나를
모듈 스코프로 옮기고 Python 스타일이 요구하는 대로 대문자화한 것 —
author를 AUTHOR로 — 이 Copyright © 2026 {author} · MIT License를
어떤 카탈로그도 본 적 없는 메시지로 바꿔 놓았습니다. 독자가 볼 수 있는
것은 아무것도 바꾸지 않은 이름 변경 하나 때문에, 그 줄의 모든 번역이 모든
언어에서 fuzzy 주기를 다시 거칠 뻔했습니다.
라이브러리는 이를 막지 않습니다. 두 철자 모두 유효한 플레이스홀더 이름입니다. 라이브러리가 하는 일은 그 이름을 지킬 가치가 있게 만드는 것입니다 — 보간은 평범한 이름이어야 하므로, 카탈로그 키 안에 들어 있는 것은 번역자가 읽을 수 있는 단어이지 식이 아닙니다.
거울상인 경우는 구조적으로 안전합니다. 변환과 서식 지정은 msgid의 일부가
아니므로, {amount:,.2f}를 {amount:,.0f}로 조여도 어떤 키도 바뀌지 않고
어디의 어떤 번역도 무효가 되지 않습니다.
nplurals=2는 서로 다른 문자열 두 개를 뜻하지 않는다¶
터키어, 헝가리어, 페르시아어, 벵골어는 모두 복수형을 둘로 선언하는데, 네
언어 모두에서 수를 세는 메시지의 두 형태가 정당하게 같은 문자열입니다 —
수사 뒤에서 명사가 단수형을 유지하므로 {n} sayfa는 한 페이지에도 열
페이지에도 맞습니다. 이 중복을 "고쳐" 주는 검토자는 번역을 망가뜨립니다.
반대 실수도 그만큼 쉽습니다. 라트비아어의 세 번째 형태는 0 전용으로
존재하고, 슬로베니아어의 두 번째는 정확히 둘을 위한 양수(dual)이며,
루마니아어의 마지막 형태는 앞의 두 형태에는 있어서는 안 되는 단어 de를
요구합니다. 이 자리들을 단수와 복수로 채우면 아무도 시험하지 않는
수에서만 틀린 카탈로그가 만들어집니다.
더 나쁜 것은, 자리의 순서가 의미론적이지 않다는 점입니다. 웨일스어는
다섯 형태를 msgstr[0]이 일반형이고 msgstr[1]이 단수형이 되도록
색인합니다. 눈에 보이는 순서대로 채우면 수를 세지 않는 모든 메시지가
찾아가는 자리에 단수형이 놓입니다.
라이브러리는 이 중 어느 것도 떠맡지 않으며, 그것이 요점입니다. 대상 언어의 복수형 규칙은 그 언어 자신의 카탈로그 헤더에 있고, 합집합/교집합 규칙은 번역이 원본보다 형태가 많아도, 적어도 되게 합니다. 라이브러리가 검사하는 것은 언어를 모르고도 검사할 수 있는 유일한 것 — 모든 형태가 필요한 플레이스홀더를 유지하는지 — 뿐입니다.
두 형태가 같은 데에는 이유가 있을 수 있다¶
아일랜드어에는 복수형이 다섯 개 있는데, 이 사이트의 빌드 보고에서는 그중
여럿이 같게 적혀 있습니다. 복사·붙여넣기 실수가 아닙니다. leathanach는
l로 시작하고, 아일랜드어 수사가 일으키는 두 어두 변화 중 어느 것도
l에는 표기되지 않습니다. 그래도 이 형태들은 실제로 일을 합니다 — 어간이
leathanach와 leathanaigh 사이에서 교체되고 열을 넘는 수는 단수형으로
되돌아갑니다 — 다만 "페이지"를 뜻하는 명사로는 그 대비가 드러나지 않을
뿐입니다.
중복된 형태를 수상하다고 표시하는 검사는 무엇이든 올바른 아일랜드어를 걸러 냅니다. 이것을 검토할 수 있는 것은 그 언어를 아는 사람뿐입니다.
한 메시지는 하나의 수에만 일치할 수 있다¶
이 사이트의 빌드 보고는 몇 페이지를 렌더링했고 얼마나 걸렸는지 말합니다.
이를 "Rendered {n} pages in {seconds} seconds"로 쓰면 무해해 보이지만
번역할 수 없습니다. gettext는 하나의 수에서 하나의 형태를 고르고, 그 수는
n입니다. seconds라는 단어는 복수형 장치가 결코 보지 못하는 수에
일치해야 할 것입니다.
해법은 두 번째 수량을 단어가 아니라 단위 기호로 만드는 것이고, 단위 기호
자체도 지역화 대상입니다. 이 사이트의 카탈로그에는 s, с, ث, שנ׳,
mp가 들어 있고, 프랑스어·스페인어·스웨덴어 조판은 영어와 달리 기호 앞에
공백을 요구합니다. 그 어느 것도 라이브러리가 할 일은 아닙니다 — 하지만 한
메시지가 두 번의 일치를 필요로 한다는 것을 알아차리는 일은 할 일이고,
그 유일한 도구는 메시지를 다르게 쓰는 것입니다.
영어 문장을 고치면 외국어 문법을 고치는 것이다¶
홈 페이지에는 "all ten language editions"라고 적혀 있었습니다. 숫자가 자꾸 낡아서 그 숫자를 지운 — 영어로는 한 단어짜리 수정이 — 복수 주어를 단수로 바꿨습니다. 스페인어, 이탈리아어, 포르투갈어, 러시아어, 우크라이나어, 그리스어, 네덜란드어, 히브리어가 모두 동사를 다시 일치시켜야 했고, 몇몇은 분사까지 바꿔야 했습니다.
영어로 읽으면 사소한 원본 수정도 하류에서는 사소하지 않습니다.
pybabel update가 하는 일인 fuzzy 표시가, 각 번역자에게 알아차릴 기회를
주는 장치입니다.
눈에 보이지 않는 차이는 모든 복사·붙여넣기에서 살아남는다¶
가이드는 (nаme)이 들어 있는 진단을 인용하는데, 이는 의도적인
이스케이프입니다. 그것이 가리키는 문자가 어떤 독자도 라틴 문자와 구별할
수 없는 키릴 а이기 때문입니다. 이 사이트의 번역자들은 그 이스케이프를
실제 문자로 바꿔 놓았습니다 — 다섯 번 따로, 다섯 개의 서로 다른
언어에서, 매번 올바르게 보이지만 틀린 페이지를 만들면서.
이것은 라이브러리가 실제로 잡아내며, 진단이 지금의 모양인 이유이기도 합니다. 글자가 문자 체계를 섞은 플레이스홀더는 두 번 보고됩니다. 한 번은 읽을 수 있게, 한 번은 이스케이프해서 — 이스케이프한 형태만이 둘을 구별하는 표기이기 때문입니다. 중괄호 안의 줄바꿈 없는 공백도 같은 이유로 코드 포인트로 출력됩니다. 카탈로그 검사기는 그 메시지가 배포되기 전에 거부합니다.
비어 있지 않다는 것이 번역되었다는 뜻은 아니다¶
msgid를 msgstr에 그대로 복사해 넣어 스캐폴딩한 카탈로그는 순진한 검사를 모두 통과합니다. 비어 있는 것도 없고, fuzzy도 없고, 메시지 집합도 정확히 일치합니다. 이 사이트의 한 언어판이 몇 시간 동안 그 상태로 배포되었습니다. 영어 원본과 바이트까지 똑같은 복사본이던 다른 언어판의 여덟 페이지도 마찬가지였습니다 — 둘 사이의 코드 블록을 비교하는 검사는 통과합니다. 같은 파일이니까요.
둘 다 번역 라이브러리가 볼 수 있는 것이 아닙니다. 시험하기는 쉽지만,
모든 항목이 원본과 달라야 한다고 요구하는 방식은 곤란합니다. OK,
제품명, 사람 이름, 약어, 코드 식별자는 모두 자기 자신으로 번역되며,
그것을 금지하는 검사는 영원히 거짓 양성을 냅니다.
대신 카탈로그 전체나 페이지 전체에 대해 비율을 재고, 그 바깥으로 튀는 것만 사람에게 보내세요. 이 사이트의 테스트가 바로 그렇게 합니다 — 각 언어판의 산문 줄을 영어 원본과 비교해서 동일한 비율이 25%를 넘으면 실패합니다. 가짜 언어판은 87%였고, 진짜 번역은 모두 4%에서 8% 사이입니다. URL이나 인용된 프로그램 출력처럼 정당하게 일치하는 줄의 작은 꼬리입니다. 두 집단은 충분히 멀리 떨어져 있어서 임계값이 정밀할 필요가 없습니다.
번역되는 것은 카탈로그만이 아니다¶
여기서 겪은 두 실패는 gettext와 아무 상관이 없었습니다.
제목을 번역하면 거기서 생성되는 앵커가 바뀌므로, 그 절을 가리키는 모든 페이지 간 링크가 깨집니다 — 조용히, 그 언어에서만. 이 사이트는 모든 제목에 영어 앵커를 고정하고, 테스트가 영어 페이지에서 기대 목록을 도출합니다.
그리고 사이트 생성기는 예순여덟 개 언어의 인터페이스 번역을 함께 배포하는데, 거기에 스와힐리어와 아일랜드어는 없습니다. 없으면 빌드는 영어로 격하되지 않습니다. 템플릿 include가 실패하고 그 언어판은 아예 빌드할 수 없습니다. 이 저장소 자체의 파일 두 개가 그 구멍을 메우려고 존재합니다.
당신의 도구에도 버그가 있다¶
이 문서가 오래된 카탈로그를 잡기 위해 권장하는 CI 단계인
pybabel update --check는, pgettext나 npgettext를 쓰는 프로젝트에서는
그 일을 할 수 없습니다. Babel 2.18.0에서는 msgctxt가 있는 카탈로그를
실행할 때마다 매번 오래되었다고 보고합니다. 비교는 Catalog.is_identical을
거치는데, 이 메서드는 각 메시지를 저장된 키로 조회합니다 — 그리고 컨텍스트가
있는 메시지의 키는 (id, context) 쌍이라, Catalog.get이 받지 못합니다.
조회는 아무것도 반환하지 않고, 카탈로그는 결코 같다고 비교되지 않습니다.
>>> from babel.messages.catalog import Catalog
>>> c = Catalog(locale="ja")
>>> c.add("Guide", "ガイド", context="navigation")
<Message 'Guide' (flags: [])>
>>> c.is_identical(c)
False
여기서 실제로 써 보다가 발견해 상류에 보고했으며, 대체 검사는 프로덕션 페이지에 있습니다.
일반적인 교훈은 불편한 쪽입니다. 항상 빨간 게이트는 게이트가 없는 것보다 나쁩니다. 팀이 꺼 버리기 때문입니다. CI 검사가 실패를 알려 주리라 믿기 전에, 그 검사가 실제로 통과할 수 있는지 확인하세요.
이 라이브러리가 무엇을 위한 것인지, 한 줄로¶
이 페이지의 대부분은 어떤 도구도 대신할 수 없는 판단입니다. 도구가 할 수 있는 일은, 번역이 자신이 번역하는 문장의 구조를 바꿀 수 없다고 — 값을 빠뜨리거나, 없던 값을 만들어 내거나, 다시 서식하거나, 당신의 객체에 손을 뻗을 수 없다고 — 보장하고, 그 사실을 고쳐야 할 사람이 바로 행동으로 옮길 수 있는 문장으로 말해 주는 것입니다. 그것이 이 라이브러리가 약속하는 전부이며, 이 사이트의 나머지는 그 약속을 지키는 방법입니다.