คู่มือ¶
หน้านี้คือเอกสารอ้างอิงฝั่งรันไทม์ ครอบคลุมทุกสิ่งที่ โค้ดแอปพลิเคชัน ของคุณ ทำกับไลบรารีนี้เมื่อมีแคตตาล็อกพร้อมแล้ว หากคุณยังไม่เคยเห็นลูปทั้งหมด — มาร์ก สกัด แปล คอมไพล์ รัน — บทแนะนำ จะพาเดินครบหนึ่งรอบ ในห้านาที การสร้างและตรวจสอบแคตตาล็อกอธิบายไว้ใน การสกัดข้อความ ส่วนวิธีที่ทีมทำให้ลูปหมุนต่อไป — รอบการอัปเดต CI แพลตฟอร์มการแปล — อยู่ใน การใช้งานจริง
ควรใช้จุดเข้าไหนดี¶
แพ็กเกจนี้ส่งออกวิธีแปลข้อความหลายวิธี เพราะแอปพลิเคชันผูกภาษากันคนละแบบ จงเลือกตามวิธีที่โปรแกรมของคุณตัดสินว่าตอนนี้มันอยู่ในภาษาใด:
| สถานการณ์ของคุณ | ใช้ |
|---|---|
| ภาษาเดียวตลอดทั้งโปรเซส — CLI แอปเดสก์ท็อป สคริปต์ | Translator เรียกในชื่อ _ |
| ภาษาเดียวต่อหนึ่งคำขอหรือต่อหนึ่ง async task — เว็บแอปพลิเคชัน | use_translations() ครอบงานนั้น แล้วใช้ tr() |
| ข้อความที่ถูกนิยามตอน import — ป้ายกำกับฟอร์ม enum ค่าคงที่ | lazy_gettext() หรือ lazy_pgettext() |
| จำนวนเป็นตัวตัดสินถ้อยคำ | ngettext() / npgettext() ในรูปแบบใดก็ได้ข้างต้น |
| เรนเดอร์แพตเทิร์นโดยไม่มีแคตตาล็อกเข้ามาเกี่ยวข้อง | compile_template() |
ทุกอย่างด้านล่างคือห้าอย่างนั้น เรียงตามลำดับนั้น
การผูกแคตตาล็อก¶
รูปแบบที่แนะนำสะท้อนการใช้งานแบบคลาสของ gettext: ผูกอ็อบเจกต์การแปลมาตรฐาน
หนึ่งครั้ง แล้วใช้ตัวประมวลผลที่เรียกได้เป็น _
import gettext
from gettext_tstrings import Translator
translations = gettext.translation("messages", localedir="locales", languages=["ja"])
_ = Translator(translations)
name = "Ada"
print(_(t"Hello {name}")) # こんにちは Ada
n = 3
print(_.ngettext(t"One file", t"{n} files", n)) # picks the right plural form for n
filename = "report.txt"
print(_.pgettext("button", t"Open {filename}")) # "button" disambiguates homonyms
ฟังก์ชันระดับโมดูลใช้ชื่อเดียวกับไลบรารีมาตรฐาน รวมถึงข้อตกลงการเรียกแบบ positional-only เช่นเดียวกัน
from gettext_tstrings import gettext, ngettext, npgettext, pgettext
gettext(t"Hello {name}", translations=translations)
ngettext(t"One file", t"{n} files", n, translations=translations)
pgettext("button", t"Open {filename}", translations=translations)
npgettext("inbox", t"One message", t"{n} messages", n, translations=translations)
tr และ ntr เป็น alias ที่ตรงกันทุกประการของ gettext และ ngettext
ภาษาแบบต่อคำขอ¶
เว็บเฟรมเวิร์กเลือกภาษาเป็นรายคำขอ เมื่อผูกการแปลของคำขอนั้นเข้ากับคอนเท็กซ์ ปัจจุบัน การเรียกระดับโมดูลทุกครั้งจะถูกแปลงเป็นภาษานั้น อย่างปลอดภัยแม้มีหลายคำขอทำงานพร้อมกัน
from gettext_tstrings import tr, use_translations
def handle(request):
name = request.user.display_name
translations = load_translations(request.locale)
with use_translations(translations):
return render(tr(t"Hello {name}"))
set_translations(translations) ผูกโดยไม่ต้องใช้บล็อก with สำหรับเฟรมเวิร์ก
ที่จัดการวงจรชีวิตของคำขอเอง ส่วน get_translations() อ่านการผูกปัจจุบัน
อาร์กิวเมนต์ translations= ที่ระบุอย่างชัดเจนจะชนะคอนเท็กซ์เสมอ
และคอนเท็กซ์ที่ยังไม่ถูกผูกจะถอยกลับไปใช้ฟังก์ชัน gettext ที่ติดตั้งแบบ global
ของไลบรารีมาตรฐาน ตัวอย่างการใช้งานจริงสำหรับ Flask และมิดเดิลแวร์ ASGI
อยู่ที่หน้า การใช้งานจริง
การแปลแบบเลื่อนเวลา¶
t-string จับค่าทันทีที่สร้าง ซึ่งไม่ถูกต้องสำหรับสตริงที่นิยามตอน import — ป้ายกำกับฟอร์ม ค่า enum ค่าคงที่ของโมดูล — ที่ต้องเรนเดอร์ในภาษาที่ใช้งานอยู่ ณ ตอนที่มันถูกใช้
from gettext_tstrings import lazy_gettext, lazy_pgettext, use_translations
SAVE = lazy_gettext(t"Save changes") # defined once, at import
OPEN = lazy_pgettext("button", t"Open file")
with use_translations(japanese):
assert str(SAVE) == "変更を保存" # rendered here, in this language
LazyString เรนเดอร์ผ่าน str(), format() และ f-string
และเปรียบเทียบเท่ากับข้อความที่เรนเดอร์แล้ว
แฮชไม่ได้โดยตั้งใจ
ข้อความของ LazyString ขึ้นกับภาษาที่ใช้งานอยู่ ค่าแฮชจึงจะเปลี่ยนไป
เมื่อสลับภาษา และทำให้ set หรือ dict ใดก็ตามที่เก็บมันไว้เสียหาย
อย่างเงียบ ๆ หากต้องใช้เป็นคีย์ ให้เรียก str() ก่อน
strict ถูกตัดสิน ณ จุดที่ เขียน ข้อความ ไม่ใช่จุดที่ข้อความถูกเรนเดอร์:
สตริงแบบเลื่อนเวลาจะเรนเดอร์ ณ ที่ใดก็ตามที่มันถูกใช้จริงในท้ายที่สุด —
ในเทมเพลต ในฟอร์ม ในบรรทัดล็อก — และที่แห่งนั้นแทบไม่เคยรู้เลยว่านี่คือ
การรันเทสต์หรือโปรดักชัน การส่ง strict=True ตั้งแต่ตอนนิยามคือสิ่งที่ทำให้
ตัวเลือกดังใน CI ผ่อนปรนในโปรดักชัน
อันเดียวกันนี้มีผลกับสตริงที่ไม่ได้ถูกเรนเดอร์ ณ จุดที่เรียกมัน
รูปพหูพจน์ขึ้นกับจำนวนที่รู้ตอนรันไทม์ จึงควรเรนเดอร์ทันทีด้วย ngettext
ณ จุดที่รู้จำนวนแล้ว
หลายภาษาพร้อมกัน¶
คำขอเดียวมักต้องใช้มากกว่าหนึ่งภาษา: หน้าที่เรนเดอร์ให้ผู้อ่าน แล้วยังจ่อคิวการแจ้งเตือนไปยังบัญชีที่ตั้งค่าไว้เป็นอีกภาษาหนึ่ง หรือสรุปรวมที่ยกคำพูดของผู้ร่วมสนทนาแต่ละคนในภาษาของเขาเอง การผูกซ้อนกันได้ และเมื่อออกจากบล็อกด้านใน การผูกของบล็อกด้านนอกจะกลับคืนมา
with use_translations(reader):
page = tr(t"Hello {name}")
with use_translations(recipient):
notice = tr(t"Hello {name}") # the recipient's language
footer = tr(t"Hello {name}") # the reader's again
เมื่อวนไปตามรายชื่อผู้รับ สตริงแบบเลื่อนเวลาคือสิ่งที่ทำงานให้: ข้อความถูกเขียนไว้ครั้งเดียวตอน import แล้วเรนเดอร์หนึ่งครั้งต่อหนึ่งภาษา
SUBJECT = lazy_gettext(t"Your order shipped")
for user in users:
with use_translations(load_translations(user.locale)):
send(user.email, str(SUBJECT))
การผูกอยู่ใน ContextVar ไม่ใช่สแตกที่แขวนไว้บนอ็อบเจกต์ที่ใช้ร่วมกัน
คำขอที่ทับซ้อนกันจึงหยิบภาษาของกันและกันไปไม่ได้ — รวมถึงกรณีที่มันออกจากบล็อก
ตามลำดับเดียวกับที่เข้าไป ซึ่งเป็นการสลับกันแบบที่สแตกทำผิดพลาด
การโหลดแคตตาล็อกทีละภาษามีต้นทุนต่ำ: gettext.translation() แจงไฟล์ .mo
แต่ละไฟล์เพียงครั้งเดียว แล้วแจกสำเนาที่ใช้แคตตาล็อกที่แจงแล้วร่วมกัน
เธรดทำงานจะสืบทอดการผูกหรือไม่ ขึ้นอยู่กับบิลด์
threading.Thread เปล่า ๆ หรือ ThreadPoolExecutor.submit
เริ่มต้นจากสำเนาของคอนเท็กซ์ของผู้เรียก หรือไม่ก็จากคอนเท็กซ์ว่าง ๆ
และตัวที่ตัดสินว่าจะเป็นแบบใดคือ sys.flags.thread_inherit_context —
ซึ่งเป็นจริงโดยปริยายบนบิลด์แบบ free-threaded และเป็นเท็จในที่อื่นทั้งหมด
โค้ดเดียวกันจึงเรนเดอร์ภาษาที่ผูกไว้บน 3.14t แต่เรนเดอร์แคตตาล็อก
gettext ที่เป็น global ของโปรเซสบน 3.14
ให้พาคอนเท็กซ์ติดไปด้วยแทนที่จะพึ่งค่าปริยาย:
ส่วน asyncio.to_thread ทำสิ่งนี้ให้คุณอยู่แล้ว
ค่าที่รู้จักโลแคล¶
ไลบรารีนี้ตัดสินว่าค่าจะไปปรากฏ ตรงไหน ในข้อความที่แปลแล้ว
มันไม่ได้แปลงค่าตัวนั้นให้เข้ากับท้องถิ่น {amount:,.2f} คือ format spec
ของ Python ที่มีพฤติกรรมตายตัว — จุลภาคทุกสามหลักและจุดก่อนทศนิยม —
และมันผลิตอักขระชุดเดิมไม่ว่าข้อความจะอยู่ในภาษาใด:
ภาษาเยอรมันเขียนตัวเลขนั้นว่า 1.234,50 ภาษาฝรั่งเศสเขียน 1 234,50
และภาษาฮินดีจัดกลุ่ม 1234567 เป็น 12,34,567 ไม่ใช่ 1,234,567
ตัวเลข สกุลเงิน วันที่ เวลา และหน่วยวัดเป็นงานของ Babel
จงจัดรูปแบบค่าก่อน แล้วค่อยวางสตริงที่เสร็จแล้วลงไป:
from babel.numbers import format_currency
total = format_currency(amount, "EUR", locale=locale)
tr(t"Your order comes to {total}")
สำหรับข้อความที่มีการนับ ตัวเลขทำงานสองอย่าง — มันเลือกรูปพหูพจน์ และมันปรากฏอยู่ในตัวข้อความ — และมีเพียงอย่างที่สองเท่านั้นที่ถูกแปลงตามท้องถิ่น จงเก็บจำนวนดิบไว้สำหรับการเลือก และส่งสตริงที่จัดรูปแบบแล้วไปแสดงผล:
from babel.numbers import format_decimal
shown = format_decimal(n, locale=locale)
_.ngettext(t"One file", t"{shown} files", n)
การจัดรูปแบบก่อนถึงจุดเรียกใช้ยังเป็นสิ่งที่กัน format spec ไม่ให้เข้าไปในแคตตาล็อกด้วย สิ่งที่นักแปลเห็นคือข้อความชิ้นที่เสร็จแล้ว ไม่ใช่ตัวเลขบวกกับคำสั่งวิธีเรนเดอร์มัน
เมื่อแคตตาล็อกผิดพลาดจะเกิดอะไรขึ้น¶
หากตัวยึดตำแหน่งของคำแปลไม่ตรงกับข้อความต้นทาง — ฟิลด์ที่หายไป ไม่รู้จัก หรือถูกเปลี่ยนรูปแบบ ซึ่งหลุดรอดการตรวจสอบมาจาก MO ที่แก้ด้วยมือ แคตตาล็อกจากผู้ขาย หรือไปป์ไลน์ที่ข้ามตัวตรวจ — พฤติกรรมเริ่มต้นคือ การเรนเดอร์ข้อความต้นทางแทนที่จะโยนข้อยกเว้น ซึ่งสอดคล้องกับสัญญาของ gettext เองที่ว่าแคตตาล็อกที่เสียต้องไม่ทำให้แอปพลิเคชันพัง
เมื่อ Hello {name} ถูกแปลเป็น こんにちは {nombre} การเรนเดอร์จะสำเร็จ
และมีคำเตือนหนึ่งรายการส่งไปยัง logger gettext_tstrings
WARNING gettext_tstrings: invalid translation for msgid 'Hello {name}'; using
source text: translation does not match the source placeholders: {name} is
missing; {nombre} is not in the source message
คำเตือนเกิดขึ้นครั้งเดียวต่อคู่ของข้อความและแพตเทิร์น ไม่ใช่ทุกครั้งที่เรนเดอร์ รายการแคตตาล็อกที่เสียจึงไม่ท่วมล็อก
สำหรับการทดสอบและ CI คุณเลือกให้ล้มเหลวแบบชัดเจนได้
strict = Translator(translations, strict=True)
tr(t"Hello {name}", translations=translations, strict=True)
การค้นหาเดียวกันจะโยนข้อยกเว้นแทน โดยพาประโยคเดียวกันไปด้วย แต่ไม่มีส่วน "using source text"
>>> strict(t"Hello {name}")
Traceback (most recent call last):
...
gettext_tstrings.errors.InvalidTranslationError: translation does not match the
source placeholders: {name} is missing; {nombre} is not in the source message
ข้อความเหล่านี้เขียนขึ้นสำหรับคนที่ลงมือแก้ปัญหาได้ ซึ่งสำหรับปัญหาในแคตตาล็อก
มักเป็นนักแปลมากกว่าโปรแกรมเมอร์ — ดังนั้นเมื่อตัวยึดตำแหน่งดูเหมือนมีอยู่แต่จริง ๆ
แล้วไม่มี ข้อความจะอธิบายเหตุผลแทนที่จะย้ำว่ามันหายไป วงเล็บปีกกาแบบเต็มความกว้าง
การซ้อนเป็น {{name}} no-break space ที่มองไม่เห็น อักษรซีริลลิกที่ปนอยู่ในอักษรละติน:
แต่ละกรณีมีถ้อยคำของตัวเอง มีรายการพร้อมตัวอย่างอยู่ใน
สำหรับนักแปล
หน้านั้นเขียนขึ้นเพื่อส่งมอบให้คนที่เป็นผู้แก้ไข .po
การเรนเดอร์แพตเทิร์นโดยไม่ใช้แคตตาล็อก¶
compile_template เปิดเผยกลไกเดียวกันในระดับที่ต่ำลงหนึ่งขั้น: มันแปลง
t-string เป็น msgid พร้อมชุดค่าที่ผูกไว้ และเรนเดอร์แพตเทิร์นใดก็ตาม
ที่คุณส่งให้
from gettext_tstrings import compile_template
name = "Ada"
compiled = compile_template(t"Hello {name}")
compiled.msgid # "Hello {name}"
compiled.placeholders # ("name",)
compiled.render("こんにちは {name}") # "こんにちは Ada"
render ตรวจสอบด้วยกฎเดียวกันและโยนข้อยกเว้นเสมอเมื่อไม่ตรงกัน
ที่นี่ไม่มีโหมดผ่อนปรน: ความผ่อนปรนมีไว้เพื่อให้การค้นหาในแคตตาล็อก
ถอยกลับไปใช้ข้อความต้นทางได้ แต่แพตเทิร์นที่คุณส่งเข้ามาเอง
ไม่มีอะไรให้ถอยกลับ
ความปลอดภัยและขอบเขต¶
แบบนี้ใช้ได้
แบบเหล่านี้ถูกปฏิเสธโดยตั้งใจ
ให้คำนวณค่าที่มีความหมายก่อน
ข้อจำกัดนี้ทำให้ได้คีย์แคตตาล็อกที่เสถียร ให้ชื่อที่มีประโยชน์แก่นักแปล และป้องกันไม่ให้สตริงที่แปลแล้วกลายเป็นภาษาสำหรับเขียนนิพจน์
การรับประกันจำกัดขอบเขตอยู่ที่โครงสร้างและการจัดรูปแบบ: คำแปลไม่มีวัน ถูกประเมินผล และไม่มีทางเพิ่มการเข้าถึงแอตทริบิวต์ การเรียกฟังก์ชัน การแปลงค่า หรือ format spec ได้ สองสิ่งยังคงเป็นความรับผิดชอบของผู้เรียก เช่นเดียวกับ gettext ของไลบรารีมาตรฐานทุกประการ — การ escape ผลลัพธ์ที่เรนเดอร์แล้วให้เหมาะกับปลายทาง (HTML เชลล์ เทอร์มินัล) และความสมบูรณ์ของแคตตาล็อก เนื่องจากแคตตาล็อกที่ประสงค์ร้ายสามารถ ทำซ้ำตัวยึดตำแหน่งเพื่อขยายขนาดผลลัพธ์ได้ ซึ่งเป็นธรรมชาติของ i18n แบบใช้ตัวยึดตำแหน่งทุกระบบ