diff --git a/build-scripts/gulp/locale-data.js b/build-scripts/gulp/locale-data.js index 14ce9bc06a3c..4cf91711d495 100755 --- a/build-scripts/gulp/locale-data.js +++ b/build-scripts/gulp/locale-data.js @@ -9,7 +9,6 @@ const outDir = join(paths.build_dir, "locale-data"); const INTL_POLYFILLS = { "intl-datetimeformat": "DateTimeFormat", - "intl-durationFormat": "DurationFormat", "intl-displaynames": "DisplayNames", "intl-listformat": "ListFormat", "intl-numberformat": "NumberFormat", diff --git a/pyproject.toml b/pyproject.toml index 5a79bd0d75d3..d373f50e27d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20241127.0" +version = "20241127.1" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/common/datetime/format_duration.ts b/src/common/datetime/format_duration.ts index a67d5f82ca9e..e20f7e18af25 100644 --- a/src/common/datetime/format_duration.ts +++ b/src/common/datetime/format_duration.ts @@ -1,4 +1,3 @@ -import { DurationFormat } from "@formatjs/intl-durationformat"; import type { DurationInput } from "@formatjs/intl-durationformat/src/types"; import memoizeOne from "memoize-one"; import type { HaDurationData } from "../../components/ha-duration-input"; @@ -49,7 +48,7 @@ export const formatNumericDuration = ( const formatDurationLongMem = memoizeOne( (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { + new Intl.DurationFormat(locale.language, { style: "long", }) ); @@ -61,7 +60,7 @@ export const formatDurationLong = ( const formatDigitalDurationMem = memoizeOne( (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { + new Intl.DurationFormat(locale.language, { style: "digital", hoursDisplay: "auto", }) @@ -72,13 +71,13 @@ export const formatDurationDigital = ( duration: HaDurationData ) => formatDigitalDurationMem(locale).format(duration); -export const DURATION_UNITS = ["ms", "s", "min", "h", "d"] as const; +export const DURATION_UNITS = ["s", "min", "h", "d"] as const; type DurationUnit = (typeof DURATION_UNITS)[number]; const formatDurationDayMem = memoizeOne( (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { + new Intl.DurationFormat(locale.language, { style: "narrow", daysDisplay: "always", }) @@ -86,7 +85,7 @@ const formatDurationDayMem = memoizeOne( const formatDurationHourMem = memoizeOne( (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { + new Intl.DurationFormat(locale.language, { style: "narrow", hoursDisplay: "always", }) @@ -94,7 +93,7 @@ const formatDurationHourMem = memoizeOne( const formatDurationMinuteMem = memoizeOne( (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { + new Intl.DurationFormat(locale.language, { style: "narrow", minutesDisplay: "always", }) @@ -102,20 +101,12 @@ const formatDurationMinuteMem = memoizeOne( const formatDurationSecondMem = memoizeOne( (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { + new Intl.DurationFormat(locale.language, { style: "narrow", secondsDisplay: "always", }) ); -const formatDurationMillisecondMem = memoizeOne( - (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { - style: "narrow", - millisecondsDisplay: "always", - }) -); - export const formatDuration = ( locale: FrontendLocaleData, duration: string, @@ -164,13 +155,6 @@ export const formatDuration = ( }; return formatDurationSecondMem(locale).format(input); } - case "ms": { - const milliseconds = Math.floor(value); - const input: DurationInput = { - milliseconds, - }; - return formatDurationMillisecondMem(locale).format(input); - } default: throw new Error("Invalid duration unit"); } diff --git a/src/data/integration.ts b/src/data/integration.ts index 846751e07c3f..c293dfc540b0 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -40,12 +40,13 @@ export interface IntegrationManifest { loggers?: string[]; quality_scale?: | "bronze" + | "silver" | "gold" - | "internal" | "platinum" - | "silver" - | "custom" - | "no_score"; + | "no_score" + | "internal" + | "legacy" + | "custom"; iot_class: | "assumed_state" | "cloud_polling" diff --git a/src/data/integration_quality_scale.ts b/src/data/integration_quality_scale.ts new file mode 100644 index 000000000000..e49bbc835dde --- /dev/null +++ b/src/data/integration_quality_scale.ts @@ -0,0 +1,39 @@ +import { mdiContentSave, mdiMedal, mdiTrophy } from "@mdi/js"; +import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg"; +import type { LocalizeKeys } from "../common/translations/localize"; + +/** + * Map integration quality scale to icon and translation key. + */ +export const QUALITY_SCALE_MAP: Record< + string, + { icon: string; translationKey: LocalizeKeys } +> = { + bronze: { + icon: mdiMedal, + translationKey: "ui.panel.config.integrations.config_entry.bronze_quality", + }, + silver: { + icon: mdiMedal, + translationKey: "ui.panel.config.integrations.config_entry.silver_quality", + }, + gold: { + icon: mdiMedal, + translationKey: "ui.panel.config.integrations.config_entry.gold_quality", + }, + platinum: { + icon: mdiTrophy, + translationKey: + "ui.panel.config.integrations.config_entry.platinum_quality", + }, + internal: { + icon: mdiHomeAssistant, + translationKey: + "ui.panel.config.integrations.config_entry.internal_integration", + }, + legacy: { + icon: mdiContentSave, + translationKey: + "ui.panel.config.integrations.config_entry.legacy_integration", + }, +}; diff --git a/src/panels/config/integrations/ha-config-integration-page.ts b/src/panels/config/integrations/ha-config-integration-page.ts index 14a3282e92f3..f44a03b00fad 100644 --- a/src/panels/config/integrations/ha-config-integration-page.ts +++ b/src/panels/config/integrations/ha-config-integration-page.ts @@ -13,7 +13,6 @@ import { mdiDownload, mdiFileCodeOutline, mdiHandExtendedOutline, - mdiMedal, mdiOpenInNew, mdiPackageVariant, mdiPlayCircleOutline, @@ -23,7 +22,6 @@ import { mdiRenameBox, mdiShapeOutline, mdiStopCircleOutline, - mdiTrophy, mdiWeb, mdiWrench, } from "@mdi/js"; @@ -107,9 +105,7 @@ import { documentationUrl } from "../../../util/documentation-url"; import { fileDownload } from "../../../util/file_download"; import type { DataEntryFlowProgressExtended } from "./ha-config-integrations"; import { showAddIntegrationDialog } from "./show-add-integration-dialog"; - -type MedalColor = "gold" | "silver" | "bronze" | "platinum"; -const MEDAL_COLORS = ["bronze", "silver", "gold", "platinum"]; +import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale"; export const renderConfigEntryError = ( hass: HomeAssistant, @@ -344,36 +340,30 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { ? html`
${this._manifest.version}
` : nothing} ${this._manifest?.quality_scale && - MEDAL_COLORS.includes(this._manifest.quality_scale) + Object.keys(QUALITY_SCALE_MAP).includes( + this._manifest.quality_scale + ) ? html`
- + ${this.hass.localize( - `ui.panel.config.integrations.config_entry.${this._manifest.quality_scale as MedalColor}_quality`, - { - quality_scale: html` - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.quality_scale" - )} - - `, - } + QUALITY_SCALE_MAP[this._manifest.quality_scale] + .translationKey )} - +
` : nothing} @@ -383,9 +373,18 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { class="warning" path=${mdiPackageVariant} > - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.custom_integration" - )} + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.custom_integration" + )} + ` : nothing} ${this._manifest?.iot_class?.startsWith("cloud_") @@ -1496,6 +1495,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { .logo-container { display: flex; justify-content: center; + margin-bottom: 8px; } .version { padding-top: 8px; @@ -1538,17 +1538,24 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { 100%; animation: shimmer 2.5s infinite; } - ha-svg-icon.bronze-medal { + ha-svg-icon.bronze-quality { color: #cd7f32; } - ha-svg-icon.silver-medal { + ha-svg-icon.silver-quality { color: silver; } - ha-svg-icon.gold-medal { + ha-svg-icon.gold-quality { color: gold; } - ha-svg-icon.platinum-medal { - color: #d9d9d9; + ha-svg-icon.platinum-quality { + color: #727272; + } + ha-svg-icon.internal-quality { + color: var(--primary-color); + } + ha-svg-icon.legacy-quality { + color: var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38)); + animation: unset; } ha-md-list-item { position: relative; diff --git a/src/translations/en.json b/src/translations/en.json index 221ad621bc24..8069cf58ad32 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4505,8 +4505,10 @@ } }, "custom_integration": "Custom integration", + "internal_integration": "Internal integration", + "legacy_integration": "Legacy integration", "custom_overwrites_core": "Custom integration that replaces a core component", - "depends_on_cloud": "Depends on Internet connection", + "depends_on_cloud": "Requires Internet", "yaml_only": "This integration cannot be setup from the UI", "no_config_flow": "This integration was not set up from the UI", "disabled_polling": "Automatic polling for updated data disabled", @@ -4521,11 +4523,10 @@ "setup_in_progress": "Initializing" }, "open_configuration_url": "Visit device", - "bronze_quality": "Bronze on our {quality_scale}", - "silver_quality": "Silver on our {quality_scale}", - "gold_quality": "Gold on our {quality_scale}", - "platinum_quality": "Platinum on our {quality_scale}", - "quality_scale": "quality scale" + "bronze_quality": "Bronze quality", + "silver_quality": "Silver quality", + "gold_quality": "Gold quality", + "platinum_quality": "Platinum quality" }, "config_flow": { "success": "Success", diff --git a/src/types.ts b/src/types.ts index 67d5f7583805..3d8a78ac50c7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import type { DurationFormatConstructor } from "@formatjs/intl-durationformat/src/types"; import type { Auth, Connection, @@ -22,7 +23,7 @@ import type { Themes } from "./data/ws-themes"; import type { ExternalMessaging } from "./external_app/external_messaging"; declare global { - /* eslint-disable no-var, no-redeclare */ + /* eslint-disable no-var */ var __DEV__: boolean; var __DEMO__: boolean; var __BUILD__: "modern" | "legacy"; @@ -30,7 +31,7 @@ declare global { var __STATIC_PATH__: string; var __BACKWARDS_COMPAT__: boolean; var __SUPERVISOR__: boolean; - /* eslint-enable no-var, no-redeclare */ + /* eslint-enable no-var */ interface Window { // Custom panel entry point url @@ -64,6 +65,12 @@ declare global { interface ImportMeta { url: string; } + + // Intl.DurationFormat is not yet part of the TypeScript standard + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Intl { + const DurationFormat: DurationFormatConstructor; + } } export interface ValueChangedEvent extends CustomEvent { diff --git a/test/common/datetime/format_duration.test.ts b/test/common/datetime/format_duration.test.ts index a713dee22ca6..cd62785320e4 100644 --- a/test/common/datetime/format_duration.test.ts +++ b/test/common/datetime/format_duration.test.ts @@ -1,5 +1,5 @@ +import "@formatjs/intl-durationformat/polyfill-force"; import { assert, describe, it } from "vitest"; - import { formatDuration } from "../../../src/common/datetime/format_duration"; import type { FrontendLocaleData } from "../../../src/data/translation"; import { @@ -21,14 +21,6 @@ const LOCALE: FrontendLocaleData = { describe("formatDuration", () => { it("works", () => { - assert.strictEqual(formatDuration(LOCALE, "0", "ms"), "0ms"); - assert.strictEqual(formatDuration(LOCALE, "1", "ms"), "1ms"); - assert.strictEqual(formatDuration(LOCALE, "10", "ms"), "10ms"); - assert.strictEqual(formatDuration(LOCALE, "100", "ms"), "100ms"); - assert.strictEqual(formatDuration(LOCALE, "1000", "ms"), "1,000ms"); - assert.strictEqual(formatDuration(LOCALE, "1001", "ms"), "1,001ms"); - assert.strictEqual(formatDuration(LOCALE, "65000", "ms"), "65,000ms"); - assert.strictEqual(formatDuration(LOCALE, "0.5", "s"), "0s 500ms"); assert.strictEqual(formatDuration(LOCALE, "1", "s"), "1s"); assert.strictEqual(formatDuration(LOCALE, "1.1", "s"), "1s 100ms");