最近总有人跑来问我,UE5项目里做中英文切换到底怎么落地。我一开始以为大家问的是那个Localization Dashboard怎么点,后来聊深了才发现,问题大多出在"为什么我收集不到文本""为什么切换语言后界面不刷新"这类实际工程环节上。这个问题确实值得写一篇完整的东西:从怎么规划收集规则,到运行时切换,再到翻译表怎么交给外包,一气呵成讲清楚。这篇内容我会结合自己做过的一个出海项目来写,把踩过的坑和最终留下的方案都摆出来,希望能帮你少走点弯路。
如果你做的游戏已经写了几百个硬编码字符串,那这篇文章就更得看了——那种状态下接本地化,基本等于把UI层重做一遍。而UE5本身的本地化能力其实相当完整,关键是你得按它的套路来写代码、摆文本,否则后面所有功能都会变得别扭。下面我按自己的实操顺序,把整套流程拆开讲。
1. 为什么UE5游戏都需要一整套本地化方案
1.1 从出海立项那天就要想清楚的事
很多人觉得中英文切换就是"把字符串换成变量"这么简单,等真做起来才发现远不止。游戏里的文本不只是界面按钮上的字,还包括弹窗提示、任务描述、物品名称、对话字幕、技能说明、加载界面的小提示……这些内容一旦散落在成千上万个蓝图和C++字符串里,想统一替换就是一场灾难。
UE5把本地化做成了引擎级能力,从文本收集、翻译管理、运行期查询到数字日期格式化,全部内置。只要你遵循它的规则,整个流程可以做到"写代码时几乎无感"。但前提是,你必须在项目早期就定好规矩。
对一个准备出海的团队,我建议立项当天就把这三件事定下来:
- 界面显示文本一律用FText,禁止用FString或直接写死的中文字符串。
- 所有需要翻译的文案,尽量集中到DataTable或单独的数据资产里管理,而不是写在每个Widget内部。
- 语言切换事件提前设计好,所有界面走后端订阅刷新,而不是各自为政地刷新。
为什么这三点这么重要?因为本地化的本质不是"翻译字符串",而是"把文本从代码中抽离出来,建立一套可被检索、可被翻译、可被替换的索引系统"。如果前期不这么做,后期改造成本会指数级上升。我自己最初那个项目就是反面教材——初期图省事,大量用FString拼UI文本,结果接本地化的时候,光是找一个任务提示在哪被赋值就花了三天。
1.2 UE5本地化方案的几种实现路径对比
在决定用哪套方案之前,我建议先看清楚市面上的几条路。这里给一个基于实际项目经验的对比,方便你判断。
| 方案 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| UE5自带Localization Dashboard | 与引擎深度集成、自动收集FText、支持蓝图/C++/UMG、自带翻译编辑器与CSV导入导出 | 学习曲线略陡、标准流程对动态文本有一定约束 | 绝大多数常规游戏项目,尤其是中小团队 |
| 自研DataTable+运行时查Key | 数据完全可控、可做热更、适合强联网数据驱动项目 | 需要自己实现收集工具、UI绑定逻辑复杂、重复造轮子 | 已有成熟数据驱动框架的团队 |
| 第三方本地化插件 | 上手快、额外提供术语管理/机器翻译等功能 | 插件维护有风险、功能冗余、可能出现兼容性问题 | 项目周期极短、要求快速出多语言Demo |
我自己的结论是:除非你有特殊理由,否则直接选UE5自带的Localization Dashboard。原因很简单,它的收集机制基于FText,而你本来就应该用FText来写UI文本。这意味着你不需要为本地化额外设计一套数据格式,也不需要写一坨扫描脚本去抓字符串。引擎在编译期、保存资产时,就会自动把所有FText记录进本地化数据库。
当然,自带方案也有一些别扭的地方,比如翻译Key的稳定性、动态加载语言包的姿势等。这些我放在后面专门讲,先顺着标准流程往下走。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Localization Dashboard:从收集到翻译的完整链路
2.1 前期准备:哪些文本能被系统抓取到
在使用Localization Dashboard之前,你得先搞清楚它的"眼睛"长在哪——也就是它到底能收集到什么文本。UE5本地化的收集机制,核心只认一种类型:FText。任何FText字面量,无论是蓝图里创建Text变量、UMG面板上直接输入的文字,还是C++里用LOCTEXT宏定义的字符串,都会被系统自动识别并登记到本地化数据库中。
反过来,下面这些写法通通不会被收集:
- C++里的FString、std::string、const char*。
- FString拼接出来的句子。
- 蓝图里用String变量存储、再赋值给Text属性。
- UMG中直接绑定到一个FString返回值的文本。
这一点怎么强调都不过分。因为很多新手在蓝图里图方便,会把UI文本暂存在String变量里,最后再转成Text显示。等到Gather的时候才发现,界面上所有文字都没进翻译表,那一刻的心情我太懂了。
那怎么判断自己代码里有没有漏网之鱼?一个笨但有效的方法是:在Project Settings里开启本地化收集后,跑一次Gather,然后看翻译列表里有没有你界面上的那批文案。如果某个按钮上的字没出现,那它八成不是FText。
2.2 创建本地化目标与收集规则
真正开始配置,走的路径是Project Settings -> Localization。UE5的这套界面第一次看可能会有点懵,但逻辑其实清晰:你先定义一个"本地化目标"(Target),告诉引擎要收集哪些内容、要支持哪些语言,然后执行一次Gather,引擎就会把匹配的文本全部捞进一个待翻译列表。
我常用的配置流程是:
- 打开Project Settings,找到Localization分类。如果项目和语言还没配置,先点Add Game Target,新建一个目标,比如叫"GameText"。
- 在Target的Cultures里添加语言。中文选Simplified Chinese(zh-Hans),英文选English(en),如果要繁中再接zh-Hant。
- 配置Gather From。这里决定引擎扫描哪些路径。默认情况下会扫描整个Content目录,但我建议按模块来,只勾选实际存在UI文本的目录,避免把插件噪音收集进来。
- 打开Localization Dashboard,执行Gather Text。这一步会扫描所有指定的Package,把其中的FText登记为"源文本"。
- 扫描完会生成待翻译条目,这个时候进入Translate阶段,边写翻译边确认。
关于Culture代码这里多说一句。UE5遵循BCP 47标准,所以简体中文是zh-Hans,繁体是zh-Hant,美式英语是en-US,通用英语是en。如果你在引擎里只看到"Chinese"而没细分,那通常指的是简体中文。这个代码后面运行时切换语言的时候也要用,记牢。
2.3 翻译工作区的填坑与批量导入
Gather完成之后,本地化Dashboard左侧会出现一个"Translation"标签页。点进去,你可以按语言筛选待翻译文本,也可以在右侧编辑区逐条填写译文。UE5会同时显示Source String和当前Culture的Translation,还会默认复制源文本作为初始翻译,方便你在原地改。
手动一条条填适合小项目,但真实的出海项目文本量动辄几千条,肯定要借助外部流程。UE5支持把翻译条目导出为CSV,交给翻译团队批量处理后再导回来。具体做法是:
- 在Translation页面里,选择目标语言,导出当前视图为CSV文件。
- CSV的横表头一般包括Namespace、Key、SourceString、Translation这些列。让翻译组只动Translation列。
- 拿到回传文件后,确认编码为UTF-8(最好带BOM,否则中文可能在Excel里乱码),然后在Dashboard里选择导入,按Key匹配回填。
这里有个关键经验:翻译条目是依赖Namespace和Key来保证唯一性的,而Key通常由源字符串或你手动指定的名字生成。一旦你改了代码里的源字符串,Key可能变化,已填的译文就会失配,表现为"翻译对不上"或"原文变了但译文还是旧的"。
所以我的习惯是:在写代码时,就为每个FText指定稳定且语义明确的Key,比如Dialog_Confirm、ItemName_IronSword,不要放任引擎自动生成随机Key。虽然自动生成的Key也能用,但只要你重构文案,翻译就全散了。
3. 运行时切换中英文的核心机制
3.1 Culture、Locale与切换API的关系
翻译表就位后,真正让游戏在运行时切换语言,靠的是引擎的国际化系统(Internationalization)。这里面有几个概念容易搞混:Culture(区域性)、Locale(区域)和Language(语言)。
简单理解,Culture代表"一种语言在某个地区的写法习惯",包含语言、数字、日期、排序等多层信息。比如en-US和en-GB虽然都是英文,但日期格式和部分词汇有差异。UE5的FInternationalization核心类,通过SetCurrentCulture接口切换当前区域性。
在C++里,一行代码的事:
cpp复制FInternationalization::Get().SetCurrentCulture(TEXT("zh-Hans"));
FInternationalization::Get().SetCurrentCulture(TEXT("en"));
切到对应Culture后,引擎会把当前所有通过FText查询的本地化字符串,自动按新Culture的翻译表重新取词。这就是为什么你只要把UI文本写成FText,切换就有一半概率能"自己动起来"。
在蓝图里,也有对应的SetCurrentCulture节点,位置在Internationalization分类下,输入一个字符串形参数即可。建议你把切换逻辑封装在GameInstance中,而不是散落在各个UI里,这样后续加语言、加流程都方便。
3.2 用蓝图通知UI刷新的完整流程
切换API本身简单,真正坑人的是"切完之后界面不刷新"。
原因在于:已经生成的TextBlock控件,不会因为你改了CurrentCulture就自动重新取值。控件在创建时已经用当时的Culture把字符串取出并显示出来了,后续Culture变化它并不知道。所以你需要一套机制,让所有打开的界面响应语言变化并主动刷新。
我实践下来最稳的流程是事件广播方案:
- 在GameInstance中定义一个动态多播事件,比如
OnLanguageChanged。 - 语言切换时,调用
SetCurrentCulture,然后立刻广播这个事件。 - 所有需要刷新文本的Widget,在Construct时绑定该事件,在Destruct时解绑。
- 事件触发后,每个Widget重新调用自己的"刷新文本"函数,把TextBlock的Text重新赋值一遍。
这个方案的好处是不需要重建整个UI,界面状态(比如滚动位置、选中项)不会丢。缺点是每个Widget都要写一段绑定和刷新逻辑,略显啰嗦。但说实话,啰嗦换来稳定,值。
如果你项目里UI层级很简单,比如就一个主界面加几个弹窗,那还有一个更暴躁的方案:切换语言后,直接把整个UIManager持有的所有Widget销毁,再重新创建根Widget。虽然看起来"重",但很多休闲游戏就一个界面,重建一次也没什么成本,而且绝对不会出现漏刷新的界面。
3.3 已打开界面不刷新的处理方案
事件广播也好,重建界面也好,背后要解决的核心问题是:如何让"已经存在于屏幕上的文本"重新走一遍翻译查询。
我之前在项目里采用的是"绑定到属性+手动触发"的组合。每个需要多语言的Widget,不直接在Designer里写死Text内容,而是把TextBlock的Text绑定到一个蓝图函数,这个函数返回一个FText。这样当语言切换广播到达后,我只需要调用每个TextBlock的SetText,重新触发绑定求值,文本就会按新Culture刷新。
这里注意:UE5的UMG绑定是按需求值的,但不会主动监听Culture变化。所以一定得有外部事件去"踹它一脚"。常见做法是:
- 在Widget内部保存好所有需要刷新的TextBlock引用,事件来了逐个SetText。
- 或者用UMG的
RebuildWidget方式强制重建整个控件树,但这会丢失部分运行时状态,慎用。
我个人的建议是宁可多写几行刷新函数,也别贪图简单去重建。因为你永远猜不到玩家会在哪个界面停留多久,一旦有状态残留,体验就很怪。
4. 踩过的坑:FText与FString、硬编码与动态文本
4.1 FText的文本引用和Format的坑
项目做久了你会发现,本地化最容易出问题的地方不在翻译本身,而在代码里如何处理"含变量的动态文本"。比如中文的"玩家你好,欢迎回来",变量是玩家名字。如果用字符串拼接,在英文里也许能拼出来,但不同语言的语序完全不同,拼接法根本没法用。
正确做法是使用FText和命名占位符。在C++里,我会这样写:
cpp复制#define LOCTEXT_NAMESPACE "GameUI"
FText FormatMessage = FText::Format(
LOCTEXT("WelcomeBack", "Welcome back, {PlayerName}!"),
FText::FromString(PlayerName)
);
#undef LOCTEXT_NAMESPACE
这样翻译人员可以在Translation Editor里看到一条带{PlayerName}占位符的文本,他可以自由调整占位符在句子中的位置,而不会破坏代码结构。反过来,如果你用FString去拼接,翻译人员看到的就是一团碎块,完全没法处理语序问题。
另外,FText::Format的占位符格式分两种:{Index}形式(如{0})和命名参数形式。命名参数可读性更好,尤其在翻译协调时,强烈推荐。
4.2 动态拼接字符串为什么收集不到
很多人问过我:"我明明用FText了,为什么Gather还是抓不到?"
十有八九是踩了动态拼接的坑。比如在C++里写:
cpp复制FText MyText = FText::FromString(FString::Printf(TEXT("%s,欢迎回来"), *PlayerName));
这里MyText虽然是个FText,但它是由FString临时转来的,里面的中文字符串在编译期并没有被登记到本地化数据库。Gather扫描器只能看到"源代码里字面出现的FText构造",看不到运行时才生成的内容。
同样的坑在蓝图里也存在:用Make Literal Text创建的是可本地化文本,但如果你先把字符串变量连接成一整句话,再转成Text,那这个Text就只是一块"普通字符串的容器",没有本地化身份。
所以务必记住一条铁律:**可被本地化的文本,必须在源代码或蓝图中以一个完整的FText构造出现,且其中包含的变量必须通过Format占位符塞进去。**任何形式的先拼字符串再转Text,都会让本地化失效。
4.3 数字、复数、百分比的本地化细节
还有一个容易被忽视的角落是数字和复数。中文里"1条消息"和"5条消息"基本只是换个数字,英文里却是"1 message"和"5 messages"。如果你在代码里写:
cpp复制FText::Format(LOCTEXT("MsgCount", "{0} messages"), Count);
那么在英语下,Count为1时你也会看到"1 messages",相当别扭。
UE5提供了基于Culture的复数规则支持。你可以在Translation Editor里为一条文本添加Plural Form,引擎会按当前Culture自动选择"单数"或"复数"的翻译。这个功能在英德俄这类复数和格变化复杂的语言里几乎是必需的,建议中文项目也提前了解,不要等到做俄语本地化时才补课。
数字本身还有个格式问题:很多语言千分位、小数点的写法不一样。UE5在FText格式化时会自动根据Culture应用数字规则,比如英文习惯1,000.5,德文是1.000,5。如果你在代码里手动ToString转数字,这些规则就全丢了。
5. 实际项目中的扩展方案:热更翻译表与外包协作
5.1 翻译表导出的CSV交接流程
当翻译任务交给外包或发行商时,UE5的Translation CSV导出功能就派上用场了。我建议从一开始就把这套交接流程固定下来,省得每次都在微信里发Excel。
具体流程:
- 在Localization Dashboard中,选择目标语言,确保翻译工作区显示的是最新Gather结果。
- 导出CSV。导出时注意选择"All"或按需筛选,通常一次导出全部就好。
- 把CSV发给翻译团队,并告知他们:只修改Translation列,不要改Namespace和Key。
- 收到回传后,先检查文件编码。Excel编辑过又另存为的CSV,很容易变成ANSI编码,导致中文乱码。用VS Code或Notepad++重新转成UTF-8 with BOM再导入。
- 在Dashboard中导入CSV,选择按Key匹配。导入后逐条抽查几条,确认没有错位。
这里的关键是"只改Translation列"。翻译团队如果手滑改了Key,导入时会创建一堆新条目,而原有条目变成未翻译,轻则界面变英文,重则触发断言。
为了防止这种事,我交付CSV前会把Namespace、Key、SourceString列全部锁定,或者干脆做一次宏格式化,让Translation列是唯一可编辑列。虽然不能从技术上完全锁死,但至少能在沟通上划清边界。
5.2 启动时动态下载语言包的工程实践
如果你做的是联网游戏,大概率希望语言包能热更新,这样发新版本时不用为了某条翻译错误强制玩家下载整包。UE5自带的本地化资源默认是随包走的,想热更需要自己做一层。
我实践过的一个可行方案是:把翻译数据抽到远端,启动时下载当前语言的CSV或JSON,然后写入游戏内的自定义翻译表。UI在查文本时,不再走FText标准查询,而是走自定义的翻译查表函数。
具体做法:
- 项目里统一用一个
LocalizationService(可以是Subsystem)。 - 所有需要本地化显示的文本,都要有一个全局唯一的Key。UI里用Key查询,而不是直接塞字符串。
- 启动时,根据当前语言从CDN拉取对应的翻译表,解析后缓存到内存。
- 界面刷新时,通过
LocalizationService::GetText(Key)获取本地化文本。 - 如果翻译表没下载成功,回退到内置的英文或中文,保证游戏不至于无文本可显示。
这个方案虽然绕开了UE5标准管道,但对控制力要求高的项目非常实用。缺点是你得自己维护Key的一致性和刷新机制。另外,如果某个UI控件在翻译表未到位时已经创建,下载完成后也要记得广播刷新事件。
如果你不想重构UI层,还有一个折中方案:把UE5的本地化存档本身(Content/Localization下的文件)打成Patch包,启动时用Chunk安装机制替换本地文件。这种方式不用改代码,但热更粒度粗,而且受平台打包策略限制,我一般只拿来应急。
5.3 后续扩展方向
本地化做到中英文切换这一步,已经能覆盖绝大多数项目的核心需求了。但如果你想往更深走,有几个方向值得提前规划。
第一个方向是字体管理。中文、日文、韩文和拉丁字母混排时,字体回退机制非常关键。UE5支持Font Fallback,你可以在项目设置里为不同字符范围指定备用字体。这个不做的话,切到日文后,某些生僻字会显示成方块。
第二个方向是区域化差异。同一个英语,英国和美国的日期格式、度量单位可能不一样;同一个中文,中国大陆和台湾地区用词也可能不同。如果你的游戏对这些细节有要求,就要在Culture细分上多花点功夫。
第三个方向是语音和字幕。语音文件的多语言版本管理、字幕与口型同步,这比文本本地化复杂一个量级。好在UE5也提供了对应的资产命名和加载机制,只是需要单独规划。
我在实际项目里最深的一个体会是:本地化不是"加一个语言包"这样单点的工作,它会渗透到UI架构、资源管理、数据流和发布流程的方方面面。越早把它当做一个系统来设计,后面越省心。
最后再分享一个小技巧:无论在蓝图还是C++里,我都习惯给语言切换入口加一个调试快捷键,比如在PC平台按F12就轮换一次语言,用来快速检查当前界面的刷新逻辑。这个小小的调试开关,在后续调多语言UI时能帮你节省大量来回切菜单的时间。
