fixing Numbers formatting (#14403)

fix #13880

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Ketan Mehta
2025-09-18 02:33:33 +05:30
committed by GitHub
parent 666b99524a
commit dc26e48f6f
36 changed files with 583 additions and 96 deletions
@@ -0,0 +1,73 @@
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');
});
});
@@ -0,0 +1,41 @@
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;
};
@@ -0,0 +1,22 @@
import { NumberFormat } from '@/localization/constants/NumberFormat';
import { detectNumberFormat } from '@/localization/utils/detectNumberFormat';
import { WorkspaceMemberNumberFormatEnum } from '~/generated/graphql';
export const getNumberFormatFromWorkspaceNumberFormat = (
numberFormat: WorkspaceMemberNumberFormatEnum,
): NumberFormat => {
switch (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.COMMAS_AND_DOT;
}
};
@@ -0,0 +1,21 @@
import { NumberFormat } from '@/localization/constants/NumberFormat';
import { WorkspaceMemberNumberFormatEnum } from '~/generated/graphql';
export const getWorkspaceNumberFormatFromNumberFormat = (
numberFormat: NumberFormat,
): WorkspaceMemberNumberFormatEnum => {
switch (numberFormat) {
case NumberFormat.SYSTEM:
return WorkspaceMemberNumberFormatEnum.SYSTEM;
case NumberFormat.COMMAS_AND_DOT:
return WorkspaceMemberNumberFormatEnum.COMMAS_AND_DOT;
case NumberFormat.SPACES_AND_COMMA:
return WorkspaceMemberNumberFormatEnum.SPACES_AND_COMMA;
case NumberFormat.DOTS_AND_COMMA:
return WorkspaceMemberNumberFormatEnum.DOTS_AND_COMMA;
case NumberFormat.APOSTROPHE_AND_DOT:
return WorkspaceMemberNumberFormatEnum.APOSTROPHE_AND_DOT;
default:
return WorkspaceMemberNumberFormatEnum.COMMAS_AND_DOT;
}
};