mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Migrate frontend translations of domains to backend (#34294)
This commit is contained in:
parent
2f415b0db1
commit
6119e79023
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Alarm control panel",
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"arm_away": "Arm {entity_name} away",
|
||||
|
1
homeassistant/components/automation/strings.json
Normal file
1
homeassistant/components/automation/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Automation" }
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Binary sensor",
|
||||
"device_automation": {
|
||||
"condition_type": {
|
||||
"is_bat_low": "{entity_name} battery is low",
|
||||
|
1
homeassistant/components/calendar/strings.json
Normal file
1
homeassistant/components/calendar/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Calendar" }
|
1
homeassistant/components/camera/strings.json
Normal file
1
homeassistant/components/camera/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Camera" }
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Climate",
|
||||
"device_automation": {
|
||||
"condition_type": {
|
||||
"is_hvac_mode": "{entity_name} is set to a specific HVAC mode",
|
||||
|
1
homeassistant/components/configurator/strings.json
Normal file
1
homeassistant/components/configurator/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Configurator" }
|
1
homeassistant/components/conversation/strings.json
Normal file
1
homeassistant/components/conversation/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Conversation" }
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Cover",
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"open": "Open {entity_name}",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Device tracker",
|
||||
"device_automation": {
|
||||
"condition_type": {
|
||||
"is_home": "{entity_name} is home",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Fan",
|
||||
"device_automation": {
|
||||
"condition_type": {
|
||||
"is_on": "{entity_name} is on",
|
||||
|
1
homeassistant/components/group/strings.json
Normal file
1
homeassistant/components/group/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Group" }
|
1
homeassistant/components/image_processing/strings.json
Normal file
1
homeassistant/components/image_processing/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Image processing" }
|
1
homeassistant/components/input_boolean/strings.json
Normal file
1
homeassistant/components/input_boolean/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Input boolean" }
|
1
homeassistant/components/input_datetime/strings.json
Normal file
1
homeassistant/components/input_datetime/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Input datetime" }
|
1
homeassistant/components/input_number/strings.json
Normal file
1
homeassistant/components/input_number/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Input number" }
|
1
homeassistant/components/input_select/strings.json
Normal file
1
homeassistant/components/input_select/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Input select" }
|
1
homeassistant/components/input_text/strings.json
Normal file
1
homeassistant/components/input_text/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Input text" }
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Light",
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"brightness_decrease": "Decrease {entity_name} brightness",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Lock",
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"lock": "Lock {entity_name}",
|
||||
|
1
homeassistant/components/mailbox/strings.json
Normal file
1
homeassistant/components/mailbox/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Mailbox" }
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Media player",
|
||||
"device_automation": {
|
||||
"condition_type": {
|
||||
"is_on": "{entity_name} is on",
|
||||
|
1
homeassistant/components/notify/strings.json
Normal file
1
homeassistant/components/notify/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Notify" }
|
1
homeassistant/components/person/strings.json
Normal file
1
homeassistant/components/person/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Person" }
|
1
homeassistant/components/plant/strings.json
Normal file
1
homeassistant/components/plant/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Plant" }
|
1
homeassistant/components/proximity/strings.json
Normal file
1
homeassistant/components/proximity/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Proximity" }
|
1
homeassistant/components/remote/strings.json
Normal file
1
homeassistant/components/remote/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Remote" }
|
1
homeassistant/components/scene/strings.json
Normal file
1
homeassistant/components/scene/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Scene" }
|
1
homeassistant/components/script/strings.json
Normal file
1
homeassistant/components/script/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Script" }
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Sensor",
|
||||
"device_automation": {
|
||||
"condition_type": {
|
||||
"is_battery_level": "Current {entity_name} battery level",
|
||||
|
1
homeassistant/components/sun/strings.json
Normal file
1
homeassistant/components/sun/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Sun" }
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Switch",
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"toggle": "Toggle {entity_name}",
|
||||
|
1
homeassistant/components/system_health/strings.json
Normal file
1
homeassistant/components/system_health/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "System Health" }
|
1
homeassistant/components/updater/strings.json
Normal file
1
homeassistant/components/updater/strings.json
Normal file
@ -0,0 +1 @@
|
||||
{ "title": "Updater" }
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Vacuum",
|
||||
"device_automation": {
|
||||
"condition_type": {
|
||||
"is_docked": "{entity_name} is docked",
|
||||
|
@ -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")
|
||||
|
@ -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",
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user