@@ -428,11 +458,15 @@ function FragmentRow({
onToggleExpand,
onRetry,
}: FragmentRowProps) {
+ const { t } = useTranslation();
+
return (
<>
|
{!summary ? (
- Loading recipient statuses...
+
+ {t('bulkPaymentStatusTracker.loadingRecipientStatuses')}
+
) : (
{summary.items.map((recipient) => (
@@ -486,11 +526,22 @@ function FragmentRow({
key={recipient.id}
className="flex items-center justify-between rounded-md border border-hi/30 px-3 py-2 text-xs"
>
- {getEmployeeName(recipient)}
+
+ {getEmployeeName(
+ recipient,
+ t('bulkPaymentStatusTracker.employeeFallback', {
+ id: recipient.employee_id,
+ })
+ )}
+
{recipient.amount} {run.asset_code}
- {toRecipientStatus(recipient.status)}
+
+ {t(
+ `bulkPaymentStatusTracker.status.${toRecipientStatus(recipient.status)}`
+ )}
+
))}
diff --git a/frontend/src/components/CSVUploader.tsx b/frontend/src/components/CSVUploader.tsx
index 26b62ac2..516fa070 100644
--- a/frontend/src/components/CSVUploader.tsx
+++ b/frontend/src/components/CSVUploader.tsx
@@ -1,4 +1,5 @@
import React, { useState, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
import { Upload, AlertCircle, CheckCircle } from 'lucide-react';
export interface CSVRow {
@@ -19,6 +20,7 @@ export const CSVUploader: React.FC = ({
onDataParsed,
validators = {},
}) => {
+ const { t } = useTranslation();
const [isDragging, setIsDragging] = useState(false);
const [parsedData, setParsedData] = useState([]);
const [fileName, setFileName] = useState(null);
@@ -33,7 +35,7 @@ export const CSVUploader: React.FC = ({
// Validate headers
const missingColumns = requiredColumns.filter((col) => !headers.includes(col));
if (missingColumns.length > 0) {
- alert(`Missing required columns: ${missingColumns.join(', ')}`);
+ alert(t('csvUploader.missingRequiredColumns', { columns: missingColumns.join(', ') }));
return [];
}
@@ -51,7 +53,7 @@ export const CSVUploader: React.FC = ({
// Validate each field
requiredColumns.forEach((col) => {
if (!row[col]) {
- errors.push(`Missing required field: ${col}`);
+ errors.push(t('csvUploader.missingRequiredField', { column: col }));
}
});
@@ -78,7 +80,7 @@ export const CSVUploader: React.FC = ({
const handleFileParse = (file: File) => {
if (!file.name.endsWith('.csv')) {
- alert('Please upload a CSV file');
+ alert(t('csvUploader.pleaseUploadCsv'));
return;
}
@@ -148,10 +150,10 @@ export const CSVUploader: React.FC = ({
@@ -159,16 +161,16 @@ export const CSVUploader: React.FC = ({
{/* File info */}
{fileName && (
- File: {fileName}
+ {t('csvUploader.file', { fileName })}
- {validRowsCount} valid rows
+ {t('csvUploader.validRows', { count: validRowsCount })}
{invalidRowsCount > 0 && (
- {invalidRowsCount} rows with errors
+ {t('csvUploader.rowsWithErrors', { count: invalidRowsCount })}
)}
@@ -178,19 +180,19 @@ export const CSVUploader: React.FC = ({
{/* Preview Table */}
{parsedData.length > 0 && (
- Preview
+ {t('csvUploader.preview')}
- | Row |
- Status |
+ {t('csvUploader.row')} |
+ {t('csvUploader.status')} |
{Object.keys(parsedData[0]?.data || {}).map((col) => (
{col}
|
))}
- Errors |
+ {t('csvUploader.errors')} |
@@ -227,7 +229,7 @@ export const CSVUploader: React.FC = ({
))}
) : (
- OK
+ {t('csvUploader.ok')}
)}
diff --git a/frontend/src/components/CertificateDownloadButton.tsx b/frontend/src/components/CertificateDownloadButton.tsx
index c463d215..70a4c237 100644
--- a/frontend/src/components/CertificateDownloadButton.tsx
+++ b/frontend/src/components/CertificateDownloadButton.tsx
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { FileText, Loader2 } from 'lucide-react';
import { downloadCertificate, getTransactionInfo } from '../services/certificateApi.js';
import { useNotification } from '../hooks/useNotification.js';
+import { useTranslation } from 'react-i18next';
interface CertificateDownloadButtonProps {
transactionHash: string;
@@ -21,6 +22,7 @@ export function CertificateDownloadButton({
const [employeeId, setEmployeeId] = useState(propEmployeeId);
const [organizationId, setOrganizationId] = useState(propOrganizationId);
const { notifySuccess, notifyError } = useNotification();
+ const { t } = useTranslation();
// Auto-fetch employee and organization info if not provided
useEffect(() => {
@@ -44,7 +46,7 @@ export function CertificateDownloadButton({
const handleDownload = async () => {
if (!transactionHash) {
- notifyError('Missing Information', 'Transaction hash is required');
+ notifyError(t('certificate.missingInformation'), t('certificate.transactionHashRequired'));
return;
}
@@ -71,8 +73,8 @@ export function CertificateDownloadButton({
if (!finalEmployeeId || !finalOrganizationId) {
notifyError(
- 'Missing Information',
- 'Unable to determine employee and organization. Please provide employeeId and organizationId.'
+ t('certificate.missingInformation'),
+ t('certificate.identityUnavailable')
);
return;
}
@@ -85,13 +87,13 @@ export function CertificateDownloadButton({
organizationId: finalOrganizationId,
});
notifySuccess(
- 'Certificate Downloaded',
- 'Payment certificate has been downloaded successfully'
+ t('certificate.downloadedTitle'),
+ t('certificate.downloadedBody')
);
} catch (error) {
notifyError(
- 'Download Failed',
- error instanceof Error ? error.message : 'Failed to download certificate'
+ t('certificate.downloadFailed'),
+ error instanceof Error ? error.message : t('certificate.downloadFailedBody')
);
} finally {
setIsDownloading(false);
@@ -111,17 +113,17 @@ export function CertificateDownloadButton({
}}
disabled={isDisabled}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-blue-500/10 hover:bg-blue-500/20 text-blue-400 text-xs font-semibold transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- title="Download Proof of Payment Certificate"
+ title={t('certificate.downloadTitle')}
>
{isDownloading || isLoadingInfo ? (
<>
- {isDownloading ? 'Generating...' : 'Loading...'}
+ {isDownloading ? t('certificate.generating') : t('certificate.loading')}
>
) : (
<>
- Certificate
+ {t('certificate.label')}
>
)}
diff --git a/frontend/src/components/ConnectAccount.tsx b/frontend/src/components/ConnectAccount.tsx
index a1c3394d..c92b6685 100644
--- a/frontend/src/components/ConnectAccount.tsx
+++ b/frontend/src/components/ConnectAccount.tsx
@@ -20,15 +20,15 @@ const ConnectAccount: React.FC = () => {
{token && (
- Social Active
+ {t('connectAccount.socialActive')}
- Session Active
+ {t('connectAccount.sessionActive')}
)}
{address && (
- Stellar
+ {t('connectAccount.stellar')}
{address.slice(0, 6)}...{address.slice(-4)}
@@ -42,7 +42,7 @@ const ConnectAccount: React.FC = () => {
}}
className="px-4 py-2 rounded-full border border-border-hi text-xs font-semibold text-text hover:bg-danger/10 hover:border-danger/30 hover:text-danger transition-colors"
>
- Exit
+ {t('connectAccount.exit')}
);
@@ -56,7 +56,7 @@ const ConnectAccount: React.FC = () => {
}}
className="px-3 py-2 text-sm font-semibold text-text hover:text-accent transition-colors"
>
- Sign In
+ {t('connectAccount.signIn')}
diff --git a/frontend/src/components/ContractErrorPanel.tsx b/frontend/src/components/ContractErrorPanel.tsx
index 83a0108c..c5fc6d7e 100644
--- a/frontend/src/components/ContractErrorPanel.tsx
+++ b/frontend/src/components/ContractErrorPanel.tsx
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { ChevronDown, ChevronUp, Copy, AlertTriangle, Info } from 'lucide-react';
import { ContractErrorDetails } from '../utils/contractErrorParser';
+import { useTranslation } from 'react-i18next';
import styles from './ContractErrorPanel.module.css';
interface Props {
@@ -10,6 +11,7 @@ interface Props {
export const ContractErrorPanel: React.FC = ({ error, className = '' }) => {
const [isExpanded, setIsExpanded] = useState(true);
+ const { t } = useTranslation();
if (!error) return null;
@@ -20,13 +22,19 @@ export const ContractErrorPanel: React.FC = ({ error, className = '' }) =
};
const isUnknown = error.code === 'UNKNOWN_FORMAT' || error.code === 'UNPARSEABLE_XDR';
+ const message = error.messageKey
+ ? t(error.messageKey, { ...error.translationValues, defaultValue: error.message })
+ : error.message;
+ const action = error.actionKey
+ ? t(error.actionKey, { ...error.translationValues, defaultValue: error.action })
+ : error.action;
return (
setIsExpanded(!isExpanded)}>
- Contract Invocation Failed
+ {t('contractError.title')}
{error.code}
@@ -37,33 +45,33 @@ export const ContractErrorPanel: React.FC = ({ error, className = '' }) =
{isExpanded && (
- {error.message}
+ {message}
- Suggested Action
+ {t('contractError.suggestedAction')}
- {error.action}
+ {action}
{(isUnknown || error.rawXdr) && (
- Raw Transaction Result (XDR)
+ {t('contractError.rawTransactionResult')}
- {error.rawXdr || 'N/A'}
+ {error.rawXdr || t('contractError.notAvailable')}
)}
diff --git a/frontend/src/components/ContractUpgradeTab.tsx b/frontend/src/components/ContractUpgradeTab.tsx
index c99bed60..77ab5fba 100644
--- a/frontend/src/components/ContractUpgradeTab.tsx
+++ b/frontend/src/components/ContractUpgradeTab.tsx
@@ -37,6 +37,7 @@ import {
} from '../services/contractUpgrade';
import UpgradeConfirmModal from './UpgradeConfirmModal';
import { useNotification } from '../hooks/useNotification';
+import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------------
// Props
@@ -78,6 +79,7 @@ function NetworkBadge({ network }: { network: string }) {
/** Upgrade log status badge */
function StatusBadge({ status }: { status: UpgradeLog['status'] }) {
+ const { t } = useTranslation();
const colorMap: Record = {
pending: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/30',
simulated: 'bg-blue-500/10 text-blue-400 border-blue-500/30',
@@ -91,7 +93,7 @@ function StatusBadge({ status }: { status: UpgradeLog['status'] }) {
- {status}
+ {t(`contractUpgrade.status.${status}`)}
);
}
@@ -106,6 +108,7 @@ interface ContractCardProps {
}
function ContractCard({ contract, onUpgrade }: ContractCardProps) {
+ const { t, i18n } = useTranslation();
const [showHistory, setShowHistory] = useState(false);
const [logs, setLogs] = useState([]);
const [logsLoading, setLogsLoading] = useState(false);
@@ -147,7 +150,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
className="shrink-0 flex items-center gap-1.5 px-3 py-2 bg-accent/15 text-accent border border-accent/30 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-accent hover:text-black transition-all"
>
- Upgrade
+ {t('contractUpgrade.upgrade')}
@@ -156,7 +159,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
- Contract ID
+ {t('contractUpgrade.contractId')}
{contract.contract_id}
@@ -165,7 +168,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
target="_blank"
rel="noreferrer"
className="text-muted hover:text-accent transition-colors shrink-0"
- title="View on Stellar Expert"
+ title={t('contractUpgrade.viewOnStellarExpert')}
>
@@ -174,7 +177,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
- Current WASM Hash
+ {t('contractUpgrade.currentWasmHash')}
@@ -184,13 +187,13 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
- Last upgraded:{' '}
+ {t('contractUpgrade.lastUpgraded')}{' '}
- {new Date(contract.last_upgraded_at).toLocaleString()}
+ {new Date(contract.last_upgraded_at).toLocaleString(i18n.language)}
{contract.last_upgraded_by && (
<>
- {' by '}
+ {' '}{t('contractUpgrade.by')}{' '}
{contract.last_upgraded_by.slice(0, 8)}…
>
)}
@@ -208,7 +211,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
>
- {logsLoading ? 'Loading history…' : 'Upgrade History'}
+ {logsLoading ? t('contractUpgrade.loadingHistory') : t('contractUpgrade.upgradeHistory')}
{showHistory ? : }
@@ -216,16 +219,16 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
{showHistory && (
{logs.length === 0 ? (
- No upgrade history yet.
+ {t('contractUpgrade.noHistory')}
) : (
- | Date |
- New Hash |
- Status |
- TX |
+ {t('contractUpgrade.date')} |
+ {t('contractUpgrade.newHash')} |
+ {t('contractUpgrade.statusLabel')} |
+ {t('contractUpgrade.tx')} |
@@ -235,7 +238,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
className="border-b border-hi/40 hover:bg-(--surface-hi) transition-colors"
>
- {new Date(log.created_at).toLocaleDateString()}
+ {new Date(log.created_at).toLocaleDateString(i18n.language)}
|
@@ -270,6 +273,7 @@ function ContractCard({ contract, onUpgrade }: ContractCardProps) {
// ---------------------------------------------------------------------------
export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabProps) {
+ const { t } = useTranslation();
const { notifyError } = useNotification();
const [contracts, setContracts] = useState([]);
@@ -283,13 +287,13 @@ export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabP
setContracts(data);
} catch (err: unknown) {
notifyError(
- 'Load Failed',
- err instanceof Error ? err.message : 'Failed to load contract registry.'
+ t('contractUpgrade.loadFailed'),
+ err instanceof Error ? err.message : t('contractUpgrade.loadFailedBody')
);
} finally {
setLoading(false);
}
- }, [notifyError]);
+ }, [notifyError, t]);
useEffect(() => {
void loadContracts();
@@ -316,7 +320,7 @@ export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabP
{/* Section header */}
- Contract Registry
+ {t('contractUpgrade.registry')}
{/* Description */}
- Manage deployed Soroban smart contract upgrades. Each upgrade triggers a simulation,
- multi-step confirmation, and on-chain execution followed by automated data migration.
+ {t('contractUpgrade.description')}
{/* No admin address warning */}
@@ -339,7 +342,7 @@ export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabP
- Connect your admin wallet to initiate contract upgrades.
+ {t('contractUpgrade.connectAdmin')}
)}
@@ -361,9 +364,9 @@ export default function ContractUpgradeTab({ adminAddress }: ContractUpgradeTabP
) : contracts.length === 0 ? (
- No contracts found in registry.
+ {t('contractUpgrade.noContracts')}
- Run the database migration to seed the contract registry.
+ {t('contractUpgrade.seedRegistry')}
) : (
diff --git a/frontend/src/components/CountdownTimer.tsx b/frontend/src/components/CountdownTimer.tsx
index 061e6432..c934f728 100644
--- a/frontend/src/components/CountdownTimer.tsx
+++ b/frontend/src/components/CountdownTimer.tsx
@@ -1,6 +1,8 @@
import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
export const CountdownTimer = ({ targetDate }: { targetDate: Date | null }) => {
+ const { t } = useTranslation();
const [timeLeft, setTimeLeft] = useState({
days: 0,
hours: 0,
@@ -40,28 +42,28 @@ export const CountdownTimer = ({ targetDate }: { targetDate: Date | null }) => {
{timeLeft.days}
- Days
+ {t('countdownTimer.days')}
:
{timeLeft.hours.toString().padStart(2, '0')}
- Hrs
+ {t('countdownTimer.hours')}
:
{timeLeft.minutes.toString().padStart(2, '0')}
- Min
+ {t('countdownTimer.minutes')}
:
{timeLeft.seconds.toString().padStart(2, '0')}
- Sec
+ {t('countdownTimer.seconds')}
);
diff --git a/frontend/src/components/DashboardSidebar.tsx b/frontend/src/components/DashboardSidebar.tsx
index 2b32a087..795843e3 100644
--- a/frontend/src/components/DashboardSidebar.tsx
+++ b/frontend/src/components/DashboardSidebar.tsx
@@ -14,25 +14,26 @@ import {
TrendingUp,
Receipt,
} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
interface NavItem {
icon: React.ComponentType<{ className?: string }>;
- label: string;
+ labelKey: string;
path: string;
}
const navItems: NavItem[] = [
- { icon: LayoutDashboard, label: 'Dashboard', path: '/' },
- { icon: Wallet, label: 'Payroll', path: '/payroll' },
- { icon: Users, label: 'Employees', path: '/employee' },
- { icon: TrendingUp, label: 'Forecast', path: '/forecast' },
- { icon: FileText, label: 'Reports', path: '/reports' },
- { icon: Receipt, label: 'Tax Compliance', path: '/tax-compliance' },
- { icon: Globe, label: 'Cross-Asset', path: '/cross-asset-payment' },
- { icon: History, label: 'History', path: '/transactions' },
- { icon: Layout, label: 'Employee Portal', path: '/portal' },
- { icon: ShieldAlert, label: 'Security Center', path: '/admin' },
- { icon: Settings, label: 'Settings', path: '/settings' },
+ { icon: LayoutDashboard, labelKey: 'nav.dashboard', path: '/' },
+ { icon: Wallet, labelKey: 'nav.payroll', path: '/payroll' },
+ { icon: Users, labelKey: 'nav.employees', path: '/employee' },
+ { icon: TrendingUp, labelKey: 'nav.forecast', path: '/forecast' },
+ { icon: FileText, labelKey: 'nav.reports', path: '/reports' },
+ { icon: Receipt, labelKey: 'nav.taxCompliance', path: '/tax-compliance' },
+ { icon: Globe, labelKey: 'nav.crossAsset', path: '/cross-asset-payment' },
+ { icon: History, labelKey: 'nav.history', path: '/transactions' },
+ { icon: Layout, labelKey: 'nav.employeePortal', path: '/portal' },
+ { icon: ShieldAlert, labelKey: 'nav.securityCenter', path: '/admin' },
+ { icon: Settings, labelKey: 'nav.settings', path: '/settings' },
];
interface DashboardSidebarProps {
@@ -42,6 +43,8 @@ interface DashboardSidebarProps {
export const DashboardSidebar: React.FC = ({
onClose,
}: DashboardSidebarProps) => {
+ const { t } = useTranslation();
+
return (
|
| |