diff --git a/CLAUDE.md b/CLAUDE.md
index 6252da6..038786a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -9,7 +9,7 @@ Code Crispies is an interactive CSS/Tailwind learning platform built with pure J
## Commands
```bash
-npm start # Start dev server at http://localhost:1312
+npm start # Start dev server at http://localhost:1234
npm run build # Production build to dist/
npm run test # Run tests once
npm run test.watch # Run tests in watch mode
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c207a79
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,54 @@
+# Code Crispies - Interactive CSS Learning Platform
+
+.PHONY: help dev build test test-watch test-coverage format clean install deploy
+
+# Default port
+PORT = 1234
+
+help:
+ @echo "Code Crispies - Development Commands"
+ @echo ""
+ @echo "Development:"
+ @echo " make dev - Start dev server (port $(PORT))"
+ @echo " make build - Production build to dist/"
+ @echo ""
+ @echo "Testing:"
+ @echo " make test - Run tests once"
+ @echo " make test-watch - Run tests in watch mode"
+ @echo " make test-coverage - Run tests with coverage"
+ @echo ""
+ @echo "Other:"
+ @echo " make format - Format all source files"
+ @echo " make clean - Remove build artifacts"
+ @echo " make install - Install dependencies"
+
+# Development
+dev:
+ npm start
+
+# Build
+build:
+ npm run build
+
+# Testing
+test:
+ npm run test
+
+test-watch:
+ npm run test.watch
+
+test-coverage:
+ npm run test.coverage
+
+# Formatting
+format:
+ npm run format
+ npm run format.lessons
+
+# Clean
+clean:
+ rm -rf dist/ node_modules/.vite/
+
+# Install
+install:
+ npm install
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..bb86bdc
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,34 @@
+{
+ description = "Code Crispies - Interactive CSS/HTML/Tailwind learning platform";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+
+ outputs = { self, nixpkgs, flake-utils }:
+ flake-utils.lib.eachDefaultSystem (system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in {
+ devShells.default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ nodejs_20
+ nodePackages.npm
+ gnumake
+ ];
+
+ shellHook = ''
+ echo "Code Crispies Development Environment"
+ echo ""
+ echo "Commands:"
+ echo " make dev - Start dev server (port 1234)"
+ echo " make build - Production build"
+ echo " make test - Run tests"
+ echo " make format - Format code"
+ echo ""
+ '';
+ };
+ }
+ );
+}
diff --git a/src/app.de.js b/src/app.de.js
deleted file mode 100644
index c893645..0000000
--- a/src/app.de.js
+++ /dev/null
@@ -1,594 +0,0 @@
-import { LessonEngine } from "./impl/LessonEngine.js";
-import { CodeEditor } from "./impl/CodeEditor.js";
-import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
-import { loadModules } from "./config/lessons.de.js";
-
-// Simplified state - LessonEngine now manages lesson state and progress
-const state = {
- userSettings: {
- disableFeedbackErrors: false
- },
- showExpected: false
-};
-
-// DOM elements - updated for new layout
-const elements = {
- // Header
- menuBtn: document.getElementById("menu-btn"),
- helpBtn: document.getElementById("help-btn"),
-
- // Left panel
- lessonTitle: document.getElementById("lesson-title"),
- lessonDescription: document.getElementById("lesson-description"),
- taskInstruction: document.getElementById("task-instruction"),
- codeInput: document.getElementById("code-input"),
- runBtn: document.getElementById("run-btn"),
- undoBtn: document.getElementById("undo-btn"),
- redoBtn: document.getElementById("redo-btn"),
- resetCodeBtn: document.getElementById("reset-code-btn"),
- hintArea: document.getElementById("hint-area"),
- editorContent: document.querySelector(".editor-content"),
- codeEditor: document.querySelector(".code-editor"),
-
- // Right panel
- previewArea: document.getElementById("preview-area"),
- showExpectedBtn: document.getElementById("show-expected-btn"),
- expectedOverlay: document.getElementById("expected-overlay"),
- previewWrapper: document.querySelector(".preview-wrapper"),
- prevBtn: document.getElementById("prev-btn"),
- nextBtn: document.getElementById("next-btn"),
- levelIndicator: document.getElementById("level-indicator"),
-
- // Sidebar
- sidebarDrawer: document.getElementById("sidebar-drawer"),
- sidebarBackdrop: document.getElementById("sidebar-backdrop"),
- closeSidebar: document.getElementById("close-sidebar"),
- moduleList: document.getElementById("module-list"),
- progressFill: document.getElementById("progress-fill"),
- progressText: document.getElementById("progress-text"),
- resetBtn: document.getElementById("reset-btn"),
- disableFeedbackToggle: document.getElementById("disable-feedback-toggle"),
-
- // Dialogs
- helpDialog: document.getElementById("help-dialog"),
- helpDialogClose: document.getElementById("help-dialog-close"),
- resetDialog: document.getElementById("reset-dialog"),
- resetDialogClose: document.getElementById("reset-dialog-close"),
- cancelReset: document.getElementById("cancel-reset"),
- confirmReset: document.getElementById("confirm-reset")
-};
-
-// Initialize the lesson engine - now the single source of truth
-const lessonEngine = new LessonEngine();
-
-// Code editor instance (initialized later)
-let codeEditor = null;
-let currentMode = "css";
-
-// ================= SIDEBAR FUNCTIONS =================
-
-// Track element that opened sidebar for focus return
-let sidebarTrigger = null;
-
-function openSidebar() {
- // Store trigger element for focus return
- sidebarTrigger = document.activeElement;
-
- elements.sidebarDrawer.classList.add("open");
- elements.sidebarBackdrop.classList.add("visible");
-
- // Move focus to close button for keyboard users
- elements.closeSidebar.focus();
-}
-
-function closeSidebar() {
- elements.sidebarDrawer.classList.remove("open");
- elements.sidebarBackdrop.classList.remove("visible");
-
- // Return focus to trigger element
- if (sidebarTrigger && typeof sidebarTrigger.focus === "function") {
- sidebarTrigger.focus();
- sidebarTrigger = null;
- }
-}
-
-// ================= EXPECTED RESULT TOGGLE =================
-
-function toggleExpectedResult() {
- state.showExpected = !state.showExpected;
-
- if (state.showExpected) {
- elements.expectedOverlay.classList.add("visible");
- elements.showExpectedBtn.textContent = "Lösung ausblenden";
- elements.showExpectedBtn.classList.add("btn-primary");
- } else {
- elements.expectedOverlay.classList.remove("visible");
- elements.showExpectedBtn.textContent = "Lösung zeigen";
- elements.showExpectedBtn.classList.remove("btn-primary");
- }
-}
-
-// ================= HINT SYSTEM =================
-
-function showHint(message, step, total, isSuccess = false) {
- const hintClass = isSuccess ? "hint hint-success" : "hint";
- elements.hintArea.innerHTML = `
-
- ${step}/${total}
- ${message}
-
- `;
-}
-
-function clearHint() {
- elements.hintArea.innerHTML = "";
-}
-
-function showSuccessHint(message) {
- elements.hintArea.innerHTML = `
-
- ✓
- ${message}
-
- `;
-}
-
-// ================= PROGRESS DISPLAY =================
-
-function updateProgressDisplay() {
- const stats = lessonEngine.getProgressStats();
- elements.progressFill.style.width = `${stats.percentComplete}%`;
- elements.progressText.textContent = `${stats.percentComplete}% abgeschlossen (${stats.totalCompleted}/${stats.totalLessons})`;
-}
-
-// ================= USER SETTINGS =================
-
-function loadUserSettings() {
- const savedSettings = localStorage.getItem("codeCrispies.settings");
- if (savedSettings) {
- try {
- const settings = JSON.parse(savedSettings);
- state.userSettings = { ...state.userSettings, ...settings };
- elements.disableFeedbackToggle.checked = !state.userSettings.disableFeedbackErrors;
- } catch (e) {
- console.error("Fehler beim Laden der Einstellungen:", e);
- }
- }
-}
-
-function saveUserSettings() {
- localStorage.setItem("codeCrispies.settings", JSON.stringify(state.userSettings));
-}
-
-// ================= MODULE INITIALIZATION =================
-
-async function initializeModules() {
- try {
- const modules = await loadModules();
- lessonEngine.setModules(modules);
-
- // Use the new renderModuleList function with both callbacks
- renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
-
- // Load saved progress and select appropriate module
- const progressData = lessonEngine.loadUserProgress();
- const lastModuleId = progressData?.lastModuleId;
-
- if (lastModuleId && modules.find((m) => m.id === lastModuleId)) {
- selectModule(lastModuleId);
- } else if (modules.length > 0) {
- selectModule(modules[0].id);
- }
-
- updateProgressDisplay();
- } catch (error) {
- console.error("Module konnten nicht geladen werden:", error);
- elements.lessonDescription.textContent = "Module konnten nicht geladen werden. Bitte Seite neu laden.";
- }
-}
-
-// ================= MODULE/LESSON SELECTION =================
-
-function selectModule(moduleId) {
- const success = lessonEngine.setModuleById(moduleId);
- if (!success) return;
-
- // Update module list UI to highlight the active module
- const moduleItems = elements.moduleList.querySelectorAll(".module-header");
- moduleItems.forEach((item) => {
- item.classList.remove("active");
- if (item.dataset.moduleId === moduleId) {
- item.classList.add("active");
- }
- });
-
- loadCurrentLesson();
- resetSuccessIndicators();
-
- // Close sidebar after selection on mobile
- if (window.innerWidth <= 768) {
- closeSidebar();
- }
-}
-
-function selectLesson(moduleId, lessonIndex) {
- const currentState = lessonEngine.getCurrentState();
- if (!currentState.module || currentState.module.id !== moduleId) {
- lessonEngine.setModuleById(moduleId);
- }
-
- lessonEngine.setLessonByIndex(lessonIndex);
- loadCurrentLesson();
-
- // Close sidebar after selection on mobile
- if (window.innerWidth <= 768) {
- closeSidebar();
- }
-}
-
-// ================= LESSON LOADING =================
-
-function resetSuccessIndicators() {
- elements.codeEditor.classList.remove("success-highlight");
- elements.lessonTitle.classList.remove("success-text");
- elements.nextBtn.classList.remove("success");
- elements.taskInstruction.classList.remove("success-instruction");
- elements.runBtn.classList.remove("success");
- elements.previewWrapper?.classList.remove("matched");
-}
-
-function updateEditorForMode(mode) {
- const editorLabel = document.querySelector(".editor-label");
-
- const modeConfig = {
- html: {
- placeholder: "HTML hier eingeben... Probiere: nav>ul>li*3 dann Tab drücken",
- label: "HTML-Editor",
- cmMode: "html"
- },
- tailwind: {
- placeholder: "Tailwind-Klassen eingeben (z.B. bg-blue-500 text-white p-4)",
- label: "Tailwind-Klassen",
- cmMode: "css"
- },
- css: {
- placeholder: "CSS-Code hier eingeben...",
- label: "CSS-Editor",
- cmMode: "css"
- }
- };
-
- const config = modeConfig[mode] || modeConfig.css;
- if (editorLabel) editorLabel.textContent = config.label;
-
- // Update CodeMirror mode if needed
- if (codeEditor && currentMode !== config.cmMode) {
- currentMode = config.cmMode;
- codeEditor.setMode(config.cmMode);
- }
-}
-
-function loadCurrentLesson() {
- const engineState = lessonEngine.getCurrentState();
-
- if (!engineState.module || !engineState.lesson) {
- return;
- }
-
- const lesson = engineState.lesson;
- const mode = lesson.mode || engineState.module?.mode || "css";
-
- // Update UI based on mode
- updateEditorForMode(mode);
-
- // Reset any success indicators
- resetSuccessIndicators();
-
- // Clear hints
- clearHint();
-
- // Hide expected overlay
- state.showExpected = false;
- elements.expectedOverlay.classList.remove("visible");
- elements.showExpectedBtn.textContent = "Lösung zeigen";
- elements.showExpectedBtn.classList.remove("btn-primary");
-
- // Update UI
- renderLesson(
- elements.lessonTitle,
- elements.lessonDescription,
- elements.taskInstruction,
- elements.previewArea,
- null, // editorPrefix no longer used
- null, // codeInput no longer used (using CodeMirror)
- null, // editorSuffix no longer used
- lesson
- );
-
- // Set user code in CodeMirror
- if (codeEditor) {
- codeEditor.setValue(engineState.userCode);
- }
-
- // Update Run button text based on completion status
- if (engineState.isCompleted) {
- elements.runBtn.innerHTML = '
Erneut anwenden';
-
- // Add completion badge if not present
- if (!document.querySelector(".completion-badge")) {
- const badge = document.createElement("span");
- badge.className = "completion-badge";
- badge.textContent = "Erledigt";
- elements.lessonTitle.appendChild(badge);
- }
- } else {
- elements.runBtn.innerHTML = '
Ausführen';
-
- // Remove completion badge if exists
- const badge = document.querySelector(".completion-badge");
- if (badge) badge.remove();
- }
-
- // Update level indicator
- renderLevelIndicator(elements.levelIndicator, engineState.lessonIndex + 1, engineState.totalLessons);
-
- // Update active lesson in sidebar
- updateActiveLessonInSidebar(engineState.module.id, engineState.lessonIndex);
-
- // Update navigation buttons
- updateNavigationButtons();
-
- // Update progress display
- updateProgressDisplay();
-
- // Focus on the code editor
- if (codeEditor) {
- codeEditor.focus();
- }
-
- // Render the expected/solution preview
- lessonEngine.renderExpectedPreview();
-}
-
-// ================= LIVE PREVIEW =================
-
-let previewTimer = null;
-
-function handleEditorChange(code) {
- if (previewTimer) {
- clearTimeout(previewTimer);
- }
-
- previewTimer = setTimeout(() => {
- runCode();
- }, 800);
-}
-
-// ================= NAVIGATION =================
-
-function updateNavigationButtons() {
- const engineState = lessonEngine.getCurrentState();
-
- elements.prevBtn.disabled = !engineState.canGoPrev;
- elements.nextBtn.disabled = !engineState.canGoNext;
-
- elements.prevBtn.classList.toggle("btn-disabled", !engineState.canGoPrev);
- elements.nextBtn.classList.toggle("btn-disabled", !engineState.canGoNext);
-}
-
-function nextLesson() {
- const success = lessonEngine.nextLesson();
- if (success) {
- loadCurrentLesson();
- }
-}
-
-function prevLesson() {
- const success = lessonEngine.previousLesson();
- if (success) {
- loadCurrentLesson();
- }
-}
-
-// ================= CODE EXECUTION =================
-
-function resetCode() {
- // Reset editor to initial code for current lesson
- lessonEngine.reset();
- const engineState = lessonEngine.getCurrentState();
- if (codeEditor && engineState.lesson) {
- codeEditor.setValue(engineState.lesson.initialCode || "");
- }
- // Clear hints and success indicators
- clearHint();
- resetSuccessIndicators();
-}
-
-function runCode() {
- const userCode = codeEditor ? codeEditor.getValue() : "";
-
- // Rotate the Run button icon
- const runButtonImg = document.querySelector("#run-btn img");
- if (runButtonImg) {
- const currentRotation = parseInt(runButtonImg.style.transform?.match(/\d+/)?.[0] || "0");
- runButtonImg.style.transform = `rotate(${currentRotation + 180}deg)`;
- }
-
- // Apply the code to the preview via LessonEngine
- lessonEngine.applyUserCode(userCode, true);
-
- // Validate code using LessonEngine
- const validationResult = lessonEngine.validateCode();
-
- if (validationResult.isValid) {
- // Show success hint
- showSuccessHint(validationResult.message || "CRISPY! ٩(◕‿◕)۶ Dein Code funktioniert.");
-
- // Update Run button
- elements.runBtn.innerHTML = '
Erneut anwenden';
- elements.runBtn.classList.add("success");
-
- // Add completion badge
- if (!document.querySelector(".completion-badge")) {
- const badge = document.createElement("span");
- badge.className = "completion-badge";
- badge.textContent = "Erledigt";
- elements.lessonTitle.appendChild(badge);
- }
-
- // Add success visual indicators
- elements.codeEditor.classList.add("success-highlight");
- elements.lessonTitle.classList.add("success-text");
- elements.nextBtn.classList.add("success");
- elements.taskInstruction.classList.add("success-instruction");
-
- // Show match animation
- elements.previewWrapper?.classList.add("matched");
- setTimeout(() => {
- elements.previewWrapper?.classList.remove("matched");
- }, 2500);
-
- updateNavigationButtons();
- updateProgressDisplay();
- } else {
- // Reset success indicators
- resetSuccessIndicators();
-
- // Show hint with step progress
- const step = validationResult.validCases + 1;
- const total = validationResult.totalCases;
-
- // Only show hints if enabled
- if (!state.userSettings.disableFeedbackErrors) {
- showHint(validationResult.message || "Weiter versuchen!", step, total);
- }
- }
-}
-
-// ================= DIALOGS =================
-
-function showHelp() {
- elements.helpDialog.showModal();
-}
-
-function closeHelpDialog() {
- elements.helpDialog.close();
-}
-
-function showResetConfirmation() {
- elements.resetDialog.showModal();
-}
-
-function closeResetDialog() {
- elements.resetDialog.close();
-}
-
-function handleResetConfirm() {
- lessonEngine.clearProgress();
- closeResetDialog();
- closeSidebar();
-
- // Reload first module
- const modules = lessonEngine.modules;
- if (modules.length > 0) {
- selectModule(modules[0].id);
- }
-
- updateProgressDisplay();
-}
-
-// ================= INITIALIZATION =================
-
-function initCodeEditor() {
- const container = elements.editorContent;
- if (!container) return;
-
- // Remove the textarea - CodeMirror will replace it
- const textarea = container.querySelector("textarea");
- if (textarea) {
- textarea.remove();
- }
-
- // Initialize CodeMirror
- codeEditor = new CodeEditor(container, {
- mode: currentMode,
- placeholder: "Code hier eingeben...",
- onChange: handleEditorChange
- });
-
- codeEditor.init("");
-}
-
-function init() {
- loadUserSettings();
-
- // Initialize CodeMirror editor
- initCodeEditor();
-
- // Load modules after editor is ready
- initializeModules().catch(console.error);
-
- // Sidebar controls
- elements.menuBtn.addEventListener("click", openSidebar);
- elements.closeSidebar.addEventListener("click", closeSidebar);
- elements.sidebarBackdrop.addEventListener("click", closeSidebar);
-
- // Expected result toggle
- elements.showExpectedBtn.addEventListener("click", toggleExpectedResult);
-
- // Navigation
- elements.prevBtn.addEventListener("click", prevLesson);
- elements.nextBtn.addEventListener("click", nextLesson);
- elements.runBtn.addEventListener("click", runCode);
-
- // Editor tools
- elements.undoBtn.addEventListener("click", () => {
- if (codeEditor) codeEditor.undo();
- });
- elements.redoBtn.addEventListener("click", () => {
- if (codeEditor) codeEditor.redo();
- });
- elements.resetCodeBtn.addEventListener("click", resetCode);
-
- // Dialogs
- elements.helpBtn.addEventListener("click", showHelp);
- elements.helpDialogClose.addEventListener("click", closeHelpDialog);
- elements.helpDialog.addEventListener("click", (e) => {
- if (e.target === elements.helpDialog) closeHelpDialog();
- });
- elements.resetBtn.addEventListener("click", showResetConfirmation);
- elements.resetDialogClose.addEventListener("click", closeResetDialog);
- elements.resetDialog.addEventListener("click", (e) => {
- if (e.target === elements.resetDialog) closeResetDialog();
- });
- elements.cancelReset.addEventListener("click", closeResetDialog);
- elements.confirmReset.addEventListener("click", handleResetConfirm);
-
- // Settings
- elements.disableFeedbackToggle.addEventListener("change", (e) => {
- state.userSettings.disableFeedbackErrors = !e.target.checked;
- saveUserSettings();
- });
-
- // Click on editor content to focus CodeMirror
- elements.editorContent?.addEventListener("click", () => {
- if (codeEditor) codeEditor.focus();
- });
-
- // Keyboard shortcuts
- document.addEventListener("keydown", (e) => {
- // Ctrl+Enter to run code
- if (e.ctrlKey && e.key === "Enter") {
- runCode();
- e.preventDefault();
- }
-
- // Escape to close sidebar (dialogs handle Escape natively)
- if (e.key === "Escape") {
- closeSidebar();
- }
- });
-}
-
-// Start the application
-init();
diff --git a/src/app.js b/src/app.js
index 776d58c..27fa5f9 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2,6 +2,7 @@ import { LessonEngine } from "./impl/LessonEngine.js";
import { CodeEditor } from "./impl/CodeEditor.js";
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
import { loadModules } from "./config/lessons.js";
+import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js";
// Simplified state - LessonEngine now manages lesson state and progress
const state = {
@@ -15,6 +16,7 @@ const state = {
const elements = {
// Header
menuBtn: document.getElementById("menu-btn"),
+ langBtn: document.getElementById("lang-btn"),
helpBtn: document.getElementById("help-btn"),
// Left panel
@@ -100,15 +102,42 @@ function toggleExpectedResult() {
if (state.showExpected) {
elements.expectedOverlay.classList.add("visible");
- elements.showExpectedBtn.textContent = "Hide Expected";
+ elements.showExpectedBtn.textContent = t("hideExpected");
elements.showExpectedBtn.classList.add("btn-primary");
} else {
elements.expectedOverlay.classList.remove("visible");
- elements.showExpectedBtn.textContent = "Show Expected";
+ elements.showExpectedBtn.textContent = t("showExpected");
elements.showExpectedBtn.classList.remove("btn-primary");
}
}
+// ================= LANGUAGE TOGGLE =================
+
+async function toggleLanguage() {
+ const currentLang = getLanguage();
+ const newLang = currentLang === "en" ? "de" : "en";
+ setLanguage(newLang);
+ applyTranslations();
+
+ // Reload lessons in new language
+ const engineState = lessonEngine.getCurrentState();
+ const currentModuleId = engineState.module?.id;
+ const currentLessonIndex = engineState.lessonIndex;
+
+ const modules = await loadModules(newLang);
+ lessonEngine.setModules(modules);
+ renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
+
+ // Restore position in current module/lesson
+ if (currentModuleId) {
+ lessonEngine.setModuleById(currentModuleId);
+ lessonEngine.setLessonByIndex(currentLessonIndex);
+ loadCurrentLesson();
+ }
+
+ updateProgressDisplay();
+}
+
// ================= HINT SYSTEM =================
function showHint(message, step, total, isSuccess = false) {
@@ -139,7 +168,11 @@ function showSuccessHint(message) {
function updateProgressDisplay() {
const stats = lessonEngine.getProgressStats();
elements.progressFill.style.width = `${stats.percentComplete}%`;
- elements.progressText.textContent = `${stats.percentComplete}% Complete (${stats.totalCompleted}/${stats.totalLessons})`;
+ elements.progressText.textContent = t("progressText", {
+ percent: stats.percentComplete,
+ completed: stats.totalCompleted,
+ total: stats.totalLessons
+ });
}
// ================= USER SETTINGS =================
@@ -165,7 +198,7 @@ function saveUserSettings() {
async function initializeModules() {
try {
- const modules = await loadModules();
+ const modules = await loadModules(getLanguage());
lessonEngine.setModules(modules);
// Use the new renderModuleList function with both callbacks
@@ -184,7 +217,7 @@ async function initializeModules() {
updateProgressDisplay();
} catch (error) {
console.error("Failed to load modules:", error);
- elements.lessonDescription.textContent = "Failed to load modules. Please refresh the page.";
+ elements.lessonDescription.textContent = t("failedToLoad");
}
}
@@ -248,7 +281,7 @@ function updateEditorForMode(mode) {
cmMode: "html"
},
tailwind: {
- placeholder: "Enter Tailwind classes (e.g., bg-blue-500 text-white p-4)",
+ placeholder: t("tailwindPlaceholder"),
label: "Tailwind Classes",
cmMode: "css"
},
@@ -296,7 +329,7 @@ function loadCurrentLesson() {
// Hide expected overlay
state.showExpected = false;
elements.expectedOverlay.classList.remove("visible");
- elements.showExpectedBtn.textContent = "Show Expected";
+ elements.showExpectedBtn.textContent = t("showExpected");
elements.showExpectedBtn.classList.remove("btn-primary");
// Update UI
@@ -318,17 +351,17 @@ function loadCurrentLesson() {
// Update Run button text based on completion status
if (engineState.isCompleted) {
- elements.runBtn.innerHTML = '
Re-run';
+ elements.runBtn.querySelector("span").textContent = t("rerun");
// Add completion badge if not present
if (!document.querySelector(".completion-badge")) {
const badge = document.createElement("span");
badge.className = "completion-badge";
- badge.textContent = "Completed";
+ badge.textContent = t("completed");
elements.lessonTitle.appendChild(badge);
}
} else {
- elements.runBtn.innerHTML = '
Run';
+ elements.runBtn.querySelector("span").textContent = t("run");
// Remove completion badge if exists
const badge = document.querySelector(".completion-badge");
@@ -448,17 +481,17 @@ function runCode() {
if (validationResult.isValid) {
// Show success hint
- showSuccessHint(validationResult.message || "CRISPY! ٩(◕‿◕)۶ Your code works correctly.");
+ showSuccessHint(validationResult.message || t("successMessage"));
// Update Run button
- elements.runBtn.innerHTML = '
Re-run';
+ elements.runBtn.querySelector("span").textContent = t("rerun");
elements.runBtn.classList.add("success");
// Add completion badge
if (!document.querySelector(".completion-badge")) {
const badge = document.createElement("span");
badge.className = "completion-badge";
- badge.textContent = "Completed";
+ badge.textContent = t("completed");
elements.lessonTitle.appendChild(badge);
}
@@ -486,7 +519,7 @@ function runCode() {
// Only show hints if enabled
if (!state.userSettings.disableFeedbackErrors) {
- showHint(validationResult.message || "Keep trying!", step, total);
+ showHint(validationResult.message || t("keepTrying"), step, total);
}
}
}
@@ -546,6 +579,9 @@ function initCodeEditor() {
}
function init() {
+ // Initialize i18n before anything else
+ initI18n();
+
loadUserSettings();
// Initialize CodeMirror editor
@@ -559,6 +595,9 @@ function init() {
elements.closeSidebar.addEventListener("click", closeSidebar);
elements.sidebarBackdrop.addEventListener("click", closeSidebar);
+ // Language toggle
+ elements.langBtn.addEventListener("click", toggleLanguage);
+
// Expected result toggle
elements.showExpectedBtn.addEventListener("click", toggleExpectedResult);
diff --git a/src/config/lessons.js b/src/config/lessons.js
index 397bd96..1fc8b8c 100644
--- a/src/config/lessons.js
+++ b/src/config/lessons.js
@@ -1,63 +1,112 @@
/**
* Lesson Config - Functions for loading lesson configurations
+ * Supports English and German lesson content
*/
-// Import lesson configs
-import basicSelectorsConfig from "../../lessons/00-basic-selectors.json";
-import advancedSelectorsConfig from "../../lessons/01-advanced-selectors.json";
-import tailwindConfig from "../../lessons/10-tailwind-basics.json";
-// CSS lessons
-import boxModelConfig from "../../lessons/01-box-model.json";
-import flexboxConfig from "../../lessons/flexbox.json";
-import responsiveConfig from "../../lessons/08-responsive.json";
-import unitsVariablesConfig from "../../lessons/05-units-variables.json";
-import transitionsAnimationsConfig from "../../lessons/06-transitions-animations.json";
-// HTML lessons
-import htmlElementsConfig from "../../lessons/20-html-elements.json";
-import htmlFormsBasicConfig from "../../lessons/21-html-forms-basic.json";
-import htmlFormsValidationConfig from "../../lessons/22-html-forms-validation.json";
-import htmlDetailsSummaryConfig from "../../lessons/23-html-details-summary.json";
-import htmlProgressMeterConfig from "../../lessons/24-html-progress-meter.json";
-import htmlDatalistConfig from "../../lessons/25-html-datalist.json";
-import htmlDataAttributesConfig from "../../lessons/26-html-data-attributes.json";
-import htmlDialogConfig from "../../lessons/27-html-dialog.json";
-import htmlFormsFieldsetConfig from "../../lessons/28-html-forms-fieldset.json";
-import htmlFigureConfig from "../../lessons/29-html-figure.json";
-import htmlTablesConfig from "../../lessons/30-html-tables.json";
-import htmlMarqueeConfig from "../../lessons/31-html-marquee.json";
-import htmlSvgConfig from "../../lessons/32-html-svg.json";
+// English lesson imports
+import basicSelectorsEN from "../../lessons/00-basic-selectors.json";
+import advancedSelectorsEN from "../../lessons/01-advanced-selectors.json";
+import boxModelEN from "../../lessons/01-box-model.json";
+import unitsVariablesEN from "../../lessons/05-units-variables.json";
+import transitionsAnimationsEN from "../../lessons/06-transitions-animations.json";
+import responsiveEN from "../../lessons/08-responsive.json";
+import tailwindEN from "../../lessons/10-tailwind-basics.json";
+import htmlElementsEN from "../../lessons/20-html-elements.json";
+import htmlFormsBasicEN from "../../lessons/21-html-forms-basic.json";
+import htmlFormsValidationEN from "../../lessons/22-html-forms-validation.json";
+import htmlDetailsSummaryEN from "../../lessons/23-html-details-summary.json";
+import htmlProgressMeterEN from "../../lessons/24-html-progress-meter.json";
+import htmlDatalistEN from "../../lessons/25-html-datalist.json";
+import htmlDataAttributesEN from "../../lessons/26-html-data-attributes.json";
+import htmlDialogEN from "../../lessons/27-html-dialog.json";
+import htmlFormsFieldsetEN from "../../lessons/28-html-forms-fieldset.json";
+import htmlFigureEN from "../../lessons/29-html-figure.json";
+import htmlTablesEN from "../../lessons/30-html-tables.json";
+import htmlMarqueeEN from "../../lessons/31-html-marquee.json";
+import htmlSvgEN from "../../lessons/32-html-svg.json";
+import flexboxEN from "../../lessons/flexbox.json";
-// Module store
-const moduleStore = [
- htmlElementsConfig,
- htmlFormsBasicConfig,
- htmlFormsValidationConfig,
- htmlDetailsSummaryConfig,
- htmlProgressMeterConfig,
- htmlDatalistConfig,
- htmlDataAttributesConfig,
- htmlDialogConfig,
- htmlFormsFieldsetConfig,
- htmlFigureConfig,
- htmlTablesConfig,
- htmlMarqueeConfig,
- htmlSvgConfig,
- boxModelConfig,
- flexboxConfig,
- responsiveConfig,
- unitsVariablesConfig,
- transitionsAnimationsConfig,
- basicSelectorsConfig,
- advancedSelectorsConfig,
- tailwindConfig
+// German lesson imports
+import basicSelectorsDE from "../../lessons/de/00-basic-selectors.json";
+import advancedSelectorsDE from "../../lessons/de/01-advanced-selectors.json";
+import boxModelDE from "../../lessons/de/01-box-model.json";
+import unitsVariablesDE from "../../lessons/de/05-units-variables.json";
+import transitionsAnimationsDE from "../../lessons/de/06-transitions-animations.json";
+import responsiveDE from "../../lessons/de/08-responsive.json";
+import tailwindDE from "../../lessons/de/10-tailwind-basics.json";
+import htmlElementsDE from "../../lessons/de/20-html-elements.json";
+import htmlFormsBasicDE from "../../lessons/de/21-html-forms-basic.json";
+import htmlFormsValidationDE from "../../lessons/de/22-html-forms-validation.json";
+import htmlDetailsSummaryDE from "../../lessons/de/23-html-details-summary.json";
+import htmlProgressMeterDE from "../../lessons/de/24-html-progress-meter.json";
+import htmlDatalistDE from "../../lessons/de/25-html-datalist.json";
+import htmlDataAttributesDE from "../../lessons/de/26-html-data-attributes.json";
+import htmlDialogDE from "../../lessons/de/27-html-dialog.json";
+import htmlFormsFieldsetDE from "../../lessons/de/28-html-forms-fieldset.json";
+import htmlFigureDE from "../../lessons/de/29-html-figure.json";
+import htmlTablesDE from "../../lessons/de/30-html-tables.json";
+import htmlMarqueeDE from "../../lessons/de/31-html-marquee.json";
+import htmlSvgDE from "../../lessons/de/32-html-svg.json";
+import flexboxDE from "../../lessons/de/flexbox.json";
+
+// English module store
+const moduleStoreEN = [
+ htmlElementsEN,
+ htmlFormsBasicEN,
+ htmlFormsValidationEN,
+ htmlDetailsSummaryEN,
+ htmlProgressMeterEN,
+ htmlDatalistEN,
+ htmlDataAttributesEN,
+ htmlDialogEN,
+ htmlFormsFieldsetEN,
+ htmlFigureEN,
+ htmlTablesEN,
+ htmlMarqueeEN,
+ htmlSvgEN,
+ boxModelEN,
+ flexboxEN,
+ responsiveEN,
+ unitsVariablesEN,
+ transitionsAnimationsEN,
+ basicSelectorsEN,
+ advancedSelectorsEN,
+ tailwindEN
+];
+
+// German module store
+const moduleStoreDE = [
+ htmlElementsDE,
+ htmlFormsBasicDE,
+ htmlFormsValidationDE,
+ htmlDetailsSummaryDE,
+ htmlProgressMeterDE,
+ htmlDatalistDE,
+ htmlDataAttributesDE,
+ htmlDialogDE,
+ htmlFormsFieldsetDE,
+ htmlFigureDE,
+ htmlTablesDE,
+ htmlMarqueeDE,
+ htmlSvgDE,
+ boxModelDE,
+ flexboxDE,
+ responsiveDE,
+ unitsVariablesDE,
+ transitionsAnimationsDE,
+ basicSelectorsDE,
+ advancedSelectorsDE,
+ tailwindDE
];
/**
- * Load all available modules
+ * Load all available modules for a given language
+ * @param {string} language - Language code ('en' or 'de')
* @returns {Promise} Promise resolving to array of modules
*/
-export async function loadModules() {
- return moduleStore.map((module) => ({
+export async function loadModules(language = "en") {
+ const store = language === "de" ? moduleStoreDE : moduleStoreEN;
+ return store.map((module) => ({
...module,
lessons: module.lessons.map((lesson) => ({
...lesson,
@@ -69,10 +118,12 @@ export async function loadModules() {
/**
* Get a module by its ID
* @param {string} moduleId - The module ID to find
+ * @param {string} language - Language code ('en' or 'de')
* @returns {Object|null} The module object or null if not found
*/
-export function getModuleById(moduleId) {
- return moduleStore.find((module) => module.id === moduleId) || null;
+export function getModuleById(moduleId, language = "en") {
+ const store = language === "de" ? moduleStoreDE : moduleStoreEN;
+ return store.find((module) => module.id === moduleId) || null;
}
/**
@@ -118,20 +169,23 @@ function validateModuleConfig(config) {
/**
* Add a custom module to the store
* @param {Object} moduleConfig - The module configuration to add
+ * @param {string} language - Language code ('en' or 'de')
* @returns {boolean} Success status
*/
-export function addCustomModule(moduleConfig) {
+export function addCustomModule(moduleConfig, language = "en") {
try {
validateModuleConfig(moduleConfig);
+ const store = language === "de" ? moduleStoreDE : moduleStoreEN;
+
// Check if module with same ID already exists
- const existingIndex = moduleStore.findIndex((m) => m.id === moduleConfig.id);
+ const existingIndex = store.findIndex((m) => m.id === moduleConfig.id);
if (existingIndex >= 0) {
// Replace existing module
- moduleStore[existingIndex] = moduleConfig;
+ store[existingIndex] = moduleConfig;
} else {
// Add new module
- moduleStore.push(moduleConfig);
+ store.push(moduleConfig);
}
return true;
diff --git a/src/helpers/renderer.js b/src/helpers/renderer.js
index edfbb33..abcc779 100644
--- a/src/helpers/renderer.js
+++ b/src/helpers/renderer.js
@@ -1,6 +1,7 @@
/**
* Renderer - Handles UI updates for the CSS learning platform
*/
+import { t } from "../i18n.js";
// Feedback elements cache
let feedbackElement = null;
@@ -77,7 +78,7 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes
lessonItem.classList.add("lesson-list-item");
lessonItem.dataset.moduleId = module.id;
lessonItem.dataset.lessonIndex = index;
- lessonItem.textContent = lesson.title || `Lesson ${index + 1}`;
+ lessonItem.textContent = lesson.title || t("lessonFallback", { index: index + 1 });
// Mark lesson as completed if in progress data
if (progress[module.id] && progress[module.id].completed.includes(index)) {
@@ -137,7 +138,7 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes
*/
export function renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl, inputEl, suffixEl, lesson) {
// Set lesson title and description
- titleEl.textContent = lesson.title || "Untitled Lesson";
+ titleEl.textContent = lesson.title || t("untitledLesson");
descriptionEl.innerHTML = lesson.description || "";
// Set task instructions
@@ -162,7 +163,7 @@ export function renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl
* @param {number} total - The total number of levels
*/
export function renderLevelIndicator(element, current, total) {
- element.textContent = `Lesson ${current} of ${total}`;
+ element.textContent = t("levelIndicator", { current, total });
}
/**
diff --git a/src/i18n.js b/src/i18n.js
new file mode 100644
index 0000000..fad4cae
--- /dev/null
+++ b/src/i18n.js
@@ -0,0 +1,287 @@
+/**
+ * Internationalization module for Code Crispies
+ */
+
+const translations = {
+ en: {
+ // Page
+ pageTitle: "CODE CRISPIES - Learn CSS Interactively",
+ skipLink: "Skip to main content",
+
+ // Header
+ menuOpen: "Open menu",
+ langSwitch: "DE",
+ langSwitchLabel: "Sprache wechseln: Deutsch",
+ help: "Help",
+
+ // Instructions
+ loading: "Loading...",
+ selectLesson: "Please select a lesson to begin.",
+ editorLabel: "CSS Editor",
+ undoTitle: "Undo (Ctrl+Z)",
+ redoTitle: "Redo (Ctrl+Shift+Z)",
+ resetCodeTitle: "Reset to initial code",
+ run: "Run",
+ rerun: "Re-run",
+
+ // Preview
+ yourOutput: "Your Output",
+ showExpected: "Show Expected",
+ hideExpected: "Hide Expected",
+ previous: "Previous",
+ next: "Next",
+ levelIndicator: "Lesson {current} of {total}",
+
+ // Sidebar
+ menu: "Menu",
+ closeMenu: "Close menu",
+ progress: "Progress",
+ progressText: "{percent}% Complete ({completed}/{total})",
+ lessons: "Lessons",
+ settings: "Settings",
+ showHints: "Show Hints",
+ resetAllProgress: "Reset All Progress",
+ openSource: "Open Source:",
+ by: "by",
+
+ // Help dialog
+ helpTitle: "Help",
+ aboutTitle: "About Code Crispies",
+ aboutText: "Code Crispies is a free, open-source platform for learning web development through hands-on exercises. No account required - just start coding!",
+ learningModesTitle: "Learning Modes",
+ modeCss: "CSS - Write CSS rules to style elements",
+ modeTailwind: "Tailwind - Apply utility classes directly in HTML",
+ modeHtml: "HTML - Practice semantic markup and native elements",
+ gettingStartedTitle: "Getting Started",
+ gettingStartedText: "Open the menu (☰) to browse lesson modules. Each module covers a specific topic with progressive exercises.",
+ completingLessonsTitle: "Completing Lessons",
+ completingStep1: "Read the task instructions on the left",
+ completingStep2: "Write your code in the editor",
+ completingStep3: "Watch the live preview update as you type",
+ completingStep4: "Follow hints to fix any issues",
+ completingStep5: "Click Next when complete",
+ editorToolsTitle: "Editor Tools",
+ editorToolUndo: "↶ Undo / ↷ Redo - Navigate edit history",
+ editorToolReset: "⟲ Reset - Restore initial code for current lesson",
+ editorToolExpected: "Show Expected - Toggle the target result overlay",
+ keyboardShortcutsTitle: "Keyboard Shortcuts",
+ shortcutRun: "Ctrl+Enter - Validate immediately",
+ shortcutUndo: "Ctrl+Z - Undo",
+ shortcutRedo: "Ctrl+Shift+Z - Redo",
+ emmetTitle: "Emmet Shortcuts (HTML mode)",
+ emmetText: "Type abbreviations and press Tab to expand:",
+ emmetClass: "div.box → div with class",
+ emmetChildren: "ul>li*3 → ul with 3 li children",
+ emmetNested: "form>input+button → nested structure",
+ emmetContent: "p{Hello} → p with text content",
+
+ // Reset dialog
+ resetDialogTitle: "Reset Progress",
+ resetDialogText: "Are you sure you want to reset all your progress? This cannot be undone.",
+ cancel: "Cancel",
+ resetAll: "Reset All",
+
+ // Dynamic content
+ completed: "Completed",
+ successMessage: "CRISPY! ٩(◕‿◕)۶ Your code works correctly.",
+ keepTrying: "Keep trying!",
+ failedToLoad: "Failed to load modules. Please refresh the page.",
+ tailwindPlaceholder: "Enter Tailwind classes (e.g., bg-blue-500 text-white p-4)",
+ lessonFallback: "Lesson {index}",
+ untitledLesson: "Untitled Lesson"
+ },
+
+ de: {
+ // Page
+ pageTitle: "CODE CRISPIES - CSS interaktiv lernen",
+ skipLink: "Zum Hauptinhalt springen",
+
+ // Header
+ menuOpen: "Menü öffnen",
+ langSwitch: "EN",
+ langSwitchLabel: "Switch language: English",
+ help: "Hilfe",
+
+ // Instructions
+ loading: "Laden...",
+ selectLesson: "Bitte wähle eine Lektion aus, um zu beginnen.",
+ editorLabel: "CSS-Editor",
+ undoTitle: "Rückgängig (Strg+Z)",
+ redoTitle: "Wiederholen (Strg+Umschalt+Z)",
+ resetCodeTitle: "Auf Anfangscode zurücksetzen",
+ run: "Ausführen",
+ rerun: "Erneut anwenden",
+
+ // Preview
+ yourOutput: "Deine Ausgabe",
+ showExpected: "Lösung zeigen",
+ hideExpected: "Lösung ausblenden",
+ previous: "Zurück",
+ next: "Weiter",
+ levelIndicator: "Lektion {current} von {total}",
+
+ // Sidebar
+ menu: "Menü",
+ closeMenu: "Menü schließen",
+ progress: "Fortschritt",
+ progressText: "{percent}% abgeschlossen ({completed}/{total})",
+ lessons: "Lektionen",
+ settings: "Einstellungen",
+ showHints: "Hinweise anzeigen",
+ resetAllProgress: "Fortschritt zurücksetzen",
+ openSource: "Open Source:",
+ by: "von",
+
+ // Help dialog
+ helpTitle: "Hilfe",
+ aboutTitle: "Über Code Crispies",
+ aboutText: "Code Crispies ist eine kostenlose Open-Source-Plattform zum Erlernen von Webentwicklung durch praktische Übungen. Kein Konto erforderlich - einfach loslegen!",
+ learningModesTitle: "Lernmodi",
+ modeCss: "CSS - Schreibe CSS-Regeln zum Stylen von Elementen",
+ modeTailwind: "Tailwind - Wende Utility-Klassen direkt im HTML an",
+ modeHtml: "HTML - Übe semantisches Markup und native Elemente",
+ gettingStartedTitle: "Erste Schritte",
+ gettingStartedText: "Öffne das Menü (☰), um Lektionsmodule zu durchsuchen. Jedes Modul behandelt ein Thema mit aufeinander aufbauenden Übungen.",
+ completingLessonsTitle: "Lektionen abschließen",
+ completingStep1: "Lies die Aufgabenstellung auf der linken Seite",
+ completingStep2: "Schreibe deinen Code im Editor",
+ completingStep3: "Beobachte die Live-Vorschau während du tippst",
+ completingStep4: "Folge den Hinweisen, um Fehler zu beheben",
+ completingStep5: "Klicke auf Weiter, wenn du fertig bist",
+ editorToolsTitle: "Editor-Werkzeuge",
+ editorToolUndo: "↶ Rückgängig / ↷ Wiederholen - Bearbeitungsverlauf navigieren",
+ editorToolReset: "⟲ Zurücksetzen - Ursprünglichen Code wiederherstellen",
+ editorToolExpected: "Lösung zeigen - Zielergebnis ein-/ausblenden",
+ keyboardShortcutsTitle: "Tastenkürzel",
+ shortcutRun: "Strg+Enter - Sofort validieren",
+ shortcutUndo: "Strg+Z - Rückgängig",
+ shortcutRedo: "Strg+Umschalt+Z - Wiederholen",
+ emmetTitle: "Emmet-Kürzel (HTML-Modus)",
+ emmetText: "Tippe Abkürzungen und drücke Tab zum Erweitern:",
+ emmetClass: "div.box → div mit Klasse",
+ emmetChildren: "ul>li*3 → ul mit 3 li-Kindern",
+ emmetNested: "form>input+button → verschachtelte Struktur",
+ emmetContent: "p{Hallo} → p mit Textinhalt",
+
+ // Reset dialog
+ resetDialogTitle: "Fortschritt zurücksetzen",
+ resetDialogText: "Bist du sicher, dass du deinen gesamten Fortschritt zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.",
+ cancel: "Abbrechen",
+ resetAll: "Alles zurücksetzen",
+
+ // Dynamic content
+ completed: "Erledigt",
+ successMessage: "CRISPY! ٩(◕‿◕)۶ Dein Code funktioniert.",
+ keepTrying: "Weiter versuchen!",
+ failedToLoad: "Module konnten nicht geladen werden. Bitte Seite neu laden.",
+ tailwindPlaceholder: "Tailwind-Klassen eingeben (z.B. bg-blue-500 text-white p-4)",
+ lessonFallback: "Lektion {index}",
+ untitledLesson: "Unbenannte Lektion"
+ }
+};
+
+let currentLang = "en";
+
+/**
+ * Detect initial language from localStorage or browser
+ */
+export function detectLanguage() {
+ // Check localStorage first
+ const stored = localStorage.getItem("codeCrispies.language");
+ if (stored && translations[stored]) {
+ return stored;
+ }
+
+ // Check browser language
+ const browserLang = navigator.language.split("-")[0];
+ if (translations[browserLang]) {
+ return browserLang;
+ }
+
+ return "en";
+}
+
+/**
+ * Get current language
+ */
+export function getLanguage() {
+ return currentLang;
+}
+
+/**
+ * Set language and persist to localStorage
+ */
+export function setLanguage(lang) {
+ if (!translations[lang]) {
+ console.warn(`Language "${lang}" not supported, falling back to English`);
+ lang = "en";
+ }
+ currentLang = lang;
+ localStorage.setItem("codeCrispies.language", lang);
+ document.documentElement.lang = lang;
+}
+
+/**
+ * Get a translation by key with optional interpolation
+ * @param {string} key - Translation key
+ * @param {Object} params - Optional parameters for interpolation
+ */
+export function t(key, params = {}) {
+ const text = translations[currentLang]?.[key] || translations.en[key] || key;
+
+ if (Object.keys(params).length === 0) {
+ return text;
+ }
+
+ // Replace {param} placeholders
+ return text.replace(/\{(\w+)\}/g, (match, param) => {
+ return params[param] !== undefined ? params[param] : match;
+ });
+}
+
+/**
+ * Apply translations to all elements with data-i18n attribute
+ */
+export function applyTranslations() {
+ // Update page title
+ document.title = t("pageTitle");
+
+ // Update elements with data-i18n attribute (text content)
+ document.querySelectorAll("[data-i18n]").forEach((el) => {
+ const key = el.dataset.i18n;
+ el.textContent = t(key);
+ });
+
+ // Update elements with data-i18n-html attribute (innerHTML)
+ document.querySelectorAll("[data-i18n-html]").forEach((el) => {
+ const key = el.dataset.i18nHtml;
+ el.innerHTML = t(key);
+ });
+
+ // Update elements with data-i18n-title attribute
+ document.querySelectorAll("[data-i18n-title]").forEach((el) => {
+ const key = el.dataset.i18nTitle;
+ el.title = t(key);
+ });
+
+ // Update elements with data-i18n-aria-label attribute
+ document.querySelectorAll("[data-i18n-aria-label]").forEach((el) => {
+ const key = el.dataset.i18nAriaLabel;
+ el.setAttribute("aria-label", t(key));
+ });
+
+ // Update elements with data-i18n-placeholder attribute
+ document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
+ const key = el.dataset.i18nPlaceholder;
+ el.placeholder = t(key);
+ });
+}
+
+/**
+ * Initialize i18n system
+ */
+export function initI18n() {
+ const lang = detectLanguage();
+ setLanguage(lang);
+ applyTranslations();
+}
diff --git a/src/index.de.html b/src/index.de.html
deleted file mode 100644
index 2304aa4..0000000
--- a/src/index.de.html
+++ /dev/null
@@ -1,213 +0,0 @@
-
-
-
-
-
-
- CODE CRISPIES - CSS interaktiv lernen
-
-
-
- Zum Hauptinhalt springen
-
-
-
-
-
-
-
-
-
- Laden...
- Laden...
-
- Bitte wähle eine Lektion aus, um zu beginnen.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Lektion 0/0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/index.html b/src/index.html
index 8f98d39..801e724 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8,11 +8,10 @@
- Skip to main content
+ Skip to main content
-
-
- Loading...
- Loading...
-
+
Loading...
+
Loading...
+
Please select a lesson to begin.
-
-
-
+
@@ -72,24 +65,20 @@
-
+
Level 0/0
-
+
@@ -98,14 +87,14 @@
-