diff --git a/package.json b/package.json index e6d2d93726..5f3c81026d 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "google-timezones-json": "^1.0.2", "hammerjs": "^2.0.8", "hls.js": "^1.2.5", - "home-assistant-js-websocket": "^8.0.0", + "home-assistant-js-websocket": "^8.0.1", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", "js-yaml": "^4.1.0", diff --git a/src/components/country-datalist.ts b/src/components/country-datalist.ts new file mode 100644 index 0000000000..c972a77fd6 --- /dev/null +++ b/src/components/country-datalist.ts @@ -0,0 +1,273 @@ +export const countries = [ + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AX", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BL", + "BM", + "BN", + "BO", + "BQ", + "BR", + "BS", + "BT", + "BV", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CK", + "CL", + "CM", + "CN", + "CO", + "CR", + "CU", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GW", + "GY", + "HK", + "HM", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IR", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KP", + "KR", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MF", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MQ", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NF", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SD", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "SS", + "ST", + "SV", + "SX", + "SY", + "SZ", + "TC", + "TD", + "TF", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "UM", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW", +]; + +export const countryDisplayNames = + Intl && "DisplayNames" in Intl + ? new Intl.DisplayNames(undefined, { + type: "region", + fallback: "code", + }) + : undefined; + +export const createCountryListEl = () => { + const list = document.createElement("datalist"); + list.id = "countries"; + for (const country of countries) { + const option = document.createElement("option"); + option.value = country; + option.innerText = countryDisplayNames + ? countryDisplayNames.of(country)! + : country; + list.appendChild(option); + } + return list; +}; diff --git a/src/components/currency-datalist.ts b/src/components/currency-datalist.ts index 69a69e7222..c725b8258c 100644 --- a/src/components/currency-datalist.ts +++ b/src/components/currency-datalist.ts @@ -158,13 +158,23 @@ export const currencies = [ "ZWL", ]; +export const currencyDisplayNames = + Intl && "DisplayNames" in Intl + ? new Intl.DisplayNames(undefined, { + type: "currency", + fallback: "code", + }) + : undefined; + export const createCurrencyListEl = () => { const list = document.createElement("datalist"); list.id = "currencies"; for (const currency of currencies) { const option = document.createElement("option"); option.value = currency; - option.innerHTML = currency; + option.innerText = currencyDisplayNames + ? currencyDisplayNames.of(currency)! + : currency; list.appendChild(option); } return list; diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index dbfbc1cc38..6264784e81 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -1,7 +1,7 @@ import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base"; import { styles } from "@material/mwc-textfield/mwc-textfield.css"; import { TemplateResult, html, PropertyValues, css } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, query } from "lit/decorators"; @customElement("ha-textfield") export class HaTextField extends TextFieldBase { @@ -17,6 +17,8 @@ export class HaTextField extends TextFieldBase { @property() public autocomplete?: string; + @query("input") public formElement!: HTMLInputElement; + override updated(changedProperties: PropertyValues) { super.updated(changedProperties); if ( diff --git a/src/components/language-datalist.ts b/src/components/language-datalist.ts new file mode 100644 index 0000000000..bac8b44c9c --- /dev/null +++ b/src/components/language-datalist.ts @@ -0,0 +1,15 @@ +import { HomeAssistant } from "../types"; + +export const createLanguageListEl = (hass: HomeAssistant) => { + const list = document.createElement("datalist"); + list.id = "languages"; + for (const [language, metadata] of Object.entries( + hass.translationMetadata.translations + )) { + const option = document.createElement("option"); + option.value = language; + option.innerText = metadata.nativeName; + list.appendChild(option); + } + return list; +}; diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index ce395d170b..d9294bc58c 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -133,11 +133,11 @@ export class HaMap extends ReactiveElement { if ( !changedProps.has("darkMode") && (!changedProps.has("hass") || - (oldHass && oldHass.themes.darkMode === this.hass.themes.darkMode)) + (oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode)) ) { return; } - const darkMode = this.darkMode ?? this.hass.themes.darkMode; + const darkMode = this.darkMode ?? this.hass.themes?.darkMode; this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode); } diff --git a/src/components/timezone-datalist.ts b/src/components/timezone-datalist.ts index 05bfa1c0ab..1d4a705408 100644 --- a/src/components/timezone-datalist.ts +++ b/src/components/timezone-datalist.ts @@ -6,7 +6,7 @@ export const createTimezoneListEl = () => { Object.keys(timezones).forEach((key) => { const option = document.createElement("option"); option.value = key; - option.innerHTML = timezones[key]; + option.innerText = timezones[key]; list.appendChild(option); }); return list; diff --git a/src/data/core.ts b/src/data/core.ts index 8d5c231e2b..a2b944e3bd 100644 --- a/src/data/core.ts +++ b/src/data/core.ts @@ -11,6 +11,8 @@ export interface ConfigUpdateValues { external_url?: string | null; internal_url?: string | null; currency?: string | null; + country?: string | null; + language?: string | null; } export interface CheckConfigResult { diff --git a/src/fake_data/demo_config.ts b/src/fake_data/demo_config.ts index ae5a528ff3..e9efe63c65 100644 --- a/src/fake_data/demo_config.ts +++ b/src/fake_data/demo_config.ts @@ -32,4 +32,6 @@ export const demoConfig: HassConfig = { internal_url: "http://homeassistant.local:8123", external_url: null, currency: "USD", + language: "en", + country: "NL", }; diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index 28de65a955..1c4e6ffbe5 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -131,21 +131,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { if (window.innerWidth > 450) { import("./particles"); } - if (matchMedia("(prefers-color-scheme: dark)").matches) { - applyThemesOnElement( - document.documentElement, - { - default_theme: "default", - default_dark_theme: null, - themes: {}, - darkMode: true, - theme: "default", - }, - undefined, - undefined, - true - ); - } } protected updated(changedProps: PropertyValues) { @@ -154,10 +139,25 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { document.querySelector("html")!.setAttribute("lang", this.language!); } if (changedProps.has("hass")) { - this.hassChanged( - this.hass!, - changedProps.get("hass") as HomeAssistant | undefined - ); + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + this.hassChanged(this.hass!, oldHass); + if (oldHass?.themes !== this.hass!.themes) { + if (matchMedia("(prefers-color-scheme: dark)").matches) { + applyThemesOnElement( + document.documentElement, + { + default_theme: "default", + default_dark_theme: null, + themes: {}, + darkMode: true, + theme: "default", + }, + undefined, + undefined, + true + ); + } + } } } diff --git a/src/onboarding/onboarding-core-config.ts b/src/onboarding/onboarding-core-config.ts index 05a996f6fe..50cb216d87 100644 --- a/src/onboarding/onboarding-core-config.ts +++ b/src/onboarding/onboarding-core-config.ts @@ -1,6 +1,4 @@ import "@material/mwc-button/mwc-button"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -25,6 +23,11 @@ import type { HomeAssistant } from "../types"; import "../components/ha-radio"; import "../components/ha-formfield"; import type { HaRadio } from "../components/ha-radio"; +import type { HaTextField } from "../components/ha-textfield"; +import "../components/ha-textfield"; +import { getLocalLanguage } from "../util/common-translation"; +import { createCountryListEl } from "../components/country-datalist"; +import { createLanguageListEl } from "../components/language-datalist"; const amsterdam: [number, number] = [52.3731339, 4.8903147]; const mql = matchMedia("(prefers-color-scheme: dark)"); @@ -50,6 +53,10 @@ class OnboardingCoreConfig extends LitElement { @state() private _timeZone?: string; + @state() private _language?: ConfigUpdateValues["language"]; + + @state() private _country?: ConfigUpdateValues["country"]; + @query("ha-locations-editor", true) private map!: HaLocationsEditor; protected render(): TemplateResult { @@ -62,15 +69,15 @@ class OnboardingCoreConfig extends LitElement { )}

- + @change=${this._handleChange} + >

@@ -105,19 +112,42 @@ class OnboardingCoreConfig extends LitElement {

- + + +
+ +
+ + @change=${this._handleChange} + > - ${this.hass.localize( "ui.panel.config.core.section.core.core_config.elevation_meters" )} - +
@@ -197,17 +227,16 @@ class OnboardingCoreConfig extends LitElement { >
- + @change=${this._handleChange} + > @@ -224,7 +253,7 @@ class OnboardingCoreConfig extends LitElement { protected firstUpdated(changedProps) { super.firstUpdated(changedProps); setTimeout( - () => this.shadowRoot!.querySelector("paper-input")!.focus(), + () => this.shadowRoot!.querySelector("ha-textfield")!.focus(), 100 ); this.addEventListener("keypress", (ev) => { @@ -234,13 +263,35 @@ class OnboardingCoreConfig extends LitElement { }); const tzInput = this.shadowRoot!.querySelector( "[name=timeZone]" - ) as PaperInputElement; - tzInput.inputElement.appendChild(createTimezoneListEl()); + ) as HaTextField; + tzInput.updateComplete.then(() => { + tzInput.shadowRoot!.appendChild(createTimezoneListEl()); + tzInput.formElement.setAttribute("list", "timezones"); + }); - const cInput = this.shadowRoot!.querySelector( + const curInput = this.shadowRoot!.querySelector( "[name=currency]" - ) as PaperInputElement; - cInput.inputElement.appendChild(createCurrencyListEl()); + ) as HaTextField; + curInput.updateComplete.then(() => { + curInput.shadowRoot!.appendChild(createCurrencyListEl()); + curInput.formElement.setAttribute("list", "currencies"); + }); + + const countryInput = this.shadowRoot!.querySelector( + "[name=country]" + ) as HaTextField; + countryInput.updateComplete.then(() => { + countryInput.shadowRoot!.appendChild(createCountryListEl()); + countryInput.formElement.setAttribute("list", "countries"); + }); + + const langInput = this.shadowRoot!.querySelector( + "[name=language]" + ) as HaTextField; + langInput.updateComplete.then(() => { + langInput.shadowRoot!.appendChild(createLanguageListEl(this.hass)); + langInput.formElement.setAttribute("list", "languages"); + }); } private get _nameValue() { @@ -260,7 +311,15 @@ class OnboardingCoreConfig extends LitElement { } private get _timeZoneValue() { - return this._timeZone; + return this._timeZone || ""; + } + + private get _languageValue() { + return this._language || ""; + } + + private get _countryValue() { + return this._country || ""; } private get _unitSystemValue() { @@ -283,7 +342,7 @@ class OnboardingCoreConfig extends LitElement { ); private _handleChange(ev: PolymerChangedEvent) { - const target = ev.currentTarget as PaperInputElement; + const target = ev.currentTarget as HaTextField; let value = target.value; @@ -335,6 +394,10 @@ class OnboardingCoreConfig extends LitElement { if (values.currency) { this._currency = values.currency; } + if (values.country) { + this._country = values.country; + } + this._language = getLocalLanguage(); } catch (err: any) { alert(`Failed to detect location information: ${err.message}`); } finally { @@ -355,6 +418,8 @@ class OnboardingCoreConfig extends LitElement { unit_system: this._unitSystemValue, time_zone: this._timeZoneValue || "UTC", currency: this._currencyValue || "EUR", + country: this._countryValue, + language: this._languageValue, }); const result = await onboardCoreConfigStep(this.hass); fireEvent(this, "onboarding-step", { @@ -380,6 +445,10 @@ class OnboardingCoreConfig extends LitElement { color: var(--secondary-text-color); } + ha-textfield { + display: block; + } + ha-locations-editor { height: 200px; } @@ -389,7 +458,11 @@ class OnboardingCoreConfig extends LitElement { } .middle-text { - margin: 24px 0; + margin: 16px 0; + } + + .row { + margin-top: 16px; } .row > * { diff --git a/src/panels/config/core/ha-config-section-general.ts b/src/panels/config/core/ha-config-section-general.ts index 67e374fc01..3e93ce8ec3 100644 --- a/src/panels/config/core/ha-config-section-general.ts +++ b/src/panels/config/core/ha-config-section-general.ts @@ -8,7 +8,14 @@ import { stopPropagation } from "../../../common/dom/stop_propagation"; import { navigate } from "../../../common/navigate"; import "../../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../../components/buttons/ha-progress-button"; -import { currencies } from "../../../components/currency-datalist"; +import { + countries, + countryDisplayNames, +} from "../../../components/country-datalist"; +import { + currencies, + currencyDisplayNames, +} from "../../../components/currency-datalist"; import "../../../components/ha-card"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; @@ -36,6 +43,10 @@ class HaConfigSectionGeneral extends LitElement { @state() private _currency?: string; + @state() private _language?: string; + + @state() private _country?: string | null; + @state() private _name?: string; @state() private _elevation?: number; @@ -179,7 +190,9 @@ class HaConfigSectionGeneral extends LitElement { ${currencies.map( (currency) => html`${currency}${currencyDisplayNames + ? currencyDisplayNames.of(currency) + : currency}` )} @@ -193,6 +206,48 @@ class HaConfigSectionGeneral extends LitElement { )} + + ${countries.map( + (country) => + html`${countryDisplayNames + ? countryDisplayNames.of(country) + : country}` + )} + + ${Object.entries( + this.hass.translationMetadata.translations + ).map( + ([code, metadata]) => + html`${metadata.nativeName}` + )} ${this.narrow ? html` @@ -240,6 +295,8 @@ class HaConfigSectionGeneral extends LitElement { ? "metric" : "us_customary"; this._currency = this.hass.config.currency; + this._country = this.hass.config.country; + this._language = this.hass.config.language; this._elevation = this.hass.config.elevation; this._timeZone = this.hass.config.time_zone; this._name = this.hass.config.location_name; @@ -291,6 +348,8 @@ class HaConfigSectionGeneral extends LitElement { unit_system: this._unitSystem, time_zone: this._timeZone, location_name: this._name, + language: this._language, + country: this._country, ...locationConfig, }); button.actionSuccess(); diff --git a/src/translations/en.json b/src/translations/en.json index ebd580a9de..aa55aa6357 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1654,6 +1654,8 @@ "elevation": "Elevation", "elevation_meters": "meters", "time_zone": "Time Zone", + "language": "Language", + "country": "Country", "unit_system": "Unit System", "unit_system_us_customary": "US customary", "unit_system_metric": "Metric", diff --git a/yarn.lock b/yarn.lock index 8dca31cd85..955af85d37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9419,7 +9419,7 @@ fsevents@^1.2.7: gulp-zopfli-green: ^3.0.1 hammerjs: ^2.0.8 hls.js: ^1.2.5 - home-assistant-js-websocket: ^8.0.0 + home-assistant-js-websocket: ^8.0.1 html-minifier: ^4.0.0 husky: ^8.0.1 idb-keyval: ^5.1.3 @@ -9492,10 +9492,10 @@ fsevents@^1.2.7: languageName: unknown linkType: soft -"home-assistant-js-websocket@npm:^8.0.0": - version: 8.0.0 - resolution: "home-assistant-js-websocket@npm:8.0.0" - checksum: 55fecd70e10ab3c74b4c6c78dbf11d56e22e9799e050096a24c74c3844c48d458f42f43c353b9b050d49acecb323a137df8667428206a07a426f030d339a42e7 +"home-assistant-js-websocket@npm:^8.0.1": + version: 8.0.1 + resolution: "home-assistant-js-websocket@npm:8.0.1" + checksum: e8b2204d58b2b1fbdf26ca1ad196fcc02ec5d18e6d867179f27246a9f2d4fe5f91de9dbbe7b82806c19dcb0af0e2b77fb48d393668b5c8c0844c201a16832023 languageName: node linkType: hard