diff --git a/app/api/generate/scene-content/route.ts b/app/api/generate/scene-content/route.ts index db9b772e..bf30ee56 100644 --- a/app/api/generate/scene-content/route.ts +++ b/app/api/generate/scene-content/route.ts @@ -67,7 +67,7 @@ export async function POST(req: NextRequest) { // Ensure outline has language from stageInfo (fallback for older outlines) const outline: SceneOutline = { ...rawOutline, - language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US') || 'zh-CN', + language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US' | 'es-MX') || 'zh-CN', }; // ── Model resolution from request headers ── diff --git a/app/page.tsx b/app/page.tsx index 68719c48..3882e5fa 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -56,7 +56,7 @@ const RECENT_OPEN_STORAGE_KEY = 'recentClassroomsOpen'; interface FormState { pdfFile: File | null; requirement: string; - language: 'zh-CN' | 'en-US'; + language: 'zh-CN' | 'en-US' | 'es-MX'; webSearch: boolean; } @@ -99,10 +99,15 @@ function HomePage() { const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY); const updates: Partial = {}; if (savedWebSearch === 'true') updates.webSearch = true; - if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US') { + if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US' || savedLanguage === 'es-MX') { updates.language = savedLanguage; } else { - const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US'; + const browserLang = navigator.language; + const detected = browserLang?.startsWith('zh') + ? 'zh-CN' + : browserLang?.startsWith('es') + ? 'es-MX' + : 'en-US'; updates.language = detected; } if (Object.keys(updates).length > 0) { diff --git a/components/generation/generation-toolbar.tsx b/components/generation/generation-toolbar.tsx index 27301bbd..508a1c0a 100644 --- a/components/generation/generation-toolbar.tsx +++ b/components/generation/generation-toolbar.tsx @@ -28,8 +28,8 @@ const MAX_PDF_SIZE_BYTES = MAX_PDF_SIZE_MB * 1024 * 1024; // ─── Types ─────────────────────────────────────────────────── export interface GenerationToolbarProps { - language: 'zh-CN' | 'en-US'; - onLanguageChange: (lang: 'zh-CN' | 'en-US') => void; + language: 'zh-CN' | 'en-US' | 'es-MX'; + onLanguageChange: (lang: 'zh-CN' | 'en-US' | 'es-MX') => void; webSearch: boolean; onWebSearchChange: (v: boolean) => void; onSettingsOpen: (section?: SettingsSection) => void; @@ -361,11 +361,15 @@ export function GenerationToolbar({ {t('toolbar.languageHint')} diff --git a/components/header.tsx b/components/header.tsx index 5a61ec96..b13636e1 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -108,7 +108,7 @@ export function Header({ currentSceneTitle }: HeaderProps) { }} className="flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-bold text-gray-500 dark:text-gray-400 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all" > - {locale === 'zh-CN' ? 'CN' : 'EN'} + {locale === 'zh-CN' ? 'CN' : locale === 'es-MX' ? 'ES' : 'EN'} {languageOpen && (
@@ -138,6 +138,19 @@ export function Header({ currentSceneTitle }: HeaderProps) { > English +
)} diff --git a/components/settings/image-settings.tsx b/components/settings/image-settings.tsx index 172bd749..590ab191 100644 --- a/components/settings/image-settings.tsx +++ b/components/settings/image-settings.tsx @@ -187,7 +187,7 @@ export function ImageSettings({ selectedProviderId }: ImageSettingsProps) { ) : ( <> - {t('settings.testConnection')} + {t('settings.testConnection')} )} diff --git a/components/settings/model-edit-dialog.tsx b/components/settings/model-edit-dialog.tsx index aae60a09..d0aa80c3 100644 --- a/components/settings/model-edit-dialog.tsx +++ b/components/settings/model-edit-dialog.tsx @@ -314,7 +314,7 @@ export function ModelEditDialog({ {testStatus === 'testing' && } {testStatus === 'success' && } {testStatus === 'error' && } - {testStatus === 'testing' ? t('settings.testing') : t('settings.testConnection')} + {testStatus === 'testing' ? t('settings.testing') : t('settings.testConnection')} {testMessage && ( diff --git a/components/settings/pdf-settings.tsx b/components/settings/pdf-settings.tsx index bfa43bdd..350cc542 100644 --- a/components/settings/pdf-settings.tsx +++ b/components/settings/pdf-settings.tsx @@ -130,7 +130,7 @@ export function PDFSettings({ selectedProviderId }: PDFSettingsProps) { ) : ( <> - {t('settings.testConnection')} + {t('settings.testConnection')} )} diff --git a/components/settings/provider-config-panel.tsx b/components/settings/provider-config-panel.tsx index 7c765c9b..1e6744ca 100644 --- a/components/settings/provider-config-panel.tsx +++ b/components/settings/provider-config-panel.tsx @@ -203,7 +203,7 @@ export function ProviderConfigPanel({ ) : ( <> - {t('settings.testConnection')} + {t('settings.testConnection')} )} diff --git a/components/settings/tts-settings.tsx b/components/settings/tts-settings.tsx index 1e6dcadd..6348fc1f 100644 --- a/components/settings/tts-settings.tsx +++ b/components/settings/tts-settings.tsx @@ -205,7 +205,7 @@ export function TTSSettings({ selectedProviderId }: TTSSettingsProps) { ) : ( )} - {t('settings.testTTS')} + {t('settings.testTTS')} diff --git a/components/settings/video-settings.tsx b/components/settings/video-settings.tsx index ffe3506e..011c2b4a 100644 --- a/components/settings/video-settings.tsx +++ b/components/settings/video-settings.tsx @@ -190,7 +190,7 @@ export function VideoSettings({ selectedProviderId }: VideoSettingsProps) { ) : ( <> - {t('settings.testConnection')} + {t('settings.testConnection')} )} diff --git a/lib/generation/scene-generator.ts b/lib/generation/scene-generator.ts index 1dc22937..aacd678a 100644 --- a/lib/generation/scene-generator.ts +++ b/lib/generation/scene-generator.ts @@ -735,7 +735,7 @@ function normalizeQuizAnswer(question: Record): string[] | unde async function generateInteractiveContent( outline: SceneOutline, aiCall: AICallFn, - language: 'zh-CN' | 'en-US' = 'zh-CN', + language: 'zh-CN' | 'en-US' | 'es-MX' = 'zh-CN', ): Promise { const config = outline.interactiveConfig!; diff --git a/lib/hooks/use-i18n.tsx b/lib/hooks/use-i18n.tsx index 4e642f4c..87d4b5a0 100644 --- a/lib/hooks/use-i18n.tsx +++ b/lib/hooks/use-i18n.tsx @@ -10,12 +10,13 @@ type I18nContextType = { }; const LOCALE_STORAGE_KEY = 'locale'; -const VALID_LOCALES: Locale[] = ['zh-CN', 'en-US']; +const VALID_LOCALES: Locale[] = ['zh-CN', 'en-US', 'es-MX']; const I18nContext = createContext(undefined); export function I18nProvider({ children }: { children: ReactNode }) { const [locale, setLocaleState] = useState(defaultLocale); + const [hydrated, setHydrated] = useState(false); // Hydrate from localStorage after mount (avoids SSR mismatch) /* eslint-disable react-hooks/set-state-in-effect -- Hydration from localStorage must happen in effect */ @@ -24,14 +25,21 @@ export function I18nProvider({ children }: { children: ReactNode }) { const stored = localStorage.getItem(LOCALE_STORAGE_KEY); if (stored && VALID_LOCALES.includes(stored as Locale)) { setLocaleState(stored as Locale); + setHydrated(true); return; } - const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US'; + const browserLang = navigator.language; + const detected = browserLang?.startsWith('zh') + ? 'zh-CN' + : browserLang?.startsWith('es') + ? 'es-MX' + : 'en-US'; localStorage.setItem(LOCALE_STORAGE_KEY, detected); setLocaleState(detected); } catch { // localStorage unavailable, keep default } + setHydrated(true); }, []); /* eslint-enable react-hooks/set-state-in-effect */ @@ -42,6 +50,17 @@ export function I18nProvider({ children }: { children: ReactNode }) { const t = (key: string): string => translate(locale, key); + // Prevent hydration mismatch: server renders with defaultLocale (zh-CN), + // but client may detect a different locale from localStorage/browser. + // Hide content briefly until the correct locale is resolved. + if (!hydrated) { + return ( + +
{children}
+
+ ); + } + return {children}; } diff --git a/lib/i18n/chat.ts b/lib/i18n/chat.ts index 1bb535d3..0baf232f 100644 --- a/lib/i18n/chat.ts +++ b/lib/i18n/chat.ts @@ -145,3 +145,77 @@ export const chatEnUS = { stopListening: 'Stop recording', }, } as const; + +export const chatEsMX = { + chat: { + lecture: 'Clase', + noConversations: 'Sin conversaciones', + startConversation: 'Escribe un mensaje para iniciar la conversación', + noMessages: 'Aún no hay mensajes', + ended: 'finalizado', + unknown: 'Desconocido', + stopDiscussion: 'Detener Discusión', + endQA: 'Finalizar Preguntas', + tabs: { + lecture: 'Notas', + chat: 'Chat', + }, + lectureNotes: { + empty: 'Las notas aparecerán aquí después de reproducir la clase', + emptyHint: 'Presiona reproducir para iniciar la clase', + pageLabel: 'Página {n}', + currentPage: 'Actual', + }, + badge: { + qa: 'P&R', + discussion: 'DISC', + lecture: 'CLASE', + }, + }, + actions: { + names: { + spotlight: 'Foco', + laser: 'Láser', + wb_open: 'Abrir Pizarra', + wb_draw_text: 'Texto en Pizarra', + wb_draw_shape: 'Forma en Pizarra', + wb_draw_chart: 'Gráfica en Pizarra', + wb_draw_latex: 'Fórmula en Pizarra', + wb_draw_table: 'Tabla en Pizarra', + wb_draw_line: 'Línea en Pizarra', + wb_clear: 'Limpiar Pizarra', + wb_delete: 'Eliminar Elemento', + wb_close: 'Cerrar Pizarra', + discussion: 'Discusión', + }, + status: { + inputStreaming: 'Esperando', + inputAvailable: 'Ejecutando', + outputAvailable: 'Completado', + outputError: 'Error', + outputDenied: 'Rechazado', + running: 'Ejecutando', + result: 'Completado', + error: 'Error', + }, + }, + agentBar: { + readyToLearn: '¿Listos para aprender juntos?', + expandedTitle: 'Configuración de Roles del Aula', + configTooltip: 'Clic para configurar los roles del aula', + voiceLabel: 'Voz', + voiceLoading: 'Cargando...', + voiceAutoAssign: 'Las voces se asignarán automáticamente', + }, + proactiveCard: { + discussion: 'Discusión', + join: 'Unirse', + skip: 'Omitir', + pause: 'Pausar', + resume: 'Reanudar', + }, + voice: { + startListening: 'Entrada de voz', + stopListening: 'Detener grabación', + }, +} as const; diff --git a/lib/i18n/common.ts b/lib/i18n/common.ts index 1bceb5d6..3e1e12ab 100644 --- a/lib/i18n/common.ts +++ b/lib/i18n/common.ts @@ -79,3 +79,44 @@ export const commonEnUS = { exportFailed: 'Export failed', }, } as const; + +export const commonEsMX = { + common: { + you: 'Tú', + confirm: 'Confirmar', + cancel: 'Cancelar', + loading: 'Cargando...', + }, + home: { + slogan: 'Aprendizaje Generativo en Aulas Interactivas Multi-Agente', + greeting: 'Hola, ', + }, + toolbar: { + languageHint: 'El curso se generará en este idioma', + pdfParser: 'Analizador', + pdfUpload: 'Subir PDF', + removePdf: 'Eliminar archivo', + webSearchOn: 'Activado', + webSearchOff: 'Clic para activar', + webSearchDesc: 'Buscar en la web información actualizada antes de generar', + webSearchProvider: 'Motor de búsqueda', + webSearchNoProvider: 'Configura la API Key del motor de búsqueda en Ajustes', + selectProvider: 'Seleccionar proveedor', + configureProvider: 'Configurar modelo', + configureProviderHint: 'Configura al menos un proveedor de modelo para generar cursos', + enterClassroom: 'Entrar al Aula', + advancedSettings: 'Ajustes Avanzados', + ttsTitle: 'Síntesis de Voz', + ttsHint: 'Elige una voz para el profesor IA', + ttsPreview: 'Vista previa', + ttsPreviewing: 'Reproduciendo...', + }, + export: { + pptx: 'Exportar PPTX', + resourcePack: 'Exportar Paquete de Recursos', + resourcePackDesc: 'PPTX + páginas interactivas', + exporting: 'Exportando...', + exportSuccess: 'Exportación exitosa', + exportFailed: 'Error al exportar', + }, +} as const; diff --git a/lib/i18n/generation.ts b/lib/i18n/generation.ts index 98694c23..517af78f 100644 --- a/lib/i18n/generation.ts +++ b/lib/i18n/generation.ts @@ -133,3 +133,71 @@ export const generationEnUS = { webSearchFailed: 'Web search failed', }, } as const; + +export const generationEsMX = { + classroom: { + recentClassrooms: 'Recientes', + today: 'Hoy', + yesterday: 'Ayer', + daysAgo: 'días atrás', + slides: 'diapositivas', + nameCopied: 'Nombre copiado', + deleteConfirmTitle: 'Eliminar', + delete: 'Eliminar', + }, + upload: { + pdfSizeLimit: 'Soporta archivos PDF de hasta 50MB', + generateFailed: 'Error al generar el aula, intenta de nuevo', + requirementPlaceholder: + 'Dime lo que quieras aprender, por ejemplo:\n"Enséñame Python desde cero en 30 minutos"\n"Explícame la Transformada de Fourier en la pizarra"\n"¿Cómo se juega el juego de mesa Avalon?"', + requirementRequired: 'Por favor ingresa los requisitos del curso', + fileTooLarge: 'Archivo demasiado grande. Selecciona un PDF menor a 50MB', + }, + generation: { + analyzingPdf: 'Analizando Documento PDF', + analyzingPdfDesc: 'Extrayendo estructura y contenido del documento...', + pdfLoadFailed: 'Error al cargar el PDF, intenta de nuevo', + pdfParseFailed: 'Error al analizar el PDF', + streamNotReadable: 'No se puede leer el flujo de generación', + generatingOutlines: 'Creando Esquema del Curso', + generatingOutlinesDesc: 'Estructurando la ruta de aprendizaje...', + generatingSlideContent: 'Generando Contenido de Páginas', + generatingSlideContentDesc: 'Creando diapositivas, exámenes y contenido interactivo...', + generatingActions: 'Generando Acciones de Enseñanza', + generatingActionsDesc: 'Orquestando narración, focos e interacciones...', + generationComplete: '¡Generación completada!', + generationFailed: 'Error en la generación', + generatingCourse: 'Generando curso', + openingClassroom: 'Abriendo el aula...', + outlineReady: 'Esquema del curso generado', + generatingFirstPage: 'Generando primera página...', + firstPageReady: '¡Primera página lista! Abriendo el aula...', + speechFailed: 'Error en la generación de voz', + retryScene: 'Reintentar', + retryingScene: 'Regenerando...', + backToHome: 'Volver al Inicio', + sessionNotFound: 'Sesión No Encontrada', + sessionNotFoundDesc: + 'Por favor ingresa los requisitos del curso para iniciar el proceso de generación.', + goBackAndRetry: 'Regresar e Intentar de Nuevo', + classroomReady: + 'Tu entorno de aprendizaje personalizado con IA se ha generado exitosamente.', + aiWorking: 'Agentes IA Trabajando...', + textTruncated: + 'El texto del documento es extenso, usando los primeros {n} caracteres para la generación', + imageTruncated: + 'Se encontraron {total} imágenes, excediendo el límite de {max}. Las imágenes extra usarán solo descripciones de texto', + agentGeneration: 'Generando Roles del Aula', + agentGenerationDesc: 'Generando roles basados en el contenido del curso...', + agentRevealTitle: 'Tus Roles del Aula', + viewAgents: 'Ver Roles', + continue: 'Continuar', + outlineRetrying: 'Problema en la generación del esquema, reintentando...', + outlineEmptyResponse: + 'El modelo no devolvió esquemas válidos. Revisa la configuración del modelo e intenta de nuevo', + outlineGenerateFailed: 'Error al generar el esquema, intenta más tarde', + webSearching: 'Búsqueda Web', + webSearchingDesc: 'Buscando en la web información actualizada', + webSearchFailed: 'Error en la búsqueda web', + }, +} as const; diff --git a/lib/i18n/index.ts b/lib/i18n/index.ts index 5fd70da5..f985433e 100644 --- a/lib/i18n/index.ts +++ b/lib/i18n/index.ts @@ -1,10 +1,10 @@ import { defaultLocale, type Locale } from './types'; export { type Locale, defaultLocale } from './types'; -import { commonZhCN, commonEnUS } from './common'; -import { stageZhCN, stageEnUS } from './stage'; -import { chatZhCN, chatEnUS } from './chat'; -import { generationZhCN, generationEnUS } from './generation'; -import { settingsZhCN, settingsEnUS } from './settings'; +import { commonZhCN, commonEnUS, commonEsMX } from './common'; +import { stageZhCN, stageEnUS, stageEsMX } from './stage'; +import { chatZhCN, chatEnUS, chatEsMX } from './chat'; +import { generationZhCN, generationEnUS, generationEsMX } from './generation'; +import { settingsZhCN, settingsEnUS, settingsEsMX } from './settings'; export const translations = { 'zh-CN': { @@ -21,6 +21,13 @@ export const translations = { ...generationEnUS, ...settingsEnUS, }, + 'es-MX': { + ...commonEsMX, + ...stageEsMX, + ...chatEsMX, + ...generationEsMX, + ...settingsEsMX, + }, } as const; export type TranslationKey = keyof (typeof translations)[typeof defaultLocale]; @@ -40,7 +47,7 @@ export function getClientTranslation(key: string): string { if (typeof window !== 'undefined') { try { const storedLocale = localStorage.getItem('locale'); - if (storedLocale === 'zh-CN' || storedLocale === 'en-US') { + if (storedLocale === 'zh-CN' || storedLocale === 'en-US' || storedLocale === 'es-MX') { locale = storedLocale; } } catch { diff --git a/lib/i18n/settings.ts b/lib/i18n/settings.ts index 3ba0be4f..0b397497 100644 --- a/lib/i18n/settings.ts +++ b/lib/i18n/settings.ts @@ -1178,3 +1178,599 @@ export const settingsEnUS = { language: 'Language', }, } as const; + +export const settingsEsMX = { + settings: { + title: 'Ajustes', + description: 'Configurar ajustes de la aplicación', + language: 'Idioma', + languageDesc: 'Seleccionar idioma de la interfaz', + theme: 'Tema', + themeDesc: 'Seleccionar modo de tema (Claro/Oscuro/Sistema)', + themeOptions: { + light: 'Claro', + dark: 'Oscuro', + system: 'Sistema', + }, + apiKey: 'Clave API', + apiKeyDesc: 'Configura tu clave API', + apiBaseUrl: 'URL del Endpoint API', + apiBaseUrlDesc: 'Configura la URL de tu endpoint API', + apiKeyRequired: 'La clave API no puede estar vacía', + model: 'Configuración del Modelo', + modelDesc: 'Configurar modelos de IA', + modelPlaceholder: 'Ingresa o selecciona el nombre del modelo', + ttsModel: 'Modelo TTS', + ttsModelDesc: 'Configurar modelos TTS', + ttsModelPlaceholder: 'Ingresa o selecciona el nombre del modelo TTS', + ttsModelOptions: { + openaiTts: 'OpenAI TTS', + azureTts: 'Azure TTS', + }, + testConnection: 'Probar Conexión', + testConnectionDesc: 'Probar que la configuración API actual esté disponible', + testing: 'Probando...', + agentSettings: 'Configuración de Agentes', + agentSettingsDesc: + 'Selecciona los agentes que participarán en la conversación. Selecciona 1 para modo de agente único, selecciona varios para modo colaborativo multi-agente.', + agentMode: 'Modo de Agente', + agentModePreset: 'Preestablecido', + agentModeAuto: 'Auto-generar', + agentModeAutoDesc: 'La IA generará automáticamente roles apropiados', + autoAgentCount: 'Cantidad de Agentes', + autoAgentCountDesc: 'Número de agentes a auto-generar (incluyendo profesor)', + atLeastOneAgent: 'Por favor selecciona al menos 1 agente', + singleAgentMode: 'Modo Agente Único', + directAnswer: 'Respuesta Directa', + multiAgentMode: 'Modo Multi-Agente', + agentsCollaborating: 'Discusión Colaborativa', + agentsCollaboratingCount: '{count} agentes seleccionados para discusión colaborativa', + maxTurns: 'Turnos Máximos de Discusión', + maxTurnsDesc: + 'El número máximo de turnos de discusión entre agentes (cada agente completa acciones y responde cuenta como un turno)', + priority: 'Prioridad', + actions: 'Acciones', + actionCount: '{count} acciones', + selectedAgent: 'Agente Seleccionado', + selectedAgents: 'Agentes Seleccionados', + required: 'Requerido', + agentNames: { + 'default-1': 'Profesor IA', + 'default-2': 'Asistente IA', + 'default-3': 'El Bromista', + 'default-4': 'Mente Curiosa', + 'default-5': 'Tomador de Notas', + 'default-6': 'Pensador Profundo', + }, + agentRoles: { + teacher: 'Profesor', + assistant: 'Asistente', + student: 'Estudiante', + }, + agentDescriptions: { + 'default-1': 'Profesor principal con explicaciones claras y estructuradas', + 'default-2': 'Apoya el aprendizaje y ayuda a aclarar puntos clave', + 'default-3': 'Aporta humor y energía al aula', + 'default-4': 'Siempre curioso, le encanta preguntar por qué y cómo', + 'default-5': 'Registra y organiza diligentemente las notas de clase', + 'default-6': 'Piensa profundamente y explora la esencia de los temas', + }, + close: 'Cerrar', + save: 'Guardar', + // Provider settings + providers: 'LLM', + addProviderDescription: 'Agrega proveedores de modelos personalizados para ampliar los modelos de IA disponibles', + providerNames: { + openai: 'OpenAI', + anthropic: 'Claude', + google: 'Gemini', + deepseek: 'DeepSeek', + qwen: 'Qwen', + kimi: 'Kimi', + minimax: 'MiniMax', + glm: 'GLM', + siliconflow: 'SiliconFlow', + }, + providerTypes: { + openai: 'Protocolo OpenAI', + anthropic: 'Protocolo Claude', + google: 'Protocolo Gemini', + }, + modelCount: 'modelos', + modelSingular: 'modelo', + defaultModel: 'Modelo Predeterminado', + webSearch: 'Búsqueda Web', + mcp: 'MCP', + knowledgeBase: 'Base de Conocimiento', + documentParser: 'Analizador de Documentos', + conversationSettings: 'Conversación', + keyboardShortcuts: 'Atajos', + generalSettings: 'General', + systemSettings: 'Sistema', + addProvider: 'Agregar', + importFromClipboard: 'Importar desde Portapapeles', + apiSecret: 'Clave API', + apiHost: 'URL Base', + requestUrl: 'URL de Solicitud', + models: 'Modelos', + addModel: 'Nuevo', + reset: 'Restablecer', + fetch: 'Obtener', + connectionSuccess: 'Conexión exitosa', + connectionFailed: 'Error de conexión', + // Model capabilities + capabilities: { + vision: 'Visión', + tools: 'Herramientas', + streaming: 'Streaming', + }, + contextWindow: 'Contexto', + contextShort: 'ctx', + outputWindow: 'Salida', + // Provider management + addProviderButton: 'Agregar', + addProviderDialog: 'Agregar Proveedor de Modelos', + providerName: 'Nombre', + providerNamePlaceholder: 'ej., Mi Proxy OpenAI', + providerNameRequired: 'Por favor ingresa el nombre del proveedor', + providerApiMode: 'Modo API', + apiModeOpenAI: 'Protocolo OpenAI', + apiModeAnthropic: 'Protocolo Claude', + apiModeGoogle: 'Protocolo Gemini', + defaultBaseUrl: 'URL Base Predeterminada', + providerIcon: 'URL del Ícono del Proveedor', + requiresApiKey: 'Requiere Clave API', + deleteProvider: 'Eliminar Proveedor', + deleteProviderConfirm: '¿Estás seguro de que deseas eliminar este proveedor?', + cannotDeleteBuiltIn: 'No se puede eliminar un proveedor integrado', + resetToDefault: 'Restablecer a Predeterminado', + resetToDefaultDescription: + 'Restaurar la lista de modelos a la configuración predeterminada (se conservarán la clave API y URL Base)', + resetConfirmDescription: + 'Esto eliminará todos los modelos personalizados y restaurará la lista de modelos integrados. La clave API y URL Base se conservarán.', + confirmReset: 'Confirmar Restablecimiento', + resetSuccess: 'Restablecido exitosamente a la configuración predeterminada', + saveSuccess: 'Ajustes guardados', + saveFailed: 'Error al guardar ajustes, intenta de nuevo', + cannotDeleteBuiltInModel: 'No se puede eliminar un modelo integrado', + cannotEditBuiltInModel: 'No se puede editar un modelo integrado', + modelIdRequired: 'Por favor ingresa el ID del modelo', + noModelsAvailable: 'No hay modelos disponibles para probar', + providerMetadata: 'Metadatos del Proveedor', + // Model editing + editModel: 'Editar Modelo', + editModelDescription: 'Editar configuración y capacidades del modelo', + addNewModel: 'Nuevo Modelo', + addNewModelDescription: 'Agregar una nueva configuración de modelo', + modelId: 'ID del Modelo', + modelIdPlaceholder: 'ej., gpt-4o', + modelName: 'Nombre para Mostrar', + modelNamePlaceholder: 'Opcional', + modelCapabilities: 'Capacidades', + advancedSettings: 'Ajustes Avanzados', + contextWindowLabel: 'Ventana de Contexto', + contextWindowPlaceholder: 'ej., 128000', + outputWindowLabel: 'Tokens Máximos de Salida', + outputWindowPlaceholder: 'ej., 4096', + testModel: 'Probar Modelo', + deleteModel: 'Eliminar', + cancelEdit: 'Cancelar', + saveModel: 'Guardar', + modelsManagementDescription: + 'Administra los modelos de este proveedor. Para seleccionar el modelo activo, ve a "General".', + // General settings + howToUse: 'Cómo Usar', + step1ConfigureProvider: + 'Ve a "Proveedores de Modelos", selecciona o agrega un proveedor y configura los ajustes de conexión (clave API, URL Base, etc.)', + step2SelectModel: 'Selecciona el modelo que deseas usar en "Modelo Activo" abajo', + step3StartUsing: 'Después de guardar, el sistema usará el modelo seleccionado', + activeModel: 'Modelo Activo', + activeModelDescription: 'Selecciona el modelo para conversaciones de IA y generación de contenido', + selectModel: 'Seleccionar Modelo', + searchModels: 'Buscar modelos', + noModelsFound: 'No se encontraron modelos coincidentes', + noConfiguredProviders: 'Sin proveedores configurados', + configureProvidersFirst: + 'Por favor configura los ajustes de conexión del proveedor en "Proveedores de Modelos" a la izquierda', + currentlyUsing: 'Usando actualmente', + // TTS settings + ttsSettings: 'Síntesis de Voz', + // ASR settings + asrSettings: 'Reconocimiento de Voz', + // Audio settings (legacy) + audioSettings: 'Ajustes de Audio', + ttsSection: 'Texto a Voz (TTS)', + asrSection: 'Reconocimiento Automático de Voz (ASR)', + ttsDescription: 'TTS (Texto a Voz) - Convierte texto en voz', + asrDescription: 'ASR (Reconocimiento Automático de Voz) - Convierte voz en texto', + enableTTS: 'Habilitar Síntesis de Voz', + ttsEnabledDescription: 'Cuando está habilitado, se generará audio de voz durante la creación del curso', + ttsVoiceConfigHint: + 'La voz de cada agente se puede configurar en "Configuración de Roles del Aula" en la página principal', + enableASR: 'Habilitar Reconocimiento de Voz', + asrEnabledDescription: 'Cuando está habilitado, los estudiantes pueden usar el micrófono para entrada de voz', + ttsProvider: 'Proveedor TTS', + ttsLanguageFilter: 'Filtro de Idioma', + allLanguages: 'Todos los Idiomas', + ttsVoice: 'Voz', + ttsSpeed: 'Velocidad', + ttsBaseUrl: 'URL Base', + ttsApiKey: 'Clave API', + asrProvider: 'Proveedor ASR', + asrLanguage: 'Idioma de Reconocimiento', + asrBaseUrl: 'URL Base', + asrApiKey: 'Clave API', + enterApiKey: 'Ingresa la Clave API', + enterCustomBaseUrl: 'Ingresa URL Base personalizada', + browserNativeNote: 'ASR nativo del navegador no requiere configuración y es completamente gratuito', + // Audio provider names + providerOpenAITTS: 'OpenAI TTS (gpt-4o-mini-tts)', + providerAzureTTS: 'Azure TTS', + providerGLMTTS: 'GLM TTS', + providerQwenTTS: 'Qwen TTS (Alibaba Cloud Bailian)', + providerElevenLabsTTS: 'ElevenLabs TTS', + providerBrowserNativeTTS: 'TTS Nativo del Navegador', + providerOpenAIWhisper: 'OpenAI ASR (gpt-4o-mini-transcribe)', + providerBrowserNative: 'ASR Nativo del Navegador', + providerQwenASR: 'Qwen ASR (Alibaba Cloud Bailian)', + providerUnpdf: 'unpdf (Integrado)', + providerMinerU: 'MinerU', + browserNativeTTSNote: + 'TTS nativo del navegador no requiere configuración y es completamente gratuito, usa las voces integradas del sistema', + testTTS: 'Probar TTS', + testASR: 'Probar ASR', + testSuccess: 'Prueba Exitosa', + testFailed: 'Prueba Fallida', + ttsTestText: 'Texto de Prueba TTS', + ttsTestSuccess: 'Prueba TTS exitosa, audio reproducido', + ttsTestFailed: 'Prueba TTS fallida', + asrTestSuccess: 'Reconocimiento de voz exitoso', + asrTestFailed: 'Reconocimiento de voz fallido', + asrResult: 'Resultado del Reconocimiento', + asrNotSupported: 'El navegador no soporta la API de Reconocimiento de Voz', + browserTTSNotSupported: 'El navegador no soporta la API de Síntesis de Voz', + browserTTSNoVoices: 'El navegador actual no tiene voces TTS disponibles', + microphoneAccessDenied: 'Acceso al micrófono denegado', + microphoneAccessFailed: 'Error al acceder al micrófono', + asrResultPlaceholder: 'El resultado del reconocimiento se mostrará después de grabar', + useThisProvider: 'Usar Este Proveedor', + fetchVoices: 'Obtener Lista de Voces', + fetchingVoices: 'Obteniendo...', + voicesFetched: 'Voces obtenidas', + fetchVoicesFailed: 'Error al obtener voces', + voiceApiKeyRequired: 'Se requiere Clave API', + voiceBaseUrlRequired: 'Se requiere URL Base', + ttsTestTextPlaceholder: 'Ingresa texto para convertir', + ttsTestTextDefault: 'Hola, esta es una prueba de voz.', + startRecording: 'Iniciar Grabación', + stopRecording: 'Detener Grabación', + recording: 'Grabando...', + transcribing: 'Transcribiendo...', + transcriptionResult: 'Resultado de Transcripción', + noTranscriptionResult: 'Sin resultado de transcripción', + baseUrlOptional: 'URL Base (Opcional)', + defaultValue: 'Predeterminado', + // TTS Voice descriptions (OpenAI) + voiceMarin: 'Recomendado - Mejor Calidad', + voiceCedar: 'Recomendado - Mejor Calidad', + voiceAlloy: 'Neutral, Equilibrado', + voiceAsh: 'Estable, Profesional', + voiceBallad: 'Elegante, Lírico', + voiceCoral: 'Cálido, Amigable', + voiceEcho: 'Masculino, Claro', + voiceFable: 'Narrativo, Vívido', + voiceNova: 'Femenino, Brillante', + voiceOnyx: 'Masculino, Profundo', + voiceSage: 'Sabio, Sereno', + voiceShimmer: 'Femenino, Suave', + voiceVerse: 'Natural, Fluido', + // TTS Voice descriptions (GLM) + glmVoiceTongtong: 'Voz predeterminada', + glmVoiceChuichui: 'Voz Chuichui', + glmVoiceXiaochen: 'Voz Xiaochen', + glmVoiceJam: 'Voz Jam', + glmVoiceKazi: 'Voz Kazi', + glmVoiceDouji: 'Voz Douji', + glmVoiceLuodo: 'Voz Luodo', + // TTS Voice descriptions (Qwen) + qwenVoiceCherry: 'Soleada, cálida y natural', + qwenVoiceSerena: 'Suave y delicada', + qwenVoiceEthan: 'Enérgico y vibrante', + qwenVoiceChelsie: 'Novia virtual anime', + qwenVoiceMomo: 'Juguetona y alegre', + qwenVoiceVivian: 'Linda y atrevida', + qwenVoiceMoon: 'Cool y apuesto', + qwenVoiceMaia: 'Intelectual y gentil', + qwenVoiceKai: 'Un SPA para tus oídos', + qwenVoiceNofish: 'Diseñador con acento peculiar', + qwenVoiceBella: 'Pequeña loli que no se emborracha', + qwenVoiceJennifer: 'Voz femenina americana de nivel cinematográfico', + qwenVoiceRyan: 'Ritmo rápido, actuación dramática', + qwenVoiceKaterina: 'Dama madura con ritmo memorable', + qwenVoiceAiden: 'Chico americano que domina la cocina', + qwenVoiceEldricSage: 'Anciano estable y sabio', + qwenVoiceMia: 'Gentil como agua de manantial', + qwenVoiceMochi: 'Pequeño adulto inteligente con inocencia infantil', + qwenVoiceBellona: 'Voz fuerte, pronunciación clara, personajes vívidos', + qwenVoiceVincent: 'Voz ronca única narrando historias de guerra y honor', + qwenVoiceBunny: 'Súper linda loli', + qwenVoiceNeil: 'Presentador de noticias profesional', + qwenVoiceElias: 'Instructor profesional', + qwenVoiceArthur: 'Voz simple curtida por los años', + qwenVoiceNini: 'Voz suave y pegajosa como pastel de arroz', + qwenVoiceEbona: 'Su susurro es como una llave oxidada', + qwenVoiceSeren: 'Voz gentil y relajante para ayudarte a dormir', + qwenVoicePip: 'Travieso pero lleno de inocencia infantil', + qwenVoiceStella: 'Voz de chica dulce y confundida que se vuelve justa al gritar', + qwenVoiceBodega: 'Tío español entusiasta', + qwenVoiceSonrisa: 'Señora latinoamericana entusiasta', + qwenVoiceAlek: 'Frío de nación guerrera, cálido bajo el abrigo de lana', + qwenVoiceDolce: 'Tío italiano relajado', + qwenVoiceSohee: 'Unnie coreana gentil y alegre', + qwenVoiceOnoAnna: 'Amiga de la infancia traviesa', + qwenVoiceLenn: 'Joven alemán racional que usa traje y escucha post-punk', + qwenVoiceEmilien: 'Hermano mayor francés romántico', + qwenVoiceAndre: 'Voz masculina magnética, natural y calmada', + qwenVoiceRadioGol: '¡Poeta del fútbol Rádio Gol!', + qwenVoiceJada: 'Señora de Shanghai vivaz', + qwenVoiceDylan: 'Chico de Beijing', + qwenVoiceLi: 'Profesora de yoga paciente', + qwenVoiceMarcus: 'Cara ancha, pocas palabras, corazón firme - sabor del viejo Shaanxi', + qwenVoiceRoy: 'Chico taiwanés humorístico y directo', + qwenVoicePeter: 'Profesional del stand-up de Tianjin', + qwenVoiceSunny: 'Chica dulce de Sichuan', + qwenVoiceEric: 'Caballero de Chengdu', + qwenVoiceRocky: 'Chico de Hong Kong humorístico', + qwenVoiceKiki: 'Chica dulce de Hong Kong', + // ASR Language names (native forms - autoglossonyms) + lang_auto: 'Detección Automática', + lang_zh: '中文', + lang_yue: '粤語', + lang_en: 'English', + lang_ja: '日本語', + lang_ko: '한국어', + lang_es: 'Español', + lang_fr: 'Français', + lang_de: 'Deutsch', + lang_ru: 'Русский', + lang_ar: 'العربية', + lang_pt: 'Português', + lang_it: 'Italiano', + lang_af: 'Afrikaans', + lang_hy: 'Հայերեն', + lang_az: 'Azərbaycan', + lang_be: 'Беларуская', + lang_bs: 'Bosanski', + lang_bg: 'Български', + lang_ca: 'Català', + lang_hr: 'Hrvatski', + lang_cs: 'Čeština', + lang_da: 'Dansk', + lang_nl: 'Nederlands', + lang_et: 'Eesti', + lang_fi: 'Suomi', + lang_gl: 'Galego', + lang_el: 'Ελληνικά', + lang_he: 'עברית', + lang_hi: 'हिन्दी', + lang_hu: 'Magyar', + lang_is: 'Íslenska', + lang_id: 'Bahasa Indonesia', + lang_kn: 'ಕನ್ನಡ', + lang_kk: 'Қазақша', + lang_lv: 'Latviešu', + lang_lt: 'Lietuvių', + lang_mk: 'Македонски', + lang_ms: 'Bahasa Melayu', + lang_mr: 'मराठी', + lang_mi: 'Te Reo Māori', + lang_ne: 'नेपाली', + lang_no: 'Norsk', + lang_fa: 'فارسی', + lang_pl: 'Polski', + lang_ro: 'Română', + lang_sr: 'Српски', + lang_sk: 'Slovenčina', + lang_sl: 'Slovenščina', + lang_sw: 'Kiswahili', + lang_sv: 'Svenska', + lang_tl: 'Tagalog', + lang_fil: 'Filipino', + lang_ta: 'தமிழ்', + lang_th: 'ไทย', + lang_tr: 'Türkçe', + lang_uk: 'Українська', + lang_ur: 'اردو', + lang_vi: 'Tiếng Việt', + lang_cy: 'Cymraeg', + // BCP-47 format language codes (for Web Speech API) + 'lang_zh-CN': '中文(简体,中国)', + 'lang_zh-TW': '中文(繁體,台灣)', + 'lang_zh-HK': '粵語(香港)', + 'lang_yue-Hant-HK': '粵語(繁體)', + 'lang_en-US': 'English (United States)', + 'lang_en-GB': 'English (United Kingdom)', + 'lang_en-AU': 'English (Australia)', + 'lang_en-CA': 'English (Canada)', + 'lang_en-IN': 'English (India)', + 'lang_en-NZ': 'English (New Zealand)', + 'lang_en-ZA': 'English (South Africa)', + 'lang_ja-JP': '日本語(日本)', + 'lang_ko-KR': '한국어(대한민국)', + 'lang_de-DE': 'Deutsch (Deutschland)', + 'lang_fr-FR': 'Français (France)', + 'lang_es-ES': 'Español (España)', + 'lang_es-MX': 'Español (México)', + 'lang_es-AR': 'Español (Argentina)', + 'lang_es-CO': 'Español (Colombia)', + 'lang_it-IT': 'Italiano (Italia)', + 'lang_pt-BR': 'Português (Brasil)', + 'lang_pt-PT': 'Português (Portugal)', + 'lang_ru-RU': 'Русский (Россия)', + 'lang_nl-NL': 'Nederlands (Nederland)', + 'lang_pl-PL': 'Polski (Polska)', + 'lang_cs-CZ': 'Čeština (Česko)', + 'lang_da-DK': 'Dansk (Danmark)', + 'lang_fi-FI': 'Suomi (Suomi)', + 'lang_sv-SE': 'Svenska (Sverige)', + 'lang_no-NO': 'Norsk (Norge)', + 'lang_tr-TR': 'Türkçe (Türkiye)', + 'lang_el-GR': 'Ελληνικά (Ελλάδα)', + 'lang_hu-HU': 'Magyar (Magyarország)', + 'lang_ro-RO': 'Română (România)', + 'lang_sk-SK': 'Slovenčina (Slovensko)', + 'lang_bg-BG': 'Български (България)', + 'lang_hr-HR': 'Hrvatski (Hrvatska)', + 'lang_ca-ES': 'Català (Espanya)', + 'lang_ar-SA': 'العربية (السعودية)', + 'lang_ar-EG': 'العربية (مصر)', + 'lang_he-IL': 'עברית (ישראל)', + 'lang_hi-IN': 'हिन्दी (भारत)', + 'lang_th-TH': 'ไทย (ประเทศไทย)', + 'lang_vi-VN': 'Tiếng Việt (Việt Nam)', + 'lang_id-ID': 'Bahasa Indonesia (Indonesia)', + 'lang_ms-MY': 'Bahasa Melayu (Malaysia)', + 'lang_fil-PH': 'Filipino (Pilipinas)', + 'lang_af-ZA': 'Afrikaans (Suid-Afrika)', + 'lang_uk-UA': 'Українська (Україна)', + // PDF settings + pdfSettings: 'Análisis de PDF', + pdfParsingSettings: 'Ajustes de Análisis de PDF', + pdfDescription: + 'Elige el motor de análisis de PDF con soporte para extracción de texto, procesamiento de imágenes y reconocimiento de tablas', + pdfProvider: 'Analizador de PDF', + pdfFeatures: 'Funciones Soportadas', + pdfApiKey: 'Clave API', + pdfBaseUrl: 'URL Base', + mineruDescription: + 'MinerU es un servicio comercial de análisis de PDF que soporta funciones avanzadas como extracción de tablas, reconocimiento de fórmulas y análisis de diseño.', + mineruApiKeyRequired: 'Necesitas solicitar una Clave API en el sitio web de MinerU antes de usar.', + mineruWarning: 'Advertencia', + mineruCostWarning: + 'MinerU es un servicio comercial y puede generar cargos. Consulta el sitio web de MinerU para detalles de precios.', + enterMinerUApiKey: 'Ingresa la Clave API de MinerU', + mineruLocalDescription: + 'MinerU soporta despliegue local con análisis avanzado de PDF (tablas, fórmulas, análisis de diseño). Requiere desplegar el servicio MinerU primero.', + mineruServerAddress: 'Dirección del servidor local MinerU (ej., http://localhost:8080)', + mineruApiKeyOptional: 'Solo requerido si el servidor tiene autenticación habilitada', + optionalApiKey: 'Clave API Opcional', + featureText: 'Extracción de Texto', + featureImages: 'Extracción de Imágenes', + featureTables: 'Extracción de Tablas', + featureFormulas: 'Reconocimiento de Fórmulas', + featureLayoutAnalysis: 'Análisis de Diseño', + featureMetadata: 'Metadatos', + // Image Generation settings + enableImageGeneration: 'Habilitar Generación de Imágenes IA', + imageGenerationDisabledHint: + 'Cuando está habilitado, las imágenes se generarán automáticamente durante la creación del curso', + imageSettings: 'Generación de Imágenes', + imageSection: 'Texto a Imagen', + imageProvider: 'Proveedor de Generación de Imágenes', + imageModel: 'Modelo de Generación de Imágenes', + providerSeedream: 'Seedream (ByteDance)', + providerQwenImage: 'Qwen Image (Alibaba)', + providerNanoBanana: 'Nano Banana (Gemini)', + providerGrokImage: 'Grok Image (xAI)', + testImageGeneration: 'Probar Generación de Imágenes', + testImageConnectivity: 'Probar Conexión', + imageConnectivitySuccess: 'Servicio de imágenes conectado exitosamente', + imageConnectivityFailed: 'Error de conexión del servicio de imágenes', + imageTestSuccess: 'Prueba de generación de imágenes exitosa', + imageTestFailed: 'Prueba de generación de imágenes fallida', + imageTestPromptPlaceholder: 'Ingresa descripción de imagen para probar', + imageTestPromptDefault: 'Un lindo gato sentado en un escritorio', + imageGenerating: 'Generando imagen...', + imageGenerationFailed: 'Error en la generación de imagen', + // Video Generation settings + enableVideoGeneration: 'Habilitar Generación de Video IA', + videoGenerationDisabledHint: + 'Cuando está habilitado, los videos se generarán automáticamente durante la creación del curso', + videoSettings: 'Generación de Video', + videoSection: 'Texto a Video', + videoProvider: 'Proveedor de Generación de Video', + videoModel: 'Modelo de Generación de Video', + providerSeedance: 'Seedance (ByteDance)', + providerKling: 'Kling (Kuaishou)', + providerVeo: 'Veo (Google)', + providerSora: 'Sora (OpenAI)', + providerGrokVideo: 'Grok Video (xAI)', + testVideoGeneration: 'Probar Generación de Video', + testVideoConnectivity: 'Probar Conexión', + videoConnectivitySuccess: 'Servicio de video conectado exitosamente', + videoConnectivityFailed: 'Error de conexión del servicio de video', + testingConnection: 'Probando...', + videoTestSuccess: 'Prueba de generación de video exitosa', + videoTestFailed: 'Prueba de generación de video fallida', + videoTestPromptDefault: 'Un lindo gato caminando sobre un escritorio', + videoGenerating: 'Generando video (est. 1-2 min)...', + videoGenerationWarning: 'La generación de video generalmente toma 1-2 minutos, por favor sé paciente', + mediaRetry: 'Reintentar', + mediaContentSensitive: 'Lo sentimos, este contenido activó una verificación de seguridad.', + mediaGenerationDisabled: 'Generación deshabilitada en ajustes', + // Agent settings (kept with main settings block above) + singleAgent: 'Agente Único', + multiAgent: 'Multi-Agente', + selectAgents: 'Seleccionar Agentes', + noVisionWarning: + 'El modelo actual no soporta visión. Las imágenes aún se pueden colocar en diapositivas, pero el modelo no puede entender el contenido de las imágenes para optimizar la selección y el diseño', + // Server provider configuration + serverConfigured: 'Servidor', + serverConfiguredNotice: + 'El administrador ha configurado una clave API para este proveedor en el servidor. Puedes usarla directamente o ingresar tu propia clave para sobrescribir.', + optionalOverride: 'Opcional — dejar vacío para usar la configuración del servidor', + // Access code + setupNeeded: 'Configuración requerida', + modelNotConfigured: 'Por favor selecciona un modelo para comenzar', + // Clear cache + dangerZone: 'Zona de Peligro', + clearCache: 'Limpiar Caché Local', + clearCacheDescription: + 'Eliminar todos los datos almacenados localmente, incluyendo registros de aulas, historial de chat, caché de audio y ajustes de la aplicación. Esta acción no se puede deshacer.', + clearCacheConfirmTitle: '¿Estás seguro de que deseas limpiar todo el caché?', + clearCacheConfirmDescription: + 'Esto eliminará permanentemente todos los siguientes datos y no se pueden recuperar:', + clearCacheConfirmItems: + 'Aulas y escenas, Historial de chat, Caché de audio e imágenes, Ajustes y preferencias de la aplicación', + clearCacheConfirmInput: 'Escribe "ELIMINAR" para continuar', + clearCacheConfirmPhrase: 'ELIMINAR', + clearCacheButton: 'Eliminar Permanentemente Todos los Datos', + clearCacheSuccess: 'Caché limpiado, la página se actualizará en breve', + clearCacheFailed: 'Error al limpiar el caché, intenta de nuevo', + // Web Search settings + webSearchSettings: 'Búsqueda Web', + webSearchApiKey: 'Clave API de Tavily', + webSearchApiKeyPlaceholder: 'Ingresa tu Clave API de Tavily', + webSearchApiKeyPlaceholderServer: 'Clave del servidor configurada, opcionalmente sobrescribir', + webSearchApiKeyHint: 'Obtén una clave API de tavily.com para búsqueda web', + webSearchBaseUrl: 'URL Base', + webSearchServerConfigured: 'Clave API de Tavily del lado del servidor configurada', + optional: 'Opcional', + }, + profile: { + title: 'Perfil', + defaultNickname: 'Estudiante', + chooseAvatar: 'Elegir Avatar', + uploadAvatar: 'Subir', + bioPlaceholder: 'Cuéntanos sobre ti — el profesor IA personalizará las clases para ti...', + avatarHint: 'Tu avatar aparecerá en las discusiones y chats del aula', + fileTooLarge: 'Imagen demasiado grande — por favor elige una menor a 5 MB', + invalidFileType: 'Por favor selecciona un archivo de imagen', + editTooltip: 'Clic para editar perfil', + }, + media: { + imageCapability: 'Generación de Imágenes', + imageHint: 'Generar imágenes en diapositivas', + videoCapability: 'Generación de Video', + videoHint: 'Generar videos en diapositivas', + ttsCapability: 'Síntesis de Voz', + ttsHint: 'El profesor IA habla en voz alta', + asrCapability: 'Reconocimiento de Voz', + asrHint: 'Entrada de voz para discusión', + provider: 'Proveedor', + model: 'Modelo', + voice: 'Voz', + speed: 'Velocidad', + language: 'Idioma', + }, +} as const; diff --git a/lib/i18n/stage.ts b/lib/i18n/stage.ts index 0a376ca1..407b2703 100644 --- a/lib/i18n/stage.ts +++ b/lib/i18n/stage.ts @@ -296,3 +296,156 @@ export const stageEnUS = { notReady: 'Available after generation completes', }, } as const; + +export const stageEsMX = { + stage: { + currentScene: 'Escena Actual', + generating: 'Generando...', + paused: 'Pausado', + generationFailed: 'Error en la generación', + confirmSwitchTitle: 'Cambiar Escena', + confirmSwitchMessage: + 'Hay un tema en curso. Cambiar de escena finalizará el tema actual. ¿Estás seguro?', + generatingNextPage: 'La escena se está generando, por favor espera...', + fullscreen: 'Pantalla completa', + exitFullscreen: 'Salir de pantalla completa', + }, + whiteboard: { + title: 'Pizarra Interactiva', + open: 'Abrir Pizarra', + clear: 'Limpiar Pizarra', + minimize: 'Minimizar Pizarra', + ready: 'Pizarra lista', + readyHint: 'Los elementos aparecerán aquí cuando la IA los agregue', + clearSuccess: 'Pizarra limpiada exitosamente', + clearError: 'Error al limpiar la pizarra: ', + resetView: 'Restablecer Vista', + restoreError: 'Error al restaurar la pizarra: ', + history: 'Historial', + restore: 'Restaurar', + noHistory: 'Sin historial aún', + restored: 'Pizarra restaurada', + elementCount: '{count} elementos', + }, + quiz: { + title: 'Examen Rápido', + subtitle: 'Pon a prueba tus conocimientos', + questionsCount: 'preguntas', + totalPrefix: '', + pointsSuffix: 'pts', + startQuiz: 'Iniciar Examen', + multipleChoiceHint: '(Opción múltiple — selecciona todas las respuestas correctas)', + inputPlaceholder: 'Escribe tu respuesta aquí...', + charCount: 'caracteres', + yourAnswer: 'Tu respuesta:', + notAnswered: 'Sin responder', + aiComment: 'Retroalimentación IA', + singleChoice: 'Opción única', + multipleChoice: 'Opción múltiple', + shortAnswer: 'Respuesta corta', + analysis: 'Análisis: ', + excellent: '¡Excelente!', + keepGoing: '¡Sigue así!', + needsReview: 'Necesita repaso', + correct: 'correcto', + incorrect: 'incorrecto', + answering: 'En Progreso', + submitAnswers: 'Enviar Respuestas', + aiGrading: 'La IA está calificando...', + aiGradingWait: 'Por favor espera, analizando tus respuestas', + quizReport: 'Reporte del Examen', + retry: 'Reintentar', + }, + roundtable: { + teacher: 'PROFESOR', + you: 'TÚ', + inputPlaceholder: 'Escribe tu mensaje...', + listening: 'Escuchando...', + processing: 'Procesando...', + noSpeechDetected: 'No se detectó voz, intenta de nuevo', + discussionEnded: 'Discusión finalizada', + qaEnded: 'Preguntas finalizadas', + thinking: 'Pensando', + yourTurn: 'Tu turno', + stopDiscussion: 'Detener Discusión', + autoPlay: 'Reproducción automática', + autoPlayOff: 'Detener reproducción automática', + speed: 'Velocidad', + voiceInput: 'Entrada de voz', + voiceInputDisabled: 'Entrada de voz deshabilitada', + textInput: 'Entrada de texto', + stopRecording: 'Detener grabación', + startRecording: 'Iniciar grabación', + }, + pbl: { + legacyFormat: 'Esta escena PBL usa un formato anterior. Por favor regenera el curso.', + emptyProject: + 'El proyecto PBL aún no se ha generado. Por favor créalo mediante la generación de curso.', + roleSelection: { + title: 'Elige Tu Rol', + description: 'Selecciona un rol para comenzar a colaborar en el proyecto', + }, + workspace: { + restart: 'Reiniciar', + confirmRestart: '¿Reiniciar todo el progreso?', + confirm: 'Confirmar', + cancel: 'Cancelar', + }, + issueboard: { + title: 'Tablero de Tareas', + noIssues: 'Sin tareas aún', + statusDone: 'Completado', + statusActive: 'Activo', + statusPending: 'Pendiente', + }, + chat: { + title: 'Discusión del Proyecto', + currentIssue: 'Tarea Actual', + mentionHint: 'Usa @question para preguntar, @judge para enviar a revisión', + placeholder: 'Escribe un mensaje...', + send: 'Enviar', + welcomeMessage: + '¡Hola! Soy tu Agente de Preguntas para esta tarea: "{title}"\n\nPara guiar tu trabajo, he preparado algunas preguntas:\n\n{questions}\n\n¡No dudes en usar @question si necesitas ayuda o aclaraciones!', + issueCompleteMessage: + '¡Tarea "{completed}" completada! Pasando a la siguiente tarea: "{next}"', + allCompleteMessage: '🎉 ¡Todas las tareas completadas! ¡Excelente trabajo en el proyecto!', + }, + guide: { + howItWorks: 'Cómo funciona', + help: 'Ayuda', + title: 'Ayuda', + step1: { + title: 'Paso 1: Elige un Rol', + desc: 'Después de generar el proyecto, selecciona un rol de la lista (roles no del sistema marcados con 🟢)', + }, + step2: { + title: 'Paso 2: Completa las Tareas', + desc: 'Cada tarea representa un objetivo de aprendizaje:', + s1: { + title: 'Ver tarea actual', + desc: 'Revisa el título, descripción y responsable de la tarea', + }, + s2: { + title: 'Obtener orientación', + example: + '@question ¿Por dónde debo empezar?\n@question ¿Cómo implemento esta función?', + desc: 'El Agente de Preguntas proporciona preguntas guía y pistas (sin respuestas directas)', + }, + s3: { + title: 'Enviar tu trabajo', + example: '@judge Ya terminé, por favor revisa mis Notas', + desc: 'El Agente Evaluador evalúa tu trabajo y da retroalimentación:', + complete: 'Avanza automáticamente a la siguiente tarea', + revision: 'Mejora según la retroalimentación', + }, + }, + step3: { + title: 'Paso 3: Completa el Proyecto', + desc: 'Cuando todas las tareas estén listas, el sistema muestra "🎉 ¡Proyecto Completado!"', + }, + }, + }, + share: { + notReady: 'Disponible después de completar la generación', + }, +} as const; diff --git a/lib/i18n/types.ts b/lib/i18n/types.ts index 6173b0be..2740631d 100644 --- a/lib/i18n/types.ts +++ b/lib/i18n/types.ts @@ -1,3 +1,3 @@ -export type Locale = 'zh-CN' | 'en-US'; +export type Locale = 'zh-CN' | 'en-US' | 'es-MX'; export const defaultLocale: Locale = 'zh-CN'; diff --git a/lib/server/classroom-generation.ts b/lib/server/classroom-generation.ts index eda67b4c..3c1b2019 100644 --- a/lib/server/classroom-generation.ts +++ b/lib/server/classroom-generation.ts @@ -96,8 +96,10 @@ function createInMemoryStore(stage: Stage): StageStore { }; } -function normalizeLanguage(language?: string): 'zh-CN' | 'en-US' { - return language === 'en-US' ? 'en-US' : 'zh-CN'; +function normalizeLanguage(language?: string): 'zh-CN' | 'en-US' | 'es-MX' { + if (language === 'en-US') return 'en-US'; + if (language === 'es-MX') return 'es-MX'; + return 'zh-CN'; } function stripCodeFences(text: string): string { diff --git a/lib/types/generation.ts b/lib/types/generation.ts index c1e6eb7a..b7b0b24b 100644 --- a/lib/types/generation.ts +++ b/lib/types/generation.ts @@ -43,7 +43,7 @@ export interface StylePreferences { interactivityLevel: 'low' | 'medium' | 'high'; includeExamples: boolean; includePractice: boolean; - language: string; // 'zh-CN', 'en-US' + language: string; // 'zh-CN', 'en-US', 'es-MX' } export interface UploadedDocument { @@ -64,7 +64,7 @@ export interface UploadedDocument { */ export interface UserRequirements { requirement: string; // Single free-form text for all user input - language: 'zh-CN' | 'en-US'; // Course language - critical for generation + language: 'zh-CN' | 'en-US' | 'es-MX'; // Course language - critical for generation userNickname?: string; // Student nickname for personalization userBio?: string; // Student background for personalization webSearch?: boolean; // Enable web search for richer context @@ -100,7 +100,7 @@ export interface SceneOutline { teachingObjective?: string; estimatedDuration?: number; // seconds order: number; - language?: 'zh-CN' | 'en-US'; // Generation language (inherited from requirements) + language?: 'zh-CN' | 'en-US' | 'es-MX'; // Generation language (inherited from requirements) // Suggested image IDs (from PDF-extracted images) suggestedImageIds?: string[]; // e.g., ["img_1", "img_3"] // AI-generated media requests (when PDF images are insufficient) @@ -124,7 +124,7 @@ export interface SceneOutline { projectDescription: string; targetSkills: string[]; issueCount?: number; - language: 'zh-CN' | 'en-US'; + language: 'zh-CN' | 'en-US' | 'es-MX'; }; }