486 lines
16 KiB
C#
486 lines
16 KiB
C#
|
|
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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|