I18n: Use locale-aware date formatting in grafana-data (#108415)

* use intl locale-aware date formatting

* format range for time picker button

* comment

* fix test setup
This commit is contained in:
Josh Hunt
2025-07-23 09:23:16 +01:00
committed by GitHub
parent b2688dd5ff
commit ba2804bcfe
8 changed files with 453 additions and 44 deletions

View File

@ -1,5 +1,90 @@
import { initRegionalFormatForTests } from '@grafana/i18n';
import * as featureToggles from '../utils/featureToggles';
import { dateTimeFormat, dateTimeFormatTimeAgo, dateTimeFormatWithAbbrevation, timeZoneAbbrevation } from './formatter';
// Default time zone ("browser") is set to Pacific/Easter in jest.config.js
const referenceDate = '2020-04-17T12:36:15.779Z';
describe('dateTimeFormat (regionalFormatPreference)', () => {
let mockGetFeatureToggle: jest.SpyInstance;
beforeAll(() => {
initRegionalFormatForTests('en-AU');
mockGetFeatureToggle = jest.spyOn(featureToggles, 'getFeatureToggle').mockImplementation((featureName) => {
return featureName === 'localeFormatPreference';
});
});
afterAll(() => {
mockGetFeatureToggle.mockRestore();
});
it('formats dates in the browsers timezone by default', () => {
expect(dateTimeFormat(referenceDate)).toBe('17/04/2020, 6:36:15 am');
});
it('formats dates in the browsers timezone by default when invalid time zone is set', () => {
const options = { timeZone: 'asdf123' };
expect(dateTimeFormat(referenceDate, options)).toBe('17/04/2020, 6:36:15 am');
});
it.each([
['UTC', '17/04/2020, 12:36:15 pm'],
['Europe/Stockholm', '17/04/2020, 2:36:15 pm'],
['Australia/Perth', '17/04/2020, 8:36:15 pm'],
['Asia/Yakutsk', '17/04/2020, 9:36:15 pm'],
['America/Panama', '17/04/2020, 7:36:15 am'],
['America/Los_Angeles', '17/04/2020, 5:36:15 am'],
['Africa/Djibouti', '17/04/2020, 3:36:15 pm'],
['Europe/London', '17/04/2020, 1:36:15 pm'],
['Europe/Berlin', '17/04/2020, 2:36:15 pm'],
['Europe/Moscow', '17/04/2020, 3:36:15 pm'],
['Europe/Madrid', '17/04/2020, 2:36:15 pm'],
['America/New_York', '17/04/2020, 8:36:15 am'],
['America/Chicago', '17/04/2020, 7:36:15 am'],
['America/Denver', '17/04/2020, 6:36:15 am'],
])('formats with the supplied time zone %s', (timeZone, expected) => {
expect(dateTimeFormat(referenceDate, { timeZone })).toBe(expected);
});
it('does not include seconds in the output if the time does not', () => {
expect(dateTimeFormat('2020-04-17T12:36:00.000Z')).toBe('17/04/2020, 6:36 am');
});
it("includes milliseconds in the output when 'defaultWithMS' is set to true", () => {
expect(dateTimeFormat('2020-04-17T12:36:15.123Z', { defaultWithMS: true })).toBe('17/04/2020, 6:36:15.123 am');
});
it.each([
['en-GB', '17/04/2020, 06:36:15'],
['en-US', '4/17/2020, 6:36:15 AM'],
['fr-FR', '17/04/2020 06:36:15'],
['es-ES', '17/4/2020, 6:36:15'],
['de-DE', '17.4.2020, 06:36:15'],
['pt-BR', '17/04/2020, 06:36:15'],
['zh-Hans', '2020/4/17 06:36:15'],
['it-IT', '17/04/2020, 06:36:15'],
['ja-JP', '2020/4/17 6:36:15'],
['id-ID', '17/4/2020, 06.36.15'],
['ko-KR', '2020. 4. 17. 오전 6:36:15'],
['ru-RU', '17.04.2020, 06:36:15'],
['cs-CZ', '17. 4. 2020 6:36:15'],
['nl-NL', '17-4-2020, 06:36:15'],
['hu-HU', '2020. 04. 17. 6:36:15'],
['pt-PT', '17/04/2020, 06:36:15'],
['pl-PL', '17.04.2020, 06:36:15'],
['sv-SE', '2020-04-17 06:36:15'],
['tr-TR', '17.04.2020 06:36:15'],
['zh-Hant', '2020/4/17 上午6:36:15'],
])('with locale %s', (locale, expected) => {
initRegionalFormatForTests(locale);
expect(dateTimeFormat(referenceDate)).toBe(expected);
});
});
describe('dateTimeFormat', () => {
describe('when no time zone have been set', () => {
const browserTime = dateTimeFormat(1587126975779, { timeZone: 'browser' });
@ -81,7 +166,7 @@ describe('dateTimeFormat', () => {
});
});
describe('DateTimeFormatISO', () => {
describe('with custom ISO format', () => {
it('should format according to ISO standard', () => {
const options = { timeZone: 'Europe/Stockholm', format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' };
expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17T14:36:15.779+02:00');
@ -95,39 +180,41 @@ describe('dateTimeFormat', () => {
expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17T14:36:15.779+02:00');
});
});
});
describe('dateTimeFormatTimeAgo', () => {
it('should return the correct format for years ago', () => {
const options = { timeZone: 'Europe/Stockholm' };
expect(dateTimeFormatTimeAgo(1587126975779, options)).toContain('years ago');
});
});
describe('dateTimeFormatWithAbbreviation', () => {
it('should return the correct format with zone abbreviation', () => {
const options = { timeZone: 'Europe/Stockholm' };
expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 14:36:15 CEST');
});
it('should return the correct format with zone abbreviation', () => {
const options = { timeZone: 'America/New_York' };
expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 08:36:15 EDT');
});
it('should return the correct format with zone abbreviation', () => {
const options = { timeZone: 'Europe/Bucharest' };
expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 15:36:15 EEST');
});
});
describe('timeZoneAbbrevation', () => {
it('should return the correct abbreviation', () => {
const options = { timeZone: 'Europe/Stockholm' };
expect(timeZoneAbbrevation(1587126975779, options)).toBe('CEST');
});
it('should return the correct abbreviation', () => {
const options = { timeZone: 'America/New_York' };
expect(timeZoneAbbrevation(1587126975779, options)).toBe('EDT');
});
it('should return the correct abbreviation', () => {
const options = { timeZone: 'Europe/Bucharest' };
expect(timeZoneAbbrevation(1587126975779, options)).toBe('EEST');
});
describe('dateTimeFormatTimeAgo', () => {
it('should return the correct format for years ago', () => {
const options = { timeZone: 'Europe/Stockholm' };
expect(dateTimeFormatTimeAgo(1587126975779, options)).toContain('years ago');
});
});
describe('dateTimeFormatWithAbbreviation', () => {
it('should return the correct format with zone abbreviation', () => {
const options = { timeZone: 'Europe/Stockholm' };
expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 14:36:15 CEST');
});
it('should return the correct format with zone abbreviation', () => {
const options = { timeZone: 'America/New_York' };
expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 08:36:15 EDT');
});
it('should return the correct format with zone abbreviation', () => {
const options = { timeZone: 'Europe/Bucharest' };
expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 15:36:15 EEST');
});
});
describe('timeZoneAbbrevation', () => {
it('should return the correct abbreviation', () => {
const options = { timeZone: 'Europe/Stockholm' };
expect(timeZoneAbbrevation(1587126975779, options)).toBe('CEST');
});
it('should return the correct abbreviation', () => {
const options = { timeZone: 'America/New_York' };
expect(timeZoneAbbrevation(1587126975779, options)).toBe('EDT');
});
it('should return the correct abbreviation', () => {
const options = { timeZone: 'Europe/Bucharest' };
expect(timeZoneAbbrevation(1587126975779, options)).toBe('EEST');
});
});

View File

@ -1,12 +1,77 @@
/* eslint-disable id-blacklist, no-restricted-imports */
import moment, { Moment } from 'moment-timezone';
import { formatDate } from '@grafana/i18n';
import { TimeZone } from '../types/time';
import { getFeatureToggle } from '../utils/featureToggles';
import { DateTimeOptions, getTimeZone } from './common';
import { systemDateFormats } from './formats';
import { DateTimeInput, toUtc, dateTimeAsMoment } from './moment_wrapper';
/**
* Converts a Grafana DateTimeInput to a plain Javascript Date object.
*/
function toDate(dateInUtc: DateTimeInput): Date {
if (dateInUtc instanceof Date) {
return dateInUtc;
}
if (typeof dateInUtc === 'string' || typeof dateInUtc === 'number') {
return new Date(dateInUtc);
}
return dateTimeAsMoment(dateInUtc).toDate();
}
/**
* Converts a Grafana timezone string to an IANA timezone string.
*/
export function toIANATimezone(grafanaTimezone: string) {
// Intl APIs will use the browser's timezone by default (if tz is undefined)
if (grafanaTimezone === 'browser') {
return undefined;
}
const zone = moment.tz.zone(grafanaTimezone);
if (!zone) {
// If the timezone is invalid, we default to the browser's timezone
return undefined;
}
return grafanaTimezone;
}
function getIntlOptions(
date: Date,
options?: DateTimeOptionsWithFormat
): Intl.DateTimeFormatOptions & { timeZone?: string } {
const timeZone = getTimeZone(options);
const intlOptions: Intl.DateTimeFormatOptions = {
year: 'numeric', // ↔ dateStyle: 'short'
month: 'numeric',
day: 'numeric',
hour: 'numeric', // ↔ timeStyle: 'short'
minute: 'numeric',
timeZone: toIANATimezone(timeZone),
};
// If the time has seconds, ensure they're included in the format
const hasSeconds = date.getSeconds() !== 0;
if (hasSeconds) {
intlOptions.second = 'numeric';
}
if (options?.defaultWithMS) {
intlOptions.second = 'numeric';
intlOptions.fractionalSecondDigits = 3; // Include milliseconds
}
return intlOptions;
}
/**
* The type describing the options that can be passed to the {@link dateTimeFormat}
* helper function to control how the date and time value passed to the function is
@ -23,17 +88,30 @@ export interface DateTimeOptionsWithFormat extends DateTimeOptions {
type DateTimeFormatter<T extends DateTimeOptions = DateTimeOptions> = (dateInUtc: DateTimeInput, options?: T) => string;
// NOTE:
// These date formatting functions now just wrap the @grafana/i18n formatting functions
// (which themselves wrap the browserIntl APIs). In the future we may deprecate these
// in favor of using @grafana/i18n directly.
/**
* Helper function to format date and time according to the specified options. If no options
* are supplied, then default values are used. For more details, see {@link DateTimeOptionsWithFormat}.
* Helper function to format date and time according to the specified options.
* If no options are supplied, then the date is formatting according to the user's locale preference.
*
* @param dateInUtc - date in UTC format, e.g. string formatted with UTC offset, UNIX epoch in seconds etc.
* @param options
*
* @public
*/
export const dateTimeFormat: DateTimeFormatter<DateTimeOptionsWithFormat> = (dateInUtc, options?) =>
toTz(dateInUtc, getTimeZone(options)).format(getFormat(options));
export const dateTimeFormat: DateTimeFormatter<DateTimeOptionsWithFormat> = (dateInUtc, options?) => {
// If a custom format is provided (or the toggle isn't enabled), use the previous implementation
if (!getFeatureToggle('localeFormatPreference') || options?.format) {
return toTz(dateInUtc, getTimeZone(options)).format(getFormat(options));
}
const dateAsDate = toDate(dateInUtc);
const intlOptions = getIntlOptions(dateAsDate, options); // TODO - if invalid timezone, use browser timezone
return formatDate(dateAsDate, intlOptions);
};
/**
* Helper function to format date and time according to the standard ISO format e.g. 2013-02-04T22:44:30.652Z.
@ -70,8 +148,18 @@ export const dateTimeFormatTimeAgo: DateTimeFormatter = (dateInUtc, options?) =>
*
* @public
*/
export const dateTimeFormatWithAbbrevation: DateTimeFormatter = (dateInUtc, options?) =>
toTz(dateInUtc, getTimeZone(options)).format(`${systemDateFormats.fullDate} z`);
export const dateTimeFormatWithAbbrevation: DateTimeFormatter = (dateInUtc, options?) => {
// If a custom format is provided (or the toggle isn't enabled), use the previous implementation
if (!getFeatureToggle('localeFormatPreference') || options?.format) {
return toTz(dateInUtc, getTimeZone(options)).format(`${systemDateFormats.fullDate} z`);
}
const dateAsDate = toDate(dateInUtc);
const intlOptions = getIntlOptions(dateAsDate, options);
intlOptions.timeZoneName = 'short';
return formatDate(dateAsDate, intlOptions);
};
/**
* Helper function to return only the time zone abbreviation for a given date and time value. If no options

View File

@ -1,9 +1,13 @@
import { initRegionalFormatForTests } from '@grafana/i18n';
import { RawTimeRange, TimeRange } from '../types/time';
import * as featureToggles from '../utils/featureToggles';
import { dateTime } from './moment_wrapper';
import {
convertRawToRange,
describeInterval,
describeTimeRange,
isRelativeTimeRange,
relativeToTimeRange,
roundInterval,
@ -310,4 +314,180 @@ describe('Range Utils', () => {
expect(relativeTimeRange.to).toEqual(604800);
});
});
describe('describeTimeRange', () => {
it.each([
['now-5m', 'now', 'Last 5 minutes'],
['now-15m', 'now', 'Last 15 minutes'],
['now-1h', 'now', 'Last 1 hour'],
['now-24h', 'now', 'Last 24 hours'],
['now-7d', 'now', 'Last 7 days'],
['now-30d', 'now', 'Last 30 days'],
['now/d', 'now/d', 'Today'],
['now/d', 'now', 'Today so far'],
['now/w', 'now/w', 'This week'],
['now/M', 'now/M', 'This month'],
['now/y', 'now/y', 'This year'],
['now-1d/d', 'now-1d/d', 'Yesterday'],
['now-1w/w', 'now-1w/w', 'Previous week'],
['now-1M/M', 'now-1M/M', 'Previous month'],
['now-1y/y', 'now-1y/y', 'Previous year'],
['now/fQ', 'now/fQ', 'This fiscal quarter'],
['now/fy', 'now/fy', 'This fiscal year'],
['now-1Q/fQ', 'now-1Q/fQ', 'Previous fiscal quarter'],
['now-1y/fy', 'now-1y/fy', 'Previous fiscal year'],
])('should return display name "%s" for range from "%s" to "%s"', (from, to, expected) => {
expect(describeTimeRange({ from, to })).toBe(expected);
});
it('should prioritize custom quick ranges over standard ranges', () => {
const customRanges = [{ from: 'now-5m', to: 'now', display: 'Lightning round!' }];
expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, customRanges)).toBe('Lightning round!');
});
it('should format with the default timezone', () => {
// Tests default to Pacific/Easter
const from = dateTime('2023-01-15T10:30:00Z');
const to = dateTime('2023-01-15T14:45:00Z');
const result = describeTimeRange({ from, to });
expect(result).toBe('2023-01-15 05:30:00 to 2023-01-15 09:45:00');
});
it('should respect timezone in datetime formatting', () => {
const from = dateTime('2023-01-15T10:30:00Z');
const to = dateTime('2023-01-15T14:45:00Z');
const result = describeTimeRange({ from, to }, 'Australia/Sydney');
expect(result).toBe('2023-01-15 21:30:00 to 2023-01-16 01:45:00');
});
it('should handle absolute from, relative to', () => {
const from = dateTime('2023-01-15T10:30:00Z');
const to = 'now';
const result = describeTimeRange({ from, to });
expect(result).toBe('2023-01-15 05:30:00 to a few seconds ago');
});
it('should handle relative from, absolute to', () => {
const from = 'now-1h';
const to = dateTime('2023-01-15T14:45:00Z');
const result = describeTimeRange({ from, to });
expect(result).toBe('an hour ago to 2023-01-15 09:45:00');
});
it('should handle invalid relative expressions', () => {
const from = dateTime('2023-01-15T10:30:00Z');
const to = 'invalid-expression';
const result = describeTimeRange({ from, to });
expect(result).toContain('Invalid date');
});
it('should handle empty custom ranges array', () => {
expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, [])).toBe('Last 5 minutes');
});
it('should handle null custom ranges', () => {
expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, undefined)).toBe('Last 5 minutes');
});
});
describe('describeTimeRange - localeFormatPreference enabled', () => {
let mockGetFeatureToggle: jest.SpyInstance;
beforeAll(() => {
initRegionalFormatForTests('en-AU');
mockGetFeatureToggle = jest.spyOn(featureToggles, 'getFeatureToggle').mockImplementation((featureName) => {
return featureName === 'localeFormatPreference';
});
});
afterAll(() => {
mockGetFeatureToggle.mockRestore();
});
it.each([
['now-5m', 'now', 'Last 5 minutes'],
['now-15m', 'now', 'Last 15 minutes'],
['now-1h', 'now', 'Last 1 hour'],
['now-7d', 'now', 'Last 7 days'],
['now/d', 'now/d', 'Today'],
['now/d', 'now', 'Today so far'],
['now/w', 'now/w', 'This week'],
['now-1d/d', 'now-1d/d', 'Yesterday'],
['now-1w/w', 'now-1w/w', 'Previous week'],
['now-1y/y', 'now-1y/y', 'Previous year'],
['now/fQ', 'now/fQ', 'This fiscal quarter'],
['now-1Q/fQ', 'now-1Q/fQ', 'Previous fiscal quarter'],
])('should return display name "%s" for range from "%s" to "%s"', (from, to, expected) => {
expect(describeTimeRange({ from, to })).toBe(expected);
});
it('should prioritize custom quick ranges over standard ranges', () => {
const customRanges = [{ from: 'now-5m', to: 'now', display: 'Lightning round!' }];
expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, customRanges)).toBe('Lightning round!');
});
it('should format with the default timezone', () => {
// Tests default to Pacific/Easter
const from = dateTime('2023-01-15T10:30:00Z');
const to = dateTime('2023-01-15T14:45:00Z');
const result = describeTimeRange({ from, to });
expect(result).toBe('15/1/23, 5:309:45am');
});
it('should respect timezone in datetime formatting', () => {
const from = dateTime('2023-01-15T10:30:00Z');
const to = dateTime('2023-01-15T14:45:00Z');
const result = describeTimeRange({ from, to }, 'Australia/Sydney');
expect(result).toBe('15/1/23, 9:30pm16/1/23, 1:45am');
});
it('should include seconds in the output if the time has seconds', () => {
const from = dateTime('2023-01-15T10:30:00Z');
const to = dateTime('2023-01-15T14:45:12Z');
const result = describeTimeRange({ from, to });
expect(result).toBe('15/1/23, 5:30:00am9:45:12am');
});
it('should handle absolute from, relative to', () => {
const from = dateTime('2023-01-15T10:30:00Z');
const to = 'now';
const result = describeTimeRange({ from, to });
expect(result).toBe('15/01/2023, 5:30 am to a few seconds ago');
});
it('should handle relative from, absolute to', () => {
const from = 'now-1h';
const to = dateTime('2023-01-15T14:45:00Z');
const result = describeTimeRange({ from, to });
expect(result).toBe('an hour ago to 15/01/2023, 9:45 am');
});
it('should handle invalid relative expressions', () => {
const from = dateTime('2023-01-15T10:30:00Z');
const to = 'invalid-expression';
const result = describeTimeRange({ from, to });
expect(result).toContain('Invalid date');
});
it('should handle empty custom ranges array', () => {
expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, [])).toBe('Last 5 minutes');
});
it('should handle null custom ranges', () => {
expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, undefined)).toBe('Last 5 minutes');
});
});
});

View File

@ -1,7 +1,10 @@
import { formatDateRange } from '@grafana/i18n';
import { RawTimeRange, TimeRange, TimeZone, IntervalValues, RelativeTimeRange, TimeOption } from '../types/time';
import { getFeatureToggle } from '../utils/featureToggles';
import * as dateMath from './datemath';
import { timeZoneAbbrevation, dateTimeFormat, dateTimeFormatTimeAgo } from './formatter';
import { timeZoneAbbrevation, dateTimeFormat, dateTimeFormatTimeAgo, toIANATimezone } from './formatter';
import { isDateTime, DateTime, dateTime } from './moment_wrapper';
import { dateTimeParse } from './parser';
@ -134,6 +137,17 @@ export function describeTextRange(expr: string): TimeOption {
return opt;
}
// TODO: Should we keep these format presets somewhere common?
const rangeFormatShort: Intl.DateTimeFormatOptions = {
dateStyle: 'short',
timeStyle: 'short',
};
const rangeFormatFull: Intl.DateTimeFormatOptions = {
dateStyle: 'short',
timeStyle: 'medium',
};
/**
* Use this function to get a properly formatted string representation of a {@link @grafana/data:RawTimeRange | range}.
*
@ -154,9 +168,25 @@ export function describeTimeRange(range: RawTimeRange, timeZone?: TimeZone, quic
const options = { timeZone };
if (isDateTime(range.from) && isDateTime(range.to)) {
return dateTimeFormat(range.from, options) + ' to ' + dateTimeFormat(range.to, options);
const fromDate = range.from.toDate();
const toDate = range.to.toDate();
if (!getFeatureToggle('localeFormatPreference')) {
return dateTimeFormat(range.from, options) + ' to ' + dateTimeFormat(range.to, options);
}
const hasSeconds = fromDate.getSeconds() !== 0 || toDate.getSeconds() !== 0;
const intlFormat = hasSeconds ? rangeFormatFull : rangeFormatShort;
const intlFormatOptions = {
...intlFormat,
timeZone: timeZone ? toIANATimezone(timeZone) : undefined,
};
return formatDateRange(fromDate, toDate, intlFormatOptions);
}
// TODO: We could update these to all use Intl APIs.
// Could we use formatRangeToParts and replace the 'other side' with the ago formatting?
if (isDateTime(range.from)) {
const parsed = dateMath.parse(range.to, true, 'utc');
return parsed ? dateTimeFormat(range.from, options) + ' to ' + dateTimeFormatTimeAgo(parsed, options) : '';

View File

@ -0,0 +1,13 @@
import { FeatureToggles } from '../types/featureToggles.gen';
type FeatureToggleName = keyof FeatureToggles;
/**
* Check a featureToggle
* @param featureName featureToggle name
* @param def default value if featureToggles aren't defined, false if not provided
* @returns featureToggle value or def.
*/
export function getFeatureToggle(featureName: FeatureToggleName, def = false) {
return window.grafanaBootData?.settings.featureToggles[featureName] ?? def;
}

View File

@ -1,8 +1,13 @@
import deepEqual from 'fast-deep-equal';
import memoize from 'micro-memoize';
import memoize, { AnyFn, Memoized } from 'micro-memoize';
const deepMemoize: typeof memoize = (fn) => memoize(fn, { isEqual: deepEqual });
function clearMemoizedCache(fn: Memoized<AnyFn>) {
fn.cache.keys.length = 0;
fn.cache.values.length = 0;
}
let regionalFormat: string | undefined;
const createDateTimeFormatter = deepMemoize((locale: string | undefined, options: Intl.DateTimeFormatOptions) => {
@ -41,5 +46,10 @@ export const formatDateRange = (
};
export const initRegionalFormat = (regionalFormatArg: string) => {
// We don't expect this to be called with a different locale during the lifetime of the app,
// so this is mostly here so we can change it during tests and clear out previously memoized values.
clearMemoizedCache(formatDate);
clearMemoizedCache(formatDuration);
regionalFormat = regionalFormatArg;
};

View File

@ -24,4 +24,4 @@ export {
} from './constants';
export { initPluginTranslations, t, Trans } from './i18n';
export type { ResourceLoader, Resources, TFunction, TransProps } from './types';
export { formatDate, formatDuration, formatDateRange } from './dates';
export { formatDate, formatDuration, formatDateRange, initRegionalFormat as initRegionalFormatForTests } from './dates';

View File

@ -7,6 +7,7 @@ export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): T
return rangeUtil.convertRawToRange({ from: option.from, to: option.to }, timeZone, undefined, commonFormat);
};
// TODO: Should we keep these format presets somewhere common?
const rangeFormatShort: Intl.DateTimeFormatOptions = {
dateStyle: 'short',
timeStyle: 'short',