"""``hermes plugins validate`` — language-pack checks (``provides_locales``). For every declared id the pack must ship ``locales/.yaml`` that parses and is text-only (a number, list or null leaf is an error: ``t()`` would silently drop it). Keys the English catalogs do not have are a WARNING naming them — a pack written against a newer or older Hermes still installs, it just carries dead keys. Core keys come from the bundled ``locales/en.yaml``; TUI/Desktop keys from ``locales/_keys.tui.json`` / ``locales/_keys.desktop.json`` (exported by the TS packages' build scripts). When an export is absent the key check for that surface is skipped, never failed. """ from __future__ import annotations import json import logging from pathlib import Path from typing import Any, Dict, List, Optional, Set from agent.i18n_layers import CORE_SURFACE, flatten, non_text_leaves, scan_locale_dir logger = logging.getLogger(__name__) _KEY_EXPORTS = {"tui": "_keys.tui.json", "desktop": "_keys.desktop.json"} def _bundled_locales_dir() -> Path: from agent.i18n import _locales_dir return _locales_dir() def reference_keys(surface: str) -> Optional[Set[str]]: """English key set for *surface*: flattened ``en.yaml`` for core, the JSON export for tui/desktop. ``None`` when the reference is unavailable (the check is then skipped).""" locales = _bundled_locales_dir() try: if surface == CORE_SURFACE: from agent.i18n_layers import parse_locale_file return set(parse_locale_file(locales / "en.yaml")) export = locales / _KEY_EXPORTS[surface] if not export.is_file(): return None data = json.loads(export.read_text(encoding="utf-8-sig")) keys = data.get("keys") if isinstance(data, dict) else data return {str(k) for k in keys} if isinstance(keys, list) else None except Exception as exc: # a broken reference is a Hermes bug, not the pack's logger.debug("i18n reference keys for %s unavailable: %s", surface, exc) return None def _load_document(path: Path) -> Any: import hermes_yaml as yaml with path.open("r", encoding="utf-8-sig") as handle: return yaml.safe_load(handle) def check_language_packs(report, manifest: dict, plugin_dir: Path) -> None: """Locale-pack admission checks; a no-op (single passing line) when nothing is declared.""" from hermes_cli.plugins_manifest import parse_provides_locales raw = manifest.get("provides_locales") declared, _meta = parse_provides_locales(raw, str(manifest.get("name") or plugin_dir.name)) if raw is None: return if not declared: report.add("locales", False, "provides_locales declares no valid language id (expected e.g. pl, pt-br)") return locales_dir = plugin_dir / "locales" found = scan_locale_dir(locales_dir) by_lang: Dict[str, List] = {} for lang_id, surface, path in found: by_lang.setdefault(lang_id, []).append((surface, path)) for lang_id in declared: files = by_lang.get(lang_id, []) if not any(surface == CORE_SURFACE for surface, _ in files): report.add(f"locale {lang_id}", False, f"provides_locales declares {lang_id!r} but locales/{lang_id}.yaml is missing") continue for surface, path in files: _check_locale_file(report, lang_id, surface, path) undeclared = sorted(set(by_lang) - set(declared)) if undeclared: report.warn(f"locales/ ships {', '.join(undeclared)} not listed in provides_locales " "(loaded anyway; declare them so users can see the pack's languages)") def _check_locale_file(report, lang_id: str, surface: str, path: Path) -> None: label = f"locale {lang_id}" if surface == CORE_SURFACE else f"locale {lang_id}.{surface}" try: document = _load_document(path) except Exception as exc: report.add(label, False, f"{path.name} failed to parse: {exc}") return if document is None: document = {} if not isinstance(document, dict): report.add(label, False, f"{path.name} must be a mapping, got {type(document).__name__}") return bad_leaves = non_text_leaves(document) if bad_leaves: report.add(label, False, f"{path.name} has non-text value(s) at: {', '.join(sorted(bad_leaves))}") return flat = flatten(document) reference = reference_keys(surface) if reference is None: report.add(label, True, f"{path.name}: {len(flat)} key(s); no {surface} key reference available, key check skipped") return unknown = sorted(set(flat) - reference) if unknown: report.warn(f"{path.name}: {len(unknown)} key(s) not in the English {surface} catalog (ignored at runtime): " + ", ".join(unknown)) report.add(label, True, f"{path.name}: {len(flat)} key(s), {len(flat) - len(unknown)} match the English {surface} catalog") __all__ = ["check_language_packs", "reference_keys"]