Search Unity

  1. We are migrating the Unity Forums to Unity Discussions. On July 12, the Unity Forums will become read-only. On July 15, Unity Discussions will become read-only until July 18, when the new design and the migrated forum contents will go live. Read our full announcement for more information and let us know if you have any questions.

Feature Request Generating key constants per localization table

Discussion in 'Localization Tools' started by Lurking-Ninja, Jun 2, 2021.

  1. Hi!

    So, I'm toying with the localization for a while now and I always (literally) end up manually updating the texts for various reasons. So I developed this (or iterations of this):
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine.Localization.Settings;
    3. using UnityEngine.ResourceManagement.AsyncOperations;
    4.  
    5. namespace LurkingNinja.MyGame
    6. {
    7.     public static class I18N
    8.     {
    9.         public const string AYS = "AYS";
    10.         public const string AYS_QUESTION = "MESSAGE";
    11.         public const string TO_DESKTOP = "TO_DESKTOP";
    12.         public const string TO_MENU = "TO_MENU";
    13.  
    14.         public const string GENERIC = "Generic";
    15.         public const string CANCEL = "CANCEL";
    16.         public const string OPTIONS = "OPTIONS";
    17.         public const string QUIT = "QUIT";
    18.  
    19.         public const string MAIN_MENU = "MainMenu";
    20.         public const string SINGLE = "SINGLE";
    21.         public const string MULTI = "MULTI";
    22.         public const string CREDITS = "CREDITS";
    23.  
    24.         public const string VERSION = "VERSION";
    25.  
    26.         public static string Get(in string table, in string key) => LocalizationSettings.StringDatabase
    27.                 .GetLocalizedString(table, key);
    28.  
    29.         public static string Get(in string table, in string key, IList<object> smart) => LocalizationSettings
    30.                 .StringDatabase.GetLocalizedString(table, key, smart);
    31.  
    32.         public static AsyncOperationHandle<string> GetAsync(in string table, in string key) => LocalizationSettings
    33.                 .StringDatabase.GetLocalizedStringAsync(table, key);
    34.  
    35.         public static AsyncOperationHandle<string>  GetAsync(in string table, in string key, IList<object> smart) =>
    36.                 LocalizationSettings.StringDatabase
    37.                         .GetLocalizedStringAsync(table, key, smart);
    38.     }
    39. }

    My question / feature request is to have an option to automatically generate a constant-list file with a static accessible class or whatever for me per table, including the table's reference. Something like the consts in this script above only in separate generated files. More and more Unity tools do this and I like it. I'd like that Localization would join to the generate source code revolution. :)
     
    Last edited by a moderator: Jun 3, 2021
  2. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,570
    We do actually have this down as something to look into in the future.
    The idea was to do something similar and to use the Key id with the Key as the name.
    E.G

    Code (csharp):
    1. public enum MyTable : long
    2. {
    3.  START_GAME = 3423423423,
    4.  EXIT_GAME. = 4534534534,
    5.  // etc
    6. }
    It's likely to be something we look into further once we are out of pre-release, so more of a longer-term feature. There's some areas that need some thinking about, keeping the code in sync especially if the tables are coming from some other sources etc.
    Thanks for the code snippet, ill add it to our task for future reference.
    I imagine you could implement something like this as an extension now :D
     
    Last edited: Nov 7, 2023
    Lurking-Ninja likes this.
  3. In the mean time I came up with this. Obviously quite crude, but it works for now. :)
    Code (CSharp):
    1. using System;
    2. using System.IO;
    3. using System.Text;
    4. using UnityEditor;
    5. using UnityEngine;
    6. using UnityEngine.Localization.Tables;
    7. using Object = UnityEngine.Object;
    8.  
    9. namespace LurkingNinja.MyGame.Editor
    10. {
    11.     public class OnAssetPostProcess : AssetPostprocessor
    12.     {
    13.         private const string PATH = "/_Eclypsia/Code/_generated/i18n/";
    14.  
    15.         private static string GetFullPath(string fileName) => $"{Application.dataPath}{PATH}{fileName}.cs";
    16.  
    17.         private static void GenerateTableAccessorFile(SharedTableData table, string fileName)
    18.         {
    19.             var genPath = GetFullPath(fileName);
    20.             using var writer = new StreamWriter(genPath, false);
    21.             writer.WriteLine(GenerateFileContent(table));
    22.             AssetDatabase.ImportAsset($"Assets{PATH}{fileName}.cs");
    23.         }
    24.  
    25.         private static string GenerateFileContent(SharedTableData table)
    26.         {
    27.             var sb = new StringBuilder();
    28.             sb.Append("//------------------------------------------------------------------------------");
    29.             sb.Append(Environment.NewLine);
    30.             sb.Append("// <auto-generated>");
    31.             sb.Append(Environment.NewLine);
    32.             sb.Append("//     This code was generated by a tool.");
    33.             sb.Append(Environment.NewLine);
    34.             sb.Append($"//     Generated: {DateTime.Now}");
    35.             sb.Append(Environment.NewLine);
    36.             sb.Append("//");
    37.             sb.Append(Environment.NewLine);
    38.             sb.Append("//     Changes to this file may cause incorrect behavior and will be lost if");
    39.             sb.Append(Environment.NewLine);
    40.             sb.Append("//     the code is regenerated.");
    41.             sb.Append(Environment.NewLine);
    42.             sb.Append("// </auto-generated>");
    43.             sb.Append(Environment.NewLine);
    44.             sb.Append("//------------------------------------------------------------------------------");
    45.             sb.Append(Environment.NewLine);
    46.             sb.Append(Environment.NewLine);
    47.             sb.Append("namespace LurkingNinja.MyGame.Internationalization");
    48.             sb.Append(Environment.NewLine);
    49.             sb.Append("{");
    50.             sb.Append(Environment.NewLine);
    51.             sb.Append($"\tpublic static class i18n_{table.TableCollectionName}");
    52.             sb.Append(Environment.NewLine);
    53.             sb.Append("\t{");
    54.             sb.Append(Environment.NewLine);
    55.             sb.Append($"\t\tpublic const string NAME = \"{table.TableCollectionName}\";");    
    56.             sb.Append(Environment.NewLine);
    57.             foreach (var entry in table.Entries)
    58.             {
    59.                 sb.Append($"\t\tpublic const long {entry.Key} = {entry.Id};");    
    60.                 sb.Append(Environment.NewLine);
    61.             }
    62.             sb.Append("\t}");
    63.             sb.Append(Environment.NewLine);
    64.             sb.Append("}");
    65.             sb.Append(Environment.NewLine);
    66.             return sb.ToString();
    67.         }
    68.  
    69.         private static string GetFileName(string fileName) =>
    70.                 Path.GetFileNameWithoutExtension(fileName).Replace(" ", "_");
    71.  
    72.         private static void OnPostprocessAllAssets(
    73.             string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    74.         {
    75.             foreach (var path in importedAssets)
    76.             {
    77.                 var obj = AssetDatabase.LoadAssetAtPath<Object>(path);
    78.                 if(obj is not SharedTableData tableData) continue;
    79.                 GenerateTableAccessorFile(tableData, GetFileName(path));
    80.             }
    81.  
    82.             foreach (var path in deletedAssets)
    83.             {
    84.                 var fileName = GetFileName(path);
    85.                 if (File.Exists(GetFullPath(fileName))) File.Delete(GetFullPath(fileName));
    86.             }
    87.         }
    88.     }
    89. }
    And the generated file looks like this:
    Code (CSharp):
    1. //------------------------------------------------------------------------------
    2. // <auto-generated>
    3. //     This code was generated by a tool.
    4. //     Generated: 6/2/2021 5:20:12 PM
    5. //
    6. //     Changes to this file may cause incorrect behavior and will be lost if
    7. //     the code is regenerated.
    8. // </auto-generated>
    9. //------------------------------------------------------------------------------
    10.  
    11. namespace LurkingNinja.MyGame.Internationalization
    12. {
    13.    public static class i18n_Generic
    14.    {
    15.       public const string NAME = "Generic";
    16.       public const long CANCEL = 9059086336;
    17.       public const long OPTIONS = 38761382981632;
    18.       public const long QUIT = 38937908654080;
    19.    }
    20. }
    The query helper
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine.Localization.Settings;
    3. using UnityEngine.ResourceManagement.AsyncOperations;
    4.  
    5. namespace LurkingNinja.MyGame.Internationalization
    6. {
    7.     public static class I18N
    8.     {
    9.         public static string Get(in string table, in long key) => LocalizationSettings.StringDatabase
    10.                 .GetLocalizedString(table, key);
    11.  
    12.         public static string Get(in string table, in long key, IList<object> smart) => LocalizationSettings
    13.                 .StringDatabase.GetLocalizedString(table, key, smart);
    14.  
    15.         public static AsyncOperationHandle<string> GetAsync(in string table, in long key) => LocalizationSettings
    16.                 .StringDatabase.GetLocalizedStringAsync(table, key);
    17.  
    18.         public static AsyncOperationHandle<string>  GetAsync(in string table, in long key, IList<object> smart) =>
    19.                 LocalizationSettings.StringDatabase
    20.                         .GetLocalizedStringAsync(table, key, smart);
    21.     }
    22. }
    Usage example (with UI Toolkit):
    Code (CSharp):
    1. _buttons[MAIN_MENU].Q<Button>("Quit").text = I18N.Get(i18n_Generic.NAME, i18n_Generic.QUIT);
     
    Last edited by a moderator: Jun 6, 2021
    karl_jones likes this.
  4. My generator evolved a bit:
    Code (CSharp):
    1.         private static string GenerateFileContent(SharedTableData table)
    2.         {
    3.             var sb = new StringBuilder();
    4.             sb.Append("//------------------------------------------------------------------------------");
    5.             sb.Append(Environment.NewLine);
    6.             sb.Append("// <auto-generated>");
    7.             sb.Append(Environment.NewLine);
    8.             sb.Append("//     This code was generated by a tool.");
    9.             sb.Append(Environment.NewLine);
    10.             sb.Append($"//     Generated: {DateTime.Now}");
    11.             sb.Append(Environment.NewLine);
    12.             sb.Append("//");
    13.             sb.Append(Environment.NewLine);
    14.             sb.Append("//     Changes to this file may cause incorrect behavior and will be lost if");
    15.             sb.Append(Environment.NewLine);
    16.             sb.Append("//     the code is regenerated.");
    17.             sb.Append(Environment.NewLine);
    18.             sb.Append("// </auto-generated>");
    19.             sb.Append(Environment.NewLine);
    20.             sb.Append("//------------------------------------------------------------------------------");
    21.             sb.Append(Environment.NewLine);
    22.             sb.Append(Environment.NewLine);
    23.             sb.Append("using System.Collections.Generic;");
    24.             sb.Append(Environment.NewLine);
    25.             sb.Append("using UnityEngine.Localization.Settings;");
    26.             sb.Append(Environment.NewLine);
    27.             sb.Append(Environment.NewLine);
    28.             sb.Append("namespace LurkingNinja.MyGame.Internationalization");
    29.             sb.Append(Environment.NewLine);
    30.             sb.Append("{");
    31.             sb.Append(Environment.NewLine);
    32.             sb.Append($"\tpublic static class i18n_{table.TableCollectionName}");
    33.             sb.Append(Environment.NewLine);
    34.             sb.Append("\t{");
    35.             sb.Append(Environment.NewLine);
    36.             sb.Append($"\t\tprivate const string NAME = \"{table.TableCollectionName}\";");        
    37.             sb.Append(Environment.NewLine);
    38.             foreach (var entry in table.Entries)
    39.             {
    40.                 sb.Append($"\t\tpublic static string {entry.Key}(List<object> o = null) => LocalizationSettings.StringDatabase.GetLocalizedString(NAME, {entry.Id}, o);");
    41.                 sb.Append(Environment.NewLine);
    42.             }
    43.             sb.Append("\t}");
    44.             sb.Append(Environment.NewLine);
    45.             sb.Append("}");
    46.             sb.Append(Environment.NewLine);
    47.             return sb.ToString();
    48.         }

    But it would be great having a way to find out inside the
    SharedTableData.Entries
    that an entry is smart entry or not (any of the implementation is a smart entry or not).
    I guess it is too late to restrict the entry keys to be valid C# identifiers? :)
     
    Last edited by a moderator: Jun 6, 2021
  5. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,570
    Interesting. You may find it easier to put the bulk of your code into a text template file and then just replace the content, a bit like how we generate the MonoBehaviour files etc.
    The Smart Entry is a table-specific thing, so storing it in the Shared Table Data would not really serve many purposes, it would just mean duplicating the data and then having to keep it in sync. Could you generate the file using a StringTableCollection instead? That would let you query the table entries to see if they are Smart. Why do you need to know if they are Smart for this?

    Haha yes and I don't think it would work for everyone. I would run the Key through some sort of converter that removes invalid characters.
     
  6. I know, it's scheduled, I just haven't got around to do that. It is a first concept, a thinking out loud code if you will. :)
    I know, and it works this way for anyone, who doesn't try to handle the entries as one concept. Like me. In my concept all the entries appear once.
    What is weird for me, that you decided to handle the entries separately. I mean semantically the English translation of KEY key and the Hungarian translation of KEY key should not mean different things. I can't cite any situations where you want one translation to be different than the other. I understand that the actual values are in separate tables, data locality is important, they need to be loadable separately, but not their behavior: I don't see any situation where one translation would be smart entry and any other language would not require the same.
    Now, of course I can and I will have to iterate over all of the locales one by one and check if the same thing is smart entry or not, but it really shouldn't be necessary.
    Less error prone if I do not provide default value and actually throw an error if the usage of such entry needs a value passed in. Like I would generate a property for entries don't need values and methods for entries do without default value.
    It is better to catch problems like this in compile time than hunting the "{0}" texts on screen during manual testing. The manual test round-trips are expensive, having compile error right away is cheaper.

    I figured, but I had to try. :D I know, I'm working on the regexp. It's harder than it looks, if you care about the most freedom of choice. C# has a surprisingly permissive syntax.
     
    Last edited by a moderator: Jun 5, 2021
    karl_jones likes this.
  7. Okay, do you have any good entry point to catch this? Because in the OnAssetPostProcessor I get callback on SharedTableData when I edit any StringTables (and presumably any asset tables as well), but I do not get callback for the Collection, I'm guessing only when I add or remove entire tables.
    The SharedTableData doesn't seem to contain any data about its parent.
    So how can I arrive to the Collection in any sensible manner? Unfortunately the new docs format isn't great communicating concepts, only syntax.

    I saw that we have some add/remove/modify/etc events somewhere. Do you think I could tap into these events from an editor script somehow?
     
  8. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,570
    When you get the callback for the asset being edited you could use LocalizationEditorSettings.GetCollectionForSharedTableData(SharedTableData) to get the collection. We do also have editor events as you said which will tell you when certain changes are made although GetCollectionForSharedTableData is probably enough. Just cast the result to a StringTable collection.
     
  9. Thanks! Appreciate it! If someone interested, here's what I ended up with for the time being:
    Code (CSharp):
    1. using System;
    2. using System.IO;
    3. using System.Linq;
    4. using System.Text;
    5. using UnityEditor;
    6. using UnityEditor.Localization;
    7. using UnityEngine;
    8. using UnityEngine.Localization.Tables;
    9. using Object = UnityEngine.Object;
    10.  
    11. namespace LurkingNinja.MyGame.Editor
    12. {
    13.     public class OnAssetPostProcess : AssetPostprocessor
    14.     {
    15.         private const string PATH = "/MyGame/Code/_generated/i18n/";
    16.         private const string TEMPLATE_PATH = "Assets/MyGame/Editor/i18n/i18n.cs.txt";
    17.      
    18.         private static string GetFullPath(string fileName) => $"{Application.dataPath}{PATH}{fileName}.cs";
    19.  
    20.         private static void GenerateTableAccessorFile(SharedTableData table, string fileName)
    21.         {
    22.          
    23.             var genPath = GetFullPath(fileName);
    24.             using var writer = new StreamWriter(genPath, false);
    25.             writer.WriteLine(GenerateFileContent(table));
    26.             AssetDatabase.ImportAsset($"Assets{PATH}{fileName}.cs");
    27.         }
    28.  
    29.         private static string KeyToCSharp(string key)
    30.         {
    31.             if(string.IsNullOrEmpty(key))
    32.                 throw new ArgumentOutOfRangeException(nameof(key), "Translation key cannot be empty or null.");
    33.             if (char.IsNumber(key[0])) key = $"_{key}";
    34.             key = key.Replace(" ", "_");
    35.             return $"@{key}";
    36.         }
    37.  
    38.         private static bool IsSmart(StringTableCollection tableCollection, long id)
    39.         {
    40.             if(tableCollection == null) return false;
    41.             return tableCollection.StringTables.Select(stable =>
    42.                     stable.GetEntry(id)).Any(tableEntry => tableEntry.IsSmart);
    43.         }
    44.  
    45.         private static string GenerateFileContent(SharedTableData table)
    46.         {
    47.             var tableCollection = LocalizationEditorSettings
    48.                     .GetStringTableCollection(table.TableCollectionNameGuid);
    49.             var template = AssetDatabase.LoadAssetAtPath<TextAsset>(TEMPLATE_PATH).text;
    50.             var sb = new StringBuilder();
    51.  
    52.             foreach (var entry in table.Entries)
    53.             {
    54.                 var key = KeyToCSharp(entry.Key);
    55.                 sb.Append($"\t\t\tpublic static string {key}");
    56.                 if(IsSmart(tableCollection, entry.Id)) sb.Append("(List<object> o)");
    57.                 sb.Append($" => LocalizationSettings.StringDatabase.GetLocalizedString(NAME, {entry.Id}, o);");
    58.                 sb.Append(Environment.NewLine);
    59.             }
    60.             return string.Format(
    61.                 template, DateTime.Now, KeyToCSharp(table.TableCollectionName), table.TableCollectionName, sb);
    62.         }
    63.  
    64.         private static string GetFileName(string fileName) =>
    65.                 Path.GetFileNameWithoutExtension(fileName).Replace(" ", "_");
    66.  
    67.         private static void OnPostprocessAllAssets(
    68.             string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    69.         {
    70.             foreach (var path in importedAssets)
    71.             {
    72.                 var obj = AssetDatabase.LoadAssetAtPath<Object>(path);
    73.                 if(obj is not SharedTableData tableData) continue;
    74.                 GenerateTableAccessorFile(tableData, GetFileName(path));
    75.             }
    76.  
    77.             foreach (var path in deletedAssets)
    78.             {
    79.                 var fileName = GetFileName(path);
    80.                 if (File.Exists(GetFullPath(fileName))) File.Delete(GetFullPath(fileName));
    81.             }
    82.         }
    83.     }
    84. }
    i18n.cs.txt
    Code (CSharp):
    1. //------------------------------------------------------------------------------
    2. // <auto-generated>
    3. //     This code was generated by a tool.
    4. //     Generated: {0}
    5. //
    6. //     Changes to this file may cause incorrect behavior and will be lost if
    7. //     the code is regenerated.
    8. // </auto-generated>
    9. //------------------------------------------------------------------------------
    10.  
    11. using System.Collections.Generic;
    12. using UnityEngine.Localization.Settings;
    13.  
    14. namespace LurkingNinja.MyGame.Internationalization
    15. {{
    16.     public static partial class I18N
    17.     {{
    18.         public static class {1}
    19.         {{
    20.             private const string NAME = "{2}";
    21. {3}
    22.         }}
    23.     }}
    24. }}
    Code (CSharp):
    1. //------------------------------------------------------------------------------
    2. // <auto-generated>
    3. //     This code was generated by a tool.
    4. //     Generated: 6/5/2021 5:35:36 PM
    5. //
    6. //     Changes to this file may cause incorrect behavior and will be lost if
    7. //     the code is regenerated.
    8. // </auto-generated>
    9. //------------------------------------------------------------------------------
    10.  
    11. using System.Collections.Generic;
    12. using UnityEngine.Localization.Settings;
    13.  
    14. namespace LurkingNinja.MyGame.Internationalization
    15. {
    16.     public static partial class I18N
    17.     {
    18.         public static class @MainMenu
    19.         {
    20.             private const string NAME = "MainMenu";
    21.             public static string @VERSION(List<object> o) => LocalizationSettings.StringDatabase.GetLocalizedString(NAME, 20358541312, o);
    22.             public static string @SINGLE => LocalizationSettings.StringDatabase.GetLocalizedString(NAME, 8068326141952);
    23.         }
    24.     }
    25. }
    And I have a partial class
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine.Localization.Settings;
    3. using UnityEngine.ResourceManagement.AsyncOperations;
    4.  
    5. namespace LurkingNinja.MyGame.Internationalization
    6. {
    7.     public static partial class I18N
    8.     {}
    9. }
     
    Last edited by a moderator: Jun 6, 2021
  10. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,570
    Lurking-Ninja likes this.
  11. D12294

    D12294

    Joined:
    Oct 6, 2020
    Posts:
    81
    @Lurking-Ninja Thanks for your Scripts! I'm trying to use them with following set up:

    Assets/Editor/LurkingNinja/I18N.cs.txt
    Assets/Editor/LurkingNinja/OnAssetPostProcess.cs
    Assets/Editor/LurkingNinja/I18N.cs
    (with partial class code)

    and in OnAssetPostProcess for pathes

    Code (CSharp):
    1. private const string PATH = "/Editor/LurkingNinja/";
    2. private const string TEMPLATE_PATH = "Assets/Editor/LurkingNinja/i18n.cs.txt";
    When I change a Key in the String Tables and save a file with name New_Table_Shared_Data is generated but also this error is thrown:

    NullReferenceException: Object reference not set to an instance of an object
    LurkingNinja.MyGame.Editor.OnAssetPostProcess+<>c.<IsSmart>b__5_1 (UnityEngine.Localization.Tables.StringTableEntry tableEntry) (at Assets/Editor/LurkingNinja/OnAssetPostProcess.cs:42)
    System.Linq.Enumerable.Any[TSource] (System.Collections.Generic.IEnumerable`1[T] source, System.Func`2[T,TResult] predicate) (at <61774763be294c9f8e2c781f10819224>:0)
    LurkingNinja.MyGame.Editor.OnAssetPostProcess.IsSmart (UnityEditor.Localization.StringTableCollection tableCollection, System.Int64 id) (at Assets/Editor/LurkingNinja/OnAssetPostProcess.cs:41)
    LurkingNinja.MyGame.Editor.OnAssetPostProcess.GenerateFileContent (UnityEngine.Localization.Tables.SharedTableData table) (at Assets/Editor/LurkingNinja/OnAssetPostProcess.cs:56)
    LurkingNinja.MyGame.Editor.OnAssetPostProcess.GenerateTableAccessorFile (UnityEngine.Localization.Tables.SharedTableData table, System.String fileName) (at Assets/Editor/LurkingNinja/OnAssetPostProcess.cs:25)
    LurkingNinja.MyGame.Editor.OnAssetPostProcess.OnPostprocessAllAssets (System.String[] importedAssets, System.String[] deletedAssets, System.String[] movedAssets, System.String[] movedFromAssetPaths) (at Assets/Editor/LurkingNinja/OnAssetPostProcess.cs:74)
    System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at <00c558282d074245ab3496e2d108079b>:0)
    Rethrow as TargetInvocationException: Exception has been thrown by the target of an invocation.
    System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at <00c558282d074245ab3496e2d108079b>:0)
    System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) (at <00c558282d074245ab3496e2d108079b>:0)
    UnityEditor.AssetPostprocessingInternal.InvokeMethod (System.Reflection.MethodInfo method, System.Object[] args) (at <948074e677924ec29383d02747cdda34>:0)
    UnityEditor.AssetPostprocessingInternal.PostprocessAllAssets (System.String[] importedAssets, System.String[] addedAssets, System.String[] deletedAssets, System.String[] movedAssets, System.String[] movedFromPathAssets, System.Boolean didDomainReload) (at <948074e677924ec29383d02747cdda34>:0)
    UnityEditor.EditorApplication:Internal_CallGlobalEventHandler()

    if my set up is right then it looks like there is an unhandled case for my String Table in the OnAssetPostProcess which causes this error. When I have Time later this day I'll try your code in a new clean Project with a single entry String Table and give you feedback if that solved the error or if it still exists.
     
  12. This shouldn't point to the Editor, it needs to be part of the run-time if you're using this for translating in-game text.

    As for the NRE, I have no idea at first sight, I have (very) basic smart entries and they are working, so it is possible that I missed something if you have more complex cases.
    Of course any feedback is welcome and appreciate it!
     
    D12294 likes this.
  13. hafu985

    hafu985

    Joined:
    Aug 27, 2021
    Posts:
    8
    Thx for you work, it saved a lot of my time !)
     
    Lurking-Ninja and karl_jones like this.