หลักการทำงาน¶
ไม่มีสิ่งใดในหน้านี้จำเป็นต่อการใช้ไลบรารี — บทแนะนำ และคู่มือ ครอบคลุมส่วนนั้นไว้แล้ว หน้านี้เลือกจะสร้างไลบรารีขึ้นใหม่จากหลักการพื้นฐานแทน: t-string คืออะไรกันแน่ msgid ตกผลึกออกมาจากมันได้อย่างไร อะไรทำให้คำแปลถูกต้อง และการอิมพลีเมนต์ทำให้การตรวจทั้งหมดนั้นมีต้นทุนเพียงเศษเสี้ยวของไมโครวินาทีได้อย่างไร อ่านหน้านี้หากคุณอยากรู้ อยากมีส่วนร่วม หรือวางแผนจะนำข้อตกลงนี้ไปอิมพลีเมนต์เอง
t-string คืออะไรกันแน่¶
f-string ผลิต str และผลิตออกมาทันที — เมื่อฟังก์ชันใดก็ตามได้รับมัน ค่าก็ถูกแทรกเข้าไปและประโยคก็ถูกผนึกเรียบร้อยแล้ว t-string (PEP 750) มีไวยากรณ์เดียวกันและประเมินนิพจน์แบบทันทีเหมือนกัน แต่ผลิตชนิดข้อมูลที่ต่างออกไป:
>>> name = "Ada"
>>> f"Hello {name}!"
'Hello Ada!'
>>> t"Hello {name}!"
Template(strings=('Hello ', '!'), interpolations=(Interpolation('Ada', 'name', None, ''),))
อ็อบเจกต์ Template นั้นเก็บชิ้นส่วนที่ไปป์ไลน์แคตตาล็อกต้องใช้ไว้ในสภาพที่ยังแยกจากกัน:
>>> template = t"Total: {amount:,.2f}"
>>> template.strings
('Total: ', '')
>>> template.interpolations[0].expression
'amount'
>>> template.interpolations[0].value
1234.5
>>> template.interpolations[0].format_spec
',.2f'
strings— ข้อความตามตัวอักษรที่ล้อมรอบส่วนแทรกค่า เรียงตามลำดับ- สำหรับส่วนแทรกค่าแต่ละตัว: นิพจน์ (expression) ในรูปข้อความซอร์ส (
'amount') ค่า (value) ที่ประเมินแล้ว (1234.5) และ conversion (!r) กับ format spec (,.2f) หากมี — ถูกพกพามาแยกกันแทนที่จะถูกนำไปใช้แล้ว
ทุกสิ่งที่ไลบรารีนี้ทำคือการบริโภคโครงสร้างนั้นอย่างมีวินัย ตัวภาษาได้ทำการแยกหนึ่งเดียวที่ i18n ต้องการให้เรียบร้อยแล้ว — ข้อความสถิตแยกจากค่า — ไลบรารีจึงไม่เคยต้องแยกวิเคราะห์ซอร์สโค้ดของคุณ และไม่เคยต้องเดาว่าค่าอยู่ตรงไหนของประโยค สิ่งที่เหลือคือการตัดสินใจสามข้อ: โครงสร้างนี้กลายเป็นคีย์แคตตาล็อกได้อย่างไร คำแปลของคีย์นั้นพูดอะไรได้บ้าง และทั้งสองเรนเดอร์กลับมารวมกันอย่างไร
จากเทมเพลตสู่ msgid¶
msgid — คีย์ที่แคตตาล็อกใช้ทำดัชนี — ถูกอนุมานจากส่วน สถิต ของเทมเพลตเท่านั้น เดินไล่ strings และ interpolations ตามลำดับในซอร์ส ทำ brace-escape ให้แต่ละส่วนข้อความตามตัวอักษร ({ กลายเป็น {{) และสำหรับส่วนแทรกค่าแต่ละตัว ปล่อยโทเคน {name} หนึ่งตัว โดยที่ name คือข้อความนิพจน์ที่ตัดช่องว่างหัวท้ายออกแล้ว จาก t"Total: {amount:,.2f}":
strings ('Total: ', '')
interpolations expression 'amount' conversion None format_spec ',.2f'
msgid 'Total: {amount}'
แต่ละส่วนของกฎนั้นมีเหตุผลรองรับ:
- นิพจน์ต้องเป็นชื่อเปล่า ๆ —
str.isidentifier()เป็นจริง และไม่ใช่คีย์เวิร์ดของ Pythont"Hello {user.name}"ถูกปฏิเสธตั้งแต่จุดเรียก msgid คือ คีย์: มันต้องออกมาเหมือนกันทุกการรันและทุกการสกัดข้อความ และนักแปลเป็นผู้อ่านมัน ตัวยึดตำแหน่งจึงต้องเป็นคำที่เสถียรและมีความหมาย — ไม่ใช่เศษโค้ดที่เชื้อเชิญให้แคตตาล็อกกลายเป็นภาษานิพจน์ - conversion และ format spec ไม่มีวันเข้าไปอยู่ใน msgid นักแปลไม่ควรต้องอ่าน
:,.2fและไม่ควรมีคำแปลใดเปลี่ยนแปลงมันได้ ผลพวงที่ควรรู้: การปรับ:,.2fเป็น:,.0fในโค้ดของคุณไม่เปลี่ยน msgid ใดเลย จึงไม่ทำให้คำแปลในภาษาใดกลายเป็นโมฆะ คีย์แคตตาล็อกติดตาม สิ่งที่ประโยคพูด ไม่ใช่วิธีจัดรูปแบบของค่า - ชื่อที่ปรากฏซ้ำต้องซ้ำการจัดรูปแบบแบบเป๊ะ ๆ
t"{x:.2f} vs {x:.3f}"ถูกปฏิเสธ เพราะทั้งสองตำแหน่งยุบรวมเป็นโทเคน{x}ตัวเดียวกัน และ msgid จะไม่สามารถบอกได้อีกต่อไปว่าการเรนเดอร์ควรใช้การจัดรูปแบบใด - msgid ว่างจะไม่ถูกนำไปค้นหาเลย เพราะ gettext สงวนมันไว้สำหรับเฮดเดอร์เมทาดาทาของแคตตาล็อกเอง
t""เรนเดอร์เป็น""โดยไม่แตะแคตตาล็อกเลย
ชุดกฎฉบับเต็ม รวมทั้งกรณีขอบที่หน้านี้ข้ามไป อยู่ที่ SPEC §2
คำแปลพูดอะไรได้บ้าง¶
แพตเทิร์นที่กลับมาจากแคตตาล็อกถูกแยกวิเคราะห์ด้วย string.Formatter — parser ตัวเดียวกับที่ str.format ใช้ ไวยากรณ์นี้จงใจยืมมาแทนที่จะประดิษฐ์ขึ้นเอง: แพตเทิร์นที่ไลบรารีนี้ยอมรับคือแพตเทิร์นที่ระบบนิเวศวงกว้างเข้าใจอยู่แล้ว จากนั้นจึงใช้การตรวจสองอย่าง
รูปทรง (shape): ทุกฟิลด์ต้องเป็น {name} เปล่า ๆ conversion หรือ format spec — รวมถึงแบบว่างอย่างชัดแจ้ง {name:} — ถูกปฏิเสธ เช่นเดียวกับฟิลด์ตามตำแหน่ง ({0}, {}) และชื่อที่มีช่องว่างล้อมรอบ ({ name }) ข้อสุดท้ายสำคัญกว่าที่เห็น: ทั้ง str.format และ GNU msgfmt ต่างปฏิเสธ { name } การยอมรับมันที่นี่จึงจะผลิตแคตตาล็อกที่ไม่มีเครื่องมืออื่นใดในห่วงโซ่สามารถตรวจสอบได้
ชื่อ (names): เซตตัวยึดตำแหน่งของแพตเทิร์นถูกเปรียบเทียบกับเซตของต้นทาง สำหรับข้อความเอกพจน์ ทุกชื่อของต้นทางเป็นสิ่ง จำเป็น (required) และไม่มีชื่ออื่นใดเป็นสิ่งที่ อนุญาต (allowed) สำหรับข้อความพหูพจน์ ทั้งสองกิ่งจะถูกผสาน:
- allowed = ยูเนียนของชื่อจากทั้งสองกิ่ง
- required = อินเตอร์เซกชันของทั้งสอง
ดังนั้นเมื่อเทียบกับ t"One file" / t"{n} files" ชื่อ n เป็นสิ่งที่อนุญาตในคำแปลของรูปใดก็ได้ แต่ไม่จำเป็นสำหรับรูปใดเลย ความไม่สมมาตรนี้เองที่ยอมให้ระบบพหูพจน์ของภาษาปลายทางแตกต่างจากของต้นทาง — ภาษาญี่ปุ่นแปลทั้งสองกิ่งด้วยรูปเดียวซึ่งน่าจะใช้ {n} ส่วนภาษาที่มีรูปมากกว่าภาษาอังกฤษอาจต้องใช้ {n} ในรูปที่ภาษาอังกฤษไม่มี
ทั้งหมดนั้นไม่ใช่เรื่องสมมุติ: แคตตาล็อกส่วนโครม (chrome) ของไซต์นี้เองมีข้อความพหูพจน์ Built {n} localized page / Built {n} localized pages — สองกิ่งภาษาอังกฤษ — และรุ่นภาษาต่าง ๆ ของไซต์แปลข้อความเดียวนี้ออกมาเป็นตั้งแต่หนึ่งรูปไปจนถึงหกรูป
เก้ารุ่นภาษาในจำนวนนั้น เรียงตามลำดับรูป
| แคตตาล็อก | จำนวนรูป | คำแปล เรียงตามลำดับรูป |
|---|---|---|
| ญี่ปุ่น | 1 | ローカライズ済みページを{n}件ビルドしました |
| ตุรกี | 2 | {n} yerelleştirilmiş sayfa oluşturuldu — สองครั้ง เหมือนกันทุกตัวอักษร: คำนามภาษาตุรกีคงรูปเอกพจน์เมื่อตามหลังตัวเลข |
| อิตาลี | 2 | Generata {n} pagina localizzata · Generate {n} pagine localizzate — กริยารูป participle ผันตามเพศและพจน์ |
| ลัตเวีย | 3 | Izveidota {n} lokalizēta lapa · Izveidotas {n} lokalizētas lapas · Izveidots {n} lokalizētu lapu — รูปที่สามมีไว้สำหรับศูนย์เท่านั้น |
| รัสเซีย | 3 | Собрана {n} локализованная страница · Собраны {n} локализованные страницы · Собрано {n} локализованных страниц |
| โปแลนด์ | 3 | Zbudowano {n} zlokalizowaną stronę · Zbudowano {n} zlokalizowane strony · Zbudowano {n} zlokalizowanych stron |
| สโลวีเนีย | 4 | Zgrajena {n} lokalizirana stran · Zgrajeni {n} lokalizirani strani · Zgrajene {n} lokalizirane strani · Zgrajenih {n} lokaliziranih strani — รูปที่สองคือทวิพจน์ (dual) สำหรับสองหน่วยพอดี |
| ไอริช | 5 | Tógadh {n} leathanach logánaithe · Tógadh {n} leathanaigh logánaithe — หนึ่ง สอง 3–6 7–10 และที่เหลือ; ก้านคำสลับรูปไปมา แต่ leathanach ขึ้นต้นด้วย l ซึ่งไม่มีการกลายเสียงต้นคำ (mutation) ของภาษาไอริชแบบใดเขียนออกมา หลายรูปจึงพ้องกัน |
| อาหรับ | 6 | ในจำนวนนั้นมี تم إنشاء صفحة مترجمة واحدة ({n}) สำหรับหนึ่งหน้าพอดี และ تم إنشاء {n} صفحات مترجمة สำหรับจำนวนไม่กี่หน้า |
ทุกแถวคือรายการที่มีอยู่จริงใน i18n/*/LC_MESSAGES/site.po ของรีโพซิทอรีนี้ ถูกเรนเดอร์โดยการ build หลายภาษาในทุกรีลีส — และมีการทดสอบตรึงตารางนี้เข้ากับแคตตาล็อกเหล่านั้น ทั้งสองจึงไม่มีทางลอยห่างออกจากกัน
ภายในขอบเขตเหล่านั้น การสลับลำดับและการใช้ซ้ำถูกปล่อยอิสระโดยตั้งใจ ทั้งสองเป็นสิ่งจำเป็นทางไวยากรณ์ในภาษาจริง และการจำกัดจำนวนครั้งที่ปรากฏจะปฏิเสธคำแปลที่ถูกต้องโดยไม่ได้ประโยชน์ด้านความปลอดภัยใด ๆ: คำแปลยังคง ประเมินผล อะไรไม่ได้อยู่ดี เพราะไม่มีเส้นทางการประเมินผลอยู่เลย — ตัวยึดตำแหน่งถูกค้นหาด้วยชื่อจากค่าที่เทมเพลตคำนวณไว้เรียบร้อยแล้ว ไม่เคยถูกป้อนให้ eval, getattr หรือแม้แต่ str.format เองเลย
การเรนเดอร์¶
การเรนเดอร์แพตเทิร์นที่ผ่านการตรวจสอบแล้วคือการเดินไล่ตามชิ้นส่วนของมัน: ปล่อยแต่ละส่วนข้อความตามตัวอักษรออกมา และสำหรับตัวยึดตำแหน่งแต่ละตัว หยิบค่าที่ส่วนแทรกค่าจับไว้แล้วใช้ conversion กับ format spec ของ ฝั่งต้นทาง — format(convert(value, conversion), format_spec) โดยระหว่างทำมีการรักษาหลักประกันสองข้อ:
- ค่าที่แตกต่างกันแต่ละค่าถูกจัดรูปแบบมากที่สุดหนึ่งครั้งต่อการเรนเดอร์ แม้ในกรณีที่คำแปลใช้ตัวยึดตำแหน่งซ้ำ การใช้ซ้ำเปลี่ยนเพียงจำนวนครั้งที่ผลลัพธ์ถูกแทรกเข้าไป ไม่ใช่จำนวนครั้งที่
__format__ของคุณรัน - สำหรับพหูพจน์ ตัวยึดตำแหน่งอ่านค่าจากกิ่งที่นิยามมัน ชื่อที่ปรากฏในทั้งสองกิ่งอ่านค่าที่ถูกจับโดยกิ่งซึ่งภาษา ต้นทาง เลือก (
singularเมื่อn == 1มิฉะนั้นplural) ส่วนชื่อที่มีเฉพาะกิ่งใดกิ่งหนึ่งอ่านจากกิ่งของตัวเองเสมอ แม้ในกรณีที่กฎพหูพจน์ของภาษาปลายทางทำให้มันใช้ได้ในรูปอื่น
เมื่อการตรวจสอบล้มเหลว ณ เวลาเรนเดอร์ การตอบสนองแบ่งตามว่าใครเป็นผู้จัดหาแพตเทิร์นมา แพตเทิร์นที่ออกมาจาก แคตตาล็อก จะถอยกลับ: บันทึกคำเตือนหนึ่งรายการแล้วเรนเดอร์ข้อความต้นทาง รักษาสัญญาของ gettext ที่ว่าแคตตาล็อกที่พังจะไม่มีวันพาแอปพลิเคชันล่ม (คู่มือแสดงทั้งสองโหมด) ส่วนแพตเทิร์นที่ผู้เรียกส่งเข้ามาโดยตรง — CompiledTemplate.render — ยกข้อยกเว้นเสมอ เพราะไม่มีข้อความต้นทางให้ถอยกลับ ไปหา ความผ่อนปรนมีไว้สำหรับการค้นแคตตาล็อก ไม่ใช่สำหรับอาร์กิวเมนต์
การวินิจฉัยเป็นส่วนหนึ่งของการออกแบบ¶
ข้อผิดพลาดของตัวยึดตำแหน่งมักไปตกอยู่ตรงหน้านักแปล ไม่ใช่โปรแกรมเมอร์ และบ่อยครั้งอยู่ในไฟล์ที่มองไม่เห็นปัญหาด้วยตาเปล่า การบอกว่า {name} is missing กับคนที่มองเห็นอักขระเหล่านั้นอยู่ตรงหน้าในโปรแกรมแก้ไขของตนคือทางตัน ข้อความวินิจฉัยจึงถูกคำนวณด้วยกฎสามข้อ:
- ชื่อที่มี อักขระล่องหน — no-break space ที่ input method สร้างขึ้น หรือ zero-width space — จะถูกพิมพ์โดยแทนที่อักขระนั้นด้วย code point ของมัน ณ ตำแหน่งเดิม:
{<U+00A0>name}เพราะผู้อ่านจำเป็นต้องเห็นว่า อยู่ตรงไหน - ชื่อที่ตัวอักษร ปนกันหลายระบบการเขียน — กรณี homoglyph — จะถูกแสดงสองครั้ง ครั้งหนึ่งแบบอ่านได้ อีกครั้งแบบ escape เพราะ
{nаme}ที่มีаแบบซีริลลิกนั้นแยกไม่ออกจาก{name}เมื่อพิมพ์ออกมา และรูปแบบที่ escape แล้ว(nаme)คือการสะกดเดียวที่บอกความแตกต่างได้ - อย่างอื่นทั้งหมดถูกแสดง ตามที่เขียนไว้
{名前}และ{café}เป็นชื่อธรรมดา การ escape พวกมันจะทำให้ผู้อ่านหาไม่เจอว่าหมายถึงสิ่งใด
ด้วยหลักการเดียวกัน ตัวยึดตำแหน่งที่ "หายไป" แต่ ดูเหมือน จะอยู่ตรงนั้นจะได้รับคำอธิบายว่าทำไมมันจึงหายไป — วงเล็บปีกกาแบบเต็มความกว้าง (full-width) จาก input method ของเอเชียตะวันออก การซ้อนเป็น {{name}} จากการ escape ไป-กลับ หรือชื่อที่อยู่นอกวงเล็บปีกกาใด ๆ ตารางการอ่านความล้มเหลว ที่เขียนไว้สำหรับนักแปลแสดงข้อความเหล่านี้แต่ละข้อความแบบคำต่อคำ
เส้นทางร้อน¶
ทั้งหมดข้างบนเกิดขึ้นกับทุกสตริงแปลที่แอปพลิเคชันเรนเดอร์ การอิมพลีเมนต์จึงถูกสร้างขึ้นรอบแนวคิดเดียว: การตรวจสอบไม่มีวันถูกข้าม ดังนั้นสิ่งที่ต้องถูกแคชก็คือการตรวจสอบ
flowchart LR
T["t-string"] --> S{"เคยเห็น<br>โครงสร้างนี้ไหม"}
S -- "เคย" --> G["ค้นแคตตาล็อก<br>ด้วย msgid ที่แคชไว้"]
S -- "ไม่เคย" --> D["อนุมาน msgid<br>แล้วแคชแผน"] --> G
G --> V{"เคยเห็น<br>แพตเทิร์นนี้ไหม"}
V -- "เคย" --> R["เรนเดอร์"]
V -- "ไม่เคย" --> C["ตรวจสอบ<br>แล้วแคชผลตัดสิน"] --> R
แคชสามตัว หนึ่งตัวต่อหนึ่งขั้นตอน:
- แผน (plan) ต่อหนึ่งโครงสร้างของจุดเรียก ทูเพิล
stringsของเทมเพลต — อ็อบเจกต์ที่อินเทอร์พรีเตอร์สร้างไว้อยู่แล้ว — คือคีย์ของแคช การค้นหาจึงไม่จัดสรรหน่วยความจำใดเลย เมื่อ hit นิพจน์ conversion และ format spec ของส่วนแทรกค่าแต่ละตัวยังคงถูกเทียบกับที่บันทึกไว้: จุดเรียกสองจุดที่มีข้อความตามตัวอักษรเหมือนกันแต่การจัดรูปแบบต่างกัน (t"{x:.2f}"กับt"{x:.3f}") ต้องไม่ชนกัน และการเทียบนั้นคือราคาของการใช้คีย์ที่อินเทอร์พรีเตอร์ยื่นให้ฟรี ๆ - ผลตัดสิน (verdict) ต่อหนึ่งแพตเทิร์น ครั้งแรกที่แคตตาล็อกตอบกลับด้วยแพตเทิร์นหนึ่ง ๆ มันจะถูกแยกวิเคราะห์และตรวจสอบ ผลลัพธ์ — แผนเรนเดอร์ที่คอมไพล์แล้ว หรือบันทึกความไม่ถูกต้อง — ถูกเก็บไว้บนแผน การเรนเดอร์ข้อความนั้นทุกครั้งถัดไปเข้าถึงมันได้ด้วยการค้นดิกชันนารีเพียงครั้งเดียว แพตเทิร์นที่ไม่ถูกต้องก็ถูกจดจำไว้เช่นกัน นี่คือเหตุผลที่รายการแคตตาล็อกที่พังเตือนเพียงครั้งเดียวแทนที่จะเตือนทุกครั้งที่เรนเดอร์
- แผนที่ผสานแล้วต่อหนึ่งคู่พหูพจน์ ถือเซตยูเนียน/อินเตอร์เซกชันไว้ เพื่อให้เลขคณิตของกิ่งเกิดขึ้นครั้งเดียวต่อข้อความ ไม่ใช่ครั้งเดียวต่อการเรียก
แคชทุกตัวมีขอบเขตจำกัด และไม่มีตัวใดเก็บ ค่า ที่ถูกแทรกเลย — เก็บเพียงโครงสร้างสถิตกับข้อความแพตเทิร์นเท่านั้น ผลลัพธ์ที่วัดโดย benchmarks/runtime.py บน CPython 3.14.6 และ macOS 26 บนแล็ปท็อป arm64: ราว 0.4 µs สำหรับข้อความหนึ่งฟิลด์ ซึ่งรวมการสร้างตัว t-string เองด้วย หรือประมาณ 2.7 เท่าของ gettext(...).format(...) ธรรมดาที่ไม่ตรวจสอบอะไรเลย ตัวเลขเหล่านี้เป็นของเครื่องเพียงเครื่องเดียว — สคริปต์จะพิมพ์อินเทอร์พรีเตอร์และแพลตฟอร์มของมันไว้ในส่วนหัว ดังนั้นจงรันมันบนฮาร์ดแวร์ที่คุณ deploy จริงก่อนจะถืออัตราส่วนใดว่าเป็นของคุณ คำอธิบายที่หัวไฟล์ core.py บันทึกผลการวัดรายตัวที่อยู่เบื้องหลังรูปทรงนั้นไว้
การนำไปอิมพลีเมนต์เอง¶
ไม่มีสิ่งใดข้างบนที่เจาะจงกับการอิมพลีเมนต์นี้: ข้อตกลงนี้ถูกเขียนไว้เป็น spec v1 และชุดทดสอบความสอดคล้องที่เครื่องอ่านได้ของมันช่วยให้ตัวสกัดข้อความ ปลั๊กอิน IDE หรือการอิมพลีเมนต์ในภาษาอื่นสามารถตรวจตัวเองกับทุกกฎที่หน้านี้อธิบายไว้ได้ การอิมพลีเมนต์นี้รันชุดทดสอบดังกล่าวในการทดสอบของตัวเอง และนั่นคือสิ่งที่กันไม่ให้หน้านี้ ข้อกำหนด และโค้ดลอยห่างจากกันไปอย่างเงียบเชียบ