// Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { HourCyclePreference } from '../types/I18N'; import { assertDev } from './assert'; function getOptionsWithPreferences( options: Intl.DateTimeFormatOptions ): Intl.DateTimeFormatOptions { const hourCyclePreference = window.SignalContext.getHourCyclePreference(); if (options.hour12 != null) { return options; } if (hourCyclePreference === HourCyclePreference.Prefer12) { return { ...options, hour12: true }; } if (hourCyclePreference === HourCyclePreference.Prefer24) { return { ...options, hour12: false }; } return options; } /** * Chrome doesn't implement hour12 correctly */ function fixBuggyOptions( locales: Array, options: Intl.DateTimeFormatOptions ): Intl.DateTimeFormatOptions { const resolvedOptions = new Intl.DateTimeFormat( locales, options ).resolvedOptions(); const resolvedLocale = new Intl.Locale(resolvedOptions.locale); let { hourCycle } = resolvedOptions; // Most languages should use either h24 or h12 if (hourCycle === 'h24') { hourCycle = 'h23'; } if (hourCycle === 'h11') { hourCycle = 'h12'; } // Only Japanese should use h11 when using hour12 time if (hourCycle === 'h12' && resolvedLocale.language === 'ja') { hourCycle = 'h11'; } return { ...options, hour12: undefined, hourCycle, }; } function getCacheKey( locales: Array, options: Intl.DateTimeFormatOptions ) { return `${locales.join(',')}:${Object.keys(options) .sort() // eslint-disable-next-line @typescript-eslint/no-explicit-any .map(key => `${key}=${(options as any)[key]}`) .join(',')}`; } const formatterCache = new Map(); export function getDateTimeFormatter( options: Intl.DateTimeFormatOptions ): Intl.DateTimeFormat { const preferredSystemLocales = window.SignalContext.getPreferredSystemLocales(); const localeOverride = window.SignalContext.getLocaleOverride(); const locales = localeOverride != null ? [localeOverride] : preferredSystemLocales; const optionsWithPreferences = getOptionsWithPreferences(options); const cacheKey = getCacheKey(locales, optionsWithPreferences); const cachedFormatter = formatterCache.get(cacheKey); if (cachedFormatter) { return cachedFormatter; } const fixedOptions = fixBuggyOptions(locales, optionsWithPreferences); const formatter = new Intl.DateTimeFormat(locales, fixedOptions); formatterCache.set(cacheKey, formatter); return formatter; } export function formatTimestamp( timestamp: number, options: Intl.DateTimeFormatOptions ): string { const formatter = getDateTimeFormatter(options); try { return formatter.format(timestamp); } catch (err) { assertDev(false, 'invalid timestamp'); return ''; } }