Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Localization of the app name (Android and iOS) and iOS permissions without the localization package

Discussion in 'Localization Tools' started by Francesco-FL, Nov 1, 2021.

  1. Francesco-FL

    Francesco-FL

    Joined:
    May 25, 2021
    Posts:
    176
    I opened a new thread because I couldn't find any forum on the internet showing how to properly get the localization of an app name (and iOS permissions) with Unity.

    Obviously, by using the Unity package (com.unity.localization) you have access to both this code (but without these changes) and all the other features concerning translation and localization.

    To have a relatively simple code that only modifies the name of the app based on the language of the device, under the advice of @karl_jones I extracted and modified the files in com.unity.localization\Editor\Platform


    Basically I copied the original scripts, I deleted everything related to the icons in the android scripts and deleted everything related to the iOS messages for the camera authorization request, etc... (I only added one that concerns the authorization for the IDFA)

    I also replaced everything related to "Locales" and "AppInfo" with another class which I called "AppInfo" (hopefully it doesn't get confusing).

    In the class I put as parameters:
    - "Localization" (the language code ("en", "it", "es", "fr"....))
    - "App_name" (the name of the app in that language)
    - "IDFA_key” (the IDFA authorization message (obviously translated))


    So after the build, first I create how many copies of AppInfo I want, based on how many locations I want to add (obviously inserting at the time of the AppInfo copy, all the data (language code, translated name, translated IDFA message))
    Then I gather all these AppInfo in a list, and I feed it to the players.


    So I copy the player scripts below, and attach the full package.
    I'm not an expert, so if you see mistakes or have simplification ideas, I'm happy to hear them.


    Code (CSharp):
    1. using UnityEngine;
    2.  
    3.  
    4.  
    5. public class AppInfo : MonoBehaviour
    6. {
    7.  
    8.     public string localization = "";
    9.     public string app_name = "";
    10.     public string idfa_key = "";
    11.  
    12.     public AppInfo(string Localization, string App_name, string Idfa_key)
    13.     {
    14.         localization = Localization;
    15.         app_name = App_name;
    16.         idfa_key = Idfa_key;
    17.     }
    18.  
    19. }

    Code (CSharp):
    1. #if UNITY_ANDROID
    2.  
    3. using System.IO;
    4. using System.Text;
    5. using System.Xml;
    6.  
    7.  
    8.  
    9. namespace UnityEditor.Localization.Platform.Android
    10. {
    11.  
    12.     internal class GradleProjectSettings
    13.     {
    14.         internal readonly string LabelName = "@string/app_name";
    15.         internal readonly string IconLabelName = "@mipmap/app_icon";
    16.         internal readonly string RoundIconLabelName = "@mipmap/app_icon_round";
    17.  
    18.         string m_ManifestFilePath;
    19.  
    20.         internal string GetManifestPath(string basePath)
    21.         {
    22.             if (string.IsNullOrEmpty(m_ManifestFilePath))
    23.             {
    24.                 var pathBuilder = new StringBuilder(basePath);
    25.                 GetSourcePath(pathBuilder);
    26.                 pathBuilder.Append(Path.DirectorySeparatorChar).Append("AndroidManifest.xml");
    27.                 m_ManifestFilePath = pathBuilder.ToString();
    28.             }
    29.             return m_ManifestFilePath;
    30.         }
    31.  
    32.         internal string GetResFolderPath(string basePath)
    33.         {
    34.             var pathBuilder = new StringBuilder(basePath);
    35.             GetSourcePath(pathBuilder);
    36.             pathBuilder.Append(Path.DirectorySeparatorChar).Append("res");
    37.             return pathBuilder.ToString();
    38.         }
    39.  
    40.         void GetSourcePath(StringBuilder sb)
    41.         {
    42.             sb.Append(Path.DirectorySeparatorChar).Append("src");
    43.             sb.Append(Path.DirectorySeparatorChar).Append("main");
    44.         }
    45.     }
    46.  
    47.  
    48.     internal class AndroidManifest : XmlDocument
    49.     {
    50.         string m_Path;
    51.         protected XmlNamespaceManager m_XmlNameSpaceManager;
    52.         readonly XmlElement m_Element;
    53.         public const string m_Namespace = "http://schemas.android.com/apk/res/android";
    54.  
    55.         public AndroidManifest(string path)
    56.         {
    57.             m_Path = path;
    58.             using (var reader = new XmlTextReader(m_Path))
    59.             {
    60.                 reader.Read();
    61.                 Load(reader);
    62.             }
    63.             m_XmlNameSpaceManager = new XmlNamespaceManager(NameTable);
    64.             m_XmlNameSpaceManager.AddNamespace("android", m_Namespace);
    65.             m_Element = SelectSingleNode("/manifest/application") as XmlElement;
    66.         }
    67.  
    68.         public void SaveIfModified()
    69.         {
    70.             var manifestOnDisk = new AndroidManifest(m_Path);
    71.  
    72.             // We do this check for incremental pipeline i.e.., when we update the AndroidManifest file in our gradle project instead of overriding it on every build.
    73.             // we check whether our changes are new and only then we save the Updated Manifest file.
    74.             if (manifestOnDisk.OuterXml != OuterXml)
    75.                 Save();
    76.         }
    77.  
    78.         public string Save()
    79.         {
    80.             return SaveAs(m_Path);
    81.         }
    82.  
    83.         public string SaveAs(string path)
    84.         {
    85.             using (var writer = new XmlTextWriter(path, new UTF8Encoding(false)))
    86.             {
    87.                 writer.Formatting = Formatting.Indented;
    88.                 Save(writer);
    89.             }
    90.             return path;
    91.         }
    92.  
    93.         XmlAttribute CreateAndroidAttribute(string key, string value)
    94.         {
    95.             XmlAttribute attr = CreateAttribute("android", key, m_Namespace);
    96.             attr.Value = value;
    97.             return attr;
    98.         }
    99.  
    100.         /// <summary>
    101.         /// Updates the selected atrribute of the application's components with the provided Value in the AndroidManifest file in the Gradle Project.
    102.         /// Refer Android documentation for more details https://developer.android.com/guide/topics/manifest/application-element
    103.         /// </summary>
    104.         /// <param name="key">The selected attribute of the application component.</param>
    105.         /// <param name="value">The value for the specified attribute.</param>
    106.         internal void SetAtrribute(string key, string value)
    107.         {
    108.             var AttributesElement = "android:" + key;
    109.             if (m_Element?.Attributes[AttributesElement] != null)// Check if attribute already exist
    110.             {
    111.                 if (m_Element?.Attributes[AttributesElement].Value != value)// Check if attribute value is different
    112.                 {
    113.                     m_Element.Attributes[AttributesElement].Value = value;
    114.                 }
    115.             }
    116.             else
    117.             {
    118.                 m_Element.Attributes.Append(CreateAndroidAttribute(key, value));
    119.             }
    120.         }
    121.     }
    122.  
    123. }
    124.  
    125. #endif

    Code (CSharp):
    1. #if UNITY_ANDROID
    2.  
    3. using System.Collections.Generic;
    4. using UnityEditor.Android;
    5.  
    6.  
    7.  
    8. namespace UnityEditor.Localization.Platform.Android
    9. {
    10.  
    11.     class LocalizationBuildPlayerAndroid : IPostGenerateGradleAndroidProject
    12.     {
    13.  
    14.         public int callbackOrder { get { return 1; } }
    15.  
    16.         public void OnPostGenerateGradleAndroidProject(string basePath)
    17.         {
    18.  
    19.             // Set the Product Name, perhaps the name that should represent the app for unsupported localizations
    20.             // At the moment the name shown for unsupported localizations, however, is the name of the executable
    21.             PlayerSettings.productName = "Name";
    22.  
    23.  
    24.             // Create the list of localizations
    25.             List<AppInfo> list_appinfo = new List<AppInfo>();
    26.  
    27.  
    28.             // English
    29.             AppInfo appinfo_en = new AppInfo("en", "Name_en", "");
    30.             list_appinfo.Add(appinfo_en);
    31.  
    32.  
    33.             // Italian
    34.             AppInfo appinfo_it = new AppInfo("it", "Name_it", "");
    35.             list_appinfo.Add(appinfo_it);
    36.  
    37.  
    38.             // Spanish
    39.             AppInfo appinfo_es = new AppInfo("es", "Name_es", "");
    40.             list_appinfo.Add(appinfo_es);
    41.  
    42.  
    43.             // French
    44.             AppInfo appinfo_fr = new AppInfo("fr", "Name_fr", "");
    45.             list_appinfo.Add(appinfo_fr);
    46.  
    47.  
    48.             // Create the localizations in the Android project
    49.             PlayerAndroid.AddLocalizationToAndroidGradleProject(basePath, list_appinfo);
    50.  
    51.         }
    52.  
    53.     }
    54.  
    55. }
    56.  
    57. #endif

    Code (CSharp):
    1. #if UNITY_ANDROID
    2.  
    3. using System.IO;
    4. using System.Text;
    5. using System.Text.RegularExpressions;
    6. using System.Collections.Generic;
    7.  
    8.  
    9.  
    10. namespace UnityEditor.Localization.Platform.Android
    11. {
    12.  
    13.  
    14.     public static class PlayerAndroid
    15.     {
    16.  
    17.         const string k_InfoFile = "strings.xml";
    18.  
    19.  
    20.  
    21.         /// <param name="projectDirectory">The root project directory to be updated. This is where the Android player was built to.</param>
    22.         public static void AddLocalizationToAndroidGradleProject(string projectDirectory, List<AppInfo> appinfo_list)
    23.         {
    24.  
    25.             var project = new GradleProjectSettings();
    26.  
    27.             foreach (AppInfo appinfo in appinfo_list)
    28.             {
    29.                 var localeIdentifier = appinfo.localization;
    30.                 var IsSpecialLocaleIdentifier = localeIdentifier.Contains("Hans") || localeIdentifier.Contains("Hant") || localeIdentifier.Contains("Latn") || localeIdentifier.Contains("Cyrl") || localeIdentifier.Contains("Arab") || localeIdentifier.Contains("valencia");
    31.                 localeIdentifier = localeIdentifier.Contains("-") ? IsSpecialLocaleIdentifier ? localeIdentifier.Replace("-", "+") : localeIdentifier.Replace("-", "-r") : localeIdentifier;
    32.                 GenerateLocalizedXmlFile(Path.Combine(Directory.CreateDirectory(Path.Combine(project.GetResFolderPath(projectDirectory), "values-b+" + localeIdentifier)).FullName, k_InfoFile), appinfo);
    33.             }
    34.  
    35.             var androidManifest = new AndroidManifest(project.GetManifestPath(projectDirectory));
    36.             androidManifest.SetAtrribute("label", project.LabelName);
    37.  
    38.             androidManifest.SaveIfModified();
    39.      
    40.         }
    41.  
    42.  
    43.         static void GenerateLocalizedXmlFile(string filePath, AppInfo appinfo)
    44.         {
    45.  
    46.             // We are adding a back slash when the entry value contains an single quote, to prevent android build failures and show the display name with apostrophe ex: " J'adore ";
    47.             // (?<!\\) - Negative Lookbehind to ignore any that already start with \\
    48.             // (?<replace>') - match colon and place it into the replace variable
    49.             var localizedValue = Regex.Replace(appinfo.app_name, @"(?<!\\)(?<replace>')", @"\'");
    50.  
    51.  
    52.             using (var stream = new StreamWriter(filePath, false, Encoding.UTF8))
    53.             {
    54.                 stream.WriteLine(
    55.                     $@"<?xml version=""1.0"" encoding=""utf-8""?>" +
    56.                     "<!--" +
    57.                     "\n" +
    58.                     $"\t{k_InfoFile}\n" +
    59.                     $"\tThis file was auto-generated for localizations\n" +
    60.                     $"-->" +
    61.                     "\n" +
    62.                     $@"<resources>
    63.                       <string name=""app_name""> {localizedValue} </string>
    64.                       </resources>");
    65.             }
    66.  
    67.         }
    68.  
    69.     }
    70.  
    71. }
    72.  
    73. #endif

    Code (CSharp):
    1. #if UNITY_IOS
    2.  
    3. using System;
    4. using System.Collections;
    5. using System.Reflection;
    6. using UnityEditor.iOS.Xcode;
    7.  
    8.  
    9.  
    10. namespace UnityEditor.Localization.Platform.iOS
    11. {
    12.     /// <summary>
    13.     /// Provides access to PBXProject internals via reflection.
    14.     /// TODO: Make this API public in Unity so we dont need to use reflection in the future.
    15.     /// </summary>
    16.     static class PBXProjectExtensions
    17.     {
    18.         readonly static Type s_GUIDList;
    19.         readonly static Type s_PBXBuildFileDat;
    20.         readonly static Type s_PBXElementArray;
    21.         readonly static Type s_PBXElementString;
    22.         readonly static Type s_PBXVariantGroupData;
    23.  
    24.         readonly static FieldInfo s_DataFileGroups;
    25.         readonly static FieldInfo s_DataFileRefsField;
    26.         readonly static FieldInfo s_KnownRegionsDict;
    27.         readonly static FieldInfo s_GroupChildren;
    28.         readonly static FieldInfo s_GroupName;
    29.         readonly static FieldInfo s_GroupPath;
    30.         readonly static FieldInfo s_PBXObjectGuid;
    31.         readonly static FieldInfo s_PBXElementArrayValues;
    32.         readonly static FieldInfo s_ProjectData;
    33.         readonly static FieldInfo s_ResourceFiles;
    34.         readonly static FieldInfo s_VariantGroupName;
    35.  
    36.         readonly static PropertyInfo s_FileRefsPath;
    37.         readonly static PropertyInfo s_ProjectResoruces;
    38.         readonly static PropertyInfo s_ProjectSection;
    39.         readonly static PropertyInfo s_ProjectSectionObjectData;
    40.         readonly static PropertyInfo s_ProjectVariantGroups;
    41.  
    42.         readonly static MethodInfo s_DataFileRefsFieldObjects;
    43.         readonly static MethodInfo s_FileRefDataCreateFromFile;
    44.         readonly static MethodInfo s_GetPropertiesRaw;
    45.         readonly static MethodInfo s_GroupsObjects;
    46.         readonly static MethodInfo s_GuidListAdd;
    47.         readonly static MethodInfo s_GUIDListContains;
    48.         readonly static MethodInfo s_PBXBuildFileDataCreateFromFile;
    49.         readonly static MethodInfo s_ProjectBuildFilesAdd;
    50.         readonly static MethodInfo s_ProjectFileRefsAdd;
    51.         readonly static MethodInfo s_ProjectBuildFilesGetForSourceFile;
    52.         readonly static MethodInfo s_RawPropertiesValuesAddValue;
    53.         readonly static MethodInfo s_RawPropertiesValuesGetValue;
    54.         readonly static MethodInfo s_ResorucesObjects;
    55.         readonly static MethodInfo s_VariantGroupsAddEntry;
    56.         readonly static MethodInfo s_VariantGroupsObjects;
    57.         readonly static MethodInfo s_VariantGroupsSetPropertyString;
    58.  
    59.         static PBXProjectExtensions()
    60.         {
    61.             var asm = typeof(PBXProject).Assembly;
    62.             const string ns = "UnityEditor.iOS.Xcode.PBX";
    63.             const BindingFlags pv = BindingFlags.Instance | BindingFlags.NonPublic;
    64.  
    65.             // Types
    66.             s_GUIDList = asm.GetType($"{ns}.GUIDList");
    67.             s_PBXBuildFileDat = asm.GetType($"{ns}.PBXBuildFileData");
    68.             s_PBXElementArray = asm.GetType($"{ns}.PBXElementArray");
    69.             s_PBXElementString = asm.GetType($"{ns}.PBXElementString");
    70.             s_PBXVariantGroupData = asm.GetType($"{ns}.PBXVariantGroupData");
    71.             var fileRefData = asm.GetType($"{ns}.PBXFileReferenceData");
    72.             var group = asm.GetType($"{ns}.PBXGroupData");
    73.             var pbxObject = asm.GetType($"{ns}.PBXObjectData");
    74.             var fileGUIDListBase = asm.GetType($"{ns}.FileGUIDListBase");
    75.             var pBXElementDict = asm.GetType($"{ns}.PBXElementDict");
    76.             var pBXProjectSection = asm.GetType($"{ns}.PBXProjectSection");
    77.             UnityEngine.Debug.Log(pBXProjectSection);
    78.  
    79.             // Fields
    80.             s_ProjectData = typeof(PBXProject).GetField("m_Data", pv);
    81.             s_DataFileRefsField = s_ProjectData.FieldType.GetField("fileRefs", pv);
    82.             s_DataFileGroups = s_ProjectData.FieldType.GetField("groups", pv);
    83.             s_ResourceFiles = fileGUIDListBase.GetField("files");
    84.             s_PBXObjectGuid = pbxObject.GetField("guid");
    85.             s_PBXElementArrayValues = s_PBXElementArray.GetField("values");
    86.             s_GroupChildren = group.GetField("children");
    87.             s_GroupName = group.GetField("name");
    88.             s_GroupPath = group.GetField("path");
    89.             s_VariantGroupName = s_PBXVariantGroupData.GetField("name");
    90.  
    91.             // Methods
    92.             s_GroupsObjects = s_DataFileGroups.FieldType.GetMethod("GetObjects");
    93.             s_DataFileRefsFieldObjects = s_DataFileRefsField.FieldType.GetMethod("GetObjects");
    94.             s_GetPropertiesRaw = pbxObject.GetMethod("GetPropertiesRaw", pv);
    95.             s_GuidListAdd = s_GUIDList.GetMethod("AddGUID");
    96.             s_GUIDListContains = s_GUIDList.GetMethod("Contains");
    97.             s_FileRefDataCreateFromFile = fileRefData.GetMethod("CreateFromFile", BindingFlags.Static | BindingFlags.Public);
    98.             s_PBXBuildFileDataCreateFromFile = s_PBXBuildFileDat.GetMethod("CreateFromFile", BindingFlags.Static | BindingFlags.Public);
    99.             s_ProjectBuildFilesAdd = typeof(PBXProject).GetMethod("BuildFilesAdd", pv);
    100.             s_ProjectFileRefsAdd = typeof(PBXProject).GetMethod("FileRefsAdd", pv);
    101.             s_ProjectBuildFilesGetForSourceFile = typeof(PBXProject).GetMethod("BuildFilesGetForSourceFile", pv);
    102.  
    103.             // Properties
    104.             s_FileRefsPath = fileRefData.GetProperty("path");
    105.             s_KnownRegionsDict = pBXElementDict.GetField("m_PrivateValue", pv);
    106.             s_ProjectSection = typeof(PBXProject).GetProperty("project", pv);
    107.             s_ProjectSectionObjectData = s_ProjectSection.PropertyType.GetProperty("project");
    108.             s_ProjectResoruces = typeof(PBXProject).GetProperty("resources", pv);
    109.             s_ProjectVariantGroups = typeof(PBXProject).GetProperty("variantGroups", pv);
    110.  
    111.             s_RawPropertiesValuesGetValue = s_KnownRegionsDict.FieldType.GetMethod("TryGetValue");
    112.             s_RawPropertiesValuesAddValue = s_KnownRegionsDict.FieldType.GetMethod("Add");
    113.             s_ResorucesObjects = s_ProjectResoruces.PropertyType.GetMethod("GetObjects");
    114.             s_VariantGroupsAddEntry = s_ProjectVariantGroups.PropertyType.GetMethod("AddEntry");
    115.             s_VariantGroupsObjects = s_ProjectVariantGroups.PropertyType.GetMethod("GetObjects");
    116.             s_VariantGroupsSetPropertyString = group.GetMethod("SetPropertyString", pv);
    117.         }
    118.  
    119.         static /* PBXFileReferenceData */ object GetFileRefDataByPath(this PBXProject project, string path)
    120.         {
    121.             var data = s_ProjectData.GetValue(project);
    122.             var fileRefs = s_DataFileRefsField.GetValue(data);
    123.             var values = s_DataFileRefsFieldObjects.Invoke(fileRefs, null) as ICollection;
    124.  
    125.             // The lookup methods provided by PBXproject dont seem to be reliable so we will just go through the assets manually.
    126.             foreach (var f in values)
    127.             {
    128.                 var fileRefPath = s_FileRefsPath.GetValue(f) as string;
    129.                 if (fileRefPath == path)
    130.                     return f;
    131.             }
    132.             return null;
    133.         }
    134.  
    135.         static /* PBXGroupData */ object GetGroupByName(this PBXProject project, string name)
    136.         {
    137.             var data = s_ProjectData.GetValue(project);
    138.             var groups = s_DataFileGroups.GetValue(data);
    139.             var groupsValues = s_GroupsObjects.Invoke(groups, null) as ICollection;
    140.  
    141.             foreach (var g in groupsValues)
    142.             {
    143.                 var groupName = s_GroupName.GetValue(g) as string;
    144.                 if (groupName == name)
    145.                     return g;
    146.             }
    147.             return null;
    148.         }
    149.  
    150.         static IList GetKnownRegions(this PBXProject project)
    151.         {
    152.             const string elementName = "knownRegions";
    153.  
    154.             var section = s_ProjectSection.GetValue(project);
    155.             var data = s_ProjectSectionObjectData.GetValue(section);
    156.             var rawProperties = s_GetPropertiesRaw.Invoke(data, null);
    157.  
    158.             object[] args = new[] { elementName, null };
    159.             var dict = s_KnownRegionsDict.GetValue(rawProperties);
    160.             var ret = (bool)s_RawPropertiesValuesGetValue.Invoke(dict, args);
    161.             if (!ret)
    162.             {
    163.                 args[1] = Activator.CreateInstance(s_PBXElementArray);
    164.                 s_RawPropertiesValuesAddValue.Invoke(dict, new object[] { elementName, args[1] });
    165.             }
    166.  
    167.             return s_PBXElementArrayValues.GetValue(args[1]) as IList;
    168.         }
    169.  
    170.         static string AddFileRefToBuild(this PBXProject project, string target, string guid)
    171.         {
    172.             var data = s_PBXBuildFileDataCreateFromFile.Invoke(null, new object[] { guid, false, null });
    173.             s_ProjectBuildFilesAdd.Invoke(project, new object[] { target, data });
    174.             return s_PBXObjectGuid.GetValue(data) as string;
    175.         }
    176.  
    177.         static void AddFileToResourceBuildPhase(this PBXProject project, string buildPhaseGuid, string fileGuid)
    178.         {
    179.             var resources = s_ProjectResoruces.GetValue(project);
    180.  
    181.             var values = s_ResorucesObjects.Invoke(resources, null) as ICollection;
    182.             foreach (var v in values)
    183.             {
    184.                 var guid = s_PBXObjectGuid.GetValue(v) as string;
    185.                 if (guid == buildPhaseGuid)
    186.                 {
    187.                     var files = s_ResourceFiles.GetValue(v);
    188.                     s_GuidListAdd.Invoke(files, new object[] { fileGuid });
    189.                 }
    190.             }
    191.         }
    192.  
    193.         public static void ClearKnownRegions(this PBXProject project)
    194.         {
    195.             var regions = project.GetKnownRegions();
    196.             regions.Clear();
    197.         }
    198.  
    199.         public static void AddKnownRegion(this PBXProject project, string code)
    200.         {
    201.             var regions = project.GetKnownRegions();
    202.             var element = Activator.CreateInstance(s_PBXElementString, code);
    203.             regions.Add(element);
    204.         }
    205.  
    206.         public static void AddLocaleVariantFile(this PBXProject project, string groupName, string code, string path)
    207.         {
    208.             /// Replaces '\' with '/'. We need to apply this function to all paths that come from the user
    209.             /// of the API because we store paths to pbxproj and on windows we may get path with '\' slashes
    210.             /// instead of '/' slashes
    211.             path = path.Replace('\\', '/');
    212.  
    213.             // Get or create the variant group
    214.             var variantGroups = s_ProjectVariantGroups.GetValue(project);
    215.             var variantGroupValues = s_VariantGroupsObjects.Invoke(variantGroups, null) as ICollection;
    216.             object group = null;
    217.             foreach (var g in variantGroupValues)
    218.             {
    219.                 var name = s_VariantGroupName.GetValue(g) as string;
    220.                 if (name == groupName)
    221.                     group = g;
    222.             }
    223.  
    224.             if (group == null)
    225.             {
    226.                 var guid = Guid.NewGuid().ToString("N").Substring(8).ToUpper();
    227.  
    228.                 group = Activator.CreateInstance(s_PBXVariantGroupData);
    229.                 s_VariantGroupName.SetValue(group, groupName);
    230.                 s_GroupPath.SetValue(group, groupName);
    231.                 s_PBXObjectGuid.SetValue(group, guid);
    232.                 s_GroupChildren.SetValue(group, Activator.CreateInstance(s_GUIDList));
    233.                 s_VariantGroupsSetPropertyString.Invoke(group, new object[] { "isa", "PBXVariantGroup" });
    234.  
    235.                 s_VariantGroupsAddEntry.Invoke(variantGroups, new object[] { group });
    236.             }
    237.  
    238.             var targetGuid = project.GetUnityMainTargetGuid();
    239.             var groupGuid = s_PBXObjectGuid.GetValue(group) as string;
    240.  
    241.             var buildFileData = s_ProjectBuildFilesGetForSourceFile.Invoke(project, new object[] { targetGuid, groupGuid });
    242.             if (buildFileData == null)
    243.             {
    244.                 var customData = project.GetGroupByName("CustomTemplate");
    245.                 var children = s_GroupChildren.GetValue(customData);
    246.                 s_GuidListAdd.Invoke(children, new object[] { groupGuid });
    247.  
    248.                 var buildFileGuid = project.AddFileRefToBuild(project.GetUnityMainTargetGuid(), groupGuid);
    249.                 var buildPhaseGuid = project.GetResourcesBuildPhaseByTarget(targetGuid);
    250.                 project.AddFileToResourceBuildPhase(buildPhaseGuid, buildFileGuid);
    251.             }
    252.  
    253.             // Add the file if it has not already been added
    254.             var fileRef = project.GetFileRefDataByPath(path);
    255.             if (fileRef == null)
    256.             {
    257.                 fileRef = s_FileRefDataCreateFromFile.Invoke(null, new object[] { path, code, PBXSourceTree.Source });
    258.                 s_ProjectFileRefsAdd.Invoke(project, new object[] { path, code, group, fileRef });
    259.             }
    260.  
    261.             // Add the file to the variant group
    262.             var fileRefsGuid = s_PBXObjectGuid.GetValue(fileRef) as string;
    263.             var groupChildren = s_GroupChildren.GetValue(group);
    264.             var res = (bool)s_GUIDListContains.Invoke(groupChildren, new object[] { fileRefsGuid });
    265.             if (!res)
    266.             {
    267.                 s_GuidListAdd.Invoke(groupChildren, new[] { fileRefsGuid });
    268.             }
    269.         }
    270.     }
    271. }
    272.  
    273. #endif

    Code (CSharp):
    1. #if UNITY_IOS
    2.  
    3. using System.IO;
    4. using System.Collections.Generic;
    5. using UnityEditor.Build;
    6. using UnityEditor.Build.Reporting;
    7. using UnityEditor.iOS.Xcode;
    8.  
    9.  
    10.  
    11. namespace UnityEditor.Localization.Platform.iOS
    12. {
    13.  
    14.     class LocalizationBuildPlayerIOS : IPostprocessBuildWithReport
    15.     {
    16.  
    17.         public int callbackOrder => 1;
    18.  
    19.         public void OnPostprocessBuild(BuildReport report)
    20.         {
    21.  
    22.             // Create the key for the IDFA
    23.             Create_IDFA_key(report.summary.outputPath, "This identifier will be used to deliver personalized ads to you.");
    24.  
    25.  
    26.             // Set the Product Name, the default name that should represent the app for unsupported localizations (?)
    27.             PlayerSettings.productName = "Name";
    28.  
    29.  
    30.             // Create the list of localizations
    31.             List<AppInfo> list_appinfo = new List<AppInfo>();
    32.  
    33.  
    34.             // English
    35.             AppInfo appinfo_en = new AppInfo("en", "Name_en", "This identifier will be used to deliver personalized ads to you.");
    36.             list_appinfo.Add(appinfo_en);
    37.  
    38.  
    39.             // Italian
    40.             AppInfo appinfo_it = new AppInfo("it", "Name_it", "Questo identificatore verrà utilizzato per fornirti annunci personalizzati.");
    41.             list_appinfo.Add(appinfo_it);
    42.  
    43.  
    44.             // Spanish
    45.             AppInfo appinfo_es = new AppInfo("es", "Name_es", "Este identificador se utilizará para ofrecerle anuncios personalizados.");
    46.             list_appinfo.Add(appinfo_es);
    47.  
    48.  
    49.             // French
    50.             AppInfo appinfo_fr = new AppInfo("fr", "Name_fr", "Cet identifiant sera utilisé pour vous proposer des publicités personnalisées.");
    51.             list_appinfo.Add(appinfo_fr);
    52.  
    53.  
    54.             // Create the localizations in the iOS project
    55.             PlayeriOS.AddLocalizationToXcodeProject(report.summary.outputPath, list_appinfo);
    56.  
    57.         }
    58.  
    59.  
    60.         void Create_IDFA_key(string pathToXcode, string IDFA_description)
    61.         {
    62.  
    63.             // Get Plist from Xcode project
    64.             string plistPath = pathToXcode + "/Info.plist";
    65.  
    66.             // Read in Plist
    67.             PlistDocument plistObj = new PlistDocument();
    68.             plistObj.ReadFromString(File.ReadAllText(plistPath));
    69.  
    70.             // Set values from the root obj
    71.             PlistElementDict plistRoot = plistObj.root;
    72.  
    73.             // Set value in plist
    74.             plistRoot.SetString("NSUserTrackingUsageDescription", IDFA_description);
    75.  
    76.             // Save
    77.             File.WriteAllText(plistPath, plistObj.WriteToString());
    78.  
    79.         }
    80.  
    81.     }
    82.  
    83. }
    84.  
    85. #endif

    Code (CSharp):
    1. #if UNITY_IOS
    2.  
    3. using System.Collections.Generic;
    4. using System.IO;
    5. using System.Text;
    6. using UnityEditor.iOS.Xcode;
    7.  
    8.  
    9.  
    10. namespace UnityEditor.Localization.Platform.iOS
    11. {
    12.  
    13.     public static class PlayeriOS
    14.     {
    15.  
    16.         const string kInfoFile = "InfoPlist.strings";
    17.  
    18.  
    19.  
    20.         /// <param name="projectDirectory">The root project directory to be updated. This is where the iOS player was built to.</param>
    21.         /// AppInfo is a class that has as parameters: localization ('en', 'it', ...), app_name ('..., ...'), idfa_key ('I use this flag for ...')
    22.         public static void AddLocalizationToXcodeProject(string projectDirectory, List<AppInfo> appinfo_list)
    23.         {
    24.  
    25.             var pbxPath = PBXProject.GetPBXProjectPath(projectDirectory);
    26.             var project = new PBXProject();
    27.             project.ReadFromFile(pbxPath);
    28.             project.ClearKnownRegions(); // Remove the deprecated regions that get added automatically.
    29.  
    30.             var plistDocument = new PlistDocument();
    31.             var plistPath = Path.Combine(projectDirectory, "Info.plist");
    32.             plistDocument.ReadFromFile(plistPath);
    33.  
    34.             var bundleLanguages = plistDocument.root.CreateArray("CFBundleLocalizations");
    35.  
    36.             foreach (AppInfo appinfo in appinfo_list)
    37.             {
    38.                 var code = appinfo.localization;
    39.                 project.AddKnownRegion(code);
    40.                 bundleLanguages.AddString(code);
    41.  
    42.                 var localeDir = code + ".lproj";
    43.                 var dir = Path.Combine(projectDirectory, localeDir);
    44.                 Directory.CreateDirectory(dir);
    45.  
    46.                 var filePath = Path.Combine(dir, kInfoFile);
    47.                 var relativePath = Path.Combine(localeDir, kInfoFile);
    48.  
    49.                 GenerateLocalizedInfoPlistFile(appinfo, plistDocument, filePath);
    50.                 project.AddLocaleVariantFile(kInfoFile, code, relativePath);
    51.             }
    52.  
    53.             plistDocument.WriteToFile(plistPath);
    54.             project.WriteToFile(pbxPath);
    55.  
    56.         }
    57.  
    58.  
    59.         static void GenerateLocalizedInfoPlistFile(AppInfo appinfo, PlistDocument plistDocument, string filePath)
    60.         {
    61.             using (var stream = new StreamWriter(filePath, false, Encoding.UTF8))
    62.             {
    63.                 stream.Write(
    64.                     "/*\n" +
    65.                     $"\t{kInfoFile}\n" +
    66.                     $"\tThis file was auto-generated for localizations\n" +
    67.                     $"*/\n\n");
    68.  
    69.                 WriteLocalizedValue("CFBundleName", stream, appinfo.app_name, plistDocument);
    70.                 WriteLocalizedValue("CFBundleDisplayName", stream, appinfo.app_name, plistDocument);
    71.                 WriteLocalizedValue("NSUserTrackingUsageDescription", stream, appinfo.idfa_key, plistDocument);
    72.             }
    73.         }
    74.  
    75.  
    76.         static void WriteLocalizedValue(string valueName, StreamWriter stream, string value, PlistDocument plistDocument)
    77.         {
    78.  
    79.             stream.WriteLine($"\"{valueName}\" = \"{value}\";");
    80.             plistDocument.root.SetString(valueName, string.Empty);
    81.  
    82.         }
    83.  
    84.     }
    85.  
    86. }
    87.  
    88. #endif
     

    Attached Files:

    Last edited: Nov 1, 2021
    khrysller, savern and karl_jones like this.