Dalam produksi¶
Tutorial menjalankan putarannya sekali, sendirian, pada program dengan satu pesan. Di proyek nyata putaran itu terus berputar: pesan berubah setelah diterjemahkan, penerjemah bekerja di tempat lain dan menurut jadwalnya sendiri, dan katalog terkompilasi dikirim bersama setiap rilis. Halaman ini adalah praktik itu — apa yang tinggal di repositori, apa yang bepergian, apa yang harus dijaga CI, dan di mana runtime mengikat sebuah bahasa.
Semuanya berjumlah enam pemeriksaan, jadi inilah semuanya lebih dulu; setiap bagian di bawah menyiapkan salah satunya.
pybabel update --checklolos — tidak ada pesan yang berubah tanpa katalognya mendengar.pybabel compilemenggerbangkan build pada status keluarnya.- Entri
fuzzyyang tersisa memang disengaja — masing-masing merender sebagai teks sumber sampai seorang penerjemah menegaskannya. - Suite pengujian merender setiap bahasa yang dikirim sekali dengan
strict=True. - Artefak produksi berisi berkas
.modan tanpa Babel. - Logger
gettext_tstringsdiarahkan ke pemantauan.
Bentuk sebuah proyek¶
myapp/
├── babel.cfg
├── pyproject.toml
├── src/
│ └── myapp/
└── locales/
├── messages.pot
├── ja/LC_MESSAGES/messages.po
└── de/LC_MESSAGES/messages.po
Commit babel.cfg, templat .pot, dan setiap .po — mereka adalah sumber
dari build terjemahan, dan diff mereka adalah cara Anda meninjau perubahan
terjemahan. Berkas .mo terkompilasi adalah artefak build: hasilkan di CI
atau saat pengemasan alih-alih meng-commit-nya, sehingga sebuah .po dan
.mo-nya tidak pernah bisa berselisih tentang apa yang dikirim.
Satu berkas berperan di setiap arah: .pot membawa pesan-pesan Anda keluar
ke penerjemah, berkas-berkas .po membawa terjemahan kembali. Sisa halaman
ini adalah apa yang berpindah di antara keduanya.
flowchart LR
code["kode sumber<br>tempat pemanggilan t-string"] -->|"pybabel extract"| pot["messages.pot"]
pot -->|"pybabel update"| po["satu .po per bahasa"]
po --> tr["penerjemah<br>atau platform"]
tr --> po
po -->|"pybabel compile (CI)"| mo["berkas .mo"]
mo --> app["aplikasi<br>saat runtime"]
Siklus setelah terjemahan pertama¶
pybabel init milik tutorial biasanya berjalan sekali, ketika sebuah bahasa
ditambahkan. Sejak itu, siklus kerjanya adalah
ekstrak → update → terjemahkan → kompilasi, dan pusatnya adalah
pybabel update, yang melipat sebuah templat segar ke dalam katalog yang ada
tanpa membuang terjemahan yang sudah ada di dalamnya.
Misalkan sapaan Hello {name} — yang sudah diterjemahkan sebagai
こんにちは {name} — ditulis ulang di kode menjadi Welcome back, {name}.
Ekstrak dan update:
$ 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
Katalog bahasa Jepang kini berisi:
#. gettext-tstrings
#: app.py:4
#, fuzzy, python-brace-format
msgid "Welcome back, {name}"
msgstr "こんにちは {name}"
Babel menyadari msgid baru itu menyerupai yang dihapus dan memasangkannya
dengan terjemahan lama — tetapi menandai pasangan itu fuzzy: tebakan mesin
yang menunggu manusia. Flag itu mengubah apa yang dikompilasi.
pybabel compile mengecualikan entri fuzzy dari .mo, sehingga sampai
seorang penerjemah menegaskan
pasangan itu, aplikasi merender teks Inggris yang baru alih-alih teks Jepang
yang basi:
$ 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
Sebuah pesan yang berubah karena itu terdegradasi dengan cara yang sama
seperti pesan yang rusak — ke bahasa sumber, tidak pernah ke terjemahan yang
kedaluwarsa. Bagian penerjemah dalam siklus ini adalah merevisi msgstr dan
menghapus flag fuzzy; kompilasi berikutnya memungut entri itu.
Nama placeholder adalah bagian dari identitas pesan
Msgid adalah kunci katalog, dan nama placeholder ada di dalamnya —
sehingga mengganti nama sebuah variabel di kode (name → user_name)
mengubah msgid dan mengirim terjemahan setiap bahasa atasnya kembali
melewati siklus fuzzy. Namai variabel yang diinterpolasi dengan kata-kata
yang akan dipahami penerjemah, dan ganti namanya hanya dengan alasan.
Pemformatan adalah bayangan cerminnya: !r dan :.2f
bukan bagian dari msgid, sehingga
mengetatkan {amount:,.2f} menjadi {amount:,.0f} tidak mengubah apa pun
di katalog mana pun. Menulis ulang kalimatnya, tentu saja, adalah
perubahan sungguhan — itulah siklus di atas.
Apa yang dijaga CI¶
Tiga kegagalan layak membuat build merah: katalog tertinggal dari kode, sebuah terjemahan merusak placeholder, atau entri yang rusak lolos sampai ke runtime. Satu langkah per kegagalan:
- 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 tidak menulis ulang apa pun dan keluar dengan status
bukan nol ketika sebuah katalog ketinggalan zaman terhadap templat yang baru
diekstrak — penjaga terhadap me-merge kode yang pesannya tidak diekstrak ulang
siapa pun. pybabel compile menjalankan pemeriksaan placeholder milik Babel
dan
checker terdaftar
paket ini.
Babel 2.18.0: --check tidak dapat menjaga katalog yang memakai konteks
Pada Babel 2.18.0, pybabel update --check melaporkan setiap katalog
yang memuat msgctxt sebagai ketinggalan zaman, pada setiap kali
dijalankan, sebaru apa pun katalog itu. Gerbang yang gagal permanen lebih
buruk daripada tidak ada gerbang, karena sebuah tim akan mematikannya —
jadi jika Anda memakai pgettext atau npgettext sama sekali, ganti
langkah ini alih-alih hidup dengannya. Membaca templat dan setiap katalog
dengan babel.messages.pofile.read_po lalu membandingkan
{(m.context, m.id) for m in catalog if m.id} adalah keseluruhan
pemeriksaannya, dan itulah yang dilakukan
build situs ini sendiri. Penyebabnya
dituliskan di Jebakan umum.
Periksa status keluarnya, bukan log-nya
pybabel compile melaporkan setiap galat placeholder, keluar dengan
status bukan nol — dan tetap menulis .mo-nya. Pipeline yang
mengompilasi lalu menyalin locales/ ke sebuah image mengirimkan katalog
yang rusak kecuali status keluar bukan nol itu benar-benar
menghentikannya. Membiarkan langkah itu menggagalkan build, seperti di
atas, adalah keseluruhan perbaikannya.
Baris terakhir adalah suite pengujian biasa Anda, dengan satu kebiasaan tambahan: di suatu tempat di dalamnya, render setidaknya satu pesan per bahasa yang dikirim melalui translator yang ketat —
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}")
— karena strict=True
melempar galat di tempat produksi akan diam-diam kembali ke teks sumber,
dan sebuah render runtime adalah satu-satunya pemeriksaan yang melihat katalog
persis seperti aplikasi akan melihatnya, .mo dan semuanya.
Bekerja dengan penerjemah dan platform¶
Berkas .po adalah format pertukaran seluruh dunia gettext, itulah alasan
pustaka ini memakainya kembali: menyerahkan penerjemahan berarti menyerahkan
sebuah berkas, entah penerimanya seorang kolega dengan editor PO atau sebuah
platform seperti Weblate atau Crowdin. Tiga hal membuat serah terima itu
berjalan baik:
Katakan untuk apa pesan itu. Sebuah komentar di kode bepergian bersama
pesannya — itulah yang dikumpulkan flag -c "Translators:":
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 ""
Seorang penerjemah melihat komentar itu di editornya, di sebelah pesannya, di
belahan dunia yang lain. Itu tuas kualitas termurah di seluruh alur kerja.
Untuk kata yang menjadi homonim dirinya sendiri — "Open" si tombol versus
"Open" si keadaan — beri pesannya sebuah konteks
dengan pgettext, yang menjadi msgctxt yang terlihat di katalog.
Biarkan platform memvalidasi placeholder. Setiap pesan yang diekstrak
dari t-string membawa flag python-brace-format, dan satu baris itulah yang
menyalakan QA placeholder di perkakas yang tidak Anda kendalikan — Weblate
mendokumentasikan pemeriksaannya, platform komersial menautkan milik mereka
pada flag yang sama, dan msgfmt --check-format menegakkannya di pipeline GNU
mana pun. Detailnya, dan apa yang ditangkap checker bawaan di luar itu, ada di
halaman ekstraksi.
Percayai jaring pengamannya persis sejauh jangkauannya. Apa pun yang kembali dari sebuah platform tetaplah data yang memasuki build Anda; gerbang CI di atas adalah yang mengubah "platformnya mungkin sudah memeriksa ini" menjadi "ini tidak mungkin dikirim dalam keadaan rusak".
Mengikat bahasa saat runtime¶
Semua sejauh ini menghasilkan katalog. Keputusan yang tersisa adalah di mana aplikasi memilih satu. Ikat sekali per lingkup sebuah bahasa — proses untuk CLI, permintaan untuk layanan web.
Perkakas baris perintah atau aplikasi desktop membaca lingkungan pengguna
sekali, saat mulai. Tidak melewatkan languages= membiarkan pustaka
standar bernegosiasi dari LANGUAGE, LC_ALL, LC_MESSAGES, dan LANG;
fallback=True mengembalikan katalog null — teks sumber — alih-alih
melempar galat ketika tak satu pun dari mereka cocok dengan katalog yang
Anda kirim.
Aplikasi web memutuskan per permintaan. Muat setiap katalog sekali saat
impor, lalu ikat yang ternegosiasi ke konteks sebelum view berjalan —
set_translations bersifat
context-local, sehingga permintaan bersamaan dalam bahasa berbeda tidak
pernah melihat ikatan satu sama lain.
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}")
Di bawah framework async — FastAPI, Starlette, dan apa pun yang ASGI —
bungkus permintaan dalam
use_translations: ikatannya hidup di
sebuah ContextVar, yang dipertahankan pergantian task async per
permintaan.
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 mewakili parsing Accept-Language Anda — sebagian
besar framework atau ekosistemnya menyediakannya; yang penting di sini
adalah ikatan di sekeliling call_next.
Dua kebiasaan runtime melengkapi gambarnya. String yang dibuat saat impor —
label formulir, nama tampilan sebuah enum — tidak boleh menangkap bahasa apa
pun yang aktif selama impor; definisikan dengan
lazy_gettext dan mereka merender dalam
bahasa yang aktif saat digunakan. Dan arahkan logger gettext_tstrings ke
tempat yang dilihat manusia: peringatannya adalah mode longgar yang melaporkan
terjemahan yang lolos dari setiap gerbang, satu baris per pesan yang rusak dan
bukan satu per render.
Pengiriman¶
Produksi membutuhkan paketnya, berkas-berkas .mo, dan tidak yang lain. Babel
adalah dependensi pengembangan dan CI — jauhkan gettext-tstrings[babel] dari
image produksi dan pasang paket polosnya di sana; rendering berjalan dengan
pustaka standar saja. Kompilasi katalog di build yang sama yang menghasilkan
artefak yang Anda deploy, sehingga berkas .mo di dalamnya persis berkas
.po yang telah ditinjau, dan tidak ada hasil kompilasi laptop siapa pun yang
pernah terkirim.
Bagaimana mereka ikut terbawa bergantung pada apa yang Anda deploy. Sebuah
wheel membawanya sebagai data paket, yang berarti katalognya harus berada di
dalam direktori paket — src/myapp/locales/, bukan locales/ di tingkat
teratas — dan backend build-nya harus diberi tahu untuk menyertakan berkas
yang biasanya disembunyikan .gitignore:
Bacalah mereka kembali melalui paketnya, bukan melalui jalur relatif terhadap pohon sumber, yang berhenti ada begitu wheel-nya terpasang:
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"])
Sebuah image kontainer punya tugas yang lebih mudah: kompilasi selama tahap build dan salin hasilnya, meninggalkan Babel di tahap itu.
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
Sebelum sebuah rilis, daftar periksa yang menjadi inti halaman ini:
pybabel update --checklolos — tidak ada pesan yang berubah tanpa katalognya mendengar.pybabel compilemenggerbangkan build pada status keluarnya.- Entri
fuzzyyang tersisa memang disengaja — masing-masing merender sebagai teks sumber sampai seorang penerjemah menegaskannya. - Suite pengujian merender setiap bahasa yang dikirim sekali dengan
strict=True. - Artefak produksi berisi berkas
.modan tanpa Babel. - Logger
gettext_tstringsdiarahkan ke pemantauan.
Ke mana selanjutnya¶
- Ekstraksi — referensi untuk paruh perkakas halaman ini: opsi pemetaan, nama fungsi kustom, mode ketat, dan setiap checker.
- Panduan — paruh runtime-nya: bentuk jamak, konteks, string tertunda, dan mode-mode kegagalan secara terperinci.
- Cara kerjanya — mengapa msgid berbentuk seperti itu, dan apa yang sebenarnya diperiksa validasi.