using System.Collections.Generic; using System.Text; using AlicizaX.Editor; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using UnityEditorInternal; namespace AlicizaX.Localization.Editor { internal class LocalizationSettingsProvider : UnityEditor.SettingsProvider { private SerializedObject _serializedObject; private SerializedProperty _languageTypes; private ReorderableList _languageList; private SerializedProperty _genLangaugeTypePath; private SerializedProperty generateScriptCodeFirstConfig; private List popConfig = new List(); // Track original state for comparison private List _originalLanguages = new List(); private bool _hasUnsavedChanges = false; public LocalizationSettingsProvider() : base("Project/Localization Settings", SettingsScope.Project) { } public override void OnActivate(string searchContext, VisualElement rootElement) { InitGUI(); } private void InitGUI() { var setting = LocalizationConfiguration.Instance; _serializedObject?.Dispose(); _serializedObject = new SerializedObject(setting); _languageTypes = _serializedObject.FindProperty("LanguageTypes"); _genLangaugeTypePath = _serializedObject.FindProperty("_genLangaugeTypePath"); generateScriptCodeFirstConfig = _serializedObject.FindProperty("generateScriptCodeFirstConfig"); // Store original language list _originalLanguages.Clear(); for (int i = 0; i < _languageTypes.arraySize; i++) { _originalLanguages.Add(_languageTypes.GetArrayElementAtIndex(i).stringValue); } // 自定义 ReorderableList _languageList = new ReorderableList(_serializedObject, _languageTypes, draggable: false, displayHeader: true, displayAddButton: true, displayRemoveButton: true); _languageList.drawHeaderCallback = rect => { EditorGUI.LabelField(rect, "Language Types"); }; _languageList.drawElementCallback = (rect, index, isActive, isFocused) => { var element = _languageTypes.GetArrayElementAtIndex(index); rect.y += 2; if (index < 2) // 前两个(中文、英文)禁止修改 { EditorGUI.BeginDisabledGroup(true); EditorGUI.TextField( new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element.stringValue); EditorGUI.EndDisabledGroup(); } else { EditorGUI.BeginChangeCheck(); string newValue = EditorGUI.TextField( new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element.stringValue); if (EditorGUI.EndChangeCheck()) { element.stringValue = newValue; _hasUnsavedChanges = true; } } }; // 禁止删除前两个 _languageList.onCanRemoveCallback = list => { return list.index >= 2; // 只有索引 >=2 的项才能删除 }; // Hook for when a language is added _languageList.onAddCallback = list => { int newIndex = _languageTypes.arraySize; _languageTypes.InsertArrayElementAtIndex(newIndex); var newElement = _languageTypes.GetArrayElementAtIndex(newIndex); newElement.stringValue = "NewLanguage"; _hasUnsavedChanges = true; }; // Hook for when a language is removed _languageList.onRemoveCallback = list => { if (list.index < 2) { EditorUtility.DisplayDialog("Cannot Remove", "The first two languages (ChineseSimplified and English) cannot be removed.", "OK"); return; } // Just remove from the list, no immediate sync _languageTypes.DeleteArrayElementAtIndex(list.index); _hasUnsavedChanges = true; }; popConfig.Clear(); if (setting.LanguageTypeNames.Count > 0) { foreach (var lang in setting.LanguageTypeNames) { string name = lang.Or("Unknown"); popConfig.Add(name); } } } public override void OnGUI(string searchContext) { if (_serializedObject == null || !_serializedObject.targetObject) { InitGUI(); } _serializedObject.Update(); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(_genLangaugeTypePath); EditorDrawing.DrawStringSelectPopup(new GUIContent("Gen Lang"), new GUIContent("None"), popConfig.ToArray(), generateScriptCodeFirstConfig.stringValue, (e) => { generateScriptCodeFirstConfig.stringValue = e; _serializedObject.ApplyModifiedProperties(); LocalizationConfiguration.Save(); }); if (GUILayout.Button("Generate Language Types")) { RegenerateLanguageTypes(); } _languageList.DoLayoutList(); if (EditorGUI.EndChangeCheck()) { _serializedObject.ApplyModifiedProperties(); LocalizationConfiguration.Save(); } // Add Save button EditorGUILayout.Space(10); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); using (new EditorGUI.DisabledGroupScope(!_hasUnsavedChanges)) { if (GUILayout.Button("Save Language Changes", GUILayout.Width(200), GUILayout.Height(30))) { ApplyLanguageChanges(); } } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); if (_hasUnsavedChanges) { EditorGUILayout.HelpBox("You have unsaved language changes. Click 'Save Language Changes' to apply them to all GameLocalizationTable assets.", MessageType.Warning); } } private void RegenerateLanguageTypes() { StringBuilder sb = new StringBuilder(); sb.AppendLine("using System.Collections.Generic;"); sb.AppendLine(""); sb.AppendLine("/// "); sb.AppendLine("/// AutoGenerate"); sb.AppendLine("/// "); sb.AppendLine("public static class LanguageTypes"); sb.AppendLine("{"); for (int i = 0; i < LocalizationConfiguration.Instance.LanguageTypeNames.Count; i++) { sb.AppendLine($"\tpublic const string {LocalizationConfiguration.Instance.LanguageTypeNames[i]} = \"{LocalizationConfiguration.Instance.LanguageTypeNames[i]}\";"); } sb.AppendLine(""); sb.AppendLine("\tpublic static readonly IReadOnlyList Languages = new List"); sb.AppendLine("\t{"); for (int i = 0; i < LocalizationConfiguration.Instance.LanguageTypeNames.Count; i++) { sb.AppendLine($"\t\t\"{LocalizationConfiguration.Instance.LanguageTypeNames[i]}\","); } sb.AppendLine("\t};"); sb.AppendLine(""); sb.AppendLine("\tpublic static string IndexToString(int index)"); sb.AppendLine("\t{"); sb.AppendLine("\t\tif (index < 0 || index >= Languages.Count) return \"Unknown\";"); sb.AppendLine("\t\treturn Languages[index];"); sb.AppendLine("\t}"); sb.AppendLine(""); sb.AppendLine("\tpublic static int StringToIndex(string s)"); sb.AppendLine("\t{"); sb.AppendLine("\t\tint index = -1;"); sb.AppendLine("\t\tfor (int i = 0; i < Languages.Count; i++)"); sb.AppendLine("\t\t{"); sb.AppendLine("\t\t\tif (Languages[i] == s)"); sb.AppendLine("\t\t\t{"); sb.AppendLine("\t\t\t\tindex = i;"); sb.AppendLine("\t\t\t\tbreak;"); sb.AppendLine("\t\t\t}"); sb.AppendLine("\t\t}"); sb.AppendLine(""); sb.AppendLine("\t\treturn index;"); sb.AppendLine("\t}"); sb.AppendLine(""); sb.AppendLine("}"); System.IO.File.WriteAllText(_genLangaugeTypePath.stringValue, sb.ToString()); AssetDatabase.Refresh(); } public override void OnDeactivate() { base.OnDeactivate(); LocalizationConfiguration.Save(); } private void ApplyLanguageChanges() { // Get current language list List currentLanguages = new List(); for (int i = 0; i < _languageTypes.arraySize; i++) { currentLanguages.Add(_languageTypes.GetArrayElementAtIndex(i).stringValue); } // Detect changes List addedLanguages = new List(); List removedLanguages = new List(); Dictionary renamedLanguages = new Dictionary(); // old -> new // Find added languages foreach (var lang in currentLanguages) { if (!_originalLanguages.Contains(lang)) { addedLanguages.Add(lang); } } // Find removed languages foreach (var lang in _originalLanguages) { if (!currentLanguages.Contains(lang)) { removedLanguages.Add(lang); } } // Detect renames (same index, different name) for (int i = 0; i < Mathf.Min(_originalLanguages.Count, currentLanguages.Count); i++) { if (_originalLanguages[i] != currentLanguages[i]) { // Check if this is a rename (not an add/remove) if (!addedLanguages.Contains(currentLanguages[i]) && !removedLanguages.Contains(_originalLanguages[i])) { renamedLanguages[_originalLanguages[i]] = currentLanguages[i]; } } } // Apply changes to all tables string[] guids = AssetDatabase.FindAssets("t:GameLocaizationTable"); int tablesUpdated = 0; foreach (string guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); GameLocaizationTable table = AssetDatabase.LoadAssetAtPath(assetPath); if (table == null) continue; bool tableModified = false; // Handle renames first foreach (var rename in renamedLanguages) { LocalizationLanguage language = table.Languages.Find(lang => lang.LanguageName == rename.Key); if (language != null) { language.LanguageName = rename.Value; language.name = rename.Value; tableModified = true; } } // Handle additions foreach (var newLang in addedLanguages) { // Skip if already exists if (table.Languages.Exists(lang => lang.LanguageName == newLang)) continue; // Create new LocalizationLanguage asset LocalizationLanguage newLanguage = ScriptableObject.CreateInstance(); newLanguage.name = newLang; newLanguage.LanguageName = newLang; newLanguage.Strings = new List(); // Synchronize keys from existing table structure foreach (var section in table.TableSheet) { foreach (var item in section.SectionSheet) { string sectionKey = section.SectionName.Replace(" ", ""); string itemKey = item.Key.Replace(" ", ""); string fullKey = sectionKey + "." + itemKey; newLanguage.Strings.Add(new LocalizationLanguage.LocalizationString { SectionId = section.Id, EntryId = item.Id, Key = fullKey, Value = string.Empty }); } } AssetDatabase.AddObjectToAsset(newLanguage, table); table.Languages.Add(newLanguage); tableModified = true; } // Handle removals foreach (var removedLang in removedLanguages) { LocalizationLanguage languageToDelete = table.Languages.Find(lang => lang.LanguageName == removedLang); if (languageToDelete != null) { table.Languages.Remove(languageToDelete); UnityEngine.Object.DestroyImmediate(languageToDelete, true); tableModified = true; } } if (tableModified) { EditorUtility.SetDirty(table); tablesUpdated++; } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); // Update original languages list _originalLanguages.Clear(); _originalLanguages.AddRange(currentLanguages); _hasUnsavedChanges = false; // Log results if (addedLanguages.Count > 0) Debug.Log($"Added {addedLanguages.Count} language(s) to {tablesUpdated} table(s): {string.Join(", ", addedLanguages)}"); if (removedLanguages.Count > 0) Debug.Log($"Removed {removedLanguages.Count} language(s) from {tablesUpdated} table(s): {string.Join(", ", removedLanguages)}"); if (renamedLanguages.Count > 0) Debug.Log($"Renamed {renamedLanguages.Count} language(s) in {tablesUpdated} table(s)"); EditorUtility.DisplayDialog("Success", $"Language changes applied to {tablesUpdated} GameLocalizationTable(s).", "OK"); } static LocalizationSettingsProvider s_provider; [SettingsProvider] public static SettingsProvider CreateMyCustomSettingsProvider() { if (s_provider == null) { s_provider = new LocalizationSettingsProvider(); } return s_provider; } } }