Support multiple languages
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user