Files
cultivation-world-simulator/tests/test_i18n_classes_coverage.py
4thfever e1091fdf5a 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>
2026-01-24 13:47:23 +08:00

334 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""测试 src/classes/ 下的本地化覆盖率和质量
此测试确保:
1. 没有硬编码的中文字符串(除了注释和测试数据)
2. 所有使用的翻译键都在 po 文件中定义
3. 所有类的信息方法返回本地化内容
4. 格式化参数使用一致
"""
import re
import ast
from pathlib import Path
from typing import Set, Dict, List
import pytest
from src.i18n import t
from src.classes.language import language_manager
# TestHardcodedStrings 类已移除
#
# 原因:项目采用了合理的 i18n 架构设计:
# - UI 字符串使用 .po 文件和 t() 函数
# - 游戏数据内容存储在 static/locales/{zh-CN,en-US}/ 目录下的 CSV 文件中
#
# 因此src/classes/ 中的很多"硬编码中文"实际上是:
# 1. 枚举类型的常量定义(如 Essence.GOLD = "gold" # 金)
# 2. 数据映射字典(用于解析用户输入或内部标识)
# 3. 从 CSV 加载数据后的属性(如 weapon.name, sect.description
#
# 这些都是架构设计的合理组成部分,不需要转换为 t() 调用。
# 如需检查未翻译的字符串,请参考 TestTranslationKeysUsage 类。
class TestTranslationKeysUsage:
"""检查翻译键的使用情况"""
@staticmethod
def extract_t_function_calls(py_file: Path) -> Set[str]:
"""从 Python 文件中提取所有 t() 函数调用的第一个参数"""
try:
content = py_file.read_text(encoding='utf-8')
tree = ast.parse(content)
msgids = set()
for node in ast.walk(tree):
# 查找 t() 函数调用
if isinstance(node, ast.Call):
# 检查函数名是否是 t
func_name = None
if isinstance(node.func, ast.Name):
func_name = node.func.id
elif isinstance(node.func, ast.Attribute):
func_name = node.func.attr
if func_name == 't' and node.args:
# 获取第一个参数msgid
first_arg = node.args[0]
if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
msgids.add(first_arg.value)
return msgids
except Exception as e:
print(f"Warning: Could not parse {py_file}: {e}")
return set()
@staticmethod
def extract_msgids_from_po(po_file: Path) -> Set[str]:
"""从 po 文件中提取所有 msgid"""
msgids = set()
try:
content = po_file.read_text(encoding='utf-8')
pattern = r'msgid\s+"([^"]*)"'
matches = re.findall(pattern, content)
# 将 po 文件中的转义序列(如 \\n \\t等转换为实际字符
# 使用字符串替换而不是 decode避免中文字符编码问题
decoded_msgids = set()
for m in matches:
if m: # 排除空字符串
# 替换常见的转义序列
decoded = m.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r').replace('\\"', '"').replace("\\'", "'").replace('\\\\', '\\')
decoded_msgids.add(decoded)
msgids = decoded_msgids
except Exception as e:
print(f"Warning: Could not read {po_file}: {e}")
return msgids
def test_all_used_msgids_are_defined(self):
"""检查所有在代码中使用的 msgid 都在 po 文件中定义"""
classes_dir = Path("src/classes")
po_file = Path("src/i18n/locales/zh_CN/LC_MESSAGES/messages.po")
if not classes_dir.exists() or not po_file.exists():
pytest.skip("Required directories not found")
# 提取 po 文件中的所有 msgid
defined_msgids = self.extract_msgids_from_po(po_file)
# 提取代码中使用的所有 msgid
used_msgids = set()
for py_file in classes_dir.rglob("*.py"):
used_msgids.update(self.extract_t_function_calls(py_file))
# 找出未定义的 msgid
undefined_msgids = used_msgids - defined_msgids
if undefined_msgids:
msg = f"\n发现 {len(undefined_msgids)} 个在代码中使用但未在 po 文件中定义的 msgid:\n"
for msgid in sorted(undefined_msgids)[:10]:
msg += f" - '{msgid}'\n"
if len(undefined_msgids) > 10:
msg += f" ... 还有 {len(undefined_msgids) - 10}\n"
pytest.fail(msg)
class TestClassesI18nIntegration:
"""测试 classes 中的类是否正确集成了国际化"""
def setup_method(self):
"""每个测试前重置语言"""
language_manager.set_language("zh-CN")
def test_realm_translation(self):
"""测试境界相关的翻译"""
from src.classes.cultivation import Realm
language_manager.set_language("zh-CN")
# 测试境界名称翻译
assert t("qi_refinement") == "练气"
assert t("foundation_establishment") == "筑基"
language_manager.set_language("en-US")
assert t("qi_refinement") == "Qi Refinement"
assert t("foundation_establishment") == "Foundation Establishment"
def test_gender_translation(self):
"""测试性别翻译"""
language_manager.set_language("zh-CN")
assert t("male") == ""
assert t("female") == ""
language_manager.set_language("en-US")
assert t("male") == "Male"
assert t("female") == "Female"
def test_alignment_translation(self):
"""测试阵营翻译"""
language_manager.set_language("zh-CN")
assert t("righteous") == ""
assert t("neutral") == "中立"
assert t("evil") == ""
language_manager.set_language("en-US")
assert t("righteous") == "Righteous"
assert t("neutral") == "Neutral"
assert t("evil") == "Evil"
def test_weapon_type_translation(self):
"""测试武器类型翻译"""
language_manager.set_language("zh-CN")
assert t("sword") == ""
assert t("saber") == ""
assert t("spear") == ""
language_manager.set_language("en-US")
assert t("sword") == "Sword"
assert t("saber") == "Saber"
assert t("spear") == "Spear"
def test_action_names_translation(self):
"""测试动作名称翻译"""
language_manager.set_language("zh-CN")
assert t("cultivate_action_name") == "修炼"
assert t("breakthrough_action_name") == "突破"
assert t("attack_action_name") == "发起战斗"
language_manager.set_language("en-US")
assert t("cultivate_action_name") == "Cultivate"
assert t("breakthrough_action_name") == "Breakthrough"
assert t("attack_action_name") == "Initiate Battle"
def test_effect_names_translation(self):
"""测试效果名称翻译"""
language_manager.set_language("zh-CN")
assert t("effect_extra_max_hp") == "最大生命值"
assert t("effect_extra_max_lifespan") == "最大寿元"
language_manager.set_language("en-US")
assert t("effect_extra_max_hp") == "Max HP"
assert t("effect_extra_max_lifespan") == "Max Lifespan"
def test_parameterized_translation_chinese(self):
"""测试带参数的中文翻译"""
language_manager.set_language("zh-CN")
result = t("{name} obtained {amount} spirit stones",
name="张三", amount=100)
assert "张三" in result
assert "获得灵石" in result
assert "100" in result
def test_parameterized_translation_english(self):
"""测试带参数的英文翻译"""
language_manager.set_language("en-US")
result = t("{name} obtained {amount} spirit stones",
name="Zhang San", amount=100)
assert "Zhang San" in result
assert "obtained" in result
assert "spirit stones" in result
assert "100" in result
class TestI18nConsistency:
"""测试国际化的一致性"""
def test_all_enum_translations_exist(self):
"""测试所有枚举类型的翻译都存在"""
language_manager.set_language("zh-CN")
# 测试主要枚举类型
enum_keys = [
# 境界
"qi_refinement", "foundation_establishment", "core_formation", "nascent_soul",
# 阶段
"early_stage", "middle_stage", "late_stage",
# 性别
"male", "female",
# 阵营
"righteous", "neutral", "evil",
# 武器类型
"sword", "saber", "spear", "staff", "fan", "whip", "zither", "flute", "hidden_weapon",
# 关系
"parent", "child", "sibling", "kin", "master", "apprentice", "lovers", "friend", "enemy",
]
missing = []
for key in enum_keys:
result = t(key)
# 如果翻译失败,会返回原始键
if result == key:
missing.append(key)
if missing:
pytest.fail(f"以下枚举键缺少中文翻译: {', '.join(missing)}")
def test_format_string_consistency(self):
"""测试格式化字符串在中英文中的参数一致性"""
from pathlib import Path
import re
zh_po = Path("src/i18n/locales/zh_CN/LC_MESSAGES/messages.po")
en_po = Path("src/i18n/locales/en_US/LC_MESSAGES/messages.po")
if not zh_po.exists() or not en_po.exists():
pytest.skip("PO files not found")
def extract_msgid_msgstr_pairs(po_file):
"""提取 msgid 和 msgstr 对"""
content = po_file.read_text(encoding='utf-8')
pairs = {}
lines = content.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
if line.startswith('msgid "') and line != 'msgid ""':
msgid = line[7:-1] # 提取引号内的内容
# 查找对应的 msgstr
i += 1
while i < len(lines) and not lines[i].strip().startswith('msgstr '):
i += 1
if i < len(lines):
msgstr = lines[i].strip()[8:-1] # 提取引号内的内容
pairs[msgid] = msgstr
i += 1
return pairs
zh_pairs = extract_msgid_msgstr_pairs(zh_po)
en_pairs = extract_msgid_msgstr_pairs(en_po)
# 检查格式化参数
inconsistent = []
for msgid in zh_pairs:
if msgid in en_pairs:
# 提取格式化参数 {param}
zh_params = set(re.findall(r'\{(\w+)\}', zh_pairs[msgid]))
en_params = set(re.findall(r'\{(\w+)\}', en_pairs[msgid]))
msgid_params = set(re.findall(r'\{(\w+)\}', msgid))
# 1. 首先检查中英文翻译之间的参数是否一致
if zh_params != en_params:
inconsistent.append({
'msgid': msgid,
'msgid_params': msgid_params,
'zh_params': zh_params,
'en_params': en_params
})
continue
# 2. 如果 msgid 本身包含参数,那么翻译必须包含完全相同的参数
# 如果 msgid 不包含参数(可能是 key则允许翻译包含参数只要中英文一致即可
if msgid_params and (zh_params != msgid_params):
inconsistent.append({
'msgid': msgid,
'msgid_params': msgid_params,
'zh_params': zh_params,
'en_params': en_params
})
if inconsistent:
msg = "\n发现格式化参数不一致的翻译:\n"
for item in inconsistent[:5]:
msg += f" msgid: {item['msgid'][:50]}...\n"
msg += f" 原始参数: {item['msgid_params']}\n"
msg += f" 中文参数: {item['zh_params']}\n"
msg += f" 英文参数: {item['en_params']}\n"
if len(inconsistent) > 5:
msg += f" ... 还有 {len(inconsistent) - 5}\n"
pytest.fail(msg)
if __name__ == "__main__":
pytest.main([__file__, "-v"])