در محیط عملیاتی¶
آموزش چرخه را یک بار، تنها، روی برنامهای با یک پیام میگرداند. در یک پروژهٔ واقعی چرخه در گردش میماند: پیامها پس از ترجمهشدن تغییر میکنند، مترجم جای دیگری و با برنامهٔ خودش کار میکند، و با هر انتشار یک کاتالوگ کامپایلشده روانه میشود. این صفحه همان تمرین است — چه چیزی در مخزن میماند، چه چیزی سفر میکند، CI باید بر چه چیزی دروازه بگذارد، و زمان اجرا زبان را کجا میبندد.
آنچه همهٔ اینها روی هم میشود، شش بررسی است؛ پس نخست همانها؛ هر بخشِ پایین یکی از آنها را برپا میکند.
pybabel update --checkقبول میشود — هیچ پیامی عوض نشده بیآنکه کاتالوگها خبردار شوند.pybabel compileساخت را بر کد خروجش دروازه میکند.- مدخلهای
fuzzyِ باقیمانده عمدیاند — هر یک تا وقتی مترجمی تأییدش نکند، بهصورت متن مبدأ رندر میشود. - مجموعهآزمون هر زبانِ روانهشده را یک بار با
strict=Trueرندر میکند. - مصنوع عملیاتی فایلهای
.moدارد و Babel ندارد. - لاگرِ
gettext_tstringsبه پایش مسیریابی شده است.
شکل یک پروژه¶
myapp/
├── babel.cfg
├── pyproject.toml
├── src/
│ └── myapp/
└── locales/
├── messages.pot
├── ja/LC_MESSAGES/messages.po
└── de/LC_MESSAGES/messages.po
فایل babel.cfg، الگوی .pot و هر .po را کامیت کنید — اینها
سرچشمههای ساختِ ترجمهاند و دیفهایشان همان راهی است که تغییرهای
ترجمه را بازبینی میکنید. فایلهای .moِ کامپایلشده مصنوع ساختاند:
آنها را بهجای کامیتکردن در CI یا هنگام بستهبندی تولید کنید تا هرگز
یک .po و .moِ آن نتوانند بر سر آنچه روانه میشود اختلاف داشته
باشند.
یک فایل در هر جهت نقشی دارد: .pot پیامهای شما را به بیرون نزد
مترجمها میبرد و فایلهای .po ترجمهها را بازمیآورند. باقی این
صفحه همان چیزی است که میان آن دو جابهجا میشود.
flowchart LR
code["کد مبدأ<br>محلهای فراخوانی t-string"] -->|"pybabel extract"| pot["messages.pot"]
pot -->|"pybabel update"| po["یک .po برای هر زبان"]
po --> tr["مترجم<br>یا پلتفرم"]
tr --> po
po -->|"pybabel compile (CI)"| mo["فایلهای .mo"]
mo --> app["برنامه<br>در زمان اجرا"]
چرخه پس از نخستین ترجمه¶
pybabel initِ آموزش معمولاً یک بار اجرا میشود، هنگام افزودن یک زبان.
از آن پس چرخهٔ کاری استخراج ← بهروزرسانی ← ترجمه ← کامپایل است و مرکزش
pybabel update است که الگوی تازه را بیآنکه ترجمههای موجود را دور
بریزد، در کاتالوگهای موجود ادغام میکند.
فرض کنید خوشآمدِ Hello {name} — که پیشتر こんにちは {name} ترجمه
شده — در کد به Welcome back, {name} بازنویسی شود. استخراج و
بهروزرسانی کنید:
$ 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
اکنون کاتالوگ ژاپنی این را دارد:
#. gettext-tstrings
#: app.py:4
#, fuzzy, python-brace-format
msgid "Welcome back, {name}"
msgstr "こんにちは {name}"
Babel متوجه شد msgid تازه شبیه یکی از حذفشدههاست و آن را با ترجمهٔ
قدیمی جفت کرد — اما جفت را با پرچم fuzzy نشان گذاشت: حدسِ یک ماشین
در انتظار یک انسان. این پرچم آنچه را که کامپایل میشود عوض میکند.
pybabel compile مدخلهای fuzzy را از .mo کنار میگذارد؛ پس تا
مترجمی جفت را تأیید نکند،
برنامه متن انگلیسیِ تازه را رندر میکند، نه یک ژاپنیِ بیات را:
$ 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
بنابراین پیامِ تغییرکرده همانگونه تنزل میکند که پیامِ خراب — به زبان
مبدأ، هرگز به ترجمهای منسوخ. سهم مترجم از چرخه، بازنگری msgstr و
حذف پرچم fuzzy است؛ کامپایل بعدی مدخل را برمیدارد.
نام جاینگهدارها بخشی از هویت پیام است
msgid کلید کاتالوگ است و نام جاینگهدار درون آن است — پس تغییر
نام یک متغیر در کد (name ← user_name) msgid را عوض میکند و
ترجمهٔ آن را در همهٔ زبانها دوباره به چرخهٔ fuzzy میفرستد.
متغیرهای درونیابیشده را با واژههایی نامگذاری کنید که مترجم
بفهمد، و تنها با دلیل تغییر نام دهید.
قالببندی تصویرِ آینهای است: !r و :.2f
جزء msgid نیستند؛ پس سفت
کردن {amount:,.2f} به {amount:,.0f} در هیچ کاتالوگی چیزی را
تغییر نمیدهد. بازنویسی خودِ جمله، البته، تغییری واقعی است — همان
چرخهٔ بالا.
CI بر چه دروازه میگذارد¶
سه شکست ارزش یک بیلد قرمز را دارند: کاتالوگها از کد عقب افتادند، ترجمهای یک جاینگهدار را شکست، یا مدخلی خراب تا زمان اجرا سُرید. برای هر شکست یک گام:
- 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 چیزی بازنویسی نمیکند و وقتی کاتالوگی از الگویِ
تازهاستخراجشده عقب باشد با وضعیت ناصفر خارج میشود — نگهبانِ در برابر
ادغام کدی که پیامهایش را کسی دوباره استخراج نکرده است.
pybabel compile بررسیهای جاینگهدارِ هم Babel و هم
بررسیکنندهٔ ثبتشدهٔ
این بسته را اجرا میکند.
Babel 2.18.0: --check نمیتواند بر کاتالوگی که از بافتار استفاده میکند دروازه بگذارد
در Babel 2.18.0، pybabel update --check هر کاتالوگی را که
msgctxt دارد عقبافتاده گزارش میکند، در هر اجرا، هر قدر هم که
بهروز باشد. دروازهای که همیشه شکست میخورد بدتر از نبودِ دروازه
است، چون تیم خاموشش میکند — پس اگر اصلاً از pgettext یا
npgettext استفاده میکنید، بهجای ساختن با آن، این گام را عوض
کنید. خواندنِ الگو و هر کاتالوگ با
babel.messages.pofile.read_po و مقایسهٔ
{(m.context, m.id) for m in catalog if m.id} تمامِ آن بررسی است، و
همان کاری است که بیلدِ خودِ این وبگاه میکند. علتش
در دامها شرح داده شده است.
وضعیت خروج را بررسی کنید، نه لاگ را
pybabel compile هر خطای جاینگهدار را گزارش میکند، با وضعیت
ناصفر خارج میشود — و .mo را به هر حال مینویسد. خط لولهای
که کامپایل میکند و سپس locales/ را در ایمیج کپی میکند، کاتالوگ
خراب را روانه میکند مگر آنکه خروج ناصفر واقعاً جلویش را بگیرد.
گذاشتن اینکه آن گام بیلد را شکست دهد، مانند بالا، تمامِ راهحل
است.
خط آخر همان مجموعهآزمون همیشگی شماست، با یک عادتِ افزوده: جایی در آن، دستکم یک پیام از هر زبانِ روانهشده را از یک مترجم سختگیر بگذرانید —
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}")
— چون strict=True
آنجا استثنا میدهد که محیط عملیاتی بیصدا فرو مینشست
و یک رندرِ زمانِ اجرا تنها بررسیای است که کاتالوگ را دقیقاً همانطور
میبیند که برنامه خواهد دید، با .mo و همهچیز.
کار با مترجمها و پلتفرمها¶
فایل .po قالبِ تبادل کل جهان gettext است، و همین دلیلِ استفادهٔ
دوبارهٔ این کتابخانه از آن است: سپردن ترجمه یعنی سپردن یک فایل، چه
گیرنده همکاری با یک ویرایشگر PO باشد و چه پلتفرمی مانند Weblate یا
Crowdin. سه چیز این دستبهدستشدن را خوب از آب درمیآورد:
بگویید پیام برای چیست. توضیحی در کد همراه پیام سفر میکند — همان
که پرچم -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 ""
مترجم آن توضیح را در ویرایشگرش میبیند، کنار پیام، آن سوی جهان. این
ارزانترین اهرم کیفیت در کل گردش کار است. برای واژهای که همنامِ خودش
است — «Open»ِ دکمه در برابر «Open»ِ وضعیت — به پیام با pgettext یک
بافتار بدهید که به یک msgctxtِ نمایان
در کاتالوگ تبدیل میشود.
بگذارید پلتفرم جاینگهدارها را اعتبارسنجی کند. هر پیامِ
استخراجشده از یک t-string پرچم python-brace-format را دارد، و همان
یک خط است که QA جاینگهدار را در ابزارهایی که در کنترل شما نیستند روشن
میکند — Weblate این بررسی را مستند میکند، پلتفرمهای تجاری بررسیِ
خودشان را بر همان پرچم سوار کردهاند و msgfmt --check-format آن را در
هر خط لولهٔ گنو اعمال میکند. جزئیات، و آنچه بررسیکنندهٔ همراه فراتر
از آنها میگیرد، در
صفحهٔ استخراج
است.
به تور ایمنی فقط همانقدر اعتماد کنید که میکِشد. هر چه از یک پلتفرم بازمیگردد هنوز دادهای است که وارد بیلد شما میشود؛ دروازههای CIِ بالا همان چیزیاند که «پلتفرم احتمالاً این را بررسی کرده» را به «این نمیتواند خراب روانه شود» تبدیل میکنند.
بستن زبان در زمان اجرا¶
هر چه تا اینجا بود کاتالوگ تولید میکند. تصمیم باقیمانده این است که برنامه کجا یکی را انتخاب کند. یک بار بهازای هر قلمروِ یک زبان ببندید — برای CLI پردازه، برای سرویس وب درخواست.
یک ابزار خط فرمان یا برنامهٔ دسکتاپ محیط کاربر را یک بار، هنگام
راهاندازی میخواند. پاس ندادن languages= میگذارد کتابخانهٔ
استاندارد از روی LANGUAGE و LC_ALL و LC_MESSAGES و LANG
مذاکره کند؛ fallback=True وقتی هیچکدام با کاتالوگی که روانه
کردهاید نخواند، بهجای استثنا یک کاتالوگ تهی — متن مبدأ —
برمیگرداند.
یک برنامهٔ وب برای هر درخواست تصمیم میگیرد. هر کاتالوگ را یک بار
هنگام import بار کنید، سپس پیش از اجرای view کاتالوگِ
مذاکرهشده را به بافتار ببندید —
set_translations بافتار-محلی
است؛ پس درخواستهای همزمان به زبانهای مختلف هرگز بستهٔ یکدیگر را
نمیبینند.
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}")
زیر فریمورکهای async — از FastAPI و Starlette تا هر چیز دیگر ASGI —
درخواست را در use_translations
بپیچید: بسته در یک ContextVar زندگی میکند که تعویض تسکهای
async آن را بهازای هر درخواست حفظ میکند.
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 نمایندهٔ parse کردن Accept-Language شماست —
بیشتر فریمورکها یا زیستبومشان یکی دارند؛ آنچه اینجا مهم است،
بستن دور call_next است.
دو عادتِ زمانِ اجرا تصویر را کامل میکنند. رشتههایی که در زمان import
ساخته میشوند — برچسب یک فرم، نام نمایشی یک enum — نباید زبانی را ثبت
کنند که هنگام import فعال بود؛ آنها را با
lazy_gettext تعریف کنید تا در زبانِ
فعال هنگام استفاده رندر شوند. و لاگر gettext_tstrings را به جایی
هدایت کنید که انسانی نگاه میکند: هشدارهایش گزارشِ حالت آسانگیر از
ترجمهای است که از همهٔ دروازهها سُریده — یک خط برای هر پیام خراب، نه
یکی برای هر رندر.
روانهسازی¶
محیط عملیاتی به بسته، فایلهای .mo و دیگر هیچ نیاز دارد. Babel
وابستگیِ توسعه و CI است — gettext-tstrings[babel] را از ایمیج
عملیاتی بیرون نگه دارید و آنجا بستهٔ خالی را نصب کنید؛ رندر تنها با
کتابخانهٔ استاندارد میگردد. کاتالوگها را در همان بیلدی کامپایل کنید
که مصنوعِ استقراری را تولید میکند، تا .moهای درونش دقیقاً همان
.poهای بازبینیشده باشند و هیچچیزِ کامپایلشده روی لپتاپ کسی هرگز
روانه نشود.
چگونه جابهجا میشوند به این بستگی دارد که چه چیزی را مستقر میکنید. یک
wheel آنها را بهعنوان دادهٔ بسته با خود میبرد، و این یعنی کاتالوگها باید
درون شاخهٔ بسته زندگی کنند — src/myapp/locales/، نه یک locales/ در
ریشه — و باید به بکاند بیلد گفته شود فایلهایی را که .gitignore معمولاً
پنهانشان میکند هم بگنجاند:
آنها را از راه خودِ بسته بازخوانی کنید، نه از راه مسیری نسبت به درخت مبدأ، که همان لحظهای که wheel نصب میشود دیگر وجود ندارد:
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"])
یک ایمیج کانتینر کار آسانتری دارد: در مرحلهٔ بیلد کامپایل کنید و نتیجه را کپی کنید، و Babel را در همان مرحله جا بگذارید.
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
پیش از یک انتشار، چکلیستی که این صفحه به آن فرومیکاهد:
pybabel update --checkمیگذرد — هیچ پیامی بیآنکه کاتالوگها بشنوند تغییر نکرده است.pybabel compileبیلد را بر وضعیت خروجش دروازه میکند.- مدخلهای
fuzzyِ باقیمانده عمدیاند — هر یک تا تأیید مترجم بهصورت متن مبدأ رندر میشود. - مجموعهآزمون هر زبانِ روانهشده را یک بار با
strict=Trueرندر میکند. - مصنوعِ عملیاتی فایلهای
.moرا دارد و Babel را ندارد. - لاگر
gettext_tstringsبه پایش هدایت شده است.
گامهای بعدی¶
- استخراج — مرجعِ نیمهٔ ابزاریِ این صفحه: گزینههای نگاشت، نامهای تابع سفارشی، حالت سختگیرانه، و همهٔ بررسیکنندهها.
- راهنما — نیمهٔ زمان اجرا: صورتهای جمع، بافتارها، رشتههای معوق، و حالتهای شکست با جزئیات.
- چگونه کار میکند — چرا msgid این شکلی است، و اعتبارسنجی واقعاً چه چیزی را بررسی میکند.