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.
Files changed (182) hide show
  1. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/MANIFEST.in +1 -0
  2. {ha_mcp_dev-7.13.0.dev1968/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1971}/PKG-INFO +1 -1
  3. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/pyproject.toml +2 -2
  4. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/__init__.py +56 -15
  5. ha_mcp_dev-7.13.0.dev1971/src/ha_mcp/settings_ui/_i18n.py +244 -0
  6. ha_mcp_dev-7.13.0.dev1971/src/ha_mcp/settings_ui/locales/README.md +18 -0
  7. ha_mcp_dev-7.13.0.dev1971/src/ha_mcp/settings_ui/locales/en.json +422 -0
  8. ha_mcp_dev-7.13.0.dev1971/src/ha_mcp/settings_ui/locales/ru.json +540 -0
  9. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/settings.html +109 -102
  10. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/settings.js +560 -311
  11. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  12. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp_dev.egg-info/SOURCES.txt +4 -0
  13. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/LICENSE +0 -0
  14. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/README.md +0 -0
  15. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/_version.py +0 -0
  20. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/backup_manager.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/browser_landing.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/__init__.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/rest_client.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/supervisor_client.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/websocket_client.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/client/websocket_listener.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/config.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/errors.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/llm_exposure.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/__init__.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/approval_queue.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/evaluator.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/handlers.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/middleware.py +0 -0
  43. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/model.py +0 -0
  44. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/persistence.py +0 -0
  45. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/policy/value_sources.py +0 -0
  46. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/py.typed +0 -0
  47. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/read_only.py +0 -0
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  54. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  55. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  56. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  57. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/server.py +0 -0
  74. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  75. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  76. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  77. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  78. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  82. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_theme.py +0 -0
  83. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  84. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/settings_ui/settings.css +0 -0
  85. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/smoke_test.py +0 -0
  86. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  87. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/strict_bps.py +0 -0
  88. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/__init__.py +0 -0
  89. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/auto_backup.py +0 -0
  90. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/backup.py +0 -0
  91. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  92. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/component_api.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/component_devices.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/device_control.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/enhanced.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/helpers.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/__init__.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/base.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/matter.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/thread.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/radio/zwave.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/reference_validator.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/registry.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  115. {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
  116. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_addons.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_areas.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_calendar.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_camera.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_categories.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_code.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_dev.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_energy.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_entities.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_groups.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_hacs.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_history.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_integrations.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_labels.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_radio.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_registry.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_resources.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_search.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_service.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_services.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_system.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_themes.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_todo.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_traces.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_updates.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_utility.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/tools_zones.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/util_helpers.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/tools/validation_middleware.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/transforms/__init__.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/transforms/categorized_search.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/transport_security.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/update_check.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/__init__.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/config_hash.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/data_paths.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/domain_handlers.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/operation_manager.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/python_sandbox.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/skill_loader.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/utils/usage_logger.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/visibility/__init__.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/visibility/model.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/visibility/persistence.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp/visibility/resolver.py +0 -0
  176. {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
  177. {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
  178. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  179. {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
  180. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/tests/__init__.py +0 -0
  181. {ha_mcp_dev-7.13.0.dev1968 → ha_mcp_dev-7.13.0.dev1971}/tests/test_constants.py +0 -0
  182. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.13.0.dev1968
3
+ Version: 7.13.0.dev1971
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -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.dev1968"
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
- # three substitution markers — two filled once at import, one per request:
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 ("__HA_MCP_CSS__", "__HA_MCP_JS__", "__HA_MCP_THEME_PREFS__"):
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 the persisted theme prefs into the served settings page.
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
- payload = html.escape(
340
+ theme_payload = html.escape(
321
341
  json.dumps(_load_theme_prefs(), separators=(",", ":")), quote=True
322
342
  )
323
- rendered = _SETTINGS_HTML.replace("__HA_MCP_THEME_PREFS__", payload, 1)
324
- if "__HA_MCP_THEME_PREFS__" in rendered:
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
- # serving a page without server-side prefs.
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
- "server-prefs placeholder was not substituted; theme prefs "
331
- "will not survive fresh origins"
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(_: Request) -> HTMLResponse:
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(_: Request) -> HTMLResponse:
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.