Unity游戏多语言工具包
由于一开始的代码没有考虑多语言场景,导致代码中提示框和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
相关文章:
Unity游戏多语言工具包
由于一开始的代码没有考虑多语言场景,导致代码中提示框和UI显示直接用了中文,最近开始提取代码的中文,提取起来太麻烦,所以拓展了之前的多语言包,降低了操作复杂度。最后把工具代码提取出来到单独项目里面,…...
实验三 I/O地址译码
一、实验目的 掌握I/O地址译码电路的工作原理。 二、实验电路 实验电路如图1所示,其中74LS74为D触发器,可直接使用实验台上数字电路实验区的D触发器,74LS138为地址译码器, Y0:280H~287H&…...
视觉语言导航(VLN):连接语言、视觉与行动的桥梁
文章目录 1. 引言:什么是VLN及其重要性?2. VLN问题定义3. 核心挑战4. 基石:关键数据集与模拟器5. 评估指标6. 主要方法与技术演进6.1 前CLIP时代:奠定基础6.2 后CLIP时代:视觉与语言的统一 7. 最新进展与前沿趋势 (202…...
计算机网络中科大 - 第7章 网络安全(详细解析)-以及案例
目录 🛡️ 第8章:网络安全(Network Security)优化整合笔记📌 本章学习目标 一、网络安全概念二、加密技术(Encryption)1. 对称加密(Symmetric Key)2. 公钥加密࿰…...
2026《数据结构》考研复习笔记一(C++基础知识)
C基础知识复习 一、数据类型二、修饰符和运算符三、Lambda函数和表达式四、数学函数五、字符串六、结构体 一、数据类型 1.1基本类型 基本类型 描述 字节(位数) 范围 char 字符类型,存储ASCLL字符 1(8位) -128…...
XCTF-web(四)
unserialize3 需要反序列化一下:O:4:“xctf”:2:{s:4:“flag”;s:3:“111”;} php_rce 题目提示rce漏洞,测试一下:?s/Index/\think\app/invokefunction&functioncall_user_func_array&vars[0]phpinfo&vars[1][]1 flag࿱…...
在Vue项目中查询所有版本号为 1.1.9 的依赖包名 的具体方法,支持 npm/yarn/pnpm 等主流工具
以下是 在Vue项目中查询所有版本号为 1.1.9 的依赖包名 的具体方法,支持 npm/yarn/pnpm 等主流工具: 一、使用 npm 1. 直接过滤依赖树 npm ls --depth0 | grep "1.1.9"说明: npm ls --depth0:仅显示直接依赖…...
若依微服务版启动小程序后端
目录标题 本地启动,dev对应 nacos里的 xxx-xxx-dev配置文件 本地启动,dev对应 nacos里的 xxx-xxx-dev配置文件...
莒县第六实验小学:举行“阅读世界 丰盈自我”淘书会
4月16日,莒县第六实验小学校园内书香四溢、笑语盈盈,以“阅读世界 丰盈自我”为主题的第二十四届读书节之“淘书会”活动火热开启。全校师生齐聚一堂,以书会友、共享阅读之乐,为春日校园增添了一抹浓厚的文化气息。 活动在悠扬的诵…...
国产数据库与Oracle数据库事务差异分析
数据库中的ACID是事务的基本特性,而在Oracle等数据库迁移到国产数据库国产中,可能因为不同数据库事务处理机制的不同,在迁移后的业务逻辑处理上存在差异。本文简要介绍了事务的ACID属性、事务的隔离级别、回滚机制和超时机制,并总…...
C++学习记录:
今天我们来学习一门新的语言,也是C语言最著名的一个分支语言:C。 在C的学习中,我们主要学习的三大组成部分:语法、STL、数据结构。 C的介绍 C的历史可追溯至1979年,当时贝尔实验室的本贾尼斯特劳斯特卢普博士在面对复杂…...
等离子体浸没离子注入(PIII)
一、PIII 是什么?基本原理和工艺 想象一下,你有一块金属或者硅片(就是做芯片的那种材料),你想给它的表面“升级”,让它变得更硬、更耐磨,或者有其他特殊功能。怎么做呢?PIII 就像是用…...
LeetCode-16.最接近的三数之和 C++实现
一 题目描述 给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。 返回这三个数的和。 假定每组输入只存在恰好一个解 示例 1: 输入:nums [-1,2,1,-4], target 1 输出&…...
【机器学习】每日一讲-朴素贝叶斯公式
文章目录 **一、朴素贝叶斯公式详解****1. 贝叶斯定理基础****2. 从贝叶斯定理到分类任务****3. 特征独立性假设****4. 条件概率的估计** **二、在AI领域的作用****1. 文本分类与自然语言处理(NLP)****2. 推荐系统****3. 医疗与生物信息学****4. 实时监控…...
C 语言中的 volatile 关键字
1、概念 volatile 是 C/C 语言中的一个类型修饰符,用于告知编译器:该变量的值可能会在程序控制流之外被意外修改(如硬件寄存器、多线程共享变量或信号处理函数等),因此编译器不应对其进行激进的优化(如缓存…...
Python自学第1天:变量,打印,类型转化
突然想学Python了。经过Deepseek的推荐,下载了一个Python3.12安装。安装过程请自行搜索。 乖乖从最基础的学起来,废话不说了,上链接,呃,打错了,上知识点。 变量的定义 # 定义一个整数类型的变量 age 10#…...
探索鸿蒙应用开发:ArkTS应用执行入口揭秘
# 探索鸿蒙应用开发:ArkTS应用执行入口揭秘 在鸿蒙应用开发的领域中,ArkTS作为声明式开发语言,为开发者们带来了便捷与高效。对于刚接触鸿蒙开发的小伙伴来说,搞清楚ArkTS应用程序的执行入口是迈向成功开发的关键一步。今天&…...
idea中提高编译速度研究
探索过程: 有三种情况: 第一种: idea中用eclipse编译器编译springboot项目,然后debug启动Application报错找不到类。 有待继续研究。 第二种: idea中用javac编译器编译springboot项目,重新构建用时&a…...
静态链接part2
编译 语义分析 由语义分析器完成,这个步骤只是完成了对表达式的语法层面的分析,它并不了解这个语句是否真的有意义(例如在C语言中两个指针做乘法运算,这个语句在语法上是合法的,但是没有什么意义;还有同样…...
Vue3+Vite+TypeScript+Element Plus开发-17.Tags-组件构建
系列文档目录 Vue3ViteTypeScript安装 Element Plus安装与配置 主页设计与router配置 静态菜单设计 Pinia引入 Header响应式菜单缩展 Mockjs引用与Axios封装 登录设计 登录成功跳转主页 多用户动态加载菜单 Pinia持久化 动态路由 -动态增加路由 动态路由-动态删除…...
MATLAB R2023b如何切换到UTF-8编码,解决乱码问题
网上都是抄来抄去,很少有动脑子的,里面说的方法都差不多,但是在R2023b上怎么试都不管用,所以静下心来分析了下,我的 ...\MATLAB\R2016b\bin\lcdata.xml 里面除了注释几乎是空的,如果这样就能用为什么要加…...
3D语义地图中的全局路径规划!iPPD:基于3D语义地图的指令引导路径规划视觉语言导航
作者: Zehao Wang, Mingxiao Li, Minye Wu, Marie-Francine Moens, Tinne Tuytelaars 单位:鲁汶大学电气工程系,鲁汶大学计算机科学系 论文标题: Instruction-guided path planning with 3D semantic maps for vision-language …...
ShellScript脚本编程
语法基础 脚本结构 我们先从这个小demo程序来窥探一下我们shell脚本的程序结构 #!/bin/bash# 注释信息echo_str"hello world"test(){echo $echo_str }test echo_str 首先我们可以通过文本编辑器(在这里我们使用linux自带文本编辑神器vim),新建一个文件…...
【HarmonyOS 5】敏感信息本地存储详解
【HarmonyOS 5】敏感信息本地存储详解 前言 鸿蒙其实自身已经通过多层次的安全机制,确保用户敏感信息本地存储安全。不过再此基础上,用户敏感信息一般三方应用还需要再进行加密存储。 本文章会从鸿蒙自身的安全机制进行展开,最后再说明本地…...
大厂面试:六大排序
前言 本篇博客集中了冒泡,选择,二分插入,快排,归并,堆排,六大排序算法 如果觉得对你有帮助,可以点点关注,点点赞,谢谢你! 1.冒泡排序 //冒泡排序ÿ…...
.exe变成Windows服务
.exe变成Windows服务) 场景步骤 1: 安装 PyInstaller和win32serviceutil步骤 2: 使用 PyInstaller 创建 .exe 文件步骤 3: 检查生成的 .exe 文件步骤 4: 安装服务步骤 5: 启动服务步骤 6: 配置服务自动启动(可选)步骤 7: 检查服务状态完整示例…...
探索鸿蒙沉浸式:打造无界交互体验
一、鸿蒙沉浸式简介 在鸿蒙系统中,沉浸式是一种极具特色的设计理念,它致力于让用户在使用应用时能够全身心投入到内容本身,而尽可能减少被系统界面元素的干扰。通常来说,就是将应用的内容区巧妙地延伸到状态栏和导航栏所在的界面…...
el-tree组件使用过滤时,不展示筛选目标的子节点
1.el官方示例过滤方法 const filterNode (value: string, data: Tree) > {if (!value) return truereturn data.label.includes(value) }2.修改后的过滤方法 /*** 树节点过滤*/ const filterNode (value, data, node) > {if (!value) return true;let parentNode no…...
超详细!Android 面试题大汇总与深度解析
一、Java 与 Kotlin 基础 1. Java 的多态是如何实现的? 多态是指在 Java 中,同一个行为具有多个不同表现形式或形态的能力。它主要通过方法重载(Overloading)和方法重写(Overriding)来实现。 方法重载&a…...
网站301搬家后谷歌一直不收录新页面怎么办?
当网站因更换域名或架构调整启用301重定向后,许多站长发现谷歌迟迟不收录新页面,甚至流量大幅下滑。 例如,301跳转设置错误可能导致权重传递失效,而新站内容与原站高度重复则可能被谷歌判定为“低价值页面”。 即使技术层面无误&a…...
