Feat/i18n (#92)

* feat: add vue-i18n

* feat: add vue-i18n

* feat: add vue-i18n

* feat: add language class

* add: en templates and configs

* add: en names

* refactor: name gender id and sect id

* feat(i18n): add gettext infrastructure for dynamic text translation (#81)

* feat(i18n): add gettext infrastructure for dynamic text translation

- Add src/i18n/ module with t() translation function
- Add .po/.mo files for zh_CN and en_US locales
- Update LanguageManager to reload translations on language change
- Add comprehensive tests (14 tests, all passing)
- Add implementation spec at docs/specs/i18n-dynamic-text.md

Phase 1 of i18n dynamic text implementation.

* feat(i18n): expand .po files with comprehensive translation entries

Add translation messages for:
- Battle result messages (fatal/non-fatal outcomes)
- Fortune event messages (item discovery, cultivation gains)
- Misfortune event messages (losses, damage, regression)
- Death reason messages
- Item exchange messages (equip, sell, discard)
- Single choice context and option labels
- Common labels (weapon, auxiliary, technique, elixir)

Both zh_CN and en_US locales updated with matching entries.

* test: add .po file integrity tests

* feat: i18n for actions

* feat: i18n for effects

* feat: i18n for gathering

* feat: i18n for classes

* feat: i18n for classes

* feat: i18n for classes

* feat: i18n for classes

* fix bugs

* fix bugs

* fix bugs

* fix bugs

* fix bugs

* fix bugs

* fix bugs

* fix bugs

* update csv

* update world info

* update prompt

* update prompt

* fix bug

* fix bug

* fix bug

* fix bug

* fix bug

* fix bug

* fix bug

* fix bug

* fix bug

* update

* update

* update

* update

* update

* update

* update

---------

Co-authored-by: Zihao Xu <xzhseh@gmail.com>
This commit is contained in:
4thfever
2026-01-24 13:47:23 +08:00
committed by GitHub
parent 6f4b648d6e
commit e1091fdf5a
243 changed files with 18297 additions and 3148 deletions

View File

@@ -6,7 +6,6 @@ Avatar 模块
from src.classes.avatar.core import (
Avatar,
Gender,
gender_strs,
)
from src.classes.avatar.info_presenter import (
@@ -20,7 +19,6 @@ __all__ = [
# 核心类
"Avatar",
"Gender",
"gender_strs",
# 信息展示函数
"get_avatar_info",
"get_avatar_structured_info",

View File

@@ -188,8 +188,9 @@ class ActionMixin:
"""
获取易读的计划动作列表字符串。
"""
from src.i18n import t
if not self.planned_actions:
return ""
return t("None")
lines = []
for i, plan in enumerate(self.planned_actions, 1):

View File

@@ -54,12 +54,13 @@ class Gender(Enum):
FEMALE = "female"
def __str__(self) -> str:
return gender_strs.get(self, self.value)
from src.i18n import t
return t(gender_msg_ids.get(self, self.value))
gender_strs = {
Gender.MALE: "",
Gender.FEMALE: "",
gender_msg_ids = {
Gender.MALE: "male",
Gender.FEMALE: "female",
}
@@ -189,18 +190,20 @@ class Avatar(
def get_sect_str(self) -> str:
"""获取宗门显示名:有宗门则返回"宗门名+职位",否则返回"散修""""
from src.i18n import t
if self.sect is None:
return "散修"
return t("Rogue Cultivator")
if self.sect_rank is None:
return self.sect.name
from src.classes.sect_ranks import get_rank_display_name
rank_name = get_rank_display_name(self.sect_rank, self.sect)
return f"{self.sect.name}{rank_name}"
return t("{sect} {rank}", sect=self.sect.name, rank=rank_name)
def get_sect_rank_name(self) -> str:
"""获取宗门职位的显示名称"""
from src.i18n import t
if self.sect is None or self.sect_rank is None:
return "散修"
return t("Rogue Cultivator")
from src.classes.sect_ranks import get_rank_display_name
return get_rank_display_name(self.sect_rank, self.sect)
@@ -314,8 +317,8 @@ class Avatar(
"""获取当前动作名称,默认返回'思考'"""
if self.current_action and self.current_action.action:
action = self.current_action.action
# 优先取 ACTION_NAME (中文名),如果没有则使用类名
return getattr(action, "ACTION_NAME", getattr(action, "name", "思考"))
# 使用 get_action_name() 获取翻译后的动作名称
return action.get_action_name()
return "思考"
def __post_init__(self):

View File

@@ -18,6 +18,7 @@ from src.utils.config import CONFIG
def _get_effects_text(avatar: "Avatar") -> str:
"""获取格式化的效果文本"""
from src.i18n import t
from src.classes.effect import format_effects_to_text
breakdown = avatar.get_effect_breakdown()
effect_parts = []
@@ -25,80 +26,83 @@ def _get_effects_text(avatar: "Avatar") -> str:
desc_str = format_effects_to_text(effects)
if desc_str:
effect_parts.append(f"[{source_name}] {desc_str}")
return "\n".join(effect_parts) if effect_parts else ""
return "\n".join(effect_parts) if effect_parts else t("None")
def get_avatar_info(avatar: "Avatar", detailed: bool = False) -> dict:
"""
获取 avatar 的信息,返回 dict根据 detailed 控制信息粒度。
"""
from src.i18n import t
region = avatar.tile.region if avatar.tile is not None else None
from src.classes.relation import get_relations_strs
relation_lines = get_relations_strs(avatar, max_lines=8)
relations_info = "".join(relation_lines) if relation_lines else ""
relations_info = t("relation_separator").join(relation_lines) if relation_lines else t("None")
magic_stone_info = str(avatar.magic_stone)
from src.classes.sect import get_sect_info_with_rank
if detailed:
weapon_info = f"{avatar.weapon.get_detailed_info()},熟练度:{avatar.weapon_proficiency:.1f}%" if avatar.weapon else ""
auxiliary_info = avatar.auxiliary.get_detailed_info() if avatar.auxiliary else ""
weapon_info = t("{weapon_name}, Proficiency: {proficiency}%",
weapon_name=avatar.weapon.get_detailed_info(),
proficiency=f"{avatar.weapon_proficiency:.1f}") if avatar.weapon else t("None")
auxiliary_info = avatar.auxiliary.get_detailed_info() if avatar.auxiliary else t("None")
sect_info = get_sect_info_with_rank(avatar, detailed=True)
alignment_info = avatar.alignment.get_detailed_info() if avatar.alignment is not None else "未知"
region_info = region.get_detailed_info() if region is not None else ""
alignment_info = avatar.alignment.get_detailed_info() if avatar.alignment is not None else t("Unknown")
region_info = region.get_detailed_info() if region is not None else t("None")
root_info = avatar.root.get_detailed_info()
technique_info = avatar.technique.get_detailed_info() if avatar.technique is not None else ""
technique_info = avatar.technique.get_detailed_info() if avatar.technique is not None else t("None")
cultivation_info = avatar.cultivation_progress.get_detailed_info()
personas_info = ", ".join([p.get_detailed_info() for p in avatar.personas]) if avatar.personas else ""
materials_info = "".join([f"{mat.get_detailed_info()}x{quantity}" for mat, quantity in avatar.materials.items()]) if avatar.materials else ""
personas_info = ", ".join([p.get_detailed_info() for p in avatar.personas]) if avatar.personas else t("None")
materials_info = t("material_separator").join([f"{mat.get_detailed_info()}x{quantity}" for mat, quantity in avatar.materials.items()]) if avatar.materials else t("None")
appearance_info = avatar.appearance.get_detailed_info(avatar.gender)
spirit_animal_info = avatar.spirit_animal.get_info() if avatar.spirit_animal is not None else ""
spirit_animal_info = avatar.spirit_animal.get_info() if avatar.spirit_animal is not None else t("None")
else:
weapon_info = avatar.weapon.get_info() if avatar.weapon is not None else ""
auxiliary_info = avatar.auxiliary.get_info() if avatar.auxiliary is not None else ""
weapon_info = avatar.weapon.get_info() if avatar.weapon is not None else t("None")
auxiliary_info = avatar.auxiliary.get_info() if avatar.auxiliary is not None else t("None")
sect_info = get_sect_info_with_rank(avatar, detailed=False)
region_info = region.get_info() if region is not None else ""
alignment_info = avatar.alignment.get_info() if avatar.alignment is not None else "未知"
region_info = region.get_info() if region is not None else t("None")
alignment_info = avatar.alignment.get_info() if avatar.alignment is not None else t("Unknown")
root_info = avatar.root.get_info()
technique_info = avatar.technique.get_info() if avatar.technique is not None else ""
technique_info = avatar.technique.get_info() if avatar.technique is not None else t("None")
cultivation_info = avatar.cultivation_progress.get_info()
personas_info = ", ".join([p.get_detailed_info() for p in avatar.personas]) if avatar.personas else ""
materials_info = "".join([f"{mat.get_info()}x{quantity}" for mat, quantity in avatar.materials.items()]) if avatar.materials else ""
personas_info = ", ".join([p.get_detailed_info() for p in avatar.personas]) if avatar.personas else t("None")
materials_info = t("material_separator").join([f"{mat.get_info()}x{quantity}" for mat, quantity in avatar.materials.items()]) if avatar.materials else t("None")
appearance_info = avatar.appearance.get_info()
spirit_animal_info = avatar.spirit_animal.get_info() if avatar.spirit_animal is not None else ""
spirit_animal_info = avatar.spirit_animal.get_info() if avatar.spirit_animal is not None else t("None")
info_dict = {
"名字": avatar.name,
"性别": str(avatar.gender),
"年龄": str(avatar.age),
"hp": str(avatar.hp),
"灵石": magic_stone_info,
"关系": relations_info,
"宗门": sect_info,
"阵营": alignment_info,
"地区": region_info,
"灵根": root_info,
"功法": technique_info,
"境界": cultivation_info,
"特质": personas_info,
"材料": materials_info,
"外貌": appearance_info,
"兵器": weapon_info,
"辅助装备": auxiliary_info,
"情绪": avatar.emotion.value,
"长期目标": avatar.long_term_objective.content if avatar.long_term_objective else "",
"短期目标": avatar.short_term_objective if avatar.short_term_objective else "",
t("Name"): avatar.name,
t("Gender"): str(avatar.gender),
t("Age"): str(avatar.age),
t("HP"): str(avatar.hp),
t("Spirit Stones"): magic_stone_info,
t("Relations"): relations_info,
t("Sect"): sect_info,
t("Alignment"): alignment_info,
t("Region"): region_info,
t("Spirit Root"): root_info,
t("Technique"): technique_info,
t("Realm"): cultivation_info,
t("Traits"): personas_info,
t("Materials"): materials_info,
t("Appearance"): appearance_info,
t("Weapon"): weapon_info,
t("Auxiliary"): auxiliary_info,
t("Emotion"): t(avatar.emotion.value),
t("Long-term Goal"): avatar.long_term_objective.content if avatar.long_term_objective else t("None"),
t("Short-term Goal"): avatar.short_term_objective if avatar.short_term_objective else t("None"),
}
if detailed:
info_dict["当前效果"] = _get_effects_text(avatar)
info_dict[t("Current Effects")] = _get_effects_text(avatar)
# 绰号:仅在存在时显示
if avatar.nickname is not None:
info_dict["绰号"] = avatar.nickname.value
info_dict[t("Nickname")] = avatar.nickname.value
# 灵兽:仅在存在时显示
if avatar.spirit_animal is not None:
info_dict["灵兽"] = spirit_animal_info
info_dict[t("Spirit Animal")] = spirit_animal_info
return info_dict
@@ -107,6 +111,7 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict:
获取结构化的角色信息,用于前端展示和交互。
"""
# 基础信息
from src.i18n import t
emoji = EMOTION_EMOJIS.get(avatar.emotion, EMOTION_EMOJIS[EmotionType.CALM])
info = {
@@ -118,13 +123,13 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict:
"realm": avatar.cultivation_progress.get_info(),
"level": avatar.cultivation_progress.level,
"hp": {"cur": avatar.hp.cur, "max": avatar.hp.max},
"alignment": str(avatar.alignment) if avatar.alignment else "未知",
"alignment": str(avatar.alignment) if avatar.alignment else t("Unknown"),
"magic_stone": avatar.magic_stone.value,
"base_battle_strength": int(get_base_strength(avatar)),
"emotion": {
"name": avatar.emotion.value,
"name": t(avatar.emotion.value),
"emoji": emoji,
"desc": avatar.emotion.value
"desc": t(avatar.emotion.value)
},
"thinking": avatar.thinking,
"short_term_objective": avatar.short_term_objective,
@@ -133,7 +138,7 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict:
"nickname_reason": avatar.nickname.reason if avatar.nickname else None,
"is_dead": avatar.is_dead,
"death_info": avatar.death_info,
"action_state": f"正在{avatar.current_action_name}"
"action_state": t("Performing {action}", action=avatar.current_action_name)
}
# 1. 特质 (Personas)
@@ -152,20 +157,19 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict:
from src.classes.sect_ranks import get_rank_display_name
sect_info["rank"] = get_rank_display_name(avatar.sect_rank, avatar.sect)
else:
sect_info["rank"] = "弟子"
sect_info["rank"] = t("Disciple")
info["sect"] = sect_info
else:
info["sect"] = None
# 补充:阵营详情
from src.classes.alignment import alignment_infos, alignment_strs
info["alignment"] = str(avatar.alignment) if avatar.alignment else "未知"
from src.classes.alignment import alignment_info_msg_ids
info["alignment"] = str(avatar.alignment) if avatar.alignment else t("Unknown")
if avatar.alignment:
cn_name = alignment_strs.get(avatar.alignment, avatar.alignment.value)
desc = alignment_infos.get(avatar.alignment, "")
desc_id = alignment_info_msg_ids.get(avatar.alignment, "")
info["alignment_detail"] = {
"name": cn_name,
"desc": desc,
"name": str(avatar.alignment),
"desc": t(desc_id) if desc_id else "",
}
# 4. 装备 (Weapon & Auxiliary)
@@ -197,7 +201,7 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict:
"name": other.name,
"relation": get_relation_label(relation, avatar, other),
"realm": other.cultivation_progress.get_info(),
"sect": other.sect.name if other.sect else "散修"
"sect": other.sect.name if other.sect else t("Rogue Cultivator")
})
info["relations"] = relations_list
@@ -210,7 +214,7 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict:
info["root"] = root_str
info["root_detail"] = {
"name": root_str,
"desc": f"包含元素:{''.join(str(e) for e in avatar.root.elements)}",
"desc": t("Contains elements: {elements}", elements=t("element_separator").join(str(e) for e in avatar.root.elements)),
"effect_desc": avatar.root.effect_desc
}
@@ -219,7 +223,7 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict:
info["spirit_animal"] = avatar.spirit_animal.get_structured_info()
# 当前效果
info["当前效果"] = _get_effects_text(avatar)
info[t("Current Effects")] = _get_effects_text(avatar)
return info
@@ -239,12 +243,13 @@ def get_avatar_expanded_info(
other_avatar: 另一个角色,如果提供则返回两人共同经历的事件,否则返回单人事件
detailed: 是否返回详细信息
"""
from src.i18n import t
info = get_avatar_info(avatar, detailed=detailed)
observed: list[str] = []
if co_region_avatars:
for other in co_region_avatars[:8]:
observed.append(f"{other.name},境界:{other.cultivation_progress.get_info()}")
observed.append(t("{name}, Realm: {realm}", name=other.name, realm=other.cultivation_progress.get_info()))
# 历史事件改为从全局事件管理器分类查询
em = avatar.world.event_manager
@@ -262,9 +267,9 @@ def get_avatar_expanded_info(
major_list = [str(e) for e in major_events]
minor_list = [str(e) for e in minor_events]
info["周围角色"] = observed
info["重大事件"] = major_list
info["短期事件"] = minor_list
info[t("Nearby Avatars")] = observed
info[t("Major Events")] = major_list
info[t("Recent Events")] = minor_list
return info
@@ -272,20 +277,30 @@ def get_other_avatar_info(from_avatar: "Avatar", to_avatar: "Avatar") -> str:
"""
仅显示几个字段名字、绰号、境界、关系、宗门、阵营、外貌、功法、武器、辅助装备、HP
"""
nickname = to_avatar.nickname.value if to_avatar.nickname else ""
sect = to_avatar.sect.name if to_avatar.sect else "散修"
tech = to_avatar.technique.get_info() if to_avatar.technique else ""
weapon = to_avatar.weapon.get_info() if to_avatar.weapon else ""
aux = to_avatar.auxiliary.get_info() if to_avatar.auxiliary else ""
from src.i18n import t
nickname = to_avatar.nickname.value if to_avatar.nickname else t("None")
sect = to_avatar.sect.name if to_avatar.sect else t("Rogue Cultivator")
tech = to_avatar.technique.get_info() if to_avatar.technique else t("None")
weapon = to_avatar.weapon.get_info() if to_avatar.weapon else t("None")
aux = to_avatar.auxiliary.get_info() if to_avatar.auxiliary else t("None")
alignment = to_avatar.alignment
# 关系可能为空
relation = from_avatar.get_relation(to_avatar) or ""
relation = from_avatar.get_relation(to_avatar) or t("None")
return (
f"{to_avatar.name},绰号:{nickname},境界:{to_avatar.cultivation_progress.get_info()}"
f"关系:{relation},宗门:{sect},阵营:{alignment}"
f"外貌:{to_avatar.appearance.get_info()},功法:{tech},兵器:{weapon},辅助:{aux}HP{to_avatar.hp}"
return t(
"{name}, Nickname: {nickname}, Realm: {realm}, Relation: {relation}, Sect: {sect}, Alignment: {alignment}, Appearance: {appearance}, Technique: {technique}, Weapon: {weapon}, Auxiliary: {aux}, HP: {hp}",
name=to_avatar.name,
nickname=nickname,
realm=to_avatar.cultivation_progress.get_info(),
relation=relation,
sect=sect,
alignment=alignment,
appearance=to_avatar.appearance.get_info(),
technique=tech,
weapon=weapon,
aux=aux,
hp=to_avatar.hp
)
@@ -294,20 +309,21 @@ def get_avatar_desc(avatar: "Avatar", detailed: bool = False) -> str:
获取角色的文本描述。
detailed=True 时包含详细的效果来源分析。
"""
from src.i18n import t
# 基础描述
lines = [f"{avatar.name}{avatar.gender} {avatar.age}"]
lines.append(f"境界: {avatar.cultivation_progress.get_info()}")
lines = [t("{name}{gender} {age} years old", name=avatar.name, gender=avatar.gender, age=avatar.age)]
lines.append(t("Realm: {realm}", realm=avatar.cultivation_progress.get_info()))
if avatar.sect:
lines.append(f"身份: {avatar.get_sect_str()}")
lines.append(t("Identity: {identity}", identity=avatar.get_sect_str()))
if detailed:
lines.append("\n--- 当前效果明细 ---")
lines.append(t("\n--- Current Effects Detail ---"))
breakdown = avatar.get_effect_breakdown()
from src.classes.effect import format_effects_to_text
if not breakdown:
lines.append("无额外效果")
lines.append(t("No additional effects"))
else:
for source_name, effects in breakdown:
# 使用现有的 format_effects_to_text 将字典转为中文描述