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:
@@ -44,8 +44,10 @@ import QuickLinksDock, { QuickLinksModePicker } from './QuickLinksDock';
|
||||
import GlobalSearch from './GlobalSearch';
|
||||
import ThemePicker from './ThemePicker';
|
||||
import NotificationBell from './NotificationBell';
|
||||
import { LanguageToggle } from './LanguageSelect';
|
||||
import { useAuth } from '../lib/auth';
|
||||
import { useGlosario, traducirMenu, traducirEtiqueta } from '../lib/glosario';
|
||||
import { useI18n, useNavLabel } from '../lib/i18n';
|
||||
import { useClinicSettings } from '../lib/clinic-settings';
|
||||
import { useChatUnread } from '../lib/chat';
|
||||
import { useQuickLinks } from '../lib/quicklinks';
|
||||
@@ -122,10 +124,12 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
const navigate = useNavigate();
|
||||
const { user, canSee, hasRole, logout, brand } = useAuth();
|
||||
const { term } = useGlosario();
|
||||
const { t } = useI18n();
|
||||
const navLabel = useNavLabel();
|
||||
const { membresias: membresiasCfg, compras: comprasCfg, comisiones: comisionesCfg, licencia: licenciaCfg } = useClinicSettings();
|
||||
const topNavItems = useMemo(
|
||||
() => TOP_NAV_ITEMS.map((it) => ({ ...it, label: traducirEtiqueta(it.label, term) })),
|
||||
[term]
|
||||
() => TOP_NAV_ITEMS.map((it) => ({ ...it, label: navLabel(traducirEtiqueta(it.label, term)) })),
|
||||
[term, navLabel]
|
||||
);
|
||||
const menuSections = useMemo(() => {
|
||||
let base = MENU_SECTIONS;
|
||||
@@ -151,8 +155,15 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
.map((s) => ({ ...s, items: s.items.filter((i) => i.href !== '/chat') }))
|
||||
.filter((s) => s.items.length > 0);
|
||||
}
|
||||
return traducirMenu(base, term);
|
||||
}, [term, membresiasCfg.activo, comprasCfg.activo, comisionesCfg.activo, licenciaCfg]);
|
||||
return traducirMenu(base, term).map((s, idx) => ({
|
||||
...s,
|
||||
// `id` conserva la etiqueta canónica en español para el estado de
|
||||
// expansión y las claves de React; `label` es la etiqueta visible.
|
||||
id: base[idx].label,
|
||||
label: navLabel(s.label),
|
||||
items: s.items.map((it) => ({ ...it, label: navLabel(it.label) })),
|
||||
}));
|
||||
}, [term, navLabel, membresiasCfg.activo, comprasCfg.activo, comisionesCfg.activo, licenciaCfg]);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [expanded, setExpanded] = useState<string[]>(['Clínica']);
|
||||
const chatUnread = useChatUnread();
|
||||
@@ -273,7 +284,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" className="lg:hidden" style={{ color: 'var(--text-heading)' }} onClick={() => setMenuOpen(true)} aria-label="Abrir menú">
|
||||
<Button variant="ghost" size="sm" className="lg:hidden" style={{ color: 'var(--text-heading)' }} onClick={() => setMenuOpen(true)} aria-label={t('user.openMenu')}>
|
||||
<Menu size={22} />
|
||||
</Button>
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
@@ -323,7 +334,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
e.currentTarget.style.color = 'var(--text-muted)';
|
||||
}}
|
||||
>
|
||||
Más <ChevronDown size={14} />
|
||||
{t('user.more')} <ChevronDown size={14} />
|
||||
</button>
|
||||
<div className="absolute top-full left-0 mt-2 w-56 rounded-2xl shadow-card border opacity-0 invisible group-hover:opacity-100 group-hover:visible transition z-40" style={{ backgroundColor: 'var(--surface)', borderColor: 'var(--border)' }}>
|
||||
<div className="p-2">
|
||||
@@ -356,6 +367,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
<GlobalSearch className="hidden md:block" />
|
||||
<NotificationBell buttonClassName="p-2 rounded-full relative transition text-theme-muted hover:bg-theme-accent-bg hover:text-theme-heading" />
|
||||
<ThemePicker />
|
||||
<LanguageToggle color="var(--text-muted)" />
|
||||
<div className="hidden sm:flex items-center gap-2 pl-2 border-l" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold" style={{ backgroundColor: 'var(--success)', color: 'var(--success-text)' }}>
|
||||
{initials || 'US'}
|
||||
@@ -369,7 +381,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.color = 'var(--text-muted)';
|
||||
}}
|
||||
title="Cerrar sesión"
|
||||
title={t('user.logout')}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
</button>
|
||||
@@ -385,7 +397,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
<div className="rounded-2xl p-4 border" style={{ backgroundColor: 'var(--bg)', borderColor: 'var(--border)' }}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Sparkles size={16} style={{ color: 'var(--accent)' }} />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-muted)' }}>Meta del mes</span>
|
||||
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-muted)' }}>{t('user.goalMonth')}</span>
|
||||
</div>
|
||||
{goal && goal.goal > 0 ? (
|
||||
<button onClick={hasRole('admin') ? openGoalModal : undefined} className={`w-full text-left ${hasRole('admin') ? 'cursor-pointer' : 'cursor-default'}`}>
|
||||
@@ -399,7 +411,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
</button>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={openGoalModal}>
|
||||
<Target size={14} className="mr-1.5" /> Definir meta
|
||||
<Target size={14} className="mr-1.5" /> {t('user.goalDefine')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -410,7 +422,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
<div className="w-full flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm font-medium flex items-center gap-2" style={{ color: 'var(--text-heading)' }}>
|
||||
<Star size={14} style={{ color: 'var(--accent)' }} />
|
||||
Accesos rápidos
|
||||
{t('nav.quickLinks')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<QuickLinksModePicker direction="down" align="right" />
|
||||
@@ -418,7 +430,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
type="button"
|
||||
onClick={() => quick.setEditing(!quick.editing)}
|
||||
style={{ color: 'var(--text-muted)' }}
|
||||
title={quick.editing ? 'Terminar edición' : 'Editar accesos rápidos'}
|
||||
title={quick.editing ? t('dock.finishEditing') : t('dock.editQuick')}
|
||||
>
|
||||
{quick.editing ? <X size={13} /> : <Pencil size={12} />}
|
||||
</button>
|
||||
@@ -456,15 +468,15 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
{quick.editing && (
|
||||
<span className="flex items-center gap-0.5 shrink-0">
|
||||
<button type="button" onClick={() => quick.move(item.href, -1)} disabled={idx === 0}
|
||||
className="disabled:opacity-30" style={{ color: 'var(--text-muted)' }} title="Subir">
|
||||
className="disabled:opacity-30" style={{ color: 'var(--text-muted)' }} title={t('dock.up')}>
|
||||
<ChevronUp size={14} />
|
||||
</button>
|
||||
<button type="button" onClick={() => quick.move(item.href, 1)} disabled={idx === quickItems.length - 1}
|
||||
className="disabled:opacity-30" style={{ color: 'var(--text-muted)' }} title="Bajar">
|
||||
className="disabled:opacity-30" style={{ color: 'var(--text-muted)' }} title={t('dock.down')}>
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
<button type="button" onClick={() => quick.remove(item.href)}
|
||||
style={{ color: 'var(--text-muted)' }} title="Quitar">
|
||||
style={{ color: 'var(--text-muted)' }} title={t('dock.remove')}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</span>
|
||||
@@ -476,16 +488,16 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
</div>
|
||||
)}
|
||||
{visibleSections.map((section) => (
|
||||
<div key={section.label} className="rounded-xl border overflow-hidden" style={{ backgroundColor: 'var(--bg)', borderColor: 'var(--border)' }}>
|
||||
<div key={section.id} className="rounded-xl border overflow-hidden" style={{ backgroundColor: 'var(--bg)', borderColor: 'var(--border)' }}>
|
||||
<button
|
||||
onClick={() => toggleSection(section.label)}
|
||||
onClick={() => toggleSection(section.id)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium"
|
||||
style={{ color: 'var(--text-heading)' }}
|
||||
>
|
||||
{section.label}
|
||||
<ChevronDown size={14} className={`transition-transform ${expanded.includes(section.label) ? 'rotate-180' : ''}`} style={{ color: 'var(--text-muted)' }} />
|
||||
<ChevronDown size={14} className={`transition-transform ${expanded.includes(section.id) ? 'rotate-180' : ''}`} style={{ color: 'var(--text-muted)' }} />
|
||||
</button>
|
||||
{expanded.includes(section.label) && (
|
||||
{expanded.includes(section.id) && (
|
||||
<ul className="px-2 pb-2 space-y-1">
|
||||
{section.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
@@ -554,7 +566,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
<aside className="fixed inset-y-0 left-0 w-80 z-50 flex flex-col overflow-y-auto lg:hidden" style={{ backgroundColor: 'var(--surface)' }}>
|
||||
<div className="flex items-center justify-between px-5 h-16 border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<img src={brand.logo_oscuro} alt={brand.nombre} className="h-8 w-auto object-contain" />
|
||||
<Button variant="ghost" size="sm" onClick={() => setMenuOpen(false)} aria-label="Cerrar menú">
|
||||
<Button variant="ghost" size="sm" onClick={() => setMenuOpen(false)} aria-label={t('user.closeMenu')}>
|
||||
<X size={22} />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -562,7 +574,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
{quickItems.length > 0 && (
|
||||
<>
|
||||
<p className="px-2 pt-1 text-[10px] uppercase tracking-wider font-medium flex items-center gap-1.5" style={{ color: 'var(--text-muted)' }}>
|
||||
<Star size={11} style={{ color: 'var(--accent)' }} /> Accesos rápidos
|
||||
<Star size={11} style={{ color: 'var(--accent)' }} /> {t('nav.quickLinks')}
|
||||
</p>
|
||||
{quickItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
@@ -632,7 +644,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'var(--accent-bg)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<LogOut size={18} /> Cerrar sesión
|
||||
<LogOut size={18} /> {t('user.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -642,18 +654,18 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
|
||||
<Modal
|
||||
isOpen={goalModalOpen}
|
||||
onClose={() => setGoalModalOpen(false)}
|
||||
title="Meta de ventas del mes"
|
||||
title={t('user.goalTitle')}
|
||||
maxWidth="sm"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setGoalModalOpen(false)}>Cancelar</Button>
|
||||
<Button onClick={saveGoal} loading={goalSaving}>Guardar meta</Button>
|
||||
<Button variant="outline" onClick={() => setGoalModalOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={saveGoal} loading={goalSaving}>{t('user.goalSave')}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>Define el objetivo de ventas (MXN) para el mes en curso.</p>
|
||||
<Input label="Meta mensual (MXN)" type="number" min={0} step={1000} value={goalInput} onChange={(e) => setGoalInput(e.target.value)} placeholder="ej. 500000" />
|
||||
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>{t('user.goalBody')}</p>
|
||||
<Input label={t('user.goalField')} type="number" min={0} step={1000} value={goalInput} onChange={(e) => setGoalInput(e.target.value)} placeholder="ej. 500000" />
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user