前言
为了实现在iOS项目中的多语言切换,我们需要对工程中的各个资源、 各个模块进行相应的国际化操作。在实际的项目需求中,一般还需要进行应用内语言切换,除了默认的跟随系统之外,还要实现动态的语言版本切换。
实现思路
字面上理解,国际化就是对应用中出现的文字、图片资源进行翻译,然后通过键值映射的方式进行切换即可,但是在实践中需要处理的地方和注意的点还是挺多的, 尤其对于旧项目进行国际化改造时,工作量还是挺大的。
总的来说,国际化就是对资源进行替换,分别涉及到了字符串国际化、图片文件等资源国际化、xib文件国际化、InfoPlist国际化、网络数据国际化等。下面我们将通过举例,介绍在各种情况下的国际化实践。
字符串国际化
字符串在整个国际化中占比最高,首先我们需要在Xcode中,创建需要国际化的语言模板,然后在相应的模板中通过"key" = "value"
的形式,对项目中出现的字符串进行翻译即可。
在Xcode中选中项目的PROJECT->Info
选项,定位到Localizations
标签,添加需要支持的语言版本:
然后,我们需要手动创建Localizable.strings
文件,用于生成本地化字符串映射文件。需要注意的是这个文件名必须设置为指定的Localizable
,这样系统才会识别。
为了简便,如果我们主语言使用的是中文的话,我们可以将中文作为key,这样的话,在代码中,我们可以直接在宏替换中写中文,如果国际化文件中没有对应的翻译时,会直接返回key,这样我们就不需要再添加简体中文的语言模板了。
新建文件,选择Strings File
模板,创建Localizable.strings
文件
创建成功后,我们选中Localizable.strings
文件,点击右侧的Localization
按钮,勾选刚才创建的语言类型
当添加完成后,我们会发现Localizable.strings
文件变成了一个文件夹样式,展开后,我们就可以看到生成的字符串模板文件了。
现在我们就可以在相应的模板中,对字符串进行国际化了。如下图所示,等号左边的是key,等号右边的是对key的翻译:
那么,在代码中是如何使用的呢?只需要在代码中,将字符串进行如下的宏替换即可:
// 本地化字符串宏定义,key:需要国际化的字符串,comment:一个注释,一般设置为nil
#define NSLocalizedString(key, comment)
// 举例
self.title = NSLocalizedString(@"钱包", nil);
对于旧项目中有大量字符串需要替换的时候,手动替换起来想想是不是很头疼呢?其实,我们也可以通过Xcode的全局替换功能,对符合规则的字符串进行匹配替换,这样会大大减轻我们的工作量,但是,也不要过度依赖替换功能,可能会对已有代码造成误操作,引起不必要的麻烦。查找替换步骤如下所示:
- 进入Xcode全局搜索模式
- 切换为Replace 模式,并修改匹配模式为:
Regular Expression
- 输入匹配的正则表达式:
(@"[^"]*[\u4E00-\u9FA5]+[^"\n]*?")
,作用是匹配到所有包含中文的OC字符串 - 在替换输入栏中输入替换表达式:
NSLocalizedString($1, nil)
,也就是上面讲到的宏,其中$1
是正则匹配到的结果值,作为参数。 - 最后执行查找,并点击按钮
Replace All
进行替换。
上面的方法虽然简单快捷,但是替换过程中很容易造成语法错误,而且很难排查,使用过程中一定要细心、谨慎。
Xib&Storyboard 国际化
xib 和Storyboard 的国际化原理其实和字符串差不多,需要开启xib 或Storyboard的国际化支持,在右侧添加需要国际化的语言,然后Xcode会自动生成xxx.strings
文件,我们只需要翻译该文件中的字符串即可,如下图所示:
通过注释,我们可以看出来是UILabel
的text属性,其中key是xib自动生成的控件的唯一编码,value则是我们需要国际化的字符串,只需要将翻译好的字符串进行替换就可以了。
这里有一点需要特别注意,当我们修改xib文件时,这个字符串模板文件中的内容是不会同步的,我们还需要添加一个脚本用于同步xib文件的更改,下面我们介绍下这个脚本的添加步骤:
-
下载脚本文件,并导入到项目根目录
-
打开Xcode 进入
Build Settings
,找到Deployment Location
和Deployment Postprocessing
选项,并设置为YES
。 -
选中项目的Target,定位到
Build Phases
选项 ,然后添加RunScript,将下面的脚本代码添加到输入框中:
python ${SRCROOT}/${TARGET_NAME}/RunScript/AutoGenStrings.py ${SRCROOT}/${TARGET_NAME}
具体操作步骤见下图:
添加完成后,每当xib或storyboard文件有改动时,只需要重新Build一下,就会更新国际化模板文件了。
图片&资源文件国际化
图片国际化
Xcode并没有给我们提供,专门针对图片资源的国际化方式。在实际开发中,我们一般会将icon图标,及常用的展示图放置到Assets.xcassets
文件中进行管理,将不常用且比较大的图片资源,直接拖放到Resource
文件夹中,针对这两种图片管理方式,我们也采取了不同的国际化方式,下面将介绍实现方式:
对于Assets.xcassets
目录中的图片,我们将通过字符串国际化的方式,加载不同语言本版的图片资源,前提是需要将多个版本的图片资源先要导入到Assets.xcassets
中,使用方法见下面示例代码:
// Localizable.strings 文件中
// Assets
"profile_banner" = "profile_banner_en";
"profile_banner" = "profile_banner_hant";
// 使用示例,这里只需要进行图片名称的字符串替换即可
UIImage *img = [UIImage imageNamed:NSLocalizedString(@"profile_banner", nil)];
文件国际化
Resource
中的图片,由于图片资源会以文件的形式,直接导入到Xcode工程中,所以可以使用文件国际化的方式,直接在Xcode中打开文件的inspector
面板,选择需要国际化的Localization版本,Xcode会自动生成国际化模板文件,我们只需要提交模板文件中的内容即可。其他格式的文件也是采用同样的国际化方式。
如上图所示,我们对Expression.plist
文件进行了国际化,只需要选择右侧的语言版本,即可以生成对应的模板文件,是不是感觉很简单呢,赶紧行动起来实践一下吧!
还有部分特殊的配置文件,比如:InfoPlist
文件等,具体实现和文件国际化的方式是一样的,这里就不再赘述了。
应用内语言切换
在实际项目需求中,我们实现了国际化功能后,也会相应的实现应用内语言切换功能,所以在国际化实践中,是相当重要的一个环节。
通过分析,我们知道,要实现应用内动态语言切换,实际是需要将整个项目的UI,重新进行一次初始化,根据当前选择的语言类型,加载对应的语言包(例如:en.lproj),这样就实现了语言的动态切换了。
下面我们通过代码,展示下实现思路:
- xxxViewModel.m 文件中语言切换相关的代码实现
### xxxViewModel.m 文件
#define kUserDefaults [NSUserDefaults standardUserDefaults]
// 1. 切换语言入口方法
- (void)switchLanguageWithType:(WLTLanguageType)languageType{
if (languageType == self.currentLanguageType) {
return;
}
self.currentLanguageType = languageType;
NSString *lanCode = nil;
switch (languageType) {
case WLTLanguageTypeFollowSystem:
lanCode = nil;
break;
case WLTLanguageTypeSimplified:
lanCode = @"zh-Hans"; //中文简体
break;
case WLTLanguageTypeTraditional:
lanCode = @"zh-Hant"; // 中文繁体
break;
case WLTLanguageTypeEnglish:
lanCode = @"en"; // 英文
break;
}
// 切换语言
[self setUserLanguage:lanCode];
// 刷新keyWindow
[kWLTAppDelegate resetKeyWindowRootViewController];
}
// 切换语言
// 注释:这里需要将用户选择的语言类型进行本地缓存,系统在加载国际化语言bundle的时候会
// 根据这个值来加载语言版本的
- (void)setUserLanguage:(NSString *)userLanguage{
//跟随手机系统
if (!userLanguage.length) {
[self resetSystemLanguage];
return;
}
//用户自定义
[kUserDefaults setObject:userLanguage forKey:WLTUserLanguageKey];
[kUserDefaults setObject:@[userLanguage] forKey:@"AppleLanguages"];
[kUserDefaults synchronize];
}
+ (NSString *)userLanguage{
return [kUserDefaults objectForKey:WLTUserLanguageKey];
}
+ (NSString *)systemLanguage{
return [[kUserDefaults objectForKey:@"AppleLanguages"] firstObject];
}
/// 重置系统语言
- (void)resetSystemLanguage{
[kUserDefaults removeObjectForKey:WLTUserLanguageKey];
[kUserDefaults setObject:nil forKey:@"AppleLanguages"];
[kUserDefaults synchronize];
}
- AppDelegate.m 文件中,重新初始化
KeyWindow
的根控制器
### AppDelegate.m 文件
- (void)resetKeyWindowRootViewController{
WLTMainViewModel *mainVm = [WLTMainViewModel viewModel];
mainVm.tabBarSelectedIndex = 3; //注释:这里要提前设置为“语言切换控制器”相应的tabbarIndex
WLTMainViewController *newMainVc = [[WLTMainViewController alloc] initWithViewModel:mainVm];
WLTLanguageViewController *lanVc = [[WLTLanguageViewController alloc] initWithViewModel:[WLTLanguageViewModel viewModel]];
lanVc.hidesBottomBarWhenPushed = YES; //注释:如果“语言切换控制器”不是导航控制器的根控制器时,需要提前设置tabbar隐藏
dispatch_async(dispatch_get_main_queue(), ^{
// 给出友好提示...
[MBProgressHUD showActivityWithStatus:NSLocalizedString(@"语言切换中...", nil)];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUD];
// 重置keyWindow,会初始化整个UI
self.mainVC = newMainVc;
self.window.rootViewController = newMainVc;
// 跳转到语言设置页,很关键的一步,让用户不会感知到UI的刷新
WLTNavigationController *currentNav = newMainVc.tabBarController.selectedViewController;
NSMutableArray *navVcs = currentNav.viewControllers.mutableCopy;
[navVcs addObject:lanVc];
currentNav.viewControllers = navVcs;
});
});
}
- 这一步是整个语言动态切换最关键也是最核心的一步,我们知道OC是一门动态语言,可以在运行时对类进行自定义操作。那么,我们将利用这一特性,来实现动态语言切换。
在上面介绍中,我们知道国际化最关键的一句代码是#define NSLocalizedString(key, comment)
这个宏替换,展开后我们得知,其内部是调用了NSBundle
的localizedStringForKey:value:
方法。所以,我们只需要在运行时,重写这个方法,返回用户选择的语言版本就可以了。
我们首先创建一个NSBundle
的分类 NSBundle+WLTUtils
,然后创建一个继承自NSBundle
的子类WLTBundle
,在子类中覆写-localizedStringForKey: value:
方法。
其实,我们也可以在分类中的+ (void)load
方法中,通过runtime 的Method Swizzle
交换默认的-localizedStringForKey: value:
方法实现,来达到此目的。
下面我们介绍第一种实现方式:
### NSBundle+WLTUtils.m 文件中
// 子类的定义
@interface WLTBundle : NSBundle
@end
@implementation NSBundle (WLTUtils)
// 处理应用内国际化语言切换
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 修改 mainBundle 是所属类为"WLTBundle"类
// mainBundle -> NSBundle 修改为: mainBundle -> WLTBundle -> NSBundle
object_setClass([NSBundle mainBundle], [WLTBundle class]);
});
}
+ (NSString *)currentLanguage{
return [WLTLanguageViewModel userLanguage];
}
@end
@implementation WLTBundle
// 覆写本地化关键发放,进行自定义操作
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName{
if ([WLTBundle wlt_mainBundle]) {
return [[WLTBundle wlt_mainBundle] localizedStringForKey:key value:value table:tableName];
} else {
return [super localizedStringForKey:key value:value table:tableName];
}
}
+ (NSBundle *)wlt_mainBundle{
if ([NSBundle currentLanguage].length) {
NSString *path = [[NSBundle mainBundle] pathForResource:[NSBundle currentLanguage] ofType:@"lproj"];
if (path.length) {
return [NSBundle bundleWithPath:path];
}
}
return nil;
}
@end
辅助工具
我们知道,如果是旧项目进行国际化改造的时候,工作量巨大,而且可能出现遗漏,覆盖不全等问题。那么,我们如何将3天的工作量,缩短到10分钟完成呢?聪明且喜欢动手的小伙伴,已经帮我们写好了自动化工具,是不是有种很崇拜的感觉呢?恩恩🤔,是的👍。
TCZLocalizableTool是GitHub 上的一位小伙伴开发的iOS国际化工具,大大缩减了iOSer们的工作量,这个工具实现了下面几个功能:
- 可以快速找出国际化文件的语法错误
- 可以快速找出未国际化的文本
- 可以找出国际化文件中未国际化的文本
- 更人性化的找出项目中未使用的图片
有兴趣的小伙伴可以在这里查看使用方法,同时也感谢这位小伙伴的开源贡献!
总结
至此,iOS国际化的相关实践就全部介绍完了,其中讲到了字符串国际化、xib&stroryboard 文件的国际化、图片&资源文件的国际化,还介绍了应用内语言切换的实现方式,最后介绍了GitHub上一位小伙伴开发的自动化工具。
总的来说iOS的国际化不算很难掌握, 但是,一般项目中可能涉及不到这个需求,希望有这方面学习需要的小伙伴能看到本篇文章,也算是尽一份绵薄之力。
文中讲到的知识点,可能有遗漏,或者不合理的地方,希望各位读者能帮笔者勘误,有什么意见或建议,可在下面进行留言交流,感谢阅读。