Support multiple languages

This commit is contained in:
Urtzi Alfaro
2025-09-25 12:14:46 +02:00
parent 6d4090f825
commit f02a980c87
66 changed files with 3274 additions and 333 deletions

View File

@@ -1,6 +1,7 @@
import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { useLocation, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getBreadcrumbs, getRouteByPath } from '../../../router/routes.config';
import {
ChevronRight,
@@ -94,7 +95,7 @@ export const Breadcrumbs = forwardRef<BreadcrumbsRef, BreadcrumbsProps>(({
items: customItems,
showHome = true,
homePath = '/',
homeLabel = 'Inicio',
homeLabel,
separator,
maxItems = 5,
truncateMiddle = true,
@@ -103,9 +104,13 @@ export const Breadcrumbs = forwardRef<BreadcrumbsRef, BreadcrumbsProps>(({
hiddenPaths = [],
itemRenderer,
}, ref) => {
const { t } = useTranslation();
const location = useLocation();
const containerRef = React.useRef<HTMLDivElement>(null);
// Set default home label if not provided
const resolvedHomeLabel = homeLabel || t('common:breadcrumbs.home', 'Inicio');
// Get breadcrumbs from router config or use custom items
const routeBreadcrumbs = getBreadcrumbs(location.pathname);
@@ -115,11 +120,11 @@ export const Breadcrumbs = forwardRef<BreadcrumbsRef, BreadcrumbsProps>(({
}
const items: BreadcrumbItem[] = [];
// Add home if enabled
if (showHome && location.pathname !== homePath) {
items.push({
label: homeLabel,
label: resolvedHomeLabel,
path: homePath,
icon: Home,
});

View File

@@ -1,6 +1,7 @@
import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button } from '../../ui';
import {
Heart,
@@ -150,15 +151,16 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
compact = false,
children,
}, ref) => {
const { t } = useTranslation();
const footerRef = React.useRef<HTMLDivElement>(null);
const currentYear = new Date().getFullYear();
// Company info - full for public pages, minimal for internal
const defaultCompanyInfo: CompanyInfo = compact ? {
name: 'Panadería IA',
name: t('common:app.name', 'Panadería IA'),
} : {
name: 'Panadería IA',
description: 'Sistema inteligente de gestión para panaderías. Optimiza tu producción, inventario y ventas con inteligencia artificial.',
name: t('common:app.name', 'Panadería IA'),
description: t('common:footer.company_description', 'Sistema inteligente de gestión para panaderías. Optimiza tu producción, inventario y ventas con inteligencia artificial.'),
email: 'contacto@panaderia-ia.com',
website: 'https://panaderia-ia.com',
};
@@ -169,33 +171,33 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
const defaultSections: FooterSection[] = compact ? [] : [
{
id: 'product',
title: 'Producto',
title: t('common:footer.sections.product', 'Producto'),
links: [
{ id: 'dashboard', label: 'Dashboard', href: '/dashboard' },
{ id: 'inventory', label: 'Inventario', href: '/inventory' },
{ id: 'production', label: 'Producción', href: '/production' },
{ id: 'sales', label: 'Ventas', href: '/sales' },
{ id: 'forecasting', label: 'Predicciones', href: '/forecasting' },
{ id: 'dashboard', label: t('common:footer.links.dashboard', 'Dashboard'), href: '/dashboard' },
{ id: 'inventory', label: t('common:footer.links.inventory', 'Inventario'), href: '/inventory' },
{ id: 'production', label: t('common:footer.links.production', 'Producción'), href: '/production' },
{ id: 'sales', label: t('common:footer.links.sales', 'Ventas'), href: '/sales' },
{ id: 'forecasting', label: t('common:footer.links.forecasting', 'Predicciones'), href: '/forecasting' },
],
},
{
id: 'support',
title: 'Soporte',
title: t('common:footer.sections.support', 'Soporte'),
links: [
{ id: 'help', label: 'Centro de Ayuda', href: '/help', icon: HelpCircle },
{ id: 'docs', label: 'Documentación', href: '/help/docs', icon: FileText },
{ id: 'contact', label: 'Contacto', href: '/help/support', icon: MessageSquare },
{ id: 'feedback', label: 'Feedback', href: '/help/feedback' },
{ id: 'help', label: t('common:footer.links.help', 'Centro de Ayuda'), href: '/help', icon: HelpCircle },
{ id: 'docs', label: t('common:footer.links.docs', 'Documentación'), href: '/help/docs', icon: FileText },
{ id: 'contact', label: t('common:footer.links.contact', 'Contacto'), href: '/help/support', icon: MessageSquare },
{ id: 'feedback', label: t('common:footer.links.feedback', 'Feedback'), href: '/help/feedback' },
],
},
{
id: 'company',
title: 'Empresa',
title: t('common:footer.sections.company', 'Empresa'),
links: [
{ id: 'about', label: 'Acerca de', href: '/about', external: true },
{ id: 'blog', label: 'Blog', href: 'https://blog.panaderia-ia.com', external: true },
{ id: 'careers', label: 'Carreras', href: 'https://careers.panaderia-ia.com', external: true },
{ id: 'press', label: 'Prensa', href: '/press', external: true },
{ id: 'about', label: t('common:footer.links.about', 'Acerca de'), href: '/about', external: true },
{ id: 'blog', label: t('common:footer.links.blog', 'Blog'), href: 'https://blog.panaderia-ia.com', external: true },
{ id: 'careers', label: t('common:footer.links.careers', 'Carreras'), href: 'https://careers.panaderia-ia.com', external: true },
{ id: 'press', label: t('common:footer.links.press', 'Prensa'), href: '/press', external: true },
],
},
];
@@ -206,19 +208,19 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
const defaultSocialLinks: SocialLink[] = compact ? [] : [
{
id: 'twitter',
label: 'Twitter',
label: t('common:footer.social_labels.twitter', 'Twitter'),
href: 'https://twitter.com/panaderia-ia',
icon: Twitter,
},
{
id: 'linkedin',
label: 'LinkedIn',
label: t('common:footer.social_labels.linkedin', 'LinkedIn'),
href: 'https://linkedin.com/company/panaderia-ia',
icon: Linkedin,
},
{
id: 'github',
label: 'GitHub',
label: t('common:footer.social_labels.github', 'GitHub'),
href: 'https://github.com/panaderia-ia',
icon: Github,
},
@@ -228,9 +230,9 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
// Privacy links
const privacyLinks: FooterLink[] = [
{ id: 'privacy', label: 'Privacidad', href: '/privacy', icon: Shield },
{ id: 'terms', label: 'Términos', href: '/terms', icon: FileText },
{ id: 'cookies', label: 'Cookies', href: '/cookies' },
{ id: 'privacy', label: t('common:footer.links.privacy', 'Privacidad'), href: '/privacy', icon: Shield },
{ id: 'terms', label: t('common:footer.links.terms', 'Términos'), href: '/terms', icon: FileText },
{ id: 'cookies', label: t('common:footer.links.cookies', 'Cookies'), href: '/cookies' },
];
// Scroll into view
@@ -375,7 +377,7 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
{socialLinksToShow.length > 0 && (
<div className="border-t border-[var(--border-primary)] pt-6 mb-6">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-sm text-[var(--text-secondary)]">Síguenos en redes sociales</p>
<p className="text-sm text-[var(--text-secondary)]">{t('common:footer.social_follow', 'Síguenos en redes sociales')}</p>
<div className="flex items-center gap-2">
{socialLinksToShow.map((social) => renderSocialLink(social))}
</div>
@@ -404,13 +406,13 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
to="/privacy"
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)] transition-colors duration-200"
>
Privacidad
{t('common:footer.links.privacy', 'Privacidad')}
</Link>
<Link
to="/terms"
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)] transition-colors duration-200"
>
Términos
{t('common:footer.links.terms', 'Términos')}
</Link>
</div>
)}

View File

@@ -148,7 +148,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
className
)}
role="banner"
aria-label="Navegación principal"
aria-label={t('common:header.main_navigation', 'Navegación principal')}
>
{/* Left section */}
<div className="flex items-center gap-2 sm:gap-4 flex-1 min-w-0 h-full">
@@ -158,7 +158,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
size="sm"
onClick={onMenuClick}
className="lg:hidden w-10 h-10 p-0 flex items-center justify-center hover:bg-[var(--bg-secondary)] active:scale-95 transition-all duration-150"
aria-label="Abrir menú de navegación"
aria-label={t('common:header.open_menu', 'Abrir menú de navegación')}
>
<Menu className="h-5 w-5 text-[var(--text-primary)]" />
</Button>
@@ -176,7 +176,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
'self-center',
sidebarCollapsed ? 'lg:block' : 'lg:hidden xl:block'
)}>
Panadería IA
{t('common:app.name', 'Panadería IA')}
</h1>
</>
)}

View File

@@ -2,6 +2,7 @@ import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { Link } from 'react-router-dom';
import { Button, ThemeToggle } from '../../ui';
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
export interface PublicHeaderProps {
className?: string;
@@ -17,6 +18,10 @@ export interface PublicHeaderProps {
* Show authentication buttons (login/register)
*/
showAuthButtons?: boolean;
/**
* Show language selector
*/
showLanguageSelector?: boolean;
/**
* Custom navigation items
*/
@@ -53,6 +58,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
logo,
showThemeToggle = true,
showAuthButtons = true,
showLanguageSelector = true,
navigationItems = [],
variant = 'default',
}, ref) => {
@@ -149,6 +155,11 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
{/* Right side actions */}
<div className="flex items-center gap-3">
{/* Language selector */}
{showLanguageSelector && (
<CompactLanguageSelector className="hidden sm:flex" />
)}
{/* Theme toggle */}
{showThemeToggle && (
<ThemeToggle
@@ -162,8 +173,8 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
{showAuthButtons && (
<div className="flex items-center gap-2">
<Link to="/login">
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
className="hidden sm:inline-flex"
>
@@ -171,7 +182,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
</Button>
</Link>
<Link to="/register">
<Button
<Button
size="sm"
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white"
>
@@ -219,7 +230,17 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
{renderNavLink(item)}
</div>
))}
{/* Mobile language selector */}
{showLanguageSelector && (
<div className="py-2 border-b border-[var(--border-primary)] sm:hidden">
<div className="text-sm font-medium text-[var(--text-secondary)] mb-2">
Idioma
</div>
<CompactLanguageSelector className="w-full" />
</div>
)}
{/* Mobile auth buttons */}
{showAuthButtons && (
<div className="flex flex-col gap-2 pt-4 sm:hidden">

View File

@@ -21,6 +21,7 @@ export interface PublicLayoutProps {
headerProps?: {
showThemeToggle?: boolean;
showAuthButtons?: boolean;
showLanguageSelector?: boolean;
navigationItems?: Array<{
id: string;
label: string;

View File

@@ -732,7 +732,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
isCollapsed ? 'justify-center p-2 h-10 w-10 mx-auto rounded-lg' : 'p-4 gap-3',
isProfileMenuOpen && 'bg-[var(--bg-secondary)]'
)}
aria-label="Menú de perfil"
aria-label={t('common:profile.profile_menu', 'Menú de perfil')}
aria-expanded={isProfileMenuOpen}
aria-haspopup="true"
>
@@ -767,25 +767,25 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
<div className="py-1">
<button
onClick={() => {
navigate('/app/settings/profile');
navigate('/app/settings/personal-info');
setIsProfileMenuOpen(false);
if (onClose) onClose();
}}
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors"
>
<User className="h-4 w-4" />
Perfil
{t('common:profile.my_profile', 'Mi perfil')}
</button>
<button
onClick={() => {
navigate('/app/settings');
navigate('/app/settings/organizations');
setIsProfileMenuOpen(false);
if (onClose) onClose();
}}
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors"
>
<Settings className="h-4 w-4" />
Configuración
<Factory className="h-4 w-4" />
{t('common:profile.my_locations', 'Mis Locales')}
</button>
</div>
@@ -795,7 +795,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors text-[var(--color-error)]"
>
<LogOut className="h-4 w-4" />
Cerrar Sesión
{t('common:profile.logout', 'Cerrar Sesión')}
</button>
</div>
</div>
@@ -852,7 +852,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
PI
</div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
Panadería IA
{t('common:app.name', 'Panadería IA')}
</h2>
</div>
<Button
@@ -860,7 +860,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
size="sm"
onClick={onClose}
className="p-2 hover:bg-[var(--bg-secondary)]"
aria-label="Cerrar navegación"
aria-label={t('common:profile.close_navigation', 'Cerrar navegación')}
>
<X className="w-5 h-5" />
</Button>
@@ -931,7 +931,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
'hover:bg-[var(--bg-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20',
isProfileMenuOpen && 'bg-[var(--bg-secondary)]'
)}
aria-label="Menú de perfil"
aria-label={t('common:profile.profile_menu', 'Menú de perfil')}
aria-expanded={isProfileMenuOpen}
aria-haspopup="true"
>
@@ -966,7 +966,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-tertiary)] transition-colors"
>
<User className="h-4 w-4" />
Perfil
{t('common:profile.profile', 'Perfil')}
</button>
<button
onClick={() => {
@@ -977,7 +977,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-tertiary)] transition-colors"
>
<Settings className="h-4 w-4" />
Configuración
{t('common:profile.settings', 'Configuración')}
</button>
</div>
@@ -987,7 +987,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-tertiary)] transition-colors text-[var(--color-error)]"
>
<LogOut className="h-4 w-4" />
Cerrar Sesión
{t('common:profile.logout', 'Cerrar Sesión')}
</button>
</div>
</div>