Localization 多國語系設定

本機系統與 App 內部切換語系

Photo by Hannah Wright on Unsplash

身為一個菜鳥工程師,將辛苦製作的 App 推向國際,多國語言是肯定要的。今天來分享一下如何針對系統語言切換及 App 內部切換語言兩種方式,Let’s Go ~

專案設定

打開專案內的 Project -> Info -> Localizations 新增你想要增加的語言

增加完後會問你要添加的地方,例如 Main.storyboard 打勾的話,就會幫你把 storyboard 中有文字的地方生成一個 main.strings 檔案,這個檔案會在你添加的語言資料夾內,所以每加一個語言,就會有一個資料夾

en 語言的資料夾

storyboard 多國語文檔

上面的左圖可以看到,"2Gf-sf-0bs.text" = "Password"; 2Gf-sf-0bs這串文字其實是該 label 的 objectID,也是讓程式尋找對應的文字的 Key ,所以如果不知道這邊的文字代表畫面上哪個物件的時候,可以用 objectID 去確認是哪個物件

這時候就會想到,阿如果我有些文字是用程式產生的呢?

程式文字多國語文檔

新增 Strings File,取名為 Localizable,接著點選檔案右側的 Localization 將需要的語言打勾,就會幫你產出好幾個檔案拉

這邊的 Key 命名就看個人習慣,自己是將不同類型的文字前面加上label, btn, alert … 等作區分

App Name 多國語文檔

最後別忘了幫 App 的大名跟著一起切換,這時候又要新增一個 Strings File,然後 key 為 "CFBundleDisplayName" 記得一定要取這個不然系統會抓不到

ViewController 內置換文字

// Localizable.string 內的格式
"alert_logInSuccess" = "LogIn Success";
"label_amountOfDiamondsText" = "%d 顆鑽石"; // %d 的地方可以傳入參數

// controller 讀取
let logInSuccessText = NSLocalizedString("PurchaseVC.Alert.logInSuccess", comment: "logIn Success")

let amountOfDiamondsText = NSLocalizedString("PurchaseVC.Label.amountOfDiamondsText", comment: "amountOfDiamondsText")
label.text = String.localizedStringWithFormat(amountOfDiamondsText, amountOfDiamonds) // print 1,000 顆鑽石

單純讀取文字的時候透過 NSLocalizedString("文字對應的KEY", comment: "這邊可以為空,也可以寫這個文字的意思")

遇到在數值前後的文字,可以在要轉換的文字中加上 %d,透過NSLocalizedString 取得文字,再透過 String.localizedStringWithFormat(文字, 數值) 就能完美讀取有數值的文字拉~

遇到 Enum 讀取字串

enum ProductID: String, CaseIterable {

case diamonds100 = "Item01"
case diamonds200 = "Item02"
case diamonds500 = "Item03"
case diamonds1000 = "Item04"
case diamonds1500 = "Item05"
case aiChatfor1Week = "Item06"
case xmasGift = "Item07"

var productName: String {
switch self {
case .diamonds100:
return NSLocalizedString("diamonds100", comment: "diamonds100")
case .diamonds200:
return NSLocalizedString("diamonds200", comment: "diamonds200")
case .diamonds500:
return NSLocalizedString("diamonds500", comment: "diamonds500")
case .diamonds1000:
return NSLocalizedString("diamonds1000", comment: "diamonds1000")
case .diamonds1500:
return NSLocalizedString("diamonds1500", comment: "diamonds1500")
case .aiChatfor1Week:
return NSLocalizedString("aiChatfor1Week", comment: "aiChatfor1Week")
case .xmasGift:
return NSLocalizedString("xmasGift", comment: "xmasGift")
}
}
}

// Localizable.string 內
"diamonds100" = "100 顆鑽石";
"diamonds200" = "200 顆鑽石";
"diamonds500" = "500 顆鑽石";
"diamonds1000" = "1000 顆鑽石";
"diamonds1500" = "1500 顆鑽石";
"aiChatfor1Week" = "AI 聊天/ 週";
"xmasGift" = "聖誕節驚喜包";

簡單來說 Main.strings 跟 InfoPlist.strings 裡面的文字們,都不用我們特別到 ViewController 內再多做設定,例如要指派給誰顯示這種事情。

再來是有實作過後才會有感的部分,Main.strings 的小缺點:
如果有新增的頁面或新增的 label or btn,自己手動加 objectID 進去 Main.string 檔案還是會吃不到,我自己的方式就是最後再建立多國語言這個步驟,或是將要新增的文字直接加去 Localizable.strings 內,再拉 label or btn 的 IBOutlet 進 ViewController,將文字指派給他這樣

App 內部切換語言

當初在實作的時候想說,系統切換語言不難那內部應該也不會太複雜吧!結果也是弄了兩天左右,最後有達到自己想要的流程很開心!

首先創建一個 Manager 控管內部切換語言,這邊我命名為 LocalizationManager ,他是一個 Singleton ,跟著 App 一起存亡

前面有提到,每新增一個語言就會多一個語言資料夾
當使用者切換語言的時候,我們要將讀取的資料夾路徑做變換,讓系統去讀取切換的語言資料夾,達到切換語言的功能

資料夾結尾都是.lproj

我最終想要達成從外部呼叫 LocalizationManager.shared.language = .english 簡單明瞭的方式

建立 enum Language ,把所有會切換的語言列入

enum Language: String {
case english = "en"
case chineseT = "zh-Hant"
case chineseS = "zh-Hans"
case system

var segmentNum: Int {
switch self {
case .english:
return 0
case .chineseT:
return 1
case .chineseS:
return 2
case .system:
return 3
}
}
}

在 manager 內建立兩個變數,一個是讀取使用者選取的語言,一個是程式要吃的路徑語言,為何要設立兩個是因為當使用者選系統語言的時候我想要記住他是選系統,但程式要吃的路徑必須要辨別是哪個語言給予不同的路徑,沒有系統這個路徑

// 當系統語言開頭為 "ZH" 之外的語言都給英語的路徑
var languageForBundle: Language = {
if let systemLanguage = Locale.preferredLanguages.first {
if systemLanguage.hasPrefix("zh") {
if systemLanguage.contains("Hant") || systemLanguage.contains("Trad") {
return .chineseT
}
return .chineseS
}
}
return .english
}()

// 當 language 有變動時儲存至 UserDefaults ,當有已存的語言就讀取,沒有則判定為系統語言
var language: Language = {
// read saved language
let languageString = UserDefaults.standard.string(forKey: UDKey.language.rawValue)
if let language = Language(rawValue: languageString ?? "") {
return language
}
// no saved so read system
return .system
}(){
didSet {
UserDefaults.standard.setValue(language.rawValue, forKey: UDKey.language.rawValue)
}
}

接著就是要怎麼讓 ViewController 讀取?

func strWithKey(key: String) -> String? {
var resource: String

if self.language == .system {
resource = self.languageForBundle.rawValue
} else {
resource = self.language.rawValue
}

guard let path = Bundle.main.path(forResource: resource, ofType: "lproj"),
let bundle = Bundle(path: path) else { return nil }

let str = bundle.localizedString(forKey: key, value: "", table: nil)
return str
}
  1. 首先建立一個 function (之後可以直接呼叫來賦予物件文字)
  2. 透過 language 獲得的值去取得語言名稱 ex: zh-Hant,丟進 Bundle.main.path(forResource: resource, ofType: “lproj”) 獲得路徑
  3. bundle.localizedString(forKey: key, value: “”, table: nil) 接收 .strings 檔案中的 Key 和語言資料夾路徑,取得要轉換的文字
// ViewController
@IBAction func changeLanguage(_ sender: UISegmentedControl) {

switch sender.selectedSegmentIndex {
// en, ch-tra, ch-sim, system
case 0:
LocalizationManager.shared.language = .english
case 1:
LocalizationManager.shared.language = .chineseT
case 2:
LocalizationManager.shared.language = .chineseS
case 3:
LocalizationManager.shared.language = .system
default:
break
}

reloadText()
}

private func reloadText() {
greetingLabel.text = (LocalizationManager.shared.strWithKey(key: LocalizedKey.label_greeting.str) ?? "") + (name ?? "")
buyDiamondsLabel.text = LocalizationManager.shared.strWithKey(key: LocalizedKey.label_buyDiamond.str)
changeLanLabel.text = LocalizationManager.shared.strWithKey(key: LocalizedKey.label_language.str)

restoreBtn.setTitle(LocalizationManager.shared.strWithKey(key: LocalizedKey.btn_restore.str), for: .normal)
hasLogedInBtnState(LogInDataManager.shared.hasAccountNPasswd())

lanSegment.setTitle(LocalizationManager.shared.strWithKey(key: LocalizedKey.label_system.str), forSegmentAt: 3)

tableView.reloadData()
}

ViewController內切換後,如果你不會重新經過生命週期的話,記得要建立一個 reload 的功能,讓文字刷新

另外因為 Key 有夠多,就把他們變成 enum 比較不容易出現打錯字的問題

enum LocalizedKey: String {

case label_greeting, label_amountOfDiamondsText, btn_logIn, btn_logOut, alert_noRestore, alert_restoreSuccess, alert_cantBuy, alert_productName, alert_pleaseLogIn, alert_accountNPass

case label_psw, label_account, label_language, label_thrLogIn, label_welcome, label_server, label_buyDiamond, btn_restore, label_system

var str: String {
self.rawValue
}
}

附上完整的 LocalizationManager

大功告成拉!!!感謝大家看到這邊,希望多少有幫助到想實作這個功能的捧友~

最近發現,有時候不是功能多難花很多時間,而是想要流程或程式更乾淨簡潔思考很久,完成後很有成就感,覺得自己又更聰明了(遠目

--

--