From 304209518d50fcf441be8e8d1a828e0b6961af37 Mon Sep 17 00:00:00 2001 From: OG-wura Date: Wed, 17 Jun 2026 12:02:24 +0100 Subject: [PATCH] feat(i18n): add i18next with French and Portuguese translations --- app/(auth)/register.tsx | 38 +-- app/(auth)/role-select.tsx | 18 +- app/(auth)/sign-in.tsx | 79 +++--- app/(tabs)/_layout.tsx | 13 +- app/(tabs)/calendar.tsx | 75 +++--- app/(tabs)/index.tsx | 49 ++-- app/(tabs)/loans.tsx | 60 ++--- app/(tabs)/reputation.tsx | 95 ++++---- app/(tabs)/settings.tsx | 172 +++++++++---- app/_layout.tsx | 7 +- .../reputation/ReputationProgressWidget.tsx | 22 +- hooks/useTranslation.ts | 21 ++ package-lock.json | 96 +++++++- package.json | 3 + src/locales/en.json | 226 ++++++++++++++++++ src/locales/fr.json | 226 ++++++++++++++++++ src/locales/i18n.ts | 124 ++++++++++ src/locales/pt.json | 225 +++++++++++++++++ 18 files changed, 1279 insertions(+), 270 deletions(-) create mode 100644 hooks/useTranslation.ts create mode 100644 src/locales/en.json create mode 100644 src/locales/fr.json create mode 100644 src/locales/i18n.ts create mode 100644 src/locales/pt.json diff --git a/app/(auth)/register.tsx b/app/(auth)/register.tsx index 4eaa835..5251bde 100644 --- a/app/(auth)/register.tsx +++ b/app/(auth)/register.tsx @@ -17,9 +17,11 @@ import { Input } from '../../components/shared/Input'; import { useUserStore } from '../../stores/user.store'; import { useAuthStore } from '../../stores/auth.store'; import { useWalletStore } from '../../stores/wallet.store'; +import { useTranslation } from '../../hooks/useTranslation'; import type { LearnerProfile } from '../../types/user.types'; export default function RegisterScreen() { + const { t } = useTranslation(); const router = useRouter(); const role = useUserStore((s) => s.role); const setProfile = useUserStore((s) => s.setProfile); @@ -112,10 +114,10 @@ export default function RegisterScreen() { {/* Headers */} - Tell us about yourself + {t('auth.register.title')} - This helps us match you with the right {isLearner ? 'sponsors' : 'learners'}. + {t('auth.register.subtitleMatch', { role: isLearner ? t('auth.register.sponsors') : t('auth.register.learners') })} @@ -131,15 +133,15 @@ export default function RegisterScreen() { - Change Photo + {t('auth.register.changePhoto')} {/* Common field */} @@ -170,14 +172,14 @@ export default function RegisterScreen() { ) : ( <> @@ -187,7 +189,7 @@ export default function RegisterScreen() { {/* Wallet address display */} - Wallet address + {t('auth.register.walletAddress')} {publicKey ? `${publicKey.slice(0, 8)}...${publicKey.slice(-8)}` - : 'Not connected'} + : t('auth.register.notConnected')} @@ -230,7 +232,7 @@ export default function RegisterScreen() { disabled={!isValid || isSubmitting} > - {isSubmitting ? 'Saving...' : 'Continue'} + {isSubmitting ? t('auth.register.saving') : t('auth.register.continue')} diff --git a/app/(auth)/role-select.tsx b/app/(auth)/role-select.tsx index 07ac030..a1add8e 100644 --- a/app/(auth)/role-select.tsx +++ b/app/(auth)/role-select.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'expo-router'; import { GraduationCap, TrendingUp, ChevronRight, ChevronLeft } from 'lucide-react-native'; import { colors } from '../../constants/colors'; import { useUserStore } from '../../stores/user.store'; +import { useTranslation } from '../../hooks/useTranslation'; import type { UserRole } from '../../types/user.types'; interface RolePillProps { @@ -99,6 +100,7 @@ function RoleCard({ } export default function RoleSelectScreen() { + const { t } = useTranslation(); const router = useRouter(); const setRole = useUserStore((s) => s.setRole); @@ -128,13 +130,13 @@ export default function RoleSelectScreen() { className="text-[32px] font-bold" style={{ color: colors.textPrimary }} > - How will you use StepFi? + {t('auth.roleSelect.title')} - Choose your role. You can switch anytime in settings. + {t('auth.roleSelect.subtitle')} @@ -142,9 +144,9 @@ export default function RoleSelectScreen() { - Score + {t('auth.signIn.score')} {scoreRange} - Rate + {t('auth.signIn.rate')} {rate} - Max + {t('auth.signIn.max')} (null); const [activeIndex, setActiveIndex] = useState(0); const [isConnecting, setIsConnecting] = useState(false); const setConnected = useWalletStore((s) => s.setConnected); + const tierNames = t('tierNames', { returnObjects: true }) as string[]; const handleScroll = (event: NativeSyntheticEvent) => { const offsetX = event.nativeEvent.contentOffset.x; @@ -203,13 +206,13 @@ export default function SignInScreen() { {/* ─── SLIDE 1: Welcome ─── */} - StepFi + {t('auth.signIn.stepfi')} - Step into your future. + {t('auth.signIn.stepIntoFuture')} - Credit without banks. Progress without limits. + {t('auth.signIn.creditWithoutBanks')} @@ -225,13 +228,13 @@ export default function SignInScreen() { - Next + {t('common.next')} @@ -242,7 +245,7 @@ export default function SignInScreen() { onPress={handleConnectWallet} > - Already have an account? Sign in + {t('auth.signIn.alreadyHaveAccount')} @@ -251,12 +254,12 @@ export default function SignInScreen() { {/* ─── SLIDE 2: Features ─── */} - - Finance what you need. - + + {t('auth.signIn.financeWhatYouNeed')} + @@ -309,7 +312,7 @@ export default function SignInScreen() { onPress={goToNextSlide} > - Next + {t('common.next')} @@ -319,24 +322,24 @@ export default function SignInScreen() { {/* ─── SLIDE 3: Reputation Tiers ─── */} - - Your score.{'\n'}Your terms. - + + {t('auth.signIn.yourScoreYourTerms')} + - Pay on time. Score goes up. Rates go down. + {t('auth.signIn.payOnTime')} @@ -379,7 +382,7 @@ export default function SignInScreen() { onPress={goToNextSlide} > - Next + {t('common.next')} @@ -405,7 +408,7 @@ export default function SignInScreen() { className="text-[32px] font-bold text-center mt-6 mb-2" style={{ color: colors.textPrimary }} > - Connect your wallet. + {t('auth.signIn.connectYourWallet')} @@ -413,14 +416,14 @@ export default function SignInScreen() { className="text-[16px] text-center" style={{ color: colors.textSecondary }} > - No passwords. No email. Just your Stellar wallet. + {t('auth.signIn.noPasswords')} - Lobstr + {t('auth.signIn.walletLobstr')} - xBull + {t('auth.signIn.walletXbull')} @@ -438,7 +441,7 @@ export default function SignInScreen() { > - {isConnecting ? 'Connecting...' : 'Connect Stellar Wallet'} + {isConnecting ? t('auth.signIn.connecting') : t('auth.signIn.connectStellarWallet')} @@ -449,7 +452,7 @@ export default function SignInScreen() { > - Don't have a wallet? + {t('auth.signIn.dontHaveWallet')} diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 7ed7dd5..52f49cd 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,8 +1,11 @@ import { Tabs } from 'expo-router'; import { Home, CreditCard, Calendar, Star, Settings } from 'lucide-react-native'; import { colors } from '../../constants/colors'; +import { useTranslation } from '../../hooks/useTranslation'; export default function TabsLayout() { + const { t } = useTranslation(); + return ( , }} /> , }} /> , }} /> , }} /> , }} /> diff --git a/app/(tabs)/calendar.tsx b/app/(tabs)/calendar.tsx index 1ed2c1d..48085f0 100644 --- a/app/(tabs)/calendar.tsx +++ b/app/(tabs)/calendar.tsx @@ -13,14 +13,12 @@ import { } from 'lucide-react-native'; import { colors } from '../../constants/colors'; import { EmptyState } from '../../components/shared/EmptyState'; -import { Loader } from '../../components/shared/Loader'; -import { useInstallmentCalendar, WEEKDAY_HEADERS, MONTH_NAMES } from '../../hooks/useInstallmentCalendar'; +import Loader from '../../components/shared/Loader'; +import { useInstallmentCalendar } from '../../hooks/useInstallmentCalendar'; +import { useTranslation } from '../../hooks/useTranslation'; +import { formatCurrency, formatDate } from '../../src/locales/i18n'; import type { CalendarDayData, CalendarEventData } from '../../hooks/useInstallmentCalendar'; -function formatCurrency(amount: number) { - return `$${amount.toLocaleString()}`; -} - function EventIndicator({ events }: { events: CalendarEventData[] }) { if (!events.length) return null; @@ -89,16 +87,16 @@ function DayCell({ ); } -function SelectedDayPanel({ day }: { day: CalendarDayData }) { +function SelectedDayPanel({ day, t }: { day: CalendarDayData; t: (key: string, opts?: any) => string }) { if (!day.events.length) { return ( - - No payments on this day - + + {t('calendar.noPaymentsOnDay')} + ); } @@ -109,16 +107,12 @@ function SelectedDayPanel({ day }: { day: CalendarDayData }) { style={{ backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border }} > - - {day.date.toLocaleDateString('en-US', { - weekday: 'long', - month: 'long', - day: 'numeric', - })} - - - {day.events.length} payment{day.events.length > 1 ? 's' : ''} - + + {formatDate(day.date, 'long')} + + + {day.events.length} {day.events.length > 1 ? t('calendar.payments') : t('calendar.payment')} + {day.events.map((event, idx) => ( - {event.isPaid ? 'Paid' : 'Due'} + {event.isPaid ? t('calendar.eventPaid') : t('calendar.eventDue')} ))} @@ -164,6 +158,9 @@ function SelectedDayPanel({ day }: { day: CalendarDayData }) { } export default function CalendarScreen() { + const { t } = useTranslation(); + const weekdayHeaders = t('weekdayHeaders', { returnObjects: true }) as string[]; + const monthNames = t('monthNames', { returnObjects: true }) as string[]; const { currentMonth, weeks, @@ -192,16 +189,16 @@ export default function CalendarScreen() { const handleExport = React.useCallback(async () => { const success = await exportToCalendar(); if (success) { - Alert.alert('Calendar Exported', 'Payment dates have been added to your calendar.'); + Alert.alert(t('calendar.calendarExported'), t('calendar.calendarExportedMessage')); } else { - Alert.alert('Permission Required', 'Calendar access was not granted. Please enable it in settings.'); + Alert.alert(t('calendar.permissionRequired'), t('calendar.permissionRequiredMessage')); } - }, [exportToCalendar]); + }, [exportToCalendar, t]); const handleScheduleReminders = React.useCallback(async () => { await scheduleReminders(); - Alert.alert('Reminders Set', 'Payment reminders have been scheduled 14, 7, 3, and 1 day before each due date.'); - }, [scheduleReminders]); + Alert.alert(t('calendar.remindersSet'), t('calendar.remindersSetMessage')); + }, [scheduleReminders, t]); if (isLoading) { return ( @@ -216,11 +213,11 @@ export default function CalendarScreen() { { void refetch(); } }} + action={{ label: t('common.tryAgain'), onPress: () => { void refetch(); } }} /> ); @@ -231,14 +228,14 @@ export default function CalendarScreen() { ); } - const monthYear = `${MONTH_NAMES[currentMonth.getMonth()]} ${currentMonth.getFullYear()}`; + const monthYear = `${monthNames[currentMonth.getMonth()]} ${currentMonth.getFullYear()}`; const totalDue = allEvents.filter((e) => !e.isPaid).length; const totalPaid = allEvents.filter((e) => e.isPaid).length; @@ -254,7 +251,7 @@ export default function CalendarScreen() { {/* Header */} - Calendar + {t('calendar.calendar')} - Payment Streak + {t('calendar.paymentStreak')} - Consecutive on-time payments + {t('calendar.paymentStreakDesc')} @@ -311,7 +308,7 @@ export default function CalendarScreen() { style={{ backgroundColor: colors.warningDim }} > - Due + {t('calendar.due')} {totalDue} @@ -322,7 +319,7 @@ export default function CalendarScreen() { style={{ backgroundColor: colors.successDim }} > - Paid + {t('calendar.paid')} {totalPaid} @@ -362,7 +359,7 @@ export default function CalendarScreen() { {/* Weekday Headers */} - {WEEKDAY_HEADERS.map((day) => ( + {weekdayHeaders.map((day) => ( {day} @@ -393,7 +390,7 @@ export default function CalendarScreen() { {/* Selected Day Details */} {selectedDay ? ( - + ) : null} diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index a03596a..ad1e942 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -20,9 +20,12 @@ import { useAuthStore } from '../../stores/auth.store'; import { loansService } from '../../services/loans.service'; import { reputationService } from '../../services/reputation.service'; import { ReputationProgressWidget } from '../../components/reputation/ReputationProgressWidget'; +import { useTranslation } from '../../hooks/useTranslation'; +import { formatCurrency, formatDate } from '../../src/locales/i18n'; import type { AvailableCredit } from '../../services/loans.service'; export default function HomeScreen() { + const { t } = useTranslation(); const router = useRouter(); const profile = useUserStore((s) => s.profile); const reputation = useUserStore((s) => s.reputation); @@ -36,7 +39,7 @@ export default function HomeScreen() { const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); - const displayName = profile?.displayName ?? 'there'; + const displayName = profile?.displayName ?? t('home.greeting'); const fetchDashboard = useCallback(async () => { setError(null); @@ -51,7 +54,7 @@ export default function HomeScreen() { if (creditData.status === 'fulfilled') setCredit(creditData.value); if (repData.status === 'fulfilled' && repData.value) setReputation(repData.value); } catch { - setError('Could not load your dashboard. Please try again.'); + setError(t('home.errorLoading')); } finally { setIsLoading(false); setIsRefreshing(false); @@ -82,11 +85,11 @@ export default function HomeScreen() { { setIsLoading(true); void fetchDashboard(); } }} + action={{ label: t('common.tryAgain'), onPress: () => { setIsLoading(true); void fetchDashboard(); } }} /> ); @@ -121,7 +124,7 @@ export default function HomeScreen() { - Available Credit + {t('home.availableCredit')} @@ -137,7 +140,7 @@ export default function HomeScreen() { style={{ backgroundColor: colors.brandGreen + '15' }} > - Active Limit + {t('home.activeLimit')} @@ -146,10 +149,10 @@ export default function HomeScreen() { - Used: ${credit?.used?.toLocaleString() ?? '0'} + {t('home.used')}: ${credit?.used?.toLocaleString() ?? '0'} - Limit: ${credit?.limit?.toLocaleString() ?? '0'} + {t('home.limit')}: ${credit?.limit?.toLocaleString() ?? '0'} @@ -166,10 +169,10 @@ export default function HomeScreen() { {/* Quick Actions Grid */} {[ - { icon: Plus, label: 'Apply', color: colors.brandGreen, route: '/(tabs)/pay' }, - { icon: ArrowUpRight, label: 'Pay', color: colors.textPrimary, route: '/(tabs)/pay' }, - { icon: History, label: 'History', color: colors.textPrimary, route: '/(tabs)/pay' }, - { icon: BadgeCheck, label: 'Vouches', color: colors.textPrimary, route: '/(tabs)/reputation' }, + { icon: Plus, label: t('home.apply'), color: colors.brandGreen, route: '/(tabs)/pay' }, + { icon: ArrowUpRight, label: t('home.pay'), color: colors.textPrimary, route: '/(tabs)/pay' }, + { icon: History, label: t('home.history'), color: colors.textPrimary, route: '/(tabs)/pay' }, + { icon: BadgeCheck, label: t('home.vouches'), color: colors.textPrimary, route: '/(tabs)/reputation' }, ].map((action, idx) => ( - Active Loans + {t('home.activeLoans')} - Loan #{loan.id.slice(0, 4)} + {t('common.loanNumber', { id: loan.id.slice(0, 4) })} - Active + {t('home.active')} - Remaining Balance + {t('home.remainingBalance')} @@ -259,7 +262,7 @@ export default function HomeScreen() { className="w-[280px] rounded-xl border p-5 justify-center items-center" style={{ backgroundColor: colors.surface, borderColor: colors.borderSubtle }} > - No active loans + {t('home.noActiveLoans')} )} @@ -268,7 +271,7 @@ export default function HomeScreen() { {/* Upcoming Payments List */} - Upcoming Payments + {t('home.upcomingPayments')} - Installment + {t('home.installment')} - Loan #{payment.loanId.slice(0, 4)} · Due {new Date(payment.dueDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} + {t('common.loanNumber', { id: payment.loanId.slice(0, 4) })} · {t('home.due')} {formatDate(new Date(payment.dueDate), 'short')} @@ -306,7 +309,7 @@ export default function HomeScreen() { {idx === 0 ? ( - Pay Now + {t('home.payNow')} ) : null} @@ -314,7 +317,7 @@ export default function HomeScreen() { )) ) : ( - No upcoming payments + {t('home.noUpcomingPayments')} )} diff --git a/app/(tabs)/loans.tsx b/app/(tabs)/loans.tsx index f0f247c..9fbc682 100644 --- a/app/(tabs)/loans.tsx +++ b/app/(tabs)/loans.tsx @@ -14,9 +14,11 @@ import { Card } from '../../components/shared/Card'; import { EmptyState } from '../../components/shared/EmptyState'; import { useLoansStore } from '../../stores/loans.store'; import { loansService } from '../../services/loans.service'; +import { useTranslation } from '../../hooks/useTranslation'; +import { formatDate } from '../../src/locales/i18n'; import type { Loan, LoanStatus } from '../../types/loan.types'; -function getStatusConfig(status: LoanStatus): { +function getStatusConfig(status: LoanStatus, t: (key: string, opts?: any) => string): { label: string; color: string; bg: string; @@ -24,15 +26,15 @@ function getStatusConfig(status: LoanStatus): { } { switch (status) { case 'active': - return { label: 'Active', color: colors.brandBlue, bg: colors.brandBlueDim, icon: Clock }; + return { label: t('loans.statusActive'), color: colors.brandBlue, bg: colors.brandBlueDim, icon: Clock }; case 'paid': - return { label: 'Paid', color: colors.success, bg: colors.successDim, icon: CheckCircle }; + return { label: t('loans.statusPaid'), color: colors.success, bg: colors.successDim, icon: CheckCircle }; case 'defaulted': - return { label: 'Defaulted', color: colors.error, bg: colors.errorDim, icon: XCircle }; + return { label: t('loans.statusDefaulted'), color: colors.error, bg: colors.errorDim, icon: XCircle }; case 'pending': - return { label: 'Pending', color: colors.warning, bg: colors.warningDim, icon: Clock }; + return { label: t('loans.statusPending'), color: colors.warning, bg: colors.warningDim, icon: Clock }; case 'cancelled': - return { label: 'Cancelled', color: colors.textMuted, bg: colors.subtle, icon: XCircle }; + return { label: t('loans.statusCancelled'), color: colors.textMuted, bg: colors.subtle, icon: XCircle }; default: return { label: status, color: colors.textMuted, bg: colors.subtle, icon: Clock }; } @@ -40,10 +42,11 @@ function getStatusConfig(status: LoanStatus): { interface LoanCardProps { loan: Loan; + t: (key: string, opts?: any) => string; } -function LoanCard({ loan }: LoanCardProps) { - const statusConfig = getStatusConfig(loan.status); +function LoanCard({ loan, t }: LoanCardProps) { + const statusConfig = getStatusConfig(loan.status, t); const StatusIcon = statusConfig.icon; const paidCount = loan.installments.filter((i) => i.paid).length; @@ -66,14 +69,10 @@ function LoanCard({ loan }: LoanCardProps) { className="text-sm font-semibold" style={{ color: colors.textPrimary }} > - Loan #{loan.id.slice(0, 8)} + {t('common.loanNumber', { id: loan.id.slice(0, 8) })} - {new Date(loan.createdAt).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} + {formatDate(new Date(loan.createdAt), 'full')} @@ -97,7 +96,7 @@ function LoanCard({ loan }: LoanCardProps) { - Total amount + {t('loans.totalAmount')} - Remaining + {t('loans.remaining')} - Installments + {t('loans.installments')} - {paidCount} of {totalCount} paid + {t('loans.installmentsPaid', { paid: paidCount, total: totalCount })} s.loans); const setLoans = useLoansStore((s) => s.setLoans); const [isLoading, setIsLoading] = useState(true); @@ -159,7 +159,7 @@ export default function LoansScreen() { const data = await loansService.getMyLoans(); setLoans(data); } catch { - setError('Could not load your loans. Please try again.'); + setError(t('loans.errorLoading')); } finally { setIsLoading(false); setIsRefreshing(false); @@ -181,11 +181,11 @@ export default function LoansScreen() { { setIsLoading(true); void fetchLoans(); } }} + action={{ label: t('common.tryAgain'), onPress: () => { setIsLoading(true); void fetchLoans(); } }} /> ); @@ -197,9 +197,9 @@ export default function LoansScreen() { {} }} + title={t('loans.noLoansYet')} + message={t('loans.noLoansMessage')} + action={{ label: t('loans.applyNow'), onPress: () => {} }} /> ); @@ -222,7 +222,7 @@ export default function LoansScreen() { className="text-2xl font-bold mt-2 mb-6" style={{ color: colors.textPrimary }} > - My Loans + {t('loans.myLoans')} {/* Summary */} @@ -232,7 +232,7 @@ export default function LoansScreen() { style={{ backgroundColor: colors.brandBlueDim }} > - Active + {t('loans.active')} {loans.filter((l) => l.status === 'active').length} @@ -243,7 +243,7 @@ export default function LoansScreen() { style={{ backgroundColor: colors.successDim }} > - Paid + {t('loans.paid')} {loans.filter((l) => l.status === 'paid').length} @@ -254,7 +254,7 @@ export default function LoansScreen() { style={{ backgroundColor: colors.warningDim }} > - Pending + {t('loans.pending')} {loans.filter((l) => l.status === 'pending').length} @@ -264,7 +264,7 @@ export default function LoansScreen() { {/* Loan list */} {loans.map((loan) => ( - + ))} diff --git a/app/(tabs)/reputation.tsx b/app/(tabs)/reputation.tsx index e3235bc..35fcd6e 100644 --- a/app/(tabs)/reputation.tsx +++ b/app/(tabs)/reputation.tsx @@ -26,6 +26,7 @@ import { useAuthStore } from '../../stores/auth.store'; import { reputationService } from '../../services/reputation.service'; import { useReputationMilestones } from '../../hooks/useReputationMilestones'; import { useVouch } from '../../hooks/useVouch'; +import { useTranslation } from '../../hooks/useTranslation'; import { TransactionStatus } from '../../types/transaction.types'; import { TierUpModal } from '../../components/reputation/TierUpModal'; import { MilestoneModal } from '../../components/reputation/MilestoneModal'; @@ -34,43 +35,21 @@ interface TipItem { icon: typeof CheckCircle; text: string; color: string; + key: string; } -const IMPROVEMENT_TIPS: TipItem[] = [ - { - icon: CheckCircle, - text: 'Pay installments on time to increase your score', - color: colors.success, - }, - { - icon: Clock, - text: 'Avoid late payments — each one reduces your score', - color: colors.warning, - }, - { - icon: Shield, - text: 'Get vouched by a mentor to boost your credit limit', - color: colors.brandBlue, - }, - { - icon: TrendingUp, - text: 'Complete more loans to build a strong payment history', - color: colors.brandGreen, - }, -]; - -function getTierDescription(tier: string): string { +function getTierDescription(tier: string, t: (key: string, opts?: any) => string): string { switch (tier.toLowerCase()) { case 'gold': - return 'Excellent trust — lowest interest rates and highest credit limits.'; + return t('reputation.descGold'); case 'silver': - return 'Good trust — competitive rates and solid credit limits.'; + return t('reputation.descSilver'); case 'bronze': - return 'Growing trust — moderate rates. Keep paying on time!'; + return t('reputation.descBronze'); case 'starter': - return 'Just getting started — build trust with your first loan.'; + return t('reputation.descStarter'); default: - return 'Build your trust score by using StepFi.'; + return t('reputation.descDefault'); } } @@ -100,6 +79,7 @@ const AnimatedScore = ({ score }: { score: number }) => { }; export default function ReputationScreen() { + const { t } = useTranslation(); const reputation = useUserStore((s) => s.reputation); const setReputation = useUserStore((s) => s.setReputation); const walletAddress = useAuthStore((s) => s.walletAddress); @@ -107,6 +87,13 @@ export default function ReputationScreen() { const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); + const IMPROVEMENT_TIPS: TipItem[] = [ + { icon: CheckCircle, text: t('reputation.tip1'), color: colors.success, key: 'tip1' }, + { icon: Clock, text: t('reputation.tip2'), color: colors.warning, key: 'tip2' }, + { icon: Shield, text: t('reputation.tip3'), color: colors.brandBlue, key: 'tip3' }, + { icon: TrendingUp, text: t('reputation.tip4'), color: colors.brandGreen, key: 'tip4' }, + ]; + const { showTierUp, newTier, @@ -146,7 +133,7 @@ export default function ReputationScreen() { const data = await reputationService.getScore(walletAddress); setReputation(data); } catch { - setError('Could not load your reputation score. Please try again.'); + setError(t('common.somethingWentWrong')); } finally { setIsLoading(false); setIsRefreshing(false); @@ -168,11 +155,11 @@ export default function ReputationScreen() { { setIsLoading(true); void fetchReputation(); } }} + action={{ label: t('common.tryAgain'), onPress: () => { setIsLoading(true); void fetchReputation(); } }} /> ); @@ -184,8 +171,8 @@ export default function ReputationScreen() { @@ -217,7 +204,7 @@ export default function ReputationScreen() { className="text-2xl font-bold mt-2 mb-6" style={{ color: colors.textPrimary }} > - Reputation Score + {t('reputation.reputationScore')} {/* Main score card */} @@ -233,7 +220,7 @@ export default function ReputationScreen() { > - / 100 + {t('reputation.scoreOutOf')} @@ -246,7 +233,7 @@ export default function ReputationScreen() { className="text-sm font-semibold capitalize" style={{ color: tierColor }} > - {reputation?.tier ?? 'Starter'} Tier + {reputation?.tier ?? t('reputation.tierStarter')} {t('reputation.tier')} @@ -255,7 +242,7 @@ export default function ReputationScreen() { className="text-sm text-center leading-5" style={{ color: colors.textMuted }} > - {getTierDescription(reputation?.tier ?? 'starter')} + {getTierDescription(reputation?.tier ?? 'starter', t)} {/* Score progress bar */} @@ -287,7 +274,7 @@ export default function ReputationScreen() { - Interest rate + {t('reputation.interestRate')} - Based on your tier + {t('reputation.basedOnTier')} - Max credit + {t('reputation.maxCredit')} - Your credit limit + {t('reputation.yourCreditLimit')} @@ -326,12 +313,12 @@ export default function ReputationScreen() { className="text-lg font-semibold mt-2 mb-3" style={{ color: colors.textPrimary }} > - How to improve your score + {t('reputation.howToImprove')} {IMPROVEMENT_TIPS.map((tip) => ( - + - Get Vouched + {t('reputation.getVouched')} @@ -364,7 +351,7 @@ export default function ReputationScreen() { - Get vouched by a mentor to increase your credit limit by 10% per vouch (max 3 vouches). + {t('reputation.vouchDescription')} @@ -373,10 +360,10 @@ export default function ReputationScreen() { - Vouch failed + {t('reputation.vouchFailed')} {vouchError.message} - Dismiss + {t('common.dismiss')} @@ -388,12 +375,12 @@ export default function ReputationScreen() { - Vouch submitted + {t('reputation.vouchSubmitted')} - TX: {vouchTxHash.slice(0, 16)}... + {t('reputation.txLabel')} {vouchTxHash.slice(0, 16)}... - Dismiss + {t('common.dismiss')} @@ -408,10 +395,10 @@ export default function ReputationScreen() { {isVouching ? ( - Submitting vouch... + {t('reputation.submittingVouch')} ) : ( - Request Vouch + {t('reputation.requestVouch')} )} diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index 5a93a42..5ba48f9 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -21,6 +21,7 @@ import { Copy, CheckCircle, Fingerprint, + Globe, } from 'lucide-react-native'; import { colors } from '../../constants/colors'; import { Card } from '../../components/shared/Card'; @@ -28,6 +29,7 @@ import { useAuthStore } from '../../stores/auth.store'; import { useUserStore } from '../../stores/user.store'; import { useWalletStore } from '../../stores/wallet.store'; import { biometricService } from '../../src/security/biometric.service'; +import { useTranslation } from '../../hooks/useTranslation'; interface MenuItemProps { icon: typeof User; @@ -80,7 +82,14 @@ function MenuItem({ ); } +const LANGUAGES = [ + { code: 'en', label: 'English' }, + { code: 'fr', label: 'Français' }, + { code: 'pt', label: 'Português' }, +]; + export default function SettingsScreen() { + const { t, currentLanguage, changeLanguage } = useTranslation(); const clearAuth = useAuthStore((s) => s.clearAuth); const walletAddress = useAuthStore((s) => s.walletAddress); const profile = useUserStore((s) => s.profile); @@ -96,6 +105,8 @@ export default function SettingsScreen() { const [pinStep, setPinStep] = useState<'create' | 'confirm'>('create'); const [pinError, setPinError] = useState(''); + const [languageModalVisible, setLanguageModalVisible] = useState(false); + useEffect(() => { async function loadState() { const enabled = await biometricService.isBiometricsEnabled(); @@ -109,7 +120,7 @@ export default function SettingsScreen() { const truncatedAddress = walletAddress ? `${walletAddress.slice(0, 6)}...${walletAddress.slice(-6)}` - : 'Not connected'; + : t('settings.notConnected'); const handleCopyAddress = () => { if (walletAddress) { @@ -120,12 +131,12 @@ export default function SettingsScreen() { const handleSignOut = () => { Alert.alert( - 'Sign out', - 'Are you sure you want to sign out? You will need to reconnect your wallet.', + t('settings.signOutTitle'), + t('settings.signOutMessage'), [ - { text: 'Cancel', style: 'cancel' }, + { text: t('common.cancel'), style: 'cancel' }, { - text: 'Sign out', + text: t('settings.signOutConfirm'), style: 'destructive', onPress: async () => { setDisconnected(); @@ -147,7 +158,7 @@ export default function SettingsScreen() { const handlePinSetupNext = useCallback(() => { if (pinInput.length < 4 || pinInput.length > 6) { - setPinError('PIN must be 4-6 digits'); + setPinError(t('settings.pinErrorLength')); return; } if (pinStep === 'create') { @@ -156,7 +167,7 @@ export default function SettingsScreen() { setPinError(''); } else { if (pinInput !== pinConfirm) { - setPinError('PINs do not match'); + setPinError(t('settings.pinErrorMatch')); return; } biometricService.setPin(pinInput).then(() => { @@ -166,7 +177,7 @@ export default function SettingsScreen() { }); }); } - }, [pinInput, pinConfirm, pinStep]); + }, [pinInput, pinConfirm, pinStep, t]); const handleToggleBiometrics = useCallback( async (value: boolean) => { @@ -196,12 +207,12 @@ export default function SettingsScreen() { } } else { Alert.alert( - 'Disable biometric lock', - 'Are you sure? Your PIN will also be removed.', + t('settings.disableBiometricTitle'), + t('settings.disableBiometricMessage'), [ - { text: 'Cancel', style: 'cancel' }, + { text: t('common.cancel'), style: 'cancel' }, { - text: 'Disable', + text: t('settings.disableBiometricConfirm'), style: 'destructive', onPress: async () => { await biometricService.disableBiometrics(); @@ -212,9 +223,11 @@ export default function SettingsScreen() { ); } }, - [openPinSetup], + [openPinSetup, t], ); + const currentLangLabel = LANGUAGES.find((l) => l.code === currentLanguage)?.label ?? LANGUAGES[0].label; + return ( - Settings + {t('settings.settings')} {/* Profile Card */} @@ -242,13 +255,13 @@ export default function SettingsScreen() { className="text-lg font-semibold" style={{ color: colors.textPrimary }} > - {profile?.displayName ?? 'StepFi User'} + {profile?.displayName ?? t('settings.defaultName')} - {profile?.role ?? 'Learner'} · {profile?.school ?? profile?.organization ?? ''} + {profile?.role ?? t('settings.defaultRole')} · {profile?.school ?? profile?.organization ?? ''} @@ -279,13 +292,13 @@ export default function SettingsScreen() { className="text-xs font-semibold uppercase tracking-wide mb-2 ml-1" style={{ color: colors.textMuted }} > - Account + {t('settings.account')} {}} @@ -293,20 +306,38 @@ export default function SettingsScreen() { {}} /> + {/* Language Section */} + + {t('settings.language')} + + + setLanguageModalVisible(true)} + /> + + {/* Security Section */} - Security + {t('settings.security')} @@ -321,14 +352,14 @@ export default function SettingsScreen() { className="text-sm font-medium" style={{ color: colors.textPrimary }} > - Biometric Lock + {t('settings.biometricLock')} {biometricsEnabled - ? 'Lock screen is active' + ? t('settings.biometricActive') : !biometricsAvailable - ? 'PIN-only mode' - : 'Require authentication to open app'} + ? t('settings.biometricPinOnly') + : t('settings.biometricRequireAuth')} - Support + {t('settings.support')} {}} @@ -359,8 +390,8 @@ export default function SettingsScreen() { {}} @@ -371,8 +402,8 @@ export default function SettingsScreen() { - StepFi v1.0.0 + {t('settings.versionLabel')} + {/* Language Picker Modal */} + setLanguageModalVisible(false)} + > + + + + {t('settings.language')} + + {LANGUAGES.map((lang) => { + const isActive = currentLanguage === lang.code; + return ( + { + await changeLanguage(lang.code); + setLanguageModalVisible(false); + }} + > + + {lang.label} + + {isActive ? ( + + ) : null} + + ); + })} + setLanguageModalVisible(false)} + > + + {t('common.cancel')} + + + + + + {/* PIN Setup Modal */} - {pinStep === 'create' ? 'Set PIN' : 'Confirm PIN'} + {pinStep === 'create' ? t('settings.setPin') : t('settings.confirmPin')} {pinStep === 'create' - ? 'Enter a 4-6 digit PIN for fallback access' - : 'Re-enter your PIN to confirm'} + ? t('settings.setPinDescription') + : t('settings.confirmPinDescription')} {pinError ? ( @@ -434,7 +526,7 @@ export default function SettingsScreen() { borderWidth: 1, borderColor: colors.borderSubtle, }} - placeholder={pinStep === 'confirm' ? 'Re-enter PIN' : 'Enter PIN'} + placeholder={pinStep === 'confirm' ? t('settings.pinPlaceholderReenter') : t('settings.pinPlaceholderEnter')} placeholderTextColor={colors.textMuted} value={pinStep === 'create' ? pinInput : pinConfirm} onChangeText={pinStep === 'create' ? setPinInput : setPinConfirm} @@ -463,7 +555,7 @@ export default function SettingsScreen() { className="text-base font-bold" style={{ color: colors.background }} > - {pinStep === 'create' ? 'Next' : 'Confirm & Enable'} + {pinStep === 'create' ? t('settings.pinNext') : t('settings.pinConfirmEnable')} @@ -478,7 +570,7 @@ export default function SettingsScreen() { className="text-sm" style={{ color: colors.textMuted }} > - Cancel + {t('settings.pinCancel')} diff --git a/app/_layout.tsx b/app/_layout.tsx index 1fbbef3..9355f9d 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,11 +1,12 @@ import { Stack, useRouter, useSegments } from 'expo-router'; -import { useEffect, useRef, useCallback } from 'react'; +import { useEffect, useRef, useCallback, useState } from 'react'; import { AppState, View } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { useAuthStore } from '../stores/auth.store'; import { useSecurityStore } from '../src/security/security.store'; import { biometricService } from '../src/security/biometric.service'; import { BiometricGate } from '../src/components/BiometricGate'; +import { initPromise } from '../src/locales/i18n'; import '../global.css'; const IDLE_TIMEOUT_MS = 5 * 60 * 1000; @@ -30,6 +31,7 @@ function useAuthGuard() { } export default function RootLayout() { + const [i18nReady, setI18nReady] = useState(false); const hydrate = useAuthStore((s) => s.hydrate); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const isLoading = useAuthStore((s) => s.isLoading); @@ -57,6 +59,7 @@ export default function RootLayout() { useEffect(() => { void hydrate(); + initPromise.then(() => setI18nReady(true)); }, [hydrate]); useAuthGuard(); @@ -104,7 +107,7 @@ export default function RootLayout() { } }, [isLocked, isAuthenticated, startIdleTimer]); - if (isLoading) { + if (isLoading || !i18nReady) { return null; } diff --git a/components/reputation/ReputationProgressWidget.tsx b/components/reputation/ReputationProgressWidget.tsx index fe4d23a..7d57155 100644 --- a/components/reputation/ReputationProgressWidget.tsx +++ b/components/reputation/ReputationProgressWidget.tsx @@ -4,8 +4,10 @@ import { useRouter } from 'expo-router'; import { TrendingUp, ChevronRight } from 'lucide-react-native'; import { colors } from '../../constants/colors'; import { useUserStore } from '../../stores/user.store'; +import { useTranslation } from '../../hooks/useTranslation'; export function ReputationProgressWidget() { + const { t } = useTranslation(); const router = useRouter(); const reputation = useUserStore((s) => s.reputation); @@ -14,20 +16,22 @@ export function ReputationProgressWidget() { const currentScore = reputation.score; const currentTier = reputation.tier.toLowerCase(); - let nextTier = 'Max'; + const tierNames = t('tierNames', { returnObjects: true }) as string[]; + + let nextTier = t('components.reputationWidget.maxTier'); let nextThreshold = 100; if (currentTier === 'starter') { - nextTier = 'Bronze'; + nextTier = tierNames[1] ?? 'Bronze'; nextThreshold = 20; } else if (currentTier === 'bronze') { - nextTier = 'Silver'; + nextTier = tierNames[2] ?? 'Silver'; nextThreshold = 50; } else if (currentTier === 'silver') { - nextTier = 'Gold'; + nextTier = tierNames[3] ?? 'Gold'; nextThreshold = 80; } else if (currentTier === 'gold') { - nextTier = 'Top'; + nextTier = t('components.reputationWidget.topTier'); nextThreshold = 100; } @@ -59,10 +63,10 @@ export function ReputationProgressWidget() { - Reputation + {t('components.reputationWidget.reputation')} - Your trust on StepFi + {t('components.reputationWidget.subtitle')} @@ -75,7 +79,7 @@ export function ReputationProgressWidget() { {currentScore} - / {nextThreshold} to {nextTier} + {t('components.reputationWidget.progressTo', { threshold: nextThreshold, tier: nextTier })} - {reputation.tier} Tier + {(tierNames[['starter', 'bronze', 'silver', 'gold'].indexOf(currentTier)] ?? reputation.tier)} {t('components.reputationWidget.tier')} diff --git a/hooks/useTranslation.ts b/hooks/useTranslation.ts new file mode 100644 index 0000000..f9ded0d --- /dev/null +++ b/hooks/useTranslation.ts @@ -0,0 +1,21 @@ +import { useTranslation as useI18nTranslation } from 'react-i18next'; +import { useCallback } from 'react'; +import i18n from '../src/locales/i18n'; + +export function useTranslation() { + const { t, i18n: i18nInstance } = useI18nTranslation(); + + const changeLanguage = useCallback( + async (lang: string) => { + await i18nInstance.changeLanguage(lang); + }, + [i18nInstance], + ); + + const currentLanguage = i18nInstance.language?.split('-')[0] ?? 'en'; + const rtl = ['ar', 'he', 'fa', 'ur'].includes(currentLanguage); + + return { t, i18n: i18nInstance, changeLanguage, currentLanguage, isRTL: rtl }; +} + +export { i18n }; diff --git a/package-lock.json b/package-lock.json index 0688180..bd46451 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,14 +20,17 @@ "expo-image-picker": "~17.0.11", "expo-linking": "~8.0.12", "expo-local-authentication": "~17.0.8", + "expo-localization": "^56.0.6", "expo-notifications": "~0.32.17", "expo-router": "~6.0.23", "expo-secure-store": "~15.0.8", "expo-status-bar": "~3.0.8", + "i18next": "^26.3.1", "lucide-react-native": "^0.562.0", "nativewind": "latest", "react": "19.1.0", "react-dom": "19.1.0", + "react-i18next": "^17.0.8", "react-native": "0.81.5", "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-reanimated": "~4.1.1", @@ -1487,10 +1490,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "engines": { "node": ">=6.9.0" } @@ -7505,6 +7507,18 @@ "expo": "*" } }, + "node_modules/expo-localization": { + "version": "56.0.6", + "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-56.0.6.tgz", + "integrity": "sha512-zzBVoUFHCVNBywcxGsspoZeIXebihOo/AnmQYE4jMv8gHCSKlLNFT+ft+0+mWcZCMs9necvUs8S8TDonAu/xBA==", + "dependencies": { + "rtl-detect": "^1.0.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "3.0.25", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz", @@ -8815,6 +8829,14 @@ "dev": true, "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -8901,6 +8923,33 @@ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/i18next": { + "version": "26.3.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", + "integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -13239,6 +13288,32 @@ "react": ">=17.0.0" } }, + "node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", @@ -13987,6 +14062,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rtl-detect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", + "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -15970,6 +16050,14 @@ "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", "license": "MIT" }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 9a20999..9f80f3d 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,17 @@ "expo-image-picker": "~17.0.11", "expo-linking": "~8.0.12", "expo-local-authentication": "~17.0.8", + "expo-localization": "^56.0.6", "expo-notifications": "~0.32.17", "expo-router": "~6.0.23", "expo-secure-store": "~15.0.8", "expo-status-bar": "~3.0.8", + "i18next": "^26.3.1", "lucide-react-native": "^0.562.0", "nativewind": "latest", "react": "19.1.0", "react-dom": "19.1.0", + "react-i18next": "^17.0.8", "react-native": "0.81.5", "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-reanimated": "~4.1.1", diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..d0ede86 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,226 @@ +{ + "common": { + "somethingWentWrong": "Something went wrong", + "tryAgain": "Try again", + "cancel": "Cancel", + "dismiss": "Dismiss", + "next": "Next", + "save": "Saving...", + "continue": "Continue", + "active": "Active", + "paid": "Paid", + "pending": "Pending", + "due": "Due", + "loanNumber": "Loan #{{id}}" + }, + "tabs": { + "home": "Home", + "loans": "Loans", + "calendar": "Calendar", + "score": "Score", + "settings": "Settings" + }, + "home": { + "greeting": "there", + "availableCredit": "Available Credit", + "activeLimit": "Active Limit", + "used": "Used", + "limit": "Limit", + "apply": "Apply", + "pay": "Pay", + "history": "History", + "vouches": "Vouches", + "activeLoans": "Active Loans", + "remainingBalance": "Remaining Balance", + "active": "Active", + "noActiveLoans": "No active loans", + "upcomingPayments": "Upcoming Payments", + "installment": "Installment", + "due": "Due", + "payNow": "Pay Now", + "noUpcomingPayments": "No upcoming payments", + "errorLoading": "Could not load your dashboard. Please try again." + }, + "loans": { + "myLoans": "My Loans", + "active": "Active", + "paid": "Paid", + "pending": "Pending", + "totalAmount": "Total amount", + "remaining": "Remaining", + "installments": "Installments", + "installmentsPaid": "{{paid}} of {{total}} paid", + "statusActive": "Active", + "statusPaid": "Paid", + "statusDefaulted": "Defaulted", + "statusPending": "Pending", + "statusCancelled": "Cancelled", + "noLoansYet": "No loans yet", + "noLoansMessage": "Apply for your first loan to finance laptops, courses, and dev tools.", + "applyNow": "Apply now", + "errorLoading": "Could not load your loans. Please try again." + }, + "calendar": { + "calendar": "Calendar", + "paymentStreak": "Payment Streak", + "paymentStreakDesc": "Consecutive on-time payments", + "due": "Due", + "paid": "Paid", + "noPaymentsOnDay": "No payments on this day", + "noPaymentsScheduled": "No payments scheduled", + "noPaymentsMessage": "Your active loans will appear here with their payment due dates.", + "calendarExported": "Calendar Exported", + "calendarExportedMessage": "Payment dates have been added to your calendar.", + "permissionRequired": "Permission Required", + "permissionRequiredMessage": "Calendar access was not granted. Please enable it in settings.", + "remindersSet": "Reminders Set", + "remindersSetMessage": "Payment reminders have been scheduled 14, 7, 3, and 1 day before each due date.", + "eventPaid": "Paid", + "eventDue": "Due", + "payment": "payment", + "payments": "payments" + }, + "reputation": { + "reputationScore": "Reputation Score", + "scoreOutOf": "/ 100", + "tier": "Tier", + "interestRate": "Interest rate", + "basedOnTier": "Based on your tier", + "maxCredit": "Max credit", + "yourCreditLimit": "Your credit limit", + "howToImprove": "How to improve your score", + "getVouched": "Get Vouched", + "vouchDescription": "Get vouched by a mentor to increase your credit limit by 10% per vouch (max 3 vouches).", + "vouchFailed": "Vouch failed", + "vouchSubmitted": "Vouch submitted", + "txLabel": "TX:", + "submittingVouch": "Submitting vouch...", + "requestVouch": "Request Vouch", + "noReputationData": "No reputation data", + "noReputationMessage": "Your trust score will appear here after your first loan activity.", + "descGold": "Excellent trust — lowest interest rates and highest credit limits.", + "descSilver": "Good trust — competitive rates and solid credit limits.", + "descBronze": "Growing trust — moderate rates. Keep paying on time!", + "descStarter": "Just getting started — build trust with your first loan.", + "descDefault": "Build your trust score by using StepFi.", + "tip1": "Pay installments on time to increase your score", + "tip2": "Avoid late payments — each one reduces your score", + "tip3": "Get vouched by a mentor to boost your credit limit", + "tip4": "Complete more loans to build a strong payment history" + }, + "settings": { + "settings": "Settings", + "account": "Account", + "security": "Security", + "support": "Support", + "editProfile": "Edit Profile", + "editProfileSubtitle": "Update your personal information", + "notifications": "Notifications", + "notificationsSubtitle": "Payment reminders and updates", + "biometricLock": "Biometric Lock", + "biometricActive": "Lock screen is active", + "biometricPinOnly": "PIN-only mode", + "biometricRequireAuth": "Require authentication to open app", + "helpSupport": "Help & Support", + "helpSupportSubtitle": "FAQs and contact support", + "aboutStepfi": "About StepFi", + "aboutStepfiSubtitle": "Version, terms, and privacy", + "signOut": "Sign out", + "signOutSubtitle": "Disconnect wallet and sign out", + "signOutTitle": "Sign out", + "signOutMessage": "Are you sure you want to sign out? You will need to reconnect your wallet.", + "signOutConfirm": "Sign out", + "disableBiometricTitle": "Disable biometric lock", + "disableBiometricMessage": "Are you sure? Your PIN will also be removed.", + "disableBiometricConfirm": "Disable", + "setPin": "Set PIN", + "confirmPin": "Confirm PIN", + "setPinDescription": "Enter a 4-6 digit PIN for fallback access", + "confirmPinDescription": "Re-enter your PIN to confirm", + "pinErrorLength": "PIN must be 4-6 digits", + "pinErrorMatch": "PINs do not match", + "pinNext": "Next", + "pinConfirmEnable": "Confirm & Enable", + "pinCancel": "Cancel", + "pinPlaceholderEnter": "Enter PIN", + "pinPlaceholderReenter": "Re-enter PIN", + "notConnected": "Not connected", + "versionLabel": "StepFi v1.0.0", + "language": "Language", + "defaultName": "StepFi User", + "defaultRole": "Learner" + }, + "auth": { + "signIn": { + "stepfi": "StepFi", + "stepIntoFuture": "Step into your future.", + "creditWithoutBanks": "Credit without banks. Progress without limits.", + "financeWhatYouNeed": "Finance what you need.", + "signInWithWallet": "Sign in with your Stellar wallet. No passwords.", + "financeLaptops": "Finance laptops, courses, and dev tools.", + "repayInstallments": "Repay in installments. Build your reputation.", + "yourScoreYourTerms": "Your score. Your terms.", + "payOnTime": "Pay on time. Score goes up. Rates go down.", + "connectYourWallet": "Connect your wallet.", + "noPasswords": "No passwords. No email. Just your Stellar wallet.", + "connecting": "Connecting...", + "connectStellarWallet": "Connect Stellar Wallet", + "dontHaveWallet": "Don't have a wallet?", + "walletLobstr": "Lobstr", + "walletXbull": "xBull", + "score": "Score", + "rate": "Rate", + "max": "Max" + }, + "roleSelect": { + "title": "How will you use StepFi?", + "subtitle": "Choose your role. You can switch anytime in settings.", + "learnerTitle": "I'm a Learner", + "learnerSubtitle": "Finance laptops, courses, and dev tools. Build your reputation with every payment.", + "learnerPill1": "Borrow", + "learnerPill2": "Repay", + "learnerPill3": "Build Credit", + "sponsorTitle": "I'm a Sponsor", + "sponsorSubtitle": "Fund the learner lending pool. Earn yield while supporting the next generation of developers.", + "sponsorPill1": "Deposit", + "sponsorPill2": "Earn Yield", + "sponsorPill3": "Support Learners" + }, + "register": { + "title": "Tell us about yourself", + "subtitleMatch": "This helps us match you with the right {{role}}.", + "sponsors": "sponsors", + "learners": "learners", + "changePhoto": "Change Photo", + "displayName": "Display Name", + "displayNamePlaceholder": "e.g. AlexLearnsWeb3", + "school": "School/Institution", + "schoolPlaceholder": "e.g. University of Blockchain", + "program": "Program/Course", + "programPlaceholder": "e.g. Smart Contract Development", + "incomeType": "Income Type (optional)", + "incomeTypePlaceholder": "e.g. Stipend, Freelance, Part-time", + "organization": "Organization", + "organizationPlaceholder": "e.g. Stellar Development Foundation", + "investmentFocus": "Investment Focus (optional)", + "investmentFocusPlaceholder": "e.g. Education, Dev tools, Africa", + "walletAddress": "Wallet address", + "notConnected": "Not connected", + "saving": "Saving...", + "continue": "Continue" + } + }, + "components": { + "reputationWidget": { + "reputation": "Reputation", + "subtitle": "Your trust on StepFi", + "progressTo": "/ {{threshold}} to {{tier}}", + "tier": "Tier", + "maxTier": "Max", + "topTier": "Top" + } + }, + "weekdayHeaders": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + "monthNames": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + "tierNames": ["Starter", "Bronze", "Silver", "Gold"] +} diff --git a/src/locales/fr.json b/src/locales/fr.json new file mode 100644 index 0000000..ae1abae --- /dev/null +++ b/src/locales/fr.json @@ -0,0 +1,226 @@ +{ + "common": { + "somethingWentWrong": "Une erreur s'est produite", + "tryAgain": "Réessayer", + "cancel": "Annuler", + "dismiss": "Ignorer", + "next": "Suivant", + "save": "Enregistrement...", + "continue": "Continuer", + "active": "Actif", + "paid": "Payé", + "pending": "En attente", + "due": "Dû", + "loanNumber": "Prêt n°{{id}}" + }, + "tabs": { + "home": "Accueil", + "loans": "Prêts", + "calendar": "Calendrier", + "score": "Score", + "settings": "Paramètres" + }, + "home": { + "greeting": "bonjour", + "availableCredit": "Crédit disponible", + "activeLimit": "Limite active", + "used": "Utilisé", + "limit": "Limite", + "apply": "Demander", + "pay": "Payer", + "history": "Historique", + "vouches": "Recommandations", + "activeLoans": "Prêts actifs", + "remainingBalance": "Solde restant", + "active": "Actif", + "noActiveLoans": "Aucun prêt actif", + "upcomingPayments": "Paiements à venir", + "installment": "Échéance", + "due": "Échéance", + "payNow": "Payer maintenant", + "noUpcomingPayments": "Aucun paiement à venir", + "errorLoading": "Impossible de charger votre tableau de bord. Veuillez réessayer." + }, + "loans": { + "myLoans": "Mes prêts", + "active": "Actifs", + "paid": "Payés", + "pending": "En attente", + "totalAmount": "Montant total", + "remaining": "Restant", + "installments": "Échéances", + "installmentsPaid": "{{paid}} sur {{total}} payés", + "statusActive": "Actif", + "statusPaid": "Payé", + "statusDefaulted": "Défaillant", + "statusPending": "En attente", + "statusCancelled": "Annulé", + "noLoansYet": "Pas encore de prêts", + "noLoansMessage": "Faites votre première demande pour financer des ordinateurs, des cours et des outils de développement.", + "applyNow": "Postuler maintenant", + "errorLoading": "Impossible de charger vos prêts. Veuillez réessayer." + }, + "calendar": { + "calendar": "Calendrier", + "paymentStreak": "Série de paiements", + "paymentStreakDesc": "Paiements consécutifs à temps", + "due": "Dû", + "paid": "Payé", + "noPaymentsOnDay": "Aucun paiement ce jour", + "noPaymentsScheduled": "Aucun paiement planifié", + "noPaymentsMessage": "Vos prêts actifs apparaîtront ici avec leurs dates d'échéance.", + "calendarExported": "Calendrier exporté", + "calendarExportedMessage": "Les dates de paiement ont été ajoutées à votre calendrier.", + "permissionRequired": "Permission requise", + "permissionRequiredMessage": "L'accès au calendrier n'a pas été accordé. Veuillez l'activer dans les paramètres.", + "remindersSet": "Rappels configurés", + "remindersSetMessage": "Les rappels de paiement ont été programmés 14, 7, 3 et 1 jour avant chaque échéance.", + "eventPaid": "Payé", + "eventDue": "Dû", + "payment": "paiement", + "payments": "paiements" + }, + "reputation": { + "reputationScore": "Score de réputation", + "scoreOutOf": "/ 100", + "tier": "Niveau", + "interestRate": "Taux d'intérêt", + "basedOnTier": "Basé sur votre niveau", + "maxCredit": "Crédit max", + "yourCreditLimit": "Votre limite de crédit", + "howToImprove": "Comment améliorer votre score", + "getVouched": "Se faire recommander", + "vouchDescription": "Faites-vous recommander par un mentor pour augmenter votre limite de crédit de 10% par recommandation (max 3).", + "vouchFailed": "Recommandation échouée", + "vouchSubmitted": "Recommandation soumise", + "txLabel": "TX :", + "submittingVouch": "Soumission de la recommandation...", + "requestVouch": "Demander une recommandation", + "noReputationData": "Aucune donnée de réputation", + "noReputationMessage": "Votre score de confiance apparaîtra ici après votre première activité de prêt.", + "descGold": "Confiance excellente — taux d'intérêt les plus bas et limites de crédit les plus élevées.", + "descSilver": "Bonne confiance — taux compétitifs et limites de crédit solides.", + "descBronze": "Confiance croissante — taux modérés. Continuez à payer à temps !", + "descStarter": "Vous débutez — bâtissez la confiance avec votre premier prêt.", + "descDefault": "Construisez votre score de confiance en utilisant StepFi.", + "tip1": "Payer les échéances à temps pour augmenter votre score", + "tip2": "Évitez les retards de paiement — chacun réduit votre score", + "tip3": "Faites-vous recommander par un mentor pour booster votre crédit", + "tip4": "Effectuez plus de prêts pour construire un historique solide" + }, + "settings": { + "settings": "Paramètres", + "account": "Compte", + "security": "Sécurité", + "support": "Assistance", + "editProfile": "Modifier le profil", + "editProfileSubtitle": "Mettre à jour vos informations personnelles", + "notifications": "Notifications", + "notificationsSubtitle": "Rappels de paiement et mises à jour", + "biometricLock": "Verrouillage biométrique", + "biometricActive": "L'écran de verrouillage est actif", + "biometricPinOnly": "Mode code PIN uniquement", + "biometricRequireAuth": "Exiger une authentification pour ouvrir l'app", + "helpSupport": "Aide et assistance", + "helpSupportSubtitle": "FAQ et contacter le support", + "aboutStepfi": "À propos de StepFi", + "aboutStepfiSubtitle": "Version, conditions et confidentialité", + "signOut": "Se déconnecter", + "signOutSubtitle": "Déconnecter le portefeuille et se déconnecter", + "signOutTitle": "Déconnexion", + "signOutMessage": "Êtes-vous sûr de vouloir vous déconnecter ? Vous devrez reconnecter votre portefeuille.", + "signOutConfirm": "Se déconnecter", + "disableBiometricTitle": "Désactiver le verrouillage biométrique", + "disableBiometricMessage": "Êtes-vous sûr ? Votre code PIN sera également supprimé.", + "disableBiometricConfirm": "Désactiver", + "setPin": "Définir le code PIN", + "confirmPin": "Confirmer le code PIN", + "setPinDescription": "Entrez un code PIN de 4 à 6 chiffres pour l'accès de secours", + "confirmPinDescription": "Saisissez à nouveau votre code PIN pour confirmer", + "pinErrorLength": "Le code PIN doit comporter 4 à 6 chiffres", + "pinErrorMatch": "Les codes PIN ne correspondent pas", + "pinNext": "Suivant", + "pinConfirmEnable": "Confirmer et activer", + "pinCancel": "Annuler", + "pinPlaceholderEnter": "Entrer le code PIN", + "pinPlaceholderReenter": "Resaisir le code PIN", + "notConnected": "Non connecté", + "versionLabel": "StepFi v1.0.0", + "language": "Langue", + "defaultName": "Utilisateur StepFi", + "defaultRole": "Apprenant" + }, + "auth": { + "signIn": { + "stepfi": "StepFi", + "stepIntoFuture": "Entrez dans votre avenir.", + "creditWithoutBanks": "Du crédit sans banque. Une progression sans limites.", + "financeWhatYouNeed": "Financez ce dont vous avez besoin.", + "signInWithWallet": "Connectez-vous avec votre portefeuille Stellar. Pas de mots de passe.", + "financeLaptops": "Financez des ordinateurs, des cours et des outils de développement.", + "repayInstallments": "Remboursez en échéances. Construisez votre réputation.", + "yourScoreYourTerms": "Votre score. Vos conditions.", + "payOnTime": "Payez à temps. Le score augmente. Les taux baissent.", + "connectYourWallet": "Connectez votre portefeuille.", + "noPasswords": "Pas de mots de passe. Pas d'email. Juste votre portefeuille Stellar.", + "connecting": "Connexion...", + "connectStellarWallet": "Connecter un portefeuille Stellar", + "dontHaveWallet": "Vous n'avez pas de portefeuille ?", + "walletLobstr": "Lobstr", + "walletXbull": "xBull", + "score": "Score", + "rate": "Taux", + "max": "Max" + }, + "roleSelect": { + "title": "Comment allez-vous utiliser StepFi ?", + "subtitle": "Choisissez votre rôle. Vous pouvez changer à tout moment dans les paramètres.", + "learnerTitle": "Je suis un apprenant", + "learnerSubtitle": "Financez des ordinateurs, des cours et des outils de développement. Construisez votre réputation à chaque paiement.", + "learnerPill1": "Emprunter", + "learnerPill2": "Rembourser", + "learnerPill3": "Construire du crédit", + "sponsorTitle": "Je suis un sponsor", + "sponsorSubtitle": "Financez le pool de prêts aux apprenants. Gagnez un rendement tout en soutenant la prochaine génération de développeurs.", + "sponsorPill1": "Déposer", + "sponsorPill2": "Gagner un rendement", + "sponsorPill3": "Soutenir les apprenants" + }, + "register": { + "title": "Parlez-nous de vous", + "subtitleMatch": "Cela nous aide à vous mettre en relation avec les bons {{role}}.", + "sponsors": "sponsors", + "learners": "apprenants", + "changePhoto": "Changer la photo", + "displayName": "Nom d'affichage", + "displayNamePlaceholder": "ex. AlexLearnsWeb3", + "school": "École/Institution", + "schoolPlaceholder": "ex. Université de la Blockchain", + "program": "Programme/Cours", + "programPlaceholder": "ex. Développement de contrats intelligents", + "incomeType": "Type de revenu (optionnel)", + "incomeTypePlaceholder": "ex. Bourse, Freelance, Temps partiel", + "organization": "Organisation", + "organizationPlaceholder": "ex. Fondation Stellar Development", + "investmentFocus": "Focus d'investissement (optionnel)", + "investmentFocusPlaceholder": "ex. Éducation, Outils de développement, Afrique", + "walletAddress": "Adresse du portefeuille", + "notConnected": "Non connecté", + "saving": "Enregistrement...", + "continue": "Continuer" + } + }, + "components": { + "reputationWidget": { + "reputation": "Réputation", + "subtitle": "Votre confiance sur StepFi", + "progressTo": "/ {{threshold}} vers {{tier}}", + "tier": "Niveau", + "maxTier": "Max", + "topTier": "Excellent" + } + }, + "weekdayHeaders": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"], + "monthNames": ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"], + "tierNames": ["Débutant", "Bronze", "Argent", "Or"] +} diff --git a/src/locales/i18n.ts b/src/locales/i18n.ts new file mode 100644 index 0000000..ef17c3a --- /dev/null +++ b/src/locales/i18n.ts @@ -0,0 +1,124 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { getLocales } from 'expo-localization'; +import * as SecureStore from 'expo-secure-store'; +import { Platform } from 'react-native'; + +import en from './en.json'; +import fr from './fr.json'; +import pt from './pt.json'; + +const LANG_KEY = 'stepfi_language'; + +const languageDetector = { + type: 'languageDetector' as const, + async: true, + detect: async (callback: (lang: string) => void) => { + let lang: string | null = null; + try { + if (Platform.OS === 'web') { + lang = localStorage.getItem(LANG_KEY); + } else { + lang = await SecureStore.getItemAsync(LANG_KEY); + } + } catch {} + if (!lang) { + try { + const locales = getLocales(); + lang = locales?.[0]?.languageCode ?? 'en'; + } catch { + lang = 'en'; + } + } + callback(lang ?? 'en'); + }, + init: () => {}, + cacheUserLanguage: async (lang: string) => { + try { + if (Platform.OS === 'web') { + localStorage.setItem(LANG_KEY, lang); + } else { + await SecureStore.setItemAsync(LANG_KEY, lang); + } + } catch {} + }, +}; + +const initPromise = i18n + .use(languageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + resources: { + en: { translation: en }, + fr: { translation: fr }, + pt: { translation: pt }, + }, + interpolation: { + escapeValue: false, + format: (value: unknown, format?: string, lng?: string) => { + if (format === 'number') { + return new Intl.NumberFormat(lng).format(value as number); + } + if (format === 'currency') { + return new Intl.NumberFormat(lng, { + style: 'currency', + currency: 'USD', + }).format(value as number); + } + if (format === 'dateShort') { + return new Intl.DateTimeFormat(lng, { + month: 'short', + day: 'numeric', + }).format(value as Date); + } + if (format === 'dateLong') { + return new Intl.DateTimeFormat(lng, { + weekday: 'long', + month: 'long', + day: 'numeric', + }).format(value as Date); + } + if (format === 'dateFull') { + return new Intl.DateTimeFormat(lng, { + month: 'short', + day: 'numeric', + year: 'numeric', + }).format(value as Date); + } + return value; + }, + } as any, + react: { + useSuspense: false, + }, + returnObjects: true, + returnNull: false, + }); + +export function formatCurrency(amount: number): string { + const lng = i18n.language ?? 'en'; + return new Intl.NumberFormat(lng, { + style: 'currency', + currency: 'USD', + }).format(amount); +} + +export function formatDate(date: Date, format: 'short' | 'long' | 'full'): string { + const lng = i18n.language ?? 'en'; + const options: Intl.DateTimeFormatOptions = + format === 'short' + ? { month: 'short', day: 'numeric' } + : format === 'long' + ? { weekday: 'long', month: 'long', day: 'numeric' } + : { month: 'short', day: 'numeric', year: 'numeric' }; + return new Intl.DateTimeFormat(lng, options).format(date); +} + +export function isRTL(language?: string): boolean { + const lang = (language ?? i18n.language ?? 'en').split('-')[0]; + return ['ar', 'he', 'fa', 'ur'].includes(lang); +} + +export { initPromise }; +export default i18n; diff --git a/src/locales/pt.json b/src/locales/pt.json new file mode 100644 index 0000000..5870e65 --- /dev/null +++ b/src/locales/pt.json @@ -0,0 +1,225 @@ +{ + "common": { + "somethingWentWrong": "Algo deu errado", + "tryAgain": "Tentar novamente", + "cancel": "Cancelar", + "dismiss": "Dispensar", + "next": "Próximo", + "save": "Salvando...", + "continue": "Continuar", + "active": "Ativo", + "paid": "Pago", + "pending": "Pendente", + "due": "Vencimento", + "loanNumber": "Empréstimo #{{id}}" + }, + "tabs": { + "home": "Início", + "loans": "Empréstimos", + "calendar": "Calendário", + "score": "Pontuação", + "settings": "Configurações" + }, + "home": { + "greeting": "olá", + "availableCredit": "Crédito disponível", + "activeLimit": "Limite ativo", + "used": "Usado", + "limit": "Limite", + "apply": "Solicitar", + "pay": "Pagar", + "history": "Histórico", + "vouches": "Recomendações", + "activeLoans": "Empréstimos ativos", + "remainingBalance": "Saldo restante", + "active": "Ativo", + "noActiveLoans": "Nenhum empréstimo ativo", + "upcomingPayments": "Pagamentos futuros", + "installment": "Parcela", + "payNow": "Pagar agora", + "noUpcomingPayments": "Nenhum pagamento futuro", + "errorLoading": "Não foi possível carregar seu painel. Tente novamente." + }, + "loans": { + "myLoans": "Meus empréstimos", + "active": "Ativos", + "paid": "Pagos", + "pending": "Pendentes", + "totalAmount": "Valor total", + "remaining": "Restante", + "installments": "Parcelas", + "installmentsPaid": "{{paid}} de {{total}} pagas", + "statusActive": "Ativo", + "statusPaid": "Pago", + "statusDefaulted": "Inadimplente", + "statusPending": "Pendente", + "statusCancelled": "Cancelado", + "noLoansYet": "Nenhum empréstimo ainda", + "noLoansMessage": "Solicite seu primeiro empréstimo para financiar laptops, cursos e ferramentas de desenvolvimento.", + "applyNow": "Solicitar agora", + "errorLoading": "Não foi possível carregar seus empréstimos. Tente novamente." + }, + "calendar": { + "calendar": "Calendário", + "paymentStreak": "Sequência de pagamentos", + "paymentStreakDesc": "Pagamentos consecutivos em dia", + "due": "Vencimento", + "paid": "Pago", + "noPaymentsOnDay": "Nenhum pagamento neste dia", + "noPaymentsScheduled": "Nenhum pagamento agendado", + "noPaymentsMessage": "Seus empréstimos ativos aparecerão aqui com suas datas de vencimento.", + "calendarExported": "Calendário exportado", + "calendarExportedMessage": "As datas de pagamento foram adicionadas ao seu calendário.", + "permissionRequired": "Permissão necessária", + "permissionRequiredMessage": "O acesso ao calendário não foi concedido. Ative-o nas configurações.", + "remindersSet": "Lembretes configurados", + "remindersSetMessage": "Lembretes de pagamento foram agendados 14, 7, 3 e 1 dia antes de cada vencimento.", + "eventPaid": "Pago", + "eventDue": "Vencimento", + "payment": "pagamento", + "payments": "pagamentos" + }, + "reputation": { + "reputationScore": "Pontuação de reputação", + "scoreOutOf": "/ 100", + "tier": "Nível", + "interestRate": "Taxa de juros", + "basedOnTier": "Baseado no seu nível", + "maxCredit": "Crédito máximo", + "yourCreditLimit": "Seu limite de crédito", + "howToImprove": "Como melhorar sua pontuação", + "getVouched": "Obter recomendação", + "vouchDescription": "Seja recomendado por um mentor para aumentar seu limite de crédito em 10% por recomendação (máx. 3).", + "vouchFailed": "Recomendação falhou", + "vouchSubmitted": "Recomendação enviada", + "txLabel": "TX:", + "submittingVouch": "Enviando recomendação...", + "requestVouch": "Solicitar recomendação", + "noReputationData": "Sem dados de reputação", + "noReputationMessage": "Sua pontuação de confiança aparecerá aqui após sua primeira atividade de empréstimo.", + "descGold": "Confiança excelente — menores taxas de juros e maiores limites de crédito.", + "descSilver": "Boa confiança — taxas competitivas e bons limites de crédito.", + "descBronze": "Confiança crescente — taxas moderadas. Continue pagando em dia!", + "descStarter": "Está começando — construa confiança com seu primeiro empréstimo.", + "descDefault": "Construa sua pontuação de confiança usando StepFi.", + "tip1": "Pague as parcelas em dia para aumentar sua pontuação", + "tip2": "Evite atrasos — cada um reduz sua pontuação", + "tip3": "Seja recomendado por um mentor para aumentar seu crédito", + "tip4": "Complete mais empréstimos para construir um histórico sólido" + }, + "settings": { + "settings": "Configurações", + "account": "Conta", + "security": "Segurança", + "support": "Suporte", + "editProfile": "Editar perfil", + "editProfileSubtitle": "Atualizar suas informações pessoais", + "notifications": "Notificações", + "notificationsSubtitle": "Lembretes de pagamento e atualizações", + "biometricLock": "Bloqueio biométrico", + "biometricActive": "Tela de bloqueio ativa", + "biometricPinOnly": "Modo apenas PIN", + "biometricRequireAuth": "Exigir autenticação para abrir o app", + "helpSupport": "Ajuda e suporte", + "helpSupportSubtitle": "Perguntas frequentes e contatar suporte", + "aboutStepfi": "Sobre o StepFi", + "aboutStepfiSubtitle": "Versão, termos e privacidade", + "signOut": "Sair", + "signOutSubtitle": "Desconectar carteira e sair", + "signOutTitle": "Sair", + "signOutMessage": "Tem certeza de que deseja sair? Você precisará reconectar sua carteira.", + "signOutConfirm": "Sair", + "disableBiometricTitle": "Desativar bloqueio biométrico", + "disableBiometricMessage": "Tem certeza? Seu PIN também será removido.", + "disableBiometricConfirm": "Desativar", + "setPin": "Definir PIN", + "confirmPin": "Confirmar PIN", + "setPinDescription": "Digite um PIN de 4 a 6 dígitos para acesso alternativo", + "confirmPinDescription": "Digite novamente seu PIN para confirmar", + "pinErrorLength": "O PIN deve ter 4-6 dígitos", + "pinErrorMatch": "Os PINs não correspondem", + "pinNext": "Próximo", + "pinConfirmEnable": "Confirmar e ativar", + "pinCancel": "Cancelar", + "pinPlaceholderEnter": "Digitar PIN", + "pinPlaceholderReenter": "Redigitar PIN", + "notConnected": "Não conectado", + "versionLabel": "StepFi v1.0.0", + "language": "Idioma", + "defaultName": "Usuário StepFi", + "defaultRole": "Aprendiz" + }, + "auth": { + "signIn": { + "stepfi": "StepFi", + "stepIntoFuture": "Entre no seu futuro.", + "creditWithoutBanks": "Crédito sem bancos. Progresso sem limites.", + "financeWhatYouNeed": "Financie o que você precisa.", + "signInWithWallet": "Entre com sua carteira Stellar. Sem senhas.", + "financeLaptops": "Financie laptops, cursos e ferramentas de desenvolvimento.", + "repayInstallments": "Pague em parcelas. Construa sua reputação.", + "yourScoreYourTerms": "Sua pontuação. Seus termos.", + "payOnTime": "Pague em dia. A pontuação sobe. As taxas caem.", + "connectYourWallet": "Conecte sua carteira.", + "noPasswords": "Sem senhas. Sem email. Apenas sua carteira Stellar.", + "connecting": "Conectando...", + "connectStellarWallet": "Conectar carteira Stellar", + "dontHaveWallet": "Não tem uma carteira?", + "walletLobstr": "Lobstr", + "walletXbull": "xBull", + "score": "Pontuação", + "rate": "Taxa", + "max": "Máx" + }, + "roleSelect": { + "title": "Como você usará o StepFi?", + "subtitle": "Escolha seu papel. Você pode alterar a qualquer momento nas configurações.", + "learnerTitle": "Sou um aprendiz", + "learnerSubtitle": "Financie laptops, cursos e ferramentas de desenvolvimento. Construa sua reputação a cada pagamento.", + "learnerPill1": "Pedir emprestado", + "learnerPill2": "Pagar", + "learnerPill3": "Construir crédito", + "sponsorTitle": "Sou um patrocinador", + "sponsorSubtitle": "Financie o pool de empréstimos para aprendizes. Ganhe rendimento enquanto apoia a próxima geração de desenvolvedores.", + "sponsorPill1": "Depositar", + "sponsorPill2": "Ganhar rendimento", + "sponsorPill3": "Apoiar aprendizes" + }, + "register": { + "title": "Conte-nos sobre você", + "subtitleMatch": "Isso nos ajuda a conectar você com os {{role}} certos.", + "sponsors": "patrocinadores", + "learners": "aprendizes", + "changePhoto": "Alterar foto", + "displayName": "Nome de exibição", + "displayNamePlaceholder": "ex. AlexLearnsWeb3", + "school": "Escola/Instituição", + "schoolPlaceholder": "ex. Universidade da Blockchain", + "program": "Programa/Curso", + "programPlaceholder": "ex. Desenvolvimento de Smart Contracts", + "incomeType": "Tipo de renda (opcional)", + "incomeTypePlaceholder": "ex. Bolsa, Freelance, Meio período", + "organization": "Organização", + "organizationPlaceholder": "ex. Stellar Development Foundation", + "investmentFocus": "Foco de investimento (opcional)", + "investmentFocusPlaceholder": "ex. Educação, Ferramentas de desenvolvimento, África", + "walletAddress": "Endereço da carteira", + "notConnected": "Não conectado", + "saving": "Salvando...", + "continue": "Continuar" + } + }, + "components": { + "reputationWidget": { + "reputation": "Reputação", + "subtitle": "Sua confiança no StepFi", + "progressTo": "/ {{threshold}} para {{tier}}", + "tier": "Nível", + "maxTier": "Máx", + "topTier": "Topo" + } + }, + "weekdayHeaders": ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"], + "monthNames": ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"], + "tierNames": ["Iniciante", "Bronze", "Prata", "Ouro"] +}