Skip to content

Commit 53e4e17

Browse files
committed
fix(locales): preserve valid best-effort entries
1 parent 294e71f commit 53e4e17

4 files changed

Lines changed: 63 additions & 6 deletions

File tree

scripts/generate_locales.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,29 @@
6464
_FEATURE_KEY_RE = re.compile(r"^features\.([a-z0-9_]+)\.label$")
6565

6666

67+
def _validate_best_effort_messages(
68+
value: object, *, context: str, path: Path
69+
) -> dict[str, str]:
70+
"""Drop invalid entries while retaining valid best-effort translations."""
71+
if not isinstance(value, dict):
72+
return _validate_string_map(value, context=context)
73+
74+
result: dict[str, str] = {}
75+
for key, text in value.items():
76+
try:
77+
result.update(_validate_string_map({key: text}, context=context))
78+
except ValueError as exc:
79+
entry = (
80+
f"{context}.{key}" if isinstance(key, str) else f"{context}[{key!r}]"
81+
)
82+
print(
83+
f"::warning file={path}::Ignoring invalid best-effort locale "
84+
f"entry {entry}: {exc}",
85+
file=sys.stderr,
86+
)
87+
return result
88+
89+
6790
def load_catalogs() -> dict[str, dict[str, str]]:
6891
"""Every canonical catalog's ``messages`` section, keyed by locale code.
6992
@@ -77,9 +100,14 @@ def load_catalogs() -> dict[str, dict[str, str]]:
77100
data = json.loads(path.read_text(encoding="utf-8"))
78101
if not isinstance(data, dict):
79102
raise ValueError(f"{path.name} must contain a JSON object")
80-
catalogs[path.stem] = _validate_string_map(
81-
data.get("messages"), context=f"{path.name}.messages"
82-
)
103+
context = f"{path.name}.messages"
104+
messages = data.get("messages")
105+
if is_best_effort_locale(path.stem):
106+
catalogs[path.stem] = _validate_best_effort_messages(
107+
messages, context=context, path=path
108+
)
109+
else:
110+
catalogs[path.stem] = _validate_string_map(messages, context=context)
83111
except (OSError, json.JSONDecodeError, ValueError) as exc:
84112
if not is_best_effort_locale(path.stem):
85113
raise

src/ha_mcp/settings_ui/_i18n.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def _load_catalog_file(path: Path) -> dict[str, Any]:
9696
"""Load and validate one catalog without coupling it to its siblings."""
9797
try:
9898
raw = json.loads(path.read_text(encoding="utf-8"))
99-
except (OSError, json.JSONDecodeError) as exc:
99+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
100100
raise ImportError(f"Invalid settings UI locale catalog: {path}") from exc
101101
if not isinstance(raw, dict):
102102
raise ValueError(f"Locale catalog {path} must contain a JSON object")

tests/src/unit/test_generate_locales.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,32 @@ def test_invalid_best_effort_catalog_warns_and_uses_english_fallback(
5050
assert "best-effort locale tlh" in stderr
5151

5252

53+
def test_invalid_best_effort_entries_warn_and_preserve_valid_messages(
54+
tmp_path: Path,
55+
monkeypatch: pytest.MonkeyPatch,
56+
capsys: pytest.CaptureFixture[str],
57+
) -> None:
58+
locales = tmp_path / "locales"
59+
locales.mkdir()
60+
(locales / "en.json").write_text(
61+
json.dumps({"messages": {"valid": "English", "blank": "Fallback"}}),
62+
encoding="utf-8",
63+
)
64+
(locales / "tlh.json").write_text(
65+
json.dumps({"messages": {"valid": "Qapla'", "blank": "", "non_string": 3}}),
66+
encoding="utf-8",
67+
)
68+
monkeypatch.setattr(generate_locales, "LOCALES_DIR", locales)
69+
70+
catalogs = generate_locales.load_catalogs()
71+
72+
assert catalogs["tlh"] == {"valid": "Qapla'"}
73+
stderr = capsys.readouterr().err
74+
assert "Ignoring invalid best-effort locale entry" in stderr
75+
assert "tlh.json.messages.blank" in stderr
76+
assert "tlh.json.messages.non_string" in stderr
77+
78+
5379
class TestResolveText:
5480
def test_override_order_is_flavor_then_features_then_addon(self) -> None:
5581
messages = {

tests/src/unit/test_settings_ui_i18n.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,12 @@ def test_invalid_best_effort_catalog_warns_without_blocking_other_locales(
107107
assert "Skipping best-effort locale tlh" in caplog.text
108108

109109

110-
def test_invalid_strict_catalog_still_blocks_loading(tmp_path: Path) -> None:
110+
@pytest.mark.parametrize("payload", [b"{not json", b"\xff"], ids=["json", "utf8"])
111+
def test_invalid_strict_catalog_still_blocks_loading(
112+
tmp_path: Path, payload: bytes
113+
) -> None:
111114
_write_catalog(tmp_path, "en", native_name="English", messages={"a": "A"})
112-
(tmp_path / "de.json").write_text("{not json", encoding="utf-8")
115+
(tmp_path / "de.json").write_bytes(payload)
113116

114117
with pytest.raises(ImportError, match=r"de\.json"):
115118
load_catalogs(tmp_path)

0 commit comments

Comments
 (0)