com.alicizax.unity.framework/Editor/Localization/LocalizationAiWriteTool.cs

486 lines
16 KiB
C#
Raw Normal View History

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<LocalizationAiTranslation> 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<string> UpdatedLanguages = new();
public List<string> 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<GameLocaizationTable>(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<GameLocaizationTable.TableData>();
}
if (table.Languages == null)
{
table.Languages = new List<LocalizationLanguage>();
}
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<GameLocaizationTable.SheetItem>();
}
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<string, string> 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<ILocalizationService>(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<LocalizationAiWriteRequest>(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<string, string> 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<string, string> 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<LocalizationLanguage.LocalizationString>();
}
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<LocalizationLanguage>();
language.name = languageName;
language.LanguageName = languageName;
language.Strings = new List<LocalizationLanguage.LocalizationString>();
AssetDatabase.AddObjectToAsset(language, table);
table.Languages.Add(language);
table.InvalidateLanguageLookup();
EditorUtility.SetDirty(table);
return language;
}
private static Dictionary<string, string> BuildTranslationMap(
List<LocalizationAiTranslation> translations,
List<string> warnings)
{
Dictionary<string, string> 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();
}
}
}