feat(i18n): add Traditional Chinese (zh-TW) locale support (#116)

* feat(i18n): add zh-TW locale support (backend infrastructure)

- 新增 LanguageType.ZH_TW 枚舉值
- 擴展 _lang_to_locale() 映射支援 zh-TW
- 創建 zh_TW 翻譯檔案結構 (messages.po + game_configs.po)
Closes #114

* feat(i18n): add zh-TW locale support (translation completion)

完成繁體中文語言的完整翻譯工作:

## 後端翻譯
- 將 messages.po 和 game_configs.po 轉換為繁體中文
- 編譯生成對應的 .mo 檔案
- 使用 OpenCC 's2t' 轉換器進行簡繁轉換

## 前端翻譯
- 新增 zh-TW.json 語言檔案
- 更新 index.ts 註冊 zh-TW 語言
- 修正 UI 語言標籤為「繁體中文」

## 翻譯統計
- messages.po: 701 個 msgid(動態字串、戰鬥、奇遇等)
- game_configs.po: 2972 個 msgid(遊戲配置)
- zh-TW.json: 347 行(前端 UI)

* feat(i18n): add zh-TW optimizations and tests

## 可選優化 1:用語本地化
- 修正前端 UI 詞彙為台灣繁體習慣
- 主要修正項目:
  - 菜單 -> 選單
  - 設置 -> 設定
  - 加載 -> 載入
  - 保存 -> 儲存
  - 程序 -> 程式
  - 其他 UI 用語調整

## 可選優化 2:測試覆蓋
- 新增 test_i18n_zh_tw.py 測試檔案
- 涵蓋 13 個測試用例:
  - 語言枚舉驗證
  - 語言切換測試
  - 日期格式驗證
  - 動態翻譯測試
  - 境界/動作/情緒翻譯測試
  - 檔案完整性檢查
  - 翻譯覆蓋率驗證
  - 回退機制測試

## 測試結果
- 所有 13 個測試用例通過
- 翻譯覆蓋率 > 95%

* fix(i18n): add polib skip check to translation coverage test

* feat(i18n): add zh-TW column to glossary with Taiwan localization
This commit is contained in:
teps3105
2026-02-01 13:28:18 +08:00
committed by GitHub
parent c981aff863
commit f0b6b62de5
10 changed files with 6034 additions and 251 deletions

186
tests/test_i18n_zh_tw.py Normal file
View File

@@ -0,0 +1,186 @@
import unittest
from unittest.mock import MagicMock, patch
import os
import sys
from pathlib import Path
# Ensure project root is in sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from src.classes.calendar import get_date_str
from src.classes.sect_region import SectRegion
from src.classes.world import World
from src.classes.language import language_manager, LanguageType
from src.classes.celestial_phenomenon import CelestialPhenomenon
from src.run.data_loader import reload_all_static_data
class TestI18nZhTW(unittest.TestCase):
"""測試繁體中文zh-TWi18n 功能"""
def setUp(self):
# 儲存當前語言
self.original_lang = str(language_manager)
def tearDown(self):
# 恢復語言
language_manager.set_language(self.original_lang)
def test_zh_tw_language_enum_exists(self):
"""驗證 ZH_TW 語言枚舉存在"""
self.assertTrue(hasattr(LanguageType, 'ZH_TW'))
self.assertEqual(LanguageType.ZH_TW.value, 'zh-TW')
def test_language_switch_to_zh_tw(self):
"""驗證可以切換到繁體中文"""
language_manager.set_language('zh-TW')
self.assertEqual(str(language_manager), 'zh-TW')
self.assertEqual(language_manager.current, LanguageType.ZH_TW)
def test_date_format_zh_tw(self):
"""驗證日期格式在繁體中文下正確顯示"""
language_manager.set_language('zh-TW')
# 13 = Year 1, Month 2 (formula: year*12 + month - 1 => 1*12 + 2 - 1 = 13)
date_str = get_date_str(13)
self.assertIn('', date_str)
self.assertIn('', date_str)
self.assertEqual(date_str, '1年2月')
def test_calendar_i18n_zh_tw(self):
"""驗證日期格式在繁體中文下正確顯示"""
language_manager.set_language('zh-TW')
# 測試日期格式
date_str = get_date_str(13)
self.assertIn('', date_str)
self.assertIn('', date_str)
def test_dynamic_translation_zh_tw(self):
"""驗證動態翻譯在繁體中文下正常工作"""
from src.i18n import t
language_manager.set_language('zh-TW')
# 測試基礎翻譯
text = t('male')
self.assertIn(text, ['', '男性']) # 接受可能的變體
# 測試帶參數翻譯
text = t('{winner} defeated {loser}', winner='張三', loser='李四')
self.assertIn('張三', text)
self.assertIn('李四', text)
self.assertIn('戰勝', text)
def test_realm_translation_zh_tw(self):
"""驗證境界翻譯在繁體中文下正確"""
# 使用已測試的日期格式功能來驗證翻譯機制
language_manager.set_language('zh-TW')
# 驗證基本翻譯機制正常工作
from src.i18n import t
result = t('qi_refinement')
self.assertIn(result, ['鍊氣', '煉氣', '練氣']) # 接受可能的變體(簡繁轉換差異)
def test_sect_region_desc_zh_tw(self):
"""驗證宗門駐地描述在繁體中文下正確"""
from src.i18n import t
language_manager.set_language('zh-TW')
# 測試宗門駐地翻譯
result = t('Sect Headquarters')
self.assertIn('宗門', result)
def test_language_fallback_zh_tw_to_zh_cn(self):
"""驗證繁體中文缺少翻譯時回退到簡體中文"""
from src.i18n import t
language_manager.set_language('zh-TW')
# 測試不存在的翻譯(應回退到簡體或原文)
text = t('non_existent_key_12345')
self.assertEqual(text, 'non_existent_key_12345')
def test_zh_tw_locale_files_exist(self):
"""驗證 zh-TW locale 檔案存在"""
from pathlib import Path
locale_dir = Path('src/i18n/locales/zh_TW/LC_MESSAGES')
# 檢查 .po 檔案
messages_po = locale_dir / 'messages.po'
self.assertTrue(messages_po.exists(), 'messages.po 應該存在')
game_configs_po = locale_dir / 'game_configs.po'
self.assertTrue(game_configs_po.exists(), 'game_configs.po 應該存在')
# 檢查 .mo 檔案
messages_mo = locale_dir / 'messages.mo'
self.assertTrue(messages_mo.exists(), 'messages.mo 應該存在')
game_configs_mo = locale_dir / 'game_configs.mo'
self.assertTrue(game_configs_mo.exists(), 'game_configs.mo 應該存在')
def test_zh_tw_po_file_integrity(self):
"""驗證 zh-TW .po 檔案完整性"""
try:
import polib
except ImportError:
self.skipTest('polib 未安裝')
locale_dir = Path('src/i18n/locales/zh_TW/LC_MESSAGES')
# 檢查 messages.po
messages_po = polib.pofile(str(locale_dir / 'messages.po'))
self.assertGreater(len(messages_po), 0, 'messages.po 應包含翻譯條目')
# 檢查 metadata
metadata = messages_po.metadata
self.assertIn('Language', metadata)
self.assertEqual(metadata.get('Language'), 'zh_TW')
def test_translation_coverage_zh_tw(self):
"""驗證 zh-TW 翻譯覆蓋率"""
try:
import polib
except ImportError:
self.skipTest('polib 未安裝')
locale_dir = Path('src/i18n/locales/zh_TW/LC_MESSAGES')
# 檢查 messages.po
messages_po = polib.pofile(str(locale_dir / 'messages.po'))
translated_count = sum(1 for entry in messages_po if entry.msgstr)
total_count = len(messages_po)
coverage = translated_count / total_count if total_count > 0 else 0
# 翻譯覆蓋率應該 > 95%
self.assertGreater(coverage, 0.95, f'翻譯覆蓋率應 > 95%,實際: {coverage:.1%}')
def test_action_translation_zh_tw(self):
"""驗證動作名稱在繁體中文下正確"""
from src.i18n import t
language_manager.set_language('zh-TW')
# 測試動作名稱翻譯
self.assertEqual(t('cultivate_action_name'), '修煉')
self.assertEqual(t('breakthrough_action_name'), '突破')
self.assertEqual(t('escape_action_name'), '逃離')
self.assertEqual(t('self_heal_action_name'), '療傷')
def test_emotion_translation_zh_tw(self):
"""驗證情緒在繁體中文下正確"""
from src.i18n import t
language_manager.set_language('zh-TW')
# 測試情緒名稱翻譯
self.assertEqual(t('emotion_calm'), '平靜')
self.assertEqual(t('emotion_happy'), '開心')
self.assertEqual(t('emotion_angry'), '憤怒')
self.assertEqual(t('emotion_sad'), '悲傷')
if __name__ == '__main__':
unittest.main()