Date formatting per workspace member settings (#6408)

Implement date formatting per workspace member settings

We'll need another round to maybe initialize all workspaces on the
default settings.

For now the default behavior is to take system settings if nothing is
found in DB.

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Lucas Bordeau
2024-07-30 14:52:10 +02:00
committed by GitHub
parent 45ebb0b824
commit ccf4d1eeec
64 changed files with 1176 additions and 165 deletions

View File

@ -0,0 +1,6 @@
export enum DateFormat {
SYSTEM = 'SYSTEM',
MONTH_FIRST = 'MMM d, yyyy', // US
DAY_FIRST = 'd MMM, yyyy', // UK
YEAR_FIRST = 'yyyy MMM d',
}

View File

@ -0,0 +1,600 @@
/**
* Standard IANA time zones.
* @see https://www.iana.org/time-zones
*/
export const IANA_TIME_ZONES = [
'Africa/Abidjan',
'Africa/Accra',
'Africa/Addis_Ababa',
'Africa/Algiers',
'Africa/Asmara',
'Africa/Asmera',
'Africa/Bamako',
'Africa/Bangui',
'Africa/Banjul',
'Africa/Bissau',
'Africa/Blantyre',
'Africa/Brazzaville',
'Africa/Bujumbura',
'Africa/Cairo',
'Africa/Casablanca',
'Africa/Ceuta',
'Africa/Conakry',
'Africa/Dakar',
'Africa/Dar_es_Salaam',
'Africa/Djibouti',
'Africa/Douala',
'Africa/El_Aaiun',
'Africa/Freetown',
'Africa/Gaborone',
'Africa/Harare',
'Africa/Johannesburg',
'Africa/Juba',
'Africa/Kampala',
'Africa/Khartoum',
'Africa/Kigali',
'Africa/Kinshasa',
'Africa/Lagos',
'Africa/Libreville',
'Africa/Lome',
'Africa/Luanda',
'Africa/Lubumbashi',
'Africa/Lusaka',
'Africa/Malabo',
'Africa/Maputo',
'Africa/Maseru',
'Africa/Mbabane',
'Africa/Mogadishu',
'Africa/Monrovia',
'Africa/Nairobi',
'Africa/Ndjamena',
'Africa/Niamey',
'Africa/Nouakchott',
'Africa/Ouagadougou',
'Africa/Porto-Novo',
'Africa/Sao_Tome',
'Africa/Timbuktu',
'Africa/Tripoli',
'Africa/Tunis',
'Africa/Windhoek',
'America/Adak',
'America/Anchorage',
'America/Anguilla',
'America/Antigua',
'America/Araguaina',
'America/Argentina/Buenos_Aires',
'America/Argentina/Catamarca',
'America/Argentina/ComodRivadavia',
'America/Argentina/Cordoba',
'America/Argentina/Jujuy',
'America/Argentina/La_Rioja',
'America/Argentina/Mendoza',
'America/Argentina/Rio_Gallegos',
'America/Argentina/Salta',
'America/Argentina/San_Juan',
'America/Argentina/San_Luis',
'America/Argentina/Tucuman',
'America/Argentina/Ushuaia',
'America/Aruba',
'America/Asuncion',
'America/Atikokan',
'America/Atka',
'America/Bahia',
'America/Bahia_Banderas',
'America/Barbados',
'America/Belem',
'America/Belize',
'America/Blanc-Sablon',
'America/Boa_Vista',
'America/Bogota',
'America/Boise',
'America/Buenos_Aires',
'America/Cambridge_Bay',
'America/Campo_Grande',
'America/Cancun',
'America/Caracas',
'America/Catamarca',
'America/Cayenne',
'America/Cayman',
'America/Chicago',
'America/Chihuahua',
'America/Coral_Harbour',
'America/Cordoba',
'America/Costa_Rica',
'America/Creston',
'America/Cuiaba',
'America/Curacao',
'America/Danmarkshavn',
'America/Dawson',
'America/Dawson_Creek',
'America/Denver',
'America/Detroit',
'America/Dominica',
'America/Edmonton',
'America/Eirunepe',
'America/El_Salvador',
'America/Ensenada',
'America/Fort_Nelson',
'America/Fort_Wayne',
'America/Fortaleza',
'America/Glace_Bay',
'America/Godthab',
'America/Goose_Bay',
'America/Grand_Turk',
'America/Grenada',
'America/Guadeloupe',
'America/Guatemala',
'America/Guayaquil',
'America/Guyana',
'America/Halifax',
'America/Havana',
'America/Hermosillo',
'America/Indiana/Indianapolis',
'America/Indiana/Knox',
'America/Indiana/Marengo',
'America/Indiana/Petersburg',
'America/Indiana/Tell_City',
'America/Indiana/Vevay',
'America/Indiana/Vincennes',
'America/Indiana/Winamac',
'America/Indianapolis',
'America/Inuvik',
'America/Iqaluit',
'America/Jamaica',
'America/Jujuy',
'America/Juneau',
'America/Kentucky/Louisville',
'America/Kentucky/Monticello',
'America/Knox_IN',
'America/Kralendijk',
'America/La_Paz',
'America/Lima',
'America/Los_Angeles',
'America/Louisville',
'America/Lower_Princes',
'America/Maceio',
'America/Managua',
'America/Manaus',
'America/Marigot',
'America/Martinique',
'America/Matamoros',
'America/Mazatlan',
'America/Mendoza',
'America/Menominee',
'America/Merida',
'America/Metlakatla',
'America/Mexico_City',
'America/Miquelon',
'America/Moncton',
'America/Monterrey',
'America/Montevideo',
'America/Montreal',
'America/Montserrat',
'America/Nassau',
'America/New_York',
'America/Nipigon',
'America/Nome',
'America/Noronha',
'America/North_Dakota/Beulah',
'America/North_Dakota/Center',
'America/North_Dakota/New_Salem',
'America/Nuuk',
'America/Ojinaga',
'America/Panama',
'America/Pangnirtung',
'America/Paramaribo',
'America/Phoenix',
'America/Port_of_Spain',
'America/Port-au-Prince',
'America/Porto_Acre',
'America/Porto_Velho',
'America/Puerto_Rico',
'America/Punta_Arenas',
'America/Rainy_River',
'America/Rankin_Inlet',
'America/Recife',
'America/Regina',
'America/Resolute',
'America/Rio_Branco',
'America/Rosario',
'America/Santa_Isabel',
'America/Santarem',
'America/Santiago',
'America/Santo_Domingo',
'America/Sao_Paulo',
'America/Scoresbysund',
'America/Shiprock',
'America/Sitka',
'America/St_Barthelemy',
'America/St_Johns',
'America/St_Kitts',
'America/St_Lucia',
'America/St_Thomas',
'America/St_Vincent',
'America/Swift_Current',
'America/Tegucigalpa',
'America/Thule',
'America/Thunder_Bay',
'America/Tijuana',
'America/Toronto',
'America/Tortola',
'America/Vancouver',
'America/Virgin',
'America/Whitehorse',
'America/Winnipeg',
'America/Yakutat',
'America/Yellowknife',
'Antarctica/Casey',
'Antarctica/Davis',
'Antarctica/DumontDUrville',
'Antarctica/Macquarie',
'Antarctica/Mawson',
'Antarctica/McMurdo',
'Antarctica/Palmer',
'Antarctica/Rothera',
'Antarctica/South_Pole',
'Antarctica/Syowa',
'Antarctica/Troll',
'Antarctica/Vostok',
'Arctic/Longyearbyen',
'Asia/Aden',
'Asia/Almaty',
'Asia/Amman',
'Asia/Anadyr',
'Asia/Aqtau',
'Asia/Aqtobe',
'Asia/Ashgabat',
'Asia/Ashkhabad',
'Asia/Atyrau',
'Asia/Baghdad',
'Asia/Bahrain',
'Asia/Baku',
'Asia/Bangkok',
'Asia/Barnaul',
'Asia/Beirut',
'Asia/Bishkek',
'Asia/Brunei',
'Asia/Calcutta',
'Asia/Chita',
'Asia/Choibalsan',
'Asia/Chongqing',
'Asia/Chungking',
'Asia/Colombo',
'Asia/Dacca',
'Asia/Damascus',
'Asia/Dhaka',
'Asia/Dili',
'Asia/Dubai',
'Asia/Dushanbe',
'Asia/Famagusta',
'Asia/Gaza',
'Asia/Harbin',
'Asia/Hebron',
'Asia/Ho_Chi_Minh',
'Asia/Hong_Kong',
'Asia/Hovd',
'Asia/Irkutsk',
'Asia/Istanbul',
'Asia/Jakarta',
'Asia/Jayapura',
'Asia/Jerusalem',
'Asia/Kabul',
'Asia/Kamchatka',
'Asia/Karachi',
'Asia/Kashgar',
'Asia/Kathmandu',
'Asia/Katmandu',
'Asia/Khandyga',
'Asia/Kolkata',
'Asia/Krasnoyarsk',
'Asia/Kuala_Lumpur',
'Asia/Kuching',
'Asia/Kuwait',
'Asia/Macao',
'Asia/Macau',
'Asia/Magadan',
'Asia/Makassar',
'Asia/Manila',
'Asia/Muscat',
'Asia/Nicosia',
'Asia/Novokuznetsk',
'Asia/Novosibirsk',
'Asia/Omsk',
'Asia/Oral',
'Asia/Phnom_Penh',
'Asia/Pontianak',
'Asia/Pyongyang',
'Asia/Qatar',
'Asia/Qostanay',
'Asia/Qyzylorda',
'Asia/Rangoon',
'Asia/Riyadh',
'Asia/Saigon',
'Asia/Sakhalin',
'Asia/Samarkand',
'Asia/Seoul',
'Asia/Shanghai',
'Asia/Singapore',
'Asia/Srednekolymsk',
'Asia/Taipei',
'Asia/Tashkent',
'Asia/Tbilisi',
'Asia/Tehran',
'Asia/Tel_Aviv',
'Asia/Thimbu',
'Asia/Thimphu',
'Asia/Tokyo',
'Asia/Tomsk',
'Asia/Ujung_Pandang',
'Asia/Ulaanbaatar',
'Asia/Ulan_Bator',
'Asia/Urumqi',
'Asia/Ust-Nera',
'Asia/Vientiane',
'Asia/Vladivostok',
'Asia/Yakutsk',
'Asia/Yangon',
'Asia/Yekaterinburg',
'Asia/Yerevan',
'Atlantic/Azores',
'Atlantic/Bermuda',
'Atlantic/Canary',
'Atlantic/Cape_Verde',
'Atlantic/Faeroe',
'Atlantic/Faroe',
'Atlantic/Jan_Mayen',
'Atlantic/Madeira',
'Atlantic/Reykjavik',
'Atlantic/South_Georgia',
'Atlantic/St_Helena',
'Atlantic/Stanley',
'Australia/ACT',
'Australia/Adelaide',
'Australia/Brisbane',
'Australia/Broken_Hill',
'Australia/Canberra',
'Australia/Currie',
'Australia/Darwin',
'Australia/Eucla',
'Australia/Hobart',
'Australia/LHI',
'Australia/Lindeman',
'Australia/Lord_Howe',
'Australia/Melbourne',
'Australia/North',
'Australia/NSW',
'Australia/Perth',
'Australia/Queensland',
'Australia/South',
'Australia/Sydney',
'Australia/Tasmania',
'Australia/Victoria',
'Australia/West',
'Australia/Yancowinna',
'Brazil/Acre',
'Brazil/DeNoronha',
'Brazil/East',
'Brazil/West',
'Canada/Atlantic',
'Canada/Central',
'Canada/Eastern',
'Canada/Mountain',
'Canada/Newfoundland',
'Canada/Pacific',
'Canada/Saskatchewan',
'Canada/Yukon',
'CET',
'Chile/Continental',
'Chile/EasterIsland',
'CST6CDT',
'Cuba',
'EET',
'Egypt',
'Eire',
'EST',
'EST5EDT',
'Etc/GMT',
'Etc/GMT-0',
'Etc/GMT-1',
'Etc/GMT-10',
'Etc/GMT-11',
'Etc/GMT-12',
'Etc/GMT-13',
'Etc/GMT-14',
'Etc/GMT-2',
'Etc/GMT-3',
'Etc/GMT-4',
'Etc/GMT-5',
'Etc/GMT-6',
'Etc/GMT-7',
'Etc/GMT-8',
'Etc/GMT-9',
'Etc/GMT+0',
'Etc/GMT+1',
'Etc/GMT+10',
'Etc/GMT+11',
'Etc/GMT+12',
'Etc/GMT+2',
'Etc/GMT+3',
'Etc/GMT+4',
'Etc/GMT+5',
'Etc/GMT+6',
'Etc/GMT+7',
'Etc/GMT+8',
'Etc/GMT+9',
'Etc/GMT0',
'Etc/Greenwich',
'Etc/UCT',
'Etc/Universal',
'Etc/UTC',
'Etc/Zulu',
'Europe/Amsterdam',
'Europe/Andorra',
'Europe/Astrakhan',
'Europe/Athens',
'Europe/Belfast',
'Europe/Belgrade',
'Europe/Berlin',
'Europe/Bratislava',
'Europe/Brussels',
'Europe/Bucharest',
'Europe/Budapest',
'Europe/Busingen',
'Europe/Chisinau',
'Europe/Copenhagen',
'Europe/Dublin',
'Europe/Gibraltar',
'Europe/Guernsey',
'Europe/Helsinki',
'Europe/Isle_of_Man',
'Europe/Istanbul',
'Europe/Jersey',
'Europe/Kaliningrad',
'Europe/Kiev',
'Europe/Kirov',
'Europe/Lisbon',
'Europe/Ljubljana',
'Europe/London',
'Europe/Luxembourg',
'Europe/Madrid',
'Europe/Malta',
'Europe/Mariehamn',
'Europe/Minsk',
'Europe/Monaco',
'Europe/Moscow',
'Europe/Nicosia',
'Europe/Oslo',
'Europe/Paris',
'Europe/Podgorica',
'Europe/Prague',
'Europe/Riga',
'Europe/Rome',
'Europe/Samara',
'Europe/San_Marino',
'Europe/Sarajevo',
'Europe/Saratov',
'Europe/Simferopol',
'Europe/Skopje',
'Europe/Sofia',
'Europe/Stockholm',
'Europe/Tallinn',
'Europe/Tirane',
'Europe/Tiraspol',
'Europe/Ulyanovsk',
'Europe/Uzhgorod',
'Europe/Vaduz',
'Europe/Vatican',
'Europe/Vienna',
'Europe/Vilnius',
'Europe/Volgograd',
'Europe/Warsaw',
'Europe/Zagreb',
'Europe/Zaporozhye',
'Europe/Zurich',
'GB',
'GB-Eire',
'GMT',
'GMT-0',
'GMT+0',
'GMT0',
'Greenwich',
'Hongkong',
'HST',
'Iceland',
'Indian/Antananarivo',
'Indian/Chagos',
'Indian/Christmas',
'Indian/Cocos',
'Indian/Comoro',
'Indian/Kerguelen',
'Indian/Mahe',
'Indian/Maldives',
'Indian/Mauritius',
'Indian/Mayotte',
'Indian/Reunion',
'Iran',
'Israel',
'Jamaica',
'Japan',
'Kwajalein',
'Libya',
'MET',
'Mexico/BajaNorte',
'Mexico/BajaSur',
'Mexico/General',
'MST',
'MST7MDT',
'Navajo',
'NZ',
'NZ-CHAT',
'Pacific/Apia',
'Pacific/Auckland',
'Pacific/Bougainville',
'Pacific/Chatham',
'Pacific/Chuuk',
'Pacific/Easter',
'Pacific/Efate',
'Pacific/Enderbury',
'Pacific/Fakaofo',
'Pacific/Fiji',
'Pacific/Funafuti',
'Pacific/Galapagos',
'Pacific/Gambier',
'Pacific/Guadalcanal',
'Pacific/Guam',
'Pacific/Honolulu',
'Pacific/Johnston',
'Pacific/Kanton',
'Pacific/Kiritimati',
'Pacific/Kosrae',
'Pacific/Kwajalein',
'Pacific/Majuro',
'Pacific/Marquesas',
'Pacific/Midway',
'Pacific/Nauru',
'Pacific/Niue',
'Pacific/Norfolk',
'Pacific/Noumea',
'Pacific/Pago_Pago',
'Pacific/Palau',
'Pacific/Pitcairn',
'Pacific/Pohnpei',
'Pacific/Ponape',
'Pacific/Port_Moresby',
'Pacific/Rarotonga',
'Pacific/Saipan',
'Pacific/Samoa',
'Pacific/Tahiti',
'Pacific/Tarawa',
'Pacific/Tongatapu',
'Pacific/Truk',
'Pacific/Wake',
'Pacific/Wallis',
'Pacific/Yap',
'Poland',
'Portugal',
'PRC',
'PST8PDT',
'ROC',
'ROK',
'Singapore',
'Turkey',
'UCT',
'Universal',
'US/Alaska',
'US/Aleutian',
'US/Arizona',
'US/Central',
'US/East-Indiana',
'US/Eastern',
'US/Hawaii',
'US/Indiana-Starke',
'US/Michigan',
'US/Mountain',
'US/Pacific',
'US/Samoa',
'UTC',
'W-SU',
'WET',
'Zulu',
];

View File

@ -0,0 +1,5 @@
export enum TimeFormat {
SYSTEM = 'SYSTEM',
HOUR_24 = 'HH:mm',
HOUR_12 = 'h:mm aa',
}

View File

@ -0,0 +1,17 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { createState } from 'twenty-ui';
export const dateTimeFormatState = createState<{
timeZone: string;
dateFormat: DateFormat;
timeFormat: TimeFormat;
}>({
key: 'dateTimeFormatState',
defaultValue: {
timeZone: detectTimeZone(),
dateFormat: DateFormat.MONTH_FIRST,
timeFormat: TimeFormat['HOUR_24'],
},
});

View File

@ -0,0 +1,69 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
describe('detectDateFormat', () => {
it('should return DateFormat.MONTH_FIRST if the detected format starts with month', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
{ type: 'month', value: '01' },
{ type: 'day', value: '01' },
{ type: 'year', value: '2022' },
],
supportedLocalesOf: () => [],
}) as any;
global.Intl.DateTimeFormat = mockDateTimeFormat;
const result = detectDateFormat();
expect(result).toBe(DateFormat.MONTH_FIRST);
});
it('should return DateFormat.DAY_FIRST if the detected format starts with day', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
{ type: 'day', value: '01' },
{ type: 'month', value: '01' },
{ type: 'year', value: '2022' },
],
}) as any;
global.Intl.DateTimeFormat = mockDateTimeFormat;
const result = detectDateFormat();
expect(result).toBe(DateFormat.DAY_FIRST);
});
it('should return DateFormat.YEAR_FIRST if the detected format starts with year', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
{ type: 'year', value: '2022' },
{ type: 'month', value: '01' },
{ type: 'day', value: '01' },
],
}) as any;
global.Intl.DateTimeFormat = mockDateTimeFormat;
const result = detectDateFormat();
expect(result).toBe(DateFormat.YEAR_FIRST);
});
it('should return DateFormat.MONTH_FIRST by default if the detected format does not match any specific order', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
{ type: 'hour', value: '12' },
{ type: 'minute', value: '00' },
{ type: 'second', value: '00' },
],
}) as any;
global.Intl.DateTimeFormat = mockDateTimeFormat;
const result = detectDateFormat();
expect(result).toBe(DateFormat.MONTH_FIRST);
});
});

View File

@ -0,0 +1,30 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
describe('detectTimeFormat', () => {
it('should return TimeFormat.HOUR_12 if the hour format is 12-hour', () => {
// Mock the resolvedOptions method to return hour12 as true
const mockResolvedOptions = jest.fn(() => ({ hour12: true }));
Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
resolvedOptions: mockResolvedOptions,
})) as any;
const result = detectTimeFormat();
expect(result).toBe(TimeFormat.HOUR_12);
expect(mockResolvedOptions).toHaveBeenCalled();
});
it('should return TimeFormat.HOUR_24 if the hour format is 24-hour', () => {
// Mock the resolvedOptions method to return hour12 as false
const mockResolvedOptions = jest.fn(() => ({ hour12: false }));
Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
resolvedOptions: mockResolvedOptions,
})) as any;
const result = detectTimeFormat();
expect(result).toBe(TimeFormat.HOUR_24);
expect(mockResolvedOptions).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,21 @@
import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel';
describe('formatTimeZoneLabel', () => {
it('should format the time zone label correctly when location is included in the label', () => {
const ianaTimeZone = 'Europe/Paris';
const expectedLabel = '(GMT+02:00) Central European Summer Time - Paris';
const formattedLabel = formatTimeZoneLabel(ianaTimeZone);
expect(formattedLabel).toEqual(expectedLabel);
});
it('should format the time zone label correctly when location is not included in the label', () => {
const ianaTimeZone = 'America/New_York';
const expectedLabel = '(GMT-04:00) Eastern Daylight Time - New York';
const formattedLabel = formatTimeZoneLabel(ianaTimeZone);
expect(formattedLabel).toEqual(expectedLabel);
});
});

View File

@ -0,0 +1,17 @@
import { DateFormat } from '@/localization/constants/DateFormat';
export const detectDateFormat = (): DateFormat => {
const date = new Date();
const formatter = new Intl.DateTimeFormat(navigator.language);
const parts = formatter.formatToParts(date);
const partOrder = parts
.filter((part) => ['year', 'month', 'day'].includes(part.type))
.map((part) => part.type);
if (partOrder[0] === 'month') return DateFormat.MONTH_FIRST;
if (partOrder[0] === 'day') return DateFormat.DAY_FIRST;
if (partOrder[0] === 'year') return DateFormat.YEAR_FIRST;
return DateFormat.MONTH_FIRST;
};

View File

@ -0,0 +1,10 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { isDefined } from '~/utils/isDefined';
export const detectTimeFormat = () => {
const isHour12 = Intl.DateTimeFormat(navigator.language, {
hour: 'numeric',
}).resolvedOptions().hour12;
if (isDefined(isHour12) && isHour12) return TimeFormat.HOUR_12;
return TimeFormat.HOUR_24;
};

View File

@ -0,0 +1,6 @@
/**
* Detects the user's time zone.
* @returns a IANA time zone
*/
export const detectTimeZone = () =>
Intl.DateTimeFormat().resolvedOptions().timeZone;

View File

@ -0,0 +1,10 @@
import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel';
import { AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL } from '@/settings/accounts/constants/AvailableTimezoneOptionsByLabel';
/**
* Finds the matching available IANA time zone select option from a given IANA time zone.
* @param value the IANA time zone to match
* @returns the matching available IANA time zone select option or undefined
*/
export const findAvailableTimeZoneOption = (value: string) =>
AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL[formatTimeZoneLabel(value)];

View File

@ -0,0 +1,15 @@
import { formatInTimeZone } from 'date-fns-tz';
import { parseDate } from '~/utils/date-utils';
export const formatDatetime = (
date: Date | string,
timeZone: string,
dateFormat: string,
timeFormat: string,
) => {
return formatInTimeZone(
parseDate(date).toJSDate(),
timeZone,
`${dateFormat} ${timeFormat}`,
);
};

View File

@ -0,0 +1,10 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { formatInTimeZone } from 'date-fns-tz';
export const formatDateISOStringToDate = (
date: string,
timeZone: string,
dateFormat: DateFormat,
) => {
return formatInTimeZone(new Date(date), timeZone, `${dateFormat}`);
};

View File

@ -0,0 +1,16 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { formatInTimeZone } from 'date-fns-tz';
export const formatDateISOStringToDateTime = (
date: string,
timeZone: string,
dateFormat: DateFormat,
timeFormat: TimeFormat,
) => {
return formatInTimeZone(
new Date(date),
timeZone,
`${dateFormat} ${timeFormat}`,
);
};

View File

@ -0,0 +1,29 @@
import { formatInTimeZone } from 'date-fns-tz';
import defaultLocale from 'date-fns/locale/en-US';
/**
* Formats a IANA time zone to a select option label.
* @param ianaTimeZone IANA time zone
* @returns Formatted label
* @example 'Europe/Paris' => '(GMT+01:00) Central European Time - Paris'
*/
export const formatTimeZoneLabel = (ianaTimeZone: string) => {
const timeZoneWithGmtOffset = formatInTimeZone(
Date.now(),
ianaTimeZone,
`(OOOO) zzzz`,
{ locale: defaultLocale },
);
const ianaTimeZoneParts = ianaTimeZone.split('/');
const location =
ianaTimeZoneParts.length > 1
? ianaTimeZoneParts.slice(-1)[0].replaceAll('_', ' ')
: undefined;
const timeZoneLabel =
!location || timeZoneWithGmtOffset.includes(location)
? timeZoneWithGmtOffset
: [timeZoneWithGmtOffset, location].join(' - ');
return timeZoneLabel;
};

View File

@ -0,0 +1,20 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
import { WorkspaceMemberDateFormatEnum } from '~/generated/graphql';
export const getDateFormatFromWorkspaceDateFormat = (
workspaceDateFormat: WorkspaceMemberDateFormatEnum,
) => {
switch (workspaceDateFormat) {
case WorkspaceMemberDateFormatEnum.System:
return detectDateFormat();
case WorkspaceMemberDateFormatEnum.MonthFirst:
return DateFormat.MONTH_FIRST;
case WorkspaceMemberDateFormatEnum.DayFirst:
return DateFormat.DAY_FIRST;
case WorkspaceMemberDateFormatEnum.YearFirst:
return DateFormat.YEAR_FIRST;
default:
return DateFormat.MONTH_FIRST;
}
};

View File

@ -0,0 +1,18 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
import { WorkspaceMemberTimeFormatEnum } from '~/generated/graphql';
export const getTimeFormatFromWorkspaceTimeFormat = (
workspaceTimeFormat: WorkspaceMemberTimeFormatEnum,
) => {
switch (workspaceTimeFormat) {
case WorkspaceMemberTimeFormatEnum.System:
return detectTimeFormat();
case WorkspaceMemberTimeFormatEnum.Hour_24:
return TimeFormat.HOUR_24;
case WorkspaceMemberTimeFormatEnum.Hour_12:
return TimeFormat.HOUR_12;
default:
return TimeFormat.HOUR_24;
}
};

View File

@ -0,0 +1,19 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { WorkspaceMemberDateFormatEnum } from '~/generated/graphql';
export const getWorkspaceDateFormatFromDateFormat = (
dateFormat: DateFormat,
) => {
switch (dateFormat) {
case DateFormat.SYSTEM:
return WorkspaceMemberDateFormatEnum.System;
case DateFormat.MONTH_FIRST:
return WorkspaceMemberDateFormatEnum.MonthFirst;
case DateFormat.DAY_FIRST:
return WorkspaceMemberDateFormatEnum.DayFirst;
case DateFormat.YEAR_FIRST:
return WorkspaceMemberDateFormatEnum.YearFirst;
default:
return WorkspaceMemberDateFormatEnum.MonthFirst;
}
};

View File

@ -0,0 +1,17 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { WorkspaceMemberTimeFormatEnum } from '~/generated/graphql';
export const getWorkspaceTimeFormatFromTimeFormat = (
timeFormat: TimeFormat,
) => {
switch (timeFormat) {
case TimeFormat.SYSTEM:
return WorkspaceMemberTimeFormatEnum.System;
case TimeFormat.HOUR_24:
return WorkspaceMemberTimeFormatEnum.Hour_24;
case TimeFormat.HOUR_12:
return WorkspaceMemberTimeFormatEnum.Hour_12;
default:
return WorkspaceMemberTimeFormatEnum.Hour_24;
}
};