Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6716ca7
Global format config. Add tests
GoodDayForSurf Apr 2, 2026
a434b9d
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 2, 2026
3a8179f
Merge branch '26_1' into 26_1_global_format
GoodDayForSurf Apr 2, 2026
722f5ca
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 2, 2026
0ec8e32
Merge branch '26_1' into 26_1_global_format
GoodDayForSurf Apr 2, 2026
d1e97af
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 2, 2026
a862455
Merge remote-tracking branch 'my/26_1_global_format' into 26_1_global…
GoodDayForSurf Apr 2, 2026
2922132
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 2, 2026
57dad57
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 2, 2026
026930e
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 2, 2026
bd86b8a
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 3, 2026
bd3bcbd
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 3, 2026
77d63d2
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 3, 2026
dbd3064
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 3, 2026
73904b5
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 3, 2026
a58d9ee
Global format config. Implementation for dateFormat, dateTimeFormat, …
GoodDayForSurf Apr 3, 2026
9c64a68
Merge branch '26_1' into 26_1_global_format
GoodDayForSurf Apr 3, 2026
9e3c6fc
Merge branch '26_1' into 26_1_global_format
GoodDayForSurf Apr 3, 2026
f0033b2
Merge branch '26_1' of https://github.com/DevExpress/DevExtreme into …
GoodDayForSurf Apr 15, 2026
b5bcacb
add dateTimePreset, tests and more minor changes
ajivanyandev Apr 16, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import {
afterEach, beforeEach, describe, expect, it,
} from '@jest/globals';
import dateLocalization from '@js/common/core/localization/date';
import config from '@js/core/config';

const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const;

const saveAndRestore = (): { save: () => void; restore: () => void } => {
let savedValues: Record<string, unknown> = {};

return {
save() {
const currentConfig = config();

savedValues = {};
GLOBAL_FORMAT_KEYS.forEach((key) => {
savedValues[key] = currentConfig[key];
});
},
restore() {
const currentConfig = config();

GLOBAL_FORMAT_KEYS.forEach((key) => {
if (savedValues[key] === undefined) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete currentConfig[key];
} else {
currentConfig[key] = savedValues[key];
}
});
},
};
};

describe('date localization - dateTimeFormatPresets', () => {
const { save, restore } = saveAndRestore();

beforeEach(() => { save(); });
afterEach(() => { restore(); });

describe('string preset override', () => {
it('should override shortDate with custom LDML pattern', () => {
config({
...config(),
dateTimeFormatPresets: {
shortDate: 'dd/MM/yyyy',
},
});

const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');

expect(result).toBe('02/01/2020');
});

it('should override shortTime with custom LDML pattern', () => {
config({
...config(),
dateTimeFormatPresets: {
shortTime: 'HH:mm:ss',
},
});

const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5, 30), 'shortTime');

expect(result).toBe('14:05:30');
});

it('should override longDate with custom LDML pattern', () => {
config({
...config(),
dateTimeFormatPresets: {
longDate: 'dd MMMM yyyy',
},
});

const result = dateLocalization.format(new Date(2020, 0, 2), 'longDate');

expect(result).toBe('02 January 2020');
});

it('should override shortDateShortTime with custom LDML pattern', () => {
config({
...config(),
dateTimeFormatPresets: {
shortDateShortTime: 'dd/MM/yyyy HH:mm',
},
});

const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), 'shortDateShortTime');

expect(result).toBe('02/01/2020 14:05');
});
});

describe('function preset override', () => {
it('should use function override for shortDate', () => {
config({
...config(),
dateTimeFormatPresets: {
shortDate: (d: Date) => `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`,
},
});

const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');

expect(result).toBe('2-1-2020');
});

it('should use function override for shortTime', () => {
config({
...config(),
dateTimeFormatPresets: {
shortTime: (d: Date) => `${d.getHours()}h${String(d.getMinutes()).padStart(2, '0')}`,
},
});

const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), 'shortTime');

expect(result).toBe('14h05');
});
});

describe('case insensitivity', () => {
it('should apply override regardless of case in format name', () => {
config({
...config(),
dateTimeFormatPresets: {
shortDate: 'dd/MM/yyyy',
},
});

const date = new Date(2020, 0, 2);

expect(dateLocalization.format(date, 'shortdate')).toBe('02/01/2020');
expect(dateLocalization.format(date, 'SHORTDATE')).toBe('02/01/2020');
expect(dateLocalization.format(date, 'ShortDate')).toBe('02/01/2020');
});
});

describe('locale map in preset', () => {
it('should resolve preset with default locale', () => {
config({
...config(),
dateTimeFormatPresets: {
shortDate: {
default: 'dd/MM/yyyy',
'de-DE': 'dd.MM.yyyy',
},
},
});

const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');

expect(result).toBe('02/01/2020');
});
});

describe('no override', () => {
it('should use built-in format when no preset override is configured', () => {
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');

// Built-in Intl format for en locale
expect(result).toBeTruthy();
expect(typeof result).toBe('string');
});

it('should leave non-preset string formats unaffected', () => {
config({
...config(),
dateTimeFormatPresets: {
shortDate: 'dd/MM/yyyy',
},
});

const result = dateLocalization.format(new Date(2020, 0, 2), 'yyyy-MM-dd');

// LDML pattern should be used directly, not affected by preset overrides
expect(result).toBe('2020-01-02');
});

it('should leave FormatObject formats unaffected', () => {
config({
...config(),
dateTimeFormatPresets: {
shortDate: 'dd/MM/yyyy',
},
});

const customFormatter = (d: Date): string => `custom:${d.getFullYear()}`;
const result = dateLocalization.format(new Date(2020, 0, 2), { formatter: customFormatter });

expect(result).toBe('custom:2020');
});

it('should not affect formatting when dateTimeFormatPresets is empty', () => {
config({
...config(),
dateTimeFormatPresets: {},
});

const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');

expect(result).toBeTruthy();
expect(typeof result).toBe('string');
});
});

describe('unknown preset key', () => {
it('should safely ignore unknown preset keys', () => {
config({
...config(),
dateTimeFormatPresets: {
unknownFormat: 'dd/MM/yyyy',
},
});

// Known presets should still work normally
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');

expect(result).toBeTruthy();
expect(typeof result).toBe('string');
});
});

describe('preset override aliases another preset', () => {
it('should support aliasing one preset to another', () => {
config({
...config(),
dateTimeFormatPresets: {
shortDate: 'longDate',
},
});

const dateLong = dateLocalization.format(new Date(2020, 0, 2), 'longDate');
const dateShort = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');

// shortDate should now format like longDate
expect(dateShort).toBe(dateLong);
});
});
});

describe('date localization - global *Format precedence', () => {
const { save, restore } = saveAndRestore();

beforeEach(() => { save(); });
afterEach(() => { restore(); });

it('should apply dateFormat for direct calls with the resolved format', () => {
config({
...config(),
dateFormat: 'dd/MM/yyyy',
});

const result = dateLocalization.format(new Date(2020, 0, 2), config().dateFormat);

expect(result).toBe('02/01/2020');
});

it('should apply dateTimeFormat for direct calls with the resolved format', () => {
config({
...config(),
dateTimeFormat: 'dd/MM/yyyy, HH:mm',
});

const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), config().dateTimeFormat);

expect(result).toBe('02/01/2020, 14:05');
});
});
33 changes: 33 additions & 0 deletions packages/devextreme/js/__internal/core/localization/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getFormatter as getLDMLDateFormatter } from '@ts/core/localization/ldml
import { getParser as getLDMLDateParser } from '@ts/core/localization/ldml/date.parser';
import numberLocalization from '@ts/core/localization/number';
import errors from '@ts/core/m_errors';
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
import { injector as dependencyInjector } from '@ts/core/utils/m_dependency_injector';
import { each } from '@ts/core/utils/m_iterator';
import { isString } from '@ts/core/utils/m_type';
Expand Down Expand Up @@ -67,6 +68,31 @@ const dateLocalization = dependencyInjector({
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this._getPatternByFormat(pattern) || pattern;
},
_resolveStringFormat(
format: string,
date: Date,
): string | undefined {
const presetOverride = resolvePresetOverride(format);

if (presetOverride === undefined) {
return undefined;
}
if (typeof presetOverride === 'function') {
return (presetOverride as DateFormatter)(date);
}
if (isString(presetOverride)) {
const pattern = FORMATS_TO_PATTERN_MAP[
(presetOverride as string).toLowerCase()
] || presetOverride as string;

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return numberLocalization.convertDigits(
getLDMLDateFormatter(pattern, this)(date),
);
}

return undefined;
},
formatUsesMonthName(format: string): boolean {
return this._expandPattern(format).indexOf('MMMM') !== -1;
},
Expand Down Expand Up @@ -139,6 +165,13 @@ const dateLocalization = dependencyInjector({
// eslint-disable-next-line no-param-reassign
format = (format as FormatObject).type ?? format;
if (isString(format)) {
const resolvedFormat = this._resolveStringFormat(format as string, date);

if (resolvedFormat !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return resolvedFormat;
}

// eslint-disable-next-line no-param-reassign
format = (FORMATS_TO_PATTERN_MAP[(format as string).toLowerCase()] || format) as string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'globalize/date';
import type { Format as LocalizationFormat, FormatObject } from '@js/localization';
import type { DateFormatter, DateParser, Format } from '@ts/core/localization/date';
import dateLocalization from '@ts/core/localization/date';
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
import * as iteratorUtils from '@ts/core/utils/m_iterator';
import { isObject } from '@ts/core/utils/m_type';
// eslint-disable-next-line import/no-extraneous-dependencies
Expand Down Expand Up @@ -186,6 +187,23 @@ if (Globalize?.formatDate) {
format = (format as FormatObject).type ?? format;

if (typeof format === 'string') {
const presetOverride = resolvePresetOverride(format);

if (presetOverride !== undefined) {
if (typeof presetOverride === 'function') {
return (presetOverride as DateFormatter)(date);
}
if (typeof presetOverride === 'string') {
// eslint-disable-next-line no-param-reassign
format = presetOverride;
} else if (isObject(presetOverride) && this._isAcceptableFormat(presetOverride)) {
formatter = Globalize.dateFormatter(presetOverride);

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this.removeRtlMarks(formatter(date));
}
}

formatCacheKey = `${Globalize.locale().locale}:${format}`;
formatter = formattersCache[formatCacheKey];
if (!formatter) {
Expand Down
14 changes: 14 additions & 0 deletions packages/devextreme/js/__internal/core/localization/intl/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { Format as LocalizationFormat, FormatObject } from '@js/localization';
import localizationCoreUtils from '@ts/core/localization/core';
import type { DateFormatter, Format } from '@ts/core/localization/date';
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
import { extend } from '@ts/core/utils/m_extend';

interface DateArgs {
Expand Down Expand Up @@ -237,6 +238,19 @@ export default {
// eslint-disable-next-line no-param-reassign
format = (format as FormatObject).type ?? format;
}

if (typeof format === 'string') {
const presetOverride = resolvePresetOverride(format);

if (presetOverride !== undefined) {
if (typeof presetOverride === 'function') {
return (presetOverride as DateFormatter)(date);
}
// eslint-disable-next-line no-param-reassign
format = presetOverride as LocalizationFormat;
}
}

const intlFormat = getIntlFormat(format);

if (intlFormat) {
Expand Down
Loading
Loading