Üretimde¶
Öğretici döngüyü bir kez, tek başına, tek mesajlı bir program üzerinde çalıştırır. Gerçek bir projede döngü dönmeye devam eder: mesajlar çevrildikten sonra değişir, çevirmen başka bir yerde ve kendi takvimiyle çalışır ve her sürümle birlikte derlenmiş bir katalog sevk edilir. Bu sayfa o pratiktir — depoda ne kalır, ne yolculuk eder, CI neyi kapılamak zorundadır ve çalışma zamanı bir dili nerede bağlar.
Hepsi altı denetime çıkıyor; o yüzden önce onlar. Aşağıdaki her bölüm bunlardan birini kurar.
pybabel update --checkgeçiyor — kataloglar haberdar olmadan hiçbir mesaj değişmemiş.pybabel compile, derlemeyi kendi çıkış durumuyla kapılıyor.- Kalan
fuzzygirdiler kasıtlı — her biri, bir çevirmen onaylayana dek kaynak metin olarak render edilir. - Test paketi, sevk edilen her dili
strict=Trueile bir kez render ediyor. - Üretim artefaktı
.modosyalarını içeriyor ve Babel'i içermiyor. gettext_tstringsgünlükçüsü izlemeye yönlendirilmiş.
Bir projenin biçimi¶
myapp/
├── babel.cfg
├── pyproject.toml
├── src/
│ └── myapp/
└── locales/
├── messages.pot
├── ja/LC_MESSAGES/messages.po
└── de/LC_MESSAGES/messages.po
babel.cfg dosyasını, .pot şablonunu ve her .po dosyasını commit'leyin —
bunlar çeviri derlemesinin kaynaklarıdır ve diff'leri, çeviri değişikliklerini
inceleme biçiminizdir. Derlenmiş .mo dosyaları derleme çıktılarıdır: onları
commit'lemek yerine CI'da ya da paketleme anında üretin; böylece bir .po
ile .mosu, neyin sevk edildiği konusunda asla anlaşmazlığa düşemez.
Bir dosyanın her yönde birer rolü vardır: .pot, mesajlarınızı çevirmenlere
götürür; .po dosyaları çevirileri geri getirir. Bu sayfanın geri kalanı,
o ikisi arasında hareket eden şeydir.
flowchart LR
code["kaynak kod<br>t-string çağrı noktaları"] -->|"pybabel extract"| pot["messages.pot"]
pot -->|"pybabel update"| po["dil başına bir .po"]
po --> tr["çevirmen<br>veya platform"]
tr --> po
po -->|"pybabel compile (CI)"| mo[".mo dosyaları"]
mo --> app["uygulama<br>çalışma zamanında"]
İlk çeviriden sonraki çevrim¶
Öğreticideki pybabel init, normalde bir dil eklendiğinde bir kez çalışır.
Ondan sonra çalışma çevrimi çıkar → güncelle → çevir → derle olur ve
merkezinde
pybabel update durur: taze bir şablonu, içlerindeki mevcut çevirileri
atmadan var olan katalogların içine katlar.
Diyelim ki Hello {name} selamı — hâlihazırda こんにちは {name} olarak
çevrilmişken — kodda Welcome back, {name} olarak yeniden yazıldı. Çıkarın
ve güncelleyin:
$ pybabel extract -F babel.cfg -c "Translators:" -o locales/messages.pot .
extracting messages from app.py (encoding="utf-8")
writing PO template file to locales/messages.pot
$ pybabel update -i locales/messages.pot -d locales
updating catalog locales/ja/LC_MESSAGES/messages.po based on locales/messages.pot
Japonca katalog artık şunu içeriyor:
#. gettext-tstrings
#: app.py:4
#, fuzzy, python-brace-format
msgid "Welcome back, {name}"
msgstr "こんにちは {name}"
Babel, yeni msgid'nin kaldırılmış bir msgid'ye benzediğini fark etti ve onu
eski çeviriyle eşleştirdi — ama çifti fuzzy olarak işaretledi: bir insanı
bekleyen bir makine tahmini. Bayrak, neyin derleneceğini değiştirir.
pybabel compile,
fuzzy girdileri .mo dosyasının dışında bırakır; böylece bir çevirmen
çifti onaylayana dek uygulama, bayat bir Japonca metin yerine yeni İngilizce
metni render eder:
$ pybabel compile -d locales
compiling catalog locales/ja/LC_MESSAGES/messages.po to locales/ja/LC_MESSAGES/messages.mo
$ python app.py
Welcome back, Ada
Değişen bir mesaj, dolayısıyla, bozuk bir mesajla aynı biçimde alçalır —
kaynak dile, asla güncelliğini yitirmiş bir çeviriye değil. Çevrimde
çevirmenin payı, msgstrı gözden geçirip fuzzy bayrağını silmektir; bir
sonraki derleme girdiyi alır.
Yer tutucu adları mesajın kimliğinin parçasıdır
msgid katalog anahtarıdır ve yer tutucunun adı onun içindedir — bu
yüzden kodda bir değişkeni yeniden adlandırmak (name → user_name)
msgid'yi değiştirir ve her dilin o mesaj çevirisini fuzzy çevriminden
yeniden geçirir. İnterpolasyona giren değişkenlere bir çevirmenin
anlayacağı sözcüklerle ad verin ve onları yalnızca bir gerekçeyle yeniden
adlandırın.
Biçimlendirme bunun ayna görüntüsüdür: !r ve :.2f
msgid'nin parçası değildir;
dolayısıyla {amount:,.2f} biçimini {amount:,.0f} olarak sıkılaştırmak
hiçbir katalogda hiçbir şeyi değiştirmez. Cümleyi yeniden yazmak ise
elbette gerçek bir değişikliktir — o da yukarıdaki çevrimdir.
CI'ın kapıladığı şeyler¶
Üç başarısızlık kırmızı bir derlemeye değer: kataloglar kodun gerisinde kaldı, bir çeviri bir yer tutucuyu bozdu ya da bozuk bir girdi çalışma zamanına sızdı. Her başarısızlığa bir adım:
- run: pybabel extract -F babel.cfg -c "Translators:" -o locales/messages.pot .
- run: pybabel update -i locales/messages.pot -d locales --check
- run: pybabel compile -d locales
- run: pytest
pybabel update --check hiçbir şeyi yeniden yazmaz ve bir katalog, taze
çıkarılmış şablona göre güncel değilse sıfırdan farklı bir kodla çıkar —
mesajları kimsenin yeniden çıkarmadığı kodun merge edilmesine karşı korkuluk
budur. pybabel compile, hem Babel'in hem de bu paketin
kayıtlı denetleyicisinin
yer tutucu denetimlerini çalıştırır.
Babel 2.18.0: --check, bağlam kullanan bir kataloğu kapılayamaz
Babel 2.18.0'da pybabel update --check, msgctxt içeren her
kataloğu, ne kadar güncel olursa olsun, her çalıştırmada güncel değil
diye raporlar. Sürekli başarısız olan bir kapı, hiç kapı olmamasından
kötüdür; çünkü ekip onu kapatır — bu yüzden pgettext ya da npgettext
kullanıyorsanız, bu adımla yaşamak yerine onu değiştirin. Şablonu ve her
kataloğu babel.messages.pofile.read_po ile okuyup
{(m.context, m.id) for m in catalog if m.id} kümelerini karşılaştırmak
denetimin tamamıdır; bu sitenin kendi derlemesinin yaptığı da
budur. Nedeni
Tuzaklar sayfasında yazılıdır.
Günlüğü değil, çıkış durumunu denetleyin
pybabel compile her yer tutucu hatasını raporlar, sıfırdan farklı bir
kodla çıkar — ve .mo dosyasını yine de yazar. Derleyip sonra
locales/ dizinini bir imaja kopyalayan bir boru hattı, o sıfırdan
farklı çıkış onu gerçekten durdurmadıkça bozuk kataloğu sevk eder.
Yukarıdaki gibi, adımın derlemeyi düşürmesine izin vermek düzeltmenin
tamamıdır.
Son satır, sıradan test paketinizdir; bir alışkanlık eklenmiş olarak: bir yerinde, sevk edilen her dilden en az bir mesajı katı bir çevirmen nesnesi üzerinden render edin —
import gettext
from gettext_tstrings import Translator
def test_catalogs_render(language: str) -> None:
translations = gettext.translation("messages", localedir="locales", languages=[language])
_ = Translator(translations, strict=True)
name = "Ada"
assert _(t"Welcome back, {name}")
— çünkü strict=True,
üretimin sessizce geri düşeceği yerde hata fırlatır
ve çalışma zamanında bir render, kataloğu tam olarak uygulamanın göreceği
gibi — .mosuyla birlikte — gören tek denetimdir.
Çevirmenlerle ve platformlarla çalışmak¶
.po dosyası bütün gettext dünyasının değiş tokuş formatıdır; bu
kütüphanenin onu yeniden kullanmasının nedeni de budur: çeviriyi devretmek
bir dosyayı devretmek demektir — alıcı ister PO editörlü bir mesai arkadaşı
olsun, ister Weblate ya da Crowdin gibi bir platform. Üç şey bu devri iyi
işletir:
Mesajın ne için olduğunu söyleyin. Koddaki bir yorum mesajla birlikte
yolculuk eder — -c "Translators:" bayrağının topladığı budur:
from gettext_tstrings import tr
name = "Ada"
# Translators: shown on the dashboard right after sign-in
print(tr(t"Welcome back, {name}"))
#. Translators: shown on the dashboard right after sign-in
#. gettext-tstrings
#: app.py:5
#, python-brace-format
msgid "Welcome back, {name}"
msgstr ""
Bir çevirmen o yorumu, dünyanın öbür ucunda, kendi editöründe, mesajın hemen
yanında görür. Bütün iş akışındaki en ucuz kalite kaldıracı budur. Kendi
kendisinin eş adlısı olan bir sözcük için — düğme olan "Open" ile durum olan
"Open" — mesaja pgettext ile bir bağlam
verin; bu, katalogda görünür bir msgctxt olur.
Yer tutucuları platform doğrulasın. Bir t-string'den çıkarılan her mesaj
python-brace-format bayrağını taşır ve denetlemediğiniz araçlarda yer
tutucu QA'sını açan şey o tek satırdır — Weblate denetimi belgeler, ticari
platformlar kendi denetimlerini aynı bayrağa bağlar ve
msgfmt --check-format onu her GNU boru hattında zorlar. Ayrıntılar ve
paketle gelen denetleyicinin bunların ötesinde yakaladıkları
çıkarma sayfasındadır.
Güvenlik ağına tam olarak uzandığı yere kadar güvenin. Bir platformdan geri gelen şey yine de derlemenize giren veridir; "platform bunu muhtemelen denetledi"yi "bu bozuk sevk edilemez"e çeviren, yukarıdaki CI kapılarıdır.
Çalışma zamanında bir dil bağlamak¶
Şimdiye kadarki her şey katalog üretir. Kalan karar, uygulamanın birini nerede seçtiğidir. Bir dilin kapsamı başına bir kez bağlayın — bir CLI için süreç, bir web servisi için istek.
Bir komut satırı aracı ya da masaüstü uygulaması, kullanıcının ortamını
bir kez, açılışta okur. languages= hiç verilmediğinde standart
kütüphane LANGUAGE, LC_ALL, LC_MESSAGES ve LANG üzerinden
pazarlık eder; fallback=True, hiçbiri sevk ettiğiniz bir katalogla
eşleşmediğinde hata fırlatmak yerine boş bir katalog — kaynak metin —
döndürür.
Bir web uygulaması istek başına karar verir. Her kataloğu içe aktarmada
bir kez yükleyin, sonra görünüm çalışmadan önce pazarlıkla seçileni
bağlama bağlayın —
set_translations bağlam-yereldir;
böylece farklı dillerdeki eşzamanlı istekler birbirinin bağlamasını asla
görmez.
import gettext
from flask import Flask, request
from gettext_tstrings import set_translations, tr
LANGUAGES = ("en", "ja", "de")
CATALOGS = {
language: gettext.translation(
"messages", localedir="locales", languages=[language], fallback=True
)
for language in LANGUAGES
}
app = Flask(__name__)
@app.before_request
def bind_language() -> None:
language = request.accept_languages.best_match(LANGUAGES) or "en"
set_translations(CATALOGS[language])
@app.get("/")
def home() -> str:
name = "Ada"
return tr(t"Welcome back, {name}")
Async çatılarda — FastAPI, Starlette ve ASGI olan başka her şey — isteği
use_translations ile sarın: bağlama
bir ContextVar içinde yaşar ve async görev geçişleri onu istek başına
korur.
import gettext
from fastapi import FastAPI, Request
from gettext_tstrings import tr, use_translations
LANGUAGES = ("en", "ja", "de")
CATALOGS = {
language: gettext.translation(
"messages", localedir="locales", languages=[language], fallback=True
)
for language in LANGUAGES
}
app = FastAPI()
@app.middleware("http")
async def bind_language(request: Request, call_next):
language = negotiate_language(request.headers.get("accept-language"), LANGUAGES)
with use_translations(CATALOGS[language]):
return await call_next(request)
negotiate_language, sizin Accept-Language ayrıştırmanızın yerini tutar
— çoğu çatı ya da ekosistemi bir tane sağlar; burada önemli olan,
call_next çevresindeki bağlamadır.
İki çalışma zamanı alışkanlığı resmi tamamlar. İçe aktarma anında oluşturulan
dizgiler — bir form etiketi, bir enum'un görünen adı — içe aktarma sırasında
hangi dil etkinse onu yakalamamalıdır; onları
lazy_gettext ile tanımlayın ki
kullanım anında etkin olan dilde render edilsinler. Ve gettext_tstrings
günlükçüsünü bir insanın baktığı bir yere yönlendirin: uyarıları, hoşgörülü
kipin her kapıdan sıyrılmış bir çeviriyi raporlamasıdır — render başına değil,
bozuk mesaj başına bir satır.
Sevkiyat¶
Üretimin pakete, .mo dosyalarına ve başka hiçbir şeye ihtiyacı yoktur.
Babel bir geliştirme ve CI bağımlılığıdır — gettext-tstrings[babel]
ekstrasını üretim imajının dışında tutun ve orada yalın paketi kurun; render,
yalnızca standart kütüphaneyle çalışır. Katalogları, dağıttığınız çıktıyı
üreten aynı derlemede derleyin; böylece içindeki .mo dosyaları tam olarak
incelenmiş .po dosyalarıdır ve birinin dizüstünde derlenmiş hiçbir şey asla
sevk edilmez.
Nasıl yol aldıkları, ne dağıttığınıza bağlıdır. Bir wheel onları paket verisi
olarak taşır; bu da katalogların paket dizininin içinde yaşaması gerektiği
anlamına gelir — src/myapp/locales/, üst düzeydeki bir locales/ değil — ve
derleme arka ucuna, .gitignoreın normalde sakladığı dosyaları da içermesi
söylenmelidir:
Onları, wheel kurulduğu anda var olmaktan çıkan kaynak ağacına göreli bir yol üzerinden değil, paketin kendisi üzerinden geri okuyun:
import gettext
from importlib.resources import as_file, files
with as_file(files("myapp") / "locales") as localedir:
translations = gettext.translation("messages", localedir=localedir, languages=["ja"])
Bir konteyner imajının işi daha kolaydır: derleme aşamasında derleyin ve sonucu kopyalayın, Babel'i o aşamada bırakarak.
FROM python:3.14-slim AS build
COPY . /src
RUN cd /src && python -m pip install ".[babel]" \
&& pybabel compile -d src/myapp/locales
FROM python:3.14-slim
COPY --from=build /src /src
RUN python -m pip install /src # no [babel]: rendering needs the stdlib only
Bir sürümden önce, bu sayfanın özetlendiği denetim listesi:
pybabel update --checkgeçiyor — katalogların haberi olmadan hiçbir mesaj değişmedi.pybabel compile, çıkış durumuyla derlemeyi kapılıyor.- Kalan
fuzzygirdiler kasıtlı — her biri, bir çevirmen onaylayana dek kaynak metin olarak render edilir. - Test paketi, sevk edilen her dili
strict=Trueile bir kez render ediyor. - Üretim çıktısı
.modosyaları içeriyor ve Babel içermiyor. gettext_tstringsgünlükçüsü izlemeye yönlendirilmiş.
Sonraki adımlar¶
- Çıkarma — bu sayfanın araç yarısının referansı: eşleme seçenekleri, özel işlev adları, katı kip ve her denetleyici.
- Kılavuz — çalışma zamanı yarısı: çoğullar, bağlamlar, ertelenmiş dizgiler ve hata kipleri ayrıntısıyla.
- Nasıl Çalışır — msgid neden bu biçimde görünür ve doğrulama gerçekte neyi denetler.