今まで国際化は気をつけながらやってきたので、ある程度ノウハウはあったと思っていましたが、String Localization · objc.ioを読んだらちらほら知らないこともあったりしたので、これまでのノウハウ + それを参考に最近、今開発中のPlayer!に実際に適用しつつ改善したことについてまとめます。
特に前半は、iOS以外の他のプラットフォームでも共通のことでけっこう有用だと思います。
ちょくちょく参考リンクも貼っているので、この記事 + それらだけ見ればiOSアプリの国際化については網羅出来るくらいの作りにしてますヽ(・ω・`)
「絶対に日本語版以外出さない」というアプリ以外は、初めから国際化対応しておくべき
「国際化対応しておく」というのは英語対応をする、という意味では無く、文言をコードに直書きせずに国際化リソースとして分離しておく、という意味です。
開発リソースの余裕が無かったり、英語対応したところでユーザーも少ないということで、初めは日本語のみでリリースすることが多く、それは全く問題無いと思っています。
初めは1カ国語のみの対応でも、ちゃんと国際化リソースとして切り出しておくことで以下のメリットがあります。
- あとから国際化対応するのが楽
- プロジェクト設定弄ったり、複製された言語ファイルの中身だけ書き換えればOK
- もし直書きだと、けっこうな量のソースの変更が生じる
- 文言一覧として分離されてメンテナンスしやすい
- 今開発中のPlayer!では、日英ともPRで文言変更してもらって僕が確認しつつマージするフローを取っています
言語リソースは、冗長気味に定義する
例えば、以下の2通りの文言を表示をしたいとして、
- "Paul invited you"
- "You invited Paul"
NG
こう定義したものを使い回すと、多言語対応の際に破綻することがあります。
"%@ invited %@"
OK
"%@ invited you"
"You invited %@"
原則、1種類の文言に対して、フルセンテンスで定義しておきましょう。
細かく分割して冗長性を減らして、それらを連結するようなアプローチは1言語では出来ても、多言語対応時に破綻します。
また、偶然同じ表現になるものも、文脈が異なるのであれば別定義にしましょう。
例えば、「走行」という意味と「走る」という意味で、ともに「Run」という表現にする場合、以下のように2種類の言語リソースとして定義しておくべきです。
# (キー名の命名は適当)
"RunNoun" = "Run";
"RunVerb" = "Run";
今の例では、2種類でしたが、名詞か動詞かという区別だけでなく、配置場所的に文脈が異なる場合なども、さらに冗長に定義しておくと安心です。
文脈によって言語リソーステーブルを分割する
というわけで文脈によって区別できるように、以下のような定義をしたくなりますが、
# Localizable.strings
"HomeRunButton" = "Run"
"ProfileRunButton" = "Run";
言語リソーステーブルで分割するとベターです。
-
Localizable.strings
: テーブルファイル名無指定で呼べる -
任意のテーブル名.strings
: テーブルファイル名を指定して呼ぶ
Objective-Cではそれぞれ使用関数が違いましたが、Swiftでは以下のtableName引数の有無でハンドリングします。
func NSLocalizedString(key: String, tableName: String? = default, bundle: NSBundle = default, value: String = default, #comment: String) -> String
僕は、Localizable.strings
には、"Cancel" = "Cancel";
など、かなり汎用的な一部の文言のみ定義して、
他は後者を用いています。
先ほどの例の場合、こうなります。
# Home.strings
"RunButton" = "Run"
# Profile.strings
"RunButton" = "Run";
// 呼び出し
NSLocalizedString("RunButton", tableName: "Home", comment: "")
フォーマット文字列
挿入文字列の順番が言語によって変わるとき
このような文言にしたいときは、
- 日: 「A ほげほげ B」
- 英: 「B foo bar A」
このような順番指定付きにすれば良いです。
- 日:
"HogeHoge" = "%@ ほげほげ %@";
-
"%1$@ ほげほげ %2$@"
としても良いけど不要
-
- 英:
"HogeHoge" = "%2$@ foo bar %1$@";
// 呼び出し
String(format: NSLocalizedString("HogeHoge", tableName: "SomeTable", comment: ""), a, b)
数値フォーマットの国際化
上の例では、String(format:...)
関数を使いましたが、数値の入った文言は以下を使うのが良いです。
static func localizedStringWithFormat(format: String, _ arguments: CVarArgType...) -> String
例えば、日本では数値は三桁コンマ区切りですが、ピリオド区切りの国もあります。
そのあたりを良い感じに出力してくれます。
// 例
let num = 12345
println(String.localizedStringWithFormat("output: %d", num)) // -> "output: 12,345
より条件のこみ入った指定にしたい場合は、 NSNumberFormatter
を使いましょう。
# String.localizedStringWithFormatと同様に三桁区切りなどを国際化する例
# あくまで例で、この場合はString.localizedStringWithFormatを使う方が良いです
var formatter = NSNumberFormatter();
formatter.formatterBehavior = .Behavior10_4
formatter.numberStyle = .DecimalStyle
formatter.stringFromNumber(12345)!
これはダメな例です。全ての国でコンマ区切りになってしまいます。フォーマットを明示的に指定するのはこれに限らず避けましょう。
var formatter = NSNumberFormatter();
formatter.positiveFormat = "#,##0"
formatter.stringFromNumber(12345)!
複数形対応
国際化 - Xcode6で導入されたstringsdictファイルを使って賢い複数形多言語対応 - Qiita にて別記事にしました。
大文字化・小文字化
以下を使うのが正しいです。
- lowercaseStringWithLocale
- ×: lowercaseString
- uppercaseStringWithLocale
- ×: uppercaseString
- capitalizedStringWithLocale
- ×: capitalizedString
この使い分けで違和感の見た目になるケースはけっこう稀な気がしますが、対応は簡単なのでやるに越したことはないです。
例としては、
"i".uppercaseStringWithLocale(locale)
大半のlocaleの出力は、I
になりますが、tr_TR
(トルコ)だと正しくİ
というローカライズされた文字列が取得出来ます。
uppercaseString
を使った場合はもちろんI
出力になってしまいます。
このメソッドに与えるlocale
については「正しいlocaleを取得する」を参照してください。
数値のフォーマット
NSNumberFormatter
・NSDateFormatter
を使いこなしましょう。
このformatter
にはデフォルトではNSLocale.currentLocale()
がセットされているので、「正しいlocaleを取得する」を参照してlocale
を上書きしましょう。
let formatter = NSNumberFormatter()
formatter.locale = locale
日付のフォーマット
NSDateFormatterStyle - NSDateFormatter Class Referenceに書いてあるdateとtimeのstyleの組み合わせで良ければ、それで済ませましょう。
styleに無い表記パターンを指定したい場合は、localeに応じたカスタムフォーマットを指定しましょう。
(書式記号はUTS #35: Unicode Locale Data Markup Language参照)
NG:
let fomatter = NSDateFormatter()
formatter.dateFormat = "MMMd日" // 日本語の場合はこう指定など、苦しい感じに
let dateString = formatter.stringFromDate(NSDate()) // -> "6月28日"
print(dateString)
OK:
let fomatter = NSDateFormatter()
let format = NSDateFormatter.dateFormatFromTemplate("dMMM", options: 0, locale: locale)
formatter.dateFormat = format
let dateString = formatter.stringFromDate(NSDate()) // -> "6月28日"
NSDateFormatter.dateFormatFromTemplate
のtemplate引数に与えた書式記号を良い感じに並び替えつつlocaleに最適な形で出力してくれます。
Formatterのインスタンスをキャッシュ
Formatterの初期化はけっこうコスト高い上に、連続的に呼ばれがちな処理なので、使い回すようにしましょう。
Swift
だとこんな感じです。
extension DateFormatter {
// Swiftでは遅延初期化される
static let mediumFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .medium
return f
}()
}
print(DateFormatter.mediumFormatter.string(from: Date()))
String Localization · objc.ioの Cache Formatter Objects
には、端末のlocale設定が変わった時に追従するようにNSNotificationCenter
使って監視するコードも載っていますが、個人的にはそこまで気を遣わなくても良いかなーと感じちゃいます。
国際化文字列のデバッグTips
Appleリファレンス: Testing Your Internationalized App
NSDoubleLocalizedStrings
-NSDoubleLocalizedStrings YES
引数付きで起動すると…
何と、文字列が2つ繰り返されて約2倍の長さになってくれます。
これで翻訳後でも概ねUIが崩れないか予めチェック出来ます( ´・‿・`)
ローカライズ漏れを検出
同様に、-NSShowNonLocalizedStrings YES
オプションで、ローカライズ漏れ(そのキーに対する所望の言語の文字列リソースが見つからない)の文字列を大文字(CAPITAL CASE)で表示してくれます。
-NSShowNonLocalizableStrings YES
オプションで、似たようなデバッグ機能がありますが、どういう時に指定するのか今いち分からなかったです(´・ω・`)
The NSShowNonLocalizableStrings user default identifies strings that are not localizable. The strings are logged to the shell in upper case. This option occasionally generates some false positives but is still useful overall.
デバッグ実行時に言語を切り替え
これも起動オプションで指定できましたが、Xcode 6からGUIで行えるようになりました。
iOS - Xcode6で言語設定の切り替えが簡単になった - Qiitaで以前書きました。
- これ知っているとけっこう捗ります
- 僕はいつも日本語指定で起動して、英語の確認時はデバッグ実行をやめてホームアイコンから起動、というようにしていることが多いです
Playgroundでlocaleを変更する
本記事書いている時にやり方見つけて、XcodeのPlaygroundでlocaleを変更する方法 - Qiitaにて別記事にしました。
正しいlocaleを取得する
NSLocale.currentLocale()
で簡単に取れるでしょ?と思うかもしれませんが、アプリの文言の国際化対応においては少し気をつけるべきことがあります。
仮に、日英のみ対応のアプリ(日本語設定以外だと英語UIになる)だとします。
そこでフランスのユーザーが使ったとすると、英語UIで使われるはずです。
そうであれば、フランスのlocaleではなく一貫して英語localeにするのが望ましい表示だと思います。
NSLocalizedString
などを使う分にはアプリが正しく設定されていれば、英語localeで表示してくれますが、以下の関数など、locale
を明示的に指定する必要のあるものを使う場合、NSLocale.currentLocale()
で取得したlocale
を使うと、今の例だとその部分だけフランス語表示になってちぐはぐなUIになってしまいます。
String.lowercaseStringWithLocale(locale)
また、NSDateFormatter
を使う場合もデフォルトではcurrentLocaleが設定されてしまっているので、正しいlocaleを明示的に設定する必要があります。
let formatter = NSDateFormatter()
formatter.locale = locale // 正しいlocaleを明示的に設定する必要
もちろん、アプリの表示言語じゃなくて実際のlocaleを設定したい場合、NSLocale.currentLocale()
でOKです。
多言語対応用の文言を設定する際の正しいlocaleの取得法
これで、上の例だと英語localeになります。
let localeIdentifier = NSBundle.mainBundle().preferredLocalizations.first! as! String
let locale = NSLocale(localeIdentifier: localeIdentifier)
アプリの言語設定は、iPhoneの設定アプリの言語設定の「使用する言語の優先順序」に従ってアプリが対応している言語とマッチされたものが選択され、それと同じロジックをコードで再現しています。
NSLocalizedString
を呼びやすくするTips
questbeat/Linを使うと、補完してくれて便利です。
Xcode 6・Swiftの組み合わせでも動きます。
key名を自動補完で選ぶと、それに適合するtableNameも自動補完してくれるなど、かなり気が利いています。
ただ、Swiftの場合、NSLocalizedString
を呼ぶと、bundle
やvalue
のオプション引数まで自動入力されてしまって煩わしいので、さらにスニペットを併用するのがオススメです。
~/Library/Developer/Xcode/UserData/CodeSnippets/
に https://gist.github.com/mono0926/9d9a9b6560860a53280c を置く(Xcode上で設定しても良いです)と、localized
と打つと良い感じに入力され、さらにLin
が補完してくれるというかなり快適な書き心地になります(´-ω-`)
XLIFFについて
長くなったので、別記事にしました:
Xcode 6で導入されたXLIFFによる国際化フローの使いドコロ - Qiita
個人的な意見
追記していくかもしれませんが、とりあえず1つだけ。
StoryboardやXibファイルの国際化の是非
StoryboardやXibファイルの国際化ファイルも可能でXcode 5くらいでそれ以前より取り回しよくなりましたが、最近は一切使わないようにしています。
- ファイルが散らばり気味になってメンテナンスしづらい
- XLIFFにエクスポートすれば取り回ししやすくなるが(Xcode 6で導入されたXLIFFによる国際化フローの使いドコロ - Qiita)
- 結局動的にコードで上書きせざるを得ないところがあって、Storyboard上の文字列のうちどこが実際にUIに表示されるのかが分かりにくい
- 初めは固定文字列だったのに後から動的に変更したくなった場合に、コードで上書くように変更するのもちょっと面倒
とはいえ、実際のUIと文言が紐付いて分かりやすい、静的な文言はコードで書かなくて良いので楽、など利点もあると思います。
ただ、アプリの規模が大きくなってくると、段々デメリットが大きくなっていくと感じているので、僕はもう初めから使わないようにしてます。