diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..4ce2507 Binary files /dev/null and b/dist.zip differ diff --git a/package-lock.json b/package-lock.json index d724763..c9e2351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,12 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.0.1", "@mui/material": "^7.0.1", + "i18next": "^25.1.2", "jotai": "^2.12.3", "qapp-core": "^1.0.30", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-i18next": "^15.5.1", "react-router-dom": "^7.3.0", "react-virtuoso": "^4.12.7" }, @@ -48,13 +50,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -169,17 +172,19 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -194,24 +199,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -259,13 +266,14 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -297,12 +305,13 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1672,11 +1681,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2270,6 +2274,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", "engines": { "node": ">=18" } @@ -2849,6 +2854,46 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "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==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.2.0.tgz", + "integrity": "sha512-ERhJICsxkw1vE7G0lhCUYv4ZxdBEs03qblt1myJs94rYRK9loJF3xDj8mgQz3LmCyp0yYrNjbN/1/GWZTZDGCA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3554,6 +3599,32 @@ "react-dom": ">=16" } }, + "node_modules/react-i18next": { + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", + "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-intersection-observer": { "version": "9.16.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", @@ -3583,14 +3654,13 @@ } }, "node_modules/react-router": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz", - "integrity": "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", + "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", + "license": "MIT", "dependencies": { - "@types/cookie": "^0.6.0", "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0", - "turbo-stream": "2.4.0" + "set-cookie-parser": "^2.6.0" }, "engines": { "node": ">=20.0.0" @@ -3606,11 +3676,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.3.0.tgz", - "integrity": "sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz", + "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==", + "license": "MIT", "dependencies": { - "react-router": "7.3.0" + "react-router": "7.6.0" }, "engines": { "node": ">=20.0.0" @@ -3770,7 +3841,8 @@ "node_modules/set-cookie-parser": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -3859,6 +3931,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3888,11 +4005,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "node_modules/turbo-stream": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", - "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3909,7 +4021,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3988,14 +4100,18 @@ } }, "node_modules/vite": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", - "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -4058,6 +4174,43 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index e487405..09724d8 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.3.0", - "react-virtuoso": "^4.12.7" + "react-virtuoso": "^4.12.7", + "i18next": "^25.1.2", + "react-i18next": "^15.5.1" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/src/components/RegisterName.tsx b/src/components/RegisterName.tsx index f449bca..5e50df4 100644 --- a/src/components/RegisterName.tsx +++ b/src/components/RegisterName.tsx @@ -30,6 +30,7 @@ import { primaryNameAtom, } from '../state/global/names'; import { Availability } from '../interfaces'; +import { useTranslation } from 'react-i18next'; const Label = styled('label')` display: block; @@ -40,6 +41,8 @@ const Label = styled('label')` `; const RegisterName = () => { + const { t } = useTranslation(['core']); + const [isOpen, setIsOpen] = useState(false); const balance = useGlobal().auth.balance; const setNames = useSetAtom(namesAtom); @@ -77,7 +80,11 @@ const RegisterName = () => { }, [namesForSale, primaryName, pendingTxs]); const registerNameFunc = async () => { if (!address) return; - const loadId = showLoading('Registering name...please wait'); + const loadId = showLoading( + t('core:new_name.responses.loading', { + postProcess: 'capitalize', + }) + ); try { setIsLoadingRegisterName(true); const res = await qortalRequest({ @@ -105,14 +112,22 @@ const RegisterName = () => { }, }; }); - showSuccess('Successfully registered a name'); + showSuccess( + t('core:new_name.responses.success', { + postProcess: 'capitalize', + }) + ); setNameValue(''); setIsOpen(false); } catch (error) { if (error instanceof Error) { showError(error.message); } else { - showError('Unable to register name'); + showError( + t('core:new_name.responses.error', { + postProcess: 'capitalize', + }) + ); } } finally { setIsLoadingRegisterName(false); @@ -175,14 +190,20 @@ const RegisterName = () => { flexShrink: 0, }} > - new name + {t('core:actions.new_name', { + postProcess: 'capitalize', + })} - {'Register name'} + + {t('core:actions.register_name', { + postProcess: 'capitalize', + })} + { padding: '10px', }} > - + setNameValue(e.target.value)} value={nameValue} - placeholder="Choose a name" + placeholder={t('core:new_name.choose_name', { + postProcess: 'capitalize', + })} /> {(!balance || (nameFee && balance && balance < nameFee)) && ( <> @@ -221,8 +244,11 @@ const RegisterName = () => { }} /> - Your balance is {balance ?? 0} QORT. A name registration - requires a {nameFee} QORT fee + {t('balance_message', { + balance: balance ?? 0, + nameFee, + postProcess: 'capitalize', + })} @@ -242,7 +268,9 @@ const RegisterName = () => { color: theme.palette.text.primary, }} /> - {nameValue} is available + + {t('core:new_name.name_available', { name: nameValue })} + )} {isNameAvailable === Availability.NOT_AVAILABLE && ( @@ -258,7 +286,9 @@ const RegisterName = () => { color: theme.palette.text.primary, }} /> - {nameValue} is unavailable + + {t('core:new_name.name_unavailable', { name: nameValue })} + )} {isNameAvailable === Availability.LOADING && ( @@ -270,7 +300,11 @@ const RegisterName = () => { }} > - Checking if name already existis + + {t('core:new_name.checking_name', { + postProcess: 'capitalize', + })} + )} @@ -284,7 +318,9 @@ const RegisterName = () => { setNameValue(''); }} > - Close + {t('core:actions.close', { + postProcess: 'capitalize', + })} diff --git a/src/hooks/useIframeListener.tsx b/src/hooks/useIframeListener.tsx index 8cf9030..2a659ff 100644 --- a/src/hooks/useIframeListener.tsx +++ b/src/hooks/useIframeListener.tsx @@ -2,14 +2,21 @@ import { useEffect } from 'react'; import { To, useNavigate } from 'react-router-dom'; import { EnumTheme, themeAtom } from '../state/global/system'; import { useSetAtom } from 'jotai'; +import { useTranslation } from 'react-i18next'; +import { supportedLanguages } from '../i18n/i18n'; + +type Language = 'de' | 'en' | 'es' | 'fr' | 'it' | 'ru'; +type Theme = 'dark' | 'light'; interface CustomWindow extends Window { - _qdnTheme: string; + _qdnTheme: Theme; + _qdnLang: Language; } const customWindow = window as unknown as CustomWindow; export const useIframe = () => { const setTheme = useSetAtom(themeAtom); + const { i18n } = useTranslation(); const navigate = useNavigate(); useEffect(() => { @@ -19,8 +26,20 @@ export const useIframe = () => { } else if (themeColorDefault === 'light') { setTheme(EnumTheme.LIGHT); } + + const languageDefault = customWindow?._qdnLang; + + if (supportedLanguages?.includes(languageDefault)) { + i18n.changeLanguage(languageDefault); + } + function handleNavigation(event: { - data: { action: string; path: To; theme: 'dark' | 'light' }; + data: { + action: string; + path: To; + theme: Theme; + language: Language; + }; }) { if (event.data?.action === 'NAVIGATE_TO_PATH' && event.data.path) { navigate(event.data.path); // Navigate directly to the specified path @@ -37,6 +56,12 @@ export const useIframe = () => { } else if (themeColor === 'light') { setTheme(EnumTheme.LIGHT); } + } else if ( + event.data?.action === 'LANGUAGE_CHANGED' && + event.data.language + ) { + if (!supportedLanguages?.includes(event.data.language)) return; + i18n.changeLanguage(event.data.language); } } diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 0000000..a4edd5d --- /dev/null +++ b/src/i18n/i18n.ts @@ -0,0 +1,56 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { + capitalizeAll, + capitalizeFirstChar, + capitalizeFirstWord, +} from './processors'; + +// Load all locale JSON files +const modules = import.meta.glob('./locales/**/*.json', { + eager: true, +}) as Record; + +// Dynamically detect unique language codes +export const supportedLanguages: string[] = Array.from( + new Set( + Object.keys(modules) + .map((path) => { + const match = path.match(/\.\/locales\/([^/]+)\//); + return match ? match[1] : null; + }) + .filter((lang): lang is string => typeof lang === 'string') + ) +); + +// Construct i18n resources object +const resources: Record> = {}; + +for (const path in modules) { + // Path format: './locales/en/core.json' + const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json$/); + if (!match) continue; + + const [, lang, ns] = match; + resources[lang] = resources[lang] || {}; + resources[lang][ns] = modules[path].default; +} + +i18n + .use(initReactI18next) + .use(capitalizeAll as any) + .use(capitalizeFirstChar as any) + .use(capitalizeFirstWord as any) + .init({ + resources, + fallbackLng: 'en', + lng: navigator.language, + supportedLngs: supportedLanguages, + ns: ['core'], + defaultNS: 'core', + interpolation: { escapeValue: false }, + react: { useSuspense: false }, + debug: import.meta.env.MODE === 'development', + }); + +export default i18n; diff --git a/src/i18n/locales/en/core.json b/src/i18n/locales/en/core.json new file mode 100644 index 0000000..712258b --- /dev/null +++ b/src/i18n/locales/en/core.json @@ -0,0 +1,26 @@ +{ + "header": { + "my_names": "my names", + "market": "names for sale" + }, + "inputs": { + "filter_names": "filter names" + }, + "actions": { + "new_name": "new name", + "register_name": "register name", + "close": "close" + }, + "new_name": { + "choose_name": "choose a name", + "balance_message": "Your balance is {{balance}} QORT. A name registration requires a {{nameFee}} QORT fee.", + "name_available": "{{name}} is available", + "name_unavailable": "{{name}} is unavailable", + "checking_name": "checking if name already exists", + "responses": { + "success": "successfully registered a name", + "error": "unable to register name", + "loading": "Registering name...please wait" + } + } +} diff --git a/src/i18n/locales/es/core.json b/src/i18n/locales/es/core.json new file mode 100644 index 0000000..2234ac5 --- /dev/null +++ b/src/i18n/locales/es/core.json @@ -0,0 +1,6 @@ +{ + "header": { + "my_names": "mis nombres", + "market": "" + } +} diff --git a/src/i18n/processors.ts b/src/i18n/processors.ts new file mode 100644 index 0000000..16afecd --- /dev/null +++ b/src/i18n/processors.ts @@ -0,0 +1,32 @@ +export const capitalizeAll = { + type: 'postProcessor', + name: 'capitalizeAll', + process: (value: string) => value.toUpperCase(), +}; + +export const capitalizeFirstChar = { + type: 'postProcessor', + name: 'capitalizeFirstChar', + process: (value: string) => value.charAt(0).toUpperCase() + value.slice(1), +}; + +export const capitalizeFirstWord = { + type: 'postProcessor', + name: 'capitalizeFirstWord', + process: (value: string) => { + if (!value?.trim()) return value; + + const trimmed = value.trimStart(); + const firstSpaceIndex = trimmed.indexOf(' '); + + if (firstSpaceIndex === -1) { + return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); + } + + const firstWord = trimmed.slice(0, firstSpaceIndex); + const restOfString = trimmed.slice(firstSpaceIndex); + const trailingSpaces = value.slice(trimmed.length); + + return firstWord.toUpperCase() + restOfString + trailingSpaces; + }, +}; diff --git a/src/main.tsx b/src/main.tsx index 1f659b6..20675a7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,11 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./index.css"; -import ThemeProviderWrapper from "./styles/theme/theme-provider.tsx"; -import { AppWrapper } from "./AppWrapper.tsx"; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import ThemeProviderWrapper from './styles/theme/theme-provider.tsx'; +import { AppWrapper } from './AppWrapper.tsx'; +import './i18n/i18n.ts'; -createRoot(document.getElementById("root")!).render( +createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Market.tsx b/src/pages/Market.tsx index 9a75b41..08c84cf 100644 --- a/src/pages/Market.tsx +++ b/src/pages/Market.tsx @@ -8,8 +8,11 @@ import { } from '../state/global/names'; import { useAtom } from 'jotai'; import { SortBy, SortDirection } from '../interfaces'; +import { useTranslation } from 'react-i18next'; export const Market = () => { + const { t } = useTranslation(['core']); + const [namesForSale] = useAtom(forSaleAtom); const [pendingTxs] = useAtom(pendingTxsAtom); const [primaryName] = useAtom(primaryNameAtom); @@ -106,7 +109,9 @@ export const Market = () => { }} > setValue(e.target.value)} size="small" diff --git a/src/pages/MyNames.tsx b/src/pages/MyNames.tsx index 7e91186..3120593 100644 --- a/src/pages/MyNames.tsx +++ b/src/pages/MyNames.tsx @@ -4,8 +4,11 @@ import { namesAtom, primaryNameAtom } from '../state/global/names'; import { NameTable } from '../components/Tables/NameTable'; import { Box, TextField } from '@mui/material'; import RegisterName from '../components/RegisterName'; +import { useTranslation } from 'react-i18next'; export const MyNames = () => { + const { t } = useTranslation(['core']); + const [names] = useAtom(namesAtom); const [value, setValue] = useState(''); const [filterValue, setFilterValue] = useState(''); @@ -50,7 +53,9 @@ export const MyNames = () => { > {' '} setValue(e.target.value)} size="small" diff --git a/src/styles/Layout.tsx b/src/styles/Layout.tsx index cb14e1a..8b64540 100644 --- a/src/styles/Layout.tsx +++ b/src/styles/Layout.tsx @@ -4,16 +4,26 @@ import { AppBar, Toolbar, Button, Box, useTheme } from '@mui/material'; import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; import StorefrontIcon from '@mui/icons-material/Storefront'; import { PendingTxsTable } from '../components/Tables/PendingTxsTable'; +import { useTranslation } from 'react-i18next'; const Layout = () => { useIframe(); const navigate = useNavigate(); const location = useLocation(); const theme = useTheme(); + const { t } = useTranslation(['core']); const navItems = [ - { label: 'My names', path: '/', Icon: FormatListBulletedIcon }, - { label: 'Names for sale', path: '/market', Icon: StorefrontIcon }, + { + label: t('core:header.my_names', { postProcess: 'capitalize' }), + path: '/', + Icon: FormatListBulletedIcon, + }, + { + label: t('core:header.market', { postProcess: 'capitalize' }), + path: '/market', + Icon: StorefrontIcon, + }, ]; return (