134 lines
6.9 KiB
Python
134 lines
6.9 KiB
Python
#!/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()
|