diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index 19af428df2e..de89d28082b 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -22,5 +22,19 @@ "armed_away": "{entity_name} armed away", "armed_night": "{entity_name} armed night" } + }, + "state": { + "_": { + "armed": "Armed", + "disarmed": "Disarmed", + "armed_home": "Armed home", + "armed_away": "Armed away", + "armed_night": "Armed night", + "armed_custom_bypass": "Armed custom bypass", + "pending": "Pending", + "arming": "Arming", + "disarming": "Disarming", + "triggered": "Triggered" + } } } diff --git a/homeassistant/components/automation/strings.json b/homeassistant/components/automation/strings.json index c52c0f44f57..adcc505b145 100644 --- a/homeassistant/components/automation/strings.json +++ b/homeassistant/components/automation/strings.json @@ -1 +1,9 @@ -{ "title": "Automation" } +{ + "title": "Automation", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 45886a86ab8..35922081e9b 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -89,5 +89,87 @@ "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" } + }, + "state": { + "battery": { + "off": "Normal", + "on": "Low" + }, + "cold": { + "off": "[%key:component::binary_sensor::state::battery::off%]", + "on": "Cold" + }, + "connectivity": { + "off": "[%key:common::state::disconnected%]", + "on": "[%key:common::state::connected%]" + }, + "door": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + }, + "garage_door": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + }, + "gas": { + "off": "Clear", + "on": "Detected" + }, + "heat": { + "off": "[%key:component::binary_sensor::state::battery::off%]", + "on": "Hot" + }, + "lock": { + "off": "[%key:common::state::locked%]", + "on": "[%key:common::state::unlocked%]" + }, + "moisture": { + "off": "Dry", + "on": "Wet" + }, + "motion": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "occupancy": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "opening": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + }, + "presence": { + "off": "[%key:component::device_tracker::state::not_home%]", + "on": "[%key:component::device_tracker::state::home%]" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "Safe", + "on": "Unsafe" + }, + "smoke": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "sound": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "vibration": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "window": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + }, + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } } } diff --git a/homeassistant/components/calendar/strings.json b/homeassistant/components/calendar/strings.json index 6c452164a39..3af9a78e607 100644 --- a/homeassistant/components/calendar/strings.json +++ b/homeassistant/components/calendar/strings.json @@ -1 +1,9 @@ -{ "title": "Calendar" } +{ + "title": "Calendar", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/camera/strings.json b/homeassistant/components/camera/strings.json index 3f8b8cc718e..3b8767ec8cd 100644 --- a/homeassistant/components/camera/strings.json +++ b/homeassistant/components/camera/strings.json @@ -1 +1,10 @@ -{ "title": "Camera" } +{ + "title": "Camera", + "state": { + "_": { + "recording": "Recording", + "streaming": "Streaming", + "idle": "[%key:common::state::idle%]" + } + } +} diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index d1c9821f892..1caf184b998 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -14,5 +14,16 @@ "set_hvac_mode": "Change HVAC mode on {entity_name}", "set_preset_mode": "Change preset on {entity_name}" } + }, + "state": { + "_": { + "off": "[%key:common::state::off%]", + "heat": "Heat", + "cool": "Cool", + "heat_cool": "Heat/Cool", + "auto": "Auto", + "dry": "Dry", + "fan_only": "Fan only" + } } } diff --git a/homeassistant/components/configurator/strings.json b/homeassistant/components/configurator/strings.json index 4231e0268a0..570c18d3cde 100644 --- a/homeassistant/components/configurator/strings.json +++ b/homeassistant/components/configurator/strings.json @@ -1 +1,9 @@ -{ "title": "Configurator" } +{ + "title": "Configurator", + "state": { + "_": { + "configure": "Configure", + "configured": "Configured" + } + } +} diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index df20bd5ca9a..de52614891f 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -25,5 +25,14 @@ "position": "{entity_name} position changes", "tilt_position": "{entity_name} tilt position changes" } + }, + "state": { + "_": { + "open": "[%key:common::state::open%]", + "opening": "Opening", + "closed": "[%key:common::state::closed%]", + "closing": "Closing", + "stopped": "Stopped" + } } } diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index 7af9dd35479..58aef884536 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -5,5 +5,11 @@ "is_home": "{entity_name} is home", "is_not_home": "{entity_name} is not home" } + }, + "state": { + "_": { + "home": "[%key:common::state::home%]", + "not_home": "[%key:common::state::not_home%]" + } } } diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index b7925e0f728..7ec4eebea7e 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -13,5 +13,11 @@ "turn_on": "Turn on {entity_name}", "turn_off": "Turn off {entity_name}" } + }, + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } } } diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index fd98cea97e5..3f015741230 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -1 +1,17 @@ -{ "title": "Group" } +{ + "title": "Group", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]", + "home": "[%key:component::device_tracker::state::home%]", + "not_home": "[%key:component::device_tracker::state::not_home%]", + "open": "[%key:common::state::open%]", + "closed": "[%key:common::state::closed%]", + "locked": "[%key:common::state::locked%]", + "unlocked": "[%key:common::state::unlocked%]", + "ok": "[%key:component::binary_sensor::state::problem::off%]", + "problem": "[%key:component::binary_sensor::state::problem::on%]" + } + } +} diff --git a/homeassistant/components/input_boolean/strings.json b/homeassistant/components/input_boolean/strings.json index b024071faf3..a32958592f2 100644 --- a/homeassistant/components/input_boolean/strings.json +++ b/homeassistant/components/input_boolean/strings.json @@ -1 +1,9 @@ -{ "title": "Input boolean" } +{ + "title": "Input boolean", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 81a4aaeb690..ec0309958e7 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -17,5 +17,11 @@ "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" } + }, + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } } } diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index f7296c9a4db..9e4c4ea726a 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -14,5 +14,11 @@ "locked": "{entity_name} locked", "unlocked": "{entity_name} unlocked" } + }, + "state": { + "_": { + "locked": "[%key:common::state::locked%]", + "unlocked": "[%key:common::state::unlocked%]" + } } } diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 51be96633bc..14f1eea131c 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -8,5 +8,15 @@ "is_paused": "{entity_name} is paused", "is_playing": "{entity_name} is playing" } + }, + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]", + "playing": "Playing", + "paused": "[%key:common::state::paused%]", + "idle": "[%key:common::state::idle%]", + "standby": "[%key:common::state::standby%]" + } } } diff --git a/homeassistant/components/person/strings.json b/homeassistant/components/person/strings.json index f7a77f0d815..c94499d92f5 100644 --- a/homeassistant/components/person/strings.json +++ b/homeassistant/components/person/strings.json @@ -1 +1,9 @@ -{ "title": "Person" } +{ + "title": "Person", + "state": { + "_": { + "home": "[%key:common::state::home%]", + "not_home": "[%key:common::state::not_home%]" + } + } +} diff --git a/homeassistant/components/plant/strings.json b/homeassistant/components/plant/strings.json index 52e1d8165e2..2478564ca88 100644 --- a/homeassistant/components/plant/strings.json +++ b/homeassistant/components/plant/strings.json @@ -1 +1,9 @@ -{ "title": "Plant Monitor" } +{ + "title": "Plant Monitor", + "state": { + "_": { + "ok": "[%key:component::binary_sensor::state::problem::off%]", + "problem": "[%key:component::binary_sensor::state::problem::on%]" + } + } +} diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index c8945c8e49a..eb9edcd450f 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -1 +1,9 @@ -{ "title": "Remote" } +{ + "title": "Remote", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/script/strings.json b/homeassistant/components/script/strings.json index b261c40510d..2d39b6ac633 100644 --- a/homeassistant/components/script/strings.json +++ b/homeassistant/components/script/strings.json @@ -1 +1,9 @@ -{ "title": "Script" } +{ + "title": "Script", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 15d52149e18..024f942280c 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -23,5 +23,11 @@ "timestamp": "{entity_name} timestamp changes", "value": "{entity_name} value changes" } + }, + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } } } diff --git a/homeassistant/components/sun/strings.json b/homeassistant/components/sun/strings.json index 5083d17aca7..980879b95cb 100644 --- a/homeassistant/components/sun/strings.json +++ b/homeassistant/components/sun/strings.json @@ -1 +1,9 @@ -{ "title": "Sun" } +{ + "title": "Sun", + "state": { + "_": { + "above_horizon": "Above horizon", + "below_horizon": "Below horizon" + } + } +} diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 1ccf7aac06b..45fa08ab1d6 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -14,5 +14,11 @@ "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" } + }, + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } } } diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json new file mode 100644 index 00000000000..985cea0aa6e --- /dev/null +++ b/homeassistant/components/timer/strings.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "active": "[%key:common::state::active%]", + "idle": "[%key:common::state::idle%]", + "paused": "[%key:common::state::paused%]" + } + } +} diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index aa02e3046e1..033946735f7 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -13,5 +13,17 @@ "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" } + }, + "state": { + "_": { + "cleaning": "Cleaning", + "docked": "Docked", + "error": "Error", + "idle": "[%key:common::state::idle%]", + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]", + "paused": "[%key:common::state::paused%]", + "returning": "Returning to dock" + } } } diff --git a/homeassistant/components/weather/strings.json b/homeassistant/components/weather/strings.json new file mode 100644 index 00000000000..c4764beb5b6 --- /dev/null +++ b/homeassistant/components/weather/strings.json @@ -0,0 +1,21 @@ +{ + "state": { + "_": { + "clear-night": "Clear, night", + "cloudy": "Cloudy", + "exceptional": "Exceptional", + "fog": "Fog", + "hail": "Hail", + "lightning": "Lightning", + "lightning-rainy": "Lightning, rainy", + "partlycloudy": "Partly cloudy", + "pouring": "Pouring", + "rainy": "Rainy", + "snowy": "Snowy", + "snowy-rainy": "Snowy, rainy", + "sunny": "Sunny", + "windy": "Windy", + "windy-variant": "Windy" + } + } +} diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json index f1bbad0ec0f..3c62a89dc25 100644 --- a/homeassistant/components/zwave/strings.json +++ b/homeassistant/components/zwave/strings.json @@ -17,5 +17,17 @@ "already_configured": "Z-Wave is already configured", "one_instance_only": "Component only supports one Z-Wave instance" } + }, + "state": { + "query_stage": { + "initializing": "[%key:component::zwave::state::_::initializing%]", + "dead": "[%key:component::zwave::state::_::dead%]" + }, + "_": { + "initializing": "Initializing", + "dead": "Dead", + "sleeping": "Sleeping", + "ready": "Ready" + } } } diff --git a/homeassistant/strings.json b/homeassistant/strings.json new file mode 100644 index 00000000000..129b633fbf3 --- /dev/null +++ b/homeassistant/strings.json @@ -0,0 +1,20 @@ +{ + "common": { + "state": { + "off": "Off", + "on": "On", + "open": "Open", + "closed": "Closed", + "connected": "Connected", + "disconnected": "Disconnected", + "locked": "Locked", + "unlocked": "Unlocked", + "active": "Active", + "idle": "Idle", + "standby": "Standby", + "paused": "Paused", + "home": "Home", + "not_home": "Away" + } + } +} diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index e315ddf033b..8d53ad5e0f9 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -2,8 +2,10 @@ from functools import partial import json import logging +import re from typing import Dict +from script.translations import upload import voluptuous as vol from voluptuous.humanize import humanize_error @@ -18,6 +20,8 @@ UNDEFINED = 0 REQUIRED = 1 REMOVED = 2 +RE_REFERENCE = r"\[\%key:(.+)\%\]" + REMOVED_TITLE_MSG = ( "config.title key has been moved out of config and into the root of strings.json. " "Starting Home Assistant 0.109 you only need to define this key in the root " @@ -26,6 +30,19 @@ REMOVED_TITLE_MSG = ( ) +def find_references(strings, prefix, found): + """Find references.""" + for key, value in strings.items(): + if isinstance(value, dict): + find_references(value, f"{prefix}::{key}", found) + continue + + match = re.match(RE_REFERENCE, value) + + if match: + found.append({"source": f"{prefix}::{key}", "ref": match.groups()[0]}) + + def removed_title_validator(config, integration, value): """Mark removed title.""" if not config.specific_integrations: @@ -36,6 +53,14 @@ def removed_title_validator(config, integration, value): return value +def lowercase_validator(value): + """Validate value is lowercase.""" + if value.lower() != value: + raise vol.Invalid("Needs to be lowercase") + + return value + + def gen_data_entry_schema( *, config: Config, @@ -92,7 +117,8 @@ def gen_strings_schema(config: Config, integration: Integration): vol.Optional("trigger_subtype"): {str: str}, }, vol.Optional("state"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys(str) + cv.schema_with_slug_keys(str, slug_validator=lowercase_validator), + slug_validator=vol.Any("_", cv.slug), ), } ) @@ -115,10 +141,23 @@ def gen_auth_schema(config: Config, integration: Integration): def gen_platform_strings_schema(config: Config, integration: Integration): - """Generate platform strings schema like strings.sensor.json.""" + """Generate platform strings schema like strings.sensor.json. + + Example of valid data: + { + "state": { + "moon__phase": { + "full": "Full" + } + } + } + """ def device_class_validator(value): - """Key validator.""" + """Key validator for platorm states. + + Platform states are only allowed to provide states for device classes they prefix. + """ if not value.startswith(f"{integration.domain}__"): raise vol.Invalid( f"Device class need to start with '{integration.domain}__'. Key {value} is invalid" @@ -128,14 +167,17 @@ def gen_platform_strings_schema(config: Config, integration: Integration): slugged = slugify(slug_friendly) if slug_friendly != slugged: - raise vol.Invalid(f"invalid device class {value}") + raise vol.Invalid( + f"invalid device class {value}. After domain__, needs to be all lowercase, no spaces." + ) return value return vol.Schema( { vol.Optional("state"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys(str), slug_validator=device_class_validator + cv.schema_with_slug_keys(str, slug_validator=lowercase_validator), + slug_validator=device_class_validator, ) } ) @@ -144,9 +186,10 @@ def gen_platform_strings_schema(config: Config, integration: Integration): ONBOARDING_SCHEMA = vol.Schema({vol.Required("area"): {str: str}}) -def validate_translation_file(config: Config, integration: Integration): +def validate_translation_file(config: Config, integration: Integration, all_strings): """Validate translation files for integration.""" strings_file = integration.path / "strings.json" + references = [] if strings_file.is_file(): strings = json.loads(strings_file.read_text()) @@ -164,6 +207,8 @@ def validate_translation_file(config: Config, integration: Integration): integration.add_error( "translations", f"Invalid strings.json: {humanize_error(strings, err)}" ) + else: + find_references(strings, "strings.json", references) for path in integration.path.glob("strings.*.json"): strings = json.loads(path.read_text()) @@ -177,9 +222,35 @@ def validate_translation_file(config: Config, integration: Integration): integration.add_warning("translations", msg) else: integration.add_error("translations", msg) + else: + find_references(strings, path.name, references) + + if config.specific_integrations: + return + + # Validate references + for reference in references: + parts = reference["ref"].split("::") + search = all_strings + key = parts.pop(0) + while parts and key in search: + search = search[key] + key = parts.pop(0) + + if parts: + print(key, list(search)) + integration.add_error( + "translations", + f"{reference['source']} contains invalid reference {reference['ref']}: Could not find {key}", + ) def validate(integrations: Dict[str, Integration], config: Config): """Handle JSON files inside integrations.""" + if config.specific_integrations: + all_strings = None + else: + all_strings = upload.generate_upload_data() + for integration in integrations.values(): - validate_translation_file(config, integration) + validate_translation_file(config, integration, all_strings) diff --git a/script/translations/migrate.py b/script/translations/migrate.py index 3ad7fd92904..fcf44e3dece 100644 --- a/script/translations/migrate.py +++ b/script/translations/migrate.py @@ -1,10 +1,14 @@ """Migrate things.""" import json +import pathlib from pprint import pprint +import re from .const import CORE_PROJECT_ID, FRONTEND_PROJECT_ID, INTEGRATIONS_DIR from .lokalise import get_api +FRONTEND_REPO = pathlib.Path("../frontend/") + def create_lookup(results): """Create a lookup table by key name.""" @@ -47,30 +51,53 @@ def rename_keys(project_id, to_migrate): pprint(lokalise.keys_bulk_update(updates)) +def list_keys_helper(lokalise, keys, params={}, *, validate=True): + """List keys in chunks so it doesn't exceed max URL length.""" + results = [] + + for i in range(0, len(keys), 100): + filter_keys = keys[i : i + 100] + from_key_data = lokalise.keys_list( + { + **params, + "filter_keys": ",".join(filter_keys), + "limit": len(filter_keys) + 1, + } + ) + + if len(from_key_data) == len(filter_keys) or not validate: + results.extend(from_key_data) + continue + + print( + f"Lookin up keys in Lokalise returns {len(from_key_data)} results, expected {len(keys)}" + ) + searched = set(filter_keys) + returned = set(create_lookup(from_key_data)) + print("Not found:", ", ".join(searched - returned)) + raise ValueError + + return results + + def migrate_project_keys_translations(from_project_id, to_project_id, to_migrate): """Migrate keys and translations from one project to another. to_migrate is Dict[from_key] = to_key. """ from_lokalise = get_api(from_project_id) - to_lokalise = get_api(to_project_id, True) - - from_key_data = from_lokalise.keys_list( - {"filter_keys": ",".join(to_migrate), "include_translations": 1} - ) - if len(from_key_data) != len(to_migrate): - print( - f"Lookin up keys in Lokalise returns {len(from_key_data)} results, expected {len(to_migrate)}" - ) - return - - from_key_lookup = create_lookup(from_key_data) + to_lokalise = get_api(to_project_id) # Fetch keys in target # We are going to skip migrating existing keys - to_key_data = to_lokalise.keys_list( - {"filter_keys": ",".join(to_migrate.values()), "include_translations": 1} - ) + print("Checking which target keys exist..") + try: + to_key_data = list_keys_helper( + to_lokalise, list(to_migrate.values()), validate=False + ) + except ValueError: + return + existing = set(create_lookup(to_key_data)) missing = [key for key in to_migrate.values() if key not in existing] @@ -79,6 +106,19 @@ def migrate_project_keys_translations(from_project_id, to_project_id, to_migrate print("All keys to migrate exist already, nothing to do") return + # Fetch keys whose translations we're importing + print("Fetch translations that we're importing..") + try: + from_key_data = list_keys_helper( + from_lokalise, + [key for key, value in to_migrate.items() if value not in existing], + {"include_translations": 1}, + ) + except ValueError: + return + + from_key_lookup = create_lookup(from_key_data) + print("Creating", ", ".join(missing)) to_key_lookup = create_lookup( to_lokalise.keys_create( @@ -169,24 +209,145 @@ def interactive_update(): print() +STATE_REWRITE = { + "Off": "[%key:common::state::off%]", + "On": "[%key:common::state::on%]", + "Unknown": "[%key:common::state::unknown%]", + "Unavailable": "[%key:common::state::unavailable%]", + "Open": "[%key:common::state::open%]", + "Closed": "[%key:common::state::closed%]", + "Connected": "[%key:common::state::connected%]", + "Disconnected": "[%key:common::state::disconnected%]", + "Locked": "[%key:common::state::locked%]", + "Unlocked": "[%key:common::state::unlocked%]", + "Active": "[%key:common::state::active%]", + "active": "[%key:common::state::active%]", + "Standby": "[%key:common::state::standby%]", + "Idle": "[%key:common::state::idle%]", + "idle": "[%key:common::state::idle%]", + "Paused": "[%key:common::state::paused%]", + "paused": "[%key:common::state::paused%]", + "Home": "[%key:common::state::home%]", + "Away": "[%key:common::state::not_home%]", + "[%key:state::default::off%]": "[%key:common::state::off%]", + "[%key:state::default::on%]": "[%key:common::state::on%]", + "[%key:state::cover::open%]": "[%key:common::state::open%]", + "[%key:state::cover::closed%]": "[%key:common::state::closed%]", + "[%key:state::lock::locked%]": "[%key:common::state::locked%]", + "[%key:state::lock::unlocked%]": "[%key:common::state::unlocked%]", +} +SKIP_DOMAIN = {"default", "scene"} +STATES_WITH_DEV_CLASS = {"binary_sensor", "zwave"} +GROUP_DELETE = {"opening", "closing", "stopped"} # They don't exist + + +def find_frontend_states(): + """Find frontend states. + + Source key -> target key + Add key to integrations strings.json + """ + frontend_states = json.loads( + (FRONTEND_REPO / "src/translations/en.json").read_text() + )["state"] + + # domain => state object + to_write = {} + to_migrate = {} + + for domain, states in frontend_states.items(): + if domain in SKIP_DOMAIN: + continue + + to_key_base = f"component::{domain}::state" + from_key_base = f"state::{domain}" + + if domain in STATES_WITH_DEV_CLASS: + + domain_to_write = dict(states) + + for device_class, dev_class_states in domain_to_write.items(): + to_device_class = "_" if device_class == "default" else device_class + for key in dev_class_states: + to_migrate[ + f"{from_key_base}::{device_class}::{key}" + ] = f"{to_key_base}::{to_device_class}::{key}" + + # Rewrite "default" device class to _ + if "default" in domain_to_write: + domain_to_write["_"] = domain_to_write.pop("default") + + else: + if domain == "group": + for key in GROUP_DELETE: + states.pop(key) + + domain_to_write = {"_": states} + + for key in states: + to_migrate[f"{from_key_base}::{key}"] = f"{to_key_base}::_::{key}" + + # Map out common values with + for dev_class_states in domain_to_write.values(): + for key, value in dev_class_states.copy().items(): + if value in STATE_REWRITE: + dev_class_states[key] = STATE_REWRITE[value] + continue + + match = re.match(r"\[\%key:state::(\w+)::(.+)\%\]", value) + + if not match: + continue + + dev_class_states[key] = "[%key:component::{}::state::{}%]".format( + *match.groups() + ) + + to_write[domain] = domain_to_write + + for domain, state in to_write.items(): + strings = INTEGRATIONS_DIR / domain / "strings.json" + if strings.is_file(): + content = json.loads(strings.read_text()) + else: + content = {} + + content["state"] = state + strings.write_text(json.dumps(content, indent=2) + "\n") + + pprint(to_migrate) + + print() + while input("Type YES to confirm: ") != "YES": + pass + + migrate_project_keys_translations(FRONTEND_PROJECT_ID, CORE_PROJECT_ID, to_migrate) + + def run(): """Migrate translations.""" - rename_keys( - CORE_PROJECT_ID, - { - "component::moon::platform::sensor::state::new_moon": "component::moon::platform::sensor::state::moon__phase::new_moon", - "component::moon::platform::sensor::state::waxing_crescent": "component::moon::platform::sensor::state::moon__phase::waxing_crescent", - "component::moon::platform::sensor::state::first_quarter": "component::moon::platform::sensor::state::moon__phase::first_quarter", - "component::moon::platform::sensor::state::waxing_gibbous": "component::moon::platform::sensor::state::moon__phase::waxing_gibbous", - "component::moon::platform::sensor::state::full_moon": "component::moon::platform::sensor::state::moon__phase::full_moon", - "component::moon::platform::sensor::state::waning_gibbous": "component::moon::platform::sensor::state::moon__phase::waning_gibbous", - "component::moon::platform::sensor::state::last_quarter": "component::moon::platform::sensor::state::moon__phase::last_quarter", - "component::moon::platform::sensor::state::waning_crescent": "component::moon::platform::sensor::state::moon__phase::waning_crescent", - "component::season::platform::sensor::state::spring": "component::season::platform::sensor::state::season__season__::spring", - "component::season::platform::sensor::state::summer": "component::season::platform::sensor::state::season__season__::summer", - "component::season::platform::sensor::state::autumn": "component::season::platform::sensor::state::season__season__::autumn", - "component::season::platform::sensor::state::winter": "component::season::platform::sensor::state::season__season__::winter", - }, - ) + # Import new common keys + # migrate_project_keys_translations( + # FRONTEND_PROJECT_ID, + # CORE_PROJECT_ID, + # { + # "state::default::off": "common::state::off", + # "state::default::on": "common::state::on", + # "state::cover::open": "common::state::open", + # "state::cover::closed": "common::state::closed", + # "state::binary_sensor::connectivity::on": "common::state::connected", + # "state::binary_sensor::connectivity::off": "common::state::disconnected", + # "state::lock::locked": "common::state::locked", + # "state::lock::unlocked": "common::state::unlocked", + # "state::timer::active": "common::state::active", + # "state::camera::idle": "common::state::idle", + # "state::media_player::standby": "common::state::standby", + # "state::media_player::paused": "common::state::paused", + # "state::device_tracker::home": "common::state::home", + # "state::device_tracker::not_home": "common::state::not_home", + # }, + # ) + + find_frontend_states() return 0 diff --git a/script/translations/upload.py b/script/translations/upload.py index b22737519b4..844c706d064 100755 --- a/script/translations/upload.py +++ b/script/translations/upload.py @@ -51,7 +51,8 @@ def run_upload_docker(): def generate_upload_data(): """Generate the data for uploading.""" - translations = {"component": {}} + translations = json.loads((INTEGRATIONS_DIR.parent / "strings.json").read_text()) + translations["component"] = {} for path in INTEGRATIONS_DIR.glob(f"*{os.sep}strings*.json"): component = path.parent.name