diff --git a/src/common/string/format-list.ts b/src/common/string/format-list.ts new file mode 100644 index 0000000000..4eb53717c1 --- /dev/null +++ b/src/common/string/format-list.ts @@ -0,0 +1,27 @@ +import memoizeOne from "memoize-one"; +import "../../resources/intl-polyfill"; +import { FrontendLocaleData } from "../../data/translation"; + +export const formatListWithAnds = ( + locale: FrontendLocaleData, + list: string[] +) => formatConjunctionList(locale).format(list); + +export const formatListWithOrs = (locale: FrontendLocaleData, list: string[]) => + formatDisjunctionList(locale).format(list); + +const formatConjunctionList = memoizeOne( + (locale: FrontendLocaleData) => + new Intl.ListFormat(locale.language, { + style: "long", + type: "conjunction", + }) +); + +const formatDisjunctionList = memoizeOne( + (locale: FrontendLocaleData) => + new Intl.ListFormat(locale.language, { + style: "long", + type: "disjunction", + }) +); diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index 04819d448a..42a97acd38 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -31,6 +31,10 @@ import { VariablesAction, WaitForTriggerAction, } from "./script"; +import { formatListWithAnds } from "../common/string/format-list"; + +const actionTranslationBaseKey = + "ui.panel.config.automation.editor.actions.type"; export const describeAction = ( hass: HomeAssistant, @@ -75,25 +79,8 @@ const tryDescribeAction = ( if (actionType === "service") { const config = action as ActionTypes["service"]; - let base: string | undefined; - - if ( - config.service_template || - (config.service && isTemplate(config.service)) - ) { - base = "Call a service based on a template"; - } else if (config.service) { - const [domain, serviceName] = config.service.split(".", 2); - const service = hass.services[domain][serviceName]; - base = service - ? `${domainToName(hass.localize, domain)}: ${service.name}` - : `Call service: ${config.service}`; - } else { - return "Call a service"; - } + const targets: string[] = []; if (config.target) { - const targets: string[] = []; - for (const [key, label] of Object.entries({ area_id: "areas", device_id: "devices", @@ -108,7 +95,12 @@ const tryDescribeAction = ( for (const targetThing of keyConf) { if (isTemplate(targetThing)) { - targets.push(`templated ${label}`); + targets.push( + hass.localize( + `${actionTranslationBaseKey}.service.description.target_template`, + { name: label } + ) + ); break; } else if (key === "entity_id") { if (targetThing.includes(".")) { @@ -125,7 +117,11 @@ const tryDescribeAction = ( computeEntityRegistryName(hass, entityReg) || targetThing ); } else { - targets.push("unknown entity"); + targets.push( + hass.localize( + `${actionTranslationBaseKey}.service.description.target_unknown_entity` + ) + ); } } } else if (key === "device_id") { @@ -133,46 +129,105 @@ const tryDescribeAction = ( if (device) { targets.push(computeDeviceName(device, hass)); } else { - targets.push("unknown device"); + targets.push( + hass.localize( + `${actionTranslationBaseKey}.service.description.target_unknown_device` + ) + ); } } else if (key === "area_id") { const area = hass.areas[targetThing]; if (area?.name) { targets.push(area.name); } else { - targets.push("unknown area"); + targets.push( + hass.localize( + `${actionTranslationBaseKey}.service.description.target_unknown_area` + ) + ); } } else { targets.push(targetThing); } } } - if (targets.length > 0) { - base += ` ${targets.join(", ")}`; - } } - return base; + if ( + config.service_template || + (config.service && isTemplate(config.service)) + ) { + return hass.localize( + `${actionTranslationBaseKey}.service.description.service_based_on_template`, + { targets: formatListWithAnds(hass.locale, targets) } + ); + } + + if (config.service) { + const [domain, serviceName] = config.service.split(".", 2); + const service = hass.services[domain][serviceName]; + return hass.localize( + `${actionTranslationBaseKey}.service.description.service_based_on_name`, + { + name: service + ? `${domainToName(hass.localize, domain)}: ${service.name}` + : config.service, + targets: formatListWithAnds(hass.locale, targets), + } + ); + } + return hass.localize( + `${actionTranslationBaseKey}.service.description.service` + ); } if (actionType === "delay") { const config = action as DelayAction; let duration: string; - if (typeof config.delay === "number") { - duration = `for ${secondsToDuration(config.delay)!}`; + duration = hass.localize( + `${actionTranslationBaseKey}.delay.description.duration_string`, + { + duration: secondsToDuration(config.delay)!, + } + ); } else if (typeof config.delay === "string") { duration = isTemplate(config.delay) - ? "based on a template" - : `for ${config.delay || "a duration"}`; + ? hass.localize( + `${actionTranslationBaseKey}.delay.description.duration_template` + ) + : hass.localize( + `${actionTranslationBaseKey}.delay.description.duration_string`, + { + duration: + config.delay || + hass.localize( + `${actionTranslationBaseKey}.delay.description.duration_unknown` + ), + } + ); } else if (config.delay) { - duration = `for ${formatDuration(config.delay)}`; + duration = hass.localize( + `${actionTranslationBaseKey}.delay.description.duration_string`, + { + duration: formatDuration(config.delay), + } + ); } else { - duration = "for a duration"; + duration = hass.localize( + `${actionTranslationBaseKey}.delay.description.duration_string`, + { + duration: hass.localize( + `${actionTranslationBaseKey}.delay.description.duration_unknown` + ), + } + ); } - return `Delay ${duration}`; + return hass.localize(`${actionTranslationBaseKey}.delay.description.full`, { + duration: duration, + }); } if (actionType === "activate_scene") { @@ -184,77 +239,139 @@ const tryDescribeAction = ( entityId = config.target?.entity_id || config.entity_id; } if (!entityId) { - return "Activate a scene"; + return hass.localize( + `${actionTranslationBaseKey}.activate_scene.description.activate_scene` + ); } const sceneStateObj = entityId ? hass.states[entityId] : undefined; - return `Activate scene ${ - sceneStateObj ? computeStateName(sceneStateObj) : entityId - }`; + return hass.localize( + `${actionTranslationBaseKey}.activate_scene.description.activate_scene_with_name`, + { name: sceneStateObj ? computeStateName(sceneStateObj) : entityId } + ); } if (actionType === "play_media") { const config = action as PlayMediaAction; const entityId = config.target?.entity_id || config.entity_id; const mediaStateObj = entityId ? hass.states[entityId] : undefined; - return `Play ${ - config.metadata.title || config.data.media_content_id || "media" - } on ${ - mediaStateObj - ? computeStateName(mediaStateObj) - : entityId || "a media player" - }`; + return hass.localize( + `${actionTranslationBaseKey}.play_media.description.full`, + { + hasMedia: config.metadata.title || config.data.media_content_id, + media: config.metadata.title || config.data.media_content_id, + hasMediaPlayer: mediaStateObj ? true : entityId !== undefined, + mediaPlayer: mediaStateObj ? computeStateName(mediaStateObj) : entityId, + } + ); } if (actionType === "wait_for_trigger") { const config = action as WaitForTriggerAction; const triggers = ensureArray(config.wait_for_trigger); if (!triggers || triggers.length === 0) { - return "Wait for a trigger"; + return hass.localize( + `${actionTranslationBaseKey}.wait_for_trigger.description.wait_for_a_trigger` + ); } - return `Wait for ${triggers - .map((trigger) => describeTrigger(trigger, hass, entityRegistry)) - .join(", ")}`; + const triggerNames = triggers.map((trigger) => + describeTrigger(trigger, hass, entityRegistry) + ); + return hass.localize( + `${actionTranslationBaseKey}.wait_for_trigger.description.wait_for_triggers_with_name`, + { triggers: formatListWithAnds(hass.locale, triggerNames) } + ); } if (actionType === "variables") { const config = action as VariablesAction; - return `Define variables ${Object.keys(config.variables).join(", ")}`; + return hass.localize( + `${actionTranslationBaseKey}.variables.description.full`, + { + names: formatListWithAnds(hass.locale, Object.keys(config.variables)), + } + ); } if (actionType === "fire_event") { const config = action as EventAction; if (isTemplate(config.event)) { - return "Fire event based on a template"; + return hass.localize( + `${actionTranslationBaseKey}.event.description.full`, + { + name: hass.localize( + `${actionTranslationBaseKey}.event.description.template` + ), + } + ); } - return `Fire event ${config.event}`; + return hass.localize(`${actionTranslationBaseKey}.event.description.full`, { + name: config.event, + }); } if (actionType === "wait_template") { - return "Wait for a template to render true"; - } - - if (actionType === "check_condition") { - return describeCondition(action as Condition, hass, entityRegistry); + return hass.localize( + `${actionTranslationBaseKey}.wait_template.description.full` + ); } if (actionType === "stop") { const config = action as StopAction; - return `Stop${config.stop ? ` because: ${config.stop}` : ""}`; + return hass.localize(`${actionTranslationBaseKey}.stop.description.full`, { + hasReason: config.stop !== undefined, + reason: config.stop, + }); } if (actionType === "if") { const config = action as IfAction; - return `Perform an action if: ${ - !config.if - ? "" - : typeof config.if === "string" - ? config.if - : ensureArray(config.if).length > 1 - ? `${ensureArray(config.if).length} conditions` - : ensureArray(config.if).length - ? describeCondition(ensureArray(config.if)[0], hass, entityRegistry) - : "" - }${config.else ? " (or else!)" : ""}`; + + let ifConditions: string[] = []; + if (Array.isArray(config.if)) { + const conditions = ensureArray(config.if); + conditions.forEach((condition) => { + ifConditions.push(describeCondition(condition, hass, entityRegistry)); + }); + } else { + ifConditions = [config.if]; + } + + let elseActions: string[] = []; + if (config.else) { + if (Array.isArray(config.else)) { + const actions = ensureArray(config.else); + actions.forEach((currentAction) => { + elseActions.push( + describeAction(hass, entityRegistry, currentAction, undefined) + ); + }); + } else { + elseActions = [ + describeAction(hass, entityRegistry, config.else, undefined), + ]; + } + } + + let thenActions: string[] = []; + if (Array.isArray(config.then)) { + const actions = ensureArray(config.then); + actions.forEach((currentAction) => { + thenActions.push( + describeAction(hass, entityRegistry, currentAction, undefined) + ); + }); + } else { + thenActions = [ + describeAction(hass, entityRegistry, config.then, undefined), + ]; + } + + return hass.localize(`${actionTranslationBaseKey}.if.description.full`, { + hasElse: config.else !== undefined, + action: formatListWithAnds(hass.locale, thenActions), + conditions: formatListWithAnds(hass.locale, ifConditions), + elseAction: formatListWithAnds(hass.locale, elseActions), + }); } if (actionType === "choose") { @@ -262,42 +379,64 @@ const tryDescribeAction = ( if (config.choose) { const numActions = ensureArray(config.choose).length + (config.default ? 1 : 0); - return `Choose between ${numActions} action${ - numActions === 1 ? "" : "s" - }`; + return hass.localize( + `${actionTranslationBaseKey}.choose.description.full`, + { number: numActions } + ); } - return "Choose an action"; + return hass.localize( + `${actionTranslationBaseKey}.choose.description.no_action` + ); } if (actionType === "repeat") { const config = action as RepeatAction; - let base = "Repeat an action"; + let chosenAction = ""; if ("count" in config.repeat) { const count = config.repeat.count; - base += ` ${count} time${Number(count) === 1 ? "" : "s"}`; + chosenAction = hass.localize( + `${actionTranslationBaseKey}.repeat.description.count`, + { count: count } + ); } else if ("while" in config.repeat) { - base += ` while ${ensureArray(config.repeat.while) - .map((condition) => describeCondition(condition, hass, entityRegistry)) - .join(", ")} is true`; + const conditions = ensureArray(config.repeat.while).map((condition) => + describeCondition(condition, hass, entityRegistry) + ); + chosenAction = hass.localize( + `${actionTranslationBaseKey}.repeat.description.while`, + { conditions: formatListWithAnds(hass.locale, conditions) } + ); } else if ("until" in config.repeat) { - base += ` until ${ensureArray(config.repeat.until) - .map((condition) => describeCondition(condition, hass, entityRegistry)) - .join(", ")} is true`; + const conditions = ensureArray(config.repeat.until).map((condition) => + describeCondition(condition, hass, entityRegistry) + ); + chosenAction = hass.localize( + `${actionTranslationBaseKey}.repeat.description.until`, + { conditions: formatListWithAnds(hass.locale, conditions) } + ); } else if ("for_each" in config.repeat) { - base += ` for every item: ${ensureArray(config.repeat.for_each) - .map((item) => JSON.stringify(item)) - .join(", ")}`; + const items = ensureArray(config.repeat.for_each).map((item) => + JSON.stringify(item) + ); + chosenAction = hass.localize( + `${actionTranslationBaseKey}.repeat.description.for_each`, + { items: formatListWithAnds(hass.locale, items) } + ); } - return base; + return hass.localize( + `${actionTranslationBaseKey}.repeat.description.full`, + { chosenAction: chosenAction } + ); } if (actionType === "check_condition") { - return `Test ${describeCondition( - action as Condition, - hass, - entityRegistry - )}`; + return hass.localize( + `${actionTranslationBaseKey}.check_condition.description.full`, + { + condition: describeCondition(action as Condition, hass, entityRegistry), + } + ); } if (actionType === "device_action") { @@ -313,7 +452,7 @@ const tryDescribeAction = ( if (localized) { return localized; } - const stateObj = hass.states[config.entity_id as string]; + const stateObj = hass.states[config.entity_id]; return `${config.type || "Perform action with"} ${ stateObj ? computeStateName(stateObj) : config.entity_id }`; @@ -322,7 +461,10 @@ const tryDescribeAction = ( if (actionType === "parallel") { const config = action as ParallelAction; const numActions = ensureArray(config.parallel).length; - return `Run ${numActions} action${numActions === 1 ? "" : "s"} in parallel`; + return hass.localize( + `${actionTranslationBaseKey}.parallel.description.full`, + { number: numActions } + ); } return actionType; diff --git a/src/translations/en.json b/src/translations/en.json index 567c33c6e3..19b0fa9956 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2573,25 +2573,50 @@ "label": "Call service", "response_variable": "Response variable", "has_optional_response": "This service can return a response, if you want to use the response, enter the name of a variable the response will be saved in", - "has_response": "This service returns a response, enter the name of a variable the response will be saved in" + "has_response": "This service returns a response, enter the name of a variable the response will be saved in", + "description": { + "service_based_on_template": "Call a service based on a template on {targets}", + "service_based_on_name": "Call a service ''{name}'' on {targets}", + "service": "Call a service", + "target_template": "templated {name}", + "target_unknown_entity": "unknown entity", + "target_unknown_device": "unknown device", + "target_unknown_area": "unknown area" + } }, "play_media": { - "label": "Play media" + "label": "Play media", + "description": { + "full": "Play {hasMedia, select, \n true {{media}} \n other {media}\n } on {hasMediaPlayer, select, \n true {{mediaPlayer}} \n other {a media player}\n }" + } }, "delay": { "label": "Wait for time to pass (delay)", - "delay": "Duration" + "delay": "Duration", + "description": { + "full": "Delay {duration}", + "duration_string": "for {string}", + "duration_template": "based on a template", + "duration_unknown": "a duration" + } }, "wait_template": { "label": "Wait for a template", "wait_template": "Wait Template", "timeout": "Timeout (optional)", - "continue_timeout": "Continue on timeout" + "continue_timeout": "Continue on timeout", + "description": { + "full": "Wait for a template to evaluate to true" + } }, "wait_for_trigger": { "label": "Wait for a trigger", "timeout": "[%key:ui::panel::config::automation::editor::actions::type::wait_template::timeout%]", - "continue_timeout": "[%key:ui::panel::config::automation::editor::actions::type::wait_template::continue_timeout%]" + "continue_timeout": "[%key:ui::panel::config::automation::editor::actions::type::wait_template::continue_timeout%]", + "description": { + "wait_for_a_trigger": "Wait for a trigger", + "wait_for_triggers_with_name": "Wait for ''{triggers}''" + } }, "condition": { "label": "Condition" @@ -2599,7 +2624,11 @@ "event": { "label": "Event", "event": "[%key:ui::panel::config::automation::editor::triggers::type::event::label%]", - "event_data": "[%key:ui::panel::config::automation::editor::triggers::type::event::event_data%]" + "event_data": "[%key:ui::panel::config::automation::editor::triggers::type::event::event_data%]", + "description": { + "full": "Fire event {name}", + "template": "based on a template" + } }, "device_id": { "label": "Device", @@ -2618,7 +2647,11 @@ }, "activate_scene": { "label": "Scene", - "scene": "Scene" + "scene": "Scene", + "description": { + "activate_scene": "Activate a scene", + "activate_scene_with_name": "Activate scene {name}" + } }, "repeat": { "label": "Repeat", @@ -2636,7 +2669,14 @@ "conditions": "Until conditions" } }, - "sequence": "Actions" + "sequence": "Actions", + "description": { + "full": "Repeat an action {chosenAction}", + "count": "{count} {count, plural,\n one {time}\n other {times}\n}", + "while": "while ''{conditions}'' is true", + "until": "until ''{conditions}'' is true", + "for_each": "for every item: {items}" + } }, "choose": { "label": "Choose", @@ -2646,26 +2686,47 @@ "add_option": "Add option", "remove_option": "Remove option", "conditions": "Conditions", - "sequence": "Actions" + "sequence": "Actions", + "description": { + "full": "Choose between {number} {number, plural,\n one {action}\n other{actions}\n}", + "no_action": "Choose an action" + } }, "if": { "label": "If-then", "if": "If", "then": "Then", "else": "Else", - "add_else": "Add else" + "add_else": "Add else", + "description": { + "full": "Perform ''{action}'' if ''{conditions}''{hasElse, select, \n true { otherwise ''{elseAction}''} \n other {}\n } " + } }, "stop": { "label": "Stop", "stop": "Reason for stopping", "response_variable": "The name of the variable to use as response", - "error": "Stop because of an unexpected error" + "error": "Stop because of an unexpected error", + "description": { + "full": "Stop {hasReason, select, \n true { because: {reason}} \n other {}\n }" + } }, "parallel": { - "label": "Run in parallel" + "label": "Run in parallel", + "description": { + "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in parallel" + } }, "variables": { - "label": "Define variables" + "label": "Define variables", + "description": { + "full": "Define variables {names}" + } + }, + "check_condition": { + "description": { + "full": "Test {condition}" + } } } } @@ -2782,7 +2843,6 @@ }, "discover_blueprint_tip": "[%key:ui::panel::config::automation::dialog_new::discover_blueprint_tip%]" }, - "editor": { "alias": "Name", "icon": "Icon",