using System; using System.Collections.Generic; using System.Reflection; using System.Text; using UnityEditor; using UnityEngine; namespace NBShaderEditor { public static class ShaderGUILocalization { public const string DefaultLanguage = "zh-CN"; public const string EnglishLanguage = "en-US"; private const string TooltipColumnSuffix = "-tip"; private const string TooltipColumnAliasSuffix = "-tooltip"; private static readonly Dictionary Tables = new Dictionary(StringComparer.OrdinalIgnoreCase); private static NBFXLanguageMode s_CachedLanguageMode; private static string s_CachedPreferredLanguage; private static bool s_CachedPreferredLanguageValid; public static void RegisterCsv(string tableName, string csvAssetPath) { tableName = NormalizeTableName(tableName); if (Tables.TryGetValue(tableName, out LocalizationTable table)) { if (string.Equals(table.CsvAssetPath, csvAssetPath, StringComparison.OrdinalIgnoreCase)) { return; } table.CsvAssetPath = csvAssetPath; table.Reload(); return; } Tables[tableName] = new LocalizationTable(csvAssetPath); } public static string GetCurrentLanguage(string tableName) { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); return GetCurrentLanguage(table); } public static string GetEditorLanguageName() { return GetLanguageFromEditor(); } private static string GetCurrentLanguage(LocalizationTable table) { string preferred = GetPreferredLanguageName(); foreach (string language in table.Languages) { if (string.Equals(language, preferred, StringComparison.OrdinalIgnoreCase)) { return language; } } return DefaultLanguage; } public static GUIContent MakeContent( string tableName, string labelKey, string labelFallback, string tooltipKey = null, string tooltipFallback = "") { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); string language = GetCurrentLanguage(table); var cacheKey = new ContentCacheKey(language, labelKey, labelFallback, tooltipKey, tooltipFallback); if (table.ContentCache.TryGetValue(cacheKey, out GUIContent cachedContent)) { return cachedContent; } var content = new GUIContent( GetLocalizedValue(table, labelKey, labelFallback, language), GetTooltip(table, labelKey, tooltipKey, tooltipFallback, language)); table.ContentCache[cacheKey] = content; return content; } public static GUIContent MakeInspectorContent(string tableName, string key, string fallback, string tip = "") { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); string language = GetCurrentLanguage(table); var cacheKey = new InspectorContentCacheKey(language, key, fallback, tip); if (table.InspectorContentCache.TryGetValue(cacheKey, out GUIContent cachedContent)) { return cachedContent; } string labelKey = "inspector." + key + ".label"; string tooltipKey = "inspector." + key + ".tip"; var content = new GUIContent( GetLocalizedValue(table, labelKey, fallback, language), GetTooltip(table, labelKey, tooltipKey, tip, language)); table.InspectorContentCache[cacheKey] = content; return content; } public static string GetInspectorText(string tableName, string key, string fallback = "") { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); string language = GetCurrentLanguage(table); return GetInspectorText(table, key, fallback, language); } public static string GetInspectorText(string tableName, string key, string fallback, string language) { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); return GetInspectorText(table, key, fallback, language); } private static string GetInspectorText(LocalizationTable table, string key, string fallback, string language) { var cacheKey = new InspectorTextCacheKey(language, key, fallback); if (table.InspectorTextCache.TryGetValue(cacheKey, out string cachedText)) { return cachedText; } cachedText = GetLocalizedValue(table, "inspector." + key, fallback, language); table.InspectorTextCache[cacheKey] = cachedText; return cachedText; } public static string[] GetInspectorOptions(string tableName, string key, string[] fallback) { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); string language = GetCurrentLanguage(table); return GetInspectorOptions(table, key, fallback, language); } public static string[] GetInspectorOptions(string tableName, string key, string[] fallback, string language) { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); return GetInspectorOptions(table, key, fallback, language); } private static string[] GetInspectorOptions(LocalizationTable table, string key, string[] fallback, string language) { if (fallback == null) { return Array.Empty(); } var cacheKey = new OptionsCacheKey(language, key, fallback); if (table.InspectorOptionsCache.TryGetValue(cacheKey, out string[] cachedValues)) { return cachedValues; } string keyPrefix = "inspector." + key + ".option"; string[] values = new string[fallback.Length]; for (int i = 0; i < fallback.Length; i++) { values[i] = GetLocalizedValue(table, keyPrefix + "." + i, fallback[i], language); } table.InspectorOptionsCache[cacheKey] = values; return values; } public static string[] GetOptions(string tableName, string keyPrefix, string[] fallback) { if (fallback == null) { return Array.Empty(); } LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); string language = GetCurrentLanguage(table); var cacheKey = new OptionsCacheKey(language, keyPrefix, fallback); if (table.OptionsCache.TryGetValue(cacheKey, out string[] cachedValues)) { return cachedValues; } string[] values = new string[fallback.Length]; for (int i = 0; i < fallback.Length; i++) { values[i] = GetLocalizedValue(table, keyPrefix + "." + i, fallback[i], language); } table.OptionsCache[cacheKey] = values; return values; } public static string Get(string tableName, string key, string fallback = "") { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); return GetLocalizedValue(table, key, fallback, GetCurrentLanguage(table)); } private static string GetLocalizedValue(LocalizationTable table, string key, string fallback, string language) { if (string.IsNullOrEmpty(key)) { return fallback; } if (table.Rows.TryGetValue(key, out Dictionary row)) { if (row.TryGetValue(language, out string localizedValue) && !string.IsNullOrEmpty(localizedValue)) { return localizedValue; } if (row.TryGetValue(DefaultLanguage, out string defaultValue) && !string.IsNullOrEmpty(defaultValue)) { return defaultValue; } } return fallback; } public static string GetTooltip(string tableName, string labelKey, string tooltipKey = null, string fallback = "") { LocalizationTable table = GetTable(tableName); table.EnsureLoaded(); return GetTooltip(table, labelKey, tooltipKey, fallback, GetCurrentLanguage(table)); } private static string GetTooltip( LocalizationTable table, string labelKey, string tooltipKey, string fallback, string language) { if (!string.IsNullOrEmpty(labelKey) && table.Rows.TryGetValue(labelKey, out Dictionary row)) { if (TryGetTooltip(row, language, out string localizedTooltip)) { return localizedTooltip; } if (TryGetTooltip(row, DefaultLanguage, out string defaultTooltip)) { return defaultTooltip; } } if (!string.IsNullOrEmpty(tooltipKey)) { return GetLocalizedValue(table, tooltipKey, fallback, language); } return fallback; } public static void Reload(string tableName = null) { if (string.IsNullOrEmpty(tableName)) { foreach (LocalizationTable table in Tables.Values) { table.Reload(); } InvalidatePreferredLanguageCache(); return; } GetTable(tableName).Reload(); InvalidatePreferredLanguageCache(); } private static void InvalidatePreferredLanguageCache() { s_CachedPreferredLanguageValid = false; } private static LocalizationTable GetTable(string tableName) { tableName = NormalizeTableName(tableName); if (!Tables.TryGetValue(tableName, out LocalizationTable table)) { table = new LocalizationTable(string.Empty); Tables[tableName] = table; } return table; } private static string NormalizeTableName(string tableName) { return string.IsNullOrEmpty(tableName) ? "default" : tableName; } private static bool IsTooltipColumn(string header) { return !string.IsNullOrEmpty(header) && (header.EndsWith(TooltipColumnSuffix, StringComparison.OrdinalIgnoreCase) || header.EndsWith(TooltipColumnAliasSuffix, StringComparison.OrdinalIgnoreCase)); } private static bool TryGetTooltip(Dictionary row, string language, out string tooltip) { tooltip = string.Empty; if (string.IsNullOrEmpty(language)) { return false; } if (row.TryGetValue(language + TooltipColumnSuffix, out tooltip) && !string.IsNullOrEmpty(tooltip)) { return true; } if (row.TryGetValue(language + TooltipColumnAliasSuffix, out tooltip) && !string.IsNullOrEmpty(tooltip)) { return true; } return false; } private static string GetPreferredLanguageName() { NBFXLanguageMode languageMode = NBFXProjectSettings.LanguageMode; if (s_CachedPreferredLanguageValid && s_CachedLanguageMode == languageMode) { return s_CachedPreferredLanguage; } string language; switch (languageMode) { case NBFXLanguageMode.Chinese: language = DefaultLanguage; break; case NBFXLanguageMode.English: language = EnglishLanguage; break; case NBFXLanguageMode.FollowEditor: default: language = GetLanguageFromEditor(); break; } s_CachedLanguageMode = languageMode; s_CachedPreferredLanguage = language; s_CachedPreferredLanguageValid = true; return language; } private static string GetLanguageFromEditor() { if (TryGetEditorLanguageName(out string language)) { return language; } return GetSystemLanguageName(); } private static bool TryGetEditorLanguageName(out string language) { language = string.Empty; Type localizationDatabaseType = typeof(Editor).Assembly.GetType("UnityEditor.LocalizationDatabase"); if (localizationDatabaseType == null) { return false; } const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; string[] memberNames = { "currentEditorLanguage", "CurrentEditorLanguage" }; foreach (string memberName in memberNames) { PropertyInfo propertyInfo = localizationDatabaseType.GetProperty(memberName, flags); if (propertyInfo != null && TryGetEditorLanguageValue(() => propertyInfo.GetValue(null, null), out language)) { return true; } FieldInfo fieldInfo = localizationDatabaseType.GetField(memberName, flags); if (fieldInfo != null && TryGetEditorLanguageValue(() => fieldInfo.GetValue(null), out language)) { return true; } } return false; } private static bool TryGetEditorLanguageValue(Func valueGetter, out string language) { language = string.Empty; try { return TryMapLanguageValue(valueGetter(), out language); } catch { return false; } } private static bool TryMapLanguageValue(object value, out string language) { language = string.Empty; if (value == null) { return false; } if (value is SystemLanguage systemLanguage) { language = MapSystemLanguage(systemLanguage); return true; } string languageName = value.ToString(); if (string.IsNullOrEmpty(languageName)) { return false; } string normalizedLanguageName = languageName.Replace("_", "-").ToLowerInvariant(); if (normalizedLanguageName.StartsWith("en", StringComparison.Ordinal) || normalizedLanguageName.Contains("english")) { language = EnglishLanguage; return true; } if (normalizedLanguageName.StartsWith("zh", StringComparison.Ordinal) || normalizedLanguageName.Contains("chinese")) { language = DefaultLanguage; return true; } language = DefaultLanguage; return true; } private static string GetSystemLanguageName() { return MapSystemLanguage(Application.systemLanguage); } private static string MapSystemLanguage(SystemLanguage systemLanguage) { switch (systemLanguage) { case SystemLanguage.English: return EnglishLanguage; case SystemLanguage.Chinese: case SystemLanguage.ChineseSimplified: case SystemLanguage.ChineseTraditional: return DefaultLanguage; default: return DefaultLanguage; } } private readonly struct ContentCacheKey : IEquatable { private readonly string _language; private readonly string _labelKey; private readonly string _labelFallback; private readonly string _tooltipKey; private readonly string _tooltipFallback; public ContentCacheKey( string language, string labelKey, string labelFallback, string tooltipKey, string tooltipFallback) { _language = language; _labelKey = labelKey; _labelFallback = labelFallback; _tooltipKey = tooltipKey; _tooltipFallback = tooltipFallback; } public bool Equals(ContentCacheKey other) { return StringEquals(_language, other._language) && StringEquals(_labelKey, other._labelKey) && StringEquals(_labelFallback, other._labelFallback) && StringEquals(_tooltipKey, other._tooltipKey) && StringEquals(_tooltipFallback, other._tooltipFallback); } public override bool Equals(object obj) { return obj is ContentCacheKey other && Equals(other); } public override int GetHashCode() { unchecked { var hash = 17; hash = hash * 31 + StringHash(_language); hash = hash * 31 + StringHash(_labelKey); hash = hash * 31 + StringHash(_labelFallback); hash = hash * 31 + StringHash(_tooltipKey); hash = hash * 31 + StringHash(_tooltipFallback); return hash; } } } private readonly struct InspectorContentCacheKey : IEquatable { private readonly string _language; private readonly string _key; private readonly string _fallback; private readonly string _tip; public InspectorContentCacheKey(string language, string key, string fallback, string tip) { _language = language; _key = key; _fallback = fallback; _tip = tip; } public bool Equals(InspectorContentCacheKey other) { return StringEquals(_language, other._language) && StringEquals(_key, other._key) && StringEquals(_fallback, other._fallback) && StringEquals(_tip, other._tip); } public override bool Equals(object obj) { return obj is InspectorContentCacheKey other && Equals(other); } public override int GetHashCode() { unchecked { var hash = 17; hash = hash * 31 + StringHash(_language); hash = hash * 31 + StringHash(_key); hash = hash * 31 + StringHash(_fallback); hash = hash * 31 + StringHash(_tip); return hash; } } } private readonly struct OptionsCacheKey : IEquatable { private readonly string _language; private readonly string _keyPrefix; private readonly string[] _fallback; private readonly int _fallbackLength; public OptionsCacheKey(string language, string keyPrefix, string[] fallback) { _language = language; _keyPrefix = keyPrefix; _fallback = fallback; _fallbackLength = fallback != null ? fallback.Length : 0; } public bool Equals(OptionsCacheKey other) { if (!StringEquals(_language, other._language) || !StringEquals(_keyPrefix, other._keyPrefix) || _fallbackLength != other._fallbackLength) { return false; } for (int i = 0; i < _fallbackLength; i++) { if (!StringEquals(_fallback[i], other._fallback[i])) { return false; } } return true; } public override bool Equals(object obj) { return obj is OptionsCacheKey other && Equals(other); } public override int GetHashCode() { unchecked { var hash = 17; hash = hash * 31 + StringHash(_language); hash = hash * 31 + StringHash(_keyPrefix); hash = hash * 31 + _fallbackLength; for (int i = 0; i < _fallbackLength; i++) { hash = hash * 31 + StringHash(_fallback[i]); } return hash; } } } private readonly struct InspectorTextCacheKey : IEquatable { private readonly string _language; private readonly string _key; private readonly string _fallback; public InspectorTextCacheKey(string language, string key, string fallback) { _language = language; _key = key; _fallback = fallback; } public bool Equals(InspectorTextCacheKey other) { return StringEquals(_language, other._language) && StringEquals(_key, other._key) && StringEquals(_fallback, other._fallback); } public override bool Equals(object obj) { return obj is InspectorTextCacheKey other && Equals(other); } public override int GetHashCode() { unchecked { var hash = 17; hash = hash * 31 + StringHash(_language); hash = hash * 31 + StringHash(_key); hash = hash * 31 + StringHash(_fallback); return hash; } } } private static bool StringEquals(string a, string b) { return string.Equals(a, b, StringComparison.Ordinal); } private static int StringHash(string value) { return value == null ? 0 : StringComparer.Ordinal.GetHashCode(value); } private sealed class LocalizationTable { private bool _loaded; private bool _warningLogged; public LocalizationTable(string csvAssetPath) { CsvAssetPath = csvAssetPath; Rows = new Dictionary>(StringComparer.OrdinalIgnoreCase); Languages = Array.Empty(); ContentCache = new Dictionary(); InspectorContentCache = new Dictionary(); InspectorTextCache = new Dictionary(); OptionsCache = new Dictionary(); InspectorOptionsCache = new Dictionary(); } public string CsvAssetPath { get; set; } public Dictionary> Rows { get; private set; } public string[] Languages { get; private set; } public Dictionary ContentCache { get; } public Dictionary InspectorContentCache { get; } public Dictionary InspectorTextCache { get; } public Dictionary OptionsCache { get; } public Dictionary InspectorOptionsCache { get; } public void Reload() { _loaded = false; _warningLogged = false; Rows = new Dictionary>(StringComparer.OrdinalIgnoreCase); Languages = Array.Empty(); ContentCache.Clear(); InspectorContentCache.Clear(); InspectorTextCache.Clear(); OptionsCache.Clear(); InspectorOptionsCache.Clear(); } public void EnsureLoaded() { if (_loaded) { return; } _loaded = true; Rows = new Dictionary>(StringComparer.OrdinalIgnoreCase); Languages = Array.Empty(); if (string.IsNullOrEmpty(CsvAssetPath)) { LogWarningOnce("ShaderGUI localization table is not registered."); return; } TextAsset csvAsset = AssetDatabase.LoadAssetAtPath(CsvAssetPath); if (csvAsset == null) { LogWarningOnce($"ShaderGUI localization csv not found at '{CsvAssetPath}'."); return; } ParseCsv(csvAsset.text); } private void ParseCsv(string csvText) { List rows = SplitRows(csvText); if (rows.Count == 0) { return; } List headers = ParseCsvRow(rows[0]); if (headers.Count < 2) { return; } var languages = new List(); for (int i = 1; i < headers.Count; i++) { if (!IsTooltipColumn(headers[i])) { languages.Add(headers[i]); } } Languages = languages.ToArray(); for (int rowIndex = 1; rowIndex < rows.Count; rowIndex++) { List values = ParseCsvRow(rows[rowIndex]); if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) { continue; } var languageMap = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 1; i < headers.Count; i++) { languageMap[headers[i]] = i < values.Count ? values[i] : string.Empty; } Rows[values[0]] = languageMap; } } private static List SplitRows(string csvText) { var rows = new List(); if (string.IsNullOrEmpty(csvText)) { return rows; } var builder = new StringBuilder(); bool inQuotes = false; for (int i = 0; i < csvText.Length; i++) { char current = csvText[i]; if (current == '"') { if (inQuotes && i + 1 < csvText.Length && csvText[i + 1] == '"') { builder.Append('"'); i++; } else { inQuotes = !inQuotes; } continue; } if (!inQuotes && (current == '\n' || current == '\r')) { if (builder.Length > 0) { rows.Add(builder.ToString()); builder.Length = 0; } if (current == '\r' && i + 1 < csvText.Length && csvText[i + 1] == '\n') { i++; } continue; } builder.Append(current); } if (builder.Length > 0) { rows.Add(builder.ToString()); } return rows; } private static List ParseCsvRow(string rowText) { var values = new List(); var builder = new StringBuilder(); bool inQuotes = false; for (int i = 0; i < rowText.Length; i++) { char current = rowText[i]; if (current == '"') { if (inQuotes && i + 1 < rowText.Length && rowText[i + 1] == '"') { builder.Append('"'); i++; } else { inQuotes = !inQuotes; } continue; } if (!inQuotes && current == ',') { values.Add(builder.ToString()); builder.Length = 0; continue; } builder.Append(current); } values.Add(builder.ToString()); return values; } private void LogWarningOnce(string message) { if (_warningLogged) { return; } _warningLogged = true; Debug.LogWarning(message); } } } }