diff --git a/Editor/Localization.meta b/Editor/Localization.meta new file mode 100644 index 0000000..ea0678b --- /dev/null +++ b/Editor/Localization.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8b833f8e32e7456bb382bc73386a0297 +timeCreated: 1758196091 \ No newline at end of file diff --git a/Editor/Localization/EditorIcons.meta b/Editor/Localization/EditorIcons.meta new file mode 100644 index 0000000..da22f4a --- /dev/null +++ b/Editor/Localization/EditorIcons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 98a1fa01bd8596b49a4e4878a3cc0d24 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Localization/EditorIcons/mask.png b/Editor/Localization/EditorIcons/mask.png new file mode 100644 index 0000000..00a3737 Binary files /dev/null and b/Editor/Localization/EditorIcons/mask.png differ diff --git a/Editor/Localization/EditorIcons/mask.png.meta b/Editor/Localization/EditorIcons/mask.png.meta new file mode 100644 index 0000000..0d3db7b --- /dev/null +++ b/Editor/Localization/EditorIcons/mask.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 74bd1971b983bb84a83b097f4ce40d02 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Localization/LocalizationConfiguration.cs b/Editor/Localization/LocalizationConfiguration.cs new file mode 100644 index 0000000..d7f15b3 --- /dev/null +++ b/Editor/Localization/LocalizationConfiguration.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AlicizaX.Localization.Editor +{ + [AlicizaX.Editor.Setting.FilePath("ProjectSettings/LocalizationConfiguration.asset")] + public class LocalizationConfiguration : AlicizaX.Editor.Setting.ScriptableSingleton + { + [SerializeField] private List LanguageTypes = new List() + { + /// + /// 简体中文。 + /// + "ChineseSimplified", + + /// + /// 英语。 + /// + "English", + + /// + /// 日语。 + /// + "Japanese", + + /// + /// 俄语。 + /// + "Russian", + }; + + + public IReadOnlyList LanguageTypeNames => LanguageTypes; + } +} diff --git a/Editor/Localization/LocalizationConfiguration.cs.meta b/Editor/Localization/LocalizationConfiguration.cs.meta new file mode 100644 index 0000000..b89f465 --- /dev/null +++ b/Editor/Localization/LocalizationConfiguration.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 26c8edcdf58c4806a5646a5ec155cc85 +timeCreated: 1758196101 \ No newline at end of file diff --git a/Editor/Localization/LocalizationSettingsProvider.cs b/Editor/Localization/LocalizationSettingsProvider.cs new file mode 100644 index 0000000..9180efe --- /dev/null +++ b/Editor/Localization/LocalizationSettingsProvider.cs @@ -0,0 +1,91 @@ + +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; + + 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"); + + // 自定义 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; + + // 只显示可编辑的 string 字段 + element.stringValue = EditorGUI.TextField( + new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), + element.stringValue); + }; + } + + public override void OnGUI(string searchContext) + { + if (_serializedObject == null || !_serializedObject.targetObject) + { + InitGUI(); + } + + _serializedObject.Update(); + EditorGUI.BeginChangeCheck(); + + // 使用 ReorderableList 绘制 + _languageList.DoLayoutList(); + + if (EditorGUI.EndChangeCheck()) + { + _serializedObject.ApplyModifiedProperties(); + LocalizationConfiguration.Save(); + } + } + + public override void OnDeactivate() + { + base.OnDeactivate(); + LocalizationConfiguration.Save(); + } + + static LocalizationSettingsProvider s_provider; + + [SettingsProvider] + public static SettingsProvider CreateMyCustomSettingsProvider() + { + if (s_provider == null) + { + s_provider = new LocalizationSettingsProvider(); + } + return s_provider; + } + } +} diff --git a/Editor/Localization/LocalizationSettingsProvider.cs.meta b/Editor/Localization/LocalizationSettingsProvider.cs.meta new file mode 100644 index 0000000..c49286a --- /dev/null +++ b/Editor/Localization/LocalizationSettingsProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5647a4aaf19f42bf852a701955e888f5 +timeCreated: 1758196661 \ No newline at end of file diff --git a/Editor/Localization/LocalizationTableWindow.meta b/Editor/Localization/LocalizationTableWindow.meta new file mode 100644 index 0000000..9ba70ac --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f61eb05483ae45343a3667fcb64c8410 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Localization/LocalizationTableWindow/ExportImport.meta b/Editor/Localization/LocalizationTableWindow/ExportImport.meta new file mode 100644 index 0000000..0a13239 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/ExportImport.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: baaf3e4bb10c71c478bcf4f9ce2d3d23 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Localization/LocalizationTableWindow/ExportImport/LocalizationExporter.cs b/Editor/Localization/LocalizationTableWindow/ExportImport/LocalizationExporter.cs new file mode 100644 index 0000000..59a1c20 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/ExportImport/LocalizationExporter.cs @@ -0,0 +1,194 @@ +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Text; +using UnityEngine; + +namespace AlicizaX.Localization.Editor +{ + public static class LocalizationExporter + { + public static void ExportLocalizationToCSV(LocalizationWindowData windowData, string filePath) + { + if (windowData == null || windowData.Languages.Count == 0) + { + Debug.LogError("Localization window data is empty."); + return; + } + + var allKeys = new List(); + foreach (var section in windowData.TableSheet) + { + foreach (var item in section.Items) + { + string key = $"{section.Id}:{item.Id}"; + allKeys.Add(key); + } + } + + var languageLookups = windowData.Languages.ToDictionary( + lang => lang.Entry.LanguageName, + lang => lang.TableSheet.SelectMany(section => section.Items) + .ToDictionary(item => $"{item.Parent.Id}:{item.Id}", item => item.Value) + ); + + using (var writer = new StreamWriter(filePath, false, Encoding.UTF8)) + { + + writer.Write("Key"); + foreach (var lang in windowData.Languages) + { + writer.Write(","); + WriteEscapedCsvField(writer, lang.Entry.LanguageName); + } + writer.WriteLine(); + + foreach (var key in allKeys) + { + WriteEscapedCsvField(writer, key); + foreach (var lang in windowData.Languages) + { + writer.Write(","); + var translations = languageLookups[lang.Entry.LanguageName]; + string value = translations.ContainsKey(key) ? translations[key] : ""; + WriteEscapedCsvField(writer, value); + } + writer.WriteLine(); + } + } + + Debug.Log($"Localization exported successfully to: {filePath}"); + } + + + private static void WriteEscapedCsvField(StreamWriter writer, string field) + { + if (string.IsNullOrEmpty(field)) + { + return; + } + + + if (field.Contains(",") || field.Contains("\"") || field.Contains("\n") || field.Contains("\r")) + { + + writer.Write("\""); + writer.Write(field.Replace("\"", "\"\"")); + writer.Write("\""); + } + else + { + writer.Write(field); + } + } + + public static void ImportLocalizationFromCSV(LocalizationWindowData windowData, string filePath) + { + if (!File.Exists(filePath)) + { + Debug.LogError($"CSV file not found: {filePath}"); + return; + } + + using (var reader = new StreamReader(filePath, Encoding.UTF8)) + { + + string headerLine = reader.ReadLine(); + if (headerLine == null) + { + Debug.LogError("CSV file is empty"); + return; + } + + var headers = ParseCsvLine(headerLine); + var languageNames = headers.Skip(1).ToList(); + + + var existingLanguages = windowData.Languages + .Where(l => languageNames.Contains(l.Entry.LanguageName)) + .ToDictionary(l => l.Entry.LanguageName); + + if (existingLanguages.Count == 0) + { + Debug.LogError("No matching languages in the Localization Window Data."); + return; + } + + + string line; + while ((line = reader.ReadLine()) != null) + { + var fields = ParseCsvLine(line); + if (fields.Length == 0) continue; + + string key = fields[0]; + string[] keyParts = key.Split(':'); + + // Get sectionId and itemId + if (keyParts.Length != 2 || + !int.TryParse(keyParts[0], out int sectionId) || + !int.TryParse(keyParts[1], out int itemId)) + { + Debug.LogWarning($"Invalid key format: {key}"); + continue; + } + + for (int i = 0; i < languageNames.Count; i++) + { + if (i + 1 >= fields.Length) break; + + string langName = languageNames[i]; + if (existingLanguages.TryGetValue(langName, out var languageData)) + { + string value = fields[i + 1]; + + var matchingSection = languageData.TableSheet.FirstOrDefault(sec => sec.Id == sectionId); + var matchingItem = matchingSection?.Items.FirstOrDefault(item => item.Id == itemId); + if (matchingItem != null) matchingItem.Value = value; + } + } + } + } + + Debug.Log("Localization CSV imported successfully into existing languages."); + } + + private static string[] ParseCsvLine(string line) + { + var fields = new List(); + var currentField = new StringBuilder(); + bool inQuotes = false; + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + + if (c == '"' && (i == 0 || line[i - 1] != '\\')) + { + if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') + { + currentField.Append('"'); + i++; + } + else + { + inQuotes = !inQuotes; + } + } + else if (c == ',' && !inQuotes) + { + fields.Add(currentField.ToString()); + currentField.Clear(); + } + else + { + currentField.Append(c); + } + } + + fields.Add(currentField.ToString()); + + return fields.ToArray(); + } + } +} diff --git a/Editor/Localization/LocalizationTableWindow/ExportImport/LocalizationExporter.cs.meta b/Editor/Localization/LocalizationTableWindow/ExportImport/LocalizationExporter.cs.meta new file mode 100644 index 0000000..a931401 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/ExportImport/LocalizationExporter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a40ca89d13c4a3448bd77e3abcfefaad \ No newline at end of file diff --git a/Editor/Localization/LocalizationTableWindow/GameLocalizationEditor.cs b/Editor/Localization/LocalizationTableWindow/GameLocalizationEditor.cs new file mode 100644 index 0000000..c39609f --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/GameLocalizationEditor.cs @@ -0,0 +1,113 @@ +// using UnityEditor.Build; +// using UnityEditor; +// using UnityEngine; +// using UHFPS.Runtime; +// using UHFPS.Tools; +// using ThunderWire.Editors; +// using System.Linq; +// using System; +// +// namespace AlicizaX.Localization.Editor +// { +// [CustomEditor(typeof(GameLocalization))] +// public class GameLocalizationEditor : InspectorEditor +// { +// private const string LOCALIZATION_SYMBOL = "UHFPS_LOCALIZATION"; +// +// public override void OnInspectorGUI() +// { +// EditorDrawing.DrawInspectorHeader(new GUIContent("Game Localization"), Target); +// EditorGUILayout.Space(); +// +// serializedObject.Update(); +// { +// string[] languages = new string[0]; +// if (Target.LocalizationTable != null) +// { +// languages = Target.LocalizationTable.Languages +// .Where(x => x != null) +// .Select((x, i) => x.LanguageName.Or("Unknown Language " + i)) +// .ToArray(); +// } +// +// Properties.Draw("LocalizationTable"); +// DrawDefaultLanguageSelector(languages); +// Properties.Draw("ShowWarnings"); +// +// if (Application.isPlaying) +// { +// EditorGUILayout.Space(); +// using (new EditorGUI.DisabledGroupScope(languages.Length <= 0)) +// { +// if (GUILayout.Button("Set Language", GUILayout.Height(25f))) +// { +// Target.ChangeLanguage(Target.DefaultLanguage); +// string name = languages[Target.DefaultLanguage]; +// Debug.Log("Language set to " + name); +// } +// } +// } +// } +// serializedObject.ApplyModifiedProperties(); +// +// if (!Application.isPlaying) +// { +// EditorGUILayout.Space(); +// EditorGUILayout.HelpBox("To enable or disable UHFPS localization, click the button below. A scripting symbol will automatically be included in the player settings to allow you to use Game Localization.", MessageType.Info); +// EditorGUILayout.Space(1f); +// +// string toggleText = CheckActivation() ? "Disable" : "Enable"; +// if (GUILayout.Button($"{toggleText} GLoc Localization", GUILayout.Height(25f))) +// { +// ToggleScriptingSymbol(); +// } +// } +// } +// +// private void DrawDefaultLanguageSelector(string[] languages) +// { +// if (Target.LocalizationTable != null && languages.Length > 0) +// { +// string selected = languages.Length > 0 && Target.DefaultLanguage >= 0 +// ? languages[Target.DefaultLanguage] +// : string.Empty; +// +// EditorDrawing.DrawStringSelectPopup(new GUIContent("Default Language"), new GUIContent("Language"), languages, selected, (lang) => +// { +// int index = Array.FindIndex(languages, x => lang == x); +// Properties["DefaultLanguage"].intValue = index; +// serializedObject.ApplyModifiedProperties(); +// }); +// } +// else +// { +// Properties["DefaultLanguage"].intValue = 0; +// } +// } +// +// private bool CheckActivation() +// { +// var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup; +// var namedBuildTarget = NamedBuildTarget.FromBuildTargetGroup(buildTargetGroup); +// string defines = PlayerSettings.GetScriptingDefineSymbols(namedBuildTarget); +// return defines.Contains(LOCALIZATION_SYMBOL); +// } +// +// private void ToggleScriptingSymbol() +// { +// var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup; +// var namedBuildTarget = NamedBuildTarget.FromBuildTargetGroup(buildTargetGroup); +// +// string defines = PlayerSettings.GetScriptingDefineSymbols(namedBuildTarget); +// string[] definesParts = defines.Split(';'); +// +// if (defines.Contains(LOCALIZATION_SYMBOL)) +// definesParts = definesParts.Except(new[] { LOCALIZATION_SYMBOL }).ToArray(); +// else +// definesParts = definesParts.Concat(new[] { LOCALIZATION_SYMBOL }).ToArray(); +// +// defines = string.Join(";", definesParts); +// PlayerSettings.SetScriptingDefineSymbols(namedBuildTarget, defines); +// } +// } +// } diff --git a/Editor/Localization/LocalizationTableWindow/GameLocalizationEditor.cs.meta b/Editor/Localization/LocalizationTableWindow/GameLocalizationEditor.cs.meta new file mode 100644 index 0000000..86ba5dd --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/GameLocalizationEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 83da11971febf634ba7c141b42267ae6 \ No newline at end of file diff --git a/Editor/Localization/LocalizationTableWindow/LocalizationTableWindow.cs b/Editor/Localization/LocalizationTableWindow/LocalizationTableWindow.cs new file mode 100644 index 0000000..34ae13d --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/LocalizationTableWindow.cs @@ -0,0 +1,577 @@ +using System; +using System.IO; +using System.Collections.Generic; +using AlicizaX.Editor; +using Sirenix.OdinInspector; +using UnityEditor.IMGUI.Controls; +using UnityEditor; +using UnityEngine; + + +namespace AlicizaX.Localization.Editor +{ + public class LocalizationTableWindow : EditorWindow + { + private List allTables = new List(); // 存储所有找到的GameLocaizationTable + private string[] tableDisplayNames; // 用于下拉框显示的名称数组 + private int selectedTableIndex = 0; // 当前选中的索引 + private string selectString = string.Empty; + private GameLocaizationTable currentTable; // 当前选中的GameLocaizationTable + + private const float k_LanguagesWidth = 200f; + private const float k_TableSheetWidth = 200f; + private float Spacing => EditorGUIUtility.standardVerticalSpacing * 2; + + private GUIStyle miniLabelButton => new GUIStyle(EditorStyles.miniButton) + { + font = EditorStyles.miniBoldLabel.font, + fontSize = EditorStyles.miniBoldLabel.fontSize + }; + + public class WindowSelection + { + public TreeViewItem TreeViewItem; + } + + public sealed class LanguageSelect : WindowSelection + { + public TempLanguageData Language; + } + + public sealed class SectionSelect : WindowSelection + { + public SheetSectionTreeView Section; + } + + public sealed class ItemSelect : WindowSelection + { + public SheetItemTreeView Item; + } + + + private LocalizationWindowData windowData; + + private SearchField searchField; + private string searchString; + private Vector2 scrollPosition; + + [SerializeField] private TreeViewState languagesTreeViewState; + private LanguagesTreeView languagesTreeView; + + [SerializeField] private TreeViewState tableSheetTreeViewState; + private TableSheetTreeView tableSheetTreeView; + + private WindowSelection selection = null; + private bool globalExpanded = false; + + private void CreateGUI() + { + searchField = new SearchField(); + } + + private void OnDestroy() + { + SaveSelection(); + } + + private void OnEnable() + { + RefreshTableList(); + } + + private void OnDisable() + { + SaveSelection(); + } + + private void RefreshTableList() + { + allTables.Clear(); + string[] guids = AssetDatabase.FindAssets("t:GameLocaizationTable"); + foreach (string guid in guids) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + GameLocaizationTable table = AssetDatabase.LoadAssetAtPath(assetPath); + if (table != null) + { + allTables.Add(table); + } + } + + tableDisplayNames = new string[allTables.Count]; + for (int i = 0; i < allTables.Count; i++) + { + tableDisplayNames[i] = allTables[i].name; + } + + if (allTables.Count > 0 && selectedTableIndex >= allTables.Count) + { + selectedTableIndex = 0; + } + + + var selectIndex = selectedTableIndex; + var lastSelect = EditorPrefs.GetString("LastSelectedGameLocaizationTable", string.Empty); + if (!string.IsNullOrEmpty(lastSelect)) + { + for (int i = 0; i < allTables.Count; i++) + { + var path = AssetDatabase.GetAssetPath(allTables[i]); + if (path.Equals(lastSelect)) + { + selectIndex = i; + } + } + } + + if (selectIndex <= tableDisplayNames.Length - 1 && selectIndex >= 0) + { + selectString = tableDisplayNames[selectIndex]; + } + + UpdateCurrentTable(); + } + + private void UpdateCurrentTable() + { + if (allTables.Count > 0 && selectedTableIndex >= 0 && selectedTableIndex < allTables.Count) + { + currentTable = allTables[selectedTableIndex]; + SaveSelection(); + } + else + { + currentTable = null; + } + + InitializeTreeView(); + } + + private void SaveSelection() + { + if (currentTable != null) + { + string path = AssetDatabase.GetAssetPath(currentTable); + EditorPrefs.SetString("LastSelectedGameLocaizationTable", path); + } + } + + private void InitializeTreeView() + { + if (!currentTable) return; + + LocalizationWindowUtility.BuildWindowData(currentTable, out windowData); + + + foreach (var section in windowData.TableSheet) + { + section.IsExpanded = globalExpanded; + } + + + languagesTreeViewState = new TreeViewState(); + languagesTreeView = new(languagesTreeViewState, windowData, currentTable) + { + OnLanguageSelect = (s) => selection = s + }; + + + tableSheetTreeViewState = new TreeViewState(); + tableSheetTreeView = new(tableSheetTreeViewState, windowData) + { + OnTableSheetSelect = (s) => selection = s + }; + } + + private void OnGUI() + { + Rect toolbarRect = new(0, 0, position.width, 20f); + GUI.Box(toolbarRect, GUIContent.none, EditorStyles.toolbar); + + + float buttonWidth = 100f; + float spacing = 5f; + + + Rect leftTitle = new(toolbarRect.xMin, 0, 40, 20f); + Rect leftPop = new(leftTitle.xMin + 40, 0, 200, 20f); + Rect saveBtn = new(toolbarRect.xMax - buttonWidth - spacing, 0, buttonWidth, 20f); + Rect genBtn = new(saveBtn.xMin - buttonWidth - spacing, 0, buttonWidth, 20f); + Rect importBtn = new(genBtn.xMin - buttonWidth - spacing, 0, buttonWidth, 20f); + Rect exportBtn = new(importBtn.xMin - buttonWidth - spacing, 0, buttonWidth, 20f); + + EditorGUI.LabelField(leftTitle, "Table", EditorStyles.boldLabel); + EditorDrawing.DrawStringSelectPopup(leftPop, tableDisplayNames, selectString, (e) => + { + selectString = e; + selectedTableIndex = allTables.FindIndex(table => table.name == e); + UpdateCurrentTable(); + }); + + if (currentTable == null) return; + + + if (GUI.Button(exportBtn, "Export CSV", EditorStyles.toolbarButton)) + { + string path = EditorUtility.SaveFilePanel("Export CSV", "", "Localization", "csv"); + LocalizationExporter.ExportLocalizationToCSV(windowData, path); + } + + + if (GUI.Button(importBtn, "Import CSV", EditorStyles.toolbarButton)) + { + string path = EditorUtility.OpenFilePanel("Export CSV", "", "csv"); + LocalizationExporter.ImportLocalizationFromCSV(windowData, path); + } + + if (GUI.Button(genBtn, "Gen Code", EditorStyles.toolbarButton)) + { + LocalizationWindowUtility.GenerateCode(currentTable); + } + + if (GUI.Button(saveBtn, "Save Asset", EditorStyles.toolbarButton)) + { + BuildLocalizationTable(); + EditorUtility.SetDirty(currentTable); + AssetDatabase.SaveAssets(); + } + + + Rect languagesRect = new Rect(5f, 25f, k_LanguagesWidth, position.height - 35f); + languagesTreeView.OnGUI(languagesRect); + + + float tableSheetStartX = languagesRect.xMax + 5f; + Rect tableSheetRect = new Rect(tableSheetStartX, 25f, k_TableSheetWidth, position.height - 35f); + tableSheetTreeView.OnGUI(tableSheetRect); + + + if (selection != null) + { + float inspectorStartX = tableSheetRect.xMax + 5f; + Rect inspectorRect = new Rect(inspectorStartX, 25f, position.width - inspectorStartX - 5f, position.height - 30f); + + if (selection is LanguageSelect language) + { + string title = language.Language.Entry.LanguageName; + GUIContent inspectorTitle = EditorGUIUtility.TrTextContentWithIcon($" INSPECTOR ({title})", "PrefabVariant On Icon"); + EditorDrawing.DrawHeaderWithBorder(ref inspectorRect, inspectorTitle, 20f, false); + + Rect inspectorViewRect = inspectorRect; + inspectorViewRect.y += Spacing; + inspectorViewRect.yMax -= Spacing; + inspectorViewRect.xMin += Spacing; + inspectorViewRect.xMax -= Spacing; + + GUILayout.BeginArea(inspectorViewRect); + OnDrawLanguageInspector(language); + GUILayout.EndArea(); + } + else if (selection is SectionSelect section) + { + string title = section.Section.Name; + GUIContent inspectorTitle = EditorGUIUtility.TrTextContentWithIcon($" INSPECTOR ({title})", "PrefabVariant On Icon"); + EditorDrawing.DrawHeaderWithBorder(ref inspectorRect, inspectorTitle, 20f, false); + + Rect inspectorViewRect = inspectorRect; + inspectorViewRect.y += Spacing; + inspectorViewRect.yMax -= Spacing; + inspectorViewRect.xMin += Spacing; + inspectorViewRect.xMax -= Spacing; + + GUILayout.BeginArea(inspectorViewRect); + OnDrawSectionInspector(section); + GUILayout.EndArea(); + } + else if (selection is ItemSelect item) + { + string title = item.Item.Key; + GUIContent inspectorTitle = EditorGUIUtility.TrTextContentWithIcon($" INSPECTOR ({title})", "PrefabVariant On Icon"); + EditorDrawing.DrawHeaderWithBorder(ref inspectorRect, inspectorTitle, 20f, false); + + Rect inspectorViewRect = inspectorRect; + inspectorViewRect.y += Spacing; + inspectorViewRect.yMax -= Spacing; + inspectorViewRect.xMin += Spacing; + inspectorViewRect.xMax -= Spacing; + + GUILayout.BeginArea(inspectorViewRect); + OnDrawSectionItemInspector(item); + GUILayout.EndArea(); + } + } + } + + private void OnDrawSectionInspector(SectionSelect section) + { + // section name change + EditorGUI.BeginChangeCheck(); + { + section.Section.Name = EditorGUILayout.TextField("Name", section.Section.Name); + } + if (EditorGUI.EndChangeCheck()) + { + section.TreeViewItem.displayName = section.Section.Name; + } + + using (new EditorGUI.DisabledGroupScope(true)) + { + int childerCount = section.TreeViewItem.children?.Count ?? 0; + EditorGUILayout.IntField(new GUIContent("Keys"), childerCount); + } + + EditorGUILayout.Space(2); + EditorDrawing.Separator(); + EditorGUILayout.Space(1); + + using (new EditorGUI.DisabledGroupScope(true)) + { + EditorGUILayout.LabelField("Id: " + section.Section.Id, EditorStyles.miniBoldLabel); + } + } + + private void OnDrawSectionItemInspector(ItemSelect item) + { + // item key change + EditorGUI.BeginChangeCheck(); + { + item.Item.Key = EditorGUILayout.TextField("Key", item.Item.Key); + } + if (EditorGUI.EndChangeCheck()) + { + item.TreeViewItem.displayName = item.Item.Key; + } + + EditorGUILayout.Space(2); + EditorDrawing.Separator(); + EditorGUILayout.Space(1); + + using (new EditorGUI.DisabledGroupScope(true)) + { + string parentName = item.Item.Parent.Name; + string parentText = item.Item.Parent.Id + $" ({parentName})"; + EditorGUILayout.LabelField("Parent Id: " + parentText, EditorStyles.miniBoldLabel); + EditorGUILayout.LabelField("Id: " + item.Item.Id, EditorStyles.miniBoldLabel); + } + } + + private void OnDrawLanguageInspector(LanguageSelect selection) + { + var language = selection.Language; + var entry = language.Entry; + var treeView = selection.TreeViewItem; + + // using (new EditorDrawing.BorderBoxScope(false)) + // { + // // language name change + // Rect nameRect = EditorGUILayout.GetControlRect(); + // + // Rect renameAssetRect = nameRect; + // renameAssetRect.xMin = nameRect.xMax + 2f; + // renameAssetRect.width = EditorGUIUtility.singleLineHeight; + // + // using (new EditorGUI.DisabledGroupScope(entry.Asset == null)) + // { + // GUIContent editIcon = EditorGUIUtility.IconContent("editicon.sml", "Rename"); + // if (GUI.Button(renameAssetRect, editIcon, EditorStyles.iconButton)) + // { + // string assetPath = AssetDatabase.GetAssetPath(entry.Asset); + // string newName = "(Language) " + entry.LanguageName; + // AssetDatabase.RenameAsset(assetPath, newName); + // } + // } + // + // } + + using (new EditorGUI.DisabledGroupScope(entry.Asset == null)) + { + // Draw search field + EditorGUILayout.Space(); + + GUIContent expandText = new GUIContent("Expand"); + float expandWidth = miniLabelButton.CalcSize(expandText).x; + + var searchRect = EditorGUILayout.GetControlRect(); + searchRect.xMax -= (expandWidth + 2f); + searchString = searchField.OnGUI(searchRect, searchString); + + Rect expandRect = new Rect(searchRect.xMax + 2f, searchRect.y, expandWidth, searchRect.height); + expandRect.y -= 1f; + + using (new EditorDrawing.BackgroundColorScope("#F7E987")) + { + if (GUI.Button(expandRect, expandText, miniLabelButton)) + { + globalExpanded = !globalExpanded; + foreach (var section in language.TableSheet) + { + section.Reference.IsExpanded = globalExpanded; + } + } + } + + if (entry.Asset != null) + { + // Draw localization data + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + { + foreach (var section in GetSearchResult(language, searchString)) + { + DrawLocalizationKey(section); + } + } + EditorGUILayout.EndScrollView(); + } + else + { + EditorGUILayout.HelpBox("To begin editing localization data, you must first assign a localization asset.", MessageType.Warning); + } + } + } + + private void DrawLocalizationKey(TempSheetSection section) + { + if (section.Items == null || section.Items.Count == 0) + return; + + using (new EditorDrawing.BorderBoxScope(false)) + { + string sectionName = section.Name.Replace(" ", ""); + section.Reference.IsExpanded = EditorGUILayout.Foldout(section.Reference.IsExpanded, new GUIContent(sectionName), true, EditorDrawing.Styles.miniBoldLabelFoldout); + + // Show section keys when expanded + if (section.Reference.IsExpanded) + { + foreach (var item in section.Items) + { + string keyName = item.Key.Replace(" ", ""); + string key = sectionName + "." + keyName; + + if (IsMultiline(item.Value)) + key += " (Multiline)"; + + using (new EditorGUILayout.VerticalScope(GUI.skin.box)) + { + // Display the expandable toggle + using (new EditorGUILayout.HorizontalScope(GUI.skin.box)) + { + item.IsExpanded = EditorGUILayout.Foldout(item.IsExpanded, new GUIContent(key), true, EditorDrawing.Styles.miniBoldLabelFoldout); + } + + if (item.IsExpanded) + { + // Show TextArea when expanded + float height = (EditorGUIUtility.standardVerticalSpacing + EditorGUIUtility.singleLineHeight) * 3; + height += EditorGUIUtility.standardVerticalSpacing; + + item.Scroll = EditorGUILayout.BeginScrollView(item.Scroll, GUILayout.Height(height)); + item.Value = EditorGUILayout.TextArea(item.Value, GUILayout.ExpandHeight(true)); + EditorGUILayout.EndScrollView(); + } + else + { + // Show TextField when collapsed + item.Value = EditorGUILayout.TextField(item.Value); + } + } + } + } + } + + EditorGUILayout.Space(1f); + } + + private IEnumerable GetSearchResult(TempLanguageData languageData, string search) + { + if (!string.IsNullOrEmpty(search)) + { + List searchResult = new(); + + foreach (var section in languageData.TableSheet) + { + List sectionItems = new(); + string sectionName = section.Name.Replace(" ", ""); + + foreach (var item in section.Items) + { + string keyName = item.Key.Replace(" ", ""); + string key = sectionName + "." + keyName; + + if (key.Contains(search)) + sectionItems.Add(item); + } + + searchResult.Add(new TempSheetSection() + { + Items = sectionItems, + Reference = section.Reference + }); + } + + return searchResult; + } + + return languageData.TableSheet; + } + + private bool IsMultiline(string text) + { + return text.Contains("\n") || text.Contains("\r"); + } + + private void BuildLocalizationTable() + { + // 1. build table sheet + currentTable.TableSheet = new(); + foreach (var section in windowData.TableSheet) + { + GameLocaizationTable.TableData tableData = new GameLocaizationTable.TableData(section.Name, section.Id); + + foreach (var item in section.Items) + { + GameLocaizationTable.SheetItem sheetItem = new GameLocaizationTable.SheetItem(item.Key, item.Id, item.isGen); + tableData.SectionSheet.Add(sheetItem); + } + + currentTable.TableSheet.Add(tableData); + } + + // 2. build table sheet for each language + IList languages = new List(); + foreach (var language in windowData.Languages) + { + if (language.Entry.Asset == null) + continue; + + LocalizationLanguage asset = language.Entry.Asset; + IList strings = new List(); + + foreach (var section in language.TableSheet) + { + string sectionKey = section.Name.Replace(" ", ""); + foreach (var item in section.Items) + { + string itemKey = item.Key.Replace(" ", ""); + string key = sectionKey + "." + itemKey; + + strings.Add(new() + { + SectionId = section.Id, + EntryId = item.Id, + Key = key, + Value = item.Value + }); + } + } + + asset.LanguageName = language.Entry.LanguageName; + asset.Strings = new(strings); + + languages.Add(asset); + EditorUtility.SetDirty(asset); + } + + currentTable.Languages = new(languages); + } + } +} diff --git a/Editor/Localization/LocalizationTableWindow/LocalizationTableWindow.cs.meta b/Editor/Localization/LocalizationTableWindow/LocalizationTableWindow.cs.meta new file mode 100644 index 0000000..76bab38 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/LocalizationTableWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8b5ae4bf262d12a4899f2a46672ed072 \ No newline at end of file diff --git a/Editor/Localization/LocalizationTableWindow/LocalizationWindowUtility.cs b/Editor/Localization/LocalizationTableWindow/LocalizationWindowUtility.cs new file mode 100644 index 0000000..dcf6bcb --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/LocalizationWindowUtility.cs @@ -0,0 +1,642 @@ +using System.Linq; +using System.Collections.Generic; +using System.Text; +using UnityEditor; +using UnityEngine; + +namespace AlicizaX.Localization.Editor +{ + public sealed class LanguageEntry + { + public string LanguageName; + public LocalizationLanguage Asset; + + public LanguageEntry() + { + } + + public LanguageEntry(LocalizationLanguage asset) + { + LanguageName = asset.LanguageName; + Asset = asset; + } + } + + public class SheetSectionTreeView + { + public int Id; + public string Name; + + public SheetSectionTreeView() + { + Id = Random.Range(10000000, 99999999); + Name = LocalizationWindowUtility.NULL; + } + } + + public class SheetItemTreeView + { + public int Id; + public string Key; + public bool isGen; + public SheetSectionTreeView Parent; + + public SheetItemTreeView() + { + Id = Random.Range(10000000, 99999999); + Key = LocalizationWindowUtility.NULL; + } + } + + public sealed class TempSheetItem + { + // Reference to the original item (shared instance) + public SheetItemTreeView Reference; + + // Language-specific localization value + public string Value; + + public int Id => Reference.Id; + + public string Key + { + get => Reference.Key; + set => Reference.Key = value; + } + + public SheetSectionTreeView Parent => Reference.Parent; + public Vector2 Scroll { get; set; } + public bool IsExpanded { get; set; } + } + + public sealed class TempSheetSection + { + // Reference to the original section (shared instance) + public SheetSectionData Reference; + + // Language-specific items + public List Items = new(); + + public int Id => Reference.Id; + + public string Name + { + get => Reference.Name; + set => Reference.Name = value; + } + } + + public sealed class SheetSectionData : SheetSectionTreeView + { + public List Items = new(); + public bool IsExpanded { get; set; } + } + + public sealed class TempLanguageData + { + public LanguageEntry Entry; + public List TableSheet = new(); + } + + public sealed class LocalizationWindowData + { + public List Languages = new(); + public List TableSheet = new(); + + public int LanguageCount = 0; + public int SectionCount = 0; + public int EntryCount = 0; + } + + public static class LocalizationWindowUtility + { + public const string NULL = "null"; + + public static void BuildWindowData(GameLocaizationTable table, out LocalizationWindowData data) + { + LocalizationWindowData windowData = new(); + + int languages = 0; + int sections = 0; + int entries = 0; + + // 1. Build the main table structure + foreach (var tableData in table.TableSheet) + { + SheetSectionData sectionData = new() + { + Id = tableData.Id, + Name = tableData.SectionName, + Items = new List() + }; + + foreach (var item in tableData.SectionSheet) + { + sectionData.Items.Add(new() + { + Id = item.Id, + Key = item.Key, + Parent = sectionData, + isGen = item.IsGen + }); + entries++; + } + + windowData.TableSheet.Add(sectionData); + sections++; + } + + windowData.SectionCount = sections; + windowData.EntryCount = entries; + + // 2. Build language-specific data that references the same sections and items + foreach (var lang in table.Languages) + { + TempLanguageData langData = new() + { + Entry = new(lang) + }; + + // Assign language name from asset + if (lang != null) + { + string name = lang.LanguageName; + langData.Entry.LanguageName = name; + } + + foreach (var globalSection in windowData.TableSheet) + { + int _sectionId = globalSection.Id; + + // Create a TempSheetSection reference and assign shared section reference + TempSheetSection tempSection = new() + { + Reference = globalSection + }; + + // For each item in the global section, we create a TempSheetItem that references it + foreach (var globalItem in globalSection.Items) + { + int _entryId = globalItem.Id; + string value = NULL; + + if (lang != null) + { + foreach (var item in lang.Strings) + { + // Try match the localized string from language asset + if (item.SectionId == _sectionId && item.EntryId == _entryId) + { + value = item.Value; + break; + } + } + } + + // Add SheetItem to section items list and assign shared item reference + tempSection.Items.Add(new() + { + Reference = globalItem, + Value = value + }); + } + + langData.TableSheet.Add(tempSection); + } + + windowData.Languages.Add(langData); + languages++; + } + + windowData.LanguageCount = languages; + data = windowData; + } + + /// + /// Assign a language asset to an existing TempLanguageData. + /// + public static void AssignLanguage(this LocalizationWindowData data, TempLanguageData languageData, LocalizationLanguage asset) + { + // Assign the asset to the language entry + languageData.Entry.Asset = asset; + + if (asset == null) + { + // If no asset is assigned, clear all values + foreach (var tempSection in languageData.TableSheet) + { + foreach (var tempItem in tempSection.Items) + { + tempItem.Value = string.Empty; + } + } + + return; + } + else + { + // Assign language name from asset + languageData.Entry.LanguageName = asset.LanguageName; + } + + // If we have a valid asset, we try to match each TempSheetItem to a corresponding LocalizationString + foreach (var tempSection in languageData.TableSheet) + { + int sectionId = tempSection.Reference.Id; + + foreach (var tempItem in tempSection.Items) + { + int entryId = tempItem.Reference.Id; + string value = string.Empty; + + foreach (var item in asset.Strings) + { + // Try match the localized string from language asset + if (item.SectionId == sectionId && item.EntryId == entryId) + { + value = item.Value; + break; + } + } + + // Assign the localization value + tempItem.Value = value; + } + } + } + + /// + /// Add a new language. + /// + public static void AddLanguage(this LocalizationWindowData data, string languageName, LocalizationLanguage asset, bool withIndex = true) + { + if (withIndex) + { + int languageIndex = ++data.LanguageCount; + languageName += " " + languageIndex; + } + + var newLanguageEntry = new LanguageEntry() + { + LanguageName = languageName, + Asset = asset + }; + + TempLanguageData newLangData = new() + { + Entry = newLanguageEntry, + TableSheet = new List() + }; + + // Reference existing sections and items + foreach (var sectionData in data.TableSheet) + { + TempSheetSection tempSection = new() + { + Reference = sectionData, + Items = new List() + }; + + foreach (var globalItem in sectionData.Items) + { + tempSection.Items.Add(new() + { + Reference = globalItem, + Value = string.Empty + }); + } + + newLangData.TableSheet.Add(tempSection); + } + + data.Languages.Add(newLangData); + } + + /// + /// Add a new section to the global TableSheet. + /// + public static SheetSectionData AddSection(this LocalizationWindowData data, string sectionName, bool withIndex = true) + { + if (withIndex) + { + int sectionIndex = ++data.SectionCount; + sectionName += " " + sectionIndex; + } + + SheetSectionData newSection = new() + { + Name = sectionName, + Items = new List() + }; + + data.TableSheet.Add(newSection); + + // Add corresponding section to each language + foreach (var lang in data.Languages) + { + lang.TableSheet.Add(new() + { + Reference = newSection, + Items = new List() + }); + } + + return newSection; + } + + /// + /// Remove a section from the global TableSheet by reference. + /// + public static void RemoveSection(this LocalizationWindowData data, SheetSectionTreeView section) + { + // Remove section + int sectionIndex = data.TableSheet.FindIndex(x => x.Id == section.Id); + if (sectionIndex != -1) data.TableSheet.RemoveAt(sectionIndex); + + // Remove the corresponding section from each language + foreach (var lang in data.Languages) + { + var tempSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == section); + if (tempSection != null) lang.TableSheet.Remove(tempSection); + } + } + + /// + /// Add a new item to a given section. + /// + public static SheetItemTreeView AddItem(this LocalizationWindowData data, SheetSectionData section, string key, bool withIndex = true) + { + if (withIndex) + { + int keyIndex = ++data.EntryCount; + key += " " + keyIndex; + } + + SheetItemTreeView newItem = new() + { + Key = key, + Parent = section + }; + + section.Items.Add(newItem); + + // Add corresponding item to each language + foreach (var lang in data.Languages) + { + var tempSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == section); + if (tempSection != null) + { + tempSection.Items.Add(new() + { + Reference = newItem, + Value = string.Empty + }); + } + } + + data.EntryCount++; + return newItem; + } + + /// + /// Remove an item by reference from the given section. + /// + public static void RemoveItem(this LocalizationWindowData data, SheetSectionTreeView section, SheetItemTreeView item) + { + // Remove item from section + int sectionIndex = data.TableSheet.FindIndex(x => x.Id == section.Id); + if (sectionIndex != -1) data.TableSheet[sectionIndex].Items.Remove(item); + + // Remove corresponding item from each language + foreach (var lang in data.Languages) + { + var tempSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == section); + if (tempSection != null) + { + var tempItem = tempSection.Items.FirstOrDefault(ti => ti.Reference == item); + if (tempItem != null) tempSection.Items.Remove(tempItem); + } + } + } + + /// + /// Moves an item within the same section to a new position. + /// + public static void OnMoveItemWithinSection(this LocalizationWindowData data, SheetSectionTreeView section, SheetItemTreeView item, int position) + { + // Find the corresponding SheetSectionData in data + var sectionData = data.TableSheet.FirstOrDefault(s => s.Id == section.Id); + if (sectionData == null) + return; + + int oldIndex = sectionData.Items.IndexOf(item); + if (oldIndex < 0) + return; + + // Clamp position + int insertTo = position > oldIndex ? position - 1 : position; + insertTo = Mathf.Clamp(insertTo, 0, sectionData.Items.Count); + + if (oldIndex == insertTo) + return; + + sectionData.Items.RemoveAt(oldIndex); + sectionData.Items.Insert(insertTo, item); + + // Update in each language + foreach (var lang in data.Languages) + { + // Find the corresponding TempSheetSection + var tempSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == sectionData); + if (tempSection == null) continue; + + int langOldIndex = tempSection.Items.FindIndex(i => i.Reference == item); + if (langOldIndex < 0) continue; + + // Clamp position + int insertToLang = position > langOldIndex ? position - 1 : position; + insertToLang = Mathf.Clamp(insertToLang, 0, tempSection.Items.Count); + if (langOldIndex == position) continue; + + var tempItem = tempSection.Items[langOldIndex]; + tempSection.Items.RemoveAt(langOldIndex); + tempSection.Items.Insert(insertToLang, tempItem); + } + } + + /// + /// Moves an item from one section to another section, inserting it at a specific position. + /// + public static void OnMoveItemToSectionAt(this LocalizationWindowData data, SheetSectionTreeView parent, SheetSectionTreeView section, SheetItemTreeView item, int position) + { + // Find both parent and target sections + var parentSection = data.TableSheet.FirstOrDefault(s => s.Id == parent.Id); + var targetSection = data.TableSheet.FirstOrDefault(s => s.Id == section.Id); + + if (parentSection == null || targetSection == null) + return; + + // Remove from parent + if (!parentSection.Items.Remove(item)) + return; + + // Insert into target at specified position + position = Mathf.Max(0, Mathf.Min(position, targetSection.Items.Count)); + + item.Parent = targetSection; + targetSection.Items.Insert(position, item); + + // Update in each language + foreach (var lang in data.Languages) + { + var langParentSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == parentSection); + var langTargetSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == targetSection); + + if (langParentSection == null || langTargetSection == null) + continue; + + // Remove the corresponding temp item from the parent section + var tempItem = langParentSection.Items.FirstOrDefault(i => i.Reference == item); + if (tempItem == null) continue; + + langParentSection.Items.Remove(tempItem); + + // Insert into target section + position = Mathf.Max(0, Mathf.Min(position, langTargetSection.Items.Count)); + langTargetSection.Items.Insert(position, tempItem); + } + } + + /// + /// Moves an item from one section to another section, adding it to the end. + /// + public static void OnMoveItemToSection(this LocalizationWindowData data, SheetSectionTreeView parent, SheetSectionTreeView section, SheetItemTreeView item) + { + // Find both parent and target sections + var parentSection = data.TableSheet.FirstOrDefault(s => s.Id == parent.Id); + var targetSection = data.TableSheet.FirstOrDefault(s => s.Id == section.Id); + + if (parentSection == null || targetSection == null) + return; + + // Remove from parent + if (!parentSection.Items.Remove(item)) + return; + + item.Parent = targetSection; + targetSection.Items.Add(item); + + // Update in each language + foreach (var lang in data.Languages) + { + var langParentSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == parentSection); + var langTargetSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == targetSection); + + if (langParentSection == null || langTargetSection == null) + continue; + + // Remove the corresponding temp item from the parent section + var tempItem = langParentSection.Items.FirstOrDefault(i => i.Reference == item); + if (tempItem == null) continue; + + langParentSection.Items.Remove(tempItem); + langTargetSection.Items.Add(tempItem); + } + } + + /// + /// Moves a section to a new position in the TableSheet list. + /// + public static void OnMoveSection(this LocalizationWindowData data, SheetSectionTreeView section, int position) + { + var sectionData = data.TableSheet.FirstOrDefault(s => s.Id == section.Id); + if (sectionData == null) + return; + + int oldIndex = data.TableSheet.IndexOf(sectionData); + if (oldIndex < 0) + return; + + // Clamp position + int insertTo = position > oldIndex ? position - 1 : position; + insertTo = Mathf.Clamp(insertTo, 0, sectionData.Items.Count); + + if (oldIndex == insertTo) + return; + + data.TableSheet.RemoveAt(oldIndex); + data.TableSheet.Insert(insertTo, sectionData); + + // Update in each language + foreach (var lang in data.Languages) + { + var langSection = lang.TableSheet.FirstOrDefault(ts => ts.Reference == sectionData); + if (langSection == null) continue; + + int langOldIndex = lang.TableSheet.IndexOf(langSection); + if (langOldIndex < 0) continue; + + // Clamp position + int insertToLang = position > langOldIndex ? position - 1 : position; + insertToLang = Mathf.Clamp(insertTo, 0, lang.TableSheet.Count); + + if (langOldIndex == position) + continue; + + lang.TableSheet.RemoveAt(langOldIndex); + lang.TableSheet.Insert(insertToLang, langSection); + } + } + + public static void GenerateCode(GameLocaizationTable table) + { + string filePath = table.GenerateScriptCodePath; + string nameSpace = table.GenerateScriptCodeFirstConfig; + List strings = new List(); + var localizationLanguage = table.Languages.Find(t => t.LanguageName == table.GenerateScriptCodeFirstConfig); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("/// "); + sb.AppendLine("/// AutoGenerate"); + sb.AppendLine("/// "); + sb.AppendLine("public static class LocalizationKey"); + sb.AppendLine("{"); + for (int i = 0; i < table.TableSheet.Count; i++) + { + var v = table.TableSheet[i]; + if (v.SectionSheet.FindIndex(t => t.IsGen) < 0) continue; + sb.AppendLine($"\tpublic sealed class {v.SectionName}"); + sb.AppendLine("\t{"); + for (int j = 0; j < v.SectionSheet.Count; j++) + { + var item = v.SectionSheet[j]; + if (!item.IsGen) continue; + string keyValue = item.Key; + string combineKey=v.SectionName+"."+item.Key; + + if (localizationLanguage != null) + { + var localizationString = localizationLanguage.Strings.Find(t => t.Key == combineKey); + keyValue = localizationString != null ? localizationString.Value : combineKey; + } + + string varibleName = item.Key.Replace(".", "_").ToUpper(); + sb.AppendLine("\t\t/// "); + sb.AppendLine($"\t\t/// {keyValue}"); + sb.AppendLine("\t\t/// "); + sb.AppendLine($"\t\tpublic const string {varibleName} = \"{combineKey}\";"); + if (j < v.SectionSheet.Count - 1) sb.AppendLine(""); + } + + sb.AppendLine("\t}"); + sb.AppendLine(""); + } + + + sb.AppendLine(); + sb.AppendLine("}"); + System.IO.File.WriteAllText(filePath, sb.ToString()); + AssetDatabase.Refresh(); + } + } +} diff --git a/Editor/Localization/LocalizationTableWindow/LocalizationWindowUtility.cs.meta b/Editor/Localization/LocalizationTableWindow/LocalizationWindowUtility.cs.meta new file mode 100644 index 0000000..251ec04 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/LocalizationWindowUtility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7ca9d65f294bd6843b5ca8d0a0eb97f6 \ No newline at end of file diff --git a/Editor/Localization/LocalizationTableWindow/Scriptable.meta b/Editor/Localization/LocalizationTableWindow/Scriptable.meta new file mode 100644 index 0000000..b82da3c --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/Scriptable.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3bcfa73f585d7aa4caee8e18fab11c4b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Localization/LocalizationTableWindow/Scriptable/GameLocaizationTableEditor.cs b/Editor/Localization/LocalizationTableWindow/Scriptable/GameLocaizationTableEditor.cs new file mode 100644 index 0000000..c3822d6 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/Scriptable/GameLocaizationTableEditor.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AlicizaX.Editor; +using UnityEngine; +using UnityEditor; +using UnityEditor.Callbacks; + +namespace AlicizaX.Localization.Editor +{ + [CustomEditor(typeof(GameLocaizationTable))] + internal class GameLocaizationTableEditor : InspectorEditor + { + private List popConfig = new List(); + + [OnOpenAsset] + public static bool OnOpenAsset(int instanceId, int line) + + { + var obj = EditorUtility.InstanceIDToObject(instanceId); + var asset = obj as GameLocaizationTable; + + if (asset == null) return false; + string path = AssetDatabase.GetAssetPath(asset); + EditorPrefs.SetString("LastSelectedGameLocaizationTable", path); + OpenLocalizationEditor(); + return true; + } + + + public override void OnEnable() + { + base.OnEnable(); + popConfig.Clear(); + if (Target.Languages.Count > 0) + { + foreach (var lang in Target.Languages) + { + string name = lang.LanguageName.Or("Unknown"); + popConfig.Add(name); + } + } + } + + public override void OnInspectorGUI() + { + EditorDrawing.DrawInspectorHeader(new GUIContent("Game Localization Table"), Target); + EditorGUILayout.Space(); + serializedObject.Update(); + { + EditorGUILayout.HelpBox("You can edit this language in the Game Localization Table Editor window.", MessageType.Info); + EditorGUILayout.Space(); + + using (new EditorDrawing.BorderBoxScope(new GUIContent("GenCode"), roundedBox: false)) + { + Properties.Draw("GenerateScriptCodePath", new GUIContent("File Path")); + EditorDrawing.DrawStringSelectPopup(new GUIContent("Gen Lang"), new GUIContent("None"), popConfig.ToArray(), Target.GenerateScriptCodeFirstConfig, (e) => { Target.GenerateScriptCodeFirstConfig = e; }); + } + } + serializedObject.ApplyModifiedProperties(); + + using (new EditorDrawing.BorderBoxScope(new GUIContent("Languages"), roundedBox: false)) + { + if (Target.Languages.Count > 0) + { + using (new EditorGUI.DisabledGroupScope(true)) + { + foreach (var lang in Target.Languages) + { + string name = lang.LanguageName.Or("Unknown"); + EditorGUILayout.ObjectField(new GUIContent(name), lang, typeof(LocalizationLanguage), false); + } + } + } + else + { + EditorGUILayout.HelpBox("There are currently no languages available, open the localization editor and add new languages.", MessageType.Info); + } + } + + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + { + if (GUILayout.Button("Open Localization Editor", GUILayout.Width(180f), GUILayout.Height(25))) + { + string path = AssetDatabase.GetAssetPath(target); + EditorPrefs.SetString("LastSelectedGameLocaizationTable", path); + OpenLocalizationEditor(); + } + + if (GUILayout.Button("Gen Code", GUILayout.Width(180f), GUILayout.Height(25))) + { + LocalizationWindowUtility.GenerateCode(Target); + } + } + GUILayout.FlexibleSpace(); + } + EditorGUILayout.EndHorizontal(); + } + + [MenuItem("Tools/AlicizaX/Localization/Open Localization Editor")] + private static void OpenLocalizationEditor() + { + EditorWindow window = EditorWindow.GetWindow(false, "Localization Editor", true); + window.minSize = new(1000, 500); + window.Show(); + } + + [MenuItem("Tools/AlicizaX/Localization/Create Localization Table")] + private static void CreateLocalizationTable() + { + string path = "Assets/Localization/"; + string fileName = "LocalizationTable.asset"; + string finalPath = Path.Combine(path, fileName); + + GameLocaizationTable table = ScriptableObject.CreateInstance(); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + AssetDatabase.CreateAsset(table, finalPath); + EditorUtility.SetDirty(table); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + table.TableSheet = new List(); + IReadOnlyList languageTypes = LocalizationConfiguration.Instance.LanguageTypeNames; + + for (int i = 0; i < languageTypes.Count; i++) + { + LocalizationLanguage asset = ScriptableObject.CreateInstance(); + asset.name = languageTypes[i]; + asset.LanguageName = languageTypes[i]; + asset.Strings = new List(); + table.Languages.Add(asset); + AssetDatabase.AddObjectToAsset(asset, table); + EditorUtility.SetDirty(asset); + } + + EditorUtility.SetDirty(table); + AssetDatabase.SaveAssets(); + } + } +} diff --git a/Editor/Localization/LocalizationTableWindow/Scriptable/GameLocaizationTableEditor.cs.meta b/Editor/Localization/LocalizationTableWindow/Scriptable/GameLocaizationTableEditor.cs.meta new file mode 100644 index 0000000..ad672f0 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/Scriptable/GameLocaizationTableEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3135dc7bd9a1fd649926188f4aa33fff \ No newline at end of file diff --git a/Editor/Localization/LocalizationTableWindow/Scriptable/LocalizationLanguageEditor.cs b/Editor/Localization/LocalizationTableWindow/Scriptable/LocalizationLanguageEditor.cs new file mode 100644 index 0000000..c1e9520 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/Scriptable/LocalizationLanguageEditor.cs @@ -0,0 +1,31 @@ +using AlicizaX.Editor; +using UnityEngine; +using UnityEditor; + + +namespace AlicizaX.Localization.Editor +{ + [CustomEditor(typeof(LocalizationLanguage))] + public class LocalizationLanguageEditor : InspectorEditor + { + public override void OnInspectorGUI() + { + EditorDrawing.DrawInspectorHeader(new GUIContent("Localization Language"), Target); + EditorGUILayout.Space(); + + serializedObject.Update(); + { + EditorGUILayout.HelpBox("You can edit this language in the Game Localization Table Editor window.", MessageType.Info); + EditorGUILayout.Space(); + + Properties.Draw("LanguageName"); + using (new EditorGUI.DisabledGroupScope(true)) + { + int entries = Target.Strings.Count; + EditorGUILayout.TextField("Strings", entries.ToString()); + } + } + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/Localization/LocalizationTableWindow/Scriptable/LocalizationLanguageEditor.cs.meta b/Editor/Localization/LocalizationTableWindow/Scriptable/LocalizationLanguageEditor.cs.meta new file mode 100644 index 0000000..0dc84ef --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/Scriptable/LocalizationLanguageEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6da7bdd80cb1d7b4d86b936f963c932a \ No newline at end of file diff --git a/Editor/Localization/LocalizationTableWindow/TreeView.meta b/Editor/Localization/LocalizationTableWindow/TreeView.meta new file mode 100644 index 0000000..1ba168b --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/TreeView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a7371740917c3134bb222528d84dd9c9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Localization/LocalizationTableWindow/TreeView/LanguagesTreeView.cs b/Editor/Localization/LocalizationTableWindow/TreeView/LanguagesTreeView.cs new file mode 100644 index 0000000..67fcc38 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/TreeView/LanguagesTreeView.cs @@ -0,0 +1,174 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using AlicizaX.Editor; +using UnityEditor.IMGUI.Controls; +using UnityEngine; +using UnityEditor; + +namespace AlicizaX.Localization.Editor +{ + public class LanguagesTreeView : TreeView + { + private const string k_DeleteCommand = "Delete"; + private const string k_SoftDeleteCommand = "SoftDelete"; + private const string k_NewLanguage = "New Language"; + + public Action OnLanguageSelect; + + private readonly LocalizationWindowData windowData; + private readonly GameLocaizationTable _table; + + internal class LanguageTreeViewItem : TreeViewItem + { + public TempLanguageData language; + + public LanguageTreeViewItem(int id, int depth, TempLanguageData language) : base(id, depth, language.Entry.LanguageName) + { + this.language = language; + } + } + + public LanguagesTreeView(TreeViewState state, LocalizationWindowData data, GameLocaizationTable table) : base(state) + { + windowData = data; + rowHeight = 20f; + _table = table; + Reload(); + } + + + protected override TreeViewItem BuildRoot() + { + var root = new TreeViewItem { id = 0, depth = -1, displayName = "Languages" }; + int id = 1; + + foreach (var lang in windowData.Languages) + { + root.AddChild(new LanguageTreeViewItem(id++, 1, lang)); + } + + if (root.children == null) + root.children = new List(); + + return root; + } + + protected override void RowGUI(RowGUIArgs args) + { + var item = args.item; + var rect = args.rowRect; + + GUIContent labelIcon = EditorGUIUtility.TrTextContentWithIcon(" " + item.displayName, "BuildSettings.Web.Small"); + Rect labelRect = new(rect.x + 2f, rect.y, rect.width - 2f, rect.height); + EditorGUI.LabelField(labelRect, labelIcon); + } + + public override void OnGUI(Rect rect) + { + EditorDrawing.DrawHeaderWithBorder(ref rect, new GUIContent("LANGUAGES"), 20f, false); + base.OnGUI(rect); + } + + private void OnAddNewLanguage() + { + windowData.AddLanguage(k_NewLanguage, null); + } + + protected override bool CanRename(TreeViewItem item) => false; + protected override bool CanMultiSelect(TreeViewItem item) => true; + + protected override void RenameEnded(RenameEndedArgs args) + { + if (!args.acceptedRename) + return; + + var renamedItem = FindItem(args.itemID, rootItem); + if (renamedItem == null) return; + + renamedItem.displayName = args.newName; + if (renamedItem is LanguageTreeViewItem item) + item.language.Entry.LanguageName = args.newName; + } + + protected override void SingleClickedItem(int id) + { + var selectedItem = FindItem(id, rootItem); + if (selectedItem != null) + { + if (selectedItem is LanguageTreeViewItem item) + { + OnLanguageSelect?.Invoke(new LocalizationTableWindow.LanguageSelect() + { + Language = item.language, + TreeViewItem = item + }); + } + } + } + + protected override void SelectionChanged(IList selectedIds) + { + if (selectedIds.Count > 1) + OnLanguageSelect?.Invoke(null); + } + + protected override bool CanStartDrag(CanStartDragArgs args) + { + var firstItem = FindItem(args.draggedItemIDs[0], rootItem); + return args.draggedItemIDs.All(id => FindItem(id, rootItem).parent == firstItem.parent); + } + + protected override void SetupDragAndDrop(SetupDragAndDropArgs args) + { + DragAndDrop.PrepareStartDrag(); + DragAndDrop.SetGenericData("IDs", args.draggedItemIDs.ToArray()); + DragAndDrop.SetGenericData("Type", "Languages"); + DragAndDrop.StartDrag("Languages"); + } + + + private void HandleCommandEvent(Event uiEvent) + { + if (uiEvent.type == EventType.ValidateCommand) + { + switch (uiEvent.commandName) + { + case k_DeleteCommand: + case k_SoftDeleteCommand: + if (HasSelection()) + uiEvent.Use(); + break; + } + } + else if (uiEvent.type == EventType.ExecuteCommand) + { + switch (uiEvent.commandName) + { + case k_DeleteCommand: + case k_SoftDeleteCommand: + DeleteSelected(); + break; + } + } + } + + private void DeleteSelected() + { + if (!HasFocus()) + return; + // + // var toDelete = GetSelection().OrderByDescending(i => i); + // if (toDelete.Count() <= 0) return; + // + // foreach (var index in toDelete) + // { + // windowData.Languages.RemoveAt(index - 1); + // } + // + // OnLanguageSelect?.Invoke(null); + // SetSelection(new int[0]); + // Reload(); + } + } +} diff --git a/Editor/Localization/LocalizationTableWindow/TreeView/LanguagesTreeView.cs.meta b/Editor/Localization/LocalizationTableWindow/TreeView/LanguagesTreeView.cs.meta new file mode 100644 index 0000000..a97a685 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/TreeView/LanguagesTreeView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a7a69a16aa08dee4b888f239c6203c71 \ No newline at end of file diff --git a/Editor/Localization/LocalizationTableWindow/TreeView/TableSheetTreeView.cs b/Editor/Localization/LocalizationTableWindow/TreeView/TableSheetTreeView.cs new file mode 100644 index 0000000..ae81fcc --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/TreeView/TableSheetTreeView.cs @@ -0,0 +1,447 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using AlicizaX.Editor; +using UnityEditor.IMGUI.Controls; +using UnityEngine; +using UnityEditor; + + +namespace AlicizaX.Localization.Editor +{ + public class TableSheetTreeView : TreeView + { + private const string k_DeleteCommand = "Delete"; + private const string k_SoftDeleteCommand = "SoftDelete"; + private const string k_NewSection = "New Section"; + private const string k_NewEntry = "New Entry"; + private const int k_TreeViewStartIndex = 100; + + public Action OnTableSheetSelect; + + private readonly LocalizationWindowData windowData; + + private bool InitiateContextMenuOnNextRepaint = false; + private int ContextSelectedID = -1; + + internal class LstrSectionTreeViewItem : TreeViewItem + { + public SheetSectionTreeView Section; + + public int Id => Section.Id; + + public LstrSectionTreeViewItem(int id, int depth, SheetSectionTreeView tableData) : base(id, depth, tableData.Name) + { + Section = tableData; + } + } + + internal class LstrEntryTreeViewItem : TreeViewItem + { + public SheetItemTreeView Item; + + public int Id => Item.Id; + + public LstrEntryTreeViewItem(int id, int depth, SheetItemTreeView item) : base(id, depth, item.Key) + { + Item = item; + } + } + + public TableSheetTreeView(TreeViewState state, LocalizationWindowData data) : base(state) + { + windowData = data; + rowHeight = 20f; + Reload(); + } + + protected override TreeViewItem BuildRoot() + { + int id = k_TreeViewStartIndex; + var root = new TreeViewItem { id = id++, depth = -1, displayName = "TableSheet" }; + + foreach (var section in windowData.TableSheet) + { + string sectionName = section.Name; + var sectionItem = new LstrSectionTreeViewItem(id++, 0, section); + + root.AddChild(sectionItem); + + // Add items within each section as children of the section. + foreach (var key in section.Items) + { + sectionItem.AddChild(new LstrEntryTreeViewItem(id++, 1, key)); + } + } + + if (root.children == null) + root.children = new List(); + + SetupDepthsFromParentsAndChildren(root); + return root; + } + + protected override void RowGUI(RowGUIArgs args) + { + var item = args.item; + var rect = args.rowRect; + + GUIContent labelIcon = new GUIContent(item.displayName); + if (item is LstrSectionTreeViewItem) labelIcon = EditorGUIUtility.TrTextContentWithIcon(" " + item.displayName, "Folder Icon"); + + Rect labelRect = new(rect.x + GetContentIndent(item), rect.y, rect.width - GetContentIndent(item), rect.height); + Rect toggleRect = new Rect(rect.xMax - EditorGUIUtility.singleLineHeight - 4f, rect.y, EditorGUIUtility.singleLineHeight, rect.height); + + EditorGUI.LabelField(labelRect, labelIcon); + if (item is LstrEntryTreeViewItem entryTreeViewItem) + { + entryTreeViewItem.Item.isGen = EditorGUI.Toggle(toggleRect, entryTreeViewItem.Item.isGen); + } + } + + public override void OnGUI(Rect rect) + { + Rect headerRect = EditorDrawing.DrawHeaderWithBorder(ref rect, new GUIContent("TABLE SHEET"), 20f, false); + headerRect.xMin = headerRect.xMax - EditorGUIUtility.singleLineHeight - EditorGUIUtility.standardVerticalSpacing; + headerRect.width = EditorGUIUtility.singleLineHeight; + headerRect.y += EditorGUIUtility.standardVerticalSpacing; + + if (GUI.Button(headerRect, EditorUtils.Styles.PlusIcon, EditorStyles.iconButton)) + { + OnAddNewSection(); + Reload(); + } + + if (InitiateContextMenuOnNextRepaint) + { + InitiateContextMenuOnNextRepaint = false; + PopUpContextMenu(); + } + + HandleCommandEvent(Event.current); + base.OnGUI(rect); + } + + private void PopUpContextMenu() + { + var selectedItem = FindItem(ContextSelectedID, rootItem); + var menu = new GenericMenu(); + + if (selectedItem is LstrSectionTreeViewItem section) + { + menu.AddItem(new GUIContent("Add Entry"), false, () => + { + OnAddNewSectionEntry(section.Section.Id); + ContextSelectedID = -1; + Reload(); + }); + + menu.AddItem(new GUIContent("Delete"), false, () => + { + DeleteLstrSection(section.Section); + ContextSelectedID = -1; + Reload(); + }); + } + else if (selectedItem is LstrEntryTreeViewItem item) + { + LstrSectionTreeViewItem parentSection = (LstrSectionTreeViewItem)item.parent; + menu.AddItem(new GUIContent("Delete"), false, () => + { + DeleteLstrEntry(parentSection.Section, item.Item); + ContextSelectedID = -1; + Reload(); + }); + } + + menu.ShowAsContext(); + } + + private void OnAddNewSection() + { + windowData.AddSection(k_NewSection); + } + + private void OnAddNewSectionEntry(int sectionId) + { + foreach (var section in windowData.TableSheet) + { + if (section.Id == sectionId) + { + windowData.AddItem(section, k_NewEntry); + break; + } + } + } + + protected override bool CanRename(TreeViewItem item) => true; + protected override bool CanMultiSelect(TreeViewItem item) => true; + + protected override void ContextClickedItem(int id) + { + InitiateContextMenuOnNextRepaint = true; + ContextSelectedID = id; + Repaint(); + } + + protected override void RenameEnded(RenameEndedArgs args) + { + if (!args.acceptedRename) + return; + + var renamedItem = FindItem(args.itemID, rootItem); + if (renamedItem == null) return; + + renamedItem.displayName = args.newName; + if (renamedItem is LstrEntryTreeViewItem item) + { + item.Item.Key = args.newName; + } + else if (renamedItem is LstrSectionTreeViewItem section) + { + section.Section.Name = args.newName; + } + } + + protected override void SingleClickedItem(int id) + { + var selectedItem = FindItem(id, rootItem); + if (selectedItem != null) + { + if (selectedItem is LstrSectionTreeViewItem section) + { + OnTableSheetSelect?.Invoke(new LocalizationTableWindow.SectionSelect() + { + Section = section.Section, + TreeViewItem = section + }); + } + else if (selectedItem is LstrEntryTreeViewItem entry) + { + OnTableSheetSelect?.Invoke(new LocalizationTableWindow.ItemSelect() + { + Item = entry.Item, + TreeViewItem = entry + }); + } + } + } + + protected override void SelectionChanged(IList selectedIds) + { + if (selectedIds.Count > 1) + OnTableSheetSelect?.Invoke(null); + } + + protected override bool CanStartDrag(CanStartDragArgs args) + { + var firstItem = FindItem(args.draggedItemIDs[0], rootItem); + return args.draggedItemIDs.All(id => FindItem(id, rootItem).parent == firstItem.parent); + } + + protected override void SetupDragAndDrop(SetupDragAndDropArgs args) + { + DragAndDrop.PrepareStartDrag(); + DragAndDrop.SetGenericData("IDs", args.draggedItemIDs.ToArray()); + DragAndDrop.SetGenericData("Type", "TableSheet"); + DragAndDrop.StartDrag("TableSheet"); + } + + protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args) + { + int[] draggedIDs = (int[])DragAndDrop.GetGenericData("IDs"); + string type = (string)DragAndDrop.GetGenericData("Type"); + + if (!type.Equals("TableSheet")) + return DragAndDropVisualMode.Rejected; + + switch (args.dragAndDropPosition) + { + case DragAndDropPosition.BetweenItems: + if (args.parentItem is LstrSectionTreeViewItem section1) + { + bool acceptDrag = false; + foreach (var draggedId in draggedIDs) + { + var draggedItem = FindItem(draggedId, rootItem); + if (draggedItem != null && draggedItem is LstrEntryTreeViewItem item) + { + if (args.performDrop) + { + if (draggedItem.parent == section1) + { + OnMoveItemWithinSection(section1.Section, item.Item, args.insertAtIndex); + } + else + { + var parentSection = (LstrSectionTreeViewItem)draggedItem.parent; + OnMoveItemToSectionAt(parentSection.Section, section1.Section, item.Item, args.insertAtIndex); + } + } + + acceptDrag = true; + } + } + + if (args.performDrop && acceptDrag) + { + Reload(); + SetSelection(new int[0]); + } + + return acceptDrag + ? DragAndDropVisualMode.Move + : DragAndDropVisualMode.Rejected; + } + else + { + bool acceptDrag = false; + foreach (var draggedId in draggedIDs) + { + var draggedItem = FindItem(draggedId, rootItem); + if (draggedItem != null && draggedItem is LstrSectionTreeViewItem section) + { + if (args.performDrop) + OnMoveSection(section.Section, args.insertAtIndex); + + acceptDrag = true; + } + } + + if (args.performDrop && acceptDrag) + { + Reload(); + SetSelection(new int[0]); + } + + return acceptDrag + ? DragAndDropVisualMode.Move + : DragAndDropVisualMode.Rejected; + } + + case DragAndDropPosition.UponItem: + if (args.parentItem is LstrSectionTreeViewItem section2) + { + bool acceptDrag = false; + foreach (var draggedId in draggedIDs) + { + var draggedItem = FindItem(draggedId, rootItem); + if (draggedItem != null && draggedItem is LstrEntryTreeViewItem item) + { + if (args.performDrop && draggedItem.parent != section2) + { + var parentSection = (LstrSectionTreeViewItem)draggedItem.parent; + OnMoveItemToSection(parentSection.Section, section2.Section, item.Item); + } + + acceptDrag = true; + } + } + + if (args.performDrop && acceptDrag) + { + Reload(); + SetSelection(new int[0]); + } + + return acceptDrag + ? DragAndDropVisualMode.Move + : DragAndDropVisualMode.Rejected; + } + + break; + + case DragAndDropPosition.OutsideItems: + break; + } + + return DragAndDropVisualMode.Rejected; + } + + private void OnMoveItemWithinSection(SheetSectionTreeView section, SheetItemTreeView item, int position) + { + windowData.OnMoveItemWithinSection(section, item, position); + } + + private void OnMoveItemToSectionAt(SheetSectionTreeView parent, SheetSectionTreeView section, SheetItemTreeView item, int position) + { + windowData.OnMoveItemToSectionAt(parent, section, item, position); + } + + private void OnMoveItemToSection(SheetSectionTreeView parent, SheetSectionTreeView section, SheetItemTreeView item) + { + windowData.OnMoveItemToSection(parent, section, item); + } + + private void OnMoveSection(SheetSectionTreeView section, int position) + { + windowData.OnMoveSection(section, position); + } + + private void HandleCommandEvent(Event uiEvent) + { + if (uiEvent.type == EventType.ValidateCommand) + { + switch (uiEvent.commandName) + { + case k_DeleteCommand: + case k_SoftDeleteCommand: + if (HasSelection()) + uiEvent.Use(); + break; + } + } + else if (uiEvent.type == EventType.ExecuteCommand) + { + switch (uiEvent.commandName) + { + case k_DeleteCommand: + case k_SoftDeleteCommand: + DeleteSelected(); + break; + } + } + } + + private void DeleteSelected() + { + var toDelete = GetSelection().OrderByDescending(i => i); + if (toDelete.Count() <= 0) return; + + foreach (var index in toDelete) + { + var selectedItem = FindItem(index, rootItem); + if (selectedItem == null) continue; + + if (selectedItem is LstrEntryTreeViewItem item) + { + var parentSection = (LstrSectionTreeViewItem)selectedItem.parent; + DeleteLstrEntry(parentSection.Section, item.Item); + } + else if (selectedItem is LstrSectionTreeViewItem section) + { + DeleteLstrSection(section.Section); + } + } + + SetSelection(new int[0]); + Reload(); + } + + private void DeleteLstrEntry(SheetSectionTreeView section, SheetItemTreeView sheetItem) + { + if (!HasFocus()) + return; + + windowData.RemoveItem(section, sheetItem); + } + + private void DeleteLstrSection(SheetSectionTreeView section) + { + if (!HasFocus()) + return; + + windowData.RemoveSection(section); + } + } +} diff --git a/Editor/Localization/LocalizationTableWindow/TreeView/TableSheetTreeView.cs.meta b/Editor/Localization/LocalizationTableWindow/TreeView/TableSheetTreeView.cs.meta new file mode 100644 index 0000000..a3c8b54 --- /dev/null +++ b/Editor/Localization/LocalizationTableWindow/TreeView/TableSheetTreeView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a087b0e0010170b4d89da49884c17106 \ No newline at end of file diff --git a/Editor/UI/GenerateTool/UISettingEditorWindow.cs b/Editor/UI/GenerateTool/UISettingEditorWindow.cs index bf0fb47..0b0e937 100644 --- a/Editor/UI/GenerateTool/UISettingEditorWindow.cs +++ b/Editor/UI/GenerateTool/UISettingEditorWindow.cs @@ -10,7 +10,7 @@ namespace AlicizaX.UI.Editor { internal class UISettingEditorWindow : OdinEditorWindow { - [MenuItem("Tools/AlicizaX/UI Setting Window")] + [MenuItem("Tools/AlicizaX/UI/Setting Window")] private static void OpenWindow() { GetWindow().Show(); diff --git a/Runtime/Localization/Event/LocalizationChangeEvent.cs b/Runtime/Localization/Event/LocalizationChangeEvent.cs index 4f57dc6..784c94a 100644 --- a/Runtime/Localization/Event/LocalizationChangeEvent.cs +++ b/Runtime/Localization/Event/LocalizationChangeEvent.cs @@ -5,14 +5,14 @@ namespace AlicizaX.Localization [Prewarm(4)] public readonly struct LocalizationChangeEvent : IEventArgs { - public readonly Language ChangedLanguage; + public readonly string ChangedLanguage; - public LocalizationChangeEvent(Language language) + public LocalizationChangeEvent(string language) { ChangedLanguage = language; } - public static void Publisher(Language language) + public static void Publisher(string language) { EventPublisher.Publish(new LocalizationChangeEvent(language)); } diff --git a/Runtime/Localization/Manager/ILocalizationModule.cs b/Runtime/Localization/Manager/ILocalizationModule.cs index a5c4268..6e54369 100644 --- a/Runtime/Localization/Manager/ILocalizationModule.cs +++ b/Runtime/Localization/Manager/ILocalizationModule.cs @@ -8,7 +8,7 @@ namespace AlicizaX.Localization.Runtime /// public interface ILocalizationModule : IModule, IModuleAwake { - public Language Language { get; } + public string Language { get; } /// /// 根据字典主键获取字典内容字符串。 @@ -17,7 +17,7 @@ namespace AlicizaX.Localization.Runtime /// 要获取的字典内容字符串。 string GetString(string key); - void ChangedLanguage(Language language); + void ChangedLanguage(string language); /// @@ -421,6 +421,16 @@ namespace AlicizaX.Localization.Runtime /// 字典值。 string GetRawString(string key); - void AddLocalizationConfig(Dictionary config); + /// + /// 增量增加多语言配置 + /// + /// + void IncreAddLocalizationConfig(GameLocaizationTable table); + + /// + /// 覆盖增加多语言配置 + /// + /// + void CoverAddLocalizationConfig(GameLocaizationTable table); } } diff --git a/Runtime/Localization/Manager/LocalizationModule.cs b/Runtime/Localization/Manager/LocalizationModule.cs index a19087f..fe1c355 100644 --- a/Runtime/Localization/Manager/LocalizationModule.cs +++ b/Runtime/Localization/Manager/LocalizationModule.cs @@ -11,14 +11,14 @@ namespace AlicizaX.Localization.Runtime internal sealed partial class LocalizationModule : ILocalizationModule { private readonly Dictionary Dic = new(); - private Language _language; + private string _language; - public Language Language + public string Language { get => _language; } - public void ChangedLanguage(Language language) + public void ChangedLanguage(string language) { if (_language == language) return; _language = language; @@ -712,11 +712,34 @@ namespace AlicizaX.Localization.Runtime return key; } - public void AddLocalizationConfig(Dictionary config) + public void IncreAddLocalizationConfig(GameLocaizationTable table) { - foreach (var item in config) + LocalizationLanguage localizationLanguage = table.GetLanguage(_language); + if (localizationLanguage == null) { - Dic.TryAdd(item.Key, item.Value); + Log.Warning($"Can not Find {_language} Strins "); + return; + } + + foreach (var item in localizationLanguage.Strings) + { + Dic.Add(item.Key, item.Value); + } + } + + public void CoverAddLocalizationConfig(GameLocaizationTable table) + { + Dic.Clear(); + LocalizationLanguage localizationLanguage = table.GetLanguage(_language); + if (localizationLanguage == null) + { + Log.Warning($"Can not Find {_language} Strins "); + return; + } + + foreach (var item in localizationLanguage.Strings) + { + Dic.Add(item.Key, item.Value); } } @@ -728,7 +751,7 @@ namespace AlicizaX.Localization.Runtime void IModuleAwake.Awake() { #if UNITY_EDITOR - _language = (Language)UnityEditor.EditorPrefs.GetInt(LocalizationComponent.PrefsKey, 1); + _language = UnityEditor.EditorPrefs.GetString(LocalizationComponent.PrefsKey, "None"); #else _language = AppBuilderSetting.Instance.Language; #endif diff --git a/Runtime/Localization/ScriptableObject.meta b/Runtime/Localization/ScriptableObject.meta new file mode 100644 index 0000000..9c667cc --- /dev/null +++ b/Runtime/Localization/ScriptableObject.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c7258e05524f4317b0c98fc4b4f06c6c +timeCreated: 1758264906 \ No newline at end of file diff --git a/Runtime/Localization/ScriptableObject/GameLocaizationTable.cs b/Runtime/Localization/ScriptableObject/GameLocaizationTable.cs new file mode 100644 index 0000000..d21acd7 --- /dev/null +++ b/Runtime/Localization/ScriptableObject/GameLocaizationTable.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Serialization; + +namespace AlicizaX.Localization +{ + public class GameLocaizationTable : ScriptableObject + { +#if UNITY_EDITOR + [SerializeField] internal string GenerateScriptCodePath = string.Empty; + [SerializeField] internal string GenerateScriptCodeFirstConfig; + + [Serializable] + public struct SheetItem + { + public int Id; + public string Key; + public bool IsGen; + + public SheetItem(string key, int id, bool isGen) + { + Id = id; + Key = key; + IsGen = isGen; + } + } + + [Serializable] + public struct TableData + { + public int Id; + public string SectionName; + public List SectionSheet; + + public TableData(string section, int id) + { + Id = id; + SectionName = section; + SectionSheet = new List(); + } + } + + public List TableSheet = new(); +#endif + public List Languages = new(); + + internal LocalizationLanguage GetLanguage(string languageCode) + { + return Languages.Find(t => t.LanguageName == languageCode); + } + } +} diff --git a/Runtime/Localization/ScriptableObject/GameLocaizationTable.cs.meta b/Runtime/Localization/ScriptableObject/GameLocaizationTable.cs.meta new file mode 100644 index 0000000..d7fb234 --- /dev/null +++ b/Runtime/Localization/ScriptableObject/GameLocaizationTable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0b8003079a7f4c54f9326bd1838f057e \ No newline at end of file diff --git a/Runtime/Localization/ScriptableObject/LocalizationLanguage.cs b/Runtime/Localization/ScriptableObject/LocalizationLanguage.cs new file mode 100644 index 0000000..2473ac3 --- /dev/null +++ b/Runtime/Localization/ScriptableObject/LocalizationLanguage.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace AlicizaX.Localization +{ + public class LocalizationLanguage : ScriptableObject + { + [Serializable] + public sealed class LocalizationString + { + public int EntryId; + public int SectionId; + + public string Key; + public string Value; + } + + public string LanguageName; + public List Strings = new(); + } +} diff --git a/Runtime/Localization/ScriptableObject/LocalizationLanguage.cs.meta b/Runtime/Localization/ScriptableObject/LocalizationLanguage.cs.meta new file mode 100644 index 0000000..53bed9b --- /dev/null +++ b/Runtime/Localization/ScriptableObject/LocalizationLanguage.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e99433e1eef4a964d98eb541d664ab70 \ No newline at end of file diff --git a/Runtime/_InternalVisibleTo.cs b/Runtime/_InternalVisibleTo.cs new file mode 100644 index 0000000..a185ee2 --- /dev/null +++ b/Runtime/_InternalVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AlicizaX.Framework.Editor")] diff --git a/Runtime/_InternalVisibleTo.cs.meta b/Runtime/_InternalVisibleTo.cs.meta new file mode 100644 index 0000000..fe12c0d --- /dev/null +++ b/Runtime/_InternalVisibleTo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 10e6d8633c6c407eb678feb2d6fe564b +timeCreated: 1758525627 \ No newline at end of file