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 deepmerge from "deepmerge";
import {EntityRegistryEntry} from "./types/homeassistant/data/entity_registry";
import {DeviceRegistryEntry} from "./types/homeassistant/data/device_registry";
import {AreaRegistryEntry} from "./types/homeassistant/data/area_registry";
import {generic} from "./types/strategy/generic";
import setupCustomLocalize from "./localize";
import StrategyArea = generic.StrategyArea;
/**
@ -68,6 +69,7 @@ class Helper {
* @private
*/
static #debug: boolean;
static customLocalize: Function;
/**
* Class constructor.
@ -140,8 +142,12 @@ class Helper {
*/
static async initialize(info: generic.DashBoardInfo): Promise<void> {
// 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.#hassStates = info.hass.states;
this.#debug = this.#strategyOptions.debug;
try {

View File

@ -4,151 +4,153 @@ import StrategyDefaults = generic.StrategyDefaults;
/**
* Default configuration for the mushroom strategy.
*/
export const configurationDefaults: StrategyDefaults = {
areas: {
undisclosed: {
area_id: "undisclosed",
floor_id: null,
name: "Undisclosed",
picture: null,
icon: "mdi:floor-plan",
labels: [],
aliases: [],
hidden: false,
export const getConfigurationDefaults = (localize: Function): StrategyDefaults => {
return {
areas: {
undisclosed: {
area_id: "undisclosed",
floor_id: null,
name: "Undisclosed",
picture: null,
icon: "mdi:floor-plan",
labels: [],
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
*/
#defaultConfig: views.ViewConfig = {
title: "Cameras",
title: Helper.customLocalize("camera.cameras"),
path: "cameras",
icon: "mdi:cctv",
subview: false,
@ -46,8 +46,10 @@ class CameraView extends AbstractView {
* @private
*/
#viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Cameras",
subtitle: Helper.getCountTemplate(CameraView.#domain, "ne", "off") + " cameras on",
title: Helper.customLocalize("camera.all_cameras"),
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
*/
#defaultConfig: views.ViewConfig = {
title: "Climates",
title: Helper.customLocalize("climate.climates"),
path: "climates",
icon: "mdi:thermostat",
subview: false,
@ -46,8 +46,10 @@ class ClimateView extends AbstractView {
* @private
*/
#viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Climates",
subtitle: Helper.getCountTemplate(ClimateView.#domain, "ne", "off") + " climates on",
title: Helper.customLocalize("climate.all_climates"),
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
*/
#defaultConfig: views.ViewConfig = {
title: "Covers",
title: Helper.customLocalize("cover.covers"),
path: "covers",
icon: "mdi:window-open",
subview: false,
@ -49,8 +49,10 @@ class CoverView extends AbstractView {
* @private
*/
#viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Covers",
subtitle: Helper.getCountTemplate(CoverView.#domain, "eq", "open") + " covers open",
title: Helper.customLocalize("cover.all_covers"),
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
*/
#defaultConfig: views.ViewConfig = {
title: "Fans",
title: Helper.customLocalize("fan.fans"),
path: "fans",
icon: "mdi:fan",
subview: false,
@ -49,8 +49,10 @@ class FanView extends AbstractView {
* @private
*/
#viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Fans",
subtitle: Helper.getCountTemplate(FanView.#domain, "eq", "on") + " fans on",
title: Helper.customLocalize("fan.all_fans"),
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
*/
#defaultConfig: views.ViewConfig = {
title: "Home",
title: Helper.customLocalize("generic.home"),
icon: "mdi:home-assistant",
path: "home",
subview: false,
@ -77,24 +77,28 @@ class HomeView extends AbstractView {
}
if (!Helper.strategyOptions.home_view.hidden.includes("greeting")) {
homeViewCards.push({
type: "custom:mushroom-template-card",
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 %}",
icon: "mdi:hand-wave",
icon_color: "orange",
tap_action: {
action: "none",
} as ActionConfig,
double_tap_action: {
action: "none",
} as ActionConfig,
hold_action: {
action: "none",
} as ActionConfig,
} as TemplateCardConfig);
const greeting =
homeViewCards.push({
type: "custom:mushroom-template-card",
primary:
`{% set time = now().hour %} {% if (time >= 18) %} ${Helper.customLocalize("generic.good_evening")},{{user}}!
{% elif (time >= 12) %} ${Helper.customLocalize("generic.good_afternoon")}, {{user}}!
{% elif (time >= 5) %} ${Helper.customLocalize("generic.good_morning")}, {{user}}!
{% else %} ${Helper.customLocalize("generic.hello")}, {{user}}! {% endif %}`,
icon: "mdi:hand-wave",
icon_color: "orange",
tap_action: {
action: "none",
} as ActionConfig,
double_tap_action: {
action: "none",
} as ActionConfig,
hold_action: {
action: "none",
} as ActionConfig,
} as TemplateCardConfig);
}
// Add quick access cards.
if (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")) {
groupedCards.push({
type: "custom:mushroom-title-card",
title: "Areas",
title: Helper.customLocalize("generic.areas"),
},
);
}

View File

@ -30,7 +30,7 @@ class LightView extends AbstractView {
* @private
*/
#defaultConfig: views.ViewConfig = {
title: "Lights",
title: Helper.customLocalize("light.lights"),
path: "lights",
icon: "mdi:lightbulb-group",
subview: false,
@ -49,8 +49,10 @@ class LightView extends AbstractView {
* @private
*/
#viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Lights",
subtitle: Helper.getCountTemplate(LightView.#domain, "eq", "on") + " lights on",
title: Helper.customLocalize("light.all_lights"),
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
*/
#defaultConfig: views.ViewConfig = {
title: "Switches",
title: Helper.customLocalize("switch.switches"),
path: "switches",
icon: "mdi:dip-switch",
subview: false,
@ -49,8 +49,10 @@ class SwitchView extends AbstractView {
* @private
*/
#viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Switches",
subtitle: Helper.getCountTemplate(SwitchView.#domain, "eq", "on") + " switches on",
title: Helper.customLocalize("switch.all_switches"),
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
*/
#defaultConfig: views.ViewConfig = {
title: "Vacuums",
title: Helper.customLocalize("vacuum.vacuums"),
path: "vacuums",
icon: "mdi:robot-vacuum",
subview: false,
@ -49,8 +49,10 @@ class VacuumView extends AbstractView {
* @private
*/
#viewControllerCardConfig: cards.ControllerCardOptions = {
title: "All Vacuums",
subtitle: Helper.getCountTemplate(VacuumView.#domain, "ne", "off") + " vacuums on",
title: Helper.customLocalize("vacuum.all_vacuums"),
subtitle:
`${Helper.getCountTemplate(VacuumView.#domain, "ne", "off")} ${Helper.customLocalize("vacuum.vacuums")} `
+ Helper.customLocalize("generic.busy"),
};
/**

View File

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