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,
WaitForTriggerAction,
} from "./script";
import { formatListWithAnds } from "../common/string/format-list";
const actionTranslationBaseKey =
"ui.panel.config.automation.editor.actions.type";
export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
@ -75,25 +79,8 @@ const tryDescribeAction = <T extends ActionType>(
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 = <T extends ActionType>(
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 = <T extends ActionType>(
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 = <T extends ActionType>(
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 = <T extends ActionType>(
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 = <T extends ActionType>(
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 = <T extends ActionType>(
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 = <T extends ActionType>(
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;

View File

@ -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",