98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
// 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<string>,
|
|
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<string>,
|
|
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<string, Intl.DateTimeFormat>();
|
|
|
|
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 '';
|
|
}
|
|
}
|