가이드¶
이 페이지는 런타임 레퍼런스입니다. 카탈로그가 준비된 뒤 애플리케이션 코드가 이 라이브러리로 하는 모든 일을 다룹니다. 표시, 추출, 번역, 컴파일, 실행이라는 전체 루프를 아직 본 적이 없다면 튜토리얼이 5분 만에 한 바퀴 안내합니다. 카탈로그 생성과 검증은 추출에서 다루며, 팀이 루프를 계속 돌리는 방법 — 업데이트 주기, CI, 번역 플랫폼 — 은 프로덕션에서가 다룹니다.
어떤 진입점을 써야 하나요?¶
애플리케이션이 언어를 바인딩하는 방식이 여럿이므로, 이 패키지도 메시지를 번역하는 방법을 여럿 내보냅니다. 프로그램이 자신이 어떤 언어인지 정하는 방식에 따라 고르세요.
| 여러분의 상황 | 사용할 것 |
|---|---|
| 프로세스 전체가 한 언어 — CLI, 데스크톱 앱, 스크립트 | Translator를 _로 호출 |
| 요청마다 또는 async 태스크마다 한 언어 — 웹 애플리케이션 | 작업을 use_translations()로 감싸고 tr() |
| import 시점에 정의되는 메시지 — 폼 레이블, enum, 상수 | lazy_gettext() 또는 lazy_pgettext() |
| 개수가 표현을 결정 | 위의 어느 형태로든 ngettext() / npgettext() |
| 카탈로그 없이 패턴만 렌더링 | compile_template() |
아래 내용은 이 다섯 가지를 그 순서대로 다룹니다.
카탈로그 바인딩¶
권장 방식은 gettext의 객체 사용법과 같습니다. 표준 번역 객체를 한 번
바인딩하고 호출 가능한 프로세서를 _로 사용합니다.
import gettext
from gettext_tstrings import Translator
translations = gettext.translation("messages", localedir="locales", languages=["ja"])
_ = Translator(translations)
name = "Ada"
print(_(t"Hello {name}")) # こんにちは Ada
n = 3
print(_.ngettext(t"One file", t"{n} files", n)) # picks the right plural form for n
filename = "report.txt"
print(_.pgettext("button", t"Open {filename}")) # "button" disambiguates homonyms
모듈 함수는 표준 라이브러리의 이름과 위치 인수를 따릅니다.
from gettext_tstrings import gettext, ngettext, npgettext, pgettext
gettext(t"Hello {name}", translations=translations)
ngettext(t"One file", t"{n} files", n, translations=translations)
pgettext("button", t"Open {filename}", translations=translations)
npgettext("inbox", t"One message", t"{n} messages", n, translations=translations)
tr, ntr은 각각 gettext, ngettext의 정확한 별칭입니다.
요청별 언어¶
웹 프레임워크는 요청마다 언어를 고릅니다. 번역을 현재 컨텍스트에 바인딩하면 동시 요청에서도 모든 모듈 호출이 각 요청의 언어를 사용합니다.
from gettext_tstrings import tr, use_translations
def handle(request):
name = request.user.display_name
translations = load_translations(request.locale)
with use_translations(translations):
return render(tr(t"Hello {name}"))
프레임워크가 생명주기를 관리한다면 set_translations()로 블록 없이
바인딩하고 get_translations()로 읽습니다. 명시한 translations=가 항상
우선합니다. 바인딩이 없으면 표준 라이브러리의 전역 gettext 함수가
fallback입니다. Flask와 ASGI 미들웨어의 실전 예제는
프로덕션에서 페이지에
있습니다.
지연 번역¶
t-string은 값을 즉시 캡처합니다. import 때 정의한 레이블, enum, 상수를 실제 사용 시점의 활성 언어로 렌더링하려면 지연 문자열을 사용하세요.
from gettext_tstrings import lazy_gettext, lazy_pgettext, use_translations
SAVE = lazy_gettext(t"Save changes") # defined once, at import
OPEN = lazy_pgettext("button", t"Open file")
with use_translations(japanese):
assert str(SAVE) == "変更を保存" # rendered here, in this language
LazyString은 str(), format(), f-string으로 렌더링되고 텍스트와
비교됩니다.
의도적으로 해시할 수 없음
텍스트가 언어에 따라 바뀝니다. 변하는 해시는 set이나 dict를 조용히
손상시킵니다. 키가 필요하면 먼저 str()을 호출하세요.
strict는 렌더링되는 곳이 아니라 메시지를 작성하는 곳에서 결정합니다.
지연 문자열은 최종적으로 쓰이는 곳 — 템플릿 안, 폼, 로그 한 줄 — 에서
렌더링되는데, 그곳은 지금이 테스트 실행인지 프로덕션인지 알지 못하는
경우가 대부분입니다. 정의 시점에 strict=True를 넘기는 것이,
호출 지점에서 렌더링되지 않는 문자열에도
CI에서는 시끄럽게, 프로덕션에서는 관대하게라는
같은 선택을 적용할 수 있게 해 줍니다.
복수형은 런타임의 수에 의존하므로 ngettext로 즉시 렌더링합니다.
여러 언어를 동시에¶
한 요청에 두 가지 이상의 언어가 필요한 경우는 흔합니다. 읽는 사람에게 보여 줄 페이지를 렌더링하면서 다른 언어로 설정된 계정에 보낼 알림을 큐에 넣거나, 참가자마다 각자의 언어로 인용하는 요약을 만드는 경우입니다. 바인딩은 중첩되며, 안쪽 블록을 빠져나오면 바깥쪽 바인딩이 복원됩니다.
with use_translations(reader):
page = tr(t"Hello {name}")
with use_translations(recipient):
notice = tr(t"Hello {name}") # the recipient's language
footer = tr(t"Hello {name}") # the reader's again
수신자 목록을 도는 자리에서는 지연 문자열이 그 일을 대신합니다. 메시지는 import 시점에 한 번만 작성되고, 언어마다 한 번씩 렌더링됩니다.
SUBJECT = lazy_gettext(t"Your order shipped")
for user in users:
with use_translations(load_translations(user.locale)):
send(user.email, str(SUBJECT))
바인딩은 공유 객체에 매달린 스택이 아니라 ContextVar이므로, 겹쳐서
실행되는 요청이 서로의 언어를 집어 올 수 없습니다. 들어간 순서 그대로
블록을 빠져나오는 경우 — 푸시다운 스택이 틀리게 처리하는 바로 그
뒤섞임 — 까지 포함해서 그렇습니다. 언어마다 카탈로그를 로드하는 비용은
저렴합니다. gettext.translation()은 각 .mo를 한 번만 파싱하고, 파싱된
카탈로그를 공유하는 복사본을 건네줍니다.
워커 스레드가 바인딩을 물려받는지는 빌드에 달려 있습니다
맨 threading.Thread나 ThreadPoolExecutor.submit은 호출자 컨텍스트의
복사본에서 시작하거나 빈 컨텍스트에서 시작하며, 둘 중 어느 쪽인지는
sys.flags.thread_inherit_context가 정합니다 — 프리스레드 빌드에서는
기본값이 참이고, 그 밖의 모든 빌드에서는 거짓입니다. 그래서 같은
코드가 3.14t에서는 바인딩된 언어를, 3.14에서는 프로세스 전역
카탈로그를 렌더링합니다. 기본값에 기대지 말고 컨텍스트를 넘기세요.
asyncio.to_thread는 이미 이 일을 대신해 줍니다.
로케일에 맞는 값¶
이 라이브러리는 값이 번역된 메시지의 어디에 나타날지를 결정합니다. 값
자체를 지역화하지는 않습니다. {amount:,.2f}는 동작이 고정된 Python 포맷
스펙이며 — 세 자리마다 쉼표, 소수점 앞에 점 — 메시지가 어떤 언어이든 같은
문자를 만들어 냅니다.
독일어는 그 숫자를 1.234,50, 프랑스어는 1 234,50으로 쓰고, 힌디어는
1234567을 1,234,567이 아니라 12,34,567로 묶습니다. 숫자, 통화, 날짜,
시각, 단위는 Babel의 몫입니다. 값을 먼저 서식화한 다음,
완성된 문자열을 놓으세요.
from babel.numbers import format_currency
total = format_currency(amount, "EUR", locale=locale)
tr(t"Your order comes to {total}")
개수가 있는 메시지에서 숫자는 두 가지 일을 합니다 — 복수형을 고르고, 텍스트에도 나타납니다 — 그런데 지역화되는 것은 두 번째뿐입니다. 선택에는 가공하지 않은 개수를 그대로 쓰고, 표시용으로는 서식화한 문자열을 넘기세요.
from babel.numbers import format_decimal
shown = format_decimal(n, locale=locale)
_.ngettext(t"One file", t"{shown} files", n)
호출 전에 서식화하는 것은 포맷 스펙을 카탈로그 밖에 두는 방법이기도 합니다. 번역자가 보는 것은 숫자와 렌더링 지시가 아니라 완성된 텍스트 조각입니다.
카탈로그가 잘못되었을 때¶
번역의 플레이스홀더가 원본과 맞지 않으면 기본 모드는 예외 대신 원본 메시지를 렌더링합니다. 잘못된 카탈로그가 애플리케이션을 중단하지 않아야 한다는 gettext의 계약을 따릅니다.
Hello {name}의 번역이 こんにちは {nombre}라면 렌더링은 성공하고
gettext_tstrings 로거에 다음 경고가 기록됩니다.
WARNING gettext_tstrings: invalid translation for msgid 'Hello {name}'; using
source text: translation does not match the source placeholders: {name} is
missing; {nombre} is not in the source message
경고는 렌더링마다가 아니라 메시지와 패턴마다 한 번만 기록되므로, 손상된 카탈로그 항목이 로그를 넘치게 하지 않습니다. 테스트와 CI에서는 strict 모드를 켜세요.
strict = Translator(translations, strict=True)
tr(t"Hello {name}", translations=translations, strict=True)
그러면 같은 조회가 예외를 냅니다.
>>> strict(t"Hello {name}")
Traceback (most recent call last):
...
gettext_tstrings.errors.InvalidTranslationError: translation does not match the
source placeholders: {name} is missing; {nombre} is not in the source message
이 메시지들은 그것을 보고 행동할 수 있는 사람을 위해 쓰였고, 카탈로그
문제라면 그 사람은 프로그래머보다 번역자인 경우가 많습니다 — 그래서
플레이스홀더가 있어 보이는데 없을 때, 메시지는 없다는 말을 되풀이하는 대신
왜 그런지 설명합니다. 전각 중괄호, 겹쳐진 {{name}}, 보이지 않는 줄 바꿈
없는 공백, 라틴 문자 사이의 키릴 문자 — 각각에 고유한 문구가 있으며,
예시와 함께 번역자를 위한 안내에
정리되어 있습니다. 그 페이지는 .po를 편집하는 사람에게 그대로 건네도록
쓰였습니다.
카탈로그 없이 패턴 렌더링¶
compile_template은 msgid와 바인딩된 값을 만들고 패턴을 렌더링합니다.
from gettext_tstrings import compile_template
name = "Ada"
compiled = compile_template(t"Hello {name}")
compiled.msgid # "Hello {name}"
compiled.placeholders # ("name",)
compiled.render("こんにちは {name}") # "こんにちは Ada"
render는 같은 규칙으로 검증하며 불일치하면 항상 예외를 냅니다.
카탈로그 조회가 없으므로 fallback도 없습니다.
보안과 범위¶
유효:
의도적으로 거부:
값을 먼저 명시적으로 계산하세요.
번역은 평가되지 않으며 속성 접근, 호출, 변환, 포맷을 추가할 수 없습니다. 일반 gettext와 마찬가지로 애플리케이션이 출력 대상의 이스케이프와 카탈로그 무결성을 책임집니다.