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')}
+ {/* 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 683a0bf..9355f9d 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,12 +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 { walletService } from '../services/wallet.service';
+import { initPromise } from '../src/locales/i18n';
import '../global.css';
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
@@ -31,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);
@@ -58,7 +59,7 @@ export default function RootLayout() {
useEffect(() => {
void hydrate();
- void walletService.initialize();
+ initPromise.then(() => setI18nReady(true));
}, [hydrate]);
useAuthGuard();
@@ -106,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 d810a73..262dd28 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,14 +22,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",
@@ -1495,10 +1498,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"
}
@@ -8047,6 +8049,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",
@@ -9374,6 +9388,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",
@@ -9460,6 +9482,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",
@@ -13979,6 +14028,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",
@@ -14736,6 +14811,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",
@@ -16915,6 +16995,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 ed3d4b9..f5f8989 100644
--- a/package.json
+++ b/package.json
@@ -26,14 +26,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"]
+}