Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/meteor/client/hooks/useTimezoneNameList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { useMemo } from 'react';

const getTimeZoneNames = (): string[] => {
const intl = Intl as typeof Intl & { supportedValuesOf?(key: 'timeZone'): string[] };
return typeof intl.supportedValuesOf === 'function' ? intl.supportedValuesOf('timeZone') : [];
const names = typeof intl.supportedValuesOf === 'function' ? intl.supportedValuesOf('timeZone') : [];
return names.includes('UTC') ? names : ['UTC', ...names];
};

export const useTimezoneNameList = (): string[] => useMemo(() => getTimeZoneNames(), []);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Field, FieldHint, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage';
import { canonicalizeTimezone } from '@rocket.chat/tools';
import type { ReactElement } from 'react';

import { useTimezoneNameList } from '../../../../../hooks/useTimezoneNameList';
Expand Down Expand Up @@ -38,7 +39,7 @@ function SelectTimezoneSettingInput({
<FieldRow>
<Select
id={_id}
value={value}
value={typeof value === 'string' ? canonicalizeTimezone(value) : value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ILivechatBusinessHour, LivechatBusinessHourTypes, Serialized } from '@rocket.chat/core-typings';
import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { canonicalizeTimezone } from '@rocket.chat/tools';
import { Page, PageFooter, PageHeader, PageScrollableContentWithShadow } from '@rocket.chat/ui-client';
import { useToastMessageDispatch, useTranslation, useRouter, useEndpoint } from '@rocket.chat/ui-contexts';
import { useId } from 'react';
Expand All @@ -14,7 +15,7 @@ import { useRemoveBusinessHour } from './useRemoveBusinessHour';

const getInitialData = (businessHourData: Serialized<ILivechatBusinessHour> | undefined) => ({
name: businessHourData?.name || '',
timezoneName: businessHourData?.timezone?.name || 'America/Sao_Paulo',
timezoneName: canonicalizeTimezone(businessHourData?.timezone?.name || 'America/Sao_Paulo'),
daysOpen: (businessHourData?.workHours || defaultWorkHours()).filter(({ open }) => !!open).map(({ day }) => day),
daysTime: (businessHourData?.workHours || defaultWorkHours())
.filter(({ open }) => !!open)
Expand Down
29 changes: 29 additions & 0 deletions packages/tools/src/timezone.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { canonicalizeTimezone } from './timezone';

describe('canonicalizeTimezone', () => {
it('returns the same value for a canonical IANA zone', () => {
expect(canonicalizeTimezone('America/Sao_Paulo')).toBe('America/Sao_Paulo');
});

it('resolves plain UTC to UTC', () => {
expect(canonicalizeTimezone('UTC')).toBe('UTC');
});

it('resolves Etc/UTC and Etc/GMT to UTC', () => {
expect(canonicalizeTimezone('Etc/UTC')).toBe('UTC');
expect(canonicalizeTimezone('Etc/GMT')).toBe('UTC');
});

it('resolves legacy moment aliases to their canonical zone', () => {
expect(canonicalizeTimezone('GMT')).toBe('UTC');
expect(canonicalizeTimezone('Zulu')).toBe('UTC');
expect(canonicalizeTimezone('Universal')).toBe('UTC');
expect(canonicalizeTimezone('US/Pacific')).toBe('America/Los_Angeles');
expect(canonicalizeTimezone('Japan')).toBe('Asia/Tokyo');
});

it('returns the input unchanged when it is not a recognized zone', () => {
const input = 'Not/A_Zone';
expect(canonicalizeTimezone(input)).toBe(input);
});
});
8 changes: 8 additions & 0 deletions packages/tools/src/timezone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ export const guessTimezoneFromOffset = (offset: string | number): string => {
};

export const guessTimezone = (): string => new Intl.DateTimeFormat().resolvedOptions().timeZone;

export const canonicalizeTimezone = (name: string): string => {
try {
return new Intl.DateTimeFormat(undefined, { timeZone: name }).resolvedOptions().timeZone;
} catch {
return name;
}
};
Loading