2020-04-23

iOS 组件化下的多语言方案

作者 货拉拉技术

这一次 P1 项目需要对司机端和用户端做国际化改造,由于项目在早期的迭代过程中没有考虑国际化的需求,项目中的字符串都是通过硬编码的形式直接写在业务逻辑里的,我们在做国际化时遇到的第一个问题就是如何将现有硬编码字符串替换为支持多语言的实现方案。经过初步统计,司机端和用户端各有约 4000-5000 的字符串,且公司内所有 iOS 项目都已经完成了组件化改造,我们需要一个比较好的方案来快速完成多语言方案替换,尽量不将过多的时间花在这类非技术性问题上。

多语言宏

对于已有的非多语言项目来说,使用苹果提供的多语言宏是最佳选择,因为苹果提供的宏可以通过 Xcode 的 Editor-Export for Localization... 功能直接将所有多语言字符串一键导出为 .xliff 文件或 .strings 文件。

首先梳理一下 Objective-C 提供的多语言宏,苹果一共提供了四个宏供开发者使用:

NSLocalizedString(key, comment)
NSLocalizedStringFromTable(key, tbl, comment)
NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment)
NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)
  • NSLocalizedString(key, comment) 宏只有两个参数,第一个是 key,第二个是给翻译人员看的注释,可以为 nil 或者空字符串。这个宏书写起来比较简洁,只能从 main bundle 中对应语言的 Localizable.strings 文件里取到对应 key 的显示语言。
  • NSLocalizedStringFromTable(key, tbl, comment) 比上一个宏多一个 table 参数,可以从 main bundle 中指定的 .strings 文件中获取 key-value 值,table 名称就是 .strings 文件的名称。
  • NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) 比上一个宏多一个 bundle 参数,可以从指定 bundle 而不是默认的 main bundle 中的 .strings 文件获取 key-value 值。
  • NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) 比上一个宏多一个 val 参数用于指定默认值,当在对应的文件中取不到对应 key-value 将使用 val 的值返回。

对于货拉拉国际化项目的需求来说,产品明确需要支持 APP 内切换语言 ,于是前两个只能跟随系统语言的宏就不在我们的考虑范围了,而后面两个宏可以指定 bundle ,这就给我们在 APP 内设置不同于系统语言时从不同的 bundle 取相应的语言提供了可能。

最终我们采用的是 NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) 这个宏,因为我们不会有指定默认值的场景。

对于 NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) 这个宏,我们需要关心的就是 tbl(table) 和 bundle 这两个值,由于货拉拉 iOS 项目全部实现了业务层级的组件化,所以我们的 .strings 文件可以根据组件来划分,以组件名作为 table 的名称,如订单组件 Order.strings,table 值即为 Order。 bundle 的值是实现 APP 内切换语言的关键所在,我们需要用一个专门的类来管理 APP 显示语言。

APP 内切换语言

为了实现 APP 内切换语言,我们实现了一个 HLLLocalization 单例类,用来管理 APP 语言设置。

首先是提供读取和设置语言的方法:

读取方法会先判断用户是否在 APP 内设置过语言,我们将用户设置的语言保存在本地,若没有设置过则读取系统语言:

- (NSString *)currentLanguage {
    if (!_currentLanguage) {
        // 优先使用自定义语言,没有则使用系统语言
        if (有自定义语言) {
                _currentLanguage = 自定义语言;
        } else {
                _currentLanguage = 系统语言;
        }
    }
    return _currentLanguage;
}

设置语言方法:

- (void)setCurrentLanguage:(NSString *)currentLanguage {
    _currentLanguage = currentLanguage;
    // 保存 currentLanguage 到本地;
}

有了这两个方法,我们就可以实现根据用户设置的语言对应到正确的 bundle 了:

- (NSBundle *)languageBundleFromRoot:(NSBundle *)aBundle {
    NSString *path = [aBundle pathForResource:self.currentLanguage ofType:@"lproj"];
    NSBundle *bundle = [NSBundle bundleWithPath:path]?:aBundle;
    return bundle;
}

看了这个方法你可能会有疑问,为什么还是需要传入一个 bundle?

Cocoapods 组件化与 bundle

上面提到货拉拉的 iOS 项目实现了组件化,组件通过 Cocoapods 的方式来管理,这使得许多 UIKit 和 Foundation 库里提供的访问资源类的方法不能直接使用了,例如 [UIImage imageNamed:@""] 方法,这个方法只能从 main bundle 中读取图片资源,而通过 Cocoapods 的 subspec 来管理子组件的项目,图片资源是存在于 subspec 的 resource_bundles 中的,关于图片资源的解决方案会再单独写一篇文章来详细说明。

而本篇需要讨论的 NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) 宏同样需要用到 bundle,由于 Cocoapods 的原因,也不能直接从 main bundle 中直接访问,需要先获取到访问资源的类所处的 framework,进而获取到多语言文件 .strings 所在的 bundle(.lproj)。

这里有一点需要特殊说明,与图片资源的处理方式略有不同的是,为了应对前期开发阶段频繁新增/改动多语言文件的情况,多语言文件并没有单独放到每一个 subspec 中,而是放在 pod 最外层的 .resource 中,也就是说,打包后语言文件没有像图片一样存放在 .../*.app/Frameworks/POD_NAME.framework/SUBSPEC_NAME.bundle/Assets.car 中,而是存放在 .../*.app/Frameworks/POD_NAME.framework/*.lproj/*.strings 中,所有子组件同一种语言的 .strings 文件都在一个 .lproj 中,方便统一替换。

最后可以通过一个宏来实现获取相应 POD_NAME 的 bundle:

#define HLL_LANGUAGE_BUNDLE [HLLLocalization languageBundleFromRoot:[NSBundle bundleForClass:[self class]]]
// HLLLocalization 需提供相应的类方法来访问单例中的实例方法

体力活

方案决定之后就是替换工作了,这就是一个要求细致的体力活,我们的字符串的最终写法变成了:

NSLocalizedStringFromTableInBundle(@"字符串", @"组件名", HLL_LANGUAGE_BUNDLE, @"")

当然项目里几千个字符串我们是不可能一个个手动去替换的,Xcode 支持正则替换,我们可以使用正则来完成大部分字符串的替换:

搜索:
@"[^"]*[\u4E00-\u9FA5]+[^"\n]*?"
替换为:
NSLocalizedStringFromTableInBundle($0, @"组件名", HLL_LANGUAGE_BUNDLE, @"")

这样项目中所有的 @"" 里面包含中文的都会被替换掉,其中还包含了大量 NSLog 中的内容,如果不需要翻译这部分内容,可以用一些小技巧,比如先将 NSLog(@" 替换为其他内容让 NSLog 的字符串不是 @"" 这种格式,在正则替换完成后再将 NSLog(@" 替换回来。另外还有 @"" 中包含反斜杠转义的字符串会被正则替换错乱,这个需要手动处理一下,需要体力活的地方就在这里了。

最后全部都替换完成保证项目可以跑起来之后,就可以通过 Xcode 的 Editor-Export for Localization... 功能将多语言文件导出提供给翻译人员或者上传到诸如 Crowdin 之类的翻译平台上进行翻译了。

作者介绍

李扬,货拉拉资深客户端工程师