Skip to content

如何做多语言 ​

灵泛引擎内置了完整的国际化(i18n)系统,采用原文替换模式——你用中文写剧本,引擎自动将原文替换为目标语言的译文。

核心设计:原文替换 ​

与传统 i18n 框架不同,灵泛引擎不需要你在剧本中写键名。你直接用原文写:

dsl
say "你好,世界" speaker="narrator"

翻译文件中,原文本身就是 key:

json
{
  "你好,世界": "Hello, World"
}

引擎在运行时自动检测:有译文就替换,没有就用原文。这意味着:

  • 开发阶段:只写母语原文,无需任何翻译文件,游戏正常运行
  • 翻译阶段:提取所有文本,制作翻译文件
  • 变量占位符:翻译 key 中保留 {变量} 占位符,翻译后仍会被替换
dsl
say "你好,{player.name}!" speaker="keeper"
json
{
  "你好,{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(全局通用翻译):

json
{
  "开始游戏": "Start Game",
  "继续游戏": "Continue",
  "设置": "Settings",
  "退出": "Exit",
  "返回标题": "Return to Title"
}

Lang/en-US/chapter1/chapter1.json(第一章翻译,子文件夹形式):

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# 代码中的对话自动翻译(走同一管道)

示例:全自动翻译 ​

dsl
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 场景:

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=center

C# 端注册一个命令:

csharp
// 在 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:

dsl
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# 场景:

dsl
button "语言设置" x=50% y=70% nav="cs_switch_language"
csharp
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() 时才加载:

  1. 检查 Lang/{lang}/ 目录是否存在
  2. 如果是目录:main.json 优先加载作为全局兑底,然后递归加载其余所有 .json 文件(子文件夹也加载),按路径排序保证加载顺序确定——后加载的覆盖先加载的
  3. 如果不是目录:降级加载单文件 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:

dsl
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=24

Stories/chapter1/chapter1.story:

dsl
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:

json
{
  "开始新游戏": "New Game",
  "继续游戏": "Continue",
  "语言设置": "Language",
  "退出游戏": "Exit"
}

Lang/en-US/title_main.json:

json
{
  "幻塔奇谭": "The Phantom Tower"
}

Lang/en-US/chapter1.json:

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/ 目录):

csharp
var i18n = ServiceProvider.GetRequiredService<II18nService>();
var languages = i18n.GetAvailableLanguages();
// 返回: ["zh-CN", "en-US", "ja-JP", ...]
// zh-CN 始终包含(默认语言)

可以用这个列表在设置界面动态生成语言选择菜单。

字体适配 ​

不同语言可能需要不同字体。通过变量结合条件判断:

dsl
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 默认字体,通常能覆盖大多数语言。

翻译工作流建议 ​

  1. 开发阶段:全程用中文写 DSL,不创建任何翻译文件
  2. 提取阶段:游戏完成后,从所有 .story 文件中提取文本(say/menu/input/text/button 的内容)
  3. 翻译阶段:为每个目标语言创建 Lang/{lang}/ 目录,按需拆分翻译文件(子文件夹分类、扁平化、单文件均可)
  4. 测试阶段:切换语言,检查翻译是否完整、UI 布局是否溢出
  5. 发布阶段:翻译文件随游戏一起打包,可选加密

注意事项

  • 翻译 key 必须与原文完全一致(包括标点、空格)
  • 带变量占位符的文本,key 中保留 {变量名} 原样
  • main.json 最先加载作为全局兑底,其余文件按路径排序加载覆盖
  • 同一原文在不同场景有不同含义时,放到不同文件中(路径排序靠后的覆盖靠前的)

基于 .NET 10 + Avalonia 12 · Apache-2.0 License