Lewati ke isi

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 --check lolos — tidak ada pesan yang berubah tanpa katalognya mendengar.
  • pybabel compile menggerbangkan build pada status keluarnya.
  • Entri fuzzy yang 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 .mo dan tanpa Babel.
  • Logger gettext_tstrings diarahkan 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 (nameuser_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.

import gettext

from gettext_tstrings import Translator

_ = Translator(gettext.translation("messages", localedir="locales", fallback=True))

name = "Ada"
print(_(t"Welcome back, {name}"))

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:

[tool.hatch.build]
# .mo files are build output, so they are gitignored; name them or the
# wheel ships without a single translation.
artifacts = ["src/myapp/locales/**/*.mo"]
[tool.setuptools.package-data]
myapp = ["locales/*/LC_MESSAGES/*.mo"]

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 --check lolos — tidak ada pesan yang berubah tanpa katalognya mendengar.
  • pybabel compile menggerbangkan build pada status keluarnya.
  • Entri fuzzy yang 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 .mo dan tanpa Babel.
  • Logger gettext_tstrings diarahkan 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.