feat: i18n full support, responsive UI, multi-model AI config, and bug fixes

Major Features:
- Internationalization (i18n) with auto-detection for ES/EN/PT
- Mobile-first responsive design for Studio and Experience
- Multi-model AI configuration (llama3.2:3b, qwen3.5:9b, gpt-oss:latest)
- Course language configuration (auto-detect or fixed per course)

Backend Changes:
- shared/common: ModelType enum for intelligent model selection
- LMS: log_ai_usage function migration (fix chat tutor 500 error)
- LMS/CMS: course language config fields (language_setting, fixed_language)
- LMS: /courses/{id}/language-config endpoint for language detection

Frontend Changes:
- Experience: Enhanced i18n with browser language detection
- Experience: Audio recording with HTTPS check and error handling
- Studio: Memory game with unique pair IDs and debug logging
- Studio: Expanded translations (250+ keys for ES, EN, PT)
- Both: Language selector in headers (mobile responsive)

Documentation:
- AI_MODELS_CONFIG.md: Multi-model configuration guide
- RESPONSIVIDAD_GUIA.md: Mobile-first design patterns
- I18N_RESPONSIVIDAD_IMPLEMENTACION.md: Implementation details
- DEBUG_AUDIO_RECORDING.md: Audio troubleshooting guide
- DEBUG_MEMORY_GAME.md: Memory game debugging steps

Bug Fixes:
- Fix chat tutor 500 error (missing log_ai_usage function)
- Fix audio recording (HTTPS check, browser compatibility)
- Fix memory game pair IDs (unique ID generation)
- Fix HotspotBlock TypeScript errors

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-23 12:24:22 -03:00
parent 0598fc4865
commit 2ff06ee7ae
26 changed files with 2993 additions and 124 deletions
@@ -66,32 +66,72 @@ export default function AudioResponsePlayer({
}, []);
const startRecording = async () => {
// Check if browser supports MediaRecorder
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.error('MediaRecorder API not supported');
alert('Your browser does not support audio recording. Please use Chrome, Firefox, or Edge.');
return;
}
// Check if page is served over HTTPS or localhost
const isSecureContext = window.isSecureContext || window.location.protocol === 'https:' || window.location.hostname === 'localhost';
if (!isSecureContext) {
console.error('getUserMedia requires secure context (HTTPS or localhost)');
alert('Audio recording requires HTTPS. Please access the site via HTTPS or localhost.');
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream);
console.log('[AudioResponse] Requesting microphone access...');
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
});
console.log('[AudioResponse] Microphone access granted');
const mediaRecorder = new MediaRecorder(stream, {
mimeType: MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'
});
mediaRecorderRef.current = mediaRecorder;
audioChunksRef.current = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data);
console.log('[AudioResponse] Data available, chunk size:', event.data.size);
}
};
mediaRecorder.onstop = () => {
console.log('[AudioResponse] Recording stopped, creating blob...');
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
setAudioBlob(audioBlob);
console.log('[AudioResponse] Blob created, size:', audioBlob.size, 'bytes');
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.onerror = (event) => {
console.error('[AudioResponse] MediaRecorder error:', event);
alert('Recording error occurred. Please try again.');
};
mediaRecorder.start();
setIsRecording(true);
setRecordingTime(0);
console.log('[AudioResponse] Recording started');
// Start speech recognition
if (recognitionRef.current) {
setTranscript("");
recognitionRef.current.start();
try {
recognitionRef.current.start();
console.log('[AudioResponse] Speech recognition started');
} catch (err) {
console.warn('[AudioResponse] Could not start speech recognition:', err);
}
}
// Start timer
@@ -99,14 +139,29 @@ export default function AudioResponsePlayer({
setRecordingTime(prev => {
const newTime = prev + 1;
if (timeLimit && newTime >= timeLimit) {
console.log('[AudioResponse] Time limit reached, stopping...');
stopRecording();
}
return newTime;
});
}, 1000);
} catch (error) {
console.error('Error accessing microphone:', error);
alert('Could not access microphone. Please check permissions.');
} catch (error: any) {
console.error('[AudioResponse] Error accessing microphone:', error);
let errorMessage = 'Could not access microphone. ';
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
errorMessage += 'Please allow microphone access and try again.';
} else if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') {
errorMessage += 'No microphone found. Please connect a microphone and try again.';
} else if (error.name === 'NotReadableError' || error.name === 'TrackStartError') {
errorMessage += 'Microphone is already in use by another application.';
} else if (error.name === 'OverconstrainedError') {
errorMessage += 'Microphone does not meet the required constraints.';
} else {
errorMessage += 'Please check permissions and try again.';
}
alert(errorMessage);
}
};
@@ -177,6 +232,31 @@ export default function AudioResponsePlayer({
return (
<div className="space-y-6" id={id}>
{/* Browser Compatibility Check */}
{typeof window !== 'undefined' && (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) && (
<div className="p-6 bg-red-500/10 border-2 border-red-500/20 rounded-2xl">
<div className="flex items-center gap-3 text-red-600 dark:text-red-400 mb-2">
<X className="w-6 h-6" />
<h3 className="text-lg font-bold">Browser Not Supported</h3>
</div>
<p className="text-sm text-gray-700 dark:text-gray-300">
Your browser does not support audio recording. Please use Chrome, Firefox, Edge, or Safari.
</p>
</div>
)}
{!window.isSecureContext && window.location.protocol !== 'https:' && window.location.hostname !== 'localhost' && (
<div className="p-6 bg-yellow-500/10 border-2 border-yellow-500/20 rounded-2xl">
<div className="flex items-center gap-3 text-yellow-600 dark:text-yellow-400 mb-2">
<BrainCircuit className="w-6 h-6" />
<h3 className="text-lg font-bold">HTTPS Required</h3>
</div>
<p className="text-sm text-gray-700 dark:text-gray-300">
Audio recording requires a secure connection (HTTPS). Please access this site via HTTPS or localhost.
</p>
</div>
)}
<div className="p-8 glass border-black/5 dark:border-white/5 rounded-3xl space-y-6 bg-black/[0.02] dark:bg-black/20">
<div className="flex items-start gap-4">
<div className="p-3 bg-purple-600/10 dark:bg-purple-500/20 rounded-xl">
+51 -5
View File
@@ -7,27 +7,71 @@ import pt from '../lib/locales/pt.json';
const translations: Record<string, any> = { en, es, pt };
// Idiomas soportados
export const SUPPORTED_LANGUAGES = ['es', 'en', 'pt'] as const;
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
interface I18nContextType {
language: string;
setLanguage: (lang: string) => void;
t: (path: string) => string;
detectBrowserLanguage: () => string;
}
const I18nContext = createContext<I18nContextType | undefined>(undefined);
/**
* Detecta el idioma del navegador del usuario
*/
function detectBrowserLanguage(): string {
if (typeof navigator === 'undefined') return 'es';
// Obtener idiomas del navegador (puede ser un array)
const browserLanguages = navigator.languages || [navigator.language || (navigator as any).userLanguage];
// Buscar el primer idioma soportado
for (const browserLang of browserLanguages) {
// Idioma completo (ej: 'en-US')
if (SUPPORTED_LANGUAGES.includes(browserLang as SupportedLanguage)) {
return browserLang;
}
// Solo código de idioma (ej: 'en' de 'en-US')
const langCode = browserLang.split('-')[0].toLowerCase();
if (SUPPORTED_LANGUAGES.includes(langCode as SupportedLanguage)) {
return langCode;
}
}
// Fallback a español
return 'es';
}
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [language, setLanguageState] = useState('es'); // Default to Spanish for Experience as it's more localized by default
const [language, setLanguageState] = useState('es');
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
// 1. Primero intentar cargar de localStorage
const savedLang = localStorage.getItem('experience_language');
if (savedLang) {
if (savedLang && SUPPORTED_LANGUAGES.includes(savedLang as SupportedLanguage)) {
setLanguageState(savedLang);
} else {
// 2. Si no hay guardado, detectar del navegador
const detectedLang = detectBrowserLanguage();
setLanguageState(detectedLang);
localStorage.setItem('experience_language', detectedLang);
}
setIsInitialized(true);
}, []);
const setLanguage = (lang: string) => {
setLanguageState(lang);
localStorage.setItem('experience_language', lang);
if (SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage)) {
setLanguageState(lang);
localStorage.setItem('experience_language', lang);
}
};
const t = (path: string): string => {
@@ -46,7 +90,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
};
return (
<I18nContext.Provider value={{ language, setLanguage, t }}>
<I18nContext.Provider value={{ language, setLanguage, t, detectBrowserLanguage }}>
{children}
</I18nContext.Provider>
);
@@ -59,3 +103,5 @@ export function useTranslation() {
}
return context;
}
export { detectBrowserLanguage };
@@ -0,0 +1,108 @@
"use client";
import { useEffect, useState, useCallback } from 'react';
import { useTranslation } from '@/context/I18nContext';
interface CourseLanguageConfig {
language_setting: 'auto' | 'fixed';
fixed_language: string | null;
}
/**
* Hook para manejar el idioma específico de un curso
*
* - Si el curso está en modo 'auto', usa el idioma del usuario
* - Si el curso está en modo 'fixed', usa el idioma fijo del curso
*
* @param courseId - ID del curso actual
* @returns El idioma que debe usarse para este curso
*/
export function useCourseLanguage(courseId: string | null) {
const { language: userLanguage } = useTranslation();
const [courseLanguage, setCourseLanguage] = useState<string>(userLanguage);
const [isLoading, setIsLoading] = useState(true);
const [config, setConfig] = useState<CourseLanguageConfig | null>(null);
// Función para cargar la configuración de idioma del curso
const loadCourseLanguageConfig = useCallback(async () => {
if (!courseId) {
setCourseLanguage(userLanguage);
setIsLoading(false);
return;
}
try {
// Importar dinámicamente para evitar circular dependencies
const { lmsApi } = await import('@/lib/api');
const response = await lmsApi.get(`/courses/${courseId}/language-config`);
const data = response.data;
setConfig(data);
// Determinar qué idioma usar
if (data.language_setting === 'fixed' && data.fixed_language) {
setCourseLanguage(data.fixed_language);
} else {
// Modo 'auto' o sin configuración: usar idioma del usuario
setCourseLanguage(userLanguage);
}
} catch (error) {
console.error('Error loading course language config:', error);
// Fallback: usar idioma del usuario
setCourseLanguage(userLanguage);
} finally {
setIsLoading(false);
}
}, [courseId, userLanguage]);
// Cargar configuración cuando cambia el courseId
useEffect(() => {
loadCourseLanguageConfig();
}, [loadCourseLanguageConfig]);
// Función para verificar si el curso usa idioma fijo
const isFixedLanguage = useCallback(() => {
return config?.language_setting === 'fixed';
}, [config]);
// Función para obtener el idioma actual (curso o usuario)
const getCurrentLanguage = useCallback(() => {
return courseLanguage;
}, [courseLanguage]);
return {
courseLanguage,
isFixedLanguage,
getCurrentLanguage,
isLoading,
refreshConfig: loadCourseLanguageConfig,
};
}
/**
* Hook para cambiar el idioma del usuario (solo funciona si el curso está en modo 'auto')
*
* @param courseId - ID del curso actual
*/
export function useCourseLanguageSwitcher(courseId: string | null) {
const { setLanguage, language } = useTranslation();
const { isFixedLanguage } = useCourseLanguage(courseId);
const canChangeLanguage = !isFixedLanguage();
const changeLanguage = (newLanguage: string) => {
if (canChangeLanguage) {
setLanguage(newLanguage);
} else {
console.warn('Cannot change language: course has fixed language setting');
}
};
return {
currentLanguage: language,
canChangeLanguage,
changeLanguage,
isFixed: isFixedLanguage(),
};
}
+204 -5
View File
@@ -5,14 +5,31 @@
"next": "Next",
"finish": "Finish",
"error": "Error",
"retry": "Retry"
"retry": "Retry",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"close": "Close",
"confirm": "Confirm",
"success": "Success",
"warning": "Warning",
"info": "Info",
"search": "Search",
"noResults": "No results",
"viewAll": "View all",
"learnMore": "Learn more"
},
"nav": {
"catalog": "Catalog",
"myLearning": "My Learning",
"bookmarks": "Bookmarks",
"profile": "Profile",
"signOut": "Sign Out"
"signOut": "Sign Out",
"selectLanguage": "Select Language",
"menu": "Menu",
"notifications": "Notifications",
"settings": "Settings"
},
"course": {
"modules": "Modules",
@@ -20,12 +37,194 @@
"progress": "Progress",
"calendar": "Timeline",
"start": "Start",
"continue": "Continue"
"continue": "Continue",
"enroll": "Enroll",
"enrolled": "Enrolled",
"price": "Price",
"free": "Free",
"duration": "Duration",
"level": "Level",
"description": "Description",
"objectives": "Objectives",
"requirements": "Requirements",
"instructor": "Instructor",
"certificate": "Certificate",
"aboutCourse": "About Course",
"content": "Content",
"reviews": "Reviews",
"faq": "FAQ"
},
"lesson": {
"summary": "AI Summary",
"transcription": "Transcription",
"complete": "Mark as Completed",
"nextLesson": "Next Lesson"
"nextLesson": "Next Lesson",
"previousLesson": "Previous Lesson",
"locked": "Locked",
"lockedMessage": "Complete previous lessons to unlock",
"markComplete": "Mark as Completed",
"inProgress": "In Progress",
"completed": "Completed",
"notStarted": "Not Started",
"dueDate": "Due Date",
"timeRemaining": "Time Remaining",
"overdue": "Overdue"
},
"auth": {
"login": "Login",
"register": "Register",
"email": "Email",
"password": "Password",
"fullName": "Full Name",
"forgotPassword": "Forgot Password?",
"resetPassword": "Reset Password",
"noAccount": "Don't have an account?",
"hasAccount": "Already have an account?",
"signIn": "Sign In",
"signUp": "Sign Up",
"orContinueWith": "Or continue with",
"google": "Google",
"microsoft": "Microsoft",
"sso": "Single Sign-On"
},
"dashboard": {
"welcome": "Welcome",
"continueLearning": "Continue Learning",
"recommendedCourses": "Recommended Courses",
"recentActivity": "Recent Activity",
"myProgress": "My Progress",
"certificates": "Certificates",
"badges": "Badges",
"stats": {
"coursesCompleted": "Courses Completed",
"hoursLearned": "Hours Learned",
"averageGrade": "Average Grade",
"currentStreak": "Current Streak"
}
},
"profile": {
"myProfile": "My Profile",
"editProfile": "Edit Profile",
"avatar": "Avatar",
"bio": "Bio",
"language": "Language",
"timezone": "Timezone",
"notifications": "Notifications",
"privacy": "Privacy",
"account": "Account",
"changePassword": "Change Password",
"deleteAccount": "Delete Account",
"portfolio": "Portfolio",
"achievements": "Achievements",
"publicProfile": "Public Profile"
},
"gamification": {
"level": "Level",
"xp": "Experience Points",
"leaderboard": "Leaderboard",
"rank": "Rank",
"points": "Points",
"badge": "Badge",
"badges": "Badges",
"earned": "Earned",
"locked": "Locked",
"nextLevel": "Next Level",
"xpToNextLevel": "XP to next level"
},
"grading": {
"grade": "Grade",
"grades": "Grades",
"score": "Score",
"percentage": "Percentage",
"passingGrade": "Passing Grade",
"failed": "Failed",
"passed": "Passed",
"excellent": "Excellent",
"good": "Good",
"regular": "Regular",
"poor": "Poor",
"feedback": "Feedback",
"submissionDate": "Submission Date",
"gradedDate": "Graded Date"
},
"quiz": {
"startQuiz": "Start Quiz",
"submitAnswers": "Submit Answers",
"questionNumber": "Question",
"of": "of",
"correctAnswer": "Correct Answer",
"yourAnswer": "Your Answer",
"explanation": "Explanation",
"tryAgain": "Try Again",
"attempts": "Attempts",
"attemptsRemaining": "attempts remaining",
"timeLimit": "Time Limit",
"timeExpired": "Time Expired"
},
"forum": {
"discussions": "Discussions",
"newThread": "New Discussion",
"title": "Title",
"content": "Content",
"post": "Post",
"reply": "Reply",
"replies": "Replies",
"views": "Views",
"lastActivity": "Last Activity",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"markAsSolved": "Mark as Solved",
"upvote": "Upvote",
"downvote": "Downvote"
},
"payments": {
"purchase": "Purchase",
"buyNow": "Buy Now",
"paymentMethod": "Payment Method",
"cardNumber": "Card Number",
"expiryDate": "Expiry Date",
"cvv": "CVV",
"cardholderName": "Cardholder Name",
"paymentSuccess": "Payment Success",
"paymentFailed": "Payment Failed",
"refund": "Refund",
"invoice": "Invoice",
"receipt": "Receipt"
},
"accessibility": {
"skipToContent": "Skip to content",
"increaseContrast": "Increase Contrast",
"decreaseContrast": "Decrease Contrast",
"largerText": "Larger Text",
"smallerText": "Smaller Text",
"screenReader": "Screen Reader"
},
"language": {
"selectLanguage": "Select Language",
"auto": "Automatic",
"spanish": "Spanish",
"english": "English",
"portuguese": "Portuguese",
"courseLanguage": "Course Language",
"interfaceLanguage": "Interface Language"
},
"errors": {
"notFound": "Not found",
"unauthorized": "Unauthorized",
"serverError": "Server error",
"networkError": "Network error",
"sessionExpired": "Session expired",
"invalidCredentials": "Invalid credentials",
"emailInUse": "Email already in use",
"weakPassword": "Weak password"
},
"dates": {
"today": "Today",
"yesterday": "Yesterday",
"tomorrow": "Tomorrow",
"daysAgo": "{{days}} days ago",
"weeksAgo": "{{weeks}} weeks ago",
"monthsAgo": "{{months}} months ago",
"yearsAgo": "{{years}} years ago"
}
}
}
+204 -5
View File
@@ -5,14 +5,31 @@
"next": "Siguiente",
"finish": "Finalizar",
"error": "Error",
"retry": "Reintentar"
"retry": "Reintentar",
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"edit": "Editar",
"close": "Cerrar",
"confirm": "Confirmar",
"success": "Éxito",
"warning": "Advertencia",
"info": "Información",
"search": "Buscar",
"noResults": "Sin resultados",
"viewAll": "Ver todo",
"learnMore": "Más información"
},
"nav": {
"catalog": "Catálogo",
"myLearning": "Mi Aprendizaje",
"bookmarks": "Marcadores",
"profile": "Perfil",
"signOut": "Cerrar Sesión"
"signOut": "Cerrar Sesión",
"selectLanguage": "Seleccionar Idioma",
"menu": "Menú",
"notifications": "Notificaciones",
"settings": "Configuración"
},
"course": {
"modules": "Módulos",
@@ -20,12 +37,194 @@
"progress": "Progreso",
"calendar": "Cronología",
"start": "Comenzar",
"continue": "Continuar"
"continue": "Continuar",
"enroll": "Inscribirse",
"enrolled": "Inscrito",
"price": "Precio",
"free": "Gratis",
"duration": "Duración",
"level": "Nivel",
"description": "Descripción",
"objectives": "Objetivos",
"requirements": "Requisitos",
"instructor": "Instructor",
"certificate": "Certificado",
"aboutCourse": "Acerca del Curso",
"content": "Contenido",
"reviews": "Reseñas",
"faq": "Preguntas Frecuentes"
},
"lesson": {
"summary": "Resumen AI",
"transcription": "Transcripción",
"complete": "Marcar como Completado",
"nextLesson": "Siguiente Lección"
"nextLesson": "Siguiente Lección",
"previousLesson": "Lección Anterior",
"locked": "Bloqueado",
"lockedMessage": "Completa las lecciones anteriores para desbloquear",
"markComplete": "Marcar como Completada",
"inProgress": "En Progreso",
"completed": "Completada",
"notStarted": "No Iniciada",
"dueDate": "Fecha Límite",
"timeRemaining": "Tiempo Restante",
"overdue": "Vencido"
},
"auth": {
"login": "Iniciar Sesión",
"register": "Registrarse",
"email": "Correo Electrónico",
"password": "Contraseña",
"fullName": "Nombre Completo",
"forgotPassword": "¿Olvidaste tu contraseña?",
"resetPassword": "Restablecer Contraseña",
"noAccount": "¿No tienes cuenta?",
"hasAccount": "¿Ya tienes cuenta?",
"signIn": "Ingresar",
"signUp": "Crear Cuenta",
"orContinueWith": "O continúa con",
"google": "Google",
"microsoft": "Microsoft",
"sso": "Inicio de Sesión Único"
},
"dashboard": {
"welcome": "Bienvenido",
"continueLearning": "Continuar Aprendiendo",
"recommendedCourses": "Cursos Recomendados",
"recentActivity": "Actividad Reciente",
"myProgress": "Mi Progreso",
"certificates": "Certificados",
"badges": "Insignias",
"stats": {
"coursesCompleted": "Cursos Completados",
"hoursLearned": "Horas Aprendidas",
"averageGrade": "Nota Promedio",
"currentStreak": "Racha Actual"
}
},
"profile": {
"myProfile": "Mi Perfil",
"editProfile": "Editar Perfil",
"avatar": "Avatar",
"bio": "Biografía",
"language": "Idioma",
"timezone": "Zona Horaria",
"notifications": "Notificaciones",
"privacy": "Privacidad",
"account": "Cuenta",
"changePassword": "Cambiar Contraseña",
"deleteAccount": "Eliminar Cuenta",
"portfolio": "Portafolio",
"achievements": "Logros",
"publicProfile": "Perfil Público"
},
"gamification": {
"level": "Nivel",
"xp": "Puntos de Experiencia",
"leaderboard": "Tabla de Clasificación",
"rank": "Rango",
"points": "Puntos",
"badge": "Insignia",
"badges": "Insignias",
"earned": "Obtenida",
"locked": "Bloqueada",
"nextLevel": "Siguiente Nivel",
"xpToNextLevel": "XP para el siguiente nivel"
},
"grading": {
"grade": "Nota",
"grades": "Notas",
"score": "Puntaje",
"percentage": "Porcentaje",
"passingGrade": "Nota de Aprobación",
"failed": "Reprobado",
"passed": "Aprobado",
"excellent": "Excelente",
"good": "Bueno",
"regular": "Regular",
"poor": "Deficiente",
"feedback": "Retroalimentación",
"submissionDate": "Fecha de Entrega",
"gradedDate": "Fecha de Calificación"
},
"quiz": {
"startQuiz": "Comenzar Cuestionario",
"submitAnswers": "Enviar Respuestas",
"questionNumber": "Pregunta",
"of": "de",
"correctAnswer": "Respuesta Correcta",
"yourAnswer": "Tu Respuesta",
"explanation": "Explicación",
"tryAgain": "Intentar de Nuevo",
"attempts": "Intentos",
"attemptsRemaining": "intentos restantes",
"timeLimit": "Límite de Tiempo",
"timeExpired": "Tiempo Expirado"
},
"forum": {
"discussions": "Discusiones",
"newThread": "Nueva Discusión",
"title": "Título",
"content": "Contenido",
"post": "Publicar",
"reply": "Responder",
"replies": "Respuestas",
"views": "Vistas",
"lastActivity": "Última Actividad",
"subscribe": "Suscribirse",
"unsubscribe": "Desuscribirse",
"markAsSolved": "Marcar como Resuelto",
"upvote": "Votar a Favor",
"downvote": "Votar en Contra"
},
"payments": {
"purchase": "Comprar",
"buyNow": "Comprar Ahora",
"paymentMethod": "Método de Pago",
"cardNumber": "Número de Tarjeta",
"expiryDate": "Fecha de Vencimiento",
"cvv": "CVV",
"cardholderName": "Nombre del Titular",
"paymentSuccess": "Pago Exitoso",
"paymentFailed": "Pago Fallido",
"refund": "Reembolso",
"invoice": "Factura",
"receipt": "Recibo"
},
"accessibility": {
"skipToContent": "Saltar al contenido",
"increaseContrast": "Aumentar Contraste",
"decreaseContrast": "Disminuir Contraste",
"largerText": "Texto Más Grande",
"smallerText": "Texto Más Pequeño",
"screenReader": "Lector de Pantalla"
},
"language": {
"selectLanguage": "Seleccionar Idioma",
"auto": "Automático",
"spanish": "Español",
"english": "Inglés",
"portuguese": "Portugués",
"courseLanguage": "Idioma del Curso",
"interfaceLanguage": "Idioma de la Interfaz"
},
"errors": {
"notFound": "No encontrado",
"unauthorized": "No autorizado",
"serverError": "Error del servidor",
"networkError": "Error de red",
"sessionExpired": "Sesión expirada",
"invalidCredentials": "Credenciales inválidas",
"emailInUse": "Correo ya en uso",
"weakPassword": "Contraseña débil"
},
"dates": {
"today": "Hoy",
"yesterday": "Ayer",
"tomorrow": "Mañana",
"daysAgo": "hace {{days}} días",
"weeksAgo": "hace {{weeks}} semanas",
"monthsAgo": "hace {{months}} meses",
"yearsAgo": "hace {{years}} años"
}
}
}
+208 -9
View File
@@ -3,29 +3,228 @@
"loading": "Carregando...",
"back": "Voltar",
"next": "Próximo",
"finish": "Finalizar",
"finish": "Concluir",
"error": "Erro",
"retry": "Repetir"
"retry": "Tentar Novamente",
"save": "Salvar",
"cancel": "Cancelar",
"delete": "Excluir",
"edit": "Editar",
"close": "Fechar",
"confirm": "Confirmar",
"success": "Sucesso",
"warning": "Aviso",
"info": "Informação",
"search": "Buscar",
"noResults": "Sem resultados",
"viewAll": "Ver tudo",
"learnMore": "Saiba mais"
},
"nav": {
"catalog": "Catálogo",
"myLearning": "Meu Aprendizado",
"bookmarks": "Favoritos",
"profile": "Perfil",
"signOut": "Sair"
"signOut": "Sair",
"selectLanguage": "Selecionar Idioma",
"menu": "Menu",
"notifications": "Notificações",
"settings": "Configurações"
},
"course": {
"modules": "Módulos",
"lessons": "Lições",
"lessons": "Aulas",
"progress": "Progresso",
"calendar": "Cronologia",
"calendar": "Cronograma",
"start": "Começar",
"continue": "Continuar"
"continue": "Continuar",
"enroll": "Inscrever-se",
"enrolled": "Inscrito",
"price": "Preço",
"free": "Gratuito",
"duration": "Duração",
"level": "Nível",
"description": "Descrição",
"objectives": "Objetivos",
"requirements": "Requisitos",
"instructor": "Instrutor",
"certificate": "Certificado",
"aboutCourse": "Sobre o Curso",
"content": "Conteúdo",
"reviews": "Avaliações",
"faq": "Perguntas Frequentes"
},
"lesson": {
"summary": "Resumo IA",
"transcription": "Transcrição",
"complete": "Marcar como Concluído",
"nextLesson": "Próxima Lição"
"complete": "Marcar como Concluída",
"nextLesson": "Próxima Aula",
"previousLesson": "Aula Anterior",
"locked": "Bloqueada",
"lockedMessage": "Complete as aulas anteriores para desbloquear",
"markComplete": "Marcar como Concluída",
"inProgress": "Em Progresso",
"completed": "Concluída",
"notStarted": "Não Iniciada",
"dueDate": "Data Limite",
"timeRemaining": "Tempo Restante",
"overdue": "Atrasada"
},
"auth": {
"login": "Entrar",
"register": "Cadastrar",
"email": "E-mail",
"password": "Senha",
"fullName": "Nome Completo",
"forgotPassword": "Esqueceu a senha?",
"resetPassword": "Redefinir Senha",
"noAccount": "Não tem uma conta?",
"hasAccount": "Já tem uma conta?",
"signIn": "Entrar",
"signUp": "Criar Conta",
"orContinueWith": "Ou continue com",
"google": "Google",
"microsoft": "Microsoft",
"sso": "Login Único"
},
"dashboard": {
"welcome": "Bem-vindo",
"continueLearning": "Continuar Aprendendo",
"recommendedCourses": "Cursos Recomendados",
"recentActivity": "Atividade Recente",
"myProgress": "Meu Progresso",
"certificates": "Certificados",
"badges": "Distintivos",
"stats": {
"coursesCompleted": "Cursos Concluídos",
"hoursLearned": "Horas Aprendidas",
"averageGrade": "Nota Média",
"currentStreak": "Sequência Atual"
}
},
"profile": {
"myProfile": "Meu Perfil",
"editProfile": "Editar Perfil",
"avatar": "Avatar",
"bio": "Biografia",
"language": "Idioma",
"timezone": "Fuso Horário",
"notifications": "Notificações",
"privacy": "Privacidade",
"account": "Conta",
"changePassword": "Alterar Senha",
"deleteAccount": "Excluir Conta",
"portfolio": "Portfólio",
"achievements": "Conquistas",
"publicProfile": "Perfil Público"
},
"gamification": {
"level": "Nível",
"xp": "Pontos de Experiência",
"leaderboard": "Classificação",
"rank": "Posição",
"points": "Pontos",
"badge": "Distintivo",
"badges": "Distintivos",
"earned": "Conquistado",
"locked": "Bloqueado",
"nextLevel": "Próximo Nível",
"xpToNextLevel": "XP para o próximo nível"
},
"grading": {
"grade": "Nota",
"grades": "Notas",
"score": "Pontuação",
"percentage": "Porcentagem",
"passingGrade": "Nota Mínima",
"failed": "Reprovado",
"passed": "Aprovado",
"excellent": "Excelente",
"good": "Bom",
"regular": "Regular",
"poor": "Insuficiente",
"feedback": "Feedback",
"submissionDate": "Data de Entrega",
"gradedDate": "Data de Avaliação"
},
"quiz": {
"startQuiz": "Iniciar Questionário",
"submitAnswers": "Enviar Respostas",
"questionNumber": "Pergunta",
"of": "de",
"correctAnswer": "Resposta Correta",
"yourAnswer": "Sua Resposta",
"explanation": "Explicação",
"tryAgain": "Tentar Novamente",
"attempts": "Tentativas",
"attemptsRemaining": "tentativas restantes",
"timeLimit": "Limite de Tempo",
"timeExpired": "Tempo Esgotado"
},
"forum": {
"discussions": "Discussões",
"newThread": "Nova Discussão",
"title": "Título",
"content": "Conteúdo",
"post": "Publicar",
"reply": "Responder",
"replies": "Respostas",
"views": "Visualizações",
"lastActivity": "Última Atividade",
"subscribe": "Inscrever-se",
"unsubscribe": "Cancelar Inscrição",
"markAsSolved": "Marcar como Resolvido",
"upvote": "Votar a Favor",
"downvote": "Votar Contra"
},
"payments": {
"purchase": "Comprar",
"buyNow": "Comprar Agora",
"paymentMethod": "Método de Pagamento",
"cardNumber": "Número do Cartão",
"expiryDate": "Data de Validade",
"cvv": "CVV",
"cardholderName": "Nome do Titular",
"paymentSuccess": "Pagamento Bem-Sucedido",
"paymentFailed": "Pagamento Falhou",
"refund": "Reembolso",
"invoice": "Fatura",
"receipt": "Recibo"
},
"accessibility": {
"skipToContent": "Pular para o conteúdo",
"increaseContrast": "Aumentar Contraste",
"decreaseContrast": "Diminuir Contraste",
"largerText": "Texto Maior",
"smallerText": "Texto Menor",
"screenReader": "Leitor de Tela"
},
"language": {
"selectLanguage": "Selecionar Idioma",
"auto": "Automático",
"spanish": "Espanhol",
"english": "Inglês",
"portuguese": "Português",
"courseLanguage": "Idioma do Curso",
"interfaceLanguage": "Idioma da Interface"
},
"errors": {
"notFound": "Não encontrado",
"unauthorized": "Não autorizado",
"serverError": "Erro do servidor",
"networkError": "Erro de rede",
"sessionExpired": "Sessão expirada",
"invalidCredentials": "Credenciais inválidas",
"emailInUse": "E-mail já em uso",
"weakPassword": "Senha fraca"
},
"dates": {
"today": "Hoje",
"yesterday": "Ontem",
"tomorrow": "Amanhã",
"daysAgo": "há {{days}} dias",
"weeksAgo": "há {{weeks}} semanas",
"monthsAgo": "há {{months}} meses",
"yearsAgo": "há {{years}} anos"
}
}
}
@@ -3,7 +3,7 @@
import { useEffect, useState, useCallback } from "react";
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { cmsApi, Lesson, Block, GradingCategory, LibraryBlock, Rubric, RubricLevel, RubricCriterion, LessonDependency, getImageUrl } from '@/lib/api';
import { cmsApi, Lesson, Block, GradingCategory, LibraryBlock, Rubric, RubricLevel, RubricCriterion, LessonDependency, getImageUrl, generateUUID } from '@/lib/api';
import {
Layout,
CheckCircle2,
@@ -103,7 +103,9 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
setImportantDateType(lessonData.important_date_type || "");
setIsPreviewable(lessonData.is_previewable || false);
if (lessonData.metadata?.blocks) {
if (lessonData.content_blocks && Array.isArray(lessonData.content_blocks)) {
setBlocks(lessonData.content_blocks);
} else if (lessonData.metadata?.blocks) {
setBlocks(lessonData.metadata.blocks);
} else {
setBlocks([
@@ -195,7 +197,8 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
}
const updated = await cmsApi.updateLesson(lesson.id, {
metadata: { ...lesson.metadata, blocks },
metadata: { ...lesson.metadata },
content_blocks: blocks,
content_url,
content_type, // Sync type to ensure backend triggers transcription
summary,
@@ -218,7 +221,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
const addBlock = (type: Block['type']) => {
const newBlock: Block = {
id: crypto.randomUUID(),
id: generateUUID(),
type,
...(type === 'description' && { content: "" }),
...(type === 'media' && { url: "", media_type: 'video' as const, config: { maxPlays: 0 } }),
@@ -4,7 +4,7 @@ import { useState, useRef } from "react";
import Image from "next/image";
import { Search, MapPin, Plus, Trash2, Image as ImageIcon, Crosshair, Wand2, Loader2 } from "lucide-react";
import AssetPickerModal from "../AssetPickerModal";
import { Asset, getImageUrl, cmsApi } from "@/lib/api";
import { Asset, getImageUrl, cmsApi, generateUUID } from "@/lib/api";
interface Hotspot {
id: string;
@@ -46,12 +46,19 @@ export default function HotspotBlock({
setIsGenerating(true);
try {
const data = await cmsApi.generateHotspots(lessonId, { image_url: imageUrl });
const newHotspots = data.map(h => ({
id: crypto.randomUUID(),
x: h.x,
y: h.y,
// Handle different response formats from AI
let hotspotsArray = Array.isArray(data) ? data : (data.hotspots || data.items || []);
if (!Array.isArray(hotspotsArray)) {
throw new Error("La respuesta de la IA no es un array válido");
}
const newHotspots = hotspotsArray.map((h: any) => ({
id: generateUUID(),
x: typeof h.x === 'number' ? h.x : 50,
y: typeof h.y === 'number' ? h.y : 50,
radius: 5,
label: h.label
label: h.label || 'Punto de interés'
}));
onChange({ hotspots: [...hotspots, ...newHotspots] });
} catch (error) {
@@ -76,7 +83,7 @@ export default function HotspotBlock({
const y = ((e.clientY - rect.top) / rect.height) * 100;
const newHotspot: Hotspot = {
id: crypto.randomUUID(),
id: generateUUID(),
x,
y,
radius: 5,
@@ -16,27 +16,38 @@ interface MemoryBlockProps {
onChange: (updates: { title?: string; pairs?: MatchingPair[] }) => void;
}
// Generate unique ID for pairs
const generatePairId = () => {
return `pair_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
export default function MemoryBlock({ id, title, pairs = [], editMode, onChange }: MemoryBlockProps) {
const addPair = () => {
const newPair: MatchingPair = {
id: Math.random().toString(36).substr(2, 9),
id: generatePairId(),
left: "",
right: ""
};
console.log('[MemoryBlock] Adding new pair:', newPair);
onChange({ pairs: [...pairs, newPair] });
};
const updatePair = (index: number, updates: Partial<MatchingPair>) => {
const newPairs = [...pairs];
newPairs[index] = { ...newPairs[index], ...updates };
console.log('[MemoryBlock] Updating pair at index', index, ':', updates);
onChange({ pairs: newPairs });
};
const removePair = (index: number) => {
console.log('[MemoryBlock] Removing pair at index', index);
const newPairs = pairs.filter((_, i) => i !== index);
onChange({ pairs: newPairs });
};
// Debug: Log pairs on render
console.log('[MemoryBlock] Render with pairs:', pairs);
if (!editMode) {
return (
<div className="space-y-8" id={id}>
+43 -6
View File
@@ -8,29 +8,64 @@ import pt from '../lib/locales/pt.json';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const translations: Record<string, any> = { en, es, pt };
// Idiomas soportados
export const SUPPORTED_LANGUAGES = ['es', 'en', 'pt'] as const;
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
interface I18nContextType {
language: string;
setLanguage: (lang: string) => void;
t: (path: string) => string;
detectBrowserLanguage: () => string;
}
const I18nContext = createContext<I18nContextType | undefined>(undefined);
/**
* Detecta el idioma del navegador del usuario
*/
function detectBrowserLanguage(): string {
if (typeof navigator === 'undefined') return 'es';
const browserLanguages = navigator.languages || [navigator.language || (navigator as any).userLanguage];
for (const browserLang of browserLanguages) {
if (SUPPORTED_LANGUAGES.includes(browserLang as SupportedLanguage)) {
return browserLang;
}
const langCode = browserLang.split('-')[0].toLowerCase();
if (SUPPORTED_LANGUAGES.includes(langCode as SupportedLanguage)) {
return langCode;
}
}
return 'es';
}
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [language, setLanguageState] = useState('es');
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
const savedLang = localStorage.getItem('studio_language');
if (savedLang) {
if (savedLang && SUPPORTED_LANGUAGES.includes(savedLang as SupportedLanguage)) {
setLanguageState(savedLang);
} else {
// Try to detect from user profile if available, but for now default or localstorage
const detectedLang = detectBrowserLanguage();
setLanguageState(detectedLang);
localStorage.setItem('studio_language', detectedLang);
}
setIsInitialized(true);
}, []);
const setLanguage = (lang: string) => {
setLanguageState(lang);
localStorage.setItem('studio_language', lang);
if (SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage)) {
setLanguageState(lang);
localStorage.setItem('studio_language', lang);
}
};
const t = (path: string): string => {
@@ -41,7 +76,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
if (result[key]) {
result = result[key];
} else {
return path; // Fallback to path if key missing
return path;
}
}
@@ -49,7 +84,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
};
return (
<I18nContext.Provider value={{ language, setLanguage, t }}>
<I18nContext.Provider value={{ language, setLanguage, t, detectBrowserLanguage }}>
{children}
</I18nContext.Provider>
);
@@ -62,3 +97,5 @@ export function useTranslation() {
}
return context;
}
export { detectBrowserLanguage };
+13
View File
@@ -11,6 +11,19 @@ const getApiBaseUrl = (defaultPort: string, envVar?: string) => {
export const API_BASE_URL = getApiBaseUrl("3001", process.env.NEXT_PUBLIC_CMS_API_URL);
export const LMS_API_BASE_URL = getApiBaseUrl("3002", process.env.NEXT_PUBLIC_LMS_API_URL);
// Polyfill for crypto.randomUUID() for non-HTTPS contexts
export function generateUUID(): string {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback for non-secure contexts
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
export const getImageUrl = (path?: string) => {
if (!path) return '';
if (path.startsWith('http')) return path;
+235 -21
View File
@@ -1,39 +1,253 @@
{
"common": {
"loading": "Cargando...",
"back": "Volver",
"next": "Siguiente",
"finish": "Finalizar",
"error": "Error",
"retry": "Reintentar",
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"edit": "Editar",
"close": "Cerrar",
"confirm": "Confirmar",
"success": "Éxito",
"warning": "Advertencia",
"create": "Crear",
"loading": "Cargando...",
"add": "Agregar",
"remove": "Eliminar",
"update": "Actualizar",
"search": "Buscar",
"back": "Volver",
"next": "Siguiente",
"filter": "Filtrar",
"export": "Exportar",
"import": "Importar",
"download": "Descargar",
"upload": "Subir",
"preview": "Vista Previa",
"publish": "Publicar",
"preview": "Previsualizar"
"draft": "Borrador",
"archive": "Archivar",
"duplicate": "Duplicar",
"settings": "Configuración"
},
"nav": {
"dashboard": "Dashboard",
"courses": "Cursos",
"globalControl": "Control Global",
"webhooks": "Webhooks",
"library": "Librería",
"questionBank": "Banco de Preguntas",
"testTemplates": "Plantillas de Pruebas",
"analytics": "Analíticas",
"students": "Estudiantes",
"instructors": "Instructores",
"settings": "Configuración",
"profile": "Perfil",
"signOut": "Cerrar Sesión"
"signOut": "Cerrar Sesión",
"selectLanguage": "Seleccionar Idioma",
"controlPanel": "Panel de Control",
"webhooks": "Webhooks",
"globalControl": "Control Global"
},
"dashboard": {
"title": "Panel de Control",
"newCourse": "Nuevo Curso",
"aiBuilder": "Constructor AI",
"noCourses": "No hay cursos disponibles."
"course": {
"createCourse": "Crear Curso",
"editCourse": "Editar Curso",
"deleteCourse": "Eliminar Curso",
"courseTitle": "Título del Curso",
"courseDescription": "Descripción del Curso",
"courseLanguage": "Idioma del Curso",
"languageAuto": "Automático (detectar del usuario)",
"languageFixed": "Fijo (siempre este idioma)",
"languageEnglish": "Inglés",
"languageSpanish": "Español",
"languagePortuguese": "Portugués",
"modules": "Módulos",
"addModule": "Agregar Módulo",
"editModule": "Editar Módulo",
"deleteModule": "Eliminar Módulo",
"moduleName": "Nombre del Módulo",
"moduleDescription": "Descripción del Módulo",
"lessons": "Lecciones",
"addLesson": "Agregar Lección",
"editLesson": "Editar Lección",
"deleteLesson": "Eliminar Lección",
"lessonTitle": "Título de la Lección",
"lessonType": "Tipo de Lección",
"blocks": "Bloques",
"addBlock": "Agregar Bloque",
"blockType": "Tipo de Bloque",
"reorderBlocks": "Reordenar Bloques",
"moveUp": "Mover Arriba",
"moveDown": "Mover Abajo",
"pricing": "Precios",
"currency": "Moneda",
"enrollment": "Inscripciones",
"published": "Publicado",
"unpublished": "No Publicado",
"visibility": "Visibilidad",
"public": "Público",
"private": "Privado"
},
"editor": {
"outline": "Estructura",
"settings": "Configuración",
"newModule": "Nuevo Módulo",
"newLesson": "Nueva Lección",
"addBlock": "Añadir Bloque de Contenido",
"publishToOrg": "Publicar en Organización",
"reading": "Lectura"
"content": {
"video": "Video",
"audio": "Audio",
"text": "Texto",
"image": "Imagen",
"document": "Documento",
"quiz": "Cuestionario",
"codeExercise": "Ejercicio de Código",
"memoryGame": "Juego de Memoria",
"dragToCube": "Arrastrar al Cubo",
"hotspots": "Puntos Calientes",
"videoMarkers": "Marcadores de Video",
"audioResponse": "Respuesta de Audio",
"fillBlanks": "Completar Espacios",
"matching": "Emparejamiento",
"ordering": "Ordenamiento",
"shortAnswer": "Respuesta Corta",
"multipleChoice": "Opción Múltiple",
"trueFalse": "Verdadero/Falso",
"essay": "Ensayo",
"uploadMedia": "Subir Archivo",
"dropFiles": "Soltar archivos aquí",
"browseFiles": "Examinar archivos",
"uploading": "Subiendo...",
"uploadComplete": "Subida Completa",
"uploadFailed": "Subida Fallida"
},
"ai": {
"generateWithAI": "Generar con IA",
"generateCourse": "Generar Curso con IA",
"generateQuiz": "Generar Cuestionario con IA",
"generateSummary": "Generar Resumen con IA",
"generateTranscription": "Transcribir con IA",
"generateQuestions": "Generar Preguntas con IA",
"topicPrompt": "Ingresa el tema del curso",
"generating": "Generando...",
"generationComplete": "Generación Completa",
"generationFailed": "Generación Fallida",
"aiPowered": "Potenciado por IA",
"model": "Modelo de IA",
"tokens": "Tokens",
"cost": "Costo"
},
"grading": {
"gradingSystem": "Sistema de Calificación",
"categories": "Categorías",
"weights": "Pesos",
"passingGrade": "Nota de Aprobación",
"percentage": "Porcentaje",
"dropLowest": "Eliminar Notas Más Bajas",
"maxAttempts": "Intentos Máximos",
"autoGrade": "Calificación Automática",
"manualGrade": "Calificación Manual",
"rubric": "Rúbrica",
"feedback": "Retroalimentación",
"grades": "Notas",
"exportGrades": "Exportar Notas",
"importGrades": "Importar Notas"
},
"students": {
"enrollments": "Inscripciones",
"enrollStudent": "Inscribir Estudiante",
"bulkEnroll": "Inscripción Masiva",
"studentProgress": "Progreso del Estudiante",
"studentGrades": "Notas del Estudiante",
"studentActivity": "Actividad del Estudiante",
"cohort": "Cohorte",
"groups": "Grupos",
"retention": "Retención",
"dropoutRisk": "Riesgo de Abandono",
"engagement": "Compromiso"
},
"analytics": {
"overview": "Resumen",
"enrollments": "Inscripciones",
"completions": "Completaciones",
"averageGrade": "Nota Promedio",
"engagement": "Compromiso",
"retention": "Retención",
"revenue": "Ingresos",
"aiUsage": "Uso de IA",
"exportReport": "Exportar Reporte",
"dateRange": "Rango de Fechas",
"compareCourses": "Comparar Cursos",
"studentPerformance": "Rendimiento Estudiantil"
},
"settings": {
"general": "Configuración General",
"branding": "Marca",
"logo": "Logotipo",
"favicon": "Favicon",
"colors": "Colores",
"platformName": "Nombre de la Plataforma",
"notifications": "Notificaciones",
"emailTemplates": "Plantillas de Email",
"integrations": "Integraciones",
"lti": "LTI",
"sso": "SSO",
"webhooks": "Webhooks",
"apiKeys": "Claves API",
"security": "Seguridad",
"backup": "Respaldo",
"export": "Exportar Datos",
"import": "Importar Datos"
},
"user": {
"profile": "Perfil",
"editProfile": "Editar Perfil",
"changePassword": "Cambiar Contraseña",
"preferences": "Preferencias",
"language": "Idioma",
"timezone": "Zona Horaria",
"notifications": "Notificaciones",
"avatar": "Avatar",
"bio": "Biografía",
"role": "Rol",
"admin": "Administrador",
"instructor": "Instructor",
"student": "Estudiante"
},
"errors": {
"required": "Campo requerido",
"invalidEmail": "Email inválido",
"minLength": "Longitud mínima: {{length}}",
"maxLength": "Longitud máxima: {{length}}",
"invalidFormat": "Formato inválido",
"alreadyExists": "Ya existe",
"notFound": "No encontrado",
"unauthorized": "No autorizado",
"serverError": "Error del servidor",
"networkError": "Error de red",
"uploadFailed": "Subida fallida",
"generationFailed": "Generación fallida"
},
"validation": {
"required": "Este campo es requerido",
"email": "Debe ser un email válido",
"url": "Debe ser una URL válida",
"number": "Debe ser un número",
"min": "Debe ser mayor o igual a {{min}}",
"max": "Debe ser menor o igual a {{max}}",
"pattern": "Formato inválido"
},
"dates": {
"today": "Hoy",
"yesterday": "Ayer",
"tomorrow": "Mañana",
"lastWeek": "Semana Pasada",
"lastMonth": "Mes Pasado",
"lastYear": "Año Pasado",
"custom": "Personalizado",
"startDate": "Fecha de Inicio",
"endDate": "Fecha de Fin",
"dueDate": "Fecha Límite",
"publishedDate": "Fecha de Publicación",
"createdDate": "Fecha de Creación",
"updatedDate": "Fecha de Actualización"
},
"accessibility": {
"skipToContent": "Saltar al contenido",
"increaseContrast": "Aumentar Contraste",
"screenReader": "Lector de Pantalla"
}
}
}