From 6119e790231eb04a2ede8d0e133de8724aae8cb2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Apr 2020 11:52:27 -0700 Subject: [PATCH] Migrate frontend translations of domains to backend (#34294) --- .../alarm_control_panel/strings.json | 1 + .../components/automation/strings.json | 1 + .../components/binary_sensor/strings.json | 1 + .../components/calendar/strings.json | 1 + homeassistant/components/camera/strings.json | 1 + homeassistant/components/climate/strings.json | 1 + .../components/configurator/strings.json | 1 + .../components/conversation/strings.json | 1 + homeassistant/components/cover/strings.json | 1 + .../components/device_tracker/strings.json | 1 + homeassistant/components/fan/strings.json | 1 + homeassistant/components/group/strings.json | 1 + .../components/image_processing/strings.json | 1 + .../components/input_boolean/strings.json | 1 + .../components/input_datetime/strings.json | 1 + .../components/input_number/strings.json | 1 + .../components/input_select/strings.json | 1 + .../components/input_text/strings.json | 1 + homeassistant/components/light/strings.json | 1 + homeassistant/components/lock/strings.json | 1 + homeassistant/components/mailbox/strings.json | 1 + .../components/media_player/strings.json | 1 + homeassistant/components/notify/strings.json | 1 + homeassistant/components/person/strings.json | 1 + homeassistant/components/plant/strings.json | 1 + .../components/proximity/strings.json | 1 + homeassistant/components/remote/strings.json | 1 + homeassistant/components/scene/strings.json | 1 + homeassistant/components/script/strings.json | 1 + homeassistant/components/sensor/strings.json | 1 + homeassistant/components/sun/strings.json | 1 + homeassistant/components/switch/strings.json | 1 + .../components/system_health/strings.json | 1 + homeassistant/components/updater/strings.json | 1 + homeassistant/components/vacuum/strings.json | 1 + script/translations/const.py | 3 +- script/translations/download.py | 4 +- script/translations/lokalise.py | 42 +++++- script/translations/migrate.py | 141 +++++++++++++++--- script/translations/upload.py | 4 +- 40 files changed, 199 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/automation/strings.json create mode 100644 homeassistant/components/calendar/strings.json create mode 100644 homeassistant/components/camera/strings.json create mode 100644 homeassistant/components/configurator/strings.json create mode 100644 homeassistant/components/conversation/strings.json create mode 100644 homeassistant/components/group/strings.json create mode 100644 homeassistant/components/image_processing/strings.json create mode 100644 homeassistant/components/input_boolean/strings.json create mode 100644 homeassistant/components/input_datetime/strings.json create mode 100644 homeassistant/components/input_number/strings.json create mode 100644 homeassistant/components/input_select/strings.json create mode 100644 homeassistant/components/input_text/strings.json create mode 100644 homeassistant/components/mailbox/strings.json create mode 100644 homeassistant/components/notify/strings.json create mode 100644 homeassistant/components/person/strings.json create mode 100644 homeassistant/components/plant/strings.json create mode 100644 homeassistant/components/proximity/strings.json create mode 100644 homeassistant/components/remote/strings.json create mode 100644 homeassistant/components/scene/strings.json create mode 100644 homeassistant/components/script/strings.json create mode 100644 homeassistant/components/sun/strings.json create mode 100644 homeassistant/components/system_health/strings.json create mode 100644 homeassistant/components/updater/strings.json diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index 928c8d2a2e2..19af428df2e 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -1,4 +1,5 @@ { + "title": "Alarm control panel", "device_automation": { "action_type": { "arm_away": "Arm {entity_name} away", diff --git a/homeassistant/components/automation/strings.json b/homeassistant/components/automation/strings.json new file mode 100644 index 00000000000..c52c0f44f57 --- /dev/null +++ b/homeassistant/components/automation/strings.json @@ -0,0 +1 @@ +{ "title": "Automation" } diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index f2b32f45304..45886a86ab8 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -1,4 +1,5 @@ { + "title": "Binary sensor", "device_automation": { "condition_type": { "is_bat_low": "{entity_name} battery is low", diff --git a/homeassistant/components/calendar/strings.json b/homeassistant/components/calendar/strings.json new file mode 100644 index 00000000000..6c452164a39 --- /dev/null +++ b/homeassistant/components/calendar/strings.json @@ -0,0 +1 @@ +{ "title": "Calendar" } diff --git a/homeassistant/components/camera/strings.json b/homeassistant/components/camera/strings.json new file mode 100644 index 00000000000..3f8b8cc718e --- /dev/null +++ b/homeassistant/components/camera/strings.json @@ -0,0 +1 @@ +{ "title": "Camera" } diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index ff071aed083..d1c9821f892 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -1,4 +1,5 @@ { + "title": "Climate", "device_automation": { "condition_type": { "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", diff --git a/homeassistant/components/configurator/strings.json b/homeassistant/components/configurator/strings.json new file mode 100644 index 00000000000..4231e0268a0 --- /dev/null +++ b/homeassistant/components/configurator/strings.json @@ -0,0 +1 @@ +{ "title": "Configurator" } diff --git a/homeassistant/components/conversation/strings.json b/homeassistant/components/conversation/strings.json new file mode 100644 index 00000000000..dc6f2b5f52b --- /dev/null +++ b/homeassistant/components/conversation/strings.json @@ -0,0 +1 @@ +{ "title": "Conversation" } diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index 90dac7c7d02..df20bd5ca9a 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -1,4 +1,5 @@ { + "title": "Cover", "device_automation": { "action_type": { "open": "Open {entity_name}", diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index 285bac2cb4b..7af9dd35479 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -1,4 +1,5 @@ { + "title": "Device tracker", "device_automation": { "condition_type": { "is_home": "{entity_name} is home", diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 98c3012c123..b7925e0f728 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -1,4 +1,5 @@ { + "title": "Fan", "device_automation": { "condition_type": { "is_on": "{entity_name} is on", diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json new file mode 100644 index 00000000000..fd98cea97e5 --- /dev/null +++ b/homeassistant/components/group/strings.json @@ -0,0 +1 @@ +{ "title": "Group" } diff --git a/homeassistant/components/image_processing/strings.json b/homeassistant/components/image_processing/strings.json new file mode 100644 index 00000000000..b635fb6aaea --- /dev/null +++ b/homeassistant/components/image_processing/strings.json @@ -0,0 +1 @@ +{ "title": "Image processing" } diff --git a/homeassistant/components/input_boolean/strings.json b/homeassistant/components/input_boolean/strings.json new file mode 100644 index 00000000000..b024071faf3 --- /dev/null +++ b/homeassistant/components/input_boolean/strings.json @@ -0,0 +1 @@ +{ "title": "Input boolean" } diff --git a/homeassistant/components/input_datetime/strings.json b/homeassistant/components/input_datetime/strings.json new file mode 100644 index 00000000000..8d51025070e --- /dev/null +++ b/homeassistant/components/input_datetime/strings.json @@ -0,0 +1 @@ +{ "title": "Input datetime" } diff --git a/homeassistant/components/input_number/strings.json b/homeassistant/components/input_number/strings.json new file mode 100644 index 00000000000..35bbbebbdd7 --- /dev/null +++ b/homeassistant/components/input_number/strings.json @@ -0,0 +1 @@ +{ "title": "Input number" } diff --git a/homeassistant/components/input_select/strings.json b/homeassistant/components/input_select/strings.json new file mode 100644 index 00000000000..c3cd5c0c71c --- /dev/null +++ b/homeassistant/components/input_select/strings.json @@ -0,0 +1 @@ +{ "title": "Input select" } diff --git a/homeassistant/components/input_text/strings.json b/homeassistant/components/input_text/strings.json new file mode 100644 index 00000000000..dac5995acad --- /dev/null +++ b/homeassistant/components/input_text/strings.json @@ -0,0 +1 @@ +{ "title": "Input text" } diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index fc089e64b36..81a4aaeb690 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -1,4 +1,5 @@ { + "title": "Light", "device_automation": { "action_type": { "brightness_decrease": "Decrease {entity_name} brightness", diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index 1645b78295d..f7296c9a4db 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -1,4 +1,5 @@ { + "title": "Lock", "device_automation": { "action_type": { "lock": "Lock {entity_name}", diff --git a/homeassistant/components/mailbox/strings.json b/homeassistant/components/mailbox/strings.json new file mode 100644 index 00000000000..84acd440044 --- /dev/null +++ b/homeassistant/components/mailbox/strings.json @@ -0,0 +1 @@ +{ "title": "Mailbox" } diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index e9cb812767b..51be96633bc 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -1,4 +1,5 @@ { + "title": "Media player", "device_automation": { "condition_type": { "is_on": "{entity_name} is on", diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json new file mode 100644 index 00000000000..b9f694e470e --- /dev/null +++ b/homeassistant/components/notify/strings.json @@ -0,0 +1 @@ +{ "title": "Notify" } diff --git a/homeassistant/components/person/strings.json b/homeassistant/components/person/strings.json new file mode 100644 index 00000000000..f7a77f0d815 --- /dev/null +++ b/homeassistant/components/person/strings.json @@ -0,0 +1 @@ +{ "title": "Person" } diff --git a/homeassistant/components/plant/strings.json b/homeassistant/components/plant/strings.json new file mode 100644 index 00000000000..d34066b4092 --- /dev/null +++ b/homeassistant/components/plant/strings.json @@ -0,0 +1 @@ +{ "title": "Plant" } diff --git a/homeassistant/components/proximity/strings.json b/homeassistant/components/proximity/strings.json new file mode 100644 index 00000000000..bc338149d3d --- /dev/null +++ b/homeassistant/components/proximity/strings.json @@ -0,0 +1 @@ +{ "title": "Proximity" } diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json new file mode 100644 index 00000000000..c8945c8e49a --- /dev/null +++ b/homeassistant/components/remote/strings.json @@ -0,0 +1 @@ +{ "title": "Remote" } diff --git a/homeassistant/components/scene/strings.json b/homeassistant/components/scene/strings.json new file mode 100644 index 00000000000..c92838ea322 --- /dev/null +++ b/homeassistant/components/scene/strings.json @@ -0,0 +1 @@ +{ "title": "Scene" } diff --git a/homeassistant/components/script/strings.json b/homeassistant/components/script/strings.json new file mode 100644 index 00000000000..b261c40510d --- /dev/null +++ b/homeassistant/components/script/strings.json @@ -0,0 +1 @@ +{ "title": "Script" } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index a05f57f4584..15d52149e18 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -1,4 +1,5 @@ { + "title": "Sensor", "device_automation": { "condition_type": { "is_battery_level": "Current {entity_name} battery level", diff --git a/homeassistant/components/sun/strings.json b/homeassistant/components/sun/strings.json new file mode 100644 index 00000000000..5083d17aca7 --- /dev/null +++ b/homeassistant/components/sun/strings.json @@ -0,0 +1 @@ +{ "title": "Sun" } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 77b842ba078..1ccf7aac06b 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -1,4 +1,5 @@ { + "title": "Switch", "device_automation": { "action_type": { "toggle": "Toggle {entity_name}", diff --git a/homeassistant/components/system_health/strings.json b/homeassistant/components/system_health/strings.json new file mode 100644 index 00000000000..c1fb1480047 --- /dev/null +++ b/homeassistant/components/system_health/strings.json @@ -0,0 +1 @@ +{ "title": "System Health" } diff --git a/homeassistant/components/updater/strings.json b/homeassistant/components/updater/strings.json new file mode 100644 index 00000000000..d4fe2079d8f --- /dev/null +++ b/homeassistant/components/updater/strings.json @@ -0,0 +1 @@ +{ "title": "Updater" } diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 4eee3f359b5..aa02e3046e1 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -1,4 +1,5 @@ { + "title": "Vacuum", "device_automation": { "condition_type": { "is_docked": "{entity_name} is docked", diff --git a/script/translations/const.py b/script/translations/const.py index f70cc72811e..3d9126610a3 100644 --- a/script/translations/const.py +++ b/script/translations/const.py @@ -1,6 +1,7 @@ """Translation constants.""" import pathlib -PROJECT_ID = "130246255a974bd3b5e8a1.51616605" +CORE_PROJECT_ID = "130246255a974bd3b5e8a1.51616605" +FRONTEND_PROJECT_ID = "3420425759f6d6d241f598.13594006" DOCKER_IMAGE = "b8329d20280263cad04f65b843e54b9e8e6909a348a678eac959550b5ef5c75f" INTEGRATIONS_DIR = pathlib.Path("homeassistant/components") diff --git a/script/translations/download.py b/script/translations/download.py index e6e4415f16d..f2f95963cb7 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -8,7 +8,7 @@ import re import subprocess from typing import Dict, List, Union -from .const import DOCKER_IMAGE, PROJECT_ID +from .const import CORE_PROJECT_ID, DOCKER_IMAGE from .error import ExitApp from .util import get_lokalise_token @@ -32,7 +32,7 @@ def run_download_docker(): "--token", get_lokalise_token(), "export", - PROJECT_ID, + CORE_PROJECT_ID, "--export_empty", "skip", "--type", diff --git a/script/translations/lokalise.py b/script/translations/lokalise.py index bcb9ecac32d..02ac81cafd2 100644 --- a/script/translations/lokalise.py +++ b/script/translations/lokalise.py @@ -1,22 +1,25 @@ """API for Lokalise.""" +from pprint import pprint + import requests -from .const import PROJECT_ID +from .const import CORE_PROJECT_ID from .util import get_lokalise_token -def get_api() -> "Lokalise": +def get_api(project_id=CORE_PROJECT_ID, debug=False) -> "Lokalise": """Get Lokalise API.""" - return Lokalise(PROJECT_ID, get_lokalise_token()) + return Lokalise(project_id, get_lokalise_token(), debug) class Lokalise: """Lokalise API.""" - def __init__(self, project_id, token): + def __init__(self, project_id, token, debug): """Initialize Lokalise API.""" self.project_id = project_id self.token = token + self.debug = debug def request(self, method, path, data): """Make a request to the Lokalise API.""" @@ -27,12 +30,20 @@ class Lokalise: else: kwargs["json"] = data + if self.debug: + print(method, f"{self.project_id}/{path}", data) + req = requests.request( method, f"https://api.lokalise.com/api2/projects/{self.project_id}/{path}", **kwargs, ) req.raise_for_status() + + if self.debug: + pprint(req.json()) + print() + return req.json() def keys_list(self, params={}): @@ -42,6 +53,13 @@ class Lokalise: """ return self.request("GET", "keys", params)["keys"] + def keys_create(self, keys): + """Create keys. + + https://app.lokalise.com/api2docs/curl/#transition-create-keys-post + """ + return self.request("POST", "keys", {"keys": keys})["keys"] + def keys_delete_multiple(self, key_ids): """Delete multiple keys. @@ -54,4 +72,18 @@ class Lokalise: https://app.lokalise.com/api2docs/curl/#transition-bulk-update-put """ - return self.request("PUT", "keys", {"keys": updates}) + return self.request("PUT", "keys", {"keys": updates})["keys"] + + def translations_list(self, params={}): + """List translations. + + https://app.lokalise.com/api2docs/curl/#transition-list-all-translations-get + """ + return self.request("GET", "translations", params)["translations"] + + def languages_list(self, params={}): + """List languages. + + https://app.lokalise.com/api2docs/curl/#transition-list-project-languages-get + """ + return self.request("GET", "languages", params)["languages"] diff --git a/script/translations/migrate.py b/script/translations/migrate.py index 7026aef2840..a292c3b443b 100644 --- a/script/translations/migrate.py +++ b/script/translations/migrate.py @@ -2,31 +2,20 @@ import json from pprint import pprint -from .const import INTEGRATIONS_DIR +from .const import CORE_PROJECT_ID, FRONTEND_PROJECT_ID, INTEGRATIONS_DIR from .lokalise import get_api -MIGRATED = {} + +def create_lookup(results): + """Create a lookup table by key name.""" + return {key["key_name"]["web"]: key for key in results} -def run(): - """Migrate translations.""" - to_migrate = {} - - for integration in INTEGRATIONS_DIR.iterdir(): - strings_file = integration / "strings.json" - if not strings_file.is_file(): - continue - - if integration.name in MIGRATED: - continue - - strings = json.loads(strings_file.read_text()) - - if "title" in strings: - from_key = f"component::{integration.name}::config::title" - to_key = f"component::{integration.name}::title" - to_migrate[from_key] = to_key +def rename_keys(to_migrate): + """Rename keys. + to_migrate is Dict[from_key] = to_key. + """ updates = [] lokalise = get_api() @@ -49,4 +38,116 @@ def run(): print("Updating keys") pprint(lokalise.keys_bulk_update(updates).json()) + +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) + + # 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} + ) + existing = set(create_lookup(to_key_data)) + + missing = [key for key in to_migrate.values() if key not in existing] + + if not missing: + print("All keys to migrate exist already, nothing to do") + return + + print("Creating", ", ".join(missing)) + to_key_lookup = create_lookup( + to_lokalise.keys_create( + [{"key_name": key, "platforms": ["web"]} for key in missing] + ) + ) + + updates = [] + + for from_key, to_key in to_migrate.items(): + # If it is not in lookup, it already existed, skipping it. + if to_key not in to_key_lookup: + continue + + updates.append( + { + "key_id": to_key_lookup[to_key]["key_id"], + "translations": [ + { + "language_iso": from_translation["language_iso"], + "translation": from_translation["translation"], + "is_reviewed": from_translation["is_reviewed"], + "is_fuzzy": from_translation["is_fuzzy"], + } + for from_translation in from_key_lookup[from_key]["translations"] + ], + } + ) + + print("Updating") + pprint(updates) + print() + print() + pprint(to_lokalise.keys_bulk_update(updates)) + + +def find_and_rename_keys(): + """Find and rename keys in core.""" + to_migrate = {} + + for integration in INTEGRATIONS_DIR.iterdir(): + strings_file = integration / "strings.json" + if not strings_file.is_file(): + continue + + strings = json.loads(strings_file.read_text()) + + if "title" in strings.get("config", {}): + from_key = f"component::{integration.name}::config::title" + to_key = f"component::{integration.name}::title" + to_migrate[from_key] = to_key + + rename_keys(to_migrate) + + +def find_different_languages(): + """Find different supported languages.""" + core_api = get_api(CORE_PROJECT_ID) + frontend_api = get_api(FRONTEND_PROJECT_ID) + + core_languages = {lang["lang_iso"] for lang in core_api.languages_list()} + frontend_languages = {lang["lang_iso"] for lang in frontend_api.languages_list()} + + print("Core minus frontend", core_languages - frontend_languages) + print("Frontend minus core", frontend_languages - core_languages) + + +def run(): + """Migrate translations.""" + # find_different_languages() + migrate_project_keys_translations( + FRONTEND_PROJECT_ID, + CORE_PROJECT_ID, + { + "domain::binary_sensor": "component::binary_sensor::title", + "domain::sensor": "component::sensor::title", + }, + ) + return 0 diff --git a/script/translations/upload.py b/script/translations/upload.py index ecd9ec405df..b22737519b4 100755 --- a/script/translations/upload.py +++ b/script/translations/upload.py @@ -6,7 +6,7 @@ import pathlib import re import subprocess -from .const import DOCKER_IMAGE, INTEGRATIONS_DIR, PROJECT_ID +from .const import CORE_PROJECT_ID, DOCKER_IMAGE, INTEGRATIONS_DIR from .error import ExitApp from .util import get_current_branch, get_lokalise_token @@ -32,7 +32,7 @@ def run_upload_docker(): "--token", get_lokalise_token(), "import", - PROJECT_ID, + CORE_PROJECT_ID, "--file", CONTAINER_FILE, "--lang_iso",