"""Rebuild the Chinese HTML from translated templates.""" import json, re, sys, glob, html as htmllib from bs4 import BeautifulSoup, Comment, NavigableString ROOT = r"C:/Users/yvmou/Downloads/limboai-readthedocs-io-en-stable/limboai-stable" SRC = ROOT + "/index.html" OUT = ROOT + "/index.zh.html" I18N = ROOT + "/_i18n" sys.path.insert(0, I18N) from serialize import collect # noqa: E402 CHECK_ONLY = "--check" in sys.argv soup = BeautifulSoup(open(SRC, encoding="utf-8").read(), "lxml") blocks = collect(soup) title_node = soup.find("title") uniq = json.load(open(I18N + "/uniq.json", encoding="utf-8"))["uniq"] def key_of(tpl, markers): return tpl + "\x00" + json.dumps(markers, sort_keys=True) key2uid = {k: v["uid"] for k, v in uniq.items()} trans = {} for f in sorted(glob.glob(I18N + "/trans/batch*.json")): for k, v in json.load(open(f, encoding="utf-8")).items(): trans[int(k)] = v for b in blocks: b["uid"] = key2uid.get(key_of(b["tpl"], b["markers"])) tok_re = re.compile(r"<(l\d+)>||<([bi])>||<(\d+)/>") def norm(t): """normalise the tolerated closing-tag variants written by the translator""" return re.sub(r"", "", t) def tokens(t): """only the tokens that MUST round-trip identically: opaque ids and anchor ids""" return sorted(m.group(0) for m in re.finditer(r"<(l\d+)>||<(\d+)/>", t) if m.group(2) or m.group(1)) def balanced(t): """/ pairs must be balanced (extra emphasis is allowed)""" depth = 0 for m in re.finditer(r"<([bi])>|", t): depth += 1 if m.group(1) else -1 if depth < 0: return False return depth == 0 def markers_ok(b, zh): zh = norm(zh) return tokens(b["tpl"]) == tokens(zh) and balanced(zh) untranslated = [b for b in blocks if b["uid"] not in trans] bad = [b for b in blocks if b["uid"] in trans and not markers_ok(b, trans[b["uid"]])] print("blocks:", len(blocks), "| untranslated:", len(untranslated), "| marker problems:", len(bad)) for b in bad[:20]: print("--- uid", b["uid"]) print(" en:", b["tpl"][:200]) print(" zh:", trans[b["uid"]][:200]) if CHECK_ONLY: sys.exit(0) def rebuild(tpl, markers): """turn a translated template back into HTML""" tpl = norm(tpl) out, pos = [], 0 for m in tok_re.finditer(tpl): out.append(htmllib.escape(tpl[pos:m.start()], quote=False)) g = m.group(0) if g.startswith("": out.append("") elif g.startswith("") or g.startswith(""): out.append("" if g.startswith("") elif g.startswith("" if g.startswith("") else: out.append(markers[re.search(r"\d+", g).group(0)]) pos = m.end() out.append(htmllib.escape(tpl[pos:], quote=False)) return "".join(out) def replace_anchor_inner(marker_html, new_inner): """keep the original attributes + inner wrapper, swap the text""" frag = BeautifulSoup(marker_html, "html.parser") root = frag.find(True) node = root while True: kids = [k for k in node.children if not isinstance(k, Comment)] if len(kids) == 1 and getattr(kids[0], "name", None): node = kids[0] else: break for k in list(node.children): k.extract() inner = BeautifulSoup(new_inner, "html.parser") for k in list(inner.children): node.append(k.extract()) return str(root) def build_block_html(tpl, markers): parts = re.split(r"(.*?)", norm(tpl), flags=re.S) out = [] for p in parts: m = re.fullmatch(r"(.*?)", p, flags=re.S) if m: inner = rebuild(m.group(2), markers) out.append(replace_anchor_inner(markers[m.group(1)], inner)) else: out.append(rebuild(p, markers)) return "".join(out) def set_inner(el, html_str): el.clear() frag = BeautifulSoup(html_str, "html.parser") for k in list(frag.children): el.append(k.extract()) collect_ids = set(id(b["node"]) for b in blocks if b["node"] is not None) # a block that *contains* other collected blocks (e.g.

…) must keep its own # markup untouched - its text is rebuilt by those nested blocks containers = set() for b in blocks: n = b["node"] if n is None: continue if any(id(d) in collect_ids for d in n.find_all(True)): containers.add(id(n)) print("container blocks kept as-is:", len(containers)) fixed = 0 for b in blocks: uid = b["uid"] zh = trans.get(uid, b["tpl"]) if uid not in trans or not markers_ok(b, zh): continue if id(b["node"]) in containers: continue headerlinks = [h.extract() for h in b["node"].find_all("a", class_="headerlink")] html_str = build_block_html(zh, b["markers"]) set_inner(b["node"], html_str) for h in headerlinks: b["node"].append(h) fixed += 1 if title_node is not None and 1451 in trans: title_node.string = trans[1451] # ---------------------------------------------------------------- polish pass ABBR_TITLE = { "No return value.": "无返回值。", "This method has no side effects. It doesn't modify any of the instance's member variables.": "该方法没有副作用,不会修改实例的任何成员变量。", "This method should typically be overridden by the user to have any effect.": "该方法通常需要由用户重写才能生效。", "This method doesn't need an instance to be called, so it can be called directly using the class name.": "该方法无需实例即可调用,因此可以直接用类名调用。", } for el in soup.find_all(attrs={"title": True}): t = el.get("title") if t in ABBR_TITLE: el["title"] = ABBR_TITLE[t] # breadcrumb for li in soup.select("li.breadcrumb-item"): if li.get_text(strip=True) == "LimboAI 1.0 documentation": li.string = "LimboAI 1.0 文档" # footer foot = soup.find("footer") if foot: info = foot.find("div", {"role": "contentinfo"}) if info and info.find("p"): info.find("p").string = "© 2023 年至今 Serhii Snitsaruk 及 LimboAI 贡献者。" for child in list(foot.children): if isinstance(child, NavigableString): s = str(child) s = s.replace("Built with", "使用 ") s = s.replace("using a", " 构建,") s = s.replace("provided by", " 由 ") child.replace_with(s) elif getattr(child, "name", None) == "a" and child.get_text(strip=True) == "theme": child.string = "主题(theme)" last = foot.contents[-1] if foot.contents else None if isinstance(last, NavigableString): last.replace_with(" 提供。\n") # code comments inside

CODE_COMMENTS = {
    "# Set common target shared between agents in a group:": "# 设置组内各智能体共享的公共目标:",
    "#   to prevent errors when the instance is freed.": "#   以避免实例被释放时出错。",
    '# - Important: Avoid specifying a type for "obj" in GDScript': '# - 重要:在 GDScript 中不要为 "obj" 指定类型',
    "# Access common target shared between agents in a group:": "# 访问组内各智能体共享的公共目标:",
    '# Check if the "speed" variable exists:': '# 检查 "speed" 变量是否存在:',
    '# Get object instance stored in the "object" variable.': '# 获取存储在 "object" 变量中的对象实例。',
    '# Get the value of the "speed" variable, with a default value of 100.0 if not found:':
        '# 获取 "speed" 变量的值,如果不存在则默认为 100.0:',
    '# Handle the "movement_started" event here.': '# 在这里处理 "movement_started" 事件。',
    "# It's quite handy when you need to pass some data to the event handler.":
        "# 当你需要向事件处理器传递一些数据时,这非常方便。",
    '# Set the behavior tree, or use a load("res://ai/trees/...") call':
        '# 设置行为树,或使用 load("res://ai/trees/...") 调用',
    '# Set the value of the "speed" variable:': '# 设置 "speed" 变量的值:',
    "# Use chained methods and delegation to set up states:": "# 使用链式方法与委托来配置状态:",
    "# `cargo` can be passed with the event when calling `dispatch()`.":
        "# 调用 `dispatch()` 时可以随事件一起传入 `cargo`。",
    "# Assume `hsm` is a LimboHSM instance and `bt_state` is a BTState instance":
        "# 假设 `hsm` 是一个 LimboHSM 实例,`bt_state` 是一个 BTState 实例",
    "# Called each time this task is ticked (aka executed).": "# 每次该任务被 tick(即被执行)时调用。",
    "# Called to generate a display name for the task (requires @tool).":
        "# 调用它来为任务生成显示名称(需要 @tool)。",
    "# Called to generate a display name for the task.": "# 调用它来为任务生成显示名称。",
    "# Called to initialize the task.": "# 调用它来初始化任务。",
    "# Called when the task is entered.": "# 当任务被进入时调用。",
    "# Called when the task is executed.": "# 当任务被执行时调用。",
    "# Called when the task is exited.": "# 当任务被退出时调用。",
    "# Example: programmatically link a BT blackboard variable to an HSM blackboard variable":
        "# 示例:用代码把 BT 黑板变量链接到 HSM 黑板变量",
    "# NOTE: variable names do not need to match": "# 注意:变量名不必相同",
    "# Strings returned from this method are displayed as warnings in the editor.":
        "# 此方法返回的字符串会作为警告显示在编辑器中。",
    "# Task parameters.": "# 任务参数。",
    "## AgentGroup node: Manages the shared Blackboard for agents in a group.":
        "## AgentGroup 节点:管理组内各智能体共享的 Blackboard。",
    '## At minimum, you only need to define the "_tick" method.': '## 至少只需要定义 "_tick" 方法。',
    "## Called each frame when this state is active.": "## 当该状态处于活动状态时每帧调用。",
    "## Called once, when state is initialized.": "## 状态初始化时调用一次。",
    "## Called when state is entered.": "## 当状态被进入时调用。",
    "## Called when state is exited.": "## 当状态被退出时调用。",
    "## Children of this node are assumed to be agents that belong to a common group.":
        "## 该节点的子节点被假定为属于同一组的智能体。",
    "## InRange condition checks if the agent is within a range of target,":
        "## InRange 条件检查智能体是否处于目标的某个范围内,",
    "## Note: Each method declaration is optional.": "## 注意:每个方法声明都是可选的。",
    "## PlayAnimation: Play an animation on AnimationPlayer, and wait for it to finish.":
        "## PlayAnimation:在 AnimationPlayer 上播放动画,并等待其播放完毕。",
    "## Returns FAILURE if the node is not found.": "## 如果找不到该节点,返回 FAILURE。",
    "## Returns SUCCESS if the agent is within the defined range;":
        "## 如果智能体处于定义的范围内,返回 SUCCESS;",
    "## Shows or hides a node and returns SUCCESS.": "## 显示或隐藏一个节点并返回 SUCCESS。",
    '## This implementation assumes that each agent has a "BTPlayer" node for AI.':
        '## 该实现假定每个智能体都有一个用于 AI 的 "BTPlayer" 节点。',
    "## defined by distance_min and distance_max.": "## 该范围由 distance_min 和 distance_max 定义。",
    "## otherwise, returns FAILURE.": "## 否则返回 FAILURE。",
}
hits = 0
for pre in soup.find_all("pre"):
    for txt in pre.find_all(string=True):
        s = str(txt)
        orig = s
        for en, zh in CODE_COMMENTS.items():
            if en in s:
                s = s.replace(en, zh)
        if s != orig:
            txt.replace_with(s)
            hits += 1
print("code comment spans translated:", hits)


# translation badge in the sidebar
logo_link = soup.select_one(".wy-side-nav-search > a")
if logo_link is not None:
    badge = BeautifulSoup(
        '

' '中文汉化版 · 基于官方英文文档翻译,欢迎对照原文阅读

', "html.parser") for k in list(badge.children): logo_link.insert_after(k.extract()) out = str(soup) out = out.replace('LimboAI 1.0 documentation", "LimboAI 1.0 中文文档") open(OUT, "w", encoding="utf-8").write(out) print("rebuilt blocks:", fixed, "->", OUT)