diff --git a/packages/frontend/@n8n/i18n/src/__tests__/setup.ts b/packages/frontend/@n8n/i18n/src/__tests__/setup.ts new file mode 100644 index 0000000000..f7fd330809 --- /dev/null +++ b/packages/frontend/@n8n/i18n/src/__tests__/setup.ts @@ -0,0 +1,2 @@ +// Avoid tests failing because of difference between local and GitHub actions timezone +process.env.TZ = 'UTC'; diff --git a/packages/frontend/@n8n/i18n/src/index.test.ts b/packages/frontend/@n8n/i18n/src/index.test.ts new file mode 100644 index 0000000000..7503d138d4 --- /dev/null +++ b/packages/frontend/@n8n/i18n/src/index.test.ts @@ -0,0 +1,34 @@ +import { I18nClass } from './index'; + +describe(I18nClass, () => { + describe('displayTimer', () => { + it('should format duration with hours, minutes and seconds', () => { + expect(new I18nClass().displayTimer(0)).toBe('0s'); + expect(new I18nClass().displayTimer(12)).toBe('0s'); + expect(new I18nClass().displayTimer(123)).toBe('0s'); + expect(new I18nClass().displayTimer(1234)).toBe('1s'); + expect(new I18nClass().displayTimer(59000)).toBe('59s'); + expect(new I18nClass().displayTimer(60000)).toBe('1m 0s'); + expect(new I18nClass().displayTimer(600000)).toBe('10m 0s'); + expect(new I18nClass().displayTimer(601234)).toBe('10m 1s'); + expect(new I18nClass().displayTimer(3599999)).toBe('59m 59s'); + expect(new I18nClass().displayTimer(3600000)).toBe('1h 0m 0s'); + expect(new I18nClass().displayTimer(3601234)).toBe('1h 0m 1s'); + expect(new I18nClass().displayTimer(6000000)).toBe('1h 40m 0s'); + expect(new I18nClass().displayTimer(100000000)).toBe('27h 46m 40s'); + }); + + it('should include milliseconds if showMs=true and the time includes sub-seconds', () => { + expect(new I18nClass().displayTimer(0, true)).toBe('0s'); + expect(new I18nClass().displayTimer(12, true)).toBe('12ms'); + expect(new I18nClass().displayTimer(123, true)).toBe('123ms'); + expect(new I18nClass().displayTimer(1012, true)).toBe('1.012s'); + expect(new I18nClass().displayTimer(1120, true)).toBe('1.12s'); + expect(new I18nClass().displayTimer(1234, true)).toBe('1.234s'); + expect(new I18nClass().displayTimer(600000, true)).toBe('10m 0s'); + expect(new I18nClass().displayTimer(601234, true)).toBe('10m 1.234s'); + expect(new I18nClass().displayTimer(3600000, true)).toBe('1h 0m 0s'); + expect(new I18nClass().displayTimer(3601234, true)).toBe('1h 0m 1.234s'); + }); + }); +}); diff --git a/packages/frontend/@n8n/i18n/src/index.ts b/packages/frontend/@n8n/i18n/src/index.ts index 53dfd18b4b..6d87495612 100644 --- a/packages/frontend/@n8n/i18n/src/index.ts +++ b/packages/frontend/@n8n/i18n/src/index.ts @@ -86,23 +86,34 @@ export class I18nClass { } displayTimer(msPassed: number, showMs = false): string { - if (msPassed < 60000) { - if (!showMs) { - return `${Math.floor(msPassed / 1000)}${this.baseText('genericHelpers.secShort')}`; - } - - if (msPassed > 0 && msPassed < 1000) { - return `${msPassed}${this.baseText('genericHelpers.millis')}`; - } - - return `${msPassed / 1000}${this.baseText('genericHelpers.secShort')}`; + if (msPassed > 0 && msPassed < 1000 && showMs) { + return `${msPassed}${this.baseText('genericHelpers.millis')}`; } - const secondsPassed = Math.floor(msPassed / 1000); - const minutesPassed = Math.floor(secondsPassed / 60); - const secondsLeft = (secondsPassed - minutesPassed * 60).toString().padStart(2, '0'); + const parts = []; + const second = 1000; + const minute = 60 * second; + const hour = 60 * minute; - return `${minutesPassed}:${secondsLeft}${this.baseText('genericHelpers.minShort')}`; + let remainingMs = msPassed; + + if (remainingMs >= hour) { + parts.push(`${Math.floor(remainingMs / hour)}${this.baseText('genericHelpers.hrsShort')}`); + remainingMs = remainingMs % hour; + } + + if (parts.length > 0 || remainingMs >= minute) { + parts.push(`${Math.floor(remainingMs / minute)}${this.baseText('genericHelpers.minShort')}`); + remainingMs = remainingMs % minute; + } + + if (!showMs) { + remainingMs -= remainingMs % second; + } + + parts.push(`${remainingMs / second}${this.baseText('genericHelpers.secShort')}`); + + return parts.join(' '); } /** @@ -201,8 +212,8 @@ export class I18nClass { * except for `eventTriggerDescription`. */ nodeText(activeNodeType?: string | null) { - const nodeType = activeNodeType ? this.shortNodeType(activeNodeType) : ''; // unused in eventTriggerDescription - const initialKey = `n8n-nodes-base.nodes.${nodeType}.nodeView`; + const shortNodeType = activeNodeType ? this.shortNodeType(activeNodeType) : ''; // unused in eventTriggerDescription + const initialKey = `n8n-nodes-base.nodes.${shortNodeType}.nodeView`; const context = this; return { diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 4fdc00f42f..ddcf360fa8 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -996,6 +996,7 @@ "generic.oauth1Api": "OAuth1 API", "generic.oauth2Api": "OAuth2 API", "genericHelpers.loading": "Loading", + "genericHelpers.hrsShort": "h", "genericHelpers.min": "min", "genericHelpers.minShort": "m", "genericHelpers.sec": "sec",