fix: add i18n support for parapharmacy products and notification bell
Run Tests on Branches / Detect Changes (push) Successful in 12s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 1m37s
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m52s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped

- Mobile: Replace hardcoded Spanish in map.tsx with t() calls
- Mobile: Add notification bell to parapharmacy product detail screen
- Mobile: Add map/product i18n keys to es.js and ca.js locales
- Web: Add useTranslation to ProductView.jsx, replace all hardcoded strings
- Web: Add 24 productView.* translation keys to es.js and ca.js locales
- Fix 'Disponible en X farmacias' to respect language setting
This commit is contained in:
Antoni Nuñez Romeu
2026-07-17 13:04:04 +02:00
parent 8a7ef81964
commit 854de92be5
7 changed files with 153 additions and 31 deletions
+26
View File
@@ -81,6 +81,32 @@ const ca = {
'product.dosage': 'Dosificació:',
'product.viewDetails': 'Veure detalls →',
// ProductView (detail)
'productView.loading': 'Carregant...',
'productView.back': 'Tornar',
'productView.notFound': 'Producte no trobat',
'productView.parapharmacy': 'Parafarmàcia',
'productView.activeIngredient': 'Principi actiu',
'productView.dosage': 'Dosificació',
'productView.form': 'Forma farmacèutica',
'productView.prescription': 'Prescripció',
'productView.commercialized': 'Comercialitzat',
'productView.yes': 'Sí',
'productView.no': 'No',
'productView.price': 'Preu',
'productView.previousPrice': 'Preu anterior',
'productView.category': 'Categoria',
'productView.brand': 'Marca',
'productView.source': 'Font',
'productView.loadingPharmacies': 'Carregant farmàcies...',
'productView.availableInPharmacies': 'Disponible a {{count}} farmàcia/es',
'productView.locating': '📍 Localitzant…',
'productView.sortedByDistance': '📍 Ordenat per distància · Restablir',
'productView.sortByDistance': '📍 Ordenar per distància',
'productView.usingLocation': 'Usant la vostra ubicació',
'productView.retry': 'Tornar a provar',
'productView.productNotFound': 'Producte no trobat',
// LoginModal
'login.login': 'Iniciar sessió',
'login.register': 'Crear compte',
+26
View File
@@ -81,6 +81,32 @@ const es = {
'product.dosage': 'Dosis:',
'product.viewDetails': 'Ver detalles →',
// ProductView (detail)
'productView.loading': 'Cargando...',
'productView.back': 'Volver',
'productView.notFound': 'Producto no encontrado',
'productView.parapharmacy': 'Parafarmacia',
'productView.activeIngredient': 'Principio activo',
'productView.dosage': 'Dosis',
'productView.form': 'Forma farmacéutica',
'productView.prescription': 'Prescripción',
'productView.commercialized': 'Comercializado',
'productView.yes': 'Sí',
'productView.no': 'No',
'productView.price': 'Precio',
'productView.previousPrice': 'Precio anterior',
'productView.category': 'Categoría',
'productView.brand': 'Marca',
'productView.source': 'Fuente',
'productView.loadingPharmacies': 'Cargando farmacias...',
'productView.availableInPharmacies': 'Disponible en {{count}} farmacia(s)',
'productView.locating': '📍 Localizando…',
'productView.sortedByDistance': '📍 Ordenado por distancia · Reset',
'productView.sortByDistance': '📍 Ordenar por distancia',
'productView.usingLocation': 'Usando tu ubicación',
'productView.retry': 'Reintentar',
'productView.productNotFound': 'Producto no encontrado',
// LoginModal
'login.login': 'Iniciar sesión',
'login.register': 'Crear cuenta',
+28 -26
View File
@@ -2,9 +2,11 @@ import React, { useState, useEffect, useMemo } from 'react';
import PharmacyMap from '../components/PharmacyMap';
import PharmacyList from '../components/PharmacyList';
import { haversineKm, getUserPosition, hasCachedPosition } from '../utils/geo';
import { useTranslation } from '../i18n';
import './ProductView.css';
export default function ProductView({ source, id, onBack }) {
const { t } = useTranslation();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -43,11 +45,11 @@ export default function ProductView({ source, id, onBack }) {
setPositionSource('browser');
setSortByDistance(true);
} catch (err) {
let msg = 'No se pudo obtener tu ubicación';
let msg = t('search.locationError');
if (err && typeof err.code === 'number') {
if (err.code === 1) msg = 'Permiso de ubicación denegado.';
else if (err.code === 2) msg = 'Ubicación no disponible.';
else if (err.code === 3) msg = 'La ubicación tardó demasiado.';
if (err.code === 1) msg = t('search.locationDenied');
else if (err.code === 2) msg = t('search.locationUnavailable');
else if (err.code === 3) msg = t('search.locationTimeout');
}
setLocationError(msg);
} finally {
@@ -72,7 +74,7 @@ export default function ProductView({ source, id, onBack }) {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error('Producto no encontrado');
throw new Error(t('productView.productNotFound'));
}
const data = await response.json();
setProduct(data);
@@ -102,7 +104,7 @@ export default function ProductView({ source, id, onBack }) {
if (loading) {
return (
<div className="product-view">
<div className="product-loading">Cargando...</div>
<div className="product-loading">{t('productView.loading')}</div>
</div>
);
}
@@ -114,7 +116,7 @@ export default function ProductView({ source, id, onBack }) {
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
Volver
{t('productView.back')}
</button>
<div className="product-error">{error}</div>
</div>
@@ -126,7 +128,7 @@ export default function ProductView({ source, id, onBack }) {
const isCima = product.source === 'cima';
const isParapharmacy = product.source !== 'cima';
const sourceColor = isCima ? '#2563eb' : '#16a34a';
const sourceLabel = isCima ? 'CIMA' : 'Parafarmacia';
const sourceLabel = isCima ? 'CIMA' : t('productView.parapharmacy');
return (
<div className="product-view">
@@ -134,7 +136,7 @@ export default function ProductView({ source, id, onBack }) {
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
Volver
{t('productView.back')}
</button>
<div className="product-header">
@@ -153,37 +155,37 @@ export default function ProductView({ source, id, onBack }) {
{isCima ? (
<>
{product.active_ingredient && (
<DetailRow label="Principio activo" value={product.active_ingredient} />
<DetailRow label={t('productView.activeIngredient')} value={product.active_ingredient} />
)}
{product.dosage && (
<DetailRow label="Dosis" value={product.dosage} />
<DetailRow label={t('productView.dosage')} value={product.dosage} />
)}
{product.form && (
<DetailRow label="Forma farmacéutica" value={product.form} />
<DetailRow label={t('productView.form')} value={product.form} />
)}
{product.prescription && (
<DetailRow label="Prescripción" value={product.prescription} />
<DetailRow label={t('productView.prescription')} value={product.prescription} />
)}
{product.commercialized !== undefined && (
<DetailRow label="Comercializado" value={product.commercialized ? 'Sí' : 'No'} />
<DetailRow label={t('productView.commercialized')} value={product.commercialized ? t('productView.yes') : t('productView.no')} />
)}
</>
) : (
<>
{product.price && (
<DetailRow label="Precio" value={`${product.price} €`} />
<DetailRow label={t('productView.price')} value={`${product.price} €`} />
)}
{product.original_price && product.original_price > product.price && (
<DetailRow label="Precio anterior" value={`${product.original_price} €`} />
<DetailRow label={t('productView.previousPrice')} value={`${product.original_price} €`} />
)}
{product.category && (
<DetailRow label="Categoría" value={product.category} />
<DetailRow label={t('productView.category')} value={product.category} />
)}
{product.brand && (
<DetailRow label="Marca" value={product.brand} />
<DetailRow label={t('productView.brand')} value={product.brand} />
)}
{product.source_url && (
<DetailRow label="Fuente" value={product.source} />
<DetailRow label={t('productView.source')} value={product.source} />
)}
</>
)}
@@ -191,11 +193,11 @@ export default function ProductView({ source, id, onBack }) {
<div className="product-pharmacies">
{loadingPharmacies ? (
<div className="pharmacies-loading">Cargando farmacias...</div>
<div className="pharmacies-loading">{t('productView.loadingPharmacies')}</div>
) : pharmacies.length > 0 ? (
<>
<h3 className="pharmacies-title">
Disponible en {pharmacies.length} {pharmacies.length === 1 ? 'farmacia' : 'farmacias'}
{t('productView.availableInPharmacies', { count: pharmacies.length })}
</h3>
<div className="pharmacy-controls">
@@ -205,19 +207,19 @@ export default function ProductView({ source, id, onBack }) {
disabled={locating}
>
{locating
? '📍 Localizando…'
? t('productView.locating')
: sortByDistance
? '📍 Ordenado por distancia · Reset'
: '📍 Ordenar por distancia'}
? t('productView.sortedByDistance')
: t('productView.sortByDistance')}
</button>
{sortByDistance && positionSource && (
<span className="location-source">Usando tu ubicación</span>
<span className="location-source">{t('productView.usingLocation')}</span>
)}
{locationError && (
<span className="location-error">
{locationError}
<button className="retry-location-btn" onClick={handleSortByDistance}>
Reintentar
{t('productView.retry')}
</button>
</span>
)}