如何做多语言
灵泛引擎内置了完整的国际化(i18n)系统,采用原文替换模式——你用中文写剧本,引擎自动将原文替换为目标语言的译文。
核心设计:原文替换
与传统 i18n 框架不同,灵泛引擎不需要你在剧本中写键名。你直接用原文写:
say "你好,世界" speaker="narrator"翻译文件中,原文本身就是 key:
{
"你好,世界": "Hello, World"
}引擎在运行时自动检测:有译文就替换,没有就用原文。这意味着:
- 开发阶段:只写母语原文,无需任何翻译文件,游戏正常运行
- 翻译阶段:提取所有文本,制作翻译文件
- 变量占位符:翻译 key 中保留
{变量}占位符,翻译后仍会被替换
say "你好,{player.name}!" speaker="keeper"{
"你好,{player.name}!": "Hello, {player.name}!"
}引擎执行顺序:原文 → 翻译 → 变量替换。翻译时 key 是 你好,{player.name}!,翻译后得到 Hello, {player.name}!,再替换 {player.name} 得到最终文本。
翻译文件结构
核心原则:语言根目录必须,内部自由组织
引擎只关心语言根目录(如 en-US、ja-JP)来区分语言。语言根目录内部的组织方式完全自由——扁平化、子文件夹分类、单个大文件,随便你选。
方式一:子文件夹分类
按 Stories 目录结构镜像组织,适合大型项目:
项目根/
├── Stories/
│ ├── title/
│ │ └── title_main.story
│ ├── chapter1/
│ │ └── chapter1.story
│ └── chapter2/
│ └── chapter2.story
├── Lang/
│ ├── en-US/ # 英文语言根
│ │ ├── main.json # 全局通用翻译
│ │ ├── title/
│ │ │ └── title_main.json # 标题场景翻译
│ │ ├── chapter1/
│ │ │ └── chapter1.json # 第一章翻译
│ │ └── chapter2/
│ │ └── chapter2.json # 第二章翻译
│ ├── ja-JP/ # 日文语言根
│ │ ├── main.json
│ │ └── ...
│ └── ko-KR/ # 韩文语言根
│ └── ...方式二:扁平化
所有翻译文件直接放在语言根目录下,适合中小型项目:
Lang/
├── en-US/
│ ├── main.json # 全局通用
│ ├── title_main.json # 标题场景
│ ├── chapter1.json # 第一章
│ └── chapter2.json # 第二章
├── ja-JP/
│ └── ...方式三:单个大文件
所有翻译合并到一个文件,适合翻译量不大的项目:
Lang/
├── en-US.json # 所有英文翻译
├── ja-JP.json
└── ...三种方式可以混用
语言根目录是目录形式(Lang/en-US/)时,引擎递归加载其下所有 .json 文件;是单文件形式(Lang/en-US.json)时,只加载该文件。两种形式按需选择,但目录形式更灵活。
文件格式
每个翻译文件是一个 JSON 对象,原文 → 译文的键值对:
Lang/en-US/main.json(全局通用翻译):
{
"开始游戏": "Start Game",
"继续游戏": "Continue",
"设置": "Settings",
"退出": "Exit",
"返回标题": "Return to Title"
}Lang/en-US/chapter1/chapter1.json(第一章翻译,子文件夹形式):
{
"你好,旅人!": "Hello, traveler!",
"欢迎来到这座古塔。": "Welcome to this ancient tower.",
"你叫什么名字?": "What's your name?"
}自动翻译覆盖范围
引擎在以下位置自动调用翻译,你无需手动处理:
| 位置 | 说明 |
|---|---|
say 命令文本 | 对话框内容自动翻译 |
extend 命令文本 | 追加对话自动翻译 |
menu 提示和选项 | 菜单提示文字、各选项文本自动翻译 |
input 提示 | 输入框提示文字自动翻译 |
text UI 元素 | 场景中的静态文本自动翻译 |
button UI 元素 | 按钮文本自动翻译 |
checkbox UI 元素 | 复选框文本自动翻译 |
C# SayAsync() | C# 代码中的对话自动翻译(走同一管道) |
示例:全自动翻译
scene "title_main" type=menu
text "灵泛引擎" x=50% y=30% fontSize=48 halign=center
button "开始游戏" x=50% y=50% nav="chapter1" halign=center
button "继续游戏" x=50% y=60% nav="load_menu" halign=center
button "设置" x=50% y=70% cmd="open_settings" halign=center
button "退出" x=50% y=80% cmd="do_exit" halign=center切换到英文后,引擎自动将 开始游戏 → Start Game,继续游戏 → Continue,等等。无需修改任何 DSL 代码。
切换语言
方式一:cmd + value(推荐,最简洁)
利用按钮的 cmd + value 参数机制,一个命令处理所有语言:
DSL 场景:
scene "language_menu" type=ui
text "选择语言 / Select Language" x=50% y=30% halign=center fontSize=28
button "简体中文" x=50% y=45% cmd="switch_lang" value="zh-CN" halign=center
button "English" x=50% y=55% cmd="switch_lang" value="en-US" halign=center
button "日本語" x=50% y=65% cmd="switch_lang" value="ja-JP" halign=center
button "返回" x=50% y=80% nav="title_main" halign=centerC# 端注册一个命令:
// 在 App 启动时注册
var cmdService = serviceProvider.GetRequiredService<ICommandService>();
var state = serviceProvider.GetRequiredService<IStateContainer>();
cmdService.RegisterCommand("switch_lang", async (value, ct) =>
{
var lang = value?.ToString() ?? "zh-CN";
state.Set(StateKeys.Scene.CurrentLanguage, lang);
// 引擎自动检测到语言变化,切换翻译缓存
// 导航回标题
await serviceProvider.GetRequiredService<ICommandPipeline>().SendAsync(
new NavigateCommand { Path = "title_main" }, ct);
});cmd + value 参数机制
cmd="命令名"指定要执行的命令value="参数值"传递参数给命令处理器value支持{占位符}表达式——点击时求值,拿到最新变量值- 一个命令名可以注册一个处理器,通过
value区分不同参数 - 详见 按钮交互参考
方式二:menu + label(纯 DSL)
如果不想写 C# 代码,可以用 menu 跳转到不同 label:
label language_menu:
menu "选择语言 / Select Language"
"简体中文" -> set_zh_cn
"English" -> set_en_us
"日本語" -> set_ja_jp
label set_zh_cn:
set "__scene_current_language" "zh-CN"
navigate "title_main"
label set_en_us:
set "__scene_current_language" "en-US"
navigate "title_main"
label set_ja_jp:
set "__scene_current_language" "ja-JP"
navigate "title_main"方式三:C# 场景
通过 call_screen 调用 C# 场景:
button "语言设置" x=50% y=70% nav="cs_switch_language"public class LanguageScene : StoryScript
{
public override string SceneName => "cs_switch_language";
public override async Task RunAsync()
{
// 显示语言选择逻辑...
var lang = _state.Get<string>("_local_lang") ?? "zh-CN";
_state.Set(StateKeys.Scene.CurrentLanguage, lang);
await NavigateAsync("title_main");
}
}语言切换原理
__scene_current_language 是引擎内置状态键(StateKeys.Scene.CurrentLanguage)。SceneView 每帧检测它的变化。一旦变化,自动调用 I18nService.SwitchLanguage() 清除翻译缓存并重新加载。
语言加载机制
按需加载
引擎启动时不加载任何翻译文件。只有第一次调用 Translate() 时才加载:
- 检查
Lang/{lang}/目录是否存在 - 如果是目录:
main.json优先加载作为全局兑底,然后递归加载其余所有.json文件(子文件夹也加载),按路径排序保证加载顺序确定——后加载的覆盖先加载的 - 如果不是目录:降级加载单文件
Lang/{lang}.json
默认语言
zh-CN 和 zh-Hans 被视为默认语言,不进行翻译——直接返回原文。这意味着:
- 中文游戏无需任何翻译文件
- 翻译文件只针对外语版本
覆盖规则
main.json(如果存在)最先加载,作为全局兑底。其余文件按路径字母排序依次加载,后加载的覆盖先加载的同名 key。这意味着:
- 如果同名原文出现在多个文件中,路径排序靠后的文件“赢”
main.json总是被覆盖(它最先加载)- 想让某些翻译优先,放到路径排序靠后的文件中(如
zzz.json)
加密支持
翻译文件支持 LFEN 加密格式。如果项目启用了资源加密(参见加密与发布指南),.json 翻译文件会被自动加密/解密,无需额外配置。
完整示例
项目结构
MyGame/
├── Stories/
│ ├── title/
│ │ └── title_main.story
│ └── chapter1/
│ └── chapter1.story
├── Lang/
│ └── en-US/ # 英文语言根
│ ├── main.json # 全局通用翻译
│ ├── title/
│ │ └── title_main.json # 标题场景翻译
│ └── chapter1/
│ └── chapter1.json # 第一章翻译
└── ...中文原文(开发阶段)
Stories/title/title_main.story:
scene "title_main" type=menu
text "幻塔奇谭" x=50% y=30% fontSize=56 halign=center color="#FFD700"
button "开始新游戏" x=50% y=50% nav="chapter1" halign=center fontSize=24
button "继续游戏" x=50% y=60% nav="load_menu" halign=center fontSize=24
button "语言设置" x=50% y=70% nav="language_menu" halign=center fontSize=24
button "退出游戏" x=50% y=80% cmd="do_exit" halign=center fontSize=24Stories/chapter1/chapter1.story:
scene "chapter1" type=game
background "images/tower.jpg"
say "你好,{player.name}!" speaker="keeper"
say "欢迎来到幻塔。这里已经有千年历史了。" speaker="keeper"
menu "你要怎么做?"
"进入塔内" -> enter_tower
"询问历史" -> ask_history
"转身离开" -> leave英文翻译
Lang/en-US/main.json:
{
"开始新游戏": "New Game",
"继续游戏": "Continue",
"语言设置": "Language",
"退出游戏": "Exit"
}Lang/en-US/title_main.json:
{
"幻塔奇谭": "The Phantom Tower"
}Lang/en-US/chapter1.json:
{
"你好,{player.name}!": "Hello, {player.name}!",
"欢迎来到幻塔。这里已经有千年历史了。": "Welcome to the Phantom Tower. It has a thousand-year history.",
"你要怎么做?": "What will you do?",
"进入塔内": "Enter the tower",
"询问历史": "Ask about its history",
"转身离开": "Turn and leave"
}切换到英文后,玩家看到的完全是英文界面,原 DSL 文件不做任何修改。
获取可用语言列表
在 C# 中获取所有已安装的语言(扫描 Lang/ 目录):
var i18n = ServiceProvider.GetRequiredService<II18nService>();
var languages = i18n.GetAvailableLanguages();
// 返回: ["zh-CN", "en-US", "ja-JP", ...]
// zh-CN 始终包含(默认语言)可以用这个列表在设置界面动态生成语言选择菜单。
字体适配
不同语言可能需要不同字体。通过变量结合条件判断:
if {__scene_current_language == "ja-JP"}
set "ui.font" "Yu Gothic"
else if {__scene_current_language == "en-US"}
set "ui.font" "Segoe UI"
else
set "ui.font" "Microsoft YaHei"
text "标题" font="{ui.font}" x=50% y=30%TIP
如果不指定 font 参数,UI 元素使用 Avalonia 默认字体,通常能覆盖大多数语言。
翻译工作流建议
- 开发阶段:全程用中文写 DSL,不创建任何翻译文件
- 提取阶段:游戏完成后,从所有
.story文件中提取文本(say/menu/input/text/button 的内容) - 翻译阶段:为每个目标语言创建
Lang/{lang}/目录,按需拆分翻译文件(子文件夹分类、扁平化、单文件均可) - 测试阶段:切换语言,检查翻译是否完整、UI 布局是否溢出
- 发布阶段:翻译文件随游戏一起打包,可选加密
注意事项
- 翻译 key 必须与原文完全一致(包括标点、空格)
- 带变量占位符的文本,key 中保留
{变量名}原样 main.json最先加载作为全局兑底,其余文件按路径排序加载覆盖- 同一原文在不同场景有不同含义时,放到不同文件中(路径排序靠后的覆盖靠前的)