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

127
tools/i18n/build_mo.py Normal file
View File

@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""将 PO 文件编译为 MO 文件
使用方法:
python tools/i18n/build_mo.py
"""
import subprocess
import sys
from pathlib import Path
def compile_po_to_mo(po_file: Path) -> bool:
"""编译单个 PO 文件为 MO 文件"""
mo_file = po_file.with_suffix('.mo')
try:
# 使用 msgfmt 编译 PO 文件
result = subprocess.run(
['msgfmt', '-o', str(mo_file), str(po_file)],
capture_output=True,
text=True,
check=True
)
print(f"[OK] {po_file.relative_to(Path.cwd())} -> {mo_file.name}")
return True
except FileNotFoundError:
print("[ERROR] msgfmt 工具未找到。请安装 gettext 工具集。")
print("\n安装方法:")
print(" - Ubuntu/Debian: sudo apt-get install gettext")
print(" - macOS: brew install gettext")
print(" - Windows: 下载 gettext-iconv-windows 或使用 WSL")
return False
except subprocess.CalledProcessError as e:
print(f"[ERROR] 编译失败: {po_file}")
if e.stderr:
print(f" 错误信息: {e.stderr}")
return False
def compile_po_to_mo_python(po_file: Path) -> bool:
"""使用 Python polib 库编译 PO 文件为 MO 文件(备用方案)"""
try:
import polib
except ImportError:
print("[ERROR] polib 库未安装。尝试使用 msgfmt...")
return False
mo_file = po_file.with_suffix('.mo')
try:
po = polib.pofile(str(po_file))
po.save_as_mofile(str(mo_file))
print(f"[OK] {po_file.relative_to(Path.cwd())} -> {mo_file.name}")
return True
except Exception as e:
print(f"[ERROR] 编译失败: {po_file}")
print(f" 错误信息: {e}")
return False
def main():
"""主函数:查找所有 PO 文件并编译为 MO 文件"""
print("="*60)
print("编译 PO 文件为 MO 文件")
print("="*60)
# 查找项目根目录
script_dir = Path(__file__).parent
project_root = script_dir.parent.parent
i18n_dir = project_root / "src" / "i18n" / "locales"
if not i18n_dir.exists():
print(f"[ERROR] 找不到 i18n 目录: {i18n_dir}")
sys.exit(1)
# 查找所有 PO 文件
po_files = list(i18n_dir.rglob("*.po"))
if not po_files:
print(f"[WARNING] 未找到 PO 文件")
sys.exit(0)
print(f"\n找到 {len(po_files)} 个 PO 文件:")
for po_file in po_files:
print(f" - {po_file.relative_to(project_root)}")
print("\n开始编译...")
print("-"*60)
# 尝试使用 msgfmt推荐
success_count = 0
use_msgfmt = True
for po_file in po_files:
if use_msgfmt:
result = compile_po_to_mo(po_file)
if not result and success_count == 0:
# 第一次失败,尝试使用 polib
print("\n切换到 polib 库...")
use_msgfmt = False
result = compile_po_to_mo_python(po_file)
else:
result = compile_po_to_mo_python(po_file)
if result:
success_count += 1
# 输出结果
print("-"*60)
print(f"\n编译完成: {success_count}/{len(po_files)} 成功")
if success_count == len(po_files):
print("\n[OK] 所有 PO 文件已成功编译为 MO 文件")
return 0
else:
print(f"\n[WARNING] {len(po_files) - success_count} 个文件编译失败")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""检查 po 文件中是否有重复的 msgid"""
import re
import sys
from pathlib import Path
from collections import Counter
def extract_msgids(filepath: Path) -> list[str]:
"""
从 po 文件中提取所有 msgid
Args:
filepath: po 文件路径
Returns:
msgid 列表(不包含空字符串)
"""
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 匹配 msgid "..." 模式
pattern = r'msgid\s+"([^"]*)"'
matches = re.findall(pattern, content)
# 过滤掉空字符串(文件头的 msgid ""
msgids = [m for m in matches if m]
return msgids
def find_duplicates(msgids: list[str]) -> dict[str, int]:
"""
找出重复的 msgid
Args:
msgids: msgid 列表
Returns:
字典,键为重复的 msgid值为出现次数
"""
counter = Counter(msgids)
duplicates = {msgid: count for msgid, count in counter.items() if count > 1}
return duplicates
def check_file(filepath: Path, lang_name: str) -> tuple[int, dict[str, int]]:
"""
检查单个 po 文件
Args:
filepath: po 文件路径
lang_name: 语言名称(用于显示)
Returns:
(msgid总数, 重复项字典)
"""
print(f"\n{'='*60}")
print(f"检查文件: {lang_name}")
print(f"路径: {filepath}")
print(f"{'='*60}")
if not filepath.exists():
print(f"[ERROR] 文件不存在")
return 0, {}
msgids = extract_msgids(filepath)
print(f"总共找到 {len(msgids)} 个 msgid 条目")
duplicates = find_duplicates(msgids)
if duplicates:
print(f"\n[WARNING] 发现 {len(duplicates)} 个重复的 msgid:")
for msgid, count in sorted(duplicates.items()):
print(f" - '{msgid}' 出现了 {count}")
else:
print(f"\n[OK] 未发现重复的 msgid")
return len(msgids), duplicates
def main():
"""主函数"""
# 获取项目根目录
script_dir = Path(__file__).parent
project_root = script_dir.parent.parent
# po 文件路径
zh_file = project_root / "src" / "i18n" / "locales" / "zh_CN" / "LC_MESSAGES" / "messages.po"
en_file = project_root / "src" / "i18n" / "locales" / "en_US" / "LC_MESSAGES" / "messages.po"
# 检查中文文件
zh_count, zh_dups = check_file(zh_file, "中文 (zh_CN)")
# 检查英文文件
en_count, en_dups = check_file(en_file, "英文 (en_US)")
# 打印总结
print(f"\n{'='*60}")
print("检查总结")
print(f"{'='*60}")
has_error = False
if zh_dups or en_dups:
print("[ERROR] 发现重复条目,需要修复")
has_error = True
else:
print("[OK] 两个文件都没有重复的 msgid")
if zh_count != en_count:
print(f"[WARNING] 中英文 msgid 数量不一致: 中文 {zh_count} 个, 英文 {en_count}")
has_error = True
else:
print(f"[OK] 中英文 msgid 数量一致: {zh_count}")
# 检查 msgid 键是否匹配
if zh_count > 0 and en_count > 0:
zh_msgids = set(extract_msgids(zh_file))
en_msgids = set(extract_msgids(en_file))
zh_only = zh_msgids - en_msgids
en_only = en_msgids - zh_msgids
if zh_only:
print(f"\n[WARNING] 只在中文中存在的 msgid ({len(zh_only)} 个):")
for msgid in sorted(zh_only)[:5]:
print(f" - '{msgid}'")
if len(zh_only) > 5:
print(f" ... 还有 {len(zh_only) - 5}")
has_error = True
if en_only:
print(f"\n[WARNING] 只在英文中存在的 msgid ({len(en_only)} 个):")
for msgid in sorted(en_only)[:5]:
print(f" - '{msgid}'")
if len(en_only) > 5:
print(f" ... 还有 {len(en_only) - 5}")
has_error = True
if not zh_only and not en_only:
print("[OK] 中英文 msgid 键完全匹配")
# 返回状态码
return 1 if has_error else 0
if __name__ == "__main__":
sys.exit(main())

13
tools/i18n/translate.py Normal file
View File

@@ -0,0 +1,13 @@
from translate_name import translate_names
def main():
print("Starting translation process...")
# 翻译姓名文件
print("Translating names...")
translate_names()
print("Translation process completed.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,74 @@
import csv
import os
from pypinyin import pinyin, Style
def translate_names():
# 获取当前脚本所在目录的父目录的父目录,即项目根目录
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
zh_dir = os.path.join(base_dir, "static", "locales", "zh-CN", "game_configs")
en_dir = os.path.join(base_dir, "static", "locales", "en-US", "game_configs")
if not os.path.exists(en_dir):
os.makedirs(en_dir, exist_ok=True)
files = ["last_name.csv", "given_name.csv"]
for filename in files:
zh_path = os.path.join(zh_dir, filename)
en_path = os.path.join(en_dir, filename)
if not os.path.exists(zh_path):
print(f"Warning: {zh_path} does not exist.")
continue
rows = []
with open(zh_path, "r", encoding="utf-8") as f:
reader = csv.reader(f)
try:
header = next(reader)
desc = next(reader)
except StopIteration:
continue
# 读取所有行
original_rows = list(reader)
# 处理表头翻译 (虽然列名是英文,但为了统一,可以这里显式定义,或者直接复用)
# last_name.csv: last_name, sect_id, cond
# given_name.csv: given_name, gender, sect_id, cond
# 描述行翻译
en_desc = []
if filename == "last_name.csv":
en_desc = ["Surname", "Sect ID", "Condition"]
elif filename == "given_name.csv":
en_desc = ["Given Name", "Gender(0=Female,1=Male)", "Sect ID", "Condition"]
else:
en_desc = desc # Fallback
for row in original_rows:
if not row or not row[0]:
if row:
rows.append(row)
continue
# 第一列是姓名
chinese_name = row[0]
# 转换为拼音Style.NORMAL 表示不带声调
py_list = pinyin(chinese_name, style=Style.NORMAL)
# 拼接,首字母大写,其余小写,例如 "si", "ma" -> "Sima"
py_name = "".join([p[0] for p in py_list]).capitalize()
new_row = [py_name] + row[1:]
rows.append(new_row)
with open(en_path, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(header) # Header key unchanged
writer.writerow(en_desc) # Use English desc
writer.writerows(rows)
print(f"Successfully translated {filename} to {en_path}")
if __name__ == "__main__":
translate_names()