常見陷阱¶
本站被翻成三十五種語言,而且每一種都是照著這份文件所教的循環跑出來的。以業界 標準來看,這是個很小的語料庫,但即使如此,也已經足以踩中大多數讓 i18n 比看起來 更難的陷阱。
以下每一節都是這裡真正出過的狀況:當時看起來是什麼樣子,以及本函式庫替你檢查的 範圍與該由你自行判斷的範圍,界線落在哪裡。
改個變數名稱,就等於把整句話重新翻一遍¶
msgid 就是目錄的 key,而被插入的名稱就在它裡面。把一個常數移到模組層級,並照
Python 風格慣例把它改成大寫——author 變成 AUTHOR——就讓
Copyright © 2026 {author} · MIT License 變成一則任何目錄都沒見過的訊息。這一行
的每一種翻譯都得重新走一趟 fuzzy 流程,每種語言都要走,而這次改名對讀者看得見的
東西毫無影響。
本函式庫不會阻止你:兩種寫法都是合法的佔位符名稱。它做的是讓這個名稱值得 保護——插值必須是單純的名稱,所以放進目錄 key 裡的東西是譯者讀得懂的詞,而不是一段運算式。
反過來的情況則是結構上就安全的。轉換與格式規格不屬於 msgid,因此把
{amount:,.2f} 收緊成 {amount:,.0f} 不會改動任何 key,也不會讓任何地方的任何
翻譯失效。
nplurals=2 不代表有兩個不同的字串¶
土耳其文、匈牙利文、波斯文與孟加拉文都宣告了兩種複數形式,而在這四種語言裡,
一則帶數量訊息的兩種形式理所當然就是同一個字串——名詞接在數詞之後仍維持單數,
所以 {n} sayfa 用在一頁與十頁都是對的。要是有審閱者把這種「重複」給「修好」了,
翻譯就壞了。
反方向的錯誤同樣容易犯。拉脫維亞文的第三種形式只為零而存在;斯洛維尼亞文的
第二種是雙數,剛好用於二;羅馬尼亞文的最後一種形式需要 de 這個字,而前兩種
偏偏不能有。把這些欄位填上一個單數與一個複數,會產出一份只在沒人測試的數量上
出錯的目錄。
更麻煩的是,這些欄位的順序並不具語意。威爾斯文替它的五種形式編號時,
msgstr[0] 是通用情況,msgstr[1] 才是單數。照直覺的順序填下去,就會把單數放在
每一則沒有數量的訊息都會取用的位置上。
本函式庫完全不承擔這些事,而這正是重點:目標語言的複數規則寫在它自己的目錄標頭 裡,而聯集/交集規則允許一則翻譯擁有比來源更多或更少的形式。它會檢查 的,是它在不懂該語言的前提下唯一能檢查的事——每一種形式都保有它需要的佔位符。
兩種形式長得一樣,也可能是有道理的¶
愛爾蘭文有五種複數形式,而在本站的建置報告裡,其中好幾種拼起來一模一樣。這不是
複製貼上出的差錯:leathanach 以 l 開頭,而愛爾蘭文數詞會觸發的兩種字首變音,
在 l 上都不會寫出來。這些形式仍然實實在在地發揮作用——詞幹會在 leathanach 與
leathanaigh 之間交替,而十以上的數量又會回到單數——只是任何意思為「頁」的名詞
都顯示不出這個對比。
任何把重複形式標記為可疑的檢查,都會誤判正確的愛爾蘭文。這件事唯一的審閱者,是 懂這個語言的人。
一則訊息只能跟一個數量取得一致¶
本站的建置報告會說出渲染了幾個頁面、花了多久。把它寫成
「Rendered {n} pages in {seconds} seconds」看起來人畜無害,卻是不可翻譯的:
gettext 只從一個數量選出一種形式,而那個數量是 n。seconds 這個字得跟一個複數
機制根本看不到的數字取得一致。
修法是把第二個數量改成單位符號而不是單字,而單位符號本身也是要在地化的:本站的
各份目錄裡就帶著 s、с、ث、שנ׳ 與 mp,而法文、西班牙文與瑞典文的排版
習慣要在符號前加一個空格,英文則不加。這些都不歸本函式庫管——但察覺到一則訊息
需要兩重一致,這件事歸你,而唯一的工具就是把訊息換個寫法。
改動一句英文,就是在改動外語的文法¶
首頁原本寫的是「all ten language editions」。把那個數字拿掉——一次只動一個英文 單字的編輯,理由是那個數字老是過時——卻讓一個複數主語變成了單數。西班牙文、 義大利文、葡萄牙文、俄文、烏克蘭文、希臘文、荷蘭文與希伯來文全都得重新調整動詞 的一致;其中好幾種連分詞也得跟著改。
一次在英文裡讀起來微不足道的來源改動,到了下游並不微不足道。把它標成 fuzzy——
pybabel update 做的正是這件事——就是讓每位譯者有機會察覺的機制。
看不見的差異能在每一次複製貼上中存活¶
指南裡引用了一則含有 (nаme) 的診斷訊息——這是一個刻意保留的跳脫寫法,因為它
指的那個字元是西里爾字母 а,沒有讀者能把它跟拉丁字母的那個分辨開來。本站的
譯者曾經五次把這個跳脫寫法改回真正的字元,發生在五種不同的語言裡,每一次都
產出一個看起來正確、其實錯誤的頁面。
這一項本函式庫確實攔得下來,而這也正是那些診斷訊息長成那個樣子的原因:字母混用 了不同書寫系統的佔位符會被顯示兩次, 一次好讀,一次跳脫,因為跳脫後的寫法是唯一能把兩者區別開來的形式。大括號裡的 no-break space 會以碼位印出,理由完全相同。目錄檢查器會在這則訊息出貨之前就先 擋下它。
非空不等於已翻譯¶
一份把 msgid 複製進 msgstr 當骨架的目錄,能通過每一項天真的檢查:沒有空的、沒有 fuzzy 的,訊息集合也完全吻合。本站有一個語言版本就這樣上線了好幾個小時。另一個 語言版本裡有八個頁面是英文原稿的逐位元組副本,情況也一樣——這種頁面能通過「比對 兩者程式碼區塊」的檢查,因為它們根本就是同一個檔案。
這兩件事都不是翻譯函式庫看得見的。兩者都很容易測,但不是靠「要求每一筆條目都必須
與來源不同」:OK、產品名稱、人名、縮寫與程式碼識別字都會翻譯成自己,禁止這種情況
的檢查會永無止境地產生偽陽性。
改為測量比率——以整份目錄或整個頁面為單位——再把離群值交給人看。本站自己的測試 做的正是這件事:它把每個語言版本的散文行拿來與英文原稿比對,相同比率超過 25% 就 失敗。那個偽造的語言版本落在 87%;每一個貨真價實的翻譯都落在 4% 到 8% 之間,也就是 那一小撮理所當然會雷同的行,例如網址與引用的程式輸出。兩群數字離得夠遠,門檻不必 訂得多精準。
要翻譯的不只有目錄¶
這裡有兩次失敗跟 gettext 一點關係都沒有。
翻譯一個標題會改動由它產生的錨點,於是每一條指向該小節的跨頁連結都會斷掉——而且 是無聲地斷,只在那一種語言裡斷。本站在每個標題上都釘住英文錨點,並由一個測試從 英文頁面推導出預期的清單。
另外,網站產生器隨附六十八種語言的介面翻譯,其中並不包含史瓦希里文與愛爾蘭文。 少了它,建置不會降級回英文;模板的 include 會失敗,那個語言版本根本建不起來。 本儲存庫自己有兩個檔案的存在就是為了補上這個缺口。
你的工具也有 bug¶
這份文件推薦用來抓出過期目錄的 CI 步驟 pybabel update --check,對任何使用
pgettext 或 npgettext 的專案都做不到那件事。在 Babel 2.18.0 上,它會把每一份
含有 msgctxt 的目錄都報成過期,每次執行都如此。這項比對是走 Catalog.is_identical
完成的,而它是用每則訊息被存放時所用的 key 去查找——對帶上下文的訊息來說,那個
key 是 (id, context) 這一組,而 Catalog.get 並不接受這種形式。查找什麼也沒
找到,於是兩份目錄永遠比不出相等:
>>> from babel.messages.catalog import Catalog
>>> c = Catalog(locale="ja")
>>> c.add("Guide", "ガイド", context="navigation")
<Message 'Guide' (flags: [])>
>>> c.is_identical(c)
False
這是在這裡試著使用它時發現的,已經回報上游,而替代的檢查則 在正式環境那一頁。
一般性的教訓則很不舒服:一道永遠亮紅燈的關卡比沒有關卡更糟,因為團隊會把它關掉。 在你信任一道 CI 檢查會正確失敗之前,先確認它真的有辦法通過。
本函式庫是為了什麼,一句話說完¶
本頁大部分內容都是沒有工具能代勞的判斷。工具能做的,是保證一則翻譯無法改動它 所翻譯的那句話的結構——不能刪掉一個值、不能憑空造一個、不能替它重新設定格式,也 不能伸手進你的物件裡——並且能用一句話把這件事說給那個必須動手修的人聽。這就是本 函式庫承諾的全部,而本站其餘的內容講的都是它如何守住這個承諾。