using System; using System.Collections.Generic; using System.Text; using AlicizaX.Localization.Runtime; using UnityEditor; using UnityEngine; namespace AlicizaX.Localization.Editor { [Serializable] public sealed class LocalizationAiWriteRequest { public string TableAssetPath; public string SectionName = "AI"; public string Key; public bool AllowUpdate = true; public bool AutoCreateSection = true; public bool AutoCreateLanguageAsset = true; public bool FillMissingLanguagesWithEmpty = true; public bool SaveAssets = true; public bool HotReloadWhenPlaying = true; public List Translations = new(); } [Serializable] public sealed class LocalizationAiTranslation { public string Language; public string Text; } [Serializable] public sealed class LocalizationAiWriteResult { public bool Success; public string TableAssetPath; public string SectionName; public string Key; public string RuntimeKey; public bool CreatedSection; public bool CreatedEntry; public bool HotReloaded; public List UpdatedLanguages = new(); public List Warnings = new(); public string Message; } public static class LocalizationAiWriteTool { private const string DefaultSectionName = "AI"; private const string LastSelectedTableKey = "LastSelectedGameLocaizationTable"; public static LocalizationAiWriteResult Execute(LocalizationAiWriteRequest request) { LocalizationAiWriteResult result = new(); if (request == null) { result.Message = "Request is null."; return result; } string tablePath = request.TableAssetPath; if (string.IsNullOrWhiteSpace(tablePath)) { tablePath = EditorPrefs.GetString(LastSelectedTableKey, string.Empty); } if (string.IsNullOrWhiteSpace(tablePath)) { result.Message = "TableAssetPath is empty."; return result; } GameLocaizationTable table = AssetDatabase.LoadAssetAtPath(tablePath); if (table == null) { result.Message = $"Can not load GameLocaizationTable from path: {tablePath}"; result.TableAssetPath = tablePath; return result; } result.TableAssetPath = tablePath; string sectionName = request.SectionName; string itemKey = request.Key; ParseAndNormalizeKey(ref sectionName, ref itemKey); if (string.IsNullOrEmpty(sectionName)) { sectionName = DefaultSectionName; } if (string.IsNullOrEmpty(itemKey)) { result.Message = "Key is empty after normalization."; return result; } if (table.TableSheet == null) { table.TableSheet = new List(); } if (table.Languages == null) { table.Languages = new List(); } int sectionIndex = FindSectionIndex(table, sectionName); if (sectionIndex < 0) { if (!request.AutoCreateSection) { result.Message = $"Section '{sectionName}' does not exist."; return result; } GameLocaizationTable.TableData newSection = new(sectionName, GenerateUniqueSectionId(table)); table.TableSheet.Add(newSection); sectionIndex = table.TableSheet.Count - 1; result.CreatedSection = true; } GameLocaizationTable.TableData section = table.TableSheet[sectionIndex]; if (section.SectionSheet == null) { section.SectionSheet = new List(); } int entryIndex = FindEntryIndex(section, itemKey); bool createdEntry = false; if (entryIndex < 0) { GameLocaizationTable.SheetItem newItem = new(itemKey, GenerateUniqueEntryId(table), true); section.SectionSheet.Add(newItem); entryIndex = section.SectionSheet.Count - 1; createdEntry = true; result.CreatedEntry = true; } else if (!request.AllowUpdate) { result.Message = $"Key '{itemKey}' already exists in section '{sectionName}'."; return result; } GameLocaizationTable.SheetItem entry = section.SectionSheet[entryIndex]; string runtimeKey = BuildRuntimeKey(section.SectionName, entry.Key); Dictionary translationMap = BuildTranslationMap(request.Translations, result.Warnings); EnsureEntryExistsForAllLanguages(table, section, entry, runtimeKey, translationMap, request, result); table.TableSheet[sectionIndex] = section; table.InvalidateLanguageLookup(); EditorUtility.SetDirty(table); if (request.SaveAssets) { AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } if (request.HotReloadWhenPlaying && Application.isPlaying && AppServices.TryGet(out var localizationService)) { localizationService.ReloadLocalizationConfig(table); result.HotReloaded = true; } result.Success = true; result.SectionName = section.SectionName; result.Key = entry.Key; result.RuntimeKey = runtimeKey; result.Message = createdEntry ? $"Added localization entry '{runtimeKey}'." : $"Updated localization entry '{runtimeKey}'."; return result; } public static string ExecuteJson(string json) { LocalizationAiWriteRequest request = Utility.Json.ToObject(json); LocalizationAiWriteResult result = Execute(request); return Utility.Json.ToJson(result); } private static void EnsureEntryExistsForAllLanguages( GameLocaizationTable table, GameLocaizationTable.TableData section, GameLocaizationTable.SheetItem entry, string runtimeKey, Dictionary translationMap, LocalizationAiWriteRequest request, LocalizationAiWriteResult result) { for (int i = 0; i < table.Languages.Count; i++) { LocalizationLanguage language = table.Languages[i]; if (language == null) { continue; } UpsertLanguageEntry( language, section.Id, entry.Id, runtimeKey, translationMap.TryGetValue(language.LanguageName, out string text) ? text : string.Empty, translationMap.ContainsKey(language.LanguageName) || request.FillMissingLanguagesWithEmpty); EditorUtility.SetDirty(language); result.UpdatedLanguages.Add(language.LanguageName); } foreach (KeyValuePair pair in translationMap) { if (HasLanguage(table, pair.Key)) { continue; } if (!request.AutoCreateLanguageAsset) { result.Warnings.Add($"Language asset '{pair.Key}' does not exist in table '{table.name}'."); continue; } LocalizationLanguage language = CreateLanguageAsset(table, pair.Key); UpsertLanguageEntry(language, section.Id, entry.Id, runtimeKey, pair.Value, true); EditorUtility.SetDirty(language); result.UpdatedLanguages.Add(language.LanguageName); } } private static void UpsertLanguageEntry( LocalizationLanguage language, int sectionId, int entryId, string runtimeKey, string value, bool shouldWrite) { if (language.Strings == null) { language.Strings = new List(); } int index = FindLanguageStringIndex(language, sectionId, entryId); if (index >= 0) { LocalizationLanguage.LocalizationString item = language.Strings[index]; item.Key = runtimeKey; if (shouldWrite) { item.Value = value ?? string.Empty; } language.Strings[index] = item; return; } if (!shouldWrite) { return; } language.Strings.Add(new LocalizationLanguage.LocalizationString { SectionId = sectionId, EntryId = entryId, Key = runtimeKey, Value = value ?? string.Empty }); } private static LocalizationLanguage CreateLanguageAsset(GameLocaizationTable table, string languageName) { LocalizationLanguage language = ScriptableObject.CreateInstance(); language.name = languageName; language.LanguageName = languageName; language.Strings = new List(); AssetDatabase.AddObjectToAsset(language, table); table.Languages.Add(language); table.InvalidateLanguageLookup(); EditorUtility.SetDirty(table); return language; } private static Dictionary BuildTranslationMap( List translations, List warnings) { Dictionary map = new(StringComparer.Ordinal); if (translations == null) { return map; } for (int i = 0; i < translations.Count; i++) { LocalizationAiTranslation translation = translations[i]; if (translation == null || string.IsNullOrWhiteSpace(translation.Language)) { continue; } string normalizedLanguage = NormalizePart(translation.Language); if (string.IsNullOrEmpty(normalizedLanguage)) { continue; } if (map.ContainsKey(normalizedLanguage)) { warnings.Add($"Duplicate language '{normalizedLanguage}' found. Last value wins."); } map[normalizedLanguage] = translation.Text ?? string.Empty; } return map; } private static bool HasLanguage(GameLocaizationTable table, string languageName) { for (int i = 0; i < table.Languages.Count; i++) { LocalizationLanguage language = table.Languages[i]; if (language != null && string.Equals(language.LanguageName, languageName, StringComparison.Ordinal)) { return true; } } return false; } private static int FindSectionIndex(GameLocaizationTable table, string sectionName) { for (int i = 0; i < table.TableSheet.Count; i++) { if (string.Equals(table.TableSheet[i].SectionName, sectionName, StringComparison.Ordinal)) { return i; } } return -1; } private static int FindEntryIndex(GameLocaizationTable.TableData section, string itemKey) { for (int i = 0; i < section.SectionSheet.Count; i++) { if (string.Equals(section.SectionSheet[i].Key, itemKey, StringComparison.Ordinal)) { return i; } } return -1; } private static int FindLanguageStringIndex(LocalizationLanguage language, int sectionId, int entryId) { for (int i = 0; i < language.Strings.Count; i++) { LocalizationLanguage.LocalizationString item = language.Strings[i]; if (item.SectionId == sectionId && item.EntryId == entryId) { return i; } } return -1; } private static int GenerateUniqueSectionId(GameLocaizationTable table) { return GenerateUniqueId(table, true); } private static int GenerateUniqueEntryId(GameLocaizationTable table) { return GenerateUniqueId(table, false); } private static int GenerateUniqueId(GameLocaizationTable table, bool checkSectionIds) { int candidate = Mathf.Abs(Guid.NewGuid().GetHashCode()); while (candidate == 0 || ContainsId(table, candidate, checkSectionIds)) { candidate = Mathf.Abs(Guid.NewGuid().GetHashCode()); } return candidate; } private static bool ContainsId(GameLocaizationTable table, int id, bool checkSectionIds) { for (int i = 0; i < table.TableSheet.Count; i++) { GameLocaizationTable.TableData section = table.TableSheet[i]; if (checkSectionIds && section.Id == id) { return true; } if (section.SectionSheet == null) { continue; } for (int j = 0; j < section.SectionSheet.Count; j++) { if (!checkSectionIds && section.SectionSheet[j].Id == id) { return true; } } } return false; } private static string BuildRuntimeKey(string sectionName, string itemKey) { return string.Concat(sectionName, ".", itemKey); } private static void ParseAndNormalizeKey(ref string sectionName, ref string itemKey) { if (!string.IsNullOrWhiteSpace(itemKey)) { int separatorIndex = itemKey.IndexOf('.'); if (separatorIndex > 0 && separatorIndex < itemKey.Length - 1) { sectionName = itemKey.Substring(0, separatorIndex); itemKey = itemKey.Substring(separatorIndex + 1); } } sectionName = NormalizePart(sectionName); itemKey = NormalizePart(itemKey); } private static string NormalizePart(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } StringBuilder builder = new(value.Length); for (int i = 0; i < value.Length; i++) { char current = value[i]; if (char.IsLetterOrDigit(current) || current == '_') { builder.Append(current); continue; } if (char.IsWhiteSpace(current) || current == '-' || current == '/') { builder.Append('_'); } } while (builder.Length > 0 && builder[0] == '_') { builder.Remove(0, 1); } while (builder.Length > 0 && builder[builder.Length - 1] == '_') { builder.Remove(builder.Length - 1, 1); } return builder.ToString(); } } }