Skip to content

Commit 294e71f

Browse files
committed
fix(locales): surface best-effort diagnostics
1 parent 91d99ef commit 294e71f

6 files changed

Lines changed: 29 additions & 23 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -809,11 +809,12 @@ language code (`cs`, `de`, `eo`, `es`, `fr`, `it`, `ko`, `nl`, `pl`, `ru`, `sv`,
809809
`tlh` is the deliberate best-effort exception. It is a hand-maintained novelty
810810
locale, remains available on all four surfaces when its files are valid, and
811811
uses English per-key fallback when they are incomplete. It is excluded from
812-
automatic translation planning so it consumes no model quota, and any
813-
`tlh`-specific catalog, completeness, literal-parity, registration, or
814-
generated-drift problem is reported as a warning rather than blocking CI or
815-
locale-sync. Every other locale and every shared, English-side pipeline failure
816-
remain hard failures.
812+
automatic translation planning so it consumes no model quota. Catalog parsing,
813+
surface registration, and generated-drift problems are reported as warnings
814+
rather than blocking CI or locale-sync. Completeness and literal parity are not
815+
checked for `tlh`: missing entries fall back to English, and imperfect novelty
816+
copy is accepted without a diagnostic. Every other locale and every shared,
817+
English-side pipeline failure remain hard failures.
817818

818819
That list of codes is itself pinned by
819820
`test_agents_md_lists_every_shipped_locale`: adding a language means adding its

scripts/generate_locales.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def _committed_text(path: Path) -> str:
250250
def check() -> int:
251251
"""Exit 1 naming every derived file that no longer matches the canon."""
252252
stale: list[str] = []
253+
best_effort_stale: list[str] = []
253254
for path, content in generated_files().items():
254255
committed = _committed_text(path)
255256
if committed != content:
@@ -260,6 +261,7 @@ def check() -> int:
260261
"out of sync; run python scripts/generate_locales.py to refresh it",
261262
file=sys.stderr,
262263
)
264+
best_effort_stale.append(relative)
263265
else:
264266
stale.append(relative)
265267
diff = difflib.unified_diff(
@@ -276,6 +278,12 @@ def check() -> int:
276278
file=sys.stderr,
277279
)
278280
return 1
281+
if best_effort_stale:
282+
print(
283+
"strict derived locale catalogs are in sync; best-effort locale "
284+
f"drift was reported above for: {best_effort_stale}"
285+
)
286+
return 0
279287
print("derived locale catalogs are in sync")
280288
return 0
281289

src/ha_mcp/settings_ui/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Any reachable HA instance works for `HOMEASSISTANT_URL` / `HOMEASSISTANT_TOKEN`.
3838
- `settings.js` — the client script, injected into `<script>__HA_MCP_JS__</script>`. Not served as a separate asset.
3939
- `settings.css` — the stylesheet, injected into `<style>__HA_MCP_CSS__</style>`.
4040
- `_i18n.py` and `locales/*.json` — auto-discovered translation catalogs. English is the per-key fallback for `messages`. Adding a language is not a single-file change: it ships on all four translated surfaces or not at all. Its `tool_groups` and `tools` sections must end up exact — the post-merge locale-sync workflow fills and verifies them, so a near-empty catalog may merge, though not a `meta`-only one: the `Decision` and `PredicateOp` words, the sentence those words are interpolated into, and one reader-addressing key are all checked ungated against the shipped catalogs — and `en.json` is the wrong starting point because it carries both of those empty by design. See `locales/README.md` for the procedure and the repository-root `AGENTS.md` § Translations for the rules CI enforces. The wheel, sdist and binary declarations match the locale directory by pattern, so a new catalog needs no packaging edit — but keep those patterns intact when touching packaging.
41-
- `tlh` is the only best-effort locale: it is excluded from machine translation and its locale-specific validation is warning-only. Invalid Klingon is skipped or falls back to English; every other locale remains strict. Keep this exception centralized in `_locale_policy.py`.
41+
- `tlh` is the only best-effort locale: it is excluded from machine translation; catalog, registration, and generated-drift problems warn, while completeness and literal parity are intentionally not checked. Invalid Klingon is skipped or falls back to English; every other locale remains strict. Keep this exception centralized in `_locale_policy.py`.
4242

4343
## Gotchas (read before editing)
4444

tests/addon/test_addon_structure.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import stat
77
import sys
8+
import warnings
89
from pathlib import Path
910

1011
import pytest
@@ -34,9 +35,10 @@ def _report_translation_issues(path: Path, issues: list[str]) -> None:
3435
return
3536
message = "; ".join(issues)
3637
if is_best_effort_locale(path.stem):
37-
print(
38-
f"::warning file={path}::best-effort locale {path.stem}: {message}",
39-
file=sys.stderr,
38+
warnings.warn(
39+
f"best-effort locale {path.stem} in {path}: {message}",
40+
pytest.PytestWarning,
41+
stacklevel=2,
4042
)
4143
return
4244
raise AssertionError(message)
@@ -50,12 +52,13 @@ def _report_translation_issues(path: Path, issues: list[str]) -> None:
5052
)
5153

5254

53-
def test_best_effort_addon_translation_issues_warn_instead_of_fail(
54-
capsys: pytest.CaptureFixture[str],
55-
) -> None:
55+
def test_best_effort_addon_translation_issues_warn_instead_of_fail() -> None:
5656
issues = ["missing configuration.example"]
57-
_report_translation_issues(Path("translations/tlh.yaml"), issues)
58-
assert "::warning file=translations/tlh.yaml" in capsys.readouterr().err
57+
with pytest.warns(
58+
pytest.PytestWarning,
59+
match=r"best-effort locale tlh.*missing configuration\.example",
60+
):
61+
_report_translation_issues(Path("translations/tlh.yaml"), issues)
5962

6063
with pytest.raises(AssertionError, match=r"missing configuration\.example"):
6164
_report_translation_issues(Path("translations/de.yaml"), issues)

tests/src/unit/test_generate_locales.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ def test_check_warns_for_stale_best_effort_output(
137137
)
138138

139139
assert generate_locales.check() == 0
140-
assert "warning" in capsys.readouterr().err.lower()
140+
captured = capsys.readouterr()
141+
assert "warning" in captured.err.lower()
142+
assert "best-effort locale drift was reported" in captured.out
141143

142144
def test_check_keeps_invalid_strict_output_hard(
143145
self,

tests/src/unit/test_settings_ui_js_behavior.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2309,14 +2309,6 @@ def _already_decided_cases() -> list[tuple[str, str]]:
23092309
return [(locale, outcome) for locale in locales for outcome in outcomes]
23102310

23112311

2312-
def test_best_effort_locales_are_outside_hard_js_copy_cases() -> None:
2313-
from ha_mcp.settings_ui._locale_policy import BEST_EFFORT_LOCALES
2314-
2315-
assert BEST_EFFORT_LOCALES.isdisjoint(
2316-
locale for locale, _ in _already_decided_cases()
2317-
)
2318-
2319-
23202312
class TestAlreadyDecidedCopy:
23212313
"""The 409 alert must read as one translated sentence.
23222314

0 commit comments

Comments
 (0)