From 7d1e0fe0785e0ead50bad79892352a842e9b4e1a Mon Sep 17 00:00:00 2001 From: inmake Date: Thu, 16 Apr 2026 13:09:47 +0500 Subject: [PATCH] Update language support and localization across components. Change HTML language attribute to English, add i18next for translations, and refactor feedback forms to utilize localized strings. Remove unused constants and adjust product handling in lead forms. Enhance accessibility and improve code structure. --- index.html | 2 +- package-lock.json | 1187 ++++++++++++++++- package.json | 2 + src/App.tsx | 6 +- src/components/Layout/Feedback.tsx | 20 +- src/components/Layout/Footer.tsx | 66 +- src/components/Layout/LanguageSwitcher.tsx | 39 + src/components/Layout/LocaleSync.tsx | 30 + src/components/modals/FeedbackFormModal.tsx | 27 +- src/components/modals/QuestionFormModal.tsx | 18 +- src/consts/feedback.ts | 9 - src/consts/projectsTags.ts | 9 - src/features/lead-form/LeadForm.tsx | 32 +- src/features/stream-demo/AvailableDemos.tsx | 12 +- src/features/stream-demo/RequestForDemo.tsx | 14 +- src/features/stream-demo/StreamPlayer.tsx | 6 +- src/features/stream-demo/StreamingProject.tsx | 6 +- src/hooks/useLocationSearch.ts | 18 + src/i18n/index.ts | 29 + src/i18n/locales/en.json | 87 ++ src/i18n/locales/ru.json | 87 ++ src/lib/productLabels.ts | 14 + src/lib/urlLang.ts | 15 + src/main.tsx | 1 + src/queries/getCountryCode.ts | 26 + src/queries/keys.ts | 1 + src/types/Product.ts | 8 +- 27 files changed, 1622 insertions(+), 149 deletions(-) create mode 100644 src/components/Layout/LanguageSwitcher.tsx create mode 100644 src/components/Layout/LocaleSync.tsx delete mode 100644 src/consts/feedback.ts delete mode 100644 src/consts/projectsTags.ts create mode 100644 src/hooks/useLocationSearch.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/locales/en.json create mode 100644 src/i18n/locales/ru.json create mode 100644 src/lib/productLabels.ts create mode 100644 src/lib/urlLang.ts create mode 100644 src/queries/getCountryCode.ts diff --git a/index.html b/index.html index f0b304f..c0f20ca 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/package-lock.json b/package-lock.json index e66d565..a5882e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,25 +10,30 @@ "dependencies": { "@tanstack/react-query": "^5.62.7", "framer-motion": "^11.17.0", + "i18next": "^26.0.5", "ky": "^1.4.0", "libphonenumber-js": "^1.11.7", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.53.0", - "react-input-mask": "^2.0.4", + "react-i18next": "^17.0.3", "react-swipeable": "^7.0.2", "usehooks-ts": "^3.1.0", "zustand": "^4.5.4" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "@types/react-input-mask": "^3.0.5", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.21", + "eslint": "^10.2.0", + "eslint-plugin-react-hooks": "^7.0.1", + "globals": "^17.4.0", "postcss": "^8.5.4", "tailwindcss": "^3.4.17", "typescript": "~5.7.2", + "typescript-eslint": "^8.58.1", "vite": "^6.0.3" } }, @@ -262,6 +267,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -723,6 +736,173 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1199,17 +1379,29 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "dev": true, "dependencies": { "csstype": "^3.2.2" } @@ -1223,13 +1415,235 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/react-input-mask": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/react-input-mask/-/react-input-mask-3.0.6.tgz", - "integrity": "sha512-+5I18WKyG3eWIj7TVPWfK1VitI9mPpS9y6jE/BfmTCe+iL27NfBw/yzKRvCFp1DRBvlvvcsiZf05bub0YC1k8A==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", + "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", "dev": true, "dependencies": { - "@types/react": "*" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/type-utils": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", + "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@vitejs/plugin-react": { @@ -1252,6 +1666,43 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1313,6 +1764,15 @@ "postcss": "^8.1.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.17", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", @@ -1337,6 +1797,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1462,6 +1934,20 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1478,7 +1964,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true + "dev": true }, "node_modules/debug": { "version": "4.4.3", @@ -1497,6 +1983,12 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1565,6 +2057,187 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1593,6 +2266,18 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -1602,6 +2287,18 @@ "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1614,6 +2311,41 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -1697,6 +2429,18 @@ "node": ">=10.13.0" } }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1709,12 +2453,75 @@ "node": ">= 0.4" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, "dependencies": { - "loose-envify": "^1.0.0" + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.5.tgz", + "integrity": "sha512-9uHb4T27TdV36phJXcbpnRPt5yzAfqHXVrdASvmHZyPuZJtrLythd+GyXhiaHV5LlpuuskbAqhwPjmfTbKbi8w==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" } }, "node_modules/is-binary-path": { @@ -1774,6 +2581,12 @@ "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -1786,7 +2599,8 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/jsesc": { "version": "3.1.0", @@ -1800,6 +2614,24 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -1812,6 +2644,15 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/ky": { "version": "1.14.3", "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", @@ -1823,6 +2664,19 @@ "url": "https://github.com/sindresorhus/ky?sponsor=1" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/libphonenumber-js": { "version": "1.12.41", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", @@ -1846,22 +2700,26 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -1893,6 +2751,21 @@ "node": ">=8.6" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -1941,6 +2814,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -1974,6 +2853,71 @@ "node": ">= 6" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2172,6 +3116,24 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2226,17 +3188,30 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, - "node_modules/react-input-mask": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/react-input-mask/-/react-input-mask-2.0.4.tgz", - "integrity": "sha512-1hwzMr/aO9tXfiroiVCx5EtKohKwLk/NT8QlJXHQ4N+yJJFyUuMT+zfTpLBwX/lK3PkuMlievIffncpMZ3HGRQ==", + "node_modules/react-i18next": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.3.tgz", + "integrity": "sha512-x4xjvUNZ56T+zfXWNedNnCET9Xq1IBYWX7IsWo5cCQ/RT+Rm7GWqt0h9PShFi4IhyMnsdiu1C6Jc4DE+/S3PFQ==", "dependencies": { - "invariant": "^2.2.4", - "warning": "^4.0.2" + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "react": ">=0.14.0", - "react-dom": ">=0.14.0" + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } } }, "node_modules/react-refresh": { @@ -2388,6 +3363,27 @@ "semver": "bin/semver.js" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2546,6 +3542,18 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -2557,6 +3565,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -2570,6 +3590,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", + "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2600,6 +3643,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -2731,12 +3783,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { - "loose-envify": "^1.0.0" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/yallist": { @@ -2745,6 +3821,39 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", diff --git a/package.json b/package.json index fd239be..2b90a26 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,13 @@ "dependencies": { "@tanstack/react-query": "^5.62.7", "framer-motion": "^11.17.0", + "i18next": "^26.0.5", "ky": "^1.4.0", "libphonenumber-js": "^1.11.7", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.53.0", + "react-i18next": "^17.0.3", "react-swipeable": "^7.0.2", "usehooks-ts": "^3.1.0", "zustand": "^4.5.4" diff --git a/src/App.tsx b/src/App.tsx index 447e542..b564d1b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,15 @@ import { Footer } from "@/components/Layout/Footer"; +import { LanguageSwitcher } from "@/components/Layout/LanguageSwitcher"; +import { LocaleSync } from "@/components/Layout/LocaleSync"; import StreamDemo from "@/features/stream-demo/StreamDemo"; export default function App() { return (
+ + {/* Без overflow-clip: иначе flex-1 + clip часто даёт пустой/обрезанный экран */} -
+
diff --git a/src/components/Layout/Feedback.tsx b/src/components/Layout/Feedback.tsx index 3b92531..c6802da 100644 --- a/src/components/Layout/Feedback.tsx +++ b/src/components/Layout/Feedback.tsx @@ -1,15 +1,14 @@ +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import type { Product } from "@/types"; import useAddReferer from "@/hooks/useAddReferer"; import { useModalStore } from "@/stores/useModalStore"; import FeedbackModal from "@/components/modals/FeedbackFormModal"; import { LeadForm } from "@/features/lead-form/LeadForm"; -const DEFAULT_STREAM_DEMO_PRODUCTS = [ - "Удаленная демонстрация", -] as Product[]; - export function Feedback() { useAddReferer(); + const { t } = useTranslation(); return (

- Хотите увеличить конверсию? + {t("feedback.titleLead")}
- Давайте обсудим детали. + {t("feedback.titleRest")}

@@ -27,13 +26,20 @@ export function Feedback() { } export function FeedbackForm() { + const { t, i18n } = useTranslation(); const { setModal } = useModalStore(); + const defaultProducts = useMemo( + (): Product[] => [t("products.remoteDemo")], + [t] + ); + return (
setModal()} />
diff --git a/src/components/Layout/Footer.tsx b/src/components/Layout/Footer.tsx index 998d91f..3023a25 100644 --- a/src/components/Layout/Footer.tsx +++ b/src/components/Layout/Footer.tsx @@ -1,4 +1,5 @@ import { PropsWithChildren } from "react"; +import { useTranslation } from "react-i18next"; import ArrowMoreIcon from "@/components/icons/ArrowMoreIcon"; import RutubeIcon from "@/components/icons/RutubeIcon"; import TelegramIcon from "@/components/icons/TgIcon"; @@ -6,6 +7,9 @@ import VkIcon from "@/components/icons/VKIcon"; import YoutubeIcon from "@/components/icons/YoutubeIcon"; export function Footer() { + const { t, i18n } = useTranslation(); + const showRuLegal = i18n.language.startsWith("ru"); + return (
@@ -13,7 +17,7 @@ export function Footer() { href={"tel:" + "8 800 770 00 67".replaceAll(" ", "")} className="lg:p-[1.667vw] p-6 flex flex-col justify-between max-md:gap-y-10 bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] lg:rounded-[1.111vw] rounded-2xl lg:aspect-[696/248] lg:w-[48.333vw] md:max-lg:w-[47.656vw] hover:bg-[#37393B99] bg-transparent transition-colors" > -
Позвонить
+
{t("footer.call")}
8 800 770 00 67
@@ -25,7 +29,7 @@ export function Footer() { href={"mailto:" + "info@graff.tech"} className="lg:p-[1.667vw] p-6 flex flex-col justify-between max-md:gap-y-10 bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] lg:rounded-[1.111vw] rounded-2xl lg:aspect-[624/248] lg:w-[43.333vw] md:max-lg:w-[47.656vw] hover:bg-[#37393B99] bg-transparent transition-colors" > -
Написать
+
{t("footer.write")}
info@graff.tech
@@ -57,35 +61,39 @@ export function Footer() {
-
-
- Юридический адрес: - - 620063, г. Екатеринбург,
ул. Большакова, д. 66, кв. 6 -
+ {showRuLegal ? ( +
+
+ {t("footer.legalAddress")} + + {t("footer.addressLine1")} +
+ {t("footer.addressLine2")} +
-
- Наш основной стек: -
-

Unreal Engine 5, C++

+
+ {t("footer.mainStack")} +
+

{t("footer.stackLine")}

+
-
-
- Реквизиты: -
-

ИНН: 6679174128

-

КПП: 667101001

-

ООО "ГРАФФ.ЭСТЕЙТ"

-

ОГРН 1246600010140

+
+ {t("footer.requisites")} +
+

{t("footer.inn")}

+

{t("footer.kpp")}

+

{t("footer.company")}

+

{t("footer.ogrn")}

+
+ {t("footer.skolkovoAlt")}
- Сколково -
+ ) : null}
diff --git a/src/components/Layout/LanguageSwitcher.tsx b/src/components/Layout/LanguageSwitcher.tsx new file mode 100644 index 0000000..65175b9 --- /dev/null +++ b/src/components/Layout/LanguageSwitcher.tsx @@ -0,0 +1,39 @@ +import { useTranslation } from "react-i18next"; +import type { AppLocale } from "@/i18n"; +import { setLangInUrl } from "@/lib/urlLang"; + +const LOCALES: AppLocale[] = ["ru", "en"]; + +export function LanguageSwitcher() { + const { i18n, t } = useTranslation(); + const current = (i18n.language.startsWith("ru") ? "ru" : "en") as AppLocale; + + function select(lang: AppLocale) { + if (lang === current) return; + void i18n.changeLanguage(lang); + setLangInUrl(lang); + } + + return ( +
+ {LOCALES.map((lang) => ( + + ))} +
+ ); +} diff --git a/src/components/Layout/LocaleSync.tsx b/src/components/Layout/LocaleSync.tsx new file mode 100644 index 0000000..186d942 --- /dev/null +++ b/src/components/Layout/LocaleSync.tsx @@ -0,0 +1,30 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import type { AppLocale } from "@/i18n"; +import { useLocationSearch } from "@/hooks/useLocationSearch"; +import { parseLangParam } from "@/lib/urlLang"; +import { useCountryCodeQuery } from "@/queries/getCountryCode"; + +/** Applies resolved locale from `?lang=` or getCountryCode; syncs ``. */ +export function LocaleSync() { + const { i18n } = useTranslation(); + const search = useLocationSearch(); + const langFromUrl = parseLangParam(new URLSearchParams(search).get("lang")); + const { data, isSuccess, isError } = useCountryCodeQuery(); + + useEffect(() => { + if (langFromUrl) { + void i18n.changeLanguage(langFromUrl); + document.documentElement.lang = langFromUrl; + return; + } + if (!isSuccess && !isError) return; + + const locale: AppLocale = + isSuccess && data?.countryCode === "RU" ? "ru" : "en"; + void i18n.changeLanguage(locale); + document.documentElement.lang = locale; + }, [langFromUrl, isSuccess, isError, data, i18n]); + + return null; +} diff --git a/src/components/modals/FeedbackFormModal.tsx b/src/components/modals/FeedbackFormModal.tsx index b4fca6f..9b39265 100644 --- a/src/components/modals/FeedbackFormModal.tsx +++ b/src/components/modals/FeedbackFormModal.tsx @@ -1,15 +1,21 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { Button } from "@/ui/Button"; import { api } from "@/lib/api"; import { useModalStore } from "@/stores/useModalStore"; -import { modalOptions } from "@/consts/feedback"; import CustomCheckbox from "@/ui/CustomCheckbox"; import CheckIcon from "@/components/icons/CheckIcon"; function FeedbackModal({ id }: { id: string }) { + const { t } = useTranslation(); const { setModal } = useModalStore(); const [selectedOptions, setSelectedOptions] = useState([]); + const modalOptions = useMemo( + () => t("modalFeedbackSources", { returnObjects: true }) as string[], + [t] + ); + async function sendSources() { await api.put(`mail/${id}`, { json: { source: selectedOptions } }); setModal(null); @@ -34,8 +40,13 @@ function FeedbackModal({ id }: { id: string }) {
- Мы получили заявку
и скоро свяжемся{" "} -
с вами! + , + brDesktop:
, + }} + />
@@ -43,7 +54,9 @@ function FeedbackModal({ id }: { id: string }) {
- Расскажите, пожалуйста,
откуда вы узнали о нас? + {t("feedbackModal.sourcesTitle1")} +
+ {t("feedbackModal.sourcesTitle2")}
    @@ -59,14 +72,14 @@ function FeedbackModal({ id }: { id: string }) { onClick={sendSources} className="md:px-[31px] max-[360px]:px-[32px] px-[43px] py-[15px] rounded-2xl max-[360px]:text-sm" > - Отправить + {t("feedbackModal.send")}
diff --git a/src/components/modals/QuestionFormModal.tsx b/src/components/modals/QuestionFormModal.tsx index c1b3e54..295e20d 100644 --- a/src/components/modals/QuestionFormModal.tsx +++ b/src/components/modals/QuestionFormModal.tsx @@ -1,16 +1,12 @@ -import { useRef, type RefObject } from "react"; +import { useMemo, useRef, type RefObject } from "react"; import { useOnClickOutside } from "usehooks-ts"; +import { useTranslation } from "react-i18next"; import { useModalStore } from "@/stores/useModalStore"; import type { Product } from "@/types"; import FeedbackModal from "./FeedbackFormModal"; import CloseIcon from "@/components/icons/CloseIcon"; import { LeadForm } from "@/features/lead-form/LeadForm"; -const DEFAULT_MODAL_PRODUCTS = [ - "Создание сайтов", - "Веб-тур по 360 сферам", -] as Product[]; - interface QuestionFormModalProps { products?: Product[]; } @@ -18,8 +14,13 @@ interface QuestionFormModalProps { export default function QuestionFormModal({ products, }: QuestionFormModalProps) { + const { t, i18n } = useTranslation(); const { setModal } = useModalStore(); + const defaultModalProducts = useMemo((): Product[] => { + return [t("products.webDev"), t("products.webTour360")]; + }, [t]); + const formRef = useRef(null); useOnClickOutside(formRef as RefObject, () => { @@ -41,9 +42,10 @@ export default function QuestionFormModal({ setModal()} /> diff --git a/src/consts/feedback.ts b/src/consts/feedback.ts deleted file mode 100644 index 3f9d43a..0000000 --- a/src/consts/feedback.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const modalOptions: string[] = [ - "Увидели на выставке или форуме", - "Видели у других застройщиков", - "Из рейтингов и статей", - "Нашли в интернете", - "Перешли по рекламе", - "Из рассылки", - "Другое", -]; diff --git a/src/consts/projectsTags.ts b/src/consts/projectsTags.ts deleted file mode 100644 index 78a4d68..0000000 --- a/src/consts/projectsTags.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Product } from "@/types"; - -export const projectsTags: Product[] = [ - "Интерактивная презентация", - "Удаленная демонстрация", - "Архитектурная визуализация", - "Создание сайтов", - "Веб-тур по 360 сферам", -]; diff --git a/src/features/lead-form/LeadForm.tsx b/src/features/lead-form/LeadForm.tsx index 8c65487..192339c 100644 --- a/src/features/lead-form/LeadForm.tsx +++ b/src/features/lead-form/LeadForm.tsx @@ -1,6 +1,7 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { api } from "@/lib/api"; -import { projectsTags } from "@/consts/projectsTags"; +import { productOptionsFromT } from "@/lib/productLabels"; import type { Product } from "@/types"; import { Button } from "@/ui/Button"; import { CheckboxesGroup } from "@/ui/CheckboxesGroup"; @@ -16,9 +17,6 @@ import type { LeadFormValues } from "./types"; export type { LeadFormValues } from "./types"; -const GENERIC_SUBMIT_ERROR = - "Не удалось отправить заявку. Попробуйте позже."; - export function LeadForm({ defaultProducts, idPrefix = "", @@ -33,8 +31,10 @@ export function LeadForm({ phonePlaceholder?: string; formClassName?: string; }) { + const { t } = useTranslation(); const { referer } = useRefererStore(); const [submitError, setSubmitError] = useState(null); + const projectOptions = useMemo(() => productOptionsFromT(t), [t]); const form = useForm({ defaultValues: { @@ -57,7 +57,7 @@ export function LeadForm({ .json<{ id: string }>(); onSuccess(id); } catch { - setSubmitError(GENERIC_SUBMIT_ERROR); + setSubmitError(t("leadForm.submitError")); } }; @@ -74,10 +74,10 @@ export function LeadForm({

) : null}
-

Нам нужно

+

{t("leadForm.needTitle")}

name="products" - options={projectsTags} + options={projectOptions} />
@@ -94,7 +94,7 @@ export function LeadForm({ required id={emailId} type="email" - placeholder="Email*" + placeholder={t("leadForm.emailPlaceholder")} {...register("email")} className="bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none btnl outline-none transition-all w-full placeholder:btnl placeholder:font-medium placeholder:select-none" /> @@ -121,28 +121,26 @@ export function LeadForm({ disabled={formState.isSubmitting} className="btnl max-md:mb-3 max-md:w-full lg:px-[2.222vw] lg:py-[1.389vw] px-8 py-5 cursor-pointer lg:rounded-[1.111vw] rounded-2xl disabled:opacity-60" > - Оставить заявку + {t("leadForm.submit")}
- - *Нажимая кнопку отправить, вы даете - {" "} + {t("leadForm.consentBefore")}{" "} - согласие на обработку персональных данных + {t("leadForm.consentLinkData")} {" "} - и принимаете + {t("leadForm.consentMiddle")} - условия политики + {t("leadForm.consentLinkPolicy")}
diff --git a/src/features/stream-demo/AvailableDemos.tsx b/src/features/stream-demo/AvailableDemos.tsx index ca094ef..e5b9db1 100644 --- a/src/features/stream-demo/AvailableDemos.tsx +++ b/src/features/stream-demo/AvailableDemos.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; import BR from "@/components/Layout/LineBreak"; import { REMOTE_DEMO_TAG, @@ -9,6 +10,7 @@ import { useSwipeable } from "react-swipeable"; import { useMediaQueries } from "@/hooks/useMediaQueries"; export default function AvailableDemos() { + const { t } = useTranslation(); const { isMd, isLg } = useMediaQueries(); const { data: streamingProjects } = useGetProjectsQuery(REMOTE_DEMO_TAG); const [current, setCurrent] = useState(0); @@ -34,7 +36,7 @@ export default function AvailableDemos() {

- Доступные
демонстрации + {t("demos.titleLine1")}
{t("demos.titleLine2")}

{!isLg && !isMd && ( @@ -69,13 +71,13 @@ export default function AvailableDemos() {

- Расскажем и покажем как это работает на созвоне + {t("demos.ctaTitle")}

- Оставить заявку + {t("demos.ctaButton")}
@@ -84,9 +86,7 @@ export default function AvailableDemos() { )}

- Клиент из любой точки мира может посмотреть жилой комплекс, даже на - нулевом этапе строительства. Он выберет лучшую планировку и оценит вид - из окон своей будущей квартиры. + {t("demos.description")}

diff --git a/src/features/stream-demo/RequestForDemo.tsx b/src/features/stream-demo/RequestForDemo.tsx index 3237b18..15852ab 100644 --- a/src/features/stream-demo/RequestForDemo.tsx +++ b/src/features/stream-demo/RequestForDemo.tsx @@ -1,10 +1,13 @@ +import { useTranslation } from "react-i18next"; import BR from "@/components/Layout/LineBreak"; import { Button } from "@/ui/Button"; import QuestionFormModal from "@/components/modals/QuestionFormModal"; import { useModalStore } from "@/stores/useModalStore"; export default function RequestForDemo() { + const { t } = useTranslation(); const { setModal } = useModalStore(); + return (

- Запись
на удаленную -
демонстрацию + {t("requestDemo.titleLine1")}
{t("requestDemo.titleLine2")} +
{t("requestDemo.titleLine3")}

- Запись на демонстрацию может быть оформлена в виде блока на сайте - застройщика или жилого комплекса. + {t("requestDemo.description")}

diff --git a/src/features/stream-demo/StreamPlayer.tsx b/src/features/stream-demo/StreamPlayer.tsx index d56cd5a..526f09c 100644 --- a/src/features/stream-demo/StreamPlayer.tsx +++ b/src/features/stream-demo/StreamPlayer.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import { VideoPlayer } from "@/ui/VideoPlayer"; const viteBase = import.meta.env.BASE_URL; @@ -7,6 +8,8 @@ const STREAMING_VIDEO_SRC = : `${viteBase.replace(/\/$/, "")}/videos/streaming.mp4`; export default function StreamPlayer({ className }: { className?: string }) { + const { t } = useTranslation(); + return (

- GRAFF.estate — модуль удаленной демонстрации — доступен на любых - устройствах, для демонстрации нужен только интернет + {t("streamPlayer.caption")}

diff --git a/src/features/stream-demo/StreamingProject.tsx b/src/features/stream-demo/StreamingProject.tsx index 4dd4ec8..956d8db 100644 --- a/src/features/stream-demo/StreamingProject.tsx +++ b/src/features/stream-demo/StreamingProject.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import { streamDemoUrlFromBuild } from "@/lib/streamDemoUrl"; import { resolveProjectImageSrc } from "@/lib/resolveProjectImageSrc"; import type { IProject } from "@/types"; @@ -21,6 +22,7 @@ export function StreamingProject({ count: number; className?: string; }) { + const { t } = useTranslation(); const imgSrc = resolveProjectImageSrc(image); const build = buildFilename?.trim() ?? ""; const streamHref = build ? streamDemoUrlFromBuild(build) : href; @@ -74,7 +76,7 @@ export function StreamingProject({ className="bg-gradient btns flex gap-2 items-center px-3 py-2 font-medium rounded-xl" href={streamHref} > - Смотреть + {t("streamingProject.watch")}
@@ -85,7 +87,7 @@ export function StreamingProject({ className="max-lg:hidden lg:group-hover:opacity-100 opacity-0 transition-opacity duration-500 absolute w-full h-full left-0 bottom-0 md:max-lg:rounded-2xl rounded-xl font-medium [backdrop-filter:blur(3px)] content-center text-center z-[3]" >
- Начать демонстрацию{" "} + {t("streamingProject.startDemo")}{" "}
diff --git a/src/hooks/useLocationSearch.ts b/src/hooks/useLocationSearch.ts new file mode 100644 index 0000000..fad84bb --- /dev/null +++ b/src/hooks/useLocationSearch.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react"; + +/** Re-renders when `location.search` changes (popstate or custom `locationchange`). */ +export function useLocationSearch(): string { + const [search, setSearch] = useState(() => window.location.search); + + useEffect(() => { + const sync = () => setSearch(window.location.search); + window.addEventListener("popstate", sync); + window.addEventListener("locationchange", sync); + return () => { + window.removeEventListener("popstate", sync); + window.removeEventListener("locationchange", sync); + }; + }, []); + + return search; +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..c17d1dc --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,29 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import en from "./locales/en.json"; +import ru from "./locales/ru.json"; + +export type AppLocale = "ru" | "en"; + +function langFromSearch(): AppLocale | null { + const raw = new URLSearchParams(window.location.search).get("lang"); + const v = raw?.trim().toLowerCase(); + if (v === "ru" || v === "en") return v; + return null; +} + +function initialLng(): AppLocale { + return langFromSearch() ?? "en"; +} + +i18n.use(initReactI18next).init({ + resources: { + ru: { translation: ru }, + en: { translation: en }, + }, + lng: initialLng(), + fallbackLng: "en", + interpolation: { escapeValue: false }, +}); + +export { i18n }; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json new file mode 100644 index 0000000..a597a47 --- /dev/null +++ b/src/i18n/locales/en.json @@ -0,0 +1,87 @@ +{ + "languageSwitcher": { + "ru": "RU", + "en": "EN", + "ariaLabel": "Language" + }, + "footer": { + "call": "Call", + "write": "Write", + "legalAddress": "Legal address:", + "addressLine1": "620063, Yekaterinburg,", + "addressLine2": "Bolshakova St., 66, apt. 6", + "mainStack": "Our core stack:", + "stackLine": "Unreal Engine 5, C++", + "requisites": "Company details:", + "inn": "TIN: 6679174128", + "kpp": "IEC: 667101001", + "company": "GRAFF.ESTATE LLC", + "ogrn": "PSRN 1246600010140", + "skolkovoAlt": "Skolkovo", + "privacy": "Privacy and personal data processing policy", + "copyright": "© 2026 GRAFF interactive. All rights reserved", + "site": "graff.tech" + }, + "demos": { + "titleLine1": "Available", + "titleLine2": "demos", + "ctaTitle": "We’ll walk you through how it works on\u00a0a call", + "ctaButton": "Request a call", + "description": "Clients anywhere in the world can explore a residential complex, even at the zero stage of construction. They can pick the best layout and assess the view from the windows of their future apartment." + }, + "streamingProject": { + "watch": "Watch", + "startDemo": "Start demo" + }, + "requestDemo": { + "titleLine1": "Book", + "titleLine2": "a remote", + "titleLine3": "demo", + "description": "A demo booking can be embedded as a block on the developer’s or residential complex’s website.", + "cta": "Request a call" + }, + "streamPlayer": { + "caption": "GRAFF.estate remote demo module works on\u00a0any device; all you need is an internet connection" + }, + "feedback": { + "titleLead": "Want to improve conversion?", + "titleRest": "Let’s discuss the details." + }, + "leadForm": { + "submitError": "Could not submit the request. Please try again later.", + "needTitle": "We need", + "namePlaceholder": "Name*", + "emailPlaceholder": "Email*", + "submit": "Submit request", + "consentBefore": "*By submitting, you give", + "consentLinkData": "consent to personal data processing", + "consentMiddle": "and accept the", + "consentLinkPolicy": "policy terms" + }, + "questionModal": { + "phonePlaceholder": "+X (XXX) XXX - XX - XX" + }, + "feedbackModal": { + "successRich": "We’ve received your request and we’ll contact you soon!", + "sourcesTitle1": "Please tell us", + "sourcesTitle2": "how you heard about us", + "send": "Send", + "skip": "Skip" + }, + "products": { + "interactivePresentation": "Interactive presentation", + "remoteDemo": "Remote demonstration", + "archViz": "Architectural visualization", + "webDev": "Website development", + "webTour360": "360° web tour" + }, + "modalFeedbackSources": [ + "Saw us at an exhibition or forum", + "Saw it with other developers", + "From rankings and articles", + "Found online", + "Came from an ad", + "From a newsletter", + "Other" + ] +} diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json new file mode 100644 index 0000000..b0c2202 --- /dev/null +++ b/src/i18n/locales/ru.json @@ -0,0 +1,87 @@ +{ + "languageSwitcher": { + "ru": "RU", + "en": "EN", + "ariaLabel": "Язык" + }, + "footer": { + "call": "Позвонить", + "write": "Написать", + "legalAddress": "Юридический адрес:", + "addressLine1": "620063, г. Екатеринбург,", + "addressLine2": "ул. Большакова, д. 66, кв. 6", + "mainStack": "Наш основной стек:", + "stackLine": "Unreal Engine 5, C++", + "requisites": "Реквизиты:", + "inn": "ИНН: 6679174128", + "kpp": "КПП: 667101001", + "company": "ООО \"ГРАФФ.ЭСТЕЙТ\"", + "ogrn": "ОГРН 1246600010140", + "skolkovoAlt": "Сколково", + "privacy": "Политика конфиденциальности и обработки персональных данных", + "copyright": "© 2026 GRAFF interactive. Все права защищены", + "site": "graff.tech" + }, + "demos": { + "titleLine1": "Доступные", + "titleLine2": "демонстрации", + "ctaTitle": "Расскажем и покажем как это работает на\u00a0созвоне", + "ctaButton": "Оставить заявку", + "description": "Клиент из любой точки мира может посмотреть жилой комплекс, даже на нулевом этапе строительства. Он выберет лучшую планировку и оценит вид из окон своей будущей квартиры." + }, + "streamingProject": { + "watch": "Смотреть", + "startDemo": "Начать демонстрацию" + }, + "requestDemo": { + "titleLine1": "Запись", + "titleLine2": "на удаленную", + "titleLine3": "демонстрацию", + "description": "Запись на демонстрацию может быть оформлена в виде блока на сайте застройщика или жилого комплекса.", + "cta": "Оставить заявку" + }, + "streamPlayer": { + "caption": "GRAFF.estate — модуль удаленной демонстрации — доступен на\u00a0любых устройствах, для\u00a0демонстрации нужен только интернет" + }, + "feedback": { + "titleLead": "Хотите увеличить конверсию?", + "titleRest": "Давайте обсудим детали." + }, + "leadForm": { + "submitError": "Не удалось отправить заявку. Попробуйте позже.", + "needTitle": "Нам нужно", + "namePlaceholder": "Имя*", + "emailPlaceholder": "Email*", + "submit": "Оставить заявку", + "consentBefore": "*Нажимая кнопку отправить, вы даете", + "consentLinkData": "согласие на обработку персональных данных", + "consentMiddle": "и принимаете", + "consentLinkPolicy": "условия политики" + }, + "questionModal": { + "phonePlaceholder": "+X (XXX) XXX - XX - XX" + }, + "feedbackModal": { + "successRich": "Мы получили заявку и скоро свяжемся с вами!", + "sourcesTitle1": "Расскажите, пожалуйста,", + "sourcesTitle2": "откуда вы узнали о нас?", + "send": "Отправить", + "skip": "Пропустить" + }, + "products": { + "interactivePresentation": "Интерактивная презентация", + "remoteDemo": "Удаленная демонстрация", + "archViz": "Архитектурная визуализация", + "webDev": "Создание сайтов", + "webTour360": "Веб-тур по 360 сферам" + }, + "modalFeedbackSources": [ + "Увидели на выставке или форуме", + "Видели у других застройщиков", + "Из рейтингов и статей", + "Нашли в интернете", + "Перешли по рекламе", + "Из рассылки", + "Другое" + ] +} diff --git a/src/lib/productLabels.ts b/src/lib/productLabels.ts new file mode 100644 index 0000000..3951cf9 --- /dev/null +++ b/src/lib/productLabels.ts @@ -0,0 +1,14 @@ +import type { TFunction } from "i18next"; + +/** Order of product checkboxes in lead forms. */ +export const PRODUCT_I18N_KEYS = [ + "products.interactivePresentation", + "products.remoteDemo", + "products.archViz", + "products.webDev", + "products.webTour360", +] as const; + +export function productOptionsFromT(t: TFunction): string[] { + return PRODUCT_I18N_KEYS.map((key) => t(key)); +} diff --git a/src/lib/urlLang.ts b/src/lib/urlLang.ts new file mode 100644 index 0000000..02f17a0 --- /dev/null +++ b/src/lib/urlLang.ts @@ -0,0 +1,15 @@ +import type { AppLocale } from "@/i18n"; + +export function parseLangParam(v: string | null): AppLocale | null { + const x = v?.trim().toLowerCase(); + if (x === "ru" || x === "en") return x; + return null; +} + +/** Updates `?lang=` without reload; fires `locationchange` for listeners. */ +export function setLangInUrl(lang: AppLocale): void { + const url = new URL(window.location.href); + url.searchParams.set("lang", lang); + window.history.replaceState({}, "", url.toString()); + window.dispatchEvent(new Event("locationchange")); +} diff --git a/src/main.tsx b/src/main.tsx index 19398db..739ac1b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import "@/i18n"; import App from "./App"; import { ModalContainer } from "@/components/Layout/ModalContainer"; import "./index.css"; diff --git a/src/queries/getCountryCode.ts b/src/queries/getCountryCode.ts new file mode 100644 index 0000000..16f3bd1 --- /dev/null +++ b/src/queries/getCountryCode.ts @@ -0,0 +1,26 @@ +import { useQuery } from "@tanstack/react-query"; +import ky from "ky"; +import { useLocationSearch } from "@/hooks/useLocationSearch"; +import { parseLangParam } from "@/lib/urlLang"; +import { queryKeys } from "@/queries/keys"; + +const COUNTRY_CODE_URL = "https://stream.graff.tech/api/getCountryCode"; + +export async function fetchCountryCode(): Promise<{ countryCode: string }> { + return ky.get(COUNTRY_CODE_URL).json<{ countryCode: string }>(); +} + +export function useCountryCodeQuery() { + const search = useLocationSearch(); + const langFromUrl = parseLangParam( + new URLSearchParams(search).get("lang") + ); + + return useQuery({ + queryKey: queryKeys.countryCode, + queryFn: fetchCountryCode, + enabled: langFromUrl === null, + staleTime: 24 * 60 * 60 * 1000, + retry: 2, + }); +} diff --git a/src/queries/keys.ts b/src/queries/keys.ts index 5d40c75..78f2344 100644 --- a/src/queries/keys.ts +++ b/src/queries/keys.ts @@ -1,4 +1,5 @@ export const queryKeys = { projects: ["projects"] as const, projectsWithTags: (tags: string[]) => ["projects", ...tags] as const, + countryCode: ["countryCode"] as const, }; diff --git a/src/types/Product.ts b/src/types/Product.ts index 5e5afed..2d9c946 100644 --- a/src/types/Product.ts +++ b/src/types/Product.ts @@ -1,6 +1,2 @@ -export type Product = - | "Интерактивная презентация" - | "Удаленная демонстрация" - | "Создание сайтов" - | "Архитектурная визуализация" - | "Веб-тур по 360 сферам"; +/** Localized product label in the API payload (matches `t('products.*')` for the active locale). */ +export type Product = string;