指南¶
本頁是執行階段參考:目錄備妥之後,應用程式碼透過本函式庫所做的一切都在這裡。 如果你還沒看過完整的循環——標記、擷取、翻譯、編譯、執行——教學 會用五分鐘走過一遍;目錄的建立與驗證請見擷取;團隊如何讓這個 循環持續轉動——更新週期、CI、翻譯平台——則在正式環境實務。
我該用哪個入口點?¶
本套件之所以匯出好幾種翻譯訊息的方式,是因為應用程式繫結語言的方式本來就有好幾種。 請依照你的程式如何決定當前語言來挑選:
| 你的情況 | 使用 |
|---|---|
| 整個行程只有一種語言——CLI、桌面應用程式、腳本 | Translator,當作 _ 呼叫 |
| 每個請求或每個非同步工作各一種語言——Web 應用程式 | 用 use_translations() 包住這段工作,再呼叫 tr() |
| 在 import 時就定義好的訊息——表單標籤、列舉、常數 | lazy_gettext() 或 lazy_pgettext() |
| 由數量決定措辭 | ngettext() / npgettext(),以上述任一形式 |
| 不牽涉目錄,只渲染一個 pattern | 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
模組層級的函式沿用標準函式庫的名稱,以及僅限位置引數的呼叫慣例:
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 分別是 gettext 與 ngettext 的完全別名。
逐請求切換語言¶
Web framework 會為每個請求挑選語言。把該請求的翻譯繫結到目前的上下文,之後每一次 模組層級的呼叫都會解析到那個語言,而且在並行的請求之間彼此安全隔離:
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}"))
對於自行管理請求生命週期的 framework,set_translations(translations) 不需要
with 區塊即可繫結;get_translations() 則讀取目前的繫結。明確傳入的
translations= 引數一律優先於上下文,而未繫結的上下文會回退到標準函式庫全域
安裝的 gettext 函式。Flask 與 ASGI 中介軟體的完整範例,請見
正式環境實務頁。
延遲翻譯¶
t-string 會立刻捕捉它的值,但對於在 import 時就定義的字串——表單標籤、列舉值、 模組常數——這並不正確,因為它們必須以被使用當下生效的語言渲染。
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。需要當作 key 時,請先呼叫 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——在自由執行緒建置上預設為真,在其他任何地方
都為假。因此同一份程式碼在 3.14t 上渲染出被繫結的語言,在 3.14 上卻渲染出行程
層級的全域目錄。請把上下文傳過去,而不要依賴預設值:
asyncio.to_thread 已經替你做好這件事了。
隨語言而異的值¶
本函式庫決定的是一個值出現在譯文訊息中的位置,而不是這個值本身的在地化。
{amount:,.2f} 是行為固定的 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)
在呼叫之前先格式化,也正是讓格式規格不會跑進目錄的做法:譯者看到的是一段完成的 文字,而不是一個數字外加一串渲染指示。
目錄出錯時會發生什麼事¶
如果翻譯的佔位符與原文不符——缺漏、未知,或被改寫格式的欄位躲過了驗證,來自手動 編輯的 MO、外部廠商的目錄,或是略過檢查器的流程——預設行為是渲染原始訊息,而不是 拋出例外。這與 gettext 自身的契約一致:損壞的目錄絕不該弄壞應用程式。
當 Hello {name} 被翻譯成 こんにちは {nombre},渲染仍會成功,並向
gettext_tstrings logger 送出一則警告:
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
警告只會就每一組訊息與 pattern 觸發一次,而不是每次渲染都觸發,因此一筆損壞的 目錄項目不會灌爆日誌。
測試與 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 的人而寫的。
不透過目錄渲染 pattern¶
compile_template 把同一套機制往下暴露一層:它把 t-string 轉成 msgid 加上一組
繫結好的值,並渲染你交給它的任何 pattern。
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 以相同的規則驗證,而且只要不相符就一律拋出例外。這裡沒有寬鬆模式:
寬鬆存在的用意,是讓目錄查詢能夠退回原文,而你自己傳進來的 pattern 沒有可退回
的對象。
安全性與適用範圍¶
這樣寫是有效的:
這些則是刻意被拒絕的:
請先算出一個有意義的值:
這項限制帶來穩定的目錄 key,讓譯者拿到有意義的名稱,也讓被翻譯的字串不至於變成 一種運算式語言。
這項保證的範圍限於結構與格式:翻譯永遠不會被求值,也永遠無法加入屬性存取、 呼叫、轉換或格式規格。有兩件事仍屬於呼叫端的責任,和標準函式庫的 gettext 一模 一樣——依輸出去向(HTML、shell、終端機)對渲染結果進行跳脫,以及維護目錄 完整性,因為惡意的目錄可以重複佔位符來放大輸出量,而這是任何以佔位符為基礎的 i18n 都固有的性質。