विषय पर बढ़ें

t-strings क्यों

एक अनुवाद-योग्य संदेश में value रखने के चार तरीक़े, एक ही संदेश पर तुलना किए हुए। चारों अपने placeholders को नाम देते हैं और अनुवादक को उनका क्रम बदलने देते हैं; फ़र्क़ इसमें है कि अनुवाद ग़लत होने पर क्या होता है, कैटलॉग आपके प्रोग्राम के कितने हिस्से तक पहुँच सकता है, और उन्हें अपनाने की क़ीमत क्या है।

तालिकाएँ पहले आती हैं, ताकि आप अपने काम की पंक्ति ढूँढ़कर सिर्फ़ उसके पीछे वाला भाग पढ़ सकें।

हर अनूदित संदेश को तीन पक्ष छूते हैं

कैटलॉग अनुवादों की फ़ाइल है — मनुष्य जब उसे संपादित करते हैं तब .po, और एप्लिकेशन के लोड करने के लिए .mo में कंपाइल (ट्यूटोरियल दोनों से गुज़रता है)। हर संदेश को तीन पक्ष छूते हैं: डेवलपर स्रोत स्ट्रिंग लिखता है, अनुवादक कैटलॉग संपादित करता है — अक्सर किसी बाहरी प्लेटफ़ॉर्म पर, किसी भी कोड समीक्षा से दूर — और एप्लिकेशन रनटाइम पर दोनों को साथ रेंडर करता है। नीचे की हर फ़ॉर्मैटिंग शैली एक ही सवाल का अलग उत्तर देती है: फ़ॉर्मैट भाषा का कितना हिस्सा कैटलॉग के नियंत्रण में जाता है? उदाहरणों में _ अनुवाद फ़ंक्शन का पारंपरिक नाम है, और tr इस लाइब्रेरी का।

आमने-सामने

जब अनुवादक से कोई ग़लती हो जाए। कैटलॉग कई हाथों से होकर गुज़रता है, और उसमें जो ग़लत होता है उसका अधिकांश अनजाने में होता है:

%(name)s .format() flufl.i18n $name t"…"
अनुवाद कोई placeholder छोड़ दे — क्या रेंडर होता है? value चुपचाप ग़ायब हो जाती है value चुपचाप ग़ायब हो जाती है value चुपचाप ग़ायब हो जाती है स्रोत संदेश, एक चेतावनी के साथ (डिफ़ॉल्ट रूप से)
अनुवाद कोई अज्ञात placeholder जोड़ दे — क्या रेंडर होता है? एक exception एक exception placeholder टेक्स्ट के रूप में दिखता रहता है स्रोत संदेश, एक चेतावनी के साथ (डिफ़ॉल्ट रूप से)
अनुवाद किसी placeholder की फ़ॉर्मैटिंग बदल दे — क्या रेंडर होता है? जो कैटलॉग ने माँगा, या exception यदि type-अक्षर अब value पर फ़िट नहीं बैठता जो कैटलॉग ने माँगा $-strings में यह कहा ही नहीं जा सकता स्रोत संदेश, एक चेतावनी के साथ
क्या रेंडर के समय placeholders की जाँच होती है? नहीं नहीं नहीं हाँ (नीचे देखें)

कैटलॉग के पास कितना अधिकार है। अनुवाद आपके रिपॉज़िटरी के बाहर से आया डेटा है, और हर शैली उसे अलग-अलग मात्रा में शक्ति सौंपती है:

%(name)s .format() flufl.i18n $name t"…"
values कहाँ से आती हैं? एक स्पष्ट mapping स्पष्ट arguments कॉलर के local और global वेरिएबल, साथ में वैकल्पिक extras t-string के भीतर कैप्चर की गई values
क्या कैटलॉग value की फ़ॉर्मैटिंग बदल सकता है? हाँ हाँ नहीं नहीं
क्या कैटलॉग ऑब्जेक्ट्स के भीतर पहुँच सकता है (attribute access)? नहीं हाँ हाँ, dotted नामों से नहीं
"वर्तमान भाषा" कहाँ रहती है? जहाँ एप्लिकेशन उसे रखे जहाँ एप्लिकेशन उसे रखे साझा application object पर भाषा-कोड का एक स्टैक एक ContextVar, प्रति task या request

एकीकरण की क़ीमत क्या है। ऊपर का सब कुछ मुफ़्त है — बशर्ते टूलिंग फ़िट बैठे; यहीं वह जगह है जहाँ शायद न बैठे:

%(name)s .format() flufl.i18n $name t"…"
न्यूनतम Python कोई भी कोई भी 3.10 3.14
परिपक्वता मानक लाइब्रेरी मानक लाइब्रेरी स्थिर रिलीज़ alpha
क्या साधारण PO/MO कैटलॉग उपयोग होते हैं? हाँ हाँ हाँ हाँ
क्या custom सोर्स एक्सट्रैक्टर चाहिए? नहीं नहीं नहीं हाँ, फ़िलहाल
Babel कौन-सा PO फ़्लैग अनुमानित करता है, जिससे मौजूदा टूल सत्यापन करें? python-format python-brace-format कोई नहीं python-brace-format

रेंडर-समय जाँच के बारे में: एकवचन संदेशों की placeholder की सटीक बराबरी की जाँच होती है। बहुवचन संदेशों की भी होती है, उस union/intersection नियम के विरुद्ध जो लक्ष्य भाषा के बहुवचन रूपों को स्रोत से भिन्न होने देता है; प्रति-रूप सख़्त जाँच कैटलॉग कंपाइल होते समय चलती है (एक्सट्रैक्शन)।

format-flag वाली पंक्ति placeholder-सचेत सत्यापन के बारे में है, कैटलॉग संगतता के बारे में नहीं। कोई नहीं का अर्थ है कि मानक gettext टूल संदेश को पढ़ते और कंपाइल तो करते हैं, पर msgfmt --check-format के पास लागू करने के लिए कोई $-placeholder व्याकरण नहीं है।

संगतता और परिपक्वता

पिछली तालिका की पहली दो पंक्तियाँ ही अपनाने का निर्णय तय करती हैं, इसलिए उन्हें खानों में छिपाने की बजाय साफ़-साफ़ कह देना बेहतर है।

%-format और .format() Python में अंतर्निहित हैं और उन्हें किसी निर्भरता की ज़रूरत ही नहीं। flufl.i18n एक परिपक्व पैकेज है, जो रिलीज़ हो चुका है और प्रोडक्शन में उपयोग होता है, तथा Python 3.10 और बाद पर चलता है। gettext-tstrings एक alpha है और उसे Python 3.14 या नया चाहिए, क्योंकि t-strings 3.14 का नया सिंटैक्स हैं — कोई back-port नहीं है और हो भी नहीं सकता। इसका विनिर्देश ही इसका स्थिर हिस्सा है; Python API 1.0 से पहले अभी बदल सकती है।

जो क़ीमत इनमें से कोई नहीं माँगता, वह है कैटलॉग संगतता। चारों साधारण POT/PO/MO फ़ाइलें बनाते हैं जिन्हें हर PO एडिटर, अनुवाद प्लेटफ़ॉर्म और GNU gettext टूल पहले से पढ़ लेता है, इसलिए नीचे का चुनाव उस तरह पलटा जा सकता है जैसे कैटलॉग का फ़ॉर्मैट बदलने पर कभी नहीं पलटा जा सकता। मौजूदा प्रोजेक्ट को ले जाने की बात माइग्रेशन पर है।

नीचे के भाग हर समझौते को विस्तार से दिखाते हैं, एक-एक तरीक़ा करके।

%-format

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

क्या ग़लत हो सकता है: क्षतिग्रस्त प्लेसहोल्डर रनटाइम exception बन जाता है, बशर्ते कैटलॉग सत्यापन उसे पहले न पकड़ ले।

कैटलॉग स्ट्रिंग printf सिंटैक्स ढोती है, जिसमें अंत में एक type-अक्षर भी है — %(name)s का s — जिसे अनदेखा करना आसान है और नुक़सान पहुँचाना भी:

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

PO एडिटर में एक-अक्षर का संपादन रनटाइम exception बन जाता है, बशर्ते कैटलॉग सत्यापन उसे पहले न पकड़ ले। GNU msgfmt --check-format इसे पकड़ तो लेता है, पर केवल python-format फ़्लैग वाले संदेशों के लिए, और तभी जब कैटलॉग आपके एप्लिकेशन तक पहुँचने से पहले वास्तव में msgfmt से होकर गुज़रे।

str.format

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

यह अंतिम type-अक्षर हटा देता है और नामित, स्वतंत्र रूप से पुनर्क्रमित होने योग्य placeholder रखता है। जो ग़लत हो सकता है, वह लेन-देन के दूसरे छोर पर चला जाता है: अनुवाद को आपके ऑब्जेक्ट्स पर अधिकार मिल जाता है।

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 key छाप देता है — क्या पढ़ा गया, यह आपके कोड ने नहीं, कैटलॉग ने तय किया। कैटलॉग कोड नहीं है, पर डेटा की तरह यात्रा करता है: अनुवाद प्लेटफ़ॉर्म तक बाहर, कई हाथों से होकर, .po बनकर वापस, .mo में कंपाइल, कभी-कभी पूरी तरह आपके प्रोजेक्ट से बाहर से vendored। .format() उस यात्रा के हर चरण को आपके पास किए गए ऑब्जेक्ट्स पर attribute पहुँच दे देता है।

$-strings और 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 कैटलॉग लुकअप से जोड़ता है। ध्यान दें कि value कभी पास नहीं की जाती: flufl.i18n प्रतिस्थापन namespace को कॉलर के globals और locals से बनाता है — कॉल स्थल पर जो भी वेरिएबल मौजूद हैं, वे संदेश को उपलब्ध हैं। एक वैकल्पिक extras mapping दोनों पर वरीयता पाती है। इसकी अनुवादक-सम्मुख सिंटैक्स में कोई अंतिम type-अक्षर या फ़ॉर्मैट स्पेसिफ़ायर नहीं है, और placeholders स्वतंत्र रूप से पुनर्क्रमित किए जा सकते हैं।

अनुपलब्ध प्रतिस्थापन कोई exception नहीं उठाता। name = "Ada" के साथ और कॉलर के namespace में कोई nombre न होने पर, कैटलॉग का अनुवाद Hello $nombre Hello $nombre के रूप में ही रेंडर होता है: अनसुलझा placeholder दिखता रहता है। यह प्रलेखित व्यवहार कॉल को विफल करने की बजाय अनूदित संदेश के शेष भाग को बचा लेता है। किसी attribute को सुलझाते या value बदलते समय उठे exceptions फिर भी आगे बढ़ सकते हैं।

एक प्रासंगिक अर्थ में flufl.i18n नंगे string.Template से अधिक सक्षम है। इसका custom Template $settings.api_key जैसे dotted placeholders स्वीकार करता है, और इसका translator उन पथों को कॉलर की values के विरुद्ध सुलझाता है। अनूदित placeholder कॉलर के किसी भी उपलब्ध local या global का नाम ले सकता है और dotted सिंटैक्स से उसके attributes में उतर सकता है। जब किसी संदेश को attribute चाहिए तो यह सुविधाजनक है, पर साथ ही कॉलर का frame कैटलॉग के प्रतिस्थापन namespace का हिस्सा बन जाता है। यहाँ की तुलना flufl.i18n 6.0.0 का वर्णन करती है, string.Template के हर संभव उपयोग का नहीं।

यह उस सवाल का जवाब भी देता है जिसे बाक़ी दोनों फ़ॉर्मैटिंग शैलियाँ पूरी तरह एप्लिकेशन पर छोड़ देती हैं: इस समय कौन-सी भाषा सक्रिय है, और उसे बदला कैसे जाए। कोई application object भाषाओं का एक स्टैक रखता है, _.push(code) और _.pop() उसे हिलाते हैं, with _.using(code): उसे नेस्ट करता है, और कोई strategy किसी भाषा-कोड के लिए कैटलॉग ढूँढ़ लेती है ताकि एप्लिकेशन को कैटलॉग ऑब्जेक्ट कभी ख़ुद न सँभालने पड़ें। जिस सर्वर को काम की एक ही इकाई के दौरान एक से अधिक भाषाओं में टेक्स्ट बनाना पड़ता है — पाठक के लिए एक पेज, और किसी ऐसे व्यक्ति के लिए सूचना जिसका खाता किसी दूसरी भाषा पर सेट है — यही वह स्थिति है जिसके लिए यह मौजूद है।

वह स्टैक उसी application object पर रहता है, जिसे पूरी प्रक्रिया साझा करती है। इसलिए दो अतिव्यापी requests एक ही स्टैक साझा करते हैं, और जो ब्लॉक समय में सख़्ती से नेस्टेड नहीं हैं वे एक-दूसरे को ग़लत भाषा थमा देते हैं:

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']

यह लाइब्रेरी वही क्षमता — bindings उसी तरह नेस्ट होती और उसी तरह खुलती हैं — किसी साझा स्टैक की बजाय एक ContextVar में रखती है, इसलिए ऊपर वाला अंतर्ग्रथन प्रति task सुलझता है। इसके समतुल्य एक साथ कई भाषाएँ पर हैं। जो यह नहीं देती वह है भाषा-कोड-से-कैटलॉग खोज: आप एक translations ऑब्जेक्ट देते हैं, जो आम स्थिति में gettext.translation() की एक ही कॉल है, और पार्स किए हुए कैटलॉग को मानक लाइब्रेरी कैश कर लेती है।

t-strings

tr(t"Hello {name}")

कैटलॉग को अब भी Hello {name} ही दिखता है और वह एक साधारण PO/MO कैटलॉग बना रहता है। फ़र्क़ इसमें है कि अनुवाद को क्या कहने की अनुमति है, और उसकी जाँच कौन करता है।

यह लाइब्रेरी रेंडर करने से पहले हर अनुवाद की स्रोत संदेश के placeholders के विरुद्ध जाँच करती है, और सिर्फ़ नंगे नाम स्वीकार करती है, और कुछ नहीं। 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-strings नया सिंटैक्स हैं, इसलिए उन्हें .pot में एक्सट्रैक्ट करने के लिए फ़िलहाल t-string-सचेत एक्सट्रैक्टर चाहिए, जैसे वह जो यह पैकेज Babel के लिए देता है

इस प्रतिबंध की क़ीमत

Python की अनिवार्यता के अलावा इस सबकी क़ीमत एक ही नियम है: इंटरपोलेशन को सादा नाम होना ही होगा।

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

यह एक वास्तविक बाधा है, और यही वह बाधा है जो ऊपर की गारंटियाँ पैदा करती है। स्रोत-पक्ष value binding और रनटाइम placeholder जाँच के साथ मिलकर यह कैटलॉग स्ट्रिंग्स को एक्सप्रेशन का मूल्यांकन करने से रोकती है और placeholder नामों को उस व्यक्ति के लिए अर्थपूर्ण रखती है जो उनका अनुवाद कर रहा है।

कोई f-string इस तरह बिलकुल उपयोग नहीं हो सकता — किसी भी लाइब्रेरी तक पहुँचते-पहुँचते वह एक पूर्ण स्ट्रिंग बन चुका होता है, इसलिए उसका अनुवाद टुकड़े का अनुवाद है। t-strings (PEP 750) स्थिर टेक्स्ट और values को अलग रखते हुए f-string-जैसा सिंटैक्स और स्पष्ट value binding बनाए रखते हैं।

Python यहाँ तक कैसे पहुँचा — दस साल के अंतराल पर दो PEP, और stdlib की वह चर्चा जो बिना उत्तर के बंद हुई — यह स्रोतों सहित पृष्ठभूमि पर बताया गया है।