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,5 +1,6 @@
import React, { forwardRef, useState, useRef, useEffect, useMemo } from 'react';
import { clsx } from 'clsx';
import { useTranslation } from 'react-i18next';
export interface DatePickerProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
label?: string;
@@ -37,7 +38,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
label,
error,
helperText,
placeholder = 'Seleccionar fecha',
placeholder,
size = 'md',
variant = 'outline',
value,
@@ -68,6 +69,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
disabled,
...props
}, ref) => {
const { t } = useTranslation(['ui']);
const datePickerId = id || `datepicker-${Math.random().toString(36).substr(2, 9)}`;
const [internalValue, setInternalValue] = useState<Date | null>(
value !== undefined ? value : defaultValue || null
@@ -110,7 +112,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
},
};
const t = translations[locale];
const localT = translations[locale];
// Format date for display
const formatDate = (date: Date | null): string => {
@@ -422,7 +424,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
id={datePickerId}
type="text"
className={inputClasses}
placeholder={placeholder}
placeholder={placeholder || t('ui:datepicker.placeholder', 'Seleccionar fecha')}
value={inputValue}
onChange={handleInputChange}
onFocus={(e) => {
@@ -475,7 +477,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
value={currentMonth}
onChange={(e) => setCurrentMonth(parseInt(e.target.value))}
>
{t.months.map((month, index) => (
{localT.months.map((month, index) => (
<option key={index} value={index}>
{month}
</option>
@@ -512,7 +514,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
{/* Weekdays */}
<div className="grid grid-cols-7 gap-1 mb-2">
{(firstDayOfWeek === 0 ? t.weekdays : [...t.weekdays.slice(1), t.weekdays[0]]).map((day) => (
{(firstDayOfWeek === 0 ? localT.weekdays : [...localT.weekdays.slice(1), localT.weekdays[0]]).map((day) => (
<div key={day} className="text-xs font-medium text-text-tertiary text-center p-2">
{day}
</div>
@@ -582,7 +584,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
className="px-3 py-1 text-sm text-color-primary hover:bg-color-primary/10 rounded transition-colors duration-150"
onClick={handleTodayClick}
>
{t.today}
{localT.today}
</button>
)}
@@ -592,7 +594,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
className="px-3 py-1 text-sm text-text-tertiary hover:text-color-error hover:bg-color-error/10 rounded transition-colors duration-150"
onClick={handleClear}
>
{t.clear}
{localT.clear}
</button>
)}
</div>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
interface PasswordCriteria {
label: string;
@@ -18,29 +19,31 @@ export const PasswordCriteria: React.FC<PasswordCriteriaProps> = ({
className = '',
showOnlyFailed = false
}) => {
const { t } = useTranslation(['ui']);
const criteria: PasswordCriteria[] = [
{
label: 'Al menos 8 caracteres',
label: t('ui:password_criteria.min_length', 'Al menos 8 caracteres'),
isValid: password.length >= 8,
checkFn: (pwd) => pwd.length >= 8
},
{
label: 'Máximo 128 caracteres',
label: t('ui:password_criteria.max_length', 'Máximo 128 caracteres'),
isValid: password.length <= 128,
checkFn: (pwd) => pwd.length <= 128
},
{
label: 'Al menos una letra mayúscula',
label: t('ui:password_criteria.uppercase', 'Al menos una letra mayúscula'),
isValid: /[A-Z]/.test(password),
regex: /[A-Z]/
},
{
label: 'Al menos una letra minúscula',
label: t('ui:password_criteria.lowercase', 'Al menos una letra minúscula'),
isValid: /[a-z]/.test(password),
regex: /[a-z]/
},
{
label: 'Al menos un número',
label: t('ui:password_criteria.number', 'Al menos un número'),
isValid: /\d/.test(password),
regex: /\d/
}
@@ -104,28 +107,28 @@ export const validatePassword = (password: string): boolean => {
);
};
export const getPasswordErrors = (password: string): string[] => {
export const getPasswordErrors = (password: string, t?: (key: string, fallback: string) => string): string[] => {
const errors: string[] = [];
if (password.length < 8) {
errors.push('La contraseña debe tener al menos 8 caracteres');
errors.push(t?.('ui:password_criteria.errors.min_length', 'La contraseña debe tener al menos 8 caracteres') ?? 'La contraseña debe tener al menos 8 caracteres');
}
if (password.length > 128) {
errors.push('La contraseña no puede exceder 128 caracteres');
errors.push(t?.('ui:password_criteria.errors.max_length', 'La contraseña no puede exceder 128 caracteres') ?? 'La contraseña no puede exceder 128 caracteres');
}
if (!/[A-Z]/.test(password)) {
errors.push('La contraseña debe contener al menos una letra mayúscula');
errors.push(t?.('ui:password_criteria.errors.uppercase', 'La contraseña debe contener al menos una letra mayúscula') ?? 'La contraseña debe contener al menos una letra mayúscula');
}
if (!/[a-z]/.test(password)) {
errors.push('La contraseña debe contener al menos una letra minúscula');
errors.push(t?.('ui:password_criteria.errors.lowercase', 'La contraseña debe contener al menos una letra minúscula') ?? 'La contraseña debe contener al menos una letra minúscula');
}
if (!/\d/.test(password)) {
errors.push('La contraseña debe contener al menos un número');
errors.push(t?.('ui:password_criteria.errors.number', 'La contraseña debe contener al menos un número') ?? 'La contraseña debe contener al menos un número');
}
return errors;
};

View File

@@ -20,6 +20,7 @@ export { StatsCard, StatsGrid } from './Stats';
export { StatusCard, getStatusColor } from './StatusCard';
export { StatusModal } from './StatusModal';
export { TenantSwitcher } from './TenantSwitcher';
export { LanguageSelector, CompactLanguageSelector } from './LanguageSelector';
// Export types
export type { ButtonProps } from './Button';