From 0e94680baba186cc959112d0c996b47f4a008194 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Sun, 7 Sep 2025 23:34:52 +0200 Subject: [PATCH 1/2] feat: use lang from uikit provider --- .storybook/decorators/withLang.tsx | 24 --- .storybook/preview.tsx | 36 ++-- commitlint.config.js | 2 +- eslint.config.mjs | 21 +- package-lock.json | 179 +++++++--------- package.json | 15 +- src/components/CalendarView/CalendarView.tsx | 15 +- .../hooks/useCalendarCellProps.ts | 23 ++- .../hooks/useCalendarGridProps.ts | 4 +- .../CalendarView/hooks/useCalendarProps.ts | 22 +- src/components/DateField/utils.ts | 58 +++--- .../DatePicker/hooks/useDatePickerProps.ts | 4 +- .../RangeDateField/RangeDateField.tsx | 2 +- .../RangeDateSelection/RangeDateSelection.tsx | 6 +- .../SelectionControl/SelectionControl.tsx | 8 +- .../hooks/useRelativeDatePickerProps.ts | 6 +- .../components/Control/Control.tsx | 22 +- .../components/PickerDialog/PickerDoc.tsx | 30 +-- .../components/PickerDialog/PickerForm.tsx | 13 +- .../useRelativeRangeDatePickerDialogState.tsx | 6 +- .../components/Presets/Presets.tsx | 6 +- .../components/Presets/defaultPresets.ts | 193 ------------------ .../components/Presets/defaultPresets.tsx | 152 ++++++++++++++ .../components/Presets/utils.ts | 17 +- .../components/Zones/Zones.tsx | 11 +- .../hooks/useRelativeRangeDatePickerState.ts | 64 +----- .../RelativeRangeDatePicker/i18n/en.json | 3 - .../RelativeRangeDatePicker/i18n/ru.json | 3 - .../RelativeRangeDatePicker/utils.ts | 15 +- src/components/types/helpers.ts | 3 + src/components/types/index.ts | 1 + src/components/utils/dates.ts | 4 +- src/components/utils/validation/datePicker.ts | 9 +- src/components/utils/validation/i18n/en.json | 7 +- src/components/utils/validation/i18n/ru.json | 8 +- .../validation/relativeRangeDatePicker.ts | 67 ++++++ src/demo/DocsDecorator/DocsDecorator.tsx | 7 +- 37 files changed, 530 insertions(+), 536 deletions(-) delete mode 100644 .storybook/decorators/withLang.tsx delete mode 100644 src/components/RelativeRangeDatePicker/components/Presets/defaultPresets.ts create mode 100644 src/components/RelativeRangeDatePicker/components/Presets/defaultPresets.tsx create mode 100644 src/components/types/helpers.ts create mode 100644 src/components/utils/validation/relativeRangeDatePicker.ts diff --git a/.storybook/decorators/withLang.tsx b/.storybook/decorators/withLang.tsx deleted file mode 100644 index 84d1252c..00000000 --- a/.storybook/decorators/withLang.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import {settings} from '@gravity-ui/date-utils'; -import type {Lang} from '@gravity-ui/uikit'; -import {configure} from '@gravity-ui/uikit'; -import type {Decorator} from '@storybook/react-webpack5'; - -export const WithLang: Decorator = (Story, context) => { - const lang = context.globals.lang; - const [key, forceRender] = React.useState(0); - - React.useEffect(() => { - configure({ - lang: lang as Lang, - }); - - settings.loadLocale(lang).then(() => { - settings.setLocale(lang); - forceRender((c) => c + 1); - }); - }, [lang]); - - return ; -}; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 24f63272..97c607e5 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -5,31 +5,26 @@ import '@gravity-ui/uikit/styles/styles.css'; import React from 'react'; -import { - Lang, - MobileProvider, - ThemeProvider, - ToasterComponent, - ToasterProvider, - configure, -} from '@gravity-ui/uikit'; +import {settings} from '@gravity-ui/date-utils'; +import {MobileProvider, ThemeProvider, ToasterComponent, ToasterProvider} from '@gravity-ui/uikit'; import {toaster} from '@gravity-ui/uikit/toaster-singleton'; import type {Decorator, Preview} from '@storybook/react-webpack5'; import {MINIMAL_VIEWPORTS} from 'storybook/viewport'; import {DocsDecorator} from '../src/demo/DocsDecorator/DocsDecorator'; -import {WithLang} from './decorators/withLang'; import {themes} from './theme'; -configure({ - lang: Lang.En, -}); +settings.loadLocale('ru'); const WithContextProvider: Decorator = (Story, context) => { return ( - + @@ -46,9 +41,6 @@ const preview: Preview = { docs: { theme: themes.light, container: DocsDecorator, - canvas: { - className: 'g-storybook-docs-decorator__canvas', - }, codePanel: true, }, jsx: {showFunctions: false}, // Do not show functions in sources @@ -61,10 +53,9 @@ const preview: Preview = { }, }, }, - decorators: [WithLang, WithContextProvider], + decorators: [WithContextProvider], globalTypes: { theme: { - defaultValue: 'light', toolbar: { title: 'Theme', icon: 'mirror', @@ -78,7 +69,6 @@ const preview: Preview = { }, }, lang: { - defaultValue: 'en', toolbar: { title: 'Language', icon: 'globe', @@ -90,7 +80,6 @@ const preview: Preview = { }, }, direction: { - defaultValue: 'ltr', toolbar: { title: 'Direction', icon: 'menu', @@ -102,7 +91,6 @@ const preview: Preview = { }, }, platform: { - defaultValue: 'desktop', toolbar: { title: 'Platform', items: [ @@ -113,6 +101,12 @@ const preview: Preview = { }, }, }, + initialGlobals: { + theme: 'light', + lang: 'en', + direction: 'ltr', + platform: 'desktop', + }, }; export default preview; diff --git a/commitlint.config.js b/commitlint.config.js index 381039af..69fdd34f 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,4 +1,4 @@ -/** @type import('@commitlint/types').UserConfig */ +/** @type {import('@commitlint/types').UserConfig} */ const config = { extends: ['@commitlint/config-conventional'], }; diff --git a/eslint.config.mjs b/eslint.config.mjs index 3e02b53d..f0564ebf 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,20 +3,23 @@ import a11yConfig from '@gravity-ui/eslint-config/a11y'; import clientConfig from '@gravity-ui/eslint-config/client'; import importOrderConfig from '@gravity-ui/eslint-config/import-order'; import prettierConfig from '@gravity-ui/eslint-config/prettier'; -import {defineConfig} from 'eslint/config'; +import {defineConfig, globalIgnores} from 'eslint/config'; import reactCompiler from 'eslint-plugin-react-compiler'; import storybookPlugin from 'eslint-plugin-storybook'; import testingLibraryPlugin from 'eslint-plugin-testing-library'; import globals from 'globals'; export default defineConfig([ - ...baseConfig, - ...clientConfig, - ...prettierConfig, - ...importOrderConfig, - ...a11yConfig, - ...storybookPlugin.configs['flat/recommended'], - {...reactCompiler.configs.recommended, rules: {'react-compiler/react-compiler': 'warn'}}, + baseConfig, + clientConfig, + prettierConfig, + importOrderConfig, + a11yConfig, + storybookPlugin.configs['flat/recommended'], + { + extends: [reactCompiler.configs.recommended], + rules: {'react-compiler/react-compiler': 'warn'}, + }, { rules: { complexity: 'off', @@ -56,5 +59,5 @@ export default defineConfig([ }, {files: ['**/__stories__/**/*.[jt]s?(x)'], rules: {'no-console': 'off'}}, {files: ['**/*.js', '!src/**/*'], languageOptions: {globals: {...globals.node}}}, - {ignores: ['dist', 'storybook-static', '!/.storybook']}, + globalIgnores(['dist', 'storybook-static', '!/.storybook']), ]); diff --git a/package-lock.json b/package-lock.json index 88893fef..756b77c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", - "@gravity-ui/date-utils": "^2.5.3", "@gravity-ui/icons": "^2.2.0", "tslib": "^2.6.2" }, @@ -69,9 +68,16 @@ "typescript": "^5.8.3" }, "peerDependencies": { - "@gravity-ui/uikit": "^7.0.0", + "@gravity-ui/date-utils": "^2.5.3", + "@gravity-ui/uikit": "^7.21.0", + "@types/react": ">=17.0.0", "react": ">=17.0.0", "react-dom": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@type/react": { + "optional": true + } } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2044,12 +2050,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -3412,6 +3416,7 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/@gravity-ui/date-utils/-/date-utils-2.5.3.tgz", "integrity": "sha512-WetzttqlW454yGsh/LBo0AxuqIFT0erENCMYAfemhu6qLiuqy35NdDGP6VA4iDkWiSPFuQ1sRoVU8tC+OwiPEg==", + "peer": true, "dependencies": { "dayjs": "1.11.10", "lodash": "^4.17.0" @@ -3559,9 +3564,9 @@ "dev": true }, "node_modules/@gravity-ui/uikit": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-7.16.2.tgz", - "integrity": "sha512-WxeDx7MKMHEjx4qjpD+W/+Yxk8/Zsqrc05gd5RGX3M2j+2tGMJzib+/eIdu1uEeuWnViTUtI4jQrVPPp2zSXNA==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-7.21.0.tgz", + "integrity": "sha512-AbXkdz8irIdW2nXyVsgOFNaUQb07CcvOgCEW3QNfS4D0+wcWk2PHoajmDMgy/mlTeN5ykeUhgSMQKa25kfKcbw==", "license": "MIT", "peer": true, "dependencies": { @@ -3569,16 +3574,17 @@ "@floating-ui/react": "^0.27.12", "@gravity-ui/i18n": "^1.8.0", "@gravity-ui/icons": "^2.13.0", + "@hello-pangea/dnd": "^18.0.1", "@tanstack/react-virtual": "^3.13.9", "blueimp-md5": "^2.19.0", "lodash": "^4.17.21", "rc-slider": "^11.1.8", - "react-beautiful-dnd": "^13.1.1", "react-transition-group": "^4.4.5", "react-virtualized-auto-sizer": "^1.0.26", "react-window": "^1.8.11", "tabbable": "^6.2.0", - "tslib": "^2.8.1" + "tslib": "^2.8.1", + "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -3728,6 +3734,24 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -8286,16 +8310,6 @@ "@types/node": "*" } }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "peer": true, - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -8612,12 +8626,14 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "devOptional": true }, "node_modules/@types/react": { "version": "18.3.23", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -8634,18 +8650,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/react-redux": { - "version": "7.1.33", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", - "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", - "peer": true, - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -8674,6 +8678,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT", + "peer": true + }, "node_modules/@types/wait-on": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.4.tgz", @@ -11620,6 +11631,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", "peer": true, "dependencies": { "tiny-invariant": "^1.0.6" @@ -11881,7 +11893,8 @@ "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "peer": true }, "node_modules/debug": { "version": "4.4.1", @@ -15198,15 +15211,6 @@ "hermes-estree": "0.25.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "peer": true, - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -20288,9 +20292,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", - "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "dev": true, "license": "MIT" }, @@ -21573,6 +21577,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT", "peer": true }, "node_modules/randombytes": { @@ -21646,25 +21651,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-docgen": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.1.tgz", @@ -21744,36 +21730,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", "peer": true, "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" }, "peerDependenciesMeta": { - "react-dom": { + "@types/react": { "optional": true }, - "react-native": { + "redux": { "optional": true } } }, - "node_modules/react-redux/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "peer": true - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -21899,13 +21878,11 @@ } }, "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.9.2" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.9", @@ -21950,11 +21927,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -24668,13 +24640,14 @@ "punycode": "^2.1.0" } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", - "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", "peer": true, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/util-deprecate": { diff --git a/package.json b/package.json index df45218e..46fab8e2 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "scripts": { "prepare": "husky", "lint": "run-p lint:*", - "lint:js": "eslint --ext .js,.jsx,.ts,.tsx --report-unused-disable-directives .", + "lint:js": "eslint --ext .js,.jsx,.mjs,.cjs,.ts,.tsx --report-unused-disable-directives .", "lint:styles": "stylelint --report-needless-disables 'src/**/*.scss'", "lint:other": "npm run prettier -- --check", "prettier": "prettier '**/*.{md,yaml,yml,json}'", @@ -53,7 +53,6 @@ }, "dependencies": { "@bem-react/classname": "^1.6.0", - "@gravity-ui/date-utils": "^2.5.3", "@gravity-ui/icons": "^2.2.0", "tslib": "^2.6.2" }, @@ -112,18 +111,22 @@ "typescript": "^5.8.3" }, "peerDependencies": { - "@gravity-ui/uikit": "^7.0.0", + "@gravity-ui/uikit": "^7.21.0", + "@gravity-ui/date-utils": "^2.5.3", + "@types/react": ">=17.0.0", "react": ">=17.0.0", "react-dom": ">=17.0.0" }, - "overrides": { - "nwsapi": "2.2.2" + "peerDependenciesMeta": { + "@type/react": { + "optional": true + } }, "nano-staged": { "*.{scss}": [ "stylelint --fix --quiet --report-needless-disables" ], - "*.{js,jsx,ts,tsx}": [ + "*.{js,jsx,mjs,cjs,ts,tsx}": [ "eslint --fix --quiet --report-unused-disable-directives" ], "*.{md,json,yml,yaml}": [ diff --git a/src/components/CalendarView/CalendarView.tsx b/src/components/CalendarView/CalendarView.tsx index a41d6338..2ebc0e8f 100644 --- a/src/components/CalendarView/CalendarView.tsx +++ b/src/components/CalendarView/CalendarView.tsx @@ -4,7 +4,7 @@ import React from 'react'; import type {DateTime} from '@gravity-ui/date-utils'; import {ChevronLeft, ChevronRight} from '@gravity-ui/icons'; -import {ArrowToggle, Button} from '@gravity-ui/uikit'; +import {ArrowToggle, Button, useLang} from '@gravity-ui/uikit'; import {block} from '../../utils/cn'; import type {AccessibilityProps, DomProps, FocusEvents, StyleProps} from '../types'; @@ -161,6 +161,7 @@ interface WeekdaysProps { } function Weekdays({state}: WeekdaysProps) { const weekdays = getWeekDays(state); + const {lang} = useLang(); return (
@@ -170,9 +171,9 @@ function Weekdays({state}: WeekdaysProps) { key={date.day()} className={b('weekday', {weekend: state.isWeekend(date)})} role="columnheader" - aria-label={formatDateTime(date, 'dddd', state.timeZone)} + aria-label={formatDateTime(date, 'dddd', state.timeZone, lang)} > - {formatDateTime(date, 'dd', state.timeZone)} + {formatDateTime(date, 'dd', state.timeZone, lang)}
); })} @@ -187,13 +188,19 @@ function CalendarGridCells({state}: CalendarGridProps) { const rowsInPeriod = state.mode === 'days' ? 6 : 4; const columnsInRow = state.mode === 'days' ? 7 : 3 + (state.mode === 'quarters' ? 1 : 0); const days = getDaysInPeriod(state); + const {lang} = useLang(); return (
{[...new Array(rowsInPeriod).keys()].map((rowIndex) => (
{state.mode === 'quarters' ? ( - {formatDateTime(days[rowIndex * columnsInRow], 'YYYY', state.timeZone)} + {formatDateTime( + days[rowIndex * columnsInRow], + 'YYYY', + state.timeZone, + lang, + )} ) : null} {days diff --git a/src/components/CalendarView/hooks/useCalendarCellProps.ts b/src/components/CalendarView/hooks/useCalendarCellProps.ts index 64ebf4ee..e5cf6050 100644 --- a/src/components/CalendarView/hooks/useCalendarCellProps.ts +++ b/src/components/CalendarView/hooks/useCalendarCellProps.ts @@ -1,6 +1,7 @@ import React from 'react'; import type {DateTime} from '@gravity-ui/date-utils'; +import {useLang} from '@gravity-ui/uikit'; import {formatDateTime} from '../../utils/dates'; @@ -9,6 +10,8 @@ import type {CalendarState, RangeCalendarState} from './types'; export function useCalendarCellProps(date: DateTime, state: CalendarState | RangeCalendarState) { const ref = React.useRef(null); + const {lang} = useLang(); + const isFocused = state.isCellFocused(date); React.useEffect(() => { if (isFocused) { @@ -32,7 +35,7 @@ export function useCalendarCellProps(date: DateTime, state: CalendarState | Rang const isCurrent = state.isCurrent(date); const isWeekend = state.isWeekend(date); - const label = getDateLabel(date, state); + const label = getDateLabel(date, state, lang); const cellProps: React.HTMLAttributes = { role: 'gridcell', @@ -66,13 +69,13 @@ export function useCalendarCellProps(date: DateTime, state: CalendarState | Rang }, }; - let formattedDate = formatDateTime(date, 'D', state.timeZone); + let formattedDate = formatDateTime(date, 'D', state.timeZone, lang); if (state.mode === 'months') { - formattedDate = formatDateTime(date, 'MMM', state.timeZone); + formattedDate = formatDateTime(date, 'MMM', state.timeZone, lang); } else if (state.mode === 'quarters') { - formattedDate = formatDateTime(date, '[Q]Q', state.timeZone); + formattedDate = formatDateTime(date, '[Q]Q', state.timeZone, lang); } else if (state.mode === 'years') { - formattedDate = formatDateTime(date, 'YYYY', state.timeZone); + formattedDate = formatDateTime(date, 'YYYY', state.timeZone, lang); } return { @@ -91,19 +94,19 @@ export function useCalendarCellProps(date: DateTime, state: CalendarState | Rang }; } -function getDateLabel(date: DateTime, state: CalendarState | RangeCalendarState) { +function getDateLabel(date: DateTime, state: CalendarState | RangeCalendarState, lang: string) { switch (state.mode) { case 'days': { - return `${formatDateTime(date, 'dddd', state.timeZone)}, ${formatDateTime(date, 'LL', state.timeZone)}`; + return `${formatDateTime(date, 'dddd', state.timeZone, lang)}, ${formatDateTime(date, 'LL', state.timeZone, lang)}`; } case 'months': { - return `${formatDateTime(date, 'MMMM YYYY', state.timeZone)}`; + return `${formatDateTime(date, 'MMMM YYYY', state.timeZone, lang)}`; } case 'quarters': { - return `${formatDateTime(date, '[Q]Q YYYY', state.timeZone)}`; + return `${formatDateTime(date, '[Q]Q YYYY', state.timeZone, lang)}`; } case 'years': { - return `${formatDateTime(date, 'YYYY', state.timeZone)}`; + return `${formatDateTime(date, 'YYYY', state.timeZone, lang)}`; } default: return ''; diff --git a/src/components/CalendarView/hooks/useCalendarGridProps.ts b/src/components/CalendarView/hooks/useCalendarGridProps.ts index 06494420..4faba3a8 100644 --- a/src/components/CalendarView/hooks/useCalendarGridProps.ts +++ b/src/components/CalendarView/hooks/useCalendarGridProps.ts @@ -1,4 +1,4 @@ -import {useFocusWithin} from '@gravity-ui/uikit'; +import {useFocusWithin, useLang} from '@gravity-ui/uikit'; import {formatDateTime} from '../../utils/dates'; @@ -11,6 +11,7 @@ export function useCalendarGridProps(state: CalendarState | RangeCalendarState) }, }); + const {lang} = useLang(); const gridProps: React.HTMLAttributes = { role: 'grid', 'aria-label': @@ -20,6 +21,7 @@ export function useCalendarGridProps(state: CalendarState | RangeCalendarState) state.focusedDate, state.mode === 'days' ? 'MMMM YYYY' : 'YYYY', state.timeZone, + lang, ), 'aria-disabled': state.disabled ? 'true' : undefined, 'aria-readonly': state.readOnly ? 'true' : undefined, diff --git a/src/components/CalendarView/hooks/useCalendarProps.ts b/src/components/CalendarView/hooks/useCalendarProps.ts index 3d8c7106..09b487af 100644 --- a/src/components/CalendarView/hooks/useCalendarProps.ts +++ b/src/components/CalendarView/hooks/useCalendarProps.ts @@ -1,6 +1,6 @@ import React from 'react'; -import {useFocusWithin} from '@gravity-ui/uikit'; +import {useFocusWithin, useLang} from '@gravity-ui/uikit'; import type {ButtonButtonProps} from '@gravity-ui/uikit'; import type {CalendarProps} from '../../Calendar/Calendar'; @@ -12,6 +12,7 @@ import type {CalendarLayout, CalendarState, RangeCalendarState} from './types'; const buttonDisabledClassName = 'yc-button_disabled g-button_disabled'; export function useCalendarProps(props: CalendarProps, state: CalendarState | RangeCalendarState) { + const {lang} = useLang(); const title = state.mode === 'years' || state.mode === 'quarters' ? `${state.startDate.year()} — ${state.endDate.year()}` @@ -19,6 +20,7 @@ export function useCalendarProps(props: CalendarProps, state: CalendarState | Ra state.focusedDate, state.mode === 'days' ? 'MMMM YYYY' : 'YYYY', state.timeZone, + lang, ); const {focusWithinProps} = useFocusWithin({ @@ -55,7 +57,7 @@ export function useCalendarProps(props: CalendarProps, state: CalendarState | Ra } }, 'aria-disabled': modeDisabled ? 'true' : undefined, - 'aria-description': getAriaDescriptionForModeButton(state.mode, state.availableModes), + 'aria-description': useAriaDescriptionForModeButton(state.mode, state.availableModes), 'aria-live': 'polite', children: title, }; @@ -70,6 +72,8 @@ export function useCalendarProps(props: CalendarProps, state: CalendarState | Ra } }); + const {t} = i18n.useTranslation(); + const previousButtonProps: ButtonButtonProps = { disabled: state.disabled, // FIXME: do not use button class name @@ -89,7 +93,7 @@ export function useCalendarProps(props: CalendarProps, state: CalendarState | Ra : () => { previousFocused.current = false; }, - 'aria-label': i18n('Previous'), + 'aria-label': t('Previous'), 'aria-disabled': previousDisabled ? 'true' : undefined, }; @@ -122,7 +126,7 @@ export function useCalendarProps(props: CalendarProps, state: CalendarState | Ra : () => { nextFocused.current = false; }, - 'aria-label': i18n('Next'), + 'aria-label': t('Next'), 'aria-disabled': previousDisabled ? 'true' : undefined, }; @@ -134,7 +138,9 @@ export function useCalendarProps(props: CalendarProps, state: CalendarState | Ra }; } -function getAriaDescriptionForModeButton(mode: CalendarLayout, availableModes: CalendarLayout[]) { +function useAriaDescriptionForModeButton(mode: CalendarLayout, availableModes: CalendarLayout[]) { + const {t} = i18n.useTranslation(); + const nextModeIndex = availableModes.indexOf(mode) + 1; const isModeLast = nextModeIndex === availableModes.length; if (isModeLast) { @@ -143,9 +149,9 @@ function getAriaDescriptionForModeButton(mode: CalendarLayout, availableModes: C const ariaLabelMap: Record = { days: '', - months: i18n('Switch to months view'), - quarters: i18n('Switch to quarters view'), - years: i18n('Switch to years view'), + months: t('Switch to months view'), + quarters: t('Switch to quarters view'), + years: t('Switch to years view'), }; const nextMode = availableModes[nextModeIndex]; return ariaLabelMap[nextMode]; diff --git a/src/components/DateField/utils.ts b/src/components/DateField/utils.ts index ddebcfa0..d9c42375 100644 --- a/src/components/DateField/utils.ts +++ b/src/components/DateField/utils.ts @@ -2,7 +2,11 @@ import React from 'react'; import {dateTime, expandFormat} from '@gravity-ui/date-utils'; import type {DateTime} from '@gravity-ui/date-utils'; +import dayjs from '@gravity-ui/date-utils/build/dayjs'; +import type {LongDateFormat} from '@gravity-ui/date-utils/build/settings/types'; +import {useLang} from '@gravity-ui/uikit'; +import type {ExtractFunctionType} from '../types'; import {mergeDateTime} from '../utils/dates'; import {i18n} from './i18n'; @@ -361,44 +365,43 @@ function doesSectionHaveLeadingZeros( function getSectionPlaceholder( sectionConfig: Pick, currentTokenValue: string, + t: TranslateFunction, ) { switch (sectionConfig.type) { case 'year': { - return i18n('year_placeholder').repeat(dateTime().format(currentTokenValue).length); + return t('year_placeholder').repeat(dateTime().format(currentTokenValue).length); } case 'quarter': { - return i18n('quarter_placeholder'); + return t('quarter_placeholder'); } case 'month': { - return i18n('month_placeholder').repeat(sectionConfig.contentType === 'letter' ? 4 : 2); + return t('month_placeholder').repeat(sectionConfig.contentType === 'letter' ? 4 : 2); } case 'day': { - return i18n('day_placeholder').repeat(2); + return t('day_placeholder').repeat(2); } case 'weekday': { - return i18n('weekday_placeholder').repeat( - sectionConfig.contentType === 'letter' ? 4 : 2, - ); + return t('weekday_placeholder').repeat(sectionConfig.contentType === 'letter' ? 4 : 2); } case 'hour': { - return i18n('hour_placeholder').repeat(2); + return t('hour_placeholder').repeat(2); } case 'minute': { - return i18n('minute_placeholder').repeat(2); + return t('minute_placeholder').repeat(2); } case 'second': { - return i18n('second_placeholder').repeat(2); + return t('second_placeholder').repeat(2); } case 'dayPeriod': { - return i18n('dayPeriod_placeholder'); + return t('dayPeriod_placeholder'); } default: { @@ -407,10 +410,12 @@ function getSectionPlaceholder( } } -export function splitFormatIntoSections(format: string) { +type TranslateFunction = ExtractFunctionType; +export function splitFormatIntoSections(format: string, t: TranslateFunction = i18n, lang = 'en') { const sections: DateFieldSectionWithoutPosition[] = []; + const localeFormats = dayjs.Ls[lang].formats as LongDateFormat; - const expandedFormat = expandFormat(format); + const expandedFormat = expandFormat(format, localeFormats); let currentTokenValue = ''; let isSeparator = false; @@ -432,7 +437,7 @@ export function splitFormatIntoSections(format: string) { currentTokenValue += char; } else { if (!isSeparator) { - addFormatSection(sections, currentTokenValue); + addFormatSection(sections, currentTokenValue, t); currentTokenValue = ''; } isSeparator = true; @@ -447,14 +452,18 @@ export function splitFormatIntoSections(format: string) { if (isSeparator) { addLiteralSection(sections, currentTokenValue); } else { - addFormatSection(sections, currentTokenValue); + addFormatSection(sections, currentTokenValue, t); } } return sections; } -function addFormatSection(sections: DateFieldSectionWithoutPosition[], token: string) { +function addFormatSection( + sections: DateFieldSectionWithoutPosition[], + token: string, + t: TranslateFunction, +) { if (!token) { return; } @@ -470,7 +479,7 @@ function addFormatSection(sections: DateFieldSectionWithoutPosition[], token: st sections.push({ ...sectionConfig, format: token, - placeholder: getSectionPlaceholder(sectionConfig, token), + placeholder: getSectionPlaceholder(sectionConfig, token, t), options: getSectionOptions(sectionConfig, token), hasLeadingZeros, }); @@ -676,13 +685,14 @@ export function isAllSegmentsValid( } export function useFormatSections(format: string) { - const usedFormat = format; - const [sections, setSections] = React.useState(() => splitFormatIntoSections(usedFormat)); - - const [previousFormat, setFormat] = React.useState(usedFormat); - if (usedFormat !== previousFormat) { - setFormat(usedFormat); - setSections(splitFormatIntoSections(usedFormat)); + const {t} = i18n.useTranslation(); + const {lang} = useLang(); + const [sections, setSections] = React.useState(() => splitFormatIntoSections(format, t)); + + const [previous, setFormat] = React.useState({format, lang}); + if (format !== previous.format || lang !== previous.lang) { + setFormat({format, lang}); + setSections(splitFormatIntoSections(format, t, lang)); } return sections; diff --git a/src/components/DatePicker/hooks/useDatePickerProps.ts b/src/components/DatePicker/hooks/useDatePickerProps.ts index eebcd1bc..2d1495d8 100644 --- a/src/components/DatePicker/hooks/useDatePickerProps.ts +++ b/src/components/DatePicker/hooks/useDatePickerProps.ts @@ -88,6 +88,8 @@ export function useDatePickerProps>( const onlyTime = state.formatInfo.hasTime && !state.formatInfo.hasDate; const calendarModes = getCalendarModes(state.formatInfo); + const {t} = i18n.useTranslation(); + return { groupProps: { ref: groupRef, @@ -118,7 +120,7 @@ export function useDatePickerProps>( ref: calendarButtonRef, size: getButtonSizeForInput(props.size), disabled: state.disabled, - 'aria-label': i18n('Calendar'), + 'aria-label': t('Calendar'), 'aria-haspopup': 'dialog', 'aria-expanded': state.isOpen, view: 'flat-secondary', diff --git a/src/components/RangeDateField/RangeDateField.tsx b/src/components/RangeDateField/RangeDateField.tsx index 63fc851e..f2f03df5 100644 --- a/src/components/RangeDateField/RangeDateField.tsx +++ b/src/components/RangeDateField/RangeDateField.tsx @@ -21,7 +21,7 @@ export type RangeDateFieldProps = DateFieldProps> & { /** * Delimiter separating the start and end parts of the range. * @default ' — ' - * */ + */ delimiter?: string; }; diff --git a/src/components/RangeDateSelection/RangeDateSelection.tsx b/src/components/RangeDateSelection/RangeDateSelection.tsx index 9774bbf5..6fd234ae 100644 --- a/src/components/RangeDateSelection/RangeDateSelection.tsx +++ b/src/components/RangeDateSelection/RangeDateSelection.tsx @@ -65,6 +65,8 @@ export function RangeDateSelection(props: RangeDateSelectionProps) { let id = React.useId(); id = props.id ?? id; + const {t} = i18n.useTranslation(); + return (
@@ -111,7 +113,7 @@ export function RangeDateSelection(props: RangeDateSelectionProps) { state.scale(1.5); state.endDragging(); }} - aria-label={i18n('Increase range')} + aria-label={t('Increase range')} > diff --git a/src/components/RangeDateSelection/components/SelectionControl/SelectionControl.tsx b/src/components/RangeDateSelection/components/SelectionControl/SelectionControl.tsx index 78868cef..7b86214e 100644 --- a/src/components/RangeDateSelection/components/SelectionControl/SelectionControl.tsx +++ b/src/components/RangeDateSelection/components/SelectionControl/SelectionControl.tsx @@ -124,6 +124,8 @@ export function SelectionControl({state, style, className, ...props}: SelectionC let id = React.useId(); id = props.id ?? id; + const {t} = i18n.useTranslation(); + return (
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} @@ -144,7 +146,7 @@ export function SelectionControl({state, style, className, ...props}: SelectionC className={b('slider-input')} type="range" step={1} - aria-label={i18n('Range')} + aria-label={t('Range')} aria-labelledby={[`${id}-range`, props['aria-labelledby']] .filter(Boolean) .join(' ')} @@ -174,7 +176,7 @@ export function SelectionControl({state, style, className, ...props}: SelectionC className={b('slider-input')} type="range" step={1} - aria-label={i18n('Start of range')} + aria-label={t('Start of range')} aria-labelledby={[`${id}-start`, props['aria-labelledby']] .filter(Boolean) .join(' ')} @@ -205,7 +207,7 @@ export function SelectionControl({state, style, className, ...props}: SelectionC className={b('slider-input')} type="range" step={1} - aria-label={i18n('End of range')} + aria-label={t('End of range')} aria-labelledby={[`${id}-end`, props['aria-labelledby']] .filter(Boolean) .join(' ')} diff --git a/src/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.ts b/src/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.ts index 15b23660..72da5ee4 100644 --- a/src/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.ts +++ b/src/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.ts @@ -134,6 +134,8 @@ export function useRelativeDatePickerProps( const groupRef = React.useRef(null); const calendarModes = getCalendarModes(datePickerState.formatInfo); + const {t} = i18n.useTranslation(); + return { groupProps: { ref: groupRef, @@ -166,7 +168,7 @@ export function useRelativeDatePickerProps( view: 'flat-secondary', style: {zIndex: 2, marginInlineEnd: 2}, selected: mode === 'relative', - 'aria-label': i18n('Formula input mode'), + 'aria-label': t('Formula input mode'), onClick: () => { setMode(mode === 'relative' ? 'absolute' : 'relative'); if (mode === 'relative') { @@ -183,7 +185,7 @@ export function useRelativeDatePickerProps( calendarButtonProps: { size: getButtonSizeForInput(props.size), disabled: state.disabled, - 'aria-label': i18n('Calendar'), + 'aria-label': t('Calendar'), 'aria-haspopup': 'dialog', 'aria-expanded': isOpen, view: 'flat-secondary', diff --git a/src/components/RelativeRangeDatePicker/components/Control/Control.tsx b/src/components/RelativeRangeDatePicker/components/Control/Control.tsx index bbd16770..5ddb51f4 100644 --- a/src/components/RelativeRangeDatePicker/components/Control/Control.tsx +++ b/src/components/RelativeRangeDatePicker/components/Control/Control.tsx @@ -1,13 +1,14 @@ import React from 'react'; import {Calendar as CalendarIcon} from '@gravity-ui/icons'; -import {Button, Icon, TextInput} from '@gravity-ui/uikit'; +import {Button, Icon, TextInput, useLang} from '@gravity-ui/uikit'; import {block} from '../../../../utils/cn'; import {getButtonSizeForInput} from '../../../utils/getButtonSizeForInput'; import type {RelativeRangeDatePickerState} from '../../hooks/useRelativeRangeDatePickerState'; import type {RelativeRangeDatePickerProps, RelativeRangeDatePickerTriggerProps} from '../../types'; import {getDefaultTitle} from '../../utils'; +import {i18n as i18nPresets} from '../Presets/i18n'; import {i18n} from './i18n'; @@ -47,6 +48,10 @@ export const Control = React.forwardRef( const {alwaysShowAsAbsolute, presetTabs, getRangeTitle} = props; const format = props.format || 'L'; + const {t} = i18n.useTranslation(); + const {t: presetsTranslations} = i18nPresets.useTranslation(); + const {lang} = useLang(); + const text = React.useMemo( () => typeof getRangeTitle === 'function' @@ -57,8 +62,19 @@ export const Control = React.forwardRef( alwaysShowAsAbsolute, format, presets: presetTabs?.flatMap(({presets}) => presets), + presetsTranslations, + lang, }), - [alwaysShowAsAbsolute, format, getRangeTitle, presetTabs, state.timeZone, state.value], + [ + alwaysShowAsAbsolute, + format, + getRangeTitle, + lang, + presetTabs, + presetsTranslations, + state.timeZone, + state.value, + ], ); const validationState = props.validationState || (state.isInvalid ? 'invalid' : undefined); @@ -122,7 +138,7 @@ export const Control = React.forwardRef( disabled={props.disabled} aria-haspopup="dialog" aria-expanded={open} - aria-label={i18n('Range date picker')} + aria-label={t('Range date picker')} onClick={onClickCalendar} tabIndex={-1} > diff --git a/src/components/RelativeRangeDatePicker/components/PickerDialog/PickerDoc.tsx b/src/components/RelativeRangeDatePicker/components/PickerDialog/PickerDoc.tsx index 168b43d3..77e443c4 100644 --- a/src/components/RelativeRangeDatePicker/components/PickerDialog/PickerDoc.tsx +++ b/src/components/RelativeRangeDatePicker/components/PickerDialog/PickerDoc.tsx @@ -8,6 +8,7 @@ import type {TableColumnConfig} from '@gravity-ui/uikit'; import {block} from '../../../../utils/cn'; import {getButtonSizeForInput} from '../../../utils/getButtonSizeForInput'; +import {PresetTitle} from '../Presets/defaultPresets'; import type {Preset} from '../Presets/defaultPresets'; import {i18n} from '../Presets/i18n'; @@ -17,37 +18,27 @@ const b = block('relative-range-date-picker-doc'); const data: Preset[] = [ { - get title() { - return i18n('Last 5 minutes'); - }, + title: , from: 'now - 5m', to: 'now', }, { - get title() { - return i18n('From start of day'); - }, + title: , from: 'now/d', to: 'now', }, { - get title() { - return i18n('This week'); - }, + title: , from: 'now/w', to: 'now/w', }, { - get title() { - return i18n('From start of week'); - }, + title: , from: 'now/w', to: 'now', }, { - get title() { - return i18n('Previous month'); - }, + title: , from: 'now - 1M/M', to: 'now - 1M/M', }, @@ -60,18 +51,19 @@ interface DocContentProps extends Omit { function DocContent({size, docs, onStartUpdate, onEndUpdate}: DocContentProps) { const isMobile = useMobile(); + const {t} = i18n.useTranslation(); const columns: TableColumnConfig[] = React.useMemo( () => [ { id: 'title', name: () => { - return i18n('Range'); + return t('Range'); }, }, { id: 'from', name: () => { - return i18n('From'); + return t('From'); }, template: (item) => ( ) : 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 (