🌐 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); copyTranslations(staticDir);
}); });
gulp.task("copy-translations-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-app", async () => { gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static; const staticDir = paths.app_output_static;
// Basic static files // Basic static files

View File

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

View File

@ -266,6 +266,7 @@ gulp.task(taskName, function () {
TRANSLATION_FRAGMENTS.forEach((fragment) => { TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment]; delete data.ui.panel[fragment];
}); });
delete data.supervisor;
return data; 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( gulp.task(
"build-translations", "build-translations",
gulp.series( gulp.series(
@ -353,41 +410,20 @@ gulp.task(
gulp.parallel(...splitTasks), gulp.parallel(...splitTasks),
"build-flattened-translations", "build-flattened-translations",
"build-translation-fingerprints", "build-translation-fingerprints",
function writeMetadata() { "build-translation-write-metadata"
return gulp )
.src( );
[
path.join(paths.translations_src, "translationMetadata.json"), gulp.task(
workDir + "/testMetadata.json", "build-supervisor-translations",
workDir + "/translationFingerprints.json", gulp.series(
], "clean-translations",
{ allowEmpty: true } "ensure-translations-build-dir",
) "build-master-translation",
.pipe(merge({})) "build-merged-translations",
.pipe( "build-translation-fragment-supervisor",
transform(function (data) { "build-translation-flatten-supervisor",
const newData = {}; "build-translation-fingerprints",
Object.entries(data).forEach(([key, value]) => { "build-translation-write-metadata"
// 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));
}
) )
); );

View File

@ -137,7 +137,12 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false, isProdBuild: false,
latestBuild: true, 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", () => gulp.task("webpack-prod-hassio", () =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,10 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } 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 { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router"; import "./hassio-panel-router";
@ -22,6 +25,17 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
protected render(): TemplateResult { 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` return html`
<hassio-panel-router <hassio-panel-router
.hass=${this.hass} .hass=${this.hass}

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import {
PropertyValues, PropertyValues,
} from "lit-element"; } from "lit-element";
import { atLeastVersion } from "../../src/common/config/version"; import { atLeastVersion } from "../../src/common/config/version";
import { computeLocalize } from "../../src/common/translations/localize";
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon"; import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import { HassioResponse } from "../../src/data/hassio/common"; import { HassioResponse } from "../../src/data/hassio/common";
import { import {
@ -29,6 +30,7 @@ import {
} from "../../src/data/supervisor/supervisor"; } from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin"; import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { getTranslation } from "../../src/util/common-translation";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -40,7 +42,9 @@ declare global {
export class SupervisorBaseElement extends urlSyncMixin( export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement) ProvideHassLitMixin(LitElement)
) { ) {
@property({ attribute: false }) public supervisor?: Supervisor; @property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "",
};
@internalProperty() private _unsubs: Record<string, UnsubscribeFunc> = {}; @internalProperty() private _unsubs: Record<string, UnsubscribeFunc> = {};
@ -49,6 +53,15 @@ export class SupervisorBaseElement extends urlSyncMixin(
Collection<unknown> Collection<unknown>
> = {}; > = {};
@internalProperty() private _resources?: Record<string, any>;
@internalProperty() private _language = "en";
public connectedCallback(): void {
super.connectedCallback();
this._initializeLocalize();
}
public disconnectedCallback() { public disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => { 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 { protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor!, ...obj }; this.supervisor = { ...this.supervisor, ...obj };
} }
protected firstUpdated(changedProps: PropertyValues): void { protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this._language !== this.hass.language) {
this._language = this.hass.language;
}
this._initializeLocalize();
this._initSupervisor(); 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) { private async _handleSupervisorStoreRefreshEvent(ev) {
const colllection = ev.detail.colllection; const colllection = ev.detail.colllection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { 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({ this._updateSupervisor({
[collection]: this._collections[collection].state, [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` return html`
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
hassio
main-page
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs} .tabs=${supervisorTabs}
main-page
supervisor
> >
<span slot="header">System</span> <span slot="header">
${this.supervisor.localize("panel.system")}
</span>
<div class="content"> <div class="content">
<div class="card-group"> <div class="card-group">
<hassio-core-info <hassio-core-info

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { computeLocalize } from "../common/translations/localize";
import { DEFAULT_PANEL } from "../data/panel"; import { DEFAULT_PANEL } from "../data/panel";
import { translationMetadata } from "../resources/translations-metadata"; import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { getLocalLanguage, getTranslation } from "../util/hass-translation"; import { getTranslation, getLocalLanguage } from "../util/hass-translation";
import { demoConfig } from "./demo_config"; import { demoConfig } from "./demo_config";
import { demoPanels } from "./demo_panels"; import { demoPanels } from "./demo_panels";
import { demoServices } from "./demo_services"; 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 { isComponentLoaded } from "../common/config/is_component_loaded";
import { restoreScroll } from "../common/decorators/restore-scroll"; import { restoreScroll } from "../common/decorators/restore-scroll";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { LocalizeFunc } from "../common/translations/localize";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-icon"; import "../components/ha-icon";
import "../components/ha-icon-button-arrow-prev"; import "../components/ha-icon-button-arrow-prev";
@ -40,7 +41,9 @@ export interface PageNavigation {
class HassTabsSubpage extends LitElement { class HassTabsSubpage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @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; @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({ 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; @property({ type: Boolean, reflect: true }) public narrow = false;
@ -71,7 +74,8 @@ class HassTabsSubpage extends LitElement {
showAdvanced: boolean | undefined, showAdvanced: boolean | undefined,
_components, _components,
_language, _language,
_narrow _narrow,
localizeFunc
) => { ) => {
const shownTabs = tabs.filter( const shownTabs = tabs.filter(
(page) => (page) =>
@ -91,7 +95,7 @@ class HassTabsSubpage extends LitElement {
.active=${page === activeTab} .active=${page === activeTab}
.narrow=${this.narrow} .narrow=${this.narrow}
.name=${page.translationKey .name=${page.translationKey
? this.hass.localize(page.translationKey) ? localizeFunc(page.translationKey)
: page.name} : page.name}
> >
${page.iconPath ${page.iconPath
@ -130,7 +134,8 @@ class HassTabsSubpage extends LitElement {
this.hass.userData?.showAdvanced, this.hass.userData?.showAdvanced,
this.hass.config.components, this.hass.config.components,
this.hass.language, this.hass.language,
this.narrow this.narrow,
this.localizeFunc || this.hass.localize
); );
const showTabs = tabs.length > 1 || !this.narrow; const showTabs = tabs.length > 1 || !this.narrow;
return html` return html`
@ -138,7 +143,7 @@ class HassTabsSubpage extends LitElement {
${this.mainPage ${this.mainPage
? html` ? html`
<ha-menu-button <ha-menu-button
.hassio=${this.hassio} .hassio=${this.supervisor}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-menu-button> ></ha-menu-button>

View File

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

View File

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

View File

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

View File

@ -199,7 +199,10 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
left: 0; left: 0;
right: 0; right: 0;
bottom: 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; padding: 16px;
font-size: 16px; font-size: 16px;
line-height: 16px; line-height: 16px;

View File

@ -314,7 +314,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
left: 0; left: 0;
right: 0; right: 0;
bottom: 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; padding: 4px 8px;
font-size: 16px; font-size: 16px;
line-height: 40px; line-height: 40px;

View File

@ -12,8 +12,8 @@ import { translationMetadata } from "../resources/translations-metadata";
import { Constructor, HomeAssistant } from "../types"; import { Constructor, HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage"; import { storeState } from "../util/ha-pref-storage";
import { import {
getLocalLanguage,
getTranslation, getTranslation,
getLocalLanguage,
getUserLanguage, getUserLanguage,
} from "../util/hass-translation"; } from "../util/hass-translation";
import { HassBaseEl } from "./hass-base-mixin"; 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 { fetchTranslationPreferences } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata"; import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { getTranslation as commonGetTranslation } from "./common-translation";
const STORAGE = window.localStorage || {}; const STORAGE = window.localStorage || {};
@ -93,55 +94,13 @@ export function getLocalLanguage() {
return "en"; 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( export async function getTranslation(
fragment: string | null, fragment: string | null,
language: string language: string
) { ) {
const metadata = translationMetadata.translations[language]; return commonGetTranslation(fragment, 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];
} }
// Load selected translation into memory immediately so it is ready when Polymer // Load selected translation into memory immediately so it is ready when Polymer
// initializes. // initializes.
getTranslation(null, getLocalLanguage()); commonGetTranslation(null, getLocalLanguage());