iOS國際化(多語言)漫談

高家二少爺發表於2019-02-14

目錄

概覽

各種資源的國際化

1.文字

2.圖片

3.nib

4.其他資源

特定模組/功能的國際化

1.APP圖示

2.應用名與許可權提示

3.啟動圖(LaunchScreen)

4.app調系統資源頁面的國際化

5.涉及服務端資料內容的國際化

app內更改語言

1.更改語言的方案

2.未做國際化的舊專案遷移


概覽

國際化的本質是為每種語言單獨提供一份資源(文字,圖片,音視訊等)。 本文術語 本地化:指單獨一種語言 國際化:多種語言的合體

在工程的Localizations中每新增一種語言,xcode會提示我們生成對應的檔案,而後也生成了對應的資料夾。

新增本地化語言

新增中文簡體後生成的資料夾與檔案
兩種語言

iOS為這些檔案提供了快捷的國際化方案。對於字串資原始檔生成相應語言的字串檔案放在對應的資料夾中,而XIB和StoryBoard則可選整個檔案和字串資源。具體的方案後續討論。

image.png

如果忘了新增某個資源的具體語言檔案,或者後續增加的資原始檔,可以通過該資原始檔的 檔案監察器File Inspector 中的 Localize按鈕新增。

image.png

Localizable.stringsInfoPlist.strings在國際化方案中是常見的。

  • Localizable.strings 這個是讀取多語言字串方法NSLocalizedString預設會載入的檔案,如果自定了這個檔名字,則使用NSLocalizedStringFromTable指定table即可
  • InfoPlist.strings info.plist的字串國際化檔案,系統預設讀取,名字固定

各種資源的國際化

1. 文字

新增了多語言的字串資原始檔處於可展開狀態,子級有著相應語言的副本。我們把相應語言的文字放在副本里面就行了。

字串檔案中具體的格式是"key" = "value";筆者發現寫成key = "value";也是不會有問題的(但是不加雙引號不能有空格,會識別不了),比如應用名稱的本地化:

應用名稱的國際化
看截圖,最終的應用名稱是後面那一個,說明有效且 被覆蓋了

使用NSLocalizedString(key, comment)來讀取字串。第二個引數comment可以是nil,可以是一段為空的字串,也可以是對key的註釋。

看一下這個方法的實現

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

...
/* Method for retrieving localized strings. */
- (NSString *)localizedStringForKey:(NSString *)key value:(nullable NSString *)value table:(nullable NSString *)tableName NS_FORMAT_ARGUMENT(1);
複製程式碼

localizedStringForKey:value:table:是NSBundle的物件方法,由此可見,可以載入不同的包名和字串資源表的字串。也提供了相關巨集

#define NSLocalizedStringFromTable(key, tbl, comment) \
	    [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
	    [bundle localizedStringForKey:(key) value:@"" table:(tbl)]
複製程式碼

重要:當找不到相應的語言strings或value時會直接返回key,如果你用英文的內容作為Key,甚至都可以不用維護英文字地化。

2. 圖片

Xcode5之後圖片資產(Assets.xcassets)不再支援國際化了,單張圖片資源的方式仍然可用,使用方式同字串。將需要國際化的圖片拖入工程,選擇檔案監察器,點選Localize並選擇多個語言後即可生成如如字串資源一樣的可展開狀態了。要配置不同語言的圖片前往該語言目錄替換即可。

圖片國際化

這個系統提供的方案支援Interface Builder版的nib資源國際化(當然Localizable Strings 方式很顯然只是字串而已),也支援+ imageNamed:載入方式的國際化。

還有個只適合用於純程式碼,不支援nib的方式。就是把圖片的名稱做字串國際化,然後再使用+ imageNamed:載入。

另外,針對+imageWithContentsOfFile:的載入方式,可以通過分類的方式,根據語言修改相應的載入路徑。

PS:圖片的國際化帶來的是多份的副本,如果國際化中需要做的具體本地化語言較多,必然造成包的急劇增大。所以建議能避免就避免。

3. nib檔案(XIB和StoryBoard)

StoryBoard本地化

nib檔案的國際化方式上面提到有兩種方式:

  • 只做字串資源(Localizable Strings)
  • 整個nib檔案(Interface Builder CocoaTouch XIB/StoryBoard)

nib檔案有一個大坑,畫重點

各個本地化的nib修改不會同步,nib的修改也不會同步至字串資源

也就是說第一種方案每增加一種語言就得再畫一個頁面,本地化語言多的話,額外工作量驚人。 第二種方案,也得自己將新增的字串拷貝出來。

如果要把更新同步的過程做成自動化,當然也是字串方便一點。用整個nib檔案做國際化比字串資源方式強的地方也就是以下兩點了

  • 圖片
  • 不同本地化不同佈局

一般也不會去根據不同本地化語言去修改佈局,阿拉伯國家也就改個文字方向,這個特性檢視自帶,全域性修改即可。

[UIView appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
複製程式碼

nib中的圖片資源用程式碼也可輕鬆解決,綜上筆者建議針對nib檔案只做字串資源(Localizable Strings)。

所以,上面的圖片方案選了支援nib的方案,然後現在不用了 ? 意不意外?驚不驚喜?

不說這個了,來說說怎麼做nib新增的需要國際化的字串同步吧。

Xcode為我們提供了ibtool工具來生成nib的strings檔案

ibtool FirstViewController.xib --generate-strings-file lanuchScreen.strings 
ibtool Main.storyboard --generate-strings-file storyBoard.strings
複製程式碼

但是ibtool生成的strings檔案是BaseStoryboard的strings(預設語言的strings),且會把我們原來的(甚至是翻譯好的)strings替換掉。還是自己用指令碼來做這個工作靠譜點,再借助Xcode 中 Run Script 來執行這段指令碼,更新的時候build一下就行了。 具體的指令碼程式碼在最後的Demo中,Run Script的新增方法: Target->Build Phases->New Run Script Phase,在shell裡面寫入下面指令

python ${SRCROOT}/${TARGET_NAME}/RunScript/AutoGenStrings.py ${SRCROOT}/${TARGET_NAME}
複製程式碼

4. 其他資源

其他資源國際化

其他資源(json、音視訊、壓縮包等等)的國際化方式與圖片資源相同,讀的時候讀 主Bundle 即可,不同語言環境下iOS自動切。

劇透:後續的應用內切換語言會利用主Bundle的這一特性。

5. 涉及服務端資料內容的國際化

這部分內容的國際化,可考慮以下兩種方案:

  • 服務端不關心當前使用者的本地化語言,返回所有適配的本地化內容,由客戶端自己控制顯示
  • 服務端獲取當前使用者的本地化資訊,返回相應的本地化內容

第一種適合適配本地化語言較少的情況,比如只適配中英文;而第二種,對配置資訊的依賴比較高,服務端需要修改的內容也是比較多的。

如果使用第一種方案,一些常用的報錯資訊或者其他業務成功等資訊可以整理成特定的code由客戶端直接做解析,減少資訊傳輸量(雖然相比單個本地化還是會大很多)。

如果使用第二種方案,可以在請求頭中帶入當前使用者的本地化資訊,服務端根據這個判斷,可以簡便得多。


特定模組/功能的國際化

1. APP圖示

除了動態圖示的方法, 暫時也查不到什麼動態修改圖示的方法了。這個方法多用於 APP的節日活動。 事實上也沒人去做這個的國際化,順帶提一下。

2. 應用名與許可權提示

應用名與許可權提示的國際化就是依賴info.plist的國際化。不同本地化檔案放不同的鍵值對即可。

應用名稱與相機許可權的簡體中文字地化

3. 啟動圖(LaunchScreen)

Xcode Overview 的 Adding Assets章節中有關於啟動圖的描述

Because the launch screen is shown before your app is running, you can only use a single root view of type UIView or UIViewController.

也就是說啟動介面的展示是發生在main函式入口之前,也就決定了我們無法動態地修改啟動圖。另外,以下nib的兩種國際化方式也是無效的。

nib的兩種國際化方式
另闢蹊徑,利用info.plist的國際化來做LaunchScreen的靜態國際化,如下:
LaunchScreen的國際化

這裡要吐槽一下,即使做了靜態國際化,以下兩種狀況是不會切換的

  • 系統切換語言的時候
  • 重啟系統

只有重新安裝app的時候才會切換 所以做啟動圖的國際化意義有限。

也看到有人說自己做一個LaunchScreenController作為啟動頁,但是這個狀況下,app啟動會黑屏一段時間,這不是想要的效果啊。

4. app調系統頁面的國際化

關於呼叫系統資源,相機,相簿,通訊錄之類,APP內修改語言暫沒找到重新整理的方法。如果你有方法,麻煩告知樓主,非常感謝。

只有在app重啟時,main函式中應用程式代理(AppDelegate)返回之前去設定偏好設定的 AppleLanguages才是有效的。

所以,目前的解決方案就是這些頁面全部自己實現。另外,導航按鈕的國際化文字也不是在 main bundle 中載入的,一般app也會自定義這個按鈕,這個不是痛點。

如有其它,歡迎補充。

app內更改語言

1.更改語言的方案

app的語言過年聚系統語言設定變化是最基本的國際化需求,更多的時候我們希望能夠做到app內部熱切換。

APP中的資源載入(Storyboard、圖片、字串)基本是在NSBundle.mainBundle()上操作的(自建私有庫或者是三方庫可能會自己做國際化,把國際化字串資源放在自己的bundle中,如MJRefresh),那麼我們只要在語言切換後把相應資源載入的bundle替換成當前語言的bundle就行了。如下替換為 字串資源載入的主要程式碼:

id value = language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"zh-Hans" ofType:@"lproj"]] : nil;
objc_setAssociatedObject([NSBundle mainBundle], &kBundleKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
……
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName {
    NSBundle *bundle = objc_getAssociatedObject(self, &kBundleKey);
    if (bundle) {
        return [bundle localizedStringForKey:key value:value table:tableName];
    } else {
        return [super localizedStringForKey:key value:value table:tableName];
    }
}
複製程式碼

相應的nib檔案中的圖片載入和UITextView中的文字也需要

如果,專案是純程式碼的,也就是說國際化涉及不到nib檔案,那就不需要這個實現方式了。自己存個當前語言標記,切換時換一個,做好判斷即可。

如果想要在第一次安裝的時候跟隨系統的語言,應用啟動後從NSUserDefault中可以讀到語言陣列,其中陣列的第一個元素即是主要語言(Primary Language),系統的當前語言。 這些語言字串當中的最後一部分是地區,會根據地區變化,後續根據這個列表判斷的時候需要注意。

 NSArray *array = [[NSUserDefaults standardUserDefaults] arrayForKey:@"AppleLanguages"];
複製程式碼

語言列表(地區為中國)
語言列表(地區為美國)

不論哪種方式,切換語言都是需要重新整理檢視的,這個無法避免。 那麼,如何優雅地重新整理UI

  • 微博的思路是,在切換語言時,傳送通知NSNotification,所有的UI控制元件監聽通知,然後在適當的時候重新整理UI。 那麼其實這麼寫,需要做的東西很多,或是通過Base類來實現,或是通過runtime實現,總之Button、Label、TextField等等都需要有一套統一的更新機制,可能不是一個最簡單的辦法。
  • 而微信切換的方案是,重新整理keyWindow的rootViewController,然後跳轉到設定頁。

這個思路有篇文章說的比較詳細,直接看:在iOS App內優雅的動態切換語言

2.未做國際化的舊專案遷移

老專案的國際化遷移也都會牽涉到以上提到的各種問題。但是以上的問題都不是主要的,主要的是那些散落在程式碼中的各種需要國際化的文字。一個一個去摳出來肯定不現實。 Xcode為我們提供了一個工具genstrings,這個工具與ibtool類似,也是匯出字串資原始檔的。只不過ibtool適用於nib檔案,而genstrings適用於原始碼檔案。支援C,Objective-C,swift(官方未明確指出,筆者嘗試通過),java等語言檔案,如下官方描述:

The genstrings tool can parse C, Objective-C, and Java code files with the .c.m, or .java filename extensions.

然而,還是有很多工作要做,看一下官方的來那個外一個描述:

If you wrote your code using the Core Foundation and Foundation macros, the simplest way to create your strings files is using the genstrings command-line tool. You can use this tool to generate a new set of strings files or update a set of existing files based on your source code.

也就是說,這個指令碼生效的前提是必須要使用NSLocalizedString系列巨集,一個一個去替換字串為這個巨集的讀取的這個工作還是得自己做的。不過,想想也是符合邏輯的,畢竟哪個字串要國際化還是得開發者自己確認。通過Find navigator自己做吧。

如何使用

//指定到en.lproj目錄下的Localizable.strings檔案,直接覆蓋
genstrings -o en.lproj *.swift
//指定到en.lproj目錄下的Localizable.strings檔案,追加內容
genstrings -a -o en.lproj *.swift
複製程式碼

其他引數可以使用man genstrings命令檢視,不再贅述。 這個命令列工具同樣有ibtool的詬病,全量輸出。所以,在使用的時候千萬小心別覆蓋了已經翻譯的內容。

看下效果:

效果

另外,這個命令一次只能解析一個檔案,簡單寫了一個遞迴指令碼:

#!/bin/bash
function getdir(){
for element in `ls $1`
do
dir_or_file=$1"/"$element
if [ -d $dir_or_file ]
then
getdir $dir_or_file
else
echo $dir_or_file
    suffix="${dir_or_file##*.}"
    if [ "$suffix"x = "swift"x ]||[ "$suffix"x = "m"x ]||[ "$suffix"x = "mm"x ];
    then
        genstrings -a -o en.lproj $dir_or_file
    fi
fi
done
}
root_dir="./"
getdir $root_dir
複製程式碼

iOS國際化至此結束,如有哪裡不清楚,歡迎檢視demo,或者留言。 最後附上:demo地址

參考文章: Internationalization and Localization Guide Using the genstrings Tool to Create Strings Files iOS國際化——通過指令碼使storyboard翻譯自增 iOS國際化 在iOS App內優雅的動態切換語言

相關文章