Swedish adaptation: Instegscertifikat and HAREC

- Editions replaced by INSTEG, HAREC and UPPGRADERING (Instegscertifikat -> HAREC)
- Question pool optional; simulator, homework tool and solutions switch off without one
- All UI strings, templates, renderers and project JS localised to sv-SE
  (decimal comma, Swedish non-breaking-space rules, Å/Ä/Ö in Morse table)
- DARC branding, membership CTA and German links removed; attribution footer added
- Page names centralised in src/pages.py
- README and docs rewritten in Swedish

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
2026-10-10 15:03:02 +02:00
co-authored by Claude Sonnet 5.5
parent 28cf11437e
commit 874838f525
81 changed files with 918 additions and 1329 deletions
+60 -84
View File
@@ -6,27 +6,28 @@
var QUESTION_COUNT = 25;
var ANSWER_LABELS = ["A", "B", "C", "D"];
// TODO: Adapt exam parts and passing thresholds once a Swedish question pool exists.
var PART_DEFINITIONS = {
B: { label: "Betriebliche Kenntnisse", minutes: 45 },
V: { label: "Kenntnisse von Vorschriften", minutes: 45 },
N: { label: "Technische Kenntnisse Klasse N", minutes: 45 },
E: { label: "Technische Kenntnisse Klasse E", minutes: 45 },
A: { label: "Technische Kenntnisse Klasse A", minutes: 60 },
H: { label: "Hausaufgabe", minutes: null }
B: { label: "Operativa kunskaper", minutes: 45 },
V: { label: "Kunskaper om föreskrifter", minutes: 45 },
N: { label: "Tekniska kunskaper, nivå N", minutes: 45 },
E: { label: "Tekniska kunskaper, nivå E", minutes: 45 },
A: { label: "Tekniska kunskaper, nivå A", minutes: 60 },
H: { label: "Övning", minutes: null }
};
var EXAM_CHOICES = [
{ id: "N", group: "primary", label: "Klasse N", detail: "B + V + N", parts: ["B", "V", "N"], color: "#47ABE8" },
{ id: "E", group: "primary", label: "Klasse E", detail: "B + V + N + E", parts: ["B", "V", "N", "E"], color: "#FE756C" },
{ id: "A", group: "primary", label: "Klasse A", detail: "B + V + N + E + A", parts: ["B", "V", "N", "E", "A"], color: "#3BB583" },
{ id: "N-E", group: "upgrade", label: "N → E", detail: "Technik E", parts: ["E"], color: "#FE756C" },
{ id: "N-A", group: "upgrade", label: "N → A", detail: "Technik E + A", parts: ["E", "A"], color: "#3BB583" },
{ id: "E-A", group: "upgrade", label: "E → A", detail: "Technik A", parts: ["A"], color: "#3BB583" },
{ id: "part-B", group: "single", label: "B", detail: "Betriebliche Kenntnisse", parts: ["B"], color: "#47ABE8" },
{ id: "part-V", group: "single", label: "V", detail: "Kenntnisse von Vorschriften", parts: ["V"], color: "#47ABE8" },
{ id: "part-N", group: "single", label: "N", detail: "Technik Klasse N", parts: ["N"], color: "#47ABE8" },
{ id: "part-E", group: "single", label: "E", detail: "Technik Klasse E", parts: ["E"], color: "#FE756C" },
{ id: "part-A", group: "single", label: "A", detail: "Technik Klasse A", parts: ["A"], color: "#3BB583" }
{ id: "N", group: "primary", label: "Nivå N", detail: "B + V + N", parts: ["B", "V", "N"], color: "#47ABE8" },
{ id: "E", group: "primary", label: "Nivå E", detail: "B + V + N + E", parts: ["B", "V", "N", "E"], color: "#FE756C" },
{ id: "A", group: "primary", label: "Nivå A", detail: "B + V + N + E + A", parts: ["B", "V", "N", "E", "A"], color: "#3BB583" },
{ id: "N-E", group: "upgrade", label: "N → E", detail: "Teknik E", parts: ["E"], color: "#FE756C" },
{ id: "N-A", group: "upgrade", label: "N → A", detail: "Teknik E + A", parts: ["E", "A"], color: "#3BB583" },
{ id: "E-A", group: "upgrade", label: "E → A", detail: "Teknik A", parts: ["A"], color: "#3BB583" },
{ id: "part-B", group: "single", label: "B", detail: "Operativa kunskaper", parts: ["B"], color: "#47ABE8" },
{ id: "part-V", group: "single", label: "V", detail: "Kunskaper om föreskrifter", parts: ["V"], color: "#47ABE8" },
{ id: "part-N", group: "single", label: "N", detail: "Teknik nivå N", parts: ["N"], color: "#47ABE8" },
{ id: "part-E", group: "single", label: "E", detail: "Teknik nivå E", parts: ["E"], color: "#FE756C" },
{ id: "part-A", group: "single", label: "A", detail: "Teknik nivå A", parts: ["A"], color: "#3BB583" }
];
function queryValue(name) {
@@ -72,7 +73,7 @@
input.select();
var copied = document.execCommand("copy");
input.remove();
return copied ? Promise.resolve() : Promise.reject(new Error("Kopieren nicht möglich"));
return copied ? Promise.resolve() : Promise.reject(new Error("Det gick inte att kopiera"));
}
function shuffled(values) {
@@ -149,7 +150,7 @@
function balancedSample(pool, count) {
if (!Array.isArray(pool) || pool.length < count) {
throw new Error("Für diesen Prüfungsteil stehen nicht genügend Fragen zur Verfügung.");
throw new Error("Det finns inte tillräckligt många frågor för den här provdelen.");
}
return pickFromTree(buildCategoryTree(pool), count);
}
@@ -196,7 +197,7 @@
var currentChoice = computed(function () {
if (!session.value) return null;
if (session.value.mode === "homework") {
return { id: "H", label: "Hausaufgabe", parts: ["H"] };
return { id: "H", label: "Övning", parts: ["H"] };
}
return EXAM_CHOICES.find(function (choice) { return choice.id === session.value.choiceId; });
});
@@ -256,17 +257,17 @@
return { homework: "task_alt", passed: "verified", oral: "record_voice_over", failed: "cancel", incomplete: "stop_circle" }[overallState.value];
});
var overallTitle = computed(function () {
return { homework: "Hausaufgabe ausgewertet", passed: "Prüfung bestanden", oral: "Mündliche Nachprüfung möglich", failed: "Prüfung nicht bestanden", incomplete: "Prüfung vorzeitig beendet" }[overallState.value];
return { homework: "Övningen är rättad", passed: "Provet godkänt", oral: "Muntlig komplettering möjlig", failed: "Provet inte godkänt", incomplete: "Provet avslutades i förtid" }[overallState.value];
});
var overallText = computed(function () {
if (overallState.value === "homework") {
var homeworkPart = session.value.parts[0];
return homeworkPart.score + " von " + homeworkPart.questions.length + " Fragen wurden richtig beantwortet.";
return homeworkPart.score + " av " + homeworkPart.questions.length + " frågor besvarades rätt.";
}
if (overallState.value === "passed") return "Gratulation! Du hast jeden Prüfungsteil mit mindestens 19 Punkten bestanden.";
if (overallState.value === "oral") return "In genau einem Prüfungsteil wurden 17 oder 18 Punkte erreicht; nach den Prüfungsregeln kann eine mündliche Nachprüfung möglich sein.";
if (overallState.value === "failed") return "Mindestens ein Prüfungsteil wurde nicht bestanden.";
return evaluatedParts().length + " von " + session.value.parts.length + " Prüfungsteilen wurden abgeschlossen.";
if (overallState.value === "passed") return "Grattis! Du har klarat varje provdel med minst 19 poäng.";
if (overallState.value === "oral") return "I exakt en provdel nåddes 17 eller 18 poäng; enligt provreglerna kan en muntlig komplettering vara möjlig.";
if (overallState.value === "failed") return "Minst en provdel är inte godkänd.";
return evaluatedParts().length + " av " + session.value.parts.length + " provdelar har slutförts.";
});
function questionData(questionState) {
@@ -331,9 +332,9 @@
function createHomeworkPart(questionIds) {
var questions = questionIds.map(function (questionId) { return catalog.value[questionId]; }).filter(Boolean);
if (questions.length !== questionIds.length) {
throw new Error("Mindestens eine Frage aus dem Hausaufgabenlink ist nicht im aktuellen Fragenkatalog enthalten.");
throw new Error("Minst en fråga i övningslänken finns inte i den aktuella frågebanken.");
}
if (questions.length === 0) throw new Error("Der Hausaufgabenlink enthält keine gültigen Fragen.");
if (questions.length === 0) throw new Error("Övningslänken innehåller inga giltiga frågor.");
return {
code: "H",
status: "pending",
@@ -481,8 +482,8 @@
function requestSubmit() {
var unanswered = currentQuestionCount.value - answeredCount.value;
var text = unanswered > 0
? "Du hast " + unanswered + " Fragen nicht beantwortet. Möchtest du " + (isHomework.value ? "die Hausaufgabe" : "den Prüfungsteil") + " trotzdem abgeben?"
: "Möchtest du " + (isHomework.value ? "die Hausaufgabe" : "diesen Prüfungsteil") + " jetzt verbindlich abgeben?";
? "Du har inte besvarat " + unanswered + " frågor. Vill du lämna in " + (isHomework.value ? "övningen" : "provdelen") + " ändå?"
: "Vill du lämna in " + (isHomework.value ? "övningen" : "den här provdelen") + " nu? Det går inte att ångra.";
if (window.confirm(text)) submitPart(false);
}
@@ -502,7 +503,7 @@
}
function finishExamEarly() {
if (!window.confirm("Möchtest du die Prüfung wirklich hier beenden? Nicht bearbeitete Teile gelten dann als nicht abgeschlossen.")) return;
if (!window.confirm("Vill du verkligen avsluta provet här? Delar som inte har gjorts räknas som ej slutförda.")) return;
showSummary();
}
@@ -540,22 +541,22 @@
}
function abortExam() {
var noun = isHomework.value ? "Hausaufgabe" : "Simulation";
if (!window.confirm("Möchtest du diese " + noun + " abbrechen? Der gespeicherte Stand wird gelöscht.")) return;
var noun = isHomework.value ? "övningen" : "simuleringen";
if (!window.confirm("Vill du avbryta " + noun + "? Det sparade läget raderas.")) return;
resetExam();
window.history.replaceState(null, "", "simulation.html");
window.history.replaceState(null, "", "simulering.html");
}
function newExam() {
resetExam();
window.history.replaceState(null, "", "simulation.html");
window.history.replaceState(null, "", "simulering.html");
}
function retryHomework() {
var questionIds = session.value && session.value.homeworkIds;
if (!questionIds) return;
clearStoredSession();
var homeworkUrl = new URL("simulation.html", window.location.href);
var homeworkUrl = new URL("simulering.html", window.location.href);
homeworkUrl.search = "?homework=" + questionIds.join("+");
window.history.replaceState(null, "", homeworkUrl.toString());
startHomework(questionIds);
@@ -568,10 +569,10 @@
}
function resultLabel(part) {
if (part.code === "H") return "Ausgewertet";
if (part.score >= 19) return "Bestanden";
if (part.score >= 17) return "Nachprüfung möglich";
return "Nicht bestanden";
if (part.code === "H") return "Rättad";
if (part.score >= 19) return "Godkänd";
if (part.score >= 17) return "Komplettering möjlig";
return "Inte godkänd";
}
function resultAlertClass(part) {
@@ -596,11 +597,11 @@
}
function resultDescription(part) {
if (part.code === "H") return part.score + " von " + part.questions.length + " Fragen wurden richtig beantwortet.";
var prefix = part.autoSubmitted ? "Die Zeit ist abgelaufen und der Prüfungsteil wurde automatisch abgegeben. " : "";
if (part.score >= 19) return prefix + "Die erforderlichen 19 Punkte wurden erreicht.";
if (part.score >= 17) return prefix + "Mit 17 oder 18 Punkten kann nach den Prüfungsregeln eine mündliche Nachprüfung möglich sein.";
return prefix + "Die erforderlichen 19 Punkte wurden nicht erreicht.";
if (part.code === "H") return part.score + " av " + part.questions.length + " frågor besvarades rätt.";
var prefix = part.autoSubmitted ? "Tiden gick ut och provdelen lämnades in automatiskt. " : "";
if (part.score >= 19) return prefix + "De nödvändiga 19 poängen uppnåddes.";
if (part.score >= 17) return prefix + "Med 17 eller 18 poäng kan en muntlig komplettering vara möjlig enligt provreglerna.";
return prefix + "De nödvändiga 19 poängen uppnåddes inte.";
}
function encodedResult(result) {
@@ -608,30 +609,6 @@
return "1." + result.choice.id + "." + result.scores.join(".");
}
function resultImageCode(result) {
if (!result) return "";
if (result.kind === "homework") return result.score === result.total ? "aenbv" : "";
if (!result.passed) return "";
var order = ["A", "E", "N", "B", "V"];
return order.filter(function (part) {
return result.choice.parts.indexOf(part) !== -1;
}).join("").toLowerCase();
}
function shareableSessionResult() {
if (!session.value) return null;
if (isHomework.value) {
return { kind: "homework", score: session.value.parts[0].score, total: session.value.parts[0].questions.length };
}
if (session.value.parts.some(function (part) { return part.status !== "evaluated"; })) return null;
return {
kind: "exam",
choice: currentChoice.value,
scores: session.value.parts.map(function (part) { return part.score; }),
passed: overallState.value === "passed"
};
}
var resultForSharing = computed(function () {
if (sharedResult.value) return sharedResult.value;
if (view.value !== "summary") return null;
@@ -640,8 +617,7 @@
function buildResultUrl(result) {
if (!result) return "";
var imageCode = resultImageCode(result);
var url = new URL(imageCode ? "result-" + imageCode + ".html" : "result.html", window.location.href);
var url = new URL("resultat.html", window.location.href);
url.search = "?result=" + encodedResult(result);
return url.toString();
}
@@ -654,19 +630,19 @@
return buildResultUrl(resultForSharing.value);
});
// There are no result images for sharing.
var resultImageUrl = computed(function () {
var imageCode = resultImageCode(resultForSharing.value);
return imageCode ? "assets/images/exam-" + imageCode + ".jpeg" : "";
return "";
});
var shareText = computed(function () {
var result = resultForSharing.value;
if (!result) return "";
if (result.kind === "homework") {
return "Meine 50ohm.de-Hausaufgabe: " + result.score + " von " + result.total + " Fragen richtig.";
return "Min övning på 50Ω: " + result.score + " av " + result.total + " frågor rätt.";
}
var scores = result.choice.parts.map(function (part, index) { return part + ": " + result.scores[index] + "/25"; }).join(", ");
return "Meine 50ohm.de-Prüfungssimulation " + result.choice.label + ": " + scores + " – " + (result.passed ? "bestanden!" : "noch nicht bestanden.");
return "Min provsimulering på 50Ω " + result.choice.label + ": " + scores + " – " + (result.passed ? "godkänd!" : "ännu inte godkänd.");
});
var socialShareLinks = computed(function () {
@@ -681,8 +657,8 @@
function copyResultLink() {
copyText(resultShareUrl.value)
.then(function () { shareMessage.value = "Ergebnislink kopiert."; })
.catch(function () { shareMessage.value = "Der Link konnte nicht automatisch kopiert werden."; });
.then(function () { shareMessage.value = "Resultatlänken kopierades."; })
.catch(function () { shareMessage.value = "Länken kunde inte kopieras automatiskt."; });
}
function shareNative() {
@@ -690,7 +666,7 @@
copyResultLink();
return;
}
navigator.share({ title: "50ohm.de-Ergebnis", text: shareText.value, url: resultShareUrl.value }).catch(function () {});
navigator.share({ title: "Resultat från 50Ω", text: shareText.value, url: resultShareUrl.value }).catch(function () {});
}
function restoreSession(homeworkQuestionIds, allowHomeworkResult) {
@@ -758,24 +734,24 @@
function loadCatalog() {
Promise.all([
fetch("assets/fragenkatalog.json"),
fetch("assets/question_pool.json"),
fetch("assets/metadata.json"),
fetch("assets/question_index.json")
])
.then(function (responses) {
var names = ["Fragenkatalog", "Metadaten", "Fragenindex"];
var names = ["Frågebanken", "Metadata", "Frågeindex"];
responses.forEach(function (response, index) {
if (!response.ok) throw new Error(names[index] + " nicht gefunden (HTTP " + response.status + ").");
if (!response.ok) throw new Error(names[index] + " hittades inte (HTTP " + response.status + ").");
});
return Promise.all(responses.map(function (response) { return response.json(); }));
})
.then(function (data) {
catalog.value = window.FiftyOhmQuestions.normalizeCatalog(data[0], data[1] || {}, data[2] || {});
if (Object.keys(catalog.value).length === 0) throw new Error("Der Fragenkatalog ist leer.");
if (Object.keys(catalog.value).length === 0) throw new Error("Frågebanken är tom.");
var parsedResult = parseSharedResult();
var homeworkQuestionIds = parseHomeworkQuestionIds();
if (queryValue("result")) {
if (!parsedResult) throw new Error("Der Ergebnislink ist ungültig.");
if (!parsedResult) throw new Error("Resultatlänken är ogiltig.");
restoreSession([], true);
var storedResult = shareableSessionResult();
if (storedResult && encodedResult(storedResult) === queryValue("result")) {
@@ -794,7 +770,7 @@
renderMathSoon(Vue);
})
.catch(function (error) {
loadError.value = error.message || "Unbekannter Ladefehler";
loadError.value = error.message || "Okänt fel vid laddning";
loading.value = false;
});
}