英文按鈕在開發機上可能只有六個字母,換成德文後卻可能占滿整行;左側的返回按鈕到了由右至左的介面中,也可能仍指向錯誤方向。若等到所有翻譯完成後才發現這些問題,修正時將同時影響版面、測試與發布時程。更穩妥的做法,是在雲端 Mac 的每次合併檢查中主動擴展字串、翻轉版面方向,並直接將不可見或無法操作的控制項判定為失敗。
先定義門檻要攔截的問題
偽本地化不是把介面替換成一組難以閱讀的字元,而是透過可控的轉換放大版面風險。建議先建立兩種模式:
expanded:保留原文,在兩側加上標記,並將字串擴展約 40%。rtl:保留可辨識的內容,但啟用由右至左的語意方向,以檢查排列與圖示。- 正常模式:作為對照組,確認測試輔助程式碼沒有改變產品行為。
門檻主要檢查四類問題:文字控制項的可見區域明顯不足;主要按鈕不存在、無法點擊或超出視窗;介面出現未經本地化入口處理的硬編碼文案;導覽、水平列表及方向圖示沒有隨 RTL 調整。
偽本地化適合揭露結構問題,並不負責判斷譯文是否正確。實際語言的驗收仍不可或缺,但不應承擔找出固定寬度這類基礎故障的成本。
建立可控的字串轉換層
不要在測試中逐一走訪視圖並修改標籤。這樣會繞過應用程式實際取得字串的路徑,也無法發現硬編碼。更可靠的方式,是讓專案既有的本地化入口支援一個只在偵錯建置中生效的轉換器。
enum PseudoMode: String {
case expanded
case rtl
}
#if DEBUG
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
guard let mode else { return value }
switch mode {
case .expanded:
let padding = String(repeating: "·", count: max(2, value.count * 2 / 5))
return "[\(value)\(padding)]"
case .rtl:
return "\(value)"
}
}
#else
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
value
}
#endif
應用程式啟動時讀取 PSEUDO_LOCALE,且只接受明確的列舉值。所有面向使用者的字串都應經過統一入口,例如 L10n.text("checkout.confirm"),再交由轉換器處理。如果某個標題沒有顯示邊界標記,就能判定它繞過了本地化層。
RTL 模式還必須在應用程式根視圖設定語意方向。UIKit 可對測試視窗設定 semanticContentAttribute = .forceRightToLeft;SwiftUI 則可在根視圖注入 layoutDirection。不要反轉圖片內容,也不要強制顛倒數字、路徑與程式碼片段的順序。
使用 UI 測試檢查可見性與截斷
測試不需要逐像素比較。可先涵蓋登入後首頁、列表、詳細資料、表單及錯誤狀態等關鍵路徑,並為主要控制項設定穩定的 accessibility identifier。測試程序透過環境變數以對應模式啟動:
func launchApp(mode: String) -> XCUIApplication {
let app = XCUIApplication()
app.launchEnvironment["PSEUDO_LOCALE"] = mode
app.launchArguments += ["-UI_TESTING", "1"]
app.launch()
return app
}
func testExpandedCheckoutLayout() {
let app = launchApp(mode: "expanded")
let confirm = app.buttons["checkout.confirm"]
XCTAssertTrue(confirm.waitForExistence(timeout: 8))
XCTAssertTrue(confirm.isHittable)
XCTAssertFalse(confirm.frame.isEmpty)
XCTAssertTrue(app.windows.firstMatch.frame.contains(confirm.frame))
}
isHittable 比單純檢查 exists 更有價值:按鈕可能存在於輔助使用樹狀結構中,卻被彈出層遮住或位於螢幕之外。對於標籤類控制項,可透過 identifier 取得 frame,並驗證它沒有超出所屬容器。不要使用 OCR 猜測文字是否遭到截斷;應優先讓關鍵元件在偵錯建置中公開 intrinsicContentSize 與實際 frame 的差值。
將檢查範圍整理成清單
| 情境 | expanded 檢查 | RTL 檢查 | 失敗證據 |
|---|---|---|---|
| 導覽列 | 標題與操作不重疊 | 返回方向與順序正確 | 介面階層、控制項 frame |
| 表單 | 標籤可換行,錯誤文案完整 | 標籤與輸入軸一致 | 欄位 identifier、目前值 |
| 列表 | 長標題不會擠掉主要操作 | 附件與箭頭位置正確 | 失敗列索引、螢幕截圖 |
| 彈出層 | 按鈕完整且可點擊 | 操作順序符合語意 | 彈出層 frame、元素樹 |
在雲端 Mac 上組織執行環境
為每個工作建立獨立的模擬器裝置集,避免並行工作共用啟動狀態。裝置集目錄應放在工作的暫存目錄中,結束時無論成功或失敗都要執行清理。測試前固定 Xcode 路徑、模擬器機型與系統版本,並將這些值寫入報告。
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
export PSEUDO_LOCALE="${PSEUDO_LOCALE:-expanded}"
RESULT_DIR="$PWD/TestResults/$PSEUDO_LOCALE"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace App.xcworkspace \
-scheme AppUITests \
-destination "platform=iOS Simulator,name=iPhone 16,OS=latest" \
-resultBundlePath "$RESULT_DIR/LayoutTests.xcresult" \
PSEUDO_LOCALE="$PSEUDO_LOCALE"
expanded 與 rtl 應作為兩個獨立工作執行,而不是在同一個模擬器工作階段中切換。如此一來,失敗原因可明確歸屬,重新執行時也不會沿用上一個方向模式。若雲端 Mac 同時執行多個工作,每個工作還必須使用獨立的 DerivedData 路徑,以免測試產物互相覆寫。
處理常見誤報與實際故障
最常見的誤報來自動畫尚未結束、系統彈出層未關閉,以及測試資料長度不穩定。解決方式不是統一增加等待時間,而是等待明確狀態:載入指示器消失、目標元素出現、視窗數量穩定。測試資料也必須固定,尤其是使用者名稱、日期與伺服器錯誤訊息。
實際故障通常集中在固定高度、單行標籤、手動計算寬度,以及僅以視覺位置表達順序的元件。修正時應優先移除限制條件,而不是針對特定語言增加例外。允許按鈕標題換行後,還要重新檢查點擊區域是否涵蓋完整文字。
發現失敗時,至少應封存以下內容:
- 目前的偽語言模式、Xcode 與模擬器版本。
- 失敗控制項的 identifier、frame、label,以及是否可點擊。
- 目前頁面的輔助使用階層。
- 對應的
xcresult與失敗時的螢幕截圖。 - 重現該頁面所需的固定測試資料。
設定合併門檻並防止洩漏
門檻應阻擋會影響工作完成的問題:主要操作不可見或無法點擊、文字截斷後失去原意、RTL 順序錯誤,以及互動控制項缺少可存取名稱。輕微的間距變化可記錄為待複核項目,不必讓所有像素差異都阻止合併。
最後再加入一項 Release 驗收:掃描建置設定與應用程式資源,確認沒有測試專用的偽語言檔案,並驗證正式建置會忽略 PSEUDO_LOCALE。同時在正常模式下執行一組冒煙測試,避免轉換層本身改變字串。
這套流程的價值不在於產生一個看起來奇怪的介面,而是把多語言版面從發布前的人工抽查,轉化為每次程式碼變更都能重複執行的工程約束。先涵蓋主要路徑,再逐步加入錯誤狀態與低頻彈出層,才能讓門檻保持穩定且易於維護。
常見問題
偽本地化測試可以取代真實語言驗收嗎?
不可以。它能快速找出固定寬度、未本地化字串與雙向版面錯誤,但最後仍須使用至少一種長文字語言及一種 RTL 語言驗收真實文案。
如何避免偽本地化程式碼進入正式版本?
把轉換器限制在 DEBUG 編譯條件內,並讓 Release 建置在偵測到 PSEUDO_LOCALE 環境變數或測試專用資源時直接失敗。
哪些版面問題應該阻擋合併?
核心操作不可見、按鈕無法點擊、文字截斷至失去意義、RTL 順序錯誤及輔助使用標籤缺失都應阻擋合併。
將下一次建置放到雲端 Mac mini
選擇配置、計費週期與節點區域,使用獨享實體機執行開發、建置與自動化工作。