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:
@@ -37,7 +37,9 @@ import QuickLinksDock, { QuickLinksModePicker } from './QuickLinksDock';
|
||||
import GlobalSearch from './GlobalSearch';
|
||||
import ThemePicker from './ThemePicker';
|
||||
import NotificationBell from './NotificationBell';
|
||||
import { LanguageToggle } from './LanguageSelect';
|
||||
import { useAuth, ROLE_LABELS } from '../lib/auth';
|
||||
import { useI18n, useClasicoLabel } from '../lib/i18n';
|
||||
import { useClinicSettings } from '../lib/clinic-settings';
|
||||
import { useChatUnread } from '../lib/chat';
|
||||
import { useQuickLinks } from '../lib/quicklinks';
|
||||
@@ -59,6 +61,8 @@ interface MenuChild {
|
||||
|
||||
interface MenuEntry {
|
||||
label: string;
|
||||
/** Etiqueta canónica (español) para estado/keys; `label` es la visible. */
|
||||
id?: string;
|
||||
icon: FC<{ size?: number | string; className?: string }>;
|
||||
href?: string;
|
||||
children?: MenuChild[];
|
||||
@@ -146,6 +150,8 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { user, canSee, hasRole, logout, brand } = useAuth();
|
||||
const { t } = useI18n();
|
||||
const cl = useClasicoLabel();
|
||||
const { membresias: membresiasCfg, compras: comprasCfg, comisiones: comisionesCfg } = useClinicSettings();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const chatUnread = useChatUnread();
|
||||
@@ -164,8 +170,13 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
if (comisionesCfg.activo) {
|
||||
tree = [...tree, { label: 'Liquidaciones', icon: Receipt, href: '/comisiones/liquidaciones' }];
|
||||
}
|
||||
return tree;
|
||||
}, [membresiasCfg.activo, comprasCfg.activo, comisionesCfg.activo]);
|
||||
return tree.map((e) => ({
|
||||
...e,
|
||||
id: e.label,
|
||||
label: cl(e.label),
|
||||
children: e.children?.map((c) => ({ ...c, label: cl(c.label) })),
|
||||
}));
|
||||
}, [membresiasCfg.activo, comprasCfg.activo, comisionesCfg.activo, cl]);
|
||||
const menuLeavesT = useMemo(
|
||||
() => menuTreeT.flatMap((e) =>
|
||||
e.children
|
||||
@@ -275,7 +286,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
const current = `${location.pathname}${location.search}`;
|
||||
return menuTreeT
|
||||
.filter((e) => e.children?.some((c) => current.startsWith(c.href.split('?')[0]) && (!c.href.includes('?') || current.includes(c.href.split('?')[1]))))
|
||||
.map((e) => e.label);
|
||||
.map((e) => e.id as string);
|
||||
});
|
||||
|
||||
const toggleGroup = (label: string) =>
|
||||
@@ -296,7 +307,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
const base = c.href.split('?')[0];
|
||||
return current === c.href || (current.startsWith(base) && (!c.href.includes('?') || current.includes(c.href.split('?')[1])));
|
||||
}))
|
||||
.map((e) => e.label);
|
||||
.map((e) => e.id as string);
|
||||
if (!abiertos.length) return;
|
||||
setOpenGroups((prev) => {
|
||||
const faltan = abiertos.filter((l) => !prev.includes(l));
|
||||
@@ -325,7 +336,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
{quickItems.length > 0 && quick.mode === 'menu' && (
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center justify-between px-4 py-2" style={{ backgroundColor: 'var(--sidebar-group-bg)' }}>
|
||||
<p className="text-[10px] uppercase tracking-wider font-medium" style={{ color: 'var(--sidebar-group-text)' }}>Accesos rápidos</p>
|
||||
<p className="text-[10px] uppercase tracking-wider font-medium" style={{ color: 'var(--sidebar-group-text)' }}>{t('nav.quickLinks')}</p>
|
||||
<span className="flex items-center gap-1">
|
||||
<QuickLinksModePicker direction="down" align="right" />
|
||||
<button
|
||||
@@ -333,7 +344,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
onClick={() => quick.setEditing(!quick.editing)}
|
||||
className="hover:text-white transition-colors"
|
||||
style={{ color: 'var(--sidebar-group-text)' }}
|
||||
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>
|
||||
@@ -363,15 +374,15 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
{quick.editing && (
|
||||
<span className="flex items-center gap-0.5 shrink-0 pr-2">
|
||||
<button type="button" onClick={() => quick.move(item.href, -1)} disabled={idx === 0}
|
||||
className="hover:text-white disabled:opacity-30 transition-colors" style={{ color: 'var(--sidebar-text)' }} title="Subir">
|
||||
className="hover:text-white disabled:opacity-30 transition-colors" style={{ color: 'var(--sidebar-text)' }} title={t('dock.up')}>
|
||||
<ChevronUp size={14} />
|
||||
</button>
|
||||
<button type="button" onClick={() => quick.move(item.href, 1)} disabled={idx === quickItems.length - 1}
|
||||
className="hover:text-white disabled:opacity-30 transition-colors" style={{ color: 'var(--sidebar-text)' }} title="Bajar">
|
||||
className="hover:text-white disabled:opacity-30 transition-colors" style={{ color: 'var(--sidebar-text)' }} title={t('dock.down')}>
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
<button type="button" onClick={() => quick.remove(item.href)}
|
||||
className="hover:text-rose-400 transition-colors" style={{ color: 'var(--sidebar-text)' }} title="Quitar">
|
||||
className="hover:text-rose-400 transition-colors" style={{ color: 'var(--sidebar-text)' }} title={t('dock.remove')}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</span>
|
||||
@@ -396,7 +407,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
const active = isActive(entry.href as string);
|
||||
const pinned = quick.isPinned(entry.href as string);
|
||||
return (
|
||||
<li key={entry.label} className="group flex items-center">
|
||||
<li key={entry.id} className="group flex items-center">
|
||||
<Link
|
||||
to={entry.href as string}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
@@ -420,7 +431,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
pinned ? 'text-amber-400' : 'opacity-0 group-hover:opacity-100 hover:text-amber-400'
|
||||
}`}
|
||||
style={{ color: pinned ? undefined : 'var(--sidebar-text)' }}
|
||||
title={pinned ? 'Quitar de accesos rápidos' : 'Fijar en accesos rápidos'}
|
||||
title={pinned ? t('user.unpinQuick') : t('user.pinQuick')}
|
||||
>
|
||||
<Star size={14} fill={pinned ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
@@ -429,13 +440,13 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
}
|
||||
|
||||
// Entry con submenú (treeview AdminLTE)
|
||||
const open = openGroups.includes(entry.label);
|
||||
const open = openGroups.includes(entry.id as string);
|
||||
const childActive = leaves.some((c) => isActive(c.href));
|
||||
return (
|
||||
<li key={entry.label}>
|
||||
<li key={entry.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleGroup(entry.label)}
|
||||
onClick={() => toggleGroup(entry.id as string)}
|
||||
className="w-full flex items-center space-x-3 px-4 py-2.5 text-sm transition-colors"
|
||||
style={{
|
||||
backgroundColor: childActive ? 'var(--sidebar-active-bg)' : 'transparent',
|
||||
@@ -488,7 +499,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
pinned ? 'text-amber-400' : 'opacity-0 group-hover:opacity-100 hover:text-amber-400'
|
||||
}`}
|
||||
style={{ color: pinned ? undefined : 'var(--sidebar-text)' }}
|
||||
title={pinned ? 'Quitar de accesos rápidos' : 'Fijar en accesos rápidos'}
|
||||
title={pinned ? t('user.unpinQuick') : t('user.pinQuick')}
|
||||
>
|
||||
<Star size={13} fill={pinned ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
@@ -513,18 +524,22 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{user?.name || 'Usuario'}</p>
|
||||
<p className="text-xs truncate" style={{ color: 'var(--sidebar-text)' }}>{user ? ROLE_LABELS[user.role] : ''}</p>
|
||||
<p className="text-xs truncate" style={{ color: 'var(--sidebar-text)' }}>{user ? (ROLE_LABELS[user.role] ? t(`role.${user.role}`) : (user.role_name || user.role)) : ''}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 transition-colors hover:text-white"
|
||||
style={{ color: 'var(--sidebar-text)' }}
|
||||
title="Cerrar sesión"
|
||||
aria-label="Cerrar sesión"
|
||||
title={t('user.logout')}
|
||||
aria-label={t('user.logout')}
|
||||
>
|
||||
<LogOut size={17} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-[11px]" style={{ color: 'var(--sidebar-text)' }}>{t('user.language')}</span>
|
||||
<LanguageToggle color="var(--sidebar-text)" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -548,7 +563,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
size="sm"
|
||||
className="lg:hidden text-white"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
aria-label="Abrir menú"
|
||||
aria-label={t('user.openMenu')}
|
||||
>
|
||||
<Menu size={20} />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user