原文 : 與佳期的個(gè)人博客(gonghonglou.com)
Xcode 新建一個(gè)工程的默認(rèn)語言是英文,所以你在 app 里粘貼復(fù)制都是顯示的 copy
paste
,你可以通過在 info.plst
文件里選擇 Localization native development region
來設(shè)置不同語言。
可是如果你想在軟件內(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
是繁體中文。
新建 .strings 配置文件
1、Command + N
新建 Strings File
文件,命令為 RDLocalizable
,會(huì)生成一份 RDLocalizable.strings
文件。
2、選中RDLocalizable.strings
文件,如下圖操作,點(diǎn)擊 Localize...
按鈕,左側(cè)彈框中選擇語言。
3、之后右側(cè)會(huì)如下圖顯示,勾選上你想要的語言即可(Base
無用)
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#>)
的。
使用 Category
為 NSBundle
類擴(kuò)展一個(gè)設(shè)置語言的方法,并且使用 runtime
為 NSBundle
動(dòng)態(tài)添加一個(gè)關(guān)于 bundle
的屬性,重載 NSBundle.mainBundle
的 localizedStringForKey
方法。目的就是將更改的字體傳給 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
以上代碼是 NSBundle
的 Category
。
解釋一下哈:
1、objc_getAssociatedObject
和 objc_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
后記
小白出手,請(qǐng)多指教。如言有誤,還望斧正!
轉(zhuǎn)載請(qǐng)保留原文地址http://gonghonglou.com/2016/10/29/set-language