feat: certification rules, content sanitization, handle draft course updates and reset content, better redis logging

This commit is contained in:
Rotimi Best
2026-03-30 08:24:55 +01:00
parent ff2ad59377
commit 5a8f50f67b
131 changed files with 5643 additions and 2134 deletions
@@ -21,17 +21,6 @@ function getSiteNameValidation() {
});
}
export const saveCertificateValidation = (fields = {}) => {
const schema = z.object({
description: z.string().max(200, 'course.navItem.certificates.description_error'),
is_certificate_downloadable: z.boolean(),
certificate_theme: z.string()
});
const { error } = schema.safeParse(fields);
return processErrors(error);
};
export const getConfirmPasswordError = ({ password, confirmPassword }) => {
return password > 6 && confirmPassword > 6 && password !== confirmPassword
? `${t.get('validations.confirm_password.not_match')}`
@@ -67,6 +67,10 @@ export class BaseApi {
return typedResult;
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return;
}
console.error(`Error in ${logContext}:`, error);
this.error = getErrMsg(error, `Error ${logContext}:`);
@@ -19,6 +19,39 @@ export const getRequestBaseUrl = () => {
return env.PUBLIC_SERVER_URL;
};
function mergeAbortSignals(...signals: Array<AbortSignal | null | undefined>) {
const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal));
if (activeSignals.length === 0) {
return undefined;
}
if (activeSignals.length === 1) {
return activeSignals[0];
}
const controller = new AbortController();
const abortWithSignal = (signal: AbortSignal) => {
if (controller.signal.aborted) {
return;
}
controller.abort(signal.reason);
};
for (const signal of activeSignals) {
if (signal.aborted) {
abortWithSignal(signal);
break;
}
signal.addEventListener('abort', () => abortWithSignal(signal), { once: true });
}
return controller.signal;
}
class ApiClient {
private config: Required<ApiClientConfig>;
@@ -70,14 +103,14 @@ class ApiClient {
}
// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const timeoutController = new AbortController();
const timeoutId = setTimeout(() => timeoutController.abort(), timeout);
const requestInit: RequestInit = {
...fetchConfig,
headers: isFormData ? undefined : headers,
body: requestBody,
signal: controller.signal
signal: mergeAbortSignals(fetchConfig.signal, timeoutController.signal)
};
let lastError: Error | null = null;
@@ -124,6 +157,10 @@ class ApiClient {
// Handle abort (timeout)
if (error instanceof Error && error.name === 'AbortError') {
if (!timeoutController.signal.aborted) {
throw error;
}
throw new ApiError('Request timeout', 408, 'Request Timeout');
}
@@ -1118,7 +1118,11 @@
"answers_required": "Angiv venligst mindst ét ​​svar, før du sender.",
"checking_submission": "Tjek din indsendelse...",
"answer_required": "Besvar venligst dette spørgsmål, før du fortsætter.",
"instructor_feedback": "Instruktør feedback"
"instructor_feedback": "Instruktør feedback",
"attempt_counter": "Forsøg {current} af {total}",
"previous_try": "Tidligere forsøg",
"next_try": "Næste forsøg",
"try_again": "Prøv igen"
},
"edit_mode": {
"delete_modal": "Slet øvelse",
@@ -1371,7 +1375,11 @@
"not_set": "Ikke indstillet",
"incorrect_badge": "Forkert"
}
}
},
"settings": "Indstillinger",
"settings_allow_multiple": "Tillad flere forsøg",
"settings_allow_multiple_helper": "Studerende kan indsende mere end én gang og gennemgå tidligere forsøg på resultatskærmen.",
"settings_save": "Gem indstillinger"
},
"new_exercise_modal": {
"heading": "Ny øvelse",
@@ -1632,18 +1640,29 @@
"later": "Luk"
}
},
"certificates": {
"progress_hint": "{current} % gennemført ({required} % kræves for at downloade)"
},
"certification": {
"rules_heading": "Certificeringsregler",
"rules_heading": "Regler",
"deadline_label": "Certificeringsfrist",
"deadline_helper": "Studerende skal opfylde fuldførelsesprocenten inden denne dato (lokal tid). Lad være tomt uden deadline.",
"threshold_label": "Minimum fuldførelse (%)",
"threshold_helper": "Kombineret fremskridt i lektion og træning kræves for at opnå certifikatet (0-100). Standard er 100 %.",
"email_message_label": "E-mail med certificering",
"email_message_helper": "Valgfri besked vist i e-mailen, når en elev opfylder kravene. Et link til certifikatsiden er altid inkluderet.",
"final_exercise_label": "Afsluttende øvelse (valgfrit)",
"final_exercise_none": "ingen",
"final_exercise_helper": "Når den er indstillet, skal eleverne mindst opnå minimumscore på denne øvelse (bedste forsøg tæller) ud over fuldførelsesprocenten ovenfor.",
"min_exercise_score_label": "Minimumsscore på sidste øvelse (%)",
"min_exercise_score_helper": "Score er en procentdel af det samlede antal point, der er mulige på den øvelse (0-100).",
"blocker_progress": "Dit kursus fremskridt er {current}%; {required} % er påkrævet.",
"blocker_deadline": "Certificeringsfristen er udløbet.",
"blocker_no_content": "Dette kursus har ingen lektioner eller øvelser at gennemføre.",
"blocker_final_not_submitted": "Indsend den nødvendige øvelse \"{exerciseTitle}\" for at kvalificere dig til certifikatet.",
"blocker_final_pending": "Dit forsøg på \"{exerciseTitle}\" bliver stadig bedømt.",
"blocker_final_score": "Din bedste score på \"{exerciseTitle}\" er {bestPercent}%; {requiredPercent} % er påkrævet.",
"blocker_final_misconfigured": "Certificeringsøvelsen \"{exerciseTitle}\" har ingen point konfigureret; bed din instruktør om at ordne det.",
"email_message_label": "E-mail besked",
"email_message_placeholder": "f.eks. Vi er stolte af dine fremskridt - fortsæt med at lære!"
},
"certificates": {
"progress_hint": "{current} % gennemført ({required} % kræves for at downloade)"
}
},
"courses": {
@@ -1118,7 +1118,11 @@
"answers_required": "Bitte geben Sie vor dem Absenden mindestens eine Antwort an.",
"checking_submission": "Ich prüfe Ihre Einreichung...",
"answer_required": "Bitte beantworten Sie diese Frage, bevor Sie fortfahren.",
"instructor_feedback": "Feedback des Dozenten"
"instructor_feedback": "Feedback des Dozenten",
"attempt_counter": "Versuch {current} von {total}",
"previous_try": "Vorheriger Versuch",
"next_try": "Nächster Versuch",
"try_again": "Versuchen Sie es erneut"
},
"edit_mode": {
"delete_modal": "Übung löschen",
@@ -1371,7 +1375,11 @@
"not_set": "Nicht festgelegt",
"incorrect_badge": "Falsch"
}
}
},
"settings": "Einstellungen",
"settings_allow_multiple": "Erlauben Sie mehrere Versuche",
"settings_allow_multiple_helper": "Studierende können mehr als einmal einreichen und frühere Versuche auf dem Ergebnisbildschirm überprüfen.",
"settings_save": "Einstellungen speichern"
},
"new_exercise_modal": {
"heading": "Neue Übung",
@@ -1632,18 +1640,29 @@
"later": "Schließen"
}
},
"certificates": {
"progress_hint": "{current} % abgeschlossen ({required} % zum Herunterladen erforderlich)"
},
"certification": {
"rules_heading": "Zertifizierungsregeln",
"rules_heading": "Regeln",
"deadline_label": "Zertifizierungsfrist",
"deadline_helper": "Die Schüler müssen den Abschlussprozentsatz vor diesem Datum/Uhrzeit (Ortszeit) erreichen. Für keine Frist leer lassen.",
"threshold_label": "Mindestabschluss (%)",
"threshold_helper": "Kombinierter Unterrichts- und Übungsfortschritt, der zum Erwerb des Zertifikats erforderlich ist (0–100). Der Standardwert ist 100 %.",
"email_message_label": "E-Mail-Nachricht zur Zertifizierung",
"email_message_helper": "Optionale Nachricht, die in der E-Mail angezeigt wird, wenn ein Student die Anforderungen erfüllt. Ein Link zur Zertifikatsseite ist immer enthalten.",
"final_exercise_label": "Abschlussübung (optional)",
"final_exercise_none": "keine",
"final_exercise_helper": "Wenn festgelegt, müssen die Schüler zusätzlich zu der oben genannten Abschlussquote mindestens die Mindestpunktzahl für diese Übung erreichen (bester Versuch zählt).",
"min_exercise_score_label": "Mindestpunktzahl bei der Abschlussübung (%)",
"min_exercise_score_helper": "Die Punktzahl ist ein Prozentsatz der bei dieser Übung möglichen Gesamtpunktzahl (0–100).",
"blocker_progress": "Ihr Kursfortschritt beträgt {current}%; {required}% ist erforderlich.",
"blocker_deadline": "Die Zertifizierungsfrist ist abgelaufen.",
"blocker_no_content": "Für diesen Kurs sind keine Lektionen oder Übungen erforderlich.",
"blocker_final_not_submitted": "Reichen Sie die erforderliche Übung „{exerciseTitle}“ ein, um sich für das Zertifikat zu qualifizieren.",
"blocker_final_pending": "Ihr Versuch bei „{exerciseTitle}“ wird noch bewertet.",
"blocker_final_score": "Ihre beste Punktzahl bei „{exerciseTitle}“ beträgt {bestPercent} %; {requiredPercent}% ist erforderlich.",
"blocker_final_misconfigured": "Für die Zertifizierungsübung „{exerciseTitle}“ sind keine Punkte konfiguriert; Bitten Sie Ihren Lehrer, das Problem zu beheben.",
"email_message_label": "E-Mail-Nachricht",
"email_message_placeholder": "z.B. Wir sind stolz auf Ihre Fortschritte – lernen Sie weiter!"
},
"certificates": {
"progress_hint": "{current} % abgeschlossen ({required} % zum Herunterladen erforderlich)"
}
},
"courses": {
@@ -978,13 +978,13 @@
"progress_hint": "{current}% complete ({required}% required to download)"
},
"certification": {
"rules_heading": "Certification rules",
"rules_heading": "Rules",
"deadline_label": "Certification deadline",
"deadline_helper": "Students must meet the completion percentage before this datetime (local time). Leave empty for no deadline.",
"threshold_label": "Minimum completion (%)",
"threshold_helper": "Combined lesson and exercise progress required to earn the certificate (0–100). Default is 100%.",
"final_exercise_label": "Final exercise (optional)",
"final_exercise_none": "None — no extra exercise score",
"final_exercise_none": "none",
"final_exercise_helper": "When set, students must reach at least the minimum score on this exercise (best attempt counts) in addition to the completion percentage above.",
"min_exercise_score_label": "Minimum score on final exercise (%)",
"min_exercise_score_helper": "Score is a percentage of the total points possible on that exercise (0–100).",
@@ -995,8 +995,7 @@
"blocker_final_pending": "Your attempt on \"{exerciseTitle}\" is still being graded.",
"blocker_final_score": "Your best score on \"{exerciseTitle}\" is {bestPercent}%; {requiredPercent}% is required.",
"blocker_final_misconfigured": "The certification exercise \"{exerciseTitle}\" has no points configured; ask your instructor to fix it.",
"email_message_label": "Certification email message",
"email_message_helper": "Optional message shown in the email when a student meets the requirements. A link to the certificate page is always included.",
"email_message_label": "Email Message",
"email_message_placeholder": "e.g. We are proud of your progress — keep learning!"
},
"completion": {
@@ -1118,7 +1118,11 @@
"answers_required": "Proporcione al menos una respuesta antes de enviar.",
"checking_submission": "Comprobando tu envío...",
"answer_required": "Por favor responda esta pregunta antes de continuar.",
"instructor_feedback": "Comentarios del instructor"
"instructor_feedback": "Comentarios del instructor",
"attempt_counter": "Intento {actual} de {total}",
"previous_try": "Intento anterior",
"next_try": "Próximo intento",
"try_again": "Inténtalo de nuevo"
},
"edit_mode": {
"delete_modal": "Eliminar ejercicio",
@@ -1371,7 +1375,11 @@
"not_set": "No establecido",
"incorrect_badge": "Incorrecto"
}
}
},
"settings": "Configuración",
"settings_allow_multiple": "Permitir múltiples intentos",
"settings_allow_multiple_helper": "Los estudiantes pueden enviar más de una vez y revisar intentos anteriores en la pantalla de resultados.",
"settings_save": "Guardar configuración"
},
"new_exercise_modal": {
"heading": "Nuevo ejercicio",
@@ -1632,18 +1640,29 @@
"later": "Cerrar"
}
},
"certificates": {
"progress_hint": "{current}% completado ({required}% requerido para descargar)"
},
"certification": {
"rules_heading": "Reglas de certificación",
"rules_heading": "Reglas",
"deadline_label": "Fecha límite de certificación",
"deadline_helper": "Los estudiantes deben alcanzar el porcentaje de finalización antes de esta fecha (hora local). Déjelo vacío sin fecha límite.",
"threshold_label": "Finalización mínima (%)",
"threshold_helper": "Progreso combinado de lección y ejercicio requerido para obtener el certificado (0–100). El valor predeterminado es 100%.",
"email_message_label": "Mensaje de correo electrónico de certificación",
"email_message_helper": "Mensaje opcional que se muestra en el correo electrónico cuando un estudiante cumple con los requisitos. Siempre se incluye un enlace a la página del certificado.",
"email_message_placeholder": "p.ej. Estamos orgullosos de tu progreso: ¡sigue aprendiendo!"
},
"certificates": {
"progress_hint": "{current}% completado ({required}% requerido para descargar)"
"final_exercise_label": "Ejercicio final (opcional)",
"final_exercise_none": "ninguno",
"final_exercise_helper": "Cuando se establece, los estudiantes deben alcanzar al menos la puntuación mínima en este ejercicio (el mejor intento cuenta) además del porcentaje de finalización anterior.",
"min_exercise_score_label": "Puntuación mínima en el ejercicio final (%)",
"min_exercise_score_helper": "La puntuación es un porcentaje del total de puntos posibles en ese ejercicio (0-100).",
"blocker_progress": "El progreso de su curso es {current}%; {required}% es obligatorio.",
"blocker_deadline": "La fecha límite de certificación ha pasado.",
"blocker_no_content": "Este curso no tiene lecciones ni ejercicios para completar.",
"blocker_final_not_submitted": "Envíe el ejercicio requerido \"{exerciseTitle}\" para calificar para el certificado.",
"blocker_final_pending": "Su intento en \"{exerciseTitle}\" aún se está calificando.",
"blocker_final_score": "Su mejor puntuación en \"{exerciseTitle}\" es {bestPercent}%; {requiredPercent}% es obligatorio.",
"blocker_final_misconfigured": "El ejercicio de certificación \"{exerciseTitle}\" no tiene puntos configurados; Pídele a tu instructor que lo arregle.",
"email_message_label": "Mensaje de correo electrónico",
"email_message_placeholder": "por ej. Estamos orgullosos de tu progreso: ¡sigue aprendiendo!"
}
},
"courses": {
@@ -1118,7 +1118,11 @@
"answers_required": "Veuillez fournir au moins une réponse avant de soumettre.",
"checking_submission": "Vérification de votre soumission...",
"answer_required": "Veuillez répondre à cette question avant de continuer.",
"instructor_feedback": "Commentaires de l'instructeur"
"instructor_feedback": "Commentaires de l'instructeur",
"attempt_counter": "Tentative {actuelle} de {total}",
"previous_try": "Tentative précédente",
"next_try": "Prochaine tentative",
"try_again": "Réessayez"
},
"edit_mode": {
"delete_modal": "Supprimer l'exercice",
@@ -1371,7 +1375,11 @@
"not_set": "Non défini",
"incorrect_badge": "Incorrect"
}
}
},
"settings": "Paramètres",
"settings_allow_multiple": "Autoriser plusieurs tentatives",
"settings_allow_multiple_helper": "Les étudiants peuvent soumettre plus d'une fois et revoir leurs tentatives antérieures sur l'écran des résultats.",
"settings_save": "Enregistrer les paramètres"
},
"new_exercise_modal": {
"heading": "Nouvel exercice",
@@ -1632,18 +1640,29 @@
"later": "Fermer"
}
},
"certificates": {
"progress_hint": "{current} % terminé ({required} % requis pour le téléchargement)"
},
"certification": {
"rules_heading": "Règles de certification",
"rules_heading": "Règles",
"deadline_label": "Date limite de certification",
"deadline_helper": "Les étudiants doivent atteindre le pourcentage d'achèvement avant cette date/heure (heure locale). Laisser vide sans date limite.",
"threshold_label": "Achèvement minimum (%)",
"threshold_helper": "Progrès combiné des leçons et des exercices requis pour obtenir le certificat (0-100). La valeur par défaut est 100 %.",
"email_message_label": "Courriel de certification",
"email_message_helper": "Message facultatif affiché dans l'e-mail lorsqu'un étudiant répond aux exigences. Un lien vers la page du certificat est toujours inclus.",
"final_exercise_label": "Exercice final (facultatif)",
"final_exercise_none": "aucun",
"final_exercise_helper": "Une fois défini, les élèves doivent atteindre au moins le score minimum pour cet exercice (la meilleure tentative compte) en plus du pourcentage d'achèvement ci-dessus.",
"min_exercise_score_label": "Note minimale à l'exercice final (%)",
"min_exercise_score_helper": "Le score est un pourcentage du total de points possibles sur cet exercice (0 à 100).",
"blocker_progress": "La progression de votre cours est de {current} % ; {obligatoire} % est requis.",
"blocker_deadline": "La date limite de certification est dépassée.",
"blocker_no_content": "Ce cours ne comporte aucune leçon ni exercice à réaliser.",
"blocker_final_not_submitted": "Soumettez l'exercice requis \"{exerciseTitle}\" pour être admissible au certificat.",
"blocker_final_pending": "Votre tentative sur \"{exerciseTitle}\" est toujours en cours d'évaluation.",
"blocker_final_score": "Votre meilleur score sur \"{exerciseTitle}\" est de {bestPercent} % ; {requiredPercent} % est requis.",
"blocker_final_misconfigured": "L'exercice de certification « {exerciseTitle} » n'a aucun point configuré ; demandez à votre instructeur de le réparer.",
"email_message_label": "Message électronique",
"email_message_placeholder": "par ex. Nous sommes fiers de vos progrès – continuez à apprendre !"
},
"certificates": {
"progress_hint": "{current} % terminé ({required} % requis pour le téléchargement)"
}
},
"courses": {
@@ -1118,7 +1118,11 @@
"answers_required": "कृपया सबमिट करने से पहले कम से कम एक उत्तर प्रदान करें।",
"checking_submission": "आपके सबमिशन की जाँच की जा रही है...",
"answer_required": "कृपया आगे बढ़ने से पहले इस प्रश्न का उत्तर दें।",
"instructor_feedback": "प्रशिक्षक प्रतिक्रिया"
"instructor_feedback": "प्रशिक्षक प्रतिक्रिया",
"attempt_counter": "{कुल} का {वर्तमान} प्रयास करें",
"previous_try": "पिछला प्रयास",
"next_try": "अगला प्रयास",
"try_again": "पुनः प्रयास करें"
},
"edit_mode": {
"delete_modal": "व्यायाम हटाएँ",
@@ -1371,7 +1375,11 @@
"not_set": "सेट नहीं",
"incorrect_badge": "गलत"
}
}
},
"settings": "सेटिंग्स",
"settings_allow_multiple": "एकाधिक प्रयासों की अनुमति दें",
"settings_allow_multiple_helper": "छात्र एक से अधिक बार सबमिट कर सकते हैं और परिणाम स्क्रीन पर पिछले प्रयासों की समीक्षा कर सकते हैं।",
"settings_save": "सेटिंग्स सहेजें"
},
"new_exercise_modal": {
"heading": "नया व्यायाम",
@@ -1632,18 +1640,29 @@
"later": "बंद करें"
}
},
"certificates": {
"progress_hint": "{वर्तमान}% पूर्ण (डाउनलोड करने के लिए {आवश्यक}% आवश्यक)"
},
"certification": {
"rules_heading": "प्रमाणन नियम",
"rules_heading": "नियम",
"deadline_label": "प्रमाणीकरण की समय सीमा",
"deadline_helper": "छात्रों को इस दिनांक समय (स्थानीय समय) से पहले पूर्णता प्रतिशत पूरा करना होगा। बिना किसी समय सीमा के खाली छोड़ दें.",
"threshold_label": "न्यूनतम पूर्णता (%)",
"threshold_helper": "प्रमाणपत्र (0-100) अर्जित करने के लिए संयुक्त पाठ और अभ्यास प्रगति की आवश्यकता है। डिफ़ॉल्ट 100% है.",
"email_message_label": "प्रमाणीकरण ईमेल संदेश",
"email_message_helper": "जब कोई छात्र आवश्यकताओं को पूरा करता है तो ईमेल में वैकल्पिक संदेश दिखाया जाता है। प्रमाणपत्र पृष्ठ का एक लिंक हमेशा शामिल किया जाता है।",
"final_exercise_label": "अंतिम अभ्यास (वैकल्पिक)",
"final_exercise_none": "कोई नहीं",
"final_exercise_helper": "निर्धारित होने पर, छात्रों को उपरोक्त पूर्णता प्रतिशत के अलावा इस अभ्यास पर कम से कम न्यूनतम अंक (सर्वोत्तम प्रयास मायने रखता है) तक पहुंचना होगा।",
"min_exercise_score_label": "अंतिम अभ्यास पर न्यूनतम स्कोर (%)",
"min_exercise_score_helper": "स्कोर उस अभ्यास पर संभावित कुल अंकों का एक प्रतिशत है (0-100)।",
"blocker_progress": "आपके पाठ्यक्रम की प्रगति {वर्तमान}% है; {आवश्यक}% आवश्यक है.",
"blocker_deadline": "प्रमाणन की समय सीमा बीत चुकी है.",
"blocker_no_content": "इस पाठ्यक्रम में पूरा करने के लिए कोई पाठ या अभ्यास नहीं है।",
"blocker_final_not_submitted": "प्रमाणपत्र के लिए अर्हता प्राप्त करने के लिए आवश्यक अभ्यास \"{exerciseTitle}\" सबमिट करें।",
"blocker_final_pending": "\"{exerciseTitle}\" पर आपका प्रयास अभी भी वर्गीकृत किया जा रहा है।",
"blocker_final_score": "\"{exerciseTitle}\" पर आपका सर्वोत्तम स्कोर {bestPercent}% है; {requiredPercent}% आवश्यक है.",
"blocker_final_misconfigured": "प्रमाणन अभ्यास \"{exerciseTitle}\" में कोई बिंदु कॉन्फ़िगर नहीं किया गया है; अपने प्रशिक्षक से इसे ठीक करने के लिए कहें।",
"email_message_label": "ईमेल संदेश",
"email_message_placeholder": "जैसे हमें आपकी प्रगति पर गर्व है - सीखते रहें!"
},
"certificates": {
"progress_hint": "{वर्तमान}% पूर्ण (डाउनलोड करने के लिए {आवश्यक}% आवश्यक)"
}
},
"courses": {
@@ -1118,7 +1118,11 @@
"answers_required": "Przed przesłaniem proszę podać co najmniej jedną odpowiedź.",
"checking_submission": "Sprawdzam Twoje zgłoszenie...",
"answer_required": "Zanim przejdziesz dalej, odpowiedz na to pytanie.",
"instructor_feedback": "Informacje zwrotne od instruktora"
"instructor_feedback": "Informacje zwrotne od instruktora",
"attempt_counter": "Próba {bieżąca} z {total}",
"previous_try": "Poprzednia próba",
"next_try": "Następna próba",
"try_again": "Spróbuj ponownie"
},
"edit_mode": {
"delete_modal": "Usuń ćwiczenie",
@@ -1371,7 +1375,11 @@
"not_set": "Nie ustawiono",
"incorrect_badge": "Niepoprawne"
}
}
},
"settings": "Ustawienia",
"settings_allow_multiple": "Zezwalaj na wiele prób",
"settings_allow_multiple_helper": "Uczniowie mogą przesłać więcej niż raz i przejrzeć wcześniejsze próby na ekranie wyników.",
"settings_save": "Zapisz ustawienia"
},
"new_exercise_modal": {
"heading": "Nowe ćwiczenie",
@@ -1632,18 +1640,29 @@
"later": "Zamknij"
}
},
"certificates": {
"progress_hint": "{bieżący}% ukończenia ({required}% wymagane do pobrania)"
},
"certification": {
"rules_heading": "Zasady certyfikacji",
"rules_heading": "Zasady",
"deadline_label": "Termin certyfikacji",
"deadline_helper": "Studenci muszą osiągnąć procent ukończenia przed tą datą (czasu lokalnego). Pozostaw puste bez terminu.",
"threshold_label": "Minimalne ukończenie (%)",
"threshold_helper": "Do uzyskania certyfikatu wymagany jest łączny postęp w lekcjach i ćwiczeniach (0–100). Wartość domyślna to 100%.",
"email_message_label": "Wiadomość e-mail z certyfikatem",
"email_message_helper": "Opcjonalna wiadomość wyświetlana w wiadomości e-mail, gdy uczeń spełni wymagania. Zawsze dołączany jest link do strony z certyfikatem.",
"final_exercise_label": "Ćwiczenie końcowe (opcjonalnie)",
"final_exercise_none": "żaden",
"final_exercise_helper": "Jeśli ta opcja jest ustawiona, uczniowie muszą osiągnąć co najmniej minimalny wynik w tym ćwiczeniu (liczą się najlepsze próby) oprócz powyższego procentu ukończenia.",
"min_exercise_score_label": "Minimalny wynik w ćwiczeniu końcowym (%)",
"min_exercise_score_helper": "Wynik to procent wszystkich punktów możliwych do uzyskania w tym ćwiczeniu (0–100).",
"blocker_progress": "Twój postęp w kursie wynosi {current}%; Wymagany jest {required}%.",
"blocker_deadline": "Termin certyfikacji minął.",
"blocker_no_content": "Ten kurs nie zawiera żadnych lekcji ani ćwiczeń do ukończenia.",
"blocker_final_not_submitted": "Wykonaj wymagane ćwiczenie „{exerciseTitle}”, aby zakwalifikować się do certyfikatu.",
"blocker_final_pending": "Twoje podejście do zadania „{exerciseTitle}” jest nadal oceniane.",
"blocker_final_score": "Twój najlepszy wynik w „{exerciseTitle}” to {bestPercent}%; Wymagany jest {requiredPercent}%.",
"blocker_final_misconfigured": "W ćwiczeniu certyfikacyjnym „{exerciseTitle}” nie skonfigurowano żadnych punktów; poproś instruktora, żeby to naprawił.",
"email_message_label": "Wiadomość e-mail",
"email_message_placeholder": "np. Jesteśmy dumni z Twoich postępów — ucz się dalej!"
},
"certificates": {
"progress_hint": "{bieżący}% ukończenia ({required}% wymagane do pobrania)"
}
},
"courses": {
@@ -1118,7 +1118,11 @@
"answers_required": "Forneça pelo menos uma resposta antes de enviar.",
"checking_submission": "Verificando seu envio...",
"answer_required": "Por favor, responda a esta pergunta antes de prosseguir.",
"instructor_feedback": "Feedback do instrutor"
"instructor_feedback": "Feedback do instrutor",
"attempt_counter": "Tentativa {atual} de {total}",
"previous_try": "Tentativa anterior",
"next_try": "Próxima tentativa",
"try_again": "Tente novamente"
},
"edit_mode": {
"delete_modal": "Excluir exercício",
@@ -1371,7 +1375,11 @@
"not_set": "Não definido",
"incorrect_badge": "Incorreto"
}
}
},
"settings": "Configurações",
"settings_allow_multiple": "Permitir múltiplas tentativas",
"settings_allow_multiple_helper": "Os alunos podem enviar mais de uma vez e revisar as tentativas anteriores na tela de resultados.",
"settings_save": "Salvar configurações"
},
"new_exercise_modal": {
"heading": "Novo exercício",
@@ -1632,18 +1640,29 @@
"later": "Fechar"
}
},
"certificates": {
"progress_hint": "{current}% concluído ({required}% necessário para download)"
},
"certification": {
"rules_heading": "Regras de certificação",
"rules_heading": "Regras",
"deadline_label": "Prazo de certificação",
"deadline_helper": "Os alunos devem atingir o percentual de conclusão antes desta data e hora (horário local). Deixe em branco para não ter prazo.",
"threshold_label": "Conclusão mínima (%)",
"threshold_helper": "Progresso combinado de aulas e exercícios necessário para obter o certificado (0–100). O padrão é 100%.",
"email_message_label": "Mensagem de e-mail de certificação",
"email_message_helper": "Mensagem opcional mostrada no e-mail quando um aluno atende aos requisitos. Um link para a página do certificado está sempre incluído.",
"final_exercise_label": "Exercício final (opcional)",
"final_exercise_none": "nenhum",
"final_exercise_helper": "Quando definido, os alunos devem atingir pelo menos a pontuação mínima neste exercício (melhores tentativas contam), além do percentual de conclusão acima.",
"min_exercise_score_label": "Nota mínima no exercício final (%)",
"min_exercise_score_helper": "A pontuação é uma porcentagem do total de pontos possíveis naquele exercício (0–100).",
"blocker_progress": "O progresso do seu curso é {current}%; {obrigatório}% é obrigatório.",
"blocker_deadline": "O prazo de certificação expirou.",
"blocker_no_content": "Este curso não possui lições ou exercícios para serem concluídos.",
"blocker_final_not_submitted": "Envie o exercício obrigatório \"{exerciseTitle}\" para se qualificar para o certificado.",
"blocker_final_pending": "Sua tentativa em \"{exerciseTitle}\" ainda está sendo avaliada.",
"blocker_final_score": "Sua melhor pontuação em \"{exerciseTitle}\" é {bestPercent}%; {requiredPercent}% é obrigatório.",
"blocker_final_misconfigured": "O exercício de certificação \"{exerciseTitle}\" não possui pontos configurados; peça ao seu instrutor para consertar.",
"email_message_label": "Mensagem de e-mail",
"email_message_placeholder": "por exemplo Estamos orgulhosos do seu progresso – continue aprendendo!"
},
"certificates": {
"progress_hint": "{current}% concluído ({required}% necessário para download)"
}
},
"courses": {
@@ -1118,7 +1118,11 @@
"answers_required": "Перед отправкой дайте хотя бы один ответ.",
"checking_submission": "Проверка вашей заявки...",
"answer_required": "Пожалуйста, ответьте на этот вопрос, прежде чем продолжить.",
"instructor_feedback": "Отзывы инструктора"
"instructor_feedback": "Отзывы инструктора",
"attempt_counter": "Попытка {текущая} из {total}",
"previous_try": "Предыдущая попытка",
"next_try": "Следующая попытка",
"try_again": "Попробуйте еще раз"
},
"edit_mode": {
"delete_modal": "Удалить упражнение",
@@ -1371,7 +1375,11 @@
"not_set": "Не установлено",
"incorrect_badge": "Неправильный"
}
}
},
"settings": "Настройки",
"settings_allow_multiple": "Разрешить несколько попыток",
"settings_allow_multiple_helper": "Учащиеся могут отправлять заявки несколько раз и просматривать предыдущие попытки на экране результатов.",
"settings_save": "Сохранить настройки"
},
"new_exercise_modal": {
"heading": "Новое упражнение",
@@ -1632,18 +1640,29 @@
"later": "Закрыть"
}
},
"certificates": {
"progress_hint": "Выполнено {current}% (для загрузки требуется {required}%)"
},
"certification": {
"rules_heading": "Правила сертификации",
"rules_heading": "Правила",
"deadline_label": "Срок сертификации",
"deadline_helper": "Учащиеся должны достичь процента завершения до этой даты и времени (по местному времени). Оставьте пустым, если срок не установлен.",
"threshold_label": "Минимальное завершение (%)",
"threshold_helper": "Для получения сертификата необходим совмещенный прогресс урока и упражнений (0–100). По умолчанию — 100%.",
"email_message_label": "Сертификационное письмо по электронной почте",
"email_message_helper": "Дополнительное сообщение, отображаемое в электронном письме, когда учащийся соответствует требованиям. Ссылка на страницу сертификата всегда включена.",
"final_exercise_label": "Заключительное упражнение (по желанию)",
"final_exercise_none": "нет",
"final_exercise_helper": "Если этот параметр установлен, учащиеся должны набрать хотя бы минимальный балл за это упражнение (засчитывается лучшая попытка) в дополнение к указанному выше проценту выполнения.",
"min_exercise_score_label": "Минимальный балл за финальное упражнение (%)",
"min_exercise_score_helper": "Оценка представляет собой процент от общего количества баллов, возможных за это упражнение (0–100).",
"blocker_progress": "Ваш прогресс по курсу составляет {current}%; Требуется {required}%.",
"blocker_deadline": "Срок сертификации прошел.",
"blocker_no_content": "В этом курсе нет уроков или упражнений, которые необходимо выполнить.",
"blocker_final_not_submitted": "Чтобы получить сертификат, отправьте необходимое упражнение «{exercisionTitle}».",
"blocker_final_pending": "Ваша попытка выполнения \"{exercisionTitle}\" все еще оценивается.",
"blocker_final_score": "Ваш лучший результат в «{exercisionTitle}» — {bestPercent}%; Требуется {requiredPercent}%.",
"blocker_final_misconfigured": "Для сертификационного упражнения «{exercisionTitle}» не настроены баллы; попросите инструктора это исправить.",
"email_message_label": "Сообщение электронной почты",
"email_message_placeholder": "например Мы гордимся вашим прогрессом — продолжайте учиться!"
},
"certificates": {
"progress_hint": "Выполнено {current}% (для загрузки требуется {required}%)"
}
},
"courses": {
@@ -1118,7 +1118,11 @@
"answers_required": "Vui lòng cung cấp ít nhất một câu trả lời trước khi gửi.",
"checking_submission": "Đang kiểm tra nội dung gửi của bạn...",
"answer_required": "Vui lòng trả lời câu hỏi này trước khi tiếp tục.",
"instructor_feedback": "Phản hồi của giảng viên"
"instructor_feedback": "Phản hồi của giảng viên",
"attempt_counter": "Hãy thử {current} trong tổng số {total}",
"previous_try": "Lần thử trước",
"next_try": "Lần thử tiếp theo",
"try_again": "Thử lại"
},
"edit_mode": {
"delete_modal": "Xóa bài tập",
@@ -1371,7 +1375,11 @@
"not_set": "Chưa đặt",
"incorrect_badge": "sai"
}
}
},
"settings": "Cài đặt",
"settings_allow_multiple": "Cho phép thử nhiều lần",
"settings_allow_multiple_helper": "Học sinh có thể gửi nhiều lần và xem lại các lần thử trước đây trên màn hình kết quả.",
"settings_save": "Lưu cài đặt"
},
"new_exercise_modal": {
"heading": "Bài tập mới",
@@ -1632,18 +1640,29 @@
"later": "Đóng"
}
},
"certificates": {
"progress_hint": "Đã hoàn thành {current}% (cần có {required}% để tải xuống)"
},
"certification": {
"rules_heading": "Quy tắc chứng nhận",
"rules_heading": "Quy tắc",
"deadline_label": "Thời hạn chứng nhận",
"deadline_helper": "Học sinh phải đáp ứng tỷ lệ hoàn thành trước ngày này (giờ địa phương). Để trống không có thời hạn.",
"threshold_label": "Hoàn thành tối thiểu (%)",
"threshold_helper": "Cần có tiến độ kết hợp của bài học và bài tập để đạt được chứng chỉ (0–100). Mặc định là 100%.",
"email_message_label": "Thông báo email chứng nhận",
"email_message_helper": "Thông báo tùy chọn hiển thị trong email khi học sinh đáp ứng yêu cầu. Một liên kết đến trang chứng chỉ luôn được bao gồm.",
"email_message_placeholder": "ví dụ. Chúng tôi tự hào về sự tiến bộ của bạn - hãy tiếp tục học hỏi!"
},
"certificates": {
"progress_hint": "Đã hoàn thành {current}% (cần có {required}% để tải xuống)"
"final_exercise_label": "Bài tập cuối cùng (tùy chọn)",
"final_exercise_none": "không có",
"final_exercise_helper": "Khi được đặt ra, học sinh phải đạt ít nhất số điểm tối thiểu trong bài tập này (tính số lần thử tốt nhất) bên cạnh tỷ lệ hoàn thành ở trên.",
"min_exercise_score_label": "Điểm tối thiểu trong bài tập cuối cùng (%)",
"min_exercise_score_helper": "Điểm là tỷ lệ phần trăm của tổng số điểm có thể đạt được trong bài tập đó (0–100).",
"blocker_progress": "Tiến độ khóa học của bạn là {current}%; {required}% là bắt buộc.",
"blocker_deadline": "Thời hạn chứng nhận đã qua.",
"blocker_no_content": "Khóa học này không có bài học hoặc bài tập nào để hoàn thành.",
"blocker_final_not_submitted": "Gửi bài tập bắt buộc \"{exerciseTitle}\" để đủ điều kiện nhận chứng chỉ.",
"blocker_final_pending": "Nỗ lực của bạn về \"{exerciseTitle}\" vẫn đang được chấm điểm.",
"blocker_final_score": "Điểm cao nhất của bạn trong \"{exerciseTitle}\" là {bestPercent}%; {requiredPercent}% là bắt buộc.",
"blocker_final_misconfigured": "Bài tập chứng nhận \"{exerciseTitle}\" không có cấu hình điểm nào; nhờ người hướng dẫn khắc phục.",
"email_message_label": "Tin nhắn email",
"email_message_placeholder": "ví dụ: Chúng tôi tự hào về sự tiến bộ của bạn - hãy tiếp tục học hỏi!"
}
},
"courses": {