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',
+ })}
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 (