diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index 49ace8720d56..5a662e9efab0 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -928,6 +928,10 @@ export default { skipToMenu: 'Spring til menu', skipToContent: 'Spring til indhold', newVersionAvailable: 'Ny version tilgængelig', + duration: (duration: string, date: Date | string, now: Date | string) => { + if (new Date(date).getTime() < new Date(now).getTime()) return `for ${duration} siden`; + return `om ${duration}`; + }, }, colors: { blue: 'Blå', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 8fb0ba271a4d..80a705159804 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -965,6 +965,10 @@ export default { revert: 'Revert', validate: 'Validate', newVersionAvailable: 'New version available', + duration: (duration: string, date: Date | string, now: Date | string) => { + if (new Date(date).getTime() < new Date(now).getTime()) return `${duration} ago`; + return `in ${duration}`; + }, }, colors: { blue: 'Blue', diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts index 26777ff0e085..24c2a0d331a4 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts @@ -282,6 +282,61 @@ describe('UmbLocalizeController', () => { }); }); + describe('duration', () => { + it('should return a duration', () => { + const now = new Date('2020-01-01T00:00:00'); + const inTwoDays = new Date(now.getTime()); + inTwoDays.setDate(inTwoDays.getDate() + 2); + inTwoDays.setHours(11, 30, 5); + + expect(controller.duration(inTwoDays, now)).to.equal('2 days, 11 hours, 30 minutes, 5 seconds'); + }); + + it('should return a date in seconds if the date is less than a minute away', () => { + const now = new Date(); + const inTenSeconds = new Date(now.getTime() + 10000); + + expect(controller.duration(inTenSeconds, now)).to.equal('10 seconds'); + }); + + it('should compare between two dates', () => { + const twoDaysAgo = new Date(); + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); + const inTwoDays = new Date(); + inTwoDays.setDate(inTwoDays.getDate() + 2); + + expect(controller.duration(inTwoDays, twoDaysAgo)).to.equal('4 days'); + }); + + it('should return a negative duration', () => { + expect(controller.duration('2020-01-01', '2019-12-30')).to.equal('2 days'); + }); + + it('should update the relative compounded time when the language changes', async () => { + const now = new Date(); + const inTwoDays = new Date(); + inTwoDays.setDate(inTwoDays.getDate() + 2); + + expect(controller.duration(inTwoDays, now)).to.equal('2 days'); + + // Switch browser to Danish + document.documentElement.lang = danishRegional.$code; + await aTimeout(0); + + expect(controller.duration(inTwoDays, now)).to.equal('2 dage'); + }); + }); + + describe('list format', () => { + it('should return a list with conjunction', () => { + expect(controller.list(['one', 'two', 'three'], { type: 'conjunction' })).to.equal('one, two, and three'); + }); + + it('should return a list with disjunction', () => { + expect(controller.list(['one', 'two', 'three'], { type: 'disjunction' })).to.equal('one, two, or three'); + }); + }); + describe('string', () => { it('should replace words prefixed with a # with translated value', async () => { const str = '#close'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts index aeaead141c47..cb9e3927a5ae 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts @@ -182,6 +182,7 @@ export class UmbLocalizationController} values - the values to format. + * @param {Intl.ListFormatOptions} options - the options to use when formatting the list. + * @returns {string} - the formatted list. + */ + list(values: Iterable, options?: Intl.ListFormatOptions): string { + return new Intl.ListFormat(this.lang(), options).format(values); + } + // TODO: for V.16 we should set type to be string | undefined. [NL] /** * Translates a string containing one or more terms. The terms should be prefixed with a `#` character. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.test.ts new file mode 100644 index 000000000000..5ea7c3cb0195 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.test.ts @@ -0,0 +1,60 @@ +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLocalizeDateElement } from './localize-date.element.js'; +import { aTimeout, expect, fixture, html } from '@open-wc/testing'; + +const english = { + type: 'localization', + alias: 'test.en', + name: 'Test English', + meta: { + culture: 'en', + localizations: { + general: { + duration: () => { + return '2 years ago'; // This is a simplified version of the actual implementation + }, + }, + }, + }, +}; + +describe('umb-localize-date', () => { + let date: Date; + let element: UmbLocalizeDateElement; + + beforeEach(async () => { + date = new Date('2020-01-01T00:00:00'); + element = await fixture(html`Fallback value`); + }); + + it('should be defined', () => { + expect(element).to.be.instanceOf(UmbLocalizeDateElement); + }); + + describe('localization', () => { + umbExtensionsRegistry.register(english); + + it('should localize a date', () => { + expect(element.shadowRoot?.textContent).to.equal('1/1/2020'); + }); + + it('should localize a date with options', async () => { + element.options = { dateStyle: 'full' }; + await element.updateComplete; + + expect(element.shadowRoot?.textContent).to.equal('Wednesday, January 1, 2020'); + }); + + it('should set a title', async () => { + await aTimeout(0); + expect(element.title).to.equal('2 years ago'); + }); + + it('should not set a title', async () => { + element.skipDuration = true; + element.title = 'Another title'; + await element.updateComplete; + expect(element.title).to.equal('Another title'); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.ts index a4024067a6ec..f47999888cbf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.ts @@ -1,4 +1,4 @@ -import { css, customElement, html, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; /** @@ -24,13 +24,35 @@ export class UmbLocalizeDateElement extends UmbLitElement { @property({ type: Object }) options?: Intl.DateTimeFormatOptions; - @state() - protected get text(): string { - return this.localize.date(this.date!, this.options); + /** + * Do not show the duration in the title. + */ + @property({ type: Boolean }) + skipDuration = false; + + override updated() { + this.#setTitle(); } override render() { - return this.date ? html`${unsafeHTML(this.text)}` : html``; + return this.date ? this.localize.date(this.date, this.options) : nothing; + } + + #setTitle() { + if (this.skipDuration) { + return; + } + + let title = ''; + + if (this.date) { + const now = new Date(); + const d = new Date(this.date); + const duration = this.localize.duration(d, now); + title = this.localize.term('general_duration', duration, d, now); + } + + this.title = title; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts index d05ddeb0c7d6..093f43c07af9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts @@ -13,7 +13,7 @@ export class UmbLocalizeRelativeTimeElement extends UmbLitElement { * @attr * @example time=10 */ - @property() + @property({ type: Number }) time!: number; /** @@ -21,7 +21,7 @@ export class UmbLocalizeRelativeTimeElement extends UmbLitElement { * @attr * @example options={ dateStyle: 'full', timeStyle: 'long', timeZone: 'Australia/Sydney' } */ - @property() + @property({ type: Object }) options?: Intl.RelativeTimeFormatOptions; /**