From 0570453a25c0e97c6690a6530f9fab380efcc9b6 Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Tue, 30 Dec 2025 15:10:38 +0100 Subject: [PATCH] feat(i18n): add JS-based internationalization - Create i18n.js module with EN/DE translations - Add data-i18n attributes to index.html for dynamic text - Update renderer.js to use translation functions - Language switcher button replaces link to German page - Stores preference in localStorage, detects browser language --- src/helpers/renderer.js | 7 +- src/i18n.js | 287 ++++++++++++++++++++++++++++++++++++++++ src/index.html | 141 +++++++++----------- 3 files changed, 355 insertions(+), 80 deletions(-) create mode 100644 src/i18n.js 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..506b8f2 --- /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: "Click Run or press Ctrl+Enter to test", + 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 - Run your code", + 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: "Klicke auf Ausführen oder drücke Strg+Enter", + 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 - Code ausführen", + 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.html b/src/index.html index 8f98d39..bd1e850 100644 --- a/src/index.html +++ b/src/index.html @@ -8,11 +8,10 @@ - +
-
-
- DE - + +
-
- Loading... -

Loading...

-
+ Loading... +

Loading...

+
Please select a lesson to begin.
-
- -
+
- +
- - - + + +
-
-
- -
+
@@ -72,24 +65,20 @@
- Your Output - + Your Output +
-
- -
+
-
- -
+
- +
Level 0/0
- +
@@ -98,14 +87,14 @@ -