mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
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:
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user