UI换了很多,spine主视觉停用
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user