Files
mtgodot-poc/script/build_zh_server_locale.py
T
2026-09-28 00:18:37 -07:00

134 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Build the 40250 newcibn locale_string.txt from its Korean keys and GBK text."""
import argparse
import hashlib
import json
import re
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
DEFAULT_ENGLISH = REPO.parent / "40250/Server Client TMP4/Server/metin2/server/share/locale/english/locale_string.txt"
DEFAULT_CHINESE = REPO.parent / "server_extracted/server_locale/locale_string.txt"
DEFAULT_OUTPUT = REPO / "build/zh-locale/server/newcibn/locale_string.txt"
QUOTED = re.compile(rb'^"(.*)"\s*;$')
FORMAT = re.compile(rb'%(?:\d+\$)?[-+#0 ]*\d*(?:\.\d+)?(?:hh|ll|[hlLzjt])?[diouxXeEfFgGaAcspD%]')
# These keys exist in 40250 but not in the older newcibn dictionary.
ADDED = {
"등급 개량에 성공했습니다.": "品级提升成功。",
"%s이 필요합니다.": "需要%s。",
"용심 추출에 성공하였습니다.": "龙心提取成功。",
"용심 추출에 실패하였습니다.": "龙心提取失败。",
"단계 개량에 필요한 재료가 아닙니다.": "这不是提升阶段所需的材料。",
"이미 장비를 착용하고 있습니다.": "已经装备了同类物品。",
"등급 개량할 수 없는 용혼석입니다.": "该龙魂石无法提升品级。",
"강화에 성공했습니다.": "强化成功。",
"<길드> 잠시후에 이용해주십시오.": "<公会>请稍后再试。",
"%d 분 %d 초 동안 채팅금지 상태입니다": "聊天禁言还剩%d分%d秒。",
"이미 같은 종류의 용혼석을 착용하고 있습니다.": "已经装备了同类龙魂石。",
"인벤이 활성화 되지 않음.": "龙魂石背包尚未开启,请先完成龙魂石任务。",
"단계 개량할 수 없는 용혼석입니다.": "该龙魂石无法提升阶段。",
"벨트 인벤토리에 아이템이 존재하면 해제할 수 없습니다.": "腰带背包中还有物品,无法卸下腰带。",
"강화창을 연 상태에서는 아이템을 옮길 수 없습니다.": "强化窗口打开时无法移动物品。",
"먼저 용혼석 퀘스트를 완료하셔야 합니다.": "请先完成龙魂石任务。",
"단계 개량에 실패했습니다.": "阶段提升失败。",
"강화에 실패했습니다.": "强化失败。",
"용혼석 추출에 실패하여 %s를 얻었습니다.": "龙魂石提取失败,但获得了%s。",
"다른 거래중(창고,교환,상점)에는 사용할 수 없습니다.": "使用仓库、交易或商店时无法进行此操作。",
"강화할 수 없는 용혼석입니다.": "该龙魂石无法强化。",
"용혼석 추출에 실패하였습니다.": "龙魂石提取失败。",
"%d 시간 %d 분 %d 초 동안 채팅금지 상태입니다": "聊天禁言还剩%d小时%d分%d秒。",
"이미 탈것을 이용중입니다.": "你已经骑乘坐骑,请先下坐骑。",
"<길드> %u의 경험치를 투자하였습니다.": "<公会>投入了%u点经验值。",
"이 아이템은 벨트 인벤토리로 옮길 수 없습니다.": "此物品无法放入腰带背包。",
"%d 초 동안 채팅금지 상태입니다": "聊天禁言还剩%d秒。",
"등급 개량에 필요한 재료가 아닙니다.": "这不是提升品级所需的材料。",
"용혼석 추출에 성공하였습니다.": "龙魂石提取成功。",
"%d 시간 %d 초 동안 채팅금지 상태입니다": "聊天禁言还剩%d小时%d秒。",
"Pregunta.": "问题。",
"착용 중인 용혼석은 추출할 수 없습니다.": "无法提取已装备的龙魂石。",
"등급 개량에 실패했습니다.": "品级提升失败。",
"외치기는 유료회원만 사용할수 있습니다": "当前无法使用世界喊话。",
"강화에 필요한 재료가 아닙니다.": "这不是强化所需的材料。",
"단계 개량에 성공했습니다.": "阶段提升成功。",
}
# These two live entries lose a printf argument. Keep their keys and restore both fields.
CORRECTED = {
"OX 상품을 지급하였습니다. ID: %d 수량: %d": "已发放OX奖品,编号:%d,数量:%d。",
"곡괭이의 수련도가 증가하였습니다! (%d/%d)": "矿镐熟练度提高了!(%d/%d)",
}
def read_pairs(path: Path):
quoted = []
for number, line in enumerate(path.read_bytes().splitlines(), 1):
line = line.strip()
if not line:
continue
match = QUOTED.fullmatch(line)
if match is None:
raise ValueError(f"{path}:{number}: malformed locale entry")
quoted.append(match.group(1))
if len(quoted) % 2:
raise ValueError(f"{path}: unmatched locale key")
pairs = dict(zip(quoted[::2], quoted[1::2]))
if len(pairs) != len(quoted) // 2:
raise ValueError(f"{path}: duplicate locale key")
return pairs
def format_args(value: bytes):
return [token for token in FORMAT.findall(value) if token != b"%%"]
def build(english: Path, chinese: Path, output: Path):
reference = read_pairs(english)
existing = read_pairs(chinese)
missing = reference.keys() - existing.keys()
added_keys = {key.encode("cp949") for key in ADDED}
if missing != added_keys:
raise ValueError(f"40250 key coverage changed: missing={len(missing)}, translations={len(added_keys)}")
result = []
for key in list(reference) + [key for key in existing if key not in reference]:
source_key = key.decode("cp949")
if source_key in CORRECTED:
value = CORRECTED[source_key].encode("gbk")
elif key in existing:
value = existing[key]
else:
value = ADDED[source_key].encode("gbk")
if format_args(key) != format_args(value):
raise ValueError(f"format arguments differ for {source_key!r}")
if b'"' in value or b'\n' in value or b'\r' in value:
raise ValueError(f"unsafe locale value for {source_key!r}")
result.extend((b'"' + key + b'";', b'"' + value + b'";', b''))
payload = b"\n".join(result) + b"\n"
output.parent.mkdir(parents=True, exist_ok=True)
output.write_bytes(payload)
report = {
"reference_keys": len(reference),
"existing_chinese_keys": len(existing),
"new_chinese_keys": len(missing),
"corrected_format_keys": len(CORRECTED),
"legacy_extra_keys": len(existing.keys() - reference.keys()),
"output_sha256": hashlib.sha256(payload).hexdigest(),
}
output.with_suffix(".report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--english", type=Path, default=DEFAULT_ENGLISH)
parser.add_argument("--chinese", type=Path, default=DEFAULT_CHINESE)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
args = parser.parse_args()
print(json.dumps(build(args.english, args.chinese, args.output), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()