t-strings কেন¶
একটি অনুবাদযোগ্য বার্তায় মান বসানোর চারটি উপায়, একই বার্তার উপর তুলনা করে দেখা। চারটিই তাদের placeholder-এ নাম দেয় আর অনুবাদককে সেগুলি পুনর্বিন্যাস করতে দেয়; তারা আলাদা হয় অনুবাদ ভুল হলে কী ঘটে তাতে, আপনার প্রোগ্রামের কতটা পর্যন্ত ক্যাটালগ পৌঁছতে পারে তাতে, আর তাদের গ্রহণ করার দাম কত তাতে।
টেবিলগুলি আগে দেওয়া হল, যাতে আপনি নিজের কাজের সারিটি খুঁজে নিয়ে কেবল তার পিছনের অংশটুকুই পড়তে পারেন।
প্রতিটি অনূদিত বার্তায় তিন পক্ষের হাত পড়ে
ক্যাটালগ হল অনুবাদের ফাইল — মানুষ যতক্ষণ সম্পাদনা করে ততক্ষণ .po,
আর অ্যাপ্লিকেশনের লোড করার জন্য কম্পাইল হয়ে .mo
(টিউটোরিয়াল দুটির মধ্য দিয়েই হাঁটে)। প্রতিটি বার্তায় তিন
পক্ষের হাত পড়ে: ডেভেলপার উৎস স্ট্রিংটি লেখেন, একজন অনুবাদক
ক্যাটালগ সম্পাদনা করেন — সচরাচর কোনও বাইরের প্ল্যাটফর্মে, কোড রিভিউ থেকে
বহু দূরে — আর অ্যাপ্লিকেশন রানটাইমে দুটিকে একসঙ্গে রেন্ডার করে।
নিচের প্রতিটি ফরম্যাটিং শৈলী একই প্রশ্নের ভিন্ন উত্তর দেয়: ফরম্যাট
ভাষার কতটা ক্যাটালগের নিয়ন্ত্রণে থাকবে? উদাহরণগুলিতে _ হল অনুবাদ
ফাংশনের প্রচলিত নাম, আর tr এই লাইব্রেরির নাম।
পাশাপাশি¶
অনুবাদক যখন ভুল করেন। একটি ক্যাটালগ বহু হাত ঘুরে আসে, আর তাতে যা যা ভুল হয় তার বেশিরভাগই অনিচ্ছাকৃত:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| অনুবাদ একটি placeholder বাদ দিলে — কী রেন্ডার হয়? | মানটি নীরবে উধাও হয় | মানটি নীরবে উধাও হয় | মানটি নীরবে উধাও হয় | উৎস বার্তা, একটি সতর্কবার্তাসহ (ডিফল্টে) |
| অনুবাদ একটি অজানা placeholder যোগ করলে — কী রেন্ডার হয়? | একটি এক্সেপশন | একটি এক্সেপশন | placeholder-টি টেক্সট হিসেবে দৃশ্যমান থাকে | উৎস বার্তা, একটি সতর্কবার্তাসহ (ডিফল্টে) |
| অনুবাদ একটি placeholder নতুন করে ফরম্যাট করলে — কী রেন্ডার হয়? | ক্যাটালগ যা চেয়েছে তা-ই, আর টাইপ-অক্ষরটি মানের সঙ্গে না মিললে একটি এক্সেপশন | ক্যাটালগ যা চেয়েছে তা-ই | $-strings-এ এটি প্রকাশই করা যায় না |
উৎস বার্তা, একটি সতর্কবার্তাসহ |
| রেন্ডারের সময় কি placeholder যাচাই হয়? | না | না | না | হ্যাঁ (নিচে দেখুন) |
ক্যাটালগের হাতে কতটা কর্তৃত্ব। একটি অনুবাদ আপনার রিপোজিটরির বাইরের ডেটা, আর প্রতিটি শৈলী তার হাতে ভিন্ন পরিমাণ ক্ষমতা তুলে দেয়:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| মান আসে কোথা থেকে? | একটি স্পষ্ট ম্যাপিং | স্পষ্ট আর্গুমেন্ট | কলারের local ও global ভেরিয়েবল, সঙ্গে ঐচ্ছিক extras |
t-string-এর ভিতরে ধরা মানগুলি |
| ক্যাটালগ কি একটি মান কীভাবে ফরম্যাট হবে তা বদলাতে পারে? | হ্যাঁ | হ্যাঁ | না | না |
| ক্যাটালগ কি অবজেক্টের ভিতরে পৌঁছতে পারে (অ্যাট্রিবিউট অ্যাক্সেস)? | না | হ্যাঁ | হ্যাঁ, ডট-যুক্ত নাম দিয়ে | না |
| "বর্তমান ভাষা" থাকে কোথায়? | অ্যাপ্লিকেশন যেখানে রাখে সেখানে | অ্যাপ্লিকেশন যেখানে রাখে সেখানে | ভাগ-করা অ্যাপ্লিকেশন অবজেক্টের উপর ভাষা-কোডের একটি স্ট্যাক | একটি ContextVar, প্রতি টাস্ক বা request-পিছু |
ইন্টিগ্রেশনের দাম কত। উপরের সবটুকুই বিনামূল্যে, যদি টুলিং মানানসই হয়; এখানেই সে না-মানানসই হতে পারে:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| ন্যূনতম Python | যেকোনও | যেকোনও | 3.10 | 3.14 |
| পরিণতি | স্ট্যান্ডার্ড লাইব্রেরি | স্ট্যান্ডার্ড লাইব্রেরি | স্থিতিশীল রিলিজ | alpha |
| সাধারণ PO/MO ক্যাটালগ ব্যবহার করে? | হ্যাঁ | হ্যাঁ | হ্যাঁ | হ্যাঁ |
| আলাদা সোর্স এক্সট্র্যাক্টর লাগে? | না | না | না | হ্যাঁ, আপাতত |
| প্রচলিত টুলের যাচাইয়ের জন্য Babel কোন PO ফ্ল্যাগ অনুমান করে? | python-format |
python-brace-format |
কোনওটিই নয় | python-brace-format |
রেন্ডার-সময়ের যাচাই প্রসঙ্গে: একবচন বার্তায় placeholder-এর হুবহু মিল দেখা হয়। বহুবচন বার্তাও যাচাই হয়, সেই union/intersection নিয়মের বিপরীতে, যা লক্ষ্য ভাষার বহুবচন রূপকে উৎসের থেকে আলাদা হতে দেয়; আর ক্যাটালগ কম্পাইল হওয়ার সময় প্রতি-রূপের কড়া যাচাইটি চলে (এক্সট্র্যাকশন)।
ফরম্যাট-ফ্ল্যাগের সারিটি 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 নেই, আর হতেও পারে না। তার স্পেসিফিকেশন-ই তার
স্থিতিশীল অংশ; 1.0-এর আগে Python API এখনও নড়তে পারে।
এদের কারও যে দামটি নেই তা হল ক্যাটালগ সামঞ্জস্য। চারটিই সাধারণ POT/PO/MO ফাইল তৈরি করে, যা প্রতিটি PO এডিটর, অনুবাদ প্ল্যাটফর্ম ও GNU gettext টুল আগে থেকেই পড়ে, তাই নিচের পছন্দটি এমনভাবে ফিরিয়ে নেওয়া যায় যা ক্যাটালগের ফরম্যাট বদলালে যেত না। বিদ্যমান কোনও প্রকল্প সরানোর কথা আছে মাইগ্রেশন-এ।
নিচের অংশগুলি প্রতিটি সমঝোতাই বিস্তারিতভাবে দেখায়, একবারে একটি পদ্ধতি।
%-format¶
কী ভুল হতে পারে: নষ্ট হয়ে যাওয়া একটি প্লেসহোল্ডার রানটাইম এক্সেপশনে পরিণত হয়, যদি না ক্যাটালগ যাচাই তার আগেই সেটি ধরে ফেলে।
ক্যাটালগ স্ট্রিং printf সিনট্যাক্স বয়ে নিয়ে চলে, শেষে টাইপ-অক্ষরসহ —
%(name)s-এর সেই s — যা চোখ এড়িয়ে যাওয়া সহজ আর নষ্ট করাও সহজ:
>>> "Hello %(name)" % {"name": "Ada"} # the trailing "s" was deleted
Traceback (most recent call last):
...
ValueError: incomplete format
PO এডিটরে এক অক্ষরের সম্পাদনা রানটাইমে একটি এক্সেপশন হয়ে দাঁড়ায়, যদি না
ক্যাটালগ যাচাই আগেই তা ধরে ফেলে। GNU msgfmt --check-format এটি ধরে বটে,
কিন্তু কেবল python-format ফ্ল্যাগ পাওয়া
বার্তাগুলির জন্যই, আর কেবল তখনই যদি ক্যাটালগটি আপনার অ্যাপ্লিকেশনে পৌঁছনোর
পথে সত্যিই msgfmt-এর মধ্য দিয়ে যায়।
str.format¶
এটি শেষের টাইপ-অক্ষরটি সরিয়ে দেয়, অথচ নাম-যুক্ত ও অবাধে পুনর্বিন্যাসযোগ্য 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 কী ছাপা হয়ে যাবে — কী পড়া হবে তা ঠিক করল ক্যাটালগ, আপনার কোড নয়।
ক্যাটালগ কোড নয়, কিন্তু সে ভ্রমণ করে ডেটার মতো: বেরিয়ে যায় অনুবাদ
প্ল্যাটফর্মে, কয়েক হাত ঘুরে ফেরে .po হয়ে, কম্পাইল হয় .mo-তে, কখনও কখনও
আপনার প্রকল্পের একেবারে বাইরে থেকে vendor করা হয়। .format() সেই যাত্রার
প্রতিটি ধাপকে আপনার পাঠানো অবজেক্টে অ্যাট্রিবিউট অ্যাক্সেস দিয়ে দেয়।
$-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 ক্যাটালগ লুকআপের সঙ্গে মেলায়।
লক্ষ করুন, মানটি কখনও পাঠানো হয় না: flufl.i18n প্রতিস্থাপনের নেমস্পেস গড়ে
কলারের globals ও locals থেকে — কল-সাইটে যে ভেরিয়েবলগুলি আছে, বার্তা তাদের
সবাইকেই পায়। একটি ঐচ্ছিক extras ম্যাপিং দুটির উপরেই অগ্রাধিকার পায়।
অনুবাদকের চোখে পড়া সিনট্যাক্সে শেষের টাইপ-অক্ষর বা ফরম্যাট স্পেসিফায়ার নেই,
আর placeholder অবাধে পুনর্বিন্যাসযোগ্যই থাকে।
অনুপলব্ধ কোনও প্রতিস্থাপন এক্সেপশন তোলে না। name = "Ada" থাকলে আর কলারের
নেমস্পেসে nombre না থাকলে Hello $nombre-এর ক্যাটালগ অনুবাদ রেন্ডার হয়
Hello $nombre হিসেবেই: অমীমাংসিত placeholder দৃশ্যমানই থেকে যায়। সেই
নথিভুক্ত আচরণ কলটি ব্যর্থ করার বদলে অনূদিত বার্তার
বাকিটা রক্ষা করে। অ্যাট্রিবিউট মীমাংসা বা মান রূপান্তরের সময় ওঠা এক্সেপশন
এখনও ছড়াতে পারে।
একটি প্রাসঙ্গিক দিক থেকে flufl.i18n নিছক string.Template-এর চেয়ে বেশি
সক্ষম। তার কাস্টম Template $settings.api_key-এর মতো
ডট-যুক্ত placeholder গ্রহণ করে, আর তার translator সেই পথগুলিকে কলারের
মানের বিপরীতে মীমাংসা করে। একটি অনূদিত placeholder কলারের যেকোনও উপলব্ধ
local বা global-এর নাম নিতে পারে এবং ডট-যুক্ত সিনট্যাক্স দিয়ে তার
অ্যাট্রিবিউট ধরে এগোতে পারে। কোনও বার্তার যখন একটি অ্যাট্রিবিউট দরকার তখন
এটি সুবিধাজনক, একই সঙ্গে কলারের ফ্রেমকে ক্যাটালগের প্রতিস্থাপন-নেমস্পেসের
অংশও করে তোলে। এখানকার তুলনাটি flufl.i18n 6.0.0 নিয়ে, string.Template-এর
সম্ভাব্য প্রতিটি ব্যবহার নিয়ে নয়।
অন্য দুটি ফরম্যাটিং শৈলী যে প্রশ্নটি পুরোপুরি অ্যাপ্লিকেশনের হাতে ছেড়ে দেয়,
এটি তারও উত্তর দেয়: কোন ভাষা এখন চলছে, আর তা বদলানো যায় কীভাবে। একটি
অ্যাপ্লিকেশন অবজেক্ট ভাষার একটি স্ট্যাক ধরে রাখে,
_.push(code) ও _.pop() তাকে নাড়ায়, with _.using(code): নেস্ট করে, আর
একটি কৌশল কোনও ভাষা-কোডের জন্য ক্যাটালগটি খুঁজে আনে, যাতে
অ্যাপ্লিকেশনকে কখনও নিজে ক্যাটালগ অবজেক্ট নাড়াচাড়া করতে না হয়। যে সার্ভারকে
একটিমাত্র কাজের এককের ভিতরেই একাধিক ভাষায় টেক্সট তৈরি করতে হয় — পাঠকের জন্য
একটি পৃষ্ঠা, আর ভিন্ন ভাষায় সেট করা কোনও অ্যাকাউন্টের জন্য একটি বিজ্ঞপ্তি —
এটি ঠিক তারই জন্য।
স্ট্যাকটি থাকে ওই অ্যাপ্লিকেশন অবজেক্টের উপর, যাকে গোটা প্রসেস ভাগ করে নেয়। ফলে পরস্পর-ওভারল্যাপ করা দুটি request একই স্ট্যাক ভাগ করে, আর যে ব্লকগুলি সময়ের হিসেবে কঠোরভাবে নেস্ট করা নয়, তারা একে অপরের হাতে ভুল ভাষা তুলে দেয়:
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-strings¶
ক্যাটালগ এখনও Hello {name}-ই দেখে আর সে সাধারণ PO/MO ক্যাটালগই থাকে।
পার্থক্য হল একটি অনুবাদ কী বলতে পারে, আর তা কে যাচাই করে।
এই লাইব্রেরি রেন্ডার করার আগে প্রতিটি অনুবাদকে উৎস বার্তার placeholder-এর
বিপরীতে যাচাই করে, আর সে কেবল খালি নামই গ্রহণ করে, আর কিছু নয়।
t"Hello {name}"-এর বিপরীতে:
| যে অনুবাদে আছে | সেটি প্রত্যাখ্যাত হয় এই কারণে |
|---|---|
{name.__class__.__mro__} |
placeholder {name.__class__.__mro__} অবশ্যই একটি সরল নাম হতে হবে, উৎস বার্তা থেকে অপরিবর্তিত অনুলিপি করা |
{name!r} |
placeholder {name} ফরম্যাটিং যোগ করছে; কেবল {name} লিখুন, কারণ মানটি কীভাবে ফরম্যাট হবে তা উৎস বার্তাই ঠিক করে |
{0} |
placeholder {0} অবশ্যই একটি সরল নাম হতে হবে, উৎস বার্তা থেকে অপরিবর্তিত অনুলিপি করা |
{nombre} |
অনুবাদ উৎসের placeholder-এর সঙ্গে মেলে না: {name} অনুপস্থিত; {nombre} উৎস বার্তায় নেই |
প্রত্যাখ্যাত মানে ক্র্যাশ নয়: ডিফল্টে লাইব্রেরি একটি সতর্কবার্তা লগ করে এবং উৎস বার্তাটি রেন্ডার করে, তাই খারাপ ক্যাটালগ কখনও অ্যাপ্লিকেশনকে ফেলে দেয় না — gettext নিজেও ঠিক এই চুক্তিটিই রাখে।
ফরম্যাটিং যেখানে লেখা হয়েছিল সেখানেই থাকে, কোডে:
:,.2f কখনও ক্যাটালগে পৌঁছয় না, তাই কোনও অনুবাদ তা বদলাতে পারে না, আর কোনও
অনুবাদককে তার দিকে তাকাতেও হয় না। তবে এটি একটি নির্দিষ্ট ফরম্যাট,
স্থানীয়কৃত নয় — ভাষাভেদে অঙ্ক ও বিভাজক বেছে নেওয়া
Babel-এর কাজ, কল করার আগেই।
আরেকটি পার্থক্য টুলিংয়ের: t-strings নতুন সিনট্যাক্স, তাই সেগুলিকে .pot-এ
এক্সট্র্যাক্ট করতে এখন একটি t-string-সচেতন এক্সট্র্যাক্টর লাগে, যেমন এই
প্যাকেজ Babel-এর জন্য যেটি দেয়।
সীমাবদ্ধতাটির দাম¶
Python-এর প্রয়োজনীয়তা বাদ দিলে এই সবটুকুর দাম একটিই নিয়ম: একটি ইন্টারপোলেশনকে সরল নাম হতেই হবে।
এটি সত্যিকারের একটি বাধা, আর উপরের নিশ্চয়তাগুলি এই বাধাটিই তৈরি করে। উৎস-দিকের মান-বাঁধাই ও রানটাইমে placeholder যাচাইয়ের সঙ্গে মিলে এটি ক্যাটালগ স্ট্রিংকে এক্সপ্রেশন মূল্যায়ন করা থেকে আটকায় আর যিনি অনুবাদ করছেন তাঁর কাছে placeholder নামগুলিকে অর্থবহ রাখে।
একটি f-string এভাবে ব্যবহারই করা যায় না — কোনও লাইব্রেরি তাকে দেখার আগেই সে একটি সমাপ্ত স্ট্রিং, তাই তাকে অনুবাদ করার অর্থ একটি টুকরো অনুবাদ করা। t-strings (PEP 750) f-string-সদৃশ সিনট্যাক্স ও স্পষ্ট মান-বাঁধাই ধরে রেখেই স্থির টেক্সট আর মানগুলিকে আলাদা রাখে।
Python কীভাবে এখানে এসে পৌঁছল — দশ বছরের ব্যবধানে দুটি PEP, আর stdlib-এর সেই আলোচনা যা উত্তর ছাড়াই বন্ধ হল — তা উৎসের লিঙ্কসহ বলা আছে পটভূমি পৃষ্ঠায়।