diff --git a/src/components/ha-language-picker.ts b/src/components/ha-language-picker.ts new file mode 100644 index 0000000000..55a18777dd --- /dev/null +++ b/src/components/ha-language-picker.ts @@ -0,0 +1,132 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; +import { caseInsensitiveStringCompare } from "../common/string/compare"; +import { HomeAssistant } from "../types"; +import "./ha-list-item"; +import "./ha-select"; +import type { HaSelect } from "./ha-select"; + +@customElement("ha-language-picker") +export class HaLanguagePicker extends LitElement { + @property() public value?: string; + + @property() public label?: string; + + @property() public supportedLanguages?: string[]; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + @property({ type: Boolean }) public nativeName = false; + + @state() _defaultLanguages: string[] = []; + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._computeDefaultLanguageOptions(); + } + + private _getLanguagesOptions = memoizeOne( + (languages: string[], language: string, nativeName: boolean) => { + let options: { label: string; value: string }[] = []; + + if (nativeName) { + const translations = this.hass.translationMetadata.translations; + options = languages.map((lang) => ({ + value: lang, + label: translations[lang]?.nativeName ?? lang, + })); + } else { + const languageDisplayNames = + Intl && "DisplayNames" in Intl + ? new Intl.DisplayNames(language, { + type: "language", + fallback: "code", + }) + : undefined; + + options = languages.map((lang) => ({ + value: lang, + label: languageDisplayNames ? languageDisplayNames.of(lang)! : lang, + })); + } + + options.sort((a, b) => + caseInsensitiveStringCompare(a.label, b.label, language) + ); + return options; + } + ); + + private _computeDefaultLanguageOptions() { + if (!this.hass.translationMetadata?.translations) { + return; + } + + this._defaultLanguages = Object.keys( + this.hass.translationMetadata.translations + ); + } + + protected render(): TemplateResult { + const value = this.value; + + const languageOptions = this._getLanguagesOptions( + this.supportedLanguages ?? this._defaultLanguages, + this.hass.locale.language, + this.nativeName + ); + + return html` + + ${languageOptions.map( + (option) => html` + ${option.label} + ` + )} + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-select { + width: 100%; + } + `; + } + + private _changed(ev): void { + const target = ev.target as HaSelect; + this.value = target.value; + fireEvent(this, "value-changed", { value: this.value }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-language-picker": HaLanguagePicker; + } +} diff --git a/src/components/ha-selector/ha-selector-language.ts b/src/components/ha-selector/ha-selector-language.ts new file mode 100644 index 0000000000..7a6e4fc355 --- /dev/null +++ b/src/components/ha-selector/ha-selector-language.ts @@ -0,0 +1,48 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { LanguageSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-language-picker"; + +@customElement("ha-selector-language") +export class HaLanguageSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: LanguageSelector; + + @property() public value?: any; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + protected render() { + return html` + + `; + } + + static styles = css` + ha-language-picker { + width: 100%; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-language": HaLanguageSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 46ee4b30ef..85193753c9 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -26,6 +26,7 @@ const LOAD_ELEMENTS = { entity: () => import("./ha-selector-entity"), statistic: () => import("./ha-selector-statistic"), file: () => import("./ha-selector-file"), + language: () => import("./ha-selector-language"), navigation: () => import("./ha-selector-navigation"), number: () => import("./ha-selector-number"), object: () => import("./ha-selector-object"), diff --git a/src/data/assist_pipeline.ts b/src/data/assist_pipeline.ts index dd8eca29fb..abf370681f 100644 --- a/src/data/assist_pipeline.ts +++ b/src/data/assist_pipeline.ts @@ -296,3 +296,8 @@ export const deleteAssistPipeline = (hass: HomeAssistant, pipelineId: string) => type: "assist_pipeline/pipeline/delete", pipeline_id: pipelineId, }); + +export const fetchAssistPipelineLanguages = (hass: HomeAssistant) => + hass.callWS<{ languages: string[] }>({ + type: "assist_pipeline/language/list", + }); diff --git a/src/data/selector.ts b/src/data/selector.ts index 21ba8c0e5a..c8b1781cf1 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -26,6 +26,7 @@ export type Selector = | LegacyEntitySelector | FileSelector | IconSelector + | LanguageSelector | LocationSelector | MediaSelector | NavigationSelector @@ -209,6 +210,10 @@ export interface IconSelector { } | null; } +export interface LanguageSelector { + language: { supported_languages?: string[] } | null; +} + export interface LocationSelector { location: { radius?: boolean; icon?: string } | null; } diff --git a/src/panels/config/core/ha-config-section-general.ts b/src/panels/config/core/ha-config-section-general.ts index 31abfd8346..0d6e95dbc7 100644 --- a/src/panels/config/core/ha-config-section-general.ts +++ b/src/panels/config/core/ha-config-section-general.ts @@ -6,13 +6,16 @@ import memoizeOne from "memoize-one"; import { UNIT_C } from "../../../common/const"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { navigate } from "../../../common/navigate"; -import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import "../../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../../components/buttons/ha-progress-button"; import { getCountryOptions } from "../../../components/country-datalist"; import { getCurrencyOptions } from "../../../components/currency-datalist"; +import "../../../components/ha-alert"; import "../../../components/ha-card"; +import "../../../components/ha-checkbox"; +import type { HaCheckbox } from "../../../components/ha-checkbox"; import "../../../components/ha-formfield"; +import "../../../components/ha-language-picker"; import "../../../components/ha-radio"; import type { HaRadio } from "../../../components/ha-radio"; import "../../../components/ha-select"; @@ -22,13 +25,10 @@ import "../../../components/map/ha-locations-editor"; import type { MarkerLocation } from "../../../components/map/ha-locations-editor"; import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core"; import { SYMBOL_TO_ISO } from "../../../data/currency"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; -import "../../../components/ha-alert"; -import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; -import type { HaCheckbox } from "../../../components/ha-checkbox"; -import "../../../components/ha-checkbox"; @customElement("ha-config-section-general") class HaConfigSectionGeneral extends LitElement { @@ -54,8 +54,6 @@ class HaConfigSectionGeneral extends LitElement { @state() private _location?: [number, number]; - @state() private _languages?: { value: string; label: string }[]; - @state() private _error?: string; @state() private _updateUnits?: boolean; @@ -255,25 +253,19 @@ class HaConfigSectionGeneral extends LitElement { ` )} - - ${this._languages?.map( - ({ value, label }) => - html`${label}` - )} + ${this.narrow ? html` @@ -330,25 +322,10 @@ class HaConfigSectionGeneral extends LitElement { this._timeZone = this.hass.config.time_zone || "Etc/GMT"; this._name = this.hass.config.location_name; this._updateUnits = true; - this._computeLanguages(); } - private _computeLanguages() { - if (!this.hass.translationMetadata?.translations) { - return; - } - this._languages = Object.entries(this.hass.translationMetadata.translations) - .sort((a, b) => - caseInsensitiveStringCompare( - a[1].nativeName, - b[1].nativeName, - this.hass.locale.language - ) - ) - .map(([value, metaData]) => ({ - value, - label: metaData.nativeName, - })); + private _handleLanguageChange(ev) { + this._language = ev.detail.value; } private _handleChange(ev) { diff --git a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts index 3bd1bd00c4..0738dfd0c9 100644 --- a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts +++ b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts @@ -9,6 +9,7 @@ import { SchemaUnion } from "../../../components/ha-form/types"; import { AssistPipeline, AssistPipelineMutableParams, + fetchAssistPipelineLanguages, } from "../../../data/assist_pipeline"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; @@ -29,6 +30,8 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { @state() private _submitting = false; + @state() private _supportedLanguages: string[] = []; + public showDialog(params: VoiceAssistantPipelineDetailsDialogParams): void { this._params = params; this._error = undefined; @@ -46,6 +49,15 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { fireEvent(this, "dialog-closed", { dialog: this.localName }); } + protected firstUpdated() { + this._getSupportedLanguages(); + } + + private async _getSupportedLanguages() { + const { languages } = await fetchAssistPipelineLanguages(this.hass); + this._supportedLanguages = languages; + } + protected render() { if (!this._params || !this._data) { return nothing; @@ -68,7 +80,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { >
+ (languages: string[]) => [ { name: "name", @@ -129,6 +141,15 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { text: {}, }, }, + { + name: "language", + required: true, + selector: { + language: { + supported_languages: languages, + }, + }, + }, { name: "conversation_engine", required: true, @@ -136,13 +157,6 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { conversation_agent: {}, }, }, - { - name: "language", - required: true, - selector: { - text: {}, - }, - }, { name: "stt_engine", selector: { diff --git a/src/panels/profile/ha-pick-language-row.ts b/src/panels/profile/ha-pick-language-row.ts index 7bab38abdc..d33eb48941 100644 --- a/src/panels/profile/ha-pick-language-row.ts +++ b/src/panels/profile/ha-pick-language-row.ts @@ -1,10 +1,9 @@ -import "@material/mwc-list/mwc-list-item"; -import { css, html, LitElement, PropertyValues } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-select"; +import "../../components/ha-language-picker"; import "../../components/ha-settings-row"; -import { HomeAssistant, Translation } from "../../types"; +import { HomeAssistant } from "../../types"; @customElement("ha-pick-language-row") export class HaPickLanguageRow extends LitElement { @@ -12,13 +11,6 @@ export class HaPickLanguageRow extends LitElement { @property() public narrow!: boolean; - @state() private _languages: (Translation & { key: string })[] = []; - - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this._computeLanguages(); - } - protected render() { return html` @@ -33,43 +25,25 @@ export class HaPickLanguageRow extends LitElement { >${this.hass.localize("ui.panel.profile.language.link_promo")} - - ${this._languages.map( - (language) => html` - ${language.nativeName} - ` - )} - + `; } - private _computeLanguages() { - if (!this.hass.translationMetadata?.translations) { - return; - } - this._languages = Object.keys( - this.hass.translationMetadata.translations - ).map((key) => ({ - key, - ...this.hass.translationMetadata.translations[key], - })); - } - private _languageSelectionChanged(ev) { // Only fire event if language was changed. This prevents select updates when // responding to hass changes. - if (ev.target.value !== this.hass.language) { + if (ev.detail.value !== this.hass.language) { fireEvent(this, "hass-language-select", ev.target.value); } }