diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..42ad5d6 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +only-built-dependencies=bufferutil,esbuild,keccak,utf-8-validate diff --git a/package.json b/package.json index 529d82c..5baa687 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,12 @@ "@wraith-protocol/sdk": "^1.4.5", "bs58": "^6.0.0", "buffer": "^6.0.3", + "i18next": "^24.2.3", + "i18next-browser-languagedetector": "^8.0.5", "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-i18next": "^15.5.1", "react-qr-reader": "3.0.0-beta-1", "react-router-dom": "^7.6.0", "viem": "^2.23.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10d68db..df4b02a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,12 @@ importers: buffer: specifier: ^6.0.3 version: 6.0.3 + i18next: + specifier: ^24.2.3 + version: 24.2.3(typescript@5.9.3) + i18next-browser-languagedetector: + specifier: ^8.0.5 + version: 8.2.1 qrcode.react: specifier: ^4.2.0 version: 4.2.0(react@19.2.5) @@ -74,6 +80,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.5(react@19.2.5) + react-i18next: + specifier: ^15.5.1 + version: 15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6))(react@19.2.5)(typescript@5.9.3) react-qr-reader: specifier: 3.0.0-beta-1 version: 3.0.0-beta-1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -3968,6 +3977,8 @@ packages: resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} engines: {node: '>=16.9.0'} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -4011,6 +4022,16 @@ packages: engines: {node: '>=18'} hasBin: true + i18next-browser-languagedetector@8.2.1: + resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} + + i18next@24.2.3: + resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -5408,6 +5429,21 @@ packages: peerDependencies: react: ^19.2.5 + react-i18next@15.7.4: + resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==} + peerDependencies: + i18next: '>= 23.4.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -6346,6 +6382,10 @@ packages: vlq@1.0.1: resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + wagmi@2.19.5: resolution: {integrity: sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==} peerDependencies: @@ -6840,6 +6880,7 @@ snapshots: - utf-8-validate - zod + '@ckb-ccc/ccc@1.1.25(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': '@bcoe/v8-coverage@0.2.3': {} '@ckb-ccc/ccc@1.1.25(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': @@ -7336,6 +7377,24 @@ snapshots: dependencies: '@hapi/hoek': 11.0.7 + '@joyid/ckb@1.1.4(typescript@5.9.3)(zod@3.25.76)': + dependencies: + '@joyid/common': 0.2.2(typescript@5.9.3)(zod@3.25.76) + '@nervosnetwork/ckb-sdk-utils': 0.109.5 + cross-fetch: 4.0.0 + uncrypto: 0.1.3 + transitivePeerDependencies: + - encoding + - typescript + - zod + + '@joyid/common@0.2.2(typescript@5.9.3)(zod@3.25.76)': + dependencies: + abitype: 0.8.7(typescript@5.9.3)(zod@3.25.76) + type-fest: 4.6.0 + transitivePeerDependencies: + - typescript + - zod '@inquirer/ansi@2.0.6': {} '@inquirer/confirm@6.1.0(@types/node@25.9.4)': @@ -7916,6 +7975,7 @@ snapshots: '@paulmillr/qr@0.2.1': {} + '@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.99.0(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3)(viem@2.47.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(wagmi@2.19.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6)))(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.5))(@types/react@19.2.14)(bufferutil@4.1.0)(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6))(react@19.2.5)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@3.25.76))': '@playwright/test@1.61.0': dependencies: playwright: 1.61.0 @@ -9728,6 +9788,7 @@ snapshots: transitivePeerDependencies: - supports-color + '@wagmi/connectors@6.2.0(d32fac3895882b0645754e2e858c70dd)': '@vitest/expect@2.0.5': dependencies: '@vitest/spy': 2.0.5 @@ -9772,6 +9833,7 @@ snapshots: '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6)))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' porto: 0.2.35(de3b9b5ab3c2ce8d2e8c6952073a517f) + porto: 0.2.35(guqvgfizw6uggvf5t4dm2gogwe) viem: 2.47.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6) optionalDependencies: typescript: 5.9.3 @@ -11933,6 +11995,9 @@ snapshots: hono@4.12.12: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 html-encoding-sniffer@3.0.0: dependencies: whatwg-encoding: 2.0.0 @@ -12005,6 +12070,15 @@ snapshots: husky@9.1.7: {} + i18next-browser-languagedetector@8.2.1: + dependencies: + '@babel/runtime': 7.29.2 + + i18next@24.2.3(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.2 + optionalDependencies: + typescript: 5.9.3 iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -13597,6 +13671,7 @@ snapshots: pony-cause@2.1.11: {} + porto@0.2.35(de3b9b5ab3c2ce8d2e8c6952073a517f): portfinder@1.0.38: dependencies: async: 3.2.6 @@ -13802,6 +13877,16 @@ snapshots: react: 19.2.5 scheduler: 0.27.0 + react-i18next@15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6))(react@19.2.5)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.2 + html-parse-stringify: 3.0.1 + i18next: 24.2.3(typescript@5.9.3) + react: 19.2.5 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + react-native: 0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6) + typescript: 5.9.3 react-is@17.0.2: {} react-is@18.3.1: {} @@ -14747,10 +14832,13 @@ snapshots: vlq@1.0.1: {} + void-elements@3.1.0: {} + wagmi@2.19.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6)))(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.5))(@types/react@19.2.14)(bufferutil@4.1.0)(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6))(react@19.2.5)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@3.25.76): dependencies: '@tanstack/react-query': 5.99.0(react@19.2.5) '@wagmi/connectors': 6.2.0(d32fac3895882b0645754e2e858c70dd) + '@wagmi/connectors': 6.2.0(pomzlx72jhm3uc2rzocjy5pjja) '@wagmi/core': 2.22.1(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.47.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6)) react: 19.2.5 use-sync-external-store: 1.4.0(react@19.2.5) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a11079a..05afb45 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,11 @@ +onlyBuiltDependencies: + - bufferutil + - esbuild + - keccak + - utf-8-validate + - utf-8-validate@5.0.10 + - utf-8-validate@6.0.6 + allowBuilds: '@swc/core': set this to true or false bufferutil: true @@ -5,3 +13,5 @@ allowBuilds: keccak: true msw: set this to true or false utf-8-validate: true + +dangerouslyAllowAllBuilds: true diff --git a/src/components/CkbReceive.tsx b/src/components/CkbReceive.tsx index 183571e..3ff067f 100644 --- a/src/components/CkbReceive.tsx +++ b/src/components/CkbReceive.tsx @@ -9,10 +9,12 @@ import { type MatchedStealthCell, type HexString, } from '@wraith-protocol/sdk/chains/ckb'; +import { useTranslation } from 'react-i18next'; import { useStealthKeys } from '@/context/StealthKeysContext'; import { CopyButton } from '@/components/CopyButton'; function CkbStealthRow({ match }: { match: MatchedStealthCell }) { + const { t } = useTranslation(); const [showKey, setShowKey] = useState(false); const keyHex = match.stealthPrivateKey.slice(2); const capacityCkb = (Number(match.capacity) / 1e8).toFixed(4); @@ -22,7 +24,7 @@ function CkbStealthRow({ match }: { match: MatchedStealthCell }) {
- Stealth Hash + {t('ckb.stealthHash')}

{match.stealthPubKeyHash}

@@ -36,7 +38,9 @@ function CkbStealthRow({ match }: { match: MatchedStealthCell }) {
- Cell + + {t('ckb.cell')} +

{match.txHash}:{match.index}

@@ -44,10 +48,10 @@ function CkbStealthRow({ match }: { match: MatchedStealthCell }) {

- Withdraw + {t('ckb.withdraw')}

- Use the private key below to sign a CKB transaction consuming this Cell. + {t('ckb.withdrawInstruction')}

@@ -57,13 +61,13 @@ function CkbStealthRow({ match }: { match: MatchedStealthCell }) { onClick={() => setShowKey(true)} className="font-mono text-[10px] uppercase tracking-widest text-outline transition-colors hover:text-primary" > - Reveal private key + {t('common.revealPrivateKey')} ) : (
- Stealth Key + {t('common.stealthKey')}
@@ -76,6 +80,7 @@ function CkbStealthRow({ match }: { match: MatchedStealthCell }) { } export function CkbReceive() { + const { t } = useTranslation(); const { wallet } = ccc.useCcc(); const signer = ccc.useSigner(); const { ckbKeys, ckbMetaAddress, setCkbKeys, setCkbMetaAddress } = useStealthKeys(); @@ -88,7 +93,7 @@ export function CkbReceive() { const deriveKeys = useCallback(async () => { if (!signer) { - setError('Connect your CKB wallet first'); + setError(t('ckb.connectWalletFirst')); return; } setIsDerivingKeys(true); @@ -109,11 +114,11 @@ export function CkbReceive() { const meta = encodeStealthMetaAddress(derived.spendingPubKey, derived.viewingPubKey); setCkbMetaAddress(meta); } catch (err) { - setError(err instanceof Error ? err.message : 'Key derivation failed'); + setError(err instanceof Error ? err.message : t('common.keyDerivationFailed')); } finally { setIsDerivingKeys(false); } - }, [signer, setCkbKeys, setCkbMetaAddress]); + }, [signer, setCkbKeys, setCkbMetaAddress, t]); const scanPayments = useCallback(async () => { if (!ckbKeys) return; @@ -130,23 +135,23 @@ export function CkbReceive() { setMatched(results); setHasScanned(true); } catch (err) { - setError(err instanceof Error ? err.message : 'Scan failed'); + setError(err instanceof Error ? err.message : t('common.scanFailed')); } finally { setIsScanning(false); } - }, [ckbKeys]); + }, [ckbKeys, t]); if (!wallet) { return (
- CKB Testnet / CKB + {t('ckb.network')}

- Receive + {t('ckb.receiveTitle')}

- Connect your CKB wallet to scan for stealth payments. + {t('ckb.receiveConnectPrompt')}

); @@ -156,13 +161,13 @@ export function CkbReceive() {
- CKB Testnet / CKB + {t('ckb.network')}

- Receive + {t('ckb.receiveTitle')}

- Derive your stealth keys, then scan for stealth Cells on CKB Testnet. + {t('ckb.receiveDescription')}

@@ -173,7 +178,7 @@ export function CkbReceive() { disabled={isDerivingKeys} className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isDerivingKeys ? 'Sign in wallet...' : 'Derive Stealth Keys'} + {isDerivingKeys ? t('common.signingInWallet') : t('ckb.deriveStealthKeys')} {error &&

{error}

}
@@ -184,7 +189,7 @@ export function CkbReceive() {
- Your Stealth Meta-Address + {t('common.yourStealthMetaAddress')}
@@ -199,11 +204,11 @@ export function CkbReceive() { disabled={isScanning} className="h-12 bg-primary px-6 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isScanning ? 'Scanning...' : 'Scan for Cells'} + {isScanning ? t('ckb.scanning') : t('ckb.scanForCells')} {hasScanned && ( - {matched.length} cell{matched.length !== 1 ? 's' : ''} found + {t('ckb.cellsFound', { count: matched.length })} )}
@@ -221,10 +226,10 @@ export function CkbReceive() { {hasScanned && matched.length === 0 && (

- No cells found + {t('ckb.noCellsFound')}

- No stealth Cells matched your keys. + {t('ckb.noCellsMatchedKeys')}

)} diff --git a/src/components/CkbSend.tsx b/src/components/CkbSend.tsx index ae5261c..4539e11 100644 --- a/src/components/CkbSend.tsx +++ b/src/components/CkbSend.tsx @@ -5,11 +5,13 @@ import { decodeStealthMetaAddress, getDeployment, } from '@wraith-protocol/sdk/chains/ckb'; +import { useTranslation } from 'react-i18next'; import { CopyButton } from '@/components/CopyButton'; const STEALTH_LOCK_CODE_HASH = getDeployment('ckb').contracts.stealthLockCodeHash; export function CkbSend() { + const { t } = useTranslation(); const { wallet } = ccc.useCcc(); const signer = ccc.useSigner(); const [recipient, setRecipient] = useState(''); @@ -24,7 +26,7 @@ export function CkbSend() { const handleSend = useCallback(async () => { if (!signer) { - setError('Connect your CKB wallet first'); + setError(t('ckb.connectWalletFirst')); return; } @@ -33,16 +35,14 @@ export function CkbSend() { try { if (!recipient.startsWith('st:ckb:')) { - setError('Enter a valid CKB meta-address (st:ckb:...)'); + setError(t('ckb.validMetaAddressError')); setIsPending(false); return; } const parsed = parseFloat(amount); if (parsed < 95) { - setError( - 'Minimum amount is 95 CKB. Stealth cells require at least ~94.5 CKB for cell capacity.', - ); + setError(t('ckb.minAmountError')); setIsPending(false); return; } @@ -73,11 +73,11 @@ export function CkbSend() { const hash = await signer.sendTransaction(tx); setTxHash(hash); } catch (err) { - setError(err instanceof Error ? err.message : 'Transaction failed'); + setError(err instanceof Error ? err.message : t('common.transactionFailed')); } finally { setIsPending(false); } - }, [signer, recipient, amount]); + }, [signer, recipient, amount, t]); const reset = () => { setRecipient(''); @@ -102,13 +102,13 @@ export function CkbSend() { return (
- CKB Testnet / CKB + {t('ckb.network')}

- Send + {t('ckb.sendTitle')}

- Connect your CKB wallet to send stealth payments. + {t('ckb.sendConnectPrompt')}

); @@ -118,14 +118,13 @@ export function CkbSend() {
- CKB Testnet / CKB + {t('ckb.network')}

- Send + {t('ckb.sendTitle')}

- Send CKB privately using stealth addresses. The recipient gets funds at a fresh Cell only - they can unlock. + {t('ckb.sendDescription')}

@@ -133,28 +132,28 @@ export function CkbSend() {
setRecipient(e.target.value)} - placeholder="st:ckb:..." + placeholder={t('ckb.recipientPlaceholder')} className="h-12 w-full border border-outline-variant bg-surface px-4 pr-20 font-mono text-sm text-primary placeholder:text-outline focus:border-primary" />
- Network fee + {t('common.networkFee')} + + + {t('ckb.networkFeeAmount')} - ~1000 shannons
- Min cell capacity + {t('ckb.minCellCapacity')} + + + {t('ckb.minCellCapacityValue')} - ~94.5 CKB
@@ -192,7 +195,7 @@ export function CkbSend() { disabled={!recipient || !amount || isPending} className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isPending ? 'Confirm in wallet...' : 'Send Privately'} + {isPending ? t('common.confirmInWallet') : t('common.sendPrivately')}
)} @@ -202,14 +205,14 @@ export function CkbSend() {
- Transfer Complete + {t('common.transferComplete')}
- Stealth Public Key + {t('ckb.stealthPublicKey')}

@@ -221,7 +224,7 @@ export function CkbSend() {

- Transaction Hash + {t('common.transactionHash')} )} diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index 0c784a9..59dfd19 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -1,6 +1,8 @@ import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; export function CopyButton({ text }: { text: string }) { + const { t } = useTranslation(); const [copied, setCopied] = useState(false); return ( ); } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 562fd1a..73c6ead 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,7 +1,9 @@ import { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { ChainSwitcher } from './ChainSwitcher'; import { WalletConnect } from './WalletConnect'; +import { LocaleSwitcher } from './LocaleSwitcher'; import { useTheme } from '@/context/ThemeContext'; const navLinks = [ @@ -12,9 +14,15 @@ const navLinks = [ export function Header() { const location = useLocation(); + const { t } = useTranslation(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const { theme, toggleTheme } = useTheme(); + const navLinks = [ + { to: '/send', label: t('nav.send') }, + { to: '/receive', label: t('nav.receive') }, + ]; + return (
@@ -47,6 +55,7 @@ export function Header() {
+
@@ -158,7 +160,7 @@ function StealthRow({
- Withdrawn —{' '} + {t('common.withdrawn')} —{' '} setShowKey(true)} className="font-mono text-[10px] uppercase tracking-widest text-outline transition-colors hover:text-primary" > - Reveal private key + {t('common.revealPrivateKey')} ) : (
- Stealth Key + {t('common.stealthKey')}
@@ -198,6 +200,7 @@ function StealthRow({ } export function HorizenReceive() { + const { t } = useTranslation(); const { isConnected, address } = useAccount(); const { signMessageAsync } = useSignMessage(); const { evmKeys, evmMetaAddress, setEvmKeys, setEvmMetaAddress } = useStealthKeys(); @@ -247,7 +250,7 @@ export function HorizenReceive() { const meta = encodeStealthMetaAddress(derived.spendingPubKey, derived.viewingPubKey); setEvmMetaAddress(meta); } catch (err) { - setError(err instanceof Error ? err.message : 'Key derivation failed'); + setError(err instanceof Error ? err.message : t('common.keyDerivationFailed')); } finally { setIsDerivingKeys(false); } @@ -279,7 +282,7 @@ export function HorizenReceive() { setMatched(results); setHasScanned(true); } catch (err) { - setError(err instanceof Error ? err.message : 'Scan failed'); + setError(err instanceof Error ? err.message : t('common.scanFailed')); } finally { setIsScanning(false); } @@ -289,13 +292,13 @@ export function HorizenReceive() { return (
- Horizen Testnet / ETH + {t('horizen.network')}

- Receive + {t('horizen.receiveTitle')}

- Connect your wallet to scan for incoming stealth transfers on Horizen. + {t('horizen.receiveConnectPrompt')}

); @@ -305,13 +308,13 @@ export function HorizenReceive() {
- Horizen Testnet / ETH + {t('horizen.network')}

- Receive + {t('horizen.receiveTitle')}

- Derive your stealth keys, register on-chain, then scan for payments. + {t('horizen.receiveDescription')}

@@ -322,7 +325,7 @@ export function HorizenReceive() { disabled={isDerivingKeys} className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isDerivingKeys ? 'Sign in wallet...' : 'Derive Keys'} + {isDerivingKeys ? t('common.signingInWallet') : t('common.deriveKeys')} {error &&

{error}

}
@@ -333,7 +336,7 @@ export function HorizenReceive() {
- Your Stealth Meta-Address + {t('common.yourStealthMetaAddress')}
@@ -344,13 +347,13 @@ export function HorizenReceive() {
- On-Chain Registration + {t('common.onChainRegistration')} {registered ? (
- Meta-address registered on-chain + {t('common.metaAddressRegistered')} {regHash && ( <> {' — '} @@ -369,7 +372,7 @@ export function HorizenReceive() { ) : (

- Register your meta-address so senders can look you up by wallet address. + {t('common.registerMetaAddressHint')}

)} @@ -392,11 +395,11 @@ export function HorizenReceive() { disabled={isScanning} className="h-12 bg-primary px-6 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isScanning ? 'Scanning...' : 'Scan for Payments'} + {isScanning ? t('common.scanning') : t('common.scanForPayments')} {hasScanned && ( - {matched.length} transfer{matched.length !== 1 ? 's' : ''} found + {t('common.transfersFound', { count: matched.length })} )}
@@ -414,10 +417,10 @@ export function HorizenReceive() { {hasScanned && matched.length === 0 && (

- No transfers found + {t('common.noTransfersFound')}

- No stealth transfers matched your keys. + {t('common.noTransfersMatchedKeys')}

)} diff --git a/src/components/HorizenSend.tsx b/src/components/HorizenSend.tsx index b05e81a..8e72914 100644 --- a/src/components/HorizenSend.tsx +++ b/src/components/HorizenSend.tsx @@ -2,11 +2,13 @@ import { useState } from 'react'; import { useAccount, useBalance, useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'; import { buildSendStealth, buildResolveName } from '@wraith-protocol/sdk/chains/evm'; import type { HexString, BuildSendStealthResult } from '@wraith-protocol/sdk/chains/evm'; +import { useTranslation } from 'react-i18next'; import { horizenTxUrl, horizenAddrUrl } from '@/lib/explorer'; import { horizenTestnet } from '@/config'; import { CopyButton } from '@/components/CopyButton'; export function HorizenSend() { + const { t } = useTranslation(); const { isConnected, address } = useAccount(); const { data: balanceData } = useBalance({ address }); @@ -42,7 +44,7 @@ export function HorizenSend() { }); const json = await response.json(); if (!json.result || json.result === '0x' || json.result.length <= 66) { - setError('Name not found'); + setError(t('common.nameNotFound')); return; } const metaBytes = ('0x' + json.result.slice(130).replace(/0+$/, '')) as HexString; @@ -68,7 +70,7 @@ export function HorizenSend() { value: result.transaction.value, }); } catch (err) { - setError(err instanceof Error ? err.message : 'Transaction failed'); + setError(err instanceof Error ? err.message : t('common.transactionFailed')); } }; @@ -100,13 +102,13 @@ export function HorizenSend() { return (
- Horizen Testnet / ETH + {t('horizen.network')}

- Send + {t('horizen.sendTitle')}

- Connect your wallet to send stealth payments on Horizen. + {t('horizen.sendConnectPrompt')}

); @@ -116,14 +118,13 @@ export function HorizenSend() {
- Horizen Testnet / ETH + {t('horizen.network')}

- Send + {t('horizen.sendTitle')}

- Send ETH privately using stealth addresses. The recipient gets funds at a fresh address - only they can control. + {t('horizen.sendDescription')}

@@ -131,21 +132,21 @@ export function HorizenSend() {
setRecipient(e.target.value)} - placeholder="st:eth:0x... or name.wraith" + placeholder={t('horizen.recipientPlaceholder')} className="h-12 w-full border border-outline-variant bg-surface px-4 pr-20 font-mono text-sm text-primary placeholder:text-outline focus:border-primary" />
{isWraithName && ( @@ -157,7 +158,7 @@ export function HorizenSend() {
- Max + {t('common.max')} ETH
{balanceData && ( - Balance: {parseFloat(balanceData.formatted).toFixed(6)} ETH + {t('common.balance')}: {parseFloat(balanceData.formatted).toFixed(6)} ETH )}
@@ -187,21 +188,27 @@ export function HorizenSend() {
- Network fee + {t('common.networkFee')} + + + {t('common.paidBySender')} - Paid by sender
- Announcer contract + {t('common.announcerContract')} + + + {t('horizen.announcerContractName')} - WraithSender
- Expected confirmation + {t('common.expectedConfirmation')} + + + {t('common.seconds_approx')} - ~5 seconds
@@ -212,7 +219,7 @@ export function HorizenSend() { disabled={!recipient || !amount || isPending} className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isPending ? 'Confirm in wallet...' : 'Send Privately'} + {isPending ? t('common.confirmInWallet') : t('common.sendPrivately')}
)} @@ -226,14 +233,18 @@ export function HorizenSend() { )} - {isConfirming ? 'Confirming...' : isSuccess ? 'Transfer Complete' : 'Pending'} + {isConfirming + ? t('common.confirming') + : isSuccess + ? t('common.transferComplete') + : t('common.pending')}
- Stealth Address + {t('common.stealthAddress')}
- Transaction Hash + {t('common.transactionHash')} diff --git a/src/components/LocaleSwitcher.tsx b/src/components/LocaleSwitcher.tsx new file mode 100644 index 0000000..1230af0 --- /dev/null +++ b/src/components/LocaleSwitcher.tsx @@ -0,0 +1,42 @@ +import { useTranslation } from 'react-i18next'; + +const LOCALES: { code: string; label: string }[] = [ + { code: 'en', label: 'EN' }, + { code: 'es', label: 'ES' }, +]; + +export function LocaleSwitcher() { + const { i18n, t } = useTranslation(); + const current = i18n.resolvedLanguage ?? i18n.language; + + const handleChange = (e: React.ChangeEvent) => { + const lang = e.target.value; + i18n.changeLanguage(lang); + // Persist preference — LanguageDetector writes to localStorage automatically, + // but we also set it explicitly so it survives cache clears. + localStorage.setItem('wraith-locale', lang); + }; + + return ( +
+ +
+ + + +
+
+ ); +} diff --git a/src/components/SolanaReceive.tsx b/src/components/SolanaReceive.tsx index 8856fa2..9c9e6c8 100644 --- a/src/components/SolanaReceive.tsx +++ b/src/components/SolanaReceive.tsx @@ -10,6 +10,7 @@ import { STEALTH_SIGNING_MESSAGE, } from '@wraith-protocol/sdk/chains/solana'; import type { MatchedAnnouncement } from '@wraith-protocol/sdk/chains/solana'; +import { useTranslation } from 'react-i18next'; import { useStealthKeys } from '@/context/StealthKeysContext'; import { CopyButton } from '@/components/CopyButton'; import { solanaTxUrl, solanaAddrUrl } from '@/lib/explorer'; @@ -22,6 +23,7 @@ function SolanaStealthRow({ match: MatchedAnnouncement; onWithdrawn: () => void; }) { + const { t } = useTranslation(); const [balance, setBalance] = useState(null); const [loadingBal, setLoadingBal] = useState(true); const [dest, setDest] = useState(''); @@ -94,7 +96,7 @@ function SolanaStealthRow({ setWithdrawHash(txId); onWithdrawn(); } catch (err) { - setError(err instanceof Error ? err.message : 'Withdraw failed'); + setError(err instanceof Error ? err.message : t('common.transactionFailed')); } finally { setWithdrawing(false); } @@ -105,7 +107,7 @@ function SolanaStealthRow({
@@ -136,14 +138,14 @@ function SolanaStealthRow({ {!withdrawHash && balance && parseFloat(balance) > 0 && (
setDest(e.target.value)} - placeholder="Destination address (base58)" + placeholder={t('solana.destinationPlaceholder')} className="h-10 flex-1 border border-outline-variant bg-surface px-3 font-mono text-xs text-primary placeholder:text-outline focus:border-primary" />
@@ -163,7 +165,7 @@ function SolanaStealthRow({
- Withdrawn —{' '} + {t('common.withdrawn')} —{' '} setShowKey(true)} className="font-mono text-[10px] uppercase tracking-widest text-outline transition-colors hover:text-primary" > - Reveal secret key + {t('common.revealSecretKey')} ) : (
- Stealth Key + {t('common.stealthKey')}
@@ -201,6 +203,7 @@ function SolanaStealthRow({ } export function SolanaReceive() { + const { t } = useTranslation(); const { connected, signMessage } = useWallet(); const { solanaKeys, solanaMetaAddress, setSolanaKeys, setSolanaMetaAddress } = useStealthKeys(); @@ -212,7 +215,7 @@ export function SolanaReceive() { const deriveKeys = useCallback(async () => { if (!signMessage) { - setError('Wallet does not support message signing'); + setError(t('solana.walletSigningNotSupported')); return; } setIsDerivingKeys(true); @@ -225,11 +228,11 @@ export function SolanaReceive() { const meta = encodeStealthMetaAddress(derived.spendingPubKey, derived.viewingPubKey); setSolanaMetaAddress(meta); } catch (err) { - setError(err instanceof Error ? err.message : 'Key derivation failed'); + setError(err instanceof Error ? err.message : t('common.keyDerivationFailed')); } finally { setIsDerivingKeys(false); } - }, [signMessage, setSolanaKeys, setSolanaMetaAddress]); + }, [signMessage, setSolanaKeys, setSolanaMetaAddress, t]); const scanPayments = useCallback(async () => { if (!solanaKeys) return; @@ -246,23 +249,23 @@ export function SolanaReceive() { setMatched(results); setHasScanned(true); } catch (err) { - setError(err instanceof Error ? err.message : 'Scan failed'); + setError(err instanceof Error ? err.message : t('common.scanFailed')); } finally { setIsScanning(false); } - }, [solanaKeys]); + }, [solanaKeys, t]); if (!connected) { return (
- Solana Devnet / SOL + {t('solana.network')}

- Receive + {t('solana.receiveTitle')}

- Connect your Solana wallet to scan for stealth payments. + {t('solana.receiveConnectPrompt')}

); @@ -272,13 +275,13 @@ export function SolanaReceive() {
- Solana Devnet / SOL + {t('solana.network')}

- Receive + {t('solana.receiveTitle')}

- Derive your stealth keys, then scan for payments on Solana Devnet. + {t('solana.receiveDescription')}

@@ -289,7 +292,7 @@ export function SolanaReceive() { disabled={isDerivingKeys} className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isDerivingKeys ? 'Sign in wallet...' : 'Derive Keys'} + {isDerivingKeys ? t('common.signingInWallet') : t('common.deriveKeys')} {error &&

{error}

}
@@ -300,7 +303,7 @@ export function SolanaReceive() {
- Your Stealth Meta-Address + {t('common.yourStealthMetaAddress')}
@@ -315,11 +318,11 @@ export function SolanaReceive() { disabled={isScanning} className="h-12 bg-primary px-6 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isScanning ? 'Scanning...' : 'Scan for Payments'} + {isScanning ? t('common.scanning') : t('common.scanForPayments')} {hasScanned && ( - {matched.length} transfer{matched.length !== 1 ? 's' : ''} found + {t('common.transfersFound', { count: matched.length })} )}
@@ -337,10 +340,10 @@ export function SolanaReceive() { {hasScanned && matched.length === 0 && (

- No transfers found + {t('common.noTransfersFound')}

- No stealth transfers matched your keys. + {t('common.noTransfersMatchedKeys')}

)} diff --git a/src/components/SolanaSend.tsx b/src/components/SolanaSend.tsx index 2fea1bd..5679468 100644 --- a/src/components/SolanaSend.tsx +++ b/src/components/SolanaSend.tsx @@ -8,11 +8,13 @@ import { } from '@solana/web3.js'; import { useWallet } from '@solana/wallet-adapter-react'; import { buildSendSol, bytesToHex } from '@wraith-protocol/sdk/chains/solana'; +import { useTranslation } from 'react-i18next'; import { solanaTxUrl, solanaAddrUrl } from '@/lib/explorer'; import { SOLANA_NETWORK } from '@/config'; import { CopyButton } from '@/components/CopyButton'; export function SolanaSend() { + const { t } = useTranslation(); const { publicKey, connected, sendTransaction } = useWallet(); const [recipient, setRecipient] = useState(''); const [amount, setAmount] = useState(''); @@ -28,7 +30,7 @@ export function SolanaSend() { const handleSend = useCallback(async () => { if (!connected || !publicKey) { - setError('Wallet not connected'); + setError(t('common.walletNotConnected')); return; } @@ -37,7 +39,7 @@ export function SolanaSend() { try { if (!recipient.startsWith('st:sol:')) { - setError('Enter a valid Solana meta-address (st:sol:...)'); + setError(t('solana.validMetaAddressError')); setIsPending(false); return; } @@ -74,11 +76,11 @@ export function SolanaSend() { await connection.confirmTransaction(signature, 'confirmed'); setIsSuccess(true); } catch (err) { - setError(err instanceof Error ? err.message : 'Transaction failed'); + setError(err instanceof Error ? err.message : t('common.transactionFailed')); } finally { setIsPending(false); } - }, [connected, publicKey, sendTransaction, recipient, amount]); + }, [connected, publicKey, sendTransaction, recipient, amount, t]); const reset = () => { setRecipient(''); @@ -102,13 +104,13 @@ export function SolanaSend() { return (
- Solana Devnet / SOL + {t('solana.network')}

- Send + {t('solana.sendTitle')}

- Connect your Solana wallet to send stealth payments. + {t('solana.sendConnectPrompt')}

); @@ -118,14 +120,13 @@ export function SolanaSend() {
- Solana Devnet / SOL + {t('solana.network')}

- Send + {t('solana.sendTitle')}

- Send SOL privately using stealth addresses. The recipient gets funds at a fresh address - only they can control. + {t('solana.sendDescription')}

@@ -133,28 +134,28 @@ export function SolanaSend() {
setRecipient(e.target.value)} - placeholder="st:sol:..." + placeholder={t('solana.recipientPlaceholder')} className="h-12 w-full border border-outline-variant bg-surface px-4 pr-20 font-mono text-sm text-primary placeholder:text-outline focus:border-primary" />
- Network fee + {t('common.networkFee')} + + + {t('solana.networkFeeAmount')} - ~5000 lamports
@@ -186,7 +189,7 @@ export function SolanaSend() { disabled={!recipient || !amount || isPending} className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {isPending ? 'Confirm in wallet...' : 'Send Privately'} + {isPending ? t('common.confirmInWallet') : t('common.sendPrivately')}
)} @@ -200,14 +203,14 @@ export function SolanaSend() { )} - {isSuccess ? 'Transfer Complete' : 'Pending'} + {isSuccess ? t('common.transferComplete') : t('common.pending')}
- Stealth Address + {t('common.stealthAddress')}
- Ephemeral Public Key + {t('common.ephemeralPublicKey')}

{bytesToHex(stealthResult.ephemeralPubKey)} @@ -234,7 +237,7 @@ export function SolanaSend() { {txHash && (

- Transaction Hash + {t('common.transactionHash')} diff --git a/src/components/StellarReceive.tsx b/src/components/StellarReceive.tsx index 225aae4..04c6e8b 100644 --- a/src/components/StellarReceive.tsx +++ b/src/components/StellarReceive.tsx @@ -18,6 +18,9 @@ import { STEALTH_SIGNING_MESSAGE, SCHEME_ID, } from '@wraith-protocol/sdk/chains/stellar'; +import type { Announcement, MatchedAnnouncement } from '@wraith-protocol/sdk/chains/stellar'; +import { useTranslation } from 'react-i18next'; +import type { MatchedAnnouncement } from '@wraith-protocol/sdk/chains/stellar'; import type { MatchedAnnouncement, StealthKeys as StellarStealthKeys, @@ -169,6 +172,7 @@ function StellarMatchCardContainer({ showPrivacyWarning: boolean; onDismissPrivacyWarning: () => void; }) { + const { t } = useTranslation(); const { address, signTransaction } = useStellarWallet(); const [balance, setBalance] = useState(null); const [balanceState, setBalanceState] = useState<'loading' | 'loaded' | 'error'>('loading'); @@ -313,7 +317,9 @@ function StellarMatchCardContainer({ const submitData = await submitRes.json(); if (!submitRes.ok) { throw new Error( - submitData.extras?.result_codes?.transaction || submitData.title || 'Transaction failed', + submitData.extras?.result_codes?.transaction || + submitData.title || + t('common.transactionFailed'), ); } @@ -321,6 +327,9 @@ function StellarMatchCardContainer({ updateActivity(txHashHex, 'confirmed'); onWithdrawn(); } catch (err) { + setError(err instanceof Error ? err.message : t('common.transactionFailed')); + setError(err instanceof Error ? err.message : 'Withdraw failed'); + // In a real robust implementation we'd check if we submitted and mark failed setRetryStatus(''); setError( err instanceof RetryExhaustedError ? err.message : err instanceof Error ? err.message : 'Withdraw failed', @@ -510,6 +519,102 @@ function StellarMatchCardContainer({ }; return ( +
+
+ +
+ {loadingBal ? ( + ... + ) : balance && parseFloat(balance) > 0 ? ( + <> + + {balance} XLM + + ) : ( + {t('common.empty')} + )} +
+
+ + {!withdrawHash && balance && parseFloat(balance) > 0 && ( +
+ +
+ setDest(e.target.value)} + placeholder="Destination address (G...)" + className="h-10 flex-1 border border-outline-variant bg-surface px-3 font-mono text-xs text-primary placeholder:text-outline focus:border-primary" + /> + +
+
+ )} + + {error &&

{error}

} + + {withdrawHash && ( +
+ + + {t('common.withdrawn')} —{' '} + + {withdrawHash.slice(0, 14)}... + + +
+ )} + +
+ {!showKey ? ( + + ) : ( +
+
+ + {t('common.stealthKey')} + + +
+ {scalarHex} +
+ )} +
+
+ + {t('stellar.network')} + +

+ {t('stellar.receiveTitle')} +

+

+ {t('stellar.receiveConnectPrompt')} +

+
+ ); + } + + return ( +
+
+ + {t('stellar.network')} + +

+ {t('stellar.receiveTitle')} +

+

+ {t('stellar.receiveDescription')} +

+
+ {!stellarKeys && ( +
+ + {error &&

{error}

} +
+ )} + + {stellarKeys && stellarMetaAddress && ( + <> +
+
+ + {t('common.yourStealthMetaAddress')} + + +
+ + {stellarMetaAddress} + +
+ +
+ + {t('common.onChainRegistration')} + + {registered ? ( +
+ + + {t('common.metaAddressRegistered')} + {regHash && ( + <> + {' — '} + + {regHash.slice(0, 14)}... + + + )} + +
+ ) : ( +
+

+ {t('common.registerMetaAddressHint')} +

+ +
+ )} +
+ +
+ + {hasScanned && ( + + {t('common.transfersFound', { count: matched.length })} + + )} +
+ + {error &&

{error}

} + + {matched.length > 0 && ( const handleExport = () => { const json = exportLabels(); const blob = new Blob([json], { type: 'application/json' }); @@ -1256,6 +1481,21 @@ export function StellarReceive() { )}
+ )} + + {hasScanned && matched.length === 0 && ( +
+

+ {t('common.noTransfersFound')} +

+

+ {t('common.noTransfersMatchedKeys')} +

+
+ )} + + )} +
) : null } /> diff --git a/src/components/StellarSend.tsx b/src/components/StellarSend.tsx index b625e50..df37545 100644 --- a/src/components/StellarSend.tsx +++ b/src/components/StellarSend.tsx @@ -16,6 +16,7 @@ import { decodeStealthMetaAddress, SCHEME_ID, } from '@wraith-protocol/sdk/chains/stellar'; +import { useTranslation } from 'react-i18next'; import { useStellarWallet } from '@/context/StellarWalletContext'; import { STELLAR_NETWORK } from '@/config'; import { StellarSendView } from '@/components/StellarSendView'; @@ -75,6 +76,7 @@ function validateAmount(value: string) { } export function StellarSend() { + const { t } = useTranslation(); const [searchParams] = useSearchParams(); const paramTo = searchParams.get('to'); const paramAmount = searchParams.get('amount'); @@ -383,7 +385,7 @@ export function StellarSend() { setTouched({ recipient: true, amount: true }); if (!address) { - setError('Wallet not connected'); + setError(t('common.walletNotConnected')); return; } @@ -400,6 +402,13 @@ export function StellarSend() { const onRetry = (attempt: number) => setRetryStatus(`Retrying (${attempt}/3)…`); try { + const metaAddress = recipient; + if (!metaAddress.startsWith('st:xlm:')) { + setError(t('stellar.validMetaAddressError')); + setIsPending(false); + return; + } + const decoded = decodeStealthMetaAddress(metaAddress); const result = generateStealthAddress(decoded.spendingPubKey, decoded.viewingPubKey); setStealthResult(result); @@ -479,7 +488,9 @@ export function StellarSend() { const submitData = await submitRes.json(); if (!submitRes.ok) { throw new Error( - submitData.extras?.result_codes?.transaction || submitData.title || 'Transaction failed', + submitData.extras?.result_codes?.transaction || + submitData.title || + t('common.transactionFailed'), ); } @@ -534,6 +545,11 @@ export function StellarSend() { setIsSuccess(true); updateActivity(txHashHex, 'confirmed'); } catch (err) { + setError(err instanceof Error ? err.message : t('common.transactionFailed')); + } finally { + setIsPending(false); + } + }, [address, recipient, amount, signTransaction, t]); setRetryStatus(''); if (txHashHex) updateActivity(txHashHex, 'failed'); setError(err instanceof Error ? err.message : 'Transaction failed'); @@ -568,6 +584,170 @@ export function StellarSend() { } }; + if (!isConnected) { + return ( +
+ + {t('stellar.network')} + +

+ {t('stellar.sendTitle')} +

+

+ {t('stellar.sendConnectPrompt')} +

+
+ ); + } + + return ( +
+
+ + {t('stellar.network')} + +

+ {t('stellar.sendTitle')} +

+

+ {t('stellar.sendDescription')} +

+
+ + {!stealthResult && ( +
+
+ +
+ setRecipient(e.target.value)} + placeholder={t('stellar.recipientPlaceholder')} + className="h-12 w-full border border-outline-variant bg-surface px-4 pr-20 font-mono text-sm text-primary placeholder:text-outline focus:border-primary" + /> + +
+
+ +
+ +
+ setAmount(e.target.value)} + placeholder="0.0" + className="h-12 w-full border border-outline-variant bg-surface px-4 pr-16 font-heading text-2xl text-primary placeholder:text-outline focus:border-primary" + /> + + XLM + +
+
+ +
+
+ + {t('common.networkFee')} + + + {t('stellar.networkFeeAmount')} + +
+
+ + {t('common.announcerContract')} + + + {t('stellar.announcerContractName')} + +
+
+ + {error &&

{error}

} + + +
+ )} + + {stealthResult && ( +
+
+ {isSuccess ? ( + + ) : ( + + )} + + {isSuccess ? t('common.transferComplete') : t('common.pending')} + +
+ +
+
+ + {t('common.stealthAddress')} + + +
+ + {txHash && ( +
+ + {t('common.transactionHash')} + + +
+ )} +
+ + {isSuccess && ( + + )} +
+ )} +
const balanceText = isBalanceLoading || isAwaitingBalance ? 'Checking...' diff --git a/src/components/WalletConnect.tsx b/src/components/WalletConnect.tsx index beedf5c..375d6d2 100644 --- a/src/components/WalletConnect.tsx +++ b/src/components/WalletConnect.tsx @@ -3,6 +3,7 @@ import { ConnectButton } from '@rainbow-me/rainbowkit'; import { useWallet } from '@solana/wallet-adapter-react'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { ccc } from '@ckb-ccc/connector-react'; +import { useTranslation } from 'react-i18next'; import { useChain } from '@/context/ChainContext'; import { useStellarWallet as useStellarWalletContext } from '@/context/StellarWalletContext'; import { useStellarWallet as useStellarWalletHook } from '@/hooks/useStellarWallet'; @@ -14,6 +15,7 @@ import { } from '@/components/FreighterConnectButton'; function HorizenButton() { + const { t } = useTranslation(); return ( {({ account, chain, openConnectModal, openAccountModal, mounted }) => { @@ -27,7 +29,7 @@ function HorizenButton() { > {!connected ? ( ) : ( + + <>
); } diff --git a/src/i18n/en.json b/src/i18n/en.json new file mode 100644 index 0000000..8d22e4c --- /dev/null +++ b/src/i18n/en.json @@ -0,0 +1,139 @@ +{ + "nav": { + "send": "Send", + "receive": "Receive" + }, + "header": { + "menuLabel": "Menu" + }, + "walletConnect": { + "connectWallet": "Connect Wallet", + "connectFreighter": "Connect Freighter" + }, + "copyButton": { + "copy": "Copy", + "copied": "Copied" + }, + "localeSwitcher": { + "label": "Language" + }, + "common": { + "send": "Send", + "receive": "Receive", + "amount": "Amount", + "recipientMetaAddress": "Recipient Meta-Address", + "networkFee": "Network fee", + "announcerContract": "Announcer contract", + "stealthAddress": "Stealth Address", + "transactionHash": "Transaction Hash", + "ephemeralPublicKey": "Ephemeral Public Key", + "stealthKey": "Stealth Key", + "newTransfer": "New Transfer", + "transferComplete": "Transfer Complete", + "pending": "Pending", + "confirming": "Confirming...", + "confirmInWallet": "Confirm in wallet...", + "sendPrivately": "Send Privately", + "paste": "Paste", + "max": "Max", + "paidBySender": "Paid by sender", + "nameNotFound": "Name not found", + "walletNotConnected": "Wallet not connected", + "transactionFailed": "Transaction failed", + "withdrawn": "Withdrawn", + "withdrawTo": "Withdraw to", + "withdraw": "Withdraw", + "revealPrivateKey": "Reveal private key", + "revealSecretKey": "Reveal secret key", + "empty": "Empty", + "yourStealthMetaAddress": "Your Stealth Meta-Address", + "onChainRegistration": "On-Chain Registration", + "metaAddressRegistered": "Meta-address registered on-chain", + "registerMetaAddressHint": "Register your meta-address so senders can look you up by wallet address.", + "registerOnChain": "Register On-Chain", + "registering": "Registering...", + "scanForPayments": "Scan for Payments", + "scanning": "Scanning...", + "transfersFound_one": "{{count}} transfer found", + "transfersFound_other": "{{count}} transfers found", + "noTransfersFound": "No transfers found", + "noTransfersMatchedKeys": "No stealth transfers matched your keys.", + "deriveKeys": "Derive Keys", + "deriveKeysTitle": "Derive Stealth Keys", + "signingInWallet": "Sign in wallet...", + "keyDerivationFailed": "Key derivation failed", + "scanFailed": "Scan failed", + "expectedConfirmation": "Expected confirmation", + "seconds_approx": "~5 seconds", + "balance": "Balance" + }, + "horizen": { + "network": "Horizen Testnet / ETH", + "sendTitle": "Send", + "receiveTitle": "Receive", + "sendConnectPrompt": "Connect your wallet to send stealth payments on Horizen.", + "receiveConnectPrompt": "Connect your wallet to scan for incoming stealth transfers on Horizen.", + "sendDescription": "Send ETH privately using stealth addresses. The recipient gets funds at a fresh address only they can control.", + "receiveDescription": "Derive your stealth keys, register on-chain, then scan for payments.", + "announcerContractName": "WraithSender", + "recipientPlaceholder": "st:eth:0x... or name.wraith" + }, + "stellar": { + "network": "Stellar Testnet / XLM", + "sendTitle": "Send", + "receiveTitle": "Receive", + "sendConnectPrompt": "Connect your Freighter wallet to send stealth payments on Stellar.", + "receiveConnectPrompt": "Connect your Freighter wallet to scan for incoming stealth transfers on Stellar.", + "sendDescription": "Send XLM privately using stealth addresses. The recipient gets funds at a fresh address only they can control.", + "receiveDescription": "Derive your stealth keys, register on-chain, then scan for payments.", + "announcerContractName": "Soroban", + "networkFeeAmount": "100 stroops", + "recipientPlaceholder": "st:xlm:...", + "validMetaAddressError": "Enter a valid Stellar meta-address (st:xlm:...)" + }, + "solana": { + "network": "Solana Devnet / SOL", + "sendTitle": "Send", + "receiveTitle": "Receive", + "sendConnectPrompt": "Connect your Solana wallet to send stealth payments.", + "receiveConnectPrompt": "Connect your Solana wallet to scan for stealth payments.", + "sendDescription": "Send SOL privately using stealth addresses. The recipient gets funds at a fresh address only they can control.", + "receiveDescription": "Derive your stealth keys, then scan for payments on Solana Devnet.", + "networkFeeAmount": "~5000 lamports", + "recipientPlaceholder": "st:sol:...", + "validMetaAddressError": "Enter a valid Solana meta-address (st:sol:...)", + "walletSigningNotSupported": "Wallet does not support message signing", + "destinationPlaceholder": "Destination address (base58)", + "cellsFound_one": "{{count}} cell found", + "cellsFound_other": "{{count}} cells found" + }, + "ckb": { + "network": "CKB Testnet / CKB", + "sendTitle": "Send", + "receiveTitle": "Receive", + "sendConnectPrompt": "Connect your CKB wallet to send stealth payments.", + "receiveConnectPrompt": "Connect your CKB wallet to scan for stealth payments.", + "sendDescription": "Send CKB privately using stealth addresses. The recipient gets funds at a fresh Cell only they can unlock.", + "receiveDescription": "Derive your stealth keys, then scan for stealth Cells on CKB Testnet.", + "amountLabel": "Amount (min 95)", + "networkFeeAmount": "~1000 shannons", + "minCellCapacity": "Min cell capacity", + "minCellCapacityValue": "~94.5 CKB", + "validMetaAddressError": "Enter a valid CKB meta-address (st:ckb:...)", + "minAmountError": "Minimum amount is 95 CKB. Stealth cells require at least ~94.5 CKB for cell capacity.", + "connectWalletFirst": "Connect your CKB wallet first", + "recipientPlaceholder": "st:ckb:...", + "stealthHash": "Stealth Hash", + "stealthPublicKey": "Stealth Public Key", + "cell": "Cell", + "withdraw": "Withdraw", + "withdrawInstruction": "Use the private key below to sign a CKB transaction consuming this Cell.", + "scanForCells": "Scan for Cells", + "scanning": "Scanning...", + "noCellsFound": "No cells found", + "noCellsMatchedKeys": "No stealth Cells matched your keys.", + "deriveStealthKeys": "Derive Stealth Keys", + "cellsFound_one": "{{count}} cell found", + "cellsFound_other": "{{count}} cells found" + } +} diff --git a/src/i18n/es.json b/src/i18n/es.json new file mode 100644 index 0000000..00aa9e7 --- /dev/null +++ b/src/i18n/es.json @@ -0,0 +1,139 @@ +{ + "nav": { + "send": "Enviar", + "receive": "Recibir" + }, + "header": { + "menuLabel": "Menú" + }, + "walletConnect": { + "connectWallet": "Conectar billetera", + "connectFreighter": "Conectar Freighter" + }, + "copyButton": { + "copy": "Copiar", + "copied": "Copiado" + }, + "localeSwitcher": { + "label": "Idioma" + }, + "common": { + "send": "Enviar", + "receive": "Recibir", + "amount": "Monto", + "recipientMetaAddress": "Meta-dirección del destinatario", + "networkFee": "Tarifa de red", + "announcerContract": "Contrato anunciador", + "stealthAddress": "Dirección sigilosa", + "transactionHash": "Hash de transacción", + "ephemeralPublicKey": "Clave pública efímera", + "stealthKey": "Clave sigilosa", + "newTransfer": "Nueva transferencia", + "transferComplete": "Transferencia completada", + "pending": "Pendiente", + "confirming": "Confirmando...", + "confirmInWallet": "Confirmar en la billetera...", + "sendPrivately": "Enviar de forma privada", + "paste": "Pegar", + "max": "Máx", + "paidBySender": "Pagado por el remitente", + "nameNotFound": "Nombre no encontrado", + "walletNotConnected": "Billetera no conectada", + "transactionFailed": "Transacción fallida", + "withdrawn": "Retirado", + "withdrawTo": "Retirar a", + "withdraw": "Retirar", + "revealPrivateKey": "Revelar clave privada", + "revealSecretKey": "Revelar clave secreta", + "empty": "Vacío", + "yourStealthMetaAddress": "Tu meta-dirección sigilosa", + "onChainRegistration": "Registro en cadena", + "metaAddressRegistered": "Meta-dirección registrada en cadena", + "registerMetaAddressHint": "Registra tu meta-dirección para que los remitentes puedan encontrarte por tu dirección de billetera.", + "registerOnChain": "Registrar en cadena", + "registering": "Registrando...", + "scanForPayments": "Buscar pagos", + "scanning": "Buscando...", + "transfersFound_one": "{{count}} transferencia encontrada", + "transfersFound_other": "{{count}} transferencias encontradas", + "noTransfersFound": "No se encontraron transferencias", + "noTransfersMatchedKeys": "Ninguna transferencia sigilosa coincidió con tus claves.", + "deriveKeys": "Derivar claves", + "deriveKeysTitle": "Derivar claves sigilosas", + "signingInWallet": "Firmando en la billetera...", + "keyDerivationFailed": "Error al derivar claves", + "scanFailed": "Error al escanear", + "expectedConfirmation": "Confirmación esperada", + "seconds_approx": "~5 segundos", + "balance": "Saldo" + }, + "horizen": { + "network": "Horizen Testnet / ETH", + "sendTitle": "Enviar", + "receiveTitle": "Recibir", + "sendConnectPrompt": "Conecta tu billetera para enviar pagos sigilosos en Horizen.", + "receiveConnectPrompt": "Conecta tu billetera para buscar transferencias sigilosas entrantes en Horizen.", + "sendDescription": "Envía ETH de forma privada usando direcciones sigilosas. El destinatario recibe fondos en una dirección nueva que solo él puede controlar.", + "receiveDescription": "Deriva tus claves sigilosas, regístrate en cadena y luego busca pagos.", + "announcerContractName": "WraithSender", + "recipientPlaceholder": "st:eth:0x... o nombre.wraith" + }, + "stellar": { + "network": "Stellar Testnet / XLM", + "sendTitle": "Enviar", + "receiveTitle": "Recibir", + "sendConnectPrompt": "Conecta tu billetera Freighter para enviar pagos sigilosos en Stellar.", + "receiveConnectPrompt": "Conecta tu billetera Freighter para buscar transferencias sigilosas entrantes en Stellar.", + "sendDescription": "Envía XLM de forma privada usando direcciones sigilosas. El destinatario recibe fondos en una dirección nueva que solo él puede controlar.", + "receiveDescription": "Deriva tus claves sigilosas, regístrate en cadena y luego busca pagos.", + "announcerContractName": "Soroban", + "networkFeeAmount": "100 stroops", + "recipientPlaceholder": "st:xlm:...", + "validMetaAddressError": "Ingresa una meta-dirección Stellar válida (st:xlm:...)" + }, + "solana": { + "network": "Solana Devnet / SOL", + "sendTitle": "Enviar", + "receiveTitle": "Recibir", + "sendConnectPrompt": "Conecta tu billetera Solana para enviar pagos sigilosos.", + "receiveConnectPrompt": "Conecta tu billetera Solana para buscar pagos sigilosos.", + "sendDescription": "Envía SOL de forma privada usando direcciones sigilosas. El destinatario recibe fondos en una dirección nueva que solo él puede controlar.", + "receiveDescription": "Deriva tus claves sigilosas y luego busca pagos en Solana Devnet.", + "networkFeeAmount": "~5000 lamports", + "recipientPlaceholder": "st:sol:...", + "validMetaAddressError": "Ingresa una meta-dirección Solana válida (st:sol:...)", + "walletSigningNotSupported": "La billetera no admite firma de mensajes", + "destinationPlaceholder": "Dirección destino (base58)", + "cellsFound_one": "{{count}} celda encontrada", + "cellsFound_other": "{{count}} celdas encontradas" + }, + "ckb": { + "network": "CKB Testnet / CKB", + "sendTitle": "Enviar", + "receiveTitle": "Recibir", + "sendConnectPrompt": "Conecta tu billetera CKB para enviar pagos sigilosos.", + "receiveConnectPrompt": "Conecta tu billetera CKB para buscar pagos sigilosos.", + "sendDescription": "Envía CKB de forma privada usando direcciones sigilosas. El destinatario recibe fondos en una nueva celda que solo él puede desbloquear.", + "receiveDescription": "Deriva tus claves sigilosas y luego busca celdas sigilosas en CKB Testnet.", + "amountLabel": "Monto (mín. 95)", + "networkFeeAmount": "~1000 shannons", + "minCellCapacity": "Capacidad mínima de celda", + "minCellCapacityValue": "~94.5 CKB", + "validMetaAddressError": "Ingresa una meta-dirección CKB válida (st:ckb:...)", + "minAmountError": "El monto mínimo es 95 CKB. Las celdas sigilosas requieren al menos ~94.5 CKB de capacidad.", + "connectWalletFirst": "Primero conecta tu billetera CKB", + "recipientPlaceholder": "st:ckb:...", + "stealthHash": "Hash sigiloso", + "stealthPublicKey": "Clave pública sigilosa", + "cell": "Celda", + "withdraw": "Retirar", + "withdrawInstruction": "Usa la clave privada a continuación para firmar una transacción CKB que consuma esta celda.", + "scanForCells": "Buscar celdas", + "scanning": "Buscando...", + "noCellsFound": "No se encontraron celdas", + "noCellsMatchedKeys": "Ninguna celda sigilosa coincidió con tus claves.", + "deriveStealthKeys": "Derivar claves sigilosas", + "cellsFound_one": "{{count}} celda encontrada", + "cellsFound_other": "{{count}} celdas encontradas" + } +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..cafa831 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,28 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import en from './en.json'; +import es from './es.json'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: en }, + es: { translation: es }, + }, + fallbackLng: 'en', + supportedLngs: ['en', 'es'], + interpolation: { + escapeValue: false, + }, + detection: { + // Check localStorage first, then navigator.language + order: ['localStorage', 'navigator'], + caches: ['localStorage'], + lookupLocalStorage: 'wraith-locale', + }, + }); + +export default i18n; diff --git a/src/main.tsx b/src/main.tsx index 9b02cd5..1da37f3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,8 @@ import { Buffer } from 'buffer'; (window as unknown as Record).Buffer = Buffer; +import '@/i18n'; +import { StrictMode, useState, useMemo, type CSSProperties } from 'react'; import { StrictMode, useState, useMemo, useEffect, type CSSProperties } from 'react'; import { createRoot } from 'react-dom/client';