import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import type { FC, ReactNode } from 'react'; import type { GlossaryKey } from '../services/odoo'; import { useClinicSettings } from './clinic-settings'; import { useGlosario, MENU_LABEL_TERMS } from './glosario'; // ============================================================ // i18n mínimo (español + inglés) sin dependencias externas. // // Diseño: // - Un diccionario plano con claves namespaced ('nav.agenda', 'common.save'...). // - El diccionario `es` contiene EXACTAMENTE los literales que usa hoy la // interfaz: en español nada cambia (regla de oro del proyecto). // - Resolución de `t(clave)`: diccionario del idioma -> diccionario `es` -> // la propia clave. Nunca se ve vacío si falta una traducción; en desarrollo // se avisa por consola cuando falta una clave en `en`. // - Interpolación simple: t('greeting', { nombre }) reemplaza {nombre}. // - Pluralización básica: si `vars.count` es número y existen `clave_one` / // `clave_other`, se elige según count === 1. // // Persistencia: localStorage ('skeen_idioma'), elegido por el usuario en el // menú de usuario o en Configuración. `clinic-settings.idioma_default` se lee // como default cuando el backend lo provee (hoy no lo persiste: ver reporte). // ============================================================ export type Locale = 'es' | 'en'; export const IDIOMA_STORAGE_KEY = 'skeen_idioma'; export interface IdiomaOpcion { code: Locale; /** Etiqueta corta para el selector (ES / EN). */ label: string; /** Nombre del idioma en su propio idioma. */ nombre: string; } export const IDIOMAS: IdiomaOpcion[] = [ { code: 'es', label: 'ES', nombre: 'Español' }, { code: 'en', label: 'EN', nombre: 'English' }, ]; export type Dict = Record; // ------------------------------------------------------------ // Diccionario ESPAÑOL: literales actuales, copiados tal cual. // ------------------------------------------------------------ export const es: Dict = { // --- comunes --- 'common.save': 'Guardar', 'common.cancel': 'Cancelar', 'common.close': 'Cerrar', 'common.delete': 'Eliminar', 'common.edit': 'Editar', 'common.search': 'Buscar', 'common.all': 'Todos', 'common.new': 'Nuevo', 'common.loading': 'Cargando…', 'common.export': 'Exportar', 'common.filter': 'Filtrar', 'common.apply': 'Aplicar', 'common.clear': 'Limpiar', 'common.viewAll': 'Ver todos', 'common.actions': 'Acciones', 'common.status': 'Estado', 'common.date': 'Fecha', 'common.time': 'Hora', 'common.total': 'Total', 'common.subtotal': 'Subtotal', 'common.notes': 'Notas', 'common.name': 'Nombre', 'common.phone': 'Teléfono', 'common.email': 'Correo', 'common.yes': 'Sí', 'common.no': 'No', 'common.back': 'Atrás', 'common.next': 'Siguiente', 'common.accept': 'Aceptar', 'common.select': 'Seleccionar', 'common.optional': 'Opcional', 'common.required': 'Obligatorio', 'common.none': 'Ninguno', 'common.saving': 'Guardando...', // --- app (arranque / sin acceso) --- 'app.bootTitle': 'No se pudo conectar', 'app.bootBody': 'El servidor no responde en este momento. Revisa tu conexión a internet e intenta de nuevo.', 'app.retry': 'Reintentar', 'app.noAccessTitle': 'Sin acceso a módulos', 'app.noAccessBody': 'Tu usuario no tiene secciones habilitadas. Contacta al administrador.', // --- login --- 'login.welcome': 'Bienvenido', 'login.subtitle': 'Inicia sesión para continuar', 'login.username': 'Usuario', 'login.password': 'Contraseña', 'login.submit': 'Entrar', 'login.errRequired': 'Escribe usuario y contraseña', 'login.errInvalid': 'Usuario o contraseña incorrectos', 'login.errGeneric': 'No se pudo iniciar sesión. Intenta de nuevo.', 'login.bookOnline': 'Reservar en línea', 'login.showPassword': 'Mostrar contraseña', 'login.hidePassword': 'Ocultar contraseña', // --- PWA / avisos --- 'pwa.installTitle': 'Instalar la app', 'pwa.installHint': 'Accede más rápido desde tu pantalla de inicio.', 'pwa.iosStepPrefix': 'Toca', 'pwa.iosStepSuffix': 'Compartir y luego «Añadir a pantalla de inicio».', 'pwa.install': 'Instalar', 'pwa.dismiss': 'Descartar', 'pwa.offline': 'Sin conexión — los cambios se guardarán al recuperar la red', // --- accesos rápidos (dock) --- 'dock.aria': 'Accesos rápidos', 'dock.hint': 'Fija accesos con la ★ del menú', 'dock.changeMode': 'Cambiar presentación de accesos rápidos', 'dock.showModes': 'Mostrar accesos rápidos', 'dock.mode.menu': 'En el menú lateral', 'dock.mode.right': 'Barra de iconos a la derecha', 'dock.mode.bottom': 'Barra de iconos abajo', 'dock.finishEditing': 'Terminar edición', 'dock.editQuick': 'Editar accesos rápidos', 'dock.up': 'Subir', 'dock.down': 'Bajar', 'dock.remove': 'Quitar', // --- navegación (menú lateral) --- 'nav.group.principal': 'Principal', 'nav.group.clinica': 'Clínica', 'nav.group.operaciones': 'Operaciones', 'nav.group.analitica': 'Analítica', 'nav.group.analiticaMas': 'Analítica y más', 'nav.group.comunicacion': 'Comunicación', 'nav.group.wacrm': 'WACRM', 'nav.quickLinks': 'Accesos rápidos', 'nav.dashboard': 'Dashboard', 'nav.agenda': 'Agenda', 'nav.consultas': 'Consultas', 'nav.mySales': 'Mis ventas', 'nav.pos': 'Punto de Venta', 'nav.sales': 'Ventas', 'nav.tickets': 'Tickets', 'nav.payments': 'Pagos', 'nav.debts': 'Adeudos', 'nav.wallet': 'Monedero', 'nav.inventory': 'Inventario', 'nav.purchases': 'Compras', 'nav.cashCuts': 'Cortes de Caja', 'nav.reports': 'Reportes', 'nav.commissions': 'Comisiones', 'nav.agendaHours': 'Horas agenda', 'nav.exportData': 'Exportar datos', 'nav.config': 'Configuración', 'nav.chat': 'Chat', 'nav.birthdays': 'Cumpleañeros', 'nav.messages': 'Mensajes', 'nav.leads': 'Leads', 'nav.memberships': 'Membresías', 'nav.liquidations': 'Liquidaciones', 'nav.packages': 'Paquetes', // --- menú del tema "clásico" (etiquetas propias) --- 'clasico.inicio': 'Inicio', 'clasico.visitas': 'Visitas', 'clasico.consultasMedicas': 'Consultas Médicas', 'clasico.expedientes': 'Expedientes', 'clasico.medicos': 'Médicos', 'clasico.cosmetologia': 'Cosmetología', 'clasico.clinica': 'Clínica', 'clasico.usuarios': 'Usuarios', 'clasico.recetas': 'Recetas', 'clasico.diagnosticos': 'Diagnósticos', 'clasico.procedimientos': 'Procedimientos médicos', 'clasico.administrativo': 'Administrativo', 'clasico.movDiario': 'Mov. diario', 'clasico.cortesCaja': 'Cortes de caja', 'clasico.ingresos': 'Ingresos', 'clasico.exportarCorreos': 'Exportar correos', 'clasico.adeudosClientes': 'Adeudos clientes', 'clasico.pagosServicios': 'Pagos | Servicios', 'clasico.pagosClientes': 'Pagos | Clientes', 'clasico.devoluciones': 'Devoluciones', 'clasico.vendedores': 'Vendedores', 'clasico.recomendaciones': 'Recomendaciones', 'clasico.concentrado': 'Concentrado', 'clasico.topVentas': 'Top ventas', 'clasico.monedero': 'Monedero', 'clasico.monederos': 'Monederos', 'clasico.reportePuntos': 'Reporte puntos', 'clasico.comunicacion': 'Comunicación', 'clasico.crm': 'CRM', 'clasico.comprobantes': 'Comprobantes del día', 'clasico.consumibles': 'Consumibles', 'clasico.inventarioActual': 'Inventario actual', 'clasico.bajas': 'Bajas', 'clasico.ordenesCompra': 'Órdenes de compra', 'clasico.ventasDia': 'Ventas del día', 'clasico.clientes': 'Clientes', // --- usuario / layout --- 'user.logout': 'Cerrar sesión', 'user.expandMenu': 'Expandir menú', 'user.collapseMenu': 'Colapsar menú', 'user.openMenu': 'Abrir menú', 'user.closeMenu': 'Cerrar menú', 'user.pinQuick': 'Fijar en accesos rápidos', 'user.unpinQuick': 'Quitar de accesos rápidos', 'user.language': 'Idioma', 'user.more': 'Más', 'user.goalTitle': 'Meta de ventas del mes', 'user.goalMonth': 'Meta del mes', 'user.goalOf': 'de', 'user.goalDefine': 'Definir meta', 'user.goalSave': 'Guardar meta', 'user.goalField': 'Meta mensual (MXN)', 'user.goalBody': 'Define el objetivo de ventas (MXN) para el mes en curso. La barra del header muestra el avance en tiempo real.', 'user.goalEditHint': 'Click para editar la meta', 'user.goalMonthHint': 'Meta de ventas del mes', 'search.placeholder': 'Buscar paciente, cita, servicio...', 'search.aria': 'Búsqueda global', 'search.clear': 'Limpiar búsqueda', 'search.noResults': 'Sin resultados para "{q}". Prueba con otro nombre o teléfono.', 'search.hintKeys': 'Usa ↑ ↓ para moverte y Enter para abrir', 'search.group.Pacientes': 'Pacientes', 'search.group.Citas próximas': 'Citas próximas', 'search.group.Servicios': 'Servicios', 'notif.title': 'Notificaciones', 'notif.loading': 'Cargando...', 'notif.empty': 'Sin notificaciones', 'role.admin': 'Administrador', 'role.medico': 'Médico', 'role.recepcion': 'Recepción', 'role.enfermeria': 'Enfermería', 'role.cosmetologia': 'Cosmetología', 'role.psicologa': 'Psicóloga', 'role.farmacia': 'Farmacia', 'role.gerencia': 'Gerencia', 'role.marketing': 'Marketing', 'role.lectura': 'Lectura', // --- dashboard --- 'dashboard.title': 'Dashboard', 'dashboard.subtitleBrand': 'Panel de control de {brand}', 'dashboard.subtitle': 'Resumen general de la clínica', 'dashboard.welcome': 'Bienvenido', 'dashboard.welcomeUser': 'Bienvenido, {name}', 'dashboard.welcomeBack': 'Bienvenido de vuelta', 'dashboard.theClinic': 'la clínica', 'dashboard.newAppointmentWord': 'Nueva', 'dashboard.checking': 'Consultando…', 'dashboard.stateDone': 'Completadas', 'dashboard.stateConfirmed': 'Confirmadas', 'dashboard.stateCancelled': 'Canceladas', 'dashboard.ariaUpcoming': 'Ver próximas citas en la agenda', 'dashboard.ariaWaiting': 'Ver pacientes en sala de espera', 'dashboard.ariaCash': 'Ir a cortes de caja', 'dashboard.ariaCharges': 'Ver cobros del día en ventas', 'dashboard.openPatient': 'Abrir ficha del paciente', 'dashboard.vip': 'VIP', 'dashboard.heroText': 'Ciencia, estética y cuidado personalizado en cada tratamiento. Gestiona tu clínica con confianza.', 'dashboard.heroTextNest': 'Ciencia, estética y cuidado personalizado en cada tratamiento. Gestiona tu clínica con calidez y confianza.', 'dashboard.viewReports': 'Ver reportes', 'dashboard.newAppointment': 'Nueva cita', 'dashboard.todayTitle': 'Hoy en la clínica', 'dashboard.upcoming': 'Próximas citas', 'dashboard.noUpcoming': 'Sin citas activas para hoy.', 'dashboard.waitingRoom': 'Sala de espera', 'dashboard.waiting_one': 'paciente en espera', 'dashboard.waiting_other': 'pacientes en espera', 'dashboard.notAvailable': 'no disponible', 'dashboard.cash': 'Caja', 'dashboard.cashOpen': 'Abierta', 'dashboard.cashClosed': 'Cerrada', 'dashboard.cashNone': 'Sin abrir', 'dashboard.noCashCut': 'Sin corte de caja hoy', 'dashboard.dayCharges': 'Cobros del día', 'dashboard.completedPayments': 'pagos completados hoy', 'dashboard.statAppointments': 'Citas Hoy', 'dashboard.statPatients': 'Pacientes', 'dashboard.statRevenue': 'Ingresos Mes', 'dashboard.statSales': 'Ventas Hoy', 'dashboard.statPending': 'Pendientes', 'dashboard.statWallet': 'Puntos Monedero', 'dashboard.thisMonth': '{n} este mes', 'dashboard.newPatients': '+{n} nuevos', 'dashboard.vsPrevMonth': 'vs mes anterior', 'dashboard.vsYesterday': 'vs ayer', 'dashboard.birthdaysTitle': 'Cumpleaños de hoy', 'dashboard.birthdaysSub': 'Pacientes que cumplen años hoy — contáctalos', 'dashboard.noBirthdays': 'Ningún paciente cumple años hoy.', 'dashboard.turning': 'cumple {age}', 'dashboard.noDoctor': 'Sin médico', 'dashboard.chartWeeklyTitle': 'Actividad semanal', 'dashboard.chartWeeklySub': 'Citas e ingresos de los últimos 7 días', 'dashboard.noDataTitle': 'Sin datos', 'dashboard.noWeeklySub': 'No hay información para la gráfica semanal.', 'dashboard.upcomingTodaySub': 'Citas programadas para hoy', 'dashboard.noAppointmentsTitle': 'Sin citas', 'dashboard.noAppointmentsSub': 'No hay citas programadas para hoy.', 'dashboard.topServicesTitle': 'Top servicios del mes', 'dashboard.topServicesSub': 'Por ingresos generados', 'dashboard.noMonthSalesSub': 'No hay ventas en el mes.', 'dashboard.attendanceTitle': 'Asistencia del mes', 'dashboard.noMonthAppointmentsSub': 'No hay citas en el mes.', 'dashboard.featuredTitle': 'Servicios destacados', 'dashboard.featuredSub': 'Catálogo activo de tratamientos', 'dashboard.noServicesTitle': 'Sin servicios', 'dashboard.noServicesSub': 'No hay servicios registrados.', 'dashboard.recentTitle': 'Actividad reciente', 'dashboard.recentSub': 'Últimos movimientos en la clínica', 'dashboard.noActivityTitle': 'Sin actividad', 'dashboard.noActivitySub': 'No hay movimientos recientes.', // --- configuración (pestañas) --- 'config.title': 'Configuración', 'config.tabs.clinica': 'Clínica', 'config.tabs.reservas': 'Reservas en línea', 'config.tabs.notificaciones': 'Notificaciones', 'config.tabs.notifEnviadas': 'Notificaciones enviadas', 'config.tabs.formularios': 'Formularios', 'config.tabs.sucursales': 'Sucursales', 'config.tabs.categorias': 'Categorías', 'config.tabs.recursos': 'Recursos', 'config.tabs.membresias': 'Membresías', 'config.tabs.compras': 'Compras', 'config.tabs.comisiones': 'Comisiones', 'config.tabs.terminologia': 'Terminología', 'config.tabs.usuarios': 'Usuarios', 'config.tabs.roles': 'Roles', 'config.tabs.recetas': 'Recetas', 'config.tabs.catalogos': 'Catálogos', 'config.tabs.tickets': 'Tickets', 'config.tabs.licencia': 'Licencia', 'config.tabs.api': 'API', 'config.tabs.webhooks': 'Webhooks', 'config.tabs.auditoria': 'Auditoría', 'config.tabs.diagnosticos': 'Diagnósticos', 'config.tabs.procedimientos': 'Procedimientos', 'config.subtitle': 'Ajustes de la clínica', 'config.pageSubtitle': 'Información y ajustes de la clínica', 'config.noPermission': 'No tienes permiso para ninguna sección de Configuración.', 'config.settings': 'Ajustes', 'config.clinicInfo': 'Información de la clínica', 'config.clinicIdentity': 'Identidad de la clínica', 'config.exchangeRate': 'Tipo de cambio USD / MXN', 'config.commissions': 'Comisiones', 'config.useThemeColor': 'Usar el color del tema', 'config.updateRate': 'Actualizar TC', // --- reportes --- 'reports.title': 'Reportes', 'reports.subtitle': 'Análisis del mes y día', 'reports.opSubtitle': 'Análisis de la operación', 'reports.export': 'Exportar', 'reports.exportData': 'Exportar datos', 'reports.exportDataTitle': 'Exportar listas de pacientes (correos, VIP, etiquetas)', 'reports.period': 'Periodo', 'reports.start': 'Inicio', 'reports.end': 'Fin', 'reports.effectiveDay': 'Día efectivo', 'reports.favAdd': 'Fijar en favoritos', 'reports.favRemove': 'Quitar de favoritos', 'reports.family.dinero': 'Dinero', 'reports.family.dinero.hint': 'Ingresos, cobros y cartera', 'reports.family.clientes': 'Clientes', 'reports.family.clientes.hint': 'Quién compra y cuánto', 'reports.family.operacion': 'Operación', 'reports.family.operacion.hint': 'Agenda, inventario y servicios', 'reports.family.equipo': 'Equipo', 'reports.family.equipo.hint': 'Rendimiento del personal', 'reports.preset.hoy': 'Hoy', 'reports.preset.ayer': 'Ayer', 'reports.preset.semana': 'Esta semana', 'reports.preset.mes': 'Este mes', 'reports.preset.mes_pasado': 'Mes pasado', 'reports.preset.anio': 'Este año', 'reports.preset.custom': 'Personalizado', 'reports.tab.general': 'General', 'reports.tab.general.desc': 'Resumen del periodo, citas y comisiones', 'reports.tab.diario': 'Movimientos diarios', 'reports.tab.diario.desc': 'Citas, ventas, cobros e inventario del día', 'reports.tab.ingresos': 'Ingresos', 'reports.tab.ingresos.desc': 'Ventas y formas de pago', 'reports.tab.impuestos': 'Impuestos', 'reports.tab.impuestos.desc': 'Base e impuesto del periodo por mes, servicio y sucursal', 'reports.tab.cortes': 'Cortes de caja', 'reports.tab.cortes.desc': 'Arqueos y cierres por día', 'reports.tab.adeudos': 'Adeudos', 'reports.tab.adeudos.desc': 'Cartera por cobrar y planes de pago', 'reports.tab.cobros': 'Cobros', 'reports.tab.cobros.desc': 'Anticipos y membresías por cobrar (pendientes y expirados)', 'reports.tab.devoluciones': 'Devoluciones', 'reports.tab.devoluciones.desc': 'Ventas devueltas y montos', 'reports.tab.top-clientes': 'Top clientes', 'reports.tab.top-clientes.desc': 'Pacientes con mayor consumo', 'reports.tab.pagos-clientes': 'Pagos · Clientes', 'reports.tab.pagos-clientes.desc': 'Cobros agrupados por paciente', 'reports.tab.recomendaciones': 'Recomendaciones', 'reports.tab.recomendaciones.desc': 'Pacientes que llegan recomendados', 'reports.tab.paquetes': 'Paquetes', 'reports.tab.paquetes.desc': 'Ventas y avance de paquetes', 'reports.tab.membresias': 'Membresías', 'reports.tab.membresias.desc': 'Altas, ingresos recurrentes y sesiones consumidas', 'reports.tab.horas-agenda': 'Horas agenda', 'reports.tab.horas-agenda.desc': 'Citas atendidas por hora y médico', 'reports.tab.recursos': 'Recursos', 'reports.tab.recursos.desc': 'Ocupación de cabinas, salas y equipos', 'reports.tab.inventario': 'Inventario', 'reports.tab.inventario.desc': 'Existencias, mínimos y caducidades', 'reports.tab.compras': 'Compras', 'reports.tab.compras.desc': 'Gasto por periodo, proveedor y artículo', 'reports.tab.margen': 'Margen', 'reports.tab.margen.desc': 'Margen por artículo: precio menos costo promedio', 'reports.tab.pagos-servicios': 'Pagos · Servicios', 'reports.tab.pagos-servicios.desc': 'Cobros agrupados por servicio', 'reports.tab.concentrado': 'Concentrado', 'reports.tab.concentrado.desc': 'Todo el periodo en una tabla', 'reports.tab.comisiones': 'Comisiones', 'reports.tab.comisiones.desc': 'Comisión por médico en el periodo', 'reports.tab.vendedores': 'Vendedores', 'reports.tab.vendedores.desc': 'Ventas atribuidas por vendedor', 'reports.tab.kpis': 'KPIs', 'reports.tab.kpis.desc': 'No-show, ocupación y conversión', 'reports.tab.exportar': 'Exportar', 'reports.tab.exportar.desc': 'Exportar datos', // --- páginas públicas (reservas / formularios) --- 'public.unavailableTitle': 'Reservas en línea no disponibles', 'public.unavailableBody': 'Por el momento no puedes agendar desde aquí. Comunícate directamente con la clínica.', 'public.others': 'Otros', 'public.errName': 'Escribe tu nombre', 'public.errPhone': 'Escribe un teléfono válido (10 dígitos)', 'public.errGeneric': 'No se pudo completar la reserva. Intenta de nuevo.', 'public.requestReceived': 'Solicitud recibida', 'public.confirmed': '¡Reserva confirmada!', 'public.folioWord': 'Folio', 'public.addCalendar': 'Agregar al calendario', 'public.manage': 'Ver o cancelar mi reserva', 'public.willConfirm': 'Tu reserva quedó registrada y la clínica la confirmará en breve.', 'public.saveLink': 'Guarda este enlace para gestionar tu reserva', 'public.copied': 'Copiado', 'public.copy': 'Copiar', 'public.stepService': 'Elige tu {term}', 'public.stepBranch': 'Elige {term}', 'public.stepDoctor': 'Elige {term}', 'public.stepDateTime': 'Elige fecha y hora', 'public.stepData': 'Tus datos', 'public.anyAvailable': 'Cualquiera disponible', 'public.noSlots': 'No hay horarios disponibles ese día. Prueba con otra fecha.', 'public.continue': 'Continuar', 'public.fullName': 'Nombre completo', 'public.phoneWhatsapp': 'Teléfono (WhatsApp)', 'public.emailOptional': 'Email (opcional)', 'public.notesOptional': 'Notas (opcional)', 'public.honeypot': 'No llenar este campo', 'public.requestBooking': 'Solicitar reserva', 'public.confirmBooking': 'Confirmar reserva', 'public.doubts': '¿Dudas?', 'public.form.errSend': 'No se pudo enviar. Intenta de nuevo.', 'public.form.errSendNet': 'No se pudo enviar. Revisa tu conexión e intenta de nuevo.', 'public.form.unavailableTitle': 'Formulario no disponible', 'public.form.unavailableBody': 'El enlace no es válido o ya expiró. Pide uno nuevo a la clínica.', 'public.form.thanks': '¡Gracias!', 'public.form.thanksName': '¡Gracias, {name}!', 'public.form.registered': 'Tu formulario quedó registrado.', 'public.form.alreadyFilled': 'Este formulario ya había sido llenado.', 'public.form.canClose': 'Ya puedes cerrar esta página.', 'public.form.form': 'Formulario', 'public.form.takesMinutes': 'Tarda unos {n} minutos.', 'public.form.legalRecord': 'Al enviar aceptas que tus datos y tu firma se guarden en el {record} de la clínica.', 'public.form.sending': 'Enviando…', 'public.form.submit': 'Enviar formulario', 'public.form.errSignature': 'Escribe el nombre de quien firma', 'public.manage.invalidLink': 'Enlace no válido', 'public.manage.notFound': 'No encontramos esta reserva.', 'public.manage.cancelError': 'No se pudo cancelar la reserva.', 'public.manage.expired': 'El enlace no es válido o ya expiró.', 'public.manage.notFoundTitle': 'No encontramos tu reserva', 'public.manage.makeBooking': 'Hacer una reserva', 'public.manage.bookAnother': 'Reservar otra cita', 'public.manage.title': 'Tu reserva', 'public.manage.cancelledOk': 'Tu reserva quedó cancelada. Si fue un error, puedes agendar de nuevo.', 'public.manage.cancelMine': 'Cancelar mi reserva', 'public.manage.confirmQuestion': '¿Seguro que quieres cancelar? Puedes dejarnos un motivo (opcional).', 'public.manage.reason': 'Motivo (opcional)', 'public.manage.yesCancel': 'Sí, cancelar', 'public.manage.cannotCancel': 'Esta reserva ya no se puede cancelar en línea', 'public.manage.leadTime': ' (con menos de {h} h de anticipación)', 'public.manage.contactPhone': ' Comunícate al {phone}.', 'public.manage.contactClinic': ' Comunícate con la clínica.', // Anticipo al reservar en línea 'public.awaitingDeposit': 'Reserva retenida: falta tu anticipo', 'public.depositAmount': 'Anticipo a pagar: {amount}', 'public.depositHelp': 'Tienes {h} horas para pagar el anticipo; si no se paga, la reserva se libera.', 'public.payDeposit': 'Pagar anticipo', 'public.depositExpires': 'El enlace vence en {h} horas', // --- Página pública de pago de un enlace de cobro --- 'pay.title': 'Pago en línea', 'pay.folioWord': 'Folio', 'pay.concept': 'Concepto', 'pay.amount': 'Monto', 'pay.forPatient': 'A nombre de {name}', 'pay.payNow': 'Pagar ahora', 'pay.paySimulated': 'Confirmar pago (modo prueba)', 'pay.paidOk': '¡Pago registrado!', 'pay.receipt': 'Comprobante', 'pay.date': 'Fecha', 'pay.notAvailable': 'El pago en línea no está disponible en este momento. Comunícate con la clínica.', 'pay.expiresAt': 'Este enlace vence el {date}', 'pay.expiredBody': 'Este enlace ya expiró. Pide uno nuevo a la clínica.', 'pay.cancelledBody': 'Este enlace fue cancelado.', 'pay.unavailableTitle': 'Enlace no disponible', 'pay.invalidLink': 'Enlace no válido', 'pay.notFound': 'No encontramos este enlace de pago.', 'pay.payError': 'No se pudo completar el pago. Intenta de nuevo.', 'pay.simulatedNote': 'Modo de prueba: no se cobra dinero real.', 'pay.secureNote': 'Pago protegido por la pasarela de la clínica.', 'pay.state.pagado': 'Pagado', // --- POS --- 'pos.title': 'Punto de Venta', 'pos.subtitle': 'Arma el ticket y cobra en un solo paso', 'pos.method.cash': 'Efectivo', 'pos.method.card': 'Tarjeta', 'pos.method.transfer': 'Transferencia', 'pos.method.credit': 'Saldo a favor', 'pos.methodLabel': 'Método de pago', 'pos.points': 'Puntos', 'pos.discount': 'Descuento', 'pos.taxIncluded': ' (incluido)', 'pos.chargeAmount': 'Cobrar {amount}', 'pos.chargeTitle': 'Cobrar (F9)', 'pos.noSellPerm': 'Sin permiso para vender', 'pos.noDiscountPerm': 'Sin permiso para aplicar descuentos', 'pos.setRate': 'Configura el tipo de cambio en Configuración', 'pos.add': 'Agregar', 'pos.addToTicket': 'Agregar al ticket', 'pos.emptyTicket': 'Vaciar ticket', 'pos.confirmEmptyTicket': '¿Vaciar el ticket?', 'pos.shortcutsHint': 'F2 buscar · F9 cobrar · +/− cantidad · Esc cerrar', 'pos.viewShortcuts': 'Ver atajos', 'pos.searchService': 'Buscar servicio... (F2)', 'pos.searchPackage': 'Buscar paquete... (F2)', 'pos.searchItem': 'Buscar artículo... (F2)', 'pos.searchPatient': 'Buscar paciente por nombre o teléfono...', 'pos.nameRequired': 'Nombre *', 'pos.phoneRequired': 'Teléfono *', 'pos.received': 'Recibido', 'pos.receivedUsd': 'Recibido (USD)', 'pos.change': 'Cambio: ', 'pos.initialCash': 'Efectivo inicial (fondo de caja)', 'pos.unitPrice': 'Precio unitario', // --- pacientes --- 'patients.subtitle': 'Directorio de pacientes', 'patients.pageSubtitle': 'Busca, crea y gestiona pacientes', 'patients.exportCsv': 'Exportar CSV', 'patients.newWord': 'Nuevo', 'patients.noExportPerm': 'Sin permiso para exportar', 'patients.searchPlaceholder': 'Buscar por nombre, apellidos o teléfono...', 'patients.clearSearch': 'Limpiar búsqueda', 'patients.allDoctors': 'Todos los médicos', 'patients.filterRecent': 'Visitas últimos 6 meses', 'patients.filterStale': 'Sin visita +6 meses', 'patients.filterIncomplete': 'Expediente incompleto', 'patients.clearFilters': 'Quitar filtros', 'patients.emptyTitle': 'Sin pacientes', 'patients.emptySub': 'No se encontraron pacientes.', 'patients.colName': 'Nombre', 'patients.colRecord': 'Expediente', 'patients.colPhone': 'Teléfono', 'patients.colAge': 'Edad', 'patients.colLastVisit': 'Última visita', 'patients.colPoints': 'Puntos', 'patients.colActions': 'Acciones', 'patients.colService': 'Servicio', 'patients.colQty': 'Cant.', 'patients.colPrice': 'Precio', 'patients.complete': 'Expediente completo', 'patients.missingRecord': 'Falta: {list}', 'patients.unassigned': 'Sin asignar', 'patients.tabInfo': 'Información general', 'patients.tabClinical': 'Historia clínica', 'patients.tabTimeline': 'Historial', 'patients.tabDocs': 'Documentos', 'patients.tabForms': 'Formularios', 'patients.scannedRecord': 'Expediente escaneado', // --- agenda --- 'agenda.subtitle': 'Gestión de citas', 'agenda.pageSubtitle': 'Filtra por fecha, estado o médico', 'agenda.waitlist': 'Lista de espera', 'agenda.closeAgendas': 'Cerrar agendas', 'agenda.express': 'Valoración express', 'agenda.newAppointmentWord': 'Nueva', 'agenda.newAppointment': 'Nueva {term}', 'agenda.editAppointment': 'Editar {term}', 'agenda.freeSlots': 'Espacios libres', 'agenda.filterFirstTime': 'Primera vez', 'agenda.filterCheckin': 'Check-in', 'agenda.filterNoShow': 'No show', 'agenda.filterToConfirm': 'Por confirmar', 'agenda.filterCancelled': 'Canceló', 'agenda.filterWaitlist': 'En lista', 'agenda.stateAll': 'Todos los estados', 'agenda.state.pending': 'Agendada', 'agenda.state.confirmed': 'Confirmada', 'agenda.state.arrived': 'En {room}', 'agenda.state.inProgress': 'En consulta', 'agenda.state.done': 'Completada', 'agenda.state.cancelled': 'Canceló', 'agenda.state.noShow': 'No se presentó', 'agenda.state.waitlist': 'Lista de espera', 'agenda.quickMode': 'Cita rápida (solo nombre y teléfono)', 'agenda.quickModeHint': 'Si el teléfono ya existe, se reutiliza ese paciente; si no, se crea con datos mínimos.', 'agenda.nameField': 'Nombre *', 'agenda.namePlaceholder': 'Nombre del paciente', 'agenda.phoneField': 'Teléfono *', 'agenda.phonePlaceholder': 'Teléfono del paciente', 'agenda.saveChanges': 'Guardar cambios', 'agenda.saveQuick': 'Guardar cita rápida', 'agenda.saveAppointment': 'Guardar cita', 'agenda.expressTitle': 'Valoración express', 'agenda.rescheduleTitle': 'Reagendar cita', 'agenda.detailTitle': 'Detalle de la cita', 'agenda.conflictTitle': 'Conflicto de horario', 'agenda.understood': 'Entendido', 'agenda.back': 'Volver', 'agenda.noDoctorsTitle': 'Sin médicos', 'agenda.noDoctorsSub': 'No hay médicos dados de alta para mostrar en la vista por médico.', 'agenda.noDoctorsSubHour': 'No hay médicos dados de alta para mostrar en la vista por hora.', 'agenda.noDoctorsSubWeek': 'No hay médicos dados de alta para mostrar en la vista semanal.', 'agenda.noResourcesTitle': 'Sin recursos', 'agenda.noResourcesSub': 'Agrega recursos en Configuración › Recursos para ver esta vista.', 'agenda.depositPending': 'Esperando anticipo', 'config.idiomaTitle': 'Idioma de la interfaz', 'config.idiomaBody': 'Elige el idioma con el que se muestra el sistema en este navegador.', // Anticipo de reservas en línea 'config.anticipo.title': 'Anticipo al reservar', 'config.anticipo.help': 'Pide un depósito al reservar en línea. La reserva queda retenida sin confirmar hasta que se pague; si no se paga a tiempo, se libera el horario.', 'config.anticipo.enable': 'Requerir anticipo', 'config.anticipo.enableHelp': 'Apagado, el portal reserva sin pedir dinero (como hasta hoy).', 'config.anticipo.type': 'Tipo de anticipo', 'config.anticipo.typePercent': 'Porcentaje del servicio', 'config.anticipo.typeFixed': 'Monto fijo', 'config.anticipo.value': 'Valor (porcentaje o monto)', 'config.anticipo.currency': 'Moneda', 'config.anticipo.hours': 'Horas para pagar', 'config.anticipo.refundable': 'Reembolsable', 'config.anticipo.refundableHelp': 'Si se cancela la cita, el anticipo se devuelve.', 'config.anticipo.simulated': 'Permitir pago simulado', 'config.anticipo.simulatedHelp': 'Permite probar el flujo sin pasarela ni dinero real. Déjalo apagado en producción.', 'config.anticipo.notice': 'Aviso para el cliente', 'config.anticipo.noticePlaceholder': 'Se requiere un anticipo para confirmar tu cita.', // Cobro automático de membresías 'config.cobroAuto.title': 'Cobro automático de membresías', 'config.cobroAuto.help': 'Genera el enlace de pago del periodo y lo envía al paciente por notificaciones, con reintentos. El periodo queda pendiente hasta que se pague.', 'config.cobroAuto.enable': 'Activar cobro automático', 'config.cobroAuto.enableHelp': 'Apagado, el proceso solo genera el adeudo (no cobra ni avisa).', 'config.cobroAuto.method': 'Método', 'config.cobroAuto.methodLink': 'Enlace de pago', 'config.cobroAuto.methodCard': 'Tarjeta guardada (Stripe)', 'config.cobroAuto.daysBefore': 'Días antes del vencimiento', 'config.cobroAuto.grace': 'Días de gracia', 'config.cobroAuto.retries': 'Días de reintento del aviso', 'config.cobroAuto.retriesHelp': 'Días (respecto al vencimiento) en que se reintenta avisar, separados por coma.', 'config.cobroAuto.cancel': 'Vencer si no paga', 'config.cobroAuto.cancelHelp': 'Al agotar los reintentos, marca la membresía vencida. Apagado, sigue activa hasta el proceso normal de vencimiento.', 'config.cobroAuto.simulated': 'Permitir pago simulado', 'config.cobroAuto.simulatedHelp': 'Permite probar el flujo sin pasarela ni dinero real. Déjalo apagado en producción.', }; // ------------------------------------------------------------ // Diccionario INGLÉS. // ------------------------------------------------------------ export const en: Dict = { // --- common --- 'common.save': 'Save', 'common.cancel': 'Cancel', 'common.close': 'Close', 'common.delete': 'Delete', 'common.edit': 'Edit', 'common.search': 'Search', 'common.all': 'All', 'common.new': 'New', 'common.loading': 'Loading…', 'common.export': 'Export', 'common.filter': 'Filter', 'common.apply': 'Apply', 'common.clear': 'Clear', 'common.viewAll': 'View all', 'common.actions': 'Actions', 'common.status': 'Status', 'common.date': 'Date', 'common.time': 'Time', 'common.total': 'Total', 'common.subtotal': 'Subtotal', 'common.notes': 'Notes', 'common.name': 'Name', 'common.phone': 'Phone', 'common.email': 'Email', 'common.yes': 'Yes', 'common.no': 'No', 'common.back': 'Back', 'common.next': 'Next', 'common.accept': 'OK', 'common.select': 'Select', 'common.optional': 'Optional', 'common.required': 'Required', 'common.none': 'None', 'common.saving': 'Saving...', // --- app --- 'app.bootTitle': 'Could not connect', 'app.bootBody': 'The server is not responding right now. Check your internet connection and try again.', 'app.retry': 'Retry', 'app.noAccessTitle': 'No module access', 'app.noAccessBody': 'Your user has no sections enabled. Contact the administrator.', // --- login --- 'login.welcome': 'Welcome', 'login.subtitle': 'Sign in to continue', 'login.username': 'Username', 'login.password': 'Password', 'login.submit': 'Sign in', 'login.errRequired': 'Enter username and password', 'login.errInvalid': 'Incorrect username or password', 'login.errGeneric': 'Could not sign in. Please try again.', 'login.bookOnline': 'Book online', 'login.showPassword': 'Show password', 'login.hidePassword': 'Hide password', // --- PWA --- 'pwa.installTitle': 'Install the app', 'pwa.installHint': 'Open it faster from your home screen.', 'pwa.iosStepPrefix': 'Tap', 'pwa.iosStepSuffix': 'Share and then “Add to Home Screen”.', 'pwa.install': 'Install', 'pwa.dismiss': 'Dismiss', 'pwa.offline': 'Offline — changes will be saved once the network returns', // --- quick links dock --- 'dock.aria': 'Quick links', 'dock.hint': 'Pin links with the ★ in the menu', 'dock.changeMode': 'Change quick links layout', 'dock.showModes': 'Show quick links', 'dock.mode.menu': 'In the side menu', 'dock.mode.right': 'Icon bar on the right', 'dock.mode.bottom': 'Icon bar at the bottom', 'dock.finishEditing': 'Finish editing', 'dock.editQuick': 'Edit quick links', 'dock.up': 'Move up', 'dock.down': 'Move down', 'dock.remove': 'Remove', // --- nav --- 'nav.group.principal': 'Main', 'nav.group.clinica': 'Clinic', 'nav.group.operaciones': 'Operations', 'nav.group.analitica': 'Analytics', 'nav.group.analiticaMas': 'Analytics & more', 'nav.group.comunicacion': 'Communication', 'nav.group.wacrm': 'WACRM', 'nav.quickLinks': 'Quick links', 'nav.dashboard': 'Dashboard', 'nav.agenda': 'Schedule', 'nav.consultas': 'Consultations', 'nav.mySales': 'My sales', 'nav.pos': 'Point of Sale', 'nav.sales': 'Sales', 'nav.tickets': 'Tickets', 'nav.payments': 'Payments', 'nav.debts': 'Debts', 'nav.wallet': 'Wallet', 'nav.inventory': 'Inventory', 'nav.purchases': 'Purchases', 'nav.cashCuts': 'Cash Register', 'nav.reports': 'Reports', 'nav.commissions': 'Commissions', 'nav.agendaHours': 'Schedule hours', 'nav.exportData': 'Export data', 'nav.config': 'Settings', 'nav.chat': 'Chat', 'nav.birthdays': 'Birthdays', 'nav.messages': 'Messages', 'nav.leads': 'Leads', 'nav.memberships': 'Memberships', 'nav.liquidations': 'Settlements', 'nav.packages': 'Packages', // --- classic theme menu labels --- 'clasico.inicio': 'Home', 'clasico.visitas': 'Visits', 'clasico.consultasMedicas': 'Medical consultations', 'clasico.expedientes': 'Records', 'clasico.medicos': 'Doctors', 'clasico.cosmetologia': 'Cosmetology', 'clasico.clinica': 'Clinic', 'clasico.usuarios': 'Users', 'clasico.recetas': 'Prescriptions', 'clasico.diagnosticos': 'Diagnoses', 'clasico.procedimientos': 'Medical procedures', 'clasico.administrativo': 'Administrative', 'clasico.movDiario': 'Daily movements', 'clasico.cortesCaja': 'Cash closings', 'clasico.ingresos': 'Revenue', 'clasico.exportarCorreos': 'Export emails', 'clasico.adeudosClientes': 'Client debts', 'clasico.pagosServicios': 'Payments | Services', 'clasico.pagosClientes': 'Payments | Clients', 'clasico.devoluciones': 'Refunds', 'clasico.vendedores': 'Sellers', 'clasico.recomendaciones': 'Referrals', 'clasico.concentrado': 'Summary', 'clasico.topVentas': 'Top sales', 'clasico.monedero': 'Wallet', 'clasico.monederos': 'Wallets', 'clasico.reportePuntos': 'Points report', 'clasico.comunicacion': 'Communication', 'clasico.crm': 'CRM', 'clasico.comprobantes': 'Daily receipts', 'clasico.consumibles': 'Consumables', 'clasico.inventarioActual': 'Current inventory', 'clasico.bajas': 'Write-offs', 'clasico.ordenesCompra': 'Purchase orders', 'clasico.ventasDia': 'Daily sales', 'clasico.clientes': 'Clients', // --- user / layout --- 'user.logout': 'Sign out', 'user.expandMenu': 'Expand menu', 'user.collapseMenu': 'Collapse menu', 'user.openMenu': 'Open menu', 'user.closeMenu': 'Close menu', 'user.pinQuick': 'Pin to quick links', 'user.unpinQuick': 'Remove from quick links', 'user.language': 'Language', 'user.more': 'More', 'user.goalTitle': 'Monthly sales goal', 'user.goalMonth': 'Monthly goal', 'user.goalOf': 'of', 'user.goalDefine': 'Set goal', 'user.goalSave': 'Save goal', 'user.goalField': 'Monthly goal (MXN)', 'user.goalBody': 'Set the sales target (MXN) for the current month. The header bar shows progress in real time.', 'user.goalEditHint': 'Click to edit the goal', 'user.goalMonthHint': 'Monthly sales goal', 'search.placeholder': 'Search patient, appointment, service...', 'search.aria': 'Global search', 'search.clear': 'Clear search', 'search.noResults': 'No results for "{q}". Try another name or phone.', 'search.hintKeys': 'Use ↑ ↓ to move and Enter to open', 'search.group.Pacientes': 'Patients', 'search.group.Citas próximas': 'Upcoming appointments', 'search.group.Servicios': 'Services', 'notif.title': 'Notifications', 'notif.loading': 'Loading...', 'notif.empty': 'No notifications', 'role.admin': 'Administrator', 'role.medico': 'Doctor', 'role.recepcion': 'Front desk', 'role.enfermeria': 'Nursing', 'role.cosmetologia': 'Cosmetology', 'role.psicologa': 'Psychologist', 'role.farmacia': 'Pharmacy', 'role.gerencia': 'Management', 'role.marketing': 'Marketing', 'role.lectura': 'Read only', // --- dashboard --- 'dashboard.title': 'Dashboard', 'dashboard.subtitleBrand': 'Control panel for {brand}', 'dashboard.subtitle': 'Clinic overview', 'dashboard.welcome': 'Welcome', 'dashboard.welcomeUser': 'Welcome, {name}', 'dashboard.welcomeBack': 'Welcome back', 'dashboard.theClinic': 'the clinic', 'dashboard.newAppointmentWord': 'New', 'dashboard.checking': 'Checking…', 'dashboard.stateDone': 'Completed', 'dashboard.stateConfirmed': 'Confirmed', 'dashboard.stateCancelled': 'Cancelled', 'dashboard.ariaUpcoming': 'View upcoming appointments in the schedule', 'dashboard.ariaWaiting': 'View patients in the waiting room', 'dashboard.ariaCash': 'Go to cash register', 'dashboard.ariaCharges': 'View the day charges in sales', 'dashboard.openPatient': 'Open patient record', 'dashboard.vip': 'VIP', 'dashboard.heroText': 'Science, aesthetics and personalized care in every treatment. Manage your clinic with confidence.', 'dashboard.heroTextNest': 'Science, aesthetics and personalized care in every treatment. Manage your clinic with warmth and confidence.', 'dashboard.viewReports': 'View reports', 'dashboard.newAppointment': 'New appointment', 'dashboard.todayTitle': 'Today at the clinic', 'dashboard.upcoming': 'Upcoming appointments', 'dashboard.noUpcoming': 'No active appointments for today.', 'dashboard.waitingRoom': 'Waiting room', 'dashboard.waiting_one': 'patient waiting', 'dashboard.waiting_other': 'patients waiting', 'dashboard.notAvailable': 'not available', 'dashboard.cash': 'Register', 'dashboard.cashOpen': 'Open', 'dashboard.cashClosed': 'Closed', 'dashboard.cashNone': 'Not opened', 'dashboard.noCashCut': 'No cash closing today', 'dashboard.dayCharges': 'Day charges', 'dashboard.completedPayments': 'payments completed today', 'dashboard.statAppointments': 'Appointments Today', 'dashboard.statPatients': 'Patients', 'dashboard.statRevenue': 'Revenue This Month', 'dashboard.statSales': 'Sales Today', 'dashboard.statPending': 'Pending', 'dashboard.statWallet': 'Wallet Points', 'dashboard.thisMonth': '{n} this month', 'dashboard.newPatients': '+{n} new', 'dashboard.vsPrevMonth': 'vs previous month', 'dashboard.vsYesterday': 'vs yesterday', 'dashboard.birthdaysTitle': 'Birthdays today', 'dashboard.birthdaysSub': 'Patients with a birthday today — reach out to them', 'dashboard.noBirthdays': 'No patient has a birthday today.', 'dashboard.turning': 'turns {age}', 'dashboard.noDoctor': 'No doctor', 'dashboard.chartWeeklyTitle': 'Weekly activity', 'dashboard.chartWeeklySub': 'Appointments and revenue for the last 7 days', 'dashboard.noDataTitle': 'No data', 'dashboard.noWeeklySub': 'No information for the weekly chart.', 'dashboard.upcomingTodaySub': 'Appointments scheduled for today', 'dashboard.noAppointmentsTitle': 'No appointments', 'dashboard.noAppointmentsSub': 'No appointments scheduled for today.', 'dashboard.topServicesTitle': 'Top services this month', 'dashboard.topServicesSub': 'By revenue generated', 'dashboard.noMonthSalesSub': 'No sales this month.', 'dashboard.attendanceTitle': 'Monthly attendance', 'dashboard.noMonthAppointmentsSub': 'No appointments this month.', 'dashboard.featuredTitle': 'Featured services', 'dashboard.featuredSub': 'Active treatment catalog', 'dashboard.noServicesTitle': 'No services', 'dashboard.noServicesSub': 'No services registered.', 'dashboard.recentTitle': 'Recent activity', 'dashboard.recentSub': 'Latest movements in the clinic', 'dashboard.noActivityTitle': 'No activity', 'dashboard.noActivitySub': 'No recent movements.', // --- config --- 'config.title': 'Settings', 'config.tabs.clinica': 'Clinic', 'config.tabs.reservas': 'Online booking', 'config.tabs.notificaciones': 'Notifications', 'config.tabs.notifEnviadas': 'Sent notifications', 'config.tabs.formularios': 'Forms', 'config.tabs.sucursales': 'Branches', 'config.tabs.categorias': 'Categories', 'config.tabs.recursos': 'Resources', 'config.tabs.membresias': 'Memberships', 'config.tabs.compras': 'Purchases', 'config.tabs.comisiones': 'Commissions', 'config.tabs.terminologia': 'Terminology', 'config.tabs.usuarios': 'Users', 'config.tabs.roles': 'Roles', 'config.tabs.recetas': 'Prescriptions', 'config.tabs.catalogos': 'Catalogs', 'config.tabs.tickets': 'Tickets', 'config.tabs.licencia': 'License', 'config.tabs.api': 'API', 'config.tabs.webhooks': 'Webhooks', 'config.tabs.auditoria': 'Audit log', 'config.tabs.diagnosticos': 'Diagnoses', 'config.tabs.procedimientos': 'Procedures', 'config.subtitle': 'Clinic settings', 'config.pageSubtitle': 'Clinic information and settings', 'config.noPermission': 'You do not have permission for any Settings section.', 'config.settings': 'Settings', 'config.clinicInfo': 'Clinic information', 'config.clinicIdentity': 'Clinic identity', 'config.exchangeRate': 'Exchange rate USD / MXN', 'config.commissions': 'Commissions', 'config.useThemeColor': 'Use theme color', 'config.updateRate': 'Update rate', // --- reports --- 'reports.title': 'Reports', 'reports.subtitle': 'Month and day analysis', 'reports.opSubtitle': 'Operation analysis', 'reports.export': 'Export', 'reports.exportData': 'Export data', 'reports.exportDataTitle': 'Export patient lists (emails, VIP, labels)', 'reports.period': 'Period', 'reports.start': 'Start', 'reports.end': 'End', 'reports.effectiveDay': 'Effective day', 'reports.favAdd': 'Pin to favorites', 'reports.favRemove': 'Remove from favorites', 'reports.family.dinero': 'Money', 'reports.family.dinero.hint': 'Revenue, charges and receivables', 'reports.family.clientes': 'Clients', 'reports.family.clientes.hint': 'Who buys and how much', 'reports.family.operacion': 'Operation', 'reports.family.operacion.hint': 'Schedule, inventory and services', 'reports.family.equipo': 'Team', 'reports.family.equipo.hint': 'Staff performance', 'reports.preset.hoy': 'Today', 'reports.preset.ayer': 'Yesterday', 'reports.preset.semana': 'This week', 'reports.preset.mes': 'This month', 'reports.preset.mes_pasado': 'Last month', 'reports.preset.anio': 'This year', 'reports.preset.custom': 'Custom', 'reports.tab.general': 'General', 'reports.tab.general.desc': 'Period summary, appointments and commissions', 'reports.tab.diario': 'Daily movements', 'reports.tab.diario.desc': 'Appointments, sales, charges and inventory for the day', 'reports.tab.ingresos': 'Revenue', 'reports.tab.ingresos.desc': 'Sales and payment methods', 'reports.tab.impuestos': 'Taxes', 'reports.tab.impuestos.desc': 'Base and tax for the period by month, service and branch', 'reports.tab.cortes': 'Cash closings', 'reports.tab.cortes.desc': 'Daily counts and closings', 'reports.tab.adeudos': 'Debts', 'reports.tab.adeudos.desc': 'Accounts receivable and payment plans', 'reports.tab.cobros': 'Charges', 'reports.tab.cobros.desc': 'Deposits and memberships pending collection (pending and expired)', 'reports.tab.devoluciones': 'Refunds', 'reports.tab.devoluciones.desc': 'Returned sales and amounts', 'reports.tab.top-clientes': 'Top clients', 'reports.tab.top-clientes.desc': 'Patients with the highest spend', 'reports.tab.pagos-clientes': 'Payments · Clients', 'reports.tab.pagos-clientes.desc': 'Charges grouped by patient', 'reports.tab.recomendaciones': 'Referrals', 'reports.tab.recomendaciones.desc': 'Patients who arrive by referral', 'reports.tab.paquetes': 'Packages', 'reports.tab.paquetes.desc': 'Package sales and progress', 'reports.tab.membresias': 'Memberships', 'reports.tab.membresias.desc': 'Sign-ups, recurring revenue and sessions used', 'reports.tab.horas-agenda': 'Schedule hours', 'reports.tab.horas-agenda.desc': 'Appointments by hour and doctor', 'reports.tab.recursos': 'Resources', 'reports.tab.recursos.desc': 'Room and equipment occupancy', 'reports.tab.inventario': 'Inventory', 'reports.tab.inventario.desc': 'Stock, minimums and expirations', 'reports.tab.compras': 'Purchases', 'reports.tab.compras.desc': 'Spending by period, supplier and item', 'reports.tab.margen': 'Margin', 'reports.tab.margen.desc': 'Margin per item: price minus average cost', 'reports.tab.pagos-servicios': 'Payments · Services', 'reports.tab.pagos-servicios.desc': 'Charges grouped by service', 'reports.tab.concentrado': 'Summary', 'reports.tab.concentrado.desc': 'The whole period in one table', 'reports.tab.comisiones': 'Commissions', 'reports.tab.comisiones.desc': 'Commission per doctor in the period', 'reports.tab.vendedores': 'Sellers', 'reports.tab.vendedores.desc': 'Sales attributed per seller', 'reports.tab.kpis': 'KPIs', 'reports.tab.kpis.desc': 'No-show, occupancy and conversion', 'reports.tab.exportar': 'Export', 'reports.tab.exportar.desc': 'Export data', // --- public pages (booking / forms) --- 'public.unavailableTitle': 'Online booking unavailable', 'public.unavailableBody': 'You cannot book from here right now. Please contact the clinic directly.', 'public.others': 'Others', 'public.errName': 'Enter your name', 'public.errPhone': 'Enter a valid phone number (10 digits)', 'public.errGeneric': 'Could not complete the booking. Please try again.', 'public.requestReceived': 'Request received', 'public.confirmed': 'Booking confirmed!', 'public.folioWord': 'Reference', 'public.addCalendar': 'Add to calendar', 'public.manage': 'View or cancel my booking', 'public.willConfirm': 'Your booking was registered and the clinic will confirm it shortly.', 'public.saveLink': 'Save this link to manage your booking', 'public.copied': 'Copied', 'public.copy': 'Copy', 'public.stepService': 'Choose your {term}', 'public.stepBranch': 'Choose {term}', 'public.stepDoctor': 'Choose {term}', 'public.stepDateTime': 'Choose date and time', 'public.stepData': 'Your details', 'public.anyAvailable': 'Anyone available', 'public.noSlots': 'No available times that day. Try another date.', 'public.continue': 'Continue', 'public.fullName': 'Full name', 'public.phoneWhatsapp': 'Phone (WhatsApp)', 'public.emailOptional': 'Email (optional)', 'public.notesOptional': 'Notes (optional)', 'public.honeypot': 'Do not fill this field', 'public.requestBooking': 'Request booking', 'public.confirmBooking': 'Confirm booking', 'public.doubts': 'Questions?', 'public.form.errSend': 'Could not send. Please try again.', 'public.form.errSendNet': 'Could not send. Check your connection and try again.', 'public.form.unavailableTitle': 'Form unavailable', 'public.form.unavailableBody': 'The link is invalid or has expired. Ask the clinic for a new one.', 'public.form.thanks': 'Thank you!', 'public.form.thanksName': 'Thank you, {name}!', 'public.form.registered': 'Your form was submitted.', 'public.form.alreadyFilled': 'This form had already been filled out.', 'public.form.canClose': 'You can close this page now.', 'public.form.form': 'Form', 'public.form.takesMinutes': 'Takes about {n} minutes.', 'public.form.legalRecord': 'By submitting you agree that your data and signature are stored in the clinic {record}.', 'public.form.sending': 'Sending…', 'public.form.submit': 'Submit form', 'public.form.errSignature': 'Enter the name of the signer', 'public.manage.invalidLink': 'Invalid link', 'public.manage.notFound': 'We could not find this booking.', 'public.manage.cancelError': 'Could not cancel the booking.', 'public.manage.expired': 'The link is invalid or has expired.', 'public.manage.notFoundTitle': 'We could not find your booking', 'public.manage.makeBooking': 'Make a booking', 'public.manage.bookAnother': 'Book another appointment', 'public.manage.title': 'Your booking', 'public.manage.cancelledOk': 'Your booking was cancelled. If it was a mistake, you can book again.', 'public.manage.cancelMine': 'Cancel my booking', 'public.manage.confirmQuestion': 'Are you sure you want to cancel? You can leave us a reason (optional).', 'public.manage.reason': 'Reason (optional)', 'public.manage.yesCancel': 'Yes, cancel', 'public.manage.cannotCancel': 'This booking can no longer be cancelled online', 'public.manage.leadTime': ' (less than {h} h in advance)', 'public.manage.contactPhone': ' Contact us at {phone}.', 'public.manage.contactClinic': ' Contact the clinic.', 'public.awaitingDeposit': 'Booking held: deposit pending', 'public.depositAmount': 'Deposit due: {amount}', 'public.depositHelp': 'You have {h} hours to pay the deposit; otherwise the booking is released.', 'public.payDeposit': 'Pay deposit', 'public.depositExpires': 'The link expires in {h} hours', 'pay.title': 'Online payment', 'pay.folioWord': 'Reference', 'pay.concept': 'Concept', 'pay.amount': 'Amount', 'pay.forPatient': 'For {name}', 'pay.payNow': 'Pay now', 'pay.paySimulated': 'Confirm payment (test mode)', 'pay.paidOk': 'Payment recorded!', 'pay.receipt': 'Receipt', 'pay.date': 'Date', 'pay.notAvailable': 'Online payment is not available right now. Please contact the clinic.', 'pay.expiresAt': 'This link expires on {date}', 'pay.expiredBody': 'This link has expired. Please ask the clinic for a new one.', 'pay.cancelledBody': 'This link was cancelled.', 'pay.unavailableTitle': 'Link unavailable', 'pay.invalidLink': 'Invalid link', 'pay.notFound': 'We could not find this payment link.', 'pay.payError': 'The payment could not be completed. Please try again.', 'pay.simulatedNote': 'Test mode: no real money is charged.', 'pay.secureNote': 'Payment protected by the clinic payment gateway.', 'pay.state.pagado': 'Paid', // --- POS --- 'pos.title': 'Point of Sale', 'pos.subtitle': 'Build the ticket and charge in one step', 'pos.method.cash': 'Cash', 'pos.method.card': 'Card', 'pos.method.transfer': 'Transfer', 'pos.method.credit': 'Store credit', 'pos.methodLabel': 'Payment method', 'pos.points': 'Points', 'pos.discount': 'Discount', 'pos.taxIncluded': ' (included)', 'pos.chargeAmount': 'Charge {amount}', 'pos.chargeTitle': 'Charge (F9)', 'pos.noSellPerm': 'No permission to sell', 'pos.noDiscountPerm': 'No permission to apply discounts', 'pos.setRate': 'Set the exchange rate in Settings', 'pos.add': 'Add', 'pos.addToTicket': 'Add to ticket', 'pos.emptyTicket': 'Empty ticket', 'pos.confirmEmptyTicket': 'Empty the ticket?', 'pos.shortcutsHint': 'F2 search · F9 charge · +/− quantity · Esc close', 'pos.viewShortcuts': 'View shortcuts', 'pos.searchService': 'Search service... (F2)', 'pos.searchPackage': 'Search package... (F2)', 'pos.searchItem': 'Search item... (F2)', 'pos.searchPatient': 'Search patient by name or phone...', 'pos.nameRequired': 'Name *', 'pos.phoneRequired': 'Phone *', 'pos.received': 'Received', 'pos.receivedUsd': 'Received (USD)', 'pos.change': 'Change: ', 'pos.initialCash': 'Opening cash (register float)', 'pos.unitPrice': 'Unit price', // --- patients --- 'patients.subtitle': 'Patient directory', 'patients.pageSubtitle': 'Search, create and manage patients', 'patients.exportCsv': 'Export CSV', 'patients.newWord': 'New', 'patients.noExportPerm': 'No permission to export', 'patients.searchPlaceholder': 'Search by name, surname or phone...', 'patients.clearSearch': 'Clear search', 'patients.allDoctors': 'All doctors', 'patients.filterRecent': 'Visits in the last 6 months', 'patients.filterStale': 'No visit in +6 months', 'patients.filterIncomplete': 'Incomplete record', 'patients.clearFilters': 'Clear filters', 'patients.emptyTitle': 'No patients', 'patients.emptySub': 'No patients found.', 'patients.colName': 'Name', 'patients.colRecord': 'Record', 'patients.colPhone': 'Phone', 'patients.colAge': 'Age', 'patients.colLastVisit': 'Last visit', 'patients.colPoints': 'Points', 'patients.colActions': 'Actions', 'patients.colService': 'Service', 'patients.colQty': 'Qty', 'patients.colPrice': 'Price', 'patients.complete': 'Record complete', 'patients.missingRecord': 'Missing: {list}', 'patients.unassigned': 'Unassigned', 'patients.tabInfo': 'General information', 'patients.tabClinical': 'Clinical history', 'patients.tabTimeline': 'History', 'patients.tabDocs': 'Documents', 'patients.tabForms': 'Forms', 'patients.scannedRecord': 'Scanned record', // --- agenda --- 'agenda.subtitle': 'Appointment management', 'agenda.pageSubtitle': 'Filter by date, status or doctor', 'agenda.waitlist': 'Waitlist', 'agenda.closeAgendas': 'Close schedules', 'agenda.express': 'Express assessment', 'agenda.newAppointmentWord': 'New', 'agenda.newAppointment': 'New {term}', 'agenda.editAppointment': 'Edit {term}', 'agenda.freeSlots': 'Free slots', 'agenda.filterFirstTime': 'First visit', 'agenda.filterCheckin': 'Check-in', 'agenda.filterNoShow': 'No show', 'agenda.filterToConfirm': 'To confirm', 'agenda.filterCancelled': 'Cancelled', 'agenda.filterWaitlist': 'On waitlist', 'agenda.stateAll': 'All statuses', 'agenda.state.pending': 'Scheduled', 'agenda.state.confirmed': 'Confirmed', 'agenda.state.arrived': 'In {room}', 'agenda.state.inProgress': 'In consultation', 'agenda.state.done': 'Completed', 'agenda.state.cancelled': 'Cancelled', 'agenda.state.noShow': 'No-show', 'agenda.state.waitlist': 'Waitlist', 'agenda.quickMode': 'Quick appointment (name and phone only)', 'agenda.quickModeHint': 'If the phone already exists, that patient is reused; otherwise a new one is created with minimal data.', 'agenda.nameField': 'Name *', 'agenda.namePlaceholder': 'Patient name', 'agenda.phoneField': 'Phone *', 'agenda.phonePlaceholder': 'Patient phone', 'agenda.saveChanges': 'Save changes', 'agenda.saveQuick': 'Save quick appointment', 'agenda.saveAppointment': 'Save appointment', 'agenda.expressTitle': 'Express assessment', 'agenda.rescheduleTitle': 'Reschedule appointment', 'agenda.detailTitle': 'Appointment details', 'agenda.conflictTitle': 'Schedule conflict', 'agenda.understood': 'Understood', 'agenda.back': 'Back', 'agenda.noDoctorsTitle': 'No doctors', 'agenda.noDoctorsSub': 'No doctors are registered to show in the by-doctor view.', 'agenda.noDoctorsSubHour': 'No doctors are registered to show in the by-hour view.', 'agenda.noDoctorsSubWeek': 'No doctors are registered to show in the weekly view.', 'agenda.noResourcesTitle': 'No resources', 'agenda.noResourcesSub': 'Add resources in Settings › Resources to see this view.', 'agenda.depositPending': 'Deposit pending', 'config.idiomaTitle': 'Interface language', 'config.idiomaBody': 'Choose the language the system displays in this browser.', 'config.anticipo.title': 'Deposit at booking', 'config.anticipo.help': 'Ask for a deposit when booking online. The booking is held without confirming until paid; if unpaid in time, the slot is released.', 'config.anticipo.enable': 'Require deposit', 'config.anticipo.enableHelp': 'Off, the portal books without asking for money (as today).', 'config.anticipo.type': 'Deposit type', 'config.anticipo.typePercent': 'Percentage of the service', 'config.anticipo.typeFixed': 'Fixed amount', 'config.anticipo.value': 'Value (percentage or amount)', 'config.anticipo.currency': 'Currency', 'config.anticipo.hours': 'Hours to pay', 'config.anticipo.refundable': 'Refundable', 'config.anticipo.refundableHelp': 'If the appointment is cancelled, the deposit is refunded.', 'config.anticipo.simulated': 'Allow simulated payment', 'config.anticipo.simulatedHelp': 'Lets you test the flow with no gateway and no real money. Keep it off in production.', 'config.anticipo.notice': 'Customer notice', 'config.anticipo.noticePlaceholder': 'A deposit is required to confirm your appointment.', 'config.cobroAuto.title': 'Automatic membership charging', 'config.cobroAuto.help': 'Generates the payment link for the period and sends it to the patient via notifications, with retries. The period stays pending until paid.', 'config.cobroAuto.enable': 'Enable automatic charging', 'config.cobroAuto.enableHelp': 'Off, the process only creates the charge (no charge or notice).', 'config.cobroAuto.method': 'Method', 'config.cobroAuto.methodLink': 'Payment link', 'config.cobroAuto.methodCard': 'Saved card (Stripe)', 'config.cobroAuto.daysBefore': 'Days before due date', 'config.cobroAuto.grace': 'Grace days', 'config.cobroAuto.retries': 'Notice retry days', 'config.cobroAuto.retriesHelp': 'Days (relative to due date) on which the notice is retried, comma separated.', 'config.cobroAuto.cancel': 'Expire if unpaid', 'config.cobroAuto.cancelHelp': 'When retries are exhausted, marks the membership as expired. Off, it stays active until the normal expiration process.', 'config.cobroAuto.simulated': 'Allow simulated payment', 'config.cobroAuto.simulatedHelp': 'Lets you test the flow with no gateway and no real money. Keep it off in production.', }; export const DICT: Record = { es, en }; // ------------------------------------------------------------ // Glosario ↔ idioma // // Regla: el glosario define el VOCABULARIO DEL NEGOCIO (paciente↔cliente, // cita↔reserva...) y el idioma define el RESTO del texto. Los packs del // glosario viven en español, así que: // - En español: se usa el término del pack configurado (manda el glosario). // - En inglés: se usa el término base en inglés (abajo), porque no hay // pack en inglés; si algún día existiera, se preferiría ese. // ------------------------------------------------------------ export const GLOSARIO_EN: Record = { patient_singular: 'Patient', patient_plural: 'Patients', appointment_singular: 'Appointment', appointment_plural: 'Appointments', visit_singular: 'Visit', visit_plural: 'Visits', record_singular: 'Record', record_plural: 'Records', doctor_singular: 'Doctor', doctor_plural: 'Doctors', staff_singular: 'Staff', staff_plural: 'Staff', branch_singular: 'Branch', branch_plural: 'Branches', room_singular: 'Room', room_plural: 'Rooms', service_singular: 'Service', service_plural: 'Services', package_singular: 'Package', package_plural: 'Packages', prescription_singular: 'Prescription', prescription_plural: 'Prescriptions', treatment_singular: 'Treatment', treatment_plural: 'Treatments', }; // Etiquetas del menú lateral y sus grupos -> clave i18n. export const MENU_LABEL_KEYS: Record = { // grupos 'Principal': 'nav.group.principal', 'Clínica': 'nav.group.clinica', 'Operaciones': 'nav.group.operaciones', 'Analítica': 'nav.group.analitica', 'Comunicación': 'nav.group.comunicacion', 'WACRM': 'nav.group.wacrm', // items 'Dashboard': 'nav.dashboard', 'Agenda': 'nav.agenda', 'Consultas': 'nav.consultas', 'Mis ventas': 'nav.mySales', 'Punto de Venta': 'nav.pos', 'Ventas': 'nav.sales', 'Tickets': 'nav.tickets', 'Pagos': 'nav.payments', 'Adeudos': 'nav.debts', 'Monedero': 'nav.wallet', 'Inventario': 'nav.inventory', 'Compras': 'nav.purchases', 'Cortes de Caja': 'nav.cashCuts', 'Reportes': 'nav.reports', 'Comisiones': 'nav.commissions', 'Horas agenda': 'nav.agendaHours', 'Exportar datos': 'nav.exportData', 'Configuración': 'nav.config', 'Chat': 'nav.chat', 'Cumpleañeros': 'nav.birthdays', 'Mensajes': 'nav.messages', 'Leads': 'nav.leads', 'Membresías': 'nav.memberships', 'Liquidaciones': 'nav.liquidations', }; // Etiquetas del tema "clásico" -> clave i18n (reutiliza nav.* cuando coincide). export const CLASICO_LABEL_KEYS: Record = { 'Inicio': 'clasico.inicio', 'Agenda': 'nav.agenda', 'Visitas': 'clasico.visitas', 'Consultas Médicas': 'clasico.consultasMedicas', 'Expedientes': 'clasico.expedientes', 'Médicos': 'clasico.medicos', 'Cosmetología': 'clasico.cosmetologia', 'Paquetes': 'nav.packages', 'Configuración': 'nav.config', 'Clínica': 'clasico.clinica', 'Usuarios': 'clasico.usuarios', 'Recetas': 'clasico.recetas', 'Diagnósticos': 'clasico.diagnosticos', 'Procedimientos médicos': 'clasico.procedimientos', 'Reportes': 'nav.reports', 'Mov. diario': 'clasico.movDiario', 'Cortes de caja': 'clasico.cortesCaja', 'Ingresos': 'clasico.ingresos', 'Inventario': 'nav.inventory', 'Comisiones': 'nav.commissions', 'Exportar correos': 'clasico.exportarCorreos', 'Administrativo': 'clasico.administrativo', 'Adeudos clientes': 'clasico.adeudosClientes', 'Pagos | Servicios': 'clasico.pagosServicios', 'Pagos | Clientes': 'clasico.pagosClientes', 'Devoluciones': 'clasico.devoluciones', 'Vendedores': 'clasico.vendedores', 'Recomendaciones': 'clasico.recomendaciones', 'Concentrado': 'clasico.concentrado', 'Top ventas': 'clasico.topVentas', 'Horas agenda': 'nav.agendaHours', 'Monedero': 'clasico.monedero', 'Monederos': 'clasico.monederos', 'Reporte puntos': 'clasico.reportePuntos', 'Comunicación': 'clasico.comunicacion', 'Chat': 'nav.chat', 'Cumpleañeros': 'nav.birthdays', 'Mensajes': 'nav.messages', 'CRM': 'clasico.crm', 'Comprobantes del día': 'clasico.comprobantes', 'Consumibles': 'clasico.consumibles', 'Inventario actual': 'clasico.inventarioActual', 'Compras': 'nav.purchases', 'Bajas': 'clasico.bajas', 'Órdenes de compra': 'clasico.ordenesCompra', 'Ventas del día': 'clasico.ventasDia', 'Tickets': 'nav.tickets', 'Punto de Venta': 'nav.pos', 'Clientes': 'clasico.clientes', 'Mis ventas': 'nav.mySales', 'Membresías': 'nav.memberships', 'Liquidaciones': 'nav.liquidations', }; // ------------------------------------------------------------ // Motor de traducción // ------------------------------------------------------------ export type TFunc = (key: string, vars?: Record) => string; const interpolar = (texto: string, vars?: Record): string => { if (!vars) return texto; return texto.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m)); }; export function translate(locale: Locale, key: string, vars?: Record): string { const dict = DICT[locale] || es; let raw: string | undefined; // Pluralización básica: clave_one / clave_other. if (vars && typeof vars.count === 'number' && dict[`${key}_one`] !== undefined) { raw = vars.count === 1 ? dict[`${key}_one`] : (dict[`${key}_other`] ?? es[`${key}_one`]); } if (raw === undefined) raw = dict[key]; if (raw === undefined) raw = es[key]; if (raw === undefined) { if (import.meta.env.DEV && locale === 'en') { // eslint-disable-next-line no-console console.warn(`[i18n] falta la clave '${key}' en el diccionario 'en'`); } return key; } return interpolar(raw, vars); } // ------------------------------------------------------------ // Provider / hook // ------------------------------------------------------------ interface I18nContextValue { locale: Locale; setLocale: (l: Locale) => void; t: TFunc; idiomas: IdiomaOpcion[]; } const I18nContext = createContext(undefined); const esLocaleValido = (v: unknown): v is Locale => v === 'es' || v === 'en'; const leerIdiomaInicial = (): { locale: Locale; explicito: boolean } => { try { const raw = localStorage.getItem(IDIOMA_STORAGE_KEY); if (esLocaleValido(raw)) return { locale: raw, explicito: true }; } catch { /* localStorage bloqueado */ } return { locale: 'es', explicito: false }; }; export const I18nProvider: FC<{ children: ReactNode }> = ({ children }) => { const inicial = useMemo(leerIdiomaInicial, []); const [locale, setLocaleState] = useState(inicial.locale); const clinic = useClinicSettings(); const setLocale = useCallback((l: Locale) => { setLocaleState(l); try { localStorage.setItem(IDIOMA_STORAGE_KEY, l); } catch { /* localStorage bloqueado: se mantiene en memoria */ } }, []); // Default por clínica: solo aplica si el usuario/visitante no eligió aún. useEffect(() => { if (inicial.explicito) return; const def = (clinic as { idioma_default?: string }).idioma_default; if (esLocaleValido(def) && def !== locale) setLocaleState(def); }, [clinic, inicial.explicito, locale]); // refleja el idioma activo. useEffect(() => { if (typeof document !== 'undefined') { document.documentElement.lang = locale; } }, [locale]); const t = useCallback((key, vars) => translate(locale, key, vars), [locale]); const value = useMemo( () => ({ locale, setLocale, t, idiomas: IDIOMAS }), [locale, setLocale, t], ); return {children}; }; export const useI18n = (): I18nContextValue => { const ctx = useContext(I18nContext); if (!ctx) throw new Error('useI18n debe usarse dentro de I18nProvider'); return ctx; }; /** * Etiqueta del menú lateral traducida: los términos de negocio los resuelve el * glosario (español) o el término base en inglés; el resto, el diccionario. */ export const useNavLabel = (): ((label: string) => string) => { const { t, locale } = useI18n(); const { term } = useGlosario(); return useCallback( (label: string): string => { // Un término de negocio ya traducido por el glosario vuelve a caer aquí // (p. ej. 'Pacientes'); es idempotente y en inglés da el término base. const termKey = MENU_LABEL_TERMS[label]; if (termKey) return locale === 'en' ? GLOSARIO_EN[termKey] : term(termKey); const key = MENU_LABEL_KEYS[label]; return key ? t(key) : label; }, [t, locale, term], ); }; /** Etiqueta del menú del tema "clásico" traducida al idioma activo. */ export const useClasicoLabel = (): ((label: string) => string) => { const { t } = useI18n(); return useCallback( (label: string): string => { const key = CLASICO_LABEL_KEYS[label]; return key ? t(key) : label; }, [t], ); }; /** * Término de negocio (glosario) resuelto según el idioma: en español manda el * pack configurado; en inglés se usa el término base en inglés. */ export const useBusinessTerm = (): ((key: GlossaryKey) => string) => { const { locale } = useI18n(); const { term } = useGlosario(); return useCallback( (key: GlossaryKey): string => (locale === 'en' ? GLOSARIO_EN[key] : term(key)), [locale, term], ); };