Pāriet uz saturu

Produkcijā

Pamācība izpilda ciklu vienreiz, vienatnē, programmai ar vienu ziņojumu. Īstā projektā cikls turpina griezties: ziņojumi mainās pēc tam, kad tie jau iztulkoti, tulkotājs strādā citur un pēc sava grafika, un kompilēts katalogs tiek piegādāts ar katru laidienu. Šī lapa ir tieši šī prakse — kas paliek repozitorijā, kas ceļo, ko CI ir jāaiztur un kur izpildlaiks piesaista valodu.

Kopsummā tas ir sešas pārbaudes, tāpēc vispirms tās; katra zemāk esošā sadaļa iestata vienu no tām.

  • pybabel update --check iziet cauri — neviens ziņojums nav mainījies, par to nedzirdot katalogiem.
  • pybabel compile aiztur būvējumu pēc tā izejas statusa.
  • Atlikušie fuzzy ieraksti ir apzināti — katrs no tiem renderējas kā avota teksts, līdz tulkotājs to apstiprina.
  • Testu kopa katru piegādāto valodu vienreiz renderē ar strict=True.
  • Produkcijas artefaktā ir .mo faili un nav Babel.
  • Žurnalizētājs gettext_tstrings ir novirzīts uz uzraudzību.

Kāda izskatās projekta forma

myapp/
├── babel.cfg
├── pyproject.toml
├── src/
│   └── myapp/
└── locales/
    ├── messages.pot
    ├── ja/LC_MESSAGES/messages.po
    └── de/LC_MESSAGES/messages.po

Iekļaujiet versiju kontrolē babel.cfg, .pot veidni un katru .po — tie ir tulkojuma būvējuma avoti, un to diff ir veids, kā jūs pārskatāt tulkojumu izmaiņas. Kompilētie .mo faili ir būvējuma artefakti: radiet tos CI vidē vai pakošanas laikā, nevis iekļaujiet versiju kontrolē, lai .po un tā .mo nekad nevarētu nesaskanēt par to, kas tiek piegādāts.

Vienam failam ir loma katrā virzienā: .pot nes jūsu ziņojumus ārā pie tulkotājiem, .po faili nes tulkojumus atpakaļ. Pārējā lapas daļa ir tas, kas pārvietojas starp tiem.

flowchart LR
  code["pirmkods<br>t-virkņu izsaukuma vietas"] -->|"pybabel extract"| pot["messages.pot"]
  pot -->|"pybabel update"| po["viens .po katrai valodai"]
  po --> tr["tulkotājs<br>vai platforma"]
  tr --> po
  po -->|"pybabel compile (CI)"| mo[".mo faili"]
  mo --> app["lietotne<br>izpildlaikā"]

Cikls pēc pirmā tulkojuma

Pamācības pybabel init parasti tiek palaists vienreiz, kad valoda tiek pievienota. No tā brīža darba cikls ir ekstrahēt → atjaunināt → iztulkot → kompilēt, un tā centrs ir pybabel update, kas ielok svaigo veidni esošajos katalogos, bet neizmet tajos jau esošos tulkojumus.

Pieņemsim, ka sveiciens Hello {name} — jau iztulkots kā こんにちは {name} — kodā tiek pārformulēts par Welcome back, {name}. Ekstrahējiet un atjauniniet:

$ 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

Japāņu katalogs tagad satur:

#. gettext-tstrings
#: app.py:4
#, fuzzy, python-brace-format
msgid "Welcome back, {name}"
msgstr "こんにちは {name}"

Babel pamanīja, ka jaunais msgid atgādina kādu noņemtu, un sapāroja to ar veco tulkojumu — bet atzīmēja pāri kā fuzzy: mašīnas minējumu, kas gaida cilvēku. Šis karogs maina to, kas tiek kompilēts. pybabel compile fuzzy ierakstus .mo failā neiekļauj, tāpēc, kamēr tulkotājs pāri neapstiprina, lietotne renderē jauno angļu tekstu, nevis novecojušo japāņu:

$ 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

Mainīts ziņojums tātad degradējas tieši tāpat kā sabojāts — uz avota valodu, nekad uz novecojušu tulkojumu. Tulkotāja daļa ciklā ir pārstrādāt msgstr un nodzēst fuzzy karogu; nākamā kompilēšana ierakstu paņem.

Vietturu nosaukumi ir daļa no ziņojuma identitātes

Msgid ir kataloga atslēga, un viettura nosaukums ir tās iekšienē — tāpēc mainīga pārdēvēšana kodā (nameuser_name) maina msgid un sūta katras valodas tulkojumu atpakaļ cauri fuzzy ciklam. Nosauciet interpolētos mainīgos ar vārdiem, ko tulkotājs sapratīs, un pārdēvējiet tos tikai ar iemeslu.

Formatējums ir spoguļattēls: !r un :.2f nav msgid daļa, tāpēc {amount:,.2f} savilkšana par {amount:,.0f} nemaina neko nevienā katalogā. Paša teikuma pārformulēšana, protams, ir īsta izmaiņa — un tas ir augšminētais cikls.

Ko CI aiztur

Trīs kļūmes ir sarkana būvējuma vērtas: katalogi ir atpalikuši no koda, tulkojums ir salauzis vietturi vai sabojāts ieraksts ir paslīdējis līdz izpildlaikam. Viens solis katrai kļūmei:

- 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 neko nepārraksta un iziet ar statusu, kas nav nulle, kad katalogs ir novecojis attiecībā pret svaigi ekstrahēto veidni — sargs pret tāda koda sapludināšanu, kura ziņojumus neviens nav no jauna ekstrahējis. pybabel compile palaiž gan Babel, gan šīs pakotnes reģistrētā pārbaudītāja vietturu pārbaudes.

Babel 2.18.0: --check nespēj aizturēt katalogu, kas lieto kontekstus

Babel 2.18.0 versijā pybabel update --check ziņo, ka katrs katalogs, kas satur msgctxt, ir novecojis — katrā izpildē, lai cik svaigs tas arī būtu. Pastāvīgi krītoši vārti ir sliktāki nekā nekādi vārti, jo komanda tos izslēdz — tāpēc, ja jūs vispār lietojat pgettext vai npgettext, aizstājiet šo soli, nevis samierinieties ar to. Veidnes un katra kataloga nolasīšana ar babel.messages.pofile.read_po un {(m.context, m.id) for m in catalog if m.id} salīdzināšana ir visa pārbaude, un tieši to dara šīs vietnes pašas būvējums. Cēlonis ir aprakstīts lapā Kļūmes.

Pārbaudiet izejas statusu, nevis žurnālu

pybabel compile ziņo par katru vietturu kļūdu, iziet ar statusu, kas nav nulle, — un .mo failu tik un tā ieraksta. Konveijers, kas kompilē un tad iekopē locales/ attēlā, piegādā sabojāto katalogu, ja vien izeja ar nenulles statusu to patiešām neaptur. Ļaut solim nogāzt būvējumu, kā augstāk, ir viss risinājums.

Pēdējā rinda ir jūsu parastā testu kopa, ar vienu pievienotu ieradumu: kaut kur tajā izrenderējiet vismaz vienu ziņojumu katrai piegādātajai valodai caur stingru tulkotāju —

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}")

— jo strict=True izraisa kļūdu tur, kur produkcija klusējot atkāptos, un renderēšana izpildlaikā ir vienīgā pārbaude, kas redz katalogu tieši tādu, kādu to redzēs lietotne, ar .mo un visu pārējo.

Darbs ar tulkotājiem un platformām

.po fails ir visas gettext pasaules apmaiņas formāts, un tieši tāpēc šī bibliotēka to izmanto atkārtoti: nodot tulkošanu tālāk nozīmē nodot failu — vienalga, vai saņēmējs ir kolēģis ar PO redaktoru vai platforma, tāda kā Weblate vai Crowdin. Trīs lietas liek šai nodošanai izdoties labi:

Pasakiet, kam ziņojums domāts. Komentārs kodā ceļo līdzi ziņojumam — tieši to savāc karogs -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 ""

Tulkotājs redz šo komentāru savā redaktorā, blakus ziņojumam, otrā pasaules malā. Tas ir lētākais kvalitātes svira visā darbplūsmā. Vārdam, kas ir pats sev homonīms — “Open” kā poga pret “Open” kā stāvoklis —, iedodiet ziņojumam kontekstu ar pgettext, kas katalogā kļūst par redzamu msgctxt.

Ļaujiet platformai validēt vietturus. Katrs no t-virknes ekstrahētais ziņojums nes python-brace-format karogu, un tieši šī viena rinda ieslēdz vietturu kvalitātes kontroli rīkos, ko jūs nekontrolējat — Weblate šo pārbaudi dokumentē, komerciālās platformas savu balsta uz to pašu karogu, un msgfmt --check-format to piemēro jebkurā GNU konveijerā. Detaļas un tas, ko komplektā iekļautais pārbaudītājs noķer papildus tām, ir ekstrakcijas lapā.

Uzticieties drošības tīklam tieši tik tālu, cik tas sniedzas. Viss, kas atnāk atpakaļ no platformas, joprojām ir dati, kas ienāk jūsu būvējumā; augstāk aprakstītie CI vārti ir tas, kas pārvērš “platforma to laikam pārbaudīja” par “tas nevar tikt piegādāts sabojāts”.

Valodas piesaiste izpildlaikā

Viss līdz šim rada katalogus. Atlikušais lēmums ir par to, kur lietotne kādu no tiem izvēlas. Piesaistiet vienreiz katrā valodas tvērumā — procesā CLI gadījumā, pieprasījumā tīmekļa servisa gadījumā.

Komandrindas rīks vai darbvirsmas lietotne nolasa lietotāja vidi vienreiz, startējot. Ja languages= netiek padots, standarta bibliotēka veic sarunas no LANGUAGE, LC_ALL, LC_MESSAGES un LANG; fallback=True atgriež tukšu katalogu — avota tekstu —, nevis izraisa kļūdu, kad neviens no tiem neatbilst jūsu piegādātajam katalogam.

import gettext

from gettext_tstrings import Translator

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

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

Tīmekļa lietotne izlemj katram pieprasījumam. Ielādējiet katru katalogu vienreiz importa laikā, tad pirms skata izpildes piesaistiet izrunāto katalogu kontekstam — set_translations ir kontekstlokāla, tāpēc vienlaicīgi pieprasījumi dažādās valodās nekad neredz cits cita piesaisti.

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}")

Asinhronos ietvaros — FastAPI, Starlette un jebkurā citā ASGI ietvarā — ietiniet pieprasījumu use_translations iekšienē: piesaiste dzīvo ContextVar mainīgajā, ko asinhrono uzdevumu pārslēgšana saglabā katram pieprasījumam atsevišķi.

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 apzīmē jūsu Accept-Language parsēšanu — lielākā daļa ietvaru vai to ekosistēmu tādu piedāvā; šeit svarīgā ir piesaiste ap call_next.

Divi izpildlaika ieradumi pabeidz ainu. Virknes, kas radītas importa laikā — formas uzraksts, enum attēlojamais nosaukums —, nedrīkst notvert to valodu, kura gadījās aktīva importa brīdī; definējiet tās ar lazy_gettext, un tās renderēsies tajā valodā, kas ir aktīva lietošanas brīdī. Un novirziet gettext_tstrings žurnalizētāju kaut kur, kur cilvēks skatās: tā brīdinājumi ir iecietīgais režīms, kas ziņo par tulkojumu, kurš izslīdējis cauri visiem vārtiem, — pa vienai rindai uz sabojātu ziņojumu, nevis pa vienai uz renderēšanu.

Piegāde

Produkcijai vajadzīga pakotne, .mo faili un nekas cits. Babel ir izstrādes un CI atkarība — turiet gettext-tstrings[babel] ārpus produkcijas attēla un instalējiet tur kailo pakotni; renderēšana darbojas ar standarta bibliotēku vien. Kompilējiet katalogus tajā pašā būvējumā, kas rada izvietojamo artefaktu, lai .mo faili tajā būtu tieši tie pārskatītie .po faili un lai nekas kompilēts uz kāda klēpjdatora nekad netiktu piegādāts.

Kā tie ceļo, ir atkarīgs no tā, ko jūs izvietojat. Wheel tos nes kā pakotnes datus, kas nozīmē, ka katalogiem jādzīvo pakotnes direktorijas iekšienēsrc/myapp/locales/, nevis augšējā līmeņa locales/ —, un būvējuma aizmugursistēmai jāpasaka, ka jāiekļauj faili, kurus .gitignore parasti noslēpj:

[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"]

Lasiet tos atpakaļ caur pakotni, nevis caur ceļu attiecībā pret avota koku, kas beidz pastāvēt brīdī, kad wheel ir uzinstalēts:

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"])

Konteinera attēlam uzdevums ir vieglāks: kompilējiet būvējuma stadijā un nokopējiet rezultātu, atstājot Babel tajā stadijā.

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

Pirms laidiena kontrolsaraksts, uz ko šī lapa sarūk:

  • pybabel update --check iziet cauri — neviens ziņojums nav mainījies, katalogiem par to nedzirdot.
  • pybabel compile aiztur būvējumu pēc sava izejas statusa.
  • Atlikušie fuzzy ieraksti ir apzināti — katrs no tiem renderējas kā avota teksts, līdz tulkotājs to apstiprina.
  • Testu kopa vienreiz izrenderē katru piegādāto valodu ar strict=True.
  • Produkcijas artefakts satur .mo failus un nekādu Babel.
  • gettext_tstrings žurnalizētājs ir novirzīts uz uzraudzību.

Kurp tālāk

  • Ekstrakcija — uzziņa par šīs lapas rīku pusi: attēlojuma opcijas, pielāgoti funkciju nosaukumi, stingrais režīms un katrs pārbaudītājs.
  • Ceļvedis — izpildlaika puse: daudzskaitļi, konteksti, atliktās virknes un kļūmju režīmi sīkumos.
  • Kā tas darbojas — kāpēc msgid izskatās tieši tā un ko validācija patiesībā pārbauda.