fase 3.6: i18n espanol/ingles (530 claves, provider con fallback, interpolacion y pluralizacion, selector de idioma en menu de usuario y configuracion, convivencia con el glosario de negocio, superficies principales y paginas publicas traducidas sin alterar el espanol)

This commit is contained in:
2026-10-11 21:19:43 +00:00
parent 3ed0d327d2
commit e749a31d10
22 changed files with 2022 additions and 411 deletions

View File

@@ -28,6 +28,8 @@ import { Card, Button, Input, PageHeader, toast } from '../components/ui';
import { odooApi, type Doctor, type ClinicSettings, type ImpuestosSettings, BRAND_DEFAULT, CLINIC_SETTINGS_DEFAULT, IMPUESTOS_DEFAULT, RESERVAS_ONLINE_DEFAULT, NOTIFICACIONES_DEFAULT } from '../services/odoo';
import { useAuth } from '../lib/auth';
import { publicarClinicSettings } from '../lib/clinic-settings';
import { useI18n } from '../lib/i18n';
import { LanguageSelect } from '../components/LanguageSelect';
const TABS = [
{ key: 'clinica', label: 'Clínica' },
@@ -67,6 +69,9 @@ const LEGACY_STORAGE_KEY = 'skeen_clinic_settings';
const Configuracion: FC = () => {
const { user, brand, setBrand, can } = useAuth();
const { t } = useI18n();
// Clave i18n de una pestaña: 'notif_enviadas' -> 'config.tabs.notifEnviadas'.
const tabKey = (k: string) => 'config.tabs.' + k.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
// Gerencia gestiona catálogos, usuarios y roles; la sección "Clínica" (datos
// de contacto, tipo de cambio y comisiones) queda para administración del sistema.
const esAdmin = user?.role === 'admin';
@@ -321,21 +326,21 @@ const Configuracion: FC = () => {
};
return (
<Layout title="Configuración" subtitle="Ajustes de la clínica">
<PageHeader title="Configuración" subtitle="Información y ajustes de la clínica" />
<Layout title={t('config.title')} subtitle={t('config.subtitle')}>
<PageHeader title={t('config.title')} subtitle={t('config.pageSubtitle')} />
{/* Tabs de secciones */}
<div className="flex flex-wrap gap-1.5 mb-6">
{tabsVisibles.map((t) => (
{tabsVisibles.map((tabItem) => (
<button
key={t.key}
key={tabItem.key}
type="button"
onClick={() => setTab(t.key)}
onClick={() => setTab(tabItem.key)}
className={`px-4 py-2 text-sm font-medium rounded-full transition ${
tab === t.key ? 'bg-theme-accent text-theme-inverse' : 'bg-theme-accent-bg text-theme-heading hover:bg-theme-bg'
tab === tabItem.key ? 'bg-theme-accent text-theme-inverse' : 'bg-theme-accent-bg text-theme-heading hover:bg-theme-bg'
}`}
>
{t.label}
{t(tabKey(tabItem.key))}
</button>
))}
</div>
@@ -343,7 +348,7 @@ const Configuracion: FC = () => {
{tabsVisibles.length === 0 ? (
<Card>
<Card.Body>
<p className="text-sm text-theme-muted">No tienes permiso para ninguna sección de Configuración.</p>
<p className="text-sm text-theme-muted">{t('config.noPermission')}</p>
</Card.Body>
</Card>
) : tab === 'usuarios' ? (
@@ -381,11 +386,11 @@ const Configuracion: FC = () => {
subcatalogo === s.key ? 'bg-theme-accent text-theme-inverse' : 'bg-theme-accent-bg text-theme-heading hover:bg-theme-bg'
}`}
>
{s.label}
{t(`config.tabs.${s.key}`)}
</button>
))}
</div>
<CatalogoPanel kind={subcatalogo as 'diagnosticos' | 'procedimientos'} titulo={subcatalogo === 'diagnosticos' ? 'Diagnósticos' : 'Procedimientos'} />
<CatalogoPanel kind={subcatalogo as 'diagnosticos' | 'procedimientos'} titulo={t(`config.tabs.${subcatalogo}`)} />
</Card.Body>
</Card>
) : tab === 'sucursales' ? (
@@ -461,10 +466,20 @@ const Configuracion: FC = () => {
) : tab === 'webhooks' ? (
<WebhooksPanel />
) : (
<>
<Card className="mb-4 sm:mb-6">
<Card.Body>
<h3 className="font-heading text-xl text-theme-heading mb-1">{t('config.idiomaTitle')}</h3>
<p className="text-sm text-theme-muted mb-3">{t('config.idiomaBody')}</p>
<div className="max-w-xs">
<LanguageSelect />
</div>
</Card.Body>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
<Card>
<Card.Body>
<h3 className="font-heading text-xl text-theme-heading mb-4">Información de la clínica</h3>
<h3 className="font-heading text-xl text-theme-heading mb-4">{t('config.clinicInfo')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<a
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(settings.direccion)}`}
@@ -529,7 +544,7 @@ const Configuracion: FC = () => {
<Card.Body>
<h3 className="font-heading text-xl text-theme-heading mb-4 flex items-center">
<Building2 size={20} className="mr-2" />
Ajustes
{t('config.settings')}
</h3>
{loading ? (
<p className="text-sm text-theme-muted">Cargando...</p>
@@ -693,7 +708,7 @@ const Configuracion: FC = () => {
{esAdmin && (
<Card>
<Card.Body>
<h3 className="font-heading text-xl text-theme-heading mb-1">Identidad de la clínica</h3>
<h3 className="font-heading text-xl text-theme-heading mb-1">{t('config.clinicIdentity')}</h3>
<p className="text-sm text-theme-muted mb-4">
Nombre, pie de página y logos que aparecen en el inicio de sesión, el menú y los
documentos impresos. Dejar un campo vacío vuelve al valor por omisión.
@@ -760,7 +775,7 @@ const Configuracion: FC = () => {
placeholder="Vacío = color del tema"
/>
{brandForm[key] && (
<Button variant="ghost" size="sm" onClick={() => setBrandForm((f) => ({ ...f, [key]: '' }))} title="Usar el color del tema">
<Button variant="ghost" size="sm" onClick={() => setBrandForm((f) => ({ ...f, [key]: '' }))} title={t('config.useThemeColor')}>
Limpiar
</Button>
)}
@@ -797,7 +812,7 @@ const Configuracion: FC = () => {
<Card.Body>
<h3 className="font-heading text-xl text-theme-heading mb-1 flex items-center">
<ArrowRightLeft size={20} className="mr-2" />
Tipo de cambio USD / MXN
{t('config.exchangeRate')}
</h3>
<p className="text-sm text-theme-muted mb-4">
Se usa para convertir ventas en dólares. Vigente:
@@ -821,7 +836,7 @@ const Configuracion: FC = () => {
</div>
<Button onClick={saveTc} disabled={tcSaving}>
<Save size={16} className="mr-2" />
{tcSaving ? 'Guardando...' : 'Actualizar TC'}
{tcSaving ? t('common.saving') : t('config.updateRate')}
</Button>
</div>
</Card.Body>
@@ -831,7 +846,7 @@ const Configuracion: FC = () => {
<Card.Body>
<h3 className="font-heading text-xl text-theme-heading mb-1 flex items-center">
<Percent size={20} className="mr-2" />
Comisiones
{t('config.commissions')}
</h3>
<p className="text-sm text-theme-muted mb-4">
Porcentaje de comisión sobre las ventas atribuidas a cada médico (visible en su página "Mis ventas").
@@ -864,7 +879,7 @@ const Configuracion: FC = () => {
</div>
<Button onClick={saveCommissionDefault} disabled={commissionDefaultSaving}>
<Save size={16} className="mr-2" />
{commissionDefaultSaving ? 'Guardando...' : 'Guardar'}
{commissionDefaultSaving ? t('common.saving') : t('common.save')}
</Button>
</div>
<div className="overflow-x-auto">
@@ -910,7 +925,7 @@ const Configuracion: FC = () => {
onClick={() => saveDoctorCommission(d.id)}
disabled={commissionSavingId === d.id}
>
{commissionSavingId === d.id ? 'Guardando...' : 'Guardar'}
{commissionSavingId === d.id ? t('common.saving') : t('common.save')}
</Button>
)}
</td>
@@ -927,6 +942,7 @@ const Configuracion: FC = () => {
</Card.Body>
</Card>
</div>
</>
)}
<ImpactoModal