
文章同步發表在我的個人 Blog
App 的語言是啟動時就決定好的,使用者在 App 裡選了語言,要怎麼讓它換?
昨天 Day 27 加入英文語系,語言完全跟隨手機系統。今天處理拿到的回饋,挑一個修,重點放在這個問題背後的技術:語言是什麼時候決定的、選擇存在哪裡、怎麼測。
先講結果:今天收到兩則回饋,先改缺少的功能:設定頁加上語言選項,選擇之後下次啟動生效。不做立即生效,也沒有新增套件。新增的單元測試 6 個、UI 測試 3 個都通過,在模擬器用設定頁選語言、關掉 App 再開,畫面確實換了語言。
第一則:重新安裝 Day 27 的版本之後,我在設定頁找不到可以切換語言的地方。
第二則:有一件事被處理了,我希望對方不用打開 App,就會收到通知。
| 回饋 | 類型 | 要花什麼 | 不做會怎樣 |
|---|---|---|---|
| 設定頁找不到語言選項 | 缺少的功能 | 一個設定列、一組測試,不需要付費帳號 | 要換語言的人要自己去 iPhone 系統設定找 |
| 對方不開 App 也收到通知 | 做不到的需求 | 遠端推播需要付費的 Apple 開發者帳號 | 對方打開 App 才看得到,未確認的卡片仍然排在首頁最上面 |
第二則的限制不在程式。專案用的是免費 Apple 帳號,Push Notifications 這項 capability 需要付費帳號,AGENTS.md 也明寫不使用遠端推播。能做的是本機通知:App 在背景被 iOS 喚醒時自己排一則,但什麼時候喚醒由 iOS 決定,做不到「立刻」。所以先改第一則,第二則留在 docs/QUESTIONS.md。
Day 27 的 AppLocale 讀的是 Bundle.main.preferredLocalizations。它是 iOS 在 App 啟動時,依 AppleLanguages 和 bundle 實際有的語系算出來的,啟動之後不會再變。
要讓 App 內的選擇生效,有兩種做法:
| 做法 | 立即生效 | 下次啟動生效 |
|---|---|---|
| 作法 | 逐處把 bundle 與 locale 傳進字串呼叫,根畫面重建 | 把選擇寫進 AppleLanguages,下次啟動由 iOS 選語系 |
| 風險 | 專案有 69 處 String(localized:) 之類的呼叫,漏改一處就中英混雜 |
使用者要自己關掉 App 再開 |
AppLocale |
要另外維護一套選語系的程式 | 不需要改 |
選下次啟動生效。AppLocale 讀的來源本來就會反映 AppleLanguages,所以日期、時間、通知文案、Firebase 登入的語言全部跟著換,不需要逐一處理。
AppleLanguages 與兩個 domain設定頁新增一列「語言」,三個選項對應的值是:
| 設定 | 寫入的 AppleLanguages |
|---|---|
| 跟隨系統 | 移除這個 key |
| 繁體中文 | ["zh-Hant"] |
| English | ["en"] |
寫在 App 自己的 UserDefaults,只影響這個 App。選了和啟動時不同的值,設定列下方出現一句「重新開啟 App 後生效」。
UserDefaults 讀值時會依序查幾個 domain,其中 argument domain(啟動參數)的優先順序比 App 儲存的值高。測試的 scheme 用 -AppleLanguages (zh-Hant) 固定語言,所以直接用 UserDefaults.standard.array(forKey:) 讀,會讀到被固定的語言,看不到使用者實際選了什麼。「目前選了哪一個」要改從 App 自己的 persistent domain 讀:
defaults.persistentDomain(forName: bundleIdentifier)?["AppleLanguages"]
這樣測試固定語言時,設定列仍然顯示使用者的選擇。
選項與 AppleLanguages 之間的轉換放在 LanguagePreference,是不依賴 UI 和模擬器的純函式。測試先寫,涵蓋:
nil、空陣列:還原成跟隨系統。["en"]、["en-US"]:還原成 English。["zh-Hant"]、["zh-Hant-TW"]:還原成繁體中文。["ja"]:不認得的值,還原成跟隨系統,也不會被程式覆寫。UserDefaults suite 再讀出來,選跟隨系統會移除 key。讀寫的部分另外包成 LanguagePreferenceStore,注入 UserDefaults 和 bundle identifier,單元測試用獨立的 suite,不碰真實的 App domain。
Share Extension 是獨立的 process,有自己的 UserDefaults。App Groups 在免費帳號下不能當成必要條件,所以它讀不到這個選擇,仍然跟隨系統語言。這是已知的限制,不處理。
先確認做法可行:在模擬器直接寫入 App 的 AppleLanguages,不帶啟動參數重新啟動。寫 en 顯示英文,寫 zh-Hant 顯示繁體中文,刪掉 key 後回到系統語言。
測試有三組:
LanguagePreferenceTests 6 個:選項對應的值、各種已儲存值的還原(含 ["ja"])、存取往返、選跟隨系統會移除 key、啟動參數蓋過時仍讀到使用者的選擇。測試先寫,第一次執行是編譯失敗(找不到型別),實作之後 6 個通過。LanguageSettingUITests 3 個:設定頁有語言選項且預設跟隨系統;選了不同的語言出現提示、選回後消失;ax3 字級下,繁體中文與英文的語言區段文字都落在視窗之內。LocalizationCoverageTests 2 個:6 個新字串都有英文和 comment,英文值不含中文字。「繁體中文」這個選項因此在英文介面顯示 Traditional Chinese。設定頁的語言區段,先是繁體中文介面,再來是英文介面。選了不同的語言,下方出現提示:


UI 測試的 scheme 固定了語言,看不到「重開之後」的結果,所以另外在不帶任何啟動參數的情況下驗證:模擬器的系統語言是英文,用設定頁選「繁體中文」,關閉 App 再用 simctl launch 開啟,首頁是繁體中文。
| 項目 | 結果 |
|---|---|
| 回饋 | 兩則,一則缺少的功能、一則做不到的需求 |
| 先改的 | 設定頁語言選項,下次啟動生效 |
| 沒改的 | 對方不開 App 也收到通知,需要付費帳號 |
| Share Extension 的語言 | 跟隨系統 |
| 實機 | 兩支手機還沒裝這一版,Google 授權畫面的語言也還沒在實機驗證 |
Day 29 Vibe Coding 總檢討:整理 30 天的工具紀錄、AI 常寫錯什麼、兩個 Agent 的結論。
官方文件