Make automation editor card headers translateable (actions) (#17027)

This commit is contained in:
Simon Lamon 2023-07-02 14:29:54 +02:00 committed by GitHub
parent a477120f13
commit 7dbb419c30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 334 additions and 105 deletions

View File

@ -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",
})
);

View File

@ -31,6 +31,10 @@ import {
VariablesAction, VariablesAction,
WaitForTriggerAction, WaitForTriggerAction,
} from "./script"; } from "./script";
import { formatListWithAnds } from "../common/string/format-list";
const actionTranslationBaseKey =
"ui.panel.config.automation.editor.actions.type";
export const describeAction = <T extends ActionType>( export const describeAction = <T extends ActionType>(
hass: HomeAssistant, hass: HomeAssistant,
@ -75,25 +79,8 @@ const tryDescribeAction = <T extends ActionType>(
if (actionType === "service") { if (actionType === "service") {
const config = action as ActionTypes["service"]; const config = action as ActionTypes["service"];
let base: string | undefined; const targets: string[] = [];
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";
}
if (config.target) { if (config.target) {
const targets: string[] = [];
for (const [key, label] of Object.entries({ for (const [key, label] of Object.entries({
area_id: "areas", area_id: "areas",
device_id: "devices", device_id: "devices",
@ -108,7 +95,12 @@ const tryDescribeAction = <T extends ActionType>(
for (const targetThing of keyConf) { for (const targetThing of keyConf) {
if (isTemplate(targetThing)) { if (isTemplate(targetThing)) {
targets.push(`templated ${label}`); targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_template`,
{ name: label }
)
);
break; break;
} else if (key === "entity_id") { } else if (key === "entity_id") {
if (targetThing.includes(".")) { if (targetThing.includes(".")) {
@ -125,7 +117,11 @@ const tryDescribeAction = <T extends ActionType>(
computeEntityRegistryName(hass, entityReg) || targetThing computeEntityRegistryName(hass, entityReg) || targetThing
); );
} else { } else {
targets.push("unknown entity"); targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_entity`
)
);
} }
} }
} else if (key === "device_id") { } else if (key === "device_id") {
@ -133,46 +129,105 @@ const tryDescribeAction = <T extends ActionType>(
if (device) { if (device) {
targets.push(computeDeviceName(device, hass)); targets.push(computeDeviceName(device, hass));
} else { } else {
targets.push("unknown device"); targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_device`
)
);
} }
} else if (key === "area_id") { } else if (key === "area_id") {
const area = hass.areas[targetThing]; const area = hass.areas[targetThing];
if (area?.name) { if (area?.name) {
targets.push(area.name); targets.push(area.name);
} else { } else {
targets.push("unknown area"); targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_area`
)
);
} }
} else { } else {
targets.push(targetThing); 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") { if (actionType === "delay") {
const config = action as DelayAction; const config = action as DelayAction;
let duration: string; let duration: string;
if (typeof config.delay === "number") { 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") { } else if (typeof config.delay === "string") {
duration = isTemplate(config.delay) duration = isTemplate(config.delay)
? "based on a template" ? hass.localize(
: `for ${config.delay || "a duration"}`; `${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) { } else if (config.delay) {
duration = `for ${formatDuration(config.delay)}`; duration = hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_string`,
{
duration: formatDuration(config.delay),
}
);
} else { } 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") { if (actionType === "activate_scene") {
@ -184,77 +239,139 @@ const tryDescribeAction = <T extends ActionType>(
entityId = config.target?.entity_id || config.entity_id; entityId = config.target?.entity_id || config.entity_id;
} }
if (!entityId) { if (!entityId) {
return "Activate a scene"; return hass.localize(
`${actionTranslationBaseKey}.activate_scene.description.activate_scene`
);
} }
const sceneStateObj = entityId ? hass.states[entityId] : undefined; const sceneStateObj = entityId ? hass.states[entityId] : undefined;
return `Activate scene ${ return hass.localize(
sceneStateObj ? computeStateName(sceneStateObj) : entityId `${actionTranslationBaseKey}.activate_scene.description.activate_scene_with_name`,
}`; { name: sceneStateObj ? computeStateName(sceneStateObj) : entityId }
);
} }
if (actionType === "play_media") { if (actionType === "play_media") {
const config = action as PlayMediaAction; const config = action as PlayMediaAction;
const entityId = config.target?.entity_id || config.entity_id; const entityId = config.target?.entity_id || config.entity_id;
const mediaStateObj = entityId ? hass.states[entityId] : undefined; const mediaStateObj = entityId ? hass.states[entityId] : undefined;
return `Play ${ return hass.localize(
config.metadata.title || config.data.media_content_id || "media" `${actionTranslationBaseKey}.play_media.description.full`,
} on ${ {
mediaStateObj hasMedia: config.metadata.title || config.data.media_content_id,
? computeStateName(mediaStateObj) media: config.metadata.title || config.data.media_content_id,
: entityId || "a media player" hasMediaPlayer: mediaStateObj ? true : entityId !== undefined,
}`; mediaPlayer: mediaStateObj ? computeStateName(mediaStateObj) : entityId,
}
);
} }
if (actionType === "wait_for_trigger") { if (actionType === "wait_for_trigger") {
const config = action as WaitForTriggerAction; const config = action as WaitForTriggerAction;
const triggers = ensureArray(config.wait_for_trigger); const triggers = ensureArray(config.wait_for_trigger);
if (!triggers || triggers.length === 0) { 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 const triggerNames = triggers.map((trigger) =>
.map((trigger) => describeTrigger(trigger, hass, entityRegistry)) describeTrigger(trigger, hass, entityRegistry)
.join(", ")}`; );
return hass.localize(
`${actionTranslationBaseKey}.wait_for_trigger.description.wait_for_triggers_with_name`,
{ triggers: formatListWithAnds(hass.locale, triggerNames) }
);
} }
if (actionType === "variables") { if (actionType === "variables") {
const config = action as VariablesAction; 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") { if (actionType === "fire_event") {
const config = action as EventAction; const config = action as EventAction;
if (isTemplate(config.event)) { 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") { if (actionType === "wait_template") {
return "Wait for a template to render true"; return hass.localize(
} `${actionTranslationBaseKey}.wait_template.description.full`
);
if (actionType === "check_condition") {
return describeCondition(action as Condition, hass, entityRegistry);
} }
if (actionType === "stop") { if (actionType === "stop") {
const config = action as StopAction; 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") { if (actionType === "if") {
const config = action as IfAction; const config = action as IfAction;
return `Perform an action if: ${
!config.if let ifConditions: string[] = [];
? "" if (Array.isArray(config.if)) {
: typeof config.if === "string" const conditions = ensureArray(config.if);
? config.if conditions.forEach((condition) => {
: ensureArray(config.if).length > 1 ifConditions.push(describeCondition(condition, hass, entityRegistry));
? `${ensureArray(config.if).length} conditions` });
: ensureArray(config.if).length } else {
? describeCondition(ensureArray(config.if)[0], hass, entityRegistry) ifConditions = [config.if];
: "" }
}${config.else ? " (or else!)" : ""}`;
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") { if (actionType === "choose") {
@ -262,42 +379,64 @@ const tryDescribeAction = <T extends ActionType>(
if (config.choose) { if (config.choose) {
const numActions = const numActions =
ensureArray(config.choose).length + (config.default ? 1 : 0); ensureArray(config.choose).length + (config.default ? 1 : 0);
return `Choose between ${numActions} action${ return hass.localize(
numActions === 1 ? "" : "s" `${actionTranslationBaseKey}.choose.description.full`,
}`; { number: numActions }
);
} }
return "Choose an action"; return hass.localize(
`${actionTranslationBaseKey}.choose.description.no_action`
);
} }
if (actionType === "repeat") { if (actionType === "repeat") {
const config = action as RepeatAction; const config = action as RepeatAction;
let base = "Repeat an action"; let chosenAction = "";
if ("count" in config.repeat) { if ("count" in config.repeat) {
const count = config.repeat.count; 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) { } else if ("while" in config.repeat) {
base += ` while ${ensureArray(config.repeat.while) const conditions = ensureArray(config.repeat.while).map((condition) =>
.map((condition) => describeCondition(condition, hass, entityRegistry)) describeCondition(condition, hass, entityRegistry)
.join(", ")} is true`; );
chosenAction = hass.localize(
`${actionTranslationBaseKey}.repeat.description.while`,
{ conditions: formatListWithAnds(hass.locale, conditions) }
);
} else if ("until" in config.repeat) { } else if ("until" in config.repeat) {
base += ` until ${ensureArray(config.repeat.until) const conditions = ensureArray(config.repeat.until).map((condition) =>
.map((condition) => describeCondition(condition, hass, entityRegistry)) describeCondition(condition, hass, entityRegistry)
.join(", ")} is true`; );
chosenAction = hass.localize(
`${actionTranslationBaseKey}.repeat.description.until`,
{ conditions: formatListWithAnds(hass.locale, conditions) }
);
} else if ("for_each" in config.repeat) { } else if ("for_each" in config.repeat) {
base += ` for every item: ${ensureArray(config.repeat.for_each) const items = ensureArray(config.repeat.for_each).map((item) =>
.map((item) => JSON.stringify(item)) JSON.stringify(item)
.join(", ")}`; );
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") { if (actionType === "check_condition") {
return `Test ${describeCondition( return hass.localize(
action as Condition, `${actionTranslationBaseKey}.check_condition.description.full`,
hass, {
entityRegistry condition: describeCondition(action as Condition, hass, entityRegistry),
)}`; }
);
} }
if (actionType === "device_action") { if (actionType === "device_action") {
@ -313,7 +452,7 @@ const tryDescribeAction = <T extends ActionType>(
if (localized) { if (localized) {
return 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"} ${ return `${config.type || "Perform action with"} ${
stateObj ? computeStateName(stateObj) : config.entity_id stateObj ? computeStateName(stateObj) : config.entity_id
}`; }`;
@ -322,7 +461,10 @@ const tryDescribeAction = <T extends ActionType>(
if (actionType === "parallel") { if (actionType === "parallel") {
const config = action as ParallelAction; const config = action as ParallelAction;
const numActions = ensureArray(config.parallel).length; 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; return actionType;

View File

@ -2573,25 +2573,50 @@
"label": "Call service", "label": "Call service",
"response_variable": "Response variable", "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_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": { "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": { "delay": {
"label": "Wait for time to pass (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": { "wait_template": {
"label": "Wait for a template", "label": "Wait for a template",
"wait_template": "Wait Template", "wait_template": "Wait Template",
"timeout": "Timeout (optional)", "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": { "wait_for_trigger": {
"label": "Wait for a trigger", "label": "Wait for a trigger",
"timeout": "[%key:ui::panel::config::automation::editor::actions::type::wait_template::timeout%]", "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": { "condition": {
"label": "Condition" "label": "Condition"
@ -2599,7 +2624,11 @@
"event": { "event": {
"label": "Event", "label": "Event",
"event": "[%key:ui::panel::config::automation::editor::triggers::type::event::label%]", "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": { "device_id": {
"label": "Device", "label": "Device",
@ -2618,7 +2647,11 @@
}, },
"activate_scene": { "activate_scene": {
"label": "Scene", "label": "Scene",
"scene": "Scene" "scene": "Scene",
"description": {
"activate_scene": "Activate a scene",
"activate_scene_with_name": "Activate scene {name}"
}
}, },
"repeat": { "repeat": {
"label": "Repeat", "label": "Repeat",
@ -2636,7 +2669,14 @@
"conditions": "Until conditions" "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": { "choose": {
"label": "Choose", "label": "Choose",
@ -2646,26 +2686,47 @@
"add_option": "Add option", "add_option": "Add option",
"remove_option": "Remove option", "remove_option": "Remove option",
"conditions": "Conditions", "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": { "if": {
"label": "If-then", "label": "If-then",
"if": "If", "if": "If",
"then": "Then", "then": "Then",
"else": "Else", "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": { "stop": {
"label": "Stop", "label": "Stop",
"stop": "Reason for stopping", "stop": "Reason for stopping",
"response_variable": "The name of the variable to use as response", "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": { "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": { "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%]" "discover_blueprint_tip": "[%key:ui::panel::config::automation::dialog_new::discover_blueprint_tip%]"
}, },
"editor": { "editor": {
"alias": "Name", "alias": "Name",
"icon": "Icon", "icon": "Icon",