Skip to content

Commit 4354dc4

Browse files
Add date/number format settings to global config (#33138)
Co-authored-by: Arman Jivanyan <arman.jivanyan@devexpress.com>
1 parent 2fb4ba4 commit 4354dc4

45 files changed

Lines changed: 1861 additions & 60 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import {
2+
afterEach, beforeEach, describe, expect, it,
3+
} from '@jest/globals';
4+
import dateLocalization from '@js/common/core/localization/date';
5+
import config from '@js/core/config';
6+
7+
const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const;
8+
type GlobalFormatKey = typeof GLOBAL_FORMAT_KEYS[number];
9+
10+
const saveAndRestore = (): { save: () => void; restore: () => void } => {
11+
let savedValues: Partial<Record<GlobalFormatKey, unknown>> = {};
12+
13+
return {
14+
save() {
15+
const currentConfig = config();
16+
17+
savedValues = {};
18+
GLOBAL_FORMAT_KEYS.forEach((key) => {
19+
savedValues[key] = currentConfig[key];
20+
});
21+
},
22+
restore() {
23+
const currentConfig = config();
24+
25+
GLOBAL_FORMAT_KEYS.forEach((key) => {
26+
if (savedValues[key] === undefined) {
27+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
28+
delete currentConfig[key];
29+
} else {
30+
currentConfig[key] = savedValues[key] as never;
31+
}
32+
});
33+
},
34+
};
35+
};
36+
37+
describe('date localization - dateTimeFormatPresets', () => {
38+
const { save, restore } = saveAndRestore();
39+
40+
beforeEach(() => { save(); });
41+
afterEach(() => { restore(); });
42+
43+
describe('string preset override', () => {
44+
it('should override shortDate with custom LDML pattern', () => {
45+
config({
46+
...config(),
47+
dateTimeFormatPresets: {
48+
shortDate: 'dd/MM/yyyy',
49+
},
50+
});
51+
52+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
53+
54+
expect(result).toBe('02/01/2020');
55+
});
56+
57+
it('should override shortTime with custom LDML pattern', () => {
58+
config({
59+
...config(),
60+
dateTimeFormatPresets: {
61+
shortTime: 'HH:mm:ss',
62+
},
63+
});
64+
65+
const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5, 30), 'shortTime');
66+
67+
expect(result).toBe('14:05:30');
68+
});
69+
70+
it('should override longDate with custom LDML pattern', () => {
71+
config({
72+
...config(),
73+
dateTimeFormatPresets: {
74+
longDate: 'dd MMMM yyyy',
75+
},
76+
});
77+
78+
const result = dateLocalization.format(new Date(2020, 0, 2), 'longDate');
79+
80+
expect(result).toBe('02 January 2020');
81+
});
82+
83+
it('should override shortDateShortTime with custom LDML pattern', () => {
84+
config({
85+
...config(),
86+
dateTimeFormatPresets: {
87+
shortDateShortTime: 'dd/MM/yyyy HH:mm',
88+
},
89+
});
90+
91+
const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), 'shortDateShortTime');
92+
93+
expect(result).toBe('02/01/2020 14:05');
94+
});
95+
});
96+
97+
describe('function preset override', () => {
98+
it('should use function override for shortDate', () => {
99+
config({
100+
...config(),
101+
dateTimeFormatPresets: {
102+
shortDate: (d: Date) => `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`,
103+
},
104+
});
105+
106+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
107+
108+
expect(result).toBe('2-1-2020');
109+
});
110+
111+
it('should use function override for shortTime', () => {
112+
config({
113+
...config(),
114+
dateTimeFormatPresets: {
115+
shortTime: (d: Date) => `${d.getHours()}h${String(d.getMinutes()).padStart(2, '0')}`,
116+
},
117+
});
118+
119+
const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), 'shortTime');
120+
121+
expect(result).toBe('14h05');
122+
});
123+
});
124+
125+
describe('case insensitivity', () => {
126+
it('should apply override regardless of case in format name', () => {
127+
config({
128+
...config(),
129+
dateTimeFormatPresets: {
130+
shortDate: 'dd/MM/yyyy',
131+
},
132+
});
133+
134+
const date = new Date(2020, 0, 2);
135+
136+
expect(dateLocalization.format(date, 'shortdate')).toBe('02/01/2020');
137+
expect(dateLocalization.format(date, 'SHORTDATE')).toBe('02/01/2020');
138+
expect(dateLocalization.format(date, 'ShortDate')).toBe('02/01/2020');
139+
});
140+
});
141+
142+
describe('locale map in preset', () => {
143+
it('should resolve preset with default locale', () => {
144+
config({
145+
...config(),
146+
dateTimeFormatPresets: {
147+
shortDate: {
148+
default: 'dd/MM/yyyy',
149+
'de-DE': 'dd.MM.yyyy',
150+
},
151+
},
152+
});
153+
154+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
155+
156+
expect(result).toBe('02/01/2020');
157+
});
158+
});
159+
160+
describe('no override', () => {
161+
it('should use built-in format when no preset override is configured', () => {
162+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
163+
164+
// Built-in Intl format for en locale
165+
expect(result).toBeTruthy();
166+
expect(typeof result).toBe('string');
167+
});
168+
169+
it('should leave non-preset string formats unaffected', () => {
170+
config({
171+
...config(),
172+
dateTimeFormatPresets: {
173+
shortDate: 'dd/MM/yyyy',
174+
},
175+
});
176+
177+
const result = dateLocalization.format(new Date(2020, 0, 2), 'yyyy-MM-dd');
178+
179+
// LDML pattern should be used directly, not affected by preset overrides
180+
expect(result).toBe('2020-01-02');
181+
});
182+
183+
it('should leave FormatObject formats unaffected', () => {
184+
config({
185+
...config(),
186+
dateTimeFormatPresets: {
187+
shortDate: 'dd/MM/yyyy',
188+
},
189+
});
190+
191+
const customFormatter = (value: number | Date): string => {
192+
const d = value instanceof Date ? value : new Date(value);
193+
return `custom:${d.getFullYear()}`;
194+
};
195+
const result = dateLocalization.format(new Date(2020, 0, 2), { formatter: customFormatter });
196+
197+
expect(result).toBe('custom:2020');
198+
});
199+
200+
it('should not affect formatting when dateTimeFormatPresets is empty', () => {
201+
config({
202+
...config(),
203+
dateTimeFormatPresets: {},
204+
});
205+
206+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
207+
208+
expect(result).toBeTruthy();
209+
expect(typeof result).toBe('string');
210+
});
211+
});
212+
213+
describe('unknown preset key', () => {
214+
it('should safely ignore unknown preset keys', () => {
215+
config({
216+
...config(),
217+
dateTimeFormatPresets: {
218+
unknownFormat: 'dd/MM/yyyy',
219+
},
220+
});
221+
222+
// Known presets should still work normally
223+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
224+
225+
expect(result).toBeTruthy();
226+
expect(typeof result).toBe('string');
227+
});
228+
});
229+
230+
describe('preset override aliases another preset', () => {
231+
it('should support aliasing one preset to another', () => {
232+
config({
233+
...config(),
234+
dateTimeFormatPresets: {
235+
shortDate: 'longDate',
236+
},
237+
});
238+
239+
const dateLong = dateLocalization.format(new Date(2020, 0, 2), 'longDate');
240+
const dateShort = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
241+
242+
// shortDate should now format like longDate
243+
expect(dateShort).toBe(dateLong);
244+
});
245+
});
246+
});
247+
248+
describe('date localization - global *Format precedence', () => {
249+
const { save, restore } = saveAndRestore();
250+
251+
beforeEach(() => { save(); });
252+
afterEach(() => { restore(); });
253+
254+
it('should apply dateFormat for direct calls with the resolved format', () => {
255+
config({
256+
...config(),
257+
dateFormat: 'dd/MM/yyyy',
258+
});
259+
260+
const result = dateLocalization.format(new Date(2020, 0, 2), config().dateFormat);
261+
262+
expect(result).toBe('02/01/2020');
263+
});
264+
265+
it('should apply dateTimeFormat for direct calls with the resolved format', () => {
266+
config({
267+
...config(),
268+
dateTimeFormat: 'dd/MM/yyyy, HH:mm',
269+
});
270+
271+
const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), config().dateTimeFormat);
272+
273+
expect(result).toBe('02/01/2020, 14:05');
274+
});
275+
});

packages/devextreme/js/__internal/core/localization/date.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getFormatter as getLDMLDateFormatter } from '@ts/core/localization/ldml
99
import { getParser as getLDMLDateParser } from '@ts/core/localization/ldml/date.parser';
1010
import numberLocalization from '@ts/core/localization/number';
1111
import errors from '@ts/core/m_errors';
12+
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
1213
import { injector as dependencyInjector } from '@ts/core/utils/m_dependency_injector';
1314
import { each } from '@ts/core/utils/m_iterator';
1415
import { isString } from '@ts/core/utils/m_type';
@@ -67,6 +68,31 @@ const dateLocalization = dependencyInjector({
6768
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
6869
return this._getPatternByFormat(pattern) || pattern;
6970
},
71+
_resolveStringFormat(
72+
format: string,
73+
date: Date,
74+
): string | undefined {
75+
const presetOverride = resolvePresetOverride(format);
76+
77+
if (presetOverride === undefined) {
78+
return undefined;
79+
}
80+
if (typeof presetOverride === 'function') {
81+
return (presetOverride as DateFormatter)(date);
82+
}
83+
if (isString(presetOverride)) {
84+
const pattern = FORMATS_TO_PATTERN_MAP[
85+
(presetOverride as string).toLowerCase()
86+
] || presetOverride as string;
87+
88+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
89+
return numberLocalization.convertDigits(
90+
getLDMLDateFormatter(pattern, this)(date),
91+
);
92+
}
93+
94+
return undefined;
95+
},
7096
formatUsesMonthName(format: string): boolean {
7197
return this._expandPattern(format).indexOf('MMMM') !== -1;
7298
},
@@ -139,6 +165,13 @@ const dateLocalization = dependencyInjector({
139165
// eslint-disable-next-line no-param-reassign
140166
format = (format as FormatObject).type ?? format;
141167
if (isString(format)) {
168+
const resolvedFormat = this._resolveStringFormat(format as string, date);
169+
170+
if (resolvedFormat !== undefined) {
171+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
172+
return resolvedFormat;
173+
}
174+
142175
// eslint-disable-next-line no-param-reassign
143176
format = (FORMATS_TO_PATTERN_MAP[(format as string).toLowerCase()] || format) as string;
144177

packages/devextreme/js/__internal/core/localization/globalize/date.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'globalize/date';
66
import type { Format as LocalizationFormat, FormatObject } from '@js/localization';
77
import type { DateFormatter, DateParser, Format } from '@ts/core/localization/date';
88
import dateLocalization from '@ts/core/localization/date';
9+
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
910
import * as iteratorUtils from '@ts/core/utils/m_iterator';
1011
import { isObject } from '@ts/core/utils/m_type';
1112
// eslint-disable-next-line import/no-extraneous-dependencies
@@ -186,6 +187,23 @@ if (Globalize?.formatDate) {
186187
format = (format as FormatObject).type ?? format;
187188

188189
if (typeof format === 'string') {
190+
const presetOverride = resolvePresetOverride(format);
191+
192+
if (presetOverride !== undefined) {
193+
if (typeof presetOverride === 'function') {
194+
return (presetOverride as DateFormatter)(date);
195+
}
196+
if (typeof presetOverride === 'string') {
197+
// eslint-disable-next-line no-param-reassign
198+
format = presetOverride;
199+
} else if (isObject(presetOverride) && this._isAcceptableFormat(presetOverride)) {
200+
formatter = Globalize.dateFormatter(presetOverride);
201+
202+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
203+
return this.removeRtlMarks(formatter(date));
204+
}
205+
}
206+
189207
formatCacheKey = `${Globalize.locale().locale}:${format}`;
190208
formatter = formattersCache[formatCacheKey];
191209
if (!formatter) {

packages/devextreme/js/__internal/core/localization/intl/date.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { Format as LocalizationFormat, FormatObject } from '@js/localization';
33
import localizationCoreUtils from '@ts/core/localization/core';
44
import type { DateFormatter, Format } from '@ts/core/localization/date';
5+
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
56
import { extend } from '@ts/core/utils/m_extend';
67

78
interface DateArgs {
@@ -237,6 +238,19 @@ export default {
237238
// eslint-disable-next-line no-param-reassign
238239
format = (format as FormatObject).type ?? format;
239240
}
241+
242+
if (typeof format === 'string') {
243+
const presetOverride = resolvePresetOverride(format);
244+
245+
if (presetOverride !== undefined) {
246+
if (typeof presetOverride === 'function') {
247+
return (presetOverride as DateFormatter)(date);
248+
}
249+
// eslint-disable-next-line no-param-reassign
250+
format = presetOverride as LocalizationFormat;
251+
}
252+
}
253+
240254
const intlFormat = getIntlFormat(format);
241255

242256
if (intlFormat) {

0 commit comments

Comments
 (0)