昨天讲怎么把包传上 Steam,今天讲怎么让这个包卖得出去。独立游戏最划算的一次「销量翻倍尝试」,往往不是再加一个玩法,而是把界面翻成英文、日文、俄文、西班牙文——Steam 的语言筛选器是免费的流量入口,而你可能只花了三天。

但本地化真正的成本不在翻译,在结构。第一天把文案硬编码进 Label.text = "开始游戏" 的项目,第 80 天要改,就得一个个场景去翻。

今日速览

  • Godot 里做本地化只有一条主路:CSV 翻译表 → 项目设置挂上 → tr() / 自动翻译。没有插件,引擎自带。
  • CSV 有硬性格式:必须 UTF-8 无 BOM(Excel 默认存 ANSI,会直接乱码)、第一列列名必须是 keys、列名是 locale(en/ja/zh/ru)、列名以 _ 开头的那列只当注释不导入。
  • TranslationServer.set_locale("ja") 是运行时切语言的唯一开关;系统语言用 OS.get_locale_language() 拿,拿不到就落到 Project Settings 里的 Fallback。
  • 切完语言界面没字?八成是字体——Godot 默认字体只覆盖拉丁字母的一小部分,中文、俄文、日文必须挂 Noto 系列做 fallback。
  • 带变量的句子不要拼字符串,用命名占位符 + format(),让译员能调换语序({character} picked up the {weapon})。

核心学习内容

一、CSV 翻译表:格式不对,导入就是白干

官方文档给的格式只有一张小表:

keys,en,zh,ja,ru
MENU_START,Start Game,开始游戏,ゲーム開始,Начать игру
MENU_QUIT,Quit,退出,終了,Выход

四条规矩,一条都不能破:

  1. UTF-8 无 BOM。用 Excel 另存 CSV 会写成 ANSI,中文全变问号。请用 LibreOffice 或 Google Sheets 导出。
  2. 左上角单元格内容被忽略,但第一列必须是 keys(列名),下面每行是唯一的键名,大小写敏感,KEY1 和 Key1 是两个键。
  3. 列名必须是合法 locale(en、en_GB、zh、ja、ru…),否则该列不生效;以 _ 开头的列(如 _comment)只做备注,不会导入。
  4. 含逗号、换行、引号的句子必须整格用双引号包起来,内部的双引号写成 ""。

导入后,CSV 旁边会生成压缩的翻译资源,并自动注册到 project.godot 的翻译列表里。想手动管控,就去 Project Settings → Localization → Translations 增删。

至于「键名还是英文原文当键」——文档说得很实际:只用英文发布、后补其他语言的,拿英文当键更省事;同时上多语言的项目,一律用 MENU_START 这类大写键,否则你以后想改英文文案,所有语言的表全得跟着改。

二、切语言:一行代码 + 一个设置项

func apply_language(code: String) -> void:
    if code == "automatic":
        TranslationServer.set_locale(OS.get_locale_language())
    else:
        TranslationServer.set_locale(code)

要点三条:

  • OS.get_locale_language() 返回的是语言(zh、en),不含地区;en_GB 这种地区差异只有在你真的区分拼写/单位时才需要。
  • 系统语言不支持时会落到 Project Settings → Internationalization → Locale → Fallback,为空则落 en。所以 en 表必须 100% 完整,它是兜底。
  • 一定要给玩家一个手动切换入口。玩家改语言的理由永远存在:翻译质量、多语言家庭、想学外语。

节点自动翻译只对 Button、Label 这类 Control 生效——text 恰好等于某个 key 时才翻。副作用:显示玩家昵称的 Label,如果昵称碰巧等于某个 key 会被翻掉。给这类节点把 Inspector 里的 Auto Translate → Mode 设为 Disabled,代码里显式 tr()。

三、字体与排版:中文玩家的第一眼就是「口口口」

默认字体只覆盖拉丁字符集的一部分。项目设置里挂好翻译后如果界面空白或全是方块,换字体,不是换代码。做法是给根 Control 一个 Theme,Default Font 用 Noto Sans SC / Noto Sans JP / Noto Sans(西里尔),并配置字体 fallback 链——文档明确说资源 Remap 不支持 DynamicFont,多语言字体要走 fallback 系统,好处是不依赖当前语言,多人聊天这种混合语言场景也正确。

排版留白也要提前算:同一句话,德语和俄语通常比英文长 20%~35%,日语偏短但字宽大。按钮宽度别按中文刚好放下就定死,留 1.3 倍余量,并打开自动换行(autowrap_mode)。这一条不改,俄语玩家看到的就是被裁掉的半句话。

四、占位符:AI 翻译最容易翻车的地方

# ❌ 拼字符串:译员无法调整语序
label.text = tr("%s picked up the %s") % ["Ogre", "Sword"]

# ✅ 命名占位符:语序可调,上下文清晰
label.text = tr("{character} picked up the {weapon}").format(
    {character = "Ogre", weapon = "Sword"})

Godot 4.6 起 CSV 还支持 ?plural 列(配合 tr_n(),注意此时 Control 自动翻译失效)和 ?context 列(同一个词在不同语境下译法不同,比如「Letter 字母 / Letter 信件」)。中文没有单复数,但英语、俄语有——数量文案从第一天就用 tr_n(),否则「1 apples」会被英文玩家截图嘲笑。

AI 翻译的死穴是它会「顺手美化」,把 {weapon} 翻成 {武器},或者把 %d 吃掉。所以流程必须是:先约束,再机翻,最后用脚本校验占位符一致性。

🎯 DeepSeek 提示词

提示词 1:把硬编码文案一次性抽成翻译表

适用场景:项目已经写了一堆 Label.text = "...",想补本地化。

你是一位 Godot 4 项目的本地化工程师。我会给你若干 GDScript 文件内容。

任务:
1. 找出所有面向玩家的硬编码字符串(UI 文本、提示、对话、道具名/描述),排除 debug 日志、节点名、内部枚举值。
2. 为每个字符串生成大写蛇形 key,命名规则:<模块>_<对象>_<用途>,如 MENU_START、ITEM_POTION_DESC、DIALOG_MERCHANT_01。
3. 输出两部分:
   A) 一张 CSV(首行 keys,en,zh),en 列填我原来的英文/中文文案,zh 列留空等我填;
   B) 每个文件需要改动的位置清单,格式为「文件:行号 → 原代码 → 替换后代码」,替换后必须使用 tr("KEY")。
4. 约束:
   - 含变量的句子必须改成命名占位符 {name} 形式,并使用 .format({});
   - 数量相关文案改用 tr_n("KEY_ONE", "KEY_MANY", n);
   - CSV 中若某格含逗号或引号,用双引号包裹并把内部引号写成两个双引号;
   - 不要修改任何逻辑、不要改动缩进风格、不要新增第三方依赖。
5. 最后列出你无法确定归属的字符串,问我而不是猜。

提示词 2:AI 翻译 + 占位符质检(一条龙)

适用场景:CSV 表结构已经有了,要补 ja / ru / es 等语言。

你是游戏本地化译者,负责把一张 Godot 4 用的 CSV 翻译表翻成 {目标语言}。

输入:CSV 文本,列结构为 keys,en,可能有 {placeholder} 形式的变量。

要求:
1. 只翻译 en 列内容,keys 列原样保留,输出列名为 {locale}。
2. 术语表(必须严格遵守,不得意译):
   {术语原文} = {指定译法}
   (未列出的专有名词保持原样,如角色名、地名)
3. 硬性约束:
   - 所有 {placeholder} 原样保留,一个都不能少、不能多、不能翻译;
   - 不增删句子,不合并条目,不改标点风格(目标语言用全角/半角按该语言习惯);
   - UI 文案优先用短词(按钮 ≤ 12 字符量级),避免换行;
   - 保持 CSV 合法:含逗号/引号的格子用双引号包裹。
4. 输出格式:
   第一部分:完整 CSV(可以直接覆盖我的文件);
   第二部分:「质检报告」表格,列 = key | 是否保留全部占位符 | 字数变化% | 需要人工确认的歧义点。
5. 如果某条文案缺少上下文无法准确翻译,优先给通用译法,并在质检报告里标注「需人工确认」,不要留空。

提示词 3:写一个语言设置面板(含存档)

适用场景:需要一个「自动 / 中文 / English / 日本語 / Русский」下拉,切完立即生效并记住。

你是一位资深 Godot 4(GDScript)开发者。请为我实现一个语言设置模块。

需求:
1. 一个 OptionButton,选项:Automatic、简体中文、English、日本語、Русский,
   对应值:automatic / zh / en / ja / ru。
2. 打开面板时,从 user://settings.json 读取上次选择;没有则用 automatic。
3. 选择变化时:
   - automatic → TranslationServer.set_locale(OS.get_locale_language())
   - 否则 → TranslationServer.set_locale(选中值)
   - 立即写回 user://settings.json(用 FileAccess + JSON.stringify,写入前确保目录存在)
4. 启动时(autoload 脚本)自动应用一次,保证主菜单就是正确语言。
5. 约束:
   - 单个脚本不超过 120 行,函数职责单一,带 docstring 注释;
   - 不要用全局变量,状态走一个 autoload 单例(给出 project.godot 里 autoload 的配置说明);
   - 处理文件不存在、JSON 解析失败两种情况,失败时静默回退到 automatic 并打印 warning;
   - 用 Godot 4 的 await / Callable 风格,不要用 Godot 3 的 yield。
6. 输出:完整脚本 + 场景节点结构说明 + 3 条自测步骤(含如何验证切语言后 UI 立即刷新)。

学习心得

第 80 天了。回头看,这个项目真正的分水岭不是「会不会写状态机」,而是有没有在一开始就把「玩家是谁」想清楚。本地化是同一件事的另一种说法:你今天写的每一个 Label.text = "...",都在决定三个月后要不要重做整个 UI 层。

对「用 DeepSeek 做游戏」这件事,今天的收获尤其具体:AI 最擅长的是机械劳动,最不擅长的是守恒。把 500 条文案抽成 key 并改写 tr(),人做要两天,DeepSeek 半小时;但把 {weapon} 原样保留这种「不许改」的事,它一定会犯几次错。所以正确分工是——AI 产出,脚本校验(用正则扫一遍 CSV,占位符集合必须与原表完全相等),人只做抽检。

另外提醒自己一句:别等文本写完再本地化。CSV 表从第一句文案就开始建,成本几乎为零;等到 3000 字对话写完再回头抽 key,那是另一个项目的工作量。

明日关注

  • 建 localization/strings.csv(keys,en,zh 三列),先把主菜单 + 设置页 30 条文案灌进去,跑通一次导入与切换。
  • 挂 Noto Sans SC / JP 字体 fallback 到根 Theme,验证中文、俄文、日文都不出现方块,并检查俄语按钮是否溢出。
  • 写一个 check_placeholders.py 小脚本:对比 CSV 各语言列的 {} 占位符集合,不一致就报错退出,接进部署前置检查。

本文由自动化学习流水线每日生成,坚持即学即记。