Refactor/i18n (#115)

重构i18n,现在game configs的配表,除了姓名这种外,都是统一的配表了。
对应的配表的名称和desc需要去i18n里取,但是其他配置不需要重复配置了。
这大大简化了之后新增i18n的心智负担。
This commit is contained in:
4thfever
2026-02-01 01:09:24 +08:00
committed by GitHub
parent 5d46b47672
commit bc3ebc006c
79 changed files with 10082 additions and 892 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import json
from typing import Dict, Any
from src.classes.action.registry import ActionRegistry
# 确保在收集注册表前加载所有动作模块(含 mutual actions
@@ -26,10 +27,22 @@ def _build_action_info(action):
info["cd_months"] = cd
return info
ACTION_INFOS = {
action.__name__: _build_action_info(action)
for action in ALL_ACTUAL_ACTION_CLASSES
}
def get_action_infos() -> Dict[str, Any]:
"""
动态获取当前语言环境下的动作描述信息
"""
return {
action.__name__: _build_action_info(action)
for action in ALL_ACTUAL_ACTION_CLASSES
}
def get_action_infos_str() -> str:
"""
获取JSON格式的动作描述字符串
"""
return json.dumps(get_action_infos(), ensure_ascii=False, indent=2)
# 为了兼容性保留 ACTION_INFOS_STR但请注意这可能是旧的导入时的快照不会随语言切换更新
# 建议使用 get_action_infos_str() 获取最新语言的描述
ACTION_INFOS = get_action_infos()
ACTION_INFOS_STR = json.dumps(ACTION_INFOS, ensure_ascii=False, indent=2)

View File

@@ -304,7 +304,7 @@ class HiddenDomain(Gathering):
# 3. 角色信息 (可选,增加故事细节)
details_list = []
details_list.append(t("【Related Avatars Information】"))
details_list.append(t("\n【Related Avatars Information】"))
for av in related_avatars:
info = av.get_info(detailed=True)
details_list.append(f"- {av.name}: {info}")

View File

@@ -34,6 +34,12 @@ class LanguageManager:
except ImportError:
# Prevent circular import crash during initialization
pass
try:
from src.classes.name import reload as reload_names
reload_names()
except ImportError:
pass
def __str__(self):

View File

@@ -14,7 +14,7 @@ from src.classes.event import Event
from src.utils.config import CONFIG
from src.utils.llm import call_llm_with_task_name
from src.run.log import get_logger
from src.classes.actions import ACTION_INFOS_STR
from src.classes.actions import get_action_infos_str
from src.i18n import t
logger = get_logger().logger
@@ -90,7 +90,7 @@ async def generate_long_term_objective(avatar: "Avatar") -> Optional[LongTermObj
infos = {
"world_info": world_info,
"avatar_info": expanded_info,
"general_action_infos": ACTION_INFOS_STR,
"general_action_infos": get_action_infos_str(),
}
# 调用LLM并自动解析JSON使用配置的模型模式

View File

@@ -47,10 +47,11 @@ class NameManager:
for g_list in self.common_given_names.values():
g_list.clear()
self.sect_given_names.clear()
# 加载姓氏
if "last_name" in game_configs:
last_name_df = game_configs["last_name"]
# 加载姓氏 (不再区分 _en 后缀,因为 config/df 已经处理了加载逻辑)
last_name_df = game_configs.get("last_name", [])
if last_name_df:
for row in last_name_df:
name = get_str(row, "last_name")
sect_id = get_int(row, "sect_id")
@@ -63,8 +64,9 @@ class NameManager:
self.common_last_names.append(name)
# 加载名字
if "given_name" in game_configs:
given_name_df = game_configs["given_name"]
given_name_df = game_configs.get("given_name", [])
if given_name_df:
for row in given_name_df:
name = get_str(row, "given_name")
gender_val = get_int(row, "gender") # 0 or 1

View File

@@ -68,10 +68,23 @@ def _get_translation() -> Optional[gettext.GNUTranslations]:
localedir=str(locale_dir),
languages=[locale_name]
)
_translations[lang] = trans
except FileNotFoundError:
# No translation file found, will use message as-is.
_translations[lang] = None
trans = None
try:
config_trans = gettext.translation(
"game_configs",
localedir=str(locale_dir),
languages=[locale_name]
)
if trans:
trans.add_fallback(config_trans)
else:
trans = config_trans
except FileNotFoundError:
pass
_translations[lang] = trans
return _translations.get(lang)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -2089,11 +2089,9 @@ msgstr "【Auction Items Information】"
msgid "\n【Related Avatars Information】"
msgstr "\n【Related Avatars Information】"
# LLM Prompt
msgid "auction_story_prompt"
@@ -3420,5 +3418,11 @@ msgstr "Hidden Domain {name} opened! Entry restricted to {realm} and below. Entr
msgid "Hidden Domain {name} opened! Entry restricted to {realm} and below. No one entered."
msgstr "Hidden Domain {name} opened! Entry restricted to {realm} and below. No one entered."
msgid "【Related Avatars Information】"
msgstr "【Related Avatars Information】"
msgid "{}: {}"
msgstr "{}: {}"
msgid "found a treasure"
msgstr "found a treasure"
msgid "perished"
msgstr "perished"

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -2261,5 +2261,13 @@ msgstr "秘境【{name}】现世!限制{realm}及以下修士进入。进入
msgid "Hidden Domain {name} opened! Entry restricted to {realm} and below. No one entered."
msgstr "秘境【{name}】现世!限制{realm}及以下修士进入。无修士进入。"
msgid "【Related Avatars Information】"
msgstr "【相关角色信息】"
msgid "{}: {}"
msgstr "{}{}"
msgid "found a treasure"
msgstr "觅得宝物"
msgid "perished"
msgstr "葬身于"

View File

@@ -10,8 +10,7 @@ from src.classes.sect import sects_by_id # 直接导入已加载的宗门数据
from src.utils.config import CONFIG
# 静态配置路径
# 使用共享配置目录,因为 tile_map 和 region_map 是语言无关的
CONFIG_DIR = CONFIG.paths.shared_game_configs
CONFIG_DIR = CONFIG.paths.game_configs
def load_cultivation_world_map() -> Map:
"""

View File

@@ -79,14 +79,19 @@ def update_paths_for_language(lang_code: str = None):
# 语言无关的配置目录
CONFIG.paths.shared_game_configs = Path("static/game_configs")
# 语言相关的配置目录
CONFIG.paths.game_configs = target_dir / "game_configs"
CONFIG.paths.localized_game_configs = target_dir / "game_configs"
# CONFIG.paths.game_configs 指向统一的数据源,不再区分语言目录
# 这里我们保留 CONFIG.paths.game_configs 作为"逻辑概念上的"配置集合根目录(虽然物理上分开了)
# 但实际上加载逻辑会在 df.py 中处理合并
CONFIG.paths.game_configs = Path("static/game_configs")
CONFIG.paths.templates = target_dir / "templates"
# 简单的存在性检查日志
if not CONFIG.paths.game_configs.exists():
print(f"[Config] Warning: Game configs dir not found at {CONFIG.paths.game_configs}")
else:
print(f"[Config] Switched paths to {lang_code}")
print(f"[Config] Switched language context to {lang_code} (Configs using Single Source)")
# 模块加载时自动初始化默认路径,确保 CONFIG.paths.game_configs 存在,避免 import 时 KeyError
update_paths_for_language()

View File

@@ -3,6 +3,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional
from src.utils.config import CONFIG
from src.i18n import t
def load_csv(path: Path) -> List[Dict[str, Any]]:
@@ -59,6 +60,32 @@ def load_csv(path: Path) -> List[Dict[str, Any]]:
else:
row_dict[header] = None
# -----------------------------------------------------------
# I18N Translation Injection
# -----------------------------------------------------------
# Try to translate name, desc and title using their IDs.
# If translation exists (and is not just the key itself), overwrite the value.
# Fallback is keeping the original value from CSV (usually Chinese reference).
title_id = row_dict.get("title_id")
if title_id and isinstance(title_id, str):
trans = t(title_id)
if trans != title_id and trans:
row_dict["title"] = trans
name_id = row_dict.get("name_id")
if name_id and isinstance(name_id, str):
trans = t(name_id)
if trans != name_id and trans:
row_dict["name"] = trans
desc_id = row_dict.get("desc_id")
if desc_id and isinstance(desc_id, str):
trans = t(desc_id)
if trans != desc_id and trans:
row_dict["desc"] = trans
# -----------------------------------------------------------
data.append(row_dict)
return data
@@ -66,17 +93,17 @@ def load_csv(path: Path) -> List[Dict[str, Any]]:
def load_game_configs() -> dict[str, List[Dict[str, Any]]]:
game_configs = {}
# 1. 加载共享配置 (Shared)
# 1. 加载共享配置 (static/game_configs/*.csv)
if hasattr(CONFIG.paths, "shared_game_configs") and CONFIG.paths.shared_game_configs.exists():
for path in CONFIG.paths.shared_game_configs.glob("*.csv"):
data = load_csv(path)
game_configs[path.stem] = data
# 2. 加载本地化配置 (Localized) - 会覆盖同名文件
if hasattr(CONFIG.paths, "game_configs") and CONFIG.paths.game_configs.exists():
for path in CONFIG.paths.game_configs.glob("*.csv"):
# 2. 加载本地化配置 (static/locales/{lang}/game_configs/*.csv)
# 如果文件名相同,覆盖共享配置
if hasattr(CONFIG.paths, "localized_game_configs") and CONFIG.paths.localized_game_configs.exists():
for path in CONFIG.paths.localized_game_configs.glob("*.csv"):
data = load_csv(path)
# 如果存在同名配置,这里会进行覆盖
game_configs[path.stem] = data
return game_configs