UI换了很多,spine主视觉停用

This commit is contained in:
FloatGaming
2026-06-15 20:17:25 +08:00
parent 5eec879582
commit 216ab08e8d
3634 changed files with 814422 additions and 382766 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d62d68047ba38344484abd8aae27b4fb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,791 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Text;
using Newtonsoft.Json;
using UnityEditor;
using UnityEditor.Localization;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using UnityEngine.Localization.Tables;
using UnityEditor.SceneManagement;
public static class UnityLocalizationProjectSync
{
private const string SessionKey = "Bansonic.Localization.Sync.Completed";
private const string RootDirectory = "Assets/Localization";
private const string SettingsDirectory = RootDirectory + "/Settings";
private const string LocalesDirectory = RootDirectory + "/Locales";
private const string TablesDirectory = RootDirectory + "/Tables";
private const string SettingsAssetPath = SettingsDirectory + "/ProjectLocalizationSettings.asset";
private const string ZhJsonPath = "Assets/StreamingAssets/localization/zh-CN.json";
private const string EnJsonPath = "Assets/StreamingAssets/localization/en-US.json";
private static readonly string[] LiteralAssetExtensions = { ".prefab", ".unity" };
private static readonly string[] SkippedPathPrefixes =
{
"Assets/Airy UI/_Demo Scenes/",
"Assets/Cartoon UI/",
"Assets/Spine Examples/",
"Assets/TextMesh Pro/Examples & Extras/",
"Assets/Localization/"
};
private static readonly Regex LocalizedTextRegex =
new Regex(@"LocalizationService\.(Get|GetFormat)\(\s*""(?<key>[^""]+)""\s*,\s*""(?<fallback>(?:\\.|[^""])*)""", RegexOptions.Compiled);
private static readonly Regex LiteralRegex =
new Regex(@"LocalizationService\.LocalizeLiteral\(\s*""(?<literal>(?:\\.|[^""])*)""\s*\)", RegexOptions.Compiled);
private static readonly Regex NoticeLiteralRegex =
new Regex(@"gNotice\.\w+\.display\(\s*""(?<literal>(?:\\.|[^""])*)""", RegexOptions.Compiled);
private static readonly Regex YamlTextRegex =
new Regex(@"^\s*m_(Text|text):\s*(?<value>.*)$", RegexOptions.Compiled);
private static readonly HashSet<string> ScriptableObjectStringExclusionPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"songName"
};
[InitializeOnLoadMethod]
private static void ScheduleAutoSync()
{
EditorApplication.delayCall += AutoSyncIfNeeded;
}
[MenuItem("Tools/Localization/Rebuild Official Localization Assets")]
public static void RebuildOfficialLocalizationAssets()
{
try
{
EnsureLocalizationAssets(forceRebuild: true);
Debug.Log("[Localization] Official localization assets rebuilt successfully.");
}
catch (Exception ex)
{
Debug.LogError($"[Localization] Rebuild failed: {ex}");
throw;
}
}
[MenuItem("Tools/Localization/Import JSON To Official Tables")]
public static void ImportJsonToOfficialTables()
{
try
{
EnsureLocalizationAssets(forceRebuild: true);
Debug.Log("[Localization] Imported JSON packs into official localization tables.");
}
catch (Exception ex)
{
Debug.LogError($"[Localization] Import JSON failed: {ex}");
throw;
}
}
[MenuItem("Tools/Localization/Export Official Tables To JSON")]
public static void ExportOfficialTablesToJson()
{
try
{
EnsureDirectories();
LocalizationSettings settings = EnsureSettingsAsset();
LocalizationEditorSettings.ActiveLocalizationSettings = settings;
Locale zhLocale = EnsureLocale("zh-CN", "Chinese (Simplified)", $"{LocalesDirectory}/zh-CN.asset");
Locale enLocale = EnsureLocale("en-US", "English (United States)", $"{LocalesDirectory}/en-US.asset");
StringTableCollection gameTextCollection = LocalizationEditorSettings.GetStringTableCollection(LocalizationService.GameTextTableCollectionName);
StringTableCollection literalCollection = LocalizationEditorSettings.GetStringTableCollection(LocalizationService.LiteralTextTableCollectionName);
Dictionary<string, string> zhEntries = ExtractEntries(gameTextCollection, zhLocale);
Dictionary<string, string> enEntries = ExtractEntries(gameTextCollection, enLocale);
Dictionary<string, string> zhLiterals = ExtractEntries(literalCollection, zhLocale);
Dictionary<string, string> enLiterals = ExtractEntries(literalCollection, enLocale);
SavePack(ZhJsonPath, "zh-CN", zhEntries, zhLiterals);
SavePack(EnJsonPath, "en-US", enEntries, enLiterals);
AssetDatabase.Refresh();
Debug.Log("[Localization] Exported official localization tables to JSON.");
}
catch (Exception ex)
{
Debug.LogError($"[Localization] Export JSON failed: {ex}");
throw;
}
}
private static void AutoSyncIfNeeded()
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
{
return;
}
if (SessionState.GetBool(SessionKey, false))
{
return;
}
SessionState.SetBool(SessionKey, true);
try
{
if (EditorApplication.isCompiling || EditorApplication.isUpdating)
{
return;
}
if (HasCoreLocalizationAssets())
{
return;
}
EnsureLocalizationShellAssets();
}
catch (Exception ex)
{
Debug.LogError($"[Localization] Auto sync failed: {ex}");
}
}
private static void EnsureLocalizationAssets(bool forceRebuild)
{
EnsureDirectories();
LocalizationSettings settings = EnsureSettingsAsset();
LocalizationEditorSettings.ActiveLocalizationSettings = settings;
Locale zhLocale = EnsureLocale("zh-CN", "Chinese (Simplified)", $"{LocalesDirectory}/zh-CN.asset");
Locale enLocale = EnsureLocale("en-US", "English (United States)", $"{LocalesDirectory}/en-US.asset");
var locales = new List<Locale> { zhLocale, enLocale };
StringTableCollection gameTextCollection = EnsureStringCollection(LocalizationService.GameTextTableCollectionName, locales);
StringTableCollection literalCollection = EnsureStringCollection(LocalizationService.LiteralTextTableCollectionName, locales);
LocalizationLanguagePack zhPack = LoadPack(ZhJsonPath);
LocalizationLanguagePack enPack = LoadPack(EnJsonPath);
Dictionary<string, string> zhEntries = BuildEntryMap(zhPack?.entries, scanCodeFallbacks: true, preferJsonValues: true);
Dictionary<string, string> enEntries = BuildEntryMap(enPack?.entries, scanCodeFallbacks: false, preferJsonValues: true);
BackfillMissingEntries(zhEntries, enEntries);
Dictionary<string, string> zhLiterals = BuildLiteralMap(zhPack?.literals, enPack?.literals, english: false);
Dictionary<string, string> enLiterals = BuildLiteralMap(zhPack?.literals, enPack?.literals, english: true);
SavePack(ZhJsonPath, "zh-CN", zhEntries, zhLiterals);
SavePack(EnJsonPath, "en-US", enEntries, enLiterals);
ApplyEntries(gameTextCollection, zhLocale, zhEntries);
ApplyEntries(gameTextCollection, enLocale, enEntries);
ApplyEntries(literalCollection, zhLocale, zhLiterals);
ApplyEntries(literalCollection, enLocale, enLiterals);
SetPreloadFlags(gameTextCollection);
SetPreloadFlags(literalCollection);
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
private static bool HasCoreLocalizationAssets()
{
if (!File.Exists(SettingsAssetPath))
{
return false;
}
LocalizationSettings settings = AssetDatabase.LoadAssetAtPath<LocalizationSettings>(SettingsAssetPath);
if (settings == null)
{
return false;
}
if (LocalizationEditorSettings.GetLocale("zh-CN") == null)
{
return false;
}
if (LocalizationEditorSettings.GetLocale("en-US") == null)
{
return false;
}
if (LocalizationEditorSettings.GetStringTableCollection(LocalizationService.GameTextTableCollectionName) == null)
{
return false;
}
if (LocalizationEditorSettings.GetStringTableCollection(LocalizationService.LiteralTextTableCollectionName) == null)
{
return false;
}
return true;
}
private static void EnsureLocalizationShellAssets()
{
EnsureDirectories();
LocalizationSettings settings = EnsureSettingsAsset();
LocalizationEditorSettings.ActiveLocalizationSettings = settings;
Locale zhLocale = EnsureLocale("zh-CN", "Chinese (Simplified)", $"{LocalesDirectory}/zh-CN.asset");
Locale enLocale = EnsureLocale("en-US", "English (United States)", $"{LocalesDirectory}/en-US.asset");
var locales = new List<Locale> { zhLocale, enLocale };
EnsureStringCollection(LocalizationService.GameTextTableCollectionName, locales);
EnsureStringCollection(LocalizationService.LiteralTextTableCollectionName, locales);
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
}
private static void EnsureDirectories()
{
Directory.CreateDirectory(SettingsDirectory);
Directory.CreateDirectory(LocalesDirectory);
Directory.CreateDirectory(TablesDirectory);
}
private static LocalizationSettings EnsureSettingsAsset()
{
var settings = AssetDatabase.LoadAssetAtPath<LocalizationSettings>(SettingsAssetPath);
if (settings != null)
{
return settings;
}
settings = ScriptableObject.CreateInstance<LocalizationSettings>();
AssetDatabase.CreateAsset(settings, SettingsAssetPath);
return settings;
}
private static Locale EnsureLocale(string code, string localeName, string assetPath)
{
Locale locale = LocalizationEditorSettings.GetLocale(code);
if (locale == null)
{
locale = AssetDatabase.LoadAssetAtPath<Locale>(assetPath);
}
if (locale == null)
{
locale = Locale.CreateLocale(new LocaleIdentifier(code));
locale.name = localeName;
locale.LocaleName = localeName;
AssetDatabase.CreateAsset(locale, assetPath);
}
if (LocalizationEditorSettings.GetLocale(code) == null)
{
LocalizationEditorSettings.AddLocale(locale);
}
EditorUtility.SetDirty(locale);
return locale;
}
private static StringTableCollection EnsureStringCollection(string tableName, IList<Locale> locales)
{
StringTableCollection collection = LocalizationEditorSettings.GetStringTableCollection(tableName);
if (collection == null)
{
collection = LocalizationEditorSettings.CreateStringTableCollection(tableName, TablesDirectory, locales);
}
for (int i = 0; i < locales.Count; i++)
{
if (collection.GetTable(locales[i].Identifier) == null)
{
collection.AddNewTable(locales[i].Identifier);
}
}
EditorUtility.SetDirty(collection);
return collection;
}
private static LocalizationLanguagePack LoadPack(string assetPath)
{
if (!File.Exists(assetPath))
{
return new LocalizationLanguagePack();
}
string json = File.ReadAllText(assetPath);
LocalizationLanguagePack pack = JsonConvert.DeserializeObject<LocalizationLanguagePack>(json);
return pack ?? new LocalizationLanguagePack();
}
private static Dictionary<string, string> BuildEntryMap(Dictionary<string, string> seedEntries, bool scanCodeFallbacks, bool preferJsonValues)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
if (seedEntries != null)
{
foreach (var pair in seedEntries)
{
if (!string.IsNullOrWhiteSpace(pair.Key) && !string.IsNullOrWhiteSpace(pair.Value))
{
result[pair.Key] = pair.Value;
}
}
}
if (!scanCodeFallbacks)
{
return result;
}
foreach (string sourceFile in EnumerateSourceFiles("Assets"))
{
string content = File.ReadAllText(sourceFile);
foreach (Match match in LocalizedTextRegex.Matches(content))
{
string key = Regex.Unescape(match.Groups["key"].Value);
string fallback = Regex.Unescape(match.Groups["fallback"].Value);
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(fallback))
{
continue;
}
if (!result.ContainsKey(key) || !preferJsonValues)
{
result[key] = fallback;
}
}
}
return result;
}
private static Dictionary<string, string> BuildLiteralMap(
Dictionary<string, string> zhSeed,
Dictionary<string, string> enSeed,
bool english)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
Dictionary<string, string> primary = english ? enSeed : zhSeed;
if (primary != null)
{
foreach (var pair in primary)
{
if (!string.IsNullOrWhiteSpace(pair.Key) && !string.IsNullOrWhiteSpace(pair.Value))
{
result[pair.Key] = pair.Value;
}
}
}
foreach (string sourceFile in EnumerateSourceFiles("Assets"))
{
string content = File.ReadAllText(sourceFile);
MergeLiteralMatches(content, LiteralRegex, result, english);
MergeLiteralMatches(content, NoticeLiteralRegex, result, english);
}
foreach (string assetPath in EnumerateLiteralAssetPaths())
{
foreach (string literal in ReadLiteralStringsFromYaml(assetPath))
{
if (!result.ContainsKey(literal))
{
result[literal] = literal;
}
}
}
foreach (string literal in EnumerateScriptableObjectLiterals())
{
if (!result.ContainsKey(literal))
{
result[literal] = literal;
}
}
return result;
}
private static void MergeLiteralMatches(string content, Regex regex, Dictionary<string, string> target, bool english)
{
foreach (Match match in regex.Matches(content))
{
string literal = Regex.Unescape(match.Groups["literal"].Value);
if (!ShouldCaptureLiteral(literal))
{
continue;
}
if (!target.ContainsKey(literal))
{
target[literal] = literal;
}
}
}
private static IEnumerable<string> EnumerateSourceFiles(string rootDirectory)
{
return Directory.EnumerateFiles(rootDirectory, "*.cs", SearchOption.AllDirectories)
.Where(path => path.IndexOf("\\Editor\\", StringComparison.OrdinalIgnoreCase) < 0);
}
private static IEnumerable<string> EnumerateLiteralAssetPaths()
{
return Directory.EnumerateFiles("Assets", "*.*", SearchOption.AllDirectories)
.Where(path => LiteralAssetExtensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase))
.Where(path => !ShouldSkipAssetPath(path));
}
private static IEnumerable<string> ReadLiteralStringsFromYaml(string assetPath)
{
foreach (string rawLine in File.ReadLines(assetPath))
{
Match match = YamlTextRegex.Match(rawLine);
if (!match.Success)
{
continue;
}
string value;
try
{
value = DecodeYamlText(match.Groups["value"].Value);
}
catch (Exception ex)
{
Debug.LogWarning($"[Localization] Skipped malformed YAML text in {assetPath}: {ex.Message}");
continue;
}
if (ShouldCaptureLiteral(value))
{
yield return value;
}
}
}
private static IEnumerable<string> EnumerateScriptableObjectLiterals()
{
string[] guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { "Assets" });
for (int i = 0; i < guids.Length; i++)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
if (string.IsNullOrWhiteSpace(assetPath) || ShouldSkipAssetPath(assetPath))
{
continue;
}
ScriptableObject asset = AssetDatabase.LoadAssetAtPath<ScriptableObject>(assetPath);
if (asset == null)
{
continue;
}
SerializedObject serializedObject = new SerializedObject(asset);
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = true;
if (iterator.propertyType != SerializedPropertyType.String)
{
continue;
}
if (ShouldSkipScriptableObjectString(asset, iterator))
{
continue;
}
string literal = iterator.stringValue;
if (ShouldCaptureLiteral(literal))
{
yield return literal.Trim();
}
}
}
}
private static bool ShouldSkipScriptableObjectString(ScriptableObject asset, SerializedProperty property)
{
if (asset == null || property == null)
{
return true;
}
if (string.Equals(property.propertyPath, "m_Script", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (asset is SongData && ScriptableObjectStringExclusionPaths.Contains(property.propertyPath))
{
return true;
}
return false;
}
private static string DecodeYamlText(string rawValue)
{
if (string.IsNullOrWhiteSpace(rawValue))
{
return string.Empty;
}
string value = rawValue.Trim();
if (value == "\"\"" || value == "''")
{
return string.Empty;
}
bool isDoubleQuoted = value.Length >= 2 &&
value.StartsWith("\"", StringComparison.Ordinal) &&
value.EndsWith("\"", StringComparison.Ordinal);
bool isSingleQuoted = value.Length >= 2 &&
value.StartsWith("'", StringComparison.Ordinal) &&
value.EndsWith("'", StringComparison.Ordinal);
if (isDoubleQuoted || isSingleQuoted)
{
if (isDoubleQuoted)
{
try
{
value = JsonConvert.DeserializeObject<string>(value);
}
catch
{
value = value.Substring(1, value.Length - 2);
}
}
else
{
value = value.Substring(1, value.Length - 2);
}
}
else
{
value = DecodeYamlEscapes(value);
}
return value.Trim();
}
private static string DecodeYamlEscapes(string value)
{
if (string.IsNullOrEmpty(value) || value.IndexOf('\\') < 0)
{
return value;
}
var builder = new StringBuilder(value.Length);
for (int i = 0; i < value.Length; i++)
{
char current = value[i];
if (current != '\\' || i + 1 >= value.Length)
{
builder.Append(current);
continue;
}
char next = value[i + 1];
switch (next)
{
case 'n':
builder.Append('\n');
i++;
break;
case 'r':
builder.Append('\r');
i++;
break;
case 't':
builder.Append('\t');
i++;
break;
case '"':
case '\'':
case '\\':
builder.Append(next);
i++;
break;
case 'u' when i + 5 < value.Length:
string hex = value.Substring(i + 2, 4);
if (ushort.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out ushort code))
{
builder.Append((char)code);
i += 5;
}
else
{
builder.Append(current);
}
break;
case 'x':
builder.Append(next);
i++;
break;
default:
builder.Append(current);
break;
}
}
return builder.ToString();
}
private static bool ShouldCaptureLiteral(string literal)
{
if (string.IsNullOrWhiteSpace(literal))
{
return false;
}
string trimmed = literal.Trim();
if (trimmed.Length == 0 || trimmed.Length > 512)
{
return false;
}
if (trimmed.StartsWith("guid:", StringComparison.OrdinalIgnoreCase))
{
return false;
}
bool hasLetterLikeContent = trimmed.Any(ch => char.IsLetter(ch) || IsCjk(ch));
return hasLetterLikeContent;
}
private static bool IsCjk(char ch)
{
return ch >= 0x4E00 && ch <= 0x9FFF;
}
private static bool ShouldSkipAssetPath(string path)
{
string normalized = path.Replace('\\', '/');
for (int i = 0; i < SkippedPathPrefixes.Length; i++)
{
if (normalized.StartsWith(SkippedPathPrefixes[i], StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static void BackfillMissingEntries(Dictionary<string, string> zhEntries, Dictionary<string, string> enEntries)
{
foreach (string key in zhEntries.Keys.ToArray())
{
if (!enEntries.ContainsKey(key))
{
enEntries[key] = zhEntries[key];
}
}
foreach (string key in enEntries.Keys.ToArray())
{
if (!zhEntries.ContainsKey(key))
{
zhEntries[key] = enEntries[key];
}
}
}
private static void ApplyEntries(StringTableCollection collection, Locale locale, Dictionary<string, string> entries)
{
if (collection == null || locale == null)
{
return;
}
StringTable table = collection.GetTable(locale.Identifier) as StringTable;
if (table == null)
{
table = collection.AddNewTable(locale.Identifier) as StringTable;
}
foreach (var pair in entries)
{
if (string.IsNullOrWhiteSpace(pair.Key))
{
continue;
}
StringTableEntry entry = table.GetEntry(pair.Key);
if (entry == null)
{
table.AddEntry(pair.Key, pair.Value ?? string.Empty);
}
else if (entry.LocalizedValue != (pair.Value ?? string.Empty))
{
entry.Value = pair.Value ?? string.Empty;
}
}
EditorUtility.SetDirty(table);
EditorUtility.SetDirty(table.SharedData);
}
private static Dictionary<string, string> ExtractEntries(StringTableCollection collection, Locale locale)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
if (collection == null || locale == null)
{
return result;
}
StringTable table = collection.GetTable(locale.Identifier) as StringTable;
if (table == null)
{
return result;
}
foreach (StringTableEntry entry in table.Values)
{
if (entry == null || string.IsNullOrWhiteSpace(entry.Key))
{
continue;
}
result[entry.Key] = entry.LocalizedValue ?? string.Empty;
}
return result;
}
private static void SavePack(string assetPath, string languageCode, Dictionary<string, string> entries, Dictionary<string, string> literals)
{
var pack = new LocalizationLanguagePack
{
languageCode = languageCode,
entries = new Dictionary<string, string>(entries, StringComparer.Ordinal),
literals = new Dictionary<string, string>(literals, StringComparer.Ordinal)
};
string json = JsonConvert.SerializeObject(pack, Formatting.Indented);
File.WriteAllText(assetPath, json);
}
private static void SetPreloadFlags(StringTableCollection collection)
{
foreach (var table in collection.StringTables)
{
LocalizationEditorSettings.SetPreloadTableFlag(table, true);
EditorUtility.SetDirty(table);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f28f5ad7c0cea940976ea0a3aeb2d53
@@ -0,0 +1,181 @@
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.Localization.Components;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections.Generic;
public class LocalizationRuntimeBootstrap : MonoBehaviour
{
private static LocalizationRuntimeBootstrap _instance;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
LocalizationService.EnsureInitialized();
if (_instance != null)
{
return;
}
GameObject go = new GameObject("LocalizationRuntimeBootstrap");
DontDestroyOnLoad(go);
_instance = go.AddComponent<LocalizationRuntimeBootstrap>();
}
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
SceneManager.sceneLoaded += OnSceneLoaded;
LocalizationService.LanguageChanged += OnLanguageChanged;
}
private void Start()
{
StartCoroutine(ApplyLocalizationDeferred());
}
private void OnDestroy()
{
if (_instance == this)
{
SceneManager.sceneLoaded -= OnSceneLoaded;
LocalizationService.LanguageChanged -= OnLanguageChanged;
_instance = null;
}
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
StartCoroutine(ApplyLocalizationDeferred());
}
private void OnLanguageChanged(string languageCode)
{
StartCoroutine(ApplyLocalizationDeferred());
}
private IEnumerator ApplyLocalizationDeferred()
{
// Some UI updates its text in Start/OnEnable one or two frames after scene load.
// Re-apply for a few frames so late-bound labels are still captured.
for (int i = 0; i < 4; i++)
{
yield return null;
ApplyLocalizationToLoadedTexts();
}
}
private static void ApplyLocalizationToLoadedTexts()
{
foreach (Text text in EnumerateSceneTexts())
{
if (!IsLocalizable(text))
{
continue;
}
LocalizedLiteralText binding = text.GetComponent<LocalizedLiteralText>();
if (binding == null)
{
binding = text.gameObject.AddComponent<LocalizedLiteralText>();
}
binding.CaptureIfNeeded(text.text, false);
binding.Refresh();
}
foreach (TMP_Text tmp in EnumerateSceneTmpTexts())
{
if (!IsLocalizable(tmp))
{
continue;
}
LocalizedLiteralText binding = tmp.GetComponent<LocalizedLiteralText>();
if (binding == null)
{
binding = tmp.gameObject.AddComponent<LocalizedLiteralText>();
}
binding.CaptureIfNeeded(tmp.text, true);
binding.Refresh();
}
}
private static IEnumerable<Text> EnumerateSceneTexts()
{
int sceneCount = SceneManager.sceneCount;
for (int i = 0; i < sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
if (!scene.IsValid() || !scene.isLoaded)
{
continue;
}
GameObject[] roots = scene.GetRootGameObjects();
for (int rootIndex = 0; rootIndex < roots.Length; rootIndex++)
{
Text[] texts = roots[rootIndex].GetComponentsInChildren<Text>(true);
for (int textIndex = 0; textIndex < texts.Length; textIndex++)
{
yield return texts[textIndex];
}
}
}
}
private static IEnumerable<TMP_Text> EnumerateSceneTmpTexts()
{
int sceneCount = SceneManager.sceneCount;
for (int i = 0; i < sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
if (!scene.IsValid() || !scene.isLoaded)
{
continue;
}
GameObject[] roots = scene.GetRootGameObjects();
for (int rootIndex = 0; rootIndex < roots.Length; rootIndex++)
{
TMP_Text[] texts = roots[rootIndex].GetComponentsInChildren<TMP_Text>(true);
for (int textIndex = 0; textIndex < texts.Length; textIndex++)
{
yield return texts[textIndex];
}
}
}
}
private static bool IsLocalizable(Component component)
{
if (component == null || component.gameObject == null)
{
return false;
}
GameObject gameObject = component.gameObject;
if (!gameObject.scene.IsValid() || !gameObject.scene.isLoaded || string.IsNullOrEmpty(gameObject.scene.name))
{
return false;
}
if (gameObject.GetComponent<LocalizedText>() != null ||
gameObject.GetComponent<LocalizedTMPText>() != null ||
gameObject.GetComponent<LocalizeStringEvent>() != null)
{
return false;
}
return true;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fda8d67303e05c74f930db1c603304da
@@ -0,0 +1,502 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
public static class LocalizationService
{
public const string LanguagePrefKey = "bansonic_language_code";
public const string DefaultLanguageCode = "zh-CN";
public const string GameTextTableCollectionName = "GameText";
public const string LiteralTextTableCollectionName = "GameLiteralText";
private static readonly object SyncRoot = new object();
private static bool _initialized;
private static string _currentLanguageCode = DefaultLanguageCode;
private static readonly Dictionary<string, Dictionary<string, string>> EntryFallbacks = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, Dictionary<string, string>> LiteralFallbacks = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
public static event Action<string> LanguageChanged;
public static string CurrentLanguageCode
{
get
{
EnsureInitialized();
return _currentLanguageCode;
}
}
public static bool IsEnglish => string.Equals(CurrentLanguageCode, "en-US", StringComparison.OrdinalIgnoreCase);
public static IReadOnlyList<string> GetAvailableLanguageCodes()
{
EnsureInitialized();
if (!LocalizationSettings.HasSettings || LocalizationSettings.AvailableLocales == null)
{
return new[] { DefaultLanguageCode, "en-US" };
}
IList<Locale> locales = LocalizationSettings.AvailableLocales.Locales;
if (locales == null || locales.Count == 0)
{
return new[] { DefaultLanguageCode, "en-US" };
}
List<string> results = new List<string>(locales.Count);
for (int i = 0; i < locales.Count; i++)
{
Locale locale = locales[i];
if (locale == null)
{
continue;
}
string normalized = NormalizeLanguageCode(locale.Identifier.Code);
if (!results.Contains(normalized))
{
results.Add(normalized);
}
}
if (results.Count == 0)
{
results.Add(DefaultLanguageCode);
results.Add("en-US");
}
else
{
if (!results.Contains(DefaultLanguageCode))
{
results.Insert(0, DefaultLanguageCode);
}
if (!results.Contains("en-US"))
{
results.Add("en-US");
}
}
return results;
}
public static void EnsureInitialized()
{
if (_initialized)
{
return;
}
lock (SyncRoot)
{
if (_initialized)
{
return;
}
_currentLanguageCode = NormalizeLanguageCode(PlayerPrefs.GetString(LanguagePrefKey, ResolveStartupLanguageCode()));
PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode);
PlayerPrefs.Save();
LoadRuntimeFallbackPack(DefaultLanguageCode);
LoadRuntimeFallbackPack("en-US");
if (LocalizationSettings.HasSettings)
{
LocalizationSettings.InitializeSynchronously = true;
var init = LocalizationSettings.InitializationOperation;
if (!init.IsDone)
{
init.WaitForCompletion();
}
if (LocalizationSettings.StringDatabase != null)
{
LocalizationSettings.StringDatabase.MissingTranslationState = (MissingTranslationBehavior)0;
}
LocalizationSettings.SelectedLocaleChanged -= HandleSelectedLocaleChanged;
LocalizationSettings.SelectedLocaleChanged += HandleSelectedLocaleChanged;
ApplySelectedLocale(_currentLanguageCode, savePreference: true, notifyListeners: false);
}
_initialized = true;
}
}
public static void SetLanguage(string languageCode)
{
EnsureInitialized();
string normalized = NormalizeLanguageCode(languageCode);
if (string.Equals(_currentLanguageCode, normalized, StringComparison.OrdinalIgnoreCase))
{
return;
}
if (LocalizationSettings.HasSettings)
{
ApplySelectedLocale(normalized, savePreference: true, notifyListeners: false);
return;
}
_currentLanguageCode = normalized;
PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode);
PlayerPrefs.Save();
LanguageChanged?.Invoke(_currentLanguageCode);
}
public static string Get(string key, string fallback = null)
{
if (TryGet(key, out string localized))
{
return localized;
}
if (string.IsNullOrWhiteSpace(key))
{
return fallback ?? string.Empty;
}
return fallback ?? key;
}
public static string GetFormat(string key, params object[] args)
{
string format = Get(key, key);
try
{
return string.Format(CultureInfo.InvariantCulture, format, args);
}
catch
{
return format;
}
}
public static string LocalizeLiteral(string source)
{
if (TryGetLiteral(source, out string localized))
{
return localized;
}
if (string.IsNullOrEmpty(source))
{
return source;
}
return source;
}
public static bool TryGet(string key, out string localized)
{
localized = null;
EnsureInitialized();
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
localized = ResolveFromTable(GameTextTableCollectionName, key);
if (!string.IsNullOrEmpty(localized))
{
return true;
}
localized = ResolveFromRuntimeFallback(EntryFallbacks, key);
return !string.IsNullOrEmpty(localized);
}
public static bool TryGetLiteral(string source, out string localized)
{
localized = null;
EnsureInitialized();
if (string.IsNullOrWhiteSpace(source))
{
return false;
}
localized = ResolveFromTable(LiteralTextTableCollectionName, source);
if (!string.IsNullOrEmpty(localized))
{
return true;
}
localized = ResolveFromRuntimeFallback(LiteralFallbacks, source);
return !string.IsNullOrEmpty(localized);
}
private static void HandleSelectedLocaleChanged(Locale locale)
{
_currentLanguageCode = NormalizeLanguageCode(locale?.Identifier.Code ?? DefaultLanguageCode);
PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode);
PlayerPrefs.Save();
LanguageChanged?.Invoke(_currentLanguageCode);
}
private static void ApplySelectedLocale(string languageCode, bool savePreference, bool notifyListeners)
{
string normalized = NormalizeLanguageCode(languageCode);
Locale targetLocale = FindLocale(normalized) ?? FindLocale(DefaultLanguageCode);
if (targetLocale == null)
{
_currentLanguageCode = normalized;
if (savePreference)
{
PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode);
PlayerPrefs.Save();
}
if (notifyListeners)
{
LanguageChanged?.Invoke(_currentLanguageCode);
}
return;
}
bool localeChanged = LocalizationSettings.SelectedLocale == null ||
!string.Equals(LocalizationSettings.SelectedLocale.Identifier.Code, targetLocale.Identifier.Code, StringComparison.OrdinalIgnoreCase);
_currentLanguageCode = NormalizeLanguageCode(targetLocale.Identifier.Code);
if (savePreference)
{
PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode);
PlayerPrefs.Save();
}
if (localeChanged)
{
LocalizationSettings.SelectedLocale = targetLocale;
}
else if (notifyListeners)
{
LanguageChanged?.Invoke(_currentLanguageCode);
}
}
private static Locale FindLocale(string languageCode)
{
if (!LocalizationSettings.HasSettings || LocalizationSettings.AvailableLocales == null)
{
return null;
}
string normalized = NormalizeLanguageCode(languageCode);
IList<Locale> locales = LocalizationSettings.AvailableLocales.Locales;
if (locales == null)
{
return null;
}
for (int i = 0; i < locales.Count; i++)
{
Locale locale = locales[i];
if (locale == null)
{
continue;
}
if (string.Equals(NormalizeLanguageCode(locale.Identifier.Code), normalized, StringComparison.OrdinalIgnoreCase))
{
return locale;
}
}
if (normalized.StartsWith("en", StringComparison.OrdinalIgnoreCase))
{
for (int i = 0; i < locales.Count; i++)
{
Locale locale = locales[i];
if (locale != null && locale.Identifier.Code.StartsWith("en", StringComparison.OrdinalIgnoreCase))
{
return locale;
}
}
}
if (normalized.StartsWith("zh", StringComparison.OrdinalIgnoreCase))
{
for (int i = 0; i < locales.Count; i++)
{
Locale locale = locales[i];
if (locale != null && locale.Identifier.Code.StartsWith("zh", StringComparison.OrdinalIgnoreCase))
{
return locale;
}
}
}
return null;
}
private static string ResolveFromTable(string tableName, string entryKey)
{
if (!LocalizationSettings.HasSettings || LocalizationSettings.StringDatabase == null)
{
return null;
}
try
{
string value = LocalizationSettings.StringDatabase.GetLocalizedString(tableName, entryKey, LocalizationSettings.SelectedLocale);
return string.IsNullOrEmpty(value) ? null : value;
}
catch (Exception ex)
{
Debug.LogWarning($"[LocalizationService] Failed to resolve '{entryKey}' from '{tableName}': {ex.Message}");
return null;
}
}
private static void LoadRuntimeFallbackPack(string languageCode)
{
string normalized = NormalizeLanguageCode(languageCode);
if (EntryFallbacks.ContainsKey(normalized) && LiteralFallbacks.ContainsKey(normalized))
{
return;
}
string path = Path.Combine(Application.streamingAssetsPath, "localization", normalized + ".json");
if (!File.Exists(path))
{
EntryFallbacks[normalized] = new Dictionary<string, string>(StringComparer.Ordinal);
LiteralFallbacks[normalized] = new Dictionary<string, string>(StringComparer.Ordinal);
return;
}
try
{
string json = File.ReadAllText(path);
LocalizationLanguagePack pack = Newtonsoft.Json.JsonConvert.DeserializeObject<LocalizationLanguagePack>(json)
?? new LocalizationLanguagePack();
EntryFallbacks[normalized] = pack.entries ?? new Dictionary<string, string>(StringComparer.Ordinal);
LiteralFallbacks[normalized] = pack.literals ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
catch (Exception ex)
{
Debug.LogWarning($"[LocalizationService] Failed to load runtime pack '{normalized}': {ex.Message}");
EntryFallbacks[normalized] = new Dictionary<string, string>(StringComparer.Ordinal);
LiteralFallbacks[normalized] = new Dictionary<string, string>(StringComparer.Ordinal);
}
}
private static string ResolveFromRuntimeFallback(Dictionary<string, Dictionary<string, string>> source, string key)
{
string normalized = NormalizeLanguageCode(_currentLanguageCode);
LoadRuntimeFallbackPack(normalized);
if (source.TryGetValue(normalized, out Dictionary<string, string> primary) &&
primary != null &&
primary.TryGetValue(key, out string localized) &&
!string.IsNullOrEmpty(localized))
{
return localized;
}
if (!string.Equals(normalized, DefaultLanguageCode, StringComparison.OrdinalIgnoreCase))
{
LoadRuntimeFallbackPack(DefaultLanguageCode);
if (source.TryGetValue(DefaultLanguageCode, out Dictionary<string, string> fallback) &&
fallback != null &&
fallback.TryGetValue(key, out localized) &&
!string.IsNullOrEmpty(localized))
{
return localized;
}
}
return null;
}
private static string ResolveStartupLanguageCode()
{
return Application.systemLanguage switch
{
SystemLanguage.English => "en-US",
_ => DefaultLanguageCode
};
}
private static string NormalizeLanguageCode(string languageCode)
{
if (string.IsNullOrWhiteSpace(languageCode))
{
return DefaultLanguageCode;
}
string trimmed = languageCode.Trim();
if (string.Equals(trimmed, "en", StringComparison.OrdinalIgnoreCase))
{
return "en-US";
}
if (string.Equals(trimmed, "zh", StringComparison.OrdinalIgnoreCase) ||
string.Equals(trimmed, "zh-Hans", StringComparison.OrdinalIgnoreCase))
{
return "zh-CN";
}
return trimmed;
}
}
[Serializable]
public sealed class LocalizationLanguagePack
{
public string languageCode;
public Dictionary<string, string> entries = new Dictionary<string, string>(StringComparer.Ordinal);
public Dictionary<string, string> literals = new Dictionary<string, string>(StringComparer.Ordinal);
}
[Serializable]
internal sealed class LocalizationLanguagePackJsonSurrogate
{
public string languageCode;
public SerializableDictionaryEntry[] entries;
public SerializableDictionaryEntry[] literals;
public LocalizationLanguagePack ToPack()
{
var pack = new LocalizationLanguagePack
{
languageCode = languageCode,
entries = new Dictionary<string, string>(StringComparer.Ordinal),
literals = new Dictionary<string, string>(StringComparer.Ordinal)
};
if (entries != null)
{
for (int i = 0; i < entries.Length; i++)
{
if (!string.IsNullOrWhiteSpace(entries[i].key))
{
pack.entries[entries[i].key] = entries[i].value ?? string.Empty;
}
}
}
if (literals != null)
{
for (int i = 0; i < literals.Length; i++)
{
if (!string.IsNullOrWhiteSpace(literals[i].key))
{
pack.literals[literals[i].key] = literals[i].value ?? string.Empty;
}
}
}
return pack;
}
}
[Serializable]
internal struct SerializableDictionaryEntry
{
public string key;
public string value;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7494cc97751c02e468e7f9e5c0781b93
@@ -0,0 +1,125 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
[DisallowMultipleComponent]
public class LocalizedLiteralText : MonoBehaviour
{
[SerializeField] private string sourceText;
[SerializeField] private bool useTmp;
[SerializeField] private bool captured;
[SerializeField] private float refreshIntervalSeconds = 0.25f;
private string _lastAppliedText;
private float _nextRefreshAt;
public void CaptureIfNeeded(string currentText, bool isTmp)
{
if (captured)
{
return;
}
sourceText = currentText ?? string.Empty;
useTmp = isTmp;
captured = true;
}
private void OnEnable()
{
LocalizationService.EnsureInitialized();
LocalizationService.LanguageChanged += HandleLanguageChanged;
_nextRefreshAt = 0f;
Refresh();
}
private void OnDisable()
{
LocalizationService.LanguageChanged -= HandleLanguageChanged;
}
private void LateUpdate()
{
if (Time.unscaledTime < _nextRefreshAt)
{
return;
}
_nextRefreshAt = Time.unscaledTime + Mathf.Max(0.05f, refreshIntervalSeconds);
CaptureRuntimeChanges();
}
public void Refresh()
{
if (!captured)
{
return;
}
string localized = LocalizationService.LocalizeLiteral(sourceText);
if (useTmp)
{
TMP_Text tmp = GetComponent<TMP_Text>();
if (tmp != null)
{
tmp.text = localized;
_lastAppliedText = localized;
}
return;
}
Text text = GetComponent<Text>();
if (text != null)
{
text.text = localized;
_lastAppliedText = localized;
}
}
private void CaptureRuntimeChanges()
{
string currentText = GetCurrentText();
if (string.IsNullOrEmpty(currentText))
{
return;
}
if (!captured)
{
CaptureIfNeeded(currentText, GetComponent<TMP_Text>() != null);
Refresh();
return;
}
if (currentText == _lastAppliedText)
{
return;
}
if (currentText == sourceText)
{
Refresh();
return;
}
sourceText = currentText;
Refresh();
}
private string GetCurrentText()
{
if (useTmp)
{
TMP_Text tmp = GetComponent<TMP_Text>();
return tmp != null ? tmp.text : null;
}
Text text = GetComponent<Text>();
return text != null ? text.text : null;
}
private void HandleLanguageChanged(string languageCode)
{
Refresh();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 620aeae46f264584d9d3dd1e0432ec8c
@@ -0,0 +1,64 @@
using TMPro;
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(TMP_Text))]
public class LocalizedTMPText : MonoBehaviour
{
[SerializeField] private string localizationKey;
[TextArea]
[SerializeField] private string fallbackText;
private TMP_Text _tmp;
private void Awake()
{
_tmp = GetComponent<TMP_Text>();
}
private void OnEnable()
{
LocalizationService.EnsureInitialized();
LocalizationService.LanguageChanged += HandleLanguageChanged;
Refresh();
}
private void OnDisable()
{
LocalizationService.LanguageChanged -= HandleLanguageChanged;
}
public void Refresh()
{
if (_tmp == null)
{
_tmp = GetComponent<TMP_Text>();
}
if (_tmp == null)
{
return;
}
string fallback = string.IsNullOrEmpty(fallbackText) ? _tmp.text : fallbackText;
if (string.IsNullOrWhiteSpace(localizationKey))
{
_tmp.text = LocalizationService.LocalizeLiteral(fallback);
return;
}
if (LocalizationService.TryGet(localizationKey, out string localized))
{
_tmp.text = localized;
return;
}
_tmp.text = LocalizationService.LocalizeLiteral(fallback);
}
private void HandleLanguageChanged(string languageCode)
{
Refresh();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4fb46605ac509e644b9c01f0b4194af9
@@ -0,0 +1,64 @@
using UnityEngine;
using UnityEngine.UI;
[DisallowMultipleComponent]
[RequireComponent(typeof(Text))]
public class LocalizedText : MonoBehaviour
{
[SerializeField] private string localizationKey;
[TextArea]
[SerializeField] private string fallbackText;
private Text _text;
private void Awake()
{
_text = GetComponent<Text>();
}
private void OnEnable()
{
LocalizationService.EnsureInitialized();
LocalizationService.LanguageChanged += HandleLanguageChanged;
Refresh();
}
private void OnDisable()
{
LocalizationService.LanguageChanged -= HandleLanguageChanged;
}
public void Refresh()
{
if (_text == null)
{
_text = GetComponent<Text>();
}
if (_text == null)
{
return;
}
string fallback = string.IsNullOrEmpty(fallbackText) ? _text.text : fallbackText;
if (string.IsNullOrWhiteSpace(localizationKey))
{
_text.text = LocalizationService.LocalizeLiteral(fallback);
return;
}
if (LocalizationService.TryGet(localizationKey, out string localized))
{
_text.text = localized;
return;
}
_text.text = LocalizationService.LocalizeLiteral(fallback);
}
private void HandleLanguageChanged(string languageCode)
{
Refresh();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e999ad3253f11cc4cbfb285bbf3c0088