From 77bdfb46641a53df9f65ad56743192ea101248c4 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Jul 2026 11:18:20 +0100 Subject: [PATCH 01/12] feat(i18n): configure i18n infrastructure, RTL support, and LanguageSwitcher --- frontend/i18next-scanner.config.js | 2 +- frontend/scripts/validate-translations.js | 2 +- frontend/src/App.tsx | 23 +++++++++++++++++++- frontend/src/components/LanguageSwitcher.tsx | 8 +++++-- frontend/src/components/index.ts | 2 ++ frontend/src/contexts/ThemeContext.tsx | 13 +++++++++-- frontend/src/i18n/config.ts | 12 ++++++++++ frontend/src/index.tsx | 1 + 8 files changed, 56 insertions(+), 7 deletions(-) diff --git a/frontend/i18next-scanner.config.js b/frontend/i18next-scanner.config.js index 6bd8ba6..fab045d 100644 --- a/frontend/i18next-scanner.config.js +++ b/frontend/i18next-scanner.config.js @@ -15,7 +15,7 @@ module.exports = { supportBasicHtmlNodes: true, keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'em', 'span'] }, - lngs: ['en', 'es', 'zh'], + lngs: ['en', 'es', 'zh', 'ar'], fallbackLng: 'en', ns: ['translation'], defaultNs: 'translation', diff --git a/frontend/scripts/validate-translations.js b/frontend/scripts/validate-translations.js index e5e36a2..b6d2f4a 100644 --- a/frontend/scripts/validate-translations.js +++ b/frontend/scripts/validate-translations.js @@ -5,7 +5,7 @@ const path = require('path'); // Configuration const LOCALES_DIR = path.join(__dirname, '../src/locales'); -const SUPPORTED_LANGUAGES = ['en', 'es', 'zh']; +const SUPPORTED_LANGUAGES = ['en', 'es', 'zh', 'ar']; // Colors for console output const colors = { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4138580..82932fb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,7 +4,7 @@ import { Box, Button, Typography } from '@mui/material'; import { Logout as LogoutIcon, AccountCircle } from '@mui/icons-material'; import FormValidationExample from './components/FormValidationExample'; import DataVisualizationExample from './components/DataVisualizationExample'; -import { AnalyticsDashboard } from './components'; +import { AnalyticsDashboard, LanguageSwitcher } from './components'; import { AuthProvider, useAuth } from './components/auth/AuthContext'; import ProtectedRoute from './components/auth/ProtectedRoute'; import { ThemeProvider } from './contexts/ThemeContext'; @@ -78,6 +78,27 @@ const DashboardShell: React.FC = () => { justifyContent: 'center', mb: { xs: 2, sm: 0 } }}> + diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index f41dc1e..1f9858e 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -12,12 +12,15 @@ import { } from '@mui/material'; import { supportedLanguages, changeLanguage, getCurrentLanguage } from '../i18n/config'; +import { SxProps, Theme } from '@mui/material'; + interface LanguageSwitcherProps { variant?: 'select' | 'chip' | 'avatar'; size?: 'small' | 'medium' | 'large'; showFlag?: boolean; showNativeName?: boolean; compact?: boolean; + sx?: SxProps; } export const LanguageSwitcher: React.FC = ({ @@ -25,7 +28,8 @@ export const LanguageSwitcher: React.FC = ({ size = 'medium', showFlag = true, showNativeName = true, - compact = false + compact = false, + sx }) => { const { i18n } = useTranslation(); const currentLang = getCurrentLanguage(); @@ -40,7 +44,7 @@ export const LanguageSwitcher: React.FC = ({ }; const renderSelectVariant = () => ( - + setTimeRange(e.target.value)}> - Last 7 Days - Last 30 Days - Last 90 Days + {t('analytics.timeRange')} + @@ -129,26 +132,26 @@ export const AnalyticsDashboard: React.FC = () => { {/* KPI Cards */} } color="#3B82F6" /> } color="#10B981" /> } color="#F59E0B" /> } color="#8B5CF6" /> @@ -161,7 +164,7 @@ export const AnalyticsDashboard: React.FC = () => { )} @@ -171,7 +174,7 @@ export const AnalyticsDashboard: React.FC = () => { {dashboardData && ( )} @@ -181,7 +184,7 @@ export const AnalyticsDashboard: React.FC = () => { {dashboardData && ( )} @@ -190,14 +193,14 @@ export const AnalyticsDashboard: React.FC = () => { - System Health & Performance + {t('dashboard.systemHealth')} - 95 ? 'good' : 'warning'} /> - - - + 95 ? 'good' : 'warning'} /> + + + diff --git a/frontend/src/components/CreditScoreCard.tsx b/frontend/src/components/CreditScoreCard.tsx index 9c0dde4..b3ce17a 100644 --- a/frontend/src/components/CreditScoreCard.tsx +++ b/frontend/src/components/CreditScoreCard.tsx @@ -17,6 +17,8 @@ import { AccessTime } from '@mui/icons-material'; import { CreditScoreCardProps, getScoreRating } from '../types/credit-score'; +import { useTranslation } from 'react-i18next'; +import { formatRelativeTime } from '../i18n/config'; export const CreditScoreCard: React.FC = ({ score, @@ -25,6 +27,7 @@ export const CreditScoreCard: React.FC = ({ loading = false, error }) => { + const { t } = useTranslation(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); @@ -41,7 +44,7 @@ export const CreditScoreCard: React.FC = ({ - Loading credit score... + {t('app.loading')} @@ -74,21 +77,9 @@ export const CreditScoreCard: React.FC = ({ return scoreDiff > 0 ? theme.palette.success.main : theme.palette.error.main; }; - const formatDate = (date: Date): string => { - const now = new Date(); - const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60)); - - if (diffInHours < 1) return 'Just now'; - if (diffInHours < 24) return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`; - - const diffInDays = Math.floor(diffInHours / 24); - if (diffInDays < 7) return `${diffInDays} day${diffInDays > 1 ? 's' : ''} ago`; - - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); + const getRatingLabelKey = (label: string): string => { + if (label === 'Very Poor') return 'veryPoor'; + return label.toLowerCase(); }; return ( @@ -115,12 +106,12 @@ export const CreditScoreCard: React.FC = ({ color="text.secondary" sx={{ fontSize: isMobile ? '0.875rem' : '1rem', fontWeight: 500 }} > - Your Credit Score + {t('creditScoring.score')} - Updated {formatDate(lastUpdated)} + {t('creditScoring.lastUpdated')}{' '}{formatRelativeTime(lastUpdated)} @@ -188,7 +179,7 @@ export const CreditScoreCard: React.FC = ({ color="text.secondary" sx={{ fontSize: '0.75rem' }} > - out of 100 + {t('creditScoring.outOf100', { defaultValue: 'out of 100' })} @@ -201,7 +192,7 @@ export const CreditScoreCard: React.FC = ({ alignItems: isMobile ? 'center' : 'flex-start' }}> = ({ fontWeight: 600 }} > - {Math.abs(scoreDiff)} point{Math.abs(scoreDiff) !== 1 ? 's' : ''} + {t('creditScoring.pointsDifference', { count: Math.abs(scoreDiff) })} )} @@ -248,7 +239,7 @@ export const CreditScoreCard: React.FC = ({ color="text.secondary" sx={{ mb: 1, display: 'block' }} > - Score Range + {t('creditScoring.scoreRange', { defaultValue: 'Score Range' })} { + const { t } = useTranslation(); -// Example async validation for checking if Stellar address exists -const validateStellarAddressExists = async (address: string): Promise => { - if (!address) return null; - - try { - // Simulate API call to check if address exists - await new Promise(resolve => setTimeout(resolve, 1000)); + // Async validation for checking if Stellar address exists + const validateStellarAddressExists = useCallback(async (address: string): Promise => { + if (!address) return null; - // Mock validation - in real implementation, you'd call Stellar Horizon API - if (address === 'GINVALID123456789012345678901234567890123456789012345678901234567890') { - return 'This Stellar address does not exist'; + try { + // Simulate API call to check if address exists + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Mock validation - in real implementation, you'd call Stellar Horizon API + if (address === 'GINVALID123456789012345678901234567890123456789012345678901234567890') { + return t('forms.stellarAddressNotExist', { defaultValue: 'This Stellar address does not exist' }); + } + + return null; + } catch (error) { + return t('forms.stellarAddressVerifyError', { defaultValue: 'Unable to verify address at this time' }); } - - return null; - } catch (error) { - return 'Unable to verify address at this time'; - } -}; + }, [t]); + + // Dynamic form configuration + const formConfigs: FormFieldConfig[] = useMemo(() => [ + { + name: 'email', + label: t('profile.email'), + type: 'email', + placeholder: t('forms.emailPlaceholder', { defaultValue: 'Enter your email' }), + required: true, + validation: ValidationRules.email(t('forms.invalidEmail')) + }, + { + name: 'stellarAddress', + label: t('forms.stellarAddressLabel', { defaultValue: 'Stellar Address' }), + type: 'text', + placeholder: t('forms.stellarAddressPlaceholder', { defaultValue: 'Enter your Stellar address (G...)' }), + required: true, + validation: ValidationRules.stellarAddress(t('forms.invalidStellarAddress', { defaultValue: 'Invalid Stellar address' })) + }, + { + name: 'amount', + label: t('transactions.amount'), + type: 'number', + placeholder: t('forms.amountPlaceholder', { defaultValue: 'Enter amount' }), + required: true, + validation: ValidationRules.positiveNumber(t('forms.amountPositive', { defaultValue: 'Amount must be greater than 0' })) + }, + { + name: 'scheduledDate', + label: t('forms.scheduledDateLabel', { defaultValue: 'Scheduled Date' }), + type: 'date', + required: true, + minDate: new Date(), + validation: ValidationRules.required(t('forms.scheduledDateRequired', { defaultValue: 'Please choose a scheduled date' })) + }, + { + name: 'scheduledTime', + label: t('forms.scheduledTimeLabel', { defaultValue: 'Scheduled Time' }), + type: 'time', + required: true, + minTime: '09:00', + maxTime: '17:00', + timeIntervalMinutes: 30, + timeFormat: '12', + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + validation: ValidationRules.required(t('forms.scheduledTimeRequired', { defaultValue: 'Please choose a scheduled time' })) + }, + { + name: 'memo', + label: t('forms.memoLabel', { defaultValue: 'Memo (Optional)' }), + type: 'textarea', + placeholder: t('forms.memoPlaceholder', { defaultValue: 'Enter a memo for this transaction' }), + validation: ValidationRules.maxLength(100, t('forms.maxLength', { count: 100 })) + } + ], [t]); -export const FormValidationExample: React.FC = () => { const { values, errors, @@ -102,17 +105,17 @@ export const FormValidationExample: React.FC = () => { memo: '' }, validationRules: { - email: ValidationRules.email(), + email: ValidationRules.email(t('forms.invalidEmail')), stellarAddress: ValidationRules.async(validateStellarAddressExists), - amount: ValidationRules.positiveNumber(), - scheduledDate: ValidationRules.required('Please choose a scheduled date'), - scheduledTime: ValidationRules.required('Please choose a scheduled time'), - memo: ValidationRules.maxLength(100) + amount: ValidationRules.positiveNumber(t('forms.amountPositive', { defaultValue: 'Amount must be greater than 0' })), + scheduledDate: ValidationRules.required(t('forms.scheduledDateRequired', { defaultValue: 'Please choose a scheduled date' })), + scheduledTime: ValidationRules.required(t('forms.scheduledTimeRequired', { defaultValue: 'Please choose a scheduled time' })), + memo: ValidationRules.maxLength(100, t('forms.maxLength', { count: 100 })) }, onSubmit: async () => { // Simulate API call await new Promise(resolve => setTimeout(resolve, 2000)); - alert('Form submitted successfully!'); + alert(t('forms.submittedSuccessfully', { defaultValue: 'Form submitted successfully!' })); }, validateOnChange: true, validateOnBlur: true, @@ -121,8 +124,8 @@ export const FormValidationExample: React.FC = () => { return (
-

Form Validation Example

-

This example demonstrates real-time validation, debouncing, and blockchain address validation.

+

{t('forms.title', { defaultValue: 'Form Validation Example' })}

+

{t('forms.subtitle', { defaultValue: 'This example demonstrates real-time validation, debouncing, and blockchain address validation.' })}

{formConfigs.map((config) => ( @@ -152,7 +155,7 @@ export const FormValidationExample: React.FC = () => { cursor: isValid && !isSubmitting ? 'pointer' : 'not-allowed' }} > - {isSubmitting ? 'Submitting...' : 'Submit Transaction'} + {isSubmitting ? t('forms.submitting', { defaultValue: 'Submitting...' }) : t('forms.submit', { defaultValue: 'Submit Transaction' })}
@@ -178,11 +181,11 @@ export const FormValidationExample: React.FC = () => { borderRadius: '4px', border: '1px solid #dee2e6' }}> -

Form Status:

+

{t('forms.status', { defaultValue: 'Form Status:' })}

    -
  • Valid: {isValid ? '✅' : '❌'}
  • -
  • Dirty: {isDirty ? '✅' : '❌'}
  • -
  • Submitting: {isSubmitting ? '✅' : '❌'}
  • +
  • {t('forms.valid', { defaultValue: 'Valid' })}: {isValid ? '✅' : '❌'}
  • +
  • {t('forms.dirty', { defaultValue: 'Dirty' })}: {isDirty ? '✅' : '❌'}
  • +
  • {t('forms.submittingLabel', { defaultValue: 'Submitting' })}: {isSubmitting ? '✅' : '❌'}
diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index 7820a16..d7afe6f 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -22,11 +22,13 @@ import { GitHub } from '@mui/icons-material'; import { useNavigate, useLocation, Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { ValidationRules } from '@chenaikit/core'; -import { useFormValidation } from '../../hooks/useFormValidation'; import { useAuth } from './AuthContext'; +import { useFormValidation } from '../../hooks/useFormValidation'; export const LoginForm: React.FC = () => { + const { t } = useTranslation(); const { login } = useAuth(); const navigate = useNavigate(); const location = useLocation(); @@ -50,8 +52,8 @@ export const LoginForm: React.FC = () => { password: '' }, validationRules: { - email: ValidationRules.email(), - password: ValidationRules.required('Password is required') + email: ValidationRules.email(t('forms.invalidEmail')), + password: ValidationRules.required(t('forms.required')) }, onSubmit: async (formValues) => { setSubmitError(null); @@ -59,7 +61,7 @@ export const LoginForm: React.FC = () => { await login(formValues.email, formValues.password, rememberMe); navigate(from, { replace: true }); } catch (err: any) { - setSubmitError(err.message || 'Failed to sign in. Please try again.'); + setSubmitError(err.message || t('auth.loginError')); } }, validateOnChange: true, @@ -74,10 +76,10 @@ export const LoginForm: React.FC = () => { - Welcome back + {t('auth.welcomeBack')} - Enter your details to access your dashboard + {t('auth.loginSubtitle')} @@ -92,7 +94,7 @@ export const LoginForm: React.FC = () => { handleChange('email', e.target.value)} @@ -115,7 +117,7 @@ export const LoginForm: React.FC = () => { { /> } label={ - - Remember me - + + {t('auth.rememberMe')} + } /> { '&:hover': { textDecoration: 'underline' } }} > - Forgot password? + {t('auth.forgotPassword')} @@ -183,25 +185,25 @@ export const LoginForm: React.FC = () => { variant="contained" disabled={isSubmitting || !isValid} fullWidth - sx={{ - py: 1.5, - borderRadius: '10px', - textTransform: 'none', - fontSize: '16px', - fontWeight: 600, - boxShadow: (theme) => theme.shadows[4], - }} + sx={{ + py: 1.5, + borderRadius: '10px', + textTransform: 'none', + fontSize: '16px', + fontWeight: 600, + boxShadow: (theme) => theme.shadows[4], + }} > {isSubmitting ? ( ) : ( - 'Sign In' + t('auth.signIn') )} - OR CONTINUE WITH + {t('auth.orContinueWith')} @@ -250,7 +252,7 @@ export const LoginForm: React.FC = () => { - Don't have an account?{' '} + {t('auth.dontHaveAccount')}{' '} { '&:hover': { textDecoration: 'underline' } }} > - Sign up + {t('auth.signUp')} diff --git a/frontend/src/components/auth/SignupForm.tsx b/frontend/src/components/auth/SignupForm.tsx index 8e78908..4cc0fe9 100644 --- a/frontend/src/components/auth/SignupForm.tsx +++ b/frontend/src/components/auth/SignupForm.tsx @@ -21,18 +21,19 @@ import { CheckCircleOutline } from '@mui/icons-material'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { ValidationRules } from '@chenaikit/core'; import { useFormValidation } from '../../hooks/useFormValidation'; import { useAuth } from './AuthContext'; interface PasswordStrength { score: number; - text: string; + textKey: string; color: string; } const getPasswordStrength = (password: string): PasswordStrength => { - if (!password) return { score: 0, text: '', color: '#e2e8f0' }; + if (!password) return { score: 0, textKey: '', color: '#e2e8f0' }; let score = 0; if (password.length >= 8) score += 1; if (/[A-Z]/.test(password)) score += 1; @@ -40,12 +41,13 @@ const getPasswordStrength = (password: string): PasswordStrength => { if (/[0-9]/.test(password)) score += 1; if (/[^A-Za-z0-9]/.test(password)) score += 1; - if (score <= 2) return { score: 33, text: 'Weak', color: '#f87171' }; - if (score <= 4) return { score: 66, text: 'Medium', color: '#fbbf24' }; - return { score: 100, text: 'Strong', color: '#34d399' }; + if (score <= 2) return { score: 33, textKey: 'auth.weak', color: '#f87171' }; + if (score <= 4) return { score: 66, textKey: 'auth.medium', color: '#fbbf24' }; + return { score: 100, textKey: 'auth.strong', color: '#34d399' }; }; export const SignupForm: React.FC = () => { + const { t } = useTranslation(); const { register } = useAuth(); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); @@ -72,20 +74,20 @@ export const SignupForm: React.FC = () => { confirmPassword: '' }, validationRules: { - email: ValidationRules.email(), - password: ValidationRules.minLength(8, 'Password must be at least 8 characters long') + email: ValidationRules.email(t('forms.invalidEmail')), + password: ValidationRules.minLength(8, t('forms.minLength', { count: 8 })) }, onSubmit: async (formValues) => { setSubmitError(null); setTermsError(null); if (!acceptTerms) { - setTermsError('You must accept the terms of service to register.'); + setTermsError(t('auth.acceptTermsError')); return; } if (formValues.password !== formValues.confirmPassword) { - setError('confirmPassword', 'Passwords do not match'); + setError('confirmPassword', t('forms.passwordMismatch')); return; } @@ -94,7 +96,7 @@ export const SignupForm: React.FC = () => { setRegisteredEmail(formValues.email); setIsVerificationSent(true); } catch (err: any) { - setSubmitError(err.message || 'Registration failed. Please try again.'); + setSubmitError(err.message || t('auth.registerError')); } }, validateOnChange: true, @@ -110,25 +112,25 @@ export const SignupForm: React.FC = () => {
- Account Created! + {t('auth.accountCreated')} - Your account was created for {registeredEmail}. You can now sign in to continue. + {t('auth.accountCreatedDesc', { email: registeredEmail })} - ); @@ -138,10 +140,10 @@ export const SignupForm: React.FC = () => { - Create an account + {t('auth.createAccount')} - Sign up to monitor blockchain analytics and credit metrics + {t('auth.signupSubtitle')} @@ -162,7 +164,7 @@ export const SignupForm: React.FC = () => { handleChange('email', e.target.value)} @@ -185,7 +187,7 @@ export const SignupForm: React.FC = () => { { - Password Strength: + {t('auth.passwordStrength')}: - {passwordStrength.text} + {passwordStrength.textKey ? t(passwordStrength.textKey) : ''} { { } label={ - I agree to the{' '} + {t('auth.iAgreeTo')}{' '} - Terms of Service + {t('app.termsTitle')} {' '} - and{' '} + {t('auth.and')}{' '} - Privacy Policy + {t('app.privacyTitle')} } @@ -320,25 +322,25 @@ export const SignupForm: React.FC = () => { variant="contained" disabled={isSubmitting || !isValid || !acceptTerms} fullWidth - sx={{ - py: 1.5, - borderRadius: '10px', - textTransform: 'none', - fontSize: '16px', - fontWeight: 600, - boxShadow: (theme) => theme.shadows[4], - }} + sx={{ + py: 1.5, + borderRadius: '10px', + textTransform: 'none', + fontSize: '16px', + fontWeight: 600, + boxShadow: (theme) => theme.shadows[4], + }} > {isSubmitting ? ( ) : ( - 'Sign Up' + t('auth.signUp') )} - Already have an account?{' '} + {t('auth.alreadyHaveAccount')}{' '} { '&:hover': { textDecoration: 'underline' } }} > - Sign in + {t('auth.signIn')} diff --git a/frontend/src/components/settings/AccountSettings.tsx b/frontend/src/components/settings/AccountSettings.tsx index 0dee7d4..e59b063 100644 --- a/frontend/src/components/settings/AccountSettings.tsx +++ b/frontend/src/components/settings/AccountSettings.tsx @@ -15,8 +15,10 @@ import { DialogActions } from '@mui/material'; import { useFormValidation } from '../../hooks/useFormValidation'; +import { useTranslation } from 'react-i18next'; import { ValidationRules } from '@chenaikit/core'; import { useThemeMode } from '../../contexts/ThemeContext'; +import { changeLanguage } from '../../i18n/config'; interface AccountSettingsProps { user: { @@ -34,6 +36,7 @@ export const AccountSettings: React.FC = ({ onUpdateAccount, onDeleteAccount }) => { + const { t } = useTranslation(); const { setTheme } = useThemeMode(); const [isSaving, setIsSaving] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); @@ -56,8 +59,8 @@ export const AccountSettings: React.FC = ({ theme: user.theme || 'system' }, validationRules: { - email: ValidationRules.email(), - name: ValidationRules.minLength(2, 'Name must be at least 2 characters') + email: ValidationRules.email(t('forms.invalidEmail')), + name: ValidationRules.minLength(2, t('forms.minLength', { count: 2 })) }, onSubmit: async (formValues) => { setIsSaving(true); @@ -80,6 +83,12 @@ export const AccountSettings: React.FC = ({ } }, [values.theme, setTheme]); + useEffect(() => { + if (values.language) { + changeLanguage(values.language); + } + }, [values.language]); + const handleDeleteAccount = async () => { if (deleteEmail !== user.email) return; setIsSaving(true); @@ -95,12 +104,12 @@ export const AccountSettings: React.FC = ({ - Profile Information + {t('profile.subtitle')} {saveSuccess && ( - Profile updated successfully! + {t('settings.profileUpdated')} )} @@ -109,7 +118,7 @@ export const AccountSettings: React.FC = ({ handleChange('name', e.target.value)} @@ -122,7 +131,7 @@ export const AccountSettings: React.FC = ({ = ({ handleChange('language', e.target.value)} @@ -153,15 +162,15 @@ export const AccountSettings: React.FC = ({ handleChange('theme', e.target.value)} SelectProps={{ native: true }} > - - - + + + @@ -173,7 +182,7 @@ export const AccountSettings: React.FC = ({ disabled={isSaving || !isValid} startIcon={isSaving ? : null} > - {isSaving ? 'Saving...' : 'Save Changes'} + {isSaving ? t('settings.saving') : t('common.save')} @@ -184,10 +193,10 @@ export const AccountSettings: React.FC = ({ - Danger Zone + {t('settings.dangerZone')} - Once you delete your account, there is no going back. Please be certain. + {t('settings.deleteAccountWarning')} setDeleteConfirmOpen(false)}> - Delete Account + {t('settings.deleteAccount')} - This action cannot be undone. To confirm, please type your email address:{' '} - {user.email} + {t('settings.confirmDeleteEmail', { email: user.email })} setDeleteEmail(e.target.value)} /> diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 8111e7b..df41215 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -21,6 +21,8 @@ import { Warning as WarningIcon } from '@mui/icons-material'; import ProfileHeader from '../components/profile/ProfileHeader'; +import { useTranslation } from 'react-i18next'; +import { formatDate, formatDateTime, formatNumber } from '../i18n/config'; interface TabPanelProps { children?: React.ReactNode; @@ -81,6 +83,7 @@ export const Profile: React.FC = ({ activity = [], onUpdateProfile }) => { + const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(0); return ( @@ -102,21 +105,21 @@ export const Profile: React.FC = ({ } }} > - - - + + + - Recent Activity + {t('profile.recentActivity')} {activity.length === 0 ? ( - No recent activity + {t('profile.noActivity')} ) : ( @@ -144,7 +147,7 @@ export const Profile: React.FC = ({ component="span" sx={{ display: 'block', color: 'text.disabled', mt: 0.5 }} > - {new Date(item.timestamp).toLocaleString()} + {formatDateTime(item.timestamp)} } @@ -161,12 +164,12 @@ export const Profile: React.FC = ({ - Account Information + {t('profile.accountInformation', { defaultValue: 'Account Information' })} - Email + {t('profile.email')} {user.email} @@ -174,19 +177,19 @@ export const Profile: React.FC = ({ - Role + {t('profile.role')} - {user.role || 'User'} + {t('profile.roleName.' + (user.role || 'user'), { defaultValue: user.role || 'User' })} {user.createdAt && ( - Member Since + {t('profile.memberSince')} - {new Date(user.createdAt).toLocaleDateString()} + {formatDate(user.createdAt)} )} @@ -199,19 +202,19 @@ export const Profile: React.FC = ({ - Security Status + {t('profile.securityStatus')} - Two-Factor Auth - + {t('profile.twoFactorEnabled')} + - Password - + {t('profile.passwordSet')} + - Active Sessions + {t('profile.activeSessions')} @@ -228,10 +231,10 @@ export const Profile: React.FC = ({ - {stats.transactions.toLocaleString()} + {formatNumber(stats.transactions)} - Total Transactions + {t('dashboard.totalTransactions')} @@ -241,10 +244,10 @@ export const Profile: React.FC = ({ - {stats.score} + {formatNumber(stats.score)} - Credit Score + {t('profile.creditScore')} @@ -254,10 +257,10 @@ export const Profile: React.FC = ({ - {stats.activeDays} + {formatNumber(stats.activeDays)} - Active Days + {t('profile.activeDays')} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index a8025a0..e0623a0 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -20,6 +20,7 @@ import AccountSettings from '../components/settings/AccountSettings'; import NotificationSettings from '../components/settings/NotificationSettings'; import SecuritySettings from '../components/settings/SecuritySettings'; import ApiKeysSettings from '../components/settings/ApiKeysSettings'; +import { useTranslation } from 'react-i18next'; interface TabPanelProps { children?: React.ReactNode; @@ -108,6 +109,7 @@ export const Settings: React.FC = ({ onDeleteApiKey, onRegenerateApiKey }) => { + const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(0); return ( @@ -115,15 +117,15 @@ export const Settings: React.FC = ({ - Dashboard + {t('navigation.dashboard')} - Settings + {t('settings.title')} - Settings + {t('settings.title')} @@ -146,22 +148,22 @@ export const Settings: React.FC = ({ } iconPosition="start" - label="Account" + label={t('profile.account')} /> } iconPosition="start" - label="Notifications" + label={t('settings.notifications')} /> } iconPosition="start" - label="Security" + label={t('settings.security')} /> } iconPosition="start" - label="API Keys" + label={t('settings.api')} /> From ec6950c85a23e9718fb26fd332015bd4721f4b40 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Jul 2026 11:46:28 +0100 Subject: [PATCH 03/12] feat(i18n): populate English, Spanish, Chinese, and Arabic translation files --- frontend/src/locales/ar.json | 116 +++++++++++++++++++++++++++++++++-- frontend/src/locales/en.json | 92 +++++++++++++++++++++++++-- frontend/src/locales/es.json | 116 +++++++++++++++++++++++++++++++++-- frontend/src/locales/zh.json | 116 +++++++++++++++++++++++++++++++++-- 4 files changed, 420 insertions(+), 20 deletions(-) diff --git a/frontend/src/locales/ar.json b/frontend/src/locales/ar.json index 88511b0..0fca546 100644 --- a/frontend/src/locales/ar.json +++ b/frontend/src/locales/ar.json @@ -22,7 +22,22 @@ "refresh": "تحديث", "settings": "الإعدادات", "help": "مساعدة", - "about": "حول" + "about": "حول", + "forgotPasswordTitle": "إعادة تعيين كلمة المرور", + "forgotPasswordDesc": "أدخل عنوان بريدك الإلكتروني وسنرسل لك رابطًا لإعادة تعيين كلمة المرور.", + "forgotPasswordSoon": "ميزة إعادة تعيين كلمة المرور ستتوفر قريبًا.", + "termsTitle": "شروط الخدمة", + "termsDesc": "تحكم شروط الخدمة هذه استخدامك لـ ChenaiKit. من خلال الوصول إلى منصتنا أو استخدامها، فإنك توافق على الالتزام بهذه الشروط. سيتم نشر وثائق الشروط الكاملة هنا قبل الإطلاق الفعلي.", + "privacyTitle": "سياسة الخصوصية", + "privacyDesc": "خصوصيتك مهمة جداً بالنسبة لنا. تقوم ChenaiKit بجمع البيانات اللازمة فقط لتقديم خدماتنا ولا تبيع المعلومات الشخصية لأطراف ثالثة أبداً. سيتم نشر وثائق سياسة الخصوصية الكاملة هنا قبل الإطلاق الفعلي.", + "signOut": "تسجيل الخروج", + "dashboardTitle": "ChenaiKit - لوحة معلومات ذكاء الأعمال والتحليلات", + "dashboardSubtitle": "رؤى متقدمة للذكاء الاصطناعي ومراقبة سلسلة الكتل (البلوكشين)", + "analyticsDashboard": "لوحة التحليلات", + "forms": "نماذج تجريبية", + "sandbox": "بيئة التجربة (صندوق الرمل)", + "profile": "الملف الشخصي", + "footer": "تم التطوير بواسطة ChenaiKit - حلول الذكاء الاصطناعي وسلسلة الكتل المتقدمة" }, "navigation": { "dashboard": "لوحة التحكم", @@ -33,6 +48,32 @@ "help": "مساعدة", "logout": "تسجيل الخروج" }, + "auth": { + "welcomeBack": "مرحباً بعودتك", + "loginSubtitle": "أدخل تفاصيل حسابك للوصول إلى لوحة المعلومات", + "rememberMe": "تذكرني", + "forgotPassword": "هل نسيت كلمة المرور؟", + "signIn": "تسجيل الدخول", + "orContinueWith": "أو المتابعة باستخدام", + "dontHaveAccount": "ليس لديك حساب؟", + "signUp": "إنشاء حساب", + "loginError": "فشل تسجيل الدخول. يرجى المحاولة مرة أخرى.", + "createAccount": "إنشاء حساب جديد", + "signupSubtitle": "سجل لمراقبة تحليلات سلسلة الكتل ومقاييس الائتمان", + "iAgreeTo": "أوافق على", + "and": "و", + "alreadyHaveAccount": "لديك حساب بالفعل؟ ", + "weak": "ضعيف", + "medium": "متوسط", + "strong": "قوي", + "passwordStrength": "قوة كلمة المرور", + "confirmPassword": "تأكيد كلمة المرور", + "acceptTermsError": "يجب عليك قبول شروط الخدمة للتسجيل.", + "registerError": "فشل التسجيل. يرجى المحاولة مرة أخرى.", + "accountCreated": "تم إنشاء الحساب!", + "accountCreatedDesc": "تم إنشاء حسابك لـ {{email}}.", + "goToSignIn": "الذهاب لتسجيل الدخول" + }, "dashboard": { "title": "لوحة التحكم", "subtitle": "نظرة عامة على المراقبة الفورية", @@ -127,7 +168,18 @@ "customRange": "نطاق مخصص", "exportReport": "تصدير التقرير", "downloadPdf": "تحميل PDF", - "downloadCsv": "تحميل CSV" + "downloadCsv": "تحميل CSV", + "totalRequests": "إجمالي الطلبات", + "avgLatency": "متوسط زمن الاستجابة", + "avgCreditScore": "متوسط درجة الائتمان", + "blockchainVol": "حجم تداول سلسلة الكتل", + "systemTrafficForecast": "حركة مرور النظام والتوقعات", + "blockchainAssets": "أصول سلسلة الكتل", + "successRate": "معدل النجاح", + "errorRate": "معدل الخطأ", + "fraudAlerts": "تنبيهات الاحتيال", + "resolvedAlerts": "التنبيهات التي تم حلها", + "fetchError": "فشل في جلب بيانات التحليلات. يرجى التأكد من تشغيل النظام الخلفي." }, "settings": { "title": "الإعدادات", @@ -158,7 +210,43 @@ "license": "الرخصة", "support": "الدعم", "documentation": "التوثيق", - "contact": "الاتصال" + "contact": "الاتصال", + "profileUpdated": "تم تحديث الملف الشخصي بنجاح!", + "light": "مظهر فاتح", + "dark": "مظهر داكن", + "system": "حسب النظام", + "saving": "جاري الحفظ...", + "dangerZone": "منطقة الخطر", + "deleteAccountWarning": "بمجرد حذف حسابك، لن تتمكن من التراجع عن ذلك. يرجى التأكد.", + "deleteAccount": "حذف الحساب", + "confirmDeleteEmail": "لا يمكن التراجع عن هذا الإجراء. للتأكيد، يرجى كتابة عنوان بريدك الإلكتروني: {{email}}", + "confirmEmail": "تأكيد البريد الإلكتروني", + "deleting": "جاري الحذف...", + "deleteForever": "حذف نهائي" + }, + "profile": { + "title": "الملف الشخصي", + "subtitle": "ملف تعريف حسابك", + "activity": "النشاط", + "account": "الحساب", + "statistics": "الإحصائيات", + "editProfile": "تعديل الملف الشخصي", + "displayName": "الاسم المعروض", + "email": "البريد الإلكتروني", + "role": "الدور", + "memberSince": "عضو منذ", + "transactions": "العمليات", + "creditScore": "درجة الائتمان", + "activeDays": "أيام النشاط", + "uploadAvatar": "تحميل الصورة الشخصية", + "changeAvatar": "تغيير الصورة الشخصية", + "saveChanges": "حفظ التغييرات", + "recentActivity": "النشاط الأخير", + "noActivity": "لا يوجد نشاط حديث", + "securityStatus": "حالة الأمان", + "twoFactorEnabled": "المصادقة الثنائية", + "passwordSet": "كلمة المرور", + "activeSessions": "الجلسات النشطة" }, "websocket": { "connected": "متصل", @@ -206,7 +294,11 @@ "improveScore": "تحسين درجتك", "scoreBreakdown": "تفصيل الدرجة", "noDataAvailable": "لا توجد بيانات ائتمانية متاحة", - "lastUpdated": "آخر تحديث" + "lastUpdated": "آخر تحديث", + "outOf100": "من 100", + "scoreRange": "نطاق الدرجات", + "pointsDifference_one": "{{count}} نقطة", + "pointsDifference_other": "{{count}} نقاط" }, "errors": { "networkError": "حدث خطأ في الشبكة", @@ -242,7 +334,21 @@ "uploadFile": "رفع ملف", "dragDrop": "اسحب وأفلت الملفات هنا", "fileTooBig": "الملف كبير جداً (أقصى {{size}})", - "invalidFileType": "نوع ملف غير صالح" + "invalidFileType": "نوع ملف غير صالح", + "stellarAddressNotExist": "عنوان Stellar هذا غير موجود", + "stellarAddressVerifyError": "تعذر التحقق من العنوان في الوقت الحالي", + "stellarAddressLabel": "عنوان Stellar", + "stellarAddressPlaceholder": "أدخل عنوان Stellar الخاص بك (G...)", + "invalidStellarAddress": "عنوان Stellar غير صالح", + "amountPlaceholder": "أدخل المبلغ", + "amountPositive": "يجب أن يكون المبلغ أكبر من 0", + "scheduledDateLabel": "التاريخ المجدول", + "scheduledDateRequired": "يرجى اختيار التاريخ المجدول", + "scheduledTimeLabel": "الوقت المجدول", + "scheduledTimeRequired": "يرجى اختيار الوقت المجدول", + "memoLabel": "ملاحظة (اختياري)", + "memoPlaceholder": "أدخل ملاحظة لهذه العملية", + "submittedSuccessfully": "تم إرسال النموذج بنجاح!" }, "time": { "now": "الآن", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index f53c041..b898f56 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -22,7 +22,22 @@ "refresh": "Refresh", "settings": "Settings", "help": "Help", - "about": "About" + "about": "About", + "forgotPasswordTitle": "Reset your password", + "forgotPasswordDesc": "Enter your email address and we'll send you a password reset link.", + "forgotPasswordSoon": "Password reset functionality coming soon.", + "termsTitle": "Terms of Service", + "termsDesc": "These Terms of Service govern your use of ChenaiKit. By accessing or using our platform, you agree to be bound by these terms. Full terms documentation will be published here prior to production launch.", + "privacyTitle": "Privacy Policy", + "privacyDesc": "Your privacy is important to us. ChenaiKit collects only the data necessary to provide our services and never sells personal information to third parties. Full privacy policy documentation will be published here prior to production launch.", + "signOut": "Sign Out", + "dashboardTitle": "ChenaiKit - BI & Analytics Dashboard", + "dashboardSubtitle": "Advanced AI Insights & Blockchain Monitoring", + "analyticsDashboard": "Analytics Dashboard", + "forms": "Forms", + "sandbox": "Sandbox", + "profile": "Profile", + "footer": "Built with ChenaiKit - Advanced AI and Blockchain Solutions" }, "navigation": { "dashboard": "Dashboard", @@ -33,6 +48,32 @@ "help": "Help", "logout": "Logout" }, + "auth": { + "welcomeBack": "Welcome back", + "loginSubtitle": "Enter your details to access your dashboard", + "rememberMe": "Remember me", + "forgotPassword": "Forgot password?", + "signIn": "Sign In", + "orContinueWith": "OR CONTINUE WITH", + "dontHaveAccount": "Don't have an account?", + "signUp": "Sign up", + "loginError": "Failed to sign in. Please try again.", + "createAccount": "Create an account", + "signupSubtitle": "Sign up to monitor blockchain analytics and credit metrics", + "iAgreeTo": "I agree to the", + "and": "and", + "alreadyHaveAccount": "Already have an account? ", + "weak": "Weak", + "medium": "Medium", + "strong": "Strong", + "passwordStrength": "Password Strength", + "confirmPassword": "Confirm Password", + "acceptTermsError": "You must accept the terms of service to register.", + "registerError": "Registration failed. Please try again.", + "accountCreated": "Account Created!", + "accountCreatedDesc": "Your account was created for {{email}}.", + "goToSignIn": "Go to Sign In" + }, "dashboard": { "title": "Dashboard", "subtitle": "Real-time monitoring overview", @@ -127,7 +168,18 @@ "customRange": "Custom Range", "exportReport": "Export Report", "downloadPdf": "Download PDF", - "downloadCsv": "Download CSV" + "downloadCsv": "Download CSV", + "totalRequests": "Total Requests", + "avgLatency": "Avg Latency", + "avgCreditScore": "Avg Credit Score", + "blockchainVol": "Blockchain Vol", + "systemTrafficForecast": "System Traffic & Forecast", + "blockchainAssets": "Blockchain Assets", + "successRate": "Success Rate", + "errorRate": "Error Rate", + "fraudAlerts": "Fraud Alerts", + "resolvedAlerts": "Resolved Alerts", + "fetchError": "Failed to fetch analytics data. Please ensure the backend is running." }, "settings": { "title": "Settings", @@ -158,7 +210,19 @@ "license": "License", "support": "Support", "documentation": "Documentation", - "contact": "Contact" + "contact": "Contact", + "profileUpdated": "Profile updated successfully!", + "light": "Light", + "dark": "Dark", + "system": "System", + "saving": "Saving...", + "dangerZone": "Danger Zone", + "deleteAccountWarning": "Once you delete your account, there is no going back. Please be certain.", + "deleteAccount": "Delete Account", + "confirmDeleteEmail": "This action cannot be undone. To confirm, please type your email address: {{email}}", + "confirmEmail": "Confirm Email", + "deleting": "Deleting...", + "deleteForever": "Delete Forever" }, "profile": { "title": "Profile", @@ -230,7 +294,11 @@ "improveScore": "Improve Your Score", "scoreBreakdown": "Score Breakdown", "noDataAvailable": "No credit data available", - "lastUpdated": "Last Updated" + "lastUpdated": "Last Updated", + "outOf100": "out of 100", + "scoreRange": "Score Range", + "pointsDifference_one": "{{count}} point", + "pointsDifference_other": "{{count}} points" }, "errors": { "networkError": "Network error occurred", @@ -266,7 +334,21 @@ "uploadFile": "Upload file", "dragDrop": "Drag and drop files here", "fileTooBig": "File is too big (max {{size}})", - "invalidFileType": "Invalid file type" + "invalidFileType": "Invalid file type", + "stellarAddressNotExist": "This Stellar address does not exist", + "stellarAddressVerifyError": "Unable to verify address at this time", + "stellarAddressLabel": "Stellar Address", + "stellarAddressPlaceholder": "Enter your Stellar address (G...)", + "invalidStellarAddress": "Invalid Stellar address", + "amountPlaceholder": "Enter amount", + "amountPositive": "Amount must be greater than 0", + "scheduledDateLabel": "Scheduled Date", + "scheduledDateRequired": "Please choose a scheduled date", + "scheduledTimeLabel": "Scheduled Time", + "scheduledTimeRequired": "Please choose a scheduled time", + "memoLabel": "Memo (Optional)", + "memoPlaceholder": "Enter a memo for this transaction", + "submittedSuccessfully": "Form submitted successfully!" }, "time": { "now": "Now", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 979084e..f53bd56 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -22,7 +22,22 @@ "refresh": "Actualizar", "settings": "Configuración", "help": "Ayuda", - "about": "Acerca de" + "about": "Acerca de", + "forgotPasswordTitle": "Restablecer su contraseña", + "forgotPasswordDesc": "Ingrese su dirección de correo electrónico y le enviaremos un enlace de restablecimiento.", + "forgotPasswordSoon": "Funcionalidad de restablecimiento de contraseña próximamente.", + "termsTitle": "Términos de Servicio", + "termsDesc": "Estos Términos de Servicio rigen el uso de ChenaiKit. Al acceder o utilizar nuestra plataforma, acepta estar sujeto a estos términos. La documentación completa de los términos se publicará aquí antes del lanzamiento de producción.", + "privacyTitle": "Política de Privacidad", + "privacyDesc": "Su privacidad es importanta para nosotros. ChenaiKit solo recopila los datos necesarios para proporcionar nuestros servicios y nunca vende información personal a terceros. La documentación completa de la política de privacidad se publicará aquí antes del lanzamiento de producción.", + "signOut": "Cerrar sesión", + "dashboardTitle": "ChenaiKit - Panel de BI y Analytics", + "dashboardSubtitle": "Información de IA avanzada y monitoreo de blockchain", + "analyticsDashboard": "Panel de Análisis", + "forms": "Formularios", + "sandbox": "Caja de arena", + "profile": "Perfil", + "footer": "Desarrollado con ChenaiKit - Soluciones avanzadas de IA y Blockchain" }, "navigation": { "dashboard": "Panel", @@ -33,6 +48,32 @@ "help": "Ayuda", "logout": "Cerrar sesión" }, + "auth": { + "welcomeBack": "Bienvenido de nuevo", + "loginSubtitle": "Ingrese sus datos para acceder a su panel", + "rememberMe": "Recordarme", + "forgotPassword": "¿Olvidó su contraseña?", + "signIn": "Iniciar sesión", + "orContinueWith": "O CONTINUAR CON", + "dontHaveAccount": "¿No tiene una cuenta?", + "signUp": "Registrarse", + "loginError": "Error al iniciar sesión. Por favor, inténtelo de nuevo.", + "createAccount": "Crear una cuenta", + "signupSubtitle": "Regístrese para monitorear análisis de blockchain y métricas de crédito", + "iAgreeTo": "Estoy de acuerdo con los", + "and": "y", + "alreadyHaveAccount": "¿Ya tiene una cuenta? ", + "weak": "Débil", + "medium": "Medio", + "strong": "Fuerte", + "passwordStrength": "Fortaleza de la contraseña", + "confirmPassword": "Confirmar contraseña", + "acceptTermsError": "Debe aceptar los términos de servicio para registrarse.", + "registerError": "Error de registro. Por favor, inténtelo de nuevo.", + "accountCreated": "¡Cuenta creada!", + "accountCreatedDesc": "Su cuenta fue creada para {{email}}.", + "goToSignIn": "Ir a Iniciar sesión" + }, "dashboard": { "title": "Panel", "subtitle": "Resumen de monitoreo en tiempo real", @@ -127,7 +168,18 @@ "customRange": "Rango Personalizado", "exportReport": "Exportar Informe", "downloadPdf": "Descargar PDF", - "downloadCsv": "Descargar CSV" + "downloadCsv": "Descargar CSV", + "totalRequests": "Total de solicitudes", + "avgLatency": "Latencia promedio", + "avgCreditScore": "Puntaje crediticio promedio", + "blockchainVol": "Volumen de blockchain", + "systemTrafficForecast": "Tráfico del sistema y pronóstico", + "blockchainAssets": "Activos de blockchain", + "successRate": "Tasa de éxito", + "errorRate": "Tasa de error", + "fraudAlerts": "Alertas de fraude", + "resolvedAlerts": "Alertas resueltas", + "fetchError": "Error al obtener datos analíticos. Asegúrese de que el backend se esté ejecutando." }, "settings": { "title": "Configuración", @@ -158,7 +210,43 @@ "license": "Licencia", "support": "Soporte", "documentation": "Documentación", - "contact": "Contacto" + "contact": "Contacto", + "profileUpdated": "¡Perfil actualizado con éxito!", + "light": "Claro", + "dark": "Oscuro", + "system": "Sistema", + "saving": "Guardando...", + "dangerZone": "Zona de peligro", + "deleteAccountWarning": "Una vez que elimine su cuenta, no hay marcha atrás. Por favor, esté seguro.", + "deleteAccount": "Eliminar cuenta", + "confirmDeleteEmail": "Esta acción no se puede deshacer. Para confirmar, escriba su dirección de correo electrónico: {{email}}", + "confirmEmail": "Confirmar correo electrónico", + "deleting": "Eliminando...", + "deleteForever": "Eliminar para siempre" + }, + "profile": { + "title": "Perfil", + "subtitle": "El perfil de su cuenta", + "activity": "Actividad", + "account": "Cuenta", + "statistics": "Estadísticas", + "editProfile": "Editar perfil", + "displayName": "Nombre visible", + "email": "Correo electrónico", + "role": "Rol", + "memberSince": "Miembro desde", + "transactions": "Transacciones", + "creditScore": "Puntaje crediticio", + "activeDays": "Días activos", + "uploadAvatar": "Cargar avatar", + "changeAvatar": "Cambiar avatar", + "saveChanges": "Guardar cambios", + "recentActivity": "Actividad reciente", + "noActivity": "Sin actividad reciente", + "securityStatus": "Estado de seguridad", + "twoFactorEnabled": "Autenticación de dos factores", + "passwordSet": "Contraseña", + "activeSessions": "Sesiones activas" }, "websocket": { "connected": "Conectado", @@ -206,7 +294,11 @@ "improveScore": "Mejorar Su Puntaje", "scoreBreakdown": "Desglose del Puntaje", "noDataAvailable": "No hay datos crediticios disponibles", - "lastUpdated": "Última Actualización" + "lastUpdated": "Última Actualización", + "outOf100": "de 100", + "scoreRange": "Rango de puntaje", + "pointsDifference_one": "{{count}} punto", + "pointsDifference_other": "{{count}} puntos" }, "errors": { "networkError": "Ocurrió un error de red", @@ -242,7 +334,21 @@ "uploadFile": "Subir archivo", "dragDrop": "Arrastre y suelte archivos aquí", "fileTooBig": "Archivo demasiado grande (máx {{size}})", - "invalidFileType": "Tipo de archivo inválido" + "invalidFileType": "Tipo de archivo inválido", + "stellarAddressNotExist": "Esta dirección de Stellar no existe", + "stellarAddressVerifyError": "No se puede verificar la dirección en este momento", + "stellarAddressLabel": "Dirección Stellar", + "stellarAddressPlaceholder": "Ingrese su dirección Stellar (G...)", + "invalidStellarAddress": "Dirección Stellar no válida", + "amountPlaceholder": "Ingrese el monto", + "amountPositive": "El monto debe ser mayor que 0", + "scheduledDateLabel": "Fecha programada", + "scheduledDateRequired": "Elija una fecha programada", + "scheduledTimeLabel": "Hora programada", + "scheduledTimeRequired": "Elija una hora programada", + "memoLabel": "Memo (Opcional)", + "memoPlaceholder": "Ingrese un memo para esta transacción", + "submittedSuccessfully": "¡Formulario enviado con éxito!" }, "time": { "now": "Ahora", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 2021f0e..4625adc 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -22,7 +22,22 @@ "refresh": "刷新", "settings": "设置", "help": "帮助", - "about": "关于" + "about": "关于", + "forgotPasswordTitle": "重置密码", + "forgotPasswordDesc": "输入您的电子邮件地址,我们将向您发送密码重置链接。", + "forgotPasswordSoon": "重置密码功能即将推出。", + "termsTitle": "服务条款", + "termsDesc": "这些服务条款适用于您使用 ChenaiKit。访问或使用我们的平台,即表示您同意受这些条款 of 约束。完整的条款文档将在正式上线前在此发布。", + "privacyTitle": "隐私政策", + "privacyDesc": "您的隐私对我们非常重要。ChenaiKit 仅收集提供服务所必需的数据,绝不向第三方出售个人信息。完整的隐私政策文档将在正式上线前在此发布。", + "signOut": "登出", + "dashboardTitle": "ChenaiKit - 商业智能与分析仪表板", + "dashboardSubtitle": "先进的 AI 见解和区块链监控", + "analyticsDashboard": "分析仪表板", + "forms": "表单示例", + "sandbox": "沙盒测试", + "profile": "个人中心", + "footer": "基于 ChenaiKit 构建 - 先进的 AI 和区块链解决方案" }, "navigation": { "dashboard": "仪表板", @@ -33,6 +48,32 @@ "help": "帮助", "logout": "登出" }, + "auth": { + "welcomeBack": "欢迎回来", + "loginSubtitle": "输入您的详细信息以访问仪表板", + "rememberMe": "记住我", + "forgotPassword": "忘记密码?", + "signIn": "登录", + "orContinueWith": "或通过以下方式继续", + "dontHaveAccount": "没有账户?", + "signUp": "注册", + "loginError": "登录失败,请重试。", + "createAccount": "创建账户", + "signupSubtitle": "注册以监控区块链分析和信用指标", + "iAgreeTo": "我同意", + "and": "和", + "alreadyHaveAccount": "已有账户? ", + "weak": "弱", + "medium": "中", + "strong": "强", + "passwordStrength": "密码强度", + "confirmPassword": "确认密码", + "acceptTermsError": "您必须接受服务条款才能注册。", + "registerError": "注册失败,请重试。", + "accountCreated": "账户已创建!", + "accountCreatedDesc": "您的账户已成功为 {{email}} 创建。", + "goToSignIn": "前往登录" + }, "dashboard": { "title": "仪表板", "subtitle": "实时监控概览", @@ -127,7 +168,18 @@ "customRange": "自定义范围", "exportReport": "导出报告", "downloadPdf": "下载PDF", - "downloadCsv": "下载CSV" + "downloadCsv": "下载CSV", + "totalRequests": "总请求数", + "avgLatency": "平均延迟", + "avgCreditScore": "平均信用评分", + "blockchainVol": "区块链交易量", + "systemTrafficForecast": "系统流量与预测", + "blockchainAssets": "区块链资产", + "successRate": "成功率", + "errorRate": "错误率", + "fraudAlerts": "欺诈警报", + "resolvedAlerts": "已解决警报", + "fetchError": "获取分析数据失败。请确保后端正在运行。" }, "settings": { "title": "设置", @@ -158,7 +210,43 @@ "license": "许可证", "support": "支持", "documentation": "文档", - "contact": "联系" + "contact": "联系", + "profileUpdated": "个人资料更新成功!", + "light": "浅色模式", + "dark": "深色模式", + "system": "系统默认", + "saving": "保存中...", + "dangerZone": "危险区域", + "deleteAccountWarning": "删除账户后将无法恢复,请谨慎操作。", + "deleteAccount": "删除账户", + "confirmDeleteEmail": "此操作无法撤销。如需确认,请输入您的电子邮件地址:{{email}}", + "confirmEmail": "确认电子邮件", + "deleting": "删除中...", + "deleteForever": "永久删除" + }, + "profile": { + "title": "个人资料", + "subtitle": "您的账户资料", + "activity": "活动", + "account": "账户", + "statistics": "统计数据", + "editProfile": "编辑个人资料", + "displayName": "显示名称", + "email": "电子邮件", + "role": "角色", + "memberSince": "注册成员自", + "transactions": "交易", + "creditScore": "信用评分", + "activeDays": "活跃天数", + "uploadAvatar": "上传头像", + "changeAvatar": "修改头像", + "saveChanges": "保存修改", + "recentActivity": "最近活动", + "noActivity": "无最近活动", + "securityStatus": "安全状态", + "twoFactorEnabled": "双因素认证", + "passwordSet": "密码", + "activeSessions": "活动会话" }, "websocket": { "connected": "已连接", @@ -206,7 +294,11 @@ "improveScore": "改善您的评分", "scoreBreakdown": "评分细分", "noDataAvailable": "无信用数据可用", - "lastUpdated": "最后更新" + "lastUpdated": "最后更新", + "outOf100": "总分 100", + "scoreRange": "评分范围", + "pointsDifference_one": "{{count}} 分", + "pointsDifference_other": "{{count}} 分" }, "errors": { "networkError": "发生网络错误", @@ -242,7 +334,21 @@ "uploadFile": "上传文件", "dragDrop": "拖放文件到这里", "fileTooBig": "文件太大(最大{{size}})", - "invalidFileType": "无效的文件类型" + "invalidFileType": "无效的文件类型", + "stellarAddressNotExist": "此 Stellar 地址不存在", + "stellarAddressVerifyError": "目前无法验证地址", + "stellarAddressLabel": "Stellar 地址", + "stellarAddressPlaceholder": "输入您的 Stellar 地址 (G...)", + "invalidStellarAddress": "无效的 Stellar 地址", + "amountPlaceholder": "输入金额", + "amountPositive": "金额必须大于 0", + "scheduledDateLabel": "预定日期", + "scheduledDateRequired": "请选择预定日期", + "scheduledTimeLabel": "预定时间", + "scheduledTimeRequired": "请选择预定时间", + "memoLabel": "备注 (选填)", + "memoPlaceholder": "输入此交易的备注", + "submittedSuccessfully": "表单提交成功!" }, "time": { "now": "现在", From 952650b9ae3ee08e58bddc7559eb8018596461bd Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Jul 2026 11:50:09 +0100 Subject: [PATCH 04/12] style(i18n): clean up unused variables and add compact switcher variant support --- frontend/src/components/LanguageSwitcher.tsx | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index 1f9858e..66edd51 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -15,7 +15,7 @@ import { supportedLanguages, changeLanguage, getCurrentLanguage } from '../i18n/ import { SxProps, Theme } from '@mui/material'; interface LanguageSwitcherProps { - variant?: 'select' | 'chip' | 'avatar'; + variant?: 'select' | 'chip' | 'avatar' | 'compact'; size?: 'small' | 'medium' | 'large'; showFlag?: boolean; showNativeName?: boolean; @@ -49,12 +49,13 @@ export const LanguageSwitcher: React.FC = ({ value={currentLang} onChange={handleLanguageChange} displayEmpty + inputProps={{ 'aria-label': 'Select language' }} sx={{ - minWidth: compact ? 120 : 200, '& .MuiSelect-select': { display: 'flex', alignItems: 'center', - gap: 1 + gap: 1, + py: size === 'small' ? 0.5 : 1 } }} > @@ -62,20 +63,20 @@ export const LanguageSwitcher: React.FC = ({ {showFlag && ( - + {language.flag} )} - - + {showNativeName && ( + {language.nativeName} - {!compact && ( - - {language.name} - - )} - + )} + {(!compact && showNativeName) && ( + + ({language.name}) + + )} ))} @@ -84,8 +85,6 @@ export const LanguageSwitcher: React.FC = ({ ); const renderChipVariant = () => { - const currentLangInfo = getCurrentLanguageInfo(); - return ( {supportedLanguages.map((language) => ( @@ -191,6 +190,8 @@ export const LanguageSwitcher: React.FC = ({ return renderChipVariant(); case 'avatar': return renderAvatarVariant(); + case 'compact': + return renderCompactVariant(); default: return renderSelectVariant(); } @@ -249,7 +250,6 @@ export const MobileLanguageSelector: React.FC = () => { // Language info display component export const LanguageInfo: React.FC = () => { - const { t, i18n } = useTranslation(); const currentLang = getCurrentLanguage(); const currentLangInfo = supportedLanguages.find(lang => lang.code === currentLang) || supportedLanguages[0]; From 0f4371ef6b69aa02c3780b638c314a80b8fdde1e Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Jul 2026 14:26:06 +0100 Subject: [PATCH 05/12] fix(lint): fix JSX parse errors in App.tsx and LanguageSwitcher.tsx --- frontend/src/App.tsx | 2 +- frontend/src/components/LanguageSwitcher.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b2345fe..af20f1e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -248,7 +248,7 @@ const DashboardShell: React.FC = () => { ⚙️ {t('app.settings')} - +
{activeDemo === 'analytics' && } diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index 28a7d1d..b04fcb1 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -120,6 +120,7 @@ export const LanguageSwitcher: React.FC = ({ ))} ); + }; const renderAvatarVariant = () => { const currentLangInfo = getCurrentLanguageInfo(); From 3f2a43bba6f29d41ce5e861720829d03f3c89147 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Jul 2026 14:39:17 +0100 Subject: [PATCH 06/12] fix(i18n): remove orphaned labelId, label, InputLabel and useId from LanguageSwitcher --- frontend/src/components/LanguageSwitcher.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index b04fcb1..f899570 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -1,4 +1,4 @@ -import React, { useId } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { Box, @@ -9,7 +9,6 @@ import { Tooltip, IconButton, Chip, - InputLabel, } from '@mui/material'; import { supportedLanguages, changeLanguage, getCurrentLanguage } from '../i18n/config'; @@ -34,7 +33,7 @@ export const LanguageSwitcher: React.FC = ({ }) => { const { i18n } = useTranslation(); const currentLang = getCurrentLanguage(); - const labelId = useId(); + const handleLanguageChange = (event: any) => { const newLanguage = event.target.value; @@ -48,8 +47,6 @@ export const LanguageSwitcher: React.FC = ({ const renderSelectVariant = () => (