Add localization

Fixed dashboard strings are replaced with a string in the language as
defined by the user.
Currently, languages `en` and `nl` are supported.
If the user-defined language isn't supported, the language will fall
back to english.
Untranslated strings will fall back to the translation keyword.

Resolves #166
This commit is contained in:
Ferry Cools
2024-12-13 06:56:23 +01:00
committed by GitHub
parent 4b927a7f7b
commit 2cd4512b4f
14 changed files with 394 additions and 188 deletions

View File

@ -1,10 +1,11 @@
import {configurationDefaults} from "./configurationDefaults"; import {getConfigurationDefaults} from "./configurationDefaults";
import {HassEntities, HassEntity} from "home-assistant-js-websocket"; import {HassEntities, HassEntity} from "home-assistant-js-websocket";
import deepmerge from "deepmerge"; import deepmerge from "deepmerge";
import {EntityRegistryEntry} from "./types/homeassistant/data/entity_registry"; import {EntityRegistryEntry} from "./types/homeassistant/data/entity_registry";
import {DeviceRegistryEntry} from "./types/homeassistant/data/device_registry"; import {DeviceRegistryEntry} from "./types/homeassistant/data/device_registry";
import {AreaRegistryEntry} from "./types/homeassistant/data/area_registry"; import {AreaRegistryEntry} from "./types/homeassistant/data/area_registry";
import {generic} from "./types/strategy/generic"; import {generic} from "./types/strategy/generic";
import setupCustomLocalize from "./localize";
import StrategyArea = generic.StrategyArea; import StrategyArea = generic.StrategyArea;
/** /**
@ -68,6 +69,7 @@ class Helper {
* @private * @private
*/ */
static #debug: boolean; static #debug: boolean;
static customLocalize: Function;
/** /**
* Class constructor. * Class constructor.
@ -140,8 +142,12 @@ class Helper {
*/ */
static async initialize(info: generic.DashBoardInfo): Promise<void> { static async initialize(info: generic.DashBoardInfo): Promise<void> {
// Initialize properties. // Initialize properties.
this.#hassStates = info.hass.states; this.customLocalize = setupCustomLocalize(info.hass);
const configurationDefaults = getConfigurationDefaults(this.customLocalize)
this.#strategyOptions = deepmerge(configurationDefaults, info.config?.strategy?.options ?? {}); this.#strategyOptions = deepmerge(configurationDefaults, info.config?.strategy?.options ?? {});
this.#hassStates = info.hass.states;
this.#debug = this.#strategyOptions.debug; this.#debug = this.#strategyOptions.debug;
try { try {

View File

@ -4,151 +4,153 @@ import StrategyDefaults = generic.StrategyDefaults;
/** /**
* Default configuration for the mushroom strategy. * Default configuration for the mushroom strategy.
*/ */
export const configurationDefaults: StrategyDefaults = { export const getConfigurationDefaults = (localize: Function): StrategyDefaults => {
areas: { return {
undisclosed: { areas: {
area_id: "undisclosed", undisclosed: {
floor_id: null, area_id: "undisclosed",
name: "Undisclosed", floor_id: null,
picture: null, name: "Undisclosed",
icon: "mdi:floor-plan", picture: null,
labels: [], icon: "mdi:floor-plan",
aliases: [], labels: [],
hidden: false, aliases: [],
hidden: false,
}
},
debug: false,
domains: {
_: {
hide_config_entities: false,
},
default: {
title: localize("generic.miscellaneous"),
showControls: false,
hidden: false,
},
light: {
title: localize("light.lights"),
showControls: true,
iconOn: "mdi:lightbulb",
iconOff: "mdi:lightbulb-off",
onService: "light.turn_on",
offService: "light.turn_off",
hidden: false,
},
fan: {
title: localize("fan.fans"),
showControls: true,
iconOn: "mdi:fan",
iconOff: "mdi:fan-off",
onService: "fan.turn_on",
offService: "fan.turn_off",
hidden: false,
},
cover: {
title: localize("cover.covers"),
showControls: true,
iconOn: "mdi:arrow-up",
iconOff: "mdi:arrow-down",
onService: "cover.open_cover",
offService: "cover.close_cover",
hidden: false,
},
switch: {
title: localize("switch.switches"),
showControls: true,
iconOn: "mdi:power-plug",
iconOff: "mdi:power-plug-off",
onService: "switch.turn_on",
offService: "switch.turn_off",
hidden: false,
},
camera: {
title: localize("camera.cameras"),
showControls: false,
hidden: false,
},
lock: {
title: localize("lock.locks"),
showControls: false,
hidden: false,
},
climate: {
title: localize("climate.climates"),
showControls: false,
hidden: false,
},
media_player: {
title: localize("media_player.media_players"),
showControls: false,
hidden: false,
},
sensor: {
title: localize("sensor.sensors"),
showControls: false,
hidden: false,
},
binary_sensor: {
title: `${localize("sensor.binary")} ` + localize("sensor.sensors"),
showControls: false,
hidden: false,
},
number: {
title: localize("generic.numbers"),
showControls: false,
hidden: false,
},
vacuum: {
title: localize("vacuum.vacuums"),
showControls: true,
hidden: false,
},
select: {
title: localize("select.selects"),
showControls: false,
hidden: false,
},
input_select: {
title: localize("input_select.input_selects"),
showControls: false,
hidden: false,
},
},
home_view: {
hidden: [],
},
views: {
home: {
order: 1,
hidden: false,
},
light: {
order: 2,
hidden: false,
},
fan: {
order: 3,
hidden: false,
},
cover: {
order: 4,
hidden: false,
},
switch: {
order: 5,
hidden: false,
},
climate: {
order: 6,
hidden: false,
},
camera: {
order: 7,
hidden: false,
},
vacuum: {
order: 8,
hidden: false,
},
} }
}, };
debug: false,
domains: {
_: {
hide_config_entities: false,
},
default: {
title: "Miscellaneous",
showControls: false,
hidden: false,
},
light: {
title: "Lights",
showControls: true,
iconOn: "mdi:lightbulb",
iconOff: "mdi:lightbulb-off",
onService: "light.turn_on",
offService: "light.turn_off",
hidden: false,
},
fan: {
title: "Fans",
showControls: true,
iconOn: "mdi:fan",
iconOff: "mdi:fan-off",
onService: "fan.turn_on",
offService: "fan.turn_off",
hidden: false,
},
cover: {
title: "Covers",
showControls: true,
iconOn: "mdi:arrow-up",
iconOff: "mdi:arrow-down",
onService: "cover.open_cover",
offService: "cover.close_cover",
hidden: false,
},
switch: {
title: "Switches",
showControls: true,
iconOn: "mdi:power-plug",
iconOff: "mdi:power-plug-off",
onService: "switch.turn_on",
offService: "switch.turn_off",
hidden: false,
},
camera: {
title: "Cameras",
showControls: false,
hidden: false,
},
lock: {
title: "Locks",
showControls: false,
hidden: false,
},
climate: {
title: "Climates",
showControls: false,
hidden: false,
},
media_player: {
title: "Media Players",
showControls: false,
hidden: false,
},
sensor: {
title: "Sensors",
showControls: false,
hidden: false,
},
binary_sensor: {
title: "Binary Sensors",
showControls: false,
hidden: false,
},
number: {
title: "Numbers",
showControls: false,
hidden: false,
},
vacuum: {
title: "Vacuums",
showControls: true,
hidden: false,
},
select: {
title: "Selects",
showControls: false,
hidden: false,
},
input_select: {
title: "Input Selects",
showControls: false,
hidden: false,
},
},
home_view: {
hidden: [],
},
views: {
home: {
order: 1,
hidden: false,
},
light: {
order: 2,
hidden: false,
},
fan: {
order: 3,
hidden: false,
},
cover: {
order: 4,
hidden: false,
},
switch: {
order: 5,
hidden: false,
},
climate: {
order: 6,
hidden: false,
},
camera: {
order: 7,
hidden: false,
},
vacuum: {
order: 8,
hidden: false,
},
}
}; };

56
src/localize.ts Normal file
View File

@ -0,0 +1,56 @@
import {HomeAssistant} from "./types/homeassistant/types";
import * as en from "./translations/en.json";
import * as nl from "./translations/nl.json";
/* Registry of currently supported languages */
const languages: Record<string, unknown> = {
en,
nl,
};
/* The fallback language if the user-defined language isn't defined */
const DEFAULT_LANG = "en";
/**
* Get a string by keyword and language.
*
* @param {string} key The keyword to look for in object notation (E.g. generic.home).
* @param {string} lang The language to get the string from (E.g. en).
*
* @return {string | undefined} The requested string or undefined if the keyword doesn't exist/on error.
*/
function getTranslatedString(key: string, lang: string): string | undefined {
try {
return key
.split(".")
.reduce(
(o, i) => (o as Record<string, unknown>)[i],
languages[lang]
) as string;
} catch (_) {
return undefined;
}
}
/**
* Set up the localization.
*
* It reads the user-defined language with a fall-back to english and returns a function to get strings from
* language-files by keyword.
*
* If the keyword is undefined, or on error, the keyword itself is returned.
*
* @param {HomeAssistant} hass The Home Assistant object.
* @return {(key: string) => string} The function to call for translating strings.
*/
export default function setupCustomLocalize(hass?: HomeAssistant): (key: string) => string {
return function (key: string) {
const lang = hass?.locale.language ?? DEFAULT_LANG;
let translated = getTranslatedString(key, lang);
if (!translated) translated = getTranslatedString(key, DEFAULT_LANG);
return translated ?? key;
};
}

61
src/translations/en.json Normal file
View File

@ -0,0 +1,61 @@
{
"camera": {
"all_cameras": "All Cameras",
"cameras": "Cameras"
},
"climate": {
"all_climates": "All Climates",
"climates": "Climates"
},
"covers": {
"all_covers": "All Covers",
"covers": "Covers"
},
"fan": {
"all_fans": "All Fans",
"fans": "Fans"
},
"generic": {
"all": "All",
"areas": "Areas",
"busy": "Busy",
"good_afternoon": "Good afternoon",
"good_evening": "Good evening",
"good_morning": "Good morning",
"hello": "Hello",
"home": "Home",
"miscellaneous": "Miscellaneous",
"numbers": "Numbers",
"off": "Off",
"on": "On",
"open": "Open"
},
"input_select": {
"input_selects": "Input Selects"
},
"light": {
"all_lights": "All Lights",
"lights": "Lights"
},
"lock": {
"locks": "Locks"
},
"media_player": {
"media_players": "Mediaplayers"
},
"select": {
"selects": "Selects"
},
"sensor": {
"binary": "Binary",
"sensors": "Sensors"
},
"switch": {
"all_switches": "All Switches",
"switches": "Switches"
},
"vacuum": {
"all_vacuums": "All Vacuums",
"vacuums": "Vacuums"
}
}

61
src/translations/nl.json Normal file
View File

@ -0,0 +1,61 @@
{
"camera": {
"all_cameras": "Alle Cameras",
"cameras": "Cameras"
},
"climate": {
"all_climates": "Alle Klimaatregelingen",
"climates": "Klimaatregelingen"
},
"covers": {
"all_covers": "Alle Bedekkingen",
"covers": "Bedekkingen"
},
"fan": {
"all_fans": "Alle Ventilatoren",
"fans": "Ventilatoren"
},
"generic": {
"all": "Alle",
"areas": "Ruimtes",
"busy": "Bezig",
"good_afternoon": "Goede middag",
"good_evening": "Goede avond",
"good_morning": "Goede morgen",
"hello": "Hallo",
"home": "Start",
"miscellaneous": "Overige",
"numbers": "Nummers",
"off": "Uit",
"on": "Aan",
"open": "Open"
},
"input_select": {
"input_selects": "Lijsten"
},
"light": {
"all_lights": "Alle Lampen",
"lights": "Lampen"
},
"lock": {
"locks": "Sloten"
},
"media_player": {
"media_players": "Mediaspelers"
},
"select": {
"selects": "Statuslijsten"
},
"sensor": {
"binary": "Binaire",
"sensors": "Sensoren"
},
"switch": {
"all_switches": "Alle Schakelaars",
"switches": "Schakelaars"
},
"vacuum": {
"all_vacuums": "Alle Afzuiging",
"vacuums": "Afzuiging"
}
}

View File

@ -30,7 +30,7 @@ class CameraView extends AbstractView {
* @private * @private
*/ */
#defaultConfig: views.ViewConfig = { #defaultConfig: views.ViewConfig = {
title: "Cameras", title: Helper.customLocalize("camera.cameras"),
path: "cameras", path: "cameras",
icon: "mdi:cctv", icon: "mdi:cctv",
subview: false, subview: false,
@ -46,8 +46,10 @@ class CameraView extends AbstractView {
* @private * @private
*/ */
#viewControllerCardConfig: cards.ControllerCardOptions = { #viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Cameras", title: Helper.customLocalize("camera.all_cameras"),
subtitle: Helper.getCountTemplate(CameraView.#domain, "ne", "off") + " cameras on", subtitle:
`${Helper.getCountTemplate(CameraView.#domain, "ne", "off")} ${Helper.customLocalize("camera.cameras")} `
+ Helper.customLocalize("generic.busy"),
}; };
/** /**

View File

@ -30,7 +30,7 @@ class ClimateView extends AbstractView {
* @private * @private
*/ */
#defaultConfig: views.ViewConfig = { #defaultConfig: views.ViewConfig = {
title: "Climates", title: Helper.customLocalize("climate.climates"),
path: "climates", path: "climates",
icon: "mdi:thermostat", icon: "mdi:thermostat",
subview: false, subview: false,
@ -46,8 +46,10 @@ class ClimateView extends AbstractView {
* @private * @private
*/ */
#viewControllerCardConfig: cards.ControllerCardOptions = { #viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Climates", title: Helper.customLocalize("climate.all_climates"),
subtitle: Helper.getCountTemplate(ClimateView.#domain, "ne", "off") + " climates on", subtitle:
`${Helper.getCountTemplate(ClimateView.#domain, "ne", "off")} ${Helper.customLocalize("climate.climates")} `
+ Helper.customLocalize("generic.busy"),
}; };
/** /**

View File

@ -30,7 +30,7 @@ class CoverView extends AbstractView {
* @private * @private
*/ */
#defaultConfig: views.ViewConfig = { #defaultConfig: views.ViewConfig = {
title: "Covers", title: Helper.customLocalize("cover.covers"),
path: "covers", path: "covers",
icon: "mdi:window-open", icon: "mdi:window-open",
subview: false, subview: false,
@ -49,8 +49,10 @@ class CoverView extends AbstractView {
* @private * @private
*/ */
#viewControllerCardConfig: cards.ControllerCardOptions = { #viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Covers", title: Helper.customLocalize("cover.all_covers"),
subtitle: Helper.getCountTemplate(CoverView.#domain, "eq", "open") + " covers open", subtitle:
`${Helper.getCountTemplate(CoverView.#domain, "eq", "open")} ${Helper.customLocalize("cover.covers")} `
+ Helper.customLocalize("generic.open"),
}; };
/** /**

View File

@ -30,7 +30,7 @@ class FanView extends AbstractView {
* @private * @private
*/ */
#defaultConfig: views.ViewConfig = { #defaultConfig: views.ViewConfig = {
title: "Fans", title: Helper.customLocalize("fan.fans"),
path: "fans", path: "fans",
icon: "mdi:fan", icon: "mdi:fan",
subview: false, subview: false,
@ -49,8 +49,10 @@ class FanView extends AbstractView {
* @private * @private
*/ */
#viewControllerCardConfig: cards.ControllerCardOptions = { #viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Fans", title: Helper.customLocalize("fan.all_fans"),
subtitle: Helper.getCountTemplate(FanView.#domain, "eq", "on") + " fans on", subtitle:
`${Helper.getCountTemplate(FanView.#domain, "eq", "on")} ${Helper.customLocalize("fan.fans")} `
+ Helper.customLocalize("generic.on"),
}; };
/** /**

View File

@ -27,7 +27,7 @@ class HomeView extends AbstractView {
* @private * @private
*/ */
#defaultConfig: views.ViewConfig = { #defaultConfig: views.ViewConfig = {
title: "Home", title: Helper.customLocalize("generic.home"),
icon: "mdi:home-assistant", icon: "mdi:home-assistant",
path: "home", path: "home",
subview: false, subview: false,
@ -77,24 +77,28 @@ class HomeView extends AbstractView {
} }
if (!Helper.strategyOptions.home_view.hidden.includes("greeting")) { if (!Helper.strategyOptions.home_view.hidden.includes("greeting")) {
homeViewCards.push({ const greeting =
type: "custom:mushroom-template-card", homeViewCards.push({
primary: "{% set time = now().hour %} {% if (time >= 18) %} Good Evening, {{user}}! {% elif (time >= 12) %} Good Afternoon, {{user}}! {% elif (time >= 5) %} Good Morning, {{user}}! {% else %} Hello, {{user}}! {% endif %}", type: "custom:mushroom-template-card",
icon: "mdi:hand-wave", primary:
icon_color: "orange", `{% set time = now().hour %} {% if (time >= 18) %} ${Helper.customLocalize("generic.good_evening")},{{user}}!
tap_action: { {% elif (time >= 12) %} ${Helper.customLocalize("generic.good_afternoon")}, {{user}}!
action: "none", {% elif (time >= 5) %} ${Helper.customLocalize("generic.good_morning")}, {{user}}!
} as ActionConfig, {% else %} ${Helper.customLocalize("generic.hello")}, {{user}}! {% endif %}`,
double_tap_action: { icon: "mdi:hand-wave",
action: "none", icon_color: "orange",
} as ActionConfig, tap_action: {
hold_action: { action: "none",
action: "none", } as ActionConfig,
} as ActionConfig, double_tap_action: {
} as TemplateCardConfig); action: "none",
} as ActionConfig,
hold_action: {
action: "none",
} as ActionConfig,
} as TemplateCardConfig);
} }
// Add quick access cards. // Add quick access cards.
if (options.quick_access_cards) { if (options.quick_access_cards) {
homeViewCards.push(...options.quick_access_cards); homeViewCards.push(...options.quick_access_cards);
@ -225,7 +229,7 @@ class HomeView extends AbstractView {
if (!Helper.strategyOptions.home_view.hidden.includes("areasTitle")) { if (!Helper.strategyOptions.home_view.hidden.includes("areasTitle")) {
groupedCards.push({ groupedCards.push({
type: "custom:mushroom-title-card", type: "custom:mushroom-title-card",
title: "Areas", title: Helper.customLocalize("generic.areas"),
}, },
); );
} }

View File

@ -30,7 +30,7 @@ class LightView extends AbstractView {
* @private * @private
*/ */
#defaultConfig: views.ViewConfig = { #defaultConfig: views.ViewConfig = {
title: "Lights", title: Helper.customLocalize("light.lights"),
path: "lights", path: "lights",
icon: "mdi:lightbulb-group", icon: "mdi:lightbulb-group",
subview: false, subview: false,
@ -49,8 +49,10 @@ class LightView extends AbstractView {
* @private * @private
*/ */
#viewControllerCardConfig: cards.ControllerCardOptions = { #viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Lights", title: Helper.customLocalize("light.all_lights"),
subtitle: Helper.getCountTemplate(LightView.#domain, "eq", "on") + " lights on", subtitle:
`${Helper.getCountTemplate(LightView.#domain, "eq", "on")} ${Helper.customLocalize("light.lights")} `
+ Helper.customLocalize("generic.on"),
}; };
/** /**

View File

@ -30,7 +30,7 @@ class SwitchView extends AbstractView {
* @private * @private
*/ */
#defaultConfig: views.ViewConfig = { #defaultConfig: views.ViewConfig = {
title: "Switches", title: Helper.customLocalize("switch.switches"),
path: "switches", path: "switches",
icon: "mdi:dip-switch", icon: "mdi:dip-switch",
subview: false, subview: false,
@ -49,8 +49,10 @@ class SwitchView extends AbstractView {
* @private * @private
*/ */
#viewControllerCardConfig: cards.ControllerCardOptions = { #viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Switches", title: Helper.customLocalize("switch.all_switches"),
subtitle: Helper.getCountTemplate(SwitchView.#domain, "eq", "on") + " switches on", subtitle:
`${Helper.getCountTemplate(SwitchView.#domain, "eq", "on")} ${Helper.customLocalize("switch.switches")} `
+ Helper.customLocalize("generic.on"),
}; };
/** /**

View File

@ -30,7 +30,7 @@ class VacuumView extends AbstractView {
* @private * @private
*/ */
#defaultConfig: views.ViewConfig = { #defaultConfig: views.ViewConfig = {
title: "Vacuums", title: Helper.customLocalize("vacuum.vacuums"),
path: "vacuums", path: "vacuums",
icon: "mdi:robot-vacuum", icon: "mdi:robot-vacuum",
subview: false, subview: false,
@ -49,8 +49,10 @@ class VacuumView extends AbstractView {
* @private * @private
*/ */
#viewControllerCardConfig: cards.ControllerCardOptions = { #viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Vacuums", title: Helper.customLocalize("vacuum.all_vacuums"),
subtitle: Helper.getCountTemplate(VacuumView.#domain, "ne", "off") + " vacuums on", subtitle:
`${Helper.getCountTemplate(VacuumView.#domain, "ne", "off")} ${Helper.customLocalize("vacuum.vacuums")} `
+ Helper.customLocalize("generic.busy"),
}; };
/** /**

View File

@ -29,7 +29,9 @@
"strictNullChecks": true, "strictNullChecks": true,
/* Completeness */ /* Completeness */
"skipLibCheck": true "skipLibCheck": true,
"resolveJsonModule": true,
}, },
"ts-node": { "ts-node": {
"compilerOptions": { "compilerOptions": {