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:
127
tools/i18n/build_mo.py
Normal file
127
tools/i18n/build_mo.py
Normal 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())
|
||||
151
tools/i18n/check_po_duplicates.py
Normal file
151
tools/i18n/check_po_duplicates.py
Normal 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
13
tools/i18n/translate.py
Normal 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()
|
||||
74
tools/i18n/translate_name.py
Normal file
74
tools/i18n/translate_name.py
Normal 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()
|
||||
Reference in New Issue
Block a user