**i18n 語系檔驗證不能停在 JSON 能否解析。**真正需要守住的是:同一個畫面需要的鍵是否存在、重複名稱是否被悄悄覆蓋,以及翻譯後是否仍保有呼叫端提供的插值參數。本文以 Python 3.11.15 的標準函式庫建立唯讀、記憶體內的測試,不載入應用程式資料,也不改寫既有語系資產。
這是資訊型搜尋內容,面向維護多語系介面的工程師與技術主管。來源筆記提供語系盤點的問題分類;本文只採用一般化的缺鍵、動態鍵與佔位符概念,不沿用原始環境、產品名稱、數量或處理成果。核心結論是:結構驗證是必要條件,不是翻譯正確或畫面正常的充分條件。
先定義可驗證的語系檔契約
i18n 語系檔驗證應先固定檔案形狀、基準語言與必要鍵集合,再檢查重複名稱及插值參數;只有語法合法,並不能保證畫面能取得正確文字,也不能證明翻譯符合使用情境。
本文的最小契約是一個扁平 JSON 物件:鍵是文字識別名稱,值必須是非空字串。範例不接受巢狀物件、陣列或數值;這是測試工具刻意設定的範圍,不是宣稱所有國際化框架都不支援那些結構。若專案使用巢狀命名空間,應另訂路徑展開規則,避免把點號鍵與真正巢狀路徑視為同一件事。
基準語言的角色是提供比較集合,而不是自動成為真相。假設所有語言都少了一個動態產生的標題鍵,單純拿各語言與英文比較,仍可能得到零缺漏。必須把畫面需求獨立寫成 required 集合,分別檢查基準及目標語言是否覆蓋。範例使用一般介面名稱,不涉及任何外部資料來源。
明確區分三種結果:解析失敗代表檔案結構不可接受;契約差異代表鍵或插值需要處理;人工審查待確認代表語意尚未核准。不要把最後一種情況塞成機械檢查通過,也不要讓一個綠色圖示同時代表三種不同證據。
在轉成字典之前攔截重複鍵
重複鍵必須在解析配對清單時攔截,不能等轉成字典後才檢查;Python 預設解碼會保留相同名稱的最後一個值,因此後續集合比較可能看不出先前文字已被覆蓋。
Python 官方 json 文件明確說明重複名稱的預設處理方式。對語系維護而言,這不是語法工具的小細節:兩次新增相同鍵時,畫面可能改用後面的值,檔案卻仍能解析。若驗證僅執行一般 json.loads,再比較字典長度,就已經錯過保留重複資訊的時間點。
範例透過 object_pairs_hook 接收有順序的名稱和值配對。在建立結果字典之前,逐一確認名稱尚未出現;只要重複就丟出 ValueError。測試先證明預設解碼確實留下第二個值,再證明嚴格載入器拒絕同一份資料。這樣同時覆蓋「問題存在」和「守門規則有效」,而不是只測一份乾淨輸入。
這個 hook 會參與物件解碼,但本文最終仍拒絕巢狀值。不要因為內部物件也經過 hook,就推論整套工具已支援所有巢狀資源形式。擴充時應同時增加合法與不合法案例,並定義錯誤應回報哪個命名空間;目前範例只回報類別,不輸出原始文字內容。
動態鍵需要獨立清單,而不是只掃字面字串
靜態字串掃描只能證明程式碼出現過哪些固定名稱,不能證明執行時不會產生其他鍵;動態選單、狀態與標題應由獨立需求清單補上,並同時檢查基準及目標語言。
搜尋固定的翻譯呼叫很適合作為盤點起點,卻不適合直接成為刪除依據。當程式把畫面型別組合成鍵,或從已列舉的元件狀態選擇文字時,掃描器看到的是變數,而不是最終值。把「沒有搜尋命中」解讀成「不會使用」,會將靜態分析的能力邊界誤認成應用程式事實。
可維護的作法是讓每個有動態鍵的元件提供有限清單,經審查後合併成必要集合。清單要有負責人和更新條件;元件新增狀態時,應同時變更清單與測試。測試案例刻意讓 panel.title 不存在於兩個語言,預期同時回報基準缺漏與目標缺漏,證明基準語言不能掩蓋共同遺漏。
目標語言多出的鍵不應立刻被刪除。它可能屬於待切換畫面、相容期或尚未納入的命名空間。本文要求目標語言涵蓋所有基準鍵與 required 鍵,並另外檢查基準是否缺少 required 鍵;沒有替目標額外鍵做使用判斷。若要整理過期鍵,必須另外結合元件契約、執行時觀察及人工核准,不能順手把檢查器變成清理器。
佔位符要比較名稱與次數,不要求句序相同
佔位符驗證應容許不同語言調整句子順序,同時保留參數名稱與出現次數;用多重集合比較,比只確認名稱集合更嚴格,但仍須清楚界定可辨識的插值語法範圍。
本文只處理雙大括號內的一般識別名稱,例如 {{name}},並容許名稱前後空白。使用 Counter 記錄每個名稱的次數,可抓出同一參數原本出現兩次、翻譯卻只剩一次的差異。兩個不同參數交換位置則不構成錯誤,因為自然語言常需要不同詞序。要求整段文字逐字相同,反而會阻礙正常翻譯。
這是一項保守的編輯政策,不是所有框架強制的語言規則。有些語言可能合理省略重複稱呼;若專案允許此類變化,應以具名例外與審查紀錄處理,不能悄悄把所有重複次數檢查移除。也不要將通過比對理解成參數型別、日期格式或複數規則都已正確。
i18next 原始文件顯示其插值能力比本文子集合更廣。範例故意拒絕點號路徑、格式選項、其他大括號寫法,以及任何未被辨識的剩餘括號;這是為了讓送入 slots() 的不支援語法顯性失敗。audit() 的插值語法與名稱/次數比對只涵蓋兩份資源的共有鍵,不是所有項目的完整語法掃描;基準獨有鍵會回報目標缺鍵,目標獨有鍵則不做插值檢查。若資源採用 ICU、巢狀參數或自訂分隔符號,應改用對應解析器,不宜不斷加長正則表示式冒充完整語法實作。
本機驗證:固定版本與可重現測試
驗證應在固定版本的本機測試檔執行,使用人工建立的記憶體內資料;預期結果包含拒絕重複鍵、找到共同缺漏與辨識插值次數差異,任何斷言失敗都必須留下非零結束碼。
前置條件是已安裝 Python 3.11.15,並準備一個新的本機練習資料夾。無須安裝第三方套件,無須連線服務。將下列完整程式儲存為 i18n_contract_test.py。版本檢查是範例的一部分,版本不同會停止,而不是混用結果。
- 在練習資料夾執行
python --version,確認顯示Python 3.11.15,再儲存完整程式。不要把檔案放進既有語系目錄。 - 執行
python i18n_contract_test.py ContractTests.test_duplicate,通過條件是該測試為ok且結束碼為零;若嚴格載入器沒有拒絕重複鍵,測試必須失敗。 - 執行
python i18n_contract_test.py ContractTests.test_dynamic_required,通過條件是同時精確辨識兩種缺漏;只找到目標語言缺漏仍算失敗。 - 執行
python i18n_contract_test.py ContractTests.test_placeholder_count,確認少一個重複參數被攔截,交換參數順序則可通過。 - 執行
python i18n_contract_test.py,預期五個測試全部為ok、總結OK且結束碼為零。額外案例確認正常資料可通過,不支援的結構與語法會被拒絕。
import json
import re
import sys
import unittest
from collections import Counter
def unique_pairs(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError('duplicate key')
result[key] = value
return result
def catalog(text):
value = json.loads(text, object_pairs_hook=unique_pairs)
if not isinstance(value, dict):
raise ValueError('expected object')
if any(not isinstance(v, str) or not v.strip() for v in value.values()):
raise ValueError('expected nonempty strings')
return value
def slots(text):
pattern = r'\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}'
remainder = re.sub(pattern, '', text)
if '{' in remainder or '}' in remainder:
raise ValueError('unsupported placeholder syntax')
return Counter(re.findall(pattern, text))
def audit(base, target, required):
errors = []
for key in sorted(required - base.keys()):
errors.append('base-missing:' + key)
for key in sorted((base.keys() | required) - target.keys()):
errors.append('target-missing:' + key)
for key in sorted(base.keys() & target.keys()):
if slots(base[key]) != slots(target[key]):
errors.append('placeholder:' + key)
return errors
class ContractTests(unittest.TestCase):
def test_duplicate(self):
text = '{"label":"first","label":"second"}'
self.assertEqual(json.loads(text)['label'], 'second')
with self.assertRaisesRegex(ValueError, 'duplicate'):
catalog(text)
def test_dynamic_required(self):
base = catalog('{"hello":"Hello {{name}}"}')
target = catalog('{"hello":"Welcome {{name}}"}')
self.assertEqual(audit(base, target, {'hello', 'panel.title'}),
['base-missing:panel.title', 'target-missing:panel.title'])
def test_placeholder_count(self):
base = catalog('{"hello":"{{name}} / {{name}}"}')
target = catalog('{"hello":"{{name}}"}')
self.assertEqual(audit(base, target, {'hello'}), ['placeholder:hello'])
self.assertEqual(slots('{{first}} {{second}}'), slots('{{second}} {{first}}'))
def test_valid(self):
base = catalog('{"hello":"Hello {{name}}"}')
target = catalog('{"hello":"Welcome {{ name }}"}')
self.assertEqual(audit(base, target, {'hello'}), [])
def test_rejected_shapes(self):
for text in ['[]', '{"x":" "}', '{"x":{}}']:
with self.assertRaises(ValueError):
catalog(text)
for text in ['{{user.name}}', '{{- name}}', '{0}']:
with self.assertRaises(ValueError):
slots(text)
if __name__ == '__main__':
if sys.version_info[:3] != (3, 11, 15):
raise SystemExit('Requires Python 3.11.15')
unittest.main(verbosity=2)
預期測試摘要(執行耗時因環境而異):
Ran 5 tests in ...s
OK
**排程環境實際結果:**使用 Python 3.11.15 執行完整測試,五個測試通過,程序結束碼為零。這是合成資料的契約測試結果,不是應用程式整合驗收,也沒有量測效能。瀏覽器語系載入、框架 fallback、畫面呈現及翻譯語意未在本次排程執行;讀者應另以固定版本的測試專案驗收。
把機械檢查與語言審查分開
機械檢查能辨識結構、缺鍵與參數差異,無法判斷術語是否符合畫面用途;應讓語言審查獨立核對情境、區域用語與控制項差異,再決定是否接受一份翻譯資源。
同一個英文詞在不同畫面可能代表不同概念。即使翻譯語法自然、插值完整,也可能讓使用者理解成另一種操作。審查者應看到介面用途、相鄰欄位與現有術語表,而不是只收到一張脫離畫面的字串表。不同控制項若被翻成完全相同的名稱,也應回到互動流程確認是否造成辨識困難。
文字系統掃描只能做提示,不能自動判定混入其他語言。縮寫、產品名稱和刻意保留的英文,可能在目標語言中完全合理。若建立警示清單,應記錄人工判讀結果與例外理由;不要透過自動替換把既有術語改成看似更在地、實際卻不一致的詞。
同理,fallback 能讓畫面有文字,不代表目標語言已完成。本文沒有啟動 i18next,也沒有驗證任何載入鏈;不將官方文件的框架行為寫成本次測試成果。畫面驗收應另觀察載入的資源、所選語言及實際顯示文字,並區分資源缺漏與顯示層問題。
來源、採用清單與衡量方式
採用此方法時,先將契約檢查列為變更審查的一部分,再以真實缺漏與人工審查結果調整政策;官方文件界定函式行為,來源筆記提供問題背景,測試結果則只支持已執行的案例。
官方查核依據如下,兩個語言版本採用相同文件與同一組測試事實:
- Python 3.11 json 文件:預設重複名稱處理,以及
object_pairs_hook接收配對清單的行為。 - Python 3.11 collections 文件:
Counter用於計算可雜湊項目的出現次數。 - i18next 官方插值文件原始檔:雙大括號插值與更廣的資料模型語法,支持本文明確標示子集合限制。
以上線前的測試資產審查而言,可用下列清單收斂討論:
- 已定義扁平或巢狀結構,不把兩種鍵空間混在一起。
- 重複鍵在轉字典前被拒絕,測試包含故意失敗的資料。
- 必要鍵清單獨立於基準語言,動態鍵有明確維護責任。
- 佔位符比較保留名稱及次數,語序變化不被誤判。
- 共有鍵的不支援插值語法明確停止,獨有鍵不納入插值比對;翻譯語意由人工審查。
- 未執行的瀏覽器與框架整合項目,沒有被包裝成已通過。
本文屬於 MOFU 技術評估內容,搜尋意圖仍為資訊型。內容成效可追蹤 GSC 查詢曝光、點擊率,以及文章導向相關技術作品頁的點擊;這些是後續觀察指標,不是本次已達成成果。工程面的衡量則看已確認的缺鍵、重複鍵與插值差異數,避免以檔案總鍵數或機械通過率代替實際品質。
**結論:先證明資源契約成立,再證明翻譯與畫面成立。**從唯讀、固定版本、合成資料的測試開始,可以建立清楚的責任邊界;擴充到其他資源語法之前,先補齊對應的通過與失敗案例,並明確區分未檢查的獨有鍵與已驗證的共有鍵。