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

Xcode 新建一个工程的默认语言是英文,所以你在 app 里粘贴复制都是显示的 copy paste,你可以通过在 info.plst 文件里选择 Localization native development region 来设置不同语言。

info.plst

可是如果你想在软件内选择设置语言,为软件添加多语言选择功能就需要一番折腾了,倒也简单,只不过还是有几个坑的。以下是一篇详细介绍为软件配置多语言选项的博客,走起 ➜ ➜ ➜

关于 NSBundle

在开始正式文章之前你或许应当先搞明白 NSBundle 是什么东西。

Bundle 是一个目录,其中包含了在程序会使用到的资源,包含了如图像、声音、程序中需要用到的文件,甚至是编译好的代码等等。而在实现软件内配置语言的时候就是通过 Bundle 的路径去获取配置文件,根据这个配置文件取出对应的字体渲染到 view 上。

当然,配置程序语言只是 Bundle 的一种用途。还可以用 Bundle 去获取工程中 info.plist 的详细信息,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取版本号:Bundle Short Version
NSString *shortVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
// 获取版本号:Bundle version
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
// 获取应用标识:Bundle identifier
NSString *bundleIdentifier = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"];
// 获取应用名称:Bundle display name
NSString *bundleDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
// 获取Bundle name
NSString *bundleName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
// 获取 app 包路径
NSString *path = [[NSBundle mainBundle] bundlePath];
// 获取 app 资源目录路径
NSString *resPath = [[NSBundle mainBundle] resourcePath];
...

大概明白 NSBundle 是怎么回事了吧,接下来就正式开始应用程序语言本地化及应用内语言设置。

配置 Project

添加语言

如下图,点击 PROJECT -> info -> Localizations 这里默认只有 English 点击下方的加号可以添加你想要的语言,比如这里添加的中文 Chinese(Simplifid) 。

注意: zh-Hans 是简体中文, zh-Hant 是繁体中文。

配置Project

新建 .strings 配置文件

1、Command + N 新建 Strings File 文件,命令为 RDLocalizable ,会生成一份 RDLocalizable.strings 文件。

2、选中RDLocalizable.strings 文件,如下图操作,点击 Localize... 按钮,左侧弹框中选择语言。

Localize...

3、之后右侧会如下图显示,勾选上你想要的语言即可(Base 无用)

Localization

4、当勾选两门语言后,会发现RDLocalizable.strings 文件可以展开并存在两个配置文件,一份英文,一份中文。
分别在两个文件内输入对应的语言,比如在英文文件里输入:

1
2
3
"收录" = "Collection";
"订阅" = "Subscription";
"我的" = "Mine";

中文文件里输入:

1
2
3
"收录" = "收录";
"订阅" = "订阅";
"我的" = "我的";

前边对应 键(key) ,后边对各个语言的 值(value)。看后面的 使用方法 就会明白了。

至此,对工程的配置已经完成。接下来要做的就是获取软件语言、设置语言、监听语言改变。。。

创建多语言设置工具类

因为该工具类比较简单,直接将代码贴出来吧,后面会介绍一些坑。因为是一个继承于 NSObject 的工具类,都是使用类方法实现功能,以便类名直接调用。

头文件.h

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
//
// RDLocalizableController.h
// rder
//
// Created by gonghonglou on 2016/10/29.
// Copyright © 2016年 gonghonglou. All rights reserved.
//

#import <Foundation/Foundation.h>

#define RDLanguageKey @"userLanguage"

#define RDCHINESE @"zh-Hans"

#define RDENGLISH @"en"

#define RDNotificationLanguageChanged @"rdLanguageChanged"

#define RDLocalizedString(key) [[RDLocalizableController bundle] localizedStringForKey:(key) value:@"" table:@"RDLocalizable"]

@interface RDLocalizableController : NSObject

/**
* 获取当前资源文件
*/
+ (NSBundle *)bundle;
/**
* 初始化语言文件
*/
+ (void)initUserLanguage;
/**
* 获取应用当前语言
*/
+ (NSString *)userLanguage;
/**
* 设置当前语言
*/
+ (void)setUserlanguage:(NSString *)language;

@end

实现文件.m

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
//
// RDLocalizableController.m
// rder
//
// Created by gonghonglou on 2016/10/29.
// Copyright © 2016年 gonghonglou. All rights reserved.
//

#import "RDLocalizableController.h"

static RDLocalizableController *currentLanguage;

@implementation RDLocalizableController

static NSBundle *bundle = nil;

// 获取当前资源文件
+ (NSBundle *)bundle{
return bundle;
}

// 初始化语言文件
+ (void)initUserLanguage{
NSString *languageString = [[NSUserDefaults standardUserDefaults] valueForKey:RDLanguageKey];
if(languageString.length == 0){
// 获取系统当前语言版本
NSArray *languagesArray = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"];
languageString = languagesArray.firstObject;
[[NSUserDefaults standardUserDefaults] setValue:languageString forKey:@"userLanguage"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
// 避免缓存会出现 zh-Hans-CN 及其他语言的的情况
if ([[RDLocalizableController chinese] containsObject:languageString]) {
languageString = [[RDLocalizableController chinese] firstObject]; // 中文
} else if ([[RDLocalizableController english] containsObject:languageString]) {
languageString = [[RDLocalizableController english] firstObject]; // 英文
} else {
languageString = [[RDLocalizableController chinese] firstObject]; // 其他默认为中文
}

// 获取文件路径
NSString *path = [[NSBundle mainBundle] pathForResource:languageString ofType:@"lproj"];
// 生成bundle
bundle = [NSBundle bundleWithPath:path];
}

// 英文类型数组
+ (NSArray *)english {
return @[@"en"];
}

// 中文类型数组
+ (NSArray *)chinese{
return @[@"zh-Hans", @"zh-Hant"];
}

// 获取应用当前语言
+ (NSString *)userLanguage {
NSString *languageString = [[NSUserDefaults standardUserDefaults] valueForKey:RDLanguageKey];
return languageString;
}

// 设置当前语言
+ (void)setUserlanguage:(NSString *)language {
if([[self userLanguage] isEqualToString:language]) return;
// 改变bundle的值
NSString *path = [[NSBundle mainBundle] pathForResource:language ofType:@"lproj"];
bundle = [NSBundle bundleWithPath:path];
// 持久化
[[NSUserDefaults standardUserDefaults] setValue:language forKey:RDLanguageKey];
[[NSUserDefaults standardUserDefaults] synchronize];

[[NSNotificationCenter defaultCenter] postNotificationName:RDNotificationLanguageChanged object:currentLanguage];
}

@end

使用方法:

1、在 AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法里初始化该工具类,并监听通知:

1
2
3
4
// 语言初始化
[RDLocalizableController initUserLanguage];
// 监控语言切换
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(languageChange:) name:RDNotificationLanguageChanged object:nil];

2、记得在 - (void)applicationWillTerminate:(UIApplication *)application方法里删除通知:

1
[[NSNotificationCenter defaultCenter] removeObserver:self name:RDNotificationLanguageChanged object:nil];

3、实现通知方法:

1
2
3
4
- (void)languageChange:(NSNotification *)note{
// 在该方法里实现重新初始化 rootViewController 的行为,并且所有带有文字的页面都要重新渲染
// 比如:[UIApplication sharedApplication].keyWindow.rootViewController = ...;
}

4、使用 RDLocalizedString(<#key#>)方法 给所有文字添加本地化语言方法:

1
2
3
label.text = RDLocalizedString(@"收录");
[button setTitle:RDLocalizedString(@"订阅") forState:UIControlStateNormal];
...

5、更改语言方法:

1
2
3
4
5
// 设置中文
[RDLocalizableController setUserlanguage:RDCHINESE];

// 设置英文
[RDLocalizableController setUserlanguage:RDENGLISH];

至此,对于应用程序语言本地化及应用内语言设置的功能就已经可以实现了。接下来是对遇到的几个坑的说明。

多语言设置的「坑」

关于更改语言后重新初始化页面

语言更改后,要重新渲染view,所以应该在更改语言之后回到根目录。不仅页面需要初始化,如果页面数据在 viewModel 里,那么该 viewModel 也应当初始化,因为字体是 RDLocalizedString(<#key#>) 这个方法从 .strings 配置文件里取出来的,更改语言后必须重新取一次。

当然也不是一定要留在根目录,有几种页面友好的解决方案:

1、更改语言功能一般会放在「我的」页面 push 出来的某一级页面,可以初始化 rootViewController 并且将之前 push 出来的几级 viewController 手动添加到 mineViewController.navigationController.viewControllers 这个数组中。这样页面就不会产生太大的错落感。

2、在每一个页面写一个检测语言改变的通知的方法。当接受到通知后就将该页面重新布局一次以更改字体。

PS:在这个问题上,感觉支付宝比微信做的界面跳转友好的多。。。

关于本地化语言的宏定义 RDLocalizedString(<#key#>)

系统自带的方法是:NSLocalizedString(<#key#>, <#comment#>),这也是一份宏定义:

1
2
#define NSLocalizedString(key, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil]

能看到它调用的是 NSBundle.mainBundle ,而我们在更改语言的工具类里的 bundle 已经更改了。
所以系统的 NSLocalizedString(<#key#>, <#comment#>) 已经失效,必须重写一份宏定义:

1
#define RDLocalizedString(key)  [[RDLocalizableController bundle] localizedStringForKey:(key) value:@"" table:@"RDLocalizable"]

1、必须使用自己的类名来调用类方法 [RDLocalizableController bundle] 以获取自己的 bundle

2、table 后的参数为 .strings 文件的文件名,若你创建的文件名为 Localizable.strings ,则该参数可为 nil ,系统默认按 Localizable.strings 查找。否则必须配置文件名,且只是文件名,不加 .stringd 后缀。

关于初始化语言 [RDLocalizableController initUserLanguage]

initUserLanguage 方法中有这样一段代码来做判断

1
2
3
4
5
6
7
if ([[RDLocalizableController chinese] containsObject:languageString]) {
languageString = [[RDLocalizableController chinese] firstObject]; // 中文
} else if ([[RDLocalizableController english] containsObject:languageString]) {
languageString = [[RDLocalizableController english] firstObject]; // 英文
} else {
languageString = [[RDLocalizableController chinese] firstObject]; // 其他默认为中文
}

各位可能会对这个判断比较疑惑,在这之前已经有判断了:先获取用户设置的语言,有则使用用户设置的语言,没有则使用系统语言。

然而因为某些原因用户设置过的语言(如:zh-Hans)会在另一个相同工程运行之后将该语言更改为zh-Hans-CZ;或者用户将系统语言设置为日本语或其他语言。

出现以上情况时 RDLocalizedString(<#key#>) 这个方法从 .strings 配置文件里是去不到对应的字体,就会返回空。
后果轻则页面一片空白了,重则直接 crash ,如:

1
NSArray *array = @[RDLocalizedString(@"收录"), RDLocalizedString(@"订阅"), RDLocalizedString(@"我的")]; // 数组不能存空

就想使用 NSLocalizedString(<#key#>, <#comment#>) 方法

1、有一种极端情况,比如:软件需要配置多国语言,很多很多的那一种。。。在 .strings 文件里配置了许多国家的语言。然而在软件内部只提供中文、英文等某几种语言,其他语言根据系统语言自适应。不想在 initUserLanguage 方法里做一大堆的乱七八糟的判断。只要在 initUserLanguage 的判断方法 else 里使用系统语言:

1
2
3
} else {
languageString = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"][0]; // 其他默认为系统语言
}

2、另一种情况,比如:每次使用 RDLocalizedString(<#key#>) 方法都要做引用 #import "RDLocalizableController.h" 好麻烦。
当然你可以把 #import "RDLocalizableController.h" 放到 .pch 文件里,哦,顺便提一下 .pch 文件会拖慢启动时间

3、还有一种情况,比如:就想使用 NSLocalizedString(<#key#>, <#comment#>) 方法,还可以解决以上两种情况

还是有方法使用 NSLocalizedString(<#key#>, <#comment#>) 的。
使用 CategoryNSBundle 类扩展一个设置语言的方法,并且使用 runtimeNSBundle 动态添加一个关于 bundle 的属性,重载 NSBundle.mainBundlelocalizedStringForKey 方法。目的就是将更改的字体传给 NSLocalizedString(<#key#>, <#comment#>) 映射的 localizedStringForKey 方法返回的 bundle ,使得更改的字体应用到系统上。

好吧,show you the code:

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
#import "NSBundle+RDLanguage.h"
#import <objc/runtime.h>

static const NSString *RDBundleKey = @"RDLanguageKey";

@interface BundleEx : NSBundle

@end

@implementation BundleEx

- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName {
NSBundle *bundle = objc_getAssociatedObject(self, &RDBundleKey);
if (bundle) {
return [bundle localizedStringForKey:key value:value table:tableName];
} else {
return [super localizedStringForKey:key value:value table:tableName];
}
}
@end


@implementation NSBundle (RDLanguage)

+ (void)setLanguage:(NSString *)language {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
object_setClass([NSBundle mainBundle], [BundleEx class]);
});
id value = language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:@"lproj"]] : nil;
objc_setAssociatedObject([NSBundle mainBundle], &RDBundleKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

以上代码是 NSBundleCategory
解释一下哈:
1、objc_getAssociatedObjectobjc_setAssociatedObject 是一对 getter、setter 方法,目的是为了给 NSBundle 类动态添加一个属性。
2、object_setClass:在 BundleEx 里实现一个 localizedStringForKey 方法,然后将 BundleEx 这个类设置给 [NSBundle mainBundle] 。目的就是相当于重载 [NSBundle mainBundle]localizedStringForKey 方法。

说明:
runtime 的具体用法和原理,由于在下才疏学浅就不多做讲解了,免得误人子弟。关于更多 runtime 的知识可以学习 一缕殇流化隐半边冰霜 写的 神经病院Objective-C Runtime入院 系列文章。

再说本篇文章,该类别新增方法的使用:
RDLocalizableController 类的 + (void)setUserlanguage:(NSString *)language 方法里,本地化存储语言之后,发送通知之前调用如下方法:

1
[NSBundle setLanguage:language];

之后,关于 RDLocalizableController 类里边关于 bundle 的操作就可以舍弃了。

注意:使用这种方法要确保你的 .strings 的文件名为 Localizable.strings
否则还是要重新设置宏定义:

1
2
#define NSLocalizedString(key, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:@“RDLocalizable”]

这样的话该宏定义会有一个警告,毕竟系统已经定义过了的,而且你还要到处重定义。。。又犯了上面第二种情况的尴尬。

到这里,该篇博客就结尾了,希望能帮助到各位一二
祝大家生活愉快,勤勉Coding

后记