Add i18n, save validation, and tolerant import handling.

Prepare the UI for English (default/fallback) and German with auto or manual locale selection, and report import issues with client-translated warnings instead of failing on minor save format changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-19 15:59:57 +02:00
parent 4b8b921e02
commit fbc2deec45
11 changed files with 1430 additions and 270 deletions
+175
View File
@@ -0,0 +1,175 @@
"""Save validation and import issue reporting."""
from __future__ import annotations
from typing import Any
# Known top-level fields (viewer baseline) new fields are OK, reported as info.
KNOWN_TOP_LEVEL_KEYS = frozenset({
"skillLevels", "skillXp", "inventory", "equipped", "flags", "pets", "coins",
"questProgress", "farmingPatches", "sessions", "exported_at",
})
IMPORTANT_TOP_LEVEL_KEYS = frozenset({
"skillLevels", "skillXp", "inventory", "flags",
})
Issue = dict[str, Any]
def issue(
level: str,
code: str,
message: str,
field: str | None = None,
params: dict[str, Any] | None = None,
) -> Issue:
out: Issue = {"level": level, "code": code, "message": message, "field": field}
if params:
out["params"] = params
return out
def has_errors(issues: list[Issue]) -> bool:
return any(i["level"] == "error" for i in issues)
def analyze_save(raw: Any, nested_parse_failures: list[str] | None = None) -> list[Issue]:
"""Structural checks before normalization."""
issues: list[Issue] = []
if not isinstance(raw, dict):
issues.append(issue(
"error", "invalid_root",
"The file is not a JSON object not a valid Idle Fantasy backup.",
))
return issues
if not raw:
issues.append(issue("error", "empty_save", "The save file is empty."))
return issues
present = set(raw.keys())
for key in sorted(present - KNOWN_TOP_LEVEL_KEYS):
issues.append(issue(
"info", "unknown_top_level",
f'Unknown field in backup: "{key}" (added by a game update?).',
field=key,
params={"field": key},
))
for key in sorted(IMPORTANT_TOP_LEVEL_KEYS - present):
issues.append(issue(
"warning", "missing_field",
f'Expected field missing: "{key}" related data will be shown empty.',
field=key,
params={"field": key},
))
for field in nested_parse_failures or []:
issues.append(issue(
"warning", "nested_json_invalid",
f'Field "{field}" could not be read as JSON raw value ignored.',
field=field,
params={"field": field},
))
_check_dict_field(raw, "skillLevels", issues)
_check_dict_field(raw, "skillXp", issues)
_check_dict_field(raw, "inventory", issues)
_check_dict_field(raw, "equipped", issues)
_check_dict_field(raw, "flags", issues)
_check_list_field(raw, "questProgress", issues)
_check_list_field(raw, "farmingPatches", issues)
_check_list_field(raw, "sessions", issues)
if "coins" in raw and not _is_number(raw["coins"]):
issues.append(issue(
"warning", "invalid_coins",
'Field "coins" is not numeric.',
field="coins",
))
if "exported_at" in raw and not _is_number(raw["exported_at"]):
issues.append(issue(
"warning", "invalid_exported_at",
'Field "exported_at" is not a valid timestamp.',
field="exported_at",
))
elif "exported_at" not in raw:
issues.append(issue(
"warning", "missing_exported_at",
"No export timestamp history comparisons may be inaccurate.",
field="exported_at",
))
skill_levels = raw.get("skillLevels")
skill_xp = raw.get("skillXp")
if isinstance(skill_levels, dict) and isinstance(skill_xp, dict):
only_levels = set(skill_levels) - set(skill_xp)
only_xp = set(skill_xp) - set(skill_levels)
if only_levels:
examples = ", ".join(sorted(only_levels)[:3])
issues.append(issue(
"info", "skill_xp_mismatch",
f"{len(only_levels)} skill(s) without XP entry (e.g. {examples}).",
field="skillXp",
params={"count": len(only_levels), "examples": examples},
))
if only_xp:
issues.append(issue(
"info", "skill_level_mismatch",
f"{len(only_xp)} XP entries without skill level.",
field="skillLevels",
params={"count": len(only_xp)},
))
return issues
def _check_dict_field(raw: dict, field: str, issues: list[Issue]) -> None:
if field not in raw:
return
value = raw[field]
if value is None:
return
if isinstance(value, str):
issues.append(issue(
"warning", "unparsed_nested_json",
f'Field "{field}" is still a text string JSON content could not be read.',
field=field,
params={"field": field},
))
elif not isinstance(value, dict):
issues.append(issue(
"warning", "invalid_type",
f'Field "{field}" has unexpected type ({type(value).__name__}).',
field=field,
params={"field": field, "type": type(value).__name__},
))
def _check_list_field(raw: dict, field: str, issues: list[Issue]) -> None:
if field not in raw:
return
value = raw[field]
if value is None:
return
if isinstance(value, str):
issues.append(issue(
"warning", "unparsed_nested_json",
f'Field "{field}" is still a text string JSON content could not be read.',
field=field,
params={"field": field},
))
elif not isinstance(value, list):
issues.append(issue(
"warning", "invalid_type",
f'Field "{field}" has unexpected type ({type(value).__name__}).',
field=field,
params={"field": field, "type": type(value).__name__},
))
def _is_number(value: Any) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool)