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
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user