#!/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()