iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Vibe Coding

神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App 系列 第 27 篇

[Day 27] 加入英文語系:String Catalog 補 290 個字串、日期與時間跟隨語言,以及用測試擋住漏翻

  • 分享至 

  • xImage
  •  

Day 27

文章同步發表在我的個人 Blog

加入英文語系:String Catalog 補 290 個字串、日期與時間跟隨語言,以及用測試擋住漏翻

App 加上英文,只要把字串翻一遍就好嗎?

昨天 Day 26 處理簽章到期的重新安裝。今天把 App 的語系從繁體中文一種,加到繁體中文和英文兩種。

先講結果:手機語言是英文,App 就顯示英文;是繁體中文,或其他任何語言,就顯示繁體中文。沒有 App 內的切換選項,也沒有新增任何套件。290 個字串都有英文和說明,由測試檢查;日期、時間、通知文案、Firebase 登入的語言,全部讀同一個地方決定。./scripts/verify.sh 最後一行是 VERIFY: PASS。

1. 字串都在一個檔案裡:String Catalog

String Catalog 是 Xcode 的字串翻譯檔,副檔名 .xcstrings,內容是 JSON。每個字串有一個 key,底下依語言放翻譯。這個專案有兩份:App 本體 288 個 key,分享功能的擴充 2 個 key,合計 290 個。

SwiftUI 的 Text("home.title") 會用 key 去查目前語言的翻譯,所以畫面程式碼不需要改。要補的是檔案裡每個 key 的英文,以及一段 comment。comment 是給翻譯的人看的說明,例如:

"home.header.date %@ %@": {
  "comment": "Date under the home title; %1 is month-day, %2 is weekday",
  "localizations": {
    "zh-Hant": { "stringUnit": { "value": "%1$@ %2$@" } },
    "en":      { "stringUnit": { "value": "%2$@, %1$@" } }
  }
}

這個 key 有兩個變數:月日和星期。中文是「10 月 11 日 週日」,英文要寫成「Sun, October 11」,順序相反。%1$@、%2$@ 指定第幾個變數,兩個語言就可以各自排順序。如果把順序寫在 Swift 程式碼裡,每加一個語言就要多一段 if;寫在 catalog 裡,新增語言只動 catalog。

所以組句子的規則是:Swift 只傳變數,句型留在 catalog。

同一個畫面,左邊是繁體中文,右邊是英文:

首頁,左:繁體中文,右:英文

2. 漏翻要讓測試擋住,不靠眼睛找

290 個字串用看的對不完。我先寫了 LocalizationCoverageTests,直接讀兩份 catalog 的 JSON,檢查四件事:

  1. 每個 key 都有 en。
  2. 每個 key 都有 comment。
  3. 中英文的變數型別要一樣,%@ 對 %@、%lld 對 %lld,位置編號不算。
  4. 英文值裡不能出現中文字。

測試一開始是紅的:290 個 key 都缺英文和 comment,列出 580 項問題,每一項都帶 catalog 路徑和 key 名稱,補完再跑到綠。

空測試也會顯示通過,所以補完之後我又故意弄壞三次:刪掉一個 en 值、把一個 %@ 改成 %lld、刪掉一個 comment。三次測試都指出正確的 key,然後把檔案還原。

另外有一個保護:key 只要有整數變數(數量),就必須有英文的單複數寫法(1 item、2 items)。目前沒有任何 key 帶數量,這個測試現在不會有任何東西可以檢查,是替之後加的字串先擋著。

3. 日期和時間不能只靠翻譯

字串翻完,畫面上還有一批不在 catalog 裡的文字:日期、時間、通知裡「明天 9:30」這類文案。這些是 Swift 程式碼用 DateFormatter 之類組出來的,語言要另外處理。

做法是建立一個 AppLocale,所有格式化的地方都讀它:

nonisolated enum AppLocale {
    static var current: Locale { resolve(preferred: Bundle.main.preferredLocalizations) }

    static func resolve(preferred: [String], twentyFourHour: Bool = false) -> Locale {
        let suffix = twentyFourHour ? "@hours=h23" : ""
        if let first = preferred.first, first == "en" || first.hasPrefix("en-") {
            return Locale(identifier: "en_US" + suffix)
        }
        return Locale(identifier: "zh-Hant-TW" + suffix)
    }
}

Bundle.main.preferredLocalizations 是 iOS 依手機語言設定,從 App 實際有的語系裡選出的結果,和 String Catalog 選字串用的是同一個來源。resolve 拿陣列當參數,是為了測試可以直接丟 ["en"]、["ja"] 進去,不用去改模擬器語言。

三個細節:

  • 只有兩個結果。英文系的語言碼都對到 en_US,其他一律 zh-Hant-TW,所以日文手機看到的是繁體中文。
  • 時間用 24 小時制。@hours=h23 是 locale 的 24 小時制選項。繁體中文預設會顯示「上午9:30」,英文會是 9:30 AM,時間軸上的欄寬和對齊都不一樣。統一用 24 小時制,兩個語言都顯示 09:30。
  • 格式函式多一個 locale 參數,預設值是 AppLocale.current。畫面程式碼不用改,測試可以傳指定的 locale 進去比對中英文的輸出。

這一輪改到的格式程式碼有首頁、行事曆、通知文案、邀請碼到期時間、分享時間,共五處。LocalizedFormatTests 有 6 個測試,對每一處比對英文輸出,並確認中文輸出和改動前一樣。

4. 既有的測試要先固定語言

英文版加進來之後,有一個風險:UI 測試的斷言都是中文,例如 app.buttons["知道了"]。模擬器語言只要被改成英文,整批都會失敗。

最直接的做法是改每個啟動 App 的地方,加上 -AppleLanguages (zh-Hant)。UI 測試裡有 25 處 XCUIApplication(),沒有共用的啟動函式,要改 25 處。

我改在 project.yml,也就是 XcodeGen 的設定檔,測試的 scheme 設定裡一次指定語言和地區:

test:
  config: Debug
  language: zh-Hant
  region: TW

xcodegen generate 之後,Xcode 的 scheme 就帶了這兩項,所有測試預設都是繁體中文。英文測試(EnglishLocalizationUITests)則在自己的啟動參數寫 -AppleLanguages (en) -AppleLocale en_US 覆寫。

這裡有一個要注意的地方:新增測試檔案之後,要重新執行 xcodegen generate。不重新產生,新檔案不在專案裡,測試會顯示執行 0 個、也不報錯。

5. 英文比較長:ax3 字級下的檢查

英文通常比中文長。App 要支援 Dynamic Type,也就是使用者調大系統字級時,文字會跟著放大。系統字級最大一級是 ax5,這次檢查的是 ax3(docs/DESIGN.md 規定的檢查級別)。

EnglishLocalizationUITests 有 5 個測試:

  • 首頁、行事曆、設定、起始頁四個畫面,英文模式下沒有任何中文字。
  • 四個畫面用 UICTContentSizeCategoryAccessibilityXL(ax3)啟動,每一個 staticText 的範圍都在視窗之內。

四個畫面的英文版在 light 和 ax3 各截了一張,放在 docs/screenshots/L10N-en-*.png,看過沒有被截斷的字,長句都換行。

設定頁在 ax3 字級的英文版,帳號說明那一段從三行變成七行,沒有截斷:

設定頁,英文,ax3 字級

6. Firebase 登入的語言

Firebase Auth 有一個 languageCode,控制它寄出的驗證信、它自己產生的登入頁用什麼語言。我在取得 Auth 實例的地方,每次都把它設成 AppLocale.firebaseLanguageCode(en 或 zh-TW)。

這只管 Firebase 這一側。Day 25 綁定 Google 時跳出的授權畫面,是 Google 自己的網頁,語言由手機或瀏覽器的設定決定,不是 Auth.languageCode 控制的。實際會顯示哪一種語言,要在實機上切換手機語言驗過才算數。

7. 目前的狀態

項目 結果
字串 290 個 key(App 288、分享擴充 2),都有英文和 comment
漏翻檢查 LocalizationCoverageTests,缺英文、缺 comment、變數不一致、英文含中文字都會失敗
日期與時間 五處格式讀 AppLocale,時間 24 小時制
既有測試 在 project.yml 固定繁體中文
英文 UI 測試 5 個通過,含 ax3 版面檢查
verify.sh VERIFY: PASS(TeammateKit 59 個測試、安全規則 99 個測試、App 的 xcodebuild 測試全過)
Google 授權畫面的語言 還沒驗證,要實機
手機桌面的 App 名稱 沒有處理,維持中文
英文翻譯的人工審閱 還沒有,是 Claude 翻的

明天預告

Day 28 收集回饋並修一個問題:把拿到的回饋分類、決定先改哪一個、實際修掉一個並寫出驗證方式。

參考資源

官方文件


上一篇
[Day 26] 雙十國慶日,簽章到期之後重新安裝:覆蓋安裝為什麼沒續期、Xcode 帳號掉了,以及 Google 授權畫面上的 OAuth
系列文
神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App 共 27 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言