fix(library): honor browser clock for cron schedules

Use the browser's hour cycle when its language matches the selected interface language, preserving regional time preferences.

Closes #1559
This commit is contained in:
neon
2026-09-26 13:41:33 +00:00
parent 68bf37582f
commit 6381f0221f
3 changed files with 44 additions and 2 deletions
@@ -1,8 +1,21 @@
import { describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { isFiveFieldCronExpression } from '@bookorbit/types'
import { parseCronToHuman } from '../cron'
function mockBrowserClock(locale: string, hour12: boolean): void {
const NativeDateTimeFormat = Intl.DateTimeFormat
vi.spyOn(Intl, 'DateTimeFormat').mockImplementation(function dateTimeFormat(requestedLocale, options) {
const formatter = new NativeDateTimeFormat(requestedLocale, options)
if (requestedLocale === undefined) {
vi.spyOn(formatter, 'resolvedOptions').mockReturnValue({ ...formatter.resolvedOptions(), locale, hour12 })
}
return formatter
})
}
afterEach(() => vi.restoreAllMocks())
describe('isFiveFieldCronExpression', () => {
it.each(['0 4 * * *', '*/30 * * * *', '0 0 * * 1', '0 4 * * MON', '0 4 * * *', '1-5/2 * * * *', '0 4 * jan-mar *'])(
'accepts %s and previews it',
@@ -21,6 +34,30 @@ describe('isFiveFieldCronExpression', () => {
})
describe('parseCronToHuman', () => {
it('uses the browser clock for an English regional preference', () => {
mockBrowserClock('en-GB', false)
expect(parseCronToHuman('31 12,0 * * *', 'en')).toBe('At 00:31 and 12:31')
})
it('preserves a 12-hour English browser preference', () => {
mockBrowserClock('en-US', true)
expect(parseCronToHuman('31 12,0 * * *', 'en')).toBe('At 12:31 AM and 12:31 PM')
})
it('uses the selected language convention when it differs from the browser language', () => {
mockBrowserClock('en-US', true)
expect(parseCronToHuman('31 12,0 * * *', 'nl')).toBe('At 00:31 and 12:31')
})
it('preserves the English convention with a Dutch browser language', () => {
mockBrowserClock('nl-BE', false)
expect(parseCronToHuman('31 12,0 * * *', 'en')).toBe('At 12:31 AM and 12:31 PM')
})
it('returns null for null input', () => {
expect(parseCronToHuman(null, 'en')).toBeNull()
})
+5 -1
View File
@@ -3,7 +3,11 @@ import cronstrue from 'cronstrue'
export function parseCronToHuman(cron: string | null | undefined, locale: string): string | null {
if (!cron) return null
try {
const use24HourTimeFormat = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hour12 === false
const browserTimeFormat = new Intl.DateTimeFormat(undefined, { hour: 'numeric' }).resolvedOptions()
const localeTimeFormat = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions()
const browserLanguage = new Intl.Locale(browserTimeFormat.locale).language
const localeLanguage = new Intl.Locale(localeTimeFormat.locale).language
const use24HourTimeFormat = (browserLanguage === localeLanguage ? browserTimeFormat : localeTimeFormat).hour12 === false
return cronstrue.toString(cron, { use24HourTimeFormat, verbose: false })
} catch {
return cron
@@ -29,6 +29,7 @@ describe('locale store', () => {
it('matches exact locales before falling back to a supported base language', async () => {
const { matchSupportedLocale } = await import('../locale')
expect(matchSupportedLocale(['en', 'en-GB', 'en-US'])).toBe('en')
expect(matchSupportedLocale(['nl-NL', 'en-US'])).toBe('nl')
expect(matchSupportedLocale(['de-DE', 'en-GB'])).toBe('de')
expect(matchSupportedLocale(['el-GR', 'en-GB'])).toBe('el')