iOS 国际化开发

** 原文发表在:https://www.xiaolei0808.com/2016/04/24/Localized-iOS/
https://blog.xiaolei.kim/ios--e5-9b-bd-e9-99-85-e5-8c-96-e5-bc-80-e5-8f-91/**
https://blog.xiaolei.kim/2016/04/25/iOSInternationalization.html

一个iOS应用程序,如果想要在多个国家和地区的AppleStore上架,是很简单的一件事情。如果想要每个国家和地区的用户都能获得良好的使用体验,首先需要做的一件事情就是能够让用户打开App的第一时间,看到的是自己熟悉的语言。也就是说App能够根据用户当前所使用的语言或者用户手动选择的语言,实时的改变App内的语言。这就是iOS国际化所要实现的目的。

国际化开发的两种情况

1.在App开发之初,就已经有了国际化开发的Future,这种情况下进行国际化是很容易的,只要在开发过程中把需要国际化的字符串进行简单的处理即可。

2.已经开发完毕,开发之初并没有进行国际化适配,突然来需求说需要进行国际化,此时面对项目中成百上千的字符串,内心一定是崩溃的。

接下来,我会分别对这两种情况来说一下开发过程。

注:演示所使用的Xcode版本为7.3。

新启动的工程

项目国际化配置

1.找到Project的Localizations选项,点击加号(+),添加需要国际化的语言(一般工程中默认支持英文,为了方便演示,我只添加了中文简体支持)。此时会弹出一个选择框,选择你所要支持的Xib文件或StoryBoard文件。不需要支持Xib或StoryBoard文件则不勾选。

这里Use Base Internationalization开启状态下,每个国际化资源文件会有个Base选项,主要针对String,Xib,Storyboard作为一个基础的模板。

是否添加Xib支持
添加多语言支持

2.创建多语言文件,一般命名为Localizable.string。如果在开发过程中不指定文件名,系统会默认在Bundle中寻找这个名称的文件。当然,也可以任意命名,在开发过程中手动指定一下文件名即可。

添加多语言文件

3.找到并选中刚刚新建的Localizable.string文件,点击Inspector下Localization选项下的Localize按钮,任意选择一个语言(我选择的是English),然后点击Localize按钮,此时Localization选项会出现应用支持的语言列表,选择需要国际化的语言,Localizable.string文件下则会多出和所选择语言对应的子文件。

点击Localize按钮
选择语言
出现语言列表
添加需要国际化的语言

代码中对国际化内容的适配

1.我在ViewController中初始化了一个Label,并用NSLocalized宏对这个Label的字符串进行国际化适配。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UILabel *localizationLabel = [[UILabel alloc] initWithFrame:CGRectMake(150, 200, 100, 20)];
    [self.view addSubview:localizationLabel];
    
    //利用NSLocalizedString宏对字符串进行国际化适配
    localizationLabel.text = NSLocalizedString(@"Hello", @"description for this key.");
    
}

既然用到了NSLocalizedString宏,我们就有必要了解一下这个宏到底干了什么。

NSLocalizedString宏探秘

点进宏定义文件:

#define NSLocalizedString(key, comment) \
        [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]
#define NSLocalizedStringFromTable(key, tbl, comment) \
        [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
        [bundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \
        [bundle localizedStringForKey:(key) value:(val) table:(tbl)]

我们会发现国际化相关所定义的4个宏。

  • NSLocalizedString(key, comment)

NSLocalizedString其实是从mainBundle中默认读取了Localizable.string中的key所对应的value。comment参数则是对key的描述,有利于翻译人员理解这个key所适用的场景。

  • NSLocalizedStringFromTable(key, tbl, comment)

NSLocalizedStringFromTable则是从mainBundle中读取指定多语言文件中的key所对应的value。tbl参数就是用于指定多语言文件名的参数。

  • NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment)

NSLocalizedStringFromTableInBundle会读取指定Bundle中所指定的多语言文件中的key所对应的value。bundle参数就是用于传入所指定的Bundle。

  • NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)

NSLocalizedStringWithDefaultValue则是从指定Bundle中读取指定多语言文件中的key所对应的value,如果取不到对应的value,允许指定一个默认的value。val参数就是用于传入默认的value值。

2.了解了各个宏定义所代表的含义,我们继续往下写。

在Localizable.strings(English)中加入如下键值对:

"Hello" = "Hello";

在Localizable.strings(Chinese(Simplified))中加入如下键值对:

"Hello" = "你好";

编译运行:

系统语言为英文时显示效果:

英文运行结果

系统语言为中文时显示效果:

中文运行结果

可见,文本会随着系统语言的改变而改变。至此,一个简单的可以跟随系统语言切换的Demo就完成了。

已开发多个版本的工程

工程开发了许久,代码也写了几万行了,这时Boss突然说要进行国际化适配。

What the ***!!!

淡定,除了女朋友,还有什么能难道程序猿哥哥的事情呢?

下面开始。

添加应用需要国际化的语言

在Project的Localizations选项,点击加号(+),添加需要国际化的语言(一般工程中默认支持英文,为了方便演示,我只添加了中文简体支持)。

批量替换字符串

既然要在已有工程上进行适配,首先要做的就是给需要进行国际化的字符串加上NSLocalizedString宏了。

  • 2B程序猿

会一处一处的修改每个文件的每处字符串,直到天荒地老、海枯石烂。

  • 非主流程序猿

各种shell、Python脚本齐上阵。

  • 文艺程序猿

Xcode大法好。正则大法好。

我选择第三种-。-

首先,我们需要一个正则,去匹配工程中所有需要替换的文本。如果你不太了解正则,点我点我!!

下面这个正则可以匹配到所有符合OC字符串格式的包含有中文的字符串。如果你用Swift,请去掉@

@"[^"]*[\u4E00-\u9FA5]+[^"\n]*?"

然后,重点来了。Command+Shift+F,进入全局搜索引擎,切换为Replace模式,并把匹配模式改为Regular Expression。

在搜索条件里输入(@"[^"]*[\u4E00-\u9FA5]+[^"\n]*?"),在下面替换内容里输入NSLocalizedString($1, nil)。此处正则表达式两边加括号的目的是为了能够在替换时用$1获取原有字符串的值,在替换时把原有值放入宏定义内key的位置。然后,搜索,可以看到搜索结果,点击Replace All,即可完成替换。

正则搜索

生成多语言文件

替换完了原有字符串,下面就是生成多语言文件了。作为一个文艺程序猿,我是不可能一处一处的把项目中的字符串挑出来再写到Localizable.strings文件里的。

好在Xcode自带了一个命令行工具genstrings。我们可以利用这个工具生成所需的多语言文件。

首先,我们需要先新建所需语言的文件夹。

cd 工程目录
mkdir en.lproj
mkdir zh-Hans.lproj

然后,遍历所有.m文件,根据每个文件内的需要国际化的字符串生成key和value。

find . -name *.m | xargs genstrings -o en.lproj
find . -name *.m | xargs genstrings -o zh-Hans.lproj

此时,en.lprojzh-Hans.lproj文件夹中就应该有了相应的Localizable.string文件了。

我们把这两个文件夹拖到工程里,然后在相应的Localizable.strings文件中,修改每个key所对应的value值就行了(这个应该是翻译干的事儿)。

至此,项目的国际化已经适配完成。

应用内动态更新语言

以上所做的国际化,只会跟随系统语言进行改变,并且需要Kill掉App重新打开才会有效果。如果有一种需求,在应用内有一个语言列表,只要选中其中一种语言,点击确定后就能立刻更新应用内所有界面的语言,此时该怎么做呢?目前有好多大厂应用,比如微信、支付宝等都支持这种切换方式(微信和支付宝的实现方式不太一样,咱们稍后再叙)。

替换系统国际化宏定义

由系统宏定义可知,如果使用了系统的NSLocalizedString宏,它是默认是从mainBundle中读取Localizable.strings中的value的。使用其他的3个宏则可以指定文件名或是Bundle,但是使用起来也是不太灵活。为了使用起来更加方便灵活,我们可以自定义一个国际化的宏,实现其内部的逻辑。

下面,给出一种我自己的实现方式。

//
//  LELocalizedHelper.h
//  Created by 金小白 on 16/4/23.
//  Copyright © 2016年 xiaolei.jin. All rights reserved.
//  国际化

#import <Foundation/Foundation.h>

#define LELocalizedString(key) [[LELocalizedHelper standardHelper] stringWithKey:key]

@interface LELocalizedHelper : NSObject

+ (instancetype)standardHelper;

- (NSBundle *)bundle;

- (NSString *)currentLanguage;

- (void)setUserLanguage:(NSString *)language;

- (NSString *)stringWithKey:(NSString *)key;

@end

//
//  LELocalizedHelper.m
//  Created by 金小白 on 16/4/23.
//  Copyright © 2016年 xiaolei.jin. All rights reserved.
//  国际化

#import "LELocalizedHelper.h"

static NSBundle *_bundle;

static NSString *const kUserLanguage = @"kUserLanguage";

@implementation LELocalizedHelper

+ (instancetype)standardHelper {
    static LELocalizedHelper *helper;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        helper = [[LELocalizedHelper alloc] init];
    });
    return helper;
}

- (instancetype)init {
    
    if (self = [super init]) {
        
        if (!_bundle) {
            
            NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
            
            NSString *userLanguage = [defaults valueForKey:kUserLanguage];
            
            //用户未手动设置过语言
            if (userLanguage.length == 0) {
                
                NSArray *languages = [[NSBundle mainBundle] preferredLocalizations];
                
                NSString *systemLanguage = languages.firstObject;
                
                userLanguage = systemLanguage;
            }
            
            if ([userLanguage isEqualToString:@"zh-HK"] || [userLanguage isEqualToString:@"zh-TW"]) {
                userLanguage = @"zh-Hant";
            }
            
            NSString *path = [[NSBundle mainBundle] pathForResource:userLanguage ofType:@"lproj"];
            
            _bundle = [NSBundle bundleWithPath:path];
        }
        
    }
    return self;
}

- (NSBundle *)bundle {
    return _bundle;
}

- (NSString *)currentLanguage {
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    
    NSString *userLanguage = [defaults valueForKey:kUserLanguage];
    
    if (userLanguage.length == 0) {
        
        NSArray *languages = [[NSBundle mainBundle] preferredLocalizations];
        
        NSString *systemLanguage = languages.firstObject;
        
        return systemLanguage;
    }
    
    return userLanguage;
}

- (void)setUserLanguage:(NSString *)language {
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    
    NSString *path = [[NSBundle mainBundle] pathForResource:language ofType:@"lproj"];
    
    _bundle = [NSBundle bundleWithPath:path];
    
    [defaults setValue:language forKey:kUserLanguage];
    
    [defaults synchronize];
}

- (NSString *)stringWithKey:(NSString *)key {
    
    if (_bundle) {
        return [_bundle localizedStringForKey:key value:nil table:@"Localizable"];
    }else {
        return NSLocalizedString(key, nil);
    }
}

@end

使用的时候,只需把原来NSLocalizedString(key, nil)的地方替换为LELocalizedString(key)即可。此时如果用户没有手动更改过语言,默认是跟随系统语言变化的。

动态刷新所有页面

替换完了宏定义,下面就是全局刷新界面了。实现方式上来说,我目前共想出了3种,实现方式上来说各有优劣。

重新载入rootViewController

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

语言改变发送通知

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

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

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

上文说过,微信和支付宝的实现方式不太一样。根据我的观察和判断,微信使用的是上面的第一种方法,也就是重新载入rootViewController的方法。因为微信切换完语言之后,迅速的切换到了根页面,又迅速的push到了设置界面,有一个界面跳跃的过程,而且原来所有的已经滑到底部的列表全部都重置到了最顶部。根据我的推断,微信项目在编写之初也是没有考虑到后期国际化的需求,迫于过大的改动成本,只好使用第一种方法。而支付宝切换过程就比较完美,实现了无缝切换,和有可能是前期编码过程中就预留了更新文本的方法,后期直接调用就可以(不排除后期投入精力进行重构的可能)。

以上就是全部的iOS文字国际化的过程。

图片国际化

网络图片国际化

很简单,只要在请求的时候根据不同的语言环境调用不同的接口就可以。

本地图片国际化

本地图片的国际化也有两种实现方式。

根据不同语言指定不同的图片名称

这种方法可以在初始化图片名称的时候使用国际化宏定义,然后在对应语言的多语言文件中,根据图片对应的key指定相应语言的value即可。

利用Xcode生成

这种方法不用对代码进行改动,只需要在工程目录中新建一个Group,放入所有需要国际化的原有图片。然后选中图片,点击右侧的Localization,选中需要支持的语言即可在原有图片下生成选中语言所对应的子文件。然后替换掉对应语言的子图片即可。

注:图片名必须和原有名字一致

生成国际化图片
国际化图片列表
英文图片
中文图片

这样,在更改系统语言的时候,图片也会随之改变了。

以上就是我总结的所有的有关iOS国际化的操作了,如果有疏漏的地方,请各位看官多多指正。共勉。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容