- {i18n('Select the interval')}
+ {t('Select the interval')}
@@ -110,10 +111,10 @@ export function PickerForm(
}
validationState={state.endValidation?.isInvalid ? 'invalid' : undefined}
errorMessage={
- state.endValidation?.errors?.join('\n') || i18n('Value is incorrect.')
+ state.endValidation?.errors?.join('\n') || t('Value is incorrect.')
}
placeholderValue={props.placeholderValue?.endOf('day')}
- label={i18n('To')}
+ label={t('To')}
value={state.end}
onUpdate={state.setEnd}
roundUp
@@ -130,7 +131,7 @@ export function PickerForm(
className={b('apply')}
width="max"
>
- {i18n('Apply')}
+ {t('Apply')}
) : null}
{props.withPresets && !props.readOnly ? (
diff --git a/src/components/RelativeRangeDatePicker/components/PickerDialog/useRelativeRangeDatePickerDialogState.tsx b/src/components/RelativeRangeDatePicker/components/PickerDialog/useRelativeRangeDatePickerDialogState.tsx
index c8c1de3b..57fd39b2 100644
--- a/src/components/RelativeRangeDatePicker/components/PickerDialog/useRelativeRangeDatePickerDialogState.tsx
+++ b/src/components/RelativeRangeDatePicker/components/PickerDialog/useRelativeRangeDatePickerDialogState.tsx
@@ -5,7 +5,8 @@ import {useControlledState} from '@gravity-ui/uikit';
import type {Value} from '../../../RelativeDatePicker';
import type {RangeValue} from '../../../types';
-import {getRangeValidationResult} from '../../hooks/useRelativeRangeDatePickerState';
+import {i18n} from '../../../utils/validation/i18n';
+import {getRangeValidationResult} from '../../../utils/validation/relativeRangeDatePicker';
import type {PickerFormProps} from './PickerForm';
@@ -110,6 +111,7 @@ export function useRelativeRangeDatePickerDialogState(props: PickerFormProps) {
setValue(getRangeValue(start, end, {...props, timeZone, allowNullableValues}), timeZone);
}
+ const {t} = i18n.useTranslation();
const validation = React.useMemo(
() =>
getRangeValidationResult(
@@ -119,6 +121,7 @@ export function useRelativeRangeDatePickerDialogState(props: PickerFormProps) {
props.maxValue,
props.isDateUnavailable,
timeZone,
+ t,
),
[
allowNullableValues,
@@ -128,6 +131,7 @@ export function useRelativeRangeDatePickerDialogState(props: PickerFormProps) {
props.minValue,
start,
timeZone,
+ t,
],
);
diff --git a/src/components/RelativeRangeDatePicker/components/Presets/Presets.tsx b/src/components/RelativeRangeDatePicker/components/Presets/Presets.tsx
index a7aaf987..f2ef300b 100644
--- a/src/components/RelativeRangeDatePicker/components/Presets/Presets.tsx
+++ b/src/components/RelativeRangeDatePicker/components/Presets/Presets.tsx
@@ -8,6 +8,7 @@ import {List, Tab, TabList, TabPanel, TabProvider} from '@gravity-ui/uikit';
import {block} from '../../../../utils/cn';
import type {Preset} from './defaultPresets';
+import {i18n} from './i18n';
import {filterPresetTabs, getDefaultPresetTabs} from './utils';
import type {PresetTab} from './utils';
@@ -31,9 +32,10 @@ export function Presets({
onChoosePreset,
presetTabs,
}: PresetProps) {
+ const {t} = i18n.useTranslation();
const tabs = React.useMemo(() => {
- return filterPresetTabs(presetTabs ?? getDefaultPresetTabs({withTime}), {minValue});
- }, [withTime, minValue, presetTabs]);
+ return filterPresetTabs(presetTabs ?? getDefaultPresetTabs({withTime, t}), {minValue});
+ }, [withTime, minValue, presetTabs, t]);
const [activeTabId, setActiveTab] = React.useState(tabs[0]?.id);
diff --git a/src/components/RelativeRangeDatePicker/components/Presets/defaultPresets.ts b/src/components/RelativeRangeDatePicker/components/Presets/defaultPresets.ts
deleted file mode 100644
index 3e718003..00000000
--- a/src/components/RelativeRangeDatePicker/components/Presets/defaultPresets.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-import {i18n} from './i18n';
-
-export interface Preset {
- from: string;
- to: string;
- title: string;
-}
-
-export const DEFAULT_DATE_PRESETS: Preset[] = [
- {
- from: 'now-1d',
- to: 'now',
- get title() {
- return i18n('Last day');
- },
- },
- {
- from: 'now-3d',
- to: 'now',
- get title() {
- return i18n('Last 3 days');
- },
- },
- {
- from: 'now-1w',
- to: 'now',
- get title() {
- return i18n('Last week');
- },
- },
- {
- from: 'now-1M',
- to: 'now',
- get title() {
- return i18n('Last month');
- },
- },
- {
- from: 'now-3M',
- to: 'now',
- get title() {
- return i18n('Last 3 months');
- },
- },
- {
- from: 'now-6M',
- to: 'now',
- get title() {
- return i18n('Last 6 months');
- },
- },
- {
- from: 'now-1y',
- to: 'now',
- get title() {
- return i18n('Last year');
- },
- },
- {
- from: 'now-3y',
- to: 'now',
- get title() {
- return i18n('Last 3 years');
- },
- },
-];
-
-export const DEFAULT_TIME_PRESETS: Preset[] = [
- {
- from: 'now-5m',
- to: 'now',
- get title() {
- return i18n('Last 5 minutes');
- },
- },
- {
- from: 'now-15m',
- to: 'now',
- get title() {
- return i18n('Last 15 minutes');
- },
- },
- {
- from: 'now-30m',
- to: 'now',
- get title() {
- return i18n('Last 30 minutes');
- },
- },
- {
- from: 'now-1h',
- to: 'now',
- get title() {
- return i18n('Last hour');
- },
- },
- {
- from: 'now-3h',
- to: 'now',
- get title() {
- return i18n('Last 3 hours');
- },
- },
- {
- from: 'now-6h',
- to: 'now',
- get title() {
- return i18n('Last 6 hours');
- },
- },
- {
- from: 'now-12h',
- to: 'now',
- get title() {
- return i18n('Last 12 hours');
- },
- },
-];
-
-export const DEFAULT_OTHERS_PRESETS: Preset[] = [
- {
- from: 'now/d',
- to: 'now/d',
- get title() {
- return i18n('Today');
- },
- },
- {
- from: 'now-1d/d',
- to: 'now-1d/d',
- get title() {
- return i18n('Yesterday');
- },
- },
- {
- from: 'now-2d/d',
- to: 'now-2d/d',
- get title() {
- return i18n('Day before yesterday');
- },
- },
- {
- from: 'now/w',
- to: 'now/w',
- get title() {
- return i18n('This week');
- },
- },
- {
- from: 'now/M',
- to: 'now/M',
- get title() {
- return i18n('This month');
- },
- },
- {
- from: 'now/y',
- to: 'now/y',
- get title() {
- return i18n('This year');
- },
- },
- {
- from: 'now/d',
- to: 'now',
- get title() {
- return i18n('From start of day');
- },
- },
- {
- from: 'now/w',
- to: 'now',
- get title() {
- return i18n('From start of week');
- },
- },
- {
- from: 'now/M',
- to: 'now',
- get title() {
- return i18n('From start of month');
- },
- },
- {
- from: 'now/y',
- to: 'now',
- get title() {
- return i18n('From start of year');
- },
- },
-];
-
-export const allPresets = DEFAULT_TIME_PRESETS.concat(DEFAULT_DATE_PRESETS, DEFAULT_OTHERS_PRESETS);
diff --git a/src/components/RelativeRangeDatePicker/components/Presets/defaultPresets.tsx b/src/components/RelativeRangeDatePicker/components/Presets/defaultPresets.tsx
new file mode 100644
index 00000000..a46216f6
--- /dev/null
+++ b/src/components/RelativeRangeDatePicker/components/Presets/defaultPresets.tsx
@@ -0,0 +1,152 @@
+import {i18n} from './i18n';
+
+export interface Preset {
+ from: string;
+ to: string;
+ title: React.ReactNode;
+}
+
+export function PresetTitle({
+ title,
+}: {
+ title: keyof (typeof i18n.keysetData)['g-date-relative-range-date-picker-presets'];
+}) {
+ const {t} = i18n.useTranslation();
+ return t(title);
+}
+
+export const DEFAULT_DATE_PRESETS: Preset[] = [
+ {
+ from: 'now-1d',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-3d',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-1w',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-1M',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-3M',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-6M',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-1y',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-3y',
+ to: 'now',
+ title:
,
+ },
+];
+
+export const DEFAULT_TIME_PRESETS: Preset[] = [
+ {
+ from: 'now-5m',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-15m',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-30m',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-1h',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-3h',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-6h',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now-12h',
+ to: 'now',
+ title:
,
+ },
+];
+
+export const DEFAULT_OTHERS_PRESETS: Preset[] = [
+ {
+ from: 'now/d',
+ to: 'now/d',
+ title:
,
+ },
+ {
+ from: 'now-1d/d',
+ to: 'now-1d/d',
+ title:
,
+ },
+ {
+ from: 'now-2d/d',
+ to: 'now-2d/d',
+ title:
,
+ },
+ {
+ from: 'now/w',
+ to: 'now/w',
+ title:
,
+ },
+ {
+ from: 'now/M',
+ to: 'now/M',
+ title:
,
+ },
+ {
+ from: 'now/y',
+ to: 'now/y',
+ title:
,
+ },
+ {
+ from: 'now/d',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now/w',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now/M',
+ to: 'now',
+ title:
,
+ },
+ {
+ from: 'now/y',
+ to: 'now',
+ title:
,
+ },
+];
+
+export const allPresets = DEFAULT_TIME_PRESETS.concat(DEFAULT_DATE_PRESETS, DEFAULT_OTHERS_PRESETS);
diff --git a/src/components/RelativeRangeDatePicker/components/Presets/utils.ts b/src/components/RelativeRangeDatePicker/components/Presets/utils.ts
index 7fa45408..8933e03c 100644
--- a/src/components/RelativeRangeDatePicker/components/Presets/utils.ts
+++ b/src/components/RelativeRangeDatePicker/components/Presets/utils.ts
@@ -1,6 +1,8 @@
import {dateTimeParse} from '@gravity-ui/date-utils';
import type {DateTime} from '@gravity-ui/date-utils';
+import type {ExtractFunctionType} from '../../../types';
+
import {
DEFAULT_DATE_PRESETS,
DEFAULT_OTHERS_PRESETS,
@@ -32,7 +34,12 @@ const countUnit = {
y: 'Last {count} year',
} as const;
-export function getPresetTitle(start: string, end: string, presets: Preset[] = allPresets) {
+export function getPresetTitle(
+ start: string,
+ end: string,
+ presets: Preset[] = allPresets,
+ t: ExtractFunctionType
= i18n,
+) {
const startText = start.replace(/\s+/g, '');
const endText = end.replace(/\s+/g, '');
@@ -48,7 +55,7 @@ export function getPresetTitle(start: string, end: string, presets: Preset[] = a
const [, count, unit] = match;
if (isDateUnit(unit)) {
const template = Number(count) === 1 ? oneUnit[unit] : countUnit[unit];
- return i18n(template, {count});
+ return t(template, {count});
}
}
}
@@ -90,15 +97,17 @@ export interface PresetTab {
export function getDefaultPresetTabs({
withTime,
minValue,
+ t = i18n,
}: {
minValue?: DateTime;
withTime?: boolean;
+ t?: (key: 'Main' | 'Other') => string;
}) {
const tabs: PresetTab[] = [];
const mainTab: PresetTab = {
id: 'main',
- title: i18n('Main'),
+ title: t('Main'),
presets: [],
};
const mainPresets = DEFAULT_DATE_PRESETS;
@@ -113,7 +122,7 @@ export function getDefaultPresetTabs({
const otherTab: PresetTab = {
id: 'other',
- title: i18n('Other'),
+ title: t('Other'),
presets: filterPresets(DEFAULT_OTHERS_PRESETS, minValue),
};
diff --git a/src/components/RelativeRangeDatePicker/components/Zones/Zones.tsx b/src/components/RelativeRangeDatePicker/components/Zones/Zones.tsx
index 796fa18e..ef799802 100644
--- a/src/components/RelativeRangeDatePicker/components/Zones/Zones.tsx
+++ b/src/components/RelativeRangeDatePicker/components/Zones/Zones.tsx
@@ -34,16 +34,10 @@ zones.unshift({
zones.unshift({
value: 'system',
- get content() {
- return i18n('system');
- },
});
zones.unshift({
value: 'default',
- get content() {
- return i18n('default');
- },
});
const b = block('relative-range-date-picker-zones');
@@ -59,6 +53,7 @@ export interface ZonesProps {
export function Zones(props: ZonesProps) {
const timeZone = normalizeTimeZone(props.value);
const size = props.isMobile ? 'xl' : props.size;
+ const {t} = i18n.useTranslation();
return (
{
const value =
- timeZone === 'system' || timeZone === 'default' ? i18n(timeZone) : timeZone;
+ timeZone === 'system' || timeZone === 'default' ? t(timeZone) : timeZone;
return (
}
@@ -103,7 +98,7 @@ export function Zones(props: ZonesProps) {
return (
- {v}
+ {value === 'system' || value === 'default' ? t(value) : v}
{getTimeZoneOffset(value)}
diff --git a/src/components/RelativeRangeDatePicker/hooks/useRelativeRangeDatePickerState.ts b/src/components/RelativeRangeDatePicker/hooks/useRelativeRangeDatePickerState.ts
index 4059ed01..2cda21fe 100644
--- a/src/components/RelativeRangeDatePicker/hooks/useRelativeRangeDatePickerState.ts
+++ b/src/components/RelativeRangeDatePicker/hooks/useRelativeRangeDatePickerState.ts
@@ -1,13 +1,12 @@
import React from 'react';
-import {dateTimeParse} from '@gravity-ui/date-utils';
import type {DateTime} from '@gravity-ui/date-utils';
import {useControlledState} from '@gravity-ui/uikit';
import type {Value} from '../../RelativeDatePicker';
import type {RangeValue} from '../../types';
-import {getValidationResult} from '../../utils/validation/datePicker';
-import {i18n} from '../i18n';
+import {i18n} from '../../utils/validation/i18n';
+import {getRangeValidationResult} from '../../utils/validation/relativeRangeDatePicker';
export type RelativeRangeDatePickerValue = RangeValue;
@@ -46,6 +45,7 @@ export function useRelativeRangeDatePickerState(
props.onUpdateTimeZone,
);
+ const {t} = i18n.useTranslation();
const validation = React.useMemo(
() =>
getRangeValidationResult(
@@ -55,6 +55,7 @@ export function useRelativeRangeDatePickerState(
props.maxValue,
props.isDateUnavailable,
timeZone,
+ t,
),
[
value,
@@ -63,6 +64,7 @@ export function useRelativeRangeDatePickerState(
props.maxValue,
props.minValue,
timeZone,
+ t,
],
);
@@ -79,59 +81,3 @@ export function useRelativeRangeDatePickerState(
...validation,
};
}
-
-export function getRangeValidationResult(
- value: RelativeRangeDatePickerValue | null,
- allowNullableValues: boolean | undefined,
- minValue: DateTime | undefined,
- maxValue: DateTime | undefined,
- isDateUnavailable: ((v: DateTime, endpoint: 'start' | 'end') => boolean) | undefined,
- timeZone: string,
-) {
- if (!value) {
- return {isInvalid: false, errors: []};
- }
-
- const startDate = value.start ? dateTimeParse(value.start.value, {timeZone}) : null;
- const endDate = value.end ? dateTimeParse(value.end.value, {timeZone, roundUp: true}) : null;
-
- const startValidationResult = getValidationResult(
- startDate,
- minValue,
- maxValue,
- isDateUnavailable ? (date) => isDateUnavailable(date, 'start') : undefined,
- timeZone,
- i18n('"From"'),
- );
-
- if (!startDate && !allowNullableValues) {
- startValidationResult.isInvalid = true;
- startValidationResult.errors.push(i18n('"From" is required.'));
- }
-
- const endValidationResult = getValidationResult(
- endDate,
- minValue,
- maxValue,
- isDateUnavailable ? (date) => isDateUnavailable(date, 'end') : undefined,
- timeZone,
- i18n('"To"'),
- );
-
- if (!endDate && !allowNullableValues) {
- endValidationResult.isInvalid = true;
- endValidationResult.errors.push(i18n('"To" is required.'));
- }
-
- if (startDate && endDate && endDate.isBefore(startDate)) {
- startValidationResult.isInvalid = true;
- startValidationResult.errors.push(i18n(`"From" can't be after "To".`));
- }
-
- return {
- isInvalid: startValidationResult.isInvalid || endValidationResult.isInvalid,
- startValidationResult,
- endValidationResult,
- errors: startValidationResult.errors.concat(endValidationResult.errors),
- };
-}
diff --git a/src/components/RelativeRangeDatePicker/i18n/en.json b/src/components/RelativeRangeDatePicker/i18n/en.json
index 96c75aa5..88891c19 100644
--- a/src/components/RelativeRangeDatePicker/i18n/en.json
+++ b/src/components/RelativeRangeDatePicker/i18n/en.json
@@ -1,8 +1,5 @@
{
"\"From\"": "\"From\"",
- "\"From\" is required.": "\"From\" is required.",
"\"To\"": "\"To\"",
- "\"To\" is required.": "\"To\" is required.",
- "\"From\" can't be after \"To\".": "\"From\" can't be after \"To\".",
"to": "to"
}
diff --git a/src/components/RelativeRangeDatePicker/i18n/ru.json b/src/components/RelativeRangeDatePicker/i18n/ru.json
index 132b99ab..630dd3d9 100644
--- a/src/components/RelativeRangeDatePicker/i18n/ru.json
+++ b/src/components/RelativeRangeDatePicker/i18n/ru.json
@@ -1,8 +1,5 @@
{
"\"From\"": "«От»",
- "\"From\" is required.": "«От» обязательно.",
"\"To\"": "«До»",
- "\"To\" is required.": "«До» обязательно.",
- "\"From\" can't be after \"To\".": "«От» не может быть позже чем «До».",
"to": "до"
}
diff --git a/src/components/RelativeRangeDatePicker/utils.ts b/src/components/RelativeRangeDatePicker/utils.ts
index 7676d72e..b9acb86e 100644
--- a/src/components/RelativeRangeDatePicker/utils.ts
+++ b/src/components/RelativeRangeDatePicker/utils.ts
@@ -1,9 +1,10 @@
import {dateTime, dateTimeParse} from '@gravity-ui/date-utils';
import type {Value} from '../RelativeDatePicker';
-import type {RangeValue} from '../types';
+import type {ExtractFunctionType, RangeValue} from '../types';
import type {Preset} from './components/Presets/defaultPresets';
+import {i18n} from './components/Presets/i18n';
import {getPresetTitle} from './components/Presets/utils';
export function resolveTimeZone(timeZone: string) {
@@ -33,6 +34,8 @@ interface GetDefaultTitleArgs {
alwaysShowAsAbsolute?: boolean;
format?: string;
presets?: Preset[];
+ presetsTranslations?: ExtractFunctionType;
+ lang?: string;
}
export function getDefaultTitle({
value,
@@ -40,6 +43,8 @@ export function getDefaultTitle({
alwaysShowAsAbsolute,
format = 'L',
presets,
+ presetsTranslations = i18n,
+ lang = 'en',
}: GetDefaultTitleArgs) {
if (!value) {
return '';
@@ -52,14 +57,16 @@ export function getDefaultTitle({
from =
value.start.type === 'relative' && !alwaysShowAsAbsolute
? value.start.value
- : (dateTimeParse(value.start.value, {timeZone})?.format(format) ?? '');
+ : (dateTimeParse(value.start.value, {timeZone, lang})?.format(format) ?? '');
}
let to = '';
if (value.end) {
to =
value.end.type === 'relative' && !alwaysShowAsAbsolute
? value.end.value
- : (dateTimeParse(value.end.value, {timeZone, roundUp: true})?.format(format) ?? '');
+ : (dateTimeParse(value.end.value, {timeZone, roundUp: true, lang})?.format(
+ format,
+ ) ?? '');
}
if (
@@ -67,7 +74,7 @@ export function getDefaultTitle({
value.start?.type === 'relative' &&
value.end?.type === 'relative'
) {
- return `${getPresetTitle(value.start.value, value.end.value, presets)}`;
+ return `${getPresetTitle(value.start.value, value.end.value, presets, presetsTranslations)}`;
}
const delimiter = ' — ';
diff --git a/src/components/types/helpers.ts b/src/components/types/helpers.ts
new file mode 100644
index 00000000..f41976e2
--- /dev/null
+++ b/src/components/types/helpers.ts
@@ -0,0 +1,3 @@
+export type ExtractFunctionType = T extends (...args: infer A) => infer R
+ ? (...args: A) => R
+ : never;
diff --git a/src/components/types/index.ts b/src/components/types/index.ts
index 7864682a..73b96f6c 100644
--- a/src/components/types/index.ts
+++ b/src/components/types/index.ts
@@ -2,3 +2,4 @@ export type * from './datePicker';
export type * from './inputs';
export type * from './style';
export type * from './events';
+export type * from './helpers';
diff --git a/src/components/utils/dates.ts b/src/components/utils/dates.ts
index bd9d07c0..ad9c7265 100644
--- a/src/components/utils/dates.ts
+++ b/src/components/utils/dates.ts
@@ -54,6 +54,6 @@ export function mergeDateTime(date: DateTime, time: DateTime) {
.set('seconds', time.second());
}
-export function formatDateTime(date: DateTime, format: string, timezone: string) {
- return dateTime({input: date, timeZone: timezone}).format(format);
+export function formatDateTime(date: DateTime, format: string, timezone: string, lang: string) {
+ return dateTime({input: date, timeZone: timezone, lang}).format(format);
}
diff --git a/src/components/utils/validation/datePicker.ts b/src/components/utils/validation/datePicker.ts
index b61e6953..67c214bc 100644
--- a/src/components/utils/validation/datePicker.ts
+++ b/src/components/utils/validation/datePicker.ts
@@ -1,5 +1,7 @@
import type {DateTime} from '@gravity-ui/date-utils';
+import type {ExtractFunctionType} from '../../types';
+
import {i18n} from './i18n';
export interface ValidationResult {
@@ -14,6 +16,7 @@ export function getValidationResult(
isDateUnavailable: ((v: DateTime) => boolean) | undefined,
timeZone: string,
valueTitle = 'Value',
+ t: ExtractFunctionType = i18n,
): ValidationResult {
const rangeOverflow = value && maxValue && maxValue.isBefore(value);
const rangeUnderflow = value && minValue && value.isBefore(minValue);
@@ -24,7 +27,7 @@ export function getValidationResult(
if (isInvalid) {
if (rangeUnderflow && minValue) {
errors.push(
- i18n('Value must be {minValue} or later.', {
+ t('Value must be {minValue} or later.', {
minValue: minValue.timeZone(timeZone).format(),
value: valueTitle,
}),
@@ -33,7 +36,7 @@ export function getValidationResult(
if (rangeOverflow && maxValue) {
errors.push(
- i18n('Value must be {maxValue} or earlier.', {
+ t('Value must be {maxValue} or earlier.', {
maxValue: maxValue.timeZone(timeZone).format(),
value: valueTitle,
}),
@@ -41,7 +44,7 @@ export function getValidationResult(
}
if (isUnavailable) {
- errors.push(i18n('Selected date unavailable.'));
+ errors.push(t('Selected date unavailable.'));
}
}
diff --git a/src/components/utils/validation/i18n/en.json b/src/components/utils/validation/i18n/en.json
index 4b89a303..770c5d88 100644
--- a/src/components/utils/validation/i18n/en.json
+++ b/src/components/utils/validation/i18n/en.json
@@ -2,5 +2,10 @@
"Value must be {minValue} or later.": "{{value}} must be {{minValue}} or later.",
"Value must be {maxValue} or earlier.": "{{value}} must be {{maxValue}} or earlier.",
"Selected date unavailable.": "Selected date unavailable.",
- "Value is required.": "{{value}} is required."
+ "Value is required.": "{{value}} is required.",
+ "\"From\"": "\"From\"",
+ "\"To\"": "\"To\"",
+ "\"From\" is required.": "\"From\" is required.",
+ "\"To\" is required.": "\"To\" is required.",
+ "\"From\" can't be after \"To\".": "\"From\" can't be after \"To\"."
}
diff --git a/src/components/utils/validation/i18n/ru.json b/src/components/utils/validation/i18n/ru.json
index 0b922100..49f11eee 100644
--- a/src/components/utils/validation/i18n/ru.json
+++ b/src/components/utils/validation/i18n/ru.json
@@ -2,5 +2,11 @@
"Value must be {minValue} or later.": "{{value}} должно ровняться {{minValue}} или быть позже.",
"Value must be {maxValue} or earlier.": "{{value}} должно ровняться {{maxValue}} или быть раньше.",
"Selected date unavailable.": "Выбранная дата недоступна.",
- "Value is required.": "{{value}} обязательно."
+ "Value is required.": "{{value}} обязательно.",
+ "\"From\"": "«От»",
+ "\"To\"": "«До»",
+ "\"From\" is required.": "«От» обязательно.",
+ "\"To\"": "«До»",
+ "\"To\" is required.": "«До» обязательно.",
+ "\"From\" can't be after \"To\".": "«От» не может быть позже чем «До»."
}
diff --git a/src/components/utils/validation/relativeRangeDatePicker.ts b/src/components/utils/validation/relativeRangeDatePicker.ts
new file mode 100644
index 00000000..c13a06ce
--- /dev/null
+++ b/src/components/utils/validation/relativeRangeDatePicker.ts
@@ -0,0 +1,67 @@
+import {dateTimeParse} from '@gravity-ui/date-utils';
+import type {DateTime} from '@gravity-ui/date-utils';
+
+import type {RelativeRangeDatePickerValue} from '../../RelativeRangeDatePicker';
+import type {ExtractFunctionType} from '../../types';
+
+import {getValidationResult} from './datePicker';
+import {i18n} from './i18n';
+
+export function getRangeValidationResult(
+ value: RelativeRangeDatePickerValue | null,
+ allowNullableValues: boolean | undefined,
+ minValue: DateTime | undefined,
+ maxValue: DateTime | undefined,
+ isDateUnavailable: ((v: DateTime, endpoint: 'start' | 'end') => boolean) | undefined,
+ timeZone: string,
+ t: ExtractFunctionType = i18n,
+) {
+ if (!value) {
+ return {isInvalid: false, errors: []};
+ }
+
+ const startDate = value.start ? dateTimeParse(value.start.value, {timeZone}) : null;
+ const endDate = value.end ? dateTimeParse(value.end.value, {timeZone, roundUp: true}) : null;
+
+ const startValidationResult = getValidationResult(
+ startDate,
+ minValue,
+ maxValue,
+ isDateUnavailable ? (date) => isDateUnavailable(date, 'start') : undefined,
+ timeZone,
+ t('"From"'),
+ t,
+ );
+
+ if (!startDate && !allowNullableValues) {
+ startValidationResult.isInvalid = true;
+ startValidationResult.errors.push(t('"From" is required.'));
+ }
+
+ const endValidationResult = getValidationResult(
+ endDate,
+ minValue,
+ maxValue,
+ isDateUnavailable ? (date) => isDateUnavailable(date, 'end') : undefined,
+ timeZone,
+ t('"To"'),
+ t,
+ );
+
+ if (!endDate && !allowNullableValues) {
+ endValidationResult.isInvalid = true;
+ endValidationResult.errors.push(t('"To" is required.'));
+ }
+
+ if (startDate && endDate && endDate.isBefore(startDate)) {
+ startValidationResult.isInvalid = true;
+ startValidationResult.errors.push(t(`"From" can't be after "To".`));
+ }
+
+ return {
+ isInvalid: startValidationResult.isInvalid || endValidationResult.isInvalid,
+ startValidationResult,
+ endValidationResult,
+ errors: startValidationResult.errors.concat(endValidationResult.errors),
+ };
+}
diff --git a/src/demo/DocsDecorator/DocsDecorator.tsx b/src/demo/DocsDecorator/DocsDecorator.tsx
index 46520dc6..e6737562 100644
--- a/src/demo/DocsDecorator/DocsDecorator.tsx
+++ b/src/demo/DocsDecorator/DocsDecorator.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import {MobileProvider, ThemeProvider, getThemeType} from '@gravity-ui/uikit';
+import {getThemeType} from '@gravity-ui/uikit';
import {DocsContainer} from '@storybook/addon-docs/blocks';
import type {DocsContainerProps} from '@storybook/addon-docs/blocks';
@@ -16,12 +16,11 @@ const b = block('docs-decorator');
export function DocsDecorator({children, context}: DocsDecoratorProps) {
const storyContext = context.getStoryContext(context.storyById());
const theme = storyContext.globals.theme;
+
return (
-
- {children}
-
+ {children}
);