最近在整理日服活动剧情相关的本地化资料时,发现很多同学第一次接触这类文本,卡得最多的并不是剧情翻译本身,而是“文件打不开”“打开后全是乱码”“中文字体显示成方块”。尤其在面对《Dive into Ocean》这种以活动剧情、角色对话和演出文本为主的材料时,文本分散、格式不统一、编码混乱,很容易让人无从下手。
这篇文章就把“活动剧情本地化”这件事拆成一条完整链路来讲:从文本格式识别、编码转换、术语表建立,到批量提取与回填、字幕制作、字体适配和常见问题排查。即使你完全没接触过游戏汉化,也能跟着文中的示例做出一套属于自己的文本处理小工具。
本文适合以下几类读者:
- 想学习游戏本地化流程,但不知道从哪入手的开发者;
- 需要批量处理日文活动文本、对话文本,正在找方案的内容创作者;
- 想给剧情视频做中文字幕,但对编码和字幕格式不熟悉的同学;
- 需要了解中文字体适配、Unicode 编码常见坑点的前端或 Unity 开发者。
读完本文后,你会掌握一套可复用的文本提取、术语管理、翻译回填和显示适配流程,遇到类似任务时可以直接套用。
1. 背景与核心概念
1.1 汉化与本地化的区别
“汉化”在玩家语境里通常指把游戏界面、剧情文本从外语翻译成中文,让中文玩家能流畅阅读。而行业里更专业的说法叫“本地化”(Localization,常缩写为 L10n)。本地化不仅包含翻译文字,还包含:
- 日期和数字格式的调整;
- 字体和排版适配;
- 文化差异处理;
- 配音和字幕同步;
- 界面空间适配。
所以在处理《Dive into Ocean》这类活动剧情时,不要只想着“把日文换成中文”。你要同时考虑文本从哪里来、是什么编码、如何进入渲染流程、中文字体是否支持、文本长度会不会溢出。这是一套工程问题,不是单纯的翻译问题。
1.2 活动剧情文本有哪些特点
游戏里的活动剧情文本,和传统软件界面文本不太一样。它通常有以下几个特点:
文本量不大,但非常分散。
一段活动剧情可能由对话、旁白、系统提示、道具描述、公告文本、任务目标等多个部分组成,每个部分可能存放在不同的文件甚至不同的数据表里。对话文本带有说话人信息。
除了台词本身,还需要记录说话人名称、表情、动作标签、语音文件名等辅助信息。存在大量占位符与转义符。
例如{0}、{name}、\n、%s,这些符号在翻译时需要保留,否则程序运行时会出现格式错乱。文本顺序需要保持稳定。
剧情文本通常按剧情节点顺序读取,导出和回填时如果打乱顺序,会导致剧情错位。
对本地化工程来说,活动剧情不是一个“大文档”,而是一批有格式约束、有依赖关系的结构化数据。理解了这一点,后续的脚本设计思路就清晰了:先提取,再翻译,最后按原结构回填。
1.3 为什么编码问题必须放在第一位
很多初学者在打开日文文本文件时看到一堆乱码,第一反应是“文件坏了”。其实绝大多数情况是编码不一致导致的。
这里先区分两个概念:
- 字符集(Character Set):定义字符和码位的关系,例如 Unicode 是一个字符集。
- 编码(Encoding):定义字符在计算机里如何用字节表示,例如 UTF-8、Shift-JIS 都是具体的编码方式。
日文文本常见的编码有 Shift-JIS、EUC-JP、UTF-8;中文文本则常见 GBK、GB2312、UTF-8。当你用 UTF-8 去读取一个 Shift-JIS 文件时,字节序列无法被正确解释,就会出现乱码。
因此,本地化流程的第一步永远是:确认源文件编码,并统一转换成团队协作使用的编码。推荐统一为 UTF-8 或 UTF-8 with BOM,因为主流编辑器和脚本语言对 UTF-8 的支持最稳定。
2. 环境准备与版本说明
2.1 运行环境
本文示例以 Python 为主。演示环境如下:
- 操作系统:Windows 10/11 或 macOS 均可;
- Python 版本:以 Python 3.10 为例,Python 3.8 以上基本都能运行;
- 编辑器:VS Code、PyCharm,或者任意你顺手的编辑器;
- 版本控制工具:Git(建议安装,用于文本版本管理)。
需要说明的是,Python 版本可以根据你的项目实际情况调整,本文示例重点是演示处理思路,并不是某一版本的专属写法。
2.2 依赖库
为了尽量降低门槛,本文核心示例只使用 Python 标准库,不需要安装第三方包。涉及 Excel 处理时,可以按需安装openpyxl;涉及复杂编码检测时,可以安装charset_normalizer。但在基础流程里,标准库已经足够。
2.3 字幕与字体工具
如果活动剧情需要做成视频字幕,推荐使用:
- Aegisub:开源字幕编辑工具,支持 ASS/SSA 字幕格式,非常适合做剧情视频字幕;
- Noto Sans CJK SC:思源黑体中文版,是一款开源中文字体,覆盖中日韩常用汉字,可以作为字幕和中文字体适配的默认选择。
如果你的实际项目运行在 Unity 中,还需要了解 Unity Localization 插件和 TextMeshPro 字体图集的处理方式,这部分会在后文展开。
2.4 示例项目结构
为了让后面每一步都有对应的落点,我们先规划一个示例目录结构:
dive_into_ocean_loc/ ├── source/ │ └── dialogue.csv ├── term/ │ └── glossary.json ├── scripts/ │ ├── convert_encoding.py │ ├── merge_translation.py │ └── check_length.py ├── output/ │ └── zh-CN.json └── subtitle/ └── episode01.asssource/:存放原始文本文件;term/:存放术语表和角色名映射;scripts/:存放批量处理脚本;output/:存放生成的目标语言文件;subtitle/:存放剧情字幕文件。
实际项目中文件结构可能更复杂,但核心思路是一致的:源文件、脚本、输出产物要分离。
3. 活动剧情文本的提取与编码处理
3.1 先识别文本格式
在做任何处理之前,先确认文本文件的存储格式。很多游戏引擎导出的文本是结构化的,常见的包括:
| 文件格式 | 典型特征 | 常见使用场景 |
|---|---|---|
| CSV | 逗号分隔,第一行通常是表头 | 对话表、道具表、任务表 |
| JSON | 花括号包裹,键值对结构 | 本地化语言包、配置表 |
| YAML | 缩进表示层级 | 配置文件、剧情脚本 |
| PO | gettext 专用格式 | 应用本地化、Web 项目 |
| ASS | 字幕脚本 | 视频字幕 |
拿到文件后,用文本编辑器打开看前几行,基本就能判断格式。这里有一个经验:不要直接用 Excel 双击打开未知 CSV 文件,否则编码和分隔符都可能被自动转换,造成二次污染。建议先用 VS Code 或 Notepad++ 以只读方式查看原始字节。
3.2 编码转换的核心写法
假设有一个日文文本文件是 Shift-JIS 编码,需要转换成 UTF-8。示例代码如下:
# 文件路径:scripts/convert_encoding.py from pathlib import Path src_path = Path("source/dialogue_sjis.txt") dst_path = Path("source/dialogue_utf8.txt") # 使用 shift_jis 读取,errors="replace" 表示遇到无法解析的字节时替换为占位符 data = src_path.read_text(encoding="shift_jis", errors="replace") # 写回 UTF-8 with BOM,BOM 对 Excel 更友好 dst_path.write_text(data, encoding="utf-8-sig") print("转换完成:", dst_path)其中errors="replace"有一个明显缺点:如果源文件不是 Shift-JIS,损坏的字节会被替换成�,而不是报错提示。所以这个参数只适合快速查看,不适合正式转换。正式处理时建议先检测编码。
手动检测可以看文件头是否有 BOM:
raw = open("source/unknown.bin", "rb").read() if raw.startswith(b"\xef\xbb\xbf"): print("UTF-8 with BOM") elif raw.startswith(b"\xff\xfe") or raw.startswith(b"\xfe\xff"): print("UTF-16") else: print("可能是其他编码,例如 Shift-JIS 或 GBK")如果需要更可靠的自动识别,可使用charset_normalizer库,但要注意:自动检测只能作为参考,最终编码还是要结合源文件来源来判断。
3.3 用 Python 读取剧情对话 CSV
CSV 是最常见的活动剧情文本格式。下面的示例演示如何安全地读取一个 UTF-8 编码的 CSV 文件:
import csv from pathlib import Path dialogue_path = Path("source/dialogue.csv") rows = [] with dialogue_path.open("r", encoding="utf-8-sig", newline="") as f: reader = csv.DictReader(f) for row in reader: rows.append(row) print(f"共读取 {len(rows)} 条对话") for row in rows[:3]: print(row)这里有两个关键点:
encoding="utf-8-sig"会自动处理 BOM 头,避免第一列表头出现\ufeff前缀。newline=""是 Python 官方文档推荐的 CSV 文件打开方式,可以避免在 Windows 平台上产生多余空行。
如果 CSV 使用的是逗号分隔但文本本身包含逗号,CSV 格式会通过引号包裹来处理。因此一定要用csv模块读取,不要用split(",")手工切分。
3.4 占位符与换行符如何保护
日文剧情文本里经常出现{0}、{name}、\n、%s等标记。翻译者如果不熟悉技术约束,很容易在改写句子时删掉这些标记,最终导致游戏运行时格式化异常。
因此,在导出翻译文本时,最好先做一次占位符扫描,把当前文本里的占位符全部识别出来:
import re def extract_placeholders(text: str) -> list: # 匹配 {0}、{name}、%s、%d 以及换行符标记 pattern = r"\{[^}]+\}|%[sd]|\\n" return re.findall(pattern, text) sample = "こんにちは、{name}!準備は% sです。\n" print(extract_placeholders(sample))需要提醒的是,上面代码里的% s只是为了在文档中显示而加了空格,真实文本中应该是%s。在实际项目中,建议把占位符匹配结果写入检查报告,作为翻译后质量检查的一项输入。
4. 从日文到中文的本地化流程实战
4.1 准备示例数据
下面我们手工构造一份简单的剧情对话 CSV,仅用于演示流程,不代表任何实际游戏文案:
id,speaker,content,translation 1001,先生,海へ行こう。,去海边吧。 1002,アヤネ,準備はいい?,准备好了吗? 1003,シズ,忘れ物はないかな。,有没有忘记带东西呢?这份数据包含四列:
id:对话唯一标识;speaker:说话人;content:原始日文文本;translation:翻译后的中文文本。
在实际工作中,translation列通常由翻译人员填写。可以实现一个简单的翻译协作流程:先把原始文本导出成 CSV,分发给翻译人员,再由脚本回收并生成最终语言包。
4.2 建立术语表
活动剧情里角色名、组织名、专有名词出现的频率很高。如果每个人翻译习惯不同,很容易出现同一个角色有多种译名。因此建议在项目早期就建立术语表。
示例术语表文件term/glossary.json:
{ "先生": "老师", "アヤネ": "绫音", "シズ": "静子", "デカゴン": "三角尺" }需要注意,上面的示例仅用于演示术语表结构,具体角色译名以实际授权翻译版本为准。术语表的作用是让所有翻译参与者在同一个命名空间下工作,减少沟通成本。
4.3 编写翻译合并脚本
当我们拿到翻译后的 CSV 时,需要把它转成游戏方便读取的 JSON 语言包。下面这段代码演示了合并过程:
# 文件路径:scripts/merge_translation.py import csv import json from pathlib import Path def merge_to_json(input_csv: str, output_json: str) -> None: result = {} with open(input_csv, "r", encoding="utf-8-sig", newline="") as f: reader = csv.DictReader(f) for row in reader: dialogue_id = row.get("id", "").strip() content = row.get("translation") or row.get("content", "") result[dialogue_id] = { "speaker": row.get("speaker", "").strip(), "content": content.strip() } with open(output_json, "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) print(f"已生成语言包:{output_json},共 {len(result)} 条") if __name__ == "__main__": merge_to_json("source/dialogue.csv", "output/zh-CN.json")这段代码有几个细节值得说明:
row.get("translation") or row.get("content", "")表示优先使用翻译列,如果翻译列为空,则退回使用原始文本。ensure_ascii=False保证 JSON 文件里的中文以明文显示,而不是被转成\uXXXX。indent=2让生成的文件便于人工 review,生产环境如果对体积敏感可以去掉缩进。
4.4 运行与验证
在项目根目录执行:
python scripts/merge_translation.py预期输出:
已生成语言包:output/zh-CN.json,共 3 条打开output/zh-CN.json,内容应该类似:
{ "1001": { "speaker": "先生", "content": "去海边吧。" }, "1002": { "speaker": "アヤネ", "content": "准备好了吗?" }, "1003": { "speaker": "シズ", "content": "有没有忘记带东西呢?" } }这里你会发现,术语表中定义的角色名还没有被自动应用。这是因为角色名替换逻辑需要足够谨慎:直接全文替换可能误伤剧情文本内部的普通词汇。更安全的做法是让翻译人员直接在翻译列中统一角色名,术语表只作为人工参考。
4.5 自动化质量检查思路
生成语言包后,还需要做几项基础检查:
- 检查是否存在完全没翻译的条目;
- 检查是否存在多余的空格或全角/半角混用;
- 检查占位符是否被破坏;
- 检查角色名是否统一。
以占位符检查为例:
import re import json with open("output/zh-CN.json", "r", encoding="utf-8") as f: data = json.load(f) pattern = r"\{[^}]+\}|%[sd]|\\n" for dialogue_id, item in data.items(): content = item["content"] if not content: print(f"警告:{dialogue_id} 的内容为空")实际项目中可以把这些检查接入 CI 或 Git Hook,每次提交前自动执行。
5. 字体与字幕:中文显示适配
5.1 字体缺失导致“方块字”问题
很多游戏或视频播放环境默认字体只覆盖日文字形,中文汉字不在范围内,于是页面上出现一个个方框。这种现象被称为“豆腐块”或“口口”。
解决思路有几种:
- 在游戏引擎中替换字体:如果你的项目是 Unity,可以创建一个包含中文字符的 TextMeshPro 字体资产,并设置为本地化后的默认字体。
- 在系统或播放器中设置 fallback 字体:例如在操作系统中安装思源黑体,并把它加入字体 fallback 链。
- 在字幕文件中强制指定字体:ASS 字幕可以通过样式指定
Noto Sans CJK SC,播放器在渲染时会优先使用该字体。
对于剧情视频字幕来说,第三种方案最直接,也是字幕组最常见的做法。
5.2 用 Aegisub 制作剧情字幕
Aegisub 可以创建 ASS 字幕文件。一个最简单的 ASS 文件结构如下:
[Script Info] ScriptType: v4.00+ PlayResX: 1920 PlayResY: 1080 [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding Style: Default,Noto Sans CJK SC,52,&H00FFFFFF,&H00FFFFFF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,1,2,20,20,40,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Dialogue: 0,0:00:01.00,0:00:03.50,Default,,0,0,0,,准备好一起去海边了吗?这里每个字段的含义都不复杂,但最关键的是Fontname。如果字体名称写得不对,播放器会回退到默认字体,进而可能再次出现方块字。推荐使用Noto Sans CJK SC,它是一款开源字体,覆盖简体中文常用字符,且可以在 Windows、macOS、Linux 上安装。
5.3 Unity 本地化与 TextMeshPro 字体
如果你是在 Unity 工程里接入中文本地化,流程会稍微复杂一点。Unity 的 Localization 插件支持通过 CSV 或 Google Sheets 导入多语言文本,然后在运行时根据所选 Locale 自动切换。
中文字体适配是主要工作点。TextMeshPro(简称 TMP)默认字体图集只包含常用西文字符,直接显示中文会变成方块。你需要做的是:
- 在 TMP 的 Font Asset Creator 中导入中文字体文件;
- 设置字符集为 Unihan 或 Dynamic;
- 生成字体图集;
- 在 Material 中指定该字体资产。
生成中文字体图集时,字符集如果选“所有 Unicode 汉字”,图集体积会非常大。更合理的做法是按活动剧情实际需要的汉字范围生成动态字体,或用“Custom Characters”手动填入所需字样。
当然,具体操作会因为 Unity 版本不同而有差异,这里更多是提供排查方向。
5.4 文本长度与显示区域
日文和中文的显示宽度不同。中文大部分汉字是“全角”字符,在渲染时占用的宽度接近两个西文字符。因此,同一句话从左边的日文变成右边的中文,行数和宽度都会变化。
可以写一个简单的显示宽度估算脚本:
# 文件路径:scripts/check_length.py import unicodedata def display_width(text: str) -> int: """估算文本显示宽度,全角字符按 2 列计算""" width = 0 for ch in text: if unicodedata.east_asian_width(ch) in ("W", "F"): width += 2 else: width += 1 return width def check_limit(text: str, max_width: int = 40) -> bool: return display_width(text) <= max_width if __name__ == "__main__": test = "准备一起去海边了吗?" print("显示宽度:", display_width(test)) print("是否超限:", not check_limit(test))这种脚本虽然不能完全替代引擎里的真实排版,但可以在早期发现明显超长文本,适合和 CI 流程结合。
6. 常见问题与排查思路
6.1 常见问题速查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 打开文件全是乱码 | 读取编码与文件实际编码不一致 | 先检测编码,再统一转为 UTF-8 |
| 中文显示成方块 | 字体或字体图集不包含中文字符 | 替换字体、生成中文字体图集、指定 fallback 字体 |
| 文本超出对话框 | 中文显示宽度大于日文原文本 | 输出前做宽度估算,适当精简文案 |
翻译后出现{0}丢失 | 翻译过程中误删占位符 | 用正则扫描占位符并建立校验机制 |
| JSON 解析失败 | 文本包含未转义双引号 | 检查并转义引号、反斜杠等特殊字符 |
CSV 第一列表头带\ufeff | 文件带 BOM,未指定 utf-8-sig | 读取时使用encoding="utf-8-sig" |
| 字幕时间轴对不上 | 台词长度发生变化 | 在 Aegisub 中按译文重新调整字幕块时长 |
6.2 排查清单
遇到本地化文件异常时,按以下顺序排查:
- 先看文件头。用十六进制编辑器观察前几个字节,确认是否有 BOM。
- 再看文件扩展名。JSON 还是 CSV?如果扩展名是 CSV 但实际是制表符分隔,需要指定
delimiter="\t"。 - 检查读取代码的 encoding 参数。逐行确认编码名称是否拼写正确。
- 检查控制台输出。用
print输出前三条记录,确认数据结构是否符合预期。 - 检查占位符。对翻译前后文本分别提取占位符,做集合比对。
- 检查字体。如果运行端看不到中文,先看是否缺少字体,再看字体图集和字体命名。
这套清单适用于绝大多数文本类本地化问题,跳过任何一步都可能延长排查时间。
7. 最佳实践与工程建议
7.1 把文本当作代码管理
很多本地化项目失败,不是因为翻译质量差,而是因为文本版本混乱。建议从第一天开始就把源文本和输出文本纳入 Git 管理。
具体做法:
- 每个语言一个目录,例如
loc/zh-CN.json、loc/ja-JP.json; - 每次同步源文本后提交一个 commit;
- 每次翻译更新后单独提交;
- 在提交信息里写清楚“同步源文本”还是“翻译更新”。
这样一旦发现问题,可以快速 diff 出具体哪句话在哪个版本被修改。
7.2 术语表与翻译记忆库
术语表适合解决“同一个词不同翻译”的问题,翻译记忆库则适合解决“相同句式重复翻译”的效率问题。
小型项目可以使用 JSON 维护术语表,并在脚本里做占位符保护;中型项目可以使用专门的本地化平台,或使用 gettext 的 PO 文件配合翻译记忆工具。关键是不要让术语表只停留在文档里,最好能变成自动检查规则。
7.3 自动化校验规则
建议在项目里维护一个独立的校验脚本,至少覆盖以下规则:
- 原始文本和译文行数一致;
- 占位符集合一致;
- 译文没有未替换完成的
TODO或空行; - 译文中的中文标点使用正确;
- 角色名和术语符合术语表;
- 文本显示宽度不超过预设阈值。
这些规则可以通过 CI 在每次提交时自动执行。如果在个人本地化项目里暂时没有 CI,也要养成“提交前跑一遍校验”的习惯。
7.4 合规与版权红线
这里需要特别强调:本文讨论的本地化流程,适用于已获得授权的项目、原创内容,或在公开素材基础上二次创作的个人学习场景。如果你在学习研究过程中制作了个人汉化内容,请注意不要传播完整成品、不要用于商业盈利,并尽可能回归到版权方认可的官方本地化版本或授权 Wiki。
技术本身是中性的,但文本内容属于版权保护范围。实际操作时优先以官方口径为准。
8. 后续学习方向
如果你完整跑完本文的示例,现在应该已经具备了一套基础的活动剧情本地化处理能力:能识别常见文本格式、能处理 Shift-JIS 和 UTF-8 的转换、能用 Python 批量提取和回填文本、能输出 JSON 语言包、能排查字体和字幕问题。
下一步可以继续研究的方向包括:
- gettext PO 文件格式与翻译工具链;
- Unity Localization 插件的高级用法;
- TextMeshPro 动态字体和字符集管理;
- 机器翻译与人工后编辑结合的工作流;
- 使用 LLM 辅助初步翻译时的术语一致性控制策略。
实际项目中,最容易出问题的地方往往是一些很不起眼的细节,比如 CSV 的换行符、JSON 的ensure_ascii参数、字体图集的字符集范围。这些坑一次踩过之后,后面就会顺畅很多。
如果你正在做剧本翻译或游戏本地化相关练习,建议先从一个小脚本开始,把手里的活动文本整理成统一格式,再逐步加入校验、字体和字幕流程。遇到问题时,只要回头检查编码、占位符和字体三步,大多数乱码和显示问题都能迅速定位。