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:
185
tests/test_save_load_language.py
Normal file
185
tests/test_save_load_language.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock, AsyncMock
|
||||
|
||||
from src.classes.world import World
|
||||
from src.classes.map import Map
|
||||
from src.classes.tile import TileType
|
||||
from src.classes.calendar import Month, Year, create_month_stamp
|
||||
from src.sim.simulator import Simulator
|
||||
from src.sim.save.save_game import save_game
|
||||
from src.classes.language import language_manager, LanguageType
|
||||
from src.utils.config import CONFIG
|
||||
|
||||
# Helper functions
|
||||
def create_test_map():
|
||||
m = Map(width=10, height=10)
|
||||
for x in range(10):
|
||||
for y in range(10):
|
||||
m.create_tile(x, y, TileType.PLAIN)
|
||||
return m
|
||||
|
||||
@pytest.fixture
|
||||
def temp_save_dir(tmp_path):
|
||||
d = tmp_path / "saves"
|
||||
d.mkdir()
|
||||
return d
|
||||
|
||||
class TestSaveLoadLanguage:
|
||||
|
||||
def test_save_records_language(self, temp_save_dir):
|
||||
"""Test that save_game records the current language in metadata."""
|
||||
# 1. Setup
|
||||
game_map = create_test_map()
|
||||
month_stamp = create_month_stamp(Year(100), Month.JANUARY)
|
||||
# Create dummy events db file to avoid errors
|
||||
events_db_path = temp_save_dir / "test_lang_zh_events.db"
|
||||
world = World.create_with_db(map=game_map, month_stamp=month_stamp, events_db_path=events_db_path)
|
||||
sim = Simulator(world)
|
||||
|
||||
# 2. Set Language
|
||||
original_lang = language_manager.current
|
||||
try:
|
||||
language_manager.set_language("zh-CN")
|
||||
|
||||
# 3. Save
|
||||
save_path = temp_save_dir / "test_lang_zh.json"
|
||||
save_game(world, sim, [], save_path)
|
||||
|
||||
# 4. Verify
|
||||
with open(save_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "meta" in data
|
||||
assert "language" in data["meta"]
|
||||
assert data["meta"]["language"] == "zh-CN"
|
||||
|
||||
# Test English
|
||||
world.event_manager.close() # Close db before new save
|
||||
|
||||
language_manager.set_language("en-US")
|
||||
save_path_en = temp_save_dir / "test_lang_en.json"
|
||||
events_db_path_en = temp_save_dir / "test_lang_en_events.db"
|
||||
|
||||
# Create new world for new db path or just update existing world?
|
||||
# Easiest is to create new world
|
||||
world_en = World.create_with_db(map=game_map, month_stamp=month_stamp, events_db_path=events_db_path_en)
|
||||
sim_en = Simulator(world_en)
|
||||
|
||||
save_game(world_en, sim_en, [], save_path_en)
|
||||
world_en.event_manager.close()
|
||||
|
||||
with open(save_path_en, "r", encoding="utf-8") as f:
|
||||
data_en = json.load(f)
|
||||
assert data_en["meta"]["language"] == "en-US"
|
||||
|
||||
finally:
|
||||
# Restore
|
||||
language_manager.set_language(str(original_lang))
|
||||
try:
|
||||
if world and hasattr(world, 'event_manager'):
|
||||
world.event_manager.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_switches_language(self, temp_save_dir):
|
||||
"""Test that loading a save with different language triggers switch."""
|
||||
|
||||
# 1. Create a minimal save file with 'en-US' language
|
||||
save_filename = "test_switch_lang.json"
|
||||
save_path = temp_save_dir / save_filename
|
||||
|
||||
save_data = {
|
||||
"meta": {
|
||||
"version": "test",
|
||||
"save_time": "2026-01-01",
|
||||
"game_time": "Y1M1",
|
||||
"language": "en-US", # Target language
|
||||
"events_db": "test_switch_lang_events.db",
|
||||
"event_count": 0
|
||||
},
|
||||
# Load game might access these, so provide minimal structure
|
||||
"world": {},
|
||||
"avatars": [],
|
||||
"events": [],
|
||||
"simulator": {}
|
||||
}
|
||||
|
||||
with open(save_path, "w", encoding="utf-8") as f:
|
||||
json.dump(save_data, f)
|
||||
|
||||
# 2. Mock dependencies
|
||||
# Ensure current is zh-CN
|
||||
language_manager._current = LanguageType.ZH_CN
|
||||
|
||||
mock_broadcast = AsyncMock()
|
||||
|
||||
# We patch load_game so we don't need a valid save file structure beyond meta
|
||||
# We patch set_language to verify it's called
|
||||
with patch('src.server.main.manager.broadcast', mock_broadcast), \
|
||||
patch('src.server.main.language_manager.set_language') as mock_set_lang, \
|
||||
patch('src.server.main.OmegaConf') as mock_conf, \
|
||||
patch.object(CONFIG.paths, "saves", temp_save_dir), \
|
||||
patch('src.server.main.load_game', return_value=(MagicMock(), MagicMock(), [])), \
|
||||
patch('src.server.main.scan_avatar_assets'):
|
||||
|
||||
from src.server.main import api_load_game, LoadGameRequest
|
||||
|
||||
req = LoadGameRequest(filename=save_filename)
|
||||
await api_load_game(req)
|
||||
|
||||
# 3. Verify
|
||||
# Verify broadcast was called with toast
|
||||
assert mock_broadcast.called
|
||||
call_args = mock_broadcast.call_args[0][0]
|
||||
assert call_args['type'] == 'toast'
|
||||
assert "en-US" in call_args['message']
|
||||
assert call_args['language'] == "en-US"
|
||||
|
||||
# Verify set_language called with correct lang
|
||||
mock_set_lang.assert_called_with("en-US")
|
||||
|
||||
# Verify config save attempted
|
||||
assert mock_conf.save.called
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_no_switch_same_language(self, temp_save_dir):
|
||||
"""Test that loading same language save does NOT trigger switch."""
|
||||
|
||||
# 1. Save with zh-CN
|
||||
save_filename = "test_same_lang.json"
|
||||
save_path = temp_save_dir / save_filename
|
||||
|
||||
save_data = {
|
||||
"meta": {"language": "zh-CN"}, # Same as current
|
||||
}
|
||||
|
||||
with open(save_path, "w", encoding="utf-8") as f:
|
||||
json.dump(save_data, f)
|
||||
|
||||
# 2. Mock
|
||||
language_manager._current = LanguageType.ZH_CN
|
||||
|
||||
mock_broadcast = AsyncMock()
|
||||
mock_set_lang = MagicMock()
|
||||
|
||||
with patch('src.server.main.manager.broadcast', mock_broadcast), \
|
||||
patch('src.server.main.language_manager.set_language', mock_set_lang), \
|
||||
patch.object(CONFIG.paths, "saves", temp_save_dir), \
|
||||
patch('src.server.main.load_game', return_value=(MagicMock(), MagicMock(), [])), \
|
||||
patch('src.server.main.scan_avatar_assets'):
|
||||
|
||||
from src.server.main import api_load_game, LoadGameRequest
|
||||
|
||||
req = LoadGameRequest(filename=save_filename)
|
||||
await api_load_game(req)
|
||||
|
||||
# 3. Verify
|
||||
# Broadcast IS called to enforce sync (toast), even if same language
|
||||
assert mock_broadcast.called
|
||||
|
||||
# But actual backend switch (expensive operation) should NOT be called
|
||||
assert not mock_set_lang.called
|
||||
Reference in New Issue
Block a user