Ask specific translations (#5560)

This commit is contained in:
Paulus Schoutsen 2020-04-18 17:14:25 -07:00 committed by GitHub
parent f91b46e88c
commit d45a674652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 397 additions and 523 deletions

View File

@ -70,6 +70,7 @@ const createWebpackConfig = ({
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version),
__DEMO__: false,
__BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
@ -221,6 +222,9 @@ const createCastConfig = ({ isProdBuild, latestBuild }) => {
outputRoot: paths.cast_root,
isProdBuild,
latestBuild,
defineOverlay: {
__BACKWARDS_COMPAT__: true,
},
});
};

View File

@ -17,6 +17,7 @@ export const mockLovelace = (
);
hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
};
customElements.whenDefined("hui-view").then(() => {

View File

@ -2,473 +2,6 @@ import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockTranslations = (hass: MockHomeAssistant) => {
hass.mockWS("frontend/get_translations", () => ({
resources: {
"component.lifx.config.abort.no_devices_found":
"No LIFX devices found on the network.",
"component.lifx.config.abort.single_instance_allowed":
"Only a single configuration of LIFX is possible.",
"component.lifx.config.step.confirm.description":
"Do you want to set up LIFX?",
"component.lifx.config.step.confirm.title": "LIFX",
"component.lifx.config.title": "LIFX",
"component.hangouts.config.abort.already_configured":
"Google Hangouts is already configured",
"component.hangouts.config.abort.unknown": "Unknown error occurred.",
"component.hangouts.config.error.invalid_2fa":
"Invalid 2 Factor Authentication, please try again.",
"component.hangouts.config.error.invalid_2fa_method":
"Invalid 2FA Method (Verify on Phone).",
"component.hangouts.config.error.invalid_login":
"Invalid Login, please try again.",
"component.hangouts.config.step.2fa.data.2fa": "2FA Pin",
"component.hangouts.config.step.2fa.title": "2-Factor-Authentication",
"component.hangouts.config.step.user.data.email": "E-Mail Address",
"component.hangouts.config.step.user.data.password": "Password",
"component.hangouts.config.step.user.title": "Google Hangouts Login",
"component.hangouts.config.title": "Google Hangouts",
"component.rainmachine.config.error.identifier_exists":
"Account already registered",
"component.rainmachine.config.error.invalid_credentials":
"Invalid credentials",
"component.rainmachine.config.step.user.data.ip_address":
"Hostname or IP Address",
"component.rainmachine.config.step.user.data.password": "Password",
"component.rainmachine.config.step.user.data.port": "Port",
"component.rainmachine.config.step.user.title":
"Fill in your information",
"component.rainmachine.config.title": "RainMachine",
"component.homematicip_cloud.config.abort.already_configured":
"Access point is already configured",
"component.homematicip_cloud.config.abort.connection_aborted":
"Could not connect to HMIP server",
"component.homematicip_cloud.config.abort.unknown":
"Unknown error occurred.",
"component.homematicip_cloud.config.error.invalid_pin":
"Invalid PIN, please try again.",
"component.homematicip_cloud.config.error.press_the_button":
"Please press the blue button.",
"component.homematicip_cloud.config.error.register_failed":
"Failed to register, please try again.",
"component.homematicip_cloud.config.error.timeout_button":
"Blue button press timeout, please try again.",
"component.homematicip_cloud.config.step.init.data.hapid":
"Access point ID (SGTIN)",
"component.homematicip_cloud.config.step.init.data.name":
"Name (optional, used as name prefix for all devices)",
"component.homematicip_cloud.config.step.init.data.pin":
"Pin Code (optional)",
"component.homematicip_cloud.config.step.init.title":
"Pick HomematicIP Access point",
"component.homematicip_cloud.config.step.link.description":
"Press the blue button on the access point and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)",
"component.homematicip_cloud.config.step.link.title": "Link Access point",
"component.homematicip_cloud.config.title": "HomematicIP Cloud",
"component.daikin.config.abort.already_configured":
"Device is already configured",
"component.daikin.config.abort.device_fail":
"Unexpected error creating device.",
"component.daikin.config.abort.device_timeout":
"Timeout connecting to the device.",
"component.daikin.config.step.user.data.host": "Host",
"component.daikin.config.step.user.description":
"Enter IP address of your Daikin AC.",
"component.daikin.config.step.user.title": "Configure Daikin AC",
"component.daikin.config.title": "Daikin AC",
"component.unifi.config.abort.already_configured":
"Controller site is already configured",
"component.unifi.config.abort.user_privilege":
"User needs to be administrator",
"component.unifi.config.error.faulty_credentials": "Bad user credentials",
"component.unifi.config.error.service_unavailable":
"No service available",
"component.unifi.config.step.user.data.host": "Host",
"component.unifi.config.step.user.data.password": "Password",
"component.unifi.config.step.user.data.port": "Port",
"component.unifi.config.step.user.data.site": "Site ID",
"component.unifi.config.step.user.data.username": "User name",
"component.unifi.config.step.user.data.verify_ssl":
"Controller using proper certificate",
"component.unifi.config.step.user.title": "Set up UniFi Controller",
"component.unifi.config.title": "UniFi Controller",
"component.nest.config.abort.already_setup":
"You can only configure a single Nest account.",
"component.nest.config.abort.authorize_url_fail":
"Unknown error generating an authorize url.",
"component.nest.config.abort.authorize_url_timeout":
"Timeout generating authorize url.",
"component.nest.config.abort.no_flows":
"You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/nest/).",
"component.nest.config.error.internal_error":
"Internal error validating code",
"component.nest.config.error.invalid_code": "Invalid code",
"component.nest.config.error.timeout": "Timeout validating code",
"component.nest.config.error.unknown": "Unknown error validating code",
"component.nest.config.step.init.data.flow_impl": "Provider",
"component.nest.config.step.init.description":
"Pick via which authentication provider you want to authenticate with Nest.",
"component.nest.config.step.init.title": "Authentication Provider",
"component.nest.config.step.link.data.code": "Pin code",
"component.nest.config.step.link.description":
"To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
"component.nest.config.step.link.title": "Link Nest Account",
"component.nest.config.title": "Nest",
"component.mailgun.config.abort.not_internet_accessible":
"Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages.",
"component.mailgun.config.abort.one_instance_allowed":
"Only a single instance is necessary.",
"component.mailgun.config.create_entry.default":
"To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.",
"component.mailgun.config.step.user.description":
"Are you sure you want to set up Mailgun?",
"component.mailgun.config.step.user.title": "Set up the Mailgun Webhook",
"component.mailgun.config.title": "Mailgun",
"component.tellduslive.config.abort.already_setup":
"TelldusLive is already configured",
"component.tellduslive.config.abort.authorize_url_fail":
"Unknown error generating an authorize url.",
"component.tellduslive.config.abort.authorize_url_timeout":
"Timeout generating authorize url.",
"component.tellduslive.config.abort.unknown": "Unknown error occurred",
"component.tellduslive.config.error.auth_error":
"Authentication error, please try again",
"component.tellduslive.config.step.auth.description":
"To link your TelldusLive account:\n 1. Click the link below\n 2. Login to Telldus Live\n 3. Authorize **{app_name}** (click **Yes**).\n 4. Come back here and click **SUBMIT**.\n\n [Link TelldusLive account]({auth_url})",
"component.tellduslive.config.step.auth.title":
"Authenticate against TelldusLive",
"component.tellduslive.config.step.user.data.host": "Host",
"component.tellduslive.config.step.user.title": "Pick endpoint.",
"component.tellduslive.config.title": "Telldus Live",
"component.esphome.config.abort.already_configured":
"ESP is already configured",
"component.esphome.config.error.connection_error":
"Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
"component.esphome.config.error.invalid_password": "Invalid password!",
"component.esphome.config.error.resolve_error":
"Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips",
"component.esphome.config.step.authenticate.data.password": "Password",
"component.esphome.config.step.authenticate.description":
"Please enter the password you set in your configuration.",
"component.esphome.config.step.authenticate.title": "Enter Password",
"component.esphome.config.step.user.data.host": "Host",
"component.esphome.config.step.user.data.port": "Port",
"component.esphome.config.step.user.description":
"Please enter connection settings of your [ESPHome](https://esphomelib.com/) node.",
"component.esphome.config.step.user.title": "ESPHome",
"component.esphome.config.title": "ESPHome",
"component.luftdaten.config.error.communication_error":
"Unable to communicate with the Luftdaten API",
"component.luftdaten.config.error.invalid_sensor":
"Sensor not available or invalid",
"component.luftdaten.config.error.sensor_exists":
"Sensor already registered",
"component.luftdaten.config.step.user.data.show_on_map": "Show on map",
"component.luftdaten.config.step.user.data.station_id":
"Luftdaten Sensor ID",
"component.luftdaten.config.step.user.title": "Define Luftdaten",
"component.luftdaten.config.title": "Luftdaten",
"component.upnp.config.abort.already_configured":
"UPnP/IGD is already configured",
"component.upnp.config.abort.incomplete_device":
"Ignoring incomplete UPnP device",
"component.upnp.config.abort.no_devices_discovered":
"No UPnP/IGDs discovered",
"component.upnp.config.abort.no_devices_found":
"No UPnP/IGD devices found on the network.",
"component.upnp.config.abort.no_sensors_or_port_mapping":
"Enable at least sensors or port mapping",
"component.upnp.config.abort.single_instance_allowed":
"Only a single configuration of UPnP/IGD is necessary.",
"component.upnp.config.step.confirm.description":
"Do you want to set up UPnP/IGD?",
"component.upnp.config.step.confirm.title": "UPnP/IGD",
"component.upnp.config.step.init.title": "UPnP/IGD",
"component.upnp.config.step.user.data.enable_port_mapping":
"Enable port mapping for Home Assistant",
"component.upnp.config.step.user.data.enable_sensors":
"Add traffic sensors",
"component.upnp.config.step.user.data.igd": "UPnP/IGD",
"component.upnp.config.step.user.title":
"Configuration options for the UPnP/IGD",
"component.upnp.config.title": "UPnP/IGD",
"component.point.config.abort.already_setup":
"You can only configure a Point account.",
"component.point.config.abort.authorize_url_fail":
"Unknown error generating an authorize url.",
"component.point.config.abort.authorize_url_timeout":
"Timeout generating authorize url.",
"component.point.config.abort.external_setup":
"Point successfully configured from another flow.",
"component.point.config.abort.no_flows":
"You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/point/).",
"component.point.config.create_entry.default":
"Successfully authenticated with Minut for your Point device(s)",
"component.point.config.error.follow_link":
"Please follow the link and authenticate before pressing Submit",
"component.point.config.error.no_token": "Not authenticated with Minut",
"component.point.config.step.auth.description":
"Please follow the link below and <b>Accept</b> access to your Minut account, then come back and press <b>Submit</b> below.\n\n[Link]({authorization_url})",
"component.point.config.step.auth.title": "Authenticate Point",
"component.point.config.step.user.data.flow_impl": "Provider",
"component.point.config.step.user.description":
"Pick via which authentication provider you want to authenticate with Point.",
"component.point.config.step.user.title": "Authentication Provider",
"component.point.config.title": "Minut Point",
"component.auth.mfa_setup.notify.abort.no_available_service":
"No notification services available.",
"component.auth.mfa_setup.notify.error.invalid_code":
"Invalid code, please try again.",
"component.auth.mfa_setup.notify.step.init.description":
"Please select one of the notification services:",
"component.auth.mfa_setup.notify.step.init.title":
"Set up one-time password delivered by notify component",
"component.auth.mfa_setup.notify.step.setup.description":
"A one-time password has been sent via **notify.{notify_service}**. Please enter it below:",
"component.auth.mfa_setup.notify.step.setup.title": "Verify setup",
"component.auth.mfa_setup.notify.title": "Notify One-Time Password",
"component.auth.mfa_setup.totp.error.invalid_code":
"Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate.",
"component.auth.mfa_setup.totp.step.init.description":
"To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.",
"component.auth.mfa_setup.totp.step.init.title":
"Set up two-factor authentication using TOTP",
"component.auth.mfa_setup.totp.title": "TOTP",
"component.emulated_roku.config.abort.name_exists": "Name already exists",
"component.emulated_roku.config.step.user.data.advertise_ip":
"Advertise IP",
"component.emulated_roku.config.step.user.data.advertise_port":
"Advertise port",
"component.emulated_roku.config.step.user.data.host_ip": "Host IP",
"component.emulated_roku.config.step.user.data.listen_port":
"Listen port",
"component.emulated_roku.config.step.user.data.name": "Name",
"component.emulated_roku.config.step.user.data.upnp_bind_multicast":
"Bind multicast (True/False)",
"component.emulated_roku.config.step.user.title":
"Define server configuration",
"component.emulated_roku.config.title": "EmulatedRoku",
"component.owntracks.config.abort.one_instance_allowed":
"Only a single instance is necessary.",
"component.owntracks.config.create_entry.default":
"\n\nOn Android, open [the OwnTracks app]({android_url}), go to preferences -> connection. Change the following settings:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `<Your name>`\n - Device ID: `<Your device name>`\n\nOn iOS, open [the OwnTracks app]({ios_url}), tap (i) icon in top left -> settings. Change the following settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `<Your name>`\n\n{secret}\n\nSee [the documentation]({docs_url}) for more information.",
"component.owntracks.config.step.user.description":
"Are you sure you want to set up OwnTracks?",
"component.owntracks.config.step.user.title": "Set up OwnTracks",
"component.owntracks.config.title": "OwnTracks",
"component.zone.config.error.name_exists": "Name already exists",
"component.zone.config.step.init.data.icon": "Icon",
"component.zone.config.step.init.data.latitude": "Latitude",
"component.zone.config.step.init.data.longitude": "Longitude",
"component.zone.config.step.init.data.name": "Name",
"component.zone.config.step.init.data.passive": "Passive",
"component.zone.config.step.init.data.radius": "Radius",
"component.zone.config.step.init.title": "Define zone parameters",
"component.zone.config.title": "Zone",
"component.hue.config.abort.all_configured":
"All Philips Hue bridges are already configured",
"component.hue.config.abort.already_configured":
"Bridge is already configured",
"component.hue.config.abort.cannot_connect":
"Unable to connect to the bridge",
"component.hue.config.abort.discover_timeout":
"Unable to discover Hue bridges",
"component.hue.config.abort.no_bridges":
"No Philips Hue bridges discovered",
"component.hue.config.abort.unknown": "Unknown error occurred",
"component.hue.config.error.linking": "Unknown linking error occurred.",
"component.hue.config.error.register_failed":
"Failed to register, please try again",
"component.hue.config.step.init.data.host": "Host",
"component.hue.config.step.init.title": "Pick Hue bridge",
"component.hue.config.step.link.description":
"Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)",
"component.hue.config.step.link.title": "Link Hub",
"component.hue.config.title": "Philips Hue",
"component.tradfri.config.abort.already_configured":
"Bridge is already configured",
"component.tradfri.config.error.cannot_connect":
"Unable to connect to the gateway.",
"component.tradfri.config.error.invalid_key":
"Failed to register with provided key. If this keeps happening, try restarting the gateway.",
"component.tradfri.config.error.timeout": "Timeout validating the code.",
"component.tradfri.config.step.auth.data.host": "Host",
"component.tradfri.config.step.auth.data.security_code": "Security Code",
"component.tradfri.config.step.auth.description":
"You can find the security code on the back of your gateway.",
"component.tradfri.config.step.auth.title": "Enter security code",
"component.tradfri.config.title": "IKEA TRÅDFRI",
"component.mqtt.config.abort.single_instance_allowed":
"Only a single configuration of MQTT is allowed.",
"component.mqtt.config.error.cannot_connect":
"Unable to connect to the broker.",
"component.mqtt.config.step.broker.data.broker": "Broker",
"component.mqtt.config.step.broker.data.discovery": "Enable discovery",
"component.mqtt.config.step.broker.data.password": "Password",
"component.mqtt.config.step.broker.data.port": "Port",
"component.mqtt.config.step.broker.data.username": "Username",
"component.mqtt.config.step.broker.description":
"Please enter the connection information of your MQTT broker.",
"component.mqtt.config.step.broker.title": "MQTT",
"component.mqtt.config.step.hassio_confirm.data.discovery":
"Enable discovery",
"component.mqtt.config.step.hassio_confirm.description":
"Do you want to configure Home Assistant to connect to the MQTT broker provided by the hass.io add-on {addon}?",
"component.mqtt.config.step.hassio_confirm.title":
"MQTT Broker via Hass.io add-on",
"component.mqtt.config.title": "MQTT",
"component.geofency.config.abort.not_internet_accessible":
"Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency.",
"component.geofency.config.abort.one_instance_allowed":
"Only a single instance is necessary.",
"component.geofency.config.create_entry.default":
"To send events to Home Assistant, you will need to setup the webhook feature in Geofency.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.",
"component.geofency.config.step.user.description":
"Are you sure you want to set up the Geofency Webhook?",
"component.geofency.config.step.user.title":
"Set up the Geofency Webhook",
"component.geofency.config.title": "Geofency Webhook",
"component.simplisafe.config.error.identifier_exists":
"Account already registered",
"component.simplisafe.config.error.invalid_credentials":
"Invalid credentials",
"component.simplisafe.config.step.user.data.code":
"Code (for Home Assistant)",
"component.simplisafe.config.step.user.data.password": "Password",
"component.simplisafe.config.step.user.data.username": "Email Address",
"component.simplisafe.config.step.user.title": "Fill in your information",
"component.simplisafe.config.title": "SimpliSafe",
"component.dialogflow.config.abort.not_internet_accessible":
"Your Home Assistant instance needs to be accessible from the internet to receive Dialogflow messages.",
"component.dialogflow.config.abort.one_instance_allowed":
"Only a single instance is necessary.",
"component.dialogflow.config.create_entry.default":
"To send events to Home Assistant, you will need to setup [webhook integration of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details.",
"component.dialogflow.config.step.user.description":
"Are you sure you want to set up Dialogflow?",
"component.dialogflow.config.step.user.title":
"Set up the Dialogflow Webhook",
"component.dialogflow.config.title": "Dialogflow",
"component.deconz.config.abort.already_configured":
"Bridge is already configured",
"component.deconz.config.abort.no_bridges":
"No deCONZ bridges discovered",
"component.deconz.config.abort.one_instance_only":
"Component only supports one deCONZ instance",
"component.deconz.config.error.no_key": "Couldn't get an API key",
"component.deconz.config.step.init.data.host": "Host",
"component.deconz.config.step.init.data.port": "Port",
"component.deconz.config.step.init.title": "Define deCONZ gateway",
"component.deconz.config.step.link.description":
'Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press "Unlock Gateway" button',
"component.deconz.config.step.link.title": "Link with deCONZ",
"component.deconz.config.step.options.data.allow_clip_sensor":
"Allow importing virtual sensors",
"component.deconz.config.step.options.data.allow_deconz_groups":
"Allow importing deCONZ groups",
"component.deconz.config.step.options.title":
"Extra configuration options for deCONZ",
"component.deconz.config.title": "deCONZ Zigbee gateway",
"component.openuv.config.error.identifier_exists":
"Coordinates already registered",
"component.openuv.config.error.invalid_api_key": "Invalid API key",
"component.openuv.config.step.user.data.api_key": "OpenUV API Key",
"component.openuv.config.step.user.data.elevation": "Elevation",
"component.openuv.config.step.user.data.latitude": "Latitude",
"component.openuv.config.step.user.data.longitude": "Longitude",
"component.openuv.config.step.user.title": "Fill in your information",
"component.openuv.config.title": "OpenUV",
"component.locative.config.title": "Locative Webhook",
"component.locative.config.step.user.title":
"Set up the Locative Webhook",
"component.locative.config.step.user.description":
"Are you sure you want to set up the Locative Webhook?",
"component.locative.config.abort.one_instance_allowed":
"Only a single instance is necessary.",
"component.locative.config.abort.not_internet_accessible":
"Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency.",
"component.locative.config.create_entry.default":
"To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.",
"component.ios.config.abort.single_instance_allowed":
"Only a single configuration of Home Assistant iOS is necessary.",
"component.ios.config.step.confirm.description":
"Do you want to set up the Home Assistant iOS component?",
"component.ios.config.step.confirm.title": "Home Assistant iOS",
"component.ios.config.title": "Home Assistant iOS",
"component.smhi.config.error.name_exists": "Name already exists",
"component.smhi.config.error.wrong_location": "Location Sweden only",
"component.smhi.config.step.user.data.latitude": "Latitude",
"component.smhi.config.step.user.data.longitude": "Longitude",
"component.smhi.config.step.user.data.name": "Name",
"component.smhi.config.step.user.title": "Location in Sweden",
"component.smhi.config.title": "Swedish weather service (SMHI)",
"component.sonos.config.abort.no_devices_found":
"No Sonos devices found on the network.",
"component.sonos.config.abort.single_instance_allowed":
"Only a single configuration of Sonos is necessary.",
"component.sonos.config.step.confirm.description":
"Do you want to set up Sonos?",
"component.sonos.config.step.confirm.title": "Sonos",
"component.sonos.config.title": "Sonos",
"component.ifttt.config.abort.not_internet_accessible":
"Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages.",
"component.ifttt.config.abort.one_instance_allowed":
"Only a single instance is necessary.",
"component.ifttt.config.create_entry.default":
'To send events to Home Assistant, you will need to use the "Make a web request" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.',
"component.ifttt.config.step.user.description":
"Are you sure you want to set up IFTTT?",
"component.ifttt.config.step.user.title":
"Set up the IFTTT Webhook Applet",
"component.ifttt.config.title": "IFTTT",
"component.twilio.config.abort.not_internet_accessible":
"Your Home Assistant instance needs to be accessible from the internet to receive Twilio messages.",
"component.twilio.config.abort.one_instance_allowed":
"Only a single instance is necessary.",
"component.twilio.config.create_entry.default":
"To send events to Home Assistant, you will need to setup [Webhooks with Twilio]({twilio_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.",
"component.twilio.config.step.user.description":
"Are you sure you want to set up Twilio?",
"component.twilio.config.step.user.title": "Set up the Twilio Webhook",
"component.twilio.config.title": "Twilio",
"component.zha.config.abort.single_instance_allowed":
"Only a single configuration of ZHA is allowed.",
"component.zha.config.error.cannot_connect":
"Unable to connect to ZHA device.",
"component.zha.config.step.user.data.radio_type": "Radio Type",
"component.zha.config.step.user.data.usb_path": "USB Device Path",
"component.zha.config.step.user.title": "ZHA",
"component.zha.config.title": "ZHA",
"component.gpslogger.config.title": "GPSLogger Webhook",
"component.gpslogger.config.step.user.title":
"Set up the GPSLogger Webhook",
"component.gpslogger.config.step.user.description":
"Are you sure you want to set up the GPSLogger Webhook?",
"component.gpslogger.config.abort.one_instance_allowed":
"Only a single instance is necessary.",
"component.gpslogger.config.abort.not_internet_accessible":
"Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger.",
"component.gpslogger.config.create_entry.default":
"To send events to Home Assistant, you will need to setup the webhook feature in GPSLogger.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.",
"component.zwave.config.abort.already_configured":
"Z-Wave is already configured",
"component.zwave.config.abort.one_instance_only":
"Component only supports one Z-Wave instance",
"component.zwave.config.error.option_error":
"Z-Wave validation failed. Is the path to the USB stick correct?",
"component.zwave.config.step.user.data.network_key":
"Network Key (leave blank to auto-generate)",
"component.zwave.config.step.user.data.usb_path": "USB Path",
"component.zwave.config.step.user.description":
"See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
"component.zwave.config.step.user.title": "Set up Z-Wave",
"component.zwave.config.title": "Z-Wave",
"component.cast.config.abort.no_devices_found":
"No Google Cast devices found on the network.",
"component.cast.config.abort.single_instance_allowed":
"Only a single configuration of Google Cast is necessary.",
"component.cast.config.step.confirm.description":
"Do you want to set up Google Cast?",
"component.cast.config.step.confirm.title": "Google Cast",
"component.cast.config.title": "Google Cast",
},
resources: {},
}));
};

View File

@ -3,6 +3,7 @@ import { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow";
import { domainToName } from "./integration";
export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf"];
@ -75,7 +76,7 @@ export const localizeConfigFlowTitle = (
const placeholders = flow.context.title_placeholders || {};
const placeholderKeys = Object.keys(placeholders);
if (placeholderKeys.length === 0) {
return localize(`component.${flow.handler}.title`);
return domainToName(localize, flow.handler);
}
const args: string[] = [];
placeholderKeys.forEach((key) => {

View File

@ -11,6 +11,14 @@ declare global {
}
}
export type TranslationCategory =
| "title"
| "state"
| "config"
| "options"
| "device_automation"
| "mfa_setup";
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language");
@ -20,6 +28,23 @@ export const saveTranslationPreferences = (
) => saveFrontendUserData(hass.connection, "language", data);
export const getHassTranslations = async (
hass: HomeAssistant,
language: string,
category: TranslationCategory,
integration?: string,
config_flow?: boolean
): Promise<{}> => {
const result = await hass.callWS<{ resources: {} }>({
type: "frontend/get_translations",
language,
category,
integration,
config_flow,
});
return result.resources;
};
export const getHassTranslationsPre109 = async (
hass: HomeAssistant,
language: string
): Promise<{}> => {

View File

@ -13,6 +13,7 @@ import {
loadDataEntryFlowDialog,
showFlowDialog,
} from "./show-dialog-data-entry-flow";
import { domainToName } from "../../data/integration";
export const loadConfigFlowDialog = loadDataEntryFlowDialog;
@ -22,17 +23,31 @@ export const showConfigFlowDialog = (
): void =>
showFlowDialog(element, dialogParams, {
loadDevicesAndAreas: true,
getFlowHandlers: (hass) =>
getConfigFlowHandlers(hass).then((handlers) =>
handlers.sort((handlerA, handlerB) =>
caseInsensitiveCompare(
hass.localize(`component.${handlerA}.title`),
hass.localize(`component.${handlerB}.title`)
)
getFlowHandlers: async (hass) => {
const [handlers] = await Promise.all([
getConfigFlowHandlers(hass),
hass.loadBackendTranslation("title", undefined, true),
]);
return handlers.sort((handlerA, handlerB) =>
caseInsensitiveCompare(
domainToName(hass.localize, handlerA),
domainToName(hass.localize, handlerB)
)
),
createFlow: createConfigFlow,
fetchFlow: fetchConfigFlow,
);
},
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createConfigFlow(hass, handler),
hass.loadBackendTranslation("config", handler),
]);
return step;
},
fetchFlow: async (hass, flowId) => {
const step = await fetchConfigFlow(hass, flowId);
await hass.loadBackendTranslation("config", step.handler);
return step;
},
handleFlowStep: handleConfigFlowStep,
deleteFlow: deleteConfigFlow,

View File

@ -25,8 +25,20 @@ export const showOptionsFlowDialog = (
},
{
loadDevicesAndAreas: false,
createFlow: createOptionsFlow,
fetchFlow: fetchOptionsFlow,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createOptionsFlow(hass, handler),
hass.loadBackendTranslation("options", configEntry.domain),
]);
return step;
},
fetchFlow: async (hass, flowId) => {
const [step] = await Promise.all([
fetchOptionsFlow(hass, flowId),
hass.loadBackendTranslation("options", configEntry.domain),
]);
return step;
},
handleFlowStep: handleOptionsFlowStep,
deleteFlow: deleteOptionsFlow,

View File

@ -20,6 +20,8 @@ import "../../components/ha-icon-next";
import { HomeAssistant } from "../../types";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { domainToName } from "../../data/integration";
import { LocalizeFunc } from "../../common/translations/localize";
interface HandlerObj {
name: string;
@ -40,31 +42,37 @@ class StepFlowPickHandler extends LitElement {
private _width?: number;
private _getHandlers = memoizeOne((h: string[], filter?: string) => {
const handlers: HandlerObj[] = h.map((handler) => {
return {
name: this.hass.localize(`component.${handler}.title`) || handler,
slug: handler,
};
});
private _getHandlers = memoizeOne(
(h: string[], filter?: string, _localize?: LocalizeFunc) => {
const handlers: HandlerObj[] = h.map((handler) => {
return {
name: domainToName(this.hass.localize, handler),
slug: handler,
};
});
if (filter) {
const options: Fuse.FuseOptions<HandlerObj> = {
keys: ["name", "slug"],
caseSensitive: false,
minMatchCharLength: 2,
threshold: 0.2,
};
const fuse = new Fuse(handlers, options);
return fuse.search(filter);
if (filter) {
const options: Fuse.FuseOptions<HandlerObj> = {
keys: ["name", "slug"],
caseSensitive: false,
minMatchCharLength: 2,
threshold: 0.2,
};
const fuse = new Fuse(handlers, options);
return fuse.search(filter);
}
return handlers.sort((a, b) =>
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
);
}
return handlers.sort((a, b) =>
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
);
});
);
protected render(): TemplateResult {
const handlers = this._getHandlers(this.handlers, this.filter);
const handlers = this._getHandlers(
this.handlers,
this.filter,
this.hass.localize
);
return html`
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>

View File

@ -67,6 +67,12 @@ export class HomeAssistantAppEl extends HassElement {
}
}
protected hassConnected() {
super.hassConnected();
// @ts-ignore
this._loadHassTranslations(this.hass!.language, "state");
}
protected hassReconnected() {
super.hassReconnected();

View File

@ -0,0 +1,89 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../components/ha-icon";
@customElement("action-badge")
class ActionBadge extends LitElement {
@property() public icon!: string;
@property() public title!: string;
@property() public badgeIcon?: string;
@property({ type: Boolean, reflect: true }) public clickable = false;
protected render(): TemplateResult {
return html`
<div class="icon">
<iron-icon .icon=${this.icon}></iron-icon>
${this.badgeIcon
? html` <ha-icon class="badge" .icon=${this.badgeIcon}></ha-icon> `
: ""}
</div>
<div class="title">${this.title}</div>
`;
}
static get styles(): CSSResult {
return css`
:host {
display: inline-flex;
flex-direction: column;
text-align: center;
color: var(--primary-text-color);
}
:host([clickable]) {
color: var(--primary-text-color);
}
.icon {
position: relative;
box-sizing: border-box;
margin: 0 auto 8px;
height: 40px;
width: 40px;
border-radius: 50%;
border: 1px solid var(--secondary-text-color);
display: flex;
align-items: center;
justify-content: center;
}
:host([clickable]) .icon {
border-color: var(--primary-color);
border-width: 2px;
}
.badge {
position: absolute;
color: var(--primary-color);
bottom: -5px;
right: -5px;
background-color: white;
border-radius: 50%;
width: 18px;
display: block;
height: 18px;
}
.title {
min-height: 2.3em;
word-break: break-word;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"action-badge": ActionBadge;
}
}

View File

@ -27,6 +27,7 @@ import { HassElement } from "../state/hass-element";
import { registerServiceWorker } from "../util/register-service-worker";
import "./onboarding-create-user";
import "./onboarding-loading";
import { HomeAssistant } from "../types";
interface OnboardingEvent<T extends ValidOnboardingStep> {
type: T;
@ -45,6 +46,8 @@ declare global {
@customElement("ha-onboarding")
class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@property() public hass?: HomeAssistant;
public translationFragment = "page-onboarding";
@property() private _loading = false;
@ -102,6 +105,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
if (changedProps.has("language")) {
document.querySelector("html")!.setAttribute("lang", this.language!);
}
if (changedProps.has("hass")) {
this.hassChanged(
this.hass!,
changedProps.get("hass") as HomeAssistant | undefined
);
}
}
private _curStep() {

View File

@ -11,7 +11,7 @@ import "../components/ha-icon";
@customElement("integration-badge")
class IntegrationBadge extends LitElement {
@property() public icon!: string;
@property() public domain!: string;
@property() public title!: string;
@ -22,7 +22,10 @@ class IntegrationBadge extends LitElement {
protected render(): TemplateResult {
return html`
<div class="icon">
<iron-icon .icon=${this.icon}></iron-icon>
<img
src="https://brands.home-assistant.io/${this.domain}/icon.png"
referrerpolicy="no-referrer"
/>
${this.badgeIcon
? html` <ha-icon class="badge" .icon=${this.badgeIcon}></ha-icon> `
: ""}
@ -44,33 +47,35 @@ class IntegrationBadge extends LitElement {
color: var(--primary-text-color);
}
img {
max-width: 100%;
max-height: 100%;
}
.icon {
position: relative;
margin: 0 auto 8px;
height: 40px;
width: 40px;
border-radius: 50%;
border: 1px solid var(--secondary-text-color);
display: flex;
align-items: center;
justify-content: center;
}
:host([clickable]) .icon {
border-color: var(--primary-color);
border-width: 2px;
}
.badge {
position: absolute;
color: var(--primary-color);
bottom: -5px;
right: -5px;
background-color: white;
color: white;
bottom: -7px;
right: -10px;
background-color: var(--label-badge-green);
border-radius: 50%;
width: 18px;
display: block;
height: 18px;
border: 2px solid white;
}
.title {

View File

@ -26,7 +26,9 @@ import {
showConfigFlowDialog,
} from "../dialogs/config-flow/show-dialog-config-flow";
import { HomeAssistant } from "../types";
import "./action-badge";
import "./integration-badge";
import { domainToName } from "../data/integration";
@customElement("onboarding-integrations")
class OnboardingIntegrations extends LitElement {
@ -42,8 +44,15 @@ class OnboardingIntegrations extends LitElement {
public connectedCallback() {
super.connectedCallback();
this.hass.loadBackendTranslation("title", undefined, true);
this._unsubEvents = subscribeConfigFlowInProgress(this.hass, (flows) => {
this._discovered = flows;
for (const flow of flows) {
// To render title placeholders
if (flow.context.title_placeholders) {
this.hass.loadBackendTranslation("config", flow.handler);
}
}
});
}
@ -62,13 +71,14 @@ class OnboardingIntegrations extends LitElement {
// Render discovered and existing entries together sorted by localized title.
const entries: Array<[string, TemplateResult]> = this._entries.map(
(entry) => {
const title = this.hass.localize(`component.${entry.domain}.title`);
const title = domainToName(this.hass.localize, entry.domain);
return [
title,
html`
<integration-badge
.domain=${entry.domain}
.title=${title}
icon="hass:check"
badgeIcon="hass:check"
></integration-badge>
`,
];
@ -83,8 +93,8 @@ class OnboardingIntegrations extends LitElement {
<button .flowId=${flow.flow_id} @click=${this._continueFlow}>
<integration-badge
clickable
.domain=${flow.handler}
.title=${title}
icon="hass:plus"
></integration-badge>
</button>
`,
@ -102,13 +112,13 @@ class OnboardingIntegrations extends LitElement {
<div class="badges">
${content}
<button @click=${this._createFlow}>
<integration-badge
<action-badge
clickable
title=${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.more_integrations"
)}
icon="hass:dots-horizontal"
></integration-badge>
></action-badge>
</button>
</div>
<div class="footer">
@ -172,14 +182,17 @@ class OnboardingIntegrations extends LitElement {
return css`
.badges {
margin-top: 24px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-items: flex-start;
}
.badges > * {
width: 24%;
min-width: 90px;
width: 96px;
margin-bottom: 24px;
}
button {
display: inline-block;
cursor: pointer;
padding: 0;
border: 0;

View File

@ -50,6 +50,11 @@ class HaConfigAutomation extends HassRouterPage {
return automations;
});
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("device_automation");
}
protected updatePageEl(pageEl, changedProps: PropertyValues) {
pageEl.hass = this.hass;
pageEl.narrow = this.narrow;

View File

@ -322,6 +322,7 @@ class HaPanelConfig extends HassRouterPage {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("title");
if (isComponentLoaded(this.hass, "cloud")) {
this._updateCloudStatus();
}

View File

@ -93,6 +93,8 @@ class HaConfigIntegrations extends HassRouterPage {
this._loadData();
getConfigFlowInProgressCollection(this.hass.connection).refresh();
});
// For config entries. Also loading config flow ones for add integration
this.hass.loadBackendTranslation("title", undefined, true);
}
protected updated(changedProps: PropertyValues) {
@ -142,6 +144,12 @@ class HaConfigIntegrations extends HassRouterPage {
}),
subscribeConfigFlowInProgress(this.hass, (flowsInProgress) => {
this._configEntriesInProgress = flowsInProgress;
for (const flow of flowsInProgress) {
// To render title placeholders
if (flow.context.title_placeholders) {
this.hass.loadBackendTranslation("config", flow.handler);
}
}
}),
];
}

View File

@ -46,6 +46,11 @@ class HaConfigScript extends HassRouterPage {
return scripts;
});
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("device_automation");
}
protected updatePageEl(pageEl, changedProps: PropertyValues) {
pageEl.hass = this.hass;
pageEl.narrow = this.narrow;

View File

@ -29,6 +29,11 @@ class PanelDeveloperTools extends LitElement {
@property() public narrow!: boolean;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("title");
}
protected render(): TemplateResult {
const page = this._page;
return html`

View File

@ -235,6 +235,11 @@ class HaPanelLogbook extends LocalizeMixin(PolymerElement) {
};
}
ready() {
super.ready();
this.hass.loadBackendTranslation("title");
}
connectedCallback() {
super.connectedCallback();
// We are unable to parse date because we use intl api to render date

View File

@ -170,6 +170,7 @@ class HaMfaModuleSetupFlow extends LocalizeMixin(EventsMixin(PolymerElement)) {
ready() {
super.ready();
this.hass.loadBackendTranslation("mfa_setup", "auth");
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._submitStep();

View File

@ -113,6 +113,14 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
}
return resp;
},
loadBackendTranslation: (category, integration?, configFlow?) =>
// @ts-ignore
this._loadHassTranslations(
this.hass?.language,
category,
integration,
configFlow
),
...getState(),
...this._pendingHass,
};

View File

@ -3,6 +3,8 @@ import { computeRTL } from "../common/util/compute_rtl";
import {
getHassTranslations,
saveTranslationPreferences,
TranslationCategory,
getHassTranslationsPre109,
} from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { Constructor, HomeAssistant } from "../types";
@ -13,6 +15,17 @@ import {
getUserLanguage,
} from "../util/hass-translation";
import { HassBaseEl } from "./hass-base-mixin";
import { atLeastVersion } from "../common/config/version";
import { debounce } from "../common/util/debounce";
interface LoadedTranslationCategory {
// individual integrations loaded for this category
integrations: string[];
// if integrations that have been set up for this category are loaded
setup: boolean;
// if
configFlow: boolean;
}
/*
* superClass needs to contain `this.hass` and `this._updateHass`.
@ -23,6 +36,11 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
// eslint-disable-next-line: variable-name
private __coreProgress?: string;
private __loadedTranslations: {
// track what things have been loaded
[category: string]: LoadedTranslationCategory;
} = {};
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.addEventListener("hass-language-select", (e) =>
@ -39,11 +57,18 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
this._selectLanguage(language, false);
}
});
this.hass!.connection.subscribeEvents(
debounce(() => {
this._refetchCachedHassTranslations(false);
}, 500),
"component_loaded"
);
this._applyTranslations(this.hass!);
}
protected hassReconnected() {
super.hassReconnected();
this._refetchCachedHassTranslations(true);
this._applyTranslations(this.hass!);
}
@ -69,18 +94,85 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
saveTranslationPreferences(this.hass, { language });
}
this._applyTranslations(this.hass);
this._refetchCachedHassTranslations(true);
}
private _applyTranslations(hass: HomeAssistant) {
document.querySelector("html")!.setAttribute("lang", hass.language);
this.style.direction = computeRTL(hass) ? "rtl" : "ltr";
this._loadCoreTranslations(hass.language);
this._loadHassTranslations(hass.language);
this._loadFragmentTranslations(hass.language, hass.panelUrl);
}
private async _loadHassTranslations(language: string) {
const resources = await getHassTranslations(this.hass!, language);
private async _loadHassTranslations(
language: string,
category: Parameters<typeof getHassTranslations>[2],
integration?: Parameters<typeof getHassTranslations>[3],
configFlow?: Parameters<typeof getHassTranslations>[4],
force = false
) {
if (
__BACKWARDS_COMPAT__ &&
!atLeastVersion(this.hass!.connection.haVersion, 0, 109)
) {
if (category !== "state") {
return;
}
const resources = await getHassTranslationsPre109(this.hass!, language);
// Ignore the repsonse if user switched languages before we got response
if (this.hass!.language !== language) {
return;
}
this._updateResources(language, resources);
return;
}
let alreadyLoaded: LoadedTranslationCategory;
if (category in this.__loadedTranslations) {
alreadyLoaded = this.__loadedTranslations[category];
} else {
alreadyLoaded = this.__loadedTranslations[category] = {
integrations: [],
setup: false,
configFlow: false,
};
}
// Check if already loaded
if (!force) {
if (integration) {
if (alreadyLoaded.integrations.includes(integration)) {
return;
}
} else if (
configFlow ? alreadyLoaded.configFlow : alreadyLoaded.setup
) {
return;
}
}
// Add to cache
if (integration) {
if (!alreadyLoaded.integrations.includes(integration)) {
alreadyLoaded.integrations.push(integration);
}
} else {
alreadyLoaded.setup = true;
if (configFlow) {
alreadyLoaded.configFlow = true;
}
}
const resources = await getHassTranslations(
this.hass!,
language,
category,
integration,
configFlow
);
// Ignore the repsonse if user switched languages before we got response
if (this.hass!.language !== language) {
@ -134,4 +226,20 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
}
this._updateHass(changes);
}
private _refetchCachedHassTranslations(includeConfigFlow: boolean) {
for (const [category, cache] of Object.entries(
this.__loadedTranslations
)) {
if (cache.setup) {
this._loadHassTranslations(
this.hass!.language,
category as TranslationCategory,
undefined,
includeConfigFlow && cache.configFlow,
true
);
}
}
}
};

View File

@ -11,6 +11,7 @@ import {
import { LocalizeFunc } from "./common/translations/localize";
import { CoreFrontendUserData } from "./data/frontend";
import { ExternalMessaging } from "./external_app/external_messaging";
import { getHassTranslations } from "./data/translation";
declare global {
/* eslint-disable no-var, no-redeclare */
@ -19,6 +20,7 @@ declare global {
var __BUILD__: "latest" | "es5";
var __VERSION__: string;
var __STATIC_PATH__: string;
var __BACKWARDS_COMPAT__: boolean;
/* eslint-enable no-var, no-redeclare */
interface Window {
@ -174,6 +176,11 @@ export interface HomeAssistant {
fetchWithAuth(path: string, init?: { [key: string]: any }): Promise<Response>;
sendWS(msg: MessageBase): void;
callWS<T>(msg: MessageBase): Promise<T>;
loadBackendTranslation(
category: Parameters<typeof getHassTranslations>[2],
integration?: Parameters<typeof getHassTranslations>[3],
configFlow?: Parameters<typeof getHassTranslations>[4]
): Promise<void>;
}
export type LightEntity = HassEntityBase & {