专名本土化(信达雅):冻结译名表 + 渲染层,并修掉校验器的正则盲点
- 新增 dfannals/glossary.py(译名表)与 dfannals/localize.py(渲染层) 人名=音译+绰号意译(Ral Fastenhatchets→拉尔·缚斧、朗古德·商熊「剖胎之寒」), 地名整体意译(Fordedwinds→涉风渡、The Volcano Of Ravens→渡鸦火山) - 生成链路本土化:素材渲染、人物表、扩写白名单全部走译名;已有产物做确定性替换 - 修复 factcheck 的 \b 盲点:Unicode 下汉字也算词字符,紧贴汉字的英文名不被检查 (曾因此漏掉编造专名)。修好后抓出并定点修掉长版第 1 集的 2 处编造 - 修正 title_form 大小写(YETI→Yeti),此前 Yeti/Goblin 一直漏译 - 新增 tests/test_localize.py(13 项回归)
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""专名本土化的回归测试。
|
||||
|
||||
重点盯住两个曾经真实踩过的坑:
|
||||
1. 只处理全名,漏掉正文里大量使用的简称(`Ral`)与单呼的姓(`Fastenhatchets`)。
|
||||
2. `YETI` 这类全大写枚举只改首字母得到 `YETI`,永远匹配不上正文里的 `Yeti`。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from dfannals import glossary, localize
|
||||
|
||||
|
||||
def test_title_form_lowercases_tail() -> None:
|
||||
assert localize.title_form("YETI") == "Yeti"
|
||||
assert localize.title_form("EAGLE_MAN") == "Eagle_man"
|
||||
assert localize.title_form("fastenhatchets") == "Fastenhatchets"
|
||||
|
||||
|
||||
def test_person_full_name() -> None:
|
||||
assert localize.translate_person("Ral Fastenhatchets") == "拉尔·缚斧"
|
||||
assert localize.translate_person("Guspu Frillyknots") == "古斯普·褶结"
|
||||
assert localize.translate_person("Rigoth Stilledearthen") == "里戈斯·静土"
|
||||
|
||||
|
||||
def test_person_with_epithet() -> None:
|
||||
name = "Langgud Tradebears The Eviscerated Chill Of Pregnancies"
|
||||
assert localize.translate_person(name) == "朗古德·商熊「剖胎之寒」"
|
||||
|
||||
|
||||
def test_person_unknown_returns_none() -> None:
|
||||
assert localize.translate_person("Zzzq Unknownsurname") is None
|
||||
|
||||
|
||||
def test_place_and_epithet() -> None:
|
||||
assert localize.translate_place("Fordedwinds") == "涉风渡"
|
||||
assert localize.translate_place("Paperhushed") == "纸寂"
|
||||
assert localize.translate_place("Gloryholes The Trickery Of Mornings") == "荣耀穴(晨之诡计)"
|
||||
|
||||
|
||||
def test_group_translation() -> None:
|
||||
assert localize.translate_group("The Volcano Of Ravens") == "渡鸦火山"
|
||||
|
||||
|
||||
def test_race_enum_and_display_form() -> None:
|
||||
assert localize.translate_race("YETI") == "雪人"
|
||||
assert localize.translate_race("DWARF") == "矮人"
|
||||
assert localize.translate_race("UNKNOWN_RACE") == "UNKNOWN_RACE"
|
||||
|
||||
|
||||
def _fake_translator() -> localize.Translator:
|
||||
"""不依赖真实世界数据的小翻译器,专测替换顺序。"""
|
||||
translator = localize.Translator(
|
||||
people={"Ral Fastenhatchets": "拉尔·缚斧", "Rigoth Stilledearthen": "里戈斯·静土"},
|
||||
places={"Fordedwinds": "涉风渡"},
|
||||
groups={"The Volcano Of Ravens": "渡鸦火山"},
|
||||
stems={"Ral": "拉尔", "Rigoth": "里戈斯"},
|
||||
surnames={"Fastenhatchets": "缚斧"},
|
||||
races={"Yeti": "雪人", "EAGLE_MAN": "鹰人"},
|
||||
)
|
||||
return translator
|
||||
|
||||
|
||||
def test_render_full_name_wins_over_short_name() -> None:
|
||||
out = _fake_translator().render("Ral Fastenhatchets守着Fordedwinds。")
|
||||
assert out == "拉尔·缚斧守着涉风渡。", out
|
||||
|
||||
|
||||
def test_render_short_name_and_bare_surname() -> None:
|
||||
out = _fake_translator().render("Ral对Rigoth说:她那一支姓Fastenhatchets。")
|
||||
assert "拉尔" in out and "里戈斯" in out and "缚斧" in out, out
|
||||
assert not any(ch.isascii() and ch.isalpha() for ch in out), out
|
||||
|
||||
|
||||
def test_render_race_display_form() -> None:
|
||||
out = _fake_translator().render("一个Yeti站了起来。军团是EAGLE_MAN。")
|
||||
assert "雪人" in out and "鹰人" in out, out
|
||||
|
||||
|
||||
def test_render_tidies_spaces() -> None:
|
||||
out = _fake_translator().render("Ral 说 完 就 走 了")
|
||||
assert "拉尔" in out and "拉尔 说" not in out, out
|
||||
|
||||
|
||||
def test_accepted_includes_pieces() -> None:
|
||||
accepted = _fake_translator().accepted()
|
||||
assert "拉尔·缚斧" in accepted
|
||||
assert "拉尔" in accepted and "缚斧" in accepted, sorted(accepted)
|
||||
|
||||
|
||||
def test_glossary_tables_are_lowercase_keys() -> None:
|
||||
for table in (glossary.STEMS, glossary.SURNAMES, glossary.PLACES):
|
||||
for key in table:
|
||||
assert key == key.lower(), f"{key} 应小写,否则 title_form 对不上"
|
||||
|
||||
|
||||
def _main() -> int:
|
||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
||||
failed = 0
|
||||
for fn in tests:
|
||||
try:
|
||||
fn()
|
||||
except AssertionError as exc:
|
||||
failed += 1
|
||||
print(f" ✗ {fn.__name__}: {exc}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
failed += 1
|
||||
print(f" ✗ {fn.__name__}: {type(exc).__name__}: {exc}")
|
||||
else:
|
||||
print(f" ✓ {fn.__name__}")
|
||||
print(f"\n{len(tests) - failed}/{len(tests)} 通过")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(_main())
|
||||
Reference in New Issue
Block a user