docs\limboai-stable

This commit is contained in:
2026-10-15 16:30:44 +08:00
parent ecebf76f47
commit 18e7b2daa8
149 changed files with 97312 additions and 0 deletions
+294
View File
@@ -0,0 +1,294 @@
"""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+)>|</l\d*>|<([bi])>|</[bi]>|<(\d+)/>")
def norm(t):
"""normalise the tolerated closing-tag variants written by the translator"""
return re.sub(r"</l\d*>", "</l>", 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+)>|</l\d*>|<(\d+)/>", t)
if m.group(2) or m.group(1))
def balanced(t):
"""<b>/<i> pairs must be balanced (extra emphasis is allowed)"""
depth = 0
for m in re.finditer(r"<([bi])>|</([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("<l"):
out.append(markers[g[2:-1]]) # nested anchor: keep as-is
elif g == "</l>":
out.append("</a>")
elif g.startswith("<b>") or g.startswith("<i>"):
out.append("<strong>" if g.startswith("<b") else "<em>")
elif g.startswith("</b") or g.startswith("</i"):
out.append("</strong>" if g.startswith("</b") else "</em>")
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 <a> 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"(<l\d+>.*?</l\d*>)", norm(tpl), flags=re.S)
out = []
for p in parts:
m = re.fullmatch(r"<l(\d+)>(.*?)</l\d*>", 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. <td><p>…) 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 <pre>
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(
'<p style="font-size:0.78em;opacity:0.8;margin:0.35em 0 0.2em;">'
'中文汉化版 · 基于官方英文文档翻译,欢迎对照原文阅读</p>', "html.parser")
for k in list(badge.children):
logo_link.insert_after(k.extract())
out = str(soup)
out = out.replace('<html class="writer-html5" lang="en"',
'<html class="writer-html5" lang="zh-CN"', 1)
out = out.replace("<title>LimboAI 1.0 documentation</title>",
"<title>LimboAI 1.0 中文文档</title>")
open(OUT, "w", encoding="utf-8").write(out)
print("rebuilt blocks:", fixed, "->", OUT)