ทำไมต้อง t-string¶
สี่วิธีในการใส่ค่าลงในข้อความที่แปลได้ เปรียบเทียบกันด้วยประโยคเดียวกัน ทั้งสี่วิธีตั้งชื่อให้ตัวยึดตำแหน่งของตนและยอมให้นักแปลสลับลำดับได้ สิ่งที่ต่างกันคือจะเกิดอะไรขึ้นเมื่อคำแปลผิด แคตตาล็อกเอื้อมถึงโปรแกรมของคุณได้มากแค่ไหน และการรับมันมาใช้มีต้นทุนเท่าใด
ตารางมาก่อน คุณจึงหาแถวที่คุณสนใจแล้วอ่านเฉพาะหัวข้อที่อยู่เบื้องหลังแถวนั้นได้
สามฝ่ายเกี่ยวข้องกับทุกข้อความที่ถูกแปล
แคตตาล็อก คือไฟล์ของคำแปล — อยู่ในรูป .po ระหว่างที่มนุษย์แก้ไข
และถูกคอมไพล์เป็น .mo เพื่อให้แอปพลิเคชันโหลด
(บทแนะนำ พาทำทั้งสองขั้นตอน) สามฝ่ายเกี่ยวข้องกับทุกข้อความ:
นักพัฒนา เขียนสตริงต้นทาง นักแปล แก้ไขแคตตาล็อก —
บ่อยครั้งบนแพลตฟอร์มภายนอกที่อยู่ห่างไกลจากการรีวิวโค้ดใด ๆ — และ
แอปพลิเคชัน นำทั้งสองส่วนมาเรนเดอร์ร่วมกันขณะรันไทม์
รูปแบบการจัดรูปแบบแต่ละแบบด้านล่างตอบคำถามเดียวกันด้วยคำตอบที่ต่างกัน:
แคตตาล็อกได้สิทธิ์ควบคุมภาษาการจัดรูปแบบมากแค่ไหน? ในตัวอย่าง _
คือชื่อตามธรรมเนียมของฟังก์ชันแปล และ tr คือฟังก์ชันของไลบรารีนี้
เทียบเคียงกัน¶
เมื่อนักแปลทำผิดพลาด แคตตาล็อกเดินทางผ่านมือหลายคน และสิ่งที่ผิดพลาดในนั้นส่วนใหญ่เกิดขึ้นโดยไม่ตั้งใจ:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| คำแปล ตัด ตัวยึดตำแหน่งทิ้ง — เรนเดอร์อะไร | ค่าหายไปเงียบ ๆ | ค่าหายไปเงียบ ๆ | ค่าหายไปเงียบ ๆ | ข้อความต้นทาง พร้อมคำเตือน (โดยค่าเริ่มต้น) |
| คำแปล เพิ่ม ตัวยึดตำแหน่งที่ไม่รู้จัก — เรนเดอร์อะไร | ข้อยกเว้น | ข้อยกเว้น | ตัวยึดตำแหน่งยังคงมองเห็นเป็นข้อความ | ข้อความต้นทาง พร้อมคำเตือน (โดยค่าเริ่มต้น) |
| คำแปล เปลี่ยนรูปแบบ ของตัวยึดตำแหน่ง — เรนเดอร์อะไร | ตามที่แคตตาล็อกสั่ง หรือข้อยกเว้นถ้าตัวอักษรบอกชนิดไม่เข้ากับค่าอีกต่อไป | ตามที่แคตตาล็อกสั่ง | เขียนไม่ได้ใน $-strings |
ข้อความต้นทาง พร้อมคำเตือน |
| ตัวยึดตำแหน่งถูกตรวจสอบตอนเรนเดอร์หรือไม่ | ไม่ | ไม่ | ไม่ | ใช่ (ดูด้านล่าง) |
แคตตาล็อกมีอำนาจแค่ไหน คำแปลคือข้อมูลจากภายนอกรีโพซิทอรีของคุณ และแต่ละรูปแบบยื่นอำนาจให้มันไม่เท่ากัน:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| ค่ามาจากไหน | แมปที่ระบุอย่างชัดแจ้ง | อาร์กิวเมนต์ที่ระบุอย่างชัดแจ้ง | ตัวแปร local และ global ของผู้เรียก บวก extras ที่เป็นทางเลือก |
ค่าที่ถูกจับไว้ภายใน t-string |
| แคตตาล็อกเปลี่ยนวิธีจัดรูปแบบค่าได้หรือไม่ | ได้ | ได้ | ไม่ได้ | ไม่ได้ |
| แคตตาล็อกล้วงเข้าไปในอ็อบเจกต์ (เข้าถึงแอตทริบิวต์) ได้หรือไม่ | ไม่ได้ | ได้ | ได้ ด้วยชื่อแบบมีจุด | ไม่ได้ |
| "ภาษาปัจจุบัน" อยู่ที่ไหน | แล้วแต่แอปพลิเคชันจะเก็บไว้ตรงไหน | แล้วแต่แอปพลิเคชันจะเก็บไว้ตรงไหน | สแตกของรหัสภาษาบนอ็อบเจกต์แอปพลิเคชันที่ใช้ร่วมกัน | ContextVar แยกตามแต่ละ task หรือแต่ละคำขอ |
การผสานเข้าระบบมีต้นทุนเท่าใด ทุกอย่างข้างต้นได้มาฟรีหากเครื่องมือเข้ากันได้ ส่วนนี้คือจุดที่มันอาจเข้ากันไม่ได้:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Python ขั้นต่ำ | เวอร์ชันใดก็ได้ | เวอร์ชันใดก็ได้ | 3.10 | 3.14 |
| ความสุกงอม | ไลบรารีมาตรฐาน | ไลบรารีมาตรฐาน | รุ่นเสถียรที่ออกแล้ว | อัลฟา |
| ใช้แคตตาล็อก PO/MO ธรรมดาหรือไม่ | ใช่ | ใช่ | ใช่ | ใช่ |
| ต้องใช้ตัวสกัดซอร์สแบบกำหนดเองหรือไม่ | ไม่ | ไม่ | ไม่ | ใช่ ในปัจจุบัน |
| Babel อนุมานแฟล็ก PO ตัวใด เพื่อให้เครื่องมือที่มีอยู่ตรวจสอบได้ | python-format |
python-brace-format |
ไม่มี | python-brace-format |
ว่าด้วยการตรวจสอบตอนเรนเดอร์: ข้อความเอกพจน์ถูกตรวจสอบว่าตัวยึดตำแหน่งตรงกันพอดี ข้อความพหูพจน์ก็ถูกตรวจสอบเช่นกัน เทียบกับ กฎยูเนียน/อินเตอร์เซกชัน ที่อนุญาตให้รูปพหูพจน์ของภาษาปลายทางแตกต่างจากของภาษาต้นทางได้ ส่วนการตรวจสอบรายรูปที่เข้มงวดกว่าจะทำงานตอนคอมไพล์แคตตาล็อก (การสกัดข้อความ)
แถวเรื่องแฟล็กรูปแบบเป็นเรื่องของการตรวจสอบที่รู้จักตัวยึดตำแหน่ง
ไม่ใช่ความเข้ากันได้ของแคตตาล็อก "ไม่มี" หมายความว่าเครื่องมือ gettext
มาตรฐานยังคงอ่านและคอมไพล์ข้อความได้ แต่ msgfmt --check-format
ไม่มีไวยากรณ์ตัวยึดตำแหน่งแบบ $ ให้นำมาใช้ตรวจ
ความเข้ากันได้และความสุกงอม¶
สองแถวแรกของตารางสุดท้ายคือแถวที่ตัดสินว่าจะรับมาใช้หรือไม่ จึงควรพูดออกมาตรง ๆ มากกว่าจะเป็นแค่ช่องในตาราง
%-format และ .format() ถูกฝังมาในตัว Python และไม่ต้องพึ่ง dependency ใดเลย
flufl.i18n เป็นแพ็กเกจที่เติบโตเต็มที่ ออกรุ่นแล้วและถูกใช้งานจริง
ทำงานบน Python 3.10 ขึ้นไป ส่วน gettext-tstrings ยังเป็นรุ่น อัลฟา
และต้องใช้ Python 3.14 ขึ้นไป เพราะ t-string เป็นไวยากรณ์ใหม่ใน 3.14 —
ไม่มี back-port และไม่มีทางมีได้ ข้อกำหนด
ของมันคือส่วนที่นิ่งแล้ว ส่วน API ฝั่ง Python ยังอาจขยับได้ก่อนถึง 1.0
สิ่งที่ไม่มีวิธีไหนต้องแลกเลยคือความเข้ากันได้ของแคตตาล็อก ทั้งสี่วิธีผลิตไฟล์ POT/PO/MO ธรรมดาที่โปรแกรมแก้ไข PO แพลตฟอร์มการแปล และเครื่องมือ GNU gettext ทุกตัวอ่านได้อยู่แล้ว ตัวเลือกด้านล่างจึงย้อนกลับได้ในแบบที่การเปลี่ยนรูปแบบแคตตาล็อกทำไม่ได้ การย้ายระบบ ว่าด้วยการย้ายโปรเจกต์ที่มีอยู่แล้ว
หัวข้อด้านล่างแสดงสิ่งที่ต้องแลกของแต่ละวิธีโดยละเอียด ทีละวิธี
%-format¶
สิ่งที่อาจผิดพลาด: ตัวยึดตำแหน่งที่เสียหายกลายเป็นข้อยกเว้นขณะรันไทม์ เว้นแต่การตรวจสอบแคตตาล็อกจะจับมันได้ก่อน
สตริงในแคตตาล็อกพกไวยากรณ์ 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 ของ GNU จับข้อผิดพลาดนี้ได้ก็จริง
แต่เฉพาะกับข้อความที่ติดแฟล็ก python-format เท่านั้น
และเฉพาะเมื่อแคตตาล็อกผ่าน msgfmt จริง ๆ ระหว่างทางไปสู่แอปพลิเคชันของคุณ
str.format¶
วิธีนี้ตัดตัวอักษรบอกชนิดที่ต่อท้ายออก โดยยังคงตัวยึดตำแหน่งแบบมีชื่อที่สลับลำดับได้อย่างอิสระ สิ่งที่อาจผิดพลาดย้ายไปอยู่อีกฝั่งของการแลกเปลี่ยน: คำแปลได้อำนาจเหนืออ็อบเจกต์ของคุณ
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
และบางครั้งก็ถูก 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
ที่เป็นทางเลือกจะมีลำดับความสำคัญเหนือทั้งสองอย่าง
ไวยากรณ์ฝั่งนักแปลของมันไม่มีตัวอักษรบอกชนิดต่อท้ายหรือตัวระบุรูปแบบ
และตัวยึดตำแหน่งยังคงสลับลำดับได้อย่างอิสระ
การแทนค่าที่ไม่พร้อมใช้งานจะไม่ raise หากมี name = "Ada" แต่ไม่มี nombre
ในเนมสเปซของผู้เรียก คำแปลในแคตตาล็อกที่เป็น Hello $nombre จะเรนเดอร์เป็น
Hello $nombre: ตัวยึดตำแหน่งที่แก้ค่าไม่ได้ยังคงมองเห็นได้
พฤติกรรมที่มีบันทึกไว้
นี้รักษาส่วนที่เหลือของข้อความที่แปลแล้วไว้แทนที่จะทำให้การเรียกล้มเหลว
ส่วนข้อยกเว้นที่เกิดขึ้นระหว่างการแก้ค่าแอตทริบิวต์หรือการแปลงค่ายังคงแพร่ต่อไปได้
flufl.i18n มีความสามารถมากกว่า string.Template เปล่า ๆ ในแง่หนึ่งที่เกี่ยวข้องกับเรื่องนี้
Template แบบกำหนดเอง ของมันรับตัวยึดตำแหน่งแบบมีจุด เช่น
$settings.api_key และ translator
ของมันจะแก้ค่าเส้นทางเหล่านั้นเทียบกับค่าของผู้เรียก
ตัวยึดตำแหน่งที่ถูกแปลจึงอาจอ้างชื่อ local หรือ global ใดก็ได้ของผู้เรียกที่มีอยู่ และด้วยไวยากรณ์แบบมีจุด
ยังไล่เข้าไปในแอตทริบิวต์ของมันได้ด้วย นั่นสะดวกเมื่อข้อความต้องการแอตทริบิวต์
แต่ในขณะเดียวกันก็ทำให้เฟรมของผู้เรียกกลายเป็นส่วนหนึ่งของเนมสเปซการแทนค่าของแคตตาล็อก
การเปรียบเทียบในหน้านี้อธิบาย 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 แทนสแตกที่ใช้ร่วมกัน
การสลับกันข้างต้นจึงแยกกันตามแต่ละ task ดูวิธีเขียนที่เทียบเท่าได้ที่
หลายภาษาพร้อมกัน
สิ่งที่ไลบรารีนี้ไม่ได้จัดหาให้คือการค้นหาแคตตาล็อกจากรหัสภาษา:
คุณเป็นฝ่ายส่งอ็อบเจกต์การแปลเข้ามา ซึ่งในกรณีทั่วไปก็คือการเรียก
gettext.translation() เพียงครั้งเดียว และไลบรารีมาตรฐานจะแคชแคตตาล็อกที่แจงแล้วไว้ให้
t-string¶
แคตตาล็อกยังคงเห็น 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 เองรักษาไว้
การจัดรูปแบบยังคงอยู่ที่เดิมที่มันถูกเขียนไว้ นั่นคือในโค้ด:
:,.2f ไม่มีวันไปถึงแคตตาล็อก ดังนั้นไม่มีคำแปลใดเปลี่ยนแปลงมันได้
และไม่มีนักแปลคนไหนต้องมองมันเลย แต่มันเป็นรูปแบบ ตายตัว ไม่ใช่รูปแบบที่ปรับตามท้องถิ่น —
การเลือกจำนวนหลักและตัวคั่นตามแต่ละภาษาเป็น
งานของ Babel ก่อนถึงจุดเรียกใช้
ความแตกต่างอีกอย่างคือเครื่องมือ: t-string เป็นไวยากรณ์ใหม่
การสกัดมันออกมาเป็น .pot ในปัจจุบันจึงต้องใช้ตัวสกัดที่รู้จัก t-string
เช่นตัวที่แพ็กเกจนี้ จัดเตรียมไว้ให้ Babel
ต้นทุนของข้อจำกัดนี้¶
นอกเหนือจากข้อกำหนดเรื่องเวอร์ชัน Python แล้ว ราคาของทั้งหมดนี้คือกฎข้อเดียว: การแทรกค่าต้องเป็นชื่อเปล่า ๆ
นั่นเป็นข้อจำกัดจริง ๆ และมันคือข้อจำกัดเดียวกันกับที่ผลิตการรับประกันข้างต้นออกมา เมื่อรวมกับการผูกค่าฝั่งซอร์สและการตรวจสอบตัวยึดตำแหน่งขณะรันไทม์ มันป้องกันไม่ให้สตริงในแคตตาล็อกประเมินนิพจน์ได้ และรักษาให้ชื่อตัวยึดตำแหน่งมีความหมายต่อคนที่ต้องแปลมัน
f-string ใช้แบบนี้ไม่ได้เลย — กว่าไลบรารีใดจะได้เห็นมัน มันก็เป็นสตริงที่เสร็จสมบูรณ์ไปแล้ว การแปลมันจึงหมายถึงการแปลเศษเสี้ยวของประโยค t-string (PEP 750) เก็บข้อความคงที่และค่าแยกจากกัน โดยยังคงไวยากรณ์แบบ f-string และการผูกค่าอย่างชัดแจ้งไว้
Python เดินทางมาถึงจุดนี้ได้อย่างไร — สอง PEP ที่ห่างกันสิบปี และการอภิปรายในไลบรารีมาตรฐานที่ปิดลงโดยไร้คำตอบ — ถูกเล่าไว้พร้อมแหล่งอ้างอิงใน ความเป็นมา