feat(i18n-mobile): add bilingual support (Català / Castellano) for React Native app
Run Tests on Branches / Detect Changes (push) Successful in 13s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 1m40s
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m37s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped

Add lightweight i18n system matching the web app architecture.
Language persisted in AsyncStorage('ff-lang').

New files:
- src/i18n/locales/ca.js (~190 translation keys)
- src/i18n/locales/es.js (~190 translation keys)
- src/i18n/LanguageContext.jsx (Provider + AsyncStorage)
- src/i18n/useTranslation.js (hook)
- src/i18n/index.js (barrel export)

Modified 16 files:
- app/_layout.tsx: wrap with LanguageProvider, translate stack titles
- app/(tabs)/_layout.tsx: translate tab bar labels
- app/(tabs)/index.tsx: translate home screen
- app/(tabs)/search.tsx: translate search screen
- app/(tabs)/alerts.tsx: translate alerts screen
- app/(tabs)/profile.tsx: translate profile + add language selector (CA/ES)
- app/(tabs)/scan.tsx: translate scanner screen
- app/auth/login.tsx: translate login/register
- app/medicine/[id].tsx: translate medicine detail
- app/pharmacy/[id].tsx: translate pharmacy detail
- app/product/[source]/[id].tsx: translate product detail
- 5 components: MedicineCard, StockBadge, SearchBar, LoadingSpinner, BarcodeScanner

Language selector in Profile screen (globe icon + CA/ES toggle).
No routes changed. No API calls affected.
This commit is contained in:
Antoni Nuñez Romeu
2026-07-17 10:22:14 +02:00
parent d12c575fcf
commit ee958d4525
21 changed files with 771 additions and 198 deletions
+7 -5
View File
@@ -3,6 +3,7 @@ import { Ionicons } from '@expo/vector-icons';
import { View, StyleSheet, useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useThemeContext } from '../../components/ThemeProvider';
import { useTranslation } from '../../src/i18n';
import { shadows } from '../../constants/theme';
const TABLET_MIN_WIDTH = 768;
@@ -25,6 +26,7 @@ export default function TabLayout() {
const { width } = useWindowDimensions();
const isTablet = width >= TABLET_MIN_WIDTH;
const { colors } = useThemeContext();
const { t } = useTranslation();
// Use safe area insets if available, otherwise use standard Android nav bar height
const bottomPadding = insets.bottom > 0 ? insets.bottom : ANDROID_NAV_BAR_HEIGHT;
@@ -62,7 +64,7 @@ export default function TabLayout() {
<Tabs.Screen
name="index"
options={{
title: 'Inicio',
title: t('nav.home'),
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
@@ -71,7 +73,7 @@ export default function TabLayout() {
<Tabs.Screen
name="search"
options={{
title: 'Buscar',
title: t('nav.search'),
tabBarIcon: ({ color, size }) => (
<Ionicons name="search" size={size} color={color} />
),
@@ -80,7 +82,7 @@ export default function TabLayout() {
<Tabs.Screen
name="scan"
options={{
title: 'Escanear',
title: t('nav.scan'),
tabBarIcon: ({ color, size }) => <ScanIcon color={color} size={size} />,
tabBarLabel: () => null,
}}
@@ -88,7 +90,7 @@ export default function TabLayout() {
<Tabs.Screen
name="alerts"
options={{
title: 'Avisos',
title: t('nav.alerts'),
tabBarIcon: ({ color, size }) => (
<Ionicons name="notifications" size={size} color={color} />
),
@@ -103,7 +105,7 @@ export default function TabLayout() {
<Tabs.Screen
name="profile"
options={{
title: 'Perfil',
title: t('nav.profile'),
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
+19 -17
View File
@@ -6,6 +6,7 @@ import { useThemeContext } from '../../components/ThemeProvider';
import { useAuth } from '../../hooks/useAuth';
import { spacing, borderRadius, shadows } from '../../constants/theme';
import { LoadingSpinner } from '../../components/LoadingSpinner';
import { useTranslation } from '../../src/i18n';
import api from '../../services/api';
const TABLET_MIN_WIDTH = 768;
@@ -22,6 +23,7 @@ interface NotificationItem {
}
export default function AlertsScreen() {
const { t } = useTranslation();
const router = useRouter();
const { width } = useWindowDimensions();
const isTablet = width >= TABLET_MIN_WIDTH;
@@ -54,7 +56,7 @@ export default function AlertsScreen() {
);
setItems(merged);
} catch (err: any) {
setError(err.message || 'No se pudieron cargar las notificaciones');
setError(err.message || t('alerts.loadError'));
} finally {
setIsLoading(false);
}
@@ -63,12 +65,12 @@ export default function AlertsScreen() {
async function handleDelete(item: NotificationItem) {
const key = `${item.scope}:${item.id}`;
Alert.alert(
'Eliminar notificación',
`¿Eliminar la notificación de ${item.medicine_name || item.medicine_nregistro}?`,
t('alerts.delete'),
`${t('alerts.deleteConfirm')} ${item.medicine_name || item.medicine_nregistro}?`,
[
{ text: 'Cancelar', style: 'cancel' },
{ text: t('alerts.cancel'), style: 'cancel' },
{
text: 'Eliminar',
text: t('alerts.confirmDelete'),
style: 'destructive',
onPress: async () => {
setDeletingId(key);
@@ -78,7 +80,7 @@ export default function AlertsScreen() {
});
setItems(prev => prev.filter(i => !(i.scope === item.scope && i.id === item.id)));
} catch (err: any) {
Alert.alert('Error', err.message || 'No se pudo eliminar');
Alert.alert('Error', err.message || t('alerts.deleteError'));
} finally {
setDeletingId(null);
}
@@ -105,8 +107,8 @@ export default function AlertsScreen() {
/>
<Text style={[styles.chipText, { color: colors.onPrimaryContainer }]}>
{item.scope === 'pharmacy'
? item.pharmacy_name || `Farmacia #${item.pharmacy_id}`
: 'Cualquier farmacia'}
? item.pharmacy_name || `${t('alerts.pharmacy')} #${item.pharmacy_id}`
: t('alerts.anyPharmacy')}
</Text>
</View>
{item.pharmacy_address && (
@@ -132,22 +134,22 @@ export default function AlertsScreen() {
}
if (isLoading || authLoading) {
return <LoadingSpinner message="Cargando notificaciones..." />;
return <LoadingSpinner message={t('alerts.loading')} />;
}
if (!isAuthenticated) {
return (
<View style={[styles.container, styles.centered, { backgroundColor: colors.background }]}>
<Ionicons name="lock-closed-outline" size={64} color={colors.border} />
<Text style={[styles.loginTitle, { color: colors.text }]}>Inicia sesión para continuar</Text>
<Text style={[styles.loginTitle, { color: colors.text }]}>{t('alerts.loginRequired')}</Text>
<Text style={[styles.loginSubtitle, { color: colors.textSecondary }]}>
Necesitas estar autenticado para ver tus notificaciones
{t('alerts.loginDescription')}
</Text>
<TouchableOpacity
style={[styles.loginButton, { backgroundColor: colors.primary }]}
onPress={() => router.push('/auth/login')}
>
<Text style={[styles.loginButtonText, { color: colors.onPrimaryContainer }]}>Iniciar Sesión</Text>
<Text style={[styles.loginButtonText, { color: colors.onPrimaryContainer }]}>{t('alerts.loginBtn')}</Text>
</TouchableOpacity>
</View>
);
@@ -156,9 +158,9 @@ export default function AlertsScreen() {
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View style={[styles.header, isTablet && styles.headerTablet]}>
<Text style={[styles.title, isTablet && styles.titleTablet, { color: colors.text }]}>Notificaciones Guardadas</Text>
<Text style={[styles.title, isTablet && styles.titleTablet, { color: colors.text }]}>{t('alerts.title')}</Text>
<Text style={[styles.subtitle, isTablet && styles.subtitleTablet, { color: colors.textSecondary }]}>
Recibe avisos cuando medicamentos sin stock se repongan
{t('alerts.description')}
</Text>
</View>
@@ -166,7 +168,7 @@ export default function AlertsScreen() {
<View style={[styles.errorContainer, isTablet && styles.errorContainerTablet, { backgroundColor: colors.dangerContainer }]}>
<Text style={[styles.errorText, { color: colors.danger }]}>{error}</Text>
<TouchableOpacity onPress={loadNotifications} style={styles.retryButton}>
<Text style={[styles.retryText, { color: colors.primary }]}>Reintentar</Text>
<Text style={[styles.retryText, { color: colors.primary }]}>{t('alerts.retry')}</Text>
</TouchableOpacity>
</View>
)}
@@ -174,9 +176,9 @@ export default function AlertsScreen() {
{!error && items.length === 0 && (
<View style={styles.emptyContainer}>
<Ionicons name="notifications-off-outline" size={isTablet ? 80 : 64} color={colors.border} />
<Text style={[styles.emptyTitle, isTablet && styles.emptyTitleTablet, { color: colors.text }]}>Sin notificaciones</Text>
<Text style={[styles.emptyTitle, isTablet && styles.emptyTitleTablet, { color: colors.text }]}>{t('alerts.empty')}</Text>
<Text style={[styles.emptyText, isTablet && styles.emptyTextTablet, { color: colors.textSecondary }]}>
Toca la campana en una farmacia sin stock para recibir notificaciones cuando se reponga.
{t('alerts.emptyDescription')}
</Text>
</View>
)}
+5 -3
View File
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet, TouchableOpacity, Image, useWindowDimensions }
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useThemeContext } from '../../components/ThemeProvider';
import { useTranslation } from '../../src/i18n';
import { spacing, borderRadius, shadows } from '../../constants/theme';
const TABLET_MIN_WIDTH = 768;
@@ -12,6 +13,7 @@ export default function HomeScreen() {
const { width } = useWindowDimensions();
const isTablet = width >= TABLET_MIN_WIDTH;
const { colors } = useThemeContext();
const { t } = useTranslation();
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
@@ -23,7 +25,7 @@ export default function HomeScreen() {
/>
<Text style={[styles.brandName, isTablet && styles.brandNameTablet, { color: colors.text }]}>FarmaClic</Text>
<Text style={[styles.description, isTablet && styles.descriptionTablet, { color: colors.textSecondary }]}>
Encuentra tus medicamentos en farmacias cercanas
{t('home.description')}
</Text>
</View>
@@ -37,7 +39,7 @@ export default function HomeScreen() {
<Ionicons name="search" size={isTablet ? 28 : 24} color={colors.onPrimaryContainer} />
</View>
<View style={styles.cardContent}>
<Text style={[styles.cardLabel, isTablet && styles.cardLabelTablet, { color: colors.onPrimaryContainer }]}>Buscar Medicamento</Text>
<Text style={[styles.cardLabel, isTablet && styles.cardLabelTablet, { color: colors.onPrimaryContainer }]}>{t('home.searchMedicine')}</Text>
<Ionicons name="chevron-forward" size={20} color={colors.onPrimaryContainer} style={{ opacity: 0.7 }} />
</View>
</TouchableOpacity>
@@ -51,7 +53,7 @@ export default function HomeScreen() {
<Ionicons name="scan" size={isTablet ? 28 : 24} color="#ffffff" />
</View>
<View style={styles.cardContent}>
<Text style={[styles.cardLabel, styles.cardLabelScan, isTablet && styles.cardLabelTablet]}>Escanear TSI</Text>
<Text style={[styles.cardLabel, styles.cardLabelScan, isTablet && styles.cardLabelTablet]}>{t('home.scanTSI')}</Text>
<Ionicons name="chevron-forward" size={20} color="#ffffff" style={{ opacity: 0.7 }} />
</View>
</TouchableOpacity>
+81 -56
View File
@@ -6,6 +6,7 @@ import * as ImagePicker from 'expo-image-picker';
import { useAuth } from '../../hooks/useAuth';
import { useThemeContext } from '../../components/ThemeProvider';
import { useThemeStore, ThemeMode } from '../../store/themeStore';
import { useTranslation } from '../../src/i18n';
import { spacing, borderRadius, shadows } from '../../constants/theme';
import api from '../../services/api';
@@ -62,10 +63,10 @@ interface Address {
created_at: string;
}
const THEME_OPTIONS: { mode: ThemeMode; label: string; icon: string }[] = [
{ mode: 'system', label: 'Sistema', icon: 'phone-portrait-outline' },
{ mode: 'light', label: 'Claro', icon: 'sunny-outline' },
{ mode: 'dark', label: 'Oscuro', icon: 'moon-outline' },
const THEME_OPTIONS: { mode: ThemeMode; labelKey: string; icon: string }[] = [
{ mode: 'system', labelKey: 'profile.themeSystem', icon: 'phone-portrait-outline' },
{ mode: 'light', labelKey: 'profile.themeLight', icon: 'sunny-outline' },
{ mode: 'dark', labelKey: 'profile.themeDark', icon: 'moon-outline' },
];
export default function ProfileScreen() {
@@ -74,6 +75,7 @@ export default function ProfileScreen() {
const isTablet = width >= TABLET_MIN_WIDTH;
const { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth();
const { colors, isDark } = useThemeContext();
const { t, lang, setLang } = useTranslation();
const themeMode = useThemeStore((s) => s.mode);
const setThemeMode = useThemeStore((s) => s.setMode);
@@ -146,10 +148,10 @@ export default function ProfileScreen() {
});
setFirstName(res.data.first_name || '');
setLastName(res.data.last_name || '');
setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' });
setConfigFeedback({ type: 'ok', text: t('profile.profileSaved') });
setTimeout(() => setShowConfig(false), 1200);
} catch (err: any) {
setConfigFeedback({ type: 'err', text: err.message || 'Error al guardar' });
setConfigFeedback({ type: 'err', text: err.message || t('profile.saveError') });
} finally { setConfigSaving(false); }
}
@@ -164,7 +166,7 @@ export default function ProfileScreen() {
async function handleTakePhoto() {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') { Alert.alert('Permiso requerido', 'Necesitamos permiso para acceder a la cámara.'); return; }
if (status !== 'granted') { Alert.alert(t('profile.cameraPermission'), t('profile.cameraPermissionDesc')); return; }
const result = await ImagePicker.launchCameraAsync({ allowsEditing: true, aspect: [1, 1], quality: 0.8, base64: true });
if (!result.canceled && result.assets[0]?.base64) {
const dataUri = `data:${result.assets[0].mimeType};base64,${result.assets[0].base64}`;
@@ -204,12 +206,12 @@ export default function ProfileScreen() {
async function handleAddressSave() {
const addr = formAddress.trim();
if (!addr) { setFormError('La dirección es obligatoria'); return; }
if (!addr) { setFormError(t('profile.addressRequired')); return; }
setFormSaving(true); setFormError('');
try {
const url = editingAddressId ? `/api/addresses/${editingAddressId}` : '/api/addresses';
const res = await fetch(url, { method: editingAddressId ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ address: addr, label: formLabel.trim(), is_default: formDefault }) });
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || 'Error al guardar'); }
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || t('profile.saveError')); }
setShowAddressForm(false); setEditingAddressId(null); loadAddresses();
} catch (err: any) { setFormError(err.message); } finally { setFormSaving(false); }
}
@@ -223,9 +225,9 @@ export default function ProfileScreen() {
}
const handleLogout = () => {
Alert.alert('Cerrar Sesión', '¿Estás seguro que deseas cerrar sesión?', [
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Cerrar Sesión', style: 'destructive', onPress: async () => { await logout(); router.replace('/auth/login'); } },
Alert.alert(t('profile.logoutTitle'), t('profile.logoutConfirm'), [
{ text: t('profile.logoutCancel'), style: 'cancel' },
{ text: t('profile.logoutConfirmBtn'), style: 'destructive', onPress: async () => { await logout(); router.replace('/auth/login'); } },
]);
};
@@ -246,12 +248,12 @@ export default function ProfileScreen() {
<View style={[styles.authIconCircle, { backgroundColor: colors.primaryContainer }]}>
<Ionicons name="person-outline" size={isTablet ? 60 : 48} color={colors.primary} />
</View>
<Text style={[styles.authTitle, isTablet && styles.authTitleTablet, { color: colors.text }]}>Inicia Sesión</Text>
<Text style={[styles.authTitle, isTablet && styles.authTitleTablet, { color: colors.text }]}>{t('profile.loginTitle')}</Text>
<Text style={[styles.authSubtitle, isTablet && styles.authSubtitleTablet, { color: colors.textSecondary }]}>
Inicia sesión para acceder a tu perfil, notificaciones y más
{t('profile.loginDescription')}
</Text>
<TouchableOpacity style={[styles.authButton, { backgroundColor: colors.primary }]} onPress={() => router.push('/auth/login')}>
<Text style={[styles.authButtonText, { color: colors.onPrimaryContainer }]}>Iniciar Sesión</Text>
<Text style={[styles.authButtonText, { color: colors.onPrimaryContainer }]}>{t('profile.loginBtn')}</Text>
</TouchableOpacity>
</View>
</View>
@@ -287,15 +289,15 @@ export default function ProfileScreen() {
<View style={[styles.card, { backgroundColor: colors.card }, shadows.card]}>
<View style={styles.cardHeader}>
<Ionicons name="person-outline" size={18} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text }]}>Datos personales</Text>
<Text style={[styles.cardTitle, { color: colors.text }]}>{t('profile.personalData')}</Text>
</View>
<View style={styles.infoGrid}>
<View style={[styles.infoBox, { backgroundColor: colors.surfaceLow }]}>
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>Nombre</Text>
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>{t('profile.firstName')}</Text>
<Text style={[styles.infoValue, { color: colors.text }]}>{firstName || '—'}</Text>
</View>
<View style={[styles.infoBox, { backgroundColor: colors.surfaceLow }]}>
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>Apellidos</Text>
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>{t('profile.lastName')}</Text>
<Text style={[styles.infoValue, { color: colors.text }]}>{lastName || '—'}</Text>
</View>
</View>
@@ -306,7 +308,7 @@ export default function ProfileScreen() {
<View style={[styles.card, { backgroundColor: colors.card }, shadows.card]}>
<View style={styles.cardHeader}>
<Ionicons name={getThemeModeIcon()} size={18} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text }]}>Apariencia</Text>
<Text style={[styles.cardTitle, { color: colors.text }]}>{t('profile.appearance')}</Text>
</View>
<View style={[styles.themePills, { backgroundColor: colors.surfaceLow }]}>
{THEME_OPTIONS.map((opt) => (
@@ -316,7 +318,30 @@ export default function ProfileScreen() {
onPress={() => setThemeMode(opt.mode)}
>
<Ionicons name={opt.icon as any} size={16} color={themeMode === opt.mode ? colors.onPrimaryContainer : colors.textSecondary} />
<Text style={[styles.themePillText, { color: themeMode === opt.mode ? colors.onPrimaryContainer : colors.textSecondary }]}>{opt.label}</Text>
<Text style={[styles.themePillText, { color: themeMode === opt.mode ? colors.onPrimaryContainer : colors.textSecondary }]}>{t(opt.labelKey)}</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Language card */}
<View style={[styles.card, { backgroundColor: colors.card }, shadows.card]}>
<View style={styles.cardHeader}>
<Ionicons name="globe-outline" size={18} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text }]}>{t('profile.language')}</Text>
</View>
<View style={[styles.themePills, { backgroundColor: colors.surfaceLow }]}>
{[
{ value: 'ca' as const, labelKey: 'profile.languageCatalan', icon: 'language-outline' },
{ value: 'es' as const, labelKey: 'profile.languageSpanish', icon: 'language-outline' },
].map((opt) => (
<TouchableOpacity
key={opt.value}
style={[styles.themePill, lang === opt.value && { backgroundColor: colors.primary }]}
onPress={() => setLang(opt.value)}
>
<Ionicons name={opt.icon as any} size={16} color={lang === opt.value ? colors.onPrimaryContainer : colors.textSecondary} />
<Text style={[styles.themePillText, { color: lang === opt.value ? colors.onPrimaryContainer : colors.textSecondary }]}>{t(opt.labelKey)}</Text>
</TouchableOpacity>
))}
</View>
@@ -328,7 +353,7 @@ export default function ProfileScreen() {
<View style={[styles.menuIconCircle, { backgroundColor: colors.surfaceLow }]}>
<Ionicons name="settings-outline" size={20} color={colors.primary} />
</View>
<Text style={[styles.menuLabel, { color: colors.text }]}>Configuración</Text>
<Text style={[styles.menuLabel, { color: colors.text }]}>{t('profile.config')}</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</TouchableOpacity>
@@ -338,7 +363,7 @@ export default function ProfileScreen() {
<View style={[styles.menuIconCircle, { backgroundColor: colors.surfaceLow }]}>
<Ionicons name="location-outline" size={20} color={colors.primary} />
</View>
<Text style={[styles.menuLabel, { color: colors.text }]}>Mis Direcciones</Text>
<Text style={[styles.menuLabel, { color: colors.text }]}>{t('profile.myAddresses')}</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</TouchableOpacity>
@@ -349,7 +374,7 @@ export default function ProfileScreen() {
<View style={[styles.menuIconCircle, { backgroundColor: colors.surfaceLow }]}>
<Ionicons name="shield-outline" size={20} color={colors.primary} />
</View>
<Text style={[styles.menuLabel, { color: colors.text }]}>Panel Admin</Text>
<Text style={[styles.menuLabel, { color: colors.text }]}>{t('profile.admin')}</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</TouchableOpacity>
</>
@@ -361,7 +386,7 @@ export default function ProfileScreen() {
<View style={[styles.card, { backgroundColor: colors.card }, shadows.card]}>
<View style={styles.cardHeader}>
<Ionicons name="time-outline" size={18} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text }]}>Búsquedas recientes</Text>
<Text style={[styles.cardTitle, { color: colors.text }]}>{t('profile.recentSearches')}</Text>
</View>
{searchHistory.map((item, i) => (
<React.Fragment key={item.id}>
@@ -381,7 +406,7 @@ export default function ProfileScreen() {
{/* Logout */}
<TouchableOpacity style={[styles.logoutCard, { backgroundColor: colors.card, borderColor: isDark ? '#5a2020' : '#fecaca' }]} onPress={handleLogout}>
<Ionicons name="log-out-outline" size={20} color={colors.danger} />
<Text style={[styles.logoutText, { color: colors.danger }]}>Cerrar Sesión</Text>
<Text style={[styles.logoutText, { color: colors.danger }]}>{t('profile.logout')}</Text>
</TouchableOpacity>
<View style={{ height: spacing.xl }} />
@@ -394,16 +419,16 @@ export default function ProfileScreen() {
<View style={[styles.modalSheet, { backgroundColor: colors.card }]}>
<View style={[styles.modalHandle, { backgroundColor: colors.border }]} />
<View style={[styles.modalHeader, { borderBottomColor: colors.surfaceLow }]}>
<Text style={[styles.modalTitle, { color: colors.text }]}>Cambiar Avatar</Text>
<Text style={[styles.modalTitle, { color: colors.text }]}>{t('profile.changeAvatar')}</Text>
<TouchableOpacity onPress={() => setShowAvatarModal(false)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<Ionicons name="close" size={24} color={colors.textSecondary} />
</TouchableOpacity>
</View>
<View style={[styles.avatarTabBar, { borderBottomColor: colors.surfaceLow }]}>
{(['presets', 'colors', 'upload'] as const).map((t) => (
<TouchableOpacity key={t} style={[styles.avatarTabBtn, avatarTab === t && { borderBottomColor: colors.primary }]} onPress={() => setAvatarTab(t)}>
<Ionicons name={t === 'presets' ? 'person-outline' : t === 'colors' ? 'color-palette-outline' : 'cloud-upload-outline'} size={18} color={avatarTab === t ? colors.primary : colors.textSecondary} />
<Text style={[styles.avatarTabLabel, { color: avatarTab === t ? colors.primary : colors.textSecondary }]}>{t === 'presets' ? 'Prediseñado' : t === 'colors' ? 'Colores' : 'Subir'}</Text>
{(['presets', 'colors', 'upload'] as const).map((tab) => (
<TouchableOpacity key={tab} style={[styles.avatarTabBtn, avatarTab === tab && { borderBottomColor: colors.primary }]} onPress={() => setAvatarTab(tab)}>
<Ionicons name={tab === 'presets' ? 'person-outline' : tab === 'colors' ? 'color-palette-outline' : 'cloud-upload-outline'} size={18} color={avatarTab === tab ? colors.primary : colors.textSecondary} />
<Text style={[styles.avatarTabLabel, { color: avatarTab === tab ? colors.primary : colors.textSecondary }]}>{tab === 'presets' ? t('profile.presetAvatar') : tab === 'colors' ? t('profile.colors') : t('profile.upload')}</Text>
</TouchableOpacity>
))}
</View>
@@ -433,8 +458,8 @@ export default function ProfileScreen() {
<Ionicons name="camera-outline" size={28} color={colors.primary} />
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.uploadTitle, { color: colors.text }]}>Tomar foto</Text>
<Text style={[styles.uploadSub, { color: colors.textSecondary }]}>Usa la cámara de tu dispositivo</Text>
<Text style={[styles.uploadTitle, { color: colors.text }]}>{t('profile.takePhoto')}</Text>
<Text style={[styles.uploadSub, { color: colors.textSecondary }]}>{t('profile.takePhotoDesc')}</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</TouchableOpacity>
@@ -443,8 +468,8 @@ export default function ProfileScreen() {
<Ionicons name="images-outline" size={28} color={colors.primary} />
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.uploadTitle, { color: colors.text }]}>Elegir de galería</Text>
<Text style={[styles.uploadSub, { color: colors.textSecondary }]}>Selecciona una imagen existente</Text>
<Text style={[styles.uploadTitle, { color: colors.text }]}>{t('profile.chooseGallery')}</Text>
<Text style={[styles.uploadSub, { color: colors.textSecondary }]}>{t('profile.chooseGalleryDesc')}</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</TouchableOpacity>
@@ -461,28 +486,28 @@ export default function ProfileScreen() {
<View style={[styles.modalSheet, { backgroundColor: colors.card }]}>
<View style={[styles.modalHandle, { backgroundColor: colors.border }]} />
<View style={[styles.modalHeader, { borderBottomColor: colors.surfaceLow }]}>
<Text style={[styles.modalTitle, { color: colors.text }]}>Configuración</Text>
<Text style={[styles.modalTitle, { color: colors.text }]}>{t('profile.configTitle')}</Text>
<TouchableOpacity onPress={() => setShowConfig(false)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<Ionicons name="close" size={24} color={colors.textSecondary} />
</TouchableOpacity>
</View>
<ScrollView style={styles.modalBody} contentContainerStyle={{ paddingBottom: spacing.xl }}>
{[
{ label: 'Nombre', value: configFirstName, onChange: setConfigFirstName, placeholder: 'Tu nombre', icon: 'person-outline' },
{ label: 'Apellidos', value: configLastName, onChange: setConfigLastName, placeholder: 'Tus apellidos', icon: 'person-outline' },
{ label: 'Correo electrónico', value: configEmail, onChange: setConfigEmail, placeholder: 'tu@email.com', icon: 'mail-outline', keyboard: 'email-address' as const },
{ label: 'Ciudad', value: configCity, onChange: setConfigCity, placeholder: 'Tu ciudad', icon: 'business-outline' },
{ label: 'Dirección', value: configAddress, onChange: setConfigAddress, placeholder: 'Calle Mayor 1, Madrid', icon: 'location-outline' },
{ labelKey: 'profile.firstNameLabel', placeholderKey: 'profile.firstNamePlaceholder', value: configFirstName, onChange: setConfigFirstName, icon: 'person-outline' },
{ labelKey: 'profile.lastNameLabel', placeholderKey: 'profile.lastNamePlaceholder', value: configLastName, onChange: setConfigLastName, icon: 'person-outline' },
{ labelKey: 'profile.email', placeholder: 'tu@email.com', value: configEmail, onChange: setConfigEmail, icon: 'mail-outline', keyboard: 'email-address' as const },
{ labelKey: 'profile.city', placeholderKey: 'profile.cityPlaceholder', value: configCity, onChange: setConfigCity, icon: 'business-outline' },
{ labelKey: 'profile.address', placeholderKey: 'profile.addressPlaceholder', value: configAddress, onChange: setConfigAddress, icon: 'location-outline' },
].map((field) => (
<View key={field.label} style={styles.modalField}>
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>{field.label}</Text>
<View key={field.labelKey} style={styles.modalField}>
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>{t(field.labelKey)}</Text>
<View style={[styles.modalInputRow, { backgroundColor: colors.surfaceLow, borderColor: colors.border }]}>
<Ionicons name={field.icon as any} size={18} color={colors.textSecondary} style={{ marginRight: spacing.sm }} />
<TextInput
style={[styles.modalInput, { color: colors.text }]}
value={field.value}
onChangeText={field.onChange}
placeholder={field.placeholder}
placeholder={field.placeholderKey ? t(field.placeholderKey) : field.placeholder}
placeholderTextColor={colors.textSecondary}
keyboardType={field.keyboard}
autoCapitalize="none"
@@ -501,10 +526,10 @@ export default function ProfileScreen() {
<View style={styles.modalActions}>
<TouchableOpacity style={[styles.modalCancelBtn, { borderColor: colors.border }]} onPress={() => setShowConfig(false)} disabled={configSaving}>
<Text style={[styles.modalCancelText, { color: colors.text }]}>Cancelar</Text>
<Text style={[styles.modalCancelText, { color: colors.text }]}>{t('profile.cancel')}</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.modalSaveBtn, { backgroundColor: colors.primary }, configSaving && { opacity: 0.6 }]} onPress={handleConfigSave} disabled={configSaving}>
{configSaving ? <ActivityIndicator color={colors.onPrimaryContainer} /> : <Text style={[styles.modalSaveText, { color: colors.onPrimaryContainer }]}>Guardar</Text>}
{configSaving ? <ActivityIndicator color={colors.onPrimaryContainer} /> : <Text style={[styles.modalSaveText, { color: colors.onPrimaryContainer }]}>{t('profile.save')}</Text>}
</TouchableOpacity>
</View>
</ScrollView>
@@ -518,7 +543,7 @@ export default function ProfileScreen() {
<View style={[styles.modalSheet, { backgroundColor: colors.card }]}>
<View style={[styles.modalHandle, { backgroundColor: colors.border }]} />
<View style={[styles.modalHeader, { borderBottomColor: colors.surfaceLow }]}>
<Text style={[styles.modalTitle, { color: colors.text }]}>Mis Direcciones</Text>
<Text style={[styles.modalTitle, { color: colors.text }]}>{t('profile.addressesTitle')}</Text>
<TouchableOpacity onPress={() => { setShowAddresses(false); setShowAddressForm(false); }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<Ionicons name="close" size={24} color={colors.textSecondary} />
</TouchableOpacity>
@@ -527,22 +552,22 @@ export default function ProfileScreen() {
{showAddressForm ? (
<View>
<View style={styles.modalField}>
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>Dirección</Text>
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>{t('profile.addressLabel')}</Text>
<View style={[styles.modalInputRow, { backgroundColor: colors.surfaceLow, borderColor: colors.border }]}>
<Ionicons name="location-outline" size={18} color={colors.textSecondary} style={{ marginRight: spacing.sm }} />
<TextInput style={[styles.modalInput, { color: colors.text }]} value={formAddress} onChangeText={setFormAddress} placeholder="Calle Mayor 1, Madrid" placeholderTextColor={colors.textSecondary} editable={!formSaving} />
<TextInput style={[styles.modalInput, { color: colors.text }]} value={formAddress} onChangeText={setFormAddress} placeholder={t('profile.addressPlaceholder')} placeholderTextColor={colors.textSecondary} editable={!formSaving} />
</View>
</View>
<View style={styles.modalField}>
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>Etiqueta (opcional)</Text>
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>{t('profile.addressOptional')}</Text>
<View style={[styles.modalInputRow, { backgroundColor: colors.surfaceLow, borderColor: colors.border }]}>
<Ionicons name="pricetag-outline" size={18} color={colors.textSecondary} style={{ marginRight: spacing.sm }} />
<TextInput style={[styles.modalInput, { color: colors.text }]} value={formLabel} onChangeText={setFormLabel} placeholder="Casa, Trabajo..." placeholderTextColor={colors.textSecondary} editable={!formSaving} />
<TextInput style={[styles.modalInput, { color: colors.text }]} value={formLabel} onChangeText={setFormLabel} placeholder={t('profile.addressLabelPlaceholder')} placeholderTextColor={colors.textSecondary} editable={!formSaving} />
</View>
</View>
<TouchableOpacity style={styles.checkboxRow} onPress={() => setFormDefault(!formDefault)} disabled={formSaving}>
<Ionicons name={formDefault ? 'checkbox' : 'square-outline'} size={22} color={formDefault ? colors.primary : colors.textSecondary} />
<Text style={[styles.checkboxLabel, { color: colors.text }]}>Dirección predeterminada</Text>
<Text style={[styles.checkboxLabel, { color: colors.text }]}>{t('profile.defaultAddress')}</Text>
</TouchableOpacity>
{formError ? (
<View style={[styles.modalFeedback, { backgroundColor: colors.dangerContainer, borderColor: isDark ? '#5a2020' : '#fecaca' }]}>
@@ -551,10 +576,10 @@ export default function ProfileScreen() {
) : null}
<View style={styles.modalActions}>
<TouchableOpacity style={[styles.modalCancelBtn, { borderColor: colors.border }]} onPress={() => { setShowAddressForm(false); setFormError(''); }} disabled={formSaving}>
<Text style={[styles.modalCancelText, { color: colors.text }]}>Cancelar</Text>
<Text style={[styles.modalCancelText, { color: colors.text }]}>{t('profile.cancel')}</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.modalSaveBtn, { backgroundColor: colors.primary }, formSaving && { opacity: 0.6 }]} onPress={handleAddressSave} disabled={formSaving}>
{formSaving ? <ActivityIndicator color={colors.onPrimaryContainer} /> : <Text style={[styles.modalSaveText, { color: colors.onPrimaryContainer }]}>{editingAddressId ? 'Actualizar' : 'Añadir'}</Text>}
{formSaving ? <ActivityIndicator color={colors.onPrimaryContainer} /> : <Text style={[styles.modalSaveText, { color: colors.onPrimaryContainer }]}>{editingAddressId ? t('profile.update') : t('profile.add')}</Text>}
</TouchableOpacity>
</View>
</View>
@@ -567,7 +592,7 @@ export default function ProfileScreen() {
{user?.address && (
<View style={[styles.addrCard, { borderColor: colors.primary, backgroundColor: isDark ? '#1a3a1c' : '#eaf7ec' }]}>
<View style={{ flex: 1 }}>
<Text style={[styles.addrBadge, { color: colors.primary }]}>Principal</Text>
<Text style={[styles.addrBadge, { color: colors.primary }]}>{t('profile.mainAddress')}</Text>
<Text style={[styles.addrText, { color: colors.text }]}>{user.address}</Text>
</View>
<Ionicons name="checkmark-circle" size={20} color={colors.primary} />
@@ -580,7 +605,7 @@ export default function ProfileScreen() {
<Text style={[styles.addrText, { color: colors.text }]}>{addr.address}</Text>
{!addr.is_default && (
<TouchableOpacity onPress={() => handleSetDefault(addr.id)}>
<Text style={[styles.addrDefaultLink, { color: colors.primary }]}>Marcar como predeterminada</Text>
<Text style={[styles.addrDefaultLink, { color: colors.primary }]}>{t('profile.setDefault')}</Text>
</TouchableOpacity>
)}
</View>
@@ -596,7 +621,7 @@ export default function ProfileScreen() {
))}
<TouchableOpacity style={[styles.addAddrBtn, { borderColor: colors.border }]} onPress={openAddAddressForm}>
<Ionicons name="add-circle-outline" size={20} color={colors.primary} />
<Text style={[styles.addAddrText, { color: colors.primary }]}>Añadir dirección</Text>
<Text style={[styles.addAddrText, { color: colors.primary }]}>{t('profile.addAddress')}</Text>
</TouchableOpacity>
</View>
)}
+19 -17
View File
@@ -4,6 +4,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import * as ImagePicker from 'expo-image-picker';
import { useThemeContext } from '../../components/ThemeProvider';
import { useTranslation } from '../../src/i18n';
import { spacing, borderRadius, shadows } from '../../constants/theme';
const TABLET_MIN_WIDTH = 768;
@@ -13,6 +14,7 @@ export default function ScanTabScreen() {
const { width } = useWindowDimensions();
const isTablet = width >= TABLET_MIN_WIDTH;
const { colors } = useThemeContext();
const { t } = useTranslation();
const [manualNumber, setManualNumber] = useState('');
const [selectedImage, setSelectedImage] = useState<string | null>(null);
@@ -20,8 +22,8 @@ export default function ScanTabScreen() {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
Alert.alert(
'Permiso requerido',
'Necesitamos acceso a la cámara para tomar fotos del dispositivo.'
t('scanner.cameraPermission'),
t('scanner.cameraPermissionDesc')
);
return;
}
@@ -41,8 +43,8 @@ export default function ScanTabScreen() {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert(
'Permiso requerido',
'Necesitamos acceso a la galería para seleccionar fotos.'
t('scanner.galleryPermission'),
t('scanner.galleryPermissionDesc')
);
return;
}
@@ -61,7 +63,7 @@ export default function ScanTabScreen() {
const handleManualSubmit = () => {
const trimmed = manualNumber.trim();
if (trimmed.length === 0) {
Alert.alert('Campo requerido', 'Por favor, introduce el número de la tarjeta.');
Alert.alert(t('scanner.fieldRequired'), t('scanner.fieldRequiredDesc'));
return;
}
router.push(`/medicine/${trimmed}`);
@@ -76,9 +78,9 @@ export default function ScanTabScreen() {
<View style={[styles.iconContainer, isTablet && styles.iconContainerTablet, { backgroundColor: colors.primaryContainer }]}>
<Ionicons name="scan" size={isTablet ? 80 : 64} color={colors.scanButton} />
</View>
<Text style={[styles.title, isTablet && styles.titleTablet, { color: colors.text }]}>Escanear TSI</Text>
<Text style={[styles.title, isTablet && styles.titleTablet, { color: colors.text }]}>{t('scanner.title')}</Text>
<Text style={[styles.description, isTablet && styles.descriptionTablet, { color: colors.textSecondary }]}>
Escanea el código de barras de tu tarjeta sanitaria para encontrar tus medicamentos
{t('scanner.description')}
</Text>
<TouchableOpacity
style={[styles.primaryButton, shadows.scanButton, isTablet && styles.primaryButtonTablet]}
@@ -87,14 +89,14 @@ export default function ScanTabScreen() {
>
<Ionicons name="scan" size={isTablet ? 28 : 24} color="#ffffff" />
<Text style={[styles.primaryButtonText, isTablet && styles.primaryButtonTextTablet]}>
Iniciar escaneo
{t('scanner.startScan')}
</Text>
</TouchableOpacity>
{/* Separator */}
<View style={styles.separator}>
<View style={[styles.separatorLine, { backgroundColor: colors.border }]} />
<Text style={[styles.separatorText, { color: colors.textSecondary }]}>o</Text>
<Text style={[styles.separatorText, { color: colors.textSecondary }]}>{t('scanner.or')}</Text>
<View style={[styles.separatorLine, { backgroundColor: colors.border }]} />
</View>
@@ -103,10 +105,10 @@ export default function ScanTabScreen() {
<Ionicons name="camera" size={24} color={colors.primary} />
<View style={styles.optionTextContainer}>
<Text style={[styles.optionTitle, isTablet && styles.optionTitleTablet, { color: colors.text }]}>
Subir foto del dispositivo
{t('scanner.uploadPhoto')}
</Text>
<Text style={[styles.optionDescription, isTablet && styles.optionDescriptionTablet, { color: colors.textSecondary }]}>
Toma o selecciona una foto del dispositivo sanitario
{t('scanner.uploadDescription')}
</Text>
</View>
</View>
@@ -117,7 +119,7 @@ export default function ScanTabScreen() {
onPress={handleTakePhoto}
>
<Ionicons name="camera-outline" size={20} color={colors.primary} />
<Text style={[styles.photoButtonText, { color: colors.primary }]}>Tomar foto</Text>
<Text style={[styles.photoButtonText, { color: colors.primary }]}>{t('scanner.takePhoto')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.photoButton, isTablet && styles.photoButtonTablet, { backgroundColor: colors.primaryContainer, borderColor: colors.primary }]}
@@ -125,13 +127,13 @@ export default function ScanTabScreen() {
onPress={handleSelectFromGallery}
>
<Ionicons name="images-outline" size={20} color={colors.primary} />
<Text style={[styles.photoButtonText, { color: colors.primary }]}>Galería</Text>
<Text style={[styles.photoButtonText, { color: colors.primary }]}>{t('scanner.gallery')}</Text>
</TouchableOpacity>
</View>
{selectedImage && (
<View style={styles.imagePreviewContainer}>
<Ionicons name="checkmark-circle" size={20} color={colors.success} />
<Text style={[styles.imagePreviewText, { color: colors.success }]}>Foto seleccionada correctamente</Text>
<Text style={[styles.imagePreviewText, { color: colors.success }]}>{t('scanner.photoSelected')}</Text>
</View>
)}
@@ -140,17 +142,17 @@ export default function ScanTabScreen() {
<Ionicons name="keypad" size={24} color={colors.primary} />
<View style={styles.optionTextContainer}>
<Text style={[styles.optionTitle, isTablet && styles.optionTitleTablet, { color: colors.text }]}>
Introducir número manualmente
{t('scanner.manualEntry')}
</Text>
<Text style={[styles.optionDescription, isTablet && styles.optionDescriptionTablet, { color: colors.textSecondary }]}>
Escribe el número de tu tarjeta sanitaria
{t('scanner.manualDescription')}
</Text>
</View>
</View>
<View style={styles.manualInputRow}>
<TextInput
style={[styles.manualInput, isTablet && styles.manualInputTablet, { backgroundColor: colors.card, borderColor: colors.border, color: colors.text }]}
placeholder="Introduce el número de tarjeta"
placeholder={t('scanner.manualPlaceholder')}
placeholderTextColor={colors.textSecondary}
value={manualNumber}
onChangeText={setManualNumber}
+10 -8
View File
@@ -14,6 +14,7 @@ import { useThemeContext } from '../../components/ThemeProvider';
import { spacing, borderRadius } from '../../constants/theme';
import { Medicine } from '../../types';
import { config } from '../../constants/config';
import { useTranslation } from '../../src/i18n';
const TABLET_MIN_WIDTH = 768;
@@ -25,6 +26,7 @@ const suggestions = [
];
export default function SearchScreen() {
const { t } = useTranslation();
const { width } = useWindowDimensions();
const isTablet = width >= TABLET_MIN_WIDTH;
const { isAuthenticated } = useAuth();
@@ -68,7 +70,7 @@ export default function SearchScreen() {
const data = await searchMedicines(debouncedQuery);
setResults(data);
} catch (err) {
setError('Error al buscar medicamentos');
setError(t('search.error'));
console.error(err);
} finally {
setIsLoading(false);
@@ -106,7 +108,7 @@ export default function SearchScreen() {
{query.length >= 2 && (results.length + products.length) > 0 && (
<View style={styles.resultsSummary}>
<Text style={[styles.resultsSummaryText, { color: colors.textSecondary }]}>
{results.length + products.length} resultados encontrados
{results.length + products.length} {t('search.resultsFound')}
</Text>
</View>
)}
@@ -114,7 +116,7 @@ export default function SearchScreen() {
{showSuggestions && (
<>
<View style={[styles.suggestionsSection, isTablet && styles.suggestionsSectionTablet]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Sugerencias</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{t('search.suggestions')}</Text>
<View style={styles.suggestionsGrid}>
{suggestions.map((s) => (
<TouchableOpacity
@@ -134,7 +136,7 @@ export default function SearchScreen() {
{isAuthenticated && recentSearches.length > 0 && (
<View style={[styles.recentSection, isTablet && styles.recentSectionTablet]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Búsquedas recientes</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{t('search.recentSearches')}</Text>
{recentSearches.map((term) => (
<TouchableOpacity
key={term}
@@ -154,7 +156,7 @@ export default function SearchScreen() {
</>
)}
{isLoading && <LoadingSpinner message="Buscando..." />}
{isLoading && <LoadingSpinner message={t('search.loading')} />}
{error && (
<View style={[styles.errorContainer, isTablet && styles.errorContainerTablet, { backgroundColor: colors.dangerContainer }]}>
@@ -164,7 +166,7 @@ export default function SearchScreen() {
{!isLoading && !error && results.length === 0 && products.length === 0 && query.length >= 2 && (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>No se encontraron resultados</Text>
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>{t('search.noResults')}</Text>
</View>
)}
@@ -176,7 +178,7 @@ export default function SearchScreen() {
<>
{products.length > 0 && (
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Parafarmacia</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{t('search.parapharmacy')}</Text>
{products.map((product) => (
<TouchableOpacity
key={`${product.source}-${product.id || product._id}`}
@@ -187,7 +189,7 @@ export default function SearchScreen() {
<View style={styles.productInfo}>
<View style={styles.badges}>
<View style={[styles.sourceBadge, { backgroundColor: '#16a34a' }]}>
<Text style={styles.badgeText}>Parafarmacia</Text>
<Text style={styles.badgeText}>{t('search.parapharmacy')}</Text>
</View>
</View>
<Text style={[styles.productName, { color: colors.text }]} numberOfLines={1}>{product.name}</Text>