diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec7f628..cc27c24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,31 @@ on: branches: [main, develop] jobs: + i18n-check: + name: i18n – missing key gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Enable corepack (installs pnpm from packageManager field) + run: | + corepack enable + corepack prepare pnpm@10.28.2 --activate + pnpm --version + + - run: pnpm install --frozen-lockfile + + - name: Check i18n keys (EN vs ES, PT, FR) + run: pnpm i18n:check + build: + name: Build & type-check runs-on: ubuntu-latest + needs: i18n-check steps: - uses: actions/checkout@v4 diff --git a/package.json b/package.json index d164f46..a9d3a82 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test": "playwright test", "test:ui": "playwright test --ui", "test:unit": "vitest run", + "i18n:check": "node scripts/check-i18n.js", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test-storybook": "test-storybook" diff --git a/scripts/check-i18n.js b/scripts/check-i18n.js new file mode 100644 index 0000000..5e10b88 --- /dev/null +++ b/scripts/check-i18n.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * scripts/check-i18n.js + * + * CI gate: verifies that every key present in the reference locale (EN) + * exists in all other supported locales (ES, PT, FR). + * + * Exit code 0 → all locales are complete. + * Exit code 1 → one or more locales have missing keys. + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const I18N_DIR = join(__dirname, '..', 'src', 'i18n'); + +const REFERENCE_LOCALE = 'en'; +const LOCALES_TO_CHECK = ['es', 'pt', 'fr']; + +/** + * Recursively collect all dot-separated key paths from a nested object. + * @param {object} obj + * @param {string} prefix + * @returns {string[]} + */ +function collectKeys(obj, prefix = '') { + const keys = []; + for (const [k, v] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${k}` : k; + if (v !== null && typeof v === 'object' && !Array.isArray(v)) { + keys.push(...collectKeys(v, fullKey)); + } else { + keys.push(fullKey); + } + } + return keys; +} + +function loadJson(locale) { + const filePath = join(I18N_DIR, `${locale}.json`); + try { + return JSON.parse(readFileSync(filePath, 'utf-8')); + } catch (err) { + console.error(`❌ Could not read ${locale}.json: ${err.message}`); + process.exit(1); + } +} + +const referenceData = loadJson(REFERENCE_LOCALE); +const referenceKeys = new Set(collectKeys(referenceData)); + +let hasErrors = false; + +for (const locale of LOCALES_TO_CHECK) { + const data = loadJson(locale); + const localeKeys = new Set(collectKeys(data)); + + const missing = [...referenceKeys].filter((k) => !localeKeys.has(k)); + const extra = [...localeKeys].filter((k) => !referenceKeys.has(k)); + + if (missing.length === 0 && extra.length === 0) { + console.log(`✅ ${locale}.json — complete (${localeKeys.size} keys)`); + } else { + hasErrors = true; + if (missing.length > 0) { + console.error(`\n❌ ${locale}.json — ${missing.length} missing key(s):`); + for (const key of missing) { + console.error(` - ${key}`); + } + } + if (extra.length > 0) { + const count = extra.length; + console.warn( + `\n⚠️ ${locale}.json — ${count} extra key(s) not in EN (ignored at runtime):`, + ); + for (const key of extra) { + console.warn(` + ${key}`); + } + } + } +} + +if (hasErrors) { + console.error('\n❌ i18n check failed — add missing keys before merging.\n'); + process.exit(1); +} else { + console.log('\n✅ All locales are complete.\n'); + process.exit(0); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index ed807d3..08ff212 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,12 +6,6 @@ import { WalletConnect } from './WalletConnect'; import { LocaleSwitcher } from './LocaleSwitcher'; import { useTheme } from '@/context/ThemeContext'; -const navLinks = [ - { to: '/send', label: 'Send' }, - { to: '/receive', label: 'Receive' }, - { to: '/vault', label: 'Vault' }, -]; - export function Header() { const location = useLocation(); const { t } = useTranslation(); diff --git a/src/components/LocaleSwitcher.tsx b/src/components/LocaleSwitcher.tsx index 1230af0..d2f2031 100644 --- a/src/components/LocaleSwitcher.tsx +++ b/src/components/LocaleSwitcher.tsx @@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next'; const LOCALES: { code: string; label: string }[] = [ { code: 'en', label: 'EN' }, { code: 'es', label: 'ES' }, + { code: 'pt', label: 'PT' }, + { code: 'fr', label: 'FR' }, ]; export function LocaleSwitcher() { diff --git a/src/components/StellarHistory.tsx b/src/components/StellarHistory.tsx index 9138fd9..d533d08 100644 --- a/src/components/StellarHistory.tsx +++ b/src/components/StellarHistory.tsx @@ -1,9 +1,11 @@ import { useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useStellarWallet } from '@/context/StellarWalletContext'; import { stellarTxUrl } from '@/lib/explorer'; import { useActivityStore, ActivityKind, ActivityStatus } from '@/stores/activityStore'; export function StellarHistory() { + const { t } = useTranslation(); const { address, isConnected } = useStellarWallet(); const { entries, clearHistory, pollPending } = useActivityStore(); @@ -37,10 +39,10 @@ export function StellarHistory() { return (

- History + {t('stellar.historyTitle')}

- Connect your Stellar wallet to view your transaction history. + {t('stellar.connectHistoryPrompt')}

); @@ -51,61 +53,61 @@ export function StellarHistory() {
- Stellar Testnet / XLM + {t('stellar.network')}

- Activity History + {t('stellar.historyTitle')}

{walletEntries.length === 0 && ( -

No activity recorded yet.

+

{t('stellar.noActivityYet')}

)} {walletEntries.length > 0 && filteredEntries.length === 0 && (

- No activity matches the filters. + {t('stellar.noActivityMatchesFilter')}

)} @@ -138,17 +140,17 @@ export function StellarHistory() {
- Direction + {t('stellar.direction')} - {tx.direction === 'in' ? 'Incoming (Received)' : 'Outgoing (Sent)'} + {tx.direction === 'in' ? t('stellar.directionIn') : t('stellar.directionOut')}
{tx.amount && (
- Amount + {t('common.amount')} {tx.amount} XLM
@@ -157,7 +159,7 @@ export function StellarHistory() { {tx.recipient && (
- Recipient / Address + {t('stellar.recipientAddress')} - Hash + {t('stellar.hash')} parseFloat(b) > 0); const assetBalances = STELLAR_ASSETS.map((asset) => { @@ -338,7 +340,7 @@ export function StellarMatchCard({
- Stealth Address + {t('common.stealthAddress')}
@@ -377,7 +379,7 @@ export function StellarMatchCard({ {!withdrawHash && hasAnyBalance && (
setVaultPassphrase(e.target.value)} - placeholder="Unlock the vault" + placeholder={t('stellar.passphrasePlaceholder')} className="h-12 w-full border border-outline-variant bg-surface px-4 font-mono text-sm text-primary placeholder:text-outline focus:border-primary" />
@@ -980,14 +980,14 @@ export function StellarReceive() { disabled={busy || !vaultPassphrase} className="h-11 bg-primary px-4 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {vaultBusy === 'saving' ? 'Saving...' : title} + {vaultBusy === 'saving' ? t('stellar.saving') : title} ) : ( @@ -996,7 +996,7 @@ export function StellarReceive() { disabled={busy || !vaultPassphrase} className="h-11 bg-primary px-4 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30" > - {vaultBusy === 'unlocking' ? 'Unlocking...' : title} + {vaultBusy === 'unlocking' ? t('stellar.unlocking') : title} )}
@@ -1304,7 +1304,7 @@ export function StellarReceive() {
setSearchQuery(e.target.value)} className="h-12 w-full border border-outline-variant bg-surface px-4 font-body text-sm text-on-surface placeholder:text-outline focus:border-primary" @@ -1312,7 +1312,7 @@ export function StellarReceive() { {filteredMatches.length === 0 && (
- No matching transfers found for "{searchQuery}" + {t('stellar.noMatchingTransfersFor', { query: searchQuery })}
)} @@ -1365,7 +1365,7 @@ export function StellarReceive() { onClick={() => setVisibleCount((v) => v + 25)} className="mt-2 h-10 w-full border border-outline-variant font-heading text-[11px] font-semibold uppercase tracking-widest text-primary transition-colors hover:bg-surface-bright" > - Show 25 more + {t('stellar.showMore')} )}
diff --git a/src/components/StellarReceiveView.tsx b/src/components/StellarReceiveView.tsx index 2ca4f3a..3646549 100644 --- a/src/components/StellarReceiveView.tsx +++ b/src/components/StellarReceiveView.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; import { stellarTxUrl } from '@/lib/explorer'; import { CopyButton } from '@/components/CopyButton'; import { StellarPaymentLink } from '@/components/StellarPaymentLink'; @@ -87,17 +88,19 @@ export function StellarReceiveView({ onFireTestNotification, onShowQR, }: StellarReceiveViewProps) { + const { t } = useTranslation(); + if (!isConnected) { return (
- Stellar Testnet / XLM + {t('stellar.network')}

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

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

); @@ -107,13 +110,13 @@ export function StellarReceiveView({
- Stellar Testnet / XLM + {t('stellar.network')}

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

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

@@ -124,7 +127,7 @@ export function StellarReceiveView({ 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')} {retryStatus &&

{retryStatus}

} {error &&

{error}

} @@ -137,15 +140,15 @@ export function StellarReceiveView({
- Your Stealth Meta-Address + {t('common.yourStealthMetaAddress')}
{onShowQR && (
)} @@ -222,7 +225,7 @@ export function StellarReceiveView({
- Background Notifications + {t('stellar.backgroundNotifications')} )}
) : (

- Enable notifications to receive alerts about incoming stealth payments even when - the tab is closed. + {t('stellar.notificationsDisabledDesc')}

)}
@@ -278,11 +277,11 @@ export function StellarReceiveView({ disabled={isScanning} className="h-12 w-full bg-primary px-6 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30 sm:w-auto" > - {isScanning ? 'Scanning...' : 'Scan for Payments'} + {isScanning ? t('common.scanning') : t('common.scanForPayments')} {hasScanned && ( - {matchCount} transfer{matchCount !== 1 ? 's' : ''} found + {t('common.transfersFound', { count: matchCount })} )}
@@ -315,7 +314,7 @@ export function StellarReceiveView({ type="text" value={searchQuery ?? ''} onChange={(e) => onSearchChange(e.target.value)} - placeholder="Search by label, tag, or address..." + placeholder={t('stellar.searchPlaceholder')} className="h-9 w-full border border-outline-variant bg-surface pl-8 pr-3 font-body text-xs text-on-surface placeholder:text-outline focus:border-primary" />
@@ -323,7 +322,7 @@ export function StellarReceiveView({ )} {onImport && ( )}
@@ -375,7 +374,7 @@ export function StellarReceiveView({ {allTags && allTags.length > 0 && (
- Tags: + {t('stellar.tags')} {allTags.map((tag) => ( )}
@@ -431,7 +430,9 @@ export function StellarReceiveView({ )} - {showHidden ? `Hide archived (${hiddenCount})` : `Show hidden (${hiddenCount})`} + {showHidden + ? t('stellar.hideArchived', { count: hiddenCount }) + : t('stellar.showHidden', { count: hiddenCount })} )}
@@ -443,10 +444,10 @@ export function StellarReceiveView({ {hasScanned && matchCount > 0 && filteredMatchCount === 0 && (

- No matching transfers + {t('stellar.noMatchingTransfers')}

- Try adjusting your search or filters. + {t('stellar.tryAdjustingFilters')}

)} @@ -454,10 +455,10 @@ export function StellarReceiveView({ {hasScanned && matchCount === 0 && (

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

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

)} diff --git a/src/components/StellarSendView.tsx b/src/components/StellarSendView.tsx index 0aec389..8ae5131 100644 --- a/src/components/StellarSendView.tsx +++ b/src/components/StellarSendView.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; import { stellarTxUrl, stellarAddrUrl } from '@/lib/explorer'; import { CopyButton } from '@/components/CopyButton'; import type { StellarAssetKey } from '@/lib/stellar/assets'; @@ -93,17 +94,19 @@ export function StellarSendView({ isScanningQR = false, scannerElement, }: StellarSendViewProps) { + const { t } = useTranslation(); + if (!isConnected) { return (
- Stellar Testnet / {assetKey} + {t('stellar.network')} / {assetKey}

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

- Connect your Freighter wallet to send stealth payments on Stellar. + {t('stellar.sendConnectPrompt')}

); @@ -113,14 +116,13 @@ export function StellarSendView({
- Stellar Testnet / {assetKey} + {t('stellar.network')} / {assetKey}

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

- Send {assetKey} privately using stealth addresses. The recipient gets funds at a fresh - address only they can control. + {t('stellar.sendDescription', { asset: assetKey })}

@@ -128,7 +130,7 @@ export function StellarSendView({
@@ -148,7 +150,7 @@ export function StellarSendView({ {onScanQRClick && ( )}
)} @@ -191,7 +193,7 @@ export function StellarSendView({ htmlFor="stellar-asset" className="font-mono text-[10px] uppercase tracking-widest text-outline" > - Asset + {t('stellar.asset')}
- Network fee + {t('common.networkFee')} + + + {t('stellar.networkFeeAmount')} - 100 stroops
- Announcer contract + {t('common.announcerContract')} + + + {t('stellar.announcerContractName')} - Soroban
- Source balance + {t('stellar.sourceBalance')}

- Predicted transfer + {t('stellar.predictedTransfer')}

- Simulating Soroban pre-flight... + {t('stellar.simulatingSoroban')}

)} @@ -293,16 +299,16 @@ export function StellarSendView({

- Predicted transfer + {t('stellar.predictedTransfer')}

- Predicted + {t('stellar.predicted')}
- Predicted fee + {t('stellar.predictedFee')} {simulationFee} @@ -310,7 +316,7 @@ export function StellarSendView({
- Predicted return value + {t('stellar.predictedReturnValue')} {simulationReturnValue} @@ -318,7 +324,7 @@ export function StellarSendView({
- Predicted contract events + {t('stellar.predictedContractEvents')} {simulationEvents.length > 0 ? (
    @@ -332,7 +338,7 @@ export function StellarSendView({ ))}
) : ( -

None

+

{t('stellar.none')}

)}
@@ -369,7 +375,7 @@ export function StellarSendView({ disabled={!canSubmit} 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 ${assetKey}`} + {isPending ? t('common.confirmInWallet') : t('stellar.sendAsset', { asset: assetKey })}
)} @@ -383,7 +389,7 @@ export function StellarSendView({ )} - {isSuccess ? 'Final Transfer' : 'Pending Transfer'} + {isSuccess ? t('stellar.finalTransfer') : t('stellar.pendingTransfer')} {assetKey}
@@ -391,7 +397,7 @@ export function StellarSendView({
- Stealth Address + {t('common.stealthAddress')}
- Final Transaction Hash + {t('stellar.finalTransactionHash')} diff --git a/src/components/StellarVault.tsx b/src/components/StellarVault.tsx index 525531f..af95917 100644 --- a/src/components/StellarVault.tsx +++ b/src/components/StellarVault.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useStellarWallet } from '@/context/StellarWalletContext'; import { StellarVaultDeposit } from './StellarVaultDeposit'; import { StellarVaultClaim } from './StellarVaultClaim'; @@ -7,6 +8,7 @@ import { VaultStatusTable } from './VaultStatusTable'; type VaultTab = 'deposit' | 'claim' | 'status'; export function StellarVault() { + const { t } = useTranslation(); const { isConnected } = useStellarWallet(); const [activeTab, setActiveTab] = useState('deposit'); @@ -14,13 +16,13 @@ export function StellarVault() { return (
- Stellar Testnet / XLM + {t('stellar.network')}

- Stealth Vault + {t('stellar.vaultTitle')}

- Connect your Freighter wallet to use the stealth vault for time-locked deposits. + {t('stellar.vaultConnectPrompt')}

); @@ -30,13 +32,13 @@ export function StellarVault() {
- Stellar Testnet / XLM + {t('stellar.network')}

- Stealth Vault + {t('stellar.vaultTitle')}

- Create time-locked deposits, claim after unlock, or refund after the refund window. + {t('stellar.vaultDescription')}

@@ -49,7 +51,7 @@ export function StellarVault() { : 'border-b-2 border-transparent text-outline hover:text-on-surface-variant' }`} > - Create Deposit + {t('stellar.createDeposit')}
diff --git a/src/components/StellarVaultClaim.tsx b/src/components/StellarVaultClaim.tsx index 29869ff..0eaa989 100644 --- a/src/components/StellarVaultClaim.tsx +++ b/src/components/StellarVaultClaim.tsx @@ -1,4 +1,5 @@ import { useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useStellarWallet } from '@/context/StellarWalletContext'; import { stellarTxUrl } from '@/lib/explorer'; import { CopyButton } from '@/components/CopyButton'; @@ -35,6 +36,7 @@ type VaultDeposit = { }; export function StellarVaultClaim() { + const { t } = useTranslation(); const { address, signMessage } = useStellarWallet(); const [deposits, setDeposits] = useState(MOCK_DEPOSITS); const [claimingId, setClaimingId] = useState(null); @@ -45,7 +47,7 @@ export function StellarVaultClaim() { const handleClaim = useCallback( async (depositId: string) => { if (!address) { - setError('Wallet not connected'); + setError(t('common.walletNotConnected')); return; } @@ -74,13 +76,13 @@ export function StellarVaultClaim() { prev.map((d) => (d.id === depositId ? { ...d, state: 'claimed' as const } : d)), ); } catch (err) { - setError(err instanceof Error ? err.message : 'Claim failed'); + setError(err instanceof Error ? err.message : t('common.transactionFailed')); setClaimState('idle'); } finally { setClaimingId(null); } }, - [address, signMessage], + [address, signMessage, t], ); const reset = () => { @@ -95,10 +97,10 @@ export function StellarVaultClaim() { return (

- Connect Wallet + {t('stellar.connectWalletTitle')}

- Connect your Freighter wallet to claim vault deposits. + {t('stellar.connectWalletVaultPrompt')}

); @@ -110,14 +112,14 @@ export function StellarVaultClaim() {
- Claim Successful + {t('stellar.claimSuccessful')}
- Transaction Hash + {t('common.transactionHash')} ); @@ -147,10 +149,10 @@ export function StellarVaultClaim() { return (

- No Claimable Deposits + {t('stellar.noClaimableDeposits')}

- No pending vault deposits found for your address. + {t('stellar.noClaimableDepositsDesc')}

); @@ -170,13 +172,13 @@ export function StellarVaultClaim() {
- Pending + {t('stellar.pendingBadge')}
- Deposit ID + {t('stellar.depositId')}
{deposit.id} @@ -186,7 +188,7 @@ export function StellarVaultClaim() {
- Amount + {t('common.amount')}
{deposit.amount} XLM @@ -195,7 +197,7 @@ export function StellarVaultClaim() {
- Unlock Ledger + {t('stellar.unlockLedger')}
{deposit.unlockLedger.toLocaleString()} @@ -204,10 +206,12 @@ export function StellarVaultClaim() {
- Refund Window + {t('stellar.refundWindow')}
- {deposit.refundWindow.toLocaleString()} ledgers + {t('stellar.refundWindowLedgers', { + value: deposit.refundWindow.toLocaleString(), + })}
@@ -220,9 +224,9 @@ export function StellarVaultClaim() { > {claimingId === deposit.id ? claimState === 'signing' - ? 'Signing...' - : 'Claiming...' - : 'Claim'} + ? t('stellar.signing') + : t('stellar.claiming') + : t('stellar.claimButton')}
))} diff --git a/src/components/StellarVaultDeposit.tsx b/src/components/StellarVaultDeposit.tsx index de7e78f..c9b71a8 100644 --- a/src/components/StellarVaultDeposit.tsx +++ b/src/components/StellarVaultDeposit.tsx @@ -1,4 +1,5 @@ import { useState, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { decodeStealthMetaAddress } from '@wraith-protocol/sdk/chains/stellar'; import { useStellarWallet } from '@/context/StellarWalletContext'; import { CopyButton } from '@/components/CopyButton'; @@ -53,6 +54,7 @@ function validateRefundWindow(value: string) { } export function StellarVaultDeposit() { + const { t } = useTranslation(); const { address } = useStellarWallet(); const [recipient, setRecipient] = useState(''); const [amount, setAmount] = useState(''); @@ -101,7 +103,7 @@ export function StellarVaultDeposit() { setTouched({ recipient: true, amount: true, unlockLedger: true, refundWindow: true }); if (!address) { - setError('Wallet not connected'); + setError(t('common.walletNotConnected')); return; } @@ -126,7 +128,7 @@ export function StellarVaultDeposit() { setTxHash(simulatedTxHash); setDepositState('success'); } catch (err) { - setError(err instanceof Error ? err.message : 'Deposit failed'); + setError(err instanceof Error ? err.message : t('common.transactionFailed')); setDepositState('idle'); } }, [ @@ -137,6 +139,7 @@ export function StellarVaultDeposit() { refundWindowValue, canSubmit, validationError, + t, ]); const reset = () => { @@ -158,7 +161,7 @@ export function StellarVaultDeposit() { <>
setTouched((prev) => ({ ...prev, recipient: true }))} aria-invalid={!!recipientError} aria-describedby="vault-recipient-error" - placeholder="st:xlm:..." + placeholder={t('stellar.recipientPlaceholder')} className="h-12 w-full border border-outline-variant bg-surface px-4 font-mono text-sm text-primary placeholder:text-outline focus:border-primary" />

@@ -177,7 +180,7 @@ export function StellarVaultDeposit() {

- Contract + {t('stellar.vaultContract')} - Stealth Vault (Coming Soon) + {t('stellar.vaultContractValue')}
@@ -259,7 +262,7 @@ export function StellarVaultDeposit() { disabled={!canSubmit} 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" > - {'Create Deposit'} + {t('stellar.createDepositButton')} )} @@ -269,14 +272,14 @@ export function StellarVaultDeposit() {
- Deposit Created + {t('stellar.depositCreated')}
- Deposit ID + {t('stellar.depositId')}
{depositId} @@ -287,7 +290,7 @@ export function StellarVaultDeposit() { {txHash && (
- Transaction Hash + {t('common.transactionHash')}
{txHash} @@ -298,24 +301,24 @@ export function StellarVaultDeposit() {
- Amount + {t('common.amount')}
{amountValue} XLM
- Unlock Ledger + {t('stellar.unlockLedger')}
{unlockLedgerValue}
- Refund Window + {t('stellar.refundWindow')}
- {refundWindowValue} ledgers after unlock + {t('stellar.refundWindowAfterUnlock', { value: refundWindowValue })}
@@ -324,7 +327,7 @@ export function StellarVaultDeposit() { onClick={reset} className="h-11 w-full border border-outline-variant font-heading text-[13px] font-semibold uppercase tracking-widest text-primary transition-colors hover:bg-surface-bright" > - New Deposit + {t('stellar.newDeposit')}
)} diff --git a/src/components/StellarWalletButton.tsx b/src/components/StellarWalletButton.tsx index 328c9c8..29f2aa2 100644 --- a/src/components/StellarWalletButton.tsx +++ b/src/components/StellarWalletButton.tsx @@ -10,6 +10,7 @@ */ import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { WALLET_META } from '@/wallets/stellar'; import type { StellarWalletState } from '@/hooks/useStellarWallet'; @@ -23,12 +24,13 @@ function truncate(key: string): string { } export function StellarWalletButton({ state }: Props) { + const { t } = useTranslation(); const { status, walletId, publicKey, openPicker, disconnect } = state; const [showMenu, setShowMenu] = useState(false); if (status === 'connecting') { return ( -
+
- Connecting… + {t('stellar.walletConnecting')}
); } @@ -53,7 +55,7 @@ export function StellarWalletButton({ state }: Props) { {showMenu && ( -
-
-

Connected via

-

{meta.name}

+
+
+

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

+

{meta.name}

)} @@ -100,12 +104,12 @@ export function StellarWalletButton({ state }: Props) { ); } diff --git a/src/components/StellarWalletPicker.tsx b/src/components/StellarWalletPicker.tsx index b382e2f..94b5897 100644 --- a/src/components/StellarWalletPicker.tsx +++ b/src/components/StellarWalletPicker.tsx @@ -15,6 +15,7 @@ */ import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { QRCodeSVG as QRCode } from 'qrcode.react'; import { WALLET_IDS, WALLET_META, type WalletId } from '@/wallets/stellar'; import type { StellarWalletState } from '@/hooks/useStellarWallet'; @@ -81,6 +82,8 @@ export function StellarWalletPicker({ state }: Props) { if (!pickerOpen) return null; + const { t } = useTranslation(); + async function handleSelect(id: WalletId) { if (pending) return; setPending(id); @@ -134,16 +137,18 @@ export function StellarWalletPicker({ state }: Props) { className="fixed inset-0 z-50 flex items-center justify-center bg-black/70" role="dialog" aria-modal="true" - aria-label="Connect Stellar wallet" + aria-label={t('stellar.walletPickerTitle')} >
{/* Header */}
-

Connect Stellar wallet

+

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

@@ -274,15 +280,17 @@ export function StellarWalletPicker({ state }: Props) { className="fixed inset-0 z-[60] flex items-center justify-center bg-black/80" role="dialog" aria-modal="true" - aria-label="Scan WalletConnect QR code" + aria-label={t('stellar.scanWithWalletConnect')} >
{status === 'connected' && (
-

Wallet connected successfully!

+

{t('stellar.walletConnectedSuccess')}

)} diff --git a/src/i18n/en.json b/src/i18n/en.json index 1085a11..43af1f2 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -90,7 +90,130 @@ "announcerContractName": "Soroban", "networkFeeAmount": "100 stroops", "recipientPlaceholder": "st:xlm:...", - "validMetaAddressError": "Enter a valid Stellar meta-address (st:xlm:...)" + "validMetaAddressError": "Enter a valid Stellar meta-address (st:xlm:...)", + "asset": "Asset", + "memo": "Memo (optional)", + "sourceBalance": "Source balance", + "predictedTransfer": "Predicted transfer", + "simulatingSoroban": "Simulating Soroban pre-flight...", + "predicted": "Predicted", + "predictedFee": "Predicted fee", + "predictedReturnValue": "Predicted return value", + "predictedContractEvents": "Predicted contract events", + "none": "None", + "sendAsset": "Send {{asset}}", + "finalTransfer": "Final Transfer", + "pendingTransfer": "Pending Transfer", + "finalTransactionHash": "Final Transaction Hash", + "scan": "Scan", + "showQrAriaLabel": "Show QR Code for Meta-Address", + "scanQrAriaLabel": "Scan QR Code", + "backgroundNotifications": "Background Notifications", + "privacyDisclosure": "Privacy Disclosure", + "privacyDisclosureText": "Your viewing key is stored encrypted in IndexedDB using your wallet-derived key. The service worker periodically scans for new payments and shows notifications. You can disable this feature at any time.", + "testNotification": "Test Notification", + "notificationsEnabledDesc": "Receive notifications when new stealth payments are detected, even when the tab is closed.", + "notificationsDisabledDesc": "Enable notifications to receive alerts about incoming stealth payments even when the tab is closed.", + "noMatchingTransfers": "No matching transfers", + "tryAdjustingFilters": "Try adjusting your search or filters.", + "searchPlaceholder": "Search by label, tag, or address...", + "transfersSearchPlaceholder": "Search by address or amount...", + "noMatchingTransfersFor": "No matching transfers found for \"{{query}}\"", + "export": "Export", + "import": "Import", + "tags": "Tags:", + "clear": "Clear", + "showHidden": "Show hidden ({{count}})", + "hideArchived": "Hide archived ({{count}})", + "showMore": "Show 25 more", + "connectHistoryPrompt": "Connect your Stellar wallet to view your transaction history.", + "historyTitle": "Activity History", + "clearHistory": "Clear History", + "typeLabel": "Type", + "statusLabel": "Status", + "allTypes": "All Types", + "allStatuses": "All Statuses", + "stealthSend": "Stealth Send", + "stealthReceive": "Stealth Receive", + "withdrawal": "Withdrawal", + "nameRegistration": "Name Registration", + "pendingStatus": "Pending", + "confirmedStatus": "Confirmed", + "failedStatus": "Failed", + "noActivityYet": "No activity recorded yet.", + "noActivityMatchesFilter": "No activity matches the filters.", + "direction": "Direction", + "directionIn": "Incoming (Received)", + "directionOut": "Outgoing (Sent)", + "recipientAddress": "Recipient / Address", + "hash": "Hash", + "vaultTitle": "Stealth Vault", + "vaultConnectPrompt": "Connect your Freighter wallet to use the stealth vault for time-locked deposits.", + "vaultDescription": "Create time-locked deposits, claim after unlock, or refund after the refund window.", + "createDeposit": "Create Deposit", + "claim": "Claim", + "statusTab": "Status", + "claimSuccessful": "Claim Successful", + "claimAnother": "Claim Another", + "noClaimableDeposits": "No Claimable Deposits", + "noClaimableDepositsDesc": "No pending vault deposits found for your address.", + "connectWalletTitle": "Connect Wallet", + "connectWalletVaultPrompt": "Connect your Freighter wallet to claim vault deposits.", + "depositId": "Deposit ID", + "unlockLedger": "Unlock Ledger", + "refundWindow": "Refund Window", + "refundWindowLedgers": "{{value}} ledgers", + "pendingBadge": "Pending", + "signing": "Signing...", + "claiming": "Claiming...", + "claimButton": "Claim", + "depositCreated": "Deposit Created", + "newDeposit": "New Deposit", + "recipientMetaAddressLabel": "Recipient Meta-Address", + "unlockLedgerLabel": "Unlock Ledger", + "refundWindowLabel": "Refund Window (ledgers)", + "vaultContract": "Contract", + "vaultContractValue": "Stealth Vault (Coming Soon)", + "createDepositButton": "Create Deposit", + "refundWindowAfterUnlock": "{{value}} ledgers after unlock", + "sponsoredWithdrawalTitle": "Sponsored Withdrawal Required", + "sponsoredWithdrawalDesc": "This stealth address can't pay its own fees. Your connected wallet will sponsor the transaction and pay the fee. Freighter will prompt you to sign the fee-bump transaction.", + "sponsoredWithdrawalMergeDesc": "The entire balance (including base reserve) will be merged into the destination address.", + "payWithConnectedWallet": "Pay with Connected Wallet", + "processing": "Processing...", + "cancel": "Cancel", + "sponsoredWithdrawComplete": "Sponsored withdrawal complete", + "feeBumpRecoveryDesc": "Fee-bump transaction sponsored by your connected wallet. All funds including base reserve have been recovered.", + "balanceError": "Balance error", + "destinationPlaceholder": "Destination address (G...)", + "walletPickerTitle": "Connect Stellar wallet", + "walletPickerClose": "Close", + "detecting": "Detecting\u2026", + "installed": "Installed", + "notDetected": "Not detected \u2014 install \u2197", + "walletPickerFooter": "Albedo and WalletConnect work in any browser \u2014 no extension needed. Other wallets require their browser extension to be installed.", + "scanWithWalletConnect": "Scan with WalletConnect", + "scanQrInstruction": "Scan this QR code with your mobile wallet app", + "openInWalletApp": "Or open in wallet app directly", + "walletConnectedSuccess": "Wallet connected successfully!", + "continueButton": "Continue", + "walletConnecting": "Connecting\u2026", + "walletConnect": "Connect wallet", + "connectedVia": "Connected via", + "disconnect": "Disconnect", + "browserVault": "Browser Vault", + "browserVaultOptIn": "Opt-in", + "browserVaultSaveDesc": "Store the derived Stellar keys encrypted in this browser for brief reuse.", + "browserVaultUnlockDesc": "Restore the last saved Stellar keys from this browser vault using your passphrase.", + "browserVaultNotReplacement": "Not a replacement for a hardware wallet.", + "passphrase": "Passphrase", + "passphrasePlaceholder": "Unlock the vault", + "saveToBrowserVault": "Save to Browser Vault", + "unlockBrowserVault": "Unlock Browser Vault", + "saving": "Saving...", + "locking": "Locking...", + "unlocking": "Unlocking...", + "lockVault": "Lock Vault" }, "solana": { "network": "Solana Devnet / SOL", diff --git a/src/i18n/es.json b/src/i18n/es.json index 18ff019..b873a77 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -90,7 +90,130 @@ "announcerContractName": "Soroban", "networkFeeAmount": "100 stroops", "recipientPlaceholder": "st:xlm:...", - "validMetaAddressError": "Ingresa una meta-dirección Stellar válida (st:xlm:...)" + "validMetaAddressError": "Ingresa una meta-dirección Stellar válida (st:xlm:...)", + "asset": "Activo", + "memo": "Memo (opcional)", + "sourceBalance": "Saldo origen", + "predictedTransfer": "Transferencia estimada", + "simulatingSoroban": "Simulando pre-vuelo de Soroban...", + "predicted": "Estimado", + "predictedFee": "Tarifa estimada", + "predictedReturnValue": "Valor de retorno estimado", + "predictedContractEvents": "Eventos de contrato estimados", + "none": "Ninguno", + "sendAsset": "Enviar {{asset}}", + "finalTransfer": "Transferencia final", + "pendingTransfer": "Transferencia pendiente", + "finalTransactionHash": "Hash de transacción final", + "scan": "Escanear", + "showQrAriaLabel": "Mostrar código QR de meta-dirección", + "scanQrAriaLabel": "Escanear código QR", + "backgroundNotifications": "Notificaciones en segundo plano", + "privacyDisclosure": "Aviso de privacidad", + "privacyDisclosureText": "Tu clave de visualización se almacena cifrada en IndexedDB usando tu clave derivada de billetera. El service worker escanea periódicamente nuevos pagos y muestra notificaciones. Puedes desactivar esta función en cualquier momento.", + "testNotification": "Notificación de prueba", + "notificationsEnabledDesc": "Recibe notificaciones cuando se detecten nuevos pagos sigilosos, incluso cuando la pestaña esté cerrada.", + "notificationsDisabledDesc": "Activa las notificaciones para recibir alertas sobre pagos sigilosos entrantes incluso cuando la pestaña esté cerrada.", + "noMatchingTransfers": "Sin transferencias coincidentes", + "tryAdjustingFilters": "Intenta ajustar tu búsqueda o filtros.", + "searchPlaceholder": "Buscar por etiqueta, tag o dirección...", + "transfersSearchPlaceholder": "Buscar por dirección o monto...", + "noMatchingTransfersFor": "No se encontraron transferencias para \"{{query}}\"", + "export": "Exportar", + "import": "Importar", + "tags": "Etiquetas:", + "clear": "Limpiar", + "showHidden": "Mostrar ocultos ({{count}})", + "hideArchived": "Ocultar archivados ({{count}})", + "showMore": "Mostrar 25 más", + "connectHistoryPrompt": "Conecta tu billetera Stellar para ver el historial de transacciones.", + "historyTitle": "Historial de actividad", + "clearHistory": "Limpiar historial", + "typeLabel": "Tipo", + "statusLabel": "Estado", + "allTypes": "Todos los tipos", + "allStatuses": "Todos los estados", + "stealthSend": "Envío sigiloso", + "stealthReceive": "Recepción sigilosa", + "withdrawal": "Retiro", + "nameRegistration": "Registro de nombre", + "pendingStatus": "Pendiente", + "confirmedStatus": "Confirmado", + "failedStatus": "Fallido", + "noActivityYet": "Aún no hay actividad registrada.", + "noActivityMatchesFilter": "Ninguna actividad coincide con los filtros.", + "direction": "Dirección", + "directionIn": "Entrante (Recibido)", + "directionOut": "Saliente (Enviado)", + "recipientAddress": "Destinatario / Dirección", + "hash": "Hash", + "vaultTitle": "Bóveda sigilosa", + "vaultConnectPrompt": "Conecta tu billetera Freighter para usar la bóveda sigilosa con depósitos temporales.", + "vaultDescription": "Crea depósitos temporales, reclama tras el desbloqueo o reembolsa tras la ventana de reembolso.", + "createDeposit": "Crear depósito", + "claim": "Reclamar", + "statusTab": "Estado", + "claimSuccessful": "Reclamo exitoso", + "claimAnother": "Reclamar otro", + "noClaimableDeposits": "Sin depósitos reclamables", + "noClaimableDepositsDesc": "No se encontraron depósitos pendientes para tu dirección.", + "connectWalletTitle": "Conectar billetera", + "connectWalletVaultPrompt": "Conecta tu billetera Freighter para reclamar depósitos de la bóveda.", + "depositId": "ID de depósito", + "unlockLedger": "Ledger de desbloqueo", + "refundWindow": "Ventana de reembolso", + "refundWindowLedgers": "{{value}} ledgers", + "pendingBadge": "Pendiente", + "signing": "Firmando...", + "claiming": "Reclamando...", + "claimButton": "Reclamar", + "depositCreated": "Depósito creado", + "newDeposit": "Nuevo depósito", + "recipientMetaAddressLabel": "Meta-dirección del destinatario", + "unlockLedgerLabel": "Ledger de desbloqueo", + "refundWindowLabel": "Ventana de reembolso (ledgers)", + "vaultContract": "Contrato", + "vaultContractValue": "Bóveda sigilosa (próximamente)", + "createDepositButton": "Crear depósito", + "refundWindowAfterUnlock": "{{value}} ledgers después del desbloqueo", + "sponsoredWithdrawalTitle": "Se requiere retiro patrocinado", + "sponsoredWithdrawalDesc": "Esta dirección sigilosa no puede pagar sus propias tarifas. Tu billetera conectada patrocinará la transacción y pagará la tarifa. Freighter te pedirá que firmes la transacción fee-bump.", + "sponsoredWithdrawalMergeDesc": "Todo el saldo (incluida la reserva base) se fusionará con la dirección de destino.", + "payWithConnectedWallet": "Pagar con billetera conectada", + "processing": "Procesando...", + "cancel": "Cancelar", + "sponsoredWithdrawComplete": "Retiro patrocinado completado", + "feeBumpRecoveryDesc": "Transacción fee-bump patrocinada por tu billetera conectada. Todos los fondos incluida la reserva base han sido recuperados.", + "balanceError": "Error de saldo", + "destinationPlaceholder": "Dirección de destino (G...)", + "walletPickerTitle": "Conectar billetera Stellar", + "walletPickerClose": "Cerrar", + "detecting": "Detectando\u2026", + "installed": "Instalado", + "notDetected": "No detectado \u2014 instalar \u2197", + "walletPickerFooter": "Albedo y WalletConnect funcionan en cualquier navegador, sin necesidad de extensión. Otras billeteras requieren que su extensión de navegador esté instalada.", + "scanWithWalletConnect": "Escanear con WalletConnect", + "scanQrInstruction": "Escanea este código QR con tu aplicación de billetera móvil", + "openInWalletApp": "O abrir directamente en la aplicación de billetera", + "walletConnectedSuccess": "¡Billetera conectada con éxito!", + "continueButton": "Continuar", + "walletConnecting": "Conectando\u2026", + "walletConnect": "Conectar billetera", + "connectedVia": "Conectado a través de", + "disconnect": "Desconectar", + "browserVault": "Bóveda del navegador", + "browserVaultOptIn": "Opt-in", + "browserVaultSaveDesc": "Guarda las claves Stellar derivadas cifradas en este navegador para reutilización breve.", + "browserVaultUnlockDesc": "Restaura las últimas claves Stellar guardadas desde la bóveda de este navegador usando tu contraseña.", + "browserVaultNotReplacement": "No es un reemplazo para una billetera de hardware.", + "passphrase": "Contraseña", + "passphrasePlaceholder": "Desbloquear la bóveda", + "saveToBrowserVault": "Guardar en la bóveda del navegador", + "unlockBrowserVault": "Desbloquear bóveda del navegador", + "saving": "Guardando...", + "locking": "Bloqueando...", + "unlocking": "Desbloqueando...", + "lockVault": "Bloquear bóveda" }, "solana": { "network": "Solana Devnet / SOL", diff --git a/src/i18n/fr.json b/src/i18n/fr.json new file mode 100644 index 0000000..b967607 --- /dev/null +++ b/src/i18n/fr.json @@ -0,0 +1,263 @@ +{ + "nav": { + "send": "Envoyer", + "receive": "Recevoir", + "schedule": "Planifier" + }, + "header": { + "menuLabel": "Menu" + }, + "walletConnect": { + "connectWallet": "Connecter le portefeuille", + "connectFreighter": "Connecter Freighter" + }, + "copyButton": { + "copy": "Copier", + "copied": "Copié" + }, + "localeSwitcher": { + "label": "Langue" + }, + "common": { + "send": "Envoyer", + "receive": "Recevoir", + "amount": "Montant", + "recipientMetaAddress": "Méta-adresse du destinataire", + "networkFee": "Frais de réseau", + "announcerContract": "Contrat annonceur", + "stealthAddress": "Adresse furtive", + "transactionHash": "Hash de transaction", + "ephemeralPublicKey": "Clé publique éphémère", + "stealthKey": "Clé furtive", + "newTransfer": "Nouveau transfert", + "transferComplete": "Transfert terminé", + "pending": "En attente", + "confirming": "Confirmation...", + "confirmInWallet": "Confirmer dans le portefeuille...", + "sendPrivately": "Envoyer en privé", + "paste": "Coller", + "max": "Max", + "paidBySender": "Payé par l'expéditeur", + "nameNotFound": "Nom introuvable", + "walletNotConnected": "Portefeuille non connecté", + "transactionFailed": "Transaction échouée", + "withdrawn": "Retiré", + "withdrawTo": "Retirer vers", + "withdraw": "Retirer", + "revealPrivateKey": "Révéler la clé privée", + "revealSecretKey": "Révéler la clé secrète", + "empty": "Vide", + "yourStealthMetaAddress": "Votre méta-adresse furtive", + "onChainRegistration": "Enregistrement on-chain", + "metaAddressRegistered": "Méta-adresse enregistrée on-chain", + "registerMetaAddressHint": "Enregistrez votre méta-adresse pour que les expéditeurs puissent vous trouver par adresse de portefeuille.", + "registerOnChain": "Enregistrer on-chain", + "registering": "Enregistrement...", + "scanForPayments": "Rechercher des paiements", + "scanning": "Recherche...", + "transfersFound_one": "{{count}} transfert trouvé", + "transfersFound_other": "{{count}} transferts trouvés", + "noTransfersFound": "Aucun transfert trouvé", + "noTransfersMatchedKeys": "Aucun transfert furtif ne correspond à vos clés.", + "deriveKeys": "Dériver les clés", + "deriveKeysTitle": "Dériver les clés furtives", + "signingInWallet": "Signer dans le portefeuille...", + "keyDerivationFailed": "Échec de la dérivation de clés", + "scanFailed": "Échec de l'analyse", + "expectedConfirmation": "Confirmation attendue", + "seconds_approx": "~5 secondes", + "balance": "Solde" + }, + "horizen": { + "network": "Horizen Testnet / ETH", + "sendTitle": "Envoyer", + "receiveTitle": "Recevoir", + "sendConnectPrompt": "Connectez votre portefeuille pour envoyer des paiements furtifs sur Horizen.", + "receiveConnectPrompt": "Connectez votre portefeuille pour rechercher des transferts furtifs entrants sur Horizen.", + "sendDescription": "Envoyez des ETH en privé via des adresses furtives. Le destinataire reçoit les fonds à une nouvelle adresse qu'il est seul à contrôler.", + "receiveDescription": "Dérivez vos clés furtives, enregistrez on-chain, puis recherchez des paiements.", + "announcerContractName": "WraithSender", + "recipientPlaceholder": "st:eth:0x... ou nom.wraith" + }, + "stellar": { + "network": "Stellar Testnet / XLM", + "sendTitle": "Envoyer", + "receiveTitle": "Recevoir", + "sendConnectPrompt": "Connectez votre portefeuille Freighter pour envoyer des paiements furtifs sur Stellar.", + "receiveConnectPrompt": "Connectez votre portefeuille Freighter pour rechercher des transferts furtifs entrants sur Stellar.", + "sendDescription": "Envoyez des XLM en privé via des adresses furtives. Le destinataire reçoit les fonds à une nouvelle adresse qu'il est seul à contrôler.", + "receiveDescription": "Dérivez vos clés furtives, enregistrez on-chain, puis recherchez des paiements.", + "announcerContractName": "Soroban", + "networkFeeAmount": "100 stroops", + "recipientPlaceholder": "st:xlm:...", + "validMetaAddressError": "Entrez une méta-adresse Stellar valide (st:xlm:...)", + "asset": "Actif", + "memo": "Mémo (facultatif)", + "sourceBalance": "Solde source", + "predictedTransfer": "Transfert estimé", + "simulatingSoroban": "Simulation Soroban en cours...", + "predicted": "Estimé", + "predictedFee": "Frais estimés", + "predictedReturnValue": "Valeur de retour estimée", + "predictedContractEvents": "Événements de contrat estimés", + "none": "Aucun", + "sendAsset": "Envoyer {{asset}}", + "finalTransfer": "Transfert final", + "pendingTransfer": "Transfert en attente", + "finalTransactionHash": "Hash de transaction final", + "scan": "Scanner", + "showQrAriaLabel": "Afficher le QR Code de la méta-adresse", + "scanQrAriaLabel": "Scanner le QR Code", + "backgroundNotifications": "Notifications en arrière-plan", + "privacyDisclosure": "Avis de confidentialité", + "privacyDisclosureText": "Votre clé de consultation est stockée chiffrée dans IndexedDB à l'aide de votre clé dérivée du portefeuille. Le service worker analyse périodiquement les nouveaux paiements et affiche des notifications. Vous pouvez désactiver cette fonctionnalité à tout moment.", + "testNotification": "Notification de test", + "notificationsEnabledDesc": "Recevez des notifications lorsque de nouveaux paiements furtifs sont détectés, même lorsque l'onglet est fermé.", + "notificationsDisabledDesc": "Activez les notifications pour recevoir des alertes sur les paiements furtifs entrants même lorsque l'onglet est fermé.", + "noMatchingTransfers": "Aucun transfert correspondant", + "tryAdjustingFilters": "Essayez d'ajuster votre recherche ou vos filtres.", + "searchPlaceholder": "Rechercher par étiquette, tag ou adresse...", + "transfersSearchPlaceholder": "Rechercher par adresse ou montant...", + "noMatchingTransfersFor": "Aucun transfert trouvé pour «\u00a0{{query}}\u00a0»", + "export": "Exporter", + "import": "Importer", + "tags": "Tags\u00a0:", + "clear": "Effacer", + "showHidden": "Afficher les cachés ({{count}})", + "hideArchived": "Masquer les archivés ({{count}})", + "showMore": "Afficher 25 de plus", + "connectHistoryPrompt": "Connectez votre portefeuille Stellar pour consulter l'historique des transactions.", + "historyTitle": "Historique d'activité", + "clearHistory": "Effacer l'historique", + "typeLabel": "Type", + "statusLabel": "Statut", + "allTypes": "Tous les types", + "allStatuses": "Tous les statuts", + "stealthSend": "Envoi furtif", + "stealthReceive": "Réception furtive", + "withdrawal": "Retrait", + "nameRegistration": "Enregistrement de nom", + "pendingStatus": "En attente", + "confirmedStatus": "Confirmé", + "failedStatus": "Échoué", + "noActivityYet": "Aucune activité enregistrée pour l'instant.", + "noActivityMatchesFilter": "Aucune activité ne correspond aux filtres.", + "direction": "Direction", + "directionIn": "Entrant (Reçu)", + "directionOut": "Sortant (Envoyé)", + "recipientAddress": "Destinataire / Adresse", + "hash": "Hash", + "vaultTitle": "Coffre furtif", + "vaultConnectPrompt": "Connectez votre portefeuille Freighter pour utiliser le coffre furtif avec des dépôts à durée limitée.", + "vaultDescription": "Créez des dépôts à durée limitée, réclamez après déverrouillage ou remboursez après la fenêtre de remboursement.", + "createDeposit": "Créer un dépôt", + "claim": "Réclamer", + "statusTab": "Statut", + "claimSuccessful": "Réclamation réussie", + "claimAnother": "Réclamer un autre", + "noClaimableDeposits": "Aucun dépôt réclamable", + "noClaimableDepositsDesc": "Aucun dépôt en attente trouvé pour votre adresse.", + "connectWalletTitle": "Connecter le portefeuille", + "connectWalletVaultPrompt": "Connectez votre portefeuille Freighter pour réclamer des dépôts du coffre.", + "depositId": "ID de dépôt", + "unlockLedger": "Ledger de déverrouillage", + "refundWindow": "Fenêtre de remboursement", + "refundWindowLedgers": "{{value}} ledgers", + "pendingBadge": "En attente", + "signing": "Signature...", + "claiming": "Réclamation...", + "claimButton": "Réclamer", + "depositCreated": "Dépôt créé", + "newDeposit": "Nouveau dépôt", + "recipientMetaAddressLabel": "Méta-adresse du destinataire", + "unlockLedgerLabel": "Ledger de déverrouillage", + "refundWindowLabel": "Fenêtre de remboursement (ledgers)", + "vaultContract": "Contrat", + "vaultContractValue": "Coffre furtif (bientôt disponible)", + "createDepositButton": "Créer un dépôt", + "refundWindowAfterUnlock": "{{value}} ledgers après déverrouillage", + "sponsoredWithdrawalTitle": "Retrait sponsorisé requis", + "sponsoredWithdrawalDesc": "Cette adresse furtive ne peut pas payer ses propres frais. Votre portefeuille connecté sponsorisera la transaction et paiera les frais. Freighter vous demandera de signer la transaction fee-bump.", + "sponsoredWithdrawalMergeDesc": "L'intégralité du solde (y compris la réserve de base) sera fusionnée dans l'adresse de destination.", + "payWithConnectedWallet": "Payer avec le portefeuille connecté", + "processing": "Traitement...", + "cancel": "Annuler", + "sponsoredWithdrawComplete": "Retrait sponsorisé terminé", + "feeBumpRecoveryDesc": "Transaction fee-bump sponsorisée par votre portefeuille connecté. Tous les fonds, y compris la réserve de base, ont été récupérés.", + "balanceError": "Erreur de solde", + "destinationPlaceholder": "Adresse de destination (G...)", + "walletPickerTitle": "Connecter un portefeuille Stellar", + "walletPickerClose": "Fermer", + "detecting": "Détection\u2026", + "installed": "Installé", + "notDetected": "Non détecté \u2014 installer \u2197", + "walletPickerFooter": "Albedo et WalletConnect fonctionnent dans n'importe quel navigateur, sans extension requise. Les autres portefeuilles nécessitent l'installation de leur extension de navigateur.", + "scanWithWalletConnect": "Scanner avec WalletConnect", + "scanQrInstruction": "Scannez ce QR Code avec votre application de portefeuille mobile", + "openInWalletApp": "Ou ouvrir directement dans l'application de portefeuille", + "walletConnectedSuccess": "Portefeuille connecté avec succès\u00a0!", + "continueButton": "Continuer", + "walletConnecting": "Connexion\u2026", + "walletConnect": "Connecter le portefeuille", + "connectedVia": "Connecté via", + "disconnect": "Déconnecter", + "browserVault": "Coffre du navigateur", + "browserVaultOptIn": "Opt-in", + "browserVaultSaveDesc": "Stockez les clés Stellar dérivées chiffrées dans ce navigateur pour une réutilisation brève.", + "browserVaultUnlockDesc": "Restaurez les dernières clés Stellar enregistrées depuis le coffre de ce navigateur en utilisant votre phrase secrète.", + "browserVaultNotReplacement": "Ne remplace pas un portefeuille matériel.", + "passphrase": "Phrase secrète", + "passphrasePlaceholder": "Déverrouiller le coffre", + "saveToBrowserVault": "Enregistrer dans le coffre du navigateur", + "unlockBrowserVault": "Déverrouiller le coffre du navigateur", + "saving": "Enregistrement...", + "locking": "Verrouillage...", + "unlocking": "Déverrouillage...", + "lockVault": "Verrouiller le coffre" + }, + "solana": { + "network": "Solana Devnet / SOL", + "sendTitle": "Envoyer", + "receiveTitle": "Recevoir", + "sendConnectPrompt": "Connectez votre portefeuille Solana pour envoyer des paiements furtifs.", + "receiveConnectPrompt": "Connectez votre portefeuille Solana pour rechercher des paiements furtifs.", + "sendDescription": "Envoyez des SOL en privé via des adresses furtives. Le destinataire reçoit les fonds à une nouvelle adresse qu'il est seul à contrôler.", + "receiveDescription": "Dérivez vos clés furtives, puis recherchez des paiements sur Solana Devnet.", + "networkFeeAmount": "~5000 lamports", + "recipientPlaceholder": "st:sol:...", + "validMetaAddressError": "Entrez une méta-adresse Solana valide (st:sol:...)", + "walletSigningNotSupported": "Le portefeuille ne prend pas en charge la signature de messages", + "destinationPlaceholder": "Adresse de destination (base58)", + "cellsFound_one": "{{count}} cellule trouvée", + "cellsFound_other": "{{count}} cellules trouvées" + }, + "ckb": { + "network": "CKB Testnet / CKB", + "sendTitle": "Envoyer", + "receiveTitle": "Recevoir", + "sendConnectPrompt": "Connectez votre portefeuille CKB pour envoyer des paiements furtifs.", + "receiveConnectPrompt": "Connectez votre portefeuille CKB pour rechercher des paiements furtifs.", + "sendDescription": "Envoyez des CKB en privé via des adresses furtives. Le destinataire reçoit les fonds dans une nouvelle cellule qu'il est seul à pouvoir déverrouiller.", + "receiveDescription": "Dérivez vos clés furtives, puis recherchez des cellules furtives sur CKB Testnet.", + "amountLabel": "Montant (min 95)", + "networkFeeAmount": "~1000 shannons", + "minCellCapacity": "Capacité minimale de cellule", + "minCellCapacityValue": "~94.5 CKB", + "validMetaAddressError": "Entrez une méta-adresse CKB valide (st:ckb:...)", + "minAmountError": "Le montant minimum est de 95 CKB. Les cellules furtives nécessitent au moins ~94.5 CKB de capacité.", + "connectWalletFirst": "Connectez d'abord votre portefeuille CKB", + "recipientPlaceholder": "st:ckb:...", + "stealthHash": "Hash furtif", + "stealthPublicKey": "Clé publique furtive", + "cell": "Cellule", + "withdraw": "Retirer", + "withdrawInstruction": "Utilisez la clé privée ci-dessous pour signer une transaction CKB consommant cette cellule.", + "scanForCells": "Rechercher des cellules", + "scanning": "Recherche...", + "noCellsFound": "Aucune cellule trouvée", + "noCellsMatchedKeys": "Aucune cellule furtive ne correspond à vos clés.", + "deriveStealthKeys": "Dériver les clés furtives", + "cellsFound_one": "{{count}} cellule trouvée", + "cellsFound_other": "{{count}} cellules trouvées" + } +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index cafa831..7d28bec 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -3,6 +3,8 @@ import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import en from './en.json'; import es from './es.json'; +import pt from './pt.json'; +import fr from './fr.json'; i18n .use(LanguageDetector) @@ -11,9 +13,11 @@ i18n resources: { en: { translation: en }, es: { translation: es }, + pt: { translation: pt }, + fr: { translation: fr }, }, fallbackLng: 'en', - supportedLngs: ['en', 'es'], + supportedLngs: ['en', 'es', 'pt', 'fr'], interpolation: { escapeValue: false, }, diff --git a/src/i18n/pt.json b/src/i18n/pt.json new file mode 100644 index 0000000..01e86c4 --- /dev/null +++ b/src/i18n/pt.json @@ -0,0 +1,263 @@ +{ + "nav": { + "send": "Enviar", + "receive": "Receber", + "schedule": "Agendar" + }, + "header": { + "menuLabel": "Menu" + }, + "walletConnect": { + "connectWallet": "Conectar carteira", + "connectFreighter": "Conectar Freighter" + }, + "copyButton": { + "copy": "Copiar", + "copied": "Copiado" + }, + "localeSwitcher": { + "label": "Idioma" + }, + "common": { + "send": "Enviar", + "receive": "Receber", + "amount": "Valor", + "recipientMetaAddress": "Meta-endereço do destinatário", + "networkFee": "Taxa de rede", + "announcerContract": "Contrato anunciador", + "stealthAddress": "Endereço furtivo", + "transactionHash": "Hash da transação", + "ephemeralPublicKey": "Chave pública efêmera", + "stealthKey": "Chave furtiva", + "newTransfer": "Nova transferência", + "transferComplete": "Transferência concluída", + "pending": "Pendente", + "confirming": "Confirmando...", + "confirmInWallet": "Confirmar na carteira...", + "sendPrivately": "Enviar de forma privada", + "paste": "Colar", + "max": "Máx", + "paidBySender": "Pago pelo remetente", + "nameNotFound": "Nome não encontrado", + "walletNotConnected": "Carteira não conectada", + "transactionFailed": "Transação falhou", + "withdrawn": "Retirado", + "withdrawTo": "Retirar para", + "withdraw": "Retirar", + "revealPrivateKey": "Revelar chave privada", + "revealSecretKey": "Revelar chave secreta", + "empty": "Vazio", + "yourStealthMetaAddress": "Seu meta-endereço furtivo", + "onChainRegistration": "Registro on-chain", + "metaAddressRegistered": "Meta-endereço registrado on-chain", + "registerMetaAddressHint": "Registre seu meta-endereço para que os remetentes possam encontrá-lo pelo endereço da carteira.", + "registerOnChain": "Registrar on-chain", + "registering": "Registrando...", + "scanForPayments": "Buscar pagamentos", + "scanning": "Buscando...", + "transfersFound_one": "{{count}} transferência encontrada", + "transfersFound_other": "{{count}} transferências encontradas", + "noTransfersFound": "Nenhuma transferência encontrada", + "noTransfersMatchedKeys": "Nenhuma transferência furtiva correspondeu às suas chaves.", + "deriveKeys": "Derivar chaves", + "deriveKeysTitle": "Derivar chaves furtivas", + "signingInWallet": "Assinar na carteira...", + "keyDerivationFailed": "Falha na derivação de chaves", + "scanFailed": "Falha na varredura", + "expectedConfirmation": "Confirmação esperada", + "seconds_approx": "~5 segundos", + "balance": "Saldo" + }, + "horizen": { + "network": "Horizen Testnet / ETH", + "sendTitle": "Enviar", + "receiveTitle": "Receber", + "sendConnectPrompt": "Conecte sua carteira para enviar pagamentos furtivos no Horizen.", + "receiveConnectPrompt": "Conecte sua carteira para buscar transferências furtivas recebidas no Horizen.", + "sendDescription": "Envie ETH de forma privada usando endereços furtivos. O destinatário recebe fundos em um novo endereço que apenas ele pode controlar.", + "receiveDescription": "Derive suas chaves furtivas, registre on-chain e busque pagamentos.", + "announcerContractName": "WraithSender", + "recipientPlaceholder": "st:eth:0x... ou nome.wraith" + }, + "stellar": { + "network": "Stellar Testnet / XLM", + "sendTitle": "Enviar", + "receiveTitle": "Receber", + "sendConnectPrompt": "Conecte sua carteira Freighter para enviar pagamentos furtivos no Stellar.", + "receiveConnectPrompt": "Conecte sua carteira Freighter para buscar transferências furtivas recebidas no Stellar.", + "sendDescription": "Envie XLM de forma privada usando endereços furtivos. O destinatário recebe fundos em um novo endereço que apenas ele pode controlar.", + "receiveDescription": "Derive suas chaves furtivas, registre on-chain e busque pagamentos.", + "announcerContractName": "Soroban", + "networkFeeAmount": "100 stroops", + "recipientPlaceholder": "st:xlm:...", + "validMetaAddressError": "Insira um meta-endereço Stellar válido (st:xlm:...)", + "asset": "Ativo", + "memo": "Memo (opcional)", + "sourceBalance": "Saldo de origem", + "predictedTransfer": "Transferência estimada", + "simulatingSoroban": "Simulando pré-voo Soroban...", + "predicted": "Estimado", + "predictedFee": "Taxa estimada", + "predictedReturnValue": "Valor de retorno estimado", + "predictedContractEvents": "Eventos de contrato estimados", + "none": "Nenhum", + "sendAsset": "Enviar {{asset}}", + "finalTransfer": "Transferência final", + "pendingTransfer": "Transferência pendente", + "finalTransactionHash": "Hash da transação final", + "scan": "Escanear", + "showQrAriaLabel": "Mostrar QR Code do meta-endereço", + "scanQrAriaLabel": "Escanear QR Code", + "backgroundNotifications": "Notificações em segundo plano", + "privacyDisclosure": "Aviso de privacidade", + "privacyDisclosureText": "Sua chave de visualização é armazenada criptografada no IndexedDB usando sua chave derivada da carteira. O service worker verifica periodicamente novos pagamentos e exibe notificações. Você pode desativar esse recurso a qualquer momento.", + "testNotification": "Notificação de teste", + "notificationsEnabledDesc": "Receba notificações quando novos pagamentos furtivos forem detectados, mesmo com a aba fechada.", + "notificationsDisabledDesc": "Ative as notificações para receber alertas sobre pagamentos furtivos recebidos mesmo com a aba fechada.", + "noMatchingTransfers": "Sem transferências correspondentes", + "tryAdjustingFilters": "Tente ajustar sua pesquisa ou filtros.", + "searchPlaceholder": "Buscar por rótulo, tag ou endereço...", + "transfersSearchPlaceholder": "Buscar por endereço ou valor...", + "noMatchingTransfersFor": "Nenhuma transferência encontrada para \"{{query}}\"", + "export": "Exportar", + "import": "Importar", + "tags": "Tags:", + "clear": "Limpar", + "showHidden": "Mostrar ocultos ({{count}})", + "hideArchived": "Ocultar arquivados ({{count}})", + "showMore": "Mostrar mais 25", + "connectHistoryPrompt": "Conecte sua carteira Stellar para ver o histórico de transações.", + "historyTitle": "Histórico de atividades", + "clearHistory": "Limpar histórico", + "typeLabel": "Tipo", + "statusLabel": "Status", + "allTypes": "Todos os tipos", + "allStatuses": "Todos os status", + "stealthSend": "Envio furtivo", + "stealthReceive": "Recebimento furtivo", + "withdrawal": "Retirada", + "nameRegistration": "Registro de nome", + "pendingStatus": "Pendente", + "confirmedStatus": "Confirmado", + "failedStatus": "Falhou", + "noActivityYet": "Nenhuma atividade registrada ainda.", + "noActivityMatchesFilter": "Nenhuma atividade corresponde aos filtros.", + "direction": "Direção", + "directionIn": "Entrada (Recebido)", + "directionOut": "Saída (Enviado)", + "recipientAddress": "Destinatário / Endereço", + "hash": "Hash", + "vaultTitle": "Cofre furtivo", + "vaultConnectPrompt": "Conecte sua carteira Freighter para usar o cofre furtivo com depósitos com prazo.", + "vaultDescription": "Crie depósitos com prazo, resgate após o desbloqueio ou reembolse após a janela de reembolso.", + "createDeposit": "Criar depósito", + "claim": "Resgatar", + "statusTab": "Status", + "claimSuccessful": "Resgate bem-sucedido", + "claimAnother": "Resgatar outro", + "noClaimableDeposits": "Sem depósitos resgatáveis", + "noClaimableDepositsDesc": "Nenhum depósito pendente encontrado para o seu endereço.", + "connectWalletTitle": "Conectar carteira", + "connectWalletVaultPrompt": "Conecte sua carteira Freighter para resgatar depósitos do cofre.", + "depositId": "ID do depósito", + "unlockLedger": "Ledger de desbloqueio", + "refundWindow": "Janela de reembolso", + "refundWindowLedgers": "{{value}} ledgers", + "pendingBadge": "Pendente", + "signing": "Assinando...", + "claiming": "Resgatando...", + "claimButton": "Resgatar", + "depositCreated": "Depósito criado", + "newDeposit": "Novo depósito", + "recipientMetaAddressLabel": "Meta-endereço do destinatário", + "unlockLedgerLabel": "Ledger de desbloqueio", + "refundWindowLabel": "Janela de reembolso (ledgers)", + "vaultContract": "Contrato", + "vaultContractValue": "Cofre furtivo (em breve)", + "createDepositButton": "Criar depósito", + "refundWindowAfterUnlock": "{{value}} ledgers após desbloqueio", + "sponsoredWithdrawalTitle": "Retirada patrocinada necessária", + "sponsoredWithdrawalDesc": "Este endereço furtivo não pode pagar suas próprias taxas. Sua carteira conectada irá patrocinar a transação e pagar a taxa. O Freighter solicitará que você assine a transação fee-bump.", + "sponsoredWithdrawalMergeDesc": "Todo o saldo (incluindo a reserva base) será mesclado no endereço de destino.", + "payWithConnectedWallet": "Pagar com carteira conectada", + "processing": "Processando...", + "cancel": "Cancelar", + "sponsoredWithdrawComplete": "Retirada patrocinada concluída", + "feeBumpRecoveryDesc": "Transação fee-bump patrocinada pela sua carteira conectada. Todos os fundos incluindo a reserva base foram recuperados.", + "balanceError": "Erro de saldo", + "destinationPlaceholder": "Endereço de destino (G...)", + "walletPickerTitle": "Conectar carteira Stellar", + "walletPickerClose": "Fechar", + "detecting": "Detectando\u2026", + "installed": "Instalado", + "notDetected": "Não detectado \u2014 instalar \u2197", + "walletPickerFooter": "Albedo e WalletConnect funcionam em qualquer navegador, sem necessidade de extensão. Outras carteiras requerem que a extensão do navegador esteja instalada.", + "scanWithWalletConnect": "Escanear com WalletConnect", + "scanQrInstruction": "Escaneie este QR Code com seu aplicativo de carteira móvel", + "openInWalletApp": "Ou abrir diretamente no aplicativo de carteira", + "walletConnectedSuccess": "Carteira conectada com sucesso!", + "continueButton": "Continuar", + "walletConnecting": "Conectando\u2026", + "walletConnect": "Conectar carteira", + "connectedVia": "Conectado via", + "disconnect": "Desconectar", + "browserVault": "Cofre do navegador", + "browserVaultOptIn": "Opt-in", + "browserVaultSaveDesc": "Armazene as chaves Stellar derivadas criptografadas neste navegador para breve reutilização.", + "browserVaultUnlockDesc": "Restaure as últimas chaves Stellar salvas do cofre deste navegador usando sua senha.", + "browserVaultNotReplacement": "Não substitui uma carteira de hardware.", + "passphrase": "Senha", + "passphrasePlaceholder": "Desbloquear o cofre", + "saveToBrowserVault": "Salvar no cofre do navegador", + "unlockBrowserVault": "Desbloquear cofre do navegador", + "saving": "Salvando...", + "locking": "Bloqueando...", + "unlocking": "Desbloqueando...", + "lockVault": "Bloquear cofre" + }, + "solana": { + "network": "Solana Devnet / SOL", + "sendTitle": "Enviar", + "receiveTitle": "Receber", + "sendConnectPrompt": "Conecte sua carteira Solana para enviar pagamentos furtivos.", + "receiveConnectPrompt": "Conecte sua carteira Solana para buscar pagamentos furtivos.", + "sendDescription": "Envie SOL de forma privada usando endereços furtivos. O destinatário recebe fundos em um novo endereço que apenas ele pode controlar.", + "receiveDescription": "Derive suas chaves furtivas e busque pagamentos no Solana Devnet.", + "networkFeeAmount": "~5000 lamports", + "recipientPlaceholder": "st:sol:...", + "validMetaAddressError": "Insira um meta-endereço Solana válido (st:sol:...)", + "walletSigningNotSupported": "A carteira não suporta assinatura de mensagens", + "destinationPlaceholder": "Endereço de destino (base58)", + "cellsFound_one": "{{count}} célula encontrada", + "cellsFound_other": "{{count}} células encontradas" + }, + "ckb": { + "network": "CKB Testnet / CKB", + "sendTitle": "Enviar", + "receiveTitle": "Receber", + "sendConnectPrompt": "Conecte sua carteira CKB para enviar pagamentos furtivos.", + "receiveConnectPrompt": "Conecte sua carteira CKB para buscar pagamentos furtivos.", + "sendDescription": "Envie CKB de forma privada usando endereços furtivos. O destinatário recebe fundos em uma nova célula que apenas ele pode desbloquear.", + "receiveDescription": "Derive suas chaves furtivas e busque células furtivas no CKB Testnet.", + "amountLabel": "Valor (mín. 95)", + "networkFeeAmount": "~1000 shannons", + "minCellCapacity": "Capacidade mínima de célula", + "minCellCapacityValue": "~94.5 CKB", + "validMetaAddressError": "Insira um meta-endereço CKB válido (st:ckb:...)", + "minAmountError": "O valor mínimo é 95 CKB. Células furtivas requerem pelo menos ~94.5 CKB de capacidade.", + "connectWalletFirst": "Conecte sua carteira CKB primeiro", + "recipientPlaceholder": "st:ckb:...", + "stealthHash": "Hash furtivo", + "stealthPublicKey": "Chave pública furtiva", + "cell": "Célula", + "withdraw": "Retirar", + "withdrawInstruction": "Use a chave privada abaixo para assinar uma transação CKB consumindo esta célula.", + "scanForCells": "Buscar células", + "scanning": "Buscando...", + "noCellsFound": "Nenhuma célula encontrada", + "noCellsMatchedKeys": "Nenhuma célula furtiva correspondeu às suas chaves.", + "deriveStealthKeys": "Derivar chaves furtivas", + "cellsFound_one": "{{count}} célula encontrada", + "cellsFound_other": "{{count}} células encontradas" + } +}