feat(billing): refacto billing (#14243)
… prices for metered billing --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
-73
@@ -1,73 +0,0 @@
|
||||
import { detectNumberFormat } from '../detectNumberFormat';
|
||||
|
||||
// Mock navigator.language
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
|
||||
describe('detectNumberFormat', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to default
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect COMMAS_AND_DOT format for en-US locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
});
|
||||
|
||||
it('should detect SPACES_AND_COMMA format for fr-FR locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'fr-FR',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('SPACES_AND_COMMA');
|
||||
});
|
||||
|
||||
it('should detect DOTS_AND_COMMA format for de-DE locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'de-DE',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('DOTS_AND_COMMA');
|
||||
});
|
||||
|
||||
it('should detect APOSTROPHE_AND_DOT format for de-CH locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'de-CH',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('APOSTROPHE_AND_DOT');
|
||||
});
|
||||
|
||||
it('should fallback to COMMAS_AND_DOT for unknown patterns', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'ja-JP', // Uses different separators not in our patterns
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
});
|
||||
|
||||
it('should handle invalid locale gracefully', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'invalid-locale',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
});
|
||||
|
||||
it('should handle missing navigator.language', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: undefined,
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
});
|
||||
});
|
||||
+27
-19
@@ -1,22 +1,30 @@
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat';
|
||||
import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
// Mock the imported modules
|
||||
jest.mock('@/localization/utils/detectDateFormat');
|
||||
|
||||
jest.mock('@/localization/utils/detection/detectDateFormat');
|
||||
jest.mock('date-fns-tz');
|
||||
|
||||
const mockDetectDateFormat = detectDateFormat as jest.MockedFunction<
|
||||
typeof detectDateFormat
|
||||
>;
|
||||
const mockFormatInTimeZone = formatInTimeZone as jest.MockedFunction<
|
||||
typeof formatInTimeZone
|
||||
>;
|
||||
|
||||
describe('formatDateISOStringToDateTimeSimplified', () => {
|
||||
const mockDate = new Date('2023-08-15T10:30:00Z');
|
||||
const mockTimeZone = 'America/New_York';
|
||||
const mockTimeFormat = 'HH:mm';
|
||||
const mockTimeFormat = TimeFormat.HOUR_24;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should format the date correctly when DATE_FORMAT is MONTH_FIRST', () => {
|
||||
detectDateFormat.mockReturnValue('MONTH_FIRST');
|
||||
formatInTimeZone.mockReturnValue('Oct 15 · 06:30');
|
||||
mockDetectDateFormat.mockReturnValue('MONTH_FIRST');
|
||||
mockFormatInTimeZone.mockReturnValue('Oct 15 · 06:30');
|
||||
|
||||
const result = formatDateISOStringToDateTimeSimplified(
|
||||
mockDate,
|
||||
@@ -24,8 +32,8 @@ describe('formatDateISOStringToDateTimeSimplified', () => {
|
||||
mockTimeFormat,
|
||||
);
|
||||
|
||||
expect(detectDateFormat).toHaveBeenCalled();
|
||||
expect(formatInTimeZone).toHaveBeenCalledWith(
|
||||
expect(mockDetectDateFormat).toHaveBeenCalled();
|
||||
expect(mockFormatInTimeZone).toHaveBeenCalledWith(
|
||||
mockDate,
|
||||
mockTimeZone,
|
||||
'MMM d · HH:mm',
|
||||
@@ -34,8 +42,8 @@ describe('formatDateISOStringToDateTimeSimplified', () => {
|
||||
});
|
||||
|
||||
it('should format the date correctly when DATE_FORMAT is DAY_FIRST', () => {
|
||||
detectDateFormat.mockReturnValue('DAY_FIRST');
|
||||
formatInTimeZone.mockReturnValue('15 Oct · 06:30');
|
||||
mockDetectDateFormat.mockReturnValue('DAY_FIRST');
|
||||
mockFormatInTimeZone.mockReturnValue('15 Oct · 06:30');
|
||||
|
||||
const result = formatDateISOStringToDateTimeSimplified(
|
||||
mockDate,
|
||||
@@ -43,8 +51,8 @@ describe('formatDateISOStringToDateTimeSimplified', () => {
|
||||
mockTimeFormat,
|
||||
);
|
||||
|
||||
expect(detectDateFormat).toHaveBeenCalled();
|
||||
expect(formatInTimeZone).toHaveBeenCalledWith(
|
||||
expect(mockDetectDateFormat).toHaveBeenCalled();
|
||||
expect(mockFormatInTimeZone).toHaveBeenCalledWith(
|
||||
mockDate,
|
||||
mockTimeZone,
|
||||
'd MMM · HH:mm',
|
||||
@@ -53,16 +61,16 @@ describe('formatDateISOStringToDateTimeSimplified', () => {
|
||||
});
|
||||
|
||||
it('should use the provided time format', () => {
|
||||
detectDateFormat.mockReturnValue('MONTH_FIRST');
|
||||
formatInTimeZone.mockReturnValue('Oct 15 · 6:30 AM');
|
||||
mockDetectDateFormat.mockReturnValue('MONTH_FIRST');
|
||||
mockFormatInTimeZone.mockReturnValue('Oct 15 · 6:30 AM');
|
||||
|
||||
const result = formatDateISOStringToDateTimeSimplified(
|
||||
mockDate,
|
||||
mockTimeZone,
|
||||
'h:mm aa',
|
||||
TimeFormat.HOUR_12,
|
||||
);
|
||||
|
||||
expect(formatInTimeZone).toHaveBeenCalledWith(
|
||||
expect(mockFormatInTimeZone).toHaveBeenCalledWith(
|
||||
mockDate,
|
||||
mockTimeZone,
|
||||
'MMM d · h:mm aa',
|
||||
@@ -71,8 +79,8 @@ describe('formatDateISOStringToDateTimeSimplified', () => {
|
||||
});
|
||||
|
||||
it('should handle different time zones', () => {
|
||||
detectDateFormat.mockReturnValue('MONTH_FIRST');
|
||||
formatInTimeZone.mockReturnValue('Oct 16 · 02:30');
|
||||
mockDetectDateFormat.mockReturnValue('MONTH_FIRST');
|
||||
mockFormatInTimeZone.mockReturnValue('Oct 16 · 02:30');
|
||||
|
||||
const result = formatDateISOStringToDateTimeSimplified(
|
||||
mockDate,
|
||||
@@ -80,7 +88,7 @@ describe('formatDateISOStringToDateTimeSimplified', () => {
|
||||
mockTimeFormat,
|
||||
);
|
||||
|
||||
expect(formatInTimeZone).toHaveBeenCalledWith(
|
||||
expect(mockFormatInTimeZone).toHaveBeenCalledWith(
|
||||
mockDate,
|
||||
'Asia/Tokyo',
|
||||
'MMM d · HH:mm',
|
||||
@@ -1,17 +0,0 @@
|
||||
import { type DateFormat } from '@/localization/constants/DateFormat';
|
||||
|
||||
export const detectDateFormat = (): keyof typeof 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 'MONTH_FIRST';
|
||||
if (partOrder[0] === 'day') return 'DAY_FIRST';
|
||||
if (partOrder[0] === 'year') return 'YEAR_FIRST';
|
||||
|
||||
return 'MONTH_FIRST';
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import { NumberFormat } from '@/localization/constants/NumberFormat';
|
||||
|
||||
const SPACE_CHARS = new Set([' ', '\u00A0', '\u202F']); // space, non-breaking space, narrow no-break space
|
||||
const APOSTROPHE_CHARS = new Set(["'", '\u2019']); // apostrophe, right single quotation mark
|
||||
|
||||
const FORMAT_PATTERNS = new Map<string, keyof typeof NumberFormat>([
|
||||
[',|.', NumberFormat.COMMAS_AND_DOT],
|
||||
['.|,', NumberFormat.DOTS_AND_COMMA],
|
||||
['space|,', NumberFormat.SPACES_AND_COMMA],
|
||||
['apostrophe|.', NumberFormat.APOSTROPHE_AND_DOT],
|
||||
]);
|
||||
|
||||
export const detectNumberFormat = (): keyof typeof NumberFormat => {
|
||||
const testNumber = 1234567.89;
|
||||
let language = navigator?.language || 'en-US';
|
||||
|
||||
let formatter: Intl.NumberFormat;
|
||||
try {
|
||||
formatter = new Intl.NumberFormat(language);
|
||||
} catch {
|
||||
formatter = new Intl.NumberFormat('en-US');
|
||||
}
|
||||
|
||||
const parts = formatter.formatToParts(testNumber);
|
||||
const thousandSeparator =
|
||||
parts.find((part) => part.type === 'group')?.value || '';
|
||||
const decimalSeparator =
|
||||
parts.find((part) => part.type === 'decimal')?.value || '';
|
||||
|
||||
let thousandCategory: string;
|
||||
if (SPACE_CHARS.has(thousandSeparator)) {
|
||||
thousandCategory = 'space';
|
||||
} else if (APOSTROPHE_CHARS.has(thousandSeparator)) {
|
||||
thousandCategory = 'apostrophe';
|
||||
} else {
|
||||
thousandCategory = thousandSeparator;
|
||||
}
|
||||
|
||||
const pattern = `${thousandCategory}|${decimalSeparator}`;
|
||||
return FORMAT_PATTERNS.get(pattern) || NumberFormat.COMMAS_AND_DOT;
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { type TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const detectTimeFormat = (): keyof typeof TimeFormat => {
|
||||
const isHour12 = Intl.DateTimeFormat(navigator.language, {
|
||||
hour: 'numeric',
|
||||
}).resolvedOptions().hour12;
|
||||
|
||||
if (isDefined(isHour12) && isHour12) {
|
||||
return 'HOUR_12';
|
||||
}
|
||||
|
||||
return 'HOUR_24';
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Detects the user's time zone.
|
||||
* @returns a IANA time zone
|
||||
*/
|
||||
export const detectTimeZone = () =>
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
|
||||
|
||||
// Mock navigator.language
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
|
||||
describe('detectCalendarStartDay', () => {
|
||||
let originalIntlLocale: typeof Intl.Locale;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset to default
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
|
||||
// Store original and mock Intl.Locale to force fallback logic
|
||||
originalIntlLocale = global.Intl.Locale;
|
||||
(global.Intl as any).Locale = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Force fallback');
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original Intl.Locale
|
||||
(global.Intl as any).Locale = originalIntlLocale;
|
||||
});
|
||||
|
||||
it('should return SUNDAY for en-US locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
expect(detectCalendarStartDay()).toBe('SUNDAY');
|
||||
});
|
||||
|
||||
it('should return MONDAY for German locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'de-DE',
|
||||
});
|
||||
expect(detectCalendarStartDay()).toBe('MONDAY');
|
||||
});
|
||||
|
||||
it('should return MONDAY for French locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'fr-FR',
|
||||
});
|
||||
expect(detectCalendarStartDay()).toBe('MONDAY');
|
||||
});
|
||||
|
||||
it('should return MONDAY for British English', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-GB',
|
||||
});
|
||||
expect(detectCalendarStartDay()).toBe('MONDAY');
|
||||
});
|
||||
|
||||
it('should return SATURDAY for Arabic locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'ar-SA',
|
||||
});
|
||||
expect(detectCalendarStartDay()).toBe('SATURDAY');
|
||||
});
|
||||
|
||||
it('should return SATURDAY for Hebrew locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'he-IL',
|
||||
});
|
||||
expect(detectCalendarStartDay()).toBe('SATURDAY');
|
||||
});
|
||||
|
||||
it('should return SUNDAY for unknown locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'xx-XX',
|
||||
});
|
||||
expect(detectCalendarStartDay()).toBe('SUNDAY');
|
||||
});
|
||||
|
||||
it('should handle missing navigator.language', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: undefined,
|
||||
});
|
||||
expect(detectCalendarStartDay()).toBe('SUNDAY');
|
||||
});
|
||||
|
||||
it('should handle Intl.Locale with weekInfo support', () => {
|
||||
// Temporarily restore original and mock with weekInfo
|
||||
(global.Intl as any).Locale = jest.fn().mockImplementation(() => ({
|
||||
weekInfo: { firstDay: 1 }, // Monday
|
||||
}));
|
||||
|
||||
expect(detectCalendarStartDay()).toBe('MONDAY');
|
||||
});
|
||||
|
||||
it('should handle Intl.Locale with Saturday firstDay', () => {
|
||||
// Temporarily restore original and mock with weekInfo
|
||||
(global.Intl as any).Locale = jest.fn().mockImplementation(() => ({
|
||||
weekInfo: { firstDay: 6 }, // Saturday
|
||||
}));
|
||||
|
||||
expect(detectCalendarStartDay()).toBe('SATURDAY');
|
||||
});
|
||||
|
||||
it('should handle Intl.Locale throwing error', () => {
|
||||
// This test uses the default mock that throws an error
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'de-DE',
|
||||
});
|
||||
|
||||
expect(detectCalendarStartDay()).toBe('MONDAY');
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat';
|
||||
|
||||
describe('detectDateFormat', () => {
|
||||
it('should return MONTH_FIRST if the detected format starts with month', () => {
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
import { detectNumberFormat } from '@/localization/utils/detection/detectNumberFormat';
|
||||
|
||||
// Mock navigator.language
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
|
||||
describe('detectNumberFormat', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to default
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect COMMAS_AND_DOT format for en-US locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'en-US',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
});
|
||||
|
||||
it('should detect SPACES_AND_COMMA format for fr-FR locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'fr-FR',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('SPACES_AND_COMMA');
|
||||
});
|
||||
|
||||
it('should detect DOTS_AND_COMMA format for de-DE locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'de-DE',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('DOTS_AND_COMMA');
|
||||
});
|
||||
|
||||
it('should detect APOSTROPHE_AND_DOT format for de-CH locale', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'de-CH',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('APOSTROPHE_AND_DOT');
|
||||
});
|
||||
|
||||
it('should fallback to COMMAS_AND_DOT for unknown patterns', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'ja-JP', // Uses different separators not in our patterns
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
});
|
||||
|
||||
it('should handle invalid locale gracefully', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'invalid-locale',
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
});
|
||||
|
||||
it('should handle missing navigator.language', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: undefined,
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
});
|
||||
|
||||
it('should detect SPACES_AND_COMMA format for locales using regular space separator', () => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
writable: true,
|
||||
value: 'nb-NO', // Norwegian uses space as thousand separator
|
||||
});
|
||||
expect(detectNumberFormat()).toBe('SPACES_AND_COMMA');
|
||||
});
|
||||
|
||||
it('should detect SPACES_AND_COMMA format for locales using non-breaking space separator', () => {
|
||||
// Mock formatToParts to return non-breaking space
|
||||
const originalNumberFormat = Intl.NumberFormat;
|
||||
(global.Intl as any).NumberFormat = Object.assign(
|
||||
jest.fn().mockImplementation(() => ({
|
||||
formatToParts: () => [
|
||||
{ type: 'integer', value: '1' },
|
||||
{ type: 'group', value: '\u00A0' }, // non-breaking space
|
||||
{ type: 'integer', value: '234' },
|
||||
{ type: 'group', value: '\u00A0' },
|
||||
{ type: 'integer', value: '567' },
|
||||
{ type: 'decimal', value: ',' },
|
||||
{ type: 'fraction', value: '89' },
|
||||
],
|
||||
})),
|
||||
{ supportedLocalesOf: jest.fn() },
|
||||
);
|
||||
|
||||
expect(detectNumberFormat()).toBe('SPACES_AND_COMMA');
|
||||
global.Intl.NumberFormat = originalNumberFormat;
|
||||
});
|
||||
|
||||
it('should detect SPACES_AND_COMMA format for locales using narrow no-break space separator', () => {
|
||||
// Mock formatToParts to return narrow no-break space
|
||||
const originalNumberFormat = Intl.NumberFormat;
|
||||
(global.Intl as any).NumberFormat = Object.assign(
|
||||
jest.fn().mockImplementation(() => ({
|
||||
formatToParts: () => [
|
||||
{ type: 'integer', value: '1' },
|
||||
{ type: 'group', value: '\u202F' }, // narrow no-break space
|
||||
{ type: 'integer', value: '234' },
|
||||
{ type: 'group', value: '\u202F' },
|
||||
{ type: 'integer', value: '567' },
|
||||
{ type: 'decimal', value: ',' },
|
||||
{ type: 'fraction', value: '89' },
|
||||
],
|
||||
})),
|
||||
{ supportedLocalesOf: jest.fn() },
|
||||
);
|
||||
|
||||
expect(detectNumberFormat()).toBe('SPACES_AND_COMMA');
|
||||
global.Intl.NumberFormat = originalNumberFormat;
|
||||
});
|
||||
|
||||
it('should detect APOSTROPHE_AND_DOT format for locales using regular apostrophe separator', () => {
|
||||
// Mock formatToParts to return apostrophe
|
||||
const originalNumberFormat = Intl.NumberFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
|
||||
formatToParts: () => [
|
||||
{ type: 'integer', value: '1' },
|
||||
{ type: 'group', value: "'" }, // apostrophe
|
||||
{ type: 'integer', value: '234' },
|
||||
{ type: 'group', value: "'" },
|
||||
{ type: 'integer', value: '567' },
|
||||
{ type: 'decimal', value: '.' },
|
||||
{ type: 'fraction', value: '89' },
|
||||
],
|
||||
}));
|
||||
|
||||
expect(detectNumberFormat()).toBe('APOSTROPHE_AND_DOT');
|
||||
global.Intl.NumberFormat = originalNumberFormat;
|
||||
});
|
||||
|
||||
it('should detect APOSTROPHE_AND_DOT format for locales using right single quotation mark separator', () => {
|
||||
// Mock formatToParts to return right single quotation mark
|
||||
const originalNumberFormat = Intl.NumberFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
|
||||
formatToParts: () => [
|
||||
{ type: 'integer', value: '1' },
|
||||
{ type: 'group', value: '\u2019' }, // right single quotation mark
|
||||
{ type: 'integer', value: '234' },
|
||||
{ type: 'group', value: '\u2019' },
|
||||
{ type: 'integer', value: '567' },
|
||||
{ type: 'decimal', value: '.' },
|
||||
{ type: 'fraction', value: '89' },
|
||||
],
|
||||
}));
|
||||
|
||||
expect(detectNumberFormat()).toBe('APOSTROPHE_AND_DOT');
|
||||
global.Intl.NumberFormat = originalNumberFormat;
|
||||
});
|
||||
|
||||
it('should handle formatToParts throwing an error', () => {
|
||||
const originalNumberFormat = Intl.NumberFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
|
||||
formatToParts: () => {
|
||||
throw new Error('formatToParts failed');
|
||||
},
|
||||
}));
|
||||
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
global.Intl.NumberFormat = originalNumberFormat;
|
||||
});
|
||||
|
||||
it('should handle Intl.NumberFormat constructor throwing an error', () => {
|
||||
const originalNumberFormat = Intl.NumberFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.NumberFormat = jest.fn().mockImplementation(() => {
|
||||
throw new Error('NumberFormat constructor failed');
|
||||
});
|
||||
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
global.Intl.NumberFormat = originalNumberFormat;
|
||||
});
|
||||
|
||||
it('should handle formatToParts returning parts without group or decimal types', () => {
|
||||
const originalNumberFormat = Intl.NumberFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
|
||||
formatToParts: () => [
|
||||
{ type: 'integer', value: '1234567' },
|
||||
{ type: 'fraction', value: '89' },
|
||||
],
|
||||
}));
|
||||
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
global.Intl.NumberFormat = originalNumberFormat;
|
||||
});
|
||||
|
||||
it('should handle unknown pattern combinations', () => {
|
||||
// Mock formatToParts to return an unknown pattern
|
||||
const originalNumberFormat = Intl.NumberFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
|
||||
formatToParts: () => [
|
||||
{ type: 'integer', value: '1' },
|
||||
{ type: 'group', value: '|' }, // unknown separator
|
||||
{ type: 'integer', value: '234' },
|
||||
{ type: 'decimal', value: ':' }, // unknown decimal
|
||||
{ type: 'fraction', value: '89' },
|
||||
],
|
||||
}));
|
||||
|
||||
expect(detectNumberFormat()).toBe('COMMAS_AND_DOT');
|
||||
global.Intl.NumberFormat = originalNumberFormat;
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
||||
import { detectTimeFormat } from '@/localization/utils/detection/detectTimeFormat';
|
||||
|
||||
describe('detectTimeFormat', () => {
|
||||
it('should return HOUR_12 if the hour format is 12-hour', () => {
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { detectTimeZone } from '@/localization/utils/detection/detectTimeZone';
|
||||
|
||||
describe('detectTimeZone', () => {
|
||||
it('should return the system timezone', () => {
|
||||
const result = detectTimeZone();
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return UTC when Intl.DateTimeFormat throws error', () => {
|
||||
const originalDateTimeFormat = global.Intl.DateTimeFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.DateTimeFormat = jest.fn().mockImplementation(() => {
|
||||
throw new Error('DateTimeFormat not supported');
|
||||
});
|
||||
|
||||
expect(detectTimeZone()).toBe('UTC');
|
||||
global.Intl.DateTimeFormat = originalDateTimeFormat;
|
||||
});
|
||||
|
||||
it('should return UTC when resolvedOptions throws error', () => {
|
||||
const originalDateTimeFormat = global.Intl.DateTimeFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
|
||||
resolvedOptions: () => {
|
||||
throw new Error('resolvedOptions failed');
|
||||
},
|
||||
}));
|
||||
|
||||
expect(detectTimeZone()).toBe('UTC');
|
||||
global.Intl.DateTimeFormat = originalDateTimeFormat;
|
||||
});
|
||||
|
||||
it('should return specific timezone when mocked', () => {
|
||||
const originalDateTimeFormat = global.Intl.DateTimeFormat;
|
||||
// @ts-expect-error - Mocking for test
|
||||
global.Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
|
||||
resolvedOptions: () => ({ timeZone: 'America/New_York' }),
|
||||
}));
|
||||
|
||||
expect(detectTimeZone()).toBe('America/New_York');
|
||||
global.Intl.DateTimeFormat = originalDateTimeFormat;
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -8,7 +8,7 @@ export const detectCalendarStartDay = (): keyof typeof CalendarStartDay => {
|
||||
// Use Intl.Locale to get the first day of the week from the user's locale
|
||||
// This requires a modern browser that supports Intl.Locale
|
||||
try {
|
||||
const locale = new Intl.Locale(navigator.language);
|
||||
const locale = new Intl.Locale(navigator?.language || 'en-US');
|
||||
|
||||
// Check if the weekInfo property is available (newer browsers)
|
||||
if (
|
||||
@@ -37,7 +37,7 @@ export const detectCalendarStartDay = (): keyof typeof CalendarStartDay => {
|
||||
}
|
||||
|
||||
// Fallback: Use a heuristic based on common locale patterns
|
||||
const language = navigator.language.toLowerCase();
|
||||
const language = (navigator?.language || 'en-US').toLowerCase();
|
||||
|
||||
// Most European countries, Australia, New Zealand start with Monday
|
||||
if (
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type DateFormat } from '@/localization/constants/DateFormat';
|
||||
|
||||
export const detectDateFormat = (): keyof typeof DateFormat => {
|
||||
try {
|
||||
const date = new Date();
|
||||
const formatter = new Intl.DateTimeFormat(navigator?.language || 'en-US');
|
||||
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 'MONTH_FIRST';
|
||||
if (partOrder[0] === 'day') return 'DAY_FIRST';
|
||||
if (partOrder[0] === 'year') return 'YEAR_FIRST';
|
||||
|
||||
return 'MONTH_FIRST';
|
||||
} catch {
|
||||
return 'MONTH_FIRST';
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NumberFormat } from '@/localization/constants/NumberFormat';
|
||||
|
||||
const SPACE_CHARS = new Set([' ', '\u00A0', '\u202F']); // space, non-breaking space, narrow no-break space
|
||||
const APOSTROPHE_CHARS = new Set(["'", '\u2019']); // apostrophe, right single quotation mark
|
||||
|
||||
const FORMAT_PATTERNS = new Map<string, keyof typeof NumberFormat>([
|
||||
[',|.', NumberFormat.COMMAS_AND_DOT],
|
||||
['.|,', NumberFormat.DOTS_AND_COMMA],
|
||||
['space|,', NumberFormat.SPACES_AND_COMMA],
|
||||
['apostrophe|.', NumberFormat.APOSTROPHE_AND_DOT],
|
||||
]);
|
||||
|
||||
export const detectNumberFormat = (): keyof typeof NumberFormat => {
|
||||
try {
|
||||
const testNumber = 1234567.89;
|
||||
const language = navigator?.language || 'en-US';
|
||||
|
||||
const formatter = new Intl.NumberFormat(language);
|
||||
const parts = formatter.formatToParts(testNumber);
|
||||
const thousandSeparator =
|
||||
parts.find((part) => part.type === 'group')?.value || '';
|
||||
const decimalSeparator =
|
||||
parts.find((part) => part.type === 'decimal')?.value || '';
|
||||
|
||||
let thousandCategory: string;
|
||||
if (SPACE_CHARS.has(thousandSeparator)) {
|
||||
thousandCategory = 'space';
|
||||
} else if (APOSTROPHE_CHARS.has(thousandSeparator)) {
|
||||
thousandCategory = 'apostrophe';
|
||||
} else {
|
||||
thousandCategory = thousandSeparator;
|
||||
}
|
||||
|
||||
const pattern = `${thousandCategory}|${decimalSeparator}`;
|
||||
return FORMAT_PATTERNS.get(pattern) || NumberFormat.COMMAS_AND_DOT;
|
||||
} catch {
|
||||
return NumberFormat.COMMAS_AND_DOT;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { type TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const detectTimeFormat = (): keyof typeof TimeFormat => {
|
||||
try {
|
||||
const isHour12 = Intl.DateTimeFormat(navigator?.language || 'en-US', {
|
||||
hour: 'numeric',
|
||||
}).resolvedOptions().hour12;
|
||||
|
||||
if (isDefined(isHour12) && isHour12) {
|
||||
return 'HOUR_12';
|
||||
}
|
||||
|
||||
return 'HOUR_24';
|
||||
} catch {
|
||||
return 'HOUR_24';
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Detects the user's time zone.
|
||||
* @returns a IANA time zone
|
||||
*/
|
||||
export const detectTimeZone = (): string => {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
} catch {
|
||||
return 'UTC';
|
||||
}
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/format-preferences/getDateFormatFromWorkspaceDateFormat';
|
||||
import { WorkspaceMemberDateFormatEnum } from '~/generated/graphql';
|
||||
|
||||
// Mock detectDateFormat
|
||||
jest.mock('@/localization/utils/detection/detectDateFormat', () => ({
|
||||
detectDateFormat: jest.fn(() => 'MONTH_FIRST'),
|
||||
}));
|
||||
|
||||
describe('getDateFormatFromWorkspaceDateFormat', () => {
|
||||
it('should return MONTH_FIRST for MONTH_FIRST workspace format', () => {
|
||||
expect(
|
||||
getDateFormatFromWorkspaceDateFormat(
|
||||
WorkspaceMemberDateFormatEnum.MONTH_FIRST,
|
||||
),
|
||||
).toBe(DateFormat.MONTH_FIRST);
|
||||
});
|
||||
|
||||
it('should return DAY_FIRST for DAY_FIRST workspace format', () => {
|
||||
expect(
|
||||
getDateFormatFromWorkspaceDateFormat(
|
||||
WorkspaceMemberDateFormatEnum.DAY_FIRST,
|
||||
),
|
||||
).toBe(DateFormat.DAY_FIRST);
|
||||
});
|
||||
|
||||
it('should return YEAR_FIRST for YEAR_FIRST workspace format', () => {
|
||||
expect(
|
||||
getDateFormatFromWorkspaceDateFormat(
|
||||
WorkspaceMemberDateFormatEnum.YEAR_FIRST,
|
||||
),
|
||||
).toBe(DateFormat.YEAR_FIRST);
|
||||
});
|
||||
|
||||
it('should return detected format for SYSTEM workspace format', () => {
|
||||
expect(
|
||||
getDateFormatFromWorkspaceDateFormat(
|
||||
WorkspaceMemberDateFormatEnum.SYSTEM,
|
||||
),
|
||||
).toBe(DateFormat.MONTH_FIRST);
|
||||
});
|
||||
|
||||
it('should return MONTH_FIRST for unknown format', () => {
|
||||
expect(
|
||||
getDateFormatFromWorkspaceDateFormat(
|
||||
'UNKNOWN' as WorkspaceMemberDateFormatEnum,
|
||||
),
|
||||
).toBe(DateFormat.MONTH_FIRST);
|
||||
});
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/format-preferences/getTimeFormatFromWorkspaceTimeFormat';
|
||||
import { WorkspaceMemberTimeFormatEnum } from '~/generated/graphql';
|
||||
|
||||
// Mock detectTimeFormat
|
||||
jest.mock('@/localization/utils/detection/detectTimeFormat', () => ({
|
||||
detectTimeFormat: jest.fn(() => 'HOUR_12'),
|
||||
}));
|
||||
|
||||
describe('getTimeFormatFromWorkspaceTimeFormat', () => {
|
||||
it('should return HOUR_24 for HOUR_24 workspace format', () => {
|
||||
expect(
|
||||
getTimeFormatFromWorkspaceTimeFormat(
|
||||
WorkspaceMemberTimeFormatEnum.HOUR_24,
|
||||
),
|
||||
).toBe(TimeFormat.HOUR_24);
|
||||
});
|
||||
|
||||
it('should return HOUR_12 for HOUR_12 workspace format', () => {
|
||||
expect(
|
||||
getTimeFormatFromWorkspaceTimeFormat(
|
||||
WorkspaceMemberTimeFormatEnum.HOUR_12,
|
||||
),
|
||||
).toBe(TimeFormat.HOUR_12);
|
||||
});
|
||||
|
||||
it('should return detected format for SYSTEM workspace format', () => {
|
||||
expect(
|
||||
getTimeFormatFromWorkspaceTimeFormat(
|
||||
WorkspaceMemberTimeFormatEnum.SYSTEM,
|
||||
),
|
||||
).toBe(TimeFormat.HOUR_12);
|
||||
});
|
||||
|
||||
it('should return HOUR_24 for unknown format', () => {
|
||||
expect(
|
||||
getTimeFormatFromWorkspaceTimeFormat(
|
||||
'UNKNOWN' as WorkspaceMemberTimeFormatEnum,
|
||||
),
|
||||
).toBe(TimeFormat.HOUR_24);
|
||||
});
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CalendarStartDay } from '@/localization/constants/CalendarStartDay';
|
||||
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
|
||||
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export const getCalendarStartDayFromWorkspaceMember = (
|
||||
workspaceMember: WorkspaceMember | CurrentWorkspaceMember,
|
||||
): CalendarStartDay => {
|
||||
const calendarStartDay = workspaceMember.calendarStartDay;
|
||||
|
||||
if (
|
||||
calendarStartDay === CalendarStartDay.SYSTEM ||
|
||||
calendarStartDay == null
|
||||
) {
|
||||
return CalendarStartDay[detectCalendarStartDay()];
|
||||
}
|
||||
|
||||
return calendarStartDay as CalendarStartDay;
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat';
|
||||
import { WorkspaceMemberDateFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getDateFormatFromWorkspaceDateFormat = (
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat';
|
||||
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { WorkspaceMemberDateFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getDateFormatFromWorkspaceMember = (
|
||||
workspaceMember: WorkspaceMember | CurrentWorkspaceMember,
|
||||
): DateFormat => {
|
||||
switch (workspaceMember.dateFormat) {
|
||||
case WorkspaceMemberDateFormatEnum.SYSTEM:
|
||||
return DateFormat[detectDateFormat()];
|
||||
case WorkspaceMemberDateFormatEnum.MONTH_FIRST:
|
||||
return DateFormat.MONTH_FIRST;
|
||||
case WorkspaceMemberDateFormatEnum.DAY_FIRST:
|
||||
return DateFormat.DAY_FIRST;
|
||||
case WorkspaceMemberDateFormatEnum.YEAR_FIRST:
|
||||
return DateFormat.YEAR_FIRST;
|
||||
default:
|
||||
return DateFormat[detectDateFormat()];
|
||||
}
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { type WorkspaceMemberFormatPreferences } from '@/localization/states/workspaceMemberFormatPreferencesState';
|
||||
import { getCalendarStartDayFromWorkspaceMember } from '@/localization/utils/format-preferences/getCalendarStartDayFromWorkspaceMember';
|
||||
import { getDateFormatFromWorkspaceMember } from '@/localization/utils/format-preferences/getDateFormatFromWorkspaceMember';
|
||||
import { getNumberFormatFromWorkspaceMember } from '@/localization/utils/format-preferences/getNumberFormatFromWorkspaceMember';
|
||||
import { getTimeFormatFromWorkspaceMember } from '@/localization/utils/format-preferences/getTimeFormatFromWorkspaceMember';
|
||||
import { getTimeZoneFromWorkspaceMember } from '@/localization/utils/format-preferences/getTimeZoneFromWorkspaceMember';
|
||||
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export const getFormatPreferencesFromWorkspaceMember = (
|
||||
workspaceMember: WorkspaceMember | CurrentWorkspaceMember,
|
||||
): WorkspaceMemberFormatPreferences => {
|
||||
return {
|
||||
timeZone: getTimeZoneFromWorkspaceMember(workspaceMember),
|
||||
dateFormat: getDateFormatFromWorkspaceMember(workspaceMember),
|
||||
timeFormat: getTimeFormatFromWorkspaceMember(workspaceMember),
|
||||
numberFormat: getNumberFormatFromWorkspaceMember(workspaceMember),
|
||||
calendarStartDay: getCalendarStartDayFromWorkspaceMember(workspaceMember),
|
||||
};
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { NumberFormat } from '@/localization/constants/NumberFormat';
|
||||
import { detectNumberFormat } from '@/localization/utils/detection/detectNumberFormat';
|
||||
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { WorkspaceMemberNumberFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getNumberFormatFromWorkspaceMember = (
|
||||
workspaceMember: WorkspaceMember | CurrentWorkspaceMember,
|
||||
): NumberFormat => {
|
||||
switch (workspaceMember.numberFormat) {
|
||||
case WorkspaceMemberNumberFormatEnum.SYSTEM:
|
||||
return NumberFormat[detectNumberFormat()];
|
||||
case WorkspaceMemberNumberFormatEnum.COMMAS_AND_DOT:
|
||||
return NumberFormat.COMMAS_AND_DOT;
|
||||
case WorkspaceMemberNumberFormatEnum.SPACES_AND_COMMA:
|
||||
return NumberFormat.SPACES_AND_COMMA;
|
||||
case WorkspaceMemberNumberFormatEnum.DOTS_AND_COMMA:
|
||||
return NumberFormat.DOTS_AND_COMMA;
|
||||
case WorkspaceMemberNumberFormatEnum.APOSTROPHE_AND_DOT:
|
||||
return NumberFormat.APOSTROPHE_AND_DOT;
|
||||
default:
|
||||
return NumberFormat[detectNumberFormat()];
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { NumberFormat } from '@/localization/constants/NumberFormat';
|
||||
import { detectNumberFormat } from '@/localization/utils/detectNumberFormat';
|
||||
import { detectNumberFormat } from '@/localization/utils/detection/detectNumberFormat';
|
||||
import { WorkspaceMemberNumberFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getNumberFormatFromWorkspaceNumberFormat = (
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectTimeFormat } from '@/localization/utils/detection/detectTimeFormat';
|
||||
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { WorkspaceMemberTimeFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getTimeFormatFromWorkspaceMember = (
|
||||
workspaceMember: WorkspaceMember | CurrentWorkspaceMember,
|
||||
): TimeFormat => {
|
||||
switch (workspaceMember.timeFormat) {
|
||||
case WorkspaceMemberTimeFormatEnum.SYSTEM:
|
||||
return TimeFormat[detectTimeFormat()];
|
||||
case WorkspaceMemberTimeFormatEnum.HOUR_24:
|
||||
return TimeFormat.HOUR_24;
|
||||
case WorkspaceMemberTimeFormatEnum.HOUR_12:
|
||||
return TimeFormat.HOUR_12;
|
||||
default:
|
||||
return TimeFormat[detectTimeFormat()];
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
||||
import { detectTimeFormat } from '@/localization/utils/detection/detectTimeFormat';
|
||||
import { WorkspaceMemberTimeFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getTimeFormatFromWorkspaceTimeFormat = (
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { detectTimeZone } from '@/localization/utils/detection/detectTimeZone';
|
||||
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export const getTimeZoneFromWorkspaceMember = (
|
||||
workspaceMember: WorkspaceMember | CurrentWorkspaceMember,
|
||||
): string => {
|
||||
return workspaceMember.timeZone === 'system' || !workspaceMember.timeZone
|
||||
? detectTimeZone()
|
||||
: workspaceMember.timeZone;
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { type WorkspaceMemberFormatPreferences } from '@/localization/states/workspaceMemberFormatPreferencesState';
|
||||
import { getWorkspaceDateFormatFromDateFormat } from '@/localization/utils/format-preferences/getWorkspaceDateFormatFromDateFormat';
|
||||
import { getWorkspaceNumberFormatFromNumberFormat } from '@/localization/utils/format-preferences/getWorkspaceNumberFormatFromNumberFormat';
|
||||
import { getWorkspaceTimeFormatFromTimeFormat } from '@/localization/utils/format-preferences/getWorkspaceTimeFormatFromTimeFormat';
|
||||
|
||||
export const getWorkspaceMemberUpdateFromFormatPreferences = (
|
||||
preferences: Partial<WorkspaceMemberFormatPreferences>,
|
||||
): Partial<CurrentWorkspaceMember> => {
|
||||
const update: Partial<CurrentWorkspaceMember> = {};
|
||||
|
||||
if (preferences.timeZone !== undefined) {
|
||||
update.timeZone = preferences.timeZone;
|
||||
}
|
||||
|
||||
if (preferences.dateFormat !== undefined) {
|
||||
update.dateFormat = getWorkspaceDateFormatFromDateFormat(
|
||||
preferences.dateFormat,
|
||||
);
|
||||
}
|
||||
|
||||
if (preferences.timeFormat !== undefined) {
|
||||
update.timeFormat = getWorkspaceTimeFormatFromTimeFormat(
|
||||
preferences.timeFormat,
|
||||
);
|
||||
}
|
||||
|
||||
if (preferences.numberFormat !== undefined) {
|
||||
update.numberFormat = getWorkspaceNumberFormatFromNumberFormat(
|
||||
preferences.numberFormat,
|
||||
);
|
||||
}
|
||||
|
||||
if (preferences.calendarStartDay !== undefined) {
|
||||
update.calendarStartDay = preferences.calendarStartDay;
|
||||
}
|
||||
|
||||
return update;
|
||||
};
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { DATE_FORMAT_WITHOUT_YEAR } from '@/localization/constants/DateFormatWithoutYear';
|
||||
import { type TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
export const formatDateISOStringToDateTimeSimplified = (
|
||||
|
||||
Reference in New Issue
Block a user