mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Improve date and time formatting with fallback for unsupported Intl.DateTimeFormat features (#7099)
* Intl.DateTimeFormat fallback * only set timezone when available or in config
This commit is contained in:
		
							parent
							
								
									20415bb5e0
								
							
						
					
					
						commit
						a94346e17e
					
				| @ -43,31 +43,119 @@ interface DateTimeStyle { | ||||
|   strftime_fmt: string; | ||||
| } | ||||
| 
 | ||||
| // only used as a fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
 | ||||
| const formatMap: { | ||||
|   [k: string]: { | ||||
|     date: { year: 'numeric' | '2-digit'; month: 'long' | 'short' | '2-digit'; day: 'numeric' | '2-digit' }; | ||||
|     time: { hour: 'numeric'; minute: 'numeric'; second?: 'numeric'; timeZoneName?: 'short' | 'long' }; | ||||
|   }; | ||||
| } = { | ||||
|   full: { | ||||
|     date: { year: 'numeric', month: 'long', day: 'numeric' }, | ||||
|     time: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'long' }, | ||||
|   }, | ||||
|   long: { | ||||
|     date: { year: 'numeric', month: 'long', day: 'numeric' }, | ||||
|     time: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'long' }, | ||||
|   }, | ||||
|   medium: { | ||||
|     date: { year: 'numeric', month: 'short', day: 'numeric' }, | ||||
|     time: { hour: 'numeric', minute: 'numeric', second: 'numeric' }, | ||||
|   }, | ||||
|   short: { date: { year: '2-digit', month: '2-digit', day: '2-digit' }, time: { hour: 'numeric', minute: 'numeric' } }, | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Attempts to get the system's time zone using Intl.DateTimeFormat. If that fails (for instance, in environments | ||||
|  * where Intl is not fully supported), it calculates the UTC offset for the current system time and returns | ||||
|  * it in a string format. | ||||
|  * | ||||
|  * Keeping the Intl.DateTimeFormat for now, as this is the recommended way to get the time zone. | ||||
|  * https://stackoverflow.com/a/34602679
 | ||||
|  * | ||||
|  * Intl.DateTimeFormat function as of April 2023, works in 95.03% of the browsers used globally | ||||
|  * https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_resolvedoptions_computed_timezone
 | ||||
|  * | ||||
|  * @returns {string} The resolved time zone or a calculated UTC offset. | ||||
|  * The returned string will either be a named time zone (e.g., "America/Los_Angeles"), or it will follow | ||||
|  * the format "UTC±HH:MM". | ||||
|  */ | ||||
| const getResolvedTimeZone = () => { | ||||
|   try { | ||||
|     return Intl.DateTimeFormat().resolvedOptions().timeZone; | ||||
|   } catch (error) { | ||||
|     const offsetMinutes = new Date().getTimezoneOffset(); | ||||
|     return `UTC${offsetMinutes < 0 ? '+' : '-'}${Math.abs(offsetMinutes / 60) | ||||
|       .toString() | ||||
|       .padStart(2, '0')}:${Math.abs(offsetMinutes % 60) | ||||
|       .toString() | ||||
|       .padStart(2, '0')}`;
 | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Formats a Unix timestamp into a human-readable date/time string. | ||||
|  * | ||||
|  * The format of the output string is determined by a configuration object passed as an argument, which | ||||
|  * may specify a time zone, 12- or 24-hour time, and various stylistic options for the date and time. | ||||
|  * If these options are not specified, the function will use system defaults or sensible fallbacks. | ||||
|  * | ||||
|  * The function is robust to environments where the Intl API is not fully supported, and includes a | ||||
|  * fallback method to create a formatted date/time string in such cases. | ||||
|  * | ||||
|  * @param {number} unixTimestamp - The Unix timestamp to be formatted. | ||||
|  * @param {DateTimeStyle} config - User configuration object. | ||||
|  * @returns {string} A formatted date/time string. | ||||
|  * | ||||
|  * @throws {Error} If the given unixTimestamp is not a valid number, the function will return 'Invalid time'. | ||||
|  */ | ||||
| export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: DateTimeStyle): string => { | ||||
|   const { timezone, time_format, date_style, time_style, strftime_fmt } = config; | ||||
|   const locale = window.navigator?.language || 'en-us'; | ||||
| 
 | ||||
|   if (isNaN(unixTimestamp)) { | ||||
|     return 'Invalid time'; | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     const date = new Date(unixTimestamp * 1000); | ||||
|     const resolvedTimeZone = getResolvedTimeZone(); | ||||
| 
 | ||||
|     // use strftime_fmt if defined in config file
 | ||||
|     // use strftime_fmt if defined in config
 | ||||
|     if (strftime_fmt) { | ||||
|       const strftime_locale = strftime.timezone(getUTCOffset(date, timezone || Intl.DateTimeFormat().resolvedOptions().timeZone)).localizeByIdentifier(locale); | ||||
|       const offset = getUTCOffset(date, timezone || resolvedTimeZone); | ||||
|       const strftime_locale = strftime.timezone(offset).localizeByIdentifier(locale); | ||||
|       return strftime_locale(strftime_fmt, date); | ||||
|     } | ||||
| 
 | ||||
|     // else use Intl.DateTimeFormat
 | ||||
|     const formatter = new Intl.DateTimeFormat(locale, { | ||||
|     // DateTime format options
 | ||||
|     const options: Intl.DateTimeFormatOptions = { | ||||
|       dateStyle: date_style, | ||||
|       timeStyle: time_style, | ||||
|       timeZone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, | ||||
|       hour12: time_format !== 'browser' ? time_format == '12hour' : undefined, | ||||
|     }); | ||||
|     return formatter.format(date); | ||||
|     }; | ||||
| 
 | ||||
|     // Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config
 | ||||
|     const isUTCOffsetFormat = /^UTC[+-]\d{2}:\d{2}$/.test(resolvedTimeZone); | ||||
|     if (timezone || !isUTCOffsetFormat) { | ||||
|       options.timeZone = timezone || resolvedTimeZone; | ||||
|     } | ||||
| 
 | ||||
|     const formatter = new Intl.DateTimeFormat(locale, options); | ||||
|     const formattedDateTime = formatter.format(date); | ||||
| 
 | ||||
|     // Regex to check for existence of time. This is needed because dateStyle/timeStyle is not always supported.
 | ||||
|     const containsTime = /\d{1,2}:\d{1,2}/.test(formattedDateTime); | ||||
| 
 | ||||
|     // fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
 | ||||
|     // This works even tough the timezone is undefined, it will use the runtime's default time zone
 | ||||
|     if (!containsTime) { | ||||
|       const dateOptions = { ...formatMap[date_style]?.date, timeZone: options.timeZone, hour12: options.hour12 }; | ||||
|       const timeOptions = { ...formatMap[time_style]?.time, timeZone: options.timeZone, hour12: options.hour12 }; | ||||
| 
 | ||||
|       return `${date.toLocaleDateString(locale, dateOptions)} ${date.toLocaleTimeString(locale, timeOptions)}`; | ||||
|     } | ||||
| 
 | ||||
|     return formattedDateTime; | ||||
|   } catch (error) { | ||||
|     return 'Invalid time'; | ||||
|   } | ||||
| @ -122,10 +210,19 @@ export const getDurationFromTimestamps = (start_time: number, end_time: number | | ||||
|  * @returns number of minutes offset from UTC | ||||
|  */ | ||||
| const getUTCOffset = (date: Date, timezone: string): number => { | ||||
|   const utcDate = new Date(date.getTime() - (date.getTimezoneOffset() * 60 * 1000)); | ||||
|   // If timezone is in UTC±HH:MM format, parse it to get offset
 | ||||
|   const utcOffsetMatch = timezone.match(/^UTC([+-])(\d{2}):(\d{2})$/); | ||||
|   if (utcOffsetMatch) { | ||||
|     const hours = parseInt(utcOffsetMatch[2], 10); | ||||
|     const minutes = parseInt(utcOffsetMatch[3], 10); | ||||
|     return (utcOffsetMatch[1] === '+' ? 1 : -1) * (hours * 60 + minutes); | ||||
|   } | ||||
| 
 | ||||
|   // Otherwise, calculate offset using provided timezone
 | ||||
|   const utcDate = new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000); | ||||
|   // locale of en-CA is required for proper locale format
 | ||||
|   let iso = utcDate.toLocaleString('en-CA', { timeZone: timezone, hour12: false }).replace(', ', 'T'); | ||||
|   iso += `.${utcDate.getMilliseconds().toString().padStart(3, '0')}`; | ||||
|   const target = new Date(`${iso}Z`); | ||||
|   return (target.getTime() - utcDate.getTime()) / 60 / 1000; | ||||
| } | ||||
| }; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user