Ruka hadi kwenye maudhui

Katika uzalishaji

Mafunzo huendesha mzunguko mara moja, peke yake, kwenye programu yenye ujumbe mmoja. Katika mradi halisi mzunguko huendelea kuzunguka: jumbe hubadilika baada ya kutafsiriwa, mfasiri hufanya kazi mahali pengine na kwa ratiba yake mwenyewe, na katalogi iliyokusanywa husafirishwa na kila toleo. Ukurasa huu ni utendaji huo — kinachobaki katika hazina, kinachosafiri, kinachopaswa kuzuiwa na CI, na mahali wakati wa utekelezaji unapofunga lugha.

Yote hayo hujumlika kuwa ukaguzi sita, hivyo hii hapa kwanza; kila sehemu iliyo hapa chini huweka mmojawapo.

  • pybabel update --check hupita — hakuna ujumbe uliobadilika bila katalogi kusikia habari zake.
  • pybabel compile huzuia ujenzi kwa hali yake ya kutoka.
  • Maingizo ya fuzzy yaliyobaki ni ya makusudi — kila moja huonyeshwa kama maandishi chanzo hadi mfasiri atakapolithibitisha.
  • Seti ya majaribio huonyesha kila lugha inayosafirishwa mara moja kwa strict=True.
  • Bidhaa ya uzalishaji ina mafaili ya .mo wala si Babel.
  • Kumbukumbu ya gettext_tstrings huelekezwa kwenye ufuatiliaji.

Umbo la mradi

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

Hifadhi babel.cfg, kiolezo cha .pot, na kila .po katika hazina — hivyo ndivyo vyanzo vya ujenzi wa tafsiri, na tofauti zake ndizo namna unavyopitia mabadiliko ya tafsiri. Mafaili ya .mo yaliyokusanywa ni bidhaa za ujenzi: yazalishe katika CI au wakati wa kufungasha badala ya kuyahifadhi katika hazina, ili .po na .mo yake zisiweze kamwe kutofautiana kuhusu kinachosafirishwa.

Faili moja lina jukumu kwa kila upande: .pot hubeba jumbe zako kwenda kwa wafasiri, mafaili ya .po hubeba tafsiri kurudi. Sehemu iliyobaki ya ukurasa huu ni kile kinachohama kati ya hayo mawili.

flowchart LR
  code["msimbo chanzo<br>mahali t-string zinapoitwa"] -->|"pybabel extract"| pot["messages.pot"]
  pot -->|"pybabel update"| po[".po moja kwa kila lugha"]
  po --> tr["mfasiri<br>au jukwaa"]
  tr --> po
  po -->|"pybabel compile (CI)"| mo["mafaili ya .mo"]
  mo --> app["programu<br>wakati wa utekelezaji"]

Mzunguko baada ya tafsiri ya kwanza

pybabel init ya mafunzo kwa kawaida huendeshwa mara moja, lugha inapoongezwa. Kuanzia hapo mzunguko wa kazi ni toa → sasisha → tafsiri → kusanya, na kitovu chake ni pybabel update, ambayo huingiza kiolezo kipya ndani ya katalogi zilizopo bila kutupa tafsiri zilizomo tayari.

Tuseme salamu Hello {name} — iliyotafsiriwa tayari kama こんにちは {name} — inaandikwa upya ndani ya msimbo kuwa Welcome back, {name}. Toa na usasishe:

$ 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

Katalogi ya Kijapani sasa ina:

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

Babel iliona kuwa msgid mpya inafanana na ile iliyoondolewa nayo ikaioanisha na tafsiri ya zamani — lakini ikaweka alama ya fuzzy kwenye jozi hiyo: kisio la mashine linalosubiri binadamu. Alama hiyo hubadilisha kinachokusanywa. pybabel compile huyaacha maingizo ya fuzzy nje ya .mo, hivyo hadi mfasiri athibitishe jozi hiyo, programu huonyesha maandishi mapya ya Kiingereza badala ya ya Kijapani yaliyopitwa na wakati:

$ 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

Kwa hiyo ujumbe uliobadilika hushuka kwa namna ileile ambayo ujumbe bovu hushuka — hadi lugha chanzo, kamwe si hadi tafsiri iliyopitwa na wakati. Sehemu ya mfasiri katika mzunguko ni kurekebisha msgstr na kufuta alama ya fuzzy; ukusanyaji unaofuata hulichukua ingizo hilo.

Majina ya vishika nafasi ni sehemu ya utambulisho wa ujumbe

msgid ndio ufunguo wa katalogi, na jina la kishika nafasi liko ndani yake — hivyo kubadilisha jina la kigezo ndani ya msimbo (nameuser_name) hubadilisha msgid na hupeleka tafsiri yake ya kila lugha kurudi kwenye mzunguko wa fuzzy. Yape vigezo vinavyoingizwa majina yaliyo maneno ambayo mfasiri atayaelewa, na yabadilishe kwa sababu tu.

Uumbizaji ni kioo chake: !r na :.2f si sehemu ya msgid, hivyo kukaza {amount:,.2f} kuwa {amount:,.0f} hakubadilishi chochote katika katalogi yoyote. Kuandika upya sentensi, bila shaka, ni mabadiliko halisi — huo ndio mzunguko ulio hapo juu.

Kile CI inachozuia

Kushindwa kwa aina tatu kunastahili ujenzi mwekundu: katalogi zimebaki nyuma ya msimbo, tafsiri imevunja kishika nafasi, au ingizo bovu limepenya hadi wakati wa utekelezaji. Hatua moja kwa kila kushindwa:

- 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 haiandiki upya chochote na hutoka kwa hali isiyo sifuri katalogi inapokuwa imepitwa na wakati ikilinganishwa na kiolezo kilichotolewa hivi punde — mlinzi dhidi ya kuunganisha msimbo ambao jumbe zake hakuna aliyezitoa upya. pybabel compile huendesha ukaguzi wa vishika nafasi wa Babel na wa kikaguzi kilichosajiliwa cha kifurushi hiki.

Babel 2.18.0: --check haiwezi kuzuia katalogi inayotumia miktadha

Kwenye Babel 2.18.0, pybabel update --check huripoti kila katalogi yenye msgctxt kuwa imepitwa na wakati, katika kila mzunguko, hata iwe ni mpya kiasi gani. Kizuizi kinachoshindwa daima ni kibaya kuliko kutokuwa na kizuizi kabisa, kwa sababu timu huizima — hivyo ukitumia pgettext au npgettext hata kidogo, badilisha hatua hii badala ya kuishi nayo. Kusoma kiolezo na kila katalogi kwa babel.messages.pofile.read_po na kulinganisha {(m.context, m.id) for m in catalog if m.id} ndio ukaguzi mzima, nao ndio ujenzi wa tovuti hii yenyewe unaofanya. Chanzo chake kimeandikwa katika Mitego.

Kagua hali ya kutoka, si kumbukumbu

pybabel compile huripoti kila hitilafu ya kishika nafasi, hutoka kwa hali isiyo sifuri — na huandika .mo hata hivyo. Mkondo unaokusanya kisha kunakili locales/ ndani ya taswira husafirisha katalogi bovu isipokuwa kutoka huko kusiko sifuri kunausimamisha kwelikweli. Kuiacha hatua hiyo iuangushe ujenzi, kama ilivyo hapo juu, ndiyo suluhisho zima.

Mstari wa mwisho ni seti yako ya kawaida ya majaribio, ikiwa na tabia moja imeongezwa: mahali fulani ndani yake, onyesha angalau ujumbe mmoja kwa kila lugha inayosafirishwa kupitia mfasiri mkali —

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

— kwa sababu strict=True huinua hitilafu pale uzalishaji ungerejea kimyakimya, na uonyeshaji wa wakati wa utekelezaji ndio ukaguzi pekee unaoiona katalogi sawasawa na programu itakavyoiona, pamoja na .mo na vyote.

Kufanya kazi na wafasiri na majukwaa

Faili la .po ndilo umbizo la kubadilishana la ulimwengu mzima wa gettext, ndiyo sababu maktaba hii hulitumia tena: kukabidhi tafsiri kunamaanisha kukabidhi faili, iwe mpokeaji ni mwenzako mwenye kihariri cha PO au jukwaa kama Weblate au Crowdin. Mambo matatu huufanya ukabidhi ufanye kazi vizuri:

Sema ujumbe ni wa nini. Maoni yaliyo ndani ya msimbo husafiri pamoja na ujumbe — hicho ndicho bendera ya -c "Translators:" inachokusanya:

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

Mfasiri huyaona maoni hayo katika kihariri chake, kando ya ujumbe, upande wa pili wa dunia. Ndicho kishikizo cha bei rahisi zaidi cha ubora katika mtiririko mzima wa kazi. Kwa neno lenye sura moja na maana mbili — "Open" kitufe dhidi ya "Open" hali — mpe ujumbe muktadha kwa pgettext, ambao huwa msgctxt inayoonekana ndani ya katalogi.

Liache jukwaa lithibitishe vishika nafasi. Kila ujumbe uliotolewa kutoka t-string hubeba bendera ya python-brace-format, na mstari huo mmoja ndio unaowasha QA ya vishika nafasi katika zana usizozidhibiti — Weblate huandika ukaguzi huo, majukwaa ya kibiashara huegemeza yao juu ya bendera ileile, na msgfmt --check-format huulazimisha katika mkondo wowote wa GNU. Maelezo, na kile kikaguzi kilichoambatishwa hukamata zaidi yake, viko kwenye ukurasa wa utoaji.

Amini wavu wa usalama kwa kadiri unavyofika tu. Chochote kinachorudi kutoka jukwaani bado ni data inayoingia katika ujenzi wako; vizuizi vya CI vilivyo hapo juu ndivyo vinavyogeuza "labda jukwaa lilikagua hili" kuwa "hili haliwezi kusafirishwa likiwa bovu".

Kufunga lugha wakati wa utekelezaji

Kila kitu hadi sasa huzalisha katalogi. Uamuzi uliobaki ni mahali programu inapochagua moja. Funga mara moja kwa kila wigo wa lugha — mchakato kwa CLI, ombi kwa huduma ya wavuti.

Zana ya mstari wa amri au programu ya kompyuta mezani husoma mazingira ya mtumiaji mara moja, wakati wa kuanza. Kutopitisha languages= huiacha maktaba sanifu ijadiliane kutoka LANGUAGE, LC_ALL, LC_MESSAGES, na LANG; fallback=True hurudisha katalogi tupu — maandishi chanzo — badala ya kuinua hitilafu wakati hakuna kati yao inayolingana na katalogi unayosafirisha.

import gettext

from gettext_tstrings import Translator

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

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

Programu ya wavuti huamua kwa kila ombi. Pakia kila katalogi mara moja wakati wa kuingiza moduli, kisha funga ile iliyojadiliwa kwenye muktadha kabla mwonekano haujaendeshwa — set_translations ni ya muktadha wa ndani, hivyo maombi yanayoendeshwa sambamba kwa lugha tofauti kamwe hayaoni ufungaji wa mwenzake.

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

Chini ya mifumo isiyolandanishwa — FastAPI, Starlette, na kingine chochote cha ASGI — zungushia ombi use_translations: ufungaji hukaa ndani ya ContextVar, ambayo ubadilishaji wa kazi zisizolandanishwa huihifadhi kwa kila ombi.

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 husimama badala ya uchanganuzi wako wa Accept-Language — mifumo mingi au mifumo ikolojia yao hutoa mmoja; kinachohusika hapa ni ufungaji unaozunguka call_next.

Tabia mbili za wakati wa utekelezaji hukamilisha picha. Mifuatano iliyotengenezwa wakati wa kuingiza moduli — lebo ya fomu, jina la kuonyesha la enum — haipaswi kunasa lugha yoyote iliyokuwa hai wakati wa kuingiza; ibainishe kwa lazy_gettext nayo itaonyeshwa kwa lugha iliyo hai wakati wa matumizi. Na elekeza kiandikaji kumbukumbu cha gettext_tstrings mahali ambapo binadamu huangalia: maonyo yake ni hali ya kuvumilia ikiripoti tafsiri iliyopenya vizuizi vyote, mstari mmoja kwa kila ujumbe bovu badala ya mmoja kwa kila uonyeshaji.

Usafirishaji

Uzalishaji unahitaji kifurushi, mafaili ya .mo, na hakuna kingine. Babel ni kitegemezi cha usanidi na CI — weka gettext-tstrings[babel] nje ya taswira ya uzalishaji na usakinishe kifurushi tupu hapo; uonyeshaji huendeshwa kwa maktaba sanifu peke yake. Kusanya katalogi ndani ya ujenzi uleule unaozalisha bidhaa unayoisambaza, ili mafaili ya .mo yaliyomo yawe hasa mafaili ya .po yaliyopitiwa, na kusiwe na kilichokusanywa kwenye kompyuta ya mtu kinachosafiri kamwe.

Jinsi yanavyosafiri hutegemea unachosambaza. Wheel hubeba mafaili hayo kama data ya kifurushi, maana yake katalogi lazima zikae ndani ya saraka ya kifurushi — src/myapp/locales/, si locales/ ya ngazi ya juu — na injini ya ujenzi lazima iambiwe kujumuisha mafaili ambayo .gitignore huyaficha kwa kawaida:

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

Yasome kupitia kifurushi badala ya kupitia njia inayohusiana na mti wa chanzo, ambao huacha kuwepo mara tu wheel inaposakinishwa:

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

Taswira ya kontena ina kazi rahisi zaidi: kusanya wakati wa hatua ya ujenzi na nakili matokeo, ukiacha Babel nyuma katika hatua hiyo.

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

Kabla ya toleo, orodha hakiki ambayo ukurasa huu hujikita ndani yake:

  • pybabel update --check hupita — hakuna ujumbe uliobadilika bila katalogi kupata habari.
  • pybabel compile huzuia ujenzi kwa kutegemea hali yake ya kutoka.
  • Maingizo ya fuzzy yaliyobaki ni ya makusudi — kila moja huonyeshwa kama maandishi chanzo hadi mfasiri alithibitishe.
  • Seti ya majaribio huonyesha kila lugha inayosafirishwa mara moja kwa strict=True.
  • Bidhaa ya uzalishaji ina mafaili ya .mo na haina Babel.
  • Kiandikaji kumbukumbu cha gettext_tstrings kimeelekezwa kwenye ufuatiliaji.

Wapi kuendelea

  • Utoaji — marejeo ya nusu ya zana ya ukurasa huu: machaguo ya ramani, majina maalum ya vitendakazi, hali kali, na kila kikaguzi.
  • Mwongozo — nusu ya wakati wa utekelezaji: wingi, miktadha, mifuatano iliyoahirishwa, na namna za kushindwa kwa kina.
  • Jinsi inavyofanya kazi — kwa nini msgid inaonekana hivyo ilivyo, na uthibitishaji hukagua nini hasa.