Search Unity

  1. Unity 2020.1 has been released.
    Dismiss Notice
  2. We are looking for feedback on the experimental Unity Safe Mode which is aiming to help you resolve compilation errors faster during project startup.
    Dismiss Notice
  3. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Import / Export Localisation Tables

Discussion in 'Localisation Tools Previews' started by fffMalzbier, Jul 1, 2019.

  1. fffMalzbier

    fffMalzbier

    Joined:
    Jun 14, 2011
    Posts:
    3,085
    A feature Im missing so far is a way to import / export the data localisation Tables from the editor to send the data to a translation department / company.
    A import interface for Json / XML or a custom exporter interface would be nice.

    Manually copy pasting Thousands of strings to and from another data source is time consuming and error prone.
     
  2. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    4,064
    Its on our road map. We are working on supporting import/export for several file types and services.
     
    Korindian likes this.
  3. fffMalzbier

    fffMalzbier

    Joined:
    Jun 14, 2011
    Posts:
    3,085
    Good to know :)
     
    karl_jones likes this.
  4. Alexis-Dev

    Alexis-Dev

    Joined:
    Apr 16, 2019
    Posts:
    69
    Hi,

    Some news about this features? :)

    Alexis
     
  5. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    4,064
    Hi. We are working on a Google sheets import export which should be available in the next release. We are also adding xliff which may be in the same release, or the next one.
    Several more formats will follow.
    These are editor only features at the moment.
     
    Alexis-Dev likes this.
  6. Exentro

    Exentro

    Joined:
    Oct 8, 2013
    Posts:
    31
    I started testing a bit the localization tool and came with that for importing/exporting strings to csv.
    I just tried with a single StringTable with 2 entries so far so don't know if it scales without issue, but it may help waiting the next release.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Globalization;
    3. using System.IO;
    4. using UnityEditor;
    5. using UnityEditor.Localization;
    6. using UnityEngine;
    7. using UnityEngine.Localization.Tables;
    8.  
    9. public class LocalizationWindow : EditorWindow
    10. {
    11.     [MenuItem("Tools/LocalizationCSV")]
    12.     public static void ShowWindow()
    13.     {
    14.         GetWindow(typeof(LocalizationWindow));
    15.     }
    16.  
    17.     private void OnEnable()
    18.     {
    19.         if (string.IsNullOrEmpty(CsvFilePath) || !Directory.Exists(Application.dataPath))
    20.         {
    21.             CsvFilePath = $"{Application.dataPath}{Path.DirectorySeparatorChar}localization_export.csv";
    22.         }
    23.     }
    24.  
    25.     [SerializeField] private string CsvFilePath;
    26.     private const char _CSV_SEPARATOR_ = ';';
    27.  
    28.     void OnGUI()
    29.     {
    30.         CsvFilePath = EditorGUILayout.TextField("CSV File Path ", CsvFilePath);
    31.         EditorGUILayout.BeginHorizontal();
    32.         if(GUILayout.Button("Export to CSV"))
    33.         {
    34.             CSVExporter.ToCSV(CsvFilePath);
    35.         }
    36.         if (GUILayout.Button("Import from CSV"))
    37.         {
    38.             CSVImporter.FromCSV(CsvFilePath);
    39.         }
    40.         EditorGUILayout.EndHorizontal();
    41.     }
    42.  
    43.     private static class CSVExporter
    44.     {
    45.         public static void ToCSV(string path)
    46.         {
    47.             foreach (AssetTableCollection collection in LocalizationEditorSettings.GetAssetTablesCollection<StringTable>())
    48.             {
    49.                 string collectionName = collection.TableName;
    50.  
    51.                 List<CultureInfo> cultures = GetCulturesInfo(collection);
    52.                 List<string> keys = GetKeys(collection.Tables[0]);
    53.  
    54.                 using (var writer = new StreamWriter(path))
    55.                 {
    56.                     WriteHeadLine(writer, collectionName, cultures);
    57.                     WriteKeysLines(writer, collection, keys);
    58.                 }
    59.  
    60.                 Debug.Log($"Csv file path : {path}");
    61.             }
    62.         }
    63.  
    64.         private static List<CultureInfo> GetCulturesInfo(AssetTableCollection collection)
    65.         {
    66.             List<CultureInfo> cultures = new List<CultureInfo>();
    67.             foreach (LocalizedTable table in collection.Tables)
    68.             {
    69.                 cultures.Add(table.LocaleIdentifier.CultureInfo);
    70.             }
    71.             return cultures;
    72.         }
    73.  
    74.         private static List<string> GetKeys(LocalizedTable table)
    75.         {
    76.             List<string> keys = new List<string>();
    77.             foreach (SharedTableData.SharedTableEntry entry in table.SharedData.Entries)
    78.             {
    79.                 keys.Add(entry.Key);
    80.             }
    81.             return keys;
    82.         }
    83.  
    84.         private static void WriteHeadLine(StreamWriter writer, string collectionName, List<CultureInfo> cultures)
    85.         {
    86.             string line = $"{collectionName}{_CSV_SEPARATOR_}Keys\\locales";
    87.             foreach (CultureInfo info in cultures)
    88.             {
    89.                 line = $"{line}{_CSV_SEPARATOR_}{info.ToString()}";
    90.             }
    91.             writer.WriteLine(line);
    92.         }
    93.  
    94.         private static void WriteKeysLines(StreamWriter writer, AssetTableCollection collection, List<string> keys)
    95.         {
    96.             foreach (string key in keys)
    97.             {
    98.                 string line = $"{collection.TableName}{_CSV_SEPARATOR_}{key}";
    99.                 foreach (LocalizedTable table in collection.Tables)
    100.                 {
    101.                     line = $"{line}{_CSV_SEPARATOR_}{((StringTable)table).GetEntry(key).Value}";
    102.                 }
    103.                 writer.WriteLine(line);
    104.             }
    105.         }
    106.     }
    107.  
    108.     private static class CSVImporter
    109.     {
    110.         private const int _INDEX_CollectionName_ = 0;
    111.         private const int _INDEX_Key_ = 1;
    112.         private const int _INDEX_ContentStart_ = 2;
    113.  
    114.         public static void FromCSV(string path)
    115.         {
    116.             using (var reader = new StreamReader(path))
    117.             {
    118.                 CultureInfo[] cultures = GetCulturesFromHeadLine(reader);
    119.  
    120.                 string line = string.Empty;
    121.                 while ((line = reader.ReadLine()) != null)
    122.                 {
    123.                     string[] content = line.Split(_CSV_SEPARATOR_);
    124.                     string collectionName = content[_INDEX_CollectionName_];
    125.                     string key = content[_INDEX_Key_];
    126.                     for (int i = _INDEX_ContentStart_; i < content.Length; i++)
    127.                     {
    128.                         CultureInfo cellCultureInfo = cultures[i - _INDEX_ContentStart_];
    129.                         LocalizedTable table = FindLocalizedTable(collectionName, cellCultureInfo);
    130.                         SetKey((StringTable)table, key, content[i]);
    131.                         EditorUtility.SetDirty(table);
    132.                     }
    133.                 }
    134.             }
    135.  
    136.             AssetDatabase.SaveAssets();
    137.         }
    138.  
    139.         private static CultureInfo[] GetCulturesFromHeadLine(StreamReader reader)
    140.         {
    141.             List<CultureInfo> cultures = new List<CultureInfo>();
    142.             string[] content = reader.ReadLine().Split(_CSV_SEPARATOR_);
    143.             for (int i = 2; i < content.Length; i++)
    144.             {
    145.                 cultures.Add(new CultureInfo(content[i], false));
    146.             }
    147.             return cultures.ToArray();
    148.         }
    149.  
    150.         private static void SetKey(StringTable table, string key, string value)
    151.         {
    152.             StringTableEntry entry = table.GetEntry(key);
    153.             if (entry.Value != value)
    154.             {
    155.                 Debug.Log($"{table.TableName} - {key} : {entry.Value} => {value}");
    156.                 entry.Value = value;
    157.             }
    158.         }
    159.  
    160.         private static LocalizedTable FindLocalizedTable(string collection_name, CultureInfo locale)
    161.         {
    162.             LocalizedTable result = null;
    163.             foreach (AssetTableCollection collection in LocalizationEditorSettings.GetAssetTablesCollection<StringTable>())
    164.             {
    165.                 if (collection.TableName.Equals(collection_name))
    166.                 {
    167.                     foreach (LocalizedTable table in collection.Tables)
    168.                     {
    169.                         if (table.LocaleIdentifier.CultureInfo.Equals(locale))
    170.                         {
    171.                             result = table;
    172.                             break;
    173.                         }
    174.                     }
    175.                 }
    176.                 if (result != null) { break; }
    177.             }
    178.             if (result == null)
    179.             {
    180.                 Debug.Log($"{collection_name} - {locale} not found !");
    181.             }
    182.             return result;
    183.         }
    184.     }
    185. }
    186.  
     
    karl_jones likes this.
  7. jamesor

    jamesor

    Joined:
    Jan 1, 2020
    Posts:
    11
    I've been using Google Sheets and wrote this little importer which is getting the job done. I welcome any feedback on how I can improve it.

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Localization.Tables;
    5. using UnityEditor;
    6.  
    7. namespace AE
    8. {
    9.     [CreateAssetMenu(fileName = "New String Importer", menuName = "Localization/String Importer")]
    10.     public class StringImporter : ScriptableObject
    11.     {
    12.         [Header("String Tables")]
    13.         public SharedTableData SharedTable;
    14.         public StringTable[] StringTables;
    15.         [Header("Data Source Files (Tab Delimited)")]
    16.         public TextAsset TabDelimitedFile;
    17.         public void Import()
    18.         {
    19.             if (SharedTable == null)
    20.             {
    21.                 Debug.LogError("A SharedTableData reference is required.");
    22.                 return;
    23.             }
    24.             if (StringTables == null || StringTables.Length == 0)
    25.             {
    26.                 Debug.LogError("One or more StringTable references are required.");
    27.                 return;
    28.             }
    29.             if (TabDelimitedFile == null)
    30.             {
    31.                 Debug.LogError("A tab delimited file reference is required.");
    32.                 return;
    33.             }
    34.             Dictionary<string, int> langNames = new Dictionary<string, int>();
    35.             for (var i = 0; i < StringTables.Length; i++)
    36.             {
    37.                 langNames.Add(StringTables[i].LocaleIdentifier.ToString(), i);
    38.             }
    39.             if (langNames.Count == 0)
    40.             {
    41.                 Debug.LogError("At least one language needs to be configured in LocalizationSettings.");
    42.                 return;
    43.             }
    44.             Dictionary<string, int> fieldNames = new Dictionary<string, int>();
    45.             var splitFile = new string[] { "\r\n", "\r", "\n" };
    46.             var splitLine = new string[] { "\t" };
    47.             var lines = TabDelimitedFile.text.Split(splitFile, StringSplitOptions.None);
    48.             var isFieldNameRow = true;
    49.             // We need at minimum the Field Names Row and a Data Row
    50.             if (lines.Length < 2)
    51.             {
    52.                 Debug.LogError("The tab delimited file needs to contain at minimum a field name row and a data row.");
    53.                 return;
    54.             }
    55.             foreach (var lang in langNames)
    56.             {
    57.                 StringTables[lang.Value].Clear();
    58.                 Undo.RecordObject(StringTables[lang.Value], "Changed translated text");
    59.                 EditorUtility.SetDirty(StringTables[lang.Value]);
    60.             }
    61.             if (SharedTable != null)
    62.             {
    63.                 Undo.RecordObject(SharedTable, "Changed translated text");
    64.                 EditorUtility.SetDirty(SharedTable);
    65.             }
    66.             for (uint i = 0; i < lines.Length; i++)
    67.             {
    68.                 var line = lines[i].Split(splitLine, StringSplitOptions.None);
    69.                 if (isFieldNameRow)
    70.                 {
    71.                     for (int j = 0; j < line.Length; j++)
    72.                     {
    73.                         fieldNames.Add(line[j], j);
    74.                     }
    75.                     isFieldNameRow = false;
    76.                 }
    77.                 else if (line[fieldNames["key"]] == "!!_IGNORE_!!")
    78.                 {
    79.                     // Ignore this row because it just contains a template forumla for Google Translate
    80.                 }
    81.                 else
    82.                 {
    83.                     foreach (var lang in langNames)
    84.                     {
    85.                         StringTables[lang.Value].AddEntry(line[fieldNames["key"]], line[fieldNames[lang.Key]]);
    86.                     }
    87.                 }
    88.             }
    89.             Debug.Log("Finished importing text");
    90.         }
    91.     }
    92.  
    93.     [CustomEditor(typeof(StringImporter))]
    94.     public class TestScriptableEditor : Editor
    95.     {
    96.         public override void OnInspectorGUI()
    97.         {
    98.             base.OnInspectorGUI();
    99.             var script = (StringImporter)target;
    100.  
    101.             if (GUILayout.Button("Import Language Strings", GUILayout.Height(40)))
    102.             {
    103.                 script.Import();
    104.             }
    105.         }
    106.     }
    107. }
    108.  
    I export my data from Google Sheets as a Tab Delimited File and bring it in as a TextAsset into an Instance of the ImporterScript. Each set of tables has it's own Importer instance. The first row uses language names like "English(en)" and "Spanish(es)" to match up with the available locals I defined iin my Localization Settings.

    importer.png
    sheets.png
     
    Last edited: May 28, 2020
    krasner likes this.
  8. krasner

    krasner

    Joined:
    Sep 9, 2014
    Posts:
    5
    I grabbed the above script and improved it a bit. It uses CSV (comma separated) export because that export preserves formatting such as newlines. Hope this helps anyone!
    Code (CSharp):
    1.  
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Localization.Tables;
    5. using UnityEditor;
    6. using System;
    7. using System.Text.RegularExpressions;
    8.  
    9. [CreateAssetMenu(fileName = "New String Importer", menuName = "Localization/String Importer")]
    10. public class StringImporter : ScriptableObject
    11. {
    12.     [Header("String Tables")]
    13.     public SharedTableData SharedTable;
    14.     public StringTable[] StringTables;
    15.     [Header("Data Source Files (Comma Delimited)")]
    16.     public TextAsset CommaSeparatedFile;
    17.     public void Import()
    18.     {
    19.         if (SharedTable == null)
    20.         {
    21.             Debug.LogError("A SharedTableData reference is required.");
    22.             return;
    23.         }
    24.         if (StringTables == null || StringTables.Length == 0)
    25.         {
    26.             Debug.LogError("One or more StringTable references are required.");
    27.             return;
    28.         }
    29.         if (CommaSeparatedFile == null)
    30.         {
    31.             Debug.LogError("A comma delimited file reference is required.");
    32.             return;
    33.         }
    34.         Dictionary<string, int> langNames = new Dictionary<string, int>();
    35.         for (var i = 0; i < StringTables.Length; i++)
    36.         {
    37.             langNames.Add(StringTables[i].LocaleIdentifier.ToString(), i);
    38.         }
    39.         if (langNames.Count == 0)
    40.         {
    41.             Debug.LogError("At least one language needs to be configured in LocalizationSettings.");
    42.             return;
    43.         }
    44.         Dictionary<string, int> fieldNames = new Dictionary<string, int>();
    45.  
    46.         string pattern = "\n(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)";
    47.  
    48.         var lines = Regex.Split(CommaSeparatedFile.text, pattern);
    49.  
    50.  
    51.        
    52.         var isFieldNameRow = true;
    53.         // We need at minimum the Field Names Row and a Data Row
    54.         if (lines.Length < 2)
    55.         {
    56.             Debug.LogError("The tab delimited file needs to contain at minimum a field name row and a data row.");
    57.             return;
    58.         }
    59.         foreach (var lang in langNames)
    60.         {
    61.             StringTables[lang.Value].Clear();
    62.             Undo.RecordObject(StringTables[lang.Value], "Changed translated text");
    63.             EditorUtility.SetDirty(StringTables[lang.Value]);
    64.         }
    65.         if (SharedTable != null)
    66.         {
    67.             Undo.RecordObject(SharedTable, "Changed translated text");
    68.             EditorUtility.SetDirty(SharedTable);
    69.         }
    70.         for (uint i = 0; i < lines.Length; i++)
    71.         {
    72.             var line = Regex.Split(lines[i], ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
    73.            
    74.            
    75.             if (isFieldNameRow)
    76.             {
    77.                 for (int j = 0; j < line.Length; j++)
    78.                 {
    79.                     Debug.Log(line[j] + "," + j);
    80.          
    81.                     fieldNames.Add(line[j], j);
    82.                 }
    83.                 isFieldNameRow = false;
    84.             }
    85.             else
    86.             {
    87.                 foreach (var lang in langNames) //hax for Japanese loc name since ofr some reason it doesn't match??
    88.                 {
    89.                     if (!fieldNames.ContainsKey(lang.Key))
    90.                     {
    91.                         foreach(var n in fieldNames)
    92.                         {
    93.                             if(n.Key.Contains(lang.Key))
    94.                             {
    95.                                 StringTables[lang.Value].AddEntry(line[fieldNames["Key"]], line[fieldNames[n.Key]].Trim('"'));
    96.                             }
    97.                         }
    98.                     }
    99.                     else
    100.                         StringTables[lang.Value].AddEntry(line[fieldNames["Key"]], line[fieldNames[lang.Key]].Trim('"'));
    101.  
    102.                 }
    103.             }
    104.         }
    105.         Debug.Log("Finished importing text");
    106.     }
    107. }
    108.  
    109. [CustomEditor(typeof(StringImporter))]
    110. public class TestScriptableEditor : Editor
    111. {
    112.     public override void OnInspectorGUI()
    113.     {
    114.         base.OnInspectorGUI();
    115.         var script = (StringImporter)target;
    116.  
    117.         if (GUILayout.Button("Import Language Strings", GUILayout.Height(40)))
    118.         {
    119.             script.Import();
    120.         }
    121.     }
    122. }
    123.  
     
    jamesor and karl_jones like this.
  9. mistergreen2016

    mistergreen2016

    Joined:
    Dec 4, 2016
    Posts:
    214
    The CSV importer worked great but took me a long time to figure out that the column language title MUST match.
    example: English(en) NOT English (en) - No space
     
    Last edited: Jun 19, 2020
unityunity