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;