using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; namespace Thry{ public class Localization : ScriptableObject { [SerializeField] Shader[] ValidateWithShaders; [SerializeField] string DefaultLanguage = "English"; [SerializeField] string[] Languages = new string[0]; [SerializeField]int SelectedLanguage = -1; [SerializeField] string[] _keys = new string[0]; [SerializeField] string[] _values = new string[0]; [SerializeField] string[] _defaultKeys = new string[0]; [SerializeField] string[] _defaultValues = new string[0]; Dictionary _localizedStrings = new Dictionary(); Dictionary _defaultKeyValues = new Dictionary(); string[] _allLanguages; bool _isLoaded = false; bool _couldNotLoad = false; // Use public static Localization Load(string guid) { string path = AssetDatabase.GUIDToAssetPath(guid); Localization l = AssetDatabase.LoadAssetAtPath(path); if(l == null) { l = ScriptableObject.CreateInstance(); l._couldNotLoad = true; return l; } l.Load(); return l; } void Load() { _allLanguages = new string[Languages.Length + 1]; _allLanguages[0] = DefaultLanguage; Array.Copy(Languages, 0, _allLanguages, 1, Languages.Length); _localizedStrings = new Dictionary(); for (int i = 0; i < _keys.Length; i++) { string[] ar = new string[Languages.Length]; Array.Copy(_values, i * Languages.Length , ar, 0, Languages.Length); _localizedStrings[_keys[i]] = ar; } _isLoaded = true; } public static Localization Create() { Localization l = ScriptableObject.CreateInstance(); l._allLanguages = new string[l.Languages.Length + 1]; l._allLanguages[0] = l.DefaultLanguage; Array.Copy(l.Languages, 0, l._allLanguages, 1, l.Languages.Length); l._localizedStrings = new Dictionary(); return l; } public void DrawDropdown() { if(_couldNotLoad) { EditorGUILayout.HelpBox("Could not load localization file", MessageType.Warning); return; } EditorGUI.BeginChangeCheck(); SelectedLanguage = EditorGUILayout.Popup(SelectedLanguage + 1, _allLanguages) - 1; if(EditorGUI.EndChangeCheck()) { ShaderEditor.Active.Reload(); EditorUtility.SetDirty(this); AssetDatabase.SaveAssets(); } } public string Get(MaterialProperty prop, string defaultValue) { if(_localizedStrings.ContainsKey(prop.name)) { string[] ar = _localizedStrings[prop.name]; if(ar.Length > SelectedLanguage && SelectedLanguage > -1) { return ar[SelectedLanguage] ?? defaultValue; } } _defaultKeyValues[prop.name] = defaultValue; return defaultValue; } public string Get(MaterialProperty prop, FieldInfo field, string defaultValue) { string id = prop.name + "." + field.DeclaringType + "." + field.Name; return Get(id, defaultValue); } public string Get(string id, string defaultValue) { if(id == null) return defaultValue; if (_localizedStrings.ContainsKey(id)) { string[] ar = _localizedStrings[id]; if (ar.Length > SelectedLanguage && SelectedLanguage > -1) { return ar[SelectedLanguage] ?? defaultValue; } } _defaultKeyValues[id] = defaultValue; return defaultValue; } // Managment void AddLanguage(string language) { if (System.Array.IndexOf(Languages, language) == -1) { System.Array.Resize(ref Languages, Languages.Length + 1); Languages[Languages.Length - 1] = language; string[] keys = _localizedStrings.Keys.ToArray(); foreach(string key in keys) { string[] ar = _localizedStrings[key]; System.Array.Resize(ref ar, ar.Length + 1); ar[ar.Length - 1] = null; _localizedStrings[key] = ar; } Save(); } } void RemoveLanguage(string language) { int index = System.Array.IndexOf(Languages, language); if (index != -1) { if(Languages.Length > 1) { for (int i = index; i < Languages.Length - 1; i++) { Languages[i] = Languages[i + 1]; } System.Array.Resize(ref Languages, Languages.Length - 1); string[] keys = _localizedStrings.Keys.ToArray(); foreach (string key in keys) { string[] ar = _localizedStrings[key]; for (int i = index; i < ar.Length - 1; i++) { ar[i] = ar[i + 1]; } System.Array.Resize(ref ar, ar.Length - 1); _localizedStrings[key] = ar; } }else { Languages = new string[0]; _localizedStrings = new Dictionary(); } Save(); } } void Save() { _defaultKeys = _defaultKeyValues.Keys.ToArray(); _defaultValues = _defaultKeyValues.Values.ToArray(); _keys = _localizedStrings.Keys.ToArray(); _values = new string[_keys.Length * Languages.Length]; for (int i = 0; i < _keys.Length; i++) { string[] ar = _localizedStrings[_keys[i]]; Array.Copy(ar, 0, _values, i * Languages.Length, ar.Length); } EditorUtility.SetDirty(this); AssetDatabase.SaveAssets(); } void Clear() { _defaultKeys = new string[0]; _defaultValues = new string[0]; _keys = new string[0]; _values = new string[0]; Languages = new string[0]; } [MenuItem("Assets/Thry/Shaders/Create Locale File", false)] static void CreateLocale() { Localization locale = ScriptableObject.CreateInstance(); Shader[] shaders = Selection.objects.Select(o => o as Shader).ToArray(); string fileName = Path.GetFileNameWithoutExtension(AssetDatabase.GetAssetPath(shaders[0])); string folderPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(shaders[0])); locale.ValidateWithShaders = shaders; AssetDatabase.CreateAsset(locale, folderPath + "/" + fileName + "_Locale.asset"); AssetDatabase.SaveAssets(); } [MenuItem("Assets/Thry/Shaders/Create Locale File", true)] static bool ValidateCreateLocale() { return Selection.objects.All(o => o is Shader); } [MenuItem("Assets/Thry/Shaders/Locale Property", false)] static void CreateShaderProperty() { Localization l = Selection.activeObject as Localization; string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(l)); string outS = $"[HideInInspector] {ShaderEditor.PROPERTY_NAME_LOCALE} (\"{guid}\", Float) = 0"; EditorGUIUtility.systemCopyBuffer = outS; } [MenuItem("Assets/Thry/Shaders/Locale Property", true)] static bool ValidateCreateShaderProperty() { return Selection.activeObject is Localization; } [CustomEditor(typeof(Localization))] public class LocaleEditor : Editor { List<(string key, string defaultValue, string newValue)> _missingKeys = new List<(string key, string defaultValue, string newValue)>(); int _selectedLanguageIndex = 0; string _searchById = ""; string _searchByTranslation = ""; string[] _searchResults = new string[0]; string _translateByValueIn = ""; string _translateByValueOut = ""; string _autoTranslateLanguageShortCode = "EN"; string ToCSVString(string s) { return "\"" + s.Replace("\"", "“") + "\""; } string FromCSVString(string s) { return s.Trim('"').Replace("“", "\""); } void ExportAsCSV(Localization locale) { string path = EditorUtility.SaveFilePanel("Export as CSV", "", locale.name, "csv"); if (string.IsNullOrEmpty(path) == false) { System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach (string language in locale.Languages) { sb.Append("," + ToCSVString(language)); } sb.AppendLine(); for(int i = 0;i < locale._keys.Length; i++) { sb.Append(ToCSVString(locale._keys[i])); for(int j = 0; j < locale.Languages.Length; j++) { sb.Append("," + ToCSVString(locale._values[i * locale.Languages.Length + j])); } sb.AppendLine(); } File.WriteAllText(path, sb.ToString()); } } void LoadFromCSV(Localization locale) { string path = EditorUtility.OpenFilePanel("Load from CSV", "", "csv"); if (string.IsNullOrEmpty(path) == false) { string[] lines = File.ReadAllLines(path); if (lines.Length > 0) { locale.Clear(); string[] languages = lines[0].Split(','); for (int i = 1; i < languages.Length; i++) { locale.AddLanguage(FromCSVString(languages[i])); } for (int i = 1; i < lines.Length; i++) { string[] values = lines[i].Split(','); if (values.Length > 0) { string key = FromCSVString(values[0]); for (int j = 1; j < values.Length; j++) { locale._values[(i - 1) * (languages.Length - 1) + j - 1] = FromCSVString(values[j]); } } } locale.Save(); } } } void UpdateMissing(Localization locale) { _missingKeys.Clear(); foreach(string key in locale._localizedStrings.Keys) { if (string.IsNullOrEmpty(locale._localizedStrings[key][_selectedLanguageIndex])) { _missingKeys.Add((key, locale._defaultKeyValues[key], locale._defaultKeyValues[key])); } } } void UpdateData(Localization locale) { locale.Load(); // create _defaultKeyValues if(locale._defaultKeyValues == null) { locale._defaultKeyValues = new Dictionary(); } for(int i = 0; i < locale._defaultKeys.Length; i++) { if(locale._defaultKeyValues.ContainsKey(locale._defaultKeys[i]) == false) locale._defaultKeyValues.Add(locale._defaultKeys[i], locale._defaultValues[i]); } // add all keys from shader foreach(var kv in locale._defaultKeyValues) { string key = kv.Key; if(key.StartsWith("footer_")) continue; if(key == ShaderEditor.PROPERTY_NAME_MASTER_LABEL) continue; if(key == ShaderEditor.PROPERTY_NAME_LABEL_FILE) continue; if(key == ShaderEditor.PROPERTY_NAME_LOCALE) continue; if(key == ShaderEditor.PROPERTY_NAME_ON_SWAP_TO_ACTIONS) continue; if(key == ShaderEditor.PROPERTY_NAME_SHADER_VERSION) continue; if(key == ShaderEditor.PROPERTY_NAME_EDITOR_DETECT) continue; if (string.IsNullOrEmpty(kv.Value) == false && !locale._localizedStrings.ContainsKey(kv.Key)) { locale._localizedStrings.Add(kv.Key, new string[locale.Languages.Length]); } } // make missing keys a list of all keys that have an empty string in the selected language UpdateMissing(locale); } private void OnEnable() { Localization locale = (Localization)target; locale.Load(); UpdateData(locale); } private void Awake() { Localization locale = (Localization)target; locale.Load(); UpdateData(locale); } public override void OnInspectorGUI() { Localization locale = (Localization)target; if(!locale._isLoaded) { UpdateData(locale); } if(GUILayout.Button("Save")) { locale.Save(); } EditorGUILayout.PropertyField(serializedObject.FindProperty("ValidateWithShaders")); if(GUILayout.Button("Load Properties from Shaders")) { // for each shader create a material & material editor so that the data is loaded into the localization object foreach(Shader s in locale.ValidateWithShaders) { ShaderEditor se = new ShaderEditor(); se.FakePartialInitilizationForLocaleGathering(s); } } locale.DefaultLanguage = EditorGUILayout.TextField("Default Language", locale.DefaultLanguage); EditorGUILayout.LabelField("Languages"); for (int i = 0; i < locale.Languages.Length; i++) { EditorGUILayout.BeginHorizontal(); locale.Languages[i] = EditorGUILayout.TextField(locale.Languages[i]); if (GUILayout.Button("Remove")) { locale.RemoveLanguage(locale.Languages[i]); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Add")) { locale.AddLanguage("New Language"); } EditorGUILayout.EndHorizontal(); // popup for selecting language EditorGUI.BeginChangeCheck(); _selectedLanguageIndex = EditorGUILayout.Popup("Language to edit", _selectedLanguageIndex, locale.Languages); if(EditorGUI.EndChangeCheck()) { _missingKeys.Clear(); } if(GUILayout.Button("Update")) { UpdateData(locale); } EditorGUILayout.Space(20); EditorGUILayout.LabelField("Import / Export", EditorStyles.boldLabel); if(GUILayout.Button("Import from CSV")) LoadFromCSV(locale); if(locale.Languages.Length == 0) return; if(GUILayout.Button("Export to CSV")) ExportAsCSV(locale); EditorGUILayout.Space(20); EditorGUILayout.LabelField("Missing Entries", EditorStyles.boldLabel); (string,string,string) kvToRemove = default; for(int i = 0; i < _missingKeys.Count && i < 10; i++) { var kv = _missingKeys[i]; EditorGUILayout.BeginHorizontal(); kv.newValue = EditorGUILayout.DelayedTextField(kv.key, kv.newValue); if(GUILayout.Button("Skip", GUILayout.Width(50))) { kvToRemove = kv; } if(GUILayout.Button("Apply", GUILayout.Width(50))) { if (!locale._localizedStrings.ContainsKey(kv.key)) { locale._localizedStrings.Add(kv.key, new string[locale.Languages.Length]); } locale._localizedStrings[kv.key][_selectedLanguageIndex] = kv.newValue; kvToRemove = kv; } _missingKeys[i] = kv; EditorGUILayout.EndHorizontal(); } if(_missingKeys.Count > 10) { EditorGUILayout.LabelField("..."); } if(kvToRemove != default) { _missingKeys.Remove(kvToRemove); } EditorGUILayout.Space(20); EditorGUILayout.LabelField("Automatic Translation using Google", EditorStyles.boldLabel); _autoTranslateLanguageShortCode = EditorGUILayout.TextField("Language Short Code", _autoTranslateLanguageShortCode); EditorGUILayout.HelpBox("Short code must be valid short code. See https://cloud.google.com/translate/docs/languages for a list of valid short codes.", MessageType.Info); if(Event.current.type == EventType.MouseDown && GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition)) { Application.OpenURL("https://cloud.google.com/translate/docs/languages"); } if(GUILayout.Button("Auto Translate")) { int _missingKeysCount = _missingKeys.Count; int i = 0; foreach((string key, string defaultValue, string newValue) in _missingKeys) { EditorUtility.DisplayProgressBar("Auto Translate", $"Translating {i}/{_missingKeysCount}", (float)i / _missingKeysCount); try { if (!locale._localizedStrings.ContainsKey(key)) { locale._localizedStrings.Add(key, new string[locale.Languages.Length]); } locale._localizedStrings[key][_selectedLanguageIndex] = WebHelper.Translate(defaultValue, _autoTranslateLanguageShortCode); } catch(Exception e) { Debug.LogError(e); } i += 1; } EditorUtility.ClearProgressBar(); locale.Save(); } EditorGUILayout.Space(20); EditorGUILayout.LabelField("Translate entries by value", EditorStyles.boldLabel); EditorGUILayout.HelpBox("This will search all properties and translate all that have the exact display name with the selected value. Suggested usecase: Panning, UV", MessageType.Info); _translateByValueIn = EditorGUILayout.TextField("Search for", _translateByValueIn); _translateByValueOut = EditorGUILayout.TextField("Translate with", _translateByValueOut); if(GUILayout.Button("Execute")) { foreach(var kv in locale._defaultKeyValues) { if(kv.Value == _translateByValueIn) { locale._localizedStrings[kv.Key][_selectedLanguageIndex] = _translateByValueOut; } } UpdateMissing(locale); } EditorGUILayout.Space(20); EditorGUILayout.LabelField("Existing Entries", EditorStyles.boldLabel); EditorGUI.BeginChangeCheck(); _searchById = EditorGUILayout.TextField("Search by id", _searchById); _searchByTranslation = EditorGUILayout.TextField("Search by translation", _searchByTranslation); if(EditorGUI.EndChangeCheck()) { List res = new List(); foreach (string key in locale._localizedStrings.Keys) { if(locale._localizedStrings[key][_selectedLanguageIndex] == null) continue; if(locale._localizedStrings[key][_selectedLanguageIndex].IndexOf(_searchByTranslation, StringComparison.OrdinalIgnoreCase) != -1 && key.IndexOf(_searchById, StringComparison.OrdinalIgnoreCase) != -1) { res.Add(key); } } _searchResults = res.ToArray(); } EditorGUILayout.Space(5); if(_searchById.Length > 0 || _searchByTranslation.Length > 0) { int count = 0; foreach (string key in _searchResults) { if(count > 50) { EditorGUILayout.LabelField("..."); break; } EditorGUILayout.BeginHorizontal(); string value = EditorGUILayout.DelayedTextField(key, locale._localizedStrings[key][_selectedLanguageIndex]); if (GUILayout.Button("Remove", GUILayout.Width(65))) { locale._localizedStrings[key][_selectedLanguageIndex] = ""; } EditorGUILayout.EndHorizontal(); locale._localizedStrings[key][_selectedLanguageIndex] = value; count++; } } } } } }