ha-mcp-dev 7.13.0.dev1968__tar.gz → 7.13.0.dev1971__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/MANIFEST.in +1 -0
- {ha_mcp_dev-7.13.0.dev1968/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1971}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/pyproject.toml +2 -2
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/__init__.py +56 -15
- ha_mcp_dev-7.13.0.dev1971/src/ha_mcp/settings_ui/_i18n.py +244 -0
- ha_mcp_dev-7.13.0.dev1971/src/ha_mcp/settings_ui/locales/README.md +18 -0
- ha_mcp_dev-7.13.0.dev1971/src/ha_mcp/settings_ui/locales/en.json +422 -0
- ha_mcp_dev-7.13.0.dev1971/src/ha_mcp/settings_ui/locales/ru.json +540 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/settings.html +109 -102
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/settings.js +560 -311
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp_dev.egg-info/SOURCES.txt +4 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/tests/test_env_manager.py +0 -0
|
@@ -3,4 +3,5 @@ include LICENSE
|
|
|
3
3
|
include src/ha_mcp/settings_ui/settings.html
|
|
4
4
|
include src/ha_mcp/settings_ui/settings.js
|
|
5
5
|
include src/ha_mcp/settings_ui/settings.css
|
|
6
|
+
recursive-include src/ha_mcp/settings_ui/locales *.json README.md
|
|
6
7
|
recursive-include src/ha_mcp/resources *.md *.json
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.13.0.
|
|
7
|
+
version = "7.13.0.dev1971"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# Upper bound widened to <3.15 so the package installs on Home Assistant's
|
|
@@ -65,7 +65,7 @@ package-dir = {"" = "src", "tests" = "tests"}
|
|
|
65
65
|
packages = { find = { where = ["src", "."], include = ["ha_mcp*", "tests"] } }
|
|
66
66
|
|
|
67
67
|
[tool.setuptools.package-data]
|
|
68
|
-
ha_mcp = ["py.typed", "_pypi_marker", "settings_ui/settings.html", "settings_ui/settings.js", "settings_ui/settings.css", "resources/skills-vendor/**/*"]
|
|
68
|
+
ha_mcp = ["py.typed", "_pypi_marker", "settings_ui/settings.html", "settings_ui/settings.js", "settings_ui/settings.css", "settings_ui/locales/*.json", "settings_ui/locales/README.md", "resources/skills-vendor/**/*"]
|
|
69
69
|
|
|
70
70
|
[tool.mypy]
|
|
71
71
|
python_version = "3.13"
|
|
@@ -33,6 +33,12 @@ from ._handlers_server import (
|
|
|
33
33
|
)
|
|
34
34
|
from ._handlers_theme import build_theme_handlers
|
|
35
35
|
from ._handlers_tools import build_tools_handlers
|
|
36
|
+
from ._i18n import (
|
|
37
|
+
LOCALE_COOKIE,
|
|
38
|
+
build_payload,
|
|
39
|
+
select_locale,
|
|
40
|
+
serialize_payload,
|
|
41
|
+
)
|
|
36
42
|
from ._persistence import (
|
|
37
43
|
_atomic_write_json,
|
|
38
44
|
_get_backup_settings_override_path,
|
|
@@ -136,11 +142,14 @@ except OSError as exc: # pragma: no cover - packaging guard
|
|
|
136
142
|
|
|
137
143
|
# The settings page HTML lives in settings.html, extracted the same way as
|
|
138
144
|
# settings.js / settings.css (a real file for editor/HTML tooling). It carries
|
|
139
|
-
#
|
|
145
|
+
# six substitution markers — two filled once at import, four per request:
|
|
140
146
|
# __HA_MCP_CSS__ -> settings.css contents (inside <style>)
|
|
141
147
|
# __HA_MCP_JS__ -> settings.js contents (inside <script>)
|
|
142
148
|
# __HA_MCP_THEME_PREFS__ -> per-request server-seeded theme prefs JSON,
|
|
143
149
|
# substituted in _render_settings_html()
|
|
150
|
+
# __HA_MCP_I18N__ -> selected merged translation catalog JSON
|
|
151
|
+
# __HA_MCP_LANG__ -> selected locale code for the html lang attribute
|
|
152
|
+
# __HA_MCP_DIR__ -> selected catalog text direction (ltr / rtl)
|
|
144
153
|
# Same import-time packaging dependency as settings.js/css (wheel package-data,
|
|
145
154
|
# MANIFEST.in, PyInstaller datas) and the same OSError guard -- but this loader
|
|
146
155
|
# raises RuntimeError, not the ImportError that settings.js/css raise.
|
|
@@ -158,7 +167,14 @@ except OSError as exc: # pragma: no cover - packaging guard
|
|
|
158
167
|
# versa) — str.replace() silently no-ops on an absent token, which would ship a
|
|
159
168
|
# page missing its CSS, JS, or server-seeded theme prefs. Assert all three
|
|
160
169
|
# markers are present.
|
|
161
|
-
for _sentinel in (
|
|
170
|
+
for _sentinel in (
|
|
171
|
+
"__HA_MCP_CSS__",
|
|
172
|
+
"__HA_MCP_JS__",
|
|
173
|
+
"__HA_MCP_THEME_PREFS__",
|
|
174
|
+
"__HA_MCP_I18N__",
|
|
175
|
+
"__HA_MCP_LANG__",
|
|
176
|
+
"__HA_MCP_DIR__",
|
|
177
|
+
):
|
|
162
178
|
if _sentinel not in _settings_html_template:
|
|
163
179
|
raise RuntimeError(
|
|
164
180
|
f"settings.html is out of sync: sentinel {_sentinel} not found. "
|
|
@@ -307,8 +323,8 @@ def _build_visibility_handlers(*, data_dir: Path) -> dict[str, Any]:
|
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
|
|
310
|
-
def _render_settings_html() -> str:
|
|
311
|
-
"""Substitute
|
|
326
|
+
def _render_settings_html(request: Request | None = None) -> str:
|
|
327
|
+
"""Substitute theme preferences and the request-selected locale.
|
|
312
328
|
|
|
313
329
|
The ``server-prefs`` head script carries a ``data-prefs`` attribute
|
|
314
330
|
with a placeholder token; request-time substitution keeps
|
|
@@ -316,19 +332,44 @@ def _render_settings_html() -> str:
|
|
|
316
332
|
body itself parseable at rest for the script-surface tests). Values
|
|
317
333
|
are sanitized enums / vetted hex colors and the JSON is HTML-escaped,
|
|
318
334
|
so the attribute cannot break out of its quoting context.
|
|
335
|
+
|
|
336
|
+
Locale priority is a user-selected cookie, the Home Assistant language
|
|
337
|
+
hint supplied by the embedded panel, the browser's Accept-Language header,
|
|
338
|
+
then English. Only the selected merged catalog is embedded in the page.
|
|
319
339
|
"""
|
|
320
|
-
|
|
340
|
+
theme_payload = html.escape(
|
|
321
341
|
json.dumps(_load_theme_prefs(), separators=(",", ":")), quote=True
|
|
322
342
|
)
|
|
323
|
-
|
|
324
|
-
|
|
343
|
+
locale = select_locale(
|
|
344
|
+
cookie_locale=request.cookies.get(LOCALE_COOKIE) if request else None,
|
|
345
|
+
ha_language=request.query_params.get("ha_lang") if request else None,
|
|
346
|
+
accept_language=request.headers.get("accept-language") if request else None,
|
|
347
|
+
)
|
|
348
|
+
locale_payload = build_payload(locale)
|
|
349
|
+
i18n_payload = serialize_payload(locale_payload)
|
|
350
|
+
direction = locale_payload["dir"]
|
|
351
|
+
rendered = (
|
|
352
|
+
_SETTINGS_HTML.replace("__HA_MCP_THEME_PREFS__", theme_payload, 1)
|
|
353
|
+
.replace("__HA_MCP_I18N__", i18n_payload, 1)
|
|
354
|
+
.replace("__HA_MCP_LANG__", locale, 1)
|
|
355
|
+
.replace("__HA_MCP_DIR__", direction, 1)
|
|
356
|
+
)
|
|
357
|
+
if any(
|
|
358
|
+
sentinel in rendered
|
|
359
|
+
for sentinel in (
|
|
360
|
+
"__HA_MCP_THEME_PREFS__",
|
|
361
|
+
"__HA_MCP_I18N__",
|
|
362
|
+
"__HA_MCP_LANG__",
|
|
363
|
+
"__HA_MCP_DIR__",
|
|
364
|
+
)
|
|
365
|
+
):
|
|
325
366
|
# str.replace silently no-ops when the placeholder vanishes in a
|
|
326
367
|
# refactor; the unit contract test catches that in CI, this line
|
|
327
|
-
# catches it loudly in a live deployment instead of quietly
|
|
328
|
-
#
|
|
368
|
+
# catches it loudly in a live deployment instead of quietly serving a
|
|
369
|
+
# page without server-side preferences or translations.
|
|
329
370
|
logger.error(
|
|
330
|
-
"
|
|
331
|
-
"
|
|
371
|
+
"settings-page request placeholder was not substituted; theme "
|
|
372
|
+
"preferences or translations may be unavailable"
|
|
332
373
|
)
|
|
333
374
|
return rendered
|
|
334
375
|
|
|
@@ -363,11 +404,11 @@ def build_settings_handlers(
|
|
|
363
404
|
so the served page is identical regardless of transport.
|
|
364
405
|
"""
|
|
365
406
|
|
|
366
|
-
async def _root_page(
|
|
367
|
-
return HTMLResponse(_render_settings_html())
|
|
407
|
+
async def _root_page(request: Request) -> HTMLResponse:
|
|
408
|
+
return HTMLResponse(_render_settings_html(request))
|
|
368
409
|
|
|
369
|
-
async def _settings_page(
|
|
370
|
-
return HTMLResponse(_render_settings_html())
|
|
410
|
+
async def _settings_page(request: Request) -> HTMLResponse:
|
|
411
|
+
return HTMLResponse(_render_settings_html(request))
|
|
371
412
|
|
|
372
413
|
handlers: dict[str, Any] = {
|
|
373
414
|
"root_page": _root_page,
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Translation catalog loading and locale selection for the settings UI.
|
|
2
|
+
|
|
3
|
+
Catalogs are discovered from ``locales/*.json`` so adding a language does not
|
|
4
|
+
require Python or JavaScript changes. English is the canonical fallback;
|
|
5
|
+
individual translations may be incomplete and safely inherit missing values.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
DEFAULT_LOCALE = "en"
|
|
16
|
+
LOCALE_COOKIE = "ha_mcp_locale"
|
|
17
|
+
LOCALES_DIR = Path(__file__).parent / "locales"
|
|
18
|
+
|
|
19
|
+
_PLACEHOLDER_RE = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validate_string_map(value: Any, *, context: str) -> dict[str, str]:
|
|
23
|
+
"""Return a validated ``str -> str`` catalog section."""
|
|
24
|
+
if value is None:
|
|
25
|
+
return {}
|
|
26
|
+
if not isinstance(value, dict):
|
|
27
|
+
raise ValueError(f"{context} must be an object")
|
|
28
|
+
result: dict[str, str] = {}
|
|
29
|
+
for key, text in value.items():
|
|
30
|
+
if not isinstance(key, str) or not isinstance(text, str):
|
|
31
|
+
raise ValueError(f"{context} must contain only string keys and values")
|
|
32
|
+
result[key] = text
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _validate_tools(value: Any, *, context: str) -> dict[str, dict[str, str]]:
|
|
37
|
+
"""Return validated optional per-tool UI translations."""
|
|
38
|
+
if value is None:
|
|
39
|
+
return {}
|
|
40
|
+
if not isinstance(value, dict):
|
|
41
|
+
raise ValueError(f"{context} must be an object")
|
|
42
|
+
result: dict[str, dict[str, str]] = {}
|
|
43
|
+
for tool_name, tool_values in value.items():
|
|
44
|
+
if not isinstance(tool_name, str) or not isinstance(tool_values, dict):
|
|
45
|
+
raise ValueError(f"{context} entries must be objects keyed by tool name")
|
|
46
|
+
translated: dict[str, str] = {}
|
|
47
|
+
for field in ("title", "description"):
|
|
48
|
+
field_value = tool_values.get(field)
|
|
49
|
+
if field_value is None:
|
|
50
|
+
continue
|
|
51
|
+
if not isinstance(field_value, str):
|
|
52
|
+
raise ValueError(f"{context}.{tool_name}.{field} must be a string")
|
|
53
|
+
translated[field] = field_value
|
|
54
|
+
unknown = set(tool_values) - {"title", "description"}
|
|
55
|
+
if unknown:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"{context}.{tool_name} has unsupported fields: {sorted(unknown)}"
|
|
58
|
+
)
|
|
59
|
+
result[tool_name] = translated
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_catalogs(directory: Path = LOCALES_DIR) -> dict[str, dict[str, Any]]:
|
|
64
|
+
"""Load and validate every JSON translation catalog in ``directory``."""
|
|
65
|
+
catalogs: dict[str, dict[str, Any]] = {}
|
|
66
|
+
try:
|
|
67
|
+
paths = sorted(directory.glob("*.json"))
|
|
68
|
+
except OSError as exc: # pragma: no cover - packaging guard
|
|
69
|
+
raise ImportError(
|
|
70
|
+
f"Unable to enumerate settings UI locales at {directory}"
|
|
71
|
+
) from exc
|
|
72
|
+
|
|
73
|
+
for path in paths:
|
|
74
|
+
locale = path.stem.lower().replace("_", "-")
|
|
75
|
+
try:
|
|
76
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
77
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
78
|
+
raise ImportError(f"Invalid settings UI locale catalog: {path}") from exc
|
|
79
|
+
if not isinstance(raw, dict):
|
|
80
|
+
raise ValueError(f"Locale catalog {path} must contain a JSON object")
|
|
81
|
+
|
|
82
|
+
meta = raw.get("meta")
|
|
83
|
+
if not isinstance(meta, dict):
|
|
84
|
+
raise ValueError(f"Locale catalog {path} must define a meta object")
|
|
85
|
+
native_name = meta.get("native_name")
|
|
86
|
+
direction = meta.get("dir", "ltr")
|
|
87
|
+
if not isinstance(native_name, str) or not native_name.strip():
|
|
88
|
+
raise ValueError(f"Locale catalog {path} needs meta.native_name")
|
|
89
|
+
if direction not in ("ltr", "rtl"):
|
|
90
|
+
raise ValueError(f"Locale catalog {path} meta.dir must be ltr or rtl")
|
|
91
|
+
|
|
92
|
+
unknown_sections = set(raw) - {"meta", "messages", "tool_groups", "tools"}
|
|
93
|
+
if unknown_sections:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Locale catalog {path} has unsupported sections: "
|
|
96
|
+
f"{sorted(unknown_sections)}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
catalogs[locale] = {
|
|
100
|
+
"meta": {"native_name": native_name, "dir": direction},
|
|
101
|
+
"messages": _validate_string_map(
|
|
102
|
+
raw.get("messages"), context=f"{path.name}.messages"
|
|
103
|
+
),
|
|
104
|
+
"tool_groups": _validate_string_map(
|
|
105
|
+
raw.get("tool_groups"), context=f"{path.name}.tool_groups"
|
|
106
|
+
),
|
|
107
|
+
"tools": _validate_tools(raw.get("tools"), context=f"{path.name}.tools"),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if DEFAULT_LOCALE not in catalogs:
|
|
111
|
+
raise ImportError(
|
|
112
|
+
f"The settings UI requires {DEFAULT_LOCALE}.json in {directory}"
|
|
113
|
+
)
|
|
114
|
+
_validate_placeholder_parity(catalogs)
|
|
115
|
+
return catalogs
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _validate_placeholder_parity(catalogs: dict[str, dict[str, Any]]) -> None:
|
|
119
|
+
"""Reject translations that drop or invent ``{name}`` placeholders."""
|
|
120
|
+
english_messages = catalogs[DEFAULT_LOCALE]["messages"]
|
|
121
|
+
english_tools = catalogs[DEFAULT_LOCALE]["tools"]
|
|
122
|
+
for locale, catalog in catalogs.items():
|
|
123
|
+
if locale == DEFAULT_LOCALE:
|
|
124
|
+
continue
|
|
125
|
+
for key, translated in catalog["messages"].items():
|
|
126
|
+
source = english_messages.get(key)
|
|
127
|
+
if source is None:
|
|
128
|
+
continue
|
|
129
|
+
source_fields = set(_PLACEHOLDER_RE.findall(source))
|
|
130
|
+
translated_fields = set(_PLACEHOLDER_RE.findall(translated))
|
|
131
|
+
if source_fields != translated_fields:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Locale {locale} message {key!r} has placeholders "
|
|
134
|
+
f"{sorted(translated_fields)}, expected {sorted(source_fields)}"
|
|
135
|
+
)
|
|
136
|
+
for tool_name, translated_tool in catalog["tools"].items():
|
|
137
|
+
source_tool = english_tools.get(tool_name, {})
|
|
138
|
+
for field, translated in translated_tool.items():
|
|
139
|
+
source = source_tool.get(field)
|
|
140
|
+
if source is None:
|
|
141
|
+
continue
|
|
142
|
+
source_fields = set(_PLACEHOLDER_RE.findall(source))
|
|
143
|
+
translated_fields = set(_PLACEHOLDER_RE.findall(translated))
|
|
144
|
+
if source_fields != translated_fields:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
f"Locale {locale} tool {tool_name!r} field {field!r} "
|
|
147
|
+
f"has placeholders {sorted(translated_fields)}, expected "
|
|
148
|
+
f"{sorted(source_fields)}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
CATALOGS = load_catalogs()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def normalize_locale(
|
|
156
|
+
value: str | None, catalogs: dict[str, dict[str, Any]] = CATALOGS
|
|
157
|
+
) -> str | None:
|
|
158
|
+
"""Resolve a locale or regional locale to a supported catalog code."""
|
|
159
|
+
if not value:
|
|
160
|
+
return None
|
|
161
|
+
candidate = value.strip().lower().replace("_", "-")
|
|
162
|
+
if candidate in catalogs:
|
|
163
|
+
return candidate
|
|
164
|
+
base = candidate.split("-", 1)[0]
|
|
165
|
+
return base if base in catalogs else None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _accept_language_candidates(header: str | None) -> list[str]:
|
|
169
|
+
"""Return Accept-Language values ordered by descending quality."""
|
|
170
|
+
if not header:
|
|
171
|
+
return []
|
|
172
|
+
candidates: list[tuple[float, int, str]] = []
|
|
173
|
+
for index, item in enumerate(header.split(",")):
|
|
174
|
+
parts = [part.strip() for part in item.split(";")]
|
|
175
|
+
language = parts[0]
|
|
176
|
+
quality = 1.0
|
|
177
|
+
for part in parts[1:]:
|
|
178
|
+
if part.startswith("q="):
|
|
179
|
+
try:
|
|
180
|
+
quality = float(part[2:])
|
|
181
|
+
except ValueError:
|
|
182
|
+
quality = 0.0
|
|
183
|
+
if language and language != "*" and quality > 0:
|
|
184
|
+
candidates.append((quality, -index, language))
|
|
185
|
+
candidates.sort(reverse=True)
|
|
186
|
+
return [language for _, _, language in candidates]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def select_locale(
|
|
190
|
+
*,
|
|
191
|
+
cookie_locale: str | None = None,
|
|
192
|
+
ha_language: str | None = None,
|
|
193
|
+
accept_language: str | None = None,
|
|
194
|
+
catalogs: dict[str, dict[str, Any]] = CATALOGS,
|
|
195
|
+
) -> str:
|
|
196
|
+
"""Choose locale: explicit cookie, HA hint, browser header, then English."""
|
|
197
|
+
for value in (cookie_locale, ha_language):
|
|
198
|
+
if selected := normalize_locale(value, catalogs):
|
|
199
|
+
return selected
|
|
200
|
+
for value in _accept_language_candidates(accept_language):
|
|
201
|
+
if selected := normalize_locale(value, catalogs):
|
|
202
|
+
return selected
|
|
203
|
+
return DEFAULT_LOCALE
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def build_payload(
|
|
207
|
+
locale: str, catalogs: dict[str, dict[str, Any]] = CATALOGS
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
"""Build a single merged catalog payload for the rendered page."""
|
|
210
|
+
selected_locale = normalize_locale(locale, catalogs) or DEFAULT_LOCALE
|
|
211
|
+
english = catalogs[DEFAULT_LOCALE]
|
|
212
|
+
selected = catalogs[selected_locale]
|
|
213
|
+
|
|
214
|
+
tools: dict[str, dict[str, str]] = {
|
|
215
|
+
name: dict(values) for name, values in english["tools"].items()
|
|
216
|
+
}
|
|
217
|
+
for name, values in selected["tools"].items():
|
|
218
|
+
tools.setdefault(name, {}).update(values)
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"locale": selected_locale,
|
|
222
|
+
"dir": selected["meta"]["dir"],
|
|
223
|
+
"messages": {**english["messages"], **selected["messages"]},
|
|
224
|
+
"tool_groups": {**english["tool_groups"], **selected["tool_groups"]},
|
|
225
|
+
"tools": tools,
|
|
226
|
+
"languages": [
|
|
227
|
+
{
|
|
228
|
+
"code": code,
|
|
229
|
+
"native_name": catalog["meta"]["native_name"],
|
|
230
|
+
"dir": catalog["meta"]["dir"],
|
|
231
|
+
}
|
|
232
|
+
for code, catalog in sorted(catalogs.items())
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def serialize_payload(payload: dict[str, Any]) -> str:
|
|
238
|
+
"""Serialize JSON safely for an inline ``application/json`` script."""
|
|
239
|
+
return (
|
|
240
|
+
json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
|
241
|
+
.replace("&", "\\u0026")
|
|
242
|
+
.replace("<", "\\u003c")
|
|
243
|
+
.replace(">", "\\u003e")
|
|
244
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Settings UI translations
|
|
2
|
+
|
|
3
|
+
The settings page discovers every `*.json` catalog in this directory. To add a
|
|
4
|
+
language, copy `en.json`, rename it to the language code (for example
|
|
5
|
+
`it.json`), translate the values, and keep the keys and `{placeholders}`
|
|
6
|
+
unchanged. No Python or JavaScript registration is required.
|
|
7
|
+
|
|
8
|
+
Catalog sections:
|
|
9
|
+
|
|
10
|
+
- `meta.native_name`: language name shown in the selector.
|
|
11
|
+
- `meta.dir`: `ltr` or `rtl`.
|
|
12
|
+
- `messages`: interface labels, help text, notices, and runtime messages.
|
|
13
|
+
- `tool_groups`: optional translations keyed by the English MCP tool tag.
|
|
14
|
+
- `tools`: optional per-tool `title` and `description` overrides keyed by the
|
|
15
|
+
stable MCP tool name. Missing values fall back to the server-provided English.
|
|
16
|
+
|
|
17
|
+
English is always the fallback, so an incomplete catalog remains usable while
|
|
18
|
+
it is being expanded.
|