skip to content
WangWindow

Search

    RPG Maker 游戏文本批量翻译脚本 RPG Maker 系列游戏的文本大多存储在 JSON 或 CSV 文件中,手动翻译效率极低。如果使用 MTool 进行挂载翻译不够优雅,优先考虑内嵌汉化(即直接替换原来的文本内容)。 本文将介绍如何用 Python 脚本批量翻译 RPG Maker 游戏文

    RPG Maker 游戏文本批量翻译脚本

    / 13 min read

    RPG Maker 游戏文本批量翻译脚本

    RPG Maker 系列游戏的文本大多存储在 JSON 或 CSV 文件中,手动翻译效率极低。如果使用 MTool 进行挂载翻译不够优雅,优先考虑内嵌汉化(即直接替换原来的文本内容)。 本文将介绍如何用 Python 脚本批量翻译 RPG Maker 游戏文本,并分享实战经验和常见坑点。

    1. 脚本功能简介

    本脚本支持:

    • 读取翻译字典(如 translation.json)(建议使用 MTool 生成,通常需要 MTool 挂载汉化的也自带翻译文件)
    • 遍历原始数据文件夹(如 data-jp),批量翻译所有 JSON/CSV 文件
    • 递归处理嵌套结构,智能替换所有文本
    • 自动处理插件参数中的嵌套 JSON 字符串,避免转义错误
    • 输出到新文件夹(如 data-cn),不破坏原始数据

    💡 小贴士

    • 建议先备份源文件,确认无误后再批量替换。

    2. 脚本核心代码

    👇👇👇

    rpg游戏翻译文本替换.py(点击展开查看)
    import json
    import csv
    from pathlib import Path
    # 配置
    TRANSLATION_FILE = "translation.json"
    DATA_DIR = "data-jp"
    OUTPUT_DIR = "data-cn"
    def load_translation(file_path):
    """加载翻译JSON文件。"""
    print(f"加载翻译文件: {file_path}")
    try:
    with open(file_path, "r", encoding="utf-8") as f:
    return json.load(f)
    except Exception as e:
    print(f"加载翻译文件出错: {e}")
    return {}
    def build_lookup_table(translation_map):
    """
    构建查找表以加速子串替换。
    返回一个字典:字符 -> (key, value)元组列表,按key长度降序排列。
    """
    print("正在构建查找表...")
    lookup = {}
    for key, value in translation_map.items():
    if not key:
    continue
    first_char = key[0]
    if first_char not in lookup:
    lookup[first_char] = []
    lookup[first_char].append((key, value))
    # 每个列表按key长度降序排列,确保最长优先匹配
    for char in lookup:
    lookup[char].sort(key=lambda x: len(x[0]), reverse=True)
    return lookup
    def translate_text(text, lookup_table):
    """
    使用查找表翻译文本。
    替换文本中出现的所有key。
    """
    if not isinstance(text, str):
    return text
    # 尝试解析嵌套的JSON字符串 (针对插件参数)
    try:
    stripped = text.strip()
    if stripped.startswith("{") or stripped.startswith("["):
    parsed = json.loads(text)
    if isinstance(parsed, (dict, list)):
    def recursive_helper(obj):
    if isinstance(obj, dict):
    return {k: recursive_helper(v) for k, v in obj.items()}
    elif isinstance(obj, list):
    return [recursive_helper(v) for v in obj]
    elif isinstance(obj, str):
    return translate_text(obj, lookup_table)
    else:
    return obj
    translated_parsed = recursive_helper(parsed)
    return json.dumps(
    translated_parsed, ensure_ascii=False, separators=(",", ":")
    )
    except Exception:
    pass
    result = []
    i = 0
    n = len(text)
    while i < n:
    char = text[i]
    match = None
    if char in lookup_table:
    for key, value in lookup_table[char]:
    if text.startswith(key, i):
    match = (key, value)
    break
    if match:
    result.append(match[1])
    i += len(match[0])
    else:
    result.append(char)
    i += 1
    return "".join(result)
    def process_json_file(file_path, output_path, lookup_table):
    """处理JSON文件。"""
    print(f"处理JSON文件: {file_path} -> {output_path}")
    try:
    with open(file_path, "r", encoding="utf-8") as f:
    data = json.load(f)
    def recursive_translate(obj):
    if isinstance(obj, dict):
    return {k: recursive_translate(v) for k, v in obj.items()}
    elif isinstance(obj, list):
    return [recursive_translate(v) for v in obj]
    elif isinstance(obj, str):
    return translate_text(obj, lookup_table)
    else:
    return obj
    translated_data = recursive_translate(data)
    with open(output_path, "w", encoding="utf-8") as f:
    json.dump(translated_data, f, ensure_ascii=False, indent=4)
    except Exception as e:
    print(f"处理JSON文件出错 {file_path}: {e}")
    def process_csv_file(file_path, output_path, lookup_table):
    """处理CSV文件。"""
    print(f"处理CSV文件: {file_path} -> {output_path}")
    try:
    rows = []
    fieldnames = None
    with open(file_path, "r", encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    fieldnames = reader.fieldnames
    for row in reader:
    rows.append(row)
    if not fieldnames:
    print(f"警告: 未找到字段 {file_path}")
    return
    translated_rows = []
    for row in rows:
    new_row = {}
    for k, v in row.items():
    if k == "Message":
    new_row[k] = translate_text(v, lookup_table)
    else:
    new_row[k] = v
    translated_rows.append(new_row)
    with open(output_path, "w", encoding="utf-8", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerows(translated_rows)
    except Exception as e:
    print(f"处理CSV文件出错 {file_path}: {e}")
    def main():
    translation_file = Path(TRANSLATION_FILE)
    data_dir = Path(DATA_DIR)
    output_dir = Path(OUTPUT_DIR)
    if not translation_file.exists():
    print(f"未找到翻译文件: {translation_file}")
    return
    translation_map = load_translation(translation_file)
    if not translation_map:
    print("未找到任何翻译内容。")
    return
    lookup_table = build_lookup_table(translation_map)
    for file_path in data_dir.rglob("*"):
    if file_path.is_file():
    # 计算相对路径并构建输出路径
    relative_path = file_path.relative_to(data_dir)
    output_path = output_dir / relative_path
    # 确保输出目录存在
    output_path.parent.mkdir(parents=True, exist_ok=True)
    if file_path.suffix.lower() == ".json":
    process_json_file(file_path, output_path, lookup_table)
    elif file_path.suffix.lower() == ".csv":
    process_csv_file(file_path, output_path, lookup_table)
    if __name__ == "__main__":
    main()
    rpg游戏翻译文本替换2.0.py(提高替换速度、优化了多行匹配)(点击展开查看)
    import json
    import csv
    from pathlib import Path
    from typing import Any
    from dataclasses import dataclass, field
    from collections import OrderedDict
    # 配置
    TRANSLATION_FILE = "A翻译.json"
    DATA_DIR = "www/data-jp"
    OUTPUT_DIR = "www/data-cn"
    @dataclass
    class TranslationMatcher:
    """纯 Python 翻译匹配器:Trie 最长匹配 + 简单 LRU 缓存。
    - trie: 前缀树,按字符逐步匹配,避免每个位置遍历大量 key。
    - cache: 限制大小的 LRU,提升重复文本/重复行的翻译速度。
    """
    trie: dict
    cache_max: int = 50_000
    cache: "OrderedDict[str, str]" = field(default_factory=OrderedDict)
    def cache_get(self, key: str) -> str | None:
    val = self.cache.get(key)
    if val is not None:
    self.cache.move_to_end(key)
    return val
    def cache_put(self, key: str, val: str) -> None:
    self.cache[key] = val
    self.cache.move_to_end(key)
    if len(self.cache) > self.cache_max:
    self.cache.popitem(last=False)
    def load_translation(file_path):
    """加载翻译JSON文件。"""
    print(f"加载翻译文件: {file_path}")
    try:
    with open(file_path, "r", encoding="utf-8") as f:
    return json.load(f)
    except Exception as e:
    print(f"加载翻译文件出错: {e}")
    return {}
    def build_lookup_table(translation_map) -> TranslationMatcher:
    """构建匹配器(Trie + LRU),用于大文件的高速替换。"""
    print("正在构建查找表(Trie)...")
    trie: dict = {}
    terminal = "\0" # 作为终止标记;正常文本中几乎不会出现
    for key, value in translation_map.items():
    if not key:
    continue
    node = trie
    for ch in key:
    node = node.setdefault(ch, {})
    node[terminal] = value
    # 把 terminal 标记也塞进 matcher 里(避免全局变量/闭包开销)
    matcher = TranslationMatcher(trie={"_root": trie, "_terminal": terminal})
    return matcher
    def translate_text(text, lookup_table: TranslationMatcher):
    """
    使用查找表翻译文本。
    替换文本中出现的所有key。
    """
    if not isinstance(text, str):
    return text
    # LRU 命中直接返回
    cached = lookup_table.cache_get(text)
    if cached is not None:
    return cached
    # 尝试解析嵌套的JSON字符串 (针对插件参数)
    try:
    stripped = text.strip()
    if stripped.startswith("{") or stripped.startswith("["):
    parsed = json.loads(text)
    if isinstance(parsed, (dict, list)):
    def recursive_helper(obj):
    if isinstance(obj, dict):
    return {k: recursive_helper(v) for k, v in obj.items()}
    elif isinstance(obj, list):
    return [recursive_helper(v) for v in obj]
    elif isinstance(obj, str):
    return translate_text(obj, lookup_table)
    else:
    return obj
    translated_parsed = recursive_helper(parsed)
    return json.dumps(
    translated_parsed, ensure_ascii=False, separators=(",", ":")
    )
    except Exception:
    pass
    root = lookup_table.trie["_root"]
    terminal = lookup_table.trie["_terminal"]
    result: list[str] = []
    i = 0
    n = len(text)
    while i < n:
    node = root
    j = i
    last_val: str | None = None
    last_j = i
    # Trie 向前走,记录“最长的终止匹配”
    while j < n:
    ch = text[j]
    nxt = node.get(ch)
    if nxt is None:
    break
    node = nxt
    j += 1
    val = node.get(terminal)
    if val is not None:
    last_val = val
    last_j = j
    if last_val is not None:
    result.append(last_val)
    i = last_j
    else:
    result.append(text[i])
    i += 1
    translated = "".join(result)
    lookup_table.cache_put(text, translated)
    return translated
    def _is_event_command_dict(obj: Any) -> bool:
    return (
    isinstance(obj, dict)
    and "code" in obj
    and "indent" in obj
    and "parameters" in obj
    and isinstance(obj.get("code"), int)
    and isinstance(obj.get("indent"), int)
    and isinstance(obj.get("parameters"), list)
    )
    def _process_event_command_list(commands: list[dict], lookup_table) -> list[dict]:
    """对RPG Maker事件命令列表做更贴近引擎的翻译处理。
    关键点:
    - Show Text: code=101 后面会跟若干 code=401 行,这些 401 共同组成一个多行文本。
    翻译表里经常用 "A\nB" 作为一个 key;若逐行翻译就无法命中。
    - Show Scrolling Text: code=105 后面会跟若干 code=405 行,同理。
    """
    def translate_any(value: Any) -> Any:
    if isinstance(value, str):
    return translate_text(value, lookup_table)
    if isinstance(value, list):
    return [translate_any(v) for v in value]
    if isinstance(value, dict):
    return {k: translate_any(v) for k, v in value.items()}
    return value
    out: list[dict] = []
    i = 0
    while i < len(commands):
    cmd = commands[i]
    if not _is_event_command_dict(cmd):
    out.append(cmd)
    i += 1
    continue
    code = cmd["code"]
    # 101: Show Text + 401 continuations
    if code == 101:
    out.append(
    {
    "code": cmd["code"],
    "indent": cmd.get("indent", 0),
    "parameters": translate_any(cmd.get("parameters", [])),
    }
    )
    j = i + 1
    lines: list[str] = []
    indent_for_401 = cmd.get("indent", 0)
    while (
    j < len(commands)
    and _is_event_command_dict(commands[j])
    and commands[j]["code"] == 401
    ):
    if commands[j].get("parameters"):
    lines.append(commands[j]["parameters"][0])
    else:
    lines.append("")
    indent_for_401 = commands[j].get("indent", indent_for_401)
    j += 1
    if lines:
    combined = "\n".join(lines)
    translated = translate_text(combined, lookup_table)
    for line in translated.split("\n"):
    out.append(
    {"code": 401, "indent": indent_for_401, "parameters": [line]}
    )
    i = j
    continue
    # 105: Show Scrolling Text + 405 continuations
    if code == 105:
    out.append(
    {
    "code": cmd["code"],
    "indent": cmd.get("indent", 0),
    "parameters": translate_any(cmd.get("parameters", [])),
    }
    )
    j = i + 1
    lines: list[str] = []
    indent_for_405 = cmd.get("indent", 0)
    while (
    j < len(commands)
    and _is_event_command_dict(commands[j])
    and commands[j]["code"] == 405
    ):
    if commands[j].get("parameters"):
    lines.append(commands[j]["parameters"][0])
    else:
    lines.append("")
    indent_for_405 = commands[j].get("indent", indent_for_405)
    j += 1
    if lines:
    combined = "\n".join(lines)
    translated = translate_text(combined, lookup_table)
    for line in translated.split("\n"):
    out.append(
    {"code": 405, "indent": indent_for_405, "parameters": [line]}
    )
    i = j
    continue
    # 其它事件命令:沿用旧行为,递归翻译 parameters 中的字符串
    out.append(
    {
    "code": cmd["code"],
    "indent": cmd.get("indent", 0),
    "parameters": translate_any(cmd.get("parameters", [])),
    }
    )
    i += 1
    return out
    def process_json_file(file_path, output_path, lookup_table):
    """处理JSON文件。"""
    print(f"处理JSON文件: {file_path} -> {output_path}")
    try:
    with open(file_path, "r", encoding="utf-8") as f:
    data = json.load(f)
    def recursive_translate(obj):
    if isinstance(obj, dict):
    # 事件页 / 公共事件等结构:{ ..., "list": [ {code, indent, parameters}, ... ] }
    if "list" in obj and isinstance(obj.get("list"), list) and obj["list"]:
    list_val = obj["list"]
    if all(_is_event_command_dict(x) for x in list_val):
    new_obj = {
    k: recursive_translate(v)
    for k, v in obj.items()
    if k != "list"
    }
    new_obj["list"] = _process_event_command_list(
    list_val, lookup_table
    )
    return new_obj
    return {k: recursive_translate(v) for k, v in obj.items()}
    elif isinstance(obj, list):
    # 兼容某些文件顶层就是事件命令数组
    if obj and all(_is_event_command_dict(x) for x in obj):
    return _process_event_command_list(obj, lookup_table)
    return [recursive_translate(v) for v in obj]
    elif isinstance(obj, str):
    return translate_text(obj, lookup_table)
    else:
    return obj
    translated_data = recursive_translate(data)
    with open(output_path, "w", encoding="utf-8") as f:
    json.dump(translated_data, f, ensure_ascii=False, indent=4)
    except Exception as e:
    print(f"处理JSON文件出错 {file_path}: {e}")
    def process_csv_file(file_path, output_path, lookup_table):
    """处理CSV文件。"""
    print(f"处理CSV文件: {file_path} -> {output_path}")
    try:
    rows = []
    fieldnames = None
    with open(file_path, "r", encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    fieldnames = reader.fieldnames
    for row in reader:
    rows.append(row)
    if not fieldnames:
    print(f"警告: 未找到字段 {file_path}")
    return
    translated_rows = []
    for row in rows:
    new_row = {}
    for k, v in row.items():
    if k == "Message":
    new_row[k] = translate_text(v, lookup_table)
    else:
    new_row[k] = v
    translated_rows.append(new_row)
    with open(output_path, "w", encoding="utf-8", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerows(translated_rows)
    except Exception as e:
    print(f"处理CSV文件出错 {file_path}: {e}")
    def main():
    translation_file = Path(TRANSLATION_FILE)
    data_dir = Path(DATA_DIR)
    output_dir = Path(OUTPUT_DIR)
    if not translation_file.exists():
    print(f"未找到翻译文件: {translation_file}")
    return
    translation_map = load_translation(translation_file)
    if not translation_map:
    print("未找到任何翻译内容。")
    return
    lookup_table = build_lookup_table(translation_map)
    for file_path in data_dir.rglob("*"):
    if file_path.is_file():
    # 计算相对路径并构建输出路径
    relative_path = file_path.relative_to(data_dir)
    output_path = output_dir / relative_path
    # 确保输出目录存在
    output_path.parent.mkdir(parents=True, exist_ok=True)
    if file_path.suffix.lower() == ".json":
    process_json_file(file_path, output_path, lookup_table)
    elif file_path.suffix.lower() == ".csv":
    process_csv_file(file_path, output_path, lookup_table)
    if __name__ == "__main__":
    main()

    3. 使用方法

    1. 准备好翻译字典(如 translation.json),格式为:
      { "原文": "译文", ... }
    2. 将原始游戏数据(如 data-jp 文件夹)和脚本放在同一目录
    3. 运行脚本:
      Terminal window
      python rpg游戏翻译文本替换.py
    4. 翻译结果会输出到 data-cn 文件夹

    4. 关键实现与注意事项

    4.1 转义字符与嵌套 JSON

    插件参数、对话选项等常用嵌套 JSON 字符串存储。直接替换会破坏转义,导致游戏报错。

    • 脚本会自动检测字符串是否为 JSON,并递归翻译后重新序列化,保证所有引号、特殊字符都被正确转义。
    • 这样可避免 SyntaxError: Unexpected token ... in JSON 等常见报错。

    4.2 字体适配与显示问题

    中文字体未适配会导致游戏内文字粗细不一、乱码或显示异常。

    • 建议在 data/System.json 中将 mainFontFilenamenumberFontFilename 设置为支持中文的字体(如 NotoSansSC-VariableFont_wght.ttfMicrosoft YaHei 等)。
    • 可在 CSS 或 System.json 里设置 fallbackFonts
    • 若字体文件较大,注意 RPG Maker 对字体格式的兼容性(如 ttf/otf/woff)。

    4.3 翻译字典的维护

    翻译字典建议用 MTool 导出,文本编辑器维护,注意避免重复、空 key。

    • 优先匹配最长 key,避免短词误替换。
    • 可用正则或脚本辅助去重。

    附录

    莉可的奇妙外卖任务-zh.png

    如有疑问,欢迎留言交流!