zh locale: pack locale_zh and add reviewed Chinese names

eterpack_pack writes locale packs in the original format (COMPRESS
entries, LZO+TEA index), and build_zh_locale.py packs locale/zh into
locale_zh.eix/.epk registered in pack/Index instead of leaving loose
files.

The reviewed translations for names and keys still in English are
kept apart from the server snapshot in data/zh_40250/*_zh_overlay.txt
(1,607 items, 231 mobs/NPCs, 45 locale_game keys) and override it at
build time. Only YMIR, the coordinate-named warp markers and the empty
TOOLTIP_UNSEAL_LEFT_TIME remain English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-28 18:45:58 +09:00
co-authored by Claude Opus 5.5
parent fd051cc3ed
commit a1ae69aa7a
9 changed files with 2180 additions and 45 deletions
+3
View File
@@ -11,5 +11,8 @@
| `mob_names_zh.txt` | 1,334 | `fdbfe4b3f3367d5842c0e9bbe0b5e5ea2dbe99fb6e566ef5789f65d3794eab5c` |
线上字段里的 1,608 个物品名及 346 个怪物/NPC 名不含汉字,本快照原样保留。
它们的审核译文(2026-09-28 确认)单独放在 `item_names_zh_overlay.txt`、
`mob_names_zh_overlay.txt`,界面回退键译文在 `locale_game_zh_overlay.txt`;
`script/build_zh_locale.py` 构建时以 overlay 覆盖快照同 VNUM / 同键的值。
旧的龙驹中文表与线上表有 21 个相同 VNUM 不同名;不把旧表覆盖到服务端。
服务端名称若再更新,需重新只读导出并核对差异后更新本快照。
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
KEY VALUE
CANNOT_USE 无法使用。
CHANNEL_NOTIFY_FULL 该频道已满!请选择其他频道。
DRAGON_SOUL_CANNOT_REFINE 该龙魂石无法强化。
DRAGON_SOUL_CANNOT_REFINE_MORE 该龙魂石已达到最高等级。
DRAGON_SOUL_EQUIP_WARNING1 龙魂石一旦装备便很难取下。
DRAGON_SOUL_EQUIP_WARNING2 确定要继续吗?
DRAGON_SOUL_EXPIRED 已过期的龙魂石
DRAGON_SOUL_INVALID_DRAGON_SOUL 龙魂石不正确。
DRAGON_SOUL_IS_NOT_DRAGON_SOUL 这不是龙魂石。
DRAGON_SOUL_NOT_DRAGON_SOUL_REFINE_STONE 这不是正确的龙魂石。
DRAGON_SOUL_NOT_ENOUGH_MATERIAL 强化材料不足。
DRAGON_SOUL_NOT_MATCHED_SLOT 该龙魂石不适合这个栏位。
DRAGON_SOUL_STEP_LEVEL1 阶段:最低级,
DRAGON_SOUL_STEP_LEVEL2 阶段:低级,
DRAGON_SOUL_STEP_LEVEL3 阶段:中级,
DRAGON_SOUL_STEP_LEVEL4 阶段:高级,
DRAGON_SOUL_STEP_LEVEL5 阶段:最高级,
DRAGON_SOUL_STRENGTH 强化:+%d
DRAGON_SOUL_UNEQUIP_WARNING1 取下时龙魂石可能会损坏。
DRAGON_SOUL_UNEQUIP_WARNING2 确定要继续吗?
DRAGON_SOUL_UNMATCHED_SLOT 栏位不正确。
FISHING_NOTIFY1 好像钓到了%s。
FISHING_NOTIFY2 %s好像上钩了。
FISHING_SUCCESS1 钓到了%s!
FISHING_SUCCESS2 把%s从水里拉上来了!
GUILD_MARK_NOT_ENOUGH_LEVEL 帮会等级需达到3级
HOW_MANY_ITEM_DO_YOU_DROP2 确定要扔掉%s %d个吗?
LOGIN_FAILURE_BLOCK_LOGIN 请选择其他服务器!
LOGIN_FAILURE_UNKNOWN 因未知原因无法登录。
REFINE_FAILURE_CAN_NOT_ATTACH %s无法镶嵌到该物品上。
REFINE_FAILURE_NO_GOLD_SOCKET 没有可镶嵌%s的金色插槽。
REFINE_FAILURE_NO_SOCKET0 没有可镶嵌%s的插槽。
TOOLTIP_ATT_SPEED 攻击速度 : +%d%%
TOOLTIP_COSTUME_ATTR_BONUS 时装属性 %d%%
TOOLTIP_DRAGON_SOUL_DECK1 地
TOOLTIP_DRAGON_SOUL_DECK2 天
TOOLTIP_HP_REGEN 生命恢复量 : +%d%%
TOOLTIP_MOV_SPEED 移动速度 : %d%%
TOOLTIP_RESIST_DARK 黑暗抗性 +%d%%
TOOLTIP_RESIST_EARTH 大地抗性 +%d%%
TOOLTIP_RESIST_ICE 冰冻抗性 +%d%%
TOOLTIP_SKILL_DURATION 持续时间 : %d秒
TOOLTIP_SP_REGEN 精力恢复量 : +%d%%
TOOLTIP_TIME_CHARGER_FIX 时间充能 %d秒
TOOLTIP_TIME_CHARGER_PER 时间充能 %d%%
+232
View File
@@ -0,0 +1,232 @@
VNUM LOCALE_NAME
101 幼狼
1096 亡灵战神
1308 真狼神守护者
1309 真狼神守护者
1310 狼神守护者
1904 真三尾银狐
1905 真三尾银狐
1906 真三尾银狐
6001 火精
6002 炎精
6003 极热精
6004 血火精
6005 火魔
6006 炎魔
6007 火怪
6008 炎怪
6009 超热怪
6051 真超热怪
6091 夜摩天
6101 冰精
6102 冷精
6103 极冰精
6104 寒冰精
6105 冰傀魔
6106 冷魔
6107 冰怪
6108 冷怪
6109 超冷怪
6151 真超冷怪
6191 寒魔
7029 冰魔
7030 冰魔鲸
7031 冰异虫
7032 雪地兽
7033 冰魔人
7034 雪魔兽人
7035 冰雕魔
7036 冰魔
7037 冰魔鲸
7038 冰异虫
7039 雪地兽
7040 冰魔人
7041 雪魔兽人
7042 冰雕魔
7043 冰魔
7044 冰魔鲸
7045 冰异虫
7046 雪地兽
7047 冰魔人
7048 雪魔兽人
7049 冰雕魔
7050 破坏者
7051 蝎人王
7052 蝎人
7053 蝎人弓箭手
7054 半兽刀兵
7055 半兽弓箭手
7056 裁决者
7057 破坏者
7058 蝎人王
7059 蝎人
7060 蝎人弓箭手
7061 半兽刀兵
7062 半兽弓箭手
7063 裁决者
7064 破坏者
7065 蝎人王
7066 蝎人
7067 蝎人弓箭手
7068 半兽刀兵
7069 半兽弓箭手
7070 裁决者
7071 火云虎
7072 火灵
7073 焰赤虎
7074 夜魔
7075 赤皇武夫
7076 火云虎
7077 火灵
7078 焰赤虎
7079 夜魔
7080 赤皇武夫
7081 火云虎
7082 火灵
7083 焰赤虎
7084 夜魔
7085 赤皇武夫
7086 天刹鬼木
7087 青叶妖姬
7088 狂暴树鬼
7089 邪魔树鬼
7090 天刹鬼木
7091 青叶妖姬
7092 狂暴树鬼
7093 邪魔树鬼
7094 天刹鬼木
7095 青叶妖姬
7096 狂暴树鬼
8041 复活节陨石
8042 复活节陨石
8043 复活节陨石
8044 复活节陨石
8045 复活节陨石
8046 复活节陨石
8047 复活节陨石
8048 复活节陨石
8049 复活节陨石
8050 复活节陨石
8051 魂魔石
8052 鬼魔石
8053 暴魔石
8054 龙魔石
8055 地魔石
8056 亡魔石
8057 炼狱火魔石
8101 鱼跃陨石
8102 蝴仙陨石
8103 鹿鸣陨石
8104 鹰神陨石
8105 盘蛇陨石
8106 虎啸陨石
8107 狮吼陨石
8108 兽目陨石
8109 凤凰陨石
8110 麒麟陨石
8111 蛟龙陨石
8600 真鬼族守护神
8601 真鬼族守护神
8602 真秘宗教主
8603 真秘宗教主
8604 真亡灵战神
8605 真亡灵战神
8606 真死神
8607 真死神
8608 真三尾银狐
8609 真三尾银狐
8610 真狼神守护者
8611 真狼神守护者
8612 真圣母蛛
8613 真圣母蛛
8614 真龟龙王
8615 真龟龙王
20035 战旗
20036 战旗
20037 战旗
20110 野猪
20201 野猪坐骑
20202 狼坐骑
20203 虎坐骑
20204 狮子坐骑
20205 战甲野猪
20206 战甲狼
20207 战甲虎
20208 战甲狮子
20209 战甲野猪
20210 战甲狼
20211 战甲虎
20212 战甲狮子
20213 公驯鹿
20214 战甲公驯鹿
20215 战甲公驯鹿
20216 母驯鹿
20217 战甲母驯鹿
20218 战甲母驯鹿
20219 万圣黑马
20367 墓穴守卫
20368 铁算盘
20369 李忠
20370 中毒的士兵
20371 古代石碑
20372 残旧石碑
20373 护卫
20374 卫兵
20375 木栅
20376 蔡基雄
20377 药还
20378 警备队长岳穆尔
20379 受伤的士兵
20380 学者
20381 村长
20382 石花
20383 工匠莫雷汉
20384 圣诞树
20385 赤龙城碑
20386 赤龙封印碑
20387 命运之锁
20388 赤龙结界门
20389 夜摩天石
20390 赤龙封印石
20391 太阳草
20392 阵亡的士兵
20393 负伤的士兵
20394 火焰之地卫兵
20395 冰原守卫
30101 库德石像
30102 玄武岩方尖碑
30103 龟岩
30104 符文桩
30111 第一道地狱之门
30112 第二道地狱之门
30113 第三道地狱之门
30114 第四道地狱之门
30115 第五道地狱之门
30116 第六道地狱之门
30117 第七道地狱之门
30118 第八道地狱之门
30119 第九道地狱之门
30120 药草袋
30121 修罗之魂
30122 猛将之魂
30123 暗黑祭坛
30124 修罗骸骨1
30125 修罗骸骨2
30126 修罗骸骨3
30127 修罗骸骨4
30128 修罗骸骨5
30129 复活节兔子
30130 墓碑
30301 龙牙矿脉
30302 红玉矿脉
30303 碧玉矿脉
30304 翡翠矿脉
30305 青玉矿脉
33001 修罗骸骨
33007 饥民
33008 南瓜杰克
33009 龙牙炼圣
33010 红玉炼圣
33011 碧玉炼圣
33012 翡翠炼圣
33013 青玉炼圣
+45 -20
View File
@@ -1,22 +1,42 @@
# 40250 简体中文适配(进行中)
# 40250 简体中文适配
## 当前可运行原型
先运行 `cmake --build build-release --target locale_proto_patch`,再运行
`python3 script/build_zh_locale.py`,在 `build/zh-locale/Client` 生成独立的测试客户端目录。
它复制 40250 `Eternexus/locale_en/locale/en` 的文件结构,配置 `locale.cfg` 为
`10002 936 zh`,并将中文内容按键合入 `locale_game.txt`、`locale_interface.txt`。
旧龙驹语言包中的占位符与 40250 不一致时保留英文原文;报告在
`build/zh-locale/zh-locale-report.json`。职业与帝国说明采用龙驹中文文本。
生成目录通过符号链接共用仓库 `Client` 的 pack 等资源,仅用于本机验证;
不会修改原始 40250 客户端,也不是可分发包。可用以下命令启动:
## 语言包(2026-09-28 起为正式 pack)
```sh
build-release/native_render/mt_native_render \
cmake --build build-native --target locale_proto_patch eterpack_pack
python3 script/build_zh_locale.py
```
生成器复制 40250 `Eternexus/locale_en/locale/en` 的文件结构到暂存目录
`build/zh-locale/locale-src/zh`,将中文内容按键合入 `locale_game.txt`、`locale_interface.txt`。
旧龙驹语言包缺失或占位符与 40250 不一致的键,改用 `data/zh_40250/locale_game_zh_overlay.txt`
中经审核的译文(同样校验占位符);报告在
`build/zh-locale/zh-locale-report.json`。职业与帝国说明采用龙驹中文文本。
暂存目录随后由 `eterpack_pack`(`extension/tests/eterpack_pack.cpp`,直接调用移植的 40250
`CEterPack::Put` / `EncryptIndexFile`)打成 `locale_zh.eix/.epk`,格式与原版 `locale_en` 等
相同:231 个文件全部为 COMPRESS(LZO),索引经 LZO + TEA 加密。工具写完后以只读方式
重新打开 pack,逐个经 `CEterPack::Get` 读回并与源文件比对。用同一工具重打原版
`locale_en` 源文件,文件名、类型、CRC 和解压内容与原版 pack 逐项一致。
输出客户端 `build/zh-locale/Client`:
- `pack/`:原版 pack 逐个符号链接,加上 `locale_zh.eix/.epk`,以及在 `locale/en/` 之前
注册 `locale/zh/` → `locale_zh` 的 `Index`(CRLF,与原版一致)。
- `locale.cfg` 与 `locale_zh.cfg`:`10002 936 zh`。
- 不留散装的 `locale/zh`:客户端默认先查散装文件,留着会绕过 pack。
仓库 `Client/` 是 `audit/assets/40250-client.json` 校验的原版资源,不做修改。
macOS 启动:
```sh
build-native/native_render/mt_native_render \
--live-client build/zh-locale/Client --login-screen --width 1024 --height 768
```
Android:`./push-android-client.sh --locale zh`(默认推送 `build/zh-locale/Client`,符号链接按
目标文件复制,并以 `locale_zh.cfg` 写入设备上的 `locale.cfg`)。
物品与怪物原型从 40250 原版解压,按线上 40250 服务端 `gb2312name` 的 VNUM 快照
只替换 `szLocaleName` 字段后按原格式
重新压缩加密;生成器验证记录数、结构大小、除名称外所有字节不变,以及写出后的
@@ -25,11 +45,15 @@ build-release/native_render/mt_native_render \
## 已核对的差异
- 40250 `locale_game.txt` 有 817 个键;当前 771 个安全替换为中文,46 个保留英文。
其中 12 个在龙驹文件中的格式占位符不一致,需逐条重译或验证。
- 40250 `locale_game.txt` 有 817 个键;771 个来自龙驹,45 个来自审核译文,
仅 `TOOLTIP_UNSEAL_LEFT_TIME` 保留英文原文(原文为空,40250 脚本未使用)。
- 40250 `locale_interface.txt` 的 326 个键均找到中文对应项。
- 40250 客户端物品原型 5,743 条、怪物/NPC 原型 1,334 条都已按 VNUM 写入
`data/zh_40250/` 中的服务端名称快照;其中分别有 1,608 和 346 个名称仍不含汉字。
`data/zh_40250/` 中的服务端名称快照;快照里分别有 1,608 和 346 个名称不含汉字。
2026-09-28 审核确认的译文放在 `item_names_zh_overlay.txt`(1,607 条)和
`mob_names_zh_overlay.txt`(231 条),构建时覆盖快照同 VNUM 的名称;快照本身保持服务端原样。
剩余英文仅为 vnum 2 占位物品 `YMIR` 和 115 个名称为地图坐标的传送点标记(游戏内不显示)。
客户端显示译名,服务端数据库仍为英文,服务端发出的含名称消息会显示英文。
- 40250 服务端(`192.168.21.203`,2026-09-27 只读 SSH 核对)当前数据库配置为
`LOCALE=newcibn`、`DB_NAME_COLUMN=gb2312name`,有 `newcibn` 语言目录。
因此客户端采用 CP936;服务端广播字典已在 2026-09-28 完成首轮补齐,名称与任务文本仍需核对。
@@ -40,12 +64,13 @@ build-release/native_render/mt_native_render \
## 后续验收
1. 根据线上 40250 VNUM 及用途逐条翻译仍为英文的 1,608 个物品、346 个怪物/NPC;
同步更新服务端名称和客户端快照。`itemdesc`、`skilldesc` 也需按 40250 键和语义逐项合并。
2. 补齐 46 个英文回退键,复核 12 个格式不兼容键。
1. 物品/怪物译名和界面回退键已在客户端补齐(overlay);是否同步写入服务端
`gb2312name` 需另行核对后决定。`itemdesc`、`skilldesc` 仍需按 40250 键和语义逐项合并。
2. (已完成)46 个英文回退键已补齐。
3. 在游戏窗口验证中文字符、字体、输入法、换行和 UI 宽度,再联机检查登录、背包、
商店、任务、公告及聊天。
4. 完成后制作注册到 `pack/Index` 的中文 pack,并在 macOS 和 Android 上分别测试。
4. 中文 pack 已注册到 `pack/Index`(见上);macOS 登录界面已确认从 pack 读取中文,
Android 真机/模拟器待测。
## 服务端适配进度(2026-09-28)
+3
View File
@@ -238,6 +238,9 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
# Offline locale data builder: retains 40250 proto layout and gameplay fields.
add_executable(locale_proto_patch ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/locale_proto_patch.cpp)
target_link_libraries(locale_proto_patch PRIVATE port_platform)
# Locale packs in the original format (script/build_zh_locale.py packs locale/zh with it).
add_executable(eterpack_pack ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/eterpack_pack.cpp)
target_link_libraries(eterpack_pack PRIVATE port_platform)
# 2V2-e: the same fake server as a process, for the Godot-driven render run
# (script/python_game_render_test.sh).
add_executable(port_fake_login_server ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/port_fake_login_server_main.cpp
+105
View File
@@ -0,0 +1,105 @@
// eterpack_pack — build a 40250 pack (<db>.eix/.epk) from a directory with the ported CEterPack, the way
// the original locale packs are made: every file COMPRESS (LZO), index LZO + TEA encrypted.
//
// eterpack_pack <db path without extension> <source dir> <pack prefix, e.g. locale/zh/>
//
// Pack names are <prefix><path relative to the source dir>, lowercased with '/' (inlineConvertPackFilename).
// An existing <db>.eix/.epk is replaced. The result is reopened read-only and every file read back
// through CEterPack::Get and compared with its source.
#include "EterPack/StdAfx.h"
#include "EterPack/EterPack.h"
#include "EterPack/EterPackManager.h"
#include "EterBase/MappedFile.h"
#include "../src/platform/UserInterface/UserInterface.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
static std::vector<char> read_file(const fs::path& path)
{
std::ifstream in(path, std::ios::binary);
return {std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
}
int main(int argc, char** argv)
{
if (argc != 4)
{
std::fprintf(stderr, "usage: %s <db> <source dir> <pack prefix>\n", argv[0]);
return 2;
}
const std::string db = argv[1];
const fs::path source = argv[2];
std::string prefix = argv[3];
if (!prefix.empty() && prefix.back() != '/')
prefix += '/';
std::vector<std::pair<std::string, fs::path>> files;
for (const auto& e : fs::recursive_directory_iterator(source))
if (e.is_regular_file())
files.emplace_back(prefix + e.path().lexically_relative(source).generic_string(), e.path());
std::sort(files.begin(), files.end());
if (files.empty())
{
std::fprintf(stderr, "no files under %s\n", source.c_str());
return 1;
}
PackSingletons();
std::error_code ec;
fs::remove(db + ".eix", ec);
fs::remove(db + ".epk", ec);
{
CEterFileDict dict;
CEterPack pack;
if (!pack.Create(dict, db.c_str(), "", false))
{
std::fprintf(stderr, "cannot create %s.eix/.epk\n", db.c_str());
return 1;
}
for (const auto& [name, path] : files)
{
const std::vector<char> data = read_file(path);
if (name.size() > FILENAME_MAX_LEN || !pack.Put(name.c_str(), data.data(), long(data.size()), COMPRESSED_TYPE_COMPRESS))
{
std::fprintf(stderr, "cannot put %s\n", name.c_str());
return 1;
}
}
if (!pack.EncryptIndexFile())
return 1;
}
// Read back as the client does.
CEterFileDict dict;
CEterPack pack;
if (!pack.Create(dict, db.c_str(), prefix.c_str(), true))
{
std::fprintf(stderr, "cannot reopen %s\n", db.c_str());
return 1;
}
int failures = 0;
for (const auto& [name, path] : files)
{
const std::vector<char> want = read_file(path);
CMappedFile mapped;
LPCVOID data = nullptr;
if (!pack.Get(mapped, name.c_str(), &data) || mapped.Size() != want.size() ||
(!want.empty() && std::memcmp(data, want.data(), want.size()) != 0))
{
std::fprintf(stderr, "read-back mismatch: %s\n", name.c_str());
++failures;
}
}
if (failures)
return 1;
std::printf("%s: %zu files, COMPRESS, encrypted index\n", db.c_str(), files.size());
return 0;
}
+97 -21
View File
@@ -3,6 +3,12 @@
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.
The files are staged in build/zh-locale/locale-src/zh and packed like the original
locale packs (eterpack_pack: COMPRESS entries, encrypted index) into locale_zh.eix/.epk.
The output Client has a pack/ of its own: links to the original packs, the zh pack and
an Index registering locale/zh/ -> locale_zh. No loose locale/zh is left next to it,
since the client searches loose files first.
"""
import argparse
@@ -17,7 +23,8 @@ 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"
BUILD_DIRS = ("build-release", "build-native")
LOCALE_CFG = "10002 936 zh\n"
DEFAULT_NAMES = REPO / "data/zh_40250"
FORMAT = re.compile(r"%(?:\([^)]*\))?[-+#0 ]*\d*(?:\.\d+)?[diouxXeEfFgGcrs%]")
KEYED_FILES = ("locale_game.txt", "locale_interface.txt")
@@ -33,35 +40,99 @@ def keyed_lines(path: Path, encoding: str):
return lines, values
def convert_names(source: Path, destination: Path):
def name_rows(path: Path):
rows = {}
if path.is_file():
for line in path.read_text(encoding="utf-8").splitlines():
parts = line.split("\t", 1)
if len(parts) == 2 and parts[0].isdigit():
rows[parts[0]] = parts[1]
return rows
def convert_names(source: Path, destination: Path, overlay: Path = None):
"""The server name snapshot, with the reviewed translations of its English names laid over it."""
names = name_rows(source)
overlaid = {vnum: name for vnum, name in name_rows(overlay).items() if vnum in names} if overlay else {}
names.update(overlaid)
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
for vnum, name in names.items():
try:
rows.append(f"{parts[0]}\t{parts[1]}\n".encode("gbk"))
rows.append(f"{vnum}\t{name}\n".encode("gbk"))
converted += 1
except UnicodeEncodeError:
unrepresentable.append(parts[0])
unrepresentable.append(vnum)
destination.write_bytes(b"".join(rows))
return {"converted": converted, "unrepresentable_vnums": unrepresentable}
return {"converted": converted, "overlaid": len(overlaid), "unrepresentable_vnums": unrepresentable}
def tool(name: str) -> Path:
for build_dir in BUILD_DIRS:
path = REPO / build_dir / "extension/src/port" / name
if path.is_file():
return path
return REPO / BUILD_DIRS[0] / "extension/src/port" / name
def register(index: bytes) -> bytes:
"""pack/Index with locale/zh/ -> locale_zh added before the locale/en/ pair (CRLF, as the original)."""
lines = index.split(b"\r\n")
if b"locale/zh/" in lines:
return index
at = lines.index(b"locale/en/")
if at % 2 != 1:
raise ValueError("pack/Index: locale/en/ is not a folder line")
lines[at:at] = [b"locale/zh/", b"locale_zh"]
return b"\r\n".join(lines)
def make_pack_dir(client_base: Path, pack_dir: Path, locale_dir: Path, pack_tool: Path):
source = client_base / "pack"
if pack_dir.is_symlink():
pack_dir.unlink() # earlier builds linked the whole directory
pack_dir.mkdir(parents=True, exist_ok=True)
for entry in source.iterdir():
if entry.name == "Index" or entry.name.lower().startswith("locale_zh."):
continue
target = pack_dir / entry.name
if target.is_symlink() or target.exists():
target.unlink()
target.symlink_to(entry.resolve())
(pack_dir / "Index").write_bytes(register((source / "Index").read_bytes()))
result = subprocess.run([str(pack_tool), str(pack_dir / "locale_zh"), str(locale_dir), "locale/zh/"],
check=True, capture_output=True, text=True)
return result.stdout.strip()
def build(en: Path, zh: Path, output: Path, client_base: Path,
proto_tool: Path = None, names_dir: Path = None):
proto_tool: Path = None, names_dir: Path = None, pack_tool: Path = None):
"""names_dir holds the server name snapshot and the reviewed *_zh_overlay.txt translations."""
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")
if pack_tool is None or not pack_tool.is_file():
raise ValueError("build eterpack_pack (cmake --build <build dir> --target eterpack_pack)")
locale_dir = output / "locale/zh"
shutil.copytree(en, locale_dir, dirs_exist_ok=True)
locale_dir = output.parent / "locale-src/zh"
if locale_dir.exists():
shutil.rmtree(locale_dir)
shutil.copytree(en, locale_dir)
loose = output / "locale/zh"
if loose.exists():
shutil.rmtree(loose) # earlier builds ran the locale from loose files
if not any((output / "locale").iterdir()):
(output / "locale").rmdir()
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")
# Reviewed translations of the keys Longju lacks or formats differently; they win over Longju.
overlay = names_dir / (Path(filename).stem + "_zh_overlay.txt") if names_dir else None
if overlay and overlay.is_file():
_, reviewed = keyed_lines(overlay, "utf-8")
chinese.update(reviewed)
translated = []
incompatible = []
merged = []
@@ -100,8 +171,10 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
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)
item_conversion = convert_names(names_dir / "item_names_zh.txt", item_names,
names_dir / "item_names_zh_overlay.txt")
mob_conversion = convert_names(names_dir / "mob_names_zh.txt", mob_names,
names_dir / "mob_names_zh_overlay.txt")
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"),
@@ -114,10 +187,12 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
"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"):
report["pack"] = make_pack_dir(client_base, output / "pack", locale_dir, pack_tool)
(output / "locale.cfg").write_text(LOCALE_CFG, encoding="ascii")
(output / "locale_zh.cfg").write_text(LOCALE_CFG, encoding="ascii")
# A local test checkout shares the original 40250 assets through symbolic links
# (pack/ holds one per original pack); push-android-client.sh copies their targets.
for name in ("sound", "BGM", "miles", "lib", "channel.inf"):
source = client_base / name
target = output / name
if not source.exists():
@@ -141,12 +216,13 @@ def main():
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("--proto-tool", type=Path, default=tool("locale_proto_patch"))
parser.add_argument("--pack-tool", type=Path, default=tool("eterpack_pack"))
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'}")
args.proto_tool, args.name_tables, args.pack_tool)
print(f"Built {args.output / 'pack/locale_zh.eix'} (.epk), registered in pack/Index")
print(f"Report: {report_path}")
+41 -4
View File
@@ -27,18 +27,55 @@ class LocaleBuildTest(unittest.TestCase):
(en / "item_proto").write_bytes(b"40250-proto")
(zh / "item_proto").write_bytes(b"old-proto")
(client / "pack").mkdir()
(client / "pack/Index").write_bytes(b"PACK\r\nlocale/de/\r\nlocale_de\r\nlocale/en/\r\nlocale_en\r\n")
(client / "pack/locale_en.eix").write_bytes(b"en")
# Stands in for eterpack_pack: records its arguments as the .eix.
pack_tool = root / "eterpack_pack"
pack_tool.write_text("#!/bin/sh\necho \"$2 $3\" > \"$1.eix\"\n")
pack_tool.chmod(0o755)
report_path = MODULE.build(en, zh, output, client)
merged = (output / "locale/zh/locale_game.txt").read_bytes().decode("gbk")
report_path = MODULE.build(en, zh, output, client, pack_tool=pack_tool)
staged = output.parent / "locale-src/zh"
merged = (staged / "locale_game.txt").read_bytes().decode("gbk")
self.assertIn("GOOD\t中文 %d\tSA", merged)
self.assertIn("BAD\t%s at %d\tSA", merged)
self.assertIn("ONLY_EN\tFallback", merged)
self.assertEqual((output / "locale/zh/item_proto").read_bytes(), b"40250-proto")
self.assertEqual((staged / "item_proto").read_bytes(), b"40250-proto")
self.assertEqual((output / "locale.cfg").read_text(), "10002 936 zh\n")
self.assertTrue((output / "pack").is_symlink())
self.assertEqual((output / "locale_zh.cfg").read_text(), "10002 936 zh\n")
# A pack/ of its own: original packs linked, locale_zh packed from the staging dir and registered.
self.assertFalse((output / "pack").is_symlink())
self.assertFalse((output / "locale/zh").exists())
self.assertTrue((output / "pack/locale_en.eix").is_symlink())
self.assertEqual((output / "pack/locale_zh.eix").read_text(), f"{staged} locale/zh/\n")
self.assertEqual((output / "pack/Index").read_bytes(),
b"PACK\r\nlocale/de/\r\nlocale_de\r\nlocale/zh/\r\nlocale_zh\r\n"
b"locale/en/\r\nlocale_en\r\n")
self.assertEqual((client / "pack/Index").read_bytes()[:4], b"PACK")
self.assertFalse((client / "pack/locale_zh.eix").exists())
report = json.loads(report_path.read_text())
self.assertEqual(report["files"]["locale_game.txt"]["incompatible_format_keys"], ["BAD"])
# Reviewed overlay: fills an English-only key and replaces a format-incompatible Longju value.
names = root / "names"
names.mkdir()
(names / "locale_game_zh_overlay.txt").write_text("KEY\tVALUE\nBAD\t%s 于 %d\nONLY_EN\t备用\n")
report_path = MODULE.build(en, zh, output, client, names_dir=names, pack_tool=pack_tool)
merged = (staged / "locale_game.txt").read_bytes().decode("gbk")
self.assertIn("BAD\t%s 于 %d\tSA", merged)
self.assertIn("ONLY_EN\t备用", merged)
game = json.loads(report_path.read_text())["files"]["locale_game.txt"]
self.assertEqual((game["english_fallback_keys"], game["incompatible_format_keys"]), ([], []))
def test_name_overlay_replaces_snapshot_rows(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "names.txt").write_text("VNUM\tLOCALE_NAME\n1\tYang\n2\t剑\n")
(root / "overlay.txt").write_text("VNUM\tLOCALE_NAME\n1\t金币\n99\t不存在\n")
result = MODULE.convert_names(root / "names.txt", root / "out.txt", root / "overlay.txt")
self.assertEqual((root / "out.txt").read_bytes().decode("gbk"), "1\t金币\n2\t剑\n")
self.assertEqual(result["overlaid"], 1)
if __name__ == "__main__":
unittest.main()