iOS開發之進階篇(4)—— 語言本地化(國際化)

1. 準備工作

本文以中英文切換爲例. 因爲系統默認語言是英文, 所以我們需要添加中文到項目中.

打開PROJECT:

localization1.png

添加簡體中文:

localization2.png

需要注意的是, 這一步必須要選一個文件進行本地化, 不然語言添加不成功:

localization3.png

添加完成:

localization4.png

新建Strings文件:

Localizable1.png

Localizable2.png

取名Localizable.strings (系統默認名):

Localizable3.png

然後點擊新建的Localizable.strings, 右邊欄實現本地化:

Localizable4.png

Localizable5.png

點擊需要本地化的語言(這些語言只有添加到項目中才可見) :

Localizable6.png

完成後就看到文件有了下拉按鈕:

Localizable7.png

這個Localizable.strings文件的目的是針對不同語言提供不同的字符串, 後文中用到.

至此, 準備工作完成.

2. 字符串本地化

添加一個字符串:

string.png

分別在中英文的Localizable.strings中添加需要本地化的字符串:

strings_en.png

strings_cn.png

其中, "KK_String"是字符串的key, 系統根據這個key去找對應的本地化目標字符串.

實現字符串本地化:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.stringLabel.text = NSLocalizedString(@"KK_String", nil);
}

效果:

run.png

擴展1
在Xcode中也可切換語言:

change_language1.png

change_language2.png

注意
但是這樣可能會和通過手機(模擬器)中的設置去更改語言有衝突(優先級不同), 爲避免花時間討論這個非必要討論問題, 本文建議直接在設置裏切換語言.

擴展2
當我們使用key去找目標字符串的時候, 如果沒有找到, 那麼系統就會把key字符串當做value字符串返回. 因此, 我們也可以直接使用英文字符串當做key來使用, 這樣就可以省略Localizable.strings (English)裏內容了.

// 中文Localizable
"String" = "字符串";

// 直接用String當做key
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.stringLabel.text = NSLocalizedString(@"String", nil);
}

3. 圖片本地化

3.1 圖片名稱本地化

其實也就是字符串的本地化. 準備兩張圖片image_en.png和image_cn.png, 然後在中文Localizable裏本地化字符串, 然後使用圖片的時候先本地化圖片名.

"image_en" = "image_cn";

self.imageView.image = [UIImage imageNamed:NSLocalizedString(@"image_en", nil)];

3.2 圖片本地化

點擊需要本地化的圖片, 在右邊欄裏選擇需要本地化的語言:

imageView.png

image_localization2.png

self.imageView.image = [UIImage imageNamed:@"image"];

run_image.png

這裏順便提一下, App圖標也是圖片, 但是這個圖標沒有Localization的選項, 因此不能對圖標進行本地化 (至少我沒找到方法).

4. App名稱/系統權限提示框本地化

在我們的工程中, 有一個info.plist文件, 裏面包含了App名稱, 請求系統權限的提示文等. 這個info文件在程序運行前就已經被系統加載好了, 我們沒辦法使用代碼去修改這些內容, 也就無法使用Localizable.strings去本地化了.
我們可以新建一個infoPlist.strings文件, 作用是將info.plist裏的字符串進行本地化.
和創建Localizable類似, 過程就不重複了.
分別在中英文的InfoPlist.strings裏面本地化App名稱/權限提示文:

en
// App名稱
"CFBundleDisplayName" = "KKLocalizeDemo";

// 權限提示文
"NSAppleMusicUsageDescription" = "App needs to get your media library permissions when using the play sound feature";
"NSCameraUsageDescription" = "App needs to get camera permissions when using the Scan QR code feature";
"NSMicrophoneUsageDescription" = "App needs to get microphone permission when using the intercom function";
"NSPhotoLibraryAddUsageDescription" = "App needs to get album permission when saving or browsing photos";
"NSPhotoLibraryUsageDescription" = "App needs your consent to access the album when saving or browsing photos";
"NSLocationAlwaysUsageDescription" = "We will use your location to search for nearby WiFi hotspots.";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "We will use your location to search for nearby WiFi hotspots.";
"NSLocationWhenInUseUsageDescription" = "We will use your location to search for nearby WiFi hotspots.";
cn
// App名稱
"CFBundleDisplayName" = "KK本地化";

// 權限提示文
"NSAppleMusicUsageDescription" = "App在使用播放聲音功能時,需要獲取媒體庫權限";
"NSCameraUsageDescription" = "App在使用掃描二維碼功能時,需要獲取相機權限";
"NSMicrophoneUsageDescription" = "App在使用語音對講功能時,需要獲取麥克風權限";
"NSPhotoLibraryAddUsageDescription" = "App在保存或者瀏覽照片/視頻時,需要獲取相冊權限";
"NSPhotoLibraryUsageDescription" = "App在保存或者瀏覽照片/視頻時,需要您的同意才能訪問相冊";
"NSLocationAlwaysUsageDescription" = "我們將使用您的位置搜索附近的WiFi熱點。";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "我們將使用您的位置搜索附近的WiFi熱點。";
"NSLocationWhenInUseUsageDescription" = "我們將使用您的位置搜索附近的WiFi熱點。";

對了, 還需要在info.plist裏面添加以下鍵值對:

    <key>CFBundleDisplayName</key>
	<string>$(PRODUCT_NAME)</string>

    <key>NSAppleMusicUsageDescription</key>
    <string>App needs to get your media library permissions when using the play sound feature</string>
    <key>NSCameraUsageDescription</key>
    <string>App needs to get camera permissions when using the Scan QR code feature</string>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>We will use your location to search for nearby WiFi hotspots.</string>
    <key>NSLocationAlwaysUsageDescription</key>
    <string>We will use your location to search for nearby WiFi hotspots.</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>We will use your location to search for nearby WiFi hotspots.</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>App needs to get microphone permission when using the intercom function</string>
    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>App needs to get album permission when saving or browsing photos/videos</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>App needs your consent to access the album when saving or browsing photos/videos</string>

App名稱:

run_name.png

請求相冊權限:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.stringLabel.text = NSLocalizedString(@"String", nil);
    self.imageView.image = [UIImage imageNamed:@"image"];
    
    // 檢查相冊權限
    [self checkAlbumAuthorizationWithBlock:nil];
}


// 檢查相冊權限
- (void)checkAlbumAuthorizationWithBlock:(void (^ _Nullable ) (BOOL authorized))block {
    
    // 判斷授權狀態
    PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
    
    if (status == PHAuthorizationStatusNotDetermined) {         // 用戶還沒有做出選擇
        
        // 彈框請求用戶授權
        [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
            if (status == PHAuthorizationStatusAuthorized) {    // 用戶第一次同意了訪問相冊權限
                if (block) block(YES);
            } else {                                            // 用戶第一次拒絕了訪問相機權限
                if (block) block(NO);
            }
        }];
        
    } else if (status == PHAuthorizationStatusAuthorized) {     // 用戶允許當前應用訪問相冊
        
        if (block) block(YES);
        
    } else  {                                                   // 用戶拒絕當前應用訪問相冊
        
        if (block) block(NO);
        
    }
}

run_photo.png

  1. storyboard/xib本地化

點擊Main.storyboard, 進行Localize…

storyboard1.png

然後點擊那個Label, 右邊欄找到Object ID:

storyboard2.png

然後在Main.string裏進行本地化:

storyboard3.png

這裏Object ID對應的是Label, 所以需要添加.text

效果和使用NSLocalizedString一致:

run_image.png

6. 多人開發中本地化

目前爲止, 我們所有例子的工程都是共用系統默認的Localizable.strings來進行本地化, 這在多人開發中很可能造成衝突. 因此, 我們需要設計屬於自己的本地化, 也就是將Localizable.strings改成自定義的. 方法是使用NSLocalizedStringFromTable取代NSLocalizedString.

NSLocalizedString和NSLocalizedStringFromTable是這樣define的:

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

從這裏我們推測出兩個信息:

  1. 本地化字符串的過程實際上是從資源文件(NSBundle)中加載不同的字符串. Localizable.strings是放在mainBundle目錄裏面的, 系統通過判斷App語言加載對應的strings文件, 然後通過key查找對應的字符串;
  2. table參數傳nil, 系統則默認加載名爲Localizable.strings的文件, 而如果改成我們自定義的名字, 則加載名字對應的文件.

new一個MyLocalizable.strings文件:

MyLocalizable.png

使用NSLocalizedStringFromTable:

self.stringLabel.text = NSLocalizedStringFromTable(@"String", @"MyLocalizable", nil);
// 也可做成宏
#define KKLocalizedTring(key)   NSLocalizedStringFromTable((key), @"MyLocalizable", nil)
// 使用自定義宏
self.stringLabel.text = KKLocalizedTring(@"String");

結果:

run_MyLocalizable.png

7. 應用內切換語言

到目前爲止, 我們所做的語言本地化都是跟隨系統語言的. 接下來我們討論如何在App中切換語言.

點擊查看Localizable.strings文件在Folder中展示:

showInFolder.png

我們看到不同語言的文件夾:

folder.png

每個文件夾對應一種語言(有多少種語言就有多少個文件夾), 裏面裝有該語言的所有本地化strings文件:

lproj.png

這些文件夾其實就是資源文件, 後綴名是lproj, 用於裝載對應語言的本地化strings文件. 這些資源文件是通過相應Bundle去加載的, Bundle有mainBundle和其他Bundle.

Bundle是一個捆綁包, 對應一個資源目錄, 其中包含了程序會使用到的資源. 這些資源可以是圖像, 聲音, 編譯好的代碼, nib文件等等(包括我們剛剛提到的.lproj文件).

回想我們之前使用的NSLocalizedString和NSLocalizedStringFromTable, Bundle使用的是mainBundle.

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

當我們的App剛啓動加載時, 系統會將系統語言所對應的.Iproj文件打包到mainBundle裏. 比如說, 當前系統語言是英文, 則系統會將en.Iproj文件打包進mainBundle; 而如果是簡體中文, 那麼就將zh-Hans.Iproj文件打包進mainBundle.
所以, 當我們使用mainBundle調用localizedStringForKey:value:table:方法, 得到的是系統指定好的本地化結果.

那麼, 如何在App內切換語言? 這個問題可以進一步分析成: 如何自主選用指定.Iproj文件(裏面包含的本地化strings文件), 來進行本地化呢?

我們反其道而行之, 將指定.Iproj資源文件生成一個path目錄, 然後使用這個path去生成一個Bundle, 然後使用這個Bundle去調用localizedStringForKey:value:table:方法, 得到的就是我們自主指定好的本地化結果啦! 🍺🍺🍺
當然還有一點, 如何找到指定.Iproj資源呢? 我們可以記錄用戶選擇的語言, 將所選語言與.Iproj資源文件名相對應, 然後將.Iproj資源文件名保存到本地. 最後在需要本地化的地方取出.Iproj資源文件名–>生成path–>生成Bundle–>調用localizedStringForKey:value:table:.

整個過程行雲流水有沒有?! 😝😝😝
當然, 如果看起來覺得頭暈, 那就讓代碼治療一切吧.

// 按鈕事件
- (IBAction)btnAction:(UIButton *)sender {
    
    __weak typeof(self) weakSelf = self;
    
    UIAlertController *alertC = [UIAlertController alertControllerWithTitle:@"設置語言" message:nil preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *systemAction = [UIAlertAction actionWithTitle:@"跟隨系統" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        [weakSelf setLanguage:KKLanguageType_System];
    }];
    UIAlertAction *enAction = [UIAlertAction actionWithTitle:@"英文" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        [weakSelf setLanguage:KKLanguageType_en];
    }];
    UIAlertAction *cnAction = [UIAlertAction actionWithTitle:@"中文" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        [weakSelf setLanguage:KKLanguageType_cn];
    }];
    [alertC addAction:systemAction];
    [alertC addAction:enAction];
    [alertC addAction:cnAction];
    [self presentViewController:alertC animated:YES completion:nil];
}

// 切換語言
- (void)setLanguage:(KKLanguageType)type {
    
    // 記錄當前語言到本地
    if (type == KKLanguageType_en) {
        [[NSUserDefaults standardUserDefaults] setObject:@"en" forKey:@"appLanguage"];
    }else if (type == KKLanguageType_cn) {
        [[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:@"appLanguage"];
    }else {
        [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"appLanguage"];
    }
    [[NSUserDefaults standardUserDefaults] synchronize];

    // 刷新UI
    dispatch_async(dispatch_get_main_queue(), ^{
        
        // 刷新按鈕
        if (type == KKLanguageType_en) {
            [self.btn setTitle:@"英文" forState:UIControlStateNormal];
        }else if (type == KKLanguageType_cn) {
            [self.btn setTitle:@"中文" forState:UIControlStateNormal];
        }else {
            [self.btn setTitle:@"跟隨系統" forState:UIControlStateNormal];
        }
        
        // 本地化Label
        NSString *language = [[NSUserDefaults standardUserDefaults] objectForKey:@"appLanguage"];
        NSString *path = [[NSBundle mainBundle] pathForResource:language ofType:@"lproj"];
        NSBundle *bundle = language ? [NSBundle bundleWithPath:path] : NSBundle.mainBundle;
        self.stringLabel.text = [bundle localizedStringForKey:@"String" value:nil table:@"Localizable"];
        
        // 本地化圖片
        self.imageView.image = [UIImage imageNamed:[bundle localizedStringForKey:@"image_en" value:nil table:@"Localizable"]];
    });
}

宏定義那一堆本地化代碼, 然後看起來就清爽了:

#define KKLocalized(key) \
    [([[NSUserDefaults standardUserDefaults] objectForKey:@"appLanguage"] ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:[[NSUserDefaults standardUserDefaults] objectForKey:@"appLanguage"] ofType:@"lproj"]] : NSBundle.mainBundle) localizedStringForKey:(key) value:nil table:@"Localizable"]

// 本地化Label
self.stringLabel.text = KKLocalized(@"String");
        
// 本地化圖片
self.imageView.image = [UIImage imageNamed:KKLocalized(@"image_en")];

本例中

  1. 跟隨系統語言是向本地的key"appLanguage"對應的值置nil, 然後取出的時候發現是nil就用mainBundle去調用方法.
  2. key"appLanguage"是自由定義的, 主要用於保存對應名稱到本地.
  3. table使用Localizable是整個項目共用的, 如果多人開發需自己定義(如本例的MyLocalizable.strings).

效果圖:

run_language.gif

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章