From 7e545882753fd0781a5abb1b37a26555099ae8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miralles=20Sol=C3=A9?= <46744682B@gcb.intranet.gencat.cat> Date: Fri, 3 Apr 2026 22:45:36 +0200 Subject: [PATCH] feat: add Catalan (ca) language support as default locale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'ca' to Locale type with VALID_LOCALES export; set defaultLocale to 'ca' - Add Catalan translations (400+ keys) to all 5 i18n modules - Update language selectors in header and home page (CN/EN/CA dropdown) - Update generation toolbar to cycle through 3 languages (中文/EN/CA) - Extend UserRequirements, SceneOutline and pblConfig language unions to include 'ca' - Update normalizeLanguage, scene-generator and scene-content route for 'ca' - Add Azure TTS Catalan voices (ca-ES-AlbaNeural, ca-ES-EnricNeural) - Update TTS preview text with Catalan fallback Co-Authored-By: Claude Sonnet 4.6 --- app/api/generate/scene-content/route.ts | 2 +- app/page.tsx | 24 +- components/agent/agent-bar.tsx | 10 +- components/generation/generation-toolbar.tsx | 12 +- components/header.tsx | 15 +- lib/audio/constants.ts | 2 + lib/generation/scene-generator.ts | 2 +- lib/hooks/use-i18n.tsx | 6 +- lib/i18n/chat.ts | 74 +++ lib/i18n/common.ts | 41 ++ lib/i18n/generation.ts | 72 +++ lib/i18n/index.ts | 25 +- lib/i18n/settings.ts | 604 +++++++++++++++++++ lib/i18n/stage.ts | 150 +++++ lib/i18n/types.ts | 6 +- lib/server/classroom-generation.ts | 6 +- lib/types/generation.ts | 6 +- 17 files changed, 1024 insertions(+), 33 deletions(-) diff --git a/app/api/generate/scene-content/route.ts b/app/api/generate/scene-content/route.ts index cf4d9f341..282b493cf 100644 --- a/app/api/generate/scene-content/route.ts +++ b/app/api/generate/scene-content/route.ts @@ -69,7 +69,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' | 'ca') || 'zh-CN', }; // ── Model resolution from request headers ── diff --git a/app/page.tsx b/app/page.tsx index c0da47614..7495875ad 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -57,14 +57,14 @@ const RECENT_OPEN_STORAGE_KEY = 'recentClassroomsOpen'; interface FormState { pdfFile: File | null; requirement: string; - language: 'zh-CN' | 'en-US'; + language: 'zh-CN' | 'en-US' | 'ca'; webSearch: boolean; } const initialFormState: FormState = { pdfFile: null, requirement: '', - language: 'zh-CN', + language: 'ca', webSearch: false, }; @@ -100,10 +100,11 @@ 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 === 'ca') { updates.language = savedLanguage; } else { - const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US'; + const lang = navigator.language ?? ''; + const detected: 'zh-CN' | 'en-US' | 'ca' = lang.startsWith('zh') ? 'zh-CN' : lang.startsWith('ca') ? 'ca' : 'en-US'; updates.language = detected; } if (Object.keys(updates).length > 0) { @@ -346,7 +347,7 @@ function HomePage() { }} 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'} + {{ 'zh-CN': 'CN', 'en-US': 'EN', 'ca': 'CA' }[locale] ?? 'EN'} {languageOpen && (
@@ -376,6 +377,19 @@ function HomePage() { > English +
)} diff --git a/components/agent/agent-bar.tsx b/components/agent/agent-bar.tsx index 9379d9353..1ffc8f049 100644 --- a/components/agent/agent-bar.tsx +++ b/components/agent/agent-bar.tsx @@ -83,7 +83,10 @@ function AgentVoicePill({ const courseLanguage = (typeof localStorage !== 'undefined' && localStorage.getItem('generationLanguage')) || 'zh-CN'; - const previewText = courseLanguage === 'en-US' ? 'Welcome to AI Classroom' : '欢迎来到AI课堂'; + const previewText = + courseLanguage === 'zh-CN' ? '欢迎来到AI课堂' : + courseLanguage === 'ca' ? "Benvingut a l'aula d'IA" : + 'Welcome to AI Classroom'; if (providerId === 'browser-native-tts') { const { promise, cancel } = playBrowserTTSPreview({ text: previewText, voice: voiceId }); @@ -308,7 +311,10 @@ function TeacherVoicePill({ const courseLanguage = (typeof localStorage !== 'undefined' && localStorage.getItem('generationLanguage')) || 'zh-CN'; - const previewText = courseLanguage === 'en-US' ? 'Welcome to AI Classroom' : '欢迎来到AI课堂'; + const previewText = + courseLanguage === 'zh-CN' ? '欢迎来到AI课堂' : + courseLanguage === 'ca' ? "Benvingut a l'aula d'IA" : + 'Welcome to AI Classroom'; if (providerId === 'browser-native-tts') { const { promise, cancel } = playBrowserTTSPreview({ text: previewText, voice: voiceId }); diff --git a/components/generation/generation-toolbar.tsx b/components/generation/generation-toolbar.tsx index 27301bbd8..6837d6680 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' | 'ca'; + onLanguageChange: (lang: 'zh-CN' | 'en-US' | 'ca') => 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 5a61ec96f..666cc6c94 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'} + {{ 'zh-CN': 'CN', 'en-US': 'EN', 'ca': 'CA' }[locale] ?? 'EN'} {languageOpen && (
@@ -138,6 +138,19 @@ export function Header({ currentSceneTitle }: HeaderProps) { > English +
)} diff --git a/lib/audio/constants.ts b/lib/audio/constants.ts index 423f5b82c..bf6ea0177 100644 --- a/lib/audio/constants.ts +++ b/lib/audio/constants.ts @@ -204,6 +204,8 @@ export const TTS_PROVIDERS: Record = { gender: 'female', }, { id: 'en-US-GuyNeural', name: 'Guy', language: 'en-US', gender: 'male' }, + { id: 'ca-ES-AlbaNeural', name: 'Alba (Català)', language: 'ca-ES', gender: 'female' }, + { id: 'ca-ES-EnricNeural', name: 'Enric (Català)', language: 'ca-ES', gender: 'male' }, ], supportedFormats: ['mp3', 'wav', 'ogg'], speedRange: { min: 0.5, max: 2.0, default: 1.0 }, diff --git a/lib/generation/scene-generator.ts b/lib/generation/scene-generator.ts index 1dc22937d..70c304ff4 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' | 'ca' = 'zh-CN', ): Promise { const config = outline.interactiveConfig!; diff --git a/lib/hooks/use-i18n.tsx b/lib/hooks/use-i18n.tsx index 4e642f4c2..c9d3d9b76 100644 --- a/lib/hooks/use-i18n.tsx +++ b/lib/hooks/use-i18n.tsx @@ -1,7 +1,7 @@ 'use client'; import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import { Locale, translate, defaultLocale } from '@/lib/i18n'; +import { Locale, translate, defaultLocale, VALID_LOCALES } from '@/lib/i18n'; type I18nContextType = { locale: Locale; @@ -10,7 +10,6 @@ type I18nContextType = { }; const LOCALE_STORAGE_KEY = 'locale'; -const VALID_LOCALES: Locale[] = ['zh-CN', 'en-US']; const I18nContext = createContext(undefined); @@ -26,7 +25,8 @@ export function I18nProvider({ children }: { children: ReactNode }) { setLocaleState(stored as Locale); return; } - const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US'; + const lang = navigator.language ?? ''; + const detected: Locale = lang.startsWith('zh') ? 'zh-CN' : lang.startsWith('ca') ? 'ca' : 'en-US'; localStorage.setItem(LOCALE_STORAGE_KEY, detected); setLocaleState(detected); } catch { diff --git a/lib/i18n/chat.ts b/lib/i18n/chat.ts index 1bb535d3e..c493fb3a7 100644 --- a/lib/i18n/chat.ts +++ b/lib/i18n/chat.ts @@ -72,6 +72,80 @@ export const chatZhCN = { }, } as const; +export const chatCa = { + chat: { + lecture: 'Lliçó', + noConversations: 'Cap conversa', + startConversation: 'Escriu un missatge per començar a xatejar', + noMessages: 'Encara no hi ha missatges', + ended: 'finalitzat', + unknown: 'Desconegut', + stopDiscussion: 'Aturar discussió', + endQA: 'Finalitzar P&R', + tabs: { + lecture: 'Notes', + chat: 'Xat', + }, + lectureNotes: { + empty: 'Les notes apareixeran aquí després de la reproducció de la lliçó', + emptyHint: 'Premeu reproduir per iniciar la lliçó', + pageLabel: 'Pàgina {n}', + currentPage: 'Actual', + }, + badge: { + qa: 'P&R', + discussion: 'DISC', + lecture: 'LLIÇ', + }, + }, + actions: { + names: { + spotlight: 'Focus', + laser: 'Làser', + wb_open: 'Obrir pissarra', + wb_draw_text: 'Text a la pissarra', + wb_draw_shape: 'Forma a la pissarra', + wb_draw_chart: 'Gràfic a la pissarra', + wb_draw_latex: 'Fórmula a la pissarra', + wb_draw_table: 'Taula a la pissarra', + wb_draw_line: 'Línia a la pissarra', + wb_clear: 'Netejar pissarra', + wb_delete: 'Eliminar element', + wb_close: 'Tancar pissarra', + discussion: 'Discussió', + }, + status: { + inputStreaming: 'En espera', + inputAvailable: 'Executant', + outputAvailable: 'Completat', + outputError: 'Error', + outputDenied: 'Denegat', + running: 'Executant', + result: 'Completat', + error: 'Error', + }, + }, + agentBar: { + readyToLearn: 'Preparats per aprendre junts?', + expandedTitle: 'Configuració de rols de l\'aula', + configTooltip: 'Clic per configurar els rols de l\'aula', + voiceLabel: 'Veu', + voiceLoading: 'Carregant...', + voiceAutoAssign: 'Les veus s\'assignaran automàticament', + }, + proactiveCard: { + discussion: 'Discussió', + join: 'Unir-se', + skip: 'Ometre', + pause: 'Pausar', + resume: 'Reprendre', + }, + voice: { + startListening: 'Entrada de veu', + stopListening: 'Aturar gravació', + }, +} as const; + export const chatEnUS = { chat: { lecture: 'Lecture', diff --git a/lib/i18n/common.ts b/lib/i18n/common.ts index 1bceb5d61..edc5cbde8 100644 --- a/lib/i18n/common.ts +++ b/lib/i18n/common.ts @@ -39,6 +39,47 @@ export const commonZhCN = { }, } as const; +export const commonCa = { + common: { + you: 'Tu', + confirm: 'Confirmar', + cancel: 'Cancel·lar', + loading: 'Carregant...', + }, + home: { + slogan: 'Aprenentatge Generatiu en Aula Interactiva Multi-Agent', + greeting: 'Hola, ', + }, + toolbar: { + languageHint: 'El curs es generarà en aquesta llengua', + pdfParser: 'Analitzador', + pdfUpload: 'Pujar PDF', + removePdf: 'Eliminar fitxer', + webSearchOn: 'Activat', + webSearchOff: 'Clic per activar', + webSearchDesc: 'Cerca informació actualitzada a la web abans de la generació', + webSearchProvider: 'Motor de cerca', + webSearchNoProvider: 'Configura la clau API de cerca a la Configuració', + selectProvider: 'Selecciona proveïdor', + configureProvider: 'Configura model', + configureProviderHint: 'Configura almenys un proveïdor de model per generar cursos', + enterClassroom: 'Entrar a l\'aula', + advancedSettings: 'Configuració avançada', + ttsTitle: 'Síntesi de veu', + ttsHint: 'Trieu una veu per al professor IA', + ttsPreview: 'Previsualitzar', + ttsPreviewing: 'Reproduint...', + }, + export: { + pptx: 'Exportar PPTX', + resourcePack: 'Exportar paquet de recursos', + resourcePackDesc: 'PPTX + pàgines interactives', + exporting: 'Exportant...', + exportSuccess: 'Exportació correcta', + exportFailed: 'Error en l\'exportació', + }, +} as const; + export const commonEnUS = { common: { you: 'You', diff --git a/lib/i18n/generation.ts b/lib/i18n/generation.ts index e5707445f..772712c50 100644 --- a/lib/i18n/generation.ts +++ b/lib/i18n/generation.ts @@ -68,6 +68,78 @@ export const generationZhCN = { }, } as const; +export const generationCa = { + classroom: { + recentClassrooms: 'Recent', + today: 'Avui', + yesterday: 'Ahir', + daysAgo: 'dies enrere', + slides: 'diapositives', + nameCopied: 'Nom copiat', + deleteConfirmTitle: 'Eliminar', + delete: 'Eliminar', + rename: 'Reanomenar', + renamePlaceholder: 'Introduïu el nom de l\'aula', + renameFailed: 'Error en reanomenar l\'aula', + }, + upload: { + pdfSizeLimit: 'Suporta fitxers PDF de fins a 50MB', + generateFailed: 'Error en generar l\'aula, torneu-ho a intentar', + requirementPlaceholder: + 'Digueu-me qualsevol cosa que vulgueu aprendre, per exemple:\n"Ensenyeu-me Python des de zero en 30 minuts"\n"Expliqueu la Transformada de Fourier a la pissarra"\n"Com es juga al joc de taula Avalon"', + requirementRequired: 'Introduïu els requisits del curs', + fileTooLarge: 'Fitxer massa gran. Seleccioneu un PDF inferior a 50MB', + }, + generation: { + // Progress steps (used dynamically via activeStep) + analyzingPdf: 'Analitzant document PDF', + analyzingPdfDesc: 'Extraient l\'estructura i contingut del document...', + pdfLoadFailed: 'Error en carregar el fitxer PDF, torneu-ho a intentar', + pdfParseFailed: 'Error en analitzar el PDF', + streamNotReadable: 'No es pot llegir el flux de generació', + generatingOutlines: 'Elaborant esquema del curs', + generatingOutlinesDesc: 'Estructurant el camí d\'aprenentatge...', + generatingSlideContent: 'Generant contingut de les pàgines', + generatingSlideContentDesc: 'Creant diapositives, qüestionaris i contingut interactiu...', + generatingActions: 'Generant accions didàctiques', + generatingActionsDesc: 'Organitzant narracions, focus i interaccions...', + generationComplete: 'Generació completada!', + generationFailed: 'Error en la generació', + generatingCourse: 'Generant el curs', + openingClassroom: 'Obrint l\'aula...', + outlineReady: 'Esquema del curs generat', + generatingFirstPage: 'Generant la primera pàgina...', + firstPageReady: 'Primera pàgina llesta! Obrint l\'aula...', + speechFailed: 'Error en la generació de veu', + retryScene: 'Reintentar', + retryingScene: 'Regenerant...', + backToHome: 'Tornar a l\'inici', + sessionNotFound: 'Sessió no trobada', + sessionNotFoundDesc: 'Ompliu els requisits del curs per iniciar el procés de generació.', + goBackAndRetry: 'Tornar enrere i reintentar', + classroomReady: 'El vostre entorn d\'aprenentatge IA personalitzat s\'ha generat correctament.', + aiWorking: 'Agents IA treballant...', + textTruncated: 'El text del document és llarg, s\'utilitzen els primers {n} caràcters per a la generació', + imageTruncated: + 'S\'han trobat {total} imatges, que superen el límit de {max}. Les imatges addicionals només usaran descripcions de text', + // Agent generation + agentGeneration: 'Generant rols de l\'aula', + agentGenerationDesc: 'Generant rols basats en el contingut del curs...', + agentRevealTitle: 'Els vostres rols de l\'aula', + viewAgents: 'Veure rols', + continue: 'Continuar', + // Outline errors + outlineRetrying: 'Problema en la generació de l\'esquema, reintentant...', + outlineEmptyResponse: + 'El model no ha retornat cap esquema vàlid. Comproveu la configuració del model i torneu-ho a intentar', + outlineGenerateFailed: 'Error en generar l\'esquema, torneu-ho a intentar més tard', + // Web Search + webSearching: 'Cerca web', + webSearchingDesc: 'Cercant a la web informació actualitzada', + webSearchFailed: 'Error en la cerca web', + }, +} as const; + export const generationEnUS = { classroom: { recentClassrooms: 'Recent', diff --git a/lib/i18n/index.ts b/lib/i18n/index.ts index 5fd70da52..84a8a2e03 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 { defaultLocale, type Locale, VALID_LOCALES } from './types'; +export { type Locale, defaultLocale, VALID_LOCALES } from './types'; +import { commonZhCN, commonEnUS, commonCa } from './common'; +import { stageZhCN, stageEnUS, stageCa } from './stage'; +import { chatZhCN, chatEnUS, chatCa } from './chat'; +import { generationZhCN, generationEnUS, generationCa } from './generation'; +import { settingsZhCN, settingsEnUS, settingsCa } from './settings'; export const translations = { 'zh-CN': { @@ -21,6 +21,13 @@ export const translations = { ...generationEnUS, ...settingsEnUS, }, + 'ca': { + ...commonCa, + ...stageCa, + ...chatCa, + ...generationCa, + ...settingsCa, + }, } as const; export type TranslationKey = keyof (typeof translations)[typeof defaultLocale]; @@ -40,8 +47,8 @@ export function getClientTranslation(key: string): string { if (typeof window !== 'undefined') { try { const storedLocale = localStorage.getItem('locale'); - if (storedLocale === 'zh-CN' || storedLocale === 'en-US') { - locale = storedLocale; + if (storedLocale && VALID_LOCALES.includes(storedLocale as Locale)) { + locale = storedLocale as Locale; } } catch { // localStorage unavailable, keep default locale diff --git a/lib/i18n/settings.ts b/lib/i18n/settings.ts index 356fea554..af2c38760 100644 --- a/lib/i18n/settings.ts +++ b/lib/i18n/settings.ts @@ -591,6 +591,610 @@ export const settingsZhCN = { }, } as const; +export const settingsCa = { + settings: { + title: 'Configuració', + description: 'Configura els paràmetres de l\'aplicació', + language: 'Idioma', + languageDesc: 'Seleccioneu l\'idioma de la interfície', + theme: 'Tema', + themeDesc: 'Seleccioneu el mode de tema (Clar/Fosc/Sistema)', + themeOptions: { + light: 'Clar', + dark: 'Fosc', + system: 'Sistema', + }, + apiKey: 'Clau API', + apiKeyDesc: 'Configureu la vostra clau API', + apiBaseUrl: 'URL de l\'endpoint API', + apiBaseUrlDesc: 'Configureu la URL de l\'endpoint API', + apiKeyRequired: 'La clau API no pot estar buida', + model: 'Configuració del model', + modelDesc: 'Configureu models IA', + modelPlaceholder: 'Introduïu o seleccioneu el nom del model', + ttsModel: 'Model TTS', + ttsModelDesc: 'Configureu models TTS', + ttsModelPlaceholder: 'Introduïu o seleccioneu el nom del model TTS', + ttsModelOptions: { + openaiTts: 'OpenAI TTS', + azureTts: 'Azure TTS', + }, + availableModels: 'Models disponibles', + modelSelectedViaVoice: 'El model es determina per la selecció de veu', + testConnection: 'Provar connexió', + testConnectionDesc: 'Proveu si la configuració API actual és disponible', + testing: 'Provant...', + agentSettings: 'Configuració d\'agents', + agentSettingsDesc: + 'Seleccioneu els agents per participar a la conversa. Seleccioneu 1 per al mode d\'agent únic, seleccioneu múltiples per al mode col·laboratiu multi-agent.', + agentMode: 'Mode d\'agent', + agentModePreset: 'Preestablert', + agentModeAuto: 'Generació automàtica', + agentModeAutoDesc: 'La IA generarà automàticament rols adequats', + autoAgentCount: 'Nombre d\'agents', + autoAgentCountDesc: 'Nombre d\'agents a generar automàticament (inclou el professor)', + atLeastOneAgent: 'Seleccioneu almenys 1 agent', + singleAgentMode: 'Mode d\'agent únic', + directAnswer: 'Resposta directa', + multiAgentMode: 'Mode multi-agent', + agentsCollaborating: 'Discussió col·laborativa', + agentsCollaboratingCount: '{count} agents seleccionats per a la discussió col·laborativa', + maxTurns: 'Torns màxims de discussió', + maxTurnsDesc: + 'El nombre màxim de torns de discussió entre agents (cada agent completa accions i respon compta com un torn)', + priority: 'Prioritat', + actions: 'Accions', + actionCount: '{count} accions', + selectedAgent: 'Agent seleccionat', + selectedAgents: 'Agents seleccionats', + required: 'Obligatori', + agentNames: { + 'default-1': 'Professor IA', + 'default-2': 'Assistent IA', + 'default-3': 'Animador de classe', + 'default-4': 'Ment curiosa', + 'default-5': 'Apuntador', + 'default-6': 'Pensador profund', + }, + agentRoles: { + teacher: 'Professor', + assistant: 'Assistent', + student: 'Estudiant', + }, + agentDescriptions: { + 'default-1': 'Professor principal amb explicacions clares i estructurades', + 'default-2': 'Dona suport a l\'aprenentatge i ajuda a aclarir els punts clau', + 'default-3': 'Aporta humor i energia a l\'aula', + 'default-4': 'Sempre curiós, li encanta preguntar el per què i el com', + 'default-5': 'Registra i organitza diligentment les notes de classe', + 'default-6': 'Pensa profundament i explora l\'essència dels temes', + }, + close: 'Tancar', + save: 'Desar', + // Provider settings + providers: 'LLM', + addProviderDescription: 'Afegiu proveïdors de models personalitzats per ampliar els models IA disponibles', + providerNames: { + openai: 'OpenAI', + anthropic: 'Claude', + google: 'Gemini', + deepseek: 'DeepSeek', + qwen: 'Qwen', + kimi: 'Kimi', + minimax: 'MiniMax', + glm: 'GLM', + siliconflow: 'SiliconFlow', + }, + providerTypes: { + openai: 'Protocol OpenAI', + anthropic: 'Protocol Claude', + google: 'Protocol Gemini', + }, + modelCount: 'models', + modelSingular: 'model', + defaultModel: 'Model per defecte', + webSearch: 'Cerca web', + mcp: 'MCP', + knowledgeBase: 'Base de coneixement', + documentParser: 'Analitzador de documents', + conversationSettings: 'Conversa', + keyboardShortcuts: 'Dreceres', + generalSettings: 'General', + systemSettings: 'Sistema', + addProvider: 'Afegir', + importFromClipboard: 'Importar del porta-retalls', + apiSecret: 'Clau API', + apiHost: 'Base URL', + requestUrl: 'URL de sol·licitud', + models: 'Models', + addModel: 'Nou', + reset: 'Restablir', + fetch: 'Obtenir', + connectionSuccess: 'Connexió correcta', + connectionFailed: 'Error de connexió', + // Model capabilities + capabilities: { + vision: 'Visió', + tools: 'Eines', + streaming: 'Streaming', + }, + contextWindow: 'Context', + contextShort: 'ctx', + outputWindow: 'Sortida', + // Provider management + addProviderButton: 'Afegir', + addProviderDialog: 'Afegir proveïdor de model', + providerName: 'Nom', + providerNamePlaceholder: 'p. ex., El meu proxy OpenAI', + providerNameRequired: 'Introduïu el nom del proveïdor', + providerApiMode: 'Mode API', + apiModeOpenAI: 'Protocol OpenAI', + apiModeAnthropic: 'Protocol Claude', + apiModeGoogle: 'Protocol Gemini', + defaultBaseUrl: 'Base URL per defecte', + providerIcon: 'URL de la icona del proveïdor', + requiresApiKey: 'Requereix clau API', + deleteProvider: 'Eliminar proveïdor', + deleteProviderConfirm: 'Esteu segurs que voleu eliminar aquest proveïdor?', + cannotDeleteBuiltIn: 'No es pot eliminar el proveïdor integrat', + resetToDefault: 'Restablir valors per defecte', + resetToDefaultDescription: + 'Restaura la llista de models a la configuració per defecte (la clau API i la Base URL es conservaran)', + resetConfirmDescription: + 'Això eliminarà tots els models personalitzats i restaurarà la llista de models per defecte integrada. La clau API i la Base URL es conservaran.', + confirmReset: 'Confirmar restabliment', + resetSuccess: 'S\'ha restablit correctament a la configuració per defecte', + saveSuccess: 'Configuració desada', + saveFailed: 'Error en desar la configuració, torneu-ho a intentar', + cannotDeleteBuiltInModel: 'No es pot eliminar el model integrat', + cannotEditBuiltInModel: 'No es pot editar el model integrat', + modelIdRequired: 'Introduïu l\'ID del model', + noModelsAvailable: 'No hi ha models disponibles per a les proves', + providerMetadata: 'Metadades del proveïdor', + // Model editing + editModel: 'Editar model', + editModelDescription: 'Editeu la configuració i les capacitats del model', + addNewModel: 'Nou model', + addNewModelDescription: 'Afegiu una nova configuració de model', + modelId: 'ID del model', + modelIdPlaceholder: 'p. ex., gpt-4o', + modelName: 'Nom de visualització', + modelNamePlaceholder: 'Opcional', + modelCapabilities: 'Capacitats', + advancedSettings: 'Configuració avançada', + contextWindowLabel: 'Finestra de context', + contextWindowPlaceholder: 'p. ex., 128000', + outputWindowLabel: 'Tokens de sortida màxims', + outputWindowPlaceholder: 'p. ex., 4096', + testModel: 'Provar model', + deleteModel: 'Eliminar', + cancelEdit: 'Cancel·lar', + saveModel: 'Desar', + modelsManagementDescription: + 'Gestioneu els models per a aquest proveïdor. Per seleccionar el model actiu, aneu a "General".', + // General settings + howToUse: 'Com utilitzar', + step1ConfigureProvider: + 'Aneu a "Proveïdors de model", seleccioneu o afegiu un proveïdor i configureu els paràmetres de connexió (clau API, Base URL, etc.)', + step2SelectModel: 'Seleccioneu el model que voleu usar a "Model actiu" a continuació', + step3StartUsing: 'Després de desar, el sistema usarà el model seleccionat', + activeModel: 'Model actiu', + activeModelDescription: 'Seleccioneu el model per a les converses IA i la generació de contingut', + selectModel: 'Seleccionar model', + searchModels: 'Cercar models', + noModelsFound: 'No s\'han trobat models coincidents', + noConfiguredProviders: 'No hi ha proveïdors configurats', + configureProvidersFirst: + 'Configureu els paràmetres de connexió del proveïdor a "Proveïdors de model" a l\'esquerra', + currentlyUsing: 'Usant actualment', + // TTS settings + ttsSettings: 'Síntesi de veu', + // ASR settings + asrSettings: 'Reconeixement de veu', + // Audio settings (legacy) + audioSettings: 'Configuració d\'àudio', + ttsSection: 'Text a veu (TTS)', + asrSection: 'Reconeixement automàtic de veu (ASR)', + ttsDescription: 'TTS (Text a veu) - Converteix text en veu', + asrDescription: 'ASR (Reconeixement automàtic de veu) - Converteix veu en text', + enableTTS: 'Activar síntesi de veu', + ttsEnabledDescription: 'Quan s\'activa, es generarà àudio de veu durant la creació del curs', + ttsVoiceConfigHint: + 'La veu per agent es pot configurar a "Configuració de rols de l\'aula" a la pàgina d\'inici', + enableASR: 'Activar reconeixement de veu', + asrEnabledDescription: 'Quan s\'activa, els estudiants poden usar el micròfon per a l\'entrada de veu', + ttsProvider: 'Proveïdor TTS', + ttsLanguageFilter: 'Filtre d\'idioma', + allLanguages: 'Tots els idiomes', + ttsVoice: 'Veu', + ttsSpeed: 'Velocitat', + ttsBaseUrl: 'Base URL', + ttsApiKey: 'Clau API', + doubaoAppId: 'App ID', + doubaoAccessKey: 'Clau d\'accés', + asrProvider: 'Proveïdor ASR', + asrLanguage: 'Idioma de reconeixement', + asrBaseUrl: 'Base URL', + asrApiKey: 'Clau API', + enterApiKey: 'Introduïu la clau API', + enterCustomBaseUrl: 'Introduïu la Base URL personalitzada', + browserNativeNote: 'L\'ASR natiu del navegador no requereix configuració i és completament gratuït', + // Audio provider names + providerOpenAITTS: 'OpenAI TTS (gpt-4o-mini-tts)', + providerAzureTTS: 'Azure TTS', + providerGLMTTS: 'GLM TTS', + providerQwenTTS: 'Qwen TTS (Alibaba Cloud Bailian)', + providerDoubaoTTS: 'Doubao TTS 2.0 (Volcengine)', + providerElevenLabsTTS: 'ElevenLabs TTS', + providerMiniMaxTTS: 'MiniMax TTS', + providerBrowserNativeTTS: 'TTS natiu del navegador', + providerOpenAIWhisper: 'OpenAI ASR (gpt-4o-mini-transcribe)', + providerBrowserNative: 'ASR natiu del navegador', + providerQwenASR: 'Qwen ASR (Alibaba Cloud Bailian)', + providerUnpdf: 'unpdf (Integrat)', + providerMinerU: 'MinerU', + browserNativeTTSNote: + 'El TTS natiu del navegador no requereix configuració i és completament gratuït, usant les veus integrades del sistema', + testTTS: 'Provar TTS', + testASR: 'Provar ASR', + testSuccess: 'Prova correcta', + testFailed: 'Prova fallida', + ttsTestText: 'Text de prova TTS', + ttsTestSuccess: 'Prova TTS correcta, àudio reproduït', + ttsTestFailed: 'Prova TTS fallida', + asrTestSuccess: 'Reconeixement de veu correcte', + asrTestFailed: 'Reconeixement de veu fallat', + asrResult: 'Resultat del reconeixement', + asrNotSupported: 'El navegador no suporta l\'API de reconeixement de veu', + browserTTSNotSupported: 'El navegador no suporta l\'API de síntesi de veu', + browserTTSNoVoices: 'El navegador actual no té veus TTS disponibles', + microphoneAccessDenied: 'Accés al micròfon denegat', + microphoneAccessFailed: 'Error en accedir al micròfon', + asrResultPlaceholder: 'El resultat del reconeixement es mostrarà després de la gravació', + useThisProvider: 'Usar aquest proveïdor', + fetchVoices: 'Obtenir llista de veus', + fetchingVoices: 'Obtenint...', + voicesFetched: 'Veus obtingudes', + fetchVoicesFailed: 'Error en obtenir les veus', + voiceApiKeyRequired: 'Es requereix clau API', + voiceBaseUrlRequired: 'Es requereix Base URL', + ttsTestTextPlaceholder: 'Introduïu el text a convertir', + ttsTestTextDefault: 'Hola, aquesta és una prova de veu.', + startRecording: 'Iniciar gravació', + stopRecording: 'Aturar gravació', + recording: 'Gravant...', + transcribing: 'Transcrivint...', + transcriptionResult: 'Resultat de la transcripció', + noTranscriptionResult: 'Cap resultat de transcripció', + baseUrlOptional: 'Base URL (Opcional)', + defaultValue: 'Per defecte', + // TTS Voice descriptions (OpenAI) + voiceMarin: 'Recomanat - Millor qualitat', + voiceCedar: 'Recomanat - Millor qualitat', + voiceAlloy: 'Neutre, equilibrat', + voiceAsh: 'Ferm, professional', + voiceBallad: 'Elegant, líric', + voiceCoral: 'Càlid, amigable', + voiceEcho: 'Masculí, clar', + voiceFable: 'Narratiu, vívid', + voiceNova: 'Femení, brillant', + voiceOnyx: 'Masculí, profund', + voiceSage: 'Savi, compost', + voiceShimmer: 'Femení, suau', + voiceVerse: 'Natural, fluid', + // TTS Voice descriptions (GLM) + glmVoiceTongtong: 'Veu per defecte', + glmVoiceChuichui: 'Veu Chuichui', + glmVoiceXiaochen: 'Veu Xiaochen', + glmVoiceJam: 'Veu Jam', + glmVoiceKazi: 'Veu Kazi', + glmVoiceDouji: 'Veu Douji', + glmVoiceLuodo: 'Veu Luodo', + // TTS Voice descriptions (Qwen) + qwenVoiceCherry: 'Alegre, càlid i natural', + qwenVoiceSerena: 'Suau i dolç', + qwenVoiceEthan: 'Enèrgic i vibrant', + qwenVoiceChelsie: 'Noia virtual anime', + qwenVoiceMomo: 'Juganera i alegre', + qwenVoiceVivian: 'Maca i atrevida', + qwenVoiceMoon: 'Fresc i elegant', + qwenVoiceMaia: 'Intel·lectual i suau', + qwenVoiceKai: 'Un SPA per a les vostres orelles', + qwenVoiceNofish: 'Dissenyadora que no pronuncia sons retroflex', + qwenVoiceBella: 'Noia que no s\'emborratxa', + qwenVoiceJennifer: 'Veu femenina americana de qualitat cinematogràfica', + qwenVoiceRyan: 'Ritme ràpid, actuació dramàtica', + qwenVoiceKaterina: 'Dona madura amb ritme memorable', + qwenVoiceAiden: 'Noi americà expert en cuina', + qwenVoiceEldricSage: 'Ancià estable i savi', + qwenVoiceMia: 'Suau com l\'aigua de primavera', + qwenVoiceMochi: 'Nen adult intel·ligent amb innocència infantil', + qwenVoiceBellona: 'Veu potent, pronunciació clara', + qwenVoiceVincent: 'Veu ronca única explicant històries', + qwenVoiceBunny: 'Noia super maca', + qwenVoiceNeil: 'Presentador de notícies professional', + qwenVoiceElias: 'Instructor professional', + qwenVoiceArthur: 'Veu senzilla impregnada d\'anys', + qwenVoiceNini: 'Veu suau i enganxosa', + qwenVoiceEbona: 'El seu murmuri és com una clau rovellada', + qwenVoiceSeren: 'Veu suau i relaxant per ajudar-vos a dormir', + qwenVoicePip: 'Entremaliat però ple d\'innocència infantil', + qwenVoiceStella: 'Veu de noia dolça i confosa', + qwenVoiceBodega: 'Oncle espanyol entusiasta', + qwenVoiceSonrisa: 'Dona llatinoamericana entusiasta', + qwenVoiceAlek: 'Fred com una nació guerrera, càlid sota l\'abric de llana', + qwenVoiceDolce: 'Oncle italià mandrós', + qwenVoiceSohee: 'Noia coreana amable i alegre', + qwenVoiceOnoAnna: 'Amic d\'infància entremaliat', + qwenVoiceLenn: 'Jove alemany racional que porta vestit i escolta post-punk', + qwenVoiceEmilien: 'Germà gran francès romàntic', + qwenVoiceAndre: 'Veu masculina magnètica, natural i tranquil·la', + qwenVoiceRadioGol: 'Poeta del futbol Rádio Gol!', + qwenVoiceJada: 'Senyora de Shanghai animada', + qwenVoiceDylan: 'Noi de Pequín', + qwenVoiceLi: 'Professora de ioga pacient', + qwenVoiceMarcus: 'Cara ampla, paraules curtes, cor sòlid', + qwenVoiceRoy: 'Noi taiwanès humorístic i directe', + qwenVoicePeter: 'Suportador professional de comèdia Tianjin', + qwenVoiceSunny: 'Noia de Sichuan dolça', + qwenVoiceEric: 'Cavaller de Chengdu', + qwenVoiceRocky: 'Noi de Hong Kong humorístic', + qwenVoiceKiki: 'Noia de Hong Kong dolça', + // ASR Language names (native forms - autoglossonyms) + lang_auto: 'Detecció 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àlisi PDF', + pdfParsingSettings: 'Configuració d\'anàlisi PDF', + pdfDescription: + 'Trieu el motor d\'anàlisi PDF amb suport per a l\'extracció de text, processament d\'imatges i reconeixement de taules', + pdfProvider: 'Analitzador PDF', + pdfFeatures: 'Funcions suportades', + pdfApiKey: 'Clau API', + pdfBaseUrl: 'Base URL', + mineruDescription: + 'MinerU és un servei comercial d\'anàlisi PDF que suporta funcions avançades com l\'extracció de taules, el reconeixement de fórmules i l\'anàlisi de disseny.', + mineruApiKeyRequired: 'Heu de sol·licitar una clau API al lloc web de MinerU abans d\'usar-lo.', + mineruWarning: 'Avís', + mineruCostWarning: + 'MinerU és un servei comercial i pot generar despeses. Consulteu el lloc web de MinerU per obtenir detalls de preus.', + enterMinerUApiKey: 'Introduïu la clau API de MinerU', + mineruLocalDescription: + 'MinerU suporta desplegament local amb anàlisi PDF avançada (taules, fórmules, anàlisi de disseny). Requereix desplegar primer el servei MinerU.', + mineruServerAddress: 'Adreça del servidor MinerU local (p. ex., http://localhost:8080)', + mineruApiKeyOptional: 'Només necessari si el servidor té autenticació activada', + optionalApiKey: 'Clau API opcional', + featureText: 'Extracció de text', + featureImages: 'Extracció d\'imatges', + featureTables: 'Extracció de taules', + featureFormulas: 'Reconeixement de fórmules', + featureLayoutAnalysis: 'Anàlisi de disseny', + featureMetadata: 'Metadades', + // Image Generation settings + enableImageGeneration: 'Activar generació d\'imatges IA', + imageGenerationDisabledHint: + 'Quan s\'activa, es generaran imatges automàticament durant la creació del curs', + imageSettings: 'Generació d\'imatges', + imageSection: 'Text a imatge', + imageProvider: 'Proveïdor de generació d\'imatges', + imageModel: 'Model de generació d\'imatges', + providerSeedream: 'Seedream (ByteDance)', + providerQwenImage: 'Qwen Image (Alibaba)', + providerNanoBanana: 'Nano Banana (Gemini)', + providerMiniMaxImage: 'MiniMax Image', + providerGrokImage: 'Grok Image (xAI)', + testImageGeneration: 'Provar generació d\'imatges', + testImageConnectivity: 'Provar connexió', + imageConnectivitySuccess: 'Servei d\'imatges connectat correctament', + imageConnectivityFailed: 'Error en la connexió del servei d\'imatges', + imageTestSuccess: 'Prova de generació d\'imatges correcta', + imageTestFailed: 'Prova de generació d\'imatges fallida', + imageTestPromptPlaceholder: 'Introduïu la descripció de la imatge per a la prova', + imageTestPromptDefault: 'Un gat maco assegut a un escriptori', + imageGenerating: 'Generant imatge...', + imageGenerationFailed: 'Error en la generació d\'imatges', + // Video Generation settings + enableVideoGeneration: 'Activar generació de vídeo IA', + videoGenerationDisabledHint: + 'Quan s\'activa, es generaran vídeos automàticament durant la creació del curs', + videoSettings: 'Generació de vídeo', + videoSection: 'Text a vídeo', + videoProvider: 'Proveïdor de generació de vídeo', + videoModel: 'Model de generació de vídeo', + providerSeedance: 'Seedance (ByteDance)', + providerKling: 'Kling (Kuaishou)', + providerVeo: 'Veo (Google)', + providerSora: 'Sora (OpenAI)', + providerMiniMaxVideo: 'MiniMax Video', + providerGrokVideo: 'Grok Video (xAI)', + testVideoGeneration: 'Provar generació de vídeo', + testVideoConnectivity: 'Provar connexió', + videoConnectivitySuccess: 'Servei de vídeo connectat correctament', + videoConnectivityFailed: 'Error en la connexió del servei de vídeo', + testingConnection: 'Provant...', + videoTestSuccess: 'Prova de generació de vídeo correcta', + videoTestFailed: 'Prova de generació de vídeo fallida', + videoTestPromptDefault: 'Un gat maco caminant per un escriptori', + videoGenerating: 'Generant vídeo (est. 1-2 min)...', + videoGenerationWarning: 'La generació de vídeo normalment triga 1-2 minuts, tingueu paciència', + mediaRetry: 'Reintentar', + mediaContentSensitive: 'Ho sentim, aquest contingut ha activat una verificació de seguretat.', + mediaGenerationDisabled: 'Generació desactivada a la configuració', + // Agent settings (kept with main settings block above) + singleAgent: 'Agent únic', + multiAgent: 'Multi-agent', + selectAgents: 'Seleccionar agents', + noVisionWarning: + 'El model actual no suporta visió. Les imatges es poden posar a les diapositives, però el model no pot entendre el contingut de les imatges per optimitzar la selecció i el disseny', + // Server provider configuration + serverConfigured: 'Servidor', + serverConfiguredNotice: + 'L\'administrador ha configurat una clau API per a aquest proveïdor al servidor. Podeu usar-la directament o introduir la vostra pròpia clau per sobreescriure-la.', + optionalOverride: 'Opcional — deixeu buit per usar la configuració del servidor', + // Access code + setupNeeded: 'Cal configuració', + modelNotConfigured: 'Seleccioneu un model per començar', + // Clear cache + dangerZone: 'Zona de perill', + clearCache: 'Netejar caché local', + clearCacheDescription: + 'Elimina totes les dades emmagatzemades localment, incloent registres d\'aula, historial de xat, caché d\'àudio i configuració de l\'aplicació. Aquesta acció no es pot desfer.', + clearCacheConfirmTitle: 'Esteu segurs que voleu netejar tota la caché?', + clearCacheConfirmDescription: + 'Això eliminarà permanentment totes les dades següents i no es podran recuperar:', + clearCacheConfirmItems: + 'Aules i escenes, Historial de xat, Caché d\'àudio i imatges, Configuració i preferències de l\'aplicació', + clearCacheConfirmInput: 'Escriviu "ELIMINAR" per continuar', + clearCacheConfirmPhrase: 'ELIMINAR', + clearCacheButton: 'Eliminar permanentment totes les dades', + clearCacheSuccess: 'Caché netejada, la pàgina s\'actualitzarà en breu', + clearCacheFailed: 'Error en netejar la caché, torneu-ho a intentar', + // Web Search settings + webSearchSettings: 'Cerca web', + webSearchApiKey: 'Clau API Tavily', + webSearchApiKeyPlaceholder: 'Introduïu la vostra clau API Tavily', + webSearchApiKeyPlaceholderServer: 'Clau del servidor configurada, podeu sobreescriure-la opcionalment', + webSearchApiKeyHint: 'Obteniu una clau API de tavily.com per a la cerca web', + webSearchBaseUrl: 'Base URL', + webSearchServerConfigured: 'La clau API Tavily del costat del servidor està configurada', + optional: 'Opcional', + }, + profile: { + title: 'Perfil', + defaultNickname: 'Estudiant', + chooseAvatar: 'Triar avatar', + uploadAvatar: 'Pujar', + bioPlaceholder: 'Expliqueu-nos alguna cosa de vosaltres — el professor IA personalitzarà les lliçons...', + avatarHint: 'El vostre avatar apareixerà a les discussions i xats de l\'aula', + fileTooLarge: 'Imatge massa gran — trieu-ne una de menys de 5 MB', + invalidFileType: 'Seleccioneu un fitxer d\'imatge', + editTooltip: 'Clic per editar el perfil', + }, + media: { + imageCapability: 'Generació d\'imatges', + imageHint: 'Generar imatges a les diapositives', + videoCapability: 'Generació de vídeo', + videoHint: 'Generar vídeos a les diapositives', + ttsCapability: 'Síntesi de veu', + ttsHint: 'El professor IA parla en veu alta', + asrCapability: 'Reconeixement de veu', + asrHint: 'Entrada de veu per a la discussió', + provider: 'Proveïdor', + model: 'Model', + voice: 'Veu', + speed: 'Velocitat', + language: 'Idioma', + }, +} as const; + export const settingsEnUS = { settings: { title: 'Settings', diff --git a/lib/i18n/stage.ts b/lib/i18n/stage.ts index 0a376ca10..e9bbd6992 100644 --- a/lib/i18n/stage.ts +++ b/lib/i18n/stage.ts @@ -147,6 +147,156 @@ export const stageZhCN = { }, } as const; +export const stageCa = { + stage: { + currentScene: 'Escena actual', + generating: 'Generant...', + paused: 'En pausa', + generationFailed: 'Error en la generació', + confirmSwitchTitle: 'Canviar escena', + confirmSwitchMessage: + 'Hi ha un tema en curs. Canviar d\'escena finalitzarà el tema actual. Esteu segurs?', + generatingNextPage: 'L\'escena s\'està generant, espereu si us plau...', + fullscreen: 'Pantalla completa', + exitFullscreen: 'Sortir de pantalla completa', + }, + whiteboard: { + title: 'Pissarra interactiva', + open: 'Obrir pissarra', + clear: 'Netejar pissarra', + minimize: 'Minimitzar pissarra', + ready: 'La pissarra està llesta', + readyHint: 'Els elements apareixeran aquí quan els afegeixi la IA', + clearSuccess: 'Pissarra netejada correctament', + clearError: 'Error en netejar la pissarra: ', + resetView: 'Restablir vista', + restoreError: 'Error en restaurar la pissarra: ', + history: 'Historial', + restore: 'Restaurar', + noHistory: 'Encara no hi ha historial', + restored: 'Pissarra restaurada', + elementCount: '{count} elements', + }, + quiz: { + title: 'Qüestionari', + subtitle: 'Posa a prova els teus coneixements', + questionsCount: 'preguntes', + totalPrefix: '', + pointsSuffix: 'pts', + startQuiz: 'Iniciar qüestionari', + multipleChoiceHint: '(Resposta múltiple — seleccioneu totes les respostes correctes)', + inputPlaceholder: 'Escriviu la vostra resposta aquí...', + charCount: 'caràcters', + yourAnswer: 'La vostra resposta:', + notAnswered: 'Sense resposta', + aiComment: 'Comentari IA', + singleChoice: 'Única', + multipleChoice: 'Múltiple', + shortAnswer: 'Resposta curta', + analysis: 'Anàlisi: ', + excellent: 'Excel·lent!', + keepGoing: 'Continua!', + needsReview: 'Cal repassar', + correct: 'correcte', + incorrect: 'incorrecte', + answering: 'En curs', + submitAnswers: 'Enviar respostes', + aiGrading: 'La IA està corregint...', + aiGradingWait: 'Espereu, analitzant les vostres respostes', + quizReport: 'Informe del qüestionari', + retry: 'Reintentar', + }, + roundtable: { + teacher: 'PROFESSOR', + you: 'TU', + inputPlaceholder: 'Escriviu el vostre missatge...', + listening: 'Escoltant...', + processing: 'Processant...', + noSpeechDetected: 'No s\'ha detectat veu, torneu-ho a intentar', + discussionEnded: 'Discussió finalitzada', + qaEnded: 'P&R finalitzat', + thinking: 'Pensant', + yourTurn: 'El vostre torn', + stopDiscussion: 'Aturar discussió', + autoPlay: 'Reproducció automàtica', + autoPlayOff: 'Aturar reproducció automàtica', + speed: 'Velocitat', + voiceInput: 'Entrada de veu', + voiceInputDisabled: 'Entrada de veu desactivada', + textInput: 'Entrada de text', + stopRecording: 'Aturar gravació', + startRecording: 'Iniciar gravació', + }, + pbl: { + legacyFormat: 'Aquesta escena PBL utilitza un format antic. Torneu a generar el curs.', + emptyProject: 'El projecte PBL encara no s\'ha generat. Creeu-lo mitjançant la generació de cursos.', + roleSelection: { + title: 'Trieu el vostre rol', + description: 'Seleccioneu un rol per col·laborar en el projecte', + }, + workspace: { + restart: 'Reiniciar', + confirmRestart: 'Restablir tot el progrés?', + confirm: 'Confirmar', + cancel: 'Cancel·lar', + }, + issueboard: { + title: 'Tauler de tasques', + noIssues: 'Encara no hi ha tasques', + statusDone: 'Fet', + statusActive: 'Actiu', + statusPending: 'Pendent', + }, + chat: { + title: 'Discussió del projecte', + currentIssue: 'Tasca actual', + mentionHint: 'Useu @question per preguntar, @judge per enviar a revisió', + placeholder: 'Escriviu un missatge...', + send: 'Enviar', + welcomeMessage: + 'Hola! Soc el vostre Agent de Preguntes per a aquesta tasca: "{title}"\n\nPer guiar el vostre treball, he preparat algunes preguntes:\n\n{questions}\n\nNo dubteu a fer-me @question qualsevol moment si necessiteu ajuda!', + issueCompleteMessage: 'Tasca "{completed}" completada! Passant a la tasca següent: "{next}"', + allCompleteMessage: '🎉 Totes les tasques completades! Bon treball en el projecte!', + }, + guide: { + howItWorks: 'Com funciona', + help: 'Ajuda', + title: 'Ajuda', + step1: { + title: 'Pas 1: Trieu un rol', + desc: 'Després de generar el projecte, seleccioneu un rol de la llista (rols no de sistema marcats amb 🟢)', + }, + step2: { + title: 'Pas 2: Completeu les tasques', + desc: 'Cada tasca representa un objectiu d\'aprenentatge:', + s1: { + title: 'Veieu la tasca actual', + desc: 'Comproveu el títol, la descripció i el responsable de la tasca', + }, + s2: { + title: 'Obteniu orientació', + example: '@question Per on haig de començar?\n@question Com implemento aquesta funcionalitat?', + desc: 'L\'Agent de Preguntes proporciona preguntes orientadores i pistes (sense respostes directes)', + }, + s3: { + title: 'Envieu el vostre treball', + example: '@judge Ja he acabat, comproveu les meves Notes', + desc: 'L\'Agent Avaluador avalua el vostre treball i dona comentaris:', + complete: 'Passa automàticament a la tasca següent', + revision: 'Millora basant-te en els comentaris', + }, + }, + step3: { + title: 'Pas 3: Completeu el projecte', + desc: 'Quan totes les tasques estiguin fetes, el sistema mostra "🎉 Projecte completat!"', + }, + }, + }, + share: { + notReady: 'Disponible quan la generació estigui completa', + }, +} as const; + export const stageEnUS = { stage: { currentScene: 'Current Scene', diff --git a/lib/i18n/types.ts b/lib/i18n/types.ts index 6173b0be3..293100571 100644 --- a/lib/i18n/types.ts +++ b/lib/i18n/types.ts @@ -1,3 +1,5 @@ -export type Locale = 'zh-CN' | 'en-US'; +export type Locale = 'zh-CN' | 'en-US' | 'ca'; -export const defaultLocale: Locale = 'zh-CN'; +export const VALID_LOCALES: Locale[] = ['zh-CN', 'en-US', 'ca']; + +export const defaultLocale: Locale = 'ca'; diff --git a/lib/server/classroom-generation.ts b/lib/server/classroom-generation.ts index 049cc9f30..4b381b0d4 100644 --- a/lib/server/classroom-generation.ts +++ b/lib/server/classroom-generation.ts @@ -98,8 +98,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' | 'ca' { + if (language === 'en-US') return 'en-US'; + if (language === 'ca') return 'ca'; + return 'zh-CN'; } function stripCodeFences(text: string): string { diff --git a/lib/types/generation.ts b/lib/types/generation.ts index c1e6eb7a7..ca1f8ac78 100644 --- a/lib/types/generation.ts +++ b/lib/types/generation.ts @@ -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' | 'ca'; // 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' | 'ca'; // 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' | 'ca'; }; }