因为个人在开发英语相关的App,所以为了以后更好的用户体验,准备加入语言切换功能,以及App国际化。实现应用内切换语言后无需退出应用即可生效,新安装App时根据用户的手机语言显示对应的App名字以及App内的语言。现在就这一需求进行实现。

关于 NSBundle

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

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

// 获取版本号: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];

App名称国际化

配置 Project

点击 PROJECT -> info -> Localizations 这里默认只有 English 点击下方的加号可以添加你想要的语言。其中,zh-Hans 是简体中文, zh-Hant 是繁体中文。

如图所示:

20170413149206183440037.png

新建 .strings 文件

在与 info.plist 文件同级目录下创建 .strings 文件,Command + N 新建 Strings File 文件,命令为 InfoPlist.strings

如图所示:

20170413149206202285102.png

配置 .strings 文件

创建成功后选中 InfoPlist.strings 文件,点击 Localize... 按钮,左侧弹框中选择语言。

如图所示:

20170413149206218824740.png

选中后再次勾选上你想要的其他语言

如图所示:

20170413149206223443381.png

勾选成功后在左侧 InfoPlist.strings 中创建了几个文件,如图:

20170413149206232317524.png

分别在各自的文件内写入如下代码,其中 iWords 是我的App在英语环境下显示的名称,你只需要在对应的文件内写入对应的名称即可,这样当手机语言变化时,就会显示这些对应文件内的 App 名称。

CFBundleDisplayName = "iWords";

到此为止,App 名称的国际化完成。切记创建 .strings 文件时一定要命名为 InfoPlist.strings

App 内部实现语言切换

新建并配置 .strings 文件

内部实现语言切换和 App 名称国际化基本一致,首先创建 .strings 配置文件,这里我命名为:DHLocalizable.strings

创建成功后依然点击右侧的 Localize... 按钮,并手动勾选其他语言,勾选完成后左侧依旧生成对于的.strings文件,如图:

20170413149206282572841.png

创建多语言切换工具类

创建继承于 NSObject 的工具类,命名为:DHLanguageTool

其中 DHLanguageTool.h 中声明基本的方法,包括初始化App语言,当前的语言,设置语言等。

#define DHLocalizedString(key)  [[DHLanguageTool bundle] localizedStringForKey:(key) value:@"" table:@"DHLocalizable"]

#import <Foundation/Foundation.h>

#define DHLanguageKey @"userLanguage"

#define DHSimplifiedChinese @"zh-Hans"

#define DHTraditionalChinese @"zh-Hant"

#define DHEnglish @"en"

@interface DHLanguageTool : NSObject

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

/**
 *  初始化语言文件
 */
+ (void)initUserLanguage;

/**
 *  获取应用当前语言
 */
+ (NSString *)userLanguage;

/**
 *  设置当前语言
 */
+ (void)setUserlanguage:(NSString *)language;

DHLanguageTool.m 文件中对其进行实现,我这里一共可以设置3种语言,其中包括简体中文,繁体中文以及英文。

#import "DHLanguageTool.h"
#import "MainTabBar.h"

static DHLanguageTool *currentLanguage;

@implementation DHLanguageTool

static NSBundle *bundle = nil;

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

// 初始化语言文件
+ (void)initUserLanguage{
    
    NSString *languageString = [[NSUserDefaults standardUserDefaults] valueForKey:DHLanguageKey];
    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 ([[DHLanguageTool SimplifiedChinese] containsObject:languageString]) {
        languageString = [[DHLanguageTool SimplifiedChinese] firstObject]; // 中文
        
    } else if ([[DHLanguageTool english] containsObject:languageString]) {
        languageString = [[DHLanguageTool english] firstObject]; // 英文
        
    }else if ([[DHLanguageTool TraditionalChinese] containsObject:languageString]) {
        languageString = [[DHLanguageTool TraditionalChinese] firstObject]; // 繁体中文
        
    } else {
        languageString = [[DHLanguageTool SimplifiedChinese] firstObject]; // 其他默认为中文
    }
    // 获取文件路径
    NSString *path = [[NSBundle mainBundle] pathForResource:languageString ofType:@"lproj"];
    // 生成bundle
    bundle = [NSBundle bundleWithPath:path];
}

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

// 简体中文类型数组
+ (NSArray *)SimplifiedChinese{
    return @[@"zh-Hans"];
}

// 繁体中文类型数组
+ (NSArray *)TraditionalChinese{
    return @[@"zh-Hant"];
}


// 获取应用当前语言
+ (NSString *)userLanguage {
    NSString *languageString = [[NSUserDefaults standardUserDefaults] valueForKey:DHLanguageKey];
    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:DHLanguageKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
    
    [UIApplication sharedApplication].keyWindow.rootViewController = nil;
    if ([UIApplication sharedApplication].keyWindow.rootViewController == nil) {
        MainTabBar *tabBar = [[MainTabBar alloc] init];
        tabBar.selectedIndex = 2;
        [UIApplication sharedApplication].keyWindow.rootViewController = tabBar;
        
        setToast(@"语言切换成功");
    }
}

工具类定义完成后在预编译文件内导入该工具类的头文件。

配置多语言切换 .strings 文件

在刚才创建的 DHLocalizable.strings 下展开并在对应的语言文件内填写需要进行语言切换的字段。如:我的英文文件和中文文件内填写如下:

"查单词" = "Search";
"记单词" = "Write";
"我" = "Me";
"邀请好友" = "Invite friends";
"给我好评" = "To evaluate";
"使用帮助" = "Help center";
"查单词" = "查单词";
"记单词" = "记单词";
"我" = "我";
"邀请好友" = "邀请好友";
"给我好评" = "给我好评";

注意:其中前边对应 键(key) ,后边对各个语言的值(value).

开始使用并初始化

self.title = DHLocalizedString(@"语言切换");

因为我们的工具类中已经定义了宏,所以在需要进行语言切换的控件进行这样的书写,书写的内容为各个语言文件中的 key。

设置完成所有字段后,在 AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法里先初始化语言。

// 初始化语言
[DHLanguageTool initUserLanguage];

当用户手机设置了简体中文、繁体中文或者英文后,点击App进入后会显示对应的语言,但如果用户设置了其他语言的话,就默认显示中文了。这里并不是唯一的,具体要适配哪种语言,可以根据实际情况进行设定。

更改其他语言

在切换语言的点击事件内调用以下方法:

// 设置简体中文
[DHLanguageTool setUserlanguage:DHSimplifiedChinese];
// 设置繁体中文
[DHLanguageTool setUserlanguage:DHTraditionalChinese];
// 设置英文
[DHLanguageTool setUserlanguage:DHEnglish];

多语言切换的坑

更改语言后页面的加载逻辑

1、重新载入rootViewController

这个方法应该是编码成本最低的方法了,只需要把原有的rootViewController移除并清空,然后重新设置一遍rootViewController就行了。但是这种实现方式会重新加载已经原来已经加载好的所有界面。

2、语言改变发送通知

在用户切换语言的时候,发送一个通知,然后在各个界面接收通知,更新所有需要更新的文本即可。这种方法适合新建的项目,在代码编写之初就预留好更新文本的方法,收到通知后调用此方法就行。如果已经是一个已上线项目,则改动成本比较高,需要改动的地方比较多。

3、.h暴露一个更新文字的方法

在用户切换语言的时候,遍历所有已经加载的界面,调用更新文字的方法。这种实现也是比较适合新建的项目,在代码编写之初就预留好更新文本的方法。如果项目已上线,则改动成本较高。

注:为了方便,我使用的方法1。

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

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

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

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

#define DHLocalizedString(key)  [[DHLanguageTool bundle] localizedStringForKey:(key) value:@"" table:@"DHLocalizable"]

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

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

关于初始化语言 [DHLocalizableController initUserLanguage]

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

if ([[DHLanguageTool SimplifiedChinese] containsObject:languageString]) {
        languageString = [[DHLanguageTool SimplifiedChinese] firstObject]; // 中文
        
    } else if ([[DHLanguageTool english] containsObject:languageString]) {
        languageString = [[DHLanguageTool english] firstObject]; // 英文
        
    }else if ([[DHLanguageTool TraditionalChinese] containsObject:languageString]) {
        languageString = [[DHLanguageTool TraditionalChinese] firstObject]; // 繁体中文
        
    } else {
        languageString = [[DHLanguageTool SimplifiedChinese] firstObject]; // 其他默认为中文
    }

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

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

出现以上情况时 DHLocalizedString(<#key#>) 这个方法从 .strings 配置文件里是去不到对应的字体,就会返回空。

后果轻则页面一片空白了,重则直接 crash ,如:

NSArray *arr = @[DHLocalizedString(@"语言切换"),DHLocalizedString(@"意见反馈"),DHLocalizedString(@"加入群组")];

如果坚持使用 NSLocalizedString(<#key#>, <#comment#>) 方法

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

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

2、比如:就想使用 NSLocalizedString(<#key#>, <#comment#>) 方法。

总归还是有方法使用 NSLocalizedString(<#key#>, <#comment#>) 的。

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

好吧,show you the code:

#import "NSBundle+DHLanguage.h"
#import <objc/runtime.h>

static const NSString *DHBundleKey = @"DHLanguageKey";
@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 (DHLanguage)

+ (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

1、objc_getAssociatedObjectobjc_setAssociatedObject 是一对 gettersetter 方法,目的是为了给 NSBundle 类动态添加一个属性。

2、object_setClassBundleEx 里实现一个 localizedStringForKey 方法,然后将 BundleEx 这个类设置给 [NSBundle mainBundle] 。目的就是相当于重载 [NSBundle mainBundle]localizedStringForKey 方法。

再说本篇文章,该类别新增方法的使用:

DHLocalizableController 类的 + (void)setUserlanguage:(NSString *)language 方法里,本地化存储语言之后调用如下方法:

[NSBundle setLanguage:language];

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

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

后记

参考链接1

参考链接2