Files
cultivation-world-simulator/tests/test_save_load_language.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

186 lines
6.9 KiB
Python

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