diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index a19757a55..9da8c472a 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -54,6 +54,7 @@ from ..const import ( ATTR_STAGE, ATTR_STARTUP, ATTR_STDIN, + ATTR_TANSLATIONS, ATTR_TIMEOUT, ATTR_TMPFS, ATTR_UART, @@ -185,6 +186,11 @@ class AddonModel(CoreSysAttributes, ABC): """Return repository of add-on.""" return self.data[ATTR_REPOSITORY] + @property + def translations(self) -> dict: + """Return add-on translations.""" + return self.data[ATTR_TANSLATIONS] + @property def latest_version(self) -> AwesomeVersion: """Return latest version of add-on.""" diff --git a/supervisor/addons/validate.py b/supervisor/addons/validate.py index 385f02337..e873eb43c 100644 --- a/supervisor/addons/validate.py +++ b/supervisor/addons/validate.py @@ -21,6 +21,7 @@ from ..const import ( ATTR_AUTO_UPDATE, ATTR_BOOT, ATTR_BUILD_FROM, + ATTR_CONFIGURATION, ATTR_DESCRIPTON, ATTR_DEVICES, ATTR_DEVICETREE, @@ -71,6 +72,7 @@ from ..const import ( ATTR_STATE, ATTR_STDIN, ATTR_SYSTEM, + ATTR_TANSLATIONS, ATTR_TIMEOUT, ATTR_TMPFS, ATTR_UART, @@ -343,12 +345,18 @@ SCHEMA_ADDON_USER = vol.Schema( ) +SCHEMA_ADDON_TRANSLATION = vol.Schema( + {vol.Optional(ATTR_CONFIGURATION): {str: str}}, extra=vol.REMOVE_EXTRA +) + + SCHEMA_ADDON_SYSTEM = vol.All( _migrate_addon_config(), _SCHEMA_ADDON_CONFIG.extend( { vol.Required(ATTR_LOCATON): str, vol.Required(ATTR_REPOSITORY): str, + vol.Optional(ATTR_TANSLATIONS, default={}): SCHEMA_ADDON_TRANSLATION, } ), ) diff --git a/supervisor/api/addons.py b/supervisor/api/addons.py index df94467b3..2d9e0365f 100644 --- a/supervisor/api/addons.py +++ b/supervisor/api/addons.py @@ -82,6 +82,7 @@ from ..const import ( ATTR_STARTUP, ATTR_STATE, ATTR_STDIN, + ATTR_TANSLATIONS, ATTR_UART, ATTR_UDEV, ATTR_UPDATE_AVAILABLE, @@ -265,6 +266,7 @@ class APIAddons(CoreSysAttributes): ATTR_SERVICES: _pretty_services(addon), ATTR_DISCOVERY: addon.discovery, ATTR_IP_ADDRESS: None, + ATTR_TANSLATIONS: addon.translations, ATTR_INGRESS: addon.with_ingress, ATTR_INGRESS_ENTRY: None, ATTR_INGRESS_URL: None, diff --git a/supervisor/const.py b/supervisor/const.py index 87c1d68d5..922ef8742 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -109,6 +109,7 @@ ATTR_CHANNEL = "channel" ATTR_CHASSIS = "chassis" ATTR_CLI = "cli" ATTR_CONFIG = "config" +ATTR_CONFIGURATION = "configuration" ATTR_CONNECTED = "connected" ATTR_CONNECTIONS = "connections" ATTR_CONTAINERS = "containers" @@ -271,6 +272,7 @@ ATTR_TIMEZONE = "timezone" ATTR_TITLE = "title" ATTR_TMPFS = "tmpfs" ATTR_TOTP = "totp" +ATTR_TANSLATIONS = "translations" ATTR_TYPE = "type" ATTR_UART = "uart" ATTR_UDEV = "udev" diff --git a/supervisor/store/data.py b/supervisor/store/data.py index 898b9fec2..98c63efbf 100644 --- a/supervisor/store/data.py +++ b/supervisor/store/data.py @@ -6,11 +6,12 @@ from typing import Any, Dict import voluptuous as vol from voluptuous.humanize import humanize_error -from ..addons.validate import SCHEMA_ADDON_CONFIG +from ..addons.validate import SCHEMA_ADDON_CONFIG, SCHEMA_ADDON_TRANSLATION from ..const import ( ATTR_LOCATON, ATTR_REPOSITORY, ATTR_SLUG, + ATTR_TANSLATIONS, FILE_SUFFIX_CONFIGURATION, REPOSITORY_CORE, REPOSITORY_LOCAL, @@ -129,6 +130,7 @@ class StoreData(CoreSysAttributes): # store addon_config[ATTR_REPOSITORY] = repository addon_config[ATTR_LOCATON] = str(addon.parent) + addon_config[ATTR_TANSLATIONS] = self._read_addon_translations(addon.parent) self.addons[addon_slug] = addon_config def _set_builtin_repositories(self): @@ -145,3 +147,29 @@ class StoreData(CoreSysAttributes): # local repository self.repositories[REPOSITORY_LOCAL] = builtin_data[REPOSITORY_LOCAL] + + def _read_addon_translations(self, addon_path: Path) -> dict: + """Read translations from add-ons folder.""" + translations_dir = addon_path / "translations" + translations = {} + + if not translations_dir.exists(): + return translations + + translation_files = [ + translation + for translation in translations_dir.glob("*") + if translation.suffix in FILE_SUFFIX_CONFIGURATION + ] + + for translation in translation_files: + try: + translations[translation.stem] = SCHEMA_ADDON_TRANSLATION( + read_json_or_yaml_file(translation) + ) + + except (JsonFileError, YamlFileError, vol.Invalid): + _LOGGER.warning("Can't read translations from %s", translation) + continue + + return translations diff --git a/tests/store/test_translation_load.py b/tests/store/test_translation_load.py new file mode 100644 index 000000000..b7aa17522 --- /dev/null +++ b/tests/store/test_translation_load.py @@ -0,0 +1,34 @@ +"""Test loading add-translation.""" +# pylint: disable=import-error,protected-access +import json +import os + +from ruamel.yaml import YAML + +from supervisor.coresys import CoreSys + +_YAML = YAML() +_YAML.allow_duplicate_keys = True + + +def test_loading_traslations(coresys: CoreSys, tmp_path): + """Test loading add-translation.""" + os.makedirs(tmp_path / "translations") + # no transaltions + assert coresys.store.data._read_addon_translations(tmp_path) == {} + + for file in ("en.json", "es.json"): + with open(tmp_path / "translations" / file, "w") as lang_file: + lang_file.write(json.dumps({"configuration": {"test": "test"}})) + + for file in ("no.yaml", "de.yaml"): + _YAML.dump( + {"configuration": {"test": "test"}}, tmp_path / "translations" / file + ) + + translations = coresys.store.data._read_addon_translations(tmp_path) + + assert translations["en"]["configuration"]["test"] == "test" + assert translations["es"]["configuration"]["test"] == "test" + assert translations["no"]["configuration"]["test"] == "test" + assert translations["de"]["configuration"]["test"] == "test"