🌐 Add MVP for translation in the Supervisor panel (#8425)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Joakim Sørensen 2021-03-02 00:37:39 +01:00 committed by GitHub
parent 5ae10e8516
commit bea20d0495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 470 additions and 208 deletions

View File

@ -85,6 +85,11 @@ gulp.task("copy-translations-app", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-translations-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static;
// Basic static files

View File

@ -10,6 +10,8 @@ require("./gen-icons-json.js");
require("./webpack.js");
require("./compress.js");
require("./rollup.js");
require("./gather-static.js");
require("./translations.js");
gulp.task(
"develop-hassio",
@ -20,6 +22,8 @@ gulp.task(
"clean-hassio",
"gen-icons-json",
"gen-index-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
)
);
@ -32,6 +36,8 @@ gulp.task(
},
"clean-hassio",
"gen-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod",
...// Don't compress running tests

View File

@ -266,6 +266,7 @@ gulp.task(taskName, function () {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
delete data.supervisor;
return data;
})
)
@ -342,6 +343,62 @@ gulp.task(
}
);
gulp.task("build-translation-fragment-supervisor", function () {
return gulp
.src(fullDir + "/*.json")
.pipe(transform((data) => data.supervisor))
.pipe(gulp.dest(workDir + "/supervisor"));
});
gulp.task("build-translation-flatten-supervisor", function () {
return gulp
.src(workDir + "/supervisor/*.json")
.pipe(
transform(function (data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
})
)
.pipe(gulp.dest(outDir));
});
gulp.task("build-translation-write-metadata", function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (value.nativeName) {
newData[key] = value;
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
});
gulp.task(
"build-translations",
gulp.series(
@ -353,41 +410,20 @@ gulp.task(
gulp.parallel(...splitTasks),
"build-flattened-translations",
"build-translation-fingerprints",
function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (value.nativeName) {
newData[key] = value;
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
}
"build-translation-write-metadata"
)
);
gulp.task(
"build-supervisor-translations",
gulp.series(
"clean-translations",
"ensure-translations-build-dir",
"build-master-translation",
"build-merged-translations",
"build-translation-fragment-supervisor",
"build-translation-flatten-supervisor",
"build-translation-fingerprints",
"build-translation-write-metadata"
)
);

View File

@ -137,7 +137,12 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false,
latestBuild: true,
})
).watch({}, doneHandler());
).watch({ ignored: /build-translations/ }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
);
});
gulp.task("webpack-prod-hassio", () =>

View File

@ -34,6 +34,7 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"

View File

@ -77,13 +77,16 @@ class HassioAddonStore extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
hassio
main-page
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Add-on Store</span>
<span slot="header">
${this.supervisor.localize("panel.store")}
</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"

View File

@ -62,6 +62,15 @@ class HassioAddonConfig extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
public computeLabel = (entry: HaFormSchema): string => {
return (
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name].name ||
entry.name
);
};
private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) => {
return schema.filter((entry) => entry.name in options || entry.required);
@ -102,6 +111,7 @@ class HassioAddonConfig extends LitElement {
? html`<ha-form
.data=${this._options!}
@value-changed=${this._configChanged}
.computeLabel=${this.computeLabel}
.schema=${this._showOptional
? this.addon.schema!
: this._filteredShchema(

View File

@ -80,6 +80,7 @@ class HassioAddonDashboard extends LitElement {
const addonTabs: PageNavigation[] = [
{
name: "Info",
translationKey: "addon.panel.info",
path: `/hassio/addon/${this.addon.slug}/info`,
iconPath: mdiInformationVariant,
},
@ -88,6 +89,7 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.documentation) {
addonTabs.push({
name: "Documentation",
translationKey: "addon.panel.documentation",
path: `/hassio/addon/${this.addon.slug}/documentation`,
iconPath: mdiFileDocument,
});
@ -97,11 +99,13 @@ class HassioAddonDashboard extends LitElement {
addonTabs.push(
{
name: "Configuration",
translationKey: "addon.panel.configuration",
path: `/hassio/addon/${this.addon.slug}/config`,
iconPath: mdiCogs,
},
{
name: "Log",
translationKey: "addon.panel.log",
path: `/hassio/addon/${this.addon.slug}/logs`,
iconPath: mdiMathLog,
}
@ -113,11 +117,12 @@ class HassioAddonDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
.route=${route}
hassio
.tabs=${addonTabs}
supervisor
>
<span slot="header">${this.addon.name}</span>
<hassio-addon-router

View File

@ -27,17 +27,15 @@ class HassioAddons extends LitElement {
protected render(): TemplateResult {
return html`
<div class="content">
<h1>Add-ons</h1>
<h1>${this.supervisor.localize("dashboard.addons")}</h1>
<div class="card-group">
${!this.supervisor.supervisor.addons?.length
? html`
<ha-card>
<div class="card-content">
You don't have any add-ons installed yet. Head over to
<button class="link" @click=${this._openStore}>
the add-on store
${this.supervisor.localize("dashboard.no_addons")}
</button>
to get started!
</div>
</ha-card>
`
@ -58,10 +56,16 @@ class HassioAddons extends LitElement {
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? "Add-on is stopped"
? this.supervisor.localize(
"dashboard.addon_stopped"
)
: addon.update_available!
? "New version available"
: "Add-on is running"}
? this.supervisor.localize(
"dashboard.addon_new_version"
)
: this.supervisor.localize(
"dashboard.addon_running"
)}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"

View File

@ -29,13 +29,16 @@ class HassioDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Dashboard</span>
<span slot="header">
${this.supervisor.localize("panel.dashboard")}
</span>
<div class="content">
<hassio-update
.hass=${this.hass}

View File

@ -10,9 +10,11 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
@ -24,7 +26,10 @@ import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
Supervisor,
supervisorApiWsRequest,
} from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@ -34,6 +39,10 @@ import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => {
return key === "os" ? version : `${key}-${version}`;
};
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -59,9 +68,12 @@ export class HassioUpdate extends LitElement {
return html`
<div class="content">
<h1>
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
${this.supervisor.localize(
"dashboard.update_available",
"count",
updatesAvailable
)}
🎉
</h1>
<div class="card-group">
${this._renderUpdateCard(
@ -110,14 +122,30 @@ export class HassioUpdate extends LitElement {
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div>
<div class="update-heading">${name} ${object.version_latest}</div>
<div class="warning">
You are currently running version ${object.version}
</div>
<div class="update-heading">${name}</div>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
${computeVersion(key, object.version!)}
</span>
</ha-settings-row>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
${computeVersion(key, object.version_latest!)}
</span>
</ha-settings-row>
</div>
<div class="card-actions">
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
<mwc-button>Release notes</mwc-button>
<mwc-button>
${this.supervisor.localize("common.release_notes")}
</mwc-button>
</a>
<ha-progress-button
.apiPath=${apiPath}
@ -126,7 +154,7 @@ export class HassioUpdate extends LitElement {
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
Update
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
</ha-card>
@ -141,10 +169,20 @@ export class HassioUpdate extends LitElement {
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
text: `Are you sure you want to update ${item.name} to version ${item.version}?`,
confirmText: "update",
dismissText: "cancel",
title: this.supervisor.localize(
"confirm.update.title",
"name",
item.name
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
item.name,
"version",
computeVersion(item.key, item.version)
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@ -152,7 +190,15 @@ export class HassioUpdate extends LitElement {
return;
}
try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
await supervisorApiWsRequest(this.hass.connection, {
method: "post",
endpoint: item.apiPath.replace("hassio", ""),
timeout: null,
});
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-colllection-refresh", {
colllection: item.key,
});
@ -165,7 +211,7 @@ export class HassioUpdate extends LitElement {
!ignoredStatusCodes.has(err.status_code)
) {
showAlertDialog(this, {
title: "Update failed",
title: this.supervisor.localize("error.update_failed"),
text: extractApiErrorMessage(err),
});
}
@ -190,9 +236,6 @@ export class HassioUpdate extends LitElement {
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
.warning {
color: var(--secondary-text-color);
}
.card-content {
height: calc(100% - 47px);
box-sizing: border-box;
@ -200,13 +243,13 @@ export class HassioUpdate extends LitElement {
.card-actions {
text-align: right;
}
.errors {
color: var(--error-color);
padding: 16px;
}
a {
text-decoration: none;
}
ha-settings-row {
padding: 0;
--paper-item-body-two-line-min-height: 32px;
}
`,
];
}

View File

@ -3,7 +3,7 @@ import { atLeastVersion } from "../../src/common/config/version";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { supervisorCollection } from "../../src/data/supervisor/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../src/types";
@ -14,6 +14,8 @@ import { SupervisorBaseElement } from "./supervisor-base-element";
export class HassioMain extends SupervisorBaseElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow!: boolean;
@ -72,18 +74,6 @@ export class HassioMain extends SupervisorBaseElement {
}
protected render() {
if (!this.supervisor || !this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (
Object.keys(supervisorCollection).some(
(colllection) => !this.supervisor![colllection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
return html`
<hassio-router
.hass=${this.hass}

View File

@ -7,7 +7,10 @@ import {
property,
TemplateResult,
} from "lit-element";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import {
Supervisor,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
@ -22,6 +25,17 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (
Object.keys(supervisorCollection).some(
(colllection) => !this.supervisor[colllection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
return html`
<hassio-panel-router
.hass=${this.hass}

View File

@ -3,22 +3,22 @@ import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
export const supervisorTabs: PageNavigation[] = [
{
name: "Dashboard",
translationKey: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: mdiViewDashboard,
},
{
name: "Add-on Store",
translationKey: "panel.store",
path: `/hassio/store`,
iconPath: mdiStore,
},
{
name: "Snapshots",
translationKey: "panel.snapshots",
path: `/hassio/snapshots`,
iconPath: mdiBackupRestore,
},
{
name: "System",
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},

View File

@ -104,13 +104,16 @@ class HassioSnapshots extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Snapshots</span>
<span slot="header">
${this.supervisor.localize("panel.snapshots")}
</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"

View File

@ -6,6 +6,7 @@ import {
PropertyValues,
} from "lit-element";
import { atLeastVersion } from "../../src/common/config/version";
import { computeLocalize } from "../../src/common/translations/localize";
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import { HassioResponse } from "../../src/data/hassio/common";
import {
@ -29,6 +30,7 @@ import {
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { getTranslation } from "../../src/util/common-translation";
declare global {
interface HASSDomEvents {
@ -40,7 +42,9 @@ declare global {
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "",
};
@internalProperty() private _unsubs: Record<string, UnsubscribeFunc> = {};
@ -49,6 +53,15 @@ export class SupervisorBaseElement extends urlSyncMixin(
Collection<unknown>
> = {};
@internalProperty() private _resources?: Record<string, any>;
@internalProperty() private _language = "en";
public connectedCallback(): void {
super.connectedCallback();
this._initializeLocalize();
}
public disconnectedCallback() {
super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => {
@ -56,15 +69,50 @@ export class SupervisorBaseElement extends urlSyncMixin(
});
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("_language")) {
if (changedProperties.get("_language") !== this._language) {
this._initializeLocalize();
}
}
}
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor!, ...obj };
this.supervisor = { ...this.supervisor, ...obj };
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (this._language !== this.hass.language) {
this._language = this.hass.language;
}
this._initializeLocalize();
this._initSupervisor();
}
private async _initializeLocalize() {
const { language, data } = await getTranslation(
null,
this._language,
"/api/hassio/app/static/translations"
);
this._resources = {
[language]: data,
};
this.supervisor = {
...this.supervisor,
localize: await computeLocalize(
this.constructor.prototype,
this._language,
this._resources
),
};
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const colllection = ev.detail.colllection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
@ -104,52 +152,54 @@ export class SupervisorBaseElement extends urlSyncMixin(
}
});
if (this.supervisor === undefined) {
Object.keys(this._collections).forEach((collection) =>
Object.keys(this._collections).forEach((collection) => {
if (
this.supervisor === undefined ||
this.supervisor[collection] === undefined
) {
this._updateSupervisor({
[collection]: this._collections[collection].state,
})
);
}
return;
});
}
});
} else {
const [
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this.supervisor = {
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
};
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
const [
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this.supervisor = {
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
};
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
}

View File

@ -32,13 +32,16 @@ class HassioSystem extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">System</span>
<span slot="header">
${this.supervisor.localize("panel.system")}
</span>
<div class="content">
<div class="card-group">
<hassio-core-info

View File

@ -8,9 +8,10 @@ export const atLeastVersion = (
return (
Number(haMajor) > major ||
(Number(haMajor) === major && (patch === undefined
? Number(haMinor) >= minor
: Number(haMinor) > minor)) ||
(Number(haMajor) === major &&
(patch === undefined
? Number(haMinor) >= minor
: Number(haMinor) > minor)) ||
(patch !== undefined &&
Number(haMajor) === major &&
Number(haMinor) === minor &&

View File

@ -16,6 +16,10 @@ export type AddonStartup =
export type AddonState = "started" | "stopped" | null;
export type AddonRepository = "core" | "local" | string;
interface AddonTranslations {
[key: string]: Record<string, Record<string, Record<string, string>>>;
}
export interface HassioAddonInfo {
advanced: boolean;
available: boolean;
@ -82,6 +86,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
slug: string;
startup: AddonStartup;
stdin: boolean;
translations: AddonTranslations;
watchdog: null | boolean;
webui: null | string;
}

View File

@ -1,5 +1,6 @@
import { Connection, getCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { LocalizeFunc } from "../../common/translations/localize";
import { HomeAssistant } from "../../types";
import { HassioAddonsInfo } from "../hassio/addon";
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
@ -46,6 +47,7 @@ interface supervisorApiRequest {
method?: "get" | "post" | "delete" | "put";
force_rest?: boolean;
data?: any;
timeout?: number | null;
}
export interface SupervisorEvent {
@ -65,6 +67,7 @@ export interface Supervisor {
os: HassioHassOSInfo;
addon: HassioAddonsInfo;
store: SupervisorStore;
localize: LocalizeFunc;
}
export const supervisorApiWsRequest = <T>(

View File

@ -7,7 +7,7 @@ import { computeLocalize } from "../common/translations/localize";
import { DEFAULT_PANEL } from "../data/panel";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import { getLocalLanguage, getTranslation } from "../util/hass-translation";
import { getTranslation, getLocalLanguage } from "../util/hass-translation";
import { demoConfig } from "./demo_config";
import { demoPanels } from "./demo_panels";
import { demoServices } from "./demo_services";

View File

@ -16,6 +16,7 @@ import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { restoreScroll } from "../common/decorators/restore-scroll";
import { navigate } from "../common/navigate";
import { LocalizeFunc } from "../common/translations/localize";
import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-icon";
import "../components/ha-icon-button-arrow-prev";
@ -40,7 +41,9 @@ export interface PageNavigation {
class HassTabsSubpage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public hassio = false;
@property({ type: Boolean }) public supervisor = false;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
@property({ type: String, attribute: "back-path" }) public backPath?: string;
@ -48,9 +51,9 @@ class HassTabsSubpage extends LitElement {
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
@property() public route!: Route;
@property({ attribute: false }) public route!: Route;
@property() public tabs!: PageNavigation[];
@property({ attribute: false }) public tabs!: PageNavigation[];
@property({ type: Boolean, reflect: true }) public narrow = false;
@ -71,7 +74,8 @@ class HassTabsSubpage extends LitElement {
showAdvanced: boolean | undefined,
_components,
_language,
_narrow
_narrow,
localizeFunc
) => {
const shownTabs = tabs.filter(
(page) =>
@ -91,7 +95,7 @@ class HassTabsSubpage extends LitElement {
.active=${page === activeTab}
.narrow=${this.narrow}
.name=${page.translationKey
? this.hass.localize(page.translationKey)
? localizeFunc(page.translationKey)
: page.name}
>
${page.iconPath
@ -130,7 +134,8 @@ class HassTabsSubpage extends LitElement {
this.hass.userData?.showAdvanced,
this.hass.config.components,
this.hass.language,
this.narrow
this.narrow,
this.localizeFunc || this.hass.localize
);
const showTabs = tabs.length > 1 || !this.narrow;
return html`
@ -138,7 +143,7 @@ class HassTabsSubpage extends LitElement {
${this.mainPage
? html`
<ha-menu-button
.hassio=${this.hassio}
.hassio=${this.supervisor}
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>

View File

@ -1,7 +1,7 @@
import { LitElement, property, PropertyValues } from "lit-element";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
import { Constructor, Resources } from "../types";
import { getLocalLanguage, getTranslation } from "../util/hass-translation";
import { getTranslation, getLocalLanguage } from "../util/hass-translation";
const empty = () => "";

View File

@ -161,8 +161,8 @@ export class HaConfigDevicePage extends LitElement {
const batteryState = batteryEntity
? this.hass.states[batteryEntity.entity_id]
: undefined;
const batteryIsBinary = batteryState
&& computeStateDomain(batteryState) === "binary_sensor";
const batteryIsBinary =
batteryState && computeStateDomain(batteryState) === "binary_sensor";
const batteryChargingState = batteryChargingEntity
? this.hass.states[batteryChargingEntity.entity_id]
: undefined;

View File

@ -1,7 +1,7 @@
import "@material/mwc-button";
import {
mdiInformationOutline,
mdiClipboardTextMultipleOutline
mdiClipboardTextMultipleOutline,
} from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
@ -169,7 +169,10 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
<th>[[localize('ui.panel.developer-tools.tabs.states.state')]]</th>
<th hidden$="[[narrow]]">
[[localize('ui.panel.developer-tools.tabs.states.attributes')]]
<paper-checkbox checked="{{_showAttributes}}" on-change="{{saveAttributeCheckboxState}}"></paper-checkbox>
<paper-checkbox
checked="{{_showAttributes}}"
on-change="{{saveAttributeCheckboxState}}"
></paper-checkbox>
</th>
</tr>
<tr>
@ -285,7 +288,9 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
_showAttributes: {
type: Boolean,
value: JSON.parse(localStorage.getItem("devToolsShowAttributes") || true),
value: JSON.parse(
localStorage.getItem("devToolsShowAttributes") || true
),
},
_entities: {

View File

@ -199,7 +199,10 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
left: 0;
right: 0;
bottom: 0;
background-color: var(--ha-picture-card-background-color, rgba(0, 0, 0, 0.3));
background-color: var(
--ha-picture-card-background-color,
rgba(0, 0, 0, 0.3)
);
padding: 16px;
font-size: 16px;
line-height: 16px;

View File

@ -314,7 +314,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
left: 0;
right: 0;
bottom: 0;
background-color: var(--ha-picture-card-background-color, rgba(0, 0, 0, 0.3));
background-color: var(
--ha-picture-card-background-color,
rgba(0, 0, 0, 0.3)
);
padding: 4px 8px;
font-size: 16px;
line-height: 40px;

View File

@ -12,8 +12,8 @@ import { translationMetadata } from "../resources/translations-metadata";
import { Constructor, HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage";
import {
getLocalLanguage,
getTranslation,
getLocalLanguage,
getUserLanguage,
} from "../util/hass-translation";
import { HassBaseEl } from "./hass-base-mixin";

View File

@ -3417,5 +3417,46 @@
}
}
}
},
"supervisor": {
"addon": {
"panel": {
"configuration": "Configuration",
"documentation": "Documentation",
"info": "Info",
"log": "Log"
}
},
"common": {
"cancel": "Cancel",
"newest_version": "Newest Version",
"release_notes": "Release notes",
"update": "Update",
"version": "Version"
},
"confirm": {
"update": {
"title": "Update ${name}",
"text": "Are you sure you want to update {name} to version {version}?"
}
},
"dashboard": {
"addon_new_version": "New version available",
"addon_running": "Add-on is running",
"addon_stopped": "Add-on is stopped",
"addons": "Add-ons",
"no_addons": "You don't have any add-ons installed yet. Head over to the add-on store to get started!",
"update_available": "{count, plural,\n one {Update}\n other {{count} Updates}\n} pending"
},
"error": {
"unknown": "Unknown error",
"update_failed": "Update failed"
},
"panel": {
"dashboard": "Dashboard",
"snapshots": "Snapshots",
"store": "Add-on Store",
"system": "System"
}
}
}

View File

@ -0,0 +1,56 @@
import { translationMetadata } from "../resources/translations-metadata";
const DEFAULT_BASE_URL = "/static/translations";
// Store loaded translations in memory so translations are available immediately
// when DOM is created in Polymer. Even a cache lookup creates noticeable latency.
const translations = {};
async function fetchTranslation(fingerprint: string, base_url: string) {
const response = await fetch(`${base_url}/${fingerprint}`, {
credentials: "same-origin",
});
if (!response.ok) {
throw new Error(
`Fail to fetch translation ${fingerprint}: HTTP response status is ${response.status}`
);
}
return response.json();
}
export async function getTranslation(
fragment: string | null,
language: string,
base_url?: string
) {
const metadata = translationMetadata.translations[language];
if (!metadata) {
if (language !== "en") {
return getTranslation(fragment, "en", base_url);
}
throw new Error("Language en is not found in metadata");
}
// nl-abcd.jon or logbook/nl-abcd.json
const fingerprint = `${fragment ? fragment + "/" : ""}${language}-${
metadata.hash
}.json`;
// Fetch translation from the server
if (!translations[fingerprint]) {
translations[fingerprint] = fetchTranslation(
fingerprint,
base_url || DEFAULT_BASE_URL
)
.then((data) => ({ language, data }))
.catch((error) => {
delete translations[fingerprint];
if (language !== "en") {
// Couldn't load selected translation. Try a fall back to en before failing.
return getTranslation(fragment, "en", base_url);
}
return Promise.reject(error);
});
}
return translations[fingerprint];
}

View File

@ -1,6 +1,7 @@
import { fetchTranslationPreferences } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import { getTranslation as commonGetTranslation } from "./common-translation";
const STORAGE = window.localStorage || {};
@ -93,55 +94,13 @@ export function getLocalLanguage() {
return "en";
}
// Store loaded translations in memory so translations are available immediately
// when DOM is created in Polymer. Even a cache lookup creates noticeable latency.
const translations = {};
async function fetchTranslation(fingerprint) {
const response = await fetch(`/static/translations/${fingerprint}`, {
credentials: "same-origin",
});
if (!response.ok) {
throw new Error(
`Fail to fetch translation ${fingerprint}: HTTP response status is ${response.status}`
);
}
return response.json();
}
export async function getTranslation(
fragment: string | null,
language: string
) {
const metadata = translationMetadata.translations[language];
if (!metadata) {
if (language !== "en") {
return getTranslation(fragment, "en");
}
throw new Error("Language en is not found in metadata");
}
// nl-abcd.jon or logbook/nl-abcd.json
const fingerprint = `${fragment ? fragment + "/" : ""}${language}-${
metadata.hash
}.json`;
// Fetch translation from the server
if (!translations[fingerprint]) {
translations[fingerprint] = fetchTranslation(fingerprint)
.then((data) => ({ language, data }))
.catch((error) => {
delete translations[fingerprint];
if (language !== "en") {
// Couldn't load selected translation. Try a fall back to en before failing.
return getTranslation(fragment, "en");
}
return Promise.reject(error);
});
}
return translations[fingerprint];
return commonGetTranslation(fragment, language);
}
// Load selected translation into memory immediately so it is ready when Polymer
// initializes.
getTranslation(null, getLocalLanguage());
commonGetTranslation(null, getLocalLanguage());