
1. 项目概述当Godot遇上AI本地化工作流的新思路最近在折腾一个用Godot引擎做的独立游戏想把它分享给更多国内的朋友汉化就成了绕不开的一步。传统的汉化流程无非是打开项目文件找到那些散落在脚本、场景、资源文件里的英文字符串然后一个个手动翻译、替换、测试。这个过程枯燥且容易出错尤其是当项目规模稍大或者字符串嵌套在复杂的GDScript逻辑里时工作量会指数级上升。我这次尝试的是一个结合了AI工具辅助的汉化思路我把它称为“Ai-Utilize”工作流。核心思想不是让AI全自动完成汉化那目前还不现实尤其是对上下文和游戏术语的把握而是利用AI作为高效的“翻译助理”和“文本处理引擎”将开发者从繁琐的查找、复制、粘贴和基础翻译中解放出来让我们能更专注于校对、语境适配和最终的效果打磨。Godot作为一个节点化、资源驱动的引擎其文本的存储方式有其特点这既是挑战也是我们设计自动化流程的切入点。这个工作流适合谁呢如果你是独立游戏开发者正头疼于个人项目的多语言支持或者是小型团队的技术美术、策划需要快速对原型或Demo进行本地化预览亦或是你接手了一个海外开源Godot项目想为社区贡献中文翻译。那么这套结合了脚本工具与AI的思路或许能给你带来一些效率上的惊喜。它不要求你精通自然语言处理但需要对Godot项目结构、基本的命令行操作或脚本编写有初步了解。2. 核心思路与方案选型为什么是“AI辅助”而非“AI全自动”在决定采用AI辅助汉化之前我评估过几种常见方案。最原始的手工替换法在超过一百个字符串后基本就不可行了维护更是噩梦。使用专业的本地化工具或插件如Godot官方推荐的GNU gettext或一些第三方插件固然是正规军但它们的学习曲线、与Godot资源系统的集成度以及对动态生成文本的支持有时会显得笨重。特别是对于快速迭代、文本散落各处的早期项目配置一套完整的i18n国际化框架的投入产出比可能不高。“Ai-Utilize”思路的核心在于解耦与增效。我们不追求一步到位的完美自动化系统而是将汉化过程拆解为几个可以独立优化、并可由AI工具强力辅助的环节文本提取将Godot项目中的所有待翻译文本.gd脚本中的字符串、.tscn场景文件中的节点属性、.tres资源中的文本等集中提取到一个结构化的文件中如JSON、CSV。AI辅助翻译将提取出的文本批量提交给大语言模型LLM利用其上下文理解能力进行初步翻译并可以附加翻译规则如“HP保留不译”、“Skill统一译为‘技能’”。翻译结果注入与校对将AI翻译的结果导回Godot项目替换原文本。开发者在此核心环节进行人工校对重点处理游戏术语、文化梗、以及AI可能误译的上下文相关语句。测试与迭代在引擎中运行测试确保UI显示正常、无文本溢出或功能错误。根据测试结果可以反向修改翻译文件并利用脚本快速重新注入。这个方案的优点很明显灵活性高。你可以从任何环节介入可以只用AI提取文本自己手动翻译也可以让AI完成大部分基础翻译你只做精校。工具链可以很轻量几个Python脚本配合Godot的命令行工具就能搭建起来。更重要的是它尊重开发者的最终控制权AI是助手不是决策者。我选择Python作为实现语言因为它有丰富的文本处理库如jsonre能方便地调用各类AI API如OpenAI、DeepSeek、智谱等也能通过subprocess模块调用Godot Editor的命令行工具进行一些自动化操作。当然你也可以用GDScript直接写编辑器插件来实现类似功能但Python在复杂文本处理和外部API调用上更通用。3. 实战准备环境、工具与项目分析3.1 工具链搭建工欲善其事必先利其器。我们不需要很复杂的环境。Godot Engine确保你安装了Godot并且知道其可执行文件的位置例如godot或godot.exe。我们将主要利用其命令行功能。Python 3.6这是我们的主力脚本语言。需要安装几个库pip install openai # 如果你使用OpenAI的API # 或者 pip install requests # 用于调用其他HTTP API代码编辑器VS Code、PyCharm或任何你顺手的编辑器用于编写和运行Python脚本。AI API密钥准备一个可用的AI服务API Key。考虑到可访问性和成本国内开发者可以选择智谱、DeepSeek等提供的服务。重要提示整个流程绝对不涉及、也不需要任何网络访问工具请使用合规、正规的AI服务平台。3.2 分析Godot项目的文本分布在动手写脚本前必须搞清楚文本藏在哪里。一个典型的Godot项目文本主要存在于以下位置GDScript/C#脚本文件 (.gd/.cs)这是最大头。字符串可能出现在print语句、变量赋值、对话框数组、UI标签设置$Label.text “Hello”等地方。难点在于字符串可能是拼接的或者包含占位符如Player %s wins! % player_name。场景文件 (.tscn) 和资源文件 (.tres): 这些是文本格式的资源文件。文本存在于各种节点的属性中例如Label节点的text属性。Button节点的text属性。RichTextLabel的bbcode_text属性。LineEdit的placeholder_text属性。自定义资源中定义的字符串属性。JSON/CSV等数据文件游戏配置、物品描述、任务文本等常放在外部数据文件中。我们的提取脚本需要能智能地识别这些位置。对于.tscn和.tres文件由于其是结构化的文本类似JSON我们可以用正则表达式或简单的解析来提取引号内的英文字符串。对于.gd脚本则需要更复杂的解析因为需要区分字符串字面量、注释和代码逻辑。注意一个常见的坑是直接全局搜索替换双引号内的内容会破坏代码逻辑比如替换了变量名或关键字。因此提取阶段必须精准定位字符串字面量。3.3 设计翻译映射文件结构提取出来的文本不能乱糟糟地堆在一起。我们需要一个结构化的文件来管理原文和译文的映射关系。JSON格式是个好选择因为它易读、易被程序处理也方便AI接口消费。一个简单的设计如下{ metadata: { project_name: MyGodotGame, godot_version: 4.2, extract_time: 2023-10-27 }, strings: [ { id: unique_hash_or_path_1, source: Hello, Warrior!, context: res://ui/main_menu.gd - Line 42, greeting label, translation: , needs_review: true }, { id: unique_hash_or_path_2, source: Press [SPACE] to jump., context: res://scenes/world/Level1.tscn - Control/Control/TutorialLabel, translation: , needs_review: true } ] }id: 唯一标识符可以用文件路径行号哈希生成用于后续精准回写。source: 提取到的原文。context: 上下文信息帮助翻译者无论是人还是AI理解该文本的用途。这对于翻译的准确性至关重要。translation: 预留的翻译结果字段初始为空。needs_review: 标记是否需要人工复审。4. 核心环节一智能文本提取脚本编写这是整个流程的基石。一个健壮的提取脚本能省去后续无数麻烦。下面我将分步骤拆解一个Python提取脚本的关键部分。4.1 提取场景与资源文件.tscn, .tres中的文本这些文件本质上是Godot序列化后的文本格式有规律可循。我们可以用正则表达式来匹配属性值中的字符串。import re import os import json from pathlib import Path def extract_from_tscn_tres(file_path): 从.tscn或.tres文件中提取文本属性。 返回一个列表每个元素是一个字典包含文本和其上下文信息。 extracted_strings [] try: with open(file_path, r, encodingutf-8) as f: content f.read() except UnicodeDecodeError: # 有些文件可能不是UTF-8尝试其他编码 with open(file_path, r, encodinglatin-1) as f: content f.read() # 匹配形如 [node nameLabel typeLabel] text Hello World [/node] 中的 text 属性 # 更通用的正则匹配 属性名 值 且值看起来像英文句子/单词 # 这个正则比较宽松可能会抓到一些非文本属性后续需要过滤。 pattern r(\w)\s*\s*([^\n]) matches re.finditer(pattern, content) for match in matches: key, value match.group(1), match.group(2) # 初步过滤只处理我们关心的属性且值包含英文字母 if key in [text, placeholder_text, hint_tooltip, bbcode_text, dialog_text] and re.search(r[a-zA-Z], value): # 进一步过滤掉很可能是路径、标识符的短字符串如playericon # 这里可以根据需要调整过滤逻辑 if len(value.split()) 1 or len(value) 4: # 简单启发式规则多于一个单词或长度大于4 string_info { id: f{file_path}:{match.start()}, # 用文件路径和匹配位置作为临时ID source: value, context: f{file_path} - property {key}, translation: , needs_review: True } # 去重如果同一文件同一位置已经提取过则跳过虽然概率低 if string_info not in extracted_strings: extracted_strings.append(string_info) return extracted_strings实操心得正则表达式虽然强大但面对Godot复杂的资源结构可能力有不逮。一个更稳健的方法是使用Godot自带的命令行工具将场景或资源导出为JSON格式然后解析JSON。例如可以编写一个简单的GDScript工具脚本利用ResourceLoader.load()和JSON.stringify()来导出资源内容再被Python脚本调用。这能100%准确解析所有属性但需要更多步骤。4.2 提取GDScript脚本.gd中的字符串脚本中的字符串提取更具挑战性因为需要区分字符串字面量和代码的其他部分。我们不能简单地匹配所有引号内的内容。一个相对安全的方法是使用Python的tokenize模块针对Python风格语法有一定效果或者编写一个简单的状态机解析器。这里提供一个基于正则和简单启发式规则的版本适用于大多数简单情况。def extract_from_gd_script(file_path): 从.gd脚本中提取字符串字面量。 这是一个简化版本可能无法处理所有复杂情况如多行字符串、转义字符。 extracted_strings [] try: with open(file_path, r, encodingutf-8) as f: lines f.readlines() except UnicodeDecodeError: return extracted_strings # 匹配双引号字符串忽略单引号Godot主要用双引号 # 这个正则不能正确处理字符串内的转义引号但对于初步提取够用。 string_pattern r(?:\\.|[^\\])* for line_num, line in enumerate(lines, 1): # 跳过注释行 if line.strip().startswith(#): continue matches re.finditer(string_pattern, line) for match in matches: full_match match.group(0) # 去掉首尾引号 content full_match[1:-1] # 过滤空字符串、纯数字、很短的可能是变量或键名的字符串 if not content or content.isdigit() or len(content) 2: continue # 过滤看起来像是文件路径、URL、正则表达式包含/ . :等 if re.search(r^[a-zA-Z]:\\|^/|^https?://|^res://|^user://, content): continue # 过滤包含常见代码片段如{ }在字符串插值中 if { in content or } in content: # 可能是格式化字符串需要特殊处理这里先简单跳过或记录 # 更佳做法是将其提取出来并在翻译时保留占位符。 pass # 只保留看起来像自然语言包含空格或常见标点结尾的字符串 if re.search(r[a-zA-Z][\s,.!?], content) and len(content.split()) 1: string_info { id: f{file_path}:{line_num}:{match.start()}, # 文件:行号:起始位置 source: content, context: f{file_path} - Line {line_num}, translation: , needs_review: True } extracted_strings.append(string_info) return extracted_strings4.3 整合提取与生成映射文件有了针对不同文件类型的提取函数我们就可以遍历整个Godot项目目录了。def extract_all_strings(project_root): 遍历项目根目录提取所有支持的文本。 all_strings [] project_root Path(project_root) # 定义要遍历的文件扩展名 text_file_extensions [.tscn, .tres, .gd] # 可以添加对.json, .csv等数据文件的处理 for ext in text_file_extensions: for file_path in project_root.rglob(f*{ext}): print(fProcessing: {file_path}) if ext in [.tscn, .tres]: strings extract_from_tscn_tres(str(file_path)) elif ext .gd: strings extract_from_gd_script(str(file_path)) else: strings [] all_strings.extend(strings) # 去重相同的source和context可能在不同地方出现合并它们 unique_strings_map {} for s in all_strings: key (s[source], s[context]) # 以原文和上下文作为唯一键 if key not in unique_strings_map: unique_strings_map[key] s else: # 如果已存在可以合并id或做其他处理 pass final_list list(unique_strings_map.values()) # 生成最终的翻译映射文件 translation_map { metadata: { project_name: project_root.name, extract_time: datetime.now().isoformat() }, strings: final_list } output_path project_root / translation_map.json with open(output_path, w, encodingutf-8, newline\n) as f: json.dump(translation_map, f, ensure_asciiFalse, indent2) print(f提取完成共找到 {len(final_list)} 个待翻译字符串。文件已保存至: {output_path}) return output_path运行这个脚本你将在项目根目录得到一个translation_map.json文件里面整齐地列出了所有需要翻译的文本及其出处。这是你交给AI助理的“任务清单”。5. 核心环节二AI辅助翻译与批量处理拿到提取好的JSON文件后我们就可以调用AI API进行批量翻译了。这里的关键是设计好给AI的提示词Prompt让翻译结果更符合游戏语境。5.1 设计翻译提示词Prompt直接让AI“翻译这段文本”效果往往一般。我们需要提供上下文和规则。我们可以将整个strings数组或者分批发送给AI。提示词可以这样设计你是一个专业的游戏本地化翻译专家。请将以下JSON数组中的source字段英文翻译成地道、简洁、符合游戏语境的中文并将结果填入translation字段。请遵循以下规则 1. 保留所有代码占位符如%s、{0}、[ITEM]等不要翻译它们确保它们在译文中的位置和格式与原文一致。 2. 游戏专有名词如角色名、技能名、物品名通常首字母大写或无空格组合除非有公认译名否则保留不译。 3. 保持语气一致。如果是UI按钮如“Save”, “Load”译为“保存”、“读取”如果是叙述性文字译文需流畅自然。 4. 如果context字段提供了文本出处如某个场景的某个标签请结合上下文理解。 5. 如果对某些翻译不确定可以在translation字段后添加注释如“待校”。 请直接返回完整的JSON数组不要添加任何解释。我们可以将上述提示词和我们的translation_map.json中的strings数组合并发送给AI。5.2 调用AI API进行翻译这里以使用OpenAI的ChatCompletion API为例其他国产API调用方式类似主要是调整请求格式和端点。import openai import json def translate_with_ai(api_key, strings_batch, modelgpt-3.5-turbo, max_tokens2000): 调用AI API翻译一批字符串。 strings_batch: 一个包含多个string字典的列表。 openai.api_key api_key # 注意实际使用中应从环境变量或安全配置读取 # 构建系统提示词 system_prompt 你是一个专业的游戏本地化翻译专家。请将以下JSON数组中的source字段英文翻译成地道、简洁、符合游戏语境的中文并将结果填入translation字段。请遵循以下规则 1. 保留所有代码占位符如%s、{0}、[ITEM]等不要翻译它们确保它们在译文中的位置和格式与原文一致。 2. 游戏专有名词如角色名、技能名、物品名通常首字母大写或无空格组合除非有公认译名否则保留不译。 3. 保持语气一致。如果是UI按钮如“Save”, “Load”译为“保存”、“读取”如果是叙述性文字译文需流畅自然。 4. 如果context字段提供了文本出处如某个场景的某个标签请结合上下文理解。 5. 如果对某些翻译不确定可以在translation字段后添加注释如“待校”。 请直接返回完整的JSON数组不要添加任何解释。 # 构建用户消息将数据作为JSON字符串嵌入 user_message json.dumps(strings_batch, ensure_asciiFalse) try: response openai.ChatCompletion.create( modelmodel, messages[ {role: system, content: system_prompt}, {role: user, content: user_message} ], temperature0.3, # 较低的温度使输出更稳定、更少创造性 max_tokensmax_tokens ) translated_text response.choices[0].message.content # AI返回的应该是JSON字符串我们解析它 translated_batch json.loads(translated_text) return translated_batch except json.JSONDecodeError as e: print(fAI返回的内容不是有效的JSON: {translated_text[:200]}...) raise e except Exception as e: print(f调用AI API时出错: {e}) return strings_batch # 返回原批次避免数据丢失5.3 分批处理与合并结果考虑到API的token限制和稳定性我们需要将大量的字符串分批发送。def batch_translate_all(translation_map_path, api_key, batch_size20): 读取翻译映射文件分批调用AI翻译并更新原文件。 with open(translation_map_path, r, encodingutf-8) as f: data json.load(f) all_strings data[strings] total len(all_strings) translated_strings [] for i in range(0, total, batch_size): batch all_strings[i:ibatch_size] print(f正在翻译第 {i//batch_size 1}/{(total-1)//batch_size 1} 批...) translated_batch translate_with_ai(api_key, batch) # 确保返回的顺序和数量一致理论上应该一致 if len(translated_batch) len(batch): translated_strings.extend(translated_batch) else: print(f警告第{i//batch_size 1}批返回数量不一致使用原始数据。) translated_strings.extend(batch) # 建议每批之间加一点延迟避免触发API速率限制 time.sleep(1) # 更新数据 data[strings] translated_strings data[metadata][translated_time] datetime.now().isoformat() data[metadata][translation_model] gpt-3.5-turbo # 记录使用的模型 # 保存到新文件避免覆盖原始提取文件 output_path translation_map_path.replace(.json, _translated.json) with open(output_path, w, encodingutf-8, newline\n) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f批量翻译完成结果已保存至: {output_path}) return output_path运行这个脚本后你会得到一个translation_map_translated.json文件里面的translation字段已经填上了AI初步翻译的中文。至此最耗时、最枯燥的“翻译”部分已经由AI助理高效完成了七八成。6. 核心环节三翻译结果回写与人工校对AI翻译的结果需要被注回原始的Godot项目中。同时人工校对是保证质量不可或缺的一步。6.1 回写脚本的设计思路回写是提取的逆过程。我们需要根据id字段它包含了文件路径和位置信息定位到原文然后用译文替换它。这里有一个关键点我们必须确保替换是精确且安全的不能破坏文件的其他部分如代码结构、缩进、其他属性。对于.tscn/.tres文件由于我们记录了属性名和匹配位置回写相对简单可以在原位置进行字符串替换。对于.gd文件由于行内可能有多个字符串且行号可能因之前的修改而变动虽然我们还没改使用行号和字符位置索引是最精确的。一个更稳健的方法是在提取阶段不仅记录位置还记录该字符串所在行的行内容或一个唯一性更强的上下文片段。回写时先定位到该行然后在行内进行精确替换。这里给出一个针对.tscn文件的简化回写示例def inject_to_tscn_tres(translated_map_path): 将翻译结果注回.tscn/.tres文件。 这是一个简化版本假设id是file_path:match_start格式。 with open(translated_map_path, r, encodingutf-8) as f: data json.load(f) # 按文件路径分组需要回写的翻译项 file_updates {} for item in data[strings]: if item.get(translation) and item[translation].strip(): # 有翻译内容 file_path, pos_str item[id].split(:, 1) match_start int(pos_str) file_updates.setdefault(file_path, []).append({ start: match_start, end: match_start len(item[source]), # 注意这里假设原文长度不变实际替换需调整 old_text: item[source], new_text: item[translation] }) # 对每个文件从后往前替换避免位置偏移 for file_path, updates in file_updates.items(): updates.sort(keylambda x: x[start], reverseTrue) # 从后往前 try: with open(file_path, r, encodingutf-8) as f: content f.read() new_content content for update in updates: # 检查原位置是否还是原文 if new_content[update[start]:update[end]] update[old_text]: new_content new_content[:update[start]] update[new_text] new_content[update[end]:] else: print(f警告文件{file_path}中位置{update[start]}的文本已变化跳过替换。) if new_content ! content: with open(file_path, w, encodingutf-8, newline\n) as f: f.write(new_content) print(f已更新: {file_path}) except Exception as e: print(f处理文件{file_path}时出错: {e})重要警告直接进行字符串位置替换风险较高尤其是当文件被其他工具修改过之后。强烈建议在回写前备份整个项目更安全的方法是使用Godot的ResourceSaver或通过编写GDScript编辑器插件来操作资源但这涉及到更深的引擎集成。对于初期尝试手动核对关键文件或小范围测试是必须的。6.2 人工校对的关键点AI翻译后必须进行人工校对。校对不应逐字逐句进行而应有重点术语一致性检查“Attack”、“Damage”、“HP”、“MP”等游戏术语在整个项目中是否翻译一致。建议建立一份项目专用的“术语表”在后续的AI翻译中可以将其作为提示词的一部分。上下文契合度结合context字段检查翻译是否贴合具体场景。例如“Menu”在标题界面可能是“菜单”在设置里可能是“选项”。文化适配检查俚语、笑话、文化梗的翻译是否恰当是否需要本地化改编。长度与UI适配中文通常比英文简短但有时也会更长。检查翻译后的文本在UI控件如按钮、标签中是否显示完整有无溢出或布局错乱。占位符与格式确认所有%s、{变量}等占位符都被正确保留且位置无误。校对过程可以在生成的translation_map_translated.json文件中直接修改translation字段然后再次运行回写脚本。也可以使用支持JSON编辑的文本编辑器或简单的GUI工具来辅助。7. 常见问题、优化与进阶技巧在实际操作中你肯定会遇到各种各样的问题。下面是我踩过的一些坑和总结的优化技巧。7.1 问题排查速查表问题现象可能原因解决方案提取脚本漏掉了大量文本正则表达式过于严格或未覆盖某些属性/字符串格式。1. 检查并放宽正则过滤条件。2. 打印调试信息查看哪些行被跳过了。3. 考虑使用Godot引擎解析资源的方法。AI翻译结果包含代码或占位符被翻译Prompt指令不够清晰或AI未严格遵守。1. 强化Prompt中关于保留占位符和专有名词的指令。2. 在提交翻译前对原文进行预处理将占位符用特殊标记包裹如__PLACEHOLDER_0__翻译后再替换回来。回写后文件损坏或Godot无法打开回写脚本替换错误破坏了文件结构如JSON/资源格式。立即使用备份恢复优化回写逻辑a) 回写前创建文件备份。b) 使用更精确的定位方式如行内容匹配。c) 先在小范围或副本上测试。翻译后UI文本显示为乱码文件编码问题。Godot默认期望UTF-8无BOM。确保所有脚本和资源文件以UTF-8无BOM编码保存。回写脚本在写文件时也应指定此编码encodingutf-8。动态生成的文本如拼接字符串未被提取提取脚本只处理静态字符串字面量。这类文本需要代码层面的国际化支持。可以修改代码将需要拼接的部分提取为可翻译的单元或使用Godot的tr()函数。这是AI辅助汉化流程的局限。7.2 流程优化建议增量提取与翻译项目开发是持续的不断有新文本加入。可以修改提取脚本使其只提取比上次提取时间戳更新的文件或者只提取未翻译的字符串translation字段为空然后合并到已有的翻译映射文件中。集成术语表创建一个glossary.json文件存储“原文-首选译文”的映射。在调用AI翻译前先根据术语表对提取的文本进行预处理标记或直接替换并在Prompt中明确告知AI优先使用术语表中的翻译。利用Godot的国际化i18n框架虽然本流程是“旁路”方案但最终可以将校对好的翻译映射文件转换为Godot官方支持的.pogettext或.csv翻译表格式。这样就能利用Godot内置的tr()函数和语言切换功能实现更规范的多语言支持。可以编写一个转换脚本将translation_map_translated.json转为Godot能识别的格式。构建简单GUI工具如果你经常需要汉化项目可以将提取、AI翻译需配置API、回写、校对等功能打包成一个简单的桌面应用如用PyQt、Tkinter提升易用性。7.3 关于AI模型选择的经验GPT-3.5-Turbo性价比高速度较快对于一般的游戏文本翻译足够用。在Prompt清晰的情况下能很好地遵守指令。GPT-4/GPT-4o理解能力和指令遵循能力更强对复杂语境、文化梗的处理更出色但成本也高得多。适合对翻译质量要求极高或文本涉及大量文学性、创意性内容的项目。国产大模型如DeepSeek, 智谱GLM访问速度快无需特殊网络环境成本通常更低。在中文表达上有时更地道。务必仔细测试其对于保留占位符、术语不译等指令的遵循程度。我个人在实际操作中的体会是不要追求一蹴而就的全自动化。将AI作为“第一译者”自己担任“主编”和“校对”是这个工作流效率最大化的关键。第一次搭建这个流程可能需要花费几个小时但一旦跑通后续同类项目的汉化效率将提升数倍。尤其是对于文本量大的项目前期在工具链上的投入会带来巨大的时间回报。最后无论工具多强大对游戏本身的理解和热爱才是产出高质量本地化的根本。