iOS 应用内切换语言

上篇文章 讲了如何对 iOS APP 进行本地化,但是本地化是随着系统语言更改才变化的,而现在很多 APP 包括微信、支付宝等,他们设置里面就可以直接切换 APP 的语言,不跟随系统语言,苹果官方并没有提供对应的解决方案,这些 App 是如何做到的呢?本文就讨论一下如何在应用内切换本地化语言

Bundle 和 NSLocalizedString

Bundle

先来了解一下 BundleBundle 其实是一个目录,里面包含了程序会使用到的资源,这些资源包含了如图像、声音、编译好的代码、nib文件等等,我们编译好的一个 APP 包其实就是一个 Bundle目录,这个目录就是 mainBundle,而在我们要实现应用内切换语言其实就是通过 mainBundle 的路径去获取语言配置文件,根据这个语言配置文件重新加载语言并刷新 UI,苹果提供了一个 NSBundle (Swift 里直接就叫 Bundle)类型帮助我们处理 Bundle

Bundle 的用途很多,配置APP语言只是其中一种用途,比如

1
2
3
4
5
6
7
8
9
10
// NSBundle 获取nib 文件
let customView = Bundle.main.loadNibNamed("CustomView", owner: nil, options: nil)?.last as! CustomView
// 获取 info.plist 的信息
// 版本号
let version = Bundle.main.infoDictionary?["CFBundleVersion"]
// 应用标识
let bundleIdentifier = Bundle.main.infoDictionary?["CFBundleIdentifier"]
//应用名称
let bundleDisplayName = Bundle.main.infoDictionary?["CFBundleDisplayName"]

NSLocalizedString

NSLocalizedString 在上篇文章使用到了, 在 OC 中它是系统定义的一个宏

1
2
3
4
5
6
7
8
#define NSLocalizedString(key, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil]
#define NSLocalizedStringFromTable(key, tbl, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
[bundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \
[bundle localizedStringForKey:(key) value:(val) table:(tbl)]

可以看到,内部实际上就是去 mainBundle 加载本地化的语言文件,这个方法从指定的 bundle 里的 table 中返回 key 所对应的值,Swift 中没有宏这个东西了,所以需要直接使用这个方法

1
2
3
// Returns a localized version of the string designated by the specified key and residing in the specified table.
// tableName 如果传nil,就会默认加载 Localizable.strings
- (NSString *)localizedStringForKey:(NSString *)key value:(nullable NSString *)value table:(nullable NSString *)tableName

所以如果我们在应用内切换语言之后,重新加载需要的语言文件,并返回对应的本地化字符串用来显示,那么就可以做到在 APP 随时切换语言了

应用内切换语言

了解了上面的内容之后,我们就可以设计如何在应用内切换语言了

APP 启动时会根据当前系统的语言去加载对应的本地化文件,我们使用 UserDefaults 把这个默认的语言作为用户语言储存下来,获取当前系统默认语言有两种方法,这两种方式都可以取到系统当前的语言代码

1
2
3
4
5
6
7
8
9
10
// 方法一,preferredLanguages 可以获取系统首选语言列表数组
let userLanguage = NSLocale.preferredLanguages.first
// 方法二,从 UserDefaults 获取系统首选语言列表数组
let userLanguages = UserDefaults.standard.object(forKey: "AppleLanguages") as! [String]
let userLanguage = userLanguages.first
//储存用户语言
UserDefaults.standard.set(userLanguage, forKey: "UserLanguage")
UserDefaults.standard.synchronize()

接下来,当用户切换语言之后,就把用户语言更新储存到 UserDefaults 中,并且根据用户切换的语言,我们使用 Bundle 去获取不同本地化语言文件的路径,然后使用 NSLocalizedString(key, tableName, bundle, value: , comment) 去获取 key 对应的本地化字符串

1
2
3
4
5
let bundlePath = Bundle.main.path(forResource: userLanguage, ofType: "lproj")
let bundle = Bundle(path: bundlePath!)
let localizedStr = NSLocalizedString("通讯录", tableName: nil, bundle: bundle!, value: "", comment: "")

上面的代码工程很多地方会用到,用户也可能会多次切换语言,可以新建一个本地化处理工具类,专门处理用户切换语言之后的本地化修改,下面是工具类的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import UIKit
func LocalizedString(_ key:String) -> String {
return LocalizationManger.shareManger.localizedString(key)
}
class LocalizationManger: NSObject {
var bundle:Bundle?
static let shareManger:LocalizationManger = {
let instance = LocalizationManger()
return instance
}()
override init() {
super.init()
initLangage()
}
func setUserLanguage(language:String) {
UserDefaults.standard.setValue(language, forKey: "UserLanguage")
UserDefaults.standard.synchronize()
let bundlePath = Bundle.main.path(forResource: language, ofType: "lproj")
bundle = Bundle.init(path: bundlePath!)
}
fileprivate func localizedString(_ key: String) -> String {
return NSLocalizedString(key, tableName: "", bundle: bundle!, value: "", comment: "")
}
func initLangage() {
if let userLanguage = UserDefaults.standard.value(forKey: "UserLanguage") as? String {
let bundlePath = Bundle.main.path(forResource: userLanguage, ofType: "lproj")
bundle = Bundle.init(path: bundlePath!)
}else {
var language = NSLocale.preferredLanguages.first
UserDefaults.standard.setValue(language, forKey: "UserLanguage")
if (language == "zh-Hans-HK") || (language == "zh-Hans-CN") || (language == "zh-Hans") || (language == "zh-Hans-US") {
language = "zh-Hans"
}else if language == "zh-HK" || language == "zh-Hant-CN" || language == "zh-Hant" {
language = "zh-Hant"
}else {
language = "en"
}
UserDefaults.standard.setValue(language, forKey: "UserLanguage")
UserDefaults.standard.synchronize()
let bundlePath = Bundle.main.path(forResource: language, ofType: "lproj")
bundle = Bundle.init(path: bundlePath!)
}
}
}

在需要本地化字符串的地方,使用如下方式赋值:

1
2
3
clickBtn.setTitle(LocalizedString("点一下,不会怀孕"), for: .normal)
label.text = LocalizedString("点一下,不会怀孕")

其他注意

切换语言之后,由于需要重新加载新的本地化文件并且渲染 View,有两种做法,

  • 第一种,在每个控制器的 viewWillAppear 中重新设置本地化文字或者通过发送通知的方式,通知控制器更新本地化文字,这种方式可以让用户流畅的切换语言,支付宝的切换语言方式就和这种方式类似

  • 第二种,类似微信的做法,当用户切换语言之后,实际上是重新设置了 AppDelegaterootViewController,这样也能做到所有的界面重新加载本地化文件

把上面的本地化工具类修改一下,增加了更换 rootViewController的方法和切换语言发送通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func setUserLanguage(language:String) {
UserDefaults.standard.setValue(language, forKey: "UserLanguage")
UserDefaults.standard.synchronize()
let bundlePath = Bundle.main.path(forResource: language, ofType: "lproj")
bundle = Bundle.init(path: bundlePath!)
//切换语言,发送通知
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ChangedUserLanguageNotification"), object: language)
}
// 重新设置新的 rootViewController
class func reloadRootViewControlle() {
let delegate = UIApplication.shared.delegate!
let tabbarVc = UITabBarController()
delegate.window??.rootViewController = tabbarVc
}

以上就是应用内切换应用的基本思路,不同类型的应用对本地化的要求可能不一样,可以视情况调整,另外,Xib 的本地化要实现应用内的切换语言,需要使用拖线的方式,在代码里赋值

参考文章:

iOS应用程序语言本地化及应用内语言设置

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!