なるようになるかも

力は多くの場合、その人の思いを超えない。

アプリの言語設定を、アプリ内から操作したい

通常、アプリの言語設定はiOS本体の設定が反映されるようになっており、アプリから制御することはできません。

「OSの言語設定を日本語のままにしつつ、アプリ内の言語設定を英語表示にしたい」というようなニーズがあるのかどうか知りませんが、現状のiOSではできないのです。(なお、Androidでは余裕で可能)

アプリが参照する言語設定について

Launch Arguments & Environment Variables : NSHipster

という記事によれば、アプリの言語設定は アプリケーションの引数 として与えられて、NSUserDefaultへ格納され、その値が参照されるとのこと。

より厳密に言えば、NSUserDefaultNSArgumentDomainに格納されるみたいです。

UserDefaultのドメインの概念についてはClassReferenceに書いてあるのだけど、通常アプリがNSUserDefaultに書き込んだ場合、/Library/Preferences/com.example.Application.plistというファイルに書き込まれます。 (アプリケーションのBundleIDがcom.example.Applicationの場合)

一方で、アプリケーションの引数として与えられた言語や環境設定はNSArgumentDomainに格納されます。その実体は/Library/Preferences/.GlobalPreferences.plistであり、値の読み込み/書き込みに関する優先順位は最も高くなっています。

アプリの言語設定はAppleLanguagesというキーで格納されています。中身は@[@"en", @"ja", @"fr"]といったような、言語コードの文字列の配列です。アプリは自身が対応する言語と、このAppleLanguages配列の要素を、先頭から順に合致するものがないか走査し、一致したらその言語でアプリを表示する…という仕組みになっているのだと考えられます。

となれば、AppleLanguages配列を書き換えてしまえば、アプリ自身によって言語設定を操作できるのではないか?と考えたわけです。

結論としては無理だったんだけど、一応失敗した方法を載せてみます。

アプリ言語設定上書き手

この辺は趣味なんだけど、まずアプリ内言語設定用の列挙体を用意して、

/**
 言語設定用の列挙体
*/
typedef NS_ENUM(NSUInteger, LaunageType){
    kLanguageEnglish  = 1000,
    kLaungageJapanese = 1001,
};

言語切替用UIButtontagに、対応する列挙体の値を設定しました。

f:id:quesera2:20131111002507p:plain

あとはsenderから受け取ったtagの値に応じて、.GlobalPreferences.plistを書き換えるメソッドを作成するだけ。

/**
 アプリの言語設定を切り替えます。

 @param sender  言語設定切り替えボタン。
        tagに対象言語を示す値(LanguageType列挙体の値)を設定する。
 */
- (IBAction)changeLanguage:(UIButton *)sender
{
    NSString *targetLanguage = nil;
    switch ((LaunageType)sender.tag) {
        case kLanguageEnglish:
            targetLanguage = @"en";
            break;
        case kLaungageJapanese:
        default:
            targetLanguage = @"ja";
            break;
    }

    NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
    NSMutableArray *itsArray = [[userDefault valueForKey:@"AppleLanguages"] mutableCopy];
    NSUInteger position = [itsArray indexOfObject:targetLanguage];
    if(position == NSNotFound){
        return;
    }

    id targetElement = [itsArray objectAtIndex:position];
    [itsArray removeObjectAtIndex:position];
    [itsArray insertObject:targetElement atIndex:0];

    [userDefault setObject:[itsArray copy] forKey:@"AppleLanguages"];
    [userDefault synchronize];

    UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *viewController = [storyBoard instantiateViewControllerWithIdentifier:@"HogeViewController"];
    [self.navigationController setViewControllers:@[viewController] animated:NO];
}
  1. sendertagから選択された言語設定を判定し、言語コードの文字列にする。
  2. UserDefaultAppleLanguagesの配列を取得する。この値はアプリをインストールした時点でOSが自動的に.GlobalPreferences.plistに格納している。
  3. 並び替えのためにmutableCopyする。
  4. 目的の言語が先頭に来るように並び替えを行う(この辺もうちょい綺麗に書けるはず)。
  5. AppleLanguagesを再設定する。先述した通り、NSArgumentDomainの優先順位は最も高いため、.GlobalPreferences.plistの値が上書きされる。
  6. 何らかの方法で新しい言語設定の値で再描画する

残念ながら、6.の部分を実現する方法がありませんでした。

上のコードではUIViewControllerを再生成してloadViewが呼び出されるようにすれば、新しい言語設定が反映されるのでは?と考えて書いたのですが、どうも.GlobalPreferences.plistはアプリ起動中にはインメモリに保持されているみたいで、アプリ動作中に動的に変更するというのは不可能みたいでした。

一応、アプリをタスクからkillして、再起動させれば言語設定を反映させることができるのですが、タスクkillという操作をユーザーに行わせるというのは負けた気分…(そもそも申請通るのか謎ですけど)。

同じようなことを考えている人は世の中いるみたいで、stackOverflowを漁ってみましたが、どうも無理みたいですね。

現状での正答

もしOSの言語設定と、アプリの言語設定を別のものにしたい場合。

アプリ側で独自に言語設定を保持しておき、localizedStringForKey:value:table:を利用して、対応する stringsテーブルからローカライズ文字列を取り出すようにする、というのが現状の正答かなと思います。

ただしこの方法には、アプリ外が出す文字列までは制御できない(例えばStore Kitが出すアラートダイアログなど)という致命的な欠陥があります。