????????由于一開始的代碼沒有考慮多語言場景,導致代碼中提示框和UI顯示直接用了中文,最近開始提取代碼的中文,提取起來太麻煩,所以拓展了之前的多語言包,降低了操作復雜度。最后把工具代碼提取出來到單獨項目里面,做一個分享,項目資源放到最后。
? ? ? ? 接入方案和使用步驟
- 導入package
- 修改EnumMessageTxt文件內容,根據自己的需求新增或刪除語言,如刪除繁體中文,則刪除第一行中的,繁體中文,刪除第三行的繁體中文縮寫|Cht,刪除多余的繁體語言包內容(可以把下面所有行都刪除),刪除后內容為
S,Key,簡體中文 //Area| 1|Area|Cn //Common2~500|Common 2|CommonTip|公共提示 3|CommonTip2|公共提示2 4|CommonTip3|公共提示 5|CommonFormatTip|提示{0} //PanelA501~601|PanelA 501|PanelABtn|按鈕
若要添加其他語言,務必要記得第三行中加入對應的縮寫
-
點擊拓展工具Tools/MessageDataTool/EnumMessageEditor,打開界面,點擊加載按鈕,選擇文件進行修改,修改完畢點擊生成代碼按鈕,等待生成完成,編譯完成即可,加載代碼在Panel腳本中,需要先初始化語言類型(縮寫),再進行加載
-
語言包加載邏輯在MessageManager.InitCyData中,根據自己項目的資源加載方式進行修改
-
由于只是簡單的分享, 所以工具可能缺少部分合法驗證,如遇到報錯等情況,可以查看堆棧信息,或者私信咨詢
? ? ? ? 邏輯介紹
? ? ? ? 項目目錄結構
? ? ? ? 1.生成的Asset資源,繁體多語言數據
? ? ? ? 2.生成的Asset資源,簡體多語言數據,路徑可以通過代碼修改,測試需要,放在Resources下加載
? ? ? ? 3.Demo場景
? ? ? ? 4.Unity Console日志重定向腳本,方便根據日志跳轉代碼中文處
? ? ? ? 5.匹配代碼中文的腳本
? ? ? ? 6.多語言工具窗口腳本
? ? ? ? 7.Demo界面
? ? ? ? 8.生成的多語言key腳本,枚舉類型
? ? ? ? 9.1,2中Asset資源對應的源Scriptable腳本
? ? ? ? 10.多語言管理器,加載多語言內容
? ? ? ? 11.多語言源txt文件,存放源內容
? ? ? 編輯器拓展功能
? ? ? ? 1.導入txt源文件,生成腳本,最初始的多語言工具拓展功能,沒有刪除,保留了下來,如果不使用預覽工具時,可使用該功能
? ? ? ? 2.多語言工具預覽和操作界面
? ? ? ? 3.匹配代碼中文?功能
?多語言txt源文件
S,Key,簡體中文,繁體中文
//Area|
1|Area|Cn|Cht
//Common2~500|Common
2|CommonTip|公共提示|公共提示繁體
3|CommonTip2|公共提示2|
4|CommonTip3|公共提示|
5|CommonFormatTip|提示{0}|
//PanelA501~601|PanelA
501|PanelABtn|按鈕|按鈕繁體
? ? ? ? ?1.首行格式固定為 S,Key,語言A,語言B,語言C 代碼會根據該行后面的語言類型數量創建對應的數據
? ? ? ? 2.第二行及后續所有以//開頭的內容,表示為注釋,不參與邏輯預算,主要功能是提示,以|分割,|后內容表示為標頭,無邏輯意義,主要功能是方面添加Key,后面結合工具介紹
? ? ? ? 3.除去首行及注釋行,其他行內容均為多語言內容,格式為 枚舉整型值|枚舉名稱|語言A文本|語言B文本...第三行這個枚舉整型值為1的是特例,表示上面各個語言的縮寫,也表示了生成的資源文件名稱,不能重復。注意并不是1中有多少種語言,這里就要添加多少種語言文本,但是至少多語言A的要有值,如果語言文本沒有定義,那么會取最前面的語言文本,也就是假如這里沒有定義語言C的多內容語言,那么語言C對應的多語言內容會取語言A的。配合生成的枚舉腳本和數據可以更直觀理清,這里實例用了兩種簡體中文和繁體中文。
? ? ? ? 生成的枚舉腳本
namespace Message
{public enum EnumMessage{//Area|/// <summary>/// Cn/// </summary>Area = 1,//Common2~500|Common/// <summary>/// 公共提示/// </summary>CommonTip = 2,/// <summary>/// 公共提示2/// </summary>CommonTip2 = 3,/// <summary>/// 公共提示/// </summary>CommonTip3 = 4,/// <summary>/// 提示{0}/// </summary>CommonFormatTip = 5,//PanelA501~601|PanelA/// <summary>/// 按鈕/// </summary>PanelABtn = 501,MaxNum,}
}
? ? ? ? ?生成的簡體中文多語言資源內容,注意名稱和上面源文件第三行的關系
????????生成的繁體中文多語言資源內容,注意名稱和上面源文件第三行的關系和空值填充?
?多語言工具預覽和操作界面
? ? ? ? 1.?選擇的多語言txt源文件路徑
? ? ? ? 2.加載:拉起選擇框,選擇要加載的源文件。重載:依據1中的路徑,重新刷新當前界面的數據。保存:保存界面數據到1中的路徑。生成腳本:先保存到源文件,再根據當前界面的數據生成枚舉腳本和Asset數據
? ? ? ? 3.輸入要搜索的多語言內容,內容為多語言A的內容
? ? ? ? 4.粘貼+搜索:粘貼剪貼板內容到3的輸入框,并進行搜索。搜索:搜索3的輸入框內容。取消搜索:隱藏搜索結果
? ? ? ? 5.搜索到的結果,可能有多條。內容從左到右依次為所在組名,枚舉整型值,枚舉名稱
? ? ? ? 6.根據當前條目生成獲取多語言值的代碼,代碼內容為
Message.MessageManager.I.GetMessage(Message.EnumMessage.PanelABtn)
代碼生成格式可以自行修改,邏輯在腳本文件MessageDataTool的GenerateMessage函數中
? ? ? ? 7.根據當前條目生成獲取Format多語言值的代碼,代碼內容為
Message.MessageManager.I.GetMessageFormat(Message.EnumMessage.PanelABtn)
注意要配合format類型的多語言使用,代碼生成格式可以修改,邏輯在腳本文件MessageDataTool的GenerateFormatMessage函數中
? ? ? ? 8.組列表,所有 // 開頭的行,選中的為綠色高亮
? ? ? ? 9.要添加的組名稱,格式需要以//開頭
? ? ? ? 10.添加新組,組名為9中內容
? ? ? ? 11.當前選中的組,可以在此處修改組名稱
? ? ? ? 12.修改組名稱按鈕,必須點擊按鈕才能修改,同時為了保證源文件的組順序不變化,這里會自動保存文件
? ? ? ? 13.組的標頭,可以理解為當前組下所有內容的枚舉名稱前綴,方面增加前綴的,可以為空
? ? ? ? 14.枚舉整型值,全局唯一索引,可以手動修改,同一組內添加條目默認遞增1,組內的第一個新建條目需要手動修改索引值為合適值
? ? ? ? 15.枚舉名稱,全局唯一,可以手動修改
? ? ? ? 16.給枚舉名稱添加標頭前綴,保證格式統一
? ? ? ? 17.多語言內容,在這里修改
? ? ? ? 18.將當前剪貼板內容粘貼到17的輸入框內
? ? ? ? 20.同6
? ? ? ? 21.同7
? ? ? ? 22.刪除當前條目,為了防止舊數據出錯,刪除條目不會修正索引,比如之前索引列表為2,3,4,刪除了3之后,索引為4不會修正為3,而是留空
? ? ? ? 23.添加新的條目,索引遞增1,若13有標頭,會自動給15輸入框中賦值標頭內容
? ? ? ? 界面的操作的邏輯都在腳本里面,因為涉及到布局代碼過多,這里不再解讀,可以搜索按鈕名稱查看對應的邏輯
? ? ? ? 匹配代碼中文腳本
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;/// <summary>
/// 匹配代碼中的中文
/// </summary>
public class MatchCnInCode : Editor
{// 正則表達式匹配中文字符static Regex chineseCharRegex = new Regex("[\u4e00-\u9fa5]");//忽略的文件夾static string[] IgnoreDir = new string[] { @"/Message/", @"/ProtoBuffer/", @"/ThirdPlugins/" };//忽略的代碼行static string[] IgnoreCode = new string[] { "[Header(", "Debugger.Log", "Debug.Log","#region" };//搜索的腳本路徑static string searchScriptPath = "Assets/HotUpdate/Scripts";[MenuItem("Tools/MatchCnInCode")]static void Method(){if (EditorUtility.DisplayDialog("提示", "開始匹配代碼中的中文", "確定", "取消")){Do();}}static void Do(){// 搜索所有的 .cs 文件var csFiles = AssetDatabase.FindAssets("t:script", new string[] { searchScriptPath });for (int i = 0; i < csFiles.Length; i++){var file = AssetDatabase.GUIDToAssetPath(csFiles[i]);bool hit = false;for (int j = 0; j < IgnoreDir.Length; j++){if (file.Contains(IgnoreDir[j])){hit = true;break;}}if (hit){continue;}var obj = AssetDatabase.LoadAssetAtPath<Object>(file);using (StreamReader sr = new StreamReader(file)){//匹配到中文bool matchCn = false;//在多行注釋中bool inMulAnnotation = false;int lineIndex = 0;while (!sr.EndOfStream){lineIndex++;//要匹配的內容bool matched = false;var line = sr.ReadLine();if (string.IsNullOrEmpty(line)){continue;}//多行注釋開始的索引int mulAnnotationStartIndex = line.IndexOf("/*");if (mulAnnotationStartIndex != -1){//在多行注釋中inMulAnnotation = true;var indexBeforeStr = line.Substring(0, mulAnnotationStartIndex);var match = chineseCharRegex.Match(indexBeforeStr);if (match.Success){Debug.LogError($"{file}=>{lineIndex}=> {match.Value} => {line} ", obj);matched = true;}}//在多行注釋中if (inMulAnnotation){//不含多行注釋開始標識的匹配串string noAnnotationStartStr = string.Empty;//該行沒有開始標識,直接找結束標識if (mulAnnotationStartIndex == -1){noAnnotationStartStr = line;}else//有開始標識,從開始標識后找結束標識防止/*/的情況{noAnnotationStartStr = line.Substring(mulAnnotationStartIndex + 2);}var mulAnnotationEndIndex = noAnnotationStartStr.IndexOf("*/");//有結尾符if (mulAnnotationEndIndex != -1){inMulAnnotation = false;var indexAfterStr = noAnnotationStartStr.Substring(mulAnnotationEndIndex);var match = chineseCharRegex.Match(indexAfterStr);if (match.Success){Debug.LogError($"{file}=>{lineIndex}=> {match.Value} => {line} ", obj);matched = true;}}}else{//查找是否有單行注釋var singleAnnotationIndex = line.IndexOf("//");if (singleAnnotationIndex == -1){bool hitIgnore = false;for (int j = 0; j < IgnoreCode.Length; j++){var logIndex = line.IndexOf(IgnoreCode[j]);if (logIndex != -1){hitIgnore = true;}}if (!hitIgnore){var match = chineseCharRegex.Match(line);if (match.Success){Debug.LogError($"{file}=>{lineIndex}=> {match.Value} => {line} ", obj);matched = true;}}}else{var match = chineseCharRegex.Match(line.Substring(0, singleAnnotationIndex));if (match.Success){Debug.LogError($"{file}=>{lineIndex}=> {match.Value} => {line} ", obj);matched = true;}}}//匹配所有行,還是匹配到一行后返回//if (matched)//{// break;//}}}}}
}
? ? ? ? ?主要邏輯就是根據正則匹配中文,同時要忽略掉注釋以及特殊字符開頭的行,注意上面的Debug.LogError輸出格式為腳本路徑=>行數=>匹配到的首字=>整行內容,下面日志重定向要用。這幾處輸出可以封裝,這里省略了封裝代碼,下面是實際運行時的日志輸出,注意這里的堆棧信息,也是日志重定向需要使用的
? ? ? ? 日志重定向腳本
using System;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditorInternal;namespace TEngine.Editor
{/// <summary>/// 日志重定向相關的實用函數。/// </summary>internal static class LogRedirection{[OnOpenAsset(0)]private static bool OnOpenAsset(int instanceID, int line){if (line <= 0){return false;}// 獲取資源路徑string assetPath = AssetDatabase.GetAssetPath(instanceID);// 判斷資源類型if (!assetPath.EndsWith(".cs")){return false;}var stackTrace = GetStackTrace();if (!string.IsNullOrEmpty(stackTrace) && stackTrace.Contains("MatchCnInCode.cs") && stackTrace.StartsWith("Assets")){//中文匹配的輸出var arr = stackTrace.Split("=>");//0:路徑 ,1: 行數var path = arr[0];var lineNum = int.Parse(arr[1]);var fullPath = UnityEngine.Application.dataPath.Substring(0, UnityEngine.Application.dataPath.LastIndexOf("Assets", StringComparison.Ordinal));fullPath = $"{fullPath}{path}";// 跳轉到目標代碼的特定行InternalEditorUtility.OpenFileAtLineExternal(fullPath.Replace('/', '\\'), lineNum);return true;}return false;}/// <summary>/// 獲取當前日志窗口選中的日志的堆棧信息。/// </summary>/// <returns>選中日志的堆棧信息實例。</returns>private static string GetStackTrace(){// 通過反射獲取ConsoleWindow類var consoleWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.ConsoleWindow");// 獲取窗口實例var fieldInfo = consoleWindowType.GetField("ms_ConsoleWindow",BindingFlags.Static |BindingFlags.NonPublic);if (fieldInfo != null){var consoleInstance = fieldInfo.GetValue(null);if (consoleInstance != null)if (EditorWindow.focusedWindow == (EditorWindow)consoleInstance){// 獲取m_ActiveText成員fieldInfo = consoleWindowType.GetField("m_ActiveText",BindingFlags.Instance |BindingFlags.NonPublic);// 獲取m_ActiveText的值if (fieldInfo != null){var activeText = fieldInfo.GetValue(consoleInstance).ToString();return activeText;}}}return null;}}
}
? ? ? ? 主要邏輯在函數OnOpenAsset中,var stackTrace = GetStackTrace();這里獲取了整個輸出的所有信息,然后下一行代碼判斷信息中是否有“MatchCnInCode.cs”以及是否以“Assets”開頭,如果滿足這兩個條件,我們需要對該日志輸出進行重定向。再根據我們上面的輸出格式腳本路徑=>行數=>匹配到的首字=>整行內容,可以取到路徑和行數,就可以實現點擊該行輸出,直接定位到日志中標記的行數了,注意這里的路徑不能多空格,結尾的空格也不行,否則會無法定位到腳本編輯工具中,所以日志輸出的時候要檢查是否有多余的空格。
資源地址?
https://download.csdn.net/download/a598211757/90628215