diff --git a/packages/devextreme-angular/src/common/core/localization/index.ts b/packages/devextreme-angular/src/common/core/localization/index.ts index 6e576bc9892d..21c8dc650fff 100644 --- a/packages/devextreme-angular/src/common/core/localization/index.ts +++ b/packages/devextreme-angular/src/common/core/localization/index.ts @@ -9,4 +9,5 @@ export { } from 'devextreme/common/core/localization'; export type { Format, + FormatLocale, } from 'devextreme/common/core/localization'; diff --git a/packages/devextreme-angular/src/common/index.ts b/packages/devextreme-angular/src/common/index.ts index 307555af2236..93fe65c177f6 100644 --- a/packages/devextreme-angular/src/common/index.ts +++ b/packages/devextreme-angular/src/common/index.ts @@ -189,6 +189,7 @@ export namespace Core { export namespace Localization { export type Format = CoreLocalizationModule.Format; export const formatDate = CoreLocalizationModule.formatDate; + export type FormatLocale = CoreLocalizationModule.FormatLocale; export const formatMessage = CoreLocalizationModule.formatMessage; export const formatNumber = CoreLocalizationModule.formatNumber; export const loadMessages = CoreLocalizationModule.loadMessages; diff --git a/packages/devextreme-angular/src/index.ts b/packages/devextreme-angular/src/index.ts index 3624a2e345a9..5a51cf8fac4b 100644 --- a/packages/devextreme-angular/src/index.ts +++ b/packages/devextreme-angular/src/index.ts @@ -272,6 +272,7 @@ export namespace Common { export namespace Localization { export type Format = import('devextreme/common/core/localization').Format; export const formatDate = (CoreLocalizationModule as any).formatDate as typeof import('devextreme/common/core/localization').formatDate; + export type FormatLocale = import('devextreme/common/core/localization').FormatLocale; export const formatMessage = (CoreLocalizationModule as any).formatMessage as typeof import('devextreme/common/core/localization').formatMessage; export const formatNumber = (CoreLocalizationModule as any).formatNumber as typeof import('devextreme/common/core/localization').formatNumber; export const loadMessages = (CoreLocalizationModule as any).loadMessages as typeof import('devextreme/common/core/localization').loadMessages; diff --git a/packages/devextreme-angular/src/ui/bar-gauge/nested/format.ts b/packages/devextreme-angular/src/ui/bar-gauge/nested/format.ts index db327f231af4..7fcd36dcebeb 100644 --- a/packages/devextreme-angular/src/ui/bar-gauge/nested/format.ts +++ b/packages/devextreme-angular/src/ui/bar-gauge/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoBarGaugeFormatComponent extends NestedOption implements OnDestro this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/bar-gauge/nested/item-text-format.ts b/packages/devextreme-angular/src/ui/bar-gauge/nested/item-text-format.ts index 8362fbbd0c48..ba0691c53173 100644 --- a/packages/devextreme-angular/src/ui/bar-gauge/nested/item-text-format.ts +++ b/packages/devextreme-angular/src/ui/bar-gauge/nested/item-text-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoBarGaugeItemTextFormatComponent extends NestedOption implements this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/bullet/nested/format.ts b/packages/devextreme-angular/src/ui/bullet/nested/format.ts index 0811e8fd12f1..bba24f313559 100644 --- a/packages/devextreme-angular/src/ui/bullet/nested/format.ts +++ b/packages/devextreme-angular/src/ui/bullet/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoBulletFormatComponent extends NestedOption implements OnDestroy, this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/card-view/nested/format.ts b/packages/devextreme-angular/src/ui/card-view/nested/format.ts index 2a431d8cac74..2773a8ce4b5b 100644 --- a/packages/devextreme-angular/src/ui/card-view/nested/format.ts +++ b/packages/devextreme-angular/src/ui/card-view/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoCardViewFormatComponent extends NestedOption implements OnDestro this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/chart/nested/argument-format.ts b/packages/devextreme-angular/src/ui/chart/nested/argument-format.ts index 6f732bc24387..2263aee3cdb7 100644 --- a/packages/devextreme-angular/src/ui/chart/nested/argument-format.ts +++ b/packages/devextreme-angular/src/ui/chart/nested/argument-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoChartArgumentFormatComponent extends NestedOption implements OnD this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/chart/nested/format.ts b/packages/devextreme-angular/src/ui/chart/nested/format.ts index f074b4b35429..a366bb866693 100644 --- a/packages/devextreme-angular/src/ui/chart/nested/format.ts +++ b/packages/devextreme-angular/src/ui/chart/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoChartFormatComponent extends NestedOption implements OnDestroy, this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/chat/nested/day-header-format.ts b/packages/devextreme-angular/src/ui/chat/nested/day-header-format.ts index 6ccfc697bb34..7a354e1e6d77 100644 --- a/packages/devextreme-angular/src/ui/chat/nested/day-header-format.ts +++ b/packages/devextreme-angular/src/ui/chat/nested/day-header-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoChatDayHeaderFormatComponent extends NestedOption implements OnD this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/chat/nested/message-timestamp-format.ts b/packages/devextreme-angular/src/ui/chat/nested/message-timestamp-format.ts index 97d0dec11275..c955a9dc6f9e 100644 --- a/packages/devextreme-angular/src/ui/chat/nested/message-timestamp-format.ts +++ b/packages/devextreme-angular/src/ui/chat/nested/message-timestamp-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoChatMessageTimestampFormatComponent extends NestedOption impleme this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/circular-gauge/nested/format.ts b/packages/devextreme-angular/src/ui/circular-gauge/nested/format.ts index 9aa1a9aeddd0..9950c331eda3 100644 --- a/packages/devextreme-angular/src/ui/circular-gauge/nested/format.ts +++ b/packages/devextreme-angular/src/ui/circular-gauge/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoCircularGaugeFormatComponent extends NestedOption implements OnD this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/data-grid/nested/format.ts b/packages/devextreme-angular/src/ui/data-grid/nested/format.ts index de2a2c42b49f..0337290799af 100644 --- a/packages/devextreme-angular/src/ui/data-grid/nested/format.ts +++ b/packages/devextreme-angular/src/ui/data-grid/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoDataGridFormatComponent extends NestedOption implements OnDestro this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/data-grid/nested/value-format.ts b/packages/devextreme-angular/src/ui/data-grid/nested/value-format.ts index b5ee1b72b209..5d4164094424 100644 --- a/packages/devextreme-angular/src/ui/data-grid/nested/value-format.ts +++ b/packages/devextreme-angular/src/ui/data-grid/nested/value-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoDataGridValueFormatComponent extends NestedOption implements OnD this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/date-box/nested/display-format.ts b/packages/devextreme-angular/src/ui/date-box/nested/display-format.ts index fd5f998d0f76..3f0b4417767b 100644 --- a/packages/devextreme-angular/src/ui/date-box/nested/display-format.ts +++ b/packages/devextreme-angular/src/ui/date-box/nested/display-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoDateBoxDisplayFormatComponent extends NestedOption implements On this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/date-range-box/nested/display-format.ts b/packages/devextreme-angular/src/ui/date-range-box/nested/display-format.ts index 8162dfc563a6..71b5a8e659cd 100644 --- a/packages/devextreme-angular/src/ui/date-range-box/nested/display-format.ts +++ b/packages/devextreme-angular/src/ui/date-range-box/nested/display-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoDateRangeBoxDisplayFormatComponent extends NestedOption implemen this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/filter-builder/nested/format.ts b/packages/devextreme-angular/src/ui/filter-builder/nested/format.ts index dc14cc5721fd..dd5f78bf06fc 100644 --- a/packages/devextreme-angular/src/ui/filter-builder/nested/format.ts +++ b/packages/devextreme-angular/src/ui/filter-builder/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoFilterBuilderFormatComponent extends NestedOption implements OnD this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/funnel/nested/format.ts b/packages/devextreme-angular/src/ui/funnel/nested/format.ts index a02ec22bf02f..6984a89f50ca 100644 --- a/packages/devextreme-angular/src/ui/funnel/nested/format.ts +++ b/packages/devextreme-angular/src/ui/funnel/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoFunnelFormatComponent extends NestedOption implements OnDestroy, this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/gantt/nested/format.ts b/packages/devextreme-angular/src/ui/gantt/nested/format.ts index ff85a6689ab0..23ef790b9079 100644 --- a/packages/devextreme-angular/src/ui/gantt/nested/format.ts +++ b/packages/devextreme-angular/src/ui/gantt/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoGanttFormatComponent extends NestedOption implements OnDestroy, this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/linear-gauge/nested/format.ts b/packages/devextreme-angular/src/ui/linear-gauge/nested/format.ts index 4bf5ae8b2fc2..3ddbdb447756 100644 --- a/packages/devextreme-angular/src/ui/linear-gauge/nested/format.ts +++ b/packages/devextreme-angular/src/ui/linear-gauge/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoLinearGaugeFormatComponent extends NestedOption implements OnDes this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/number-box/nested/format.ts b/packages/devextreme-angular/src/ui/number-box/nested/format.ts index c929f38077b1..ceb353076c28 100644 --- a/packages/devextreme-angular/src/ui/number-box/nested/format.ts +++ b/packages/devextreme-angular/src/ui/number-box/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoNumberBoxFormatComponent extends NestedOption implements OnDestr this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/pie-chart/nested/argument-format.ts b/packages/devextreme-angular/src/ui/pie-chart/nested/argument-format.ts index a45c15dcd46b..3c69d35e45cd 100644 --- a/packages/devextreme-angular/src/ui/pie-chart/nested/argument-format.ts +++ b/packages/devextreme-angular/src/ui/pie-chart/nested/argument-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoPieChartArgumentFormatComponent extends NestedOption implements this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/pie-chart/nested/format.ts b/packages/devextreme-angular/src/ui/pie-chart/nested/format.ts index a87e8ddccaed..3ceee99b85b0 100644 --- a/packages/devextreme-angular/src/ui/pie-chart/nested/format.ts +++ b/packages/devextreme-angular/src/ui/pie-chart/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoPieChartFormatComponent extends NestedOption implements OnDestro this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/polar-chart/nested/argument-format.ts b/packages/devextreme-angular/src/ui/polar-chart/nested/argument-format.ts index 45d01b92a105..f6ecfd0b4f4a 100644 --- a/packages/devextreme-angular/src/ui/polar-chart/nested/argument-format.ts +++ b/packages/devextreme-angular/src/ui/polar-chart/nested/argument-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoPolarChartArgumentFormatComponent extends NestedOption implement this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/polar-chart/nested/format.ts b/packages/devextreme-angular/src/ui/polar-chart/nested/format.ts index 5ea27a77569b..9587438cae68 100644 --- a/packages/devextreme-angular/src/ui/polar-chart/nested/format.ts +++ b/packages/devextreme-angular/src/ui/polar-chart/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoPolarChartFormatComponent extends NestedOption implements OnDest this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/range-selector/nested/argument-format.ts b/packages/devextreme-angular/src/ui/range-selector/nested/argument-format.ts index 4e2a7c56323e..5162092542bb 100644 --- a/packages/devextreme-angular/src/ui/range-selector/nested/argument-format.ts +++ b/packages/devextreme-angular/src/ui/range-selector/nested/argument-format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoRangeSelectorArgumentFormatComponent extends NestedOption implem this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/range-selector/nested/format.ts b/packages/devextreme-angular/src/ui/range-selector/nested/format.ts index 9f47d5414d11..977e8eada7d5 100644 --- a/packages/devextreme-angular/src/ui/range-selector/nested/format.ts +++ b/packages/devextreme-angular/src/ui/range-selector/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoRangeSelectorFormatComponent extends NestedOption implements OnD this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/range-slider/nested/format.ts b/packages/devextreme-angular/src/ui/range-slider/nested/format.ts index 64849ffee3fd..59265c6f8718 100644 --- a/packages/devextreme-angular/src/ui/range-slider/nested/format.ts +++ b/packages/devextreme-angular/src/ui/range-slider/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoRangeSliderFormatComponent extends NestedOption implements OnDes this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/sankey/nested/format.ts b/packages/devextreme-angular/src/ui/sankey/nested/format.ts index 6ef55d44c398..60997263ec5a 100644 --- a/packages/devextreme-angular/src/ui/sankey/nested/format.ts +++ b/packages/devextreme-angular/src/ui/sankey/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoSankeyFormatComponent extends NestedOption implements OnDestroy, this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/slider/nested/format.ts b/packages/devextreme-angular/src/ui/slider/nested/format.ts index 4b771dd5f8ff..c6270638fab3 100644 --- a/packages/devextreme-angular/src/ui/slider/nested/format.ts +++ b/packages/devextreme-angular/src/ui/slider/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoSliderFormatComponent extends NestedOption implements OnDestroy, this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/sparkline/nested/format.ts b/packages/devextreme-angular/src/ui/sparkline/nested/format.ts index 78024ae92d18..4ef413a0f5bb 100644 --- a/packages/devextreme-angular/src/ui/sparkline/nested/format.ts +++ b/packages/devextreme-angular/src/ui/sparkline/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoSparklineFormatComponent extends NestedOption implements OnDestr this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/tree-list/nested/format.ts b/packages/devextreme-angular/src/ui/tree-list/nested/format.ts index d36a8fec8795..7246a36e5568 100644 --- a/packages/devextreme-angular/src/ui/tree-list/nested/format.ts +++ b/packages/devextreme-angular/src/ui/tree-list/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoTreeListFormatComponent extends NestedOption implements OnDestro this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-angular/src/ui/tree-map/nested/format.ts b/packages/devextreme-angular/src/ui/tree-map/nested/format.ts index 9d13424edea3..8b1f3f9b3662 100644 --- a/packages/devextreme-angular/src/ui/tree-map/nested/format.ts +++ b/packages/devextreme-angular/src/ui/tree-map/nested/format.ts @@ -8,6 +8,7 @@ import { SkipSelf, Input } from '@angular/core'; +import type { FormatLocale } from 'devextreme/common/core/localization'; import type { Format } from 'devextreme/common'; import { @@ -40,6 +41,14 @@ export class DxoTreeMapFormatComponent extends NestedOption implements OnDestroy this._setOption('formatter', value); } + @Input() + get locale(): FormatLocale { + return this._getOption('locale'); + } + set locale(value: FormatLocale) { + this._setOption('locale', value); + } + @Input() get parser(): ((value: string) => number | Date) { return this._getOption('parser'); diff --git a/packages/devextreme-metadata/make-angular-metadata.ts b/packages/devextreme-metadata/make-angular-metadata.ts index 188681c21b41..791b014255aa 100644 --- a/packages/devextreme-metadata/make-angular-metadata.ts +++ b/packages/devextreme-metadata/make-angular-metadata.ts @@ -46,6 +46,7 @@ Ng.makeMetadata({ ...replaceTypesMutations, + removeMembers(/\/localization:FormatObject\.locale/), removeMembers(/\/grids:ColumnBase.ai/), removeMembers(/\/grids:GridBaseOptions.aiAssistant/), removeMembers(/\/grids:AIAssistant/), diff --git a/packages/devextreme-react/src/bar-gauge.ts b/packages/devextreme-react/src/bar-gauge.ts index e572011c3655..4e3d374fc427 100644 --- a/packages/devextreme-react/src/bar-gauge.ts +++ b/packages/devextreme-react/src/bar-gauge.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, TooltipHiddenEvent, TooltipShownEvent, BarGaugeBarInfo, BarGaugeLegendItem } from "devextreme/viz/bar_gauge"; import type { AnimationEaseMode, Font as ChartsFont, TextOverflow, WordWrap, DashStyle } from "devextreme/common/charts"; import type { HorizontalAlignment, VerticalEdge, ExportFormat, Format as CommonFormat, Position, template, Orientation } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -273,6 +273,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; @@ -315,6 +316,7 @@ const Geometry = Object.assign(_ type IItemTextFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/bullet.ts b/packages/devextreme-react/src/bullet.ts index a5e815701ec6..4b17a1fe423d 100644 --- a/packages/devextreme-react/src/bullet.ts +++ b/packages/devextreme-react/src/bullet.ts @@ -10,8 +10,8 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, TooltipHiddenEvent, TooltipShownEvent } from "devextreme/viz/bullet"; import type { DashStyle, Font as ChartsFont } from "devextreme/common/charts"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; import type { Format as CommonFormat, template } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -119,6 +119,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/card-view.ts b/packages/devextreme-react/src/card-view.ts index 2b73f72d900c..87aeca528064 100644 --- a/packages/devextreme-react/src/card-view.ts +++ b/packages/devextreme-react/src/card-view.ts @@ -20,7 +20,7 @@ import type { ContentReadyEvent as TabPanelContentReadyEvent, DisposingEvent as import type { LocateInMenuMode, ShowTextMode } from "devextreme/ui/toolbar"; import type { CollectionWidgetItem } from "devextreme/ui/collection/ui.collection_widget.base"; import type { HeaderFilterSearchConfig, HeaderFilterTexts, SelectionColumnDisplayMode, DataChangeType, FilterType, ColumnHeaderFilter as GridsColumnHeaderFilter, ColumnChooserMode, ColumnChooserSearchConfig, ColumnChooserSelectionConfig, HeaderFilterGroupInterval, ColumnHeaderFilterSearchConfig, DataChange, FilterPanel as GridsFilterPanel, FilterPanelTexts as GridsFilterPanelTexts, PagerPageSize } from "devextreme/common/grids"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { Format as LocalizationFormat, FormatLocale } from "devextreme/common/core/localization"; import type { DataSourceOptions } from "devextreme/data/data_source"; import type { Store } from "devextreme/data/store"; import type { AIIntegration } from "devextreme/common/ai-integration"; @@ -1366,6 +1366,7 @@ const Form = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/chart.ts b/packages/devextreme-react/src/chart.ts index 65e1242de340..0f234fb386a6 100644 --- a/packages/devextreme-react/src/chart.ts +++ b/packages/devextreme-react/src/chart.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { ArgumentAxisClickEvent, DisposingEvent, DoneEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, LegendClickEvent, PointClickEvent, SeriesClickEvent, TooltipHiddenEvent, TooltipShownEvent, ZoomEndEvent, ZoomStartEvent, chartPointAggregationInfoObject, chartSeriesObject, ChartSeriesAggregationMethod, dxChartAnnotationConfig, AggregatedPointsPosition, ChartLabelDisplayMode, FinancialChartReductionLevel, chartPointObject, dxChartPointInfo, ChartTooltipLocation, ChartZoomAndPanMode, EventKeyModifier } from "devextreme/viz/chart"; import type { AnimationEaseMode, DashStyle, Font as ChartsFont, TextOverflow, AnnotationType, WordWrap, TimeInterval, ChartsDataType, ScaleBreak, ScaleBreakLineStyle, RelativePosition, DiscreteAxisDivisionMode, ArgumentAxisHoverMode, ChartsAxisLabelOverlap, AxisScaleType, VisualRangeUpdateMode, ChartsColor, SeriesHoverMode, HatchDirection, PointInteractionMode, PointSymbol, SeriesSelectionMode, SeriesType, ValueErrorBarDisplayMode, ValueErrorBarType, LegendItem, LegendHoverMode, ValueAxisVisualRangeUpdateMode } from "devextreme/common/charts"; import type { template, HorizontalAlignment, VerticalAlignment, Format as CommonFormat, Position, VerticalEdge, ExportFormat, Orientation } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { Format as LocalizationFormat, FormatLocale } from "devextreme/common/core/localization"; import type { ChartSeries } from "devextreme/viz/common"; import type * as CommonChartTypes from "devextreme/common/charts"; @@ -587,6 +587,7 @@ const ArgumentAxis = Object.assign string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; @@ -1921,6 +1922,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/chat.ts b/packages/devextreme-react/src/chat.ts index b536ca714887..9ee42ffca9ad 100644 --- a/packages/devextreme-react/src/chat.ts +++ b/packages/devextreme-react/src/chat.ts @@ -12,6 +12,7 @@ import type { Message, AttachmentDownloadClickEvent, DisposingEvent, Initialized import type { DisposingEvent as FileUploaderDisposingEvent, InitializedEvent as FileUploaderInitializedEvent, BeforeSendEvent, ContentReadyEvent, DropZoneEnterEvent, DropZoneLeaveEvent, FilesUploadedEvent, OptionChangedEvent, ProgressEvent, UploadAbortedEvent, UploadedEvent, UploadErrorEvent, UploadStartedEvent, ValueChangedEvent, UploadHttpMethod, FileUploadMode } from "devextreme/ui/file_uploader"; import type { DisposingEvent as SpeechToTextDisposingEvent, InitializedEvent as SpeechToTextInitializedEvent, ContentReadyEvent as SpeechToTextContentReadyEvent, OptionChangedEvent as SpeechToTextOptionChangedEvent, CustomSpeechRecognizer as SpeechToTextCustomSpeechRecognizer, EndEvent, ErrorEvent, ResultEvent, StartClickEvent, StopClickEvent, SpeechRecognitionConfig as SpeechToTextSpeechRecognitionConfig } from "devextreme/ui/speech_to_text"; import type { DisposingEvent as ButtonGroupDisposingEvent, InitializedEvent as ButtonGroupInitializedEvent, ContentReadyEvent as ButtonGroupContentReadyEvent, OptionChangedEvent as ButtonGroupOptionChangedEvent, dxButtonGroupItem, ItemClickEvent, SelectionChangedEvent } from "devextreme/ui/button_group"; +import type { FormatLocale } from "devextreme/common/core/localization"; import type { Format, ValidationStatus, ButtonType, template, ButtonStyle, SingleMultipleOrNone } from "devextreme/common"; import type { CollectionWidgetItem } from "devextreme/ui/collection/ui.collection_widget.base"; @@ -231,6 +232,7 @@ const CustomSpeechRecognizer = Object.assign string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: Format | string; @@ -405,6 +407,7 @@ const Item = Object.assign(_componen type IMessageTimestampFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: Format | string; diff --git a/packages/devextreme-react/src/circular-gauge.ts b/packages/devextreme-react/src/circular-gauge.ts index 8610e11f92e1..380414e5227a 100644 --- a/packages/devextreme-react/src/circular-gauge.ts +++ b/packages/devextreme-react/src/circular-gauge.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, TooltipHiddenEvent, TooltipShownEvent, CircularGaugeLabelOverlap, CircularGaugeElementOrientation } from "devextreme/viz/circular_gauge"; import type { AnimationEaseMode, DashStyle, Font as ChartsFont, LabelOverlap, ChartsColor, Palette, PaletteExtensionMode, TextOverflow, WordWrap } from "devextreme/common/charts"; import type { ExportFormat, Format as CommonFormat, HorizontalEdge, VerticalEdge, HorizontalAlignment, template } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -246,6 +246,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/common/core/localization.ts b/packages/devextreme-react/src/common/core/localization.ts index 6dbb7f75a2a6..6f35e4992209 100644 --- a/packages/devextreme-react/src/common/core/localization.ts +++ b/packages/devextreme-react/src/common/core/localization.ts @@ -9,4 +9,5 @@ export { } from "devextreme/common/core/localization"; export type { Format, + FormatLocale, } from "devextreme/common/core/localization"; diff --git a/packages/devextreme-react/src/common/index.ts b/packages/devextreme-react/src/common/index.ts index 0b83f3212b65..9abc26031327 100644 --- a/packages/devextreme-react/src/common/index.ts +++ b/packages/devextreme-react/src/common/index.ts @@ -189,6 +189,7 @@ export namespace Core { export namespace Localization { export type Format = CoreLocalizationModule.Format; export const formatDate = CoreLocalizationModule.formatDate; + export type FormatLocale = CoreLocalizationModule.FormatLocale; export const formatMessage = CoreLocalizationModule.formatMessage; export const formatNumber = CoreLocalizationModule.formatNumber; export const loadMessages = CoreLocalizationModule.loadMessages; diff --git a/packages/devextreme-react/src/data-grid.ts b/packages/devextreme-react/src/data-grid.ts index 13c6fbf8cbf0..1579f6f4bb5c 100644 --- a/packages/devextreme-react/src/data-grid.ts +++ b/packages/devextreme-react/src/data-grid.ts @@ -20,7 +20,7 @@ import type { ContentReadyEvent as TabPanelContentReadyEvent, DisposingEvent as import type { AIIntegration } from "devextreme/common/ai-integration"; import type { dxPopupOptions, dxPopupToolbarItem, ToolbarLocation } from "devextreme/ui/popup"; import type { AnimationConfig, CollisionResolution, PositionConfig, AnimationState, AnimationType, CollisionResolutionCombination } from "devextreme/common/core/animation"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { Format as LocalizationFormat, FormatLocale } from "devextreme/common/core/localization"; import type { DataSourceOptions } from "devextreme/data/data_source"; import type { Store } from "devextreme/data/store"; import type { LocateInMenuMode, ShowTextMode } from "devextreme/ui/toolbar"; @@ -1945,6 +1945,7 @@ const Form = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; @@ -3950,6 +3951,7 @@ const ValidationRule = Object.assign string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/date-box.ts b/packages/devextreme-react/src/date-box.ts index 565ccdbb3504..60b3228c6612 100644 --- a/packages/devextreme-react/src/date-box.ts +++ b/packages/devextreme-react/src/date-box.ts @@ -13,6 +13,7 @@ import type { ContentReadyEvent as ButtonContentReadyEvent, DisposingEvent as Bu import type { DisposingEvent as CalendarDisposingEvent, InitializedEvent as CalendarInitializedEvent, ValueChangedEvent as CalendarValueChangedEvent, DisabledDate, CalendarZoomLevel, OptionChangedEvent, CalendarSelectionMode, WeekNumberRule } from "devextreme/ui/calendar"; import type { AnimationConfig, CollisionResolution, PositionConfig, AnimationState, AnimationType, CollisionResolutionCombination } from "devextreme/common/core/animation"; import type { HorizontalAlignment, VerticalAlignment, TextEditorButtonLocation, template, DayOfWeek, ValidationMessageMode, Position as CommonPosition, ValidationStatus, Format, PositionAlignment, Direction, ButtonStyle, ButtonType, ToolbarItemLocation, ToolbarItemComponent } from "devextreme/common"; +import type { FormatLocale } from "devextreme/common/core/localization"; import type { event } from "devextreme/events/events.types"; import type { EventInfo } from "devextreme/common/core/events"; import type { Component } from "devextreme/core/component"; @@ -296,6 +297,7 @@ const Collision = Object.assign type IDisplayFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: Format | string; diff --git a/packages/devextreme-react/src/date-range-box.ts b/packages/devextreme-react/src/date-range-box.ts index a52af5e8f715..085029dc2fa6 100644 --- a/packages/devextreme-react/src/date-range-box.ts +++ b/packages/devextreme-react/src/date-range-box.ts @@ -13,6 +13,7 @@ import type { ContentReadyEvent as ButtonContentReadyEvent, DisposingEvent as Bu import type { DisposingEvent as CalendarDisposingEvent, InitializedEvent as CalendarInitializedEvent, ValueChangedEvent as CalendarValueChangedEvent, DisabledDate, CalendarZoomLevel, OptionChangedEvent, CalendarSelectionMode, WeekNumberRule } from "devextreme/ui/calendar"; import type { AnimationConfig, CollisionResolution, PositionConfig, AnimationState, AnimationType, CollisionResolutionCombination } from "devextreme/common/core/animation"; import type { HorizontalAlignment, VerticalAlignment, TextEditorButtonLocation, template, DayOfWeek, ValidationMessageMode, Position as CommonPosition, ValidationStatus, Format, PositionAlignment, Direction, ButtonStyle, ButtonType, ToolbarItemLocation, ToolbarItemComponent } from "devextreme/common"; +import type { FormatLocale } from "devextreme/common/core/localization"; import type { event } from "devextreme/events/events.types"; import type { EventInfo } from "devextreme/common/core/events"; import type { Component } from "devextreme/core/component"; @@ -301,6 +302,7 @@ const Collision = Object.assign type IDisplayFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: Format | string; diff --git a/packages/devextreme-react/src/filter-builder.ts b/packages/devextreme-react/src/filter-builder.ts index 7c4e2bffbd77..c533d19de872 100644 --- a/packages/devextreme-react/src/filter-builder.ts +++ b/packages/devextreme-react/src/filter-builder.ts @@ -10,7 +10,7 @@ import NestedOption from "./core/nested-option"; import type { ContentReadyEvent, DisposingEvent, EditorPreparedEvent, EditorPreparingEvent, InitializedEvent, ValueChangedEvent, dxFilterBuilderField, FieldInfo, FilterBuilderOperation } from "devextreme/ui/filter_builder"; import type { DataType, template, Format as CommonFormat } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { Format as LocalizationFormat, FormatLocale } from "devextreme/common/core/localization"; import type { DataSourceOptions } from "devextreme/data/data_source"; import type { Store } from "devextreme/data/store"; @@ -195,6 +195,7 @@ const FilterOperationDescriptions = Object.assign string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/funnel.ts b/packages/devextreme-react/src/funnel.ts index 3ea5469bb050..9402964546ca 100644 --- a/packages/devextreme-react/src/funnel.ts +++ b/packages/devextreme-react/src/funnel.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, ItemClickEvent, LegendClickEvent, dxFunnelItem, FunnelLegendItem } from "devextreme/viz/funnel"; import type { DashStyle, Font as ChartsFont, TextOverflow, WordWrap, HatchDirection, LabelPosition } from "devextreme/common/charts"; import type { ExportFormat, Format as CommonFormat, HorizontalAlignment, VerticalEdge, HorizontalEdge, Position, template, Orientation } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -216,6 +216,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/gantt.ts b/packages/devextreme-react/src/gantt.ts index 1f534ad623eb..5add9ebb9a4f 100644 --- a/packages/devextreme-react/src/gantt.ts +++ b/packages/devextreme-react/src/gantt.ts @@ -12,7 +12,7 @@ import type { ContentReadyEvent, ContextMenuPreparingEvent, CustomCommandEvent, import type { HorizontalAlignment, template, DataType, Format as CommonFormat, SortOrder, SearchMode, ToolbarItemLocation, ToolbarItemComponent, SingleMultipleOrNone } from "devextreme/common"; import type { dxTreeListColumn, dxTreeListRowObject } from "devextreme/ui/tree_list"; import type { FilterOperation, FilterType, ColumnHeaderFilter as GridsColumnHeaderFilter, SelectedFilterOperation, HeaderFilterGroupInterval, ColumnHeaderFilterSearchConfig, HeaderFilterSearchConfig } from "devextreme/common/grids"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { Format as LocalizationFormat, FormatLocale } from "devextreme/common/core/localization"; import type { DataSourceOptions } from "devextreme/data/data_source"; import type { Store } from "devextreme/data/store"; import type { dxContextMenuItem } from "devextreme/ui/context_menu"; @@ -463,6 +463,7 @@ const FilterRow = Object.assign type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/linear-gauge.ts b/packages/devextreme-react/src/linear-gauge.ts index ad5c90c11afa..10cfde9d5d93 100644 --- a/packages/devextreme-react/src/linear-gauge.ts +++ b/packages/devextreme-react/src/linear-gauge.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, TooltipHiddenEvent, TooltipShownEvent } from "devextreme/viz/linear_gauge"; import type { AnimationEaseMode, DashStyle, Font as ChartsFont, LabelOverlap, ChartsColor, Palette, PaletteExtensionMode, TextOverflow, WordWrap } from "devextreme/common/charts"; import type { ExportFormat, Format as CommonFormat, Orientation, HorizontalAlignment, VerticalAlignment, HorizontalEdge, VerticalEdge, template } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -235,6 +235,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/number-box.ts b/packages/devextreme-react/src/number-box.ts index 0058c3bcb0b3..096934883119 100644 --- a/packages/devextreme-react/src/number-box.ts +++ b/packages/devextreme-react/src/number-box.ts @@ -11,6 +11,7 @@ import NestedOption from "./core/nested-option"; import type { ChangeEvent, ContentReadyEvent, CopyEvent, CutEvent, DisposingEvent, EnterKeyEvent, FocusInEvent, FocusOutEvent, InitializedEvent, InputEvent, KeyDownEvent, KeyUpEvent, PasteEvent, ValueChangedEvent } from "devextreme/ui/number_box"; import type { ContentReadyEvent as ButtonContentReadyEvent, DisposingEvent as ButtonDisposingEvent, InitializedEvent as ButtonInitializedEvent, dxButtonOptions, ClickEvent, OptionChangedEvent } from "devextreme/ui/button"; import type { TextEditorButtonLocation, Format as CommonFormat, ButtonStyle, template, ButtonType } from "devextreme/common"; +import type { FormatLocale } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -112,6 +113,7 @@ const Button = Object.assign(_comp type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/pie-chart.ts b/packages/devextreme-react/src/pie-chart.ts index 26da830023f8..068997303e68 100644 --- a/packages/devextreme-react/src/pie-chart.ts +++ b/packages/devextreme-react/src/pie-chart.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DoneEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, LegendClickEvent, PointClickEvent, TooltipHiddenEvent, TooltipShownEvent, dxPieChartAnnotationConfig, PieChartAnnotationLocation, dxPieChartCommonAnnotationConfig, PieChartSeriesInteractionMode, SmallValuesGroupingMode, PieChartLegendItem, PieChartLegendHoverMode, PieChartSeries, dxPieChartPointInfo } from "devextreme/viz/pie_chart"; import type { AnimationEaseMode, DashStyle, Font as ChartsFont, TextOverflow, AnnotationType, WordWrap, ChartsDataType, ChartsColor, HatchDirection, LabelPosition } from "devextreme/common/charts"; import type { template, Format as CommonFormat, ExportFormat, HorizontalAlignment, Position, Orientation, VerticalEdge } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -264,6 +264,7 @@ const AnnotationBorder = Object.assign string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; @@ -604,6 +605,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/polar-chart.ts b/packages/devextreme-react/src/polar-chart.ts index 318ca1ad6a84..b8d1f49dad2d 100644 --- a/packages/devextreme-react/src/polar-chart.ts +++ b/packages/devextreme-react/src/polar-chart.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { ArgumentAxisClickEvent, DisposingEvent, DoneEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, LegendClickEvent, PointClickEvent, SeriesClickEvent, TooltipHiddenEvent, TooltipShownEvent, ZoomEndEvent, ZoomStartEvent, dxPolarChartAnnotationConfig, dxPolarChartCommonAnnotationConfig, PolarChartSeriesType, PolarChartSeries, dxPolarChartPointInfo } from "devextreme/viz/polar_chart"; import type { AnimationEaseMode, DashStyle, Font as ChartsFont, TextOverflow, AnnotationType, WordWrap, ChartsDataType, DiscreteAxisDivisionMode, ArgumentAxisHoverMode, LabelOverlap, TimeInterval, AxisScaleType, ChartsColor, SeriesHoverMode, HatchDirection, RelativePosition, PointInteractionMode, PointSymbol, SeriesSelectionMode, ValueErrorBarDisplayMode, ValueErrorBarType, LegendItem, LegendHoverMode, ValueAxisVisualRangeUpdateMode } from "devextreme/common/charts"; import type { template, Format as CommonFormat, ExportFormat, HorizontalAlignment, Position, Orientation, VerticalEdge } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { Format as LocalizationFormat, FormatLocale } from "devextreme/common/core/localization"; import type * as CommonChartTypes from "devextreme/common/charts"; @@ -462,6 +462,7 @@ const ArgumentAxisTick = Object.assign string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; @@ -1307,6 +1308,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/range-selector.ts b/packages/devextreme-react/src/range-selector.ts index 619ffeb648e9..6f82b2faf93e 100644 --- a/packages/devextreme-react/src/range-selector.ts +++ b/packages/devextreme-react/src/range-selector.ts @@ -10,8 +10,8 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, ValueChangedEvent, BackgroundImageLocation, ChartAxisScale, AxisScale } from "devextreme/viz/range_selector"; import type { chartPointAggregationInfoObject, chartSeriesObject, ChartSeriesAggregationMethod, dxChartCommonSeriesSettings, FinancialChartReductionLevel } from "devextreme/viz/chart"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; import type { Format as CommonFormat, SliderValueChangeMode, HorizontalAlignment, ExportFormat, VerticalEdge } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; import type { DashStyle, ScaleBreakLineStyle, Palette, PaletteExtensionMode, ChartsDataType, ChartsColor, SeriesHoverMode, HatchDirection, Font as ChartsFont, RelativePosition, PointInteractionMode, PointSymbol, SeriesSelectionMode, SeriesType, ValueErrorBarDisplayMode, ValueErrorBarType, LabelOverlap, TimeInterval, ScaleBreak, DiscreteAxisDivisionMode, TextOverflow, WordWrap } from "devextreme/common/charts"; import type { ChartSeries } from "devextreme/viz/common"; @@ -149,6 +149,7 @@ const AggregationInterval = Object.assign string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; @@ -824,6 +825,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/range-slider.ts b/packages/devextreme-react/src/range-slider.ts index 694b80b22c12..84790a4ab0fb 100644 --- a/packages/devextreme-react/src/range-slider.ts +++ b/packages/devextreme-react/src/range-slider.ts @@ -9,8 +9,8 @@ import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponent import NestedOption from "./core/nested-option"; import type { ContentReadyEvent, DisposingEvent, InitializedEvent, ValueChangedEvent } from "devextreme/ui/range_slider"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; import type { Format as CommonFormat, VerticalEdge, TooltipShowMode } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -79,6 +79,7 @@ const RangeSlider = memo( type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/sankey.ts b/packages/devextreme-react/src/sankey.ts index 0634ebb9c2ac..e526c10aa3f1 100644 --- a/packages/devextreme-react/src/sankey.ts +++ b/packages/devextreme-react/src/sankey.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, LinkClickEvent, NodeClickEvent, dxSankeyNode, SankeyColorMode } from "devextreme/viz/sankey"; import type { DashStyle, HatchDirection, Font as ChartsFont, TextOverflow, WordWrap } from "devextreme/common/charts"; import type { ExportFormat, Format as CommonFormat, HorizontalAlignment, VerticalEdge, template } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -188,6 +188,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/slider.ts b/packages/devextreme-react/src/slider.ts index cd3edf0e37fa..0fb3fda6e238 100644 --- a/packages/devextreme-react/src/slider.ts +++ b/packages/devextreme-react/src/slider.ts @@ -9,8 +9,8 @@ import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponent import NestedOption from "./core/nested-option"; import type { ContentReadyEvent, DisposingEvent, InitializedEvent, ValueChangedEvent } from "devextreme/ui/slider"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; import type { Format as CommonFormat, VerticalEdge, TooltipShowMode } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -79,6 +79,7 @@ const Slider = memo( type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/sparkline.ts b/packages/devextreme-react/src/sparkline.ts index ef8e8f06368f..5bdf2e1140f7 100644 --- a/packages/devextreme-react/src/sparkline.ts +++ b/packages/devextreme-react/src/sparkline.ts @@ -10,8 +10,8 @@ import NestedOption from "./core/nested-option"; import type { DisposingEvent, DrawnEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, TooltipHiddenEvent, TooltipShownEvent } from "devextreme/viz/sparkline"; import type { DashStyle, Font as ChartsFont } from "devextreme/common/charts"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; import type { Format as CommonFormat, template } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -119,6 +119,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/tree-list.ts b/packages/devextreme-react/src/tree-list.ts index 3b6aa39c3305..3156318ddad5 100644 --- a/packages/devextreme-react/src/tree-list.ts +++ b/packages/devextreme-react/src/tree-list.ts @@ -20,7 +20,7 @@ import type { AIIntegration } from "devextreme/common/ai-integration"; import type { dxPopupOptions, dxPopupToolbarItem, ToolbarLocation } from "devextreme/ui/popup"; import type { AnimationConfig, CollisionResolution, PositionConfig, AnimationState, AnimationType, CollisionResolutionCombination } from "devextreme/common/core/animation"; import type { ValidationRuleType, HorizontalAlignment, VerticalAlignment, template, TextEditorButtonLocation, ButtonStyle, ButtonType, DataType, Format as CommonFormat, SortOrder, SearchMode, ComparisonOperator, TextBoxPredefinedButton, TextEditorButton, LabelMode, MaskMode, EditorStyle, ValidationMessageMode, Position as CommonPosition, ValidationStatus, PositionAlignment, Mode, Direction, ToolbarItemLocation, ToolbarItemComponent, DisplayMode, DragDirection, DragHighlight, ScrollMode, ScrollbarMode, SingleMultipleOrNone, TabsIconPosition, TabsStyle } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { Format as LocalizationFormat, FormatLocale } from "devextreme/common/core/localization"; import type { DataSourceOptions } from "devextreme/data/data_source"; import type { Store } from "devextreme/data/store"; import type { event } from "devextreme/events/events.types"; @@ -1723,6 +1723,7 @@ const Form = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-react/src/tree-map.ts b/packages/devextreme-react/src/tree-map.ts index 639891c08dee..8eca0cea83d1 100644 --- a/packages/devextreme-react/src/tree-map.ts +++ b/packages/devextreme-react/src/tree-map.ts @@ -11,7 +11,7 @@ import NestedOption from "./core/nested-option"; import type { ClickEvent, DisposingEvent, DrawnEvent, DrillEvent, ExportedEvent, ExportingEvent, FileSavingEvent, IncidentOccurredEvent, InitializedEvent, NodesInitializedEvent, NodesRenderingEvent, TreeMapColorizerType, dxTreeMapNode } from "devextreme/viz/tree_map"; import type { DashStyle, Palette, PaletteExtensionMode, Font as ChartsFont, TextOverflow, WordWrap } from "devextreme/common/charts"; import type { ExportFormat, Format as CommonFormat, HorizontalAlignment, VerticalEdge, template } from "devextreme/common"; -import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; +import type { FormatLocale, Format as LocalizationFormat } from "devextreme/common/core/localization"; type ReplaceFieldTypes = { [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P]; @@ -192,6 +192,7 @@ const Font = Object.assign(_componen type IFormatProps = React.PropsWithChildren<{ currency?: string; formatter?: ((value: number | Date) => string); + locale?: FormatLocale; parser?: ((value: string) => number | Date); precision?: number; type?: CommonFormat | string; diff --git a/packages/devextreme-vue/src/bar-gauge.ts b/packages/devextreme-vue/src/bar-gauge.ts index 14035e18614e..e5dcde6b9e28 100644 --- a/packages/devextreme-vue/src/bar-gauge.ts +++ b/packages/devextreme-vue/src/bar-gauge.ts @@ -36,6 +36,7 @@ import { Orientation, } from "devextreme/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -372,6 +373,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -380,6 +382,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, @@ -418,6 +421,7 @@ const DxItemTextFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -426,6 +430,7 @@ const DxItemTextFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/bullet.ts b/packages/devextreme-vue/src/bullet.ts index ad52d15b06d2..ca3e7693aeec 100644 --- a/packages/devextreme-vue/src/bullet.ts +++ b/packages/devextreme-vue/src/bullet.ts @@ -20,11 +20,12 @@ import { Font, } from "devextreme/common/charts"; import { - Format, -} from "devextreme/common"; -import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; +import { + Format, +} from "devextreme/common"; import { prepareConfigurationComponentConfig } from "./core/index"; type AccessibleOptions = Pick string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/card-view.ts b/packages/devextreme-vue/src/card-view.ts index b7febbe1d4fc..19fe59db811e 100644 --- a/packages/devextreme-vue/src/card-view.ts +++ b/packages/devextreme-vue/src/card-view.ts @@ -178,6 +178,7 @@ import { } from "devextreme/ui/toolbar"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { AIIntegration, @@ -1784,6 +1785,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -1792,6 +1794,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/chart.ts b/packages/devextreme-vue/src/chart.ts index f9bf57235da1..64395953dae5 100644 --- a/packages/devextreme-vue/src/chart.ts +++ b/packages/devextreme-vue/src/chart.ts @@ -95,6 +95,7 @@ import { ChartSeries, } from "devextreme/viz/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import * as CommonChartTypes from "devextreme/common/charts"; @@ -751,6 +752,7 @@ const DxArgumentFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -759,6 +761,7 @@ const DxArgumentFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, @@ -1924,6 +1927,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -1932,6 +1936,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/chat.ts b/packages/devextreme-vue/src/chat.ts index 62b453875482..4199b8b5c364 100644 --- a/packages/devextreme-vue/src/chat.ts +++ b/packages/devextreme-vue/src/chat.ts @@ -37,6 +37,7 @@ import { } from "devextreme/data/store"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { Format as CommonFormat, @@ -400,6 +401,7 @@ const DxDayHeaderFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -408,6 +410,7 @@ const DxDayHeaderFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, @@ -641,6 +644,7 @@ const DxMessageTimestampFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -649,6 +653,7 @@ const DxMessageTimestampFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/circular-gauge.ts b/packages/devextreme-vue/src/circular-gauge.ts index 40e451e8f31d..7062184251d3 100644 --- a/packages/devextreme-vue/src/circular-gauge.ts +++ b/packages/devextreme-vue/src/circular-gauge.ts @@ -39,6 +39,7 @@ import { HorizontalAlignment, } from "devextreme/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -330,6 +331,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -338,6 +340,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/common/core/localization.ts b/packages/devextreme-vue/src/common/core/localization.ts index 6dbb7f75a2a6..6f35e4992209 100644 --- a/packages/devextreme-vue/src/common/core/localization.ts +++ b/packages/devextreme-vue/src/common/core/localization.ts @@ -9,4 +9,5 @@ export { } from "devextreme/common/core/localization"; export type { Format, + FormatLocale, } from "devextreme/common/core/localization"; diff --git a/packages/devextreme-vue/src/common/index.ts b/packages/devextreme-vue/src/common/index.ts index 0b83f3212b65..9abc26031327 100644 --- a/packages/devextreme-vue/src/common/index.ts +++ b/packages/devextreme-vue/src/common/index.ts @@ -189,6 +189,7 @@ export namespace Core { export namespace Localization { export type Format = CoreLocalizationModule.Format; export const formatDate = CoreLocalizationModule.formatDate; + export type FormatLocale = CoreLocalizationModule.FormatLocale; export const formatMessage = CoreLocalizationModule.formatMessage; export const formatNumber = CoreLocalizationModule.formatNumber; export const loadMessages = CoreLocalizationModule.loadMessages; diff --git a/packages/devextreme-vue/src/data-grid.ts b/packages/devextreme-vue/src/data-grid.ts index b4802090d21c..8de724bda21c 100644 --- a/packages/devextreme-vue/src/data-grid.ts +++ b/packages/devextreme-vue/src/data-grid.ts @@ -239,6 +239,7 @@ import { } from "devextreme/ui/form"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { LocateInMenuMode, @@ -2608,6 +2609,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -2616,6 +2618,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, @@ -4695,6 +4698,7 @@ const DxValueFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -4703,6 +4707,7 @@ const DxValueFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/date-box.ts b/packages/devextreme-vue/src/date-box.ts index a5f4ceb804db..3e61cb8a74b0 100644 --- a/packages/devextreme-vue/src/date-box.ts +++ b/packages/devextreme-vue/src/date-box.ts @@ -43,6 +43,7 @@ import { } from "devextreme/ui/calendar"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { dxPopupOptions, @@ -571,6 +572,7 @@ const DxDisplayFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -579,6 +581,7 @@ const DxDisplayFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/date-range-box.ts b/packages/devextreme-vue/src/date-range-box.ts index a5b641516aca..b21f6e3db85a 100644 --- a/packages/devextreme-vue/src/date-range-box.ts +++ b/packages/devextreme-vue/src/date-range-box.ts @@ -42,6 +42,7 @@ import { } from "devextreme/ui/calendar"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { dxPopupOptions, @@ -580,6 +581,7 @@ const DxDisplayFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -588,6 +590,7 @@ const DxDisplayFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/filter-builder.ts b/packages/devextreme-vue/src/filter-builder.ts index 3fec5e29e1c8..2fcb30e04c30 100644 --- a/packages/devextreme-vue/src/filter-builder.ts +++ b/packages/devextreme-vue/src/filter-builder.ts @@ -22,6 +22,7 @@ import { } from "devextreme/common"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { DataSourceOptions, @@ -273,6 +274,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -281,6 +283,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/funnel.ts b/packages/devextreme-vue/src/funnel.ts index a1b5f01f1b76..243a918b9c8d 100644 --- a/packages/devextreme-vue/src/funnel.ts +++ b/packages/devextreme-vue/src/funnel.ts @@ -49,6 +49,7 @@ import { Orientation, } from "devextreme/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -356,6 +357,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -364,6 +366,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/gantt.ts b/packages/devextreme-vue/src/gantt.ts index 702c6156dbf9..1fc2c18b897d 100644 --- a/packages/devextreme-vue/src/gantt.ts +++ b/packages/devextreme-vue/src/gantt.ts @@ -73,6 +73,7 @@ import { } from "devextreme/common/grids"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { DataSourceOptions, @@ -692,6 +693,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -700,6 +702,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/linear-gauge.ts b/packages/devextreme-vue/src/linear-gauge.ts index ac6e961ab6e4..9f2a7aeef624 100644 --- a/packages/devextreme-vue/src/linear-gauge.ts +++ b/packages/devextreme-vue/src/linear-gauge.ts @@ -39,6 +39,7 @@ import { VerticalEdge, } from "devextreme/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -327,6 +328,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -335,6 +337,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/number-box.ts b/packages/devextreme-vue/src/number-box.ts index 89f8c38b7c8b..876d3f4693b3 100644 --- a/packages/devextreme-vue/src/number-box.ts +++ b/packages/devextreme-vue/src/number-box.ts @@ -35,6 +35,7 @@ import { } from "devextreme/common"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { dxButtonOptions, @@ -274,6 +275,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -282,6 +284,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/pie-chart.ts b/packages/devextreme-vue/src/pie-chart.ts index 5c36f2ec8605..69fd6ce4da9e 100644 --- a/packages/devextreme-vue/src/pie-chart.ts +++ b/packages/devextreme-vue/src/pie-chart.ts @@ -65,6 +65,7 @@ import { VerticalEdge, } from "devextreme/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -434,6 +435,7 @@ const DxArgumentFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -442,6 +444,7 @@ const DxArgumentFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, @@ -716,6 +719,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -724,6 +728,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/polar-chart.ts b/packages/devextreme-vue/src/polar-chart.ts index 509f756e93e4..3fb97dcfae35 100644 --- a/packages/devextreme-vue/src/polar-chart.ts +++ b/packages/devextreme-vue/src/polar-chart.ts @@ -78,6 +78,7 @@ import { VerticalEdge, } from "devextreme/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import * as CommonChartTypes from "devextreme/common/charts"; @@ -627,6 +628,7 @@ const DxArgumentFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -635,6 +637,7 @@ const DxArgumentFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, @@ -1354,6 +1357,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -1362,6 +1366,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/range-selector.ts b/packages/devextreme-vue/src/range-selector.ts index 664e13887dbd..8c304a0c7f54 100644 --- a/packages/devextreme-vue/src/range-selector.ts +++ b/packages/devextreme-vue/src/range-selector.ts @@ -56,6 +56,10 @@ import { dxChartCommonSeriesSettings, FinancialChartReductionLevel, } from "devextreme/viz/chart"; +import { + FormatLocale, + Format as LocalizationFormat, +} from "devextreme/common/core/localization"; import { Format, SliderValueChangeMode, @@ -66,9 +70,6 @@ import { import { ChartSeries, } from "devextreme/viz/common"; -import { - Format as LocalizationFormat, -} from "devextreme/common/core/localization"; import * as CommonChartTypes from "devextreme/common/charts"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -283,6 +284,7 @@ const DxArgumentFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -291,6 +293,7 @@ const DxArgumentFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, @@ -881,6 +884,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -889,6 +893,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/range-slider.ts b/packages/devextreme-vue/src/range-slider.ts index eff249b51b3d..344747792f0b 100644 --- a/packages/devextreme-vue/src/range-slider.ts +++ b/packages/devextreme-vue/src/range-slider.ts @@ -19,6 +19,7 @@ import { TooltipShowMode, } from "devextreme/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -178,6 +179,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -186,6 +188,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/sankey.ts b/packages/devextreme-vue/src/sankey.ts index cf0a7b69bbb5..5e633f0363e9 100644 --- a/packages/devextreme-vue/src/sankey.ts +++ b/packages/devextreme-vue/src/sankey.ts @@ -43,6 +43,7 @@ import { WordWrap, } from "devextreme/common/charts"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -309,6 +310,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -317,6 +319,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/slider.ts b/packages/devextreme-vue/src/slider.ts index 596ebd42b828..7df7103d82c5 100644 --- a/packages/devextreme-vue/src/slider.ts +++ b/packages/devextreme-vue/src/slider.ts @@ -19,6 +19,7 @@ import { TooltipShowMode, } from "devextreme/common"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -169,6 +170,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -177,6 +179,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/sparkline.ts b/packages/devextreme-vue/src/sparkline.ts index abf4e0f7caf1..c4e59af08d1f 100644 --- a/packages/devextreme-vue/src/sparkline.ts +++ b/packages/devextreme-vue/src/sparkline.ts @@ -29,11 +29,12 @@ import { Font, } from "devextreme/common/charts"; import { - Format, -} from "devextreme/common"; -import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; +import { + Format, +} from "devextreme/common"; import { prepareConfigurationComponentConfig } from "./core/index"; type AccessibleOptions = Pick string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/tree-list.ts b/packages/devextreme-vue/src/tree-list.ts index fdcf823ef218..d47595055626 100644 --- a/packages/devextreme-vue/src/tree-list.ts +++ b/packages/devextreme-vue/src/tree-list.ts @@ -234,6 +234,7 @@ import { } from "devextreme/ui/form"; import { Format, + FormatLocale, } from "devextreme/common/core/localization"; import { event, @@ -2392,6 +2393,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -2400,6 +2402,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme-vue/src/tree-map.ts b/packages/devextreme-vue/src/tree-map.ts index 134ae9bc17f0..50b147f137c5 100644 --- a/packages/devextreme-vue/src/tree-map.ts +++ b/packages/devextreme-vue/src/tree-map.ts @@ -46,6 +46,7 @@ import { WordWrap, } from "devextreme/common/charts"; import { + FormatLocale, Format as LocalizationFormat, } from "devextreme/common/core/localization"; import { prepareConfigurationComponentConfig } from "./core/index"; @@ -328,6 +329,7 @@ const DxFormatConfig = { "update:hoveredElement": null, "update:currency": null, "update:formatter": null, + "update:locale": null, "update:parser": null, "update:precision": null, "update:type": null, @@ -336,6 +338,7 @@ const DxFormatConfig = { props: { currency: String, formatter: Function as PropType<((value: number | Date) => string)>, + locale: [Function, String] as PropType, parser: Function as PropType<((value: string) => number | Date)>, precision: Number, type: String as PropType, diff --git a/packages/devextreme/js/__internal/core/format_helper.test.ts b/packages/devextreme/js/__internal/core/format_helper.test.ts new file mode 100644 index 000000000000..c7653c41af1c --- /dev/null +++ b/packages/devextreme/js/__internal/core/format_helper.test.ts @@ -0,0 +1,52 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import coreLocalization from '@js/common/core/localization/core'; + +import formatHelper from './format_helper'; +import type { LocaleAwareFormatter } from './global_format_config'; +import { withFormatLocale } from './global_format_config'; + +describe('formatHelper composite date format', () => { + let savedLocale = ''; + + beforeEach(() => { + savedLocale = coreLocalization.locale(); + coreLocalization.locale('en'); + }); + + afterEach(() => { + coreLocalization.locale(savedLocale); + }); + + const ticks = [new Date(2024, 5, 15, 13, 0), new Date(2024, 5, 16, 14, 30)]; + + it('should build composite format from several parts', () => { + const format = formatHelper.getDateFormatByTicks(ticks) as LocaleAwareFormatter; + + expect(typeof format).toBe('function'); + expect(format(ticks[1])).toBe('Sunday, 16 2:30 PM'); + }); + + it('should apply locale to each part of composite format', () => { + const format = formatHelper.getDateFormatByTicks(ticks); + const localizedFormat = withFormatLocale(format, 'de') as LocaleAwareFormatter; + + expect(localizedFormat(ticks[1])).toBe('Sonntag, 16 14:30'); + }); + + it('should apply locale to month name part', () => { + const format = formatHelper.getDateFormatByDifferences({ month: true, day: true, hour: true }, 'hour'); + const localizedFormat = withFormatLocale(format, 'de') as LocaleAwareFormatter; + + expect(localizedFormat(new Date(2024, 2, 16, 14, 30))).toBe('Mär 16 14:30'); + }); + + it('should not change original composite format', () => { + const format = formatHelper.getDateFormatByTicks(ticks) as LocaleAwareFormatter; + + withFormatLocale(format, 'de'); + + expect(format(ticks[1])).toBe('Sunday, 16 2:30 PM'); + }); +}); diff --git a/packages/devextreme/js/__internal/core/format_helper.ts b/packages/devextreme/js/__internal/core/format_helper.ts index 51c6e611db40..616af996b7d9 100644 --- a/packages/devextreme/js/__internal/core/format_helper.ts +++ b/packages/devextreme/js/__internal/core/format_helper.ts @@ -14,7 +14,33 @@ import { } from '@js/core/utils/type'; import { injector as dependencyInjector } from '@ts/core/utils/dependency_injector'; -import { getGlobalFormatByDataType } from './global_format_config'; +import type { FormatLocale, LocaleAwareFormatter } from './global_format_config'; +import { getGlobalFormatByDataType, withFormatLocale } from './global_format_config'; + +type DateFormatter = (date: Date) => string; + +const createLocaleAwareFormatter = ( + createFormatter: (locale?: FormatLocale) => DateFormatter, +): LocaleAwareFormatter => { + const create = (locale?: FormatLocale): LocaleAwareFormatter => { + const formatter: LocaleAwareFormatter = createFormatter(locale); + formatter.withLocale = create; + + return formatter; + }; + + return create(); +}; + +const getMonthNames = (format: string, type?: string, locale?: string): string[] => ( + dateLocalization as unknown as { + getMonthNames: (format: string, type?: string, locale?: string) => string[]; + } +).getMonthNames(format, type, locale); + +const resolveLocale = (locale?: FormatLocale): string | undefined => ( + isFunction(locale) ? locale() : locale +); export default dependencyInjector({ format(value, format?) { @@ -70,9 +96,9 @@ export default dependencyInjector({ return formatParts[0]; } - return function(date): string { - return formatParts.map((formatPart) => dateLocalization.format(date, formatPart)).join(' '); - }; + return createLocaleAwareFormatter((locale) => (date): string => formatParts + .map((formatPart) => dateLocalization.format(date, withFormatLocale(formatPart, locale))) + .join(' ')); }, getDateFormatByDifferences(dateDifferences, intervalFormat?) { @@ -118,10 +144,10 @@ export default dependencyInjector({ if (dateDifferences.month && dateDifferences.day) { if (intervalFormat) { - const monthDayFormatter = function(date): string { - const monthName = dateLocalization.getMonthNames('abbreviated')[date.getMonth()]; - return `${monthName} ${dateLocalization.format(date, 'day')}`; - }; + const monthDayFormatter = createLocaleAwareFormatter((locale) => (date): string => { + const monthName = getMonthNames('abbreviated', undefined, resolveLocale(locale))[date.getMonth()]; + return `${monthName} ${dateLocalization.format(date, withFormatLocale('day', locale))}`; + }); resultFormat.unshift(monthDayFormatter); } else { resultFormat.unshift('monthandday'); @@ -136,10 +162,10 @@ export default dependencyInjector({ if (intervalFormat) { resultFormat.unshift('day'); } else { - const dayFormatter = function(date): string { - const dayOfWeek = dateLocalization.format(date, 'dayofweek'); - return `${dayOfWeek}, ${dateLocalization.format(date, 'day')}`; - }; + const dayFormatter = createLocaleAwareFormatter((locale) => (date): string => { + const dayOfWeek = dateLocalization.format(date, withFormatLocale('dayofweek', locale)); + return `${dayOfWeek}, ${dateLocalization.format(date, withFormatLocale('day', locale))}`; + }); resultFormat.unshift(dayFormatter); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return diff --git a/packages/devextreme/js/__internal/core/global_format_config.test.ts b/packages/devextreme/js/__internal/core/global_format_config.test.ts index e1d64db9a456..5c1ad353ba29 100644 --- a/packages/devextreme/js/__internal/core/global_format_config.test.ts +++ b/packages/devextreme/js/__internal/core/global_format_config.test.ts @@ -1,17 +1,31 @@ import { afterEach, beforeEach, describe, expect, it, } from '@jest/globals'; +import coreLocalization from '@js/common/core/localization/core'; import config from '@js/core/config'; -import { getGlobalFormatByDataType, resolvePresetOverride } from './global_format_config'; +import type { LocaleAwareFormatter } from './global_format_config'; +import { + applyDataTypePreset, + getDateFormatLocale, + getEffectiveFormatLocale, + getFormatterOptions, + getGlobalFormatByDataType, + getRawGlobalFormatByDataType, + isLocaleOnlyFormat, + resolvePresetOverride, + withFormatLocale, +} from './global_format_config'; const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; type GlobalFormatKey = typeof GLOBAL_FORMAT_KEYS[number]; describe('global_format_config', () => { let savedValues: Partial> = {}; + let savedLocale = ''; beforeEach(() => { + savedLocale = coreLocalization.locale(); const currentConfig = config(); savedValues = {}; @@ -21,6 +35,7 @@ describe('global_format_config', () => { }); afterEach(() => { + coreLocalization.locale(savedLocale); const currentConfig = config(); GLOBAL_FORMAT_KEYS.forEach((key) => { @@ -204,4 +219,366 @@ describe('global_format_config', () => { expect(resolvePresetOverride('shortTime')).toBe('HH:mm'); }); }); + + describe('getGlobalFormatByDataType with format locale', () => { + it('should resolve locale map entry by message locale', () => { + config({ + ...config(), + numberFormat: { + de: { locale: 'en-US', minimumFractionDigits: 2 }, + default: { locale: 'de-DE', minimumFractionDigits: 2 }, + }, + }); + coreLocalization.locale('de'); + + expect(getGlobalFormatByDataType('number')).toEqual({ + locale: 'en-US', + minimumFractionDigits: 2, + }); + }); + + it('should resolve preset override from locale map', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: { + de: 'dd.MM.yyyy', + default: 'dd/MM/yyyy', + }, + }, + }); + coreLocalization.locale('de'); + + expect(resolvePresetOverride('shortDate')).toBe('dd.MM.yyyy'); + }); + }); + + describe('getEffectiveFormatLocale - global config dataType resolution', () => { + it('should resolve locale via getGlobalFormatByDataType when global config type matches preset', () => { + config({ + ...config(), + timeFormat: { + default: { + locale: 'de-DE', + type: 'shortTime', + }, + }, + }); + coreLocalization.locale('en'); + + expect(getEffectiveFormatLocale(undefined, undefined, 'shortTime')).toBe('de-DE'); + }); + + it('should resolve locale via getGlobalFormatByDataType for implicit shortDate preset', () => { + config({ + ...config(), + dateFormat: { + default: { + locale: 'de-DE', + }, + }, + }); + coreLocalization.locale('en'); + + expect(getEffectiveFormatLocale(undefined, undefined, 'shortDate')).toBe('de-DE'); + }); + + it('should infer dataType from Intl format object options', () => { + config({ + ...config(), + timeFormat: { + default: { + locale: 'de-DE', + }, + }, + }); + coreLocalization.locale('en'); + + expect(getEffectiveFormatLocale({ + hour: 'numeric', + minute: 'numeric', + })).toBe('de-DE'); + }); + + it('should use explicit dataType with getGlobalFormatByDataType', () => { + config({ + ...config(), + dateFormat: { + default: { + locale: 'de-DE', + }, + }, + }); + coreLocalization.locale('en'); + + expect(getEffectiveFormatLocale(undefined, 'date')).toBe('de-DE'); + }); + }); + + describe('getEffectiveFormatLocale', () => { + it('should return string locale from format object', () => { + expect(getEffectiveFormatLocale({ locale: 'en-US' })).toBe('en-US'); + }); + + it('should evaluate function locale', () => { + expect(getEffectiveFormatLocale({ locale: () => 'de-DE' })).toBe('de-DE'); + }); + + it('should prefer own format locale over global format locale', () => { + config({ + ...config(), + numberFormat: { + default: { locale: 'en-US' }, + }, + }); + + expect(getEffectiveFormatLocale({ locale: 'ja' }, 'number')).toBe('ja'); + }); + + it('should fall back to global numberFormat locale', () => { + config({ + ...config(), + numberFormat: { + default: { locale: 'en-US', minimumFractionDigits: 2 }, + }, + }); + coreLocalization.locale('de'); + + expect(getEffectiveFormatLocale({ type: 'fixedPoint', precision: 0 }, 'number')).toBe('en-US'); + }); + + it('should fall back to message locale when no format locale is configured', () => { + coreLocalization.locale('de'); + + expect(getEffectiveFormatLocale({ type: 'fixedPoint', precision: 0 }, 'number')).toBe('de'); + }); + }); + + describe('getDateFormatLocale', () => { + beforeEach(() => { + coreLocalization.locale('en'); + config({ + ...config(), + dateFormat: { default: { type: 'shortDate', locale: 'de-DE' } }, + }); + }); + + it('should resolve locale from source preset when it is aliased to another preset', () => { + expect(getDateFormatLocale('shortDate', 'longDate')).toBe('de-DE'); + }); + + it('should resolve locale from source preset when it is aliased to Intl options', () => { + expect(getDateFormatLocale('shortDate', { weekday: 'long' })).toBe('de-DE'); + }); + + it('should prefer source format own locale over aliased format locale', () => { + expect(getDateFormatLocale( + { type: 'shortDate', locale: 'ja' }, + { type: 'longDate', locale: 'fr' }, + )).toBe('ja'); + }); + + it('should use aliased format own locale when source has none', () => { + expect(getDateFormatLocale('shortDate', { type: 'longDate', locale: 'fr' })).toBe('fr'); + }); + + it('should fall back to message locale for unmapped preset', () => { + expect(getDateFormatLocale('longDate')).toBe('en'); + }); + + it('should use the preset override locale when the resolved format is omitted', () => { + config({ + ...config(), + dateTimeFormatPresets: { + longDate: { default: { type: 'longDate', locale: 'de' } }, + }, + }); + + expect(getDateFormatLocale('longDate')).toBe('de'); + expect(getDateFormatLocale({ type: 'longDate' })).toBe('de'); + expect(getDateFormatLocale({ type: 'longDate', locale: 'ja' })).toBe('ja'); + }); + + it('should keep an explicit resolved format ahead of a preset override', () => { + config({ + ...config(), + dateTimeFormatPresets: { + longDate: { default: { type: 'longDate', locale: 'de' } }, + }, + }); + + expect(getDateFormatLocale('longDate', { type: 'longDate', locale: 'fr' })).toBe('fr'); + expect(getDateFormatLocale('shortDate', 'longDate')).toBe('de-DE'); + }); + }); + + describe('getRawGlobalFormatByDataType', () => { + it('should return locale-only datetime format without preset', () => { + config({ ...config(), dateTimeFormat: { default: { locale: 'de' } } }); + + expect(getRawGlobalFormatByDataType('datetime')).toEqual({ locale: 'de' }); + expect(getGlobalFormatByDataType('datetime')).toEqual({ locale: 'de', type: 'shortDateShortTime' }); + }); + + it('should return locale-only time format without preset', () => { + config({ ...config(), timeFormat: { default: { locale: 'de' } } }); + + expect(getRawGlobalFormatByDataType('time')).toEqual({ locale: 'de' }); + expect(getGlobalFormatByDataType('time')).toEqual({ locale: 'de', type: 'shortTime' }); + }); + }); + + describe('isLocaleOnlyFormat', () => { + it.each([ + { format: { locale: 'de' }, expected: true }, + { format: { locale: () => 'de' }, expected: true }, + { format: { locale: 'de', type: undefined }, expected: true }, + { format: { locale: 'de', type: undefined, precision: undefined }, expected: true }, + { format: { locale: 'de', type: 'shortDate' }, expected: false }, + { format: { locale: 'de', type: null }, expected: false }, + { format: { locale: undefined, type: undefined }, expected: false }, + { format: { locale: 'de', maximumFractionDigits: 1 }, expected: false }, + { format: { type: 'shortDate' }, expected: false }, + { format: 'shortDate', expected: false }, + { format: undefined, expected: false }, + ])('should return $expected for $format', ({ format, expected }) => { + expect(isLocaleOnlyFormat(format as never)).toBe(expected); + }); + }); + + describe('withFormatLocale', () => { + const createLocaleAwareFormatter = (locale?: unknown): LocaleAwareFormatter => { + const formatter: LocaleAwareFormatter = () => `text:${String(locale)}`; + formatter.withLocale = createLocaleAwareFormatter; + return formatter; + }; + + it('should rebuild locale-aware function format with locale', () => { + const format = withFormatLocale(createLocaleAwareFormatter(), 'de') as LocaleAwareFormatter; + + expect(format(new Date())).toBe('text:de'); + }); + + it('should rebuild locale-aware formatter inside format object', () => { + const format = withFormatLocale({ type: null, formatter: createLocaleAwareFormatter() } as never, 'de') as { + formatter: LocaleAwareFormatter; locale: string; + }; + + expect(format.formatter(new Date())).toBe('text:de'); + expect(format.locale).toBe('de'); + }); + + it('should keep formatter inside format object when it is not locale-aware', () => { + const formatter = (): string => 'text'; + const format = withFormatLocale({ formatter } as never, 'de') as { formatter: unknown }; + + expect(format.formatter).toBe(formatter); + }); + + it('should wrap string preset into format object with locale', () => { + expect(withFormatLocale('month', 'de')).toEqual({ type: 'month', locale: 'de' }); + }); + + it('should add locale to format object', () => { + expect(withFormatLocale({ type: 'fixedPoint', precision: 2 }, 'de')) + .toEqual({ type: 'fixedPoint', precision: 2, locale: 'de' }); + }); + + it('should keep function locale as is', () => { + const locale = (): string => 'de'; + + expect(withFormatLocale('month', locale)).toEqual({ type: 'month', locale }); + }); + + it('should return function format unchanged', () => { + const formatter = (): string => 'text'; + + expect(withFormatLocale(formatter, 'de')).toBe(formatter); + }); + + it('should return format unchanged without locale', () => { + const format = { type: 'fixedPoint' }; + + expect(withFormatLocale(format, undefined)).toBe(format); + expect(withFormatLocale('month', undefined)).toBe('month'); + }); + + it('should not mutate the original format object', () => { + const format = { type: 'fixedPoint', precision: 2 }; + + withFormatLocale(format, 'de'); + + expect(format).toEqual({ type: 'fixedPoint', precision: 2 }); + }); + }); + + describe('getFormatterOptions', () => { + it('should remove locale metadata from format object', () => { + expect(getFormatterOptions({ + locale: 'en-US', + minimumFractionDigits: 2, + })).toEqual({ + minimumFractionDigits: 2, + }); + }); + + it('should return non-object format as-is', () => { + expect(getFormatterOptions('fixedPoint')).toBe('fixedPoint'); + }); + + it('should not mutate the original format object', () => { + const formatObject = { locale: 'en-US', precision: 2 }; + + getFormatterOptions(formatObject); + + expect(formatObject).toEqual({ locale: 'en-US', precision: 2 }); + }); + }); + + describe('applyDataTypePreset', () => { + it('should add a datetime preset to a locale-only format', () => { + expect(applyDataTypePreset({ locale: 'en-US' }, 'datetime')).toEqual({ + locale: 'en-US', + type: 'shortDateShortTime', + }); + }); + + it('should add a time preset to a locale-only format', () => { + expect(applyDataTypePreset({ locale: 'de-DE' }, 'time')).toEqual({ + locale: 'de-DE', + type: 'shortTime', + }); + }); + + it('should add a datetime preset when optional members are undefined', () => { + expect(applyDataTypePreset({ locale: 'de', type: undefined }, 'datetime')).toEqual({ + locale: 'de', + type: 'shortDateShortTime', + }); + }); + + it('should leave a locale-only date format unchanged', () => { + expect(applyDataTypePreset({ locale: 'de' }, 'date')).toEqual({ locale: 'de' }); + }); + + it('should leave a format that already has a type unchanged', () => { + const format = { locale: 'en-US', type: 'longDate' }; + + expect(applyDataTypePreset(format, 'datetime')).toBe(format); + }); + + it('should resolve a locale-only dateTimeFormat with the datetime preset', () => { + config({ + ...config(), + dateTimeFormat: { + default: { locale: 'en' }, + }, + }); + + expect(getGlobalFormatByDataType('datetime')).toEqual({ + locale: 'en', + type: 'shortDateShortTime', + }); + }); + }); }); diff --git a/packages/devextreme/js/__internal/core/global_format_config.ts b/packages/devextreme/js/__internal/core/global_format_config.ts index 49e5c3856b82..541bd5f93e89 100644 --- a/packages/devextreme/js/__internal/core/global_format_config.ts +++ b/packages/devextreme/js/__internal/core/global_format_config.ts @@ -1,7 +1,9 @@ import coreLocalization from '@js/common/core/localization/core'; import config from '@js/core/config'; -import { isFunction, isPlainObject, isString } from '@js/core/utils/type'; -import type { Format } from '@js/localization'; +import { + isDefined, isFunction, isPlainObject, isString, +} from '@js/core/utils/type'; +import type { Format, FormatObject } from '@js/localization'; import parentLocales from '@ts/core/localization/cldr-data/parent_locales'; import getParentLocale from '@ts/core/localization/parentLocale'; @@ -13,6 +15,53 @@ type GlobalFormatOptionName = 'dateFormat' | 'dateTimeFormat' | 'timeFormat' | ' const hasOwn = Object.prototype.hasOwnProperty; +const GLOBAL_FORMAT_DATA_TYPES = ['time', 'datetime', 'date']; + +const DEFAULT_IMPLICIT_PRESET_BY_DATA_TYPE = { + date: 'shortdate', + datetime: 'shortdateshorttime', + time: 'shorttime', +}; + +const DATA_TYPE_FORMAT_PRESET: Record = { + datetime: 'shortDateShortTime', + time: 'shortTime', +}; + +export const isLocaleOnlyFormat = (format: Format | undefined): format is FormatObject => { + if (!isPlainObject(format)) { + return false; + } + + const formatObject = format as FormatObject; + const definedKeys = Object.entries(formatObject) + .filter(([, value]) => value !== undefined); + + return definedKeys.length === 1 + && definedKeys[0][0] === 'locale' + && isDefined(formatObject.locale); +}; + +// A locale-only object has no date or time fields. Intl then formats it as a date, +// so datetime and time values lose their time. Attach the preset for that data type. +export const applyDataTypePreset = ( + format: Format | undefined, + dataType?: string, +): Format | undefined => { + const preset = dataType ? DATA_TYPE_FORMAT_PRESET[dataType] : undefined; + + if (!preset || !isLocaleOnlyFormat(format)) { + return format; + } + + return { + ...format, + type: preset, + }; +}; + +export type FormatLocale = string | (() => string); + const resolveByLocaleMap = (localeMap: LocaleMap): Format | undefined => { let currentLocale: string | false = coreLocalization.locale(); @@ -49,13 +98,19 @@ const resolveConfigValue = (value: GlobalFormatValue): Format | undefined => { return undefined; }; +const resolveFormatLocaleProperty = (formatObject: FormatObject): string | undefined => { + const formatLocale = formatObject.locale; + + return isFunction(formatLocale) ? formatLocale() : formatLocale; +}; + const resolveGlobalFormat = (optionName: GlobalFormatOptionName): Format | undefined => { const { [optionName]: optionValue } = config(); return resolveConfigValue(optionValue); }; -export const getGlobalFormatByDataType = (dataType: string): Format | undefined => { +export const getRawGlobalFormatByDataType = (dataType: string): Format | undefined => { switch (dataType) { case 'date': return resolveGlobalFormat('dateFormat'); @@ -70,6 +125,138 @@ export const getGlobalFormatByDataType = (dataType: string): Format | undefined } }; +export const getGlobalFormatByDataType = (dataType: string): Format | undefined => ( + applyDataTypePreset(getRawGlobalFormatByDataType(dataType), dataType) +); + +export type LocaleAwareFormatter = ((value: Date) => string) & { + withLocale?: (locale?: FormatLocale) => LocaleAwareFormatter; +}; + +const getLocaleAwareFormatter = (formatter: unknown): LocaleAwareFormatter | undefined => ( + isFunction((formatter as LocaleAwareFormatter | undefined)?.withLocale) + ? formatter as LocaleAwareFormatter + : undefined +); + +export const withFormatLocale = (format: Format, locale?: FormatLocale): Format | undefined => { + if (!locale || !isDefined(format)) { + return format; + } + + if (isFunction(format)) { + return getLocaleAwareFormatter(format)?.withLocale?.(locale) ?? format; + } + + if (isString(format)) return { type: format, locale }; + + const formatter = getLocaleAwareFormatter((format as FormatObject).formatter); + + return { + ...format, + ...formatter && { formatter: formatter.withLocale?.(locale) }, + locale, + } as FormatObject; +}; + +const getOwnFormatLocale = (format: Format | undefined): string | undefined => ( + isPlainObject(format) ? resolveFormatLocaleProperty(format as FormatObject) : undefined +); + +const getFormatType = (format: Format | undefined): string | undefined => ( + isPlainObject(format) ? (format as FormatObject).type : undefined +); + +const resolveDataTypeFromGlobalConfig = (presetName: string | undefined): string | undefined => { + if (!presetName) { + return undefined; + } + + const lowerPreset = String(presetName).toLowerCase(); + + for (const dataType of GLOBAL_FORMAT_DATA_TYPES) { + const globalFormatType = getFormatType(getGlobalFormatByDataType(dataType)); + + if (globalFormatType?.toLowerCase() === lowerPreset) { + return dataType; + } + } + + for (const dataType of GLOBAL_FORMAT_DATA_TYPES) { + if (DEFAULT_IMPLICIT_PRESET_BY_DATA_TYPE[dataType] === lowerPreset) { + return dataType; + } + } + + return undefined; +}; + +const inferDataTypeFromFormatObject = (format: Format): string | undefined => { + if (!isPlainObject(format)) { + return undefined; + } + + const { + hour, minute, second, year, month, day, weekday, + } = format as Intl.DateTimeFormatOptions; + const hasTime = hour !== undefined || minute !== undefined || second !== undefined; + const hasDate = year !== undefined + || month !== undefined + || day !== undefined + || weekday !== undefined; + + if (hasTime && hasDate) { + return 'datetime'; + } + + if (hasTime) { + return 'time'; + } + + if (hasDate) { + return 'date'; + } + + return undefined; +}; + +export const getFormatterOptions = (format: Format): Format => { + if (!isPlainObject(format) || !Object.hasOwnProperty.call(format, 'locale')) { + return format; + } + + const stripped = { ...format } as FormatObject; + delete stripped.locale; + + return stripped; +}; + +export const getEffectiveFormatLocale = ( + format: Format, + dataType?: string, + presetName?: string, +): string => { + const ownLocale = getOwnFormatLocale(format); + + if (ownLocale) { + return ownLocale; + } + + const resolvedDataType = dataType + ?? resolveDataTypeFromGlobalConfig(presetName ?? getFormatType(format)) + ?? inferDataTypeFromFormatObject(format); + + if (resolvedDataType) { + const globalFormatLocale = getOwnFormatLocale(getGlobalFormatByDataType(resolvedDataType)); + + if (globalFormatLocale) { + return globalFormatLocale; + } + } + + return coreLocalization.locale(); +}; + export const resolvePresetOverride = (presetName: string): Format | undefined => { const { dateTimeFormatPresets: presets } = config(); @@ -87,7 +274,36 @@ export const resolvePresetOverride = (presetName: string): Format | undefined => return resolveConfigValue(presets[matchedKey]); }; +const resolveFormatWhenOmitted = (sourceFormat: Format): Format => { + const presetName = isString(sourceFormat) ? sourceFormat : getFormatType(sourceFormat); + const presetOverride = presetName ? resolvePresetOverride(presetName) : undefined; + + return presetOverride !== undefined && !isFunction(presetOverride) + ? presetOverride + : sourceFormat; +}; + +export const getDateFormatLocale = ( + sourceFormat: Format, + resolvedFormat?: Format, +): string => { + const effectiveResolved = resolvedFormat ?? resolveFormatWhenOmitted(sourceFormat); + const localeSource = getOwnFormatLocale(sourceFormat) ? sourceFormat : effectiveResolved; + const sourcePresetName = isString(sourceFormat) + ? sourceFormat + : getFormatType(sourceFormat) ?? (isString(effectiveResolved) ? effectiveResolved : undefined); + + return getEffectiveFormatLocale( + isPlainObject(localeSource) ? localeSource : undefined, + undefined, + sourcePresetName, + ); +}; + export default { getGlobalFormatByDataType, resolvePresetOverride, + getEffectiveFormatLocale, + getDateFormatLocale, + getFormatterOptions, }; diff --git a/packages/devextreme/js/__internal/core/localization/core.ts b/packages/devextreme/js/__internal/core/localization/core.ts index 726a34befea6..34ec5d0c4687 100644 --- a/packages/devextreme/js/__internal/core/localization/core.ts +++ b/packages/devextreme/js/__internal/core/localization/core.ts @@ -29,8 +29,9 @@ export default dependencyInjector({ getValueByClosestLocale( getter: (locale: string) => TValue | undefined, + startLocale?: string, ): TValue | undefined { - let locale: string = this.locale(); + let locale: string = startLocale ?? this.locale(); let value = getter(locale); let isRootLocale = false; diff --git a/packages/devextreme/js/__internal/core/localization/date.ts b/packages/devextreme/js/__internal/core/localization/date.ts index 112cd6d32bee..ff705d02024f 100644 --- a/packages/devextreme/js/__internal/core/localization/date.ts +++ b/packages/devextreme/js/__internal/core/localization/date.ts @@ -8,6 +8,7 @@ import defaultDateNames from '@ts/core/localization/default_date_names'; import intlDateLocalization from '@ts/core/localization/intl/date'; import { getFormat as getLDMLDateFormat } from '@ts/core/localization/ldml/date.format'; import { getFormatter as getLDMLDateFormatter } from '@ts/core/localization/ldml/date.formatter'; +import type { LdlmDateLocalization } from '@ts/core/localization/ldml/date.parser'; import { getParser as getLDMLDateParser } from '@ts/core/localization/ldml/date.parser'; import numberLocalization from '@ts/core/localization/number'; import { injector as dependencyInjector } from '@ts/core/utils/dependency_injector'; @@ -71,6 +72,7 @@ const dateLocalization = dependencyInjector({ _resolveStringFormat( format: string, date: Date, + dateParts: LdlmDateLocalization = this, ): string | undefined { const presetOverride = resolvePresetOverride(format); @@ -86,7 +88,7 @@ const dateLocalization = dependencyInjector({ ] || presetOverride; return numberLocalization.convertDigits( - getLDMLDateFormatter(pattern, this)(date), + getLDMLDateFormatter(pattern, dateParts)(date), ); } @@ -148,7 +150,11 @@ const dateLocalization = dependencyInjector({ return undefined; }, - format(date: Date, format: LocalizationFormat): Date | string | undefined { + format( + date: Date, + format: LocalizationFormat, + dateParts: LdlmDateLocalization = this, + ): Date | string | undefined { if (!date) { return undefined; } @@ -168,7 +174,7 @@ const dateLocalization = dependencyInjector({ // eslint-disable-next-line no-param-reassign format = (format as FormatObject).type ?? format; if (isString(format)) { - const resolvedFormat = this._resolveStringFormat(format, date); + const resolvedFormat = this._resolveStringFormat(format, date, dateParts); if (resolvedFormat !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return @@ -178,7 +184,7 @@ const dateLocalization = dependencyInjector({ // eslint-disable-next-line no-param-reassign format = (FORMATS_TO_PATTERN_MAP[format.toLowerCase()] || format) as string; - return numberLocalization.convertDigits(getLDMLDateFormatter(format, this)(date)); + return numberLocalization.convertDigits(getLDMLDateFormatter(format, dateParts)(date)); } } @@ -190,7 +196,11 @@ const dateLocalization = dependencyInjector({ return formatter(date); }, - parse(text: string, format?: FormatObject | string): Date | null | undefined { + parse( + text: string, + format?: FormatObject | string, + dateParts: LdlmDateLocalization = this, + ): Date | null | undefined { // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; // eslint-disable-next-line @typescript-eslint/init-declarations @@ -229,7 +239,7 @@ const dateLocalization = dependencyInjector({ if (ldmlFormat) { // eslint-disable-next-line no-param-reassign text = numberLocalization.convertDigits(text, true); - return getLDMLDateParser(ldmlFormat, this)(text); + return getLDMLDateParser(ldmlFormat, dateParts)(text); } errors.log('W0012'); diff --git a/packages/devextreme/js/__internal/core/localization/format.locale.test.ts b/packages/devextreme/js/__internal/core/localization/format.locale.test.ts new file mode 100644 index 000000000000..ba064d7002ea --- /dev/null +++ b/packages/devextreme/js/__internal/core/localization/format.locale.test.ts @@ -0,0 +1,388 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import coreLocalization from '@js/common/core/localization/core'; +import dateLocalization from '@js/common/core/localization/date'; +import numberLocalization from '@js/common/core/localization/number'; +import config from '@js/core/config'; +import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; + +const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; +type GlobalFormatKey = typeof GLOBAL_FORMAT_KEYS[number]; + +const saveAndRestore = (): { save: () => void; restore: () => void } => { + let savedValues: Partial> = {}; + let savedLocale = ''; + + return { + save() { + savedLocale = coreLocalization.locale(); + const currentConfig = config(); + + savedValues = {}; + GLOBAL_FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }, + restore() { + coreLocalization.locale(savedLocale); + 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] as never; + } + }); + }, + }; +}; + +describe('format locale integration', () => { + const { save, restore } = saveAndRestore(); + + beforeEach(() => { save(); }); + afterEach(() => { restore(); }); + + describe('numbers', () => { + it('should format using global numberFormat locale instead of message locale', () => { + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + expect(numberLocalization.format(1234.56)).toBe('1,234.56'); + }); + + it('should apply global numberFormat locale to explicit format type', () => { + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + expect(numberLocalization.format(1234.56, { type: 'fixedPoint', precision: 0 })).toBe('1,235'); + }); + + it('should prefer explicit format locale over global numberFormat locale', () => { + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + expect(numberLocalization.format(1234.56, { + type: 'fixedPoint', + precision: 0, + locale: 'de-DE', + })).toBe('1.235'); + }); + + it('should apply explicit locale separators to an LDML format object', () => { + coreLocalization.locale('en'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'fr', + }, + }, + }); + + expect(numberLocalization.format(1234.5, { + type: '#,##0.00', + locale: 'de-DE', + })).toBe('1.234,50'); + expect(numberLocalization.parse('1.234,50', { + type: '#,##0.00', + locale: 'de-DE', + })).toBe(1234.5); + }); + + it('should parse using effective number format locale separators', () => { + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + expect(numberLocalization.parse('1234.56')).toBe(1234.56); + }); + + it('should apply message locale separators to LDML global numberFormat', () => { + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: '#,##0.00', + }); + + expect(numberLocalization.format(1234.5)).toBe('1.234,50'); + }); + + it('should apply dynamic global numberFormat locale at format time', () => { + let dynamicLocale = 'en-US'; + + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: () => dynamicLocale, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + expect(numberLocalization.format(1234.56)).toBe('1,234.56'); + + dynamicLocale = 'de-DE'; + expect(numberLocalization.format(1234.56)).toBe('1.234,56'); + }); + + it('should use global numberFormat locale for decimal and thousands separators', () => { + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + expect(numberLocalization.getDecimalSeparator()).toBe('.'); + expect(numberLocalization.getThousandsSeparator()).toBe(','); + }); + + it('should not pass locale metadata to Intl.NumberFormat options', () => { + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 3, + maximumFractionDigits: 3, + }, + }, + }); + + const numberFormatSpy = jest.spyOn(Intl, 'NumberFormat'); + numberLocalization.format(1.234); + + numberFormatSpy.mock.calls.forEach(([, options]) => { + expect(options).not.toHaveProperty('locale'); + }); + + numberFormatSpy.mockRestore(); + }); + }); + + describe('dates', () => { + it.each([ + { alias: 'longDate', expected: 'Donnerstag, 2. Januar 2020' }, + { alias: { default: { type: 'longDate' } }, expected: 'Donnerstag, 2. Januar 2020' }, + { alias: { default: { year: 'numeric', month: 'long', day: 'numeric' } }, expected: '2. Januar 2020' }, + ])('should keep source preset locale when dateTimeFormatPresets aliases it to $alias', ({ alias, expected }) => { + coreLocalization.locale('en'); + config({ + ...config(), + dateFormat: { default: { type: 'shortDate', locale: 'de-DE' } }, + dateTimeFormatPresets: { shortDate: alias }, + } as never); + + expect(dateLocalization.format(new Date(2020, 0, 2), 'shortDate')).toBe(expected); + }); + + it('should format implicit shortDate using global dateFormat locale', () => { + coreLocalization.locale('en'); + config({ + ...config(), + dateFormat: { + default: { + locale: 'de-DE', + type: 'shortDate', + }, + }, + }); + + expect(dateLocalization.format(new Date(2020, 0, 2), { + locale: 'de-DE', + type: 'shortDate', + })).toBe('2.1.2020'); + }); + + it('should format implicit shortDate preset using global dateFormat locale', () => { + coreLocalization.locale('en'); + config({ + ...config(), + dateFormat: { + default: { + locale: 'de-DE', + type: 'shortDate', + }, + }, + }); + + expect(dateLocalization.format(new Date(2020, 0, 2), 'shortDate')).toBe('2.1.2020'); + }); + + it('should keep time when a locale-only dateTimeFormat is applied', () => { + coreLocalization.locale('en'); + config({ + ...config(), + dateTimeFormat: { + default: { locale: 'de-DE' }, + }, + }); + + expect(dateLocalization.format( + new Date(2024, 5, 15, 14, 30), + getGlobalFormatByDataType('datetime'), + )).toBe('15.6.2024, 14:30'); + }); + + it('should format using explicit date format locale', () => { + coreLocalization.locale('en'); + + expect(dateLocalization.format(new Date(2020, 0, 2), { + locale: 'de-DE', + year: 'numeric', + month: '2-digit', + day: '2-digit', + })).toBe('02.01.2020'); + }); + + it('should format LDML month and day names using the format locale', () => { + coreLocalization.locale('en'); + + expect(dateLocalization.format(new Date(2020, 5, 15), { + type: 'EEEE, dd MMMM yyyy', + locale: 'de', + })).toBe('Montag, 15 Juni 2020'); + }); + + it('should not pass locale metadata to Intl.DateTimeFormat options', () => { + const dateTimeFormatSpy = jest.spyOn(Intl, 'DateTimeFormat'); + + coreLocalization.locale('en'); + + dateLocalization.format(new Date(2021, 5, 15), { + locale: 'de-DE', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + + dateTimeFormatSpy.mock.calls.forEach(([, options]) => { + expect(options).not.toHaveProperty('locale'); + }); + + dateTimeFormatSpy.mockRestore(); + }); + }); + + describe('date parsing', () => { + const date = new Date(2020, 0, 2); + + it.each([ + { type: 'shortDate', locale: 'en-US' }, + { type: 'longDate', locale: 'en-US' }, + { type: 'monthAndYear', locale: 'en-US' }, + { + year: 'numeric', month: 'long', day: 'numeric', locale: 'en-US', + }, + ])('should parse text formatted with explicit format locale %j', (format) => { + coreLocalization.locale('de'); + + const text = dateLocalization.format(date, format as never) as string; + const parsed = dateLocalization.parse(text, format as never) as Date; + + expect(parsed.getFullYear()).toBe(2020); + expect(parsed.getMonth()).toBe(0); + }); + + it('should parse month names using global dateFormat locale', () => { + coreLocalization.locale('de'); + config({ + ...config(), + dateFormat: { default: { type: 'longDate', locale: 'en-US' } }, + }); + + const parsed = dateLocalization.parse('Thursday, January 2, 2020', 'longDate') as Date; + + expect(parsed).toEqual(date); + }); + + it('should parse an LDML pattern using the format locale month names', () => { + coreLocalization.locale('en'); + const format = { type: 'dd MMMM yyyy', locale: 'de' }; + + const parsed = dateLocalization.parse('15 Juni 2020', format as never) as Date; + + expect(parsed).toEqual(new Date(2020, 5, 15)); + }); + + it('should keep parsing month names in message locale without format locale', () => { + coreLocalization.locale('de'); + + const text = dateLocalization.format(date, 'longDate') as string; + + expect(dateLocalization.parse(text, 'longDate')).toEqual(date); + }); + }); + + describe('currency', () => { + it('should parse currency using explicit format locale separators', () => { + coreLocalization.locale('de'); + + expect(numberLocalization.parse('€1,234.50', { + type: 'currency', currency: 'EUR', locale: 'en-US', + } as never)).toBe(1234.5); + }); + + it('should use global numberFormat locale for OpenXML currency format', () => { + coreLocalization.locale('de'); + const messageLocaleFormat = numberLocalization.getOpenXmlCurrencyFormat('EUR'); + + config({ + ...config(), + numberFormat: { default: { locale: 'en-US' } }, + }); + + expect(numberLocalization.getOpenXmlCurrencyFormat('EUR')).not.toBe(messageLocaleFormat); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/core/localization/globalize/currency.ts b/packages/devextreme/js/__internal/core/localization/globalize/currency.ts index 677462c74845..89e68932a9b0 100644 --- a/packages/devextreme/js/__internal/core/localization/globalize/currency.ts +++ b/packages/devextreme/js/__internal/core/localization/globalize/currency.ts @@ -4,6 +4,7 @@ import '@ts/core/localization/currency'; import 'globalize/currency'; import config from '@js/core/config'; +import { getEffectiveFormatLocale, getFormatterOptions } from '@ts/core/global_format_config'; import type { FormatConfig, LocalizationFormat, NormalizedConfig } from '@ts/core/localization/number'; import numberLocalization from '@ts/core/localization/number'; import openXmlCurrencyFormat from '@ts/core/localization/open_xml_currency_format'; @@ -11,6 +12,7 @@ import openXmlCurrencyFormat from '@ts/core/localization/open_xml_currency_forma import Globalize from 'globalize'; const CURRENCY_STYLES = ['symbol', 'accounting']; +const NUMBER_DATA_TYPE = 'number'; type Formatter = (value: number) => string; @@ -22,6 +24,7 @@ if (Globalize?.formatCurrency) { const formattersCache: Record = {}; const getFormatter = ( + formatLocale: string, currency: string | undefined, format: string | FormatConfig | undefined, ): Formatter => { @@ -31,13 +34,13 @@ if (Globalize?.formatCurrency) { let formatCacheKey: string; if (typeof format === 'object') { - formatCacheKey = `${Globalize.locale().locale}:${currency}:${JSON.stringify(format)}`; + formatCacheKey = `${formatLocale}:${currency}:${JSON.stringify(format)}`; } else { - formatCacheKey = `${Globalize.locale().locale}:${currency}:${format}`; + formatCacheKey = `${formatLocale}:${currency}:${format}`; } formatter = formattersCache[formatCacheKey]; if (!formatter) { - formatter = Globalize.currencyFormatter(currency, format); + formatter = Globalize(formatLocale).currencyFormatter(currency, format); formattersCache[formatCacheKey] = formatter; } @@ -50,6 +53,7 @@ if (Globalize?.formatCurrency) { const currency = formatConfig?.currency ?? config().defaultCurrency; return getFormatter( + getEffectiveFormatLocale(formatConfig, NUMBER_DATA_TYPE), currency, this._normalizeFormatConfig(format, formatConfig, value), )(value); @@ -96,7 +100,11 @@ if (Globalize?.formatCurrency) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this._formatNumber(value, this._parseNumberFormatString('currency'), normalizedFormat); } if (!normalizedFormat.type && normalizedFormat.currency) { - return getFormatter(normalizedFormat.currency, normalizedFormat)(value); + return getFormatter( + getEffectiveFormatLocale(normalizedFormat, NUMBER_DATA_TYPE), + normalizedFormat.currency, + getFormatterOptions(normalizedFormat) as FormatConfig, + )(value); } } @@ -109,12 +117,15 @@ if (Globalize?.formatCurrency) { currency = config().defaultCurrency; } + const formatLocale = getEffectiveFormatLocale(undefined, NUMBER_DATA_TYPE); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return Globalize.cldr.main(`numbers/currencies/${currency}`); + return Globalize(formatLocale).cldr.main(`numbers/currencies/${currency}`); }, getOpenXmlCurrencyFormat(currency?: string): string | undefined { const currencySymbol = this.getCurrencySymbol(currency).symbol; - const accountingFormat = Globalize.cldr.main('numbers/currencyFormats-numberSystem-latn').accounting; + const formatLocale = getEffectiveFormatLocale(undefined, NUMBER_DATA_TYPE); + const accountingFormat = Globalize(formatLocale).cldr.main('numbers/currencyFormats-numberSystem-latn').accounting; return openXmlCurrencyFormat(currencySymbol, accountingFormat); }, diff --git a/packages/devextreme/js/__internal/core/localization/globalize/date.ts b/packages/devextreme/js/__internal/core/localization/globalize/date.ts index ea9e2a5ca44e..147026b7440c 100644 --- a/packages/devextreme/js/__internal/core/localization/globalize/date.ts +++ b/packages/devextreme/js/__internal/core/localization/globalize/date.ts @@ -4,9 +4,14 @@ import '@ts/core/localization/globalize/number'; import 'globalize/date'; import type { Format as LocalizationFormat, FormatObject } from '@js/localization'; -import { resolvePresetOverride } from '@ts/core/global_format_config'; +import { + getDateFormatLocale, + getFormatterOptions, + resolvePresetOverride, +} from '@ts/core/global_format_config'; import type { DateFormatter, DateParser, Format } from '@ts/core/localization/date'; import dateLocalization from '@ts/core/localization/date'; +import { bindDatePartsToLocale } from '@ts/core/localization/ldml/date.parser'; 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 @@ -22,6 +27,73 @@ type GlobalizeFormat = { const ACCEPTABLE_JSON_FORMAT_PROPERTIES = ['skeleton', 'date', 'time', 'datetime', 'raw']; const RTL_MARKS_REGEX = /[\u200E\u200F]/g; +type GlobalizeDateFormatOptions = FormatObject & { + raw?: string; + skeleton?: string; + date?: string; + time?: string; + datetime?: string; +}; + +const GLOBALIZE_DEFAULT_DATE_FORMAT: GlobalizeDateFormatOptions = { skeleton: 'yMd' }; + +const resolveGlobalizeLocale = (formatLocale: string): string => { + const currentLocale = Globalize.locale().locale as string; + + if (formatLocale === currentLocale) { + return currentLocale; + } + + Globalize.locale(formatLocale); + try { + return Globalize.locale().locale as string; + } finally { + Globalize.locale(currentLocale); + } +}; + +interface GlobalizeInstance { + cldr: { main: (path: string) => unknown }; + parseDate: (value: string, format?: GlobalizeDateFormatOptions | string) => Date | null; +} + +const getGlobalizeByLocale = (formatLocale?: string): GlobalizeInstance => { + if (!formatLocale) { + return Globalize as GlobalizeInstance; + } + + const resolvedLocale = resolveGlobalizeLocale(formatLocale); + + return resolvedLocale === Globalize.locale().locale + ? Globalize as GlobalizeInstance + : new Globalize(resolvedLocale) as GlobalizeInstance; +}; + +const getCldrMain = ( + path: string, + formatLocale?: string, +): TValue => getGlobalizeByLocale(formatLocale).cldr.main(path) as TValue; + +const hasDefinedOption = (format: FormatObject): boolean => Object + .values(format) + .some((value) => value !== undefined); + +const getGlobalizeDateFormatter = ( + formatLocale: string, + format: GlobalizeDateFormatOptions | string, +): DateFormatter => { + const resolvedLocale = resolveGlobalizeLocale(formatLocale); + const currentLocale = Globalize.locale().locale; + + if (resolvedLocale === currentLocale) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return Globalize.dateFormatter(format); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return new Globalize(resolvedLocale).dateFormatter(format); +}; + if (Globalize?.formatDate) { if (Globalize.locale().locale === 'en') { Globalize.locale('en'); @@ -96,7 +168,20 @@ if (Globalize?.formatDate) { return 'globalize'; }, - _getPatternByFormat(format: string): string | undefined { + _getGlobalizeFormatterOptions( + format: string, + formatLocale?: string, + ): GlobalizeDateFormatOptions { + if (format.toLowerCase() === 'datetime-local') { + return { raw: 'yyyy-MM-ddTHH\':\'mm\':\'ss' }; + } + + return { + raw: this._getPatternByFormat(format, formatLocale) || format, + }; + }, + + _getPatternByFormat(format: string, formatLocale?: string): string | undefined { // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; const lowerFormat = format.toLowerCase(); @@ -111,39 +196,49 @@ if (Globalize?.formatDate) { } let result: string = 'path' in globalizeFormat - ? that._getFormatStringByPath(globalizeFormat.path) + ? that._getFormatStringByPath(globalizeFormat.path, formatLocale) : globalizeFormat.pattern; if ('parts' in globalizeFormat) { iteratorUtils.each(globalizeFormat.parts, (index: number, part: string): void => { - result = result.replace(`{${index}}`, that._getPatternByFormat(part)); + result = result.replace(`{${index}}`, that._getPatternByFormat(part, formatLocale) ?? ''); }); } return result; }, - _getFormatStringByPath(path: string): string { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return Globalize.locale().main(`dates/calendars/gregorian/${path}`); + _getFormatStringByPath(path: string, formatLocale?: string): string { + return getCldrMain(`dates/calendars/gregorian/${path}`, formatLocale); }, - getPeriodNames(format?: Format, type?: string): string[] { + getPeriodNames(format?: Format, type?: string, formatLocale?: string): string[] { const nameFormat = format || 'wide'; const nameType = type === 'format' ? type : 'stand-alone'; - const json: Record = Globalize.locale().main(`dates/calendars/gregorian/dayPeriods/${nameType}/${nameFormat}`); + const json = getCldrMain>( + `dates/calendars/gregorian/dayPeriods/${nameType}/${nameFormat}`, + formatLocale, + ); return [json.am, json.pm]; }, - getMonthNames(format: Format, type?: string): string[] { - const months: Record = Globalize.locale().main(`dates/calendars/gregorian/months/${type === 'format' ? type : 'stand-alone'}/${format || 'wide'}`); + getMonthNames(format: Format, type?: string, formatLocale?: string): string[] { + const nameType = type === 'format' ? type : 'stand-alone'; + const months = getCldrMain>( + `dates/calendars/gregorian/months/${nameType}/${format || 'wide'}`, + formatLocale, + ); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return iteratorUtils.map(months, (month: string): string => month); }, - getDayNames(format: Format): string[] { - const days: Record = Globalize.locale().main(`dates/calendars/gregorian/days/stand-alone/${format || 'wide'}`); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getDayNames(format: Format, type?: string, formatLocale?: string): string[] { + const days = getCldrMain>( + `dates/calendars/gregorian/days/stand-alone/${format || 'wide'}`, + formatLocale, + ); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return iteratorUtils.map(days, (day: string): string => day); @@ -167,11 +262,6 @@ if (Globalize?.formatDate) { return date; } - // eslint-disable-next-line @typescript-eslint/init-declarations - let formatter: DateFormatter; - // eslint-disable-next-line @typescript-eslint/init-declarations - let formatCacheKey: string; - if (typeof format === 'function') { return (format as DateFormatter)(date); } @@ -181,45 +271,71 @@ if (Globalize?.formatDate) { return (format.formatter as DateFormatter)(date); } - // eslint-disable-next-line no-param-reassign - format = (format as FormatObject).type ?? format; + const sourceFormat = format; + let resolvedFormat: LocalizationFormat = (format as FormatObject).type ?? format; - if (typeof format === 'string') { - const presetOverride = resolvePresetOverride(format); + if (typeof resolvedFormat === 'string') { + const presetOverride = resolvePresetOverride(resolvedFormat); 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)); - } + + resolvedFormat = presetOverride as LocalizationFormat; } + } - formatCacheKey = `${Globalize.locale().locale}:${format}`; + // eslint-disable-next-line @typescript-eslint/init-declarations + let formatter: DateFormatter; + // eslint-disable-next-line @typescript-eslint/init-declarations + let formatCacheKey: string; + const formatLocale = getDateFormatLocale(sourceFormat, resolvedFormat); + + if (typeof resolvedFormat === 'string') { + const resolvedLocale = resolveGlobalizeLocale(formatLocale); + formatCacheKey = `${resolvedLocale}:${resolvedFormat}`; formatter = formattersCache[formatCacheKey]; if (!formatter) { - // eslint-disable-next-line no-param-reassign - format = { - // @ts-expect-error - raw: this._getPatternByFormat(format) || format, - }; + const globalizeFormat = this._getGlobalizeFormatterOptions(resolvedFormat, formatLocale); - formatter = Globalize.dateFormatter(format); + formatter = getGlobalizeDateFormatter(formatLocale, globalizeFormat); formattersCache[formatCacheKey] = formatter; } - } else { - if (!this._isAcceptableFormat(format)) { - return undefined; - } + } else if (isObject(resolvedFormat)) { + const typedFormat = resolvedFormat as FormatObject; + const typeFormat = typedFormat.type; + + if (typeFormat && typeof typeFormat === 'string') { + const resolvedLocale = resolveGlobalizeLocale(formatLocale); + formatCacheKey = `${resolvedLocale}:${typeFormat}`; + formatter = formattersCache[formatCacheKey]; + if (!formatter) { + const globalizeFormat = this._getGlobalizeFormatterOptions(typeFormat, formatLocale); + + formatter = getGlobalizeDateFormatter(formatLocale, globalizeFormat); + formattersCache[formatCacheKey] = formatter; + } + } else { + const strippedOptions = getFormatterOptions(typedFormat) as FormatObject; + let formatterOptions: GlobalizeDateFormatOptions | undefined = strippedOptions; + + if (!this._isAcceptableFormat(strippedOptions)) { + // Locale-only formats stay without a preset for dates. Globalize rejects + // an empty options object; its own default is numeric year, month, and day. + formatterOptions = hasDefinedOption(strippedOptions) + ? undefined + : GLOBALIZE_DEFAULT_DATE_FORMAT; + } + + if (!formatterOptions) { + return undefined; + } - formatter = Globalize.dateFormatter(format); + formatter = getGlobalizeDateFormatter(formatLocale, formatterOptions); + } + } else { + return undefined; } // eslint-disable-next-line @typescript-eslint/no-unsafe-return @@ -231,35 +347,43 @@ if (Globalize?.formatDate) { return undefined; } - if (!format || typeof format === 'function' || (isObject(format) && !this._isAcceptableFormat(format))) { - if (format) { - const parsedValue: Date | null | undefined = this.callBase(text, format); - if (parsedValue) { - return parsedValue; - } - } + if (typeof format === 'function') { + const parsedValue: Date | null | undefined = this.callBase(text, format); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return parsedValue ?? Globalize.parseDate(text); + } + + if (!format) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Globalize.parseDate(text); } - if ((format as FormatObject).parser) { - // @ts-expect-error - return (format.parser as DateParser)(text); + const formatLocale = getDateFormatLocale(format); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const dateParts = bindDatePartsToLocale(this, formatLocale); + const parserFormat = getFormatterOptions(format) as FormatObject | string; + + if (isObject(parserFormat) && !this._isAcceptableFormat(parserFormat)) { + const parsedValue: Date | null | undefined = this.callBase(text, format, dateParts); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return parsedValue ?? getGlobalizeByLocale(formatLocale).parseDate(text); } - if (typeof format === 'string') { - // eslint-disable-next-line no-param-reassign - format = { - // @ts-expect-error - raw: this._getPatternByFormat(format) || format, - }; + if ((parserFormat as FormatObject).parser) { + return ((parserFormat as FormatObject).parser as DateParser)(text); } - const parsedDate: Date | null | undefined = Globalize.parseDate(text, format); + const globalizeFormat = typeof parserFormat === 'string' + ? { raw: this._getPatternByFormat(parserFormat, formatLocale) || parserFormat } + : parserFormat; + + const parsedDate: Date | null | undefined = getGlobalizeByLocale(formatLocale) + .parseDate(text, globalizeFormat); // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return parsedDate ?? this.callBase(text, format); + return parsedDate ?? this.callBase(text, globalizeFormat, dateParts); }, _isAcceptableFormat(format: FormatObject): boolean { diff --git a/packages/devextreme/js/__internal/core/localization/globalize/number.ts b/packages/devextreme/js/__internal/core/localization/globalize/number.ts index 0326f42c54c8..94e5d083dd7a 100644 --- a/packages/devextreme/js/__internal/core/localization/globalize/number.ts +++ b/packages/devextreme/js/__internal/core/localization/globalize/number.ts @@ -2,14 +2,23 @@ import '@ts/core/localization/globalize/core'; import 'globalize/number'; import errors from '@js/core/errors'; -import type { - FormatConfig, LocalizationFormat, NormalizedConfig, NumberFormatter, +import { + getEffectiveFormatLocale, + getFormatterOptions, + getGlobalFormatByDataType, +} from '@ts/core/global_format_config'; +import numberLocalization, { + type FormatConfig, + isLdmlPatternFormat, + type LocalizationFormat, + type NormalizedConfig, + type NumberFormatter, } from '@ts/core/localization/number'; -import numberLocalization from '@ts/core/localization/number'; // eslint-disable-next-line import/no-extraneous-dependencies import Globalize from 'globalize'; const MAX_FRACTION_DIGITS = 20; +const NUMBER_DATA_TYPE = 'number'; if (Globalize?.formatNumber) { if (Globalize.locale().locale === 'en') { @@ -18,20 +27,23 @@ if (Globalize?.formatNumber) { const formattersCache: Record = {}; - const getFormatter = (format: string | NormalizedConfig | undefined): NumberFormatter => { + const getFormatter = ( + formatLocale: string, + format: string | NormalizedConfig | undefined, + ): NumberFormatter => { // eslint-disable-next-line @typescript-eslint/init-declarations let formatter: NumberFormatter; // eslint-disable-next-line @typescript-eslint/init-declarations let formatCacheKey: string; if (typeof format === 'object') { - formatCacheKey = `${Globalize.locale().locale}:${JSON.stringify(format)}`; + formatCacheKey = `${formatLocale}:${JSON.stringify(format)}`; } else { - formatCacheKey = `${Globalize.locale().locale}:${format}`; + formatCacheKey = `${formatLocale}:${format}`; } formatter = formattersCache[formatCacheKey]; if (!formatter) { - formatter = Globalize.numberFormatter(format); + formatter = Globalize(formatLocale).numberFormatter(format); formattersCache[formatCacheKey] = formatter; } @@ -49,8 +61,27 @@ if (Globalize?.formatNumber) { return this.callBase.apply(this, [value, format, formatConfig]); } - return getFormatter(this._normalizeFormatConfig(format, formatConfig, value))(value); + return getFormatter( + getEffectiveFormatLocale(formatConfig, NUMBER_DATA_TYPE), + this._normalizeFormatConfig(format, formatConfig, value), + )(value); }, + + getDecimalSeparator(format?: FormatConfig | string): string { + const formatLocale = getEffectiveFormatLocale(format, NUMBER_DATA_TYPE); + + return getFormatter(formatLocale, { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + })(1.2)[1]; + }, + + getThousandsSeparator(format?: FormatConfig | string): string { + const formatLocale = getEffectiveFormatLocale(format, NUMBER_DATA_TYPE); + + return getFormatter(formatLocale, {})(10000)[2]; + }, + _normalizeFormatConfig( format: string, formatConfig: FormatConfig, @@ -105,13 +136,22 @@ if (Globalize?.formatNumber) { return value; } + const globalNumberFormat = getGlobalFormatByDataType(NUMBER_DATA_TYPE); + + if (!format && globalNumberFormat) { + // eslint-disable-next-line no-param-reassign + format = globalNumberFormat as LocalizationFormat; + } + // eslint-disable-next-line no-param-reassign format = this._normalizeFormat(format); // eslint-disable-next-line @stylistic/no-mixed-operators if (!format || typeof format !== 'function' && !(format as FormatConfig).type && !(format as FormatConfig).formatter) { - // @ts-expect-error - return getFormatter(format)(value); + const formatLocale = getEffectiveFormatLocale(format, NUMBER_DATA_TYPE); + const formatterOptions = getFormatterOptions(format) as NormalizedConfig; + + return getFormatter(formatLocale, formatterOptions)(value); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return @@ -126,7 +166,7 @@ if (Globalize?.formatNumber) { return undefined; } - if (format && (typeof format === 'string' || format.parser)) { + if (format && (typeof format === 'string' || format.parser || isLdmlPatternFormat(format))) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.callBase.apply(this, [text, format]); } @@ -137,7 +177,9 @@ if (Globalize?.formatNumber) { errors.log('W0011'); } - let result: number = Globalize.parseNumber(text); + let result: number = Globalize( + getEffectiveFormatLocale(format, NUMBER_DATA_TYPE), + ).parseNumber(text); if (isNaN(result)) { result = this.callBase.apply(this, [text, format]); diff --git a/packages/devextreme/js/__internal/core/localization/intl/date.ts b/packages/devextreme/js/__internal/core/localization/intl/date.ts index 4964694a7146..8a558fc50ffd 100644 --- a/packages/devextreme/js/__internal/core/localization/intl/date.ts +++ b/packages/devextreme/js/__internal/core/localization/intl/date.ts @@ -1,8 +1,14 @@ /* eslint-disable spellcheck/spell-checker */ import type { Format as LocalizationFormat, FormatObject } from '@js/localization'; -import { resolvePresetOverride } from '@ts/core/global_format_config'; +import { + getDateFormatLocale, + getEffectiveFormatLocale, + getFormatterOptions, + resolvePresetOverride, +} from '@ts/core/global_format_config'; import localizationCoreUtils from '@ts/core/localization/core'; import type { DateFormatter, Format } from '@ts/core/localization/date'; +import { bindDatePartsToLocale } from '@ts/core/localization/ldml/date.parser'; import { extend } from '@ts/core/utils/m_extend'; interface DateArgs { @@ -19,22 +25,32 @@ const SYMBOLS_TO_REMOVE_REGEX = /[\u200E\u200F]/g; const NARROW_NO_BREAK_SPACE_REGEX = /[\u202F]/g; const formattersCache: Record = {}; -const getFormatter = (format: Intl.DateTimeFormatOptions): DateFormatter => { - const key = `${localizationCoreUtils.locale()}/${JSON.stringify(format)}`; +const getFormatter = ( + format: Intl.DateTimeFormatOptions, + formatLocale = localizationCoreUtils.locale(), +): DateFormatter => { + const key = `${formatLocale}/${JSON.stringify(format)}`; if (!formattersCache[key]) { - formattersCache[key] = new Intl.DateTimeFormat(localizationCoreUtils.locale(), format).format; + formattersCache[key] = new Intl.DateTimeFormat(formatLocale, format).format; } return formattersCache[key]; }; -function formatDateTime(date: Date, format: Intl.DateTimeFormatOptions): string { - return getFormatter(format)(date) +function formatDateTime( + date: Date, + format: Intl.DateTimeFormatOptions, + formatLocale = localizationCoreUtils.locale(), +): string { + return getFormatter(format, formatLocale)(date) .replace(SYMBOLS_TO_REMOVE_REGEX, '') .replace(NARROW_NO_BREAK_SPACE_REGEX, ' '); } -const getIntlFormatter = (format: Intl.DateTimeFormatOptions) => (date: Date): string => { +const getIntlFormatter = ( + format: Intl.DateTimeFormatOptions, + formatLocale = localizationCoreUtils.locale(), +) => (date: Date): string => { // NOTE: Intl in some browsers formates dates with timezone offset // which was at the moment for this date. // But the method "new Date" creates date using current offset. @@ -61,14 +77,16 @@ const getIntlFormatter = (format: Intl.DateTimeFormatOptions) => (date: Date): s } const utcFormat = extend({ timeZone: 'UTC' }, format); - return formatDateTime(utcDate, utcFormat); + return formatDateTime(utcDate, utcFormat, formatLocale); } - return formatDateTime(date, format); + return formatDateTime(date, format, formatLocale); }; -// eslint-disable-next-line @stylistic/max-len -const formatNumber = (number: number): string => new Intl.NumberFormat(localizationCoreUtils.locale()).format(number); +const formatNumber = ( + number: number, + locale = localizationCoreUtils.locale(), +): string => new Intl.NumberFormat(locale).format(number); const getAlternativeNumeralsMap = (() => { const numeralsMapCache: Record> = {}; @@ -133,7 +151,8 @@ const intlFormats = { Object.defineProperty(intlFormats, 'shortdateshorttime', { get() { - const defaultOptions = Intl.DateTimeFormat(localizationCoreUtils.locale()).resolvedOptions(); + const formatLocale = getEffectiveFormatLocale(undefined, 'datetime'); + const defaultOptions = Intl.DateTimeFormat(formatLocale).resolvedOptions(); return { year: defaultOptions.year, month: defaultOptions.month, day: defaultOptions.day, hour: 'numeric', minute: 'numeric', @@ -144,15 +163,59 @@ Object.defineProperty(intlFormats, 'shortdateshorttime', { // eslint-disable-next-line @typescript-eslint/no-unsafe-return const getIntlFormat = (format): Intl.DateTimeFormatOptions => typeof format === 'string' && intlFormats[format.toLowerCase()]; +const formatWithIntlPreset = ( + date: Date, + presetName: string, + formatLocale: string, +): string | undefined => { + const intlFormat = getIntlFormat(presetName); + + if (!intlFormat) { + return undefined; + } + + return getIntlFormatter(intlFormat, formatLocale)(date); +}; + +const formatWithIntlOptions = ( + date: Date, + format: FormatObject | Intl.DateTimeFormatOptions, + formatLocale: string, +): string => { + const typeFormat = (format as FormatObject).type; + + if (typeFormat && typeof typeFormat === 'string') { + const intlPresetResult = formatWithIntlPreset(date, typeFormat, formatLocale); + + if (intlPresetResult !== undefined) { + return intlPresetResult; + } + } + + const formatterOptions = getFormatterOptions(format) as Intl.DateTimeFormatOptions; + + return getIntlFormatter(formatterOptions, formatLocale)(date); +}; + const monthNameStrategies = { - standalone(monthIndex: number, monthFormat: Intl.DateTimeFormatOptions['month']): string { + standalone( + monthIndex: number, + monthFormat: Intl.DateTimeFormatOptions['month'], + locale: string, + ): string { const date = new Date(1999, monthIndex, 13, 1); - return getIntlFormatter({ month: monthFormat })(date); + return getIntlFormatter({ month: monthFormat }, locale)(date); }, - format(monthIndex: number, monthFormat: Intl.DateTimeFormatOptions['month']): string { + format( + monthIndex: number, + monthFormat: Intl.DateTimeFormatOptions['month'], + locale: string, + ): string { const date = new Date(0, monthIndex, 13, 1); - const dateString = normalizeMonth(getIntlFormatter({ day: 'numeric', month: monthFormat })(date)); + const dateString = normalizeMonth( + getIntlFormatter({ day: 'numeric', month: monthFormat }, locale)(date), + ); const parts = dateString.split(' ').filter((part) => !part.includes('13')); if (parts.length === 1) { @@ -161,7 +224,7 @@ const monthNameStrategies = { return parts[0].length > parts[1].length ? parts[0] : parts[1]; // NOTE: For "lt" locale } - return monthNameStrategies.standalone(monthIndex, monthFormat); + return monthNameStrategies.standalone(monthIndex, monthFormat, locale); }, }; @@ -169,7 +232,11 @@ export default { engine(): string { return 'intl'; }, - getMonthNames(format: Format, type?: string): string[] { + getMonthNames( + format: Format, + type?: string, + locale = localizationCoreUtils.locale(), + ): string[] { // eslint-disable-next-line @typescript-eslint/no-shadow const intlFormats: Record, Intl.DateTimeFormatOptions['month']> = { wide: 'long', @@ -183,33 +250,45 @@ export default { return Array.from( { length: 12 }, // eslint-disable-next-line @typescript-eslint/no-unsafe-return - (_, monthIndex): string => monthNameStrategies[nameType](monthIndex, monthFormat), + (_, monthIndex): string => monthNameStrategies[nameType](monthIndex, monthFormat, locale), ); }, - getDayNames(format: Format): string[] { - // eslint-disable-next-line @typescript-eslint/no-shadow - const intlFormats: Record = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getDayNames(format: Format, type?: string, locale = localizationCoreUtils.locale()): string[] { + const intlDayFormats: Record = { wide: 'long', abbreviated: 'short', short: 'narrow', narrow: 'narrow', }; - // eslint-disable-next-line @typescript-eslint/no-shadow - const getIntlDayNames = (format: Intl.DateTimeFormatOptions['weekday']): string[] => Array.from( + const getIntlDayNames = ( + dayFormat: Intl.DateTimeFormatOptions['weekday'], + ): string[] => Array.from( { length: 7 }, - (_, dayIndex) => getIntlFormatter({ weekday: format })(new Date(0, 0, dayIndex)), + (_, dayIndex) => getIntlFormatter( + { weekday: dayFormat }, + locale, + )(new Date(0, 0, dayIndex)), ); - return getIntlDayNames(intlFormats[format || 'wide']); + return getIntlDayNames(intlDayFormats[format || 'wide']); }, - getPeriodNames(): string[] { - const hour12Formatter = getIntlFormatter({ hour: 'numeric', hour12: true }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getPeriodNames( + format?: Format, + type?: string, + locale = localizationCoreUtils.locale(), + ): string[] { + const hour12Formatter = getIntlFormatter( + { hour: 'numeric', hour12: true }, + locale, + ); return [1, 13].map((hours) => { - const hourNumberText = formatNumber(1); // NOTE: For "bn" locale + const hourNumberText = formatNumber(1, locale); // NOTE: For "bn" locale const timeParts = hour12Formatter(new Date(0, 0, 1, hours)).split(hourNumberText); if (timeParts.length !== 2) { @@ -231,38 +310,52 @@ export default { return date; } - // TODO: refactor (extract code form base) - if (typeof format !== 'function' && !(format as FormatObject).formatter) { - // eslint-disable-next-line no-param-reassign - format = (format as FormatObject).type ?? format; + if (typeof format === 'function') { + return (format as DateFormatter)(date); } - if (typeof format === 'string') { - const presetOverride = resolvePresetOverride(format); + if ((format as FormatObject).formatter) { + return ((format as FormatObject).formatter as DateFormatter)(date); + } + + const sourceFormat = format; + let resolvedFormat: LocalizationFormat = (format as FormatObject).type ?? format; + + if (typeof resolvedFormat === 'string') { + const presetOverride = resolvePresetOverride(resolvedFormat); if (presetOverride !== undefined) { if (typeof presetOverride === 'function') { return (presetOverride as DateFormatter)(date); } - // eslint-disable-next-line no-param-reassign - format = presetOverride as LocalizationFormat; + + resolvedFormat = presetOverride as LocalizationFormat; } } - const intlFormat = getIntlFormat(format); + const formatLocale = getDateFormatLocale(sourceFormat, resolvedFormat); - if (intlFormat) { - return getIntlFormatter(intlFormat)(date); - } + if (typeof resolvedFormat === 'string') { + const intlPresetResult = formatWithIntlPreset(date, resolvedFormat, formatLocale); + + if (intlPresetResult !== undefined) { + return intlPresetResult; + } + + const dateParts = formatLocale === localizationCoreUtils.locale() + ? this + : bindDatePartsToLocale(this, formatLocale); - const formatType = typeof format; - if ((format as FormatObject).formatter || formatType === 'function' || formatType === 'string') { // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.callBase.apply(this, [date, format]); + return this.callBase(date, resolvedFormat, dateParts); + } + + if (typeof resolvedFormat === 'object') { + return formatWithIntlOptions(date, resolvedFormat as FormatObject, formatLocale); } - // @ts-expect-error - return getIntlFormatter(format)(date); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.callBase.apply(this, [date, resolvedFormat]); }, parse(dateString: string, format?: FormatObject | string): Date | null | undefined { @@ -275,8 +368,14 @@ export default { formatter = (date: Date): string => normalizeMonth(this.format(date, format)); } + const formatLocale = format ? getDateFormatLocale(format) : localizationCoreUtils.locale(); + const dateParts = formatLocale === localizationCoreUtils.locale() + ? this + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + : bindDatePartsToLocale(this, formatLocale); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.callBase(dateString, formatter ?? format); + return this.callBase(dateString, formatter ?? format, dateParts); }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -295,16 +394,15 @@ export default { const dateArgs: DateArgs = this._generateDateArgs(formatParts, dateParts); - // eslint-disable-next-line @typescript-eslint/no-shadow - const constructDate = (dateArgs: DateArgs, ampmShift: boolean): Date => { + const constructDate = (args: DateArgs, ampmShift: boolean): Date => { const hoursShift = ampmShift ? 12 : 0; return new Date( - dateArgs.year, - dateArgs.month, - dateArgs.day, - (dateArgs.hours + hoursShift) % 24, - dateArgs.minutes, - dateArgs.seconds, + args.year, + args.month, + args.day, + (args.hours + hoursShift) % 24, + args.minutes, + args.seconds, ); }; const constructValidDate = (ampmShift: boolean): Date | undefined => { @@ -364,13 +462,14 @@ export default { return this.callBase.apply(this, [format]); }, getTimeSeparator(): string { + const formatLocale = getEffectiveFormatLocale(undefined, 'time'); const formatOptions: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric', hour12: false, }; return normalizeNumerals( - formatDateTime(new Date(2001, 1, 1, 11, 11), formatOptions), + formatDateTime(new Date(2001, 1, 1, 11, 11), formatOptions, formatLocale), ).replace(/\d/g, ''); }, @@ -381,8 +480,9 @@ export default { return this.callBase(format); } const intlFormat = extend({}, intlFormats[format.toLowerCase()]); + const messageLocale = localizationCoreUtils.locale(); const date = new Date(2001, 2, 4, 5, 6, 7); - let formattedDate = getIntlFormatter(intlFormat)(date); + let formattedDate = getIntlFormatter(intlFormat, messageLocale)(date); formattedDate = normalizeNumerals(formattedDate); diff --git a/packages/devextreme/js/__internal/core/localization/intl/number.ts b/packages/devextreme/js/__internal/core/localization/intl/number.ts index e8a0bf86a32f..b28e84dc294b 100644 --- a/packages/devextreme/js/__internal/core/localization/intl/number.ts +++ b/packages/devextreme/js/__internal/core/localization/intl/number.ts @@ -1,5 +1,9 @@ import dxConfig from '@js/core/config'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { + getEffectiveFormatLocale, + getFormatterOptions, + getGlobalFormatByDataType, +} from '@ts/core/global_format_config'; import accountingFormats from '@ts/core/localization/cldr-data/accounting_formats'; import localizationCoreUtils from '@ts/core/localization/core'; import type { FormatConfig as BaseFormatConfig, LocalizationFormat } from '@ts/core/localization/number'; @@ -19,20 +23,24 @@ type IntlFormatter = Intl.NumberFormat['format']; const CURRENCY_STYLES = ['standard', 'accounting'] as const; const MAX_FRACTION_DIGITS = 20; +const NUMBER_DATA_TYPE = 'number'; const detectCurrencySymbolRegex = /([^\s0]+)?(\s*)0*[.,]*0*(\s*)([^\s0]+)?/; const formattersCache: Record = {}; -const getFormatter = (format: NormalizedConfig): IntlFormatter => { - const key = `${localizationCoreUtils.locale()}/${JSON.stringify(format)}`; +const getFormatter = (formatLocale: string, format: NormalizedConfig): IntlFormatter => { + const key = `${formatLocale}/${JSON.stringify(format)}`; if (!formattersCache[key]) { - formattersCache[key] = new Intl.NumberFormat(localizationCoreUtils.locale(), format).format; + formattersCache[key] = new Intl.NumberFormat(formatLocale, format).format; } return formattersCache[key]; }; -const getCurrencyFormatter = (currency: string): Intl.NumberFormat => new Intl.NumberFormat(localizationCoreUtils.locale(), { style: 'currency', currency }); +const getCurrencyFormatter = (currency: string): Intl.NumberFormat => new Intl.NumberFormat( + getEffectiveFormatLocale(undefined, NUMBER_DATA_TYPE), + { style: 'currency', currency }, +); export default { engine(): string { @@ -44,7 +52,23 @@ export default { return this.callBase.apply(this, [value, format, formatConfig]); } - return getFormatter(this._normalizeFormatConfig(format, formatConfig, value))(value); + const formatLocale = getEffectiveFormatLocale(formatConfig, NUMBER_DATA_TYPE); + const normalizedConfig = this._normalizeFormatConfig(format, formatConfig, value); + + return getFormatter(formatLocale, normalizedConfig)(value); + }, + getDecimalSeparator(format?: FormatConfig | string): string { + const formatLocale = getEffectiveFormatLocale(format, NUMBER_DATA_TYPE); + + return getFormatter(formatLocale, { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + })(1.2)[1]; + }, + getThousandsSeparator(format?: FormatConfig | string): string { + const formatLocale = getEffectiveFormatLocale(format, NUMBER_DATA_TYPE); + + return getFormatter(formatLocale, {})(10000)[2]; }, _normalizeFormatConfig( format: string, @@ -107,7 +131,7 @@ export default { return value; } - const globalNumberFormat = getGlobalFormatByDataType('number'); + const globalNumberFormat = getGlobalFormatByDataType(NUMBER_DATA_TYPE); if (!format && globalNumberFormat) { // eslint-disable-next-line no-param-reassign @@ -123,7 +147,10 @@ export default { // eslint-disable-next-line @stylistic/no-mixed-operators if (!format || typeof format !== 'function' && !(format as FormatConfig).type && !(format as FormatConfig).formatter) { - return getFormatter(format)(value); + const formatLocale = getEffectiveFormatLocale(format, NUMBER_DATA_TYPE); + const formatterOptions = getFormatterOptions(format) as NormalizedConfig; + + return getFormatter(formatLocale, formatterOptions)(value); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return @@ -164,8 +191,11 @@ export default { const targetCurrency = currency || dxConfig().defaultCurrency; const currencySymbol: string = this._getCurrencySymbolInfo(targetCurrency).symbol; const closestAccountingFormat: string | undefined = localizationCoreUtils - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - .getValueByClosestLocale((locale: string): string => accountingFormats[locale]); + .getValueByClosestLocale( + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + (locale: string): string => accountingFormats[locale], + getEffectiveFormatLocale(undefined, NUMBER_DATA_TYPE), + ); return openXmlCurrencyFormat(currencySymbol, closestAccountingFormat); }, diff --git a/packages/devextreme/js/__internal/core/localization/ldml/date.parser.ts b/packages/devextreme/js/__internal/core/localization/ldml/date.parser.ts index 43752f07fbba..8d2f8c45be2b 100644 --- a/packages/devextreme/js/__internal/core/localization/ldml/date.parser.ts +++ b/packages/devextreme/js/__internal/core/localization/ldml/date.parser.ts @@ -11,6 +11,30 @@ export interface LdlmDateLocalization { getDayNames: (format: Format, type?: string) => string[]; } +type LocaleDateNamesGetter = (format: Format, type?: string, locale?: string) => string[]; + +export interface LocaleAwareDateLocalization extends LdlmDateLocalization { + getMonthNames: LocaleDateNamesGetter; + getPeriodNames: LocaleDateNamesGetter; + getDayNames: LocaleDateNamesGetter; +} + +export const bindDatePartsToLocale = ( + dateParts: LocaleAwareDateLocalization, + locale: string, +): LdlmDateLocalization => { + const result = Object.create(dateParts) as LdlmDateLocalization; + + result.getMonthNames = (format, type): string[] => dateParts + .getMonthNames(format, type, locale); + result.getPeriodNames = (format, type): string[] => dateParts + .getPeriodNames(format, type, locale); + result.getDayNames = (format, type): string[] => dateParts + .getDayNames(format, type, locale); + + return result; +}; + const FORMAT_TYPES: Record = { 3: 'abbreviated', 4: 'wide', diff --git a/packages/devextreme/js/__internal/core/localization/number.ts b/packages/devextreme/js/__internal/core/localization/number.ts index 67c8f5bbede1..6054616fa161 100644 --- a/packages/devextreme/js/__internal/core/localization/number.ts +++ b/packages/devextreme/js/__internal/core/localization/number.ts @@ -47,6 +47,12 @@ export type FormatConfig = PublicFormatConfig & { export type LocalizationFormat = Format | FormatConfig; +export const isLdmlPatternFormat = (format?: FormatConfig | string): boolean => { + const formatType = typeof format === 'string' ? format : format?.type; + + return typeof formatType === 'string' && (formatType.includes('0') || formatType.includes('#')); +}; + export interface NormalizedConfig { minimumFractionDigits?: number; maximumFractionDigits?: number; @@ -252,18 +258,22 @@ const numberLocalizationBase = { return format; }, - _getSeparators(): FormatterConfig { + _getSeparators(format?: LocalizationFormat): FormatterConfig { + const separatorFormat = typeof format === 'function' ? undefined : format; + return { - decimalSeparator: this.getDecimalSeparator(), - thousandsSeparator: this.getThousandsSeparator(), + decimalSeparator: this.getDecimalSeparator(separatorFormat), + thousandsSeparator: this.getThousandsSeparator(separatorFormat), }; }, - getThousandsSeparator(): string { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getThousandsSeparator(format?: FormatConfig | string): string { return this.format(10000, 'fixedPoint')[2] as string; }, - getDecimalSeparator(): string { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getDecimalSeparator(format?: FormatConfig | string): string { return this.format(1.2, { type: 'fixedPoint', precision: 1 })[1] as string; }, @@ -287,7 +297,7 @@ const numberLocalizationBase = { }, getNegativeEtalonRegExp(format: LocalizationFormat): RegExp { - const separators: FormatterConfig = this._getSeparators(); + const separators: FormatterConfig = this._getSeparators(format); const digitalRegExp = new RegExp(`[0-9${escapeRegExp(separators.decimalSeparator + separators.thousandsSeparator)}]+`, 'g'); const specialCharacters = ['\\', '(', ')', '[', ']', '*', '+', '$', '^', '?', '|', '{', '}']; @@ -348,7 +358,7 @@ const numberLocalizationBase = { const numberConfig: FormatObject | undefined = this._parseNumberFormatString((format as FormatConfig).type); if (!numberConfig) { - const formatterConfig: FormatterConfig = this._getSeparators(); + const formatterConfig: FormatterConfig = this._getSeparators(format); formatterConfig.unlimitedIntegerDigits = (format as FormatConfig).unlimitedIntegerDigits; // @ts-expect-error @@ -371,13 +381,14 @@ const numberLocalizationBase = { text = this.convertDigits(text, true); - if (format && typeof format !== 'string') { + if (format && typeof format !== 'string' && !isLdmlPatternFormat(format)) { // Current parser functionality provided as-is and // is independent of the most of capabilities of formatter. + // An LDML pattern object is the string pattern plus locale, which this parser does apply. errors.log('W0011'); } - const decimalSeparator: string = this.getDecimalSeparator(); + const decimalSeparator: string = this.getDecimalSeparator(format); const regExp = new RegExp(`[^0-9${escapeRegExp(decimalSeparator)}]`, 'g'); const cleanedText = text .replace(regExp, '') diff --git a/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts index 06b73325448e..49099c142b77 100644 --- a/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts +++ b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts @@ -118,6 +118,34 @@ describe('Formatting', () => { expect(getCurrentValueText(field, value, null)).toBe('9/5/2017, 12:30 PM'); }); + + it('locale-only format keeps time for datetime', () => { + const field: Field = { + dataType: 'datetime', + format: { locale: 'en-US' }, + }; + const value = new Date(2024, 5, 15, 14, 30); + + expect(getCurrentValueText(field, value, null)).toBe('6/15/2024, 2:30 PM'); + }); + + it('locale-only format stays date-only for date', () => { + const field: Field = { + dataType: 'date', + format: { locale: 'en-US' }, + }; + const value = new Date(2024, 5, 15, 14, 30); + + expect(getCurrentValueText(field, value, null)).toBe('6/15/2024'); + }); + + it('locale-only number format stays a number', () => { + const value = 1234.5; + const format = { locale: 'en-US' }; + + expect(getCurrentValueText({ dataType: 'number', format }, value, null)) + .toBe(getCurrentValueText({ format }, value, null)); + }); }); describe('getFilterOperations', () => { diff --git a/packages/devextreme/js/__internal/filter_builder/utils.ts b/packages/devextreme/js/__internal/filter_builder/utils.ts index e96903317764..d9cc22f427d0 100644 --- a/packages/devextreme/js/__internal/filter_builder/utils.ts +++ b/packages/devextreme/js/__internal/filter_builder/utils.ts @@ -16,7 +16,7 @@ import formatHelper from '@js/format_helper'; import type { CustomOperation, DataType, Field } from '@js/ui/filter_builder'; import filterUtils from '@js/ui/shared/filtering'; import errors from '@js/ui/widget/ui.errors'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { applyDataTypePreset, getGlobalFormatByDataType } from '@ts/core/global_format_config'; import { getConfig } from './between'; import filterOperationsDictionary from './filter_operations_dictionary'; @@ -88,7 +88,10 @@ function getDateFormat(dataType: DataType | undefined): Format | undefined { } function getFormattedValueText(field: Field, value: FieldValue): string { - const fieldFormat = field.format ?? getDateFormat(field.dataType); + const fieldFormat = applyDataTypePreset( + field.format ?? getDateFormat(field.dataType), + field.dataType, + ); if (isBoolean(value)) { const trueText = field.trueText ?? messageLocalization.format('dxDataGrid-trueText'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/column_format_locale.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/column_format_locale.integration.test.ts new file mode 100644 index 000000000000..206865aaec2e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/column_format_locale.integration.test.ts @@ -0,0 +1,67 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import coreLocalization from '@js/common/core/localization/core'; +import config from '@js/core/config'; + +import { + afterTest, + beforeTest, + createDataGrid, +} from './__mock__/helpers/utils'; + +describe('column format locale override', () => { + beforeEach(() => { + beforeTest(); + coreLocalization.locale('en'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'de-DE', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + dateTimeFormat: { + default: { + locale: 'de-DE', + type: 'shortDateShortTime', + }, + }, + }); + }); + + afterEach(() => { + afterTest(); + coreLocalization.locale('en'); + const currentConfig = config(); + delete currentConfig.numberFormat; + delete currentConfig.dateTimeFormat; + }); + + it('should keep the global format pattern and apply the column locale', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ + id: 1, + amount: 12345.678, + shippedAt: new Date(2024, 5, 15, 14, 30), + }], + columns: [ + { dataField: 'amount', dataType: 'number', format: { locale: 'en' } }, + { dataField: 'amount', dataType: 'number' }, + { dataField: 'shippedAt', dataType: 'datetime', format: { locale: 'en-US' } }, + { dataField: 'shippedAt', dataType: 'datetime' }, + ], + }); + + const cellText = (columnIndex: number): string => ( + (instance.getCellElement(0, columnIndex) as HTMLElement).textContent ?? '' + ); + + expect(cellText(0)).toBe('12,345.678'); + expect(cellText(1)).toBe('12.345,68'); + expect(cellText(2)).toBe('6/15/2024, 2:30 PM'); + expect(cellText(3)).toBe('15.6.2024, 14:30'); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/columns_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/columns_controller.ts index d26057090f0d..27de75d39e11 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/columns_controller.ts @@ -21,6 +21,7 @@ import Store from '@js/data/abstract_store'; import type { Grouping, GroupPanel } from '@js/ui/data_grid'; import filterUtils from '@js/ui/shared/filtering'; import errors from '@js/ui/widget/ui.errors'; +import { applyDataTypePreset } from '@ts/core/global_format_config'; import inflector from '@ts/core/utils/inflector'; import type { SortingInfo } from '@ts/data/utils'; import type { @@ -1327,7 +1328,10 @@ export class ColumnsController extends modules.Controller { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- skip empty text || getAlignmentByDataType(dataType, this.option('rtlEnabled')); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- skip falsy format - column.format = column.format || gridCoreUtils.getFormatByDataType(dataType); + column.format = applyDataTypePreset( + column.format || gridCoreUtils.getFormatByDataType(dataType), + dataType, + ); column.customizeText = column.customizeText ?? getCustomizeTextByDataType(dataType); column.defaultFilterOperations = column.defaultFilterOperations ?? (lookup ? [] : (DATATYPE_OPERATIONS[dataType] ?? [])); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts index 4e25785dad45..e187e59f7716 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts @@ -7,7 +7,7 @@ import { isDefined, isString, type, } from '@js/core/utils/type'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { applyDataTypePreset, getGlobalFormatByDataType } from '@ts/core/global_format_config'; import { getTreeNodeByPath, setTreeNodeByPath } from '@ts/grids/new/grid_core/utils/tree/index'; import type { ComponentType } from 'inferno'; @@ -64,10 +64,13 @@ export function normalizeColumn( const columnDataTypeDefaultOptions = defaultColumnPropertiesByDataType[dataType]; const shouldUseInferredFormat = column.dataType === undefined || columnFromDataOptions?.dataType === dataType; - const columnFormat = column.format - ?? (shouldUseInferredFormat ? columnFromDataOptions?.format : undefined) - ?? getGlobalColumnFormat(dataType) - ?? columnDataTypeDefaultOptions?.format; + const columnFormat = applyDataTypePreset( + column.format + ?? (shouldUseInferredFormat ? columnFromDataOptions?.format : undefined) + ?? getGlobalColumnFormat(dataType) + ?? columnDataTypeDefaultOptions?.format, + dataType, + ); const caption = captionize(column.name); const colWithDefaults = { diff --git a/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.display_format_locale.test.ts b/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.display_format_locale.test.ts new file mode 100644 index 000000000000..954f102e4490 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.display_format_locale.test.ts @@ -0,0 +1,256 @@ +import { + afterEach, describe, expect, it, +} from '@jest/globals'; +import coreLocalization from '@js/common/core/localization/core'; +import dateLocalization from '@js/common/core/localization/date'; +import config from '@js/core/config'; +import type { Properties } from '@js/ui/date_box'; +import DateBox from '@js/ui/date_box'; +import DateRangeBox from '@js/ui/date_range_box'; + +const widgets: { dispose: () => void }[] = []; +const value = new Date(2024, 5, 15, 14, 30); + +const createDateBox = (options: Partial): HTMLInputElement => { + const element = document.body.appendChild(document.createElement('div')); + const instance = new DateBox(element, options); + + widgets.push(instance); + + return element.querySelector('.dx-texteditor-input') as HTMLInputElement; +}; + +describe('locale-only displayFormat follows the editor type', () => { + afterEach(() => { + widgets.forEach((instance) => instance.dispose()); + widgets.length = 0; + document.body.innerHTML = ''; + }); + + it('keeps time for a datetime calendar', () => { + const input = createDateBox({ + type: 'datetime', + pickerType: 'calendar', + value, + displayFormat: { locale: 'en-US' }, + }); + + expect(input.value).toBe('6/15/2024, 2:30 PM'); + }); + + it('keeps time for datetime rollers', () => { + const input = createDateBox({ + type: 'datetime', + pickerType: 'rollers', + value, + displayFormat: { locale: 'de-DE' }, + }); + + expect(input.value).toBe('15.6.2024, 14:30'); + }); + + it('keeps time for a time editor', () => { + const input = createDateBox({ + type: 'time', + pickerType: 'list', + value, + displayFormat: { locale: 'en-US' }, + }); + + expect(input.value).toBe('2:30 PM'); + }); + + it('uses the format locale for month names in an LDML pattern', () => { + const input = createDateBox({ + type: 'date', + value: new Date(2024, 5, 15), + displayFormat: { type: 'dd MMMM yyyy', locale: 'de' }, + }); + + expect(input.value).toBe('15 Juni 2024'); + }); + + it('stays date-only for a date editor', () => { + const input = createDateBox({ + type: 'date', + pickerType: 'calendar', + value, + displayFormat: { locale: 'en-US' }, + }); + + expect(input.value).toBe('6/15/2024'); + }); + + it('keeps time in the datetime mask', () => { + const input = createDateBox({ + type: 'datetime', + pickerType: 'calendar', + useMaskBehavior: true, + value, + displayFormat: { locale: 'de-DE' }, + }); + + expect(input.value).toBe('15.6.2024, 14:30'); + }); + + it('stays date-only in DateRangeBox editors', () => { + const element = document.body.appendChild(document.createElement('div')); + const instance = new DateRangeBox(element, { + value: [value, value], + displayFormat: { locale: 'en-US' }, + }); + + widgets.push(instance); + + const inputs = element.querySelectorAll('.dx-texteditor-input'); + + expect(inputs[0].value).toBe('6/15/2024'); + expect(inputs[1].value).toBe('6/15/2024'); + }); + + it('edits a longDate mask that uses another locale', () => { + const element = document.body.appendChild(document.createElement('div')); + const instance = new DateBox(element, { + type: 'date', + pickerType: 'calendar', + useMaskBehavior: true, + value, + displayFormat: { type: 'longDate', locale: 'de' }, + }); + + widgets.push(instance); + + const input = element.querySelector('.dx-texteditor-input') as HTMLInputElement; + const textBeforeEdit = input.value; + + expect(textBeforeEdit).toContain('Juni'); + + const editor = instance as DateBox & { _upDownArrowHandler: (step: number) => void }; + + expect(() => editor._upDownArrowHandler(1)).not.toThrow(); + expect(input.value).not.toBe(textBeforeEdit); + }); + + it('rebuilds the mask when a locale function changes before the value', () => { + let currentLocale = 'de'; + const element = document.body.appendChild(document.createElement('div')); + const instance = new DateBox(element, { + type: 'date', + pickerType: 'calendar', + useMaskBehavior: true, + value, + displayFormat: { type: 'longDate', locale: () => currentLocale }, + }); + + widgets.push(instance); + + const input = element.querySelector('.dx-texteditor-input') as HTMLInputElement; + + expect(input.value).toContain('Juni'); + + currentLocale = 'en'; + instance.option('value', new Date(2024, 6, 20)); + + expect(input.value).toContain('July'); + + const editor = instance as DateBox & { _upDownArrowHandler: (step: number) => void }; + const textBeforeEdit = input.value; + + expect(() => editor._upDownArrowHandler(1)).not.toThrow(); + expect(input.value).not.toBe(textBeforeEdit); + expect(input.value).toContain('July'); + }); + + it('pastes a localized longDate into a mask that uses another locale', () => { + const element = document.body.appendChild(document.createElement('div')); + const instance = new DateBox(element, { + type: 'date', + pickerType: 'calendar', + useMaskBehavior: true, + value, + displayFormat: { type: 'longDate', locale: 'de' }, + }); + + widgets.push(instance); + + const input = element.querySelector('.dx-texteditor-input') as HTMLInputElement; + const pastedDate = new Date(2024, 6, 20); + const pastedText = dateLocalization.format(pastedDate, { + type: 'longDate', + locale: 'de', + }) as string; + + expect(input.value).toContain('Juni'); + expect(pastedText).toContain('Juli'); + + input.setSelectionRange(0, input.value.length); + + const editor = instance as DateBox & { + _maskPasteHandler: (e: { + originalEvent: { clipboardData: { getData: () => string } }; + preventDefault: () => void; + }) => void; + _maskValue?: Date | null; + }; + + editor._maskPasteHandler({ + originalEvent: { + clipboardData: { + getData: () => pastedText, + }, + }, + preventDefault: () => {}, + }); + + expect(editor._maskValue?.getFullYear()).toBe(2024); + expect(editor._maskValue?.getMonth()).toBe(6); + expect(editor._maskValue?.getDate()).toBe(20); + expect(input.value).toContain('Juli'); + }); + + it('edits a longDate mask whose locale comes from a preset override', () => { + const savedLocale = coreLocalization.locale(); + const savedPresets = config().dateTimeFormatPresets; + + coreLocalization.locale('en'); + config({ + ...config(), + dateTimeFormatPresets: { + longDate: { default: { type: 'longDate', locale: 'de' } }, + }, + }); + + try { + const element = document.body.appendChild(document.createElement('div')); + const instance = new DateBox(element, { + type: 'date', + pickerType: 'calendar', + useMaskBehavior: true, + value, + displayFormat: 'longDate', + }); + + widgets.push(instance); + + const input = element.querySelector('.dx-texteditor-input') as HTMLInputElement; + const textBeforeEdit = input.value; + + expect(textBeforeEdit).toContain('Juni'); + + const editor = instance as DateBox & { _upDownArrowHandler: (step: number) => void }; + + expect(() => editor._upDownArrowHandler(1)).not.toThrow(); + expect(input.value).not.toBe(textBeforeEdit); + } finally { + const currentConfig = config(); + + if (savedPresets === undefined) { + delete currentConfig.dateTimeFormatPresets; + } else { + currentConfig.dateTimeFormatPresets = savedPresets; + } + + coreLocalization.locale(savedLocale); + } + }); +}); diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts index 4d8734d85f9f..5081812454d8 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts @@ -14,6 +14,7 @@ import { inputType } from '@js/core/utils/support'; import { isDate as isDateType, isNumeric, isString } from '@js/core/utils/type'; import { getWindow, hasWindow } from '@js/core/utils/window'; import type { DxEvent, InteractionEvent } from '@js/events'; +import type { Format } from '@js/localization'; import type dxDateBox from '@js/ui/date_box'; import type { DateBoxBaseOptions, @@ -23,6 +24,7 @@ import type { Properties as dxDateBoxOptions, } from '@js/ui/date_box'; import type { ToolbarItem } from '@js/ui/popup'; +import { applyDataTypePreset } from '@ts/core/global_format_config'; import type { OptionChanged, OwnLeafOptions } from '@ts/core/widget/types'; import Calendar from '@ts/ui/date_box/date_box.strategy.calendar'; import CalendarWithTime from '@ts/ui/date_box/date_box.strategy.calendar_with_time'; @@ -408,7 +410,7 @@ class DateBox< rightPadding: number; } { const { displayFormat } = this.option(); - const format = this._strategy.getDisplayFormat(displayFormat); + const format = this._resolveDisplayFormat(displayFormat); const longestValue = dateLocalization.format( uiDateUtils.getLongestDate( format, @@ -566,11 +568,18 @@ class DateBox< this._getSubmitElement().val(submitValue ?? ''); } + _resolveDisplayFormat(displayFormat?: Format | null): Format { + const { type = 'date' } = this.option(); + const format = this._strategy.getDisplayFormat(displayFormat); + + return applyDataTypePreset(format, type) ?? format; + } + _getDisplayedText(value?: DateLike): string { const { mode = 'text', displayFormat: displayFormatOption } = this.option(); if (mode === 'text') { - const displayFormat = this._strategy.getDisplayFormat(displayFormatOption); + const displayFormat = this._resolveDisplayFormat(displayFormatOption); return dateLocalization.format(value, displayFormat) as string; } const format = this._getFormatByMode(mode); @@ -659,7 +668,7 @@ class DateBox< _getParsedDate(text?: string): Date | undefined { const { displayFormat } = this.option(); - const strategyDisplayFormat = this._strategy.getDisplayFormat(displayFormat); + const strategyDisplayFormat = this._resolveDisplayFormat(displayFormat); const parsedText = this._strategy.getParsedText(text, strategyDisplayFormat as string); return parsedText ?? undefined; diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts index c5386f8e4ad4..dfbc65b0cd5f 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts @@ -1,5 +1,6 @@ import eventsEngine from '@js/common/core/events/core/events_engine'; import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '@js/common/core/events/utils/index'; +import coreLocalization from '@js/common/core/localization/core'; import { getFormat } from '@js/common/core/localization/ldml/date.format'; import { getRegExpInfo } from '@js/common/core/localization/ldml/date.parser'; import numberLocalization from '@js/common/core/localization/number'; @@ -13,7 +14,13 @@ import { import type { DxEvent, InteractionEvent } from '@js/events'; import type dxDateBox from '@js/ui/date_box'; import type { DateLike } from '@js/ui/date_box'; +import { getDateFormatLocale } from '@ts/core/global_format_config'; import dateLocalization from '@ts/core/localization/date'; +import { + bindDatePartsToLocale, + getParser as getLDMLDateParser, + type LocaleAwareDateLocalization, +} from '@ts/core/localization/ldml/date.parser'; import type { OptionChanged } from '@ts/core/widget/types'; import type { KeyboardKeyDownEvent } from '@ts/events/core/keyboard_processor'; import type { DateBoxBaseProperties } from '@ts/ui/date_box/date_box.base'; @@ -50,6 +57,10 @@ class DateBoxMask< _formatPattern?: string | null; + _formatLocaleCache?: string; + + _formatDateParts?: ReturnType; + _pendingIMEDigit?: string | null; _isIMEDigitProcessed?: boolean; @@ -367,13 +378,34 @@ class DateBoxMask< return caretEnd - caretStart === text.length; } + _invalidateMaskIfFormatLocaleChanged(): void { + const { displayFormat } = this.option(); + const format = this._resolveDisplayFormat(displayFormat); + const formatLocale = getDateFormatLocale(format); + + if (formatLocale === this._formatLocaleCache) { + return; + } + + this._formatLocaleCache = formatLocale; + + if (!this._formatPattern) { + return; + } + + this._formatPattern = null; + this._prepareRegExpInfo(); + } + _getFormatPattern(): string { + this._invalidateMaskIfFormatLocaleChanged(); + if (this._formatPattern) { return this._formatPattern; } const { displayFormat } = this.option(); - const format = this._strategy.getDisplayFormat(displayFormat); + const format = this._resolveDisplayFormat(displayFormat); const isLDMLPattern = isString(format) && !dateLocalization._getPatternByFormat(format); if (isLDMLPattern) { @@ -478,7 +510,15 @@ class DateBoxMask< } _prepareRegExpInfo(): void { - this._regExpInfo = getRegExpInfo(this._getFormatPattern(), dateLocalization); + const { displayFormat } = this.option(); + const format = this._resolveDisplayFormat(displayFormat); + const formatLocale = getDateFormatLocale(format); + const dateParts = formatLocale === coreLocalization.locale() + ? dateLocalization + : bindDatePartsToLocale(dateLocalization as LocaleAwareDateLocalization, formatLocale); + + this._formatDateParts = dateParts; + this._regExpInfo = getRegExpInfo(this._getFormatPattern(), dateParts); const { regexp } = this._regExpInfo; const { source } = regexp; const { flags } = regexp; @@ -517,6 +557,8 @@ class DateBoxMask< return; } + this._invalidateMaskIfFormatLocaleChanged(); + const { text } = this.option(); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const newText = text || this._getDisplayedText(this._maskValue); @@ -775,7 +817,11 @@ class DateBoxMask< const { text } = this.option(); // @ts-expect-error text const newText = this._replaceSelectedText(text, this._caret(), clipboardText(e)); - const date = dateLocalization.parse(newText, this._getFormatPattern()); + const textToParse = numberLocalization.convertDigits(newText, true); + const date = getLDMLDateParser( + this._getFormatPattern(), + this._formatDateParts ?? dateLocalization, + )(textToParse); if (date && this._isDateValid(date)) { this._maskValue = date; diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.calendar.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.calendar.ts index 0ab84134c7fc..69809fb44d36 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.calendar.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.calendar.ts @@ -8,7 +8,7 @@ import type { ClickEvent } from '@js/ui/button'; import type { ValueChangedEvent } from '@js/ui/calendar'; import type { ToolbarItem } from '@js/ui/popup'; import { current, isMaterial } from '@js/ui/themes'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { applyDataTypePreset, getGlobalFormatByDataType } from '@ts/core/global_format_config'; import { splitPair } from '@ts/core/utils/m_common'; import Calendar from '@ts/ui/calendar/calendar'; import type { CellEvent } from '@ts/ui/calendar/calendar.base_view'; @@ -94,9 +94,12 @@ class CalendarStrategy< } getDisplayFormat(displayFormat?: Format | null): Format { + const { type = 'date' } = this.dateBox.option(); const globalDateFormat: Format = getGlobalFormatByDataType('date'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || globalDateFormat || 'shortdate'; + const format = displayFormat || globalDateFormat || 'shortdate'; + + return applyDataTypePreset(format, type) ?? format; } _closeDropDownByEnter(): boolean { diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.calendar_with_time.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.calendar_with_time.ts index 3e8917c552af..b0f4cd50a2c5 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.calendar_with_time.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.calendar_with_time.ts @@ -6,7 +6,7 @@ import { getWidth } from '@js/core/utils/size'; import { getWindow } from '@js/core/utils/window'; import type { DxEvent } from '@js/events'; import type { Format } from '@js/localization'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { applyDataTypePreset, getGlobalFormatByDataType } from '@ts/core/global_format_config'; import type { BoxItemData } from '@ts/ui/box'; import Box from '@ts/ui/box'; import type { DateBoxBaseProperties } from '@ts/ui/date_box/date_box.base'; @@ -52,7 +52,9 @@ class CalendarWithTimeStrategy extends CalendarStrategy { getDisplayFormat(displayFormat?: Format | null): Format { const globalDateTimeFormat: Format = getGlobalFormatByDataType('datetime'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || globalDateTimeFormat || 'shortdateshorttime'; + const format = displayFormat || globalDateTimeFormat || 'shortdateshorttime'; + + return applyDataTypePreset(format, 'datetime') ?? format; } _is24HourFormat(): boolean { diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.date_view.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.date_view.ts index a53abab2efa1..e878b6c50eff 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.date_view.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.date_view.ts @@ -4,7 +4,7 @@ import $ from '@js/core/renderer'; import { inputType } from '@js/core/utils/support'; import { getWindow } from '@js/core/utils/window'; import type { Format } from '@js/localization'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { applyDataTypePreset, getGlobalFormatByDataType } from '@ts/core/global_format_config'; import type { DateBoxBaseProperties } from '@ts/ui/date_box/date_box.base'; import DateBoxStrategy from '@ts/ui/date_box/date_box.strategy'; import dateUtils from '@ts/ui/date_box/date_utils'; @@ -38,7 +38,9 @@ class DateViewStrategy extends DateBoxStrategy { : undefined; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || globalFormat || dateUtils.FORMATS_MAP[type]; + const format = displayFormat || globalFormat || dateUtils.FORMATS_MAP[type]; + + return applyDataTypePreset(format, type) ?? format; } popupConfig(config: PopupProperties): PopupProperties { diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.list.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.list.ts index a67aa10a7f8d..e3a078ec4e8b 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.list.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.list.ts @@ -9,7 +9,7 @@ import { getWindow } from '@js/core/utils/window'; import type { DxEvent } from '@js/events'; import type { Format } from '@js/localization'; import type { ItemClickEvent } from '@js/ui/list'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { applyDataTypePreset, getGlobalFormatByDataType } from '@ts/core/global_format_config'; import type { DateBoxBaseProperties } from '@ts/ui/date_box/date_box.base'; import DateBoxStrategy from '@ts/ui/date_box/date_box.strategy'; import dateUtils from '@ts/ui/date_box/date_utils'; @@ -55,7 +55,9 @@ class ListStrategy extends DateBoxStrategy { getDisplayFormat(displayFormat?: Format | null): Format { const globalTimeFormat: Format = getGlobalFormatByDataType('time'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || globalTimeFormat || 'shorttime'; + const format = displayFormat || globalTimeFormat || 'shorttime'; + + return applyDataTypePreset(format, 'time') ?? format; } popupConfig(popupConfig: PopupProperties): PopupProperties { diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.native.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.native.ts index 82e521504d40..1a90935f70cb 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.native.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.native.ts @@ -4,7 +4,7 @@ import dateSerialization from '@js/core/utils/date_serialization'; import { inputType } from '@js/core/utils/support'; import type { Format } from '@js/localization'; import type { TextBoxType } from '@js/ui/text_box'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { applyDataTypePreset, getGlobalFormatByDataType } from '@ts/core/global_format_config'; import type { DateBoxBaseProperties } from '@ts/ui/date_box/date_box.base'; import DateBoxStrategy from '@ts/ui/date_box/date_box.strategy'; import dateUtils from '@ts/ui/date_box/date_utils'; @@ -71,7 +71,10 @@ class NativeStrategy extends DateBoxStrategy { : undefined; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || globalFormat || dateUtils.FORMATS_MAP[type] as string; + const format = displayFormat || globalFormat || dateUtils.FORMATS_MAP[type] as string; + const dataType = type === 'datetime-local' ? 'datetime' : type; + + return applyDataTypePreset(format, dataType) ?? format; } renderInputMinMax($input?: dxElementWrapper): void { diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.ts index dd02b1eda4e8..4ad332ce0d65 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.strategy.ts @@ -5,6 +5,7 @@ import $ from '@js/core/renderer'; import type { DxEvent, InteractionEvent } from '@js/events'; import type { Format } from '@js/localization'; import type { ToolbarItem } from '@js/ui/popup'; +import { applyDataTypePreset } from '@ts/core/global_format_config'; import type Calendar from '@ts/ui/calendar/calendar'; import type { DateBoxBaseProperties } from '@ts/ui/date_box/date_box.base'; import type DateBox from '@ts/ui/date_box/date_box.base'; @@ -62,8 +63,11 @@ class DateBoxStrategy< } getDisplayFormat(displayFormat?: Format | null): Format { + const { type = 'date' } = this.dateBox.option(); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || 'shortdate'; + const format = displayFormat || 'shortdate'; + + return applyDataTypePreset(format, type) ?? format; } supportedKeys(): Record) => void> { diff --git a/packages/devextreme/js/__internal/ui/number_box/__tests__/number_box.currency_locale.test.ts b/packages/devextreme/js/__internal/ui/number_box/__tests__/number_box.currency_locale.test.ts new file mode 100644 index 000000000000..6990baf0f2f3 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/number_box/__tests__/number_box.currency_locale.test.ts @@ -0,0 +1,36 @@ +import { + afterEach, describe, expect, it, jest, +} from '@jest/globals'; +import errors from '@js/core/errors'; +import NumberBox from '@js/ui/number_box'; + +const widgets: NumberBox[] = []; + +describe('NumberBox currency format with locale', () => { + afterEach(() => { + widgets.forEach((widget) => widget.dispose()); + widgets.length = 0; + document.body.innerHTML = ''; + jest.restoreAllMocks(); + }); + + it('does not warn W0011 when the edited text is parsed', () => { + const log = jest.spyOn(errors, 'log'); + const element = document.body.appendChild(document.createElement('div')); + const instance = new NumberBox(element, { + value: 1234.5, + format: { style: 'currency', locale: 'fr', currency: 'EUR' }, + }); + + widgets.push(instance); + + const editor = instance as NumberBox & { + _parse: (text: string, format: string) => number | undefined; + _getFormatPattern: () => string; + }; + const parsed = editor._parse('1234,5', editor._getFormatPattern()); + + expect(log.mock.calls.some((call) => call[0] === 'W0011')).toBe(false); + expect(parsed).toBe(1234.5); + }); +}); diff --git a/packages/devextreme/js/__internal/ui/number_box/__tests__/number_box.precision_locale.test.ts b/packages/devextreme/js/__internal/ui/number_box/__tests__/number_box.precision_locale.test.ts new file mode 100644 index 000000000000..7b6156ceba86 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/number_box/__tests__/number_box.precision_locale.test.ts @@ -0,0 +1,99 @@ +import { + afterEach, describe, expect, it, jest, +} from '@jest/globals'; +import config from '@js/core/config'; +import NumberBox from '@js/ui/number_box'; +import type { CaretRange } from '@ts/ui/text_box/utils.caret'; + +interface NumberBoxMaskInternals { + _tryParse: (text: string, selection: CaretRange, char?: string) => number | undefined; + _getDecimalSeparator: () => string; + _getPrecisionLimits: (pattern: string) => { min: number; max: number }; +} + +const widgets: NumberBox[] = []; + +describe('NumberBox precision limits with a locale-only Intl format', () => { + afterEach(() => { + widgets.forEach((widget) => widget.dispose()); + widgets.length = 0; + document.body.innerHTML = ''; + config({ numberFormat: undefined }); + jest.restoreAllMocks(); + }); + + const createEditor = (format?: Record): NumberBoxMaskInternals => { + const element = document.body.appendChild(document.createElement('div')); + const instance = new NumberBox(element, { value: 1234, format }); + + widgets.push(instance); + + return instance as unknown as NumberBoxMaskInternals; + }; + + const typeDecimalSeparator = (editor: NumberBoxMaskInternals): number | undefined => editor + ._tryParse('1234', { start: 4, end: 4 }, editor._getDecimalSeparator()); + + it('accepts the decimal separator when the format only specifies a locale', () => { + const editor = createEditor({ locale: 'fr' }); + + expect(editor._getDecimalSeparator()).toBe(','); + expect(typeDecimalSeparator(editor)).toBeDefined(); + }); + + it('accepts the decimal separator when the format is an empty object', () => { + const editor = createEditor({}); + + expect(typeDecimalSeparator(editor)).toBeDefined(); + }); + + it('accepts the decimal separator when the global number format only specifies a locale', () => { + config({ numberFormat: { default: { locale: 'fr' } } }); + const editor = createEditor(); + + expect(editor._getDecimalSeparator()).toBe(','); + expect(typeDecimalSeparator(editor)).toBeDefined(); + }); + + it('accepts the decimal separator when the global number format is an empty object', () => { + config({ numberFormat: { default: {} } }); + const editor = createEditor(); + + expect(typeDecimalSeparator(editor)).toBeDefined(); + }); + + it('limits fraction digits to the Intl default for a locale-only format', () => { + const editor = createEditor({ locale: 'fr' }); + + expect(editor._tryParse('1234,567', { start: 8, end: 8 }, '8')).toBe(1234.567); + }); + + it('accepts the decimal separator when the format only limits significant digits', () => { + const editor = createEditor({ locale: 'fr', maximumSignificantDigits: 6 }); + + expect(typeDecimalSeparator(editor)).toBeDefined(); + }); + + it('rejects the decimal separator when the format forbids fraction digits', () => { + const editor = createEditor({ locale: 'fr', maximumFractionDigits: 0 }); + + expect(typeDecimalSeparator(editor)).toBeUndefined(); + }); + + it('uses the pattern precision when Intl is unavailable', () => { + const editor = createEditor({ locale: 'fr' }); + const intl = globalThis.Intl; + + Reflect.deleteProperty(globalThis, 'Intl'); + + try { + expect(editor._getPrecisionLimits('#,##0.00##')).toEqual({ min: 2, max: 4 }); + } finally { + Object.defineProperty(globalThis, 'Intl', { + value: intl, + configurable: true, + writable: true, + }); + } + }); +}); diff --git a/packages/devextreme/js/__internal/ui/number_box/number_box.caret.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.caret.ts index 426ef848ce10..7629b2ffc768 100644 --- a/packages/devextreme/js/__internal/ui/number_box/number_box.caret.ts +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.caret.ts @@ -18,8 +18,11 @@ const mockEscapedStubs = (format: string): string => format.replace( (stub) => stub.split('').map(() => ' ').join('').substr(2), ); -const getDigitCountBeforeIndex = (index: number, text: string): number => { - const decimalSeparator: string = number.getDecimalSeparator(); +const getDigitCountBeforeIndex = ( + index: number, + text: string, + decimalSeparator: string, +): number => { const regExp = new RegExp(`[^0-9${escapeRegExp(decimalSeparator)}]`, 'g'); const textBeforePosition = text.slice(0, index); @@ -106,10 +109,11 @@ export const getCaretAfterFormat = ( formatted: string, caret: CaretPosition, format: Format, + decimalSeparator: string = number.getDecimalSeparator(), ): CaretBoundaries => { const normalizedCaret = getCaretWithOffset(caret, 0); - const point: string = number.getDecimalSeparator(); + const point = decimalSeparator; const realSeparatorOccurrenceIndex = getRealSeparatorIndex(format).occurrence; const pointPosition = isSeparatorBasedString(text) ? 0 @@ -121,7 +125,7 @@ export const getCaretAfterFormat = ( if (isCaretOnFloat) { const relativeIndex = normalizedCaret.start - pointPosition - 1; - const digitsBefore = getDigitCountBeforeIndex(relativeIndex, textParts[1]); + const digitsBefore = getDigitCountBeforeIndex(relativeIndex, textParts[1], point); const newPosition = formattedParts[1] ? newPointPosition + 1 + getDigitPositionByIndex(digitsBefore, formattedParts[1]) + 1 : formatted.length; @@ -131,7 +135,7 @@ export const getCaretAfterFormat = ( const formattedIntPart = trimNonNumericCharsFromEnd(formattedParts[0]); const positionFromEnd = textParts[0].length - normalizedCaret.start; - const digitsFromEnd = getDigitCountBeforeIndex(positionFromEnd, reverseText(textParts[0])); + const digitsFromEnd = getDigitCountBeforeIndex(positionFromEnd, reverseText(textParts[0]), point); const newPositionFromEnd = getDigitPositionByIndex(digitsFromEnd, reverseText(formattedIntPart)); const newPositionFromBegin = formattedIntPart.length - (newPositionFromEnd + 1); diff --git a/packages/devextreme/js/__internal/ui/number_box/number_box.mask.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.mask.ts index 72f9dabe79e3..c0356cd6918f 100644 --- a/packages/devextreme/js/__internal/ui/number_box/number_box.mask.ts +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.mask.ts @@ -14,7 +14,9 @@ import { import type { DxEvent, NativeEventInfo } from '@js/events'; import type { Format, FormatObject } from '@js/localization'; import type { Properties } from '@js/ui/number_box'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { + getEffectiveFormatLocale, getFormatterOptions, getGlobalFormatByDataType, +} from '@ts/core/global_format_config'; import { getFormat as getLDMLFormat } from '@ts/core/localization/ldml/number'; import type { OptionChanged } from '@ts/core/widget/types'; import type { SupportedKeys } from '@ts/core/widget/widget'; @@ -87,6 +89,8 @@ class NumberBoxMask extends NumberBoxBase { _currentFormat?: Format; + _intlFormatLocaleCache?: string; + _getDefaultOptions(): NumberBoxMaskProperties { return { ...super._getDefaultOptions(), @@ -125,8 +129,77 @@ class NumberBoxMask extends NumberBoxBase { : getGlobalFormatByDataType('number'); } + _usesIntlFormatOption(formatOption: Format | undefined): boolean { + const formatObject = asFormatObject(formatOption); + + return !!formatObject + && !isFunction(formatObject.formatter) + && !formatObject.type + && !formatObject.parser; + } + + _getFormatArgumentForNumberLocalization(maskFormat: Format): Format { + const formatOption = this._getEffectiveFormatOption(); + + if (this._usesIntlFormatOption(formatOption)) { + return formatOption; + } + + return this._withExplicitFormatLocale(maskFormat); + } + + // The generated mask is an LDML string, so a typed format's locale would otherwise be lost. + _withExplicitFormatLocale(maskFormat: Format): Format { + const formatObject = asFormatObject(this._getEffectiveFormatOption()); + + if (!formatObject || !isDefined(formatObject.locale) || !isString(maskFormat)) { + return maskFormat; + } + + return { + type: maskFormat, + locale: formatObject.locale, + }; + } + + _getExplicitLocaleFormat(): FormatObject | undefined { + const formatObject = asFormatObject(this._getEffectiveFormatOption()); + + if (!formatObject || !isDefined(formatObject.locale)) { + return undefined; + } + + return { locale: formatObject.locale }; + } + + _getDecimalSeparator(): string { + return number.getDecimalSeparator(this._getExplicitLocaleFormat()); + } + + _getThousandsSeparator(): string { + return number.getThousandsSeparator(this._getExplicitLocaleFormat()); + } + + _invalidateFormatPatternIfIntlLocaleChanged(): void { + const formatOption = this._getEffectiveFormatOption(); + const formatObject = asFormatObject(formatOption); + const tracksLocale = !isFunction(formatOption) + && !isFunction(formatObject?.formatter); + + if (!tracksLocale) { + return; + } + + const locale = getEffectiveFormatLocale(formatOption, 'number'); + + if (locale !== this._intlFormatLocaleCache) { + this._intlFormatLocaleCache = locale; + delete this._currentFormat; + } + } + _getTextSeparatorIndex(text: string): number { - const decimalSeparator: string = number.getDecimalSeparator(); + const decimalSeparator: string = this._getDecimalSeparator(); const realSeparatorOccurrenceIndex = getRealSeparatorIndex(this._getFormatPattern()).occurrence; return getNthOccurrence(text, decimalSeparator, realSeparatorOccurrenceIndex); } @@ -260,7 +333,7 @@ class NumberBoxMask extends NumberBoxBase { return false; } - const decimalSeparator = number.getDecimalSeparator(); + const decimalSeparator = this._getDecimalSeparator(); const isDecimalSeparatorNext = text.charAt(caret.end ?? 0) === decimalSeparator; const isSeparatorKey = this._lastKey === decimalSeparator || this._lastKey === '.' || this._lastKey === ','; @@ -290,7 +363,7 @@ class NumberBoxMask extends NumberBoxBase { if (this._lastKeyName === MINUS_KEY) { enteredChar = ''; } else if (e.which === NUMPAD_DOT_KEY_CODE) { - enteredChar = number.getDecimalSeparator(); + enteredChar = this._getDecimalSeparator(); } const newValue = this._tryParse(normalizedText, caret, enteredChar); @@ -364,7 +437,7 @@ class NumberBoxMask extends NumberBoxBase { return; } - const decimalSeparator: string = number.getDecimalSeparator(); + const decimalSeparator: string = this._getDecimalSeparator(); if (char === decimalSeparator) { const decimalSeparatorIndex = text.indexOf(decimalSeparator); if (this._isNonStubAfter(decimalSeparatorIndex + 1)) { @@ -421,9 +494,11 @@ class NumberBoxMask extends NumberBoxBase { } } + const formatArg = this._withExplicitFormatLocale(format); + return number.parse( text.substr(integerPartStartIndex), - isString(format) ? format : asFormatObject(format), + isString(formatArg) ? formatArg : asFormatObject(formatArg), ); } @@ -434,10 +509,14 @@ class NumberBoxMask extends NumberBoxBase { value: number, valueFormat: Format, ) => string | undefined; - return isDefined(value) ? formatter(value, format) : ''; + const formatArg = this._getFormatArgumentForNumberLocalization(format); + + return isDefined(value) ? formatter(value, formatArg) : ''; } _getFormatPattern(): Format { + this._invalidateFormatPatternIfIntlLocaleChanged(); + if (!this._currentFormat) { this._updateFormat(); } @@ -467,7 +546,8 @@ class NumberBoxMask extends NumberBoxBase { const format = this._getFormatPattern(); if (isString(format)) { const signParts = format.split(';'); - const sign: number = number.getSign(text, format); + const signFormat = this._getFormatArgumentForNumberLocalization(format); + const sign: number = number.getSign(text, signFormat); signParts[1] = signParts[1] || `-${signParts[0]}`; return sign < 0 ? signParts[1] : signParts[0]; @@ -478,7 +558,7 @@ class NumberBoxMask extends NumberBoxBase { _removeStubs(text: string, excludeComma?: boolean): string { const format = this._getFormatForSign(text); - const thousandsSeparator: string = number.getThousandsSeparator(); + const thousandsSeparator: string = this._getThousandsSeparator(); const stubs = this._getStubs(format); let result = text; @@ -530,7 +610,7 @@ class NumberBoxMask extends NumberBoxBase { const { start = 0, end = 0 } = selection ?? {}; const isTextSelected = start !== end; const isWholeTextSelected = isTextSelected && start === 0 && end === text.length; - const decimalSeparator: string = number.getDecimalSeparator(); + const decimalSeparator: string = this._getDecimalSeparator(); if (isWholeTextSelected && char === decimalSeparator) { return 0; @@ -585,7 +665,8 @@ class NumberBoxMask extends NumberBoxBase { } _getParsedValue(text: string, format: Format): NumberBoxValue | undefined { - const signFormat = asFormatObject(format)?.formatter ?? format; + const formatArg = this._getFormatArgumentForNumberLocalization(format); + const signFormat = asFormatObject(formatArg)?.formatter ?? formatArg; const sign: number = number.getSign(text, signFormat); const textWithoutStubs = this._removeStubs(text, true); const parsedValue = this._parse(textWithoutStubs, format); @@ -601,7 +682,7 @@ class NumberBoxMask extends NumberBoxBase { } const caret = this._caret(); - const point: string = number.getDecimalSeparator(); + const point: string = this._getDecimalSeparator(); const pointIndex = this._getTextSeparatorIndex(text); const isCaretOnFloat = pointIndex >= 0 && pointIndex < (caret?.start ?? 0); const textParts = this._removeStubs(text, true).split(point); @@ -639,6 +720,7 @@ class NumberBoxMask extends NumberBoxBase { normalizedText, caret, this._getFormatPattern(), + this._getDecimalSeparator(), ); this._input().val(text); @@ -771,7 +853,7 @@ class NumberBoxMask extends NumberBoxBase { } _isStub(str: string | null | undefined, allowMultipleChars?: boolean): boolean { - const escapedDecimalSeparator = escapeRegExp(number.getDecimalSeparator()); + const escapedDecimalSeparator = escapeRegExp(this._getDecimalSeparator()); const regExpString = `^[^0-9${escapedDecimalSeparator}]+$`; const stubRegExp = new RegExp(regExpString, 'g'); @@ -787,6 +869,29 @@ class NumberBoxMask extends NumberBoxBase { } _getPrecisionLimits(pattern: string): { min: number; max: number } { + const formatOption = this._getEffectiveFormatOption(); + + if (this._usesIntlFormatOption(formatOption) && typeof Intl !== 'undefined') { + const formatObject = asFormatObject(formatOption) ?? {}; + const locale = getEffectiveFormatLocale(formatOption, 'number'); + const { + minimumFractionDigits, + maximumFractionDigits, + maximumSignificantDigits, + } = new Intl.NumberFormat( + locale, + getFormatterOptions(formatObject) as Intl.NumberFormatOptions, + ).resolvedOptions(); + + if (!isDefined(maximumFractionDigits) && isDefined(maximumSignificantDigits)) { + return { min: 0, max: maximumSignificantDigits }; + } + + const min = minimumFractionDigits ?? 0; + + return { min, max: maximumFractionDigits ?? min }; + } + const realSeparatorIndex = getRealSeparatorIndex(pattern).index; const floatPart = (splitByIndex(pattern, realSeparatorIndex)[1] || '').replace(/[^#0]/g, ''); const minPrecision = floatPart.replace(/^(0*)#*/, '$1').length; @@ -981,6 +1086,8 @@ class NumberBoxMask extends NumberBoxBase { delete this._lastKeyName; delete this._parsedValue; delete this._focusOutOccurs; + delete this._currentFormat; + delete this._intlFormatLocaleCache; clearTimeout(this._caretTimeout); delete this._caretTimeout; } diff --git a/packages/devextreme/js/__internal/viz/axes/smart_formatter.test.ts b/packages/devextreme/js/__internal/viz/axes/smart_formatter.test.ts new file mode 100644 index 000000000000..a7fea7d21507 --- /dev/null +++ b/packages/devextreme/js/__internal/viz/axes/smart_formatter.test.ts @@ -0,0 +1,169 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import coreLocalization from '@js/common/core/localization/core'; +import config from '@js/core/config'; + +import { smartFormatter } from './smart_formatter'; + +const GLOBAL_FORMAT_KEYS = ['numberFormat', 'dateTimeFormat'] as const; + +const datetimeTick = new Date(2024, 5, 15, 14, 30); +const monthTick = new Date(2024, 5, 1); + +const formatDatetime = ( + tick: Date, + tickInterval: string, + format?: unknown, +): string => smartFormatter(tick, { + labelOptions: { format }, + ticks: [], + tickInterval, + dataType: 'datetime', + type: 'continuous', +}); + +const formatNumeric = ( + tick: number, + ticks: number[], + tickInterval: number, + format?: unknown, +): string => smartFormatter(tick, { + labelOptions: { format }, + ticks, + tickInterval, + dataType: 'numeric', + type: 'continuous', +}); + +describe('smartFormatter with locale-only format', () => { + let savedLocale = ''; + let savedFormats: Record = {}; + + beforeEach(() => { + savedLocale = coreLocalization.locale(); + coreLocalization.locale('en'); + + const currentConfig = config() as Record; + savedFormats = {}; + GLOBAL_FORMAT_KEYS.forEach((key) => { + savedFormats[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + coreLocalization.locale(savedLocale); + + const currentConfig = config() as Record; + GLOBAL_FORMAT_KEYS.forEach((key) => { + currentConfig[key] = savedFormats[key]; + }); + }); + + describe('datetime axis', () => { + it.each([ + { tickInterval: 'hour', tick: datetimeTick, expected: '2:30 PM' }, + { tickInterval: 'month', tick: monthTick, expected: 'June' }, + ])('should auto-format by $tickInterval interval without locale', ({ tickInterval, tick, expected }) => { + expect(formatDatetime(tick, tickInterval)).toBe(expected); + }); + + it.each([ + { tickInterval: 'hour', tick: datetimeTick, expected: '14:30' }, + { tickInterval: 'month', tick: monthTick, expected: 'Juni' }, + ])('should keep auto-format by $tickInterval interval for own locale-only format', ({ tickInterval, tick, expected }) => { + expect(formatDatetime(tick, tickInterval, { locale: 'de' })).toBe(expected); + }); + + it.each([ + { tickInterval: 'hour', tick: datetimeTick, expected: '14:30' }, + { tickInterval: 'month', tick: monthTick, expected: 'Juni' }, + ])('should keep auto-format by $tickInterval interval for global locale-only dateTimeFormat', ({ tickInterval, tick, expected }) => { + config({ ...config(), dateTimeFormat: { default: { locale: 'de' } } }); + + expect(formatDatetime(tick, tickInterval)).toBe(expected); + }); + + it('should support function locale', () => { + expect(formatDatetime(monthTick, 'month', { locale: () => 'de' })).toBe('Juni'); + }); + + it('should keep explicit format with locale as is', () => { + expect(formatDatetime(datetimeTick, 'hour', { type: 'shortDate', locale: 'de' })).toBe('15.6.2024'); + }); + }); + + describe('numeric axis', () => { + it.each([ + { + name: 'fraction precision', tick: 1.5, ticks: [1, 1.25, 1.5], tickInterval: 0.25, en: '1.50', de: '1,50', + }, + { + name: 'thousands grouping', tick: 20000, ticks: [10000, 20000], tickInterval: 10000, en: '20K', de: '20K', + }, + { + name: 'millions modifier', tick: 1500000, ticks: [1000000, 1500000], tickInterval: 500000, en: '1.5M', de: '1,5M', + }, + ])('should keep auto $name', ({ + tick, ticks, tickInterval, en, de, + }) => { + expect(formatNumeric(tick, ticks, tickInterval)).toBe(en); + expect(formatNumeric(tick, ticks, tickInterval, { locale: 'de' })).toBe(de); + }); + + it('should keep auto precision for global locale-only numberFormat', () => { + config({ ...config(), numberFormat: { default: { locale: 'de' } } }); + + expect(formatNumeric(1.5, [1, 1.25, 1.5], 0.25)).toBe('1,50'); + }); + + it('should keep explicit format with locale as is', () => { + expect(formatNumeric(1.5, [1, 1.25, 1.5], 0.25, { type: 'fixedPoint', precision: 3, locale: 'de' })).toBe('1,500'); + }); + }); + + describe('logarithmic axis', () => { + const formatLogarithmic = (tick: number, format?: unknown): string => smartFormatter(tick, { + labelOptions: { format }, + ticks: [0.1, 1, 10], + tickInterval: 1, + type: 'logarithmic', + logarithmBase: 10, + dataType: 'numeric', + }); + + it('should apply own locale-only format to small ticks', () => { + expect(formatLogarithmic(0.5)).toBe('0.5'); + expect(formatLogarithmic(0.5, { locale: 'de' })).toBe('0,5'); + }); + + it('should keep auto modifier for large ticks with own locale-only format', () => { + expect(formatLogarithmic(1500000, { locale: 'de' })).toBe('2M'); + }); + }); + + describe('datetime axis with transitions', () => { + const lastDayTick = new Date(2024, 5, 15, 23, 0); + const nextDayTick = new Date(2024, 5, 16, 1, 0); + + const formatTransition = (format?: unknown): string => smartFormatter(nextDayTick, { + labelOptions: { format }, + ticks: [lastDayTick, nextDayTick], + tickInterval: 'hour', + showTransition: true, + dataType: 'datetime', + type: 'continuous', + }); + + it('should apply own locale-only format to each part of composite format', () => { + expect(formatTransition()).toBe('16 1:00 AM'); + expect(formatTransition({ locale: 'de' })).toBe('16 01:00'); + }); + + it('should apply global locale-only dateTimeFormat to each part of composite format', () => { + config({ ...config(), dateTimeFormat: { default: { locale: 'de' } } }); + + expect(formatTransition()).toBe('16 01:00'); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/viz/axes/smart_formatter.ts b/packages/devextreme/js/__internal/viz/axes/smart_formatter.ts index 7a3e72e62800..6b43440b9ce8 100644 --- a/packages/devextreme/js/__internal/viz/axes/smart_formatter.ts +++ b/packages/devextreme/js/__internal/viz/axes/smart_formatter.ts @@ -16,7 +16,12 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import formatHelper from '@ts/core/format_helper'; -import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { + applyDataTypePreset, + getRawGlobalFormatByDataType, + isLocaleOnlyFormat, + withFormatLocale, +} from '@ts/core/global_format_config'; import { dateUtils } from '@ts/core/utils/m_date'; import { adjust, getExponent, getPrecision } from '@ts/core/utils/m_math'; import { @@ -142,7 +147,7 @@ function createFormat(type) { return { type, formatter }; } -function formatLogarithmicNumber(tick) { +function formatLogarithmicNumber(tick, localeOnlyFormat?) { const log10Tick = log10(abs(tick)); let type; @@ -151,10 +156,10 @@ function formatLogarithmicNumber(tick) { } else if (log10Tick < -4) { type = EXPONENTIAL; } else { - return _format(adjust(tick)); + return _format(adjust(tick), localeOnlyFormat); } - return _format(tick, { type, precision: 0 }); + return _format(tick, withFormatLocale({ type, precision: 0 }, localeOnlyFormat?.locale)); } function getDateTimeFormat(tick, { showTransition, ticks, tickInterval }) { @@ -288,6 +293,7 @@ export function smartFormatter(tick, options) { let { tickInterval } = options; const stringTick = abs(tick).toString(); let { format } = options.labelOptions; + let localeOnlyFormat; const { ticks } = options; const isLogarithmic = options.type === 'logarithmic'; const globalFormatDataType = options.dataType === 'datetime' ? 'datetime' : 'number'; @@ -301,7 +307,12 @@ export function smartFormatter(tick, options) { } if (!isDefined(format)) { - format = getGlobalFormatByDataType(globalFormatDataType); + format = getRawGlobalFormatByDataType(globalFormatDataType); + } + + if (isLocaleOnlyFormat(format)) { + localeOnlyFormat = format; + format = undefined; } if (!isDefined(format) && options.type !== 'discrete' && tick && (options.logarithmBase === 10 || !isLogarithmic)) { @@ -312,7 +323,7 @@ export function smartFormatter(tick, options) { } if (isLogarithmic) { - return formatLogarithmicNumber(tick); + return formatLogarithmicNumber(tick, localeOnlyFormat); } let separatedTickInterval = splitDecimalNumber(tickInterval); if (separatedTickInterval < 2) { @@ -336,6 +347,10 @@ export function smartFormatter(tick, options) { } } + format = isDefined(format) + ? withFormatLocale(format, localeOnlyFormat?.locale) + : applyDataTypePreset(localeOnlyFormat, globalFormatDataType); + return _format(tick, format); } diff --git a/packages/devextreme/js/__internal/viz/core/tooltip.ts b/packages/devextreme/js/__internal/viz/core/tooltip.ts index 9a3505fec8fe..56fcf81a6d65 100644 --- a/packages/devextreme/js/__internal/viz/core/tooltip.ts +++ b/packages/devextreme/js/__internal/viz/core/tooltip.ts @@ -18,6 +18,7 @@ import { Color } from '@ts/color'; import { domAdapter } from '@ts/core/dom_adapter'; import formatHelper from '@ts/core/format_helper'; +import { applyDataTypePreset } from '@ts/core/global_format_config'; import { renderer as $ } from '@ts/core/renderer'; import type { Renderer as CoreRenderer } from '@ts/core/renderer_base'; import { isCssVariableReference } from '@ts/core/utils/css_variables'; @@ -465,9 +466,9 @@ export let Tooltip = class Tooltip { } } - formatValue(value: ThemeValue, _specialFormat?: string): string { + formatValue(value: ThemeValue, _specialFormat?: string, dataType?: string): string { const options = _specialFormat ? getSpecialFormatOptions(this._options, _specialFormat) : this._options; - return format(value, options.format); + return format(value, applyDataTypePreset(options.format, dataType)); } getOptions(): ThemeValue { diff --git a/packages/devextreme/js/__internal/viz/range_selector/range_selector.ts b/packages/devextreme/js/__internal/viz/range_selector/range_selector.ts index 5d1d834376d5..cbef40cb2360 100644 --- a/packages/devextreme/js/__internal/viz/range_selector/range_selector.ts +++ b/packages/devextreme/js/__internal/viz/range_selector/range_selector.ts @@ -4,6 +4,12 @@ import type { VisualRange } from '@js/common/charts'; import { registerComponent } from '@ts/core/component_registrator'; import formatHelper from '@ts/core/format_helper'; +import { + applyDataTypePreset, + getRawGlobalFormatByDataType, + isLocaleOnlyFormat, + withFormatLocale, +} from '@ts/core/global_format_config'; import { dateUtils } from '@ts/core/utils/m_date'; import { extend } from '@ts/core/utils/m_extend'; import { each } from '@ts/core/utils/m_iterator'; @@ -1460,6 +1466,15 @@ class RangeSelector extends BaseWidget { sliderMarkerOptions.borderColor = this._getOption(CONTAINER_BACKGROUND_COLOR, true); + const valueDataType = isValueTypeDatetime ? DATETIME : 'number'; + const ownOrGlobalFormat = sliderMarkerOptions.format + ?? getRawGlobalFormatByDataType(valueDataType); + const localeOnlyFormat = isLocaleOnlyFormat(ownOrGlobalFormat) ? ownOrGlobalFormat : undefined; + + if (localeOnlyFormat) { + sliderMarkerOptions.format = undefined; + } + if (!sliderMarkerOptions.format && !argRange.isEmpty()) { if (doNotSnap && isNumeric(scaleOptions.startValue)) { sliderMarkerOptions.format = { @@ -1487,6 +1502,13 @@ class RangeSelector extends BaseWidget { sliderMarkerOptions.format = formatHelper.getDateFormatByTicks(tickIntervalsInfo.ticks); } } + + if (localeOnlyFormat) { + sliderMarkerOptions.format = isDefined(sliderMarkerOptions.format) + ? withFormatLocale(sliderMarkerOptions.format, localeOnlyFormat.locale) + : applyDataTypePreset(localeOnlyFormat, valueDataType); + } + return sliderMarkerOptions; } diff --git a/packages/devextreme/js/__internal/viz/series/base_series.ts b/packages/devextreme/js/__internal/viz/series/base_series.ts index 10e1c6c04c9a..55ae5150e8eb 100644 --- a/packages/devextreme/js/__internal/viz/series/base_series.ts +++ b/packages/devextreme/js/__internal/viz/series/base_series.ts @@ -17,6 +17,7 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ /* eslint-disable @typescript-eslint/prefer-optional-chain */ +import { applyDataTypePreset } from '@ts/core/global_format_config'; import { paintedColor } from '@ts/core/utils/css_variables'; import { noop as _noop } from '@ts/core/utils/m_common'; import { extend as _extend } from '@ts/core/utils/m_extend'; @@ -1087,6 +1088,11 @@ export let Series = class Series { }); parsedOptions.label = getLabelOptions(labelOptions, styles.labelColor); + parsedOptions.label.format = applyDataTypePreset(parsedOptions.label.format, this.valueType); + parsedOptions.label.argumentFormat = applyDataTypePreset( + parsedOptions.label.argumentFormat, + this.argumentType, + ); if (this.areErrorBarsVisible()) { parsedOptions.errorBars = options.valueErrorBar; diff --git a/packages/devextreme/js/__internal/viz/series/points/range_symbol_point.ts b/packages/devextreme/js/__internal/viz/series/points/range_symbol_point.ts index 63d3e247a324..9c284b448f83 100644 --- a/packages/devextreme/js/__internal/viz/series/points/range_symbol_point.ts +++ b/packages/devextreme/js/__internal/viz/series/points/range_symbol_point.ts @@ -494,12 +494,12 @@ export default _extend({}, symbolPoint, { const initialMinValue = that.initialMinValue; const initialValue = that.initialValue; const initialArgument = that.initialArgument; - const minValue = tooltip.formatValue(initialMinValue); - const value = tooltip.formatValue(initialValue); + const minValue = tooltip.formatValue(initialMinValue, undefined, that.series.valueType); + const value = tooltip.formatValue(initialValue, undefined, that.series.valueType); return { argument: initialArgument, - argumentText: tooltip.formatValue(initialArgument, 'argument'), + argumentText: tooltip.formatValue(initialArgument, 'argument', that.series.argumentType), valueText: `${minValue} - ${value}`, rangeValue1Text: minValue, rangeValue2Text: value, diff --git a/packages/devextreme/js/__internal/viz/series/points/symbol_point.ts b/packages/devextreme/js/__internal/viz/series/points/symbol_point.ts index 5a9158a173cf..a477b1a4bed4 100644 --- a/packages/devextreme/js/__internal/viz/series/points/symbol_point.ts +++ b/packages/devextreme/js/__internal/viz/series/points/symbol_point.ts @@ -705,8 +705,8 @@ export default { {}, labelFormatObject, { - argumentText: tooltip.formatValue(that.initialArgument, 'argument'), - valueText: tooltip.formatValue(that.initialValue), + argumentText: tooltip.formatValue(that.initialArgument, 'argument', that.series.argumentType), + valueText: tooltip.formatValue(that.initialValue, undefined, that.series.valueType), }, _isDefined(labelFormatObject.percent) ? { percentText: tooltip.formatValue(labelFormatObject.percent, 'percent') } : {}, _isDefined(labelFormatObject.total) ? { totalText: tooltip.formatValue(labelFormatObject.total) } : {}, diff --git a/packages/devextreme/js/common/core/localization.d.ts b/packages/devextreme/js/common/core/localization.d.ts index 5583ec5f0d94..b3d75a597a90 100644 --- a/packages/devextreme/js/common/core/localization.d.ts +++ b/packages/devextreme/js/common/core/localization.d.ts @@ -75,8 +75,15 @@ export function parseDate(text: string, format: Format): Date; */ export function parseNumber(text: string, format: Format): number; -type ExternalFormat = Intl.DateTimeFormatOptions - | Intl.NumberFormatOptions; +/** + * @docid + * @public + */ +export type FormatLocale = string | (() => string); + +type ExternalFormat = (Intl.DateTimeFormatOptions | Intl.NumberFormatOptions) & { + locale?: FormatLocale; +}; type PredefinedFormat = FormatType; @@ -130,4 +137,9 @@ export interface FormatObject { * @type Enums.Format|string */ type?: PredefinedFormat | string; + /** + * @docid Format.locale + * @public + */ + locale?: FormatLocale; } diff --git a/packages/devextreme/testing/tests/DevExpress.localization/localization.globalize.tests.js b/packages/devextreme/testing/tests/DevExpress.localization/localization.globalize.tests.js index 02a4ec2b0a06..97eee6b45240 100644 --- a/packages/devextreme/testing/tests/DevExpress.localization/localization.globalize.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.localization/localization.globalize.tests.js @@ -12,6 +12,7 @@ import numberLocalization from 'common/core/localization/number'; import dateLocalization from 'common/core/localization/date'; import messageLocalization from 'common/core/localization/message'; import config from 'core/config'; +import coreLocalization from 'common/core/localization/core'; import ExcelJSLocalizationFormatTests from '../DevExpress.exporter/exceljsParts/exceljs.format.tests.js'; @@ -930,6 +931,147 @@ QUnit.module('Format helper', () => { delete String.prototype.format; }); }); + + QUnit.module('Global formatting config - format locale (spec, globalize)', () => { + const saveGlobalFormats = () => { + const globalConfig = config(); + + return { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, + }; + }; + const restoreGlobalFormats = (saved) => { + const globalConfig = config(); + + Object.keys(saved).forEach((key) => { + const value = saved[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); + }; + + QUnit.test('numberFormat locale overrides message locale for Globalize formatting', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = coreLocalization.locale(); + const savedGlobalizeLocale = Globalize.locale().locale; + + try { + Globalize.locale('de'); + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + assert.strictEqual(numberLocalization.format(1234.56), '1,234.56'); + } finally { + Globalize.locale(savedGlobalizeLocale); + coreLocalization.locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('dateFormat locale overrides message locale for shortDate preset', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = coreLocalization.locale(); + const savedGlobalizeLocale = Globalize.locale().locale; + + try { + Globalize.locale('en'); + coreLocalization.locale('en'); + config({ + ...config(), + dateFormat: { + default: { + locale: 'de-DE', + type: 'shortDate', + }, + }, + }); + + assert.strictEqual( + dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), + '2.1.2020', + ); + } finally { + Globalize.locale(savedGlobalizeLocale); + coreLocalization.locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('locale map selects entry by message locale and applies format locale', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = coreLocalization.locale(); + const savedGlobalizeLocale = Globalize.locale().locale; + + try { + Globalize.locale('de'); + coreLocalization.locale('de'); + config({ + ...config(), + numberFormat: { + de: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + default: { + locale: 'de-DE', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + assert.strictEqual(numberLocalization.format(1234.56), '1,234.56'); + } finally { + Globalize.locale(savedGlobalizeLocale); + coreLocalization.locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('locale-only date format uses the Globalize default date', function(assert) { + const savedLocale = coreLocalization.locale(); + const savedGlobalizeLocale = Globalize.locale().locale; + const date = new Date(2020, 0, 2); + + try { + Globalize.locale('en'); + coreLocalization.locale('en'); + + const localizedDate = Globalize('de').dateFormatter()(date); + + assert.strictEqual(dateLocalization.format(date, { locale: 'de' }), localizedDate); + assert.strictEqual( + dateLocalization.format(date, { locale: 'de', type: undefined }), + localizedDate, + ); + assert.notEqual(localizedDate, Globalize('en').dateFormatter()(date)); + assert.strictEqual( + dateLocalization.format(date, { locale: 'de', day: 'numeric' }), + undefined, + ); + } finally { + Globalize.locale(savedGlobalizeLocale); + coreLocalization.locale(savedLocale); + } + }); + }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js index b78eaa959015..c77c880174b1 100644 --- a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js @@ -1,5 +1,7 @@ import '../../helpers/noIntl.js'; import Intl from 'intl'; +import $ from 'jquery'; +import DateBox from 'ui/date_box'; import sharedTests from './sharedParts/localization.shared.js'; import dateLocalization from 'common/core/localization/date'; import numberLocalization from 'common/core/localization/number'; @@ -15,6 +17,7 @@ if(Intl.__disableRegExpRestore) { } const SYMBOLS_TO_REMOVE_REGEX = /[\u200E\u200F]/g; +const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input'; const ROUNDING_BUG_NUMBERS = [4.645, -4.645, 35.855, -35.855]; const ROUNDING_CORRECTION = { '4.64': '4.65', @@ -949,6 +952,220 @@ QUnit.module('Global formatting config (spec, intl)', () => { }); }); +QUnit.module('Global formatting config - format locale (spec, intl)', () => { + const saveGlobalFormats = () => { + const globalConfig = config(); + + return { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, + }; + }; + const restoreGlobalFormats = (saved) => { + const globalConfig = config(); + + Object.keys(saved).forEach((key) => { + const value = saved[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); + }; + + QUnit.test('numberFormat locale overrides message locale for Intl formatting', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = locale(); + + try { + locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + assert.strictEqual(numberLocalization.format(1234.56), '1,234.56'); + } finally { + locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('dateFormat locale overrides message locale for shortDate preset', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = locale(); + + try { + locale('en'); + config({ + ...config(), + dateFormat: { + default: { + locale: 'de-DE', + type: 'shortDate', + }, + }, + }); + + assert.strictEqual( + dateLocalization.format(new Date(2020, 0, 2), { + locale: 'de-DE', + type: 'shortDate', + }), + '2.1.2020', + ); + } finally { + locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('numberFormat dynamic locale function is applied at format time', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = locale(); + let dynamicLocale = 'en-US'; + + try { + locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: () => dynamicLocale, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + assert.strictEqual(numberLocalization.format(1234.56), '1,234.56'); + + dynamicLocale = 'de-DE'; + assert.strictEqual(numberLocalization.format(1234.56), '1.234,56'); + } finally { + locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('locale map selects entry by message locale and applies format locale', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = locale(); + + try { + locale('de'); + config({ + ...config(), + numberFormat: { + de: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + default: { + locale: 'de-DE', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + assert.strictEqual(numberLocalization.format(1234.56), '1,234.56'); + } finally { + locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('LDML numberFormat uses message locale separators', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = locale(); + + try { + locale('de'); + config({ + ...config(), + numberFormat: '#,##0.00', + }); + + assert.strictEqual(numberLocalization.format(1234.5), '1.234,50'); + } finally { + locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('format object without locale keeps message locale behavior', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = locale(); + + try { + locale('de'); + config({ + ...config(), + numberFormat: { + default: { + type: 'fixedPoint', + precision: 2, + }, + }, + }); + + const result = numberLocalization.format(1234.56); + assert.ok(result.indexOf(',56') > -1, 'uses de decimal separator'); + assert.ok(result.indexOf('.') > -1, 'uses de thousands separator'); + } finally { + locale(savedLocale); + restoreGlobalFormats(saved); + } + }); + + QUnit.test('DateBox implicit displayFormat uses global dateFormat locale', function(assert) { + const saved = saveGlobalFormats(); + const savedLocale = locale(); + const $element = $('
').appendTo('#qunit-fixture'); + + try { + locale('en'); + config({ + ...config(), + dateFormat: { + default: { + locale: 'de-DE', + type: 'shortDate', + }, + }, + }); + + $element.dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + pickerType: 'calendar', + }); + + assert.strictEqual( + $element.find(`.${TEXTEDITOR_INPUT_CLASS}`).val(), + '2.1.2020', + 'date input uses de-DE shortDate pattern', + ); + } finally { + $element.remove(); + locale(savedLocale); + restoreGlobalFormats(saved); + } + }); +}); + QUnit.module('Exceljs format', () => { ExcelJSLocalizationFormatTests.runCurrencyTests([ { value: 'USD', expected: '$#,##0_);\\($#,##0\\)' }, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index dd288cf7db84..75114fca4d0f 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3452,6 +3452,28 @@ QUnit.module('Global formatting config (spec)', { assert.strictEqual($input.val(), '02/01/2020, 14:05'); }); + QUnit.test('locale-only displayFormat on datetime keeps time', function(assert) { + config({ + ...config(), + dateTimeFormat: { + default: { + locale: 'de-DE', + type: 'shortDateShortTime', + }, + }, + }); + + const $element = $('#dateBox').dxDateBox({ + type: 'datetime', + value: new Date(2024, 5, 15, 14, 30), + displayFormat: { locale: 'en-US' }, + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.val(), '6/15/2024, 2:30 PM'); + }); + QUnit.test('implicit time displayFormat uses global timeFormat', function(assert) { config({ ...config(), @@ -3580,4 +3602,33 @@ QUnit.module('Global formatting config (spec)', { assert.strictEqual($input.val(), '02/01/2020'); }); + + QUnit.test('explicit displayFormat keeps priority over global dateFormat locale', function(assert) { + const savedLocale = localization.locale(); + + try { + localization.locale('en'); + config({ + ...config(), + dateFormat: { + default: { + locale: 'de-DE', + type: 'shortDate', + }, + }, + }); + + const $element = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + displayFormat: 'yyyy-MM-dd', + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.val(), '2020-01-02', 'explicit displayFormat wins over global dateFormat locale'); + } finally { + localization.locale(savedLocale); + } + }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberbox.localization.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberbox.localization.tests.js index 89e4fa2b391c..38ba4d1f5c46 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberbox.localization.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberbox.localization.tests.js @@ -10,6 +10,60 @@ import 'ui/text_box/ui.text_editor'; const TEXTEDITOR_INPUT_CLASS = '.dx-texteditor-input'; +const cloneNumberFormat = (value) => { + if(value === undefined) { + return undefined; + } + + if(typeof value === 'string') { + return value; + } + + return JSON.parse(JSON.stringify(value)); +}; + +const restoreGlobalNumberFormat = (savedNumberFormat) => { + const currentConfig = config(); + + if(savedNumberFormat === undefined) { + delete currentConfig.numberFormat; + } else { + currentConfig.numberFormat = cloneNumberFormat(savedNumberFormat); + } +}; + +const saveNumberBoxCustomRules = () => ( + NumberBox._classCustomRules ? [...NumberBox._classCustomRules] : [] +); + +const restoreNumberBoxCustomRules = (savedRules) => { + NumberBox._classCustomRules = savedRules ? [...savedRules] : []; +}; + +const saveLocalizationState = () => ({ + locale: localization.locale(), + numberFormat: cloneNumberFormat(config().numberFormat), + numberBoxCustomRules: saveNumberBoxCustomRules(), +}); + +const restoreLocalizationState = (savedState) => { + localization.locale(savedState.locale); + restoreGlobalNumberFormat(savedState.numberFormat); + restoreNumberBoxCustomRules(savedState.numberBoxCustomRules); +}; + +const localizationModuleHooks = { + beforeEach: function() { + this.savedLocalizationState = saveLocalizationState(); + delete config().numberFormat; + localization.locale('en'); + }, + + afterEach: function() { + restoreLocalizationState(this.savedLocalizationState); + }, +}; + QUnit.testStart(function() { const markup = '
\ @@ -21,22 +75,30 @@ QUnit.testStart(function() { const moduleConfig = { beforeEach: function() { + localizationModuleHooks.beforeEach.call(this); + this.$element = $('#numberbox').dxNumberBox({ format: '#0.##', value: '', - useMaskBehavior: true + useMaskBehavior: true, }); this.input = this.$element.find(TEXTEDITOR_INPUT_CLASS); this.instance = this.$element.dxNumberBox('instance'); this.keyboard = keyboardMock(this.input, true); - } + }, + + afterEach: function() { + localizationModuleHooks.afterEach.call(this); + }, }; QUnit.module('localization: separator keys', moduleConfig, () => { QUnit.test('pressing "." should clear selected text if it contains a decimal separator (T1199553)', function(assert) { + localization.locale('en'); + this.instance.option({ format: '0#.00', - value: 123.45 + value: 123.45, }); this.keyboard @@ -47,40 +109,24 @@ QUnit.module('localization: separator keys', moduleConfig, () => { }); QUnit.test('pressing the "." key on the numpad should clear the selected text regardless of the char produced (T1199553)', function(assert) { - const currentLocale = localization.locale(); const NUMPAD_DOT_KEY_CODE = 110; - try { - localization.locale('fr-ca'); - this.instance.option({ - format: '0#.00', - value: 123.45 - }); - - this.keyboard - .caret({ start: 0, end: 6 }) - .type('.', { which: NUMPAD_DOT_KEY_CODE }); - - assert.strictEqual(this.input.val(), '0,00', 'mask value is cleared'); - } finally { - localization.locale(currentLocale); - } + localization.locale('fr-ca'); + + this.instance.option({ + format: '0#.00', + value: 123.45, + }); + + this.keyboard + .caret({ start: 0, end: 6 }) + .type('.', { which: NUMPAD_DOT_KEY_CODE }); + + assert.strictEqual(this.input.val(), '0,00', 'mask value is cleared'); }); }); -QUnit.module('localization: global number format', { - beforeEach: function() { - this.savedConfig = { ...config() }; - this.savedLocale = localization.locale(); - localization.locale('en'); - }, - - afterEach: function() { - localization.locale(this.savedLocale); - config(this.savedConfig); - NumberBox.defaultOptions([]); - }, -}, () => { +QUnit.module('localization: global number format', localizationModuleHooks, () => { QUnit.test('uses global numberFormat when local format is not set', function(assert) { config({ ...config(), @@ -138,3 +184,159 @@ QUnit.module('localization: global number format', { assert.strictEqual($input.val(), '1,235', 'defaultOptions format wins over global'); }); }); + +QUnit.module('localization: global number format locale', localizationModuleHooks, () => { + QUnit.test('uses format locale from global numberFormat map with de message locale', function(assert) { + localization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + const $element = $('#numberbox').dxNumberBox({ + value: 1234.56, + useMaskBehavior: true, + }); + const $input = $element.find(TEXTEDITOR_INPUT_CLASS); + + assert.strictEqual($input.val(), '1,234.56', 'display uses en-US separators'); + }); + + QUnit.test('parses value using effective format locale separators', function(assert) { + localization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + const $element = $('#numberbox').dxNumberBox({ + value: null, + useMaskBehavior: true, + }); + const instance = $element.dxNumberBox('instance'); + const $input = $element.find(TEXTEDITOR_INPUT_CLASS); + const keyboard = keyboardMock($input, true); + + keyboard + .caret({ start: 0, end: 0 }) + .type('1234.56') + .change(); + + assert.strictEqual(instance.option('value'), 1234.56, 'parses en-US decimal separator'); + }); + + QUnit.test('dynamic format locale function is applied in NumberBox', function(assert) { + let dynamicLocale = 'en-US'; + + localization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: () => dynamicLocale, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + const $element = $('#numberbox').dxNumberBox({ + value: 1234.56, + useMaskBehavior: true, + }); + const $input = $element.find(TEXTEDITOR_INPUT_CLASS); + + assert.strictEqual($input.val(), '1,234.56', 'initial render uses en-US'); + + dynamicLocale = 'de-DE'; + $element.dxNumberBox('instance').repaint(); + + assert.strictEqual($element.find(TEXTEDITOR_INPUT_CLASS).val(), '1.234,56', 'repaint re-evaluates the locale function'); + + dynamicLocale = 'en-US'; + $element.dxNumberBox('instance').option('value', 99.5); + + assert.strictEqual($element.find(TEXTEDITOR_INPUT_CLASS).val(), '99.50', 'new value re-evaluates the locale function'); + }); + + QUnit.test('local format option keeps type; global numberFormat locale applies to culture', function(assert) { + localization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + const $element = $('#numberbox').dxNumberBox({ + value: 1234.56, + useMaskBehavior: true, + format: { + type: 'fixedPoint', + precision: 0, + }, + }); + const $input = $element.find(TEXTEDITOR_INPUT_CLASS); + + assert.strictEqual($input.val(), '1,235', 'precision from local format; en-US from global numberFormat'); + }); + + QUnit.test('typed format keeps its explicit locale when the mask is generated', function(assert) { + localization.locale('en'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'fr', + }, + }, + }); + + const format = { + type: 'fixedPoint', + precision: 2, + locale: 'de-DE', + }; + const $element = $('#numberbox').dxNumberBox({ + value: 1234.5, + useMaskBehavior: true, + format, + }); + const $input = $element.find(TEXTEDITOR_INPUT_CLASS); + + assert.strictEqual($input.val(), '1.234,50', 'display uses de-DE separators, not fr'); + + $element.dxNumberBox('dispose'); + + const $empty = $('#numberbox').dxNumberBox({ + value: null, + useMaskBehavior: true, + format, + }); + const instance = $empty.dxNumberBox('instance'); + const keyboard = keyboardMock($empty.find(TEXTEDITOR_INPUT_CLASS), true); + + keyboard + .caret({ start: 0, end: 0 }) + .type('1234,5') + .change(); + + assert.strictEqual(instance.option('value'), 1234.5, 'parse uses de-DE separators'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js index 51da67218c09..0d1de6a01377 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import config from 'core/config'; +import localization from 'localization'; import translator2DModule from 'viz/translators/translator2d'; import { Range } from 'viz/translators/range'; import tickGeneratorModule from 'viz/axes/tick_generator'; @@ -219,6 +220,56 @@ QUnit.test('datetime axis labels use global dateTimeFormat when label.format is }, [new Date(2020, 0, 2)], 'day', ['2020-01-02']); }); +QUnit.test('value axis labels use global numberFormat locale when label.format is not set', function(assert) { + const savedLocale = localization.locale(); + + try { + localization.locale('de'); + config({ + ...config(), + numberFormat: { + default: { + locale: 'en-US', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }); + + this.testTickLabelFormat(assert, [1500], 100, ['1,500.00']); + } finally { + localization.locale(savedLocale); + } +}); + +QUnit.test('datetime axis labels use global dateTimeFormat locale when label.format is not set', function(assert) { + const savedLocale = localization.locale(); + + try { + localization.locale('en'); + config({ + ...config(), + dateTimeFormat: { + default: { + locale: 'de-DE', + day: '2-digit', + month: '2-digit', + year: 'numeric', + }, + }, + }); + + this.testFormat(assert, { + argumentType: 'datetime', + label: { + visible: true, + }, + }, [new Date(2020, 0, 2)], 'day', ['02.01.2020']); + } finally { + localization.locale(savedLocale); + } +}); + QUnit.module('Auto formatting. Tick labels. Numeric.', environment); QUnit.test('formatter should support short notations of numbers', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.viz.rangeSelector/common.part2.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.rangeSelector/common.part2.tests.js index 1aeaabbf35b1..4812d97a44f5 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.rangeSelector/common.part2.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.rangeSelector/common.part2.tests.js @@ -7,6 +7,7 @@ import { import seriesDataSourceModule from 'viz/range_selector/series_data_source'; import { DataSource } from 'common/data/data_source/data_source'; import dateUtils from 'core/utils/date'; +import config from 'core/config'; import { Title, stubClass @@ -1113,6 +1114,104 @@ QUnit.test('Auto format for sliderMarker when minorTickInterval and tickInterval assert.strictEqual(this.slidersController.update.lastCall.args[4].format(new Date(2017, 1, 1, 1, 1, 1, 123)), '1:01:01 AM 123', 'Slider markers auto format'); }); +QUnit.test('Auto format for sliderMarker with own locale-only format - composite format uses locale', function(assert) { + this.createWidget({ + scale: { + startValue: new Date(2006, 10, 1, 1, 1, 1), + endValue: new Date(2006, 10, 1, 1, 1, 4), + tickInterval: { milliseconds: 500 }, + minorTickInterval: { milliseconds: 100 } + }, + marker: { + visible: false + }, + sliderMarker: { + format: { locale: 'de' } + } + }); + + assert.strictEqual(this.slidersController.update.lastCall.args[4].format(new Date(2017, 1, 1, 1, 1, 1, 123)), '01:01:01 123', 'Slider markers auto format'); +}); + +QUnit.test('Auto format for sliderMarker with global locale-only dateTimeFormat - composite format uses locale', function(assert) { + const savedDateTimeFormat = config().dateTimeFormat; + + try { + config({ dateTimeFormat: { default: { locale: 'de' } } }); + + this.createWidget({ + scale: { + startValue: new Date(2006, 10, 1, 1, 1, 1), + endValue: new Date(2006, 10, 1, 1, 1, 4), + tickInterval: { milliseconds: 500 }, + minorTickInterval: { milliseconds: 100 } + }, + marker: { + visible: false + } + }); + + assert.strictEqual(this.slidersController.update.lastCall.args[4].format(new Date(2017, 1, 1, 1, 1, 1, 123)), '01:01:01 123', 'Slider markers auto format'); + } finally { + const currentConfig = config(); + + if(savedDateTimeFormat === undefined) { + delete currentConfig.dateTimeFormat; + } else { + currentConfig.dateTimeFormat = savedDateTimeFormat; + } + } +}); + +QUnit.test('Auto format for sliderMarker with own locale-only format - string auto format gets locale', function(assert) { + this.createWidget({ + behavior: { + snapToTicks: true + }, + scale: { + startValue: new Date(2006, 10, 1), + endValue: new Date(2010, 1, 1), + tickInterval: { months: 1 }, + minorTickInterval: { days: 14 } + }, + sliderMarker: { + format: { locale: 'de' } + } + }); + + assert.deepEqual(this.slidersController.update.lastCall.args[4].format, { type: 'shortdate', locale: 'de' }); +}); + +QUnit.test('Explicit sliderMarker format is kept when global dateTimeFormat is locale-only', function(assert) { + const savedDateTimeFormat = config().dateTimeFormat; + + try { + config({ dateTimeFormat: { default: { locale: 'de' } } }); + + this.createWidget({ + scale: { + startValue: new Date(2006, 10, 1), + endValue: new Date(2010, 1, 1), + tickInterval: { months: 1 }, + minorTickInterval: { days: 14 } + }, + sliderMarker: { + format: 'shortdate' + } + }); + + assert.strictEqual(this.slidersController.update.lastCall.args[4].format, 'shortdate'); + } finally { + const currentConfig = config(); + + if(savedDateTimeFormat === undefined) { + delete currentConfig.dateTimeFormat; + } else { + currentConfig.dateTimeFormat = savedDateTimeFormat; + } + } +}); + QUnit.test('Auto format for sliderMarker when minorTickInterval and tickInterval have different units but snapToTicks true - take minorTickInterval', function(assert) { this.createWidget({ behavior: { diff --git a/packages/devextreme/testing/tests/DevExpress.viz.rangeSelector/common.part3.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.rangeSelector/common.part3.tests.js index 6b0d5fe5bad7..d43f32d4b973 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.rangeSelector/common.part3.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.rangeSelector/common.part3.tests.js @@ -1323,6 +1323,23 @@ QUnit.test('Init selection format before calculate canvas for number type', func assert.deepEqual(this.slidersController.update.lastCall.args[4].format, { type: 'fixedPoint', precision: 1 }, 'format'); }); +QUnit.test('Init selection format with own locale-only format for number type keeps auto precision', function(assert) { + this.createWidget({ + sliderMarker: { + format: { locale: 'de' } + }, + scale: { + startValue: 20, + endValue: 180 + }, + behavior: { + snapToTicks: false + } + }); + + assert.deepEqual(this.slidersController.update.lastCall.args[4].format, { type: 'fixedPoint', precision: 1, locale: 'de' }, 'format'); +}); + QUnit.test('format for sliders', function(assert) { this.createWidget({ sliderMarker: { @@ -1361,6 +1378,22 @@ QUnit.test('Init selection format before calculate canvas for dateTime type', fu assert.ok(formatsAreEqual(options.format, expectedFormat), 'format selection for number type'); }); +QUnit.test('Init selection format with own locale-only format for dateTime type applies locale to custom day formatter', function(assert) { + this.createWidget({ + sliderMarker: { + format: { locale: 'de' } + }, + scale: { + minorTickInterval: { days: 1 }, + tickInterval: { weeks: 1 }, + startValue: new Date(2010, 2, 13), + endValue: new Date(2010, 2, 20) + } + }); + + assert.strictEqual(this.slidersController.update.lastCall.args[4].format(new Date(2010, 2, 13)), 'Samstag, 13'); +}); + // B217786 QUnit.test('range small width', function(assert) { this.createWidget({ diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 2fae404c0176..d35548ef27b0 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -1329,7 +1329,12 @@ declare module DevExpress.common { /** * @deprecated Attention! This type is for internal purposes only. If you used it previously, please submit a ticket to our {@link https://supportcenter.devexpress.com/ticket/create Support Center}. We will check if there is an alternative solution. */ - type ExternalFormat = Intl.DateTimeFormatOptions | Intl.NumberFormatOptions; + type ExternalFormat = ( + | Intl.DateTimeFormatOptions + | Intl.NumberFormatOptions + ) & { + locale?: FormatLocale; + }; export type FieldChooserLayout = 0 | 1 | 2; /** @@ -1365,6 +1370,10 @@ declare module DevExpress.common { | 'minute' | 'second' | 'shortDateShortTime'; + /** + * [descr:FormatLocale] + */ + export type FormatLocale = string | (() => string); /** * @deprecated Attention! This type is for internal purposes only. If you used it previously, please submit a ticket to our {@link https://supportcenter.devexpress.com/ticket/create Support Center}. We will check if there is an alternative solution. */ @@ -1393,6 +1402,10 @@ declare module DevExpress.common { * [descr:Format.type] */ type?: PredefinedFormat | string; + /** + * [descr:Format.locale] + */ + locale?: FormatLocale; } /** * [descr:GlobalConfig]