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);
}
}