پرش به محتویات

چرا t-string؟

چهار راه برای گذاشتن یک مقدار در یک پیام ترجمه‌پذیر، روی یک پیام واحد مقایسه شده‌اند. هر چهار روش جای‌نگهدارهایشان را نام می‌برند و به مترجم اجازهٔ جابه‌جایی‌شان را می‌دهند؛ تفاوتشان در این است که وقتی ترجمه‌ای خراب باشد چه می‌شود، کاتالوگ تا کجای برنامهٔ شما دستش می‌رسد، و پذیرفتنشان چه هزینه‌ای دارد.

جدول‌ها نخست می‌آیند تا بتوانید ردیفی را که برایتان مهم است بیابید و تنها بخشِ پشتِ آن را بخوانید.

سه طرف به هر پیام ترجمه‌شده دست می‌زنند

کاتالوگ فایل ترجمه‌هاست — تا وقتی انسان‌ها ویرایشش می‌کنند .po است و برای بار شدن در برنامه به .mo کامپایل می‌شود (آموزش هر دو را می‌پیماید). سه طرف به هر پیام دست می‌زنند: توسعه‌دهنده رشتهٔ مبدأ را می‌نویسد، مترجم کاتالوگ را ویرایش می‌کند — اغلب روی یک پلتفرم بیرونی، دور از هر بازبینیِ کد — و برنامه آن دو را در زمان اجرا با هم رندر می‌کند. هر سبک قالب‌بندی در ادامه به یک پرسش واحد پاسخ متفاوتی می‌دهد: کاتالوگ چه مقدار از زبان قالب‌بندی را می‌تواند در کنترل بگیرد؟ در مثال‌ها _ نام مرسوم تابع ترجمه است و tr نام تابع این کتابخانه.

کنار هم

وقتی مترجم اشتباه می‌کند. یک کاتالوگ از دست‌های بسیاری می‌گذرد و بیشترِ آنچه در آن خراب می‌شود تصادفی است:

%(name)s .format() flufl.i18n $name t"…"
ترجمه‌ای جای‌نگهداری را حذف می‌کند — چه رندر می‌شود؟ مقدار بی‌سروصدا ناپدید می‌شود مقدار بی‌سروصدا ناپدید می‌شود مقدار بی‌سروصدا ناپدید می‌شود پیام مبدأ، همراه با یک هشدار (به‌طور پیش‌فرض)
ترجمه‌ای جای‌نگهداری ناشناخته اضافه می‌کند — چه رندر می‌شود؟ یک استثنا یک استثنا جای‌نگهدار به‌صورت متن دیده می‌ماند پیام مبدأ، همراه با یک هشدار (به‌طور پیش‌فرض)
ترجمه‌ای جای‌نگهداری را دوباره قالب‌بندی می‌کند — چه رندر می‌شود؟ همان که کاتالوگ خواسته، یا یک استثنا اگر حرف نوع دیگر به مقدار نخورد همان که کاتالوگ خواسته در رشته‌های $ بیان‌شدنی نیست پیام مبدأ، همراه با یک هشدار
آیا جای‌نگهدارها در زمان رندر بررسی می‌شوند؟ نه نه نه بله (پایین را ببینید)

کاتالوگ چه اقتداری دارد. ترجمه داده‌ای از بیرونِ مخزن شماست و هر سبک اندازهٔ متفاوتی از قدرت به دستش می‌دهد:

%(name)s .format() flufl.i18n $name t"…"
مقدارها از کجا می‌آیند؟ یک نگاشتِ صریح آرگومان‌های صریح متغیرهای محلی و سراسریِ فراخواننده، به‌علاوهٔ extrasِ اختیاری مقدارهای ثبت‌شده درون خودِ t-string
آیا کاتالوگ می‌تواند شیوهٔ قالب‌بندی یک مقدار را تغییر دهد؟ بله بله نه نه
آیا کاتالوگ می‌تواند به درون اشیاء برسد (دسترسی به خصیصه)؟ نه بله بله، با نام‌های نقطه‌دار نه
«زبان جاری» کجا زندگی می‌کند؟ هرجا که برنامه بگذاردش هرجا که برنامه بگذاردش پشته‌ای از کدهای زبان روی شیءِ برنامهٔ مشترک یک ContextVar، به‌ازای هر تسک یا درخواست

یکپارچه‌سازی چه هزینه‌ای دارد. هر چه بالا آمد اگر ابزار جور باشد رایگان است؛ این‌جا همان‌جایی است که ممکن است نباشد:

%(name)s .format() flufl.i18n $name t"…"
کمینهٔ پایتون هر نسخه هر نسخه 3.10 3.14
بلوغ کتابخانهٔ استاندارد کتابخانهٔ استاندارد انتشار پایدار آلفا
از کاتالوگ‌های معمولی PO/MO استفاده می‌کند؟ بله بله بله بله
به استخراج‌کنندهٔ مبدأ سفارشی نیاز دارد؟ نه نه نه بله، فعلاً
Babel کدام پرچم PO را برداشت می‌کند تا ابزارهای موجود اعتبارسنجی کنند؟ python-format python-brace-format هیچ‌کدام python-brace-format

دربارهٔ بررسیِ زمانِ رندر: پیام‌های مفرد برای تطابق دقیق جای‌نگهدارها بررسی می‌شوند. پیام‌های جمع هم بررسی می‌شوند، در برابر قاعدهٔ اجتماع/اشتراک که می‌گذارد صورت‌های جمع زبان مقصد با مبدأ متفاوت باشد؛ بررسیِ سخت‌گیرانه‌ترِ به‌ازای هر صورت هنگام کامپایل کاتالوگ‌ها اجرا می‌شود (استخراج).

ردیف پرچم قالب‌بندی دربارهٔ اعتبارسنجیِ آگاه از جای‌نگهدار است، نه سازگاری کاتالوگ. «هیچ‌کدام» یعنی ابزارهای استاندارد gettext همچنان پیام را می‌خوانند و کامپایل می‌کنند، اما msgfmt --check-format هیچ دستور زبانی برای جای‌نگهدارهای $ ندارد که اعمال کند.

سازگاری و بلوغ

دو ردیف نخستِ جدول آخر همان‌هایی‌اند که پذیرش را تعیین می‌کنند؛ پس می‌ارزد به‌روشنی گفته شوند، نه در قالب خانه‌های یک جدول.

‏%-format و ‎.format()‎ در خودِ پایتون تعبیه شده‌اند و هیچ وابستگی‌ای نمی‌خواهند. flufl.i18n بسته‌ای بالغ است، منتشرشده و در کاربرد عملیاتی، که روی پایتون 3.10 به بعد اجرا می‌شود. gettext-tstrings یک آلفا است و به پایتون 3.14 یا جدیدتر نیاز دارد، چون t-string نحو تازه‌ای در 3.14 است — نه پس‌انتقالی هست و نه می‌تواند باشد. مشخصات آن بخش پایدارش است؛ APIِ پایتون هنوز ممکن است پیش از 1.0 تکان بخورد.

آنچه هیچ‌یک از آن‌ها هزینه‌اش نمی‌کند، سازگاری کاتالوگ است. هر چهار روش فایل‌های معمولی POT/PO/MO تولید می‌کنند که هر ویرایشگر PO، هر پلتفرم ترجمه و هر ابزار GNU gettext همین حالا می‌خواندشان؛ پس انتخابِ پایین بازگشت‌پذیر است، به شیوه‌ای که عوض‌کردن قالبِ کاتالوگ نبود. مهاجرت جابه‌جاکردن یک پروژهٔ موجود را پوشش می‌دهد.

بخش‌های پایین هر بده‌بستان را به‌تفصیل نشان می‌دهند، هر بار یک روش.

‏%-format

_("Hello %(name)s") % {"name": name}

چه چیزی می‌تواند خراب شود: یک جای‌نگهدارِ آسیب‌دیده به استثنای زمان اجرا بدل می‌شود، مگر آن‌که اعتبارسنجی کاتالوگ زودتر بگیردش.

رشتهٔ کاتالوگ نحو printf را با خود دارد، از جمله حرفِ نوعِ پایانی — همان s در %(name)s — که هم آسان از چشم می‌افتد و هم آسان آسیب می‌بیند:

>>> "Hello %(name)" % {"name": "Ada"}  # the trailing "s" was deleted
Traceback (most recent call last):
  ...
ValueError: incomplete format

یک ویرایش تک‌نویسه‌ای در ویرایشگر PO به استثنایی در زمان اجرا تبدیل می‌شود، مگر آن‌که اعتبارسنجی کاتالوگ زودتر بگیردش. msgfmt --check-format گنو این یکی را می‌گیرد، اما فقط برای پیام‌هایی که پرچم python-format دارند، و فقط اگر کاتالوگ در مسیرش به برنامهٔ شما واقعاً از msgfmt بگذرد.

‏str.format

_("Hello {name}").format(name=name)

این روش حرفِ نوعِ پایانی را کنار می‌گذارد و جای‌نگهداری نام‌دار و آزادانه جابه‌جاشدنی نگه می‌دارد. آنچه می‌تواند خراب شود به سوی دیگر ماجرا می‌رود: ترجمه بر اشیاء شما قدرت پیدا می‌کند.

str.format یک زبان عبارتِ کوچک است، و فراخواندنش روی یک رشته یعنی سپردن حق استفاده از آن زبان به همان رشته:

>>> "{name.__class__.__mro__}".format(name="Ada")
"(<class 'str'>, <class 'object'>)"
>>> settings.api_key = "sk-live-…"
>>> "{conf.api_key}".format(conf=settings)
'sk-live-…'

حالا آن رشته‌های تحت‌اللفظی را با هر چیزی که _() برمی‌گرداند جایگزین کنید. اگر ترجمهٔ Hello {name} به شکل {conf.api_key} بازگردد، رندر کردنش کلید API شما را چاپ می‌کند — کاتالوگ، نه کد شما، تصمیم گرفت چه چیزی خوانده شود. کاتالوگ کد نیست، اما مانند داده سفر می‌کند: به یک پلتفرم ترجمه می‌رود، از چند دست می‌گذرد، به شکل .po بازمی‌گردد، به .mo کامپایل می‌شود، و گاهی به‌کلی از بیرون پروژهٔ شما وارد می‌شود. .format() به هر ایستگاهِ این سفر، دسترسی به خصیصه‌های اشیائی را می‌دهد که شما پاس داده‌اید.

رشته‌های $ و flufl.i18n

from flufl.i18n import initialize

_ = initialize("example")

name = "Ada"
print(_("Hello $name"))  # Hello Ada — the value came from the caller's locals

کلاس string.Template در کتابخانهٔ استاندارد زبانِ درون‌یابیِ $name را فراهم می‌کند، اما خودش یک API ترجمه نیست. flufl.i18n آن سبک را با جست‌وجوی کاتالوگ gettext ترکیب می‌کند. دقت کنید که مقدار هرگز پاس داده نمی‌شود: flufl.i18n فضای نامِ جانشینی را از متغیرهای سراسری و محلیِ فراخواننده می‌سازد — هر متغیری که در محل فراخوانی وجود داشته باشد، در دسترس پیام است. یک نگاشت اختیاری extras بر هر دو مقدم است. نحوِ رو به مترجمِ آن نه حرفِ نوعِ پایانی دارد و نه مشخصهٔ قالب‌بندی، و جای‌نگهدارها آزادانه جابه‌جاشدنی می‌مانند.

جانشینیِ در دسترس نبودن، استثنا پرتاب نمی‌کند. با name = "Ada" و بدون هیچ nombre در فضای نام فراخواننده، ترجمهٔ کاتالوگیِ Hello $nombre به شکل Hello $nombre رندر می‌شود: جای‌نگهدارِ حل‌نشده دیده می‌ماند. این رفتار مستندشده باقیِ پیام ترجمه‌شده را حفظ می‌کند به‌جای آن‌که فراخوانی را شکست دهد. استثناهایی که هنگام حل یک خصیصه یا تبدیل یک مقدار برمی‌خیزند همچنان می‌توانند منتشر شوند.

flufl.i18n از یک string.Template خالی از یک جهتِ مرتبط تواناتر است. قالب سفارشیِ آن جای‌نگهدارهای نقطه‌دار مانند $settings.api_key را می‌پذیرد و مترجمِ آن این مسیرها را در برابر مقدارهای فراخواننده حل می‌کند. جای‌نگهدارِ ترجمه‌شده می‌تواند هر متغیر محلی یا سراسریِ در دسترسِ فراخواننده را نام ببرد و، با نحو نقطه‌دار، خصیصه‌هایش را بپیماید. این وقتی پیامی به خصیصه‌ای نیاز دارد راحت است، و در همان حال فریمِ فراخواننده را بخشی از فضای نامِ جانشینیِ کاتالوگ می‌کند. مقایسهٔ این‌جا flufl.i18n نسخهٔ 6.0.0 را توصیف می‌کند، نه هر کاربرد ممکنی از string.Template را.

به پرسشی هم پاسخ می‌دهد که آن دو سبک قالب‌بندی دیگر یکسره به عهدهٔ برنامه می‌گذارند: کدام زبان جاری است و چگونه باید عوضش کرد. یک شیءِ برنامه پشته‌ای از زبان‌ها را نگه می‌دارد، _.push(code) و _.pop() آن را جابه‌جا می‌کنند، with _.using(code): تودرتو می‌شود، و یک راهبرد کاتالوگِ متناظر با یک کد زبان را می‌یابد تا برنامه هرگز خودش با شیءهای کاتالوگ سروکار نداشته باشد. کارسازی که باید در یک واحدِ کاریِ واحد متن را به بیش از یک زبان تولید کند — صفحه‌ای برای خواننده، اعلانی برای کسی که حسابش زبان دیگری دارد — همان موردی است که این امکان برایش ساخته شده.

پشته روی همان شیءِ برنامه زندگی می‌کند که کل فرایند در آن شریک است. پس دو درخواستِ هم‌پوشان یک پشتهٔ واحد دارند، و بلوک‌هایی که در زمان به‌طور دقیق تودرتو نیستند، زبان اشتباه را دست هم می‌دهند:

async def greet(code, delay):
    with _.using(code):
        await asyncio.sleep(delay)
        return _("Hello $name")


async def main():
    return await asyncio.gather(greet("fr", 0.01), greet("ja", 0.02))
>>> asyncio.run(main())  # "fr" entered first and left first, so it read "ja" off the top
['こんにちは Ada', 'Bonjour Ada']

این کتابخانه همان توانایی را نگه می‌دارد — بسته‌ها به همان شکل تودرتو می‌شوند و باز می‌شوند — اما در یک ContextVar به‌جای یک پشتهٔ مشترک؛ پس درهم‌بافتگیِ بالا به‌ازای هر تسک حل می‌شود. معادل‌ها در چند زبان به‌طور هم‌زمان آمده‌اند. آنچه فراهم نمی‌کند، جست‌وجوی کاتالوگ از روی کد زبان است: شما یک شیء translations می‌دهید که در حالت متعارف یک فراخوانی gettext.translation() است، و کتابخانهٔ استاندارد کاتالوگِ تجزیه‌شده را کش می‌کند.

‏t-string

tr(t"Hello {name}")

کاتالوگ همچنان Hello {name} را می‌بیند و یک کاتالوگ معمولی PO/MO می‌ماند. تفاوت در آن است که ترجمه اجازه دارد چه بگوید، و چه کسی آن را بررسی می‌کند.

این کتابخانه هر ترجمه را پیش از رندر در برابر جای‌نگهدارهای پیام مبدأ اعتبارسنجی می‌کند و تنها نام‌های خالی را می‌پذیرد و بس. در برابر t"Hello {name}":

ترجمه‌ای که شامل این باشد با این پیام رد می‌شود
{name.__class__.__mro__} placeholder {name.__class__.__mro__} must be a plain name, copied from the source message unchanged
{name!r} placeholder {name} adds formatting; write {name} on its own, because the source message decides how the value is formatted
{0} placeholder {0} must be a plain name, copied from the source message unchanged
{nombre} translation does not match the source placeholders: {name} is missing; {nombre} is not in the source message

ردشدن به معنای کرش نیست: به‌طور پیش‌فرض کتابخانه یک هشدار ثبت می‌کند و پیام مبدأ را رندر می‌کند، پس یک کاتالوگ بد هرگز برنامه را از پا نمی‌اندازد — همان پیمانی که خود gettext نگه می‌دارد.

قالب‌بندی همان‌جا می‌ماند که نوشته شده، در کد:

amount = 1234.5
tr(t"Total: {amount:,.2f}")  # msgid is "Total: {amount}"

:,.2f هرگز به کاتالوگ نمی‌رسد؛ پس هیچ ترجمه‌ای نمی‌تواند تغییرش دهد و هیچ مترجمی مجبور نیست به آن نگاه کند. اما این قالبی ثابت است، نه بومی‌سازی‌شده — گزینش رقم‌ها و جداکننده‌ها به‌ازای هر زبان کار Babel است، پیش از فراخوانی.

یک تفاوت دیگر ابزار است: t-string نحو تازه‌ای است، پس استخراجش به درون یک .pot فعلاً به استخراج‌کننده‌ای آگاه از t-string نیاز دارد؛ مانند همانی که این بسته برای Babel فراهم می‌کند.

هزینهٔ آن محدودیت

فراتر از الزامِ نسخهٔ پایتون، بهای همهٔ این‌ها یک قاعده است: درون‌یابی باید یک نام ساده باشد.

tr(t"Hello {user.name}")  # raises InvalidTemplateError at the call site
name = user.name  # compute it first
tr(t"Hello {name}")

این یک قید واقعی است، و همان قیدی است که تضمین‌های بالا را تولید می‌کند. در کنار بستنِ مقدارها در سمت مبدأ و بررسی جای‌نگهدارها در زمان اجرا، نمی‌گذارد رشته‌های کاتالوگ عبارت ارزیابی کنند و نام جای‌نگهدارها را برای کسی که ترجمه‌شان می‌کند معنادار نگه می‌دارد.

یک f-string را اصلاً نمی‌توان این‌گونه به کار برد — تا هر کتابخانه‌ای آن را ببیند، دیگر یک رشتهٔ تمام‌شده است و ترجمه‌اش یعنی ترجمهٔ یک تکه. t-string‌ها (PEP 750) متن ثابت و مقدارها را جدا نگه می‌دارند و در همان حال نحوِ شبیه f-string و بستنِ صریح مقدارها را حفظ می‌کنند.

این‌که پایتون چگونه به این‌جا رسید — دو PEP با ده سال فاصله، و بحث کتابخانهٔ استاندارد که بی‌پاسخ بسته شد — با منابعش در پیشینه روایت شده است.