using System; using System.Collections.Generic; using System.IO; using System.Text; using AlicizaX.Editor; using UnityEditor; using UnityEditorInternal; using UnityEngine; namespace AlicizaX.Localization.Editor { internal sealed class LocalizationSettingsProvider : EditorWindow { private const string WindowTitle = "Localization Settings"; private const string MenuPath = "AlicizaX/Localization/Open Localization Settings"; private SerializedObject _serializedObject; private SerializedProperty _languageTypes; private SerializedProperty _genLangaugeTypePath; private SerializedProperty _generateScriptCodeFirstConfig; private SerializedProperty _generateLanguageTypesNamespace; private SerializedProperty _generateLanguageTypesTemplate; private ReorderableList _languageList; private readonly List _languagePopupOptions = new(); private readonly List _originalLanguages = new(); private readonly List _localizationTables = new(); private Vector2 _scrollPosition; private bool _hasUnsavedChanges; [MenuItem(MenuPath)] private static void Open() { LocalizationSettingsProvider window = GetWindow(); window.titleContent = new GUIContent(WindowTitle); window.minSize = new Vector2(760f, 520f); window.Show(); } private void OnEnable() { InitGUI(); RefreshLocalizationTables(); } private void OnFocus() { RefreshLocalizationTables(); } private void OnDisable() { _serializedObject?.Dispose(); _serializedObject = null; LocalizationConfiguration.Save(); } private void InitGUI() { LocalizationConfiguration setting = LocalizationConfiguration.Instance; _serializedObject?.Dispose(); _serializedObject = new SerializedObject(setting); _languageTypes = _serializedObject.FindProperty("LanguageTypes"); _genLangaugeTypePath = _serializedObject.FindProperty("_genLangaugeTypePath"); _generateScriptCodeFirstConfig = _serializedObject.FindProperty("generateScriptCodeFirstConfig"); _generateLanguageTypesNamespace = _serializedObject.FindProperty("generateLanguageTypesNamespace"); _generateLanguageTypesTemplate = _serializedObject.FindProperty("generateLanguageTypesTemplate"); CaptureOriginalLanguages(); BuildLanguageList(); RefreshLanguagePopupOptions(); } private void CaptureOriginalLanguages() { _originalLanguages.Clear(); if (_languageTypes == null) { return; } for (int i = 0; i < _languageTypes.arraySize; i++) { _originalLanguages.Add(_languageTypes.GetArrayElementAtIndex(i).stringValue); } } private void BuildLanguageList() { _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) => { SerializedProperty element = _languageTypes.GetArrayElementAtIndex(index); rect.y += 2f; bool isBuiltInLanguage = index < 2; using (new EditorGUI.DisabledGroupScope(isBuiltInLanguage)) { 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 => list.index >= 2; _languageList.onAddCallback = list => { int newIndex = _languageTypes.arraySize; _languageTypes.InsertArrayElementAtIndex(newIndex); SerializedProperty newElement = _languageTypes.GetArrayElementAtIndex(newIndex); newElement.stringValue = "NewLanguage"; _hasUnsavedChanges = true; }; _languageList.onRemoveCallback = list => { if (list.index < 2) { EditorUtility.DisplayDialog("Cannot Remove", "The first two languages (ChineseSimplified and English) cannot be removed.", "OK"); return; } _languageTypes.DeleteArrayElementAtIndex(list.index); _hasUnsavedChanges = true; }; } private void RefreshLanguagePopupOptions() { _languagePopupOptions.Clear(); IReadOnlyList languageNames = LocalizationConfiguration.Instance.LanguageTypeNames; for (int i = 0; i < languageNames.Count; i++) { string name = languageNames[i].Or("Unknown"); _languagePopupOptions.Add(name); } } private void RefreshLocalizationTables() { _localizationTables.Clear(); string[] guids = AssetDatabase.FindAssets("t:GameLocaizationTable"); for (int i = 0; i < guids.Length; i++) { string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]); GameLocaizationTable table = AssetDatabase.LoadAssetAtPath(assetPath); if (table != null) { _localizationTables.Add(table); } } } private void OnGUI() { if (_serializedObject == null || !_serializedObject.targetObject) { InitGUI(); } _serializedObject.Update(); RefreshLanguagePopupOptions(); EditorGUILayout.Space(); _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); DrawLanguageTypesSection(); EditorGUILayout.Space(8f); DrawLanguageTypesGenerateSection(); EditorGUILayout.Space(8f); DrawLocalizationTablesSection(); EditorGUILayout.EndScrollView(); if (_serializedObject.ApplyModifiedProperties()) { LocalizationConfiguration.Save(); } } private void DrawLanguageTypesSection() { using (new EditorDrawing.BorderBoxScope(new GUIContent("Language Types"), roundedBox: false)) { _languageList.DoLayoutList(); EditorGUILayout.Space(10f); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); using (new EditorGUI.DisabledGroupScope(!_hasUnsavedChanges)) { if (GUILayout.Button("Save Language Changes", GUILayout.Width(200f), GUILayout.Height(30f))) { 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 DrawLanguageTypesGenerateSection() { using (new EditorDrawing.BorderBoxScope(new GUIContent("Generate LanguageTypes"), roundedBox: false)) { EditorGUILayout.PropertyField(_genLangaugeTypePath, new GUIContent("File Path")); EditorGUILayout.PropertyField(_generateLanguageTypesNamespace, new GUIContent("Namespace")); EditorDrawing.DrawStringSelectPopup( new GUIContent("生成多语言Key索引文件时的注释首选语言"), new GUIContent("None"), _languagePopupOptions.ToArray(), _generateScriptCodeFirstConfig.stringValue, selected => { _generateScriptCodeFirstConfig.stringValue = selected; _serializedObject.ApplyModifiedProperties(); LocalizationConfiguration.Save(); }); EditorGUILayout.LabelField("Template", EditorStyles.boldLabel); EditorGUILayout.HelpBox("Use placeholders: {NAMESPACE_START}, {NAMESPACE_END}, {LANGUAGE_CONSTANTS}, {LANGUAGE_LIST}", MessageType.None); EditorGUI.BeginChangeCheck(); string template = EditorGUILayout.TextArea(_generateLanguageTypesTemplate.stringValue, GUILayout.MinHeight(240f)); if (EditorGUI.EndChangeCheck()) { _generateLanguageTypesTemplate.stringValue = template; } EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Reset Template", GUILayout.Width(140f))) { _generateLanguageTypesTemplate.stringValue = LocalizationConfiguration.DefaultTemplate; } if (GUILayout.Button("Generate Language Types", GUILayout.Width(180f))) { RegenerateLanguageTypes(); } EditorGUILayout.EndHorizontal(); } } private void DrawLocalizationTablesSection() { using (new EditorDrawing.BorderBoxScope(new GUIContent("LocalizationTable"), roundedBox: false)) { if (GUILayout.Button("Refresh LocalizationTable List", GUILayout.Width(220f))) { RefreshLocalizationTables(); } EditorGUILayout.Space(4f); if (_localizationTables.Count == 0) { EditorGUILayout.HelpBox("No LocalizationTable assets found in the project.", MessageType.Info); return; } for (int i = 0; i < _localizationTables.Count; i++) { DrawLocalizationTableEntry(_localizationTables[i], i); if (i < _localizationTables.Count - 1) { EditorGUILayout.Space(6f); } } } } private void DrawLocalizationTableEntry(GameLocaizationTable table, int index) { SerializedObject tableSerializedObject = new SerializedObject(table); SerializedProperty pathProperty = tableSerializedObject.FindProperty("GenerateScriptCodePath"); SerializedProperty namespaceProperty = tableSerializedObject.FindProperty("GenerateScriptCodeNamespace"); using (new EditorDrawing.BorderBoxScope(new GUIContent($"LocalizationTable {index + 1}"), roundedBox: false)) { using (new EditorGUI.DisabledGroupScope(true)) { EditorGUILayout.ObjectField("Table", table, typeof(GameLocaizationTable), false); } tableSerializedObject.Update(); EditorGUILayout.PropertyField(pathProperty, new GUIContent("Gen Code Path")); EditorGUILayout.PropertyField(namespaceProperty, new GUIContent("Namespace")); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Gen Code", GUILayout.Width(120f))) { tableSerializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(table); LocalizationWindowUtility.GenerateCode(table); } EditorGUILayout.EndHorizontal(); if (tableSerializedObject.ApplyModifiedProperties()) { EditorUtility.SetDirty(table); AssetDatabase.SaveAssets(); } } } private void RegenerateLanguageTypes() { string filePath = _genLangaugeTypePath.stringValue; if (string.IsNullOrWhiteSpace(filePath)) { EditorUtility.DisplayDialog("Invalid Path", "LanguageTypes output path cannot be empty.", "OK"); return; } string directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } string template = string.IsNullOrEmpty(_generateLanguageTypesTemplate.stringValue) ? LocalizationConfiguration.DefaultTemplate : _generateLanguageTypesTemplate.stringValue; string generatedCode = BuildLanguageTypesCode(template, _generateLanguageTypesNamespace.stringValue, LocalizationConfiguration.Instance.LanguageTypeNames); File.WriteAllText(filePath, generatedCode, Encoding.UTF8); AssetDatabase.Refresh(); } private static string BuildLanguageTypesCode(string template, string namespaceName, IReadOnlyList languages) { StringBuilder constantsBuilder = new StringBuilder(); StringBuilder listBuilder = new StringBuilder(); for (int i = 0; i < languages.Count; i++) { string languageName = languages[i]; constantsBuilder.AppendLine($" public const string {languageName} = \"{languageName}\";"); listBuilder.AppendLine($" \"{languageName}\","); } string namespaceStart = string.IsNullOrWhiteSpace(namespaceName) ? string.Empty : $"namespace {namespaceName}{Environment.NewLine}{{{Environment.NewLine}"; string namespaceEnd = string.IsNullOrWhiteSpace(namespaceName) ? string.Empty : $"{Environment.NewLine}}}"; return template .Replace("{NAMESPACE_START}", namespaceStart) .Replace("{NAMESPACE_END}", namespaceEnd) .Replace("{LANGUAGE_CONSTANTS}", constantsBuilder.ToString().TrimEnd()) .Replace("{LANGUAGE_LIST}", listBuilder.ToString().TrimEnd()); } private void ApplyLanguageChanges() { List currentLanguages = new List(); for (int i = 0; i < _languageTypes.arraySize; i++) { currentLanguages.Add(_languageTypes.GetArrayElementAtIndex(i).stringValue); } List addedLanguages = new List(); List removedLanguages = new List(); Dictionary renamedLanguages = new Dictionary(); foreach (string lang in currentLanguages) { if (!_originalLanguages.Contains(lang)) { addedLanguages.Add(lang); } } foreach (string lang in _originalLanguages) { if (!currentLanguages.Contains(lang)) { removedLanguages.Add(lang); } } for (int i = 0; i < Mathf.Min(_originalLanguages.Count, currentLanguages.Count); i++) { if (_originalLanguages[i] != currentLanguages[i]) { if (!addedLanguages.Contains(currentLanguages[i]) && !removedLanguages.Contains(_originalLanguages[i])) { renamedLanguages[_originalLanguages[i]] = currentLanguages[i]; } } } 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; foreach (KeyValuePair 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; } } foreach (string newLang in addedLanguages) { if (table.Languages.Exists(lang => lang.LanguageName == newLang)) { continue; } LocalizationLanguage newLanguage = ScriptableObject.CreateInstance(); newLanguage.name = newLang; newLanguage.LanguageName = newLang; newLanguage.Strings = new List(); foreach (GameLocaizationTable.TableData section in table.TableSheet) { foreach (GameLocaizationTable.SheetItem item in section.SectionSheet) { string sectionKey = section.SectionName.Replace(" ", string.Empty); string itemKey = item.Key.Replace(" ", string.Empty); 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; } foreach (string 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(); _originalLanguages.Clear(); _originalLanguages.AddRange(currentLanguages); _hasUnsavedChanges = false; RefreshLocalizationTables(); 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"); } } }