diff --git a/src/common/datetime/relative_time.ts b/src/common/datetime/relative_time.ts index bf3722a977f2..b28748f90f26 100644 --- a/src/common/datetime/relative_time.ts +++ b/src/common/datetime/relative_time.ts @@ -2,24 +2,40 @@ import memoizeOne from "memoize-one"; import type { FrontendLocaleData } from "../../data/translation"; import { selectUnit } from "../util/select-unit"; +export enum RelativeTimeFormat { + relative = "long", + relative_short = "short", + relative_narrow = "narrow", +} + +export type RelativeTimeStyle = `${RelativeTimeFormat}`; + +export function isRelativeTimeFormat( + format: string +): format is RelativeTimeFormat { + return Object.keys(RelativeTimeFormat).includes(format as RelativeTimeFormat); +} + const formatRelTimeMem = memoizeOne( - (locale: FrontendLocaleData) => - new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" }) + (locale: FrontendLocaleData, style: RelativeTimeStyle) => + new Intl.RelativeTimeFormat(locale.language, { numeric: "auto", style }) ); export const relativeTime = ( from: Date, locale: FrontendLocaleData, to?: Date, + format?: RelativeTimeFormat, includeTense = true ): string => { const diff = selectUnit(from, to, locale); + const style: RelativeTimeStyle = format ? RelativeTimeFormat[format] : "long"; if (includeTense) { - return formatRelTimeMem(locale).format(diff.value, diff.unit); + return formatRelTimeMem(locale, style).format(diff.value, diff.unit); } return Intl.NumberFormat(locale.language, { style: "unit", unit: diff.unit, - unitDisplay: "long", + unitDisplay: style, }).format(Math.abs(diff.value)); }; diff --git a/src/components/ha-relative-time.ts b/src/components/ha-relative-time.ts index cb9cd7d7b16e..77abde4ffb01 100644 --- a/src/components/ha-relative-time.ts +++ b/src/components/ha-relative-time.ts @@ -2,7 +2,10 @@ import { parseISO } from "date-fns"; import type { PropertyValues } from "lit"; import { ReactiveElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { relativeTime } from "../common/datetime/relative_time"; +import { + relativeTime, + type RelativeTimeFormat, +} from "../common/datetime/relative_time"; import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter"; import type { HomeAssistant } from "../types"; @@ -12,6 +15,8 @@ class HaRelativeTime extends ReactiveElement { @property({ attribute: false }) public datetime?: string | Date; + @property({ attribute: false }) public format?: RelativeTimeFormat; + @property({ type: Boolean }) public capitalize = false; private _interval?: number; @@ -65,7 +70,12 @@ class HaRelativeTime extends ReactiveElement { ? parseISO(this.datetime) : this.datetime; - const relTime = relativeTime(date, this.hass.locale); + const relTime = relativeTime( + date, + this.hass.locale, + undefined, + this.format + ); this.innerHTML = this.capitalize ? capitalizeFirstLetter(relTime) : relTime; diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index 7c330ccde18c..bf5f2a015eba 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -74,7 +74,7 @@ class RenderedTimeTracker { renderTime(from: Date, to: Date): void { this.entries.push(html` - ${relativeTime(from, this.hass.locale, to, false)} later + ${relativeTime(from, this.hass.locale, to, undefined, false)} later `); this.lastReportedTime = to; diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts index 908e34d5e05c..56c320a79050 100644 --- a/src/dialogs/more-info/controls/more-info-update.ts +++ b/src/dialogs/more-info/controls/more-info-update.ts @@ -106,6 +106,7 @@ class MoreInfoUpdate extends LitElement { lastAutomaticBackupDate, this.hass.locale, now, + undefined, true ), } diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts index b238ad9767ab..e6be358531b6 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts @@ -155,6 +155,7 @@ class HaBackupOverviewBackups extends LitElement { lastAttemptDate, this.hass.locale, now, + undefined, true ), } @@ -177,6 +178,7 @@ class HaBackupOverviewBackups extends LitElement { new Date(lastUploadedBackup.date), this.hass.locale, now, + undefined, true ), count: Object.keys(lastUploadedBackup.agents) @@ -245,6 +247,7 @@ class HaBackupOverviewBackups extends LitElement { lastAttemptDate, this.hass.locale, now, + undefined, true ), } @@ -264,6 +267,7 @@ class HaBackupOverviewBackups extends LitElement { new Date(lastUploadedBackup.date), this.hass.locale, now, + undefined, true ), count: Object.keys(lastUploadedBackup.agents) @@ -286,6 +290,7 @@ class HaBackupOverviewBackups extends LitElement { new Date(lastBackup.date), this.hass.locale, now, + undefined, true ), count: Object.keys(lastBackup.agents).length, diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index b7d04e1a305b..df4a54856d85 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -184,6 +184,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { .hass=${this.hass} .content=${this._config.state_content} .name=${this._config.name} + .format=${this._config.format} > `; diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts index ef98a7e24fd0..ded513f7141b 100644 --- a/src/panels/lovelace/badges/types.ts +++ b/src/panels/lovelace/badges/types.ts @@ -2,6 +2,7 @@ import type { ActionConfig } from "../../../data/lovelace/config/action"; import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; import type { LegacyStateFilter } from "../common/evaluate-filter"; import type { Condition } from "../common/validate-condition"; +import type { TimestampRenderingFormat } from "../components/types"; import type { EntityFilterEntityConfig } from "../entity-rows/types"; import type { DisplayType } from "./hui-entity-badge"; @@ -42,6 +43,7 @@ export interface EntityBadgeConfig extends LovelaceBadgeConfig { tap_action?: ActionConfig; hold_action?: ActionConfig; double_tap_action?: ActionConfig; + format?: TimestampRenderingFormat; /** * @deprecated use `show_state`, `show_name`, `icon_type` */ diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index a7fe97c773f2..ab5b4897fbc3 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -278,6 +278,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { .hass=${this.hass} .content=${this._config.state_content} .name=${this._config.name} + .format=${this._config.format} > `; diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index ef6a20f31bde..81ce6512faf1 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -537,6 +537,7 @@ export interface TileCardConfig extends LovelaceCardConfig { icon_double_tap_action?: ActionConfig; features?: LovelaceCardFeatureConfig[]; features_position?: "bottom" | "inline"; + format?: TimestampRenderingFormat; } export interface HeadingCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/components/hui-timestamp-display.ts b/src/panels/lovelace/components/hui-timestamp-display.ts index cc7ed22f4753..33e986569eb5 100644 --- a/src/panels/lovelace/components/hui-timestamp-display.ts +++ b/src/panels/lovelace/components/hui-timestamp-display.ts @@ -5,7 +5,11 @@ import { customElement, property, state } from "lit/decorators"; import { formatDate } from "../../../common/datetime/format_date"; import { formatDateTime } from "../../../common/datetime/format_date_time"; import { formatTime } from "../../../common/datetime/format_time"; -import { relativeTime } from "../../../common/datetime/relative_time"; +import { + isRelativeTimeFormat, + RelativeTimeFormat, + relativeTime, +} from "../../../common/datetime/relative_time"; import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter"; import type { FrontendLocaleData } from "../../../data/translation"; import type { HomeAssistant } from "../../../types"; @@ -19,7 +23,7 @@ const FORMATS: Record< datetime: formatDateTime, time: formatTime, }; -const INTERVAL_FORMAT = ["relative", "total"]; +const INTERVAL_FORMAT = [...Object.keys(RelativeTimeFormat), "total"]; @customElement("hui-timestamp-display") class HuiTimestampDisplay extends LitElement { @@ -109,10 +113,15 @@ class HuiTimestampDisplay extends LitElement { private _updateRelative(): void { if (this.ts && this.hass?.localize) { - this._relative = - this._format === "relative" - ? relativeTime(this.ts, this.hass!.locale) - : relativeTime(new Date(), this.hass!.locale, this.ts, false); + this._relative = isRelativeTimeFormat(this._format) + ? relativeTime(this.ts, this.hass!.locale, undefined, this._format) + : relativeTime( + new Date(), + this.hass!.locale, + this.ts, + undefined, + false + ); this._relative = this.capitalize ? capitalizeFirstLetter(this._relative) diff --git a/src/panels/lovelace/components/types.ts b/src/panels/lovelace/components/types.ts index e05443f873eb..abfad26e7a51 100644 --- a/src/panels/lovelace/components/types.ts +++ b/src/panels/lovelace/components/types.ts @@ -9,6 +9,8 @@ export interface ConditionalBaseConfig extends LovelaceCardConfig { export const TIMESTAMP_RENDERING_FORMATS = [ "relative", + "relative_narrow", + "relative_short", "total", "date", "time", diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts index 6732fda5e530..3963ab02de07 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts @@ -33,6 +33,7 @@ import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct"; import { configElementStyle } from "./config-elements-style"; import "./hui-card-features-editor"; +import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types"; const badgeConfigStruct = assign( baseLovelaceBadgeConfig, @@ -49,6 +50,7 @@ const badgeConfigStruct = assign( show_entity_picture: optional(boolean()), tap_action: optional(actionConfigStruct), image: optional(string()), // For old badge config support + format: optional(enums(TIMESTAMP_RENDERING_FORMATS)), }) ); diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts index 7e9bf434fd89..7081607ffdfb 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts @@ -37,6 +37,7 @@ import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import type { EditDetailElementEvent, EditSubElementEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; import "./hui-card-features-editor"; +import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -57,6 +58,7 @@ const cardConfigStruct = assign( icon_double_tap_action: optional(actionConfigStruct), features: optional(array(any())), features_position: optional(enums(["bottom", "inline"])), + format: optional(enums(TIMESTAMP_RENDERING_FORMATS)), }) ); diff --git a/src/state-display/state-display.ts b/src/state-display/state-display.ts index b91411020d03..f2041095b3e9 100644 --- a/src/state-display/state-display.ts +++ b/src/state-display/state-display.ts @@ -11,6 +11,7 @@ import type { UpdateEntity } from "../data/update"; import { computeUpdateStateDisplay } from "../data/update"; import "../panels/lovelace/components/hui-timestamp-display"; import type { HomeAssistant } from "../types"; +import type { TimestampRenderingFormat } from "../panels/lovelace/components/types"; const TIMESTAMP_STATE_DOMAINS = ["button", "input_button", "scene"]; @@ -59,6 +60,8 @@ class StateDisplay extends LitElement { @property({ attribute: false }) public name?: string; + @property({ attribute: false }) public format?: TimestampRenderingFormat; + @property({ type: Boolean, attribute: "dash-unavailable" }) public dashUnavailable?: boolean; @@ -90,7 +93,7 @@ class StateDisplay extends LitElement { `; @@ -133,6 +136,7 @@ class StateDisplay extends LitElement { `; diff --git a/test/common/datetime/relative_time.test.ts b/test/common/datetime/relative_time.test.ts index 031f6350ca2a..4df7eb85724a 100644 --- a/test/common/datetime/relative_time.test.ts +++ b/test/common/datetime/relative_time.test.ts @@ -34,7 +34,10 @@ describe("relativeTime", () => { assert.strictEqual(relativeTime(now, locale, now), "now"); }); it("returns 0 seconds without tense", () => { - assert.strictEqual(relativeTime(now, locale, now, false), "0 seconds"); + assert.strictEqual( + relativeTime(now, locale, now, undefined, false), + "0 seconds" + ); }); }); @@ -52,12 +55,12 @@ describe("relativeTime", () => { it("without tense", () => { assert.strictEqual( - relativeTime(date1, locale, date2, false), + relativeTime(date1, locale, date2, undefined, false), "33 seconds" ); assert.strictEqual( - relativeTime(date2, locale, date1, false), + relativeTime(date2, locale, date1, undefined, false), "33 seconds" ); }); @@ -77,12 +80,12 @@ describe("relativeTime", () => { it("without tense", () => { assert.strictEqual( - relativeTime(date1, locale, date2, false), + relativeTime(date1, locale, date2, undefined, false), "2 minutes" ); assert.strictEqual( - relativeTime(date2, locale, date1, false), + relativeTime(date2, locale, date1, undefined, false), "2 minutes" ); }); @@ -101,9 +104,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "2 hours"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "2 hours" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "2 hours"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "2 hours" + ); }); }); @@ -120,9 +129,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "23 hours"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "23 hours" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "23 hours"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "23 hours" + ); }); }); @@ -139,9 +154,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "1 day"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "1 day" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "1 day"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "1 day" + ); }); }); @@ -158,9 +179,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "2 days"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "2 days" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "2 days"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "2 days" + ); }); }); @@ -178,9 +205,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "5 days"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "5 days" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "5 days"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "5 days" + ); }); }); @@ -201,12 +234,12 @@ describe("relativeTime", () => { it("without tense", () => { assert.strictEqual( - relativeTime(date1, locale_monday, date2, false), + relativeTime(date1, locale_monday, date2, undefined, false), "1 week" ); assert.strictEqual( - relativeTime(date2, locale_monday, date1, false), + relativeTime(date2, locale_monday, date1, undefined, false), "1 week" ); }); @@ -227,9 +260,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "1 week"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "1 week" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "1 week"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "1 week" + ); }); }); @@ -250,12 +289,12 @@ describe("relativeTime", () => { it("without tense", () => { assert.strictEqual( - relativeTime(date1, locale_monday, date2, false), + relativeTime(date1, locale_monday, date2, undefined, false), "5 days" ); assert.strictEqual( - relativeTime(date2, locale_monday, date1, false), + relativeTime(date2, locale_monday, date1, undefined, false), "5 days" ); }); @@ -275,9 +314,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "2 weeks"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "2 weeks" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "2 weeks"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "2 weeks" + ); }); }); @@ -294,9 +339,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "4 weeks"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "4 weeks" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "4 weeks"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "4 weeks" + ); }); }); @@ -313,9 +364,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "1 month"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "1 month" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "1 month"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "1 month" + ); }); }); @@ -333,12 +390,12 @@ describe("relativeTime", () => { it("without tense", () => { assert.strictEqual( - relativeTime(date1, locale, date2, false), + relativeTime(date1, locale, date2, undefined, false), "11 months" ); assert.strictEqual( - relativeTime(date2, locale, date1, false), + relativeTime(date2, locale, date1, undefined, false), "11 months" ); }); @@ -357,9 +414,15 @@ describe("relativeTime", () => { }); it("without tense", () => { - assert.strictEqual(relativeTime(date1, locale, date2, false), "1 year"); + assert.strictEqual( + relativeTime(date1, locale, date2, undefined, false), + "1 year" + ); - assert.strictEqual(relativeTime(date2, locale, date1, false), "1 year"); + assert.strictEqual( + relativeTime(date2, locale, date1, undefined, false), + "1 year" + ); }); }); });