feat: complete i18n support for all components (6 languages)

Add full internationalization across 127+ components and pages.
790+ translation keys in en, tr, kmr, ckb, ar, fa locales.
Remove duplicate keys and delete unused .json locale files.
This commit is contained in:
2026-02-22 04:48:20 +03:00
parent df22c9ba10
commit d282f609aa
129 changed files with 22442 additions and 4186 deletions
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
@@ -13,6 +14,7 @@ interface PoolCategorySelectorProps {
const POOL_CATEGORIES = Object.values(ValidatorPoolCategory);
export function PoolCategorySelector({ currentCategory, onCategoryChange, disabled }: PoolCategorySelectorProps) {
const { t } = useTranslation();
const [selectedCategory, setSelectedCategory] = useState<ValidatorPoolCategory>(currentCategory || POOL_CATEGORIES[0]);
const handleSubmit = () => {
@@ -23,7 +25,7 @@ export function PoolCategorySelector({ currentCategory, onCategoryChange, disabl
<div className="space-y-4">
<Select value={selectedCategory} onValueChange={(value) => setSelectedCategory(value as ValidatorPoolCategory)} disabled={disabled}>
<SelectTrigger className="w-full bg-gray-800 border-gray-700 text-white">
<SelectValue placeholder="Select a pool category..." />
<SelectValue placeholder={t('poolCategory.placeholder')} />
</SelectTrigger>
<SelectContent>
{POOL_CATEGORIES.map(cat => (
@@ -33,7 +35,7 @@ export function PoolCategorySelector({ currentCategory, onCategoryChange, disabl
</Select>
<Button onClick={handleSubmit} disabled={disabled || !selectedCategory} className="w-full">
{disabled && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{currentCategory ? 'Switch Category' : 'Join Pool'}
{currentCategory ? t('poolCategory.switchCategory') : t('poolCategory.joinPool')}
</Button>
</div>
);
+64 -62
View File
@@ -10,6 +10,7 @@ import { AlertCircle, CheckCircle2 } from 'lucide-react';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useWallet } from '@/contexts/WalletContext';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { web3FromAddress, web3Enable } from '@pezkuwi/extension-dapp';
import {
getStakingInfo,
@@ -46,6 +47,7 @@ async function getInjectorSigner(address: string) {
export const StakingDashboard: React.FC = () => {
const { assetHubApi, peopleApi, selectedAccount, isAssetHubReady, isPeopleReady } = usePezkuwi();
const { balances, refreshBalances } = useWallet();
const { t } = useTranslation();
const [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
const [validators, setValidators] = useState<string[]>([]);
@@ -92,7 +94,7 @@ export const StakingDashboard: React.FC = () => {
}
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch staking data:', error);
toast.error('Failed to fetch staking information');
toast.error(t('staking.fetchError'));
} finally {
setIsLoadingData(false);
}
@@ -139,11 +141,11 @@ export const StakingDashboard: React.FC = () => {
}
}, 3000);
} else {
toast.error(result.error || 'Failed to record trust score');
toast.error(result.error || t('staking.recordFailed'));
}
} catch (error) {
if (import.meta.env.DEV) console.error('Record trust score failed:', error);
toast.error(error instanceof Error ? error.message : 'Failed to record trust score');
toast.error(error instanceof Error ? error.message : t('staking.recordFailed'));
} finally {
setIsRecordingScore(false);
}
@@ -169,11 +171,11 @@ export const StakingDashboard: React.FC = () => {
}
}, 3000);
} else {
toast.error(result.error || 'Failed to claim reward');
toast.error(result.error || t('staking.claimFailed'));
}
} catch (error) {
if (import.meta.env.DEV) console.error('Claim reward failed:', error);
toast.error(error instanceof Error ? error.message : 'Failed to claim reward');
toast.error(error instanceof Error ? error.message : t('staking.claimFailed'));
} finally {
setIsClaimingReward(false);
}
@@ -188,11 +190,11 @@ export const StakingDashboard: React.FC = () => {
// Validate
if (parseFloat(bondAmount) < parseFloat(minNominatorBond)) {
throw new Error(`Minimum bond is ${minNominatorBond} HEZ`);
throw new Error(t('staking.minBondError', { amount: minNominatorBond }));
}
if (parseFloat(bondAmount) > parseFloat(balances.HEZ)) {
throw new Error('Insufficient HEZ balance');
throw new Error(t('staking.insufficientHez'));
}
const injector = await getInjectorSigner(selectedAccount.address);
@@ -242,7 +244,7 @@ export const StakingDashboard: React.FC = () => {
if (!assetHubApi || !selectedAccount || selectedValidators.length === 0) return;
if (!stakingInfo || parseFloat(stakingInfo.bonded) === 0) {
toast.error('You must bond tokens before nominating validators');
toast.error(t('staking.bondBeforeNominate'));
return;
}
@@ -288,7 +290,7 @@ export const StakingDashboard: React.FC = () => {
const amount = parseAmount(unbondAmount);
if (!stakingInfo || parseFloat(unbondAmount) > parseFloat(stakingInfo.active)) {
throw new Error('Insufficient staked amount');
throw new Error(t('staking.insufficientStaked'));
}
const injector = await getInjectorSigner(selectedAccount.address);
@@ -329,7 +331,7 @@ export const StakingDashboard: React.FC = () => {
if (!assetHubApi || !selectedAccount) return;
if (!stakingInfo || parseFloat(stakingInfo.redeemable) === 0) {
toast.info('No tokens available to withdraw');
toast.info(t('staking.noWithdraw'));
return;
}
@@ -346,7 +348,7 @@ export const StakingDashboard: React.FC = () => {
({ status, dispatchError }) => {
if (status.isInBlock) {
if (dispatchError) {
let errorMessage = 'Withdrawal failed';
let errorMessage = t('staking.withdrawFailed');
if (dispatchError.isModule) {
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
@@ -354,7 +356,7 @@ export const StakingDashboard: React.FC = () => {
toast.error(errorMessage);
setIsLoading(false);
} else {
toast.success(`Withdrew ${stakingInfo.redeemable} HEZ`);
toast.success(t('staking.withdrawSuccess', { amount: stakingInfo.redeemable }));
refreshBalances();
setTimeout(() => {
if (assetHubApi && selectedAccount) {
@@ -388,7 +390,7 @@ export const StakingDashboard: React.FC = () => {
({ status, dispatchError }) => {
if (status.isInBlock) {
if (dispatchError) {
let errorMessage = 'Failed to start score tracking';
let errorMessage = t('staking.scoreTrackingFailed');
if (dispatchError.isModule) {
const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
@@ -396,7 +398,7 @@ export const StakingDashboard: React.FC = () => {
toast.error(errorMessage);
setIsLoading(false);
} else {
toast.success('Score tracking started successfully! Your staking score will now accumulate over time.');
toast.success(t('staking.scoreTrackingSuccess'));
// Refresh staking data after a delay
setTimeout(() => {
if (assetHubApi && selectedAccount) {
@@ -410,7 +412,7 @@ export const StakingDashboard: React.FC = () => {
);
} catch (error) {
if (import.meta.env.DEV) console.error('Start score tracking failed:', error);
toast.error(error instanceof Error ? error.message : 'Failed to start score tracking');
toast.error(error instanceof Error ? error.message : t('staking.scoreTrackingFailed'));
setIsLoading(false);
}
};
@@ -422,7 +424,7 @@ export const StakingDashboard: React.FC = () => {
} else {
// Max 16 nominations
if (prev.length >= 16) {
toast.info('Maximum 16 validators can be nominated');
toast.info(t('staking.maxValidators'));
return prev;
}
return [...prev, validator];
@@ -431,7 +433,7 @@ export const StakingDashboard: React.FC = () => {
};
if (isLoadingData) {
return <LoadingState message="Loading staking data..." />;
return <LoadingState message={t('staking.loadingData')} />;
}
return (
@@ -440,35 +442,35 @@ export const StakingDashboard: React.FC = () => {
<div className={`grid grid-cols-1 md:grid-cols-2 ${pezRewards ? 'lg:grid-cols-5' : 'lg:grid-cols-4'} gap-4`}>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Total Bonded</CardTitle>
<CardTitle className="text-sm text-gray-400">{t('staking.totalBonded')}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">
{stakingInfo?.bonded || '0'} HEZ
</div>
<p className="text-xs text-gray-500 mt-1">
Active: {stakingInfo?.active || '0'} HEZ
{t('staking.activeAmount', { amount: stakingInfo?.active || '0' })}
</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Unlocking</CardTitle>
<CardTitle className="text-sm text-gray-400">{t('staking.unlocking')}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-500">
{stakingInfo?.unlocking.reduce((sum, u) => sum + parseFloat(u.amount), 0).toFixed(2) || '0'} HEZ
</div>
<p className="text-xs text-gray-500 mt-1">
{stakingInfo?.unlocking.length || 0} chunk(s)
{t('staking.chunks', { count: stakingInfo?.unlocking.length || 0 })}
</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Redeemable</CardTitle>
<CardTitle className="text-sm text-gray-400">{t('staking.redeemable')}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-500">
@@ -480,14 +482,14 @@ export const StakingDashboard: React.FC = () => {
disabled={!stakingInfo || parseFloat(stakingInfo.redeemable) === 0 || isLoading}
className="mt-2 w-full"
>
Withdraw
{t('staking.withdraw')}
</Button>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Staking Score</CardTitle>
<CardTitle className="text-sm text-gray-400">{t('staking.stakingScore')}</CardTitle>
</CardHeader>
<CardContent>
{stakingInfo?.hasStartedScoreTracking ? (
@@ -497,29 +499,29 @@ export const StakingDashboard: React.FC = () => {
{stakingInfo.stakingScore}/100
</div>
<p className="text-xs text-gray-500 mt-1">
Duration: {stakingInfo.stakingDuration
? `${Math.floor(stakingInfo.stakingDuration / (24 * 60 * 10))} days`
: '0 days'}
{t('staking.scoreDuration', { days: stakingInfo.stakingDuration
? Math.floor(stakingInfo.stakingDuration / (24 * 60 * 10))
: 0 })}
</p>
</>
) : (
<>
<div className="text-lg font-bold text-yellow-500">Waiting for data...</div>
<div className="text-lg font-bold text-yellow-500">{t('staking.waitingData')}</div>
<p className="text-xs text-gray-500 mt-1">
Score tracking started. A noter will submit your staking data soon.
{t('staking.scoreTrackingStartedInfo')}
</p>
</>
)
) : (
<>
<div className="text-2xl font-bold text-gray-500">Not Started</div>
<div className="text-2xl font-bold text-gray-500">{t('staking.notStarted')}</div>
<Button
size="sm"
onClick={handleStartScoreTracking}
disabled={!stakingInfo || isLoading}
className="mt-2 w-full bg-purple-600 hover:bg-purple-700"
>
Start Score Tracking
{t('staking.startScoreTracking')}
</Button>
</>
)}
@@ -531,7 +533,7 @@ export const StakingDashboard: React.FC = () => {
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-gray-400">PEZ Rewards</CardTitle>
<CardTitle className="text-sm text-gray-400">{t('staking.pezRewards')}</CardTitle>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
pezRewards.epochStatus === 'Open'
? 'bg-green-500/20 text-green-400'
@@ -539,22 +541,22 @@ export const StakingDashboard: React.FC = () => {
? 'bg-orange-500/20 text-orange-400'
: 'bg-gray-500/20 text-gray-400'
}`}>
{pezRewards.epochStatus === 'Open' ? 'Open' : pezRewards.epochStatus === 'ClaimPeriod' ? 'Claim Period' : 'Closed'}
{pezRewards.epochStatus === 'Open' ? t('staking.epochOpen') : pezRewards.epochStatus === 'ClaimPeriod' ? t('staking.epochClaimPeriod') : t('staking.epochClosed')}
</span>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
<p className="text-xs text-gray-500">Epoch {pezRewards.currentEpoch}</p>
<p className="text-xs text-gray-500">{t('staking.epoch', { epoch: pezRewards.currentEpoch })}</p>
{/* Open epoch: Record score or show recorded score */}
{pezRewards.epochStatus === 'Open' && (
pezRewards.hasRecordedThisEpoch ? (
<div>
<div className="text-lg font-bold text-green-400">
Score: {pezRewards.userScoreCurrentEpoch}
{t('staking.scoreLabel', { score: pezRewards.userScoreCurrentEpoch })}
</div>
<p className="text-xs text-gray-500">Recorded for this epoch</p>
<p className="text-xs text-gray-500">{t('staking.recordedForEpoch')}</p>
</div>
) : (
<Button
@@ -563,7 +565,7 @@ export const StakingDashboard: React.FC = () => {
disabled={isRecordingScore}
className="w-full bg-green-600 hover:bg-green-700"
>
{isRecordingScore ? 'Recording...' : 'Record Trust Score'}
{isRecordingScore ? t('staking.recording') : t('staking.recordTrustScore')}
</Button>
)
)}
@@ -577,7 +579,7 @@ export const StakingDashboard: React.FC = () => {
<div className="space-y-1">
{pezRewards.claimableRewards.map((reward) => (
<div key={reward.epoch} className="flex items-center justify-between">
<span className="text-xs text-gray-400">Epoch {reward.epoch}: {reward.amount} PEZ</span>
<span className="text-xs text-gray-400">{t('staking.epochReward', { epoch: reward.epoch, amount: reward.amount })}</span>
<Button
size="sm"
variant="outline"
@@ -585,7 +587,7 @@ export const StakingDashboard: React.FC = () => {
disabled={isClaimingReward}
className="h-6 text-xs px-2 border-orange-500 text-orange-400 hover:bg-orange-500/20"
>
{isClaimingReward ? '...' : 'Claim'}
{isClaimingReward ? '...' : t('staking.claim')}
</Button>
</div>
))}
@@ -601,18 +603,18 @@ export const StakingDashboard: React.FC = () => {
{/* Main Staking Interface */}
<Card className="bg-gray-900 border-gray-800">
<CardHeader>
<CardTitle className="text-xl text-white">Staking</CardTitle>
<CardTitle className="text-xl text-white">{t('staking.title')}</CardTitle>
<CardDescription className="text-gray-400">
Stake HEZ to secure the network and earn rewards.
{t('staking.description')}
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="stake">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="stake">Stake</TabsTrigger>
<TabsTrigger value="nominate">Nominate</TabsTrigger>
<TabsTrigger value="pool">Validator Pool</TabsTrigger>
<TabsTrigger value="unstake">Unstake</TabsTrigger>
<TabsTrigger value="stake">{t('staking.tabStake')}</TabsTrigger>
<TabsTrigger value="nominate">{t('staking.tabNominate')}</TabsTrigger>
<TabsTrigger value="pool">{t('staking.tabPool')}</TabsTrigger>
<TabsTrigger value="unstake">{t('staking.tabUnstake')}</TabsTrigger>
</TabsList>
{/* STAKE TAB */}
@@ -620,12 +622,12 @@ export const StakingDashboard: React.FC = () => {
<Alert className="bg-blue-900/20 border-blue-500">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-sm">
Minimum bond: {minNominatorBond} HEZ. Bonded tokens are locked and earn rewards when nominated validators produce blocks.
{t('staking.minBondInfo', { amount: minNominatorBond })}
</AlertDescription>
</Alert>
<div>
<Label>Amount to Bond (HEZ)</Label>
<Label>{t('staking.amountToBond')}</Label>
<Input
type="number"
placeholder={`Min: ${minNominatorBond}`}
@@ -635,12 +637,12 @@ export const StakingDashboard: React.FC = () => {
disabled={isLoading}
/>
<div className="flex justify-between mt-1 text-xs text-gray-400">
<span>Available: {balances.HEZ} HEZ</span>
<span>{t('staking.available', { amount: balances.HEZ })}</span>
<button
onClick={() => setBondAmount(balances.HEZ)}
className="text-blue-400 hover:text-blue-300"
>
Max
{t('staking.max')}
</button>
</div>
</div>
@@ -650,7 +652,7 @@ export const StakingDashboard: React.FC = () => {
disabled={isLoading || !bondAmount || parseFloat(bondAmount) < parseFloat(minNominatorBond)}
className="w-full bg-green-600 hover:bg-green-700"
>
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 ? 'Bond Additional' : 'Bond Tokens'}
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 ? t('staking.bondAdditional') : t('staking.bondTokens')}
</Button>
</TabsContent>
@@ -659,13 +661,13 @@ export const StakingDashboard: React.FC = () => {
<Alert className="bg-purple-900/20 border-purple-500">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-sm">
Select up to 16 validators to nominate. Your stake will be distributed to active validators.
{stakingInfo && parseFloat(stakingInfo.bonded) === 0 && ' You must bond tokens first.'}
{t('staking.nominateInfo')}
{stakingInfo && parseFloat(stakingInfo.bonded) === 0 && ` ${t('staking.bondFirst')}`}
</AlertDescription>
</Alert>
<div className="space-y-2">
<Label>Active Validators ({validators.length})</Label>
<Label>{t('staking.activeValidators', { count: validators.length })}</Label>
<div className="max-h-64 overflow-y-auto space-y-2 border border-gray-700 rounded-lg p-3 bg-gray-800">
{validators.map((validator) => (
<div
@@ -687,7 +689,7 @@ export const StakingDashboard: React.FC = () => {
))}
</div>
<p className="text-xs text-gray-400">
Selected: {selectedValidators.length}/16
{t('staking.selected', { count: selectedValidators.length })}
</p>
</div>
@@ -696,7 +698,7 @@ export const StakingDashboard: React.FC = () => {
disabled={isLoading || selectedValidators.length === 0 || !stakingInfo || parseFloat(stakingInfo.bonded) === 0}
className="w-full bg-purple-600 hover:bg-purple-700"
>
Nominate Validators
{t('staking.nominateValidators')}
</Button>
</TabsContent>
@@ -710,12 +712,12 @@ export const StakingDashboard: React.FC = () => {
<Alert className="bg-yellow-900/20 border-yellow-500">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-sm">
Unbonded tokens will be locked for {bondingDuration} eras (~{Math.floor(bondingDuration / 4)} days) before withdrawal.
{t('staking.unbondInfo', { eras: bondingDuration, days: Math.floor(bondingDuration / 4) })}
</AlertDescription>
</Alert>
<div>
<Label>Amount to Unbond (HEZ)</Label>
<Label>{t('staking.amountToUnbond')}</Label>
<Input
type="number"
placeholder={`Max: ${stakingInfo?.active || '0'}`}
@@ -725,24 +727,24 @@ export const StakingDashboard: React.FC = () => {
disabled={isLoading}
/>
<div className="flex justify-between mt-1 text-xs text-gray-400">
<span>Staked: {stakingInfo?.active || '0'} HEZ</span>
<span>{t('staking.staked', { amount: stakingInfo?.active || '0' })}</span>
<button
onClick={() => setUnbondAmount(stakingInfo?.active || '0')}
className="text-blue-400 hover:text-blue-300"
>
Max
{t('staking.max')}
</button>
</div>
</div>
{stakingInfo && stakingInfo.unlocking.length > 0 && (
<div className="bg-gray-800 rounded-lg p-3 space-y-2">
<Label className="text-sm">Unlocking Chunks</Label>
<Label className="text-sm">{t('staking.unlockingChunks')}</Label>
{stakingInfo.unlocking.map((chunk, i) => (
<div key={i} className="flex justify-between text-sm">
<span className="text-gray-400">{chunk.amount} HEZ</span>
<span className="text-gray-500">
Era {chunk.era} ({chunk.blocksRemaining > 0 ? `~${Math.floor(chunk.blocksRemaining / 600)} blocks` : 'Ready'})
{t('staking.eraInfo', { era: chunk.era })} ({chunk.blocksRemaining > 0 ? t('staking.blocksRemaining', { blocks: Math.floor(chunk.blocksRemaining / 600) }) : t('staking.ready')})
</span>
</div>
))}
@@ -755,7 +757,7 @@ export const StakingDashboard: React.FC = () => {
className="w-full bg-red-600 hover:bg-red-700"
variant="destructive"
>
Unbond Tokens
{t('staking.unbondTokens')}
</Button>
</TabsContent>
</Tabs>
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
@@ -16,6 +17,7 @@ import { Loader2, Users, UserCheck, UserX } from 'lucide-react';
import { PoolCategorySelector } from './PoolCategorySelector';
export function ValidatorPoolDashboard() {
const { t } = useTranslation();
const { api, selectedAccount } = usePezkuwi();
const [poolMember, setPoolMember] = useState<ValidatorPoolCategory | null>(null);
const [poolSize, setPoolSize] = useState(0);
@@ -49,7 +51,7 @@ export function ValidatorPoolDashboard() {
}
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch validator pool data:', error);
toast.error('Failed to fetch pool data');
toast.error(t('validatorPool.fetchError'));
} finally {
setLoading(false);
}
@@ -64,7 +66,7 @@ export function ValidatorPoolDashboard() {
setActionLoading(true);
try {
await joinValidatorPool(api, selectedAccount, category);
toast.success(`Joined the ${category} pool`);
toast.success(t('validatorPool.joinedPool', { category }));
fetchData();
} catch (error) {
if (import.meta.env.DEV) console.error('Join pool error:', error);
@@ -79,7 +81,7 @@ export function ValidatorPoolDashboard() {
setActionLoading(true);
try {
await leaveValidatorPool(api, selectedAccount);
toast.success('Left the validator pool');
toast.success(t('validatorPool.leftPool'));
fetchData();
} catch (error) {
if (import.meta.env.DEV) console.error('Leave pool error:', error);
@@ -94,7 +96,7 @@ export function ValidatorPoolDashboard() {
setActionLoading(true);
try {
await updateValidatorCategory(api, selectedAccount, newCategory);
toast.success(`Switched to ${newCategory}`);
toast.success(t('validatorPool.switchedCategory', { category: newCategory }));
fetchData();
} catch (error) {
if (import.meta.env.DEV) console.error('Switch category error:', error);
@@ -109,7 +111,7 @@ export function ValidatorPoolDashboard() {
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-12 text-center">
<Loader2 className="w-8 h-8 animate-spin text-green-500 mx-auto mb-4" />
<p className="text-gray-400">Loading validator pool info...</p>
<p className="text-gray-400">{t('validatorPool.loading')}</p>
</CardContent>
</Card>
);
@@ -118,22 +120,22 @@ export function ValidatorPoolDashboard() {
return (
<Card className="bg-gray-900 border-gray-800">
<CardHeader>
<CardTitle className="text-white">Validator Pool</CardTitle>
<CardDescription>Join a pool to support the network and earn rewards.</CardDescription>
<CardTitle className="text-white">{t('validatorPool.title')}</CardTitle>
<CardDescription>{t('validatorPool.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-gray-800 rounded-lg">
<div className="flex items-center text-gray-400 mb-2">
<Users className="w-4 h-4 mr-2" />
Pool Size
{t('validatorPool.poolSize')}
</div>
<div className="text-2xl font-bold text-white">{poolSize}</div>
</div>
<div className="p-4 bg-gray-800 rounded-lg">
<div className="flex items-center text-gray-400 mb-2">
<UserCheck className="w-4 h-4 mr-2" />
Active Validators
{t('validatorPool.activeValidators')}
</div>
<div className="text-2xl font-bold text-white">{validatorCount}</div>
</div>
@@ -144,7 +146,7 @@ export function ValidatorPoolDashboard() {
<div className="space-y-4">
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
<p className="text-green-400">
You are in the <span className="font-bold">{poolMember}</span> pool
{t('validatorPool.inPool', { category: poolMember })}
</p>
</div>
<PoolCategorySelector
@@ -163,13 +165,13 @@ export function ValidatorPoolDashboard() {
) : (
<UserX className="mr-2 h-4 w-4" />
)}
Leave Pool
{t('validatorPool.leavePool')}
</Button>
</div>
) : (
<div className="space-y-4">
<div className="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<p className="text-yellow-400">You are not currently in a validator pool</p>
<p className="text-yellow-400">{t('validatorPool.notInPool')}</p>
</div>
<PoolCategorySelector
onCategoryChange={handleJoin}
@@ -179,7 +181,7 @@ export function ValidatorPoolDashboard() {
)
) : (
<div className="p-6 text-center">
<p className="text-gray-400">Please connect your wallet to manage pool membership</p>
<p className="text-gray-400">{t('validatorPool.connectWallet')}</p>
</div>
)}
</CardContent>