Jerry's Blog

Less is more, Live and learning

Hello,I'm an iOS developer in China.


iOS国际化实践

前言

为了实现在iOS项目中的多语言切换,我们需要对工程中的各个资源、 各个模块进行相应的国际化操作。在实际的项目需求中,一般还需要进行应用内语言切换,除了默认的跟随系统之外,还要实现动态的语言版本切换。

实现思路

字面上理解,国际化就是对应用中出现的文字、图片资源进行翻译,然后通过键值映射的方式进行切换即可,但是在实践中需要处理的地方和注意的点还是挺多的, 尤其对于旧项目进行国际化改造时,工作量还是挺大的。

总的来说,国际化就是对资源进行替换,分别涉及到了字符串国际化、图片文件等资源国际化、xib文件国际化、InfoPlist国际化、网络数据国际化等。下面我们将通过举例,介绍在各种情况下的国际化实践。

字符串国际化

字符串在整个国际化中占比最高,首先我们需要在Xcode中,创建需要国际化的语言模板,然后在相应的模板中通过"key" = "value"的形式,对项目中出现的字符串进行翻译即可。

在Xcode中选中项目的PROJECT->Info选项,定位到Localizations标签,添加需要支持的语言版本:

img_01

然后,我们需要手动创建Localizable.strings 文件,用于生成本地化字符串映射文件。需要注意的是这个文件名必须设置为指定的Localizable ,这样系统才会识别。

为了简便,如果我们主语言使用的是中文的话,我们可以将中文作为key,这样的话,在代码中,我们可以直接在宏替换中写中文,如果国际化文件中没有对应的翻译时,会直接返回key,这样我们就不需要再添加简体中文的语言模板了。

新建文件,选择Strings File 模板,创建Localizable.strings文件

img_02

img_03

创建成功后,我们选中Localizable.strings文件,点击右侧的Localization按钮,勾选刚才创建的语言类型

img_04

当添加完成后,我们会发现Localizable.strings文件变成了一个文件夹样式,展开后,我们就可以看到生成的字符串模板文件了。

img_05

现在我们就可以在相应的模板中,对字符串进行国际化了。如下图所示,等号左边的是key,等号右边的是对key的翻译:

img_06

那么,在代码中是如何使用的呢?只需要在代码中,将字符串进行如下的宏替换即可:

// 本地化字符串宏定义,key:需要国际化的字符串,comment:一个注释,一般设置为nil
#define NSLocalizedString(key, comment)

// 举例
self.title = NSLocalizedString(@"钱包", nil);

对于旧项目中有大量字符串需要替换的时候,手动替换起来想想是不是很头疼呢?其实,我们也可以通过Xcode的全局替换功能,对符合规则的字符串进行匹配替换,这样会大大减轻我们的工作量,但是,也不要过度依赖替换功能,可能会对已有代码造成误操作,引起不必要的麻烦。查找替换步骤如下所示:

  1. 进入Xcode全局搜索模式
  2. 切换为Replace 模式,并修改匹配模式为:Regular Expression
  3. 输入匹配的正则表达式:(@"[^"]*[\u4E00-\u9FA5]+[^"\n]*?"),作用是匹配到所有包含中文的OC字符串
  4. 在替换输入栏中输入替换表达式:NSLocalizedString($1, nil),也就是上面讲到的宏,其中$1是正则匹配到的结果值,作为参数。
  5. 最后执行查找,并点击按钮Replace All进行替换。

img_12

上面的方法虽然简单快捷,但是替换过程中很容易造成语法错误,而且很难排查,使用过程中一定要细心、谨慎。

Xib&Storyboard 国际化

xib 和Storyboard 的国际化原理其实和字符串差不多,需要开启xib 或Storyboard的国际化支持,在右侧添加需要国际化的语言,然后Xcode会自动生成xxx.strings文件,我们只需要翻译该文件中的字符串即可,如下图所示:

img_08

通过注释,我们可以看出来是UILabel的text属性,其中key是xib自动生成的控件的唯一编码,value则是我们需要国际化的字符串,只需要将翻译好的字符串进行替换就可以了。

这里有一点需要特别注意,当我们修改xib文件时,这个字符串模板文件中的内容是不会同步的,我们还需要添加一个脚本用于同步xib文件的更改,下面我们介绍下这个脚本的添加步骤:

  1. 下载脚本文件,并导入到项目根目录

  2. 打开Xcode 进入Build Settings,找到Deployment LocationDeployment Postprocessing选项,并设置为YES

  3. 选中项目的Target,定位到Build Phases选项 ,然后添加RunScript,将下面的脚本代码添加到输入框中:

python ${SRCROOT}/${TARGET_NAME}/RunScript/AutoGenStrings.py ${SRCROOT}/${TARGET_NAME}

具体操作步骤见下图:

img_10

img_09

添加完成后,每当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会自动生成国际化模板文件,我们只需要提交模板文件中的内容即可。其他格式的文件也是采用同样的国际化方式。

img_11

如上图所示,我们对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)这个宏替换,展开后我们得知,其内部是调用了NSBundlelocalizedStringForKey: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的国际化不算很难掌握, 但是,一般项目中可能涉及不到这个需求,希望有这方面学习需要的小伙伴能看到本篇文章,也算是尽一份绵薄之力。

文中讲到的知识点,可能有遗漏,或者不合理的地方,希望各位读者能帮笔者勘误,有什么意见或建议,可在下面进行留言交流,感谢阅读。

更早的文章

自建IPA分发平台

前言目前iOS App的内侧分发渠道很多,包括:蒲公英、fir.im等等三方分发平台,用户注册账号后,可以直接上传Archive后的ipa文件即可通过生成的应用下载页来下载安装。但是,为了满足个性化以及品牌宣传等需求,我们需要自建H5下载页,并配置好相关数据也可以制定自定义的分发下载渠道。配置步骤首先我们需要配置一个plist文件,包含2种尺寸的icon图标、app基本信息、和ipa文件的下载地址等,下面我们就以一个例子,介绍下具体的实现步骤:1. Plist 文件# manifest-d...…

iOS继续阅读