iOS應(yīng)用程序語言本地化及應(yīng)用內(nèi)語言設(shè)置

原文 : 與佳期的個(gè)人博客(gonghonglou.com)

Xcode 新建一個(gè)工程的默認(rèn)語言是英文,所以你在 app 里粘貼復(fù)制都是顯示的 copy paste,你可以通過在 info.plst 文件里選擇 Localization native development region 來設(shè)置不同語言。

info.plst

可是如果你想在軟件內(nèi)選擇設(shè)置語言,為軟件添加多語言選擇功能就需要一番折騰了,倒也簡單,只不過還是有幾個(gè)坑的。以下是一篇詳細(xì)介紹為軟件配置多語言選項(xiàng)的博客,走起 ? ? ?

關(guān)于 NSBundle

在開始正式文章之前你或許應(yīng)當(dāng)先搞明白 NSBundle 是什么東西。

Bundle 是一個(gè)目錄,其中包含了在程序會(huì)使用到的資源,包含了如圖像、聲音、程序中需要用到的文件,甚至是編譯好的代碼等等。而在實(shí)現(xiàn)軟件內(nèi)配置語言的時(shí)候就是通過 Bundle 的路徑去獲取配置文件,根據(jù)這個(gè)配置文件取出對(duì)應(yīng)的字體渲染到 view 上。

當(dāng)然,配置程序語言只是 Bundle 的一種用途。還可以用 Bundle 去獲取工程中 info.plist 的詳細(xì)信息,比如:

// 獲取版本號(hào):Bundle Short Version
NSString *shortVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
// 獲取版本號(hào):Bundle version
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
// 獲取應(yīng)用標(biāo)識(shí):Bundle identifier
NSString *bundleIdentifier = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"];
// 獲取應(yīng)用名稱: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];
...

大概明白 NSBundle 是怎么回事了吧,接下來就正式開始應(yīng)用程序語言本地化及應(yīng)用內(nèi)語言設(shè)置。

配置 Project

添加語言

如下圖,點(diǎn)擊 PROJECT -> info -> Localizations 這里默認(rèn)只有 English 點(diǎn)擊下方的加號(hào)可以添加你想要的語言,比如這里添加的中文 Chinese(Simplifid) 。

注意: zh-Hans 是簡體中文, zh-Hant 是繁體中文。

配置Project

新建 .strings 配置文件

1、Command + N 新建 Strings File 文件,命令為 RDLocalizable ,會(huì)生成一份 RDLocalizable.strings 文件。

2、選中RDLocalizable.strings 文件,如下圖操作,點(diǎn)擊 Localize... 按鈕,左側(cè)彈框中選擇語言。

Localize...

3、之后右側(cè)會(huì)如下圖顯示,勾選上你想要的語言即可(Base 無用)

Localization

4、當(dāng)勾選兩門語言后,會(huì)發(fā)現(xiàn)RDLocalizable.strings 文件可以展開并存在兩個(gè)配置文件,一份英文,一份中文。
分別在兩個(gè)文件內(nèi)輸入對(duì)應(yīng)的語言,比如在英文文件里輸入:

"收錄" = "Collection";
"訂閱" = "Subscription";
"我的" = "Mine";

中文文件里輸入:

"收錄" = "收錄";
"訂閱" = "訂閱";
"我的" = "我的";

前邊對(duì)應(yīng) 鍵(key) ,后邊對(duì)各個(gè)語言的 值(value)。看后面的 ** 使用方法** 就會(huì)明白了。

至此,對(duì)工程的配置已經(jīng)完成。接下來要做的就是獲取軟件語言、設(shè)置語言、監(jiān)聽語言改變。。。

創(chuàng)建多語言設(shè)置工具類

因?yàn)樵摴ぞ哳惐容^簡單,直接將代碼貼出來吧,后面會(huì)介紹一些坑。因?yàn)槭且粋€(gè)繼承于 NSObject 的工具類,都是使用類方法實(shí)現(xiàn)功能,以便類名直接調(diào)用。

頭文件.h

//
//  RDLocalizableController.h
//  rder
//
//  Created by gonghonglou on 2016/10/29.
//  Copyright ? 2016年 gonghonglou. All rights reserved.
//

#import <Foundation/Foundation.h>

#define RDLanguageKey @"userLanguage"

#define RDCHINESE @"zh-Hans"

#define RDENGLISH @"en"

#define RDNotificationLanguageChanged @"rdLanguageChanged"

#define RDLocalizedString(key)  [[RDLocalizableController bundle] localizedStringForKey:(key) value:@"" table:@"RDLocalizable"]

@interface RDLocalizableController : NSObject

/**
 *  獲取當(dāng)前資源文件
 */
+ (NSBundle *)bundle;
/**
 *  初始化語言文件
 */
+ (void)initUserLanguage;
/**
 *  獲取應(yīng)用當(dāng)前語言
 */
+ (NSString *)userLanguage;
/**
 *  設(shè)置當(dāng)前語言
 */
+ (void)setUserlanguage:(NSString *)language;

@end

實(shí)現(xiàn)文件.m

//
//  RDLocalizableController.m
//  rder
//
//  Created by gonghonglou on 2016/10/29.
//  Copyright ? 2016年 gonghonglou. All rights reserved.
//

#import "RDLocalizableController.h"

static RDLocalizableController *currentLanguage;

@implementation RDLocalizableController

static NSBundle *bundle = nil;

// 獲取當(dāng)前資源文件
+ (NSBundle *)bundle{
    return bundle;
}

// 初始化語言文件
+ (void)initUserLanguage{
    NSString *languageString = [[NSUserDefaults standardUserDefaults] valueForKey:RDLanguageKey];
    if(languageString.length == 0){
        // 獲取系統(tǒng)當(dāng)前語言版本
        NSArray *languagesArray = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"];
        languageString = languagesArray.firstObject;
        [[NSUserDefaults standardUserDefaults] setValue:languageString forKey:@"userLanguage"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
    // 避免緩存會(huì)出現(xiàn) zh-Hans-CN 及其他語言的的情況
    if ([[RDLocalizableController chinese] containsObject:languageString]) {
        languageString = [[RDLocalizableController chinese] firstObject]; // 中文
    } else if ([[RDLocalizableController english] containsObject:languageString]) {
        languageString = [[RDLocalizableController english] firstObject]; // 英文
    } else {
        languageString = [[RDLocalizableController chinese] firstObject]; // 其他默認(rèn)為中文
    }
    
    // 獲取文件路徑
    NSString *path = [[NSBundle mainBundle] pathForResource:languageString ofType:@"lproj"];
    // 生成bundle
    bundle = [NSBundle bundleWithPath:path];
}

// 英文類型數(shù)組
+ (NSArray *)english {
    return @[@"en"];
}

// 中文類型數(shù)組
+ (NSArray *)chinese{
    return @[@"zh-Hans", @"zh-Hant"];
}

// 獲取應(yīng)用當(dāng)前語言
+ (NSString *)userLanguage {
    NSString *languageString = [[NSUserDefaults standardUserDefaults] valueForKey:RDLanguageKey];
    return languageString;
}

// 設(shè)置當(dāng)前語言
+ (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:RDLanguageKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
    
    [[NSNotificationCenter defaultCenter] postNotificationName:RDNotificationLanguageChanged object:currentLanguage];
}

@end

使用方法:

1、在 AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法里初始化該工具類,并監(jiān)聽通知:

// 語言初始化
[RDLocalizableController initUserLanguage];
// 監(jiān)控語言切換
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(languageChange:) name:RDNotificationLanguageChanged object:nil];

2、記得在 - (void)applicationWillTerminate:(UIApplication *)application方法里刪除通知:

[[NSNotificationCenter defaultCenter] removeObserver:self name:RDNotificationLanguageChanged object:nil];

3、實(shí)現(xiàn)通知方法:

- (void)languageChange:(NSNotification *)note{
    // 在該方法里實(shí)現(xiàn)重新初始化 rootViewController 的行為,并且所有帶有文字的頁面都要重新渲染
    // 比如:[UIApplication sharedApplication].keyWindow.rootViewController = ...;
}

4、使用 RDLocalizedString(<#key#>)方法 給所有文字添加本地化語言方法:

label.text = RDLocalizedString(@"收錄");
[button setTitle:RDLocalizedString(@"訂閱") forState:UIControlStateNormal];
...

5、更改語言方法:

// 設(shè)置中文
[RDLocalizableController setUserlanguage:RDCHINESE];

// 設(shè)置英文
[RDLocalizableController setUserlanguage:RDENGLISH];

至此,對(duì)于應(yīng)用程序語言本地化及應(yīng)用內(nèi)語言設(shè)置的功能就已經(jīng)可以實(shí)現(xiàn)了。接下來是對(duì)遇到的幾個(gè)坑的說明。

多語言設(shè)置的「坑」

關(guān)于更改語言后重新初始化頁面

語言更改后,要重新渲染view,所以應(yīng)該在更改語言之后回到根目錄。不僅頁面需要初始化,如果頁面數(shù)據(jù)在 viewModel 里,那么該 viewModel 也應(yīng)當(dāng)初始化,因?yàn)樽煮w是 RDLocalizedString(<#key#>) 這個(gè)方法從 .strings 配置文件里取出來的,更改語言后必須重新取一次。

當(dāng)然也不是一定要留在根目錄,有幾種頁面友好的解決方案:

1、更改語言功能一般會(huì)放在「我的」頁面 push 出來的某一級(jí)頁面,可以初始化 rootViewController 并且將之前 push 出來的幾級(jí) viewController 手動(dòng)添加到 mineViewController.navigationController.viewControllers 這個(gè)數(shù)組中。這樣頁面就不會(huì)產(chǎn)生太大的錯(cuò)落感。

2、在每一個(gè)頁面寫一個(gè)檢測(cè)語言改變的通知的方法。當(dāng)接受到通知后就將該頁面重新布局一次以更改字體。

PS:在這個(gè)問題上,感覺支付寶比微信做的界面跳轉(zhuǎn)友好的多。。。

關(guān)于本地化語言的宏定義 RDLocalizedString(<#key#>)

系統(tǒng)自帶的方法是:NSLocalizedString(<#key#>, <#comment#>),這也是一份宏定義:

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

能看到它調(diào)用的是 NSBundle.mainBundle ,而我們?cè)诟恼Z言的工具類里的 bundle 已經(jīng)更改了。
所以系統(tǒng)的 NSLocalizedString(<#key#>, <#comment#>) 已經(jīng)失效,必須重寫一份宏定義:

#define RDLocalizedString(key)  [[RDLocalizableController bundle] localizedStringForKey:(key) value:@"" table:@"RDLocalizable"]

1、必須使用自己的類名來調(diào)用類方法 [RDLocalizableController bundle] 以獲取自己的 bundle

2、table 后的參數(shù)為 .strings 文件的文件名,若你創(chuàng)建的文件名為 Localizable.strings ,則該參數(shù)可為 nil ,系統(tǒng)默認(rèn)按 Localizable.strings 查找。否則必須配置文件名,且只是文件名,不加 .stringd 后綴。

關(guān)于初始化語言 [RDLocalizableController initUserLanguage]

initUserLanguage 方法中有這樣一段代碼來做判斷

if ([[RDLocalizableController chinese] containsObject:languageString]) {
    languageString = [[RDLocalizableController chinese] firstObject]; // 中文
} else if ([[RDLocalizableController english] containsObject:languageString]) {
    languageString = [[RDLocalizableController english] firstObject]; // 英文
} else {
    languageString = [[RDLocalizableController chinese] firstObject]; // 其他默認(rèn)為中文
}

各位可能會(huì)對(duì)這個(gè)判斷比較疑惑,在這之前已經(jīng)有判斷了:先獲取用戶設(shè)置的語言,有則使用用戶設(shè)置的語言,沒有則使用系統(tǒng)語言。

然而因?yàn)槟承┰蛴脩粼O(shè)置過的語言(如:zh-Hans)會(huì)在另一個(gè)相同工程運(yùn)行之后將該語言更改為zh-Hans-CZ;或者用戶將系統(tǒng)語言設(shè)置為日本語或其他語言。

出現(xiàn)以上情況時(shí) RDLocalizedString(<#key#>) 這個(gè)方法從 .strings 配置文件里是去不到對(duì)應(yīng)的字體,就會(huì)返回空。
后果輕則頁面一片空白了,重則直接 crash ,如:

NSArray *array = @[RDLocalizedString(@"收錄"), RDLocalizedString(@"訂閱"), RDLocalizedString(@"我的")]; // 數(shù)組不能存空

就想使用 NSLocalizedString(<#key#>, <#comment#>) 方法

1、有一種極端情況,比如:軟件需要配置多國語言,很多很多的那一種。。。在 .strings 文件里配置了許多國家的語言。然而在軟件內(nèi)部只提供中文、英文等某幾種語言,其他語言根據(jù)系統(tǒng)語言自適應(yīng)。不想在 initUserLanguage 方法里做一大堆的亂七八糟的判斷。只要在 initUserLanguage 的判斷方法 else 里使用系統(tǒng)語言:

} else {
 languageString = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"][0]; // 其他默認(rèn)為系統(tǒng)語言
}

2、另一種情況,比如:每次使用 RDLocalizedString(<#key#>) 方法都要做引用 #import "RDLocalizableController.h" 好麻煩。
當(dāng)然你可以把 #import "RDLocalizableController.h" 放到 .pch 文件里,哦,順便提一下 .pch 文件會(huì)拖慢啟動(dòng)時(shí)間

3、還有一種情況,比如:就想使用 NSLocalizedString(<#key#>, <#comment#>) 方法,還可以解決以上兩種情況

還是有方法使用 NSLocalizedString(<#key#>, <#comment#>) 的。
使用 CategoryNSBundle 類擴(kuò)展一個(gè)設(shè)置語言的方法,并且使用 runtimeNSBundle 動(dòng)態(tài)添加一個(gè)關(guān)于 bundle 的屬性,重載 NSBundle.mainBundlelocalizedStringForKey 方法。目的就是將更改的字體傳給 NSLocalizedString(<#key#>, <#comment#>) 映射的 localizedStringForKey 方法返回的 bundle ,使得更改的字體應(yīng)用到系統(tǒng)上。

好吧,show you the code:

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

static const NSString *RDBundleKey = @"RDLanguageKey";

@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 (RDLanguage)

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

以上代碼是 NSBundleCategory
解釋一下哈:
1、objc_getAssociatedObjectobjc_setAssociatedObject 是一對(duì) getter、setter 方法,目的是為了給 NSBundle 類動(dòng)態(tài)添加一個(gè)屬性。
2、object_setClass:在 BundleEx 里實(shí)現(xiàn)一個(gè) localizedStringForKey 方法,然后將 BundleEx 這個(gè)類設(shè)置給 [NSBundle mainBundle] 。目的就是相當(dāng)于重載 [NSBundle mainBundle]localizedStringForKey 方法。

說明:
runtime 的具體用法和原理,由于在下才疏學(xué)淺就不多做講解了,免得誤人子弟。關(guān)于更多 runtime 的知識(shí)可以學(xué)習(xí) 一縷殤流化隱半邊冰霜
寫的 神經(jīng)病院Objective-C Runtime入院
系列文章。

再說本篇文章,該類別新增方法的使用:
RDLocalizableController 類的 + (void)setUserlanguage:(NSString *)language 方法里,本地化存儲(chǔ)語言之后,發(fā)送通知之前調(diào)用如下方法:

[NSBundle setLanguage:language];

之后,關(guān)于 RDLocalizableController 類里邊關(guān)于 bundle 的操作就可以舍棄了。

注意:使用這種方法要確保你的 .strings 的文件名為 Localizable.strings
否則還是要重新設(shè)置宏定義:

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

這樣的話該宏定義會(huì)有一個(gè)警告,畢竟系統(tǒng)已經(jīng)定義過了的,而且你還要到處重定義。。。又犯了上面第二種情況的尷尬。

到這里,該篇博客就結(jié)尾了,希望能幫助到各位一二
祝大家生活愉快,勤勉Coding

2018.03.14 更新

這是16年的文章,現(xiàn)在我又更新了一篇關(guān)于文章「iOS應(yīng)用程序語言本地化及應(yīng)用內(nèi)語言設(shè)置」的文章,整理并添加了一些新的內(nèi)容,以及開源了「GHLLocalizable」這款應(yīng)用內(nèi)語言設(shè)置的工具類,歡迎使用:

GHLLocalizable:iOS及應(yīng)用內(nèi)語言設(shè)置:http://gonghonglou.com/2018/03/14/GHLLocalizable/

GHLLocalizable GitHub 地址:https://github.com/gonghonglou/GHLLocalizable

后記

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,616評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,112評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,355評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,727評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,928評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,165評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評(píng)論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,585評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,892評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容