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

@@ -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>