mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-05 17:09:48 +00:00
Use generic picker for language picker (#27631)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
@@ -24,7 +24,7 @@ import "./ha-svg-icon";
|
|||||||
|
|
||||||
@customElement("ha-generic-picker")
|
@customElement("ha-generic-picker")
|
||||||
export class HaGenericPicker extends LitElement {
|
export class HaGenericPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
// eslint-disable-next-line lit/no-native-attributes
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
@@ -68,6 +68,21 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: "not-found-label", type: String })
|
@property({ attribute: "not-found-label", type: String })
|
||||||
public notFoundLabel?: string;
|
public notFoundLabel?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "popover-placement" })
|
||||||
|
public popoverPlacement:
|
||||||
|
| "bottom"
|
||||||
|
| "top"
|
||||||
|
| "left"
|
||||||
|
| "right"
|
||||||
|
| "top-start"
|
||||||
|
| "top-end"
|
||||||
|
| "right-start"
|
||||||
|
| "right-end"
|
||||||
|
| "bottom-start"
|
||||||
|
| "bottom-end"
|
||||||
|
| "left-start"
|
||||||
|
| "left-end" = "bottom-start";
|
||||||
|
|
||||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||||
|
|
||||||
@@ -135,7 +150,7 @@ export class HaGenericPicker extends LitElement {
|
|||||||
style="--body-width: ${this._popoverWidth}px;"
|
style="--body-width: ${this._popoverWidth}px;"
|
||||||
without-arrow
|
without-arrow
|
||||||
distance="-4"
|
distance="-4"
|
||||||
placement="bottom-start"
|
.placement=${this.popoverPlacement}
|
||||||
for="picker"
|
for="picker"
|
||||||
auto-size="vertical"
|
auto-size="vertical"
|
||||||
auto-size-padding="16"
|
auto-size-padding="16"
|
||||||
@@ -144,9 +159,7 @@ export class HaGenericPicker extends LitElement {
|
|||||||
trap-focus
|
trap-focus
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label=${this.hass.localize(
|
aria-label=${this.label || "Select option"}
|
||||||
"ui.components.target-picker.add_target"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
${this._renderComboBox()}
|
${this._renderComboBox()}
|
||||||
</wa-popover>
|
</wa-popover>
|
||||||
@@ -159,9 +172,7 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@closed=${this._hidePicker}
|
@closed=${this._hidePicker}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label=${this.hass.localize(
|
aria-label=${this.label || "Select option"}
|
||||||
"ui.components.target-picker.add_target"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
${this._renderComboBox(true)}
|
${this._renderComboBox(true)}
|
||||||
</ha-bottom-sheet>`
|
</ha-bottom-sheet>`
|
||||||
@@ -179,7 +190,8 @@ export class HaGenericPicker extends LitElement {
|
|||||||
<ha-picker-combo-box
|
<ha-picker-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.label=${this.searchLabel ?? this.hass.localize("ui.common.search")}
|
.label=${this.searchLabel ??
|
||||||
|
(this.hass?.localize("ui.common.search") || "Search")}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
.rowRenderer=${this.rowRenderer}
|
.rowRenderer=${this.rowRenderer}
|
||||||
|
|||||||
@@ -1,56 +1,58 @@
|
|||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
|
||||||
import { formatLanguageCode } from "../common/language/format_language";
|
import { formatLanguageCode } from "../common/language/format_language";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import type { FrontendLocaleData } from "../data/translation";
|
import type { FrontendLocaleData } from "../data/translation";
|
||||||
import { translationMetadata } from "../resources/translations-metadata";
|
import { translationMetadata } from "../resources/translations-metadata";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import "./ha-generic-picker";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
|
||||||
|
|
||||||
export const getLanguageOptions = (
|
export const getLanguageOptions = (
|
||||||
languages: string[],
|
languages: string[],
|
||||||
nativeName: boolean,
|
nativeName: boolean,
|
||||||
noSort: boolean,
|
noSort: boolean,
|
||||||
locale?: FrontendLocaleData
|
locale?: FrontendLocaleData
|
||||||
) => {
|
): PickerComboBoxItem[] => {
|
||||||
let options: { label: string; value: string }[] = [];
|
let options: PickerComboBoxItem[] = [];
|
||||||
|
|
||||||
if (nativeName) {
|
if (nativeName) {
|
||||||
const translations = translationMetadata.translations;
|
const translations = translationMetadata.translations;
|
||||||
options = languages.map((lang) => {
|
options = languages.map((lang) => {
|
||||||
let label = translations[lang]?.nativeName;
|
let primary = translations[lang]?.nativeName;
|
||||||
if (!label) {
|
if (!primary) {
|
||||||
try {
|
try {
|
||||||
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
|
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
|
||||||
label = new Intl.DisplayNames(lang, {
|
primary = new Intl.DisplayNames(lang, {
|
||||||
type: "language",
|
type: "language",
|
||||||
fallback: "code",
|
fallback: "code",
|
||||||
}).of(lang)!;
|
}).of(lang)!;
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
label = lang;
|
primary = lang;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
value: lang,
|
id: lang,
|
||||||
label,
|
primary,
|
||||||
|
search_labels: [primary],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else if (locale) {
|
} else if (locale) {
|
||||||
options = languages.map((lang) => ({
|
options = languages.map((lang) => ({
|
||||||
value: lang,
|
id: lang,
|
||||||
label: formatLanguageCode(lang, locale),
|
primary: formatLanguageCode(lang, locale),
|
||||||
|
search_labels: [formatLanguageCode(lang, locale)],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!noSort && locale) {
|
if (!noSort && locale) {
|
||||||
options.sort((a, b) =>
|
options.sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
caseInsensitiveStringCompare(a.primary, b.primary, locale.language)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
@@ -80,115 +82,69 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
@state() _defaultLanguages: string[] = [];
|
@state() _defaultLanguages: string[] = [];
|
||||||
|
|
||||||
@query("ha-select") private _select!: HaSelect;
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this._computeDefaultLanguageOptions();
|
this._computeDefaultLanguageOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
|
|
||||||
const localeChanged =
|
|
||||||
changedProperties.has("hass") &&
|
|
||||||
this.hass &&
|
|
||||||
changedProperties.get("hass") &&
|
|
||||||
changedProperties.get("hass").locale.language !==
|
|
||||||
this.hass.locale.language;
|
|
||||||
if (
|
|
||||||
changedProperties.has("languages") ||
|
|
||||||
changedProperties.has("value") ||
|
|
||||||
localeChanged
|
|
||||||
) {
|
|
||||||
this._select.layoutOptions();
|
|
||||||
if (!this.disabled && this._select.value !== this.value) {
|
|
||||||
fireEvent(this, "value-changed", { value: this._select.value });
|
|
||||||
}
|
|
||||||
if (!this.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const languageOptions = this._getLanguagesOptions(
|
|
||||||
this.languages ?? this._defaultLanguages,
|
|
||||||
this.nativeName,
|
|
||||||
this.noSort,
|
|
||||||
this.hass?.locale
|
|
||||||
);
|
|
||||||
const selectedItemIndex = languageOptions.findIndex(
|
|
||||||
(option) => option.value === this.value
|
|
||||||
);
|
|
||||||
if (selectedItemIndex === -1) {
|
|
||||||
this.value = undefined;
|
|
||||||
}
|
|
||||||
if (localeChanged) {
|
|
||||||
this._select.select(selectedItemIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getLanguagesOptions = memoizeOne(getLanguageOptions);
|
private _getLanguagesOptions = memoizeOne(getLanguageOptions);
|
||||||
|
|
||||||
private _computeDefaultLanguageOptions() {
|
private _computeDefaultLanguageOptions() {
|
||||||
this._defaultLanguages = Object.keys(translationMetadata.translations);
|
this._defaultLanguages = Object.keys(translationMetadata.translations);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
private _getItems = () =>
|
||||||
const languageOptions = this._getLanguagesOptions(
|
this._getLanguagesOptions(
|
||||||
this.languages ?? this._defaultLanguages,
|
this.languages ?? this._defaultLanguages,
|
||||||
this.nativeName,
|
this.nativeName,
|
||||||
this.noSort,
|
this.noSort,
|
||||||
this.hass?.locale
|
this.hass?.locale
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _valueRenderer = (value) => {
|
||||||
|
const language = this._getItems().find(
|
||||||
|
(lang) => lang.id === value
|
||||||
|
)?.primary;
|
||||||
|
return html`<span slot="headline">${language ?? value}</span> `;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected render() {
|
||||||
const value =
|
const value =
|
||||||
this.value ??
|
this.value ??
|
||||||
(this.required && !this.disabled
|
(this.required && !this.disabled ? this._getItems()[0].id : this.value);
|
||||||
? languageOptions[0]?.value
|
|
||||||
: this.value);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-generic-picker
|
||||||
.label=${this.label ??
|
.hass=${this.hass}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
popover-placement="bottom-end"
|
||||||
|
.notFoundLabel=${this.hass?.localize(
|
||||||
|
"ui.components.language-picker.no_match"
|
||||||
|
)}
|
||||||
|
.placeholder=${this.label ??
|
||||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||||
"Language")}
|
"Language")}
|
||||||
.value=${value || ""}
|
.value=${value}
|
||||||
.required=${this.required}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@selected=${this._changed}
|
.getItems=${this._getItems}
|
||||||
@closed=${stopPropagation}
|
@value-changed=${this._changed}
|
||||||
fixedMenuPosition
|
hide-clear-icon
|
||||||
naturalMenuWidth
|
></ha-generic-picker>
|
||||||
.inlineArrow=${this.inlineArrow}
|
|
||||||
>
|
|
||||||
${languageOptions.length === 0
|
|
||||||
? html`<ha-list-item value=""
|
|
||||||
>${this.hass?.localize(
|
|
||||||
"ui.components.language-picker.no_languages"
|
|
||||||
) || "No languages"}</ha-list-item
|
|
||||||
>`
|
|
||||||
: languageOptions.map(
|
|
||||||
(option) => html`
|
|
||||||
<ha-list-item .value=${option.value}
|
|
||||||
>${option.label}</ha-list-item
|
|
||||||
>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ha-select>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
ha-select {
|
ha-generic-picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 200px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
private _changed(ev): void {
|
private _changed(ev: ValueChangedEvent<string>): void {
|
||||||
const target = ev.target as HaSelect;
|
ev.stopPropagation();
|
||||||
if (this.disabled || target.value === "" || target.value === this.value) {
|
this.value = ev.detail.value;
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.value = target.value;
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
|
|||||||
|
|
||||||
@customElement("ha-picker-combo-box")
|
@customElement("ha-picker-combo-box")
|
||||||
export class HaPickerComboBox extends LitElement {
|
export class HaPickerComboBox extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
// eslint-disable-next-line lit/no-native-attributes
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
@@ -140,7 +140,9 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`<ha-textfield
|
return html`<ha-textfield
|
||||||
.label=${this.label ?? this.hass.localize("ui.common.search")}
|
.label=${this.label ??
|
||||||
|
this.hass?.localize("ui.common.search") ??
|
||||||
|
"Search"}
|
||||||
@input=${this._filterChanged}
|
@input=${this._filterChanged}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
@@ -159,12 +161,18 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
private _defaultNotFoundItem = memoizeOne(
|
private _defaultNotFoundItem = memoizeOne(
|
||||||
(
|
(
|
||||||
label: this["notFoundLabel"],
|
label: this["notFoundLabel"],
|
||||||
localize: LocalizeFunc
|
localize?: LocalizeFunc
|
||||||
): PickerComboBoxItemWithLabel => ({
|
): PickerComboBoxItemWithLabel => ({
|
||||||
id: NO_MATCHING_ITEMS_FOUND_ID,
|
id: NO_MATCHING_ITEMS_FOUND_ID,
|
||||||
primary: label || localize("ui.components.combo-box.no_match"),
|
primary:
|
||||||
|
label ||
|
||||||
|
(localize && localize("ui.components.combo-box.no_match")) ||
|
||||||
|
"No matching items found",
|
||||||
icon_path: mdiMagnify,
|
icon_path: mdiMagnify,
|
||||||
a11y_label: label || localize("ui.components.combo-box.no_match"),
|
a11y_label:
|
||||||
|
label ||
|
||||||
|
(localize && localize("ui.components.combo-box.no_match")) ||
|
||||||
|
"No matching items found",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -189,13 +197,13 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
entityA.sorting_label!,
|
entityA.sorting_label!,
|
||||||
entityB.sorting_label!,
|
entityB.sorting_label!,
|
||||||
this.hass.locale.language
|
this.hass?.locale.language ?? navigator.language
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!sortedItems.length) {
|
if (!sortedItems.length) {
|
||||||
sortedItems.push(
|
sortedItems.push(
|
||||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
|
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,8 +257,20 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const textfield = ev.target as HaTextField;
|
const textfield = ev.target as HaTextField;
|
||||||
const searchString = textfield.value.trim();
|
const searchString = textfield.value.trim();
|
||||||
|
|
||||||
|
if (!searchString) {
|
||||||
|
this._items = this._allItems;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const index = this._fuseIndex(this._allItems);
|
const index = this._fuseIndex(this._allItems);
|
||||||
const fuse = new HaFuse(this._allItems, { shouldSort: false }, index);
|
const fuse = new HaFuse(
|
||||||
|
this._allItems,
|
||||||
|
{
|
||||||
|
shouldSort: false,
|
||||||
|
minMatchCharLength: Math.min(searchString.length, 2),
|
||||||
|
},
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(searchString);
|
const results = fuse.multiTermsSearch(searchString);
|
||||||
let filteredItems = this._allItems as PickerComboBoxItem[];
|
let filteredItems = this._allItems as PickerComboBoxItem[];
|
||||||
@@ -258,7 +278,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const items = results.map((result) => result.item);
|
const items = results.map((result) => result.item);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
items.push(
|
items.push(
|
||||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
|
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const additionalItems = this._getAdditionalItems(searchString);
|
const additionalItems = this._getAdditionalItems(searchString);
|
||||||
@@ -431,6 +451,17 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._virtualizerElement?.items.length === 1 &&
|
||||||
|
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
|
||||||
|
) {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: firstItem.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this._selectedItemIndex === -1) {
|
if (this._selectedItemIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -438,7 +469,9 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
// if filter button is focused
|
// if filter button is focused
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const item: any = this._virtualizerElement?.items[this._selectedItemIndex];
|
const item = this._virtualizerElement?.items[
|
||||||
|
this._selectedItemIndex
|
||||||
|
] as PickerComboBoxItem;
|
||||||
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
||||||
fireEvent(this, "value-changed", { value: item.id });
|
fireEvent(this, "value-changed", { value: item.id });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,12 +193,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
).map(
|
).map(
|
||||||
(lang) =>
|
(lang) =>
|
||||||
html`<ha-md-menu-item
|
html`<ha-md-menu-item
|
||||||
.value=${lang.value}
|
.value=${lang.id}
|
||||||
@click=${this._handlePickLanguage}
|
@click=${this._handlePickLanguage}
|
||||||
@keydown=${this._handlePickLanguage}
|
@keydown=${this._handlePickLanguage}
|
||||||
.selected=${this._language === lang.value}
|
.selected=${this._language === lang.id}
|
||||||
>
|
>
|
||||||
${lang.label}
|
${lang.primary}
|
||||||
</ha-md-menu-item>`
|
</ha-md-menu-item>`
|
||||||
)}
|
)}
|
||||||
</ha-md-button-menu>`
|
</ha-md-button-menu>`
|
||||||
|
|||||||
@@ -758,6 +758,7 @@
|
|||||||
},
|
},
|
||||||
"language-picker": {
|
"language-picker": {
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"no_match": "No matching languages found",
|
||||||
"no_languages": "No languages available"
|
"no_languages": "No languages available"
|
||||||
},
|
},
|
||||||
"tts-picker": {
|
"tts-picker": {
|
||||||
|
|||||||
Reference in New Issue
Block a user