V produkciji¶
Vadnica zanko požene enkrat, sami in na programu z enim samim sporočilom. V resničnem projektu se zanka vrti naprej: sporočila se spremenijo, potem ko so že prevedena, prevajalec dela drugje in po svojem urniku, kompiliran katalog pa gre z vsako izdajo v svet. Ta stran je ta praksa — kaj ostane v repozitoriju, kaj potuje, kaj mora zapirati CI in kje se med izvajanjem veže jezik.
Vse skupaj se sešteje v šest preverjanj, zato so tu najprej; vsak spodnji razdelek eno od njih nastavi.
pybabel update --checkuspe — nobeno sporočilo se ni spremenilo, ne da bi za to izvedeli katalogi.pybabel compilegradnjo zapira po svojem izhodnem stanju.- Preostali vnosi
fuzzyso namerni — vsak se izriše kot izvorno besedilo, dokler ga prevajalec ne potrdi. - Testna zbirka vsak odpremljeni jezik enkrat izriše s
strict=True. - Produkcijski izdelek vsebuje datoteke
.moin nobenega Babela. - Dnevnik
gettext_tstringsje usmerjen v nadzorni sistem.
Oblika projekta¶
myapp/
├── babel.cfg
├── pyproject.toml
├── src/
│ └── myapp/
└── locales/
├── messages.pot
├── ja/LC_MESSAGES/messages.po
└── de/LC_MESSAGES/messages.po
V repozitorij dodajte babel.cfg, predlogo .pot in vsak .po — to so viri
prevajalske gradnje in njihove razlike so način, kako pregledujete spremembe
prevodov. Kompilirane datoteke .mo so gradbeni izdelki: proizvajajte jih v CI
ali ob pakiranju, namesto da bi jih dodajali v repozitorij, tako da si .po in
njegov .mo nikoli ne moreta biti neenotna o tem, kaj se odpremi.
Ena datoteka ima vlogo v vsako smer: .pot nosi vaša sporočila ven k
prevajalcem, datoteke .po pa prevode nazaj. Preostanek te strani je to,
kar se giblje med njima.
flowchart LR
code["izvorna koda<br>klicna mesta s t-nizi"] -->|"pybabel extract"| pot["messages.pot"]
pot -->|"pybabel update"| po["en .po na jezik"]
po --> tr["prevajalec<br>ali platforma"]
tr --> po
po -->|"pybabel compile (CI)"| mo["datoteke .mo"]
mo --> app["aplikacija<br>med izvajanjem"]
Cikel po prvem prevodu¶
pybabel init iz vadnice se navadno požene enkrat, ko se doda jezik. Od tam
naprej je delovni cikel izvleci → posodobi → prevedi → kompiliraj, njegovo središče
pa je pybabel update, ki svežo predlogo zloži v obstoječe kataloge, ne da bi
zavrgel prevode, ki so že v njih.
Recimo, da je pozdrav Hello {name} — že preveden kot こんにちは {name} — v
kodi preoblikovan v Welcome back, {name}. Izvlecite in posodobite:
$ 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
Japonski katalog zdaj vsebuje:
#. gettext-tstrings
#: app.py:4
#, fuzzy, python-brace-format
msgid "Welcome back, {name}"
msgstr "こんにちは {name}"
Babel je opazil, da je novi msgid podoben odstranjenemu, in ga je združil s
starim prevodom — a je par označil kot fuzzy: strojna domneva, ki čaka na
človeka. Zastavica spremeni to, kaj se kompilira. pybabel compile ohlapne
vnose iz .mo izpusti, zato aplikacija, dokler prevajalec para ne potrdi,
izriše novo angleško besedilo namesto zastarelega japonskega:
$ 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
Spremenjeno sporočilo se torej poslabša enako kot pokvarjeno — do izvornega
jezika, nikoli do zastarelega prevoda. Prevajalčev delež v ciklu je, da
msgstr popravi in zastavico fuzzy izbriše; naslednja kompilacija vnos
pobere.
Imena ograd so del identitete sporočila
Msgid je katalogni ključ in ime ograde je v njem — zato preimenovanje
spremenljivke v kodi (name → user_name) spremeni msgid in pošlje njegov
prevod v vsakem jeziku nazaj skozi ohlapni cikel. Interpolirane
spremenljivke poimenujte z besedami, ki jih bo prevajalec razumel, in jih
preimenujte samo z razlogom.
Oblikovanje je zrcalna slika: !r in :.2f nista del
msgida, zato zaostritev
{amount:,.2f} v {amount:,.0f} v nobenem katalogu ne spremeni ničesar.
Preoblikovanje povedi pa je seveda resnična sprememba — in to je zgornji
cikel.
Kaj zapira CI¶
Tri odpovedi so vredne rdeče gradnje: katalogi so zaostali za kodo, prevod je pokvaril ogrado ali pa se je pokvarjen vnos prebil vse do izvajanja. Po en korak na odpoved:
- 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 ničesar ne prepiše in se konča z neničelnim stanjem,
kadar je katalog zastarel glede na sveže izvlečeno predlogo — to je zaščita
pred združevanjem kode, katere sporočil ni nihče znova izvlekel. pybabel
compile požene preverjanja ograd tako Babela kot
registriranega preverjevalnika
tega paketa.
Babel 2.18.0: --check ne more zapreti kataloga, ki uporablja kontekste
Na Babelu 2.18.0 pybabel update --check vsak katalog, ki vsebuje
msgctxt, javi kot zastarel, ob vsakem teku, naj bo še tako svež.
Trajno padajoča zaščita je slabša od nobene, ker jo ekipa izklopi — če
torej pgettext ali npgettext sploh uporabljate, ta korak raje
nadomestite, kot da živite z njim. Branje predloge in vsakega kataloga s
babel.messages.pofile.read_po ter primerjava
{(m.context, m.id) for m in catalog if m.id} je celotno preverjanje — in
prav to počne gradnja tega spletišča. Vzrok je
popisan med Pastmi.
Preverjajte izhodno stanje, ne dnevnika
pybabel compile javi vsako napako pri ogradah, konča se z neničelnim
stanjem — .mo pa vseeno zapiše. Cevovod, ki kompilira in nato
locales/ prekopira v sliko, odpremi pokvarjen katalog, razen če ga
neničelno stanje res ustavi. Da korak podre gradnjo, kot zgoraj, je
celoten popravek.
Zadnja vrstica je vaša običajna testna zbirka z eno dodano navado: nekje v njej izrišite vsaj eno sporočilo na odpremljeni jezik skozi strog prevajalnik —
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}")
— ker strict=True sproži izjemo tam, kjer bi se produkcija tiho vrnila na izvirnik,
izris med izvajanjem pa je edino preverjanje, ki katalog vidi natanko tako, kot
ga bo videla aplikacija, z .mo in vsem.
Delo s prevajalci in platformami¶
Datoteka .po je izmenjevalni format celotnega sveta gettexta in prav zato jo
ta knjižnica ponovno uporablja: predati prevajanje pomeni predati datoteko, naj
je prejemnik sodelavec z urejevalnikom PO ali platforma, kakršni sta Weblate in
Crowdin. Predajo naredijo dobro tri stvari:
Povejte, čemu je sporočilo namenjeno. Komentar v kodi potuje s sporočilom —
prav to zbira zastavica -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 ""
Prevajalec ta komentar vidi v svojem urejevalniku, ob sporočilu, na drugem
koncu sveta. To je najcenejši vzvod kakovosti v celotnem delovnem procesu. Pri
besedi, ki je sama sebi homonim — »Odpri« kot gumb proti »Odprto« kot stanje —,
sporočilu dodajte kontekst s pgettext, ki v
katalogu postane viden msgctxt.
Pustite platformi, da preveri ograde. Vsako sporočilo, izvlečeno iz t-niza,
nosi zastavico python-brace-format in prav ta ena vrstica vklopi nadzor
kakovosti ograd v orodjih, ki jih ne nadzorujete — Weblate to preverjanje
dokumentira, komercialne platforme svojega vežejo na isto zastavico,
msgfmt --check-format pa ga uveljavlja v vsakem cevovodu GNU. Podrobnosti in
kaj priloženi preverjevalnik ujame vrh tega, so na
strani o ekstrakciji.
Varnostni mreži zaupajte natanko toliko, kolikor sega. Kar koli se vrne s platforme, so še vedno podatki, ki vstopajo v vašo gradnjo; zgornje zaščite v CI so tisto, kar »platforma je to najbrž preverila« spremeni v »to se ne more odpremiti pokvarjeno«.
Vezava jezika med izvajanjem¶
Vse doslej proizvaja kataloge. Preostala odločitev je, kje aplikacija enega izbere. Vežite enkrat na obseg enega jezika — na proces pri orodju ukazne vrstice, na zahtevo pri spletni storitvi.
Orodje ukazne vrstice ali namizna aplikacija okolje uporabnika prebere
enkrat, ob zagonu. Če languages= ne podate, se standardna knjižnica
pogaja iz LANGUAGE, LC_ALL, LC_MESSAGES in LANG; fallback=True
vrne ničelni katalog — izvorno besedilo —, namesto da bi sprožil izjemo,
kadar se noben od njih ne ujema s katalogom, ki ga odpremljate.
Spletna aplikacija se odloči za vsako zahtevo posebej. Vsak katalog
naložite enkrat ob uvozu, nato pa izpogajanega vežite na kontekst, preden
steče pogled — set_translations je
krajevno vezan na kontekst, zato sočasne zahteve v različnih jezikih nikoli
ne vidijo vezav druga druge.
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}")
Pod asinhronimi ogrodji — FastAPI, Starlette in kar koli drugega z ASGI —
zahtevo ovijte v use_translations:
vezava živi v ContextVar, ki ga preklapljanje asinhronih opravil ohrani
za vsako zahtevo posebej.
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 predstavlja vaše razčlenjevanje glave Accept-Language —
večina ogrodij ali njihovih ekosistemov ga ponuja; tukaj je pomembna vezava
okoli call_next.
Sliko dopolnita dve navadi med izvajanjem. Nizi, ustvarjeni ob uvozu — oznaka
obrazca, prikazno ime naštevnega tipa —, ne smejo ujeti jezika, ki je bil
dejaven med uvozom; določite jih z
lazy_gettext in izrisali se bodo v jeziku,
dejavnem ob rabi. In dnevnik gettext_tstrings usmerite tja, kamor kdo
gleda: njegova opozorila so prizanesljivi način, ki javlja prevod, izmuznjen
vsem zaščitam, po eno vrstico na pokvarjeno sporočilo namesto po eno na izris.
Odprema¶
Produkcija potrebuje paket, datoteke .mo in nič drugega. Babel je odvisnost
razvoja in CI — gettext-tstrings[babel] pustite zunaj produkcijske slike in
tam namestite goli paket; izris teče izključno na standardni knjižnici.
Kataloge kompilirajte v isti gradnji, ki proizvede izdelek, ki ga razmestite,
tako da so datoteke .mo v njem natanko pregledane datoteke .po in da se ne
odpremi nič, kar je bilo kompilirano na nekem prenosniku.
Kako potujejo, je odvisno od tega, kaj razmeščate. Datoteka wheel jih nosi kot
podatke paketa, kar pomeni, da morajo katalogi živeti znotraj imenika paketa
— src/myapp/locales/, ne v vrhnjem locales/ — gradbenemu zaledju pa je
treba povedati, naj vključi datoteke, ki jih .gitignore sicer skriva:
Berite jih nazaj skozi paket in ne po poti, ki je relativna na izvorno drevo: ta neha obstajati v trenutku, ko je wheel nameščen:
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"])
Slika vsebnika ima lažje delo: kompilirajte med gradbeno stopnjo in prekopirajte rezultat, Babel pa pustite v tisti stopnji.
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
Pred izdajo je kontrolni seznam, na katerega se ta stran skrči:
pybabel update --checkuspe — nobeno sporočilo se ni spremenilo, ne da bi za to izvedeli katalogi.pybabel compilegradnjo zapira po svojem izhodnem stanju.- Preostali vnosi
fuzzyso namerni — vsak se izriše kot izvorno besedilo, dokler ga prevajalec ne potrdi. - Testna zbirka vsak odpremljeni jezik enkrat izriše s
strict=True. - Produkcijski izdelek vsebuje datoteke
.moin nobenega Babela. - Dnevnik
gettext_tstringsje usmerjen v nadzorni sistem.
Kam naprej¶
- Ekstrakcija — referenca za orodno polovico te strani: možnosti preslikav, lastna imena funkcij, strogi način in vsak preverjevalnik.
- Vodnik — izvajalna polovica: množina, konteksti, odloženi nizi in načini odpovedi do podrobnosti.
- Kako deluje — zakaj je msgid videti tako, kot je videti, in kaj preverjanje v resnici preveri.