feat(locale): configure Chinese (zh) localization for client and align with 40250 server

This commit is contained in:
shen
2026-09-19 18:47:00 -07:00
parent f22c0f8e31
commit 4a2b953a4f
315 changed files with 14997 additions and 17 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ func _init() -> void:
_mock = "--mock" in OS.get_cmdline_user_args() or "--mock" in OS.get_cmdline_args()
_loc = Locale.new()
_loc.setup(_assets, "en")
_loc.setup(_assets, Locale.get_default_lang())
_audio = Audio.new()
get_root().add_child(_audio)
+10 -4
View File
@@ -458,7 +458,11 @@ func setup(m2client: Node, assets_root: String,
if ClassDB.class_exists("Metin2Proto") and assets_root != "":
proto = ClassDB.instantiate("Metin2Proto")
add_child(proto)
var ip := assets_root.path_join("locale/locale/en/item_proto")
var LocaleScript = load("res://locale.gd")
var cur_lang: String = LocaleScript.get_default_lang() if LocaleScript and LocaleScript.has_method("get_default_lang") else "zh"
var ip := assets_root.path_join("locale/locale/%s/item_proto" % cur_lang)
if not FileAccess.file_exists(ip):
ip = assets_root.path_join("locale/locale/en/item_proto")
if FileAccess.file_exists(ip):
proto.call("load_item_proto", ip)
if quickbar and "proto" in quickbar:
@@ -468,7 +472,9 @@ func setup(m2client: Node, assets_root: String,
chat.set_proto(proto)
if quest_dialog and quest_dialog.has_method("set_proto"):
quest_dialog.set_proto(proto)
var mp := assets_root.path_join("locale/locale/en/mob_proto")
var mp := assets_root.path_join("locale/locale/%s/mob_proto" % cur_lang)
if not FileAccess.file_exists(mp):
mp = assets_root.path_join("locale/locale/en/mob_proto")
if FileAccess.file_exists(mp):
proto.call("load_mob_proto", mp)
await _yield()
@@ -480,7 +486,7 @@ func setup(m2client: Node, assets_root: String,
if net_world and "title_name_resolver" in net_world and assets_root != "":
# §8.8 称号名:introloading.__RegisterTitleName -> localeInfo.PVP_LEVEL<grade>
var _tt_loc: RefCounted = load("res://locale.gd").new()
_tt_loc.setup(assets_root, "en")
_tt_loc.setup(assets_root, cur_lang)
net_world.title_name_resolver = func(g: int) -> String:
var k := "PVP_LEVEL%d" % g
return _tt_loc.t(k) if _tt_loc.has(k) else ""
@@ -515,7 +521,7 @@ func setup(m2client: Node, assets_root: String,
var _possessive := ""
if assets_root != "":
var _poss_loc: RefCounted = load("res://locale.gd").new()
_poss_loc.setup(assets_root, "en")
_poss_loc.setup(assets_root, cur_lang)
if _poss_loc.has("POSSESSIVE_MORPHENE"):
_possessive = _poss_loc.t("POSSESSIVE_MORPHENE")
ground_items.setup(client, _mount, func() -> Node: return player, proto, item_list,
+54 -2
View File
@@ -3,23 +3,75 @@
# 用 preload("res://locale.gd").new()setup() 后 t("KEY", [args]).
extends RefCounted
var _lang := "en"
var _lang := "zh"
var _tab := {} # KEY -> value
var _item_names := {} # int vnum -> String name
var _mob_names := {} # int vnum -> String name
var _base := ""
func setup(assets_root: String, lang := "en") -> void:
static func get_default_lang() -> String:
var env_lang := OS.get_environment("MT_LOCALE").strip_edges().to_lower()
if not env_lang.is_empty():
return env_lang
return "zh"
func setup(assets_root: String, lang := "") -> void:
_base = assets_root.path_join("locale/locale")
if lang.is_empty():
lang = get_default_lang()
set_lang(lang)
func set_lang(lang: String) -> void:
_lang = lang
_tab.clear()
_item_names.clear()
_mob_names.clear()
var dir := _base.path_join(lang)
if not DirAccess.dir_exists_absolute(dir):
push_warning("[locale] no such lang: " + dir)
return
for f in ["locale_interface.txt", "locale_game.txt", "itemdesc.txt", "skilldesc.txt"]:
_load_table(dir.path_join(f))
_load_name_table(dir.path_join("item_names.txt"), _item_names)
_load_name_table(dir.path_join("mob_names.txt"), _mob_names)
func _load_name_table(path: String, target_dict: Dictionary) -> void:
if not FileAccess.file_exists(path):
return
var fa := FileAccess.open(path, FileAccess.READ)
if fa == null:
return
while not fa.eof_reached():
var line := fa.get_line().strip_edges()
if line.is_empty() or line.begins_with("#") or line.begins_with("VNUM"):
continue
var tab := line.find("\t")
if tab < 0:
tab = line.find(" ")
if tab < 0:
continue
var k_str := line.substr(0, tab).strip_edges()
var val := line.substr(tab + 1).strip_edges()
if k_str.is_valid_int():
var vnum := int(k_str)
target_dict[vnum] = val
_tab["ITEM_%d" % vnum] = val
func item_name(vnum: int, fallback: String = "") -> String:
if _item_names.has(vnum):
return String(_item_names[vnum])
return fallback if not fallback.is_empty() else ("物品 #%d" % vnum)
func mob_name(vnum: int, fallback: String = "") -> String:
if _mob_names.has(vnum):
return String(_mob_names[vnum])
return fallback if not fallback.is_empty() else ("怪物 #%d" % vnum)
func has_item_name(vnum: int) -> bool:
return _item_names.has(vnum)
func has_mob_name(vnum: int) -> bool:
return _mob_names.has(vnum)
func _load_table(path: String) -> void:
if not FileAccess.file_exists(path):
+1 -1
View File
@@ -26,7 +26,7 @@ var _t := 0.0
func _init() -> void:
_assets = AssetRoot.path()
_loc = Locale.new()
_loc.setup(_assets, "en")
_loc.setup(_assets, Locale.get_default_lang())
_audio = Audio.new()
get_root().add_child(_audio)
_audio.setup(_assets)
+104
View File
@@ -0,0 +1,104 @@
extends SceneTree
const Locale = preload("res://locale.gd")
const ItemTooltip = preload("res://ui/item_tooltip.gd")
func _init() -> void:
print("=== Starting test_chinese_locale ===")
var assets := AssetRoot.path()
var passed := 0
var failed := 0
# 1. Default language
var default_lang := Locale.get_default_lang()
if default_lang == "zh":
print("PASS: Default language is 'zh'")
passed += 1
else:
printerr("FAIL: Default language expected 'zh', got '%s'" % default_lang)
failed += 1
# 2. Locale setup
var loc = Locale.new()
loc.setup(assets, "zh")
if loc.lang() == "zh":
print("PASS: Locale lang is 'zh'")
passed += 1
else:
printerr("FAIL: Locale lang expected 'zh', got '%s'" % loc.lang())
failed += 1
# 3. Item names
var sword_name := loc.item_name(10)
if sword_name == "武士刀+0":
print("PASS: Item 10 name is 武士刀+0")
passed += 1
else:
printerr("FAIL: Item 10 name expected '武士刀+0', got '%s'" % sword_name)
failed += 1
var yang_name := loc.item_name(1)
if yang_name == "金币":
print("PASS: Item 1 name is 金币")
passed += 1
else:
printerr("FAIL: Item 1 name expected '金币', got '%s'" % yang_name)
failed += 1
var dress_name := loc.item_name(11901)
if dress_name == "礼服":
print("PASS: Item 11901 name is 礼服")
passed += 1
else:
printerr("FAIL: Item 11901 name expected '礼服', got '%s'" % dress_name)
failed += 1
# 4. Mob names
var dog_name := loc.mob_name(101)
if dog_name == "幼狼":
print("PASS: Mob 101 name is 幼狼")
passed += 1
else:
printerr("FAIL: Mob 101 name expected '幼狼', got '%s'" % dog_name)
failed += 1
# 5. System messages
var arrow_msg := loc.t("CANNOT_SHOOT_EMPTY_ARROW")
if arrow_msg == "没有箭了,请装备箭":
print("PASS: CANNOT_SHOOT_EMPTY_ARROW translation matches")
passed += 1
else:
printerr("FAIL: CANNOT_SHOOT_EMPTY_ARROW expected '没有箭了,请装备箭', got '%s'" % arrow_msg)
failed += 1
var align_msg := loc.t("ALIGNMENT_NAME")
if align_msg == "善恶值 :":
print("PASS: ALIGNMENT_NAME translation matches")
passed += 1
else:
printerr("FAIL: ALIGNMENT_NAME expected '善恶值 :', got '%s'" % align_msg)
failed += 1
# 6. ItemTooltip integration
var tt = ItemTooltip.new()
var tt_ok := tt.setup(assets, "zh")
if tt_ok:
print("PASS: ItemTooltip setup succeeded for zh")
passed += 1
else:
printerr("FAIL: ItemTooltip setup failed")
failed += 1
var model := tt.build_model(10, 1, {})
if String(model.get("title", "")) == "武士刀+0":
print("PASS: ItemTooltip build_model resolved title 武士刀+0 without proto")
passed += 1
else:
printerr("FAIL: Expected title '武士刀+0', got '%s'" % model.get("title", ""))
failed += 1
print("\n=== %d / %d CHINESE LOCALE TESTS PASSED (Failed: %d) ===" % [passed, passed + failed, failed])
if failed > 0:
quit(1)
else:
quit(0)
+55 -9
View File
@@ -257,15 +257,40 @@ const PERCENT_APPLIES := {
}
var _descriptions: Dictionary = {}
var _item_names: Dictionary = {}
var _skill_names: Dictionary = {}
var _proto_node: Object = null
var _assets_root: String = ""
func setup(assets_root: String, lang := "en", proto_node: Object = null) -> bool:
func setup(assets_root: String, lang := "", proto_node: Object = null) -> bool:
_descriptions.clear()
_item_names.clear()
_skill_names.clear()
_assets_root = assets_root
_proto_node = proto_node
if lang.is_empty():
var LocaleScript = load("res://locale.gd")
if LocaleScript and LocaleScript.has_method("get_default_lang"):
lang = LocaleScript.get_default_lang()
else:
lang = "zh"
var names_path := assets_root.path_join("locale/locale/%s/item_names.txt" % lang)
if FileAccess.file_exists(names_path):
var nf := FileAccess.open(names_path, FileAccess.READ)
if nf:
while not nf.eof_reached():
var line := nf.get_line().strip_edges()
if line.is_empty() or line.begins_with("#") or line.begins_with("VNUM"):
continue
var tab := line.find("\t")
if tab < 0:
tab = line.find(" ")
if tab < 0:
continue
var k := line.substr(0, tab).strip_edges()
var v := line.substr(tab + 1).strip_edges()
if k.is_valid_int():
_item_names[int(k)] = v
var path := assets_root.path_join("locale/locale/%s/itemdesc.txt" % lang)
if FileAccess.file_exists(path):
var file := FileAccess.open(path, FileAccess.READ)
@@ -282,27 +307,40 @@ func setup(assets_root: String, lang := "en", proto_node: Object = null) -> bool
"description": parts[2].strip_edges() if parts.size() > 2 else "",
}
_load_skill_names(assets_root, lang)
return not _descriptions.is_empty() or not _skill_names.is_empty()
return not _descriptions.is_empty() or not _skill_names.is_empty() or not _item_names.is_empty()
func get_proto_node() -> Object:
return _proto_node
func get_stone_info(stone_vnum: int) -> Dictionary:
var name := "魂石 #%d" % stone_vnum
var name := ""
if _item_names.has(stone_vnum):
name = String(_item_names[stone_vnum])
var affect := ""
var icon_path := ""
if _proto_node and _proto_node.has_method("item"):
if name.is_empty() and _proto_node and _proto_node.has_method("item"):
var sp: Dictionary = _proto_node.item(stone_vnum)
if not sp.is_empty():
name = String(sp.get("locale_name", sp.get("name", name)))
name = String(sp.get("locale_name", sp.get("name", "")))
var sa: Array = sp.get("applies", [])
if not sa.is_empty():
var atype := int(sa[0].get("type", 0))
var aval := int(sa[0].get("value", 0))
if atype != 0 and aval != 0:
affect = format_apply_string(atype, aval)
if name.begins_with("魂石 #") and _descriptions.has(stone_vnum):
name = String(_descriptions[stone_vnum].get("name", name))
elif not affect.is_empty() or (_proto_node and _proto_node.has_method("item")):
var sp: Dictionary = _proto_node.item(stone_vnum)
if not sp.is_empty():
var sa: Array = sp.get("applies", [])
if not sa.is_empty():
var atype := int(sa[0].get("type", 0))
var aval := int(sa[0].get("value", 0))
if atype != 0 and aval != 0:
affect = format_apply_string(atype, aval)
if name.is_empty() and _descriptions.has(stone_vnum):
name = String(_descriptions[stone_vnum].get("name", ""))
if name.is_empty():
name = "魂石 #%d" % stone_vnum
return {"name": name, "affect": affect, "icon_path": icon_path}
func format_apply_string(apply_type: int, value: int) -> String:
@@ -317,7 +355,11 @@ func build_model(vnum: int, count: int, proto_data: Dictionary, instance_data: D
if vnum <= 0:
return {}
var name := String(proto_data.get("locale_name", proto_data.get("name", "物品 #%d" % vnum)))
var name: String = _item_names.get(vnum, "")
if name.is_empty():
name = String(proto_data.get("locale_name", proto_data.get("name", "")))
if name.is_empty() and _descriptions.has(vnum):
name = String(_descriptions[vnum].get("name", ""))
if name.is_empty():
name = "物品 #%d" % vnum
var sockets: Array = instance_data.get("sockets", proto_data.get("sockets", []))
@@ -530,7 +572,11 @@ func build_model(vnum: int, count: int, proto_data: Dictionary, instance_data: D
func format(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictionary = {}) -> String:
if vnum <= 0:
return ""
var name := String(proto_data.get("locale_name", proto_data.get("name", "物品 #%d" % vnum)))
var name: String = _item_names.get(vnum, "")
if name.is_empty():
name = String(proto_data.get("locale_name", proto_data.get("name", "")))
if name.is_empty() and _descriptions.has(vnum):
name = String(_descriptions[vnum].get("name", ""))
if name.is_empty():
name = "物品 #%d" % vnum
var sockets: Array = instance_data.get("sockets", proto_data.get("sockets", []))