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
+228
View File
@@ -0,0 +1,228 @@
const ca = {
// Tab bar
'nav.home': 'Inici',
'nav.search': 'Cercar',
'nav.scan': 'Escanejar',
'nav.alerts': 'Alertes',
'nav.profile': 'Perfil',
// Stack screens
'stack.medicine': 'Medicament',
'stack.pharmacy': 'Farmàcia',
'stack.scanner': 'Escanejar',
'stack.login': 'Iniciar Sessió',
'stack.register': 'Registrar-se',
// Home
'home.description': 'Trobeu els vostres medicaments a farmàcies properes',
'home.searchMedicine': 'Cercar Medicament',
'home.scanTSI': 'Escanejar TSI',
// Search
'search.placeholder': 'Cercar medicaments...',
'search.suggestions': 'Suggeriments',
'search.recentSearches': 'Cerques recents',
'search.loading': 'Cercant...',
'search.noResults': 'No s\'han trobat resultats',
'search.resultsFound': 'resultats trobats',
'search.parapharmacy': 'Parafarmàcia',
'search.error': 'Error en cercar medicaments',
// Login
'login.tab.login': 'Iniciar Sessió',
'login.tab.register': 'Crear Compte',
'login.title.login': 'Benvingut de nou',
'login.title.register': 'Crea el teu compte',
'login.subtitle.login': 'Inicia sessió per gestionar el teu perfil i notificacions.',
'login.subtitle.register': 'Desa la teva adreça i rep notificacions quan arribin medicaments.',
'login.username': 'Usuari',
'login.password': 'Contrasenya',
'login.confirmPassword': 'Confirmar contrasenya',
'login.hint.username': '3-32 caràcters per a l\'usuari',
'login.hint.password': 'Mínim 8 caràcters per a la contrasenya',
'login.biometric': 'Iniciar amb biometria',
'login.link.toLogin': 'Ja tens compte? Inicia sessió',
'login.link.toRegister': 'No tens compte? Registra\'t',
'login.alert.error': 'Error',
'login.alert.fillFields': 'Si us plau, completeu tots els camps',
'login.alert.passwordLength': 'La contrasenya ha de tenir almenys 8 caràcters',
'login.alert.passwordMismatch': 'Les contrasenyes no coincideixen',
'login.alert.success': 'Èxit',
'login.alert.accountCreated': 'Compte creat correctament',
'login.alert.ok': 'D\'acord',
'login.alert.createError': 'No s\'ha pogut crear el compte',
'login.alert.wrongCredentials': 'Credencials incorrectes',
'login.alert.noBiometrics': 'No hi ha credencials biomètriques guardades',
'login.alert.biometricFailed': 'Autenticació biomètrica fallida',
'login.alert.biometricError': 'Error en l\'autenticació biomètrica',
// Scanner
'scanner.title': 'Escanejar TSI',
'scanner.description': 'Escanegeu el codi de barres de la vostra targeta sanitària per trobar els vostres medicaments',
'scanner.startScan': 'Iniciar escaneig',
'scanner.or': 'o',
'scanner.uploadPhoto': 'Pujar foto del dispositiu',
'scanner.uploadDescription': 'Tireu o seleccioneu una foto del dispositiu sanitari',
'scanner.takePhoto': 'Fer foto',
'scanner.gallery': 'Galeria',
'scanner.photoSelected': 'Foto seleccionada correctament',
'scanner.manualEntry': 'Introduir número manualment',
'scanner.manualDescription': 'Escriviu el número de la vostra targeta sanitària',
'scanner.manualPlaceholder': 'Introduïu el número de targeta',
'scanner.cameraPermission': 'Permís de càmera requerit',
'scanner.cameraPermissionDesc': 'Necessitem accés a la càmera per fer fotos del dispositiu.',
'scanner.galleryPermission': 'Permís requerit',
'scanner.galleryPermissionDesc': 'Necessitem accés a la galeria per seleccionar fotos.',
'scanner.fieldRequired': 'Camp requerit',
'scanner.fieldRequiredDesc': 'Si us plau, introduïu el número de la targeta.',
// Alerts
'alerts.title': 'Notificacions Desades',
'alerts.description': 'Rebeu avisos quan medicaments sense estoc es repoixin',
'alerts.loading': 'Carregant notificacions...',
'alerts.loginRequired': 'Inicieu sessió per continuar',
'alerts.loginDescription': 'Necessiteu estar autenticat per veure les vostres notificacions',
'alerts.loginBtn': 'Iniciar Sessió',
'alerts.empty': 'Sense notificacions',
'alerts.emptyDescription': 'Toqueu la campana a una farmàcia sense estoc per rebre notificacions quan es repoixi.',
'alerts.retry': 'Tornar a provar',
'alerts.delete': 'Eliminar notificació',
'alerts.deleteConfirm': 'Eliminar la notificació de',
'alerts.cancel': 'Cancel·lar',
'alerts.confirmDelete': 'Eliminar',
'alerts.deleteError': 'No s\'ha pogut eliminar',
'alerts.loadError': 'No s\'han pogut carregar les notificacions',
'alerts.pharmacy': 'Farmàcia',
'alerts.anyPharmacy': 'Qualsevol farmàcia',
// Profile
'profile.title': 'Perfil',
'profile.loginTitle': 'Iniciar Sessió',
'profile.loginDescription': 'Inicieu sessió per accedir al vostre perfil, notificacions i més',
'profile.loginBtn': 'Iniciar Sessió',
'profile.personalData': 'Dades personals',
'profile.firstName': 'Nom',
'profile.lastName': 'Cognoms',
'profile.appearance': 'Aparença',
'profile.config': 'Configuració',
'profile.myAddresses': 'Les Meves Adreces',
'profile.admin': 'Panell Admin',
'profile.recentSearches': 'Cerques recents',
'profile.logout': 'Tancar Sessió',
'profile.logoutConfirm': 'Esteu segur que voleu tancar la sessió?',
'profile.logoutTitle': 'Tancar Sessió',
'profile.logoutCancel': 'Cancel·lar',
'profile.logoutConfirmBtn': 'Tancar Sessió',
'profile.changeAvatar': 'Canviar Avatar',
'profile.presetAvatar': 'Predissenyat',
'profile.colors': 'Colors',
'profile.upload': 'Pujar',
'profile.takePhoto': 'Fer foto',
'profile.takePhotoDesc': 'Useu la càmera del vostre dispositiu',
'profile.chooseGallery': 'Trieu de galeria',
'profile.chooseGalleryDesc': 'Seleccioneu una imatge existent',
'profile.configTitle': 'Configuració',
'profile.firstNameLabel': 'Nom',
'profile.firstNamePlaceholder': 'El vostre nom',
'profile.lastNameLabel': 'Cognoms',
'profile.lastNamePlaceholder': 'Els vostres cognoms',
'profile.email': 'Correu electrònic',
'profile.city': 'Ciutat',
'profile.cityPlaceholder': 'La vostra ciutat',
'profile.address': 'Adreça',
'profile.addressPlaceholder': 'Carrer Major 1, Barcelona',
'profile.cancel': 'Cancel·lar',
'profile.save': 'Desar',
'profile.profileSaved': 'Perfil desat.',
'profile.saveError': 'Error en desar',
'profile.addressRequired': 'L\'adreça és obligatòria',
'profile.addressesTitle': 'Les Meves Adreces',
'profile.addressLabel': 'Adreça',
'profile.addressOptional': 'Etiqueta (opcional)',
'profile.addressLabelPlaceholder': 'Casa, Treball...',
'profile.defaultAddress': 'Adreça predeterminada',
'profile.update': 'Actualitzar',
'profile.add': 'Afegir',
'profile.mainAddress': 'Principal',
'profile.setDefault': 'Establir com a predeterminada',
'profile.addAddress': 'Afegir adreça',
'profile.cameraPermission': 'Permís requerit',
'profile.cameraPermissionDesc': 'Necessitem permís per accedir a la càmera.',
'profile.themeSystem': 'Sistema',
'profile.themeLight': 'Clar',
'profile.themeDark': 'Fosc',
'profile.language': 'Idioma',
'profile.languageTitle': 'Idioma de l\'aplicació',
'profile.languageCatalan': 'Català',
'profile.languageSpanish': 'Castellà',
// Medicine detail
'medicine.loading': 'Carregant medicament...',
'medicine.notFound': 'Medicament no trobat',
'medicine.activeIngredient': 'Principi actiu',
'medicine.laboratory': 'Laboratori',
'medicine.form': 'Forma farmacèutica',
'medicine.dosage': 'Dosificació',
'medicine.price': 'Preu',
'medicine.notAvailable': 'No disponible',
'medicine.registration': 'Registre',
'medicine.mapSoon': 'Mapa properament…',
'medicine.pharmacies': 'Farmàcies',
'medicine.locating': 'Localitzant…',
'medicine.distanceReset': 'Distància · Restablir',
'medicine.sortByDistance': 'Ordenar per distància',
'medicine.retry': 'Tornar a provar',
'medicine.noPharmacies': 'No hi ha farmàcies disponibles',
'medicine.checkPrice': 'Consultar preu',
'medicine.howToGet': 'Com arribar',
'medicine.locationDenied': 'Permís d\'ubicació denegat',
'medicine.locationError': 'No s\'ha pogut obtenir la vostra ubicació',
// Pharmacy detail
'pharmacy.loading': 'Carregant farmàcia...',
'pharmacy.notFound': 'Farmàcia no trobada',
'pharmacy.call': 'Trucar',
'pharmacy.howToGet': 'Com arribar',
'pharmacy.medications': 'Medicaments',
'pharmacy.noMedications': 'No hi ha medicaments disponibles',
'pharmacy.reg': 'Reg: ',
'pharmacy.stock': 'Estoc: ',
'pharmacy.notificationsActive': 'Notificacions actives',
'pharmacy.notifyWhenAvailable': 'Notificar-me quan hi hagi estoc',
// Product detail
'product.loading': 'Carregant producte...',
'product.notFound': 'Producte no trobat',
'product.noImage': 'Sense imatge',
'product.parapharmacy': 'Parafarmàcia',
'product.cimaDetails': 'Detalls CIMA',
'product.activeIngredient': 'Principi actiu',
'product.dosage': 'Dosificació',
'product.form': 'Forma farmacèutica',
'product.dispensation': 'Tipus de dispensació',
'product.commercialized': 'Comercialitzat',
'product.yes': 'Sí',
'product.no': 'No',
'product.productInfo': 'Informació del producte',
'product.price': 'Preu',
'product.previousPrice': 'Preu anterior',
'product.category': 'Categoria',
'product.brand': 'Marca',
'product.source': 'Font',
'product.images': 'Imatges',
// Components
'medicineCard.noPrice': 'Sense preu',
'stockBadge.outOfStock': 'Sense estoc',
'stockBadge.lowStock': 'Baix',
'stockBadge.inStock': 'Disponible',
'loadingSpinner.loading': 'Carregant...',
'barcodeScanner.permissionRequired': 'Permís de càmera requerit',
'barcodeScanner.permissionDesc': 'Necessitem accés a la càmera per escanejar codis de barres',
'barcodeScanner.grantPermission': 'Concedir permís',
'barcodeScanner.cancel': 'Cancel·lar',
'barcodeScanner.scanningHint': 'Apunteu la càmera al codi de barres del medicament',
'barcodeScanner.scanAgain': 'Escanejar de nou',
};
export default ca;
+228
View File
@@ -0,0 +1,228 @@
const es = {
// Tab bar
'nav.home': 'Inicio',
'nav.search': 'Buscar',
'nav.scan': 'Escanear',
'nav.alerts': 'Avisos',
'nav.profile': 'Perfil',
// Stack screens
'stack.medicine': 'Medicamento',
'stack.pharmacy': 'Farmacia',
'stack.scanner': 'Escanear',
'stack.login': 'Iniciar Sesión',
'stack.register': 'Registrarse',
// Login / Register
'login.tab.login': 'Iniciar Sesión',
'login.tab.register': 'Crear Cuenta',
'login.title.login': 'Bienvenido de nuevo',
'login.title.register': 'Crea tu cuenta',
'login.subtitle.login': 'Inicia sesión para gestionar tu perfil y notificaciones.',
'login.subtitle.register': 'Guarda tu dirección y recibe notificaciones cuando lleguen medicamentos.',
'login.username': 'Usuario',
'login.password': 'Contraseña',
'login.confirmPassword': 'Confirmar contraseña',
'login.hint.username': '3-32 caracteres para el usuario',
'login.hint.password': 'Mínimo 8 caracteres para la contraseña',
'login.biometric': 'Iniciar con biometría',
'login.link.toLogin': '¿Ya tienes cuenta? Inicia sesión',
'login.link.toRegister': '¿No tienes cuenta? Regístrate',
'login.alert.error': 'Error',
'login.alert.fillFields': 'Por favor completa todos los campos',
'login.alert.passwordLength': 'La contraseña debe tener al menos 8 caracteres',
'login.alert.passwordMismatch': 'Las contraseñas no coinciden',
'login.alert.success': 'Éxito',
'login.alert.accountCreated': 'Cuenta creada correctamente',
'login.alert.ok': 'OK',
'login.alert.createError': 'No se pudo crear la cuenta',
'login.alert.wrongCredentials': 'Credenciales incorrectas',
'login.alert.noBiometrics': 'No hay credenciales biométricas guardadas',
'login.alert.biometricFailed': 'Autenticación biométrica fallida',
'login.alert.biometricError': 'Error en la autenticación biométrica',
// Home
'home.description': 'Encuentra tus medicamentos en farmacias cercanas',
'home.searchMedicine': 'Buscar Medicamento',
'home.scanTSI': 'Escanear TSI',
// Search
'search.placeholder': 'Buscar medicamentos...',
'search.suggestions': 'Sugerencias',
'search.recentSearches': 'Búsquedas recientes',
'search.loading': 'Buscando...',
'search.noResults': 'No se encontraron resultados',
'search.resultsFound': 'resultados encontrados',
'search.parapharmacy': 'Parafarmacia',
'search.error': 'Error al buscar medicamentos',
// Scanner
'scanner.title': 'Escanear TSI',
'scanner.description': 'Escanea el código de barras de tu tarjeta sanitaria para encontrar tus medicamentos',
'scanner.startScan': 'Iniciar escaneo',
'scanner.or': 'o',
'scanner.uploadPhoto': 'Subir foto del dispositivo',
'scanner.uploadDescription': 'Toma o selecciona una foto del dispositivo sanitario',
'scanner.takePhoto': 'Tomar foto',
'scanner.gallery': 'Galería',
'scanner.photoSelected': 'Foto seleccionada correctamente',
'scanner.manualEntry': 'Introducir número manualmente',
'scanner.manualDescription': 'Escribe el número de tu tarjeta sanitaria',
'scanner.manualPlaceholder': 'Introduce el número de tarjeta',
'scanner.cameraPermission': 'Permiso de cámara requerido',
'scanner.cameraPermissionDesc': 'Necesitamos acceso a la cámara para tomar fotos del dispositivo.',
'scanner.galleryPermission': 'Permiso requerido',
'scanner.galleryPermissionDesc': 'Necesitamos acceso a la galería para seleccionar fotos.',
'scanner.fieldRequired': 'Campo requerido',
'scanner.fieldRequiredDesc': 'Por favor, introduce el número de la tarjeta.',
// Alerts
'alerts.title': 'Notificaciones Guardadas',
'alerts.description': 'Recibe avisos cuando medicamentos sin stock se repongan',
'alerts.loading': 'Cargando notificaciones...',
'alerts.loginRequired': 'Inicia sesión para continuar',
'alerts.loginDescription': 'Necesitas estar autenticado para ver tus notificaciones',
'alerts.loginBtn': 'Iniciar Sesión',
'alerts.empty': 'Sin notificaciones',
'alerts.emptyDescription': 'Toca la campana en una farmacia sin stock para recibir notificaciones cuando se reponga.',
'alerts.retry': 'Reintentar',
'alerts.delete': 'Eliminar notificación',
'alerts.deleteConfirm': '¿Eliminar la notificación de',
'alerts.cancel': 'Cancelar',
'alerts.confirmDelete': 'Eliminar',
'alerts.deleteError': 'No se pudo eliminar',
'alerts.loadError': 'No se pudieron cargar las notificaciones',
'alerts.pharmacy': 'Farmacia',
'alerts.anyPharmacy': 'Cualquier farmacia',
// Profile
'profile.title': 'Perfil',
'profile.loginTitle': 'Inicia Sesión',
'profile.loginDescription': 'Inicia sesión para acceder a tu perfil, notificaciones y más',
'profile.loginBtn': 'Iniciar Sesión',
'profile.personalData': 'Datos personales',
'profile.firstName': 'Nombre',
'profile.lastName': 'Apellidos',
'profile.appearance': 'Apariencia',
'profile.config': 'Configuración',
'profile.myAddresses': 'Mis Direcciones',
'profile.admin': 'Panel Admin',
'profile.recentSearches': 'Búsquedas recientes',
'profile.logout': 'Cerrar Sesión',
'profile.logoutConfirm': '¿Estás seguro que deseas cerrar sesión?',
'profile.logoutTitle': 'Cerrar Sesión',
'profile.logoutCancel': 'Cancelar',
'profile.logoutConfirmBtn': 'Cerrar Sesión',
'profile.changeAvatar': 'Cambiar Avatar',
'profile.presetAvatar': 'Prediseñado',
'profile.colors': 'Colores',
'profile.upload': 'Subir',
'profile.takePhoto': 'Tomar foto',
'profile.takePhotoDesc': 'Usa la cámara de tu dispositivo',
'profile.chooseGallery': 'Elegir de galería',
'profile.chooseGalleryDesc': 'Selecciona una imagen existente',
'profile.configTitle': 'Configuración',
'profile.firstNameLabel': 'Nombre',
'profile.firstNamePlaceholder': 'Tu nombre',
'profile.lastNameLabel': 'Apellidos',
'profile.lastNamePlaceholder': 'Tus apellidos',
'profile.email': 'Correo electrónico',
'profile.city': 'Ciudad',
'profile.cityPlaceholder': 'Tu ciudad',
'profile.address': 'Dirección',
'profile.addressPlaceholder': 'Calle Mayor 1, Madrid',
'profile.cancel': 'Cancelar',
'profile.save': 'Guardar',
'profile.profileSaved': 'Perfil guardado.',
'profile.saveError': 'Error al guardar',
'profile.addressRequired': 'La dirección es obligatoria',
'profile.addressesTitle': 'Mis Direcciones',
'profile.addressLabel': 'Dirección',
'profile.addressOptional': 'Etiqueta (opcional)',
'profile.addressLabelPlaceholder': 'Casa, Trabajo...',
'profile.defaultAddress': 'Dirección predeterminada',
'profile.update': 'Actualizar',
'profile.add': 'Añadir',
'profile.mainAddress': 'Principal',
'profile.setDefault': 'Marcar como predeterminada',
'profile.addAddress': 'Añadir dirección',
'profile.cameraPermission': 'Permiso requerido',
'profile.cameraPermissionDesc': 'Necesitamos permiso para acceder a la cámara.',
'profile.themeSystem': 'Sistema',
'profile.themeLight': 'Claro',
'profile.themeDark': 'Oscuro',
'profile.language': 'Idioma',
'profile.languageTitle': 'Idioma de la aplicación',
'profile.languageCatalan': 'Català',
'profile.languageSpanish': 'Castellano',
// Medicine detail
'medicine.loading': 'Cargando medicamento...',
'medicine.notFound': 'Medicamento no encontrado',
'medicine.activeIngredient': 'Principio activo',
'medicine.laboratory': 'Laboratorio',
'medicine.form': 'Forma farmacéutica',
'medicine.dosage': 'Dosificación',
'medicine.price': 'Precio',
'medicine.notAvailable': 'No disponible',
'medicine.registration': 'Registro',
'medicine.mapSoon': 'Mapa próximamente…',
'medicine.pharmacies': 'Farmacias',
'medicine.locating': 'Localizando…',
'medicine.distanceReset': 'Distancia · Reset',
'medicine.sortByDistance': 'Ordenar por distancia',
'medicine.retry': 'Reintentar',
'medicine.noPharmacies': 'No hay farmacias disponibles',
'medicine.checkPrice': 'Consultar precio',
'medicine.howToGet': 'Cómo llegar',
'medicine.locationDenied': 'Permiso de ubicación denegado',
'medicine.locationError': 'No se pudo obtener tu ubicación',
// Pharmacy detail
'pharmacy.loading': 'Cargando farmacia...',
'pharmacy.notFound': 'Farmacia no encontrada',
'pharmacy.call': 'Llamar',
'pharmacy.howToGet': 'Cómo llegar',
'pharmacy.medications': 'Medicamentos',
'pharmacy.noMedications': 'No hay medicamentos disponibles',
'pharmacy.reg': 'Reg: ',
'pharmacy.stock': 'Stock: ',
'pharmacy.notificationsActive': 'Notificaciones activas',
'pharmacy.notifyWhenAvailable': 'Notificarme cuando haya stock',
// Product detail
'product.loading': 'Cargando producto...',
'product.notFound': 'Producto no encontrado',
'product.noImage': 'Sin imagen',
'product.parapharmacy': 'Parafarmacia',
'product.cimaDetails': 'Detalles CIMA',
'product.activeIngredient': 'Principio activo',
'product.dosage': 'Dosificación',
'product.form': 'Forma farmacéutica',
'product.dispensation': 'Tipo de dispensación',
'product.commercialized': 'Comercializado',
'product.yes': 'Sí',
'product.no': 'No',
'product.productInfo': 'Información del producto',
'product.price': 'Precio',
'product.previousPrice': 'Precio anterior',
'product.category': 'Categoría',
'product.brand': 'Marca',
'product.source': 'Fuente',
'product.images': 'Imágenes',
// Components
'medicineCard.noPrice': 'Sin precio',
'stockBadge.outOfStock': 'Sin stock',
'stockBadge.lowStock': 'Bajo',
'stockBadge.inStock': 'Disponible',
'loadingSpinner.loading': 'Cargando...',
'barcodeScanner.permissionRequired': 'Permiso de cámara requerido',
'barcodeScanner.permissionDesc': 'Necesitamos acceso a la cámara para escanear códigos de barras',
'barcodeScanner.grantPermission': 'Conceder permiso',
'barcodeScanner.cancel': 'Cancelar',
'barcodeScanner.scanningHint': 'Apunta la cámara al código de barras del medicamento',
'barcodeScanner.scanAgain': 'Escanear de nuevo',
};
export default es;