155 lines
6.5 KiB
Python
155 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Build a CP936 Chinese locale on top of the 40250 English client resources.
|
|
|
|
The Longju locale is an older client version. Only keyed UI text with compatible
|
|
format arguments is overlaid; 40250 proto, scripts, and UI layouts stay intact.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
DEFAULT_EN = REPO.parent / "40250/Server Client TMP4/Client/Eternexus/locale_en/locale/en"
|
|
DEFAULT_ZH = REPO.parent / "server_extracted/client_locale/longju/locale/taiwan"
|
|
DEFAULT_OUTPUT = REPO / "build/zh-locale/Client"
|
|
DEFAULT_PROTO_TOOL = REPO / "build-release/extension/src/port/locale_proto_patch"
|
|
DEFAULT_NAMES = REPO / "data/zh_40250"
|
|
FORMAT = re.compile(r"%(?:\([^)]*\))?[-+#0 ]*\d*(?:\.\d+)?[diouxXeEfFgGcrs%]")
|
|
KEYED_FILES = ("locale_game.txt", "locale_interface.txt")
|
|
|
|
|
|
def keyed_lines(path: Path, encoding: str):
|
|
lines = path.read_bytes().decode(encoding).splitlines()
|
|
values = {}
|
|
for line in lines:
|
|
parts = line.split("\t")
|
|
if len(parts) >= 2 and parts[0]:
|
|
values[parts[0]] = parts[1]
|
|
return lines, values
|
|
|
|
|
|
def convert_names(source: Path, destination: Path):
|
|
converted = 0
|
|
unrepresentable = []
|
|
rows = []
|
|
for line in source.read_text(encoding="utf-8").splitlines():
|
|
parts = line.split("\t", 1)
|
|
if len(parts) != 2 or not parts[0].isdigit():
|
|
continue
|
|
try:
|
|
rows.append(f"{parts[0]}\t{parts[1]}\n".encode("gbk"))
|
|
converted += 1
|
|
except UnicodeEncodeError:
|
|
unrepresentable.append(parts[0])
|
|
destination.write_bytes(b"".join(rows))
|
|
return {"converted": converted, "unrepresentable_vnums": unrepresentable}
|
|
|
|
|
|
def build(en: Path, zh: Path, output: Path, client_base: Path,
|
|
proto_tool: Path = None, names_dir: Path = None):
|
|
if not (en / "locale_game.txt").is_file() or not (zh / "locale_game.txt").is_file():
|
|
raise ValueError("English 40250 and Chinese Longju locale directories are required")
|
|
|
|
locale_dir = output / "locale/zh"
|
|
shutil.copytree(en, locale_dir, dirs_exist_ok=True)
|
|
report = {"english_source": str(en), "chinese_source": str(zh), "files": {}}
|
|
|
|
for filename in KEYED_FILES:
|
|
english_lines, english = keyed_lines(en / filename, "cp1252")
|
|
_, chinese = keyed_lines(zh / filename, "gbk")
|
|
translated = []
|
|
incompatible = []
|
|
merged = []
|
|
for line in english_lines:
|
|
parts = line.split("\t")
|
|
if len(parts) >= 2 and parts[0] in chinese:
|
|
key = parts[0]
|
|
candidate = chinese[key]
|
|
if FORMAT.findall(parts[1]) == FORMAT.findall(candidate):
|
|
parts[1] = candidate
|
|
translated.append(key)
|
|
else:
|
|
incompatible.append(key)
|
|
line = "\t".join(parts)
|
|
merged.append(line)
|
|
(locale_dir / filename).write_bytes(("\n".join(merged) + "\n").encode("gbk"))
|
|
report["files"][filename] = {
|
|
"reference_keys": len(english),
|
|
"translated_keys": len(translated),
|
|
"english_fallback_keys": sorted(english.keys() - set(translated)),
|
|
"incompatible_format_keys": sorted(incompatible),
|
|
"chinese_only_keys": sorted(chinese.keys() - english.keys()),
|
|
}
|
|
|
|
# These files are prose, not versioned game data. Keep the 40250 UI layouts
|
|
# and prototype binaries copied above; only replace their explanatory text.
|
|
for filename in (
|
|
"jobdesc_warrior.txt", "jobdesc_assassin.txt", "jobdesc_sura.txt",
|
|
"jobdesc_shaman.txt", "empiredesc_a.txt", "empiredesc_b.txt", "empiredesc_c.txt",
|
|
):
|
|
if (zh / filename).is_file():
|
|
shutil.copyfile(zh / filename, locale_dir / filename)
|
|
|
|
if proto_tool is not None:
|
|
if not proto_tool.is_file() or names_dir is None:
|
|
raise ValueError("build locale_proto_patch and provide the Chinese name tables")
|
|
item_names = output.parent / "item_names_gbk.txt"
|
|
mob_names = output.parent / "mob_names_gbk.txt"
|
|
item_conversion = convert_names(names_dir / "item_names_zh.txt", item_names)
|
|
mob_conversion = convert_names(names_dir / "mob_names_zh.txt", mob_names)
|
|
result = subprocess.run(
|
|
[str(proto_tool), str(en / "item_proto"), str(en / "mob_proto"),
|
|
str(item_names), str(mob_names), str(locale_dir / "item_proto"),
|
|
str(locale_dir / "mob_proto")],
|
|
check=True, capture_output=True, text=True,
|
|
)
|
|
report["proto"] = {
|
|
"item_name_source": item_conversion,
|
|
"mob_name_source": mob_conversion,
|
|
"patch_result": result.stdout.strip().splitlines(),
|
|
}
|
|
|
|
(output / "locale.cfg").write_text("10002 936 zh\n", encoding="ascii")
|
|
# A local test checkout can share the original 40250 packs without copying
|
|
# gigabytes of assets. Distribution builds must package these separately.
|
|
for name in ("pack", "sound", "BGM", "miles", "lib", "channel.inf"):
|
|
source = client_base / name
|
|
target = output / name
|
|
if not source.exists():
|
|
continue
|
|
if target.is_symlink():
|
|
if target.resolve() == source.resolve():
|
|
continue
|
|
target.unlink()
|
|
elif target.exists():
|
|
raise ValueError(f"refusing to replace existing client resource: {target}")
|
|
target.symlink_to(source.resolve())
|
|
report_path = output.parent / "zh-locale-report.json"
|
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
return report_path
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--english", type=Path, default=DEFAULT_EN)
|
|
parser.add_argument("--chinese", type=Path, default=DEFAULT_ZH)
|
|
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
|
parser.add_argument("--client-base", type=Path, default=REPO / "Client")
|
|
parser.add_argument("--proto-tool", type=Path, default=DEFAULT_PROTO_TOOL)
|
|
parser.add_argument("--name-tables", type=Path, default=DEFAULT_NAMES)
|
|
args = parser.parse_args()
|
|
report_path = build(args.english, args.chinese, args.output, args.client_base,
|
|
args.proto_tool, args.name_tables)
|
|
print(f"Built {args.output / 'locale/zh'}")
|
|
print(f"Report: {report_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|