【Unity】[iOS]Xcode projectの Localization 自動化

iPhone

多言語に Localization 対応したXcodeプロジェクトファイルをUnityでできるだけ自動生成するための手順についてご紹介します。

多言語対応は.StringファイルやLocalizationの設定などXcodeに多くの設定が必要となり、全てを手動でやっていると対応が漏れてしまうリスクがあります。

以前の記事でUnityのiOSアプリの Localization についてまとめましたが、Unityが生成するXcodeのプロジェクトファイルに自動でLocalization情報を組み込むことができず、XcodeのConfigや必要なファイルを手動で設定・追加する方法にとどめていました。

今回は自動化できるところは自動化し、Xcodeでの操作をなるべく少なくするための対応について説明します。




開発環境

開発環境をまとめておきます。

  • Unity 2019.4.18f1
  • Xcode 12.4
  • iOS 14.4

今回、UnityEditor.iOS.XcodeというUnity標準のXcode EditorツールとXCodeEditor for Unity というアセットの両方を試しました。

XcodeEditor for Unityはあまりメンテナンスされていないようで、プロジェクトの保存でプロジェクトがXcode 12.4では読み込めないという問題にぶち当たりました。

XcodeEditor for Unityの方が直感的に使いやすくてこちらを使いたかったのですが断念した経緯があります。

一方、UnityEditor.iOS.XcodeはいまいちどのAPIをたたけばどうなるのか、詳しく説明しているサイトが少なく、情報をあまり入手できませんでした。。。

なので、今回はUnityEditor.iOS.Xcodeをわかる範囲使用して実装しています。

スクリプトを配置する

Buildで生成されたXcodeプロジェクトファイルに手を入れるためのスクリプトファイルを以下のようにAssets/Editorに配置します。

PostXcodeBuild.csの配置

Xcode projectを自動でLocalizationするスクリプト

今回、Xcodeのプロジェクトファイル、Info.plistと呼ばれる設定ファイル、言語依存の定義を行うInfoPlist.stringの3つのファイルを編集します。

Localizationとその他Info.plistに設定が必要な項目を設定できるようなスクリプトにしてあります。

以下がコードになります。長くて汚いコードで申し訳ないです。

using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using System.Collections;
using System.Collections.Generic;
#if UNITY_IOS
using UnityEditor.iOS.Xcode;
#endif

public class PostXcodeBuild
{
#if UNITY_IOS
    private struct InfoplistInfo
    {
        public string key;
        public string value;
        public InfoplistInfo(string str1, string str2)
        {
            key = str1;
            value = str2;
        }
    };

    private struct LocalizationInfo
    {
        public string lang;
        public bool isdefault;
        public InfoplistInfo[] infoplist;
        public LocalizationInfo(string langstr, bool flg, InfoplistInfo[] info )
        {
            lang = langstr;
            infoplist = info;
            isdefault = flg;
        }
    };
    private struct CommonInfoPlistInfo
    {
        public InfoplistInfo[] infoplist;
        public CommonInfoPlistInfo(InfoplistInfo[] info)
        {
            infoplist = info;
        }
    };

    private static LocalizationInfo[] localizationInfo = {
        new LocalizationInfo("en", true, new InfoplistInfo[]
        {new InfoplistInfo("CFBundleDisplayName",            "Camera Info"),
         new InfoplistInfo("NSUserTrackingUsageDescription", "Please set to Allow to avoid displaying inappropriate advertisements"),
         new InfoplistInfo("NSCameraUsageDescription",       "Please set to Allow to use camera for displaying preview and getting camera parameters")
        }),
        new LocalizationInfo("ja", false, new InfoplistInfo[]
        {new InfoplistInfo("CFBundleDisplayName",            "カメラパラメータ"),
         new InfoplistInfo("NSUserTrackingUsageDescription", "不適切な広告の表示を避けるために”トラッキングを許可”に設定してください"),
         new InfoplistInfo("NSCameraUsageDescription",       "カメラのプレビュー表示とパラメータ取得のためにカメラ使用を許可設定にしてください。")
        })
    };

    private static string[] skadnetworkitems = new string[] { "cstr6suwn9.skadnetwork"};


    static void createInfoPlistString(string pjdirpath, LocalizationInfo localizationinfo)
    {
        string dirpath = Path.Combine(pjdirpath, "Unity-iPhone Tests");

        if (!Directory.Exists(Path.Combine(dirpath, string.Format("{0}.lproj", localizationinfo.lang)))) {
            Directory.CreateDirectory(Path.Combine(dirpath, string.Format("{0}.lproj", localizationinfo.lang)));
        }
        string plistpath = Path.Combine(dirpath, string.Format("{0}.lproj/InfoPlist.strings", localizationinfo.lang));
        StreamWriter w = new StreamWriter(plistpath, false);
        foreach (InfoplistInfo info in localizationinfo.infoplist) {
            string convertedval = System.Text.Encoding.UTF8.GetString(
                System.Text.Encoding.Convert(
                    System.Text.Encoding.Unicode,
                    System.Text.Encoding.UTF8,
                    System.Text.Encoding.Unicode.GetBytes(info.value)
                    )
            );
            w.WriteLine(string.Format(info.key + " = \"{0}\";", convertedval));
        }
        w.Close();
    }

    static void addknownRegions(string pjdirpath, LocalizationInfo[] info)
    {
        string strtmp = "";
        string pjpath = PBXProject.GetPBXProjectPath(pjdirpath);

        foreach (LocalizationInfo infotmp in info) {
            strtmp += "\t\t" + infotmp.lang + ",\n";
        }
        strtmp += "\t\t);\n";

        StreamReader r = new StreamReader(pjpath);
        string prjstr = "";
        string linetmp = "";
        while (r.Peek() >= 0) {
            linetmp = r.ReadLine();
            if (linetmp.IndexOf("knownRegions") != -1) {
                prjstr += linetmp + "\n";
                prjstr += strtmp;
                while (true) {
                    linetmp = r.ReadLine();
                    if (linetmp.IndexOf(");") != -1) {
                        break;
                    }
                }
            } else {
                prjstr += linetmp + "\n";
            }
        }
        r.Close();
        StreamWriter sw = new StreamWriter(pjpath, false);
        sw.Write(prjstr);
        sw.Close();
    }

    static void addLocalizationInfoPlist(string pjdirpath, LocalizationInfo[] info)
    {
        string plistPath = Path.Combine(pjdirpath, "Info.plist");
        PlistDocument plist = new PlistDocument();

        plist.ReadFromFile(plistPath);
        var array = plist.root.CreateArray("CFBundleLocalizations");
        foreach (LocalizationInfo infotmp in info) {
            array.AddString(infotmp.lang);
        }
        var rootDict = plist.root;
        foreach (LocalizationInfo infotmp in info) {
            if(infotmp.isdefault) {
                foreach (InfoplistInfo pinfo in infotmp.infoplist) {
                    string convertedval = System.Text.Encoding.UTF8.GetString(
                        System.Text.Encoding.Convert(
                            System.Text.Encoding.Unicode,
                            System.Text.Encoding.UTF8,
                            System.Text.Encoding.Unicode.GetBytes(pinfo.value)
                    ));
                    rootDict.SetString(pinfo.key, convertedval);
                }
            }
        }
        plist.WriteToFile(plistPath);
    }

    static void addSkAdNetworkItems(string pjdirpath, string[] skadvallist)
    {
        string plistPath = Path.Combine(pjdirpath, "Info.plist");
        PlistDocument plist = new PlistDocument();
        plist.ReadFromFile(plistPath);

        if (skadvallist != null) {
            var array = plist.root.CreateArray("SKAdNetworkItems");
            foreach (string value in skadvallist) {
                PlistElementDict dict = array.AddDict();
                dict.SetString("SKAdNetworkIdentifier", value);
            }
        }
        plist.WriteToFile(plistPath);
    }

    [PostProcessBuild]
    public static void SetXcodePlist(BuildTarget buildTarget, string pathToBuiltProject)
    {
        if (buildTarget != BuildTarget.iOS) {
            return;
        }

        foreach (LocalizationInfo entry in localizationInfo) {
            /* add infoplist.string */
            createInfoPlistString(pathToBuiltProject, entry);
        }

        /* add knownregions to project */
        addknownRegions(pathToBuiltProject, localizationInfo);

        /* add localization to infoplist */
        addLocalizationInfoPlist(pathToBuiltProject, localizationInfo);

        /* add addSkAdNetworkItems */
        addSkAdNetworkItems(pathToBuiltProject, skadnetworkitems);
    }
#endif
}

スクリプトの解説

SetXcodePlist

SetXcodePlistはビルド後に実行されるスクリプトです。buildTargetがiOS以外の場合は動かしたくないので、最初のif文ではじいています。iOSの場合は以下が実行されます。

各設定

localizationInfoはローカライズを定義した配列です。メンバのinfoplistにはローカライズしたいキーと値を複数設定できます。

この値はInfoPlist.stringに設定されます。また、デフォルトがtrueのものはInfo.plistにも設定されます。

commonInfoPlistArrayとcommonInfoPlistはInfo.plistに設定したいkeyと値になります。配列で設定が必要なもの(SKAdNetworkItemsなど)はcommonInfoPlistArrayで定義します。

createInfoPlistString

InfoPlist.stringを生成します。localizationInfoの配列数だけ設定します。

addknownRegions

プロジェクトファイル内のknownRegionsに言語を設定します。

なお、UnityEditor.iOS.Xcode経由での設定方法がわからなかったので、強引にファイルを書き換えて追加しています。

ここの記述方法を知っている方がいれば是非教えてほしいです。

addLocalizationInfoPlist

Info.plistにLocalizationに関する情報を書き込みます。デフォルト言語に設定してある情報を書き込みます。

addSkAdNetworkItems

Info.plistにskadnetworkitemsを書き込みます。

生成されるXcodeプロジェクトの確認

実行してXcodeプロジェクトを起動すると、以下のように情報が書き込まれていることが確認できます。

Unity-iPhone PROJECT 設定

0 Filesとなっているのは実はファイルの関連付けがまだできていないためです。これも自動化したかったのですが、まだ調査中ですので、判明し次第記事更新します。

Info.plistに設定したパラメータも反映されています。

Info.plist

一方、InfoPlist.stringはまだ追加されていないことになっています。言語Japaneseのチェック欄はあるのですが、チェックが付いていません。

InfoPlist.string

チェックを付けるとファイルを追加するか問われるのでUse fileを選択して追加します。

ファイル追加ダイアログ
ファイルツリー

チェックを付けると、フォルダツリーにも複数のInfoPlist.stringが表示され、言語に応じた値が書き込まれていることが確認できます。

おわりに

非常にスマートではないスクリプトですが、ないより遥かにマシで、作業が楽になりました。手動でチェックが必要な個所はもう少しアセットを調べて全自動でいけるように改善していこうと思います。

コメント

  1. […] […]

  2. […] […]

  3. […] 【Unity】[iOS]Xcode projectの Localization 自動化 _ hirokuma.blog (特にPBXProjectファイルの書き換えについて非常に参考になりました) […]

  4. […] 【Unity】[iOS]Xcode projectの Localization 自動化 […]

  5. より:

    同じ問題に直面し、参考にさせていただきました。ありがとうございます。
    ソースを載せることは出来ませんが、Unity上でのInfoPlist.stringの関連付けやTarget Membershipへの追加自動化は下記アセットを追加することで実装できました。
    まだお困りのようでしたら、参考にしていただければ幸いです。

    https://github.com/superbderrick/UnityiOSLocalization