Unlock a World of Learning: Your Interactive Multilingual Educational App Awaits!
Explore Animals, Fruits, Colors, and More in English, Malay, and Arabic with Fun Sounds and Activities!
Dive into an engaging educational experience designed specifically for young learners! This interactive web application brings various categories to life with high-quality visuals, crystal-clear pronunciations in multiple languages, fun sound effects, and activities to reinforce learning. Let's explore what makes this tool special.
Highlights
- Multilingual Mastery: Seamlessly switch between English, Malay, and Arabic, complete with accurate pronunciations using enhanced voice synthesis.
- Interactive Learning: Engage with categories like Animals (with real sounds!), Fruits, Shapes, and more through tappable elements, sound effects, and a fun test mode.
- Personalized Progress: Utilize the star rating system to track familiarity with each item and focus learning efforts effectively.
Embark on a Learning Adventure
This educational platform is crafted to make learning foundational concepts enjoyable and effective for children. It combines visual aids, auditory reinforcement, and interactive elements across numerous categories, fostering curiosity and knowledge retention.
Core Features Explained
Language Immersion
Effortlessly toggle between English, Malay, and Arabic using dedicated language buttons. The entire interface, including text labels and pronunciations, adapts instantly. Arabic content is displayed with correct Right-to-Left (RTL) directionality for an authentic experience.
Categorized Content
Learning is organized into intuitive tabs, covering a wide range of topics:
- Animals (Featuring distinct animal sounds!)
- Fruits
- Vegetables
- Geometric Shapes
- Colors
- Household Tools
- Numbers
- Groceries
- Clothes
Each category presents items as interactive cards containing an image, the word in the selected language, a pronunciation button, a star rating system, and a button for specific sound effects (like animal calls).
Auditory Reinforcement
Hearing is believing! Click the "Hear Pronunciation" button on any card to listen to the word spoken clearly in the selected language, utilizing your browser's built-in enhanced speech synthesis capabilities. You can repeat the pronunciation as many times as needed. Additionally, tapping elements triggers subtle sound effects, and the Animals category includes specific calls for added fun.
Interactive Engagement
- Tap Sounds: Engaging sound effects play upon interaction with cards.
- Star Rating: Children can rate their understanding or liking of each item using a 1-to-5 star system. These ratings are saved locally in the browser.
- Test Mode: Ready to check your knowledge? Activate the Test Mode! It presents questions based on the items learned, providing immediate feedback and a final score.
Child-Friendly Design
The interface uses bright colors, large buttons, clear fonts (like "Comic Sans MS" for a playful feel), and rounded elements, creating a welcoming and easy-to-navigate environment for children.
Feature Availability Across Categories
The table below summarizes the key interactive features available within each learning category:
| Category | Image | Multilingual Word | Pronunciation (TTS) | Specific Sound Effect | Star Rating | Test Mode Inclusion |
|---|---|---|---|---|---|---|
| Animals | Yes | Yes | Yes | Yes (Animal Calls) | Yes | Yes |
| Fruits | Yes | Yes | Yes | No | Yes | Yes |
| Vegetables | Yes | Yes | Yes | No | Yes | Yes |
| Geometric Shapes | Yes | Yes | Yes | No | Yes | Yes |
| Colors | Yes (Color Swatch) | Yes | Yes | No | Yes | Yes |
| Household Tools | Yes | Yes | Yes | No | Yes | Yes |
| Numbers | Yes (Numeral Image) | Yes | Yes | No | Yes | Yes |
| Groceries | Yes | Yes | Yes | No | Yes | Yes |
| Clothes | Yes | Yes | Yes | No | Yes | Yes |
Visualizing the Application's Structure
This mindmap illustrates the hierarchical organization of the educational application, showcasing the relationship between languages, categories, items, and the available interactive features.
Comparing Category Engagement Potential
This chart offers a perspective on the potential engagement levels and feature richness across the different learning categories within the app. Factors considered include the presence of unique sounds (like animal calls), visual distinctiveness, and general appeal to young children.
Exploring Animal Sounds
Animal sounds are a fantastic way to engage children. They add an extra layer of interactivity and fun to the learning process, helping kids associate the visual representation of an animal with its characteristic sound. The video below showcases various animal sounds, similar to the type of audio integrated into the 'Animals' category of this app.
A fun exploration of animal sounds for children.
The Complete Educational App Code
Below is the full HTML, CSS, and JavaScript code required to run this educational web application. Simply save this entire block as an HTML file (e.g., `learning_app.html`) and open it in a modern web browser that supports the Web Speech API (like Chrome, Edge, or Firefox).
Note: This code uses placeholder URLs for images and animal sounds (primarily from Wikimedia Commons and Google's Sound Library for demonstration). For a production version, you would replace these with your own hosted assets. Pronunciation relies on the browser's built-in Text-to-Speech (TTS) capabilities; quality may vary.
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Educational Categories - Multilingual Learning App</title>
<style>
/* Basic Reset & Typography */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Comic Sans MS", cursive, sans-serif; /* Child-friendly font */
background: linear-gradient(to bottom right, #f0f9ff, #e6f2ff); /* Soft gradient background */
color: #333;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 2rem;
}
h1 {
margin: 1.5rem 0 0.5rem 0;
font-size: 2.2rem;
color: #cc9900; /* Main title color */
text-align: center;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
h2 { /* Used for section titles within the app content area */
font-size: 1.6rem;
color: #34495e; /* Darker blue for subtitles */
margin-top: 1rem;
margin-bottom: 0.5rem;
}
/* Language Switcher */
#languageSwitcher {
margin-bottom: 1.5rem;
display: flex;
justify-content: center;
gap: 0.8rem;
flex-wrap: wrap;
}
#languageSwitcher button {
padding: 0.6rem 1.2rem;
font-size: 1rem;
font-weight: bold;
border-radius: 25px;
border: none;
cursor: pointer;
background-color: #3498db; /* Bright blue */
color: white;
box-shadow: 0 4px 8px rgba(52, 152, 219, 0.4);
transition: background-color 0.3s, transform 0.1s;
user-select: none;
}
#languageSwitcher button:active {
transform: scale(0.95);
}
#languageSwitcher button.active,
#languageSwitcher button:hover {
background-color: #2980b9; /* Darker blue */
box-shadow: 0 6px 12px rgba(41, 128, 185, 0.5);
}
/* Tabs Container */
.tabs {
width: 95%;
max-width: 1200px;
background: white;
border-radius: 15px;
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
margin-bottom: 2rem;
}
.tab-buttons {
display: flex;
background: #3498db; /* Bright blue */
flex-wrap: wrap; /* Allow tabs to wrap on smaller screens */
justify-content: center;
}
.tab-buttons button {
flex-grow: 1;
min-width: 100px; /* Ensure buttons have some minimum width */
background: transparent;
border: none;
color: white;
padding: 0.8rem 0.5rem;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
transition: background-color 0.3s;
border-bottom: 4px solid transparent;
text-align: center;
user-select: none;
}
.tab-buttons button.active {
background-color: #2980b9; /* Darker blue */
border-bottom: 4px solid #f39c12; /* Orange highlight */
}
.tab-buttons button:hover:not(.active) {
background-color: #5dade2; /* Lighter blue on hover */
}
/* Tab Content */
.tab-content {
padding: 1.5rem 2rem;
max-height: 70vh; /* Limit height and allow scrolling */
overflow-y: auto;
}
/* Category Grid */
.category-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); /* Responsive grid */
gap: 1.5rem;
margin-top: 1rem;
}
/* Individual card styling */
.card {
background: #f0f9ff; /* Very light blue card background */
border-radius: 12px;
border: 1px solid #bde0fe; /* Light blue border */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 15px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 280px; /* Fixed height for consistency */
position: relative;
user-select: none;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
}
.card:active {
transform: scale(0.97);
}
.card img {
max-width: 100%;
height: 120px;
object-fit: contain;
margin-bottom: 10px;
border-radius: 10px;
border: 3px solid #3498db; /* Blue border */
background: white;
padding: 3px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer; /* Indicate tappable image for sound */
}
.card .word {
margin: 0.5rem 0 0.5rem 0;
font-size: 1.2rem;
font-weight: 700;
color: #2c3e50; /* Dark slate blue text */
min-height: 45px; /* Ensure space for multi-line words */
line-height: 1.3;
display: flex; /* Center align text vertically */
align-items: center;
justify-content: center;
}
/* Pronounce button */
.btn-pronounce {
background-color: #ff9800; /* Orange */
border: none;
border-radius: 20px;
color: white;
padding: 0.6rem 1rem;
font-weight: bold;
font-size: 0.95rem;
cursor: pointer;
margin-top: 10px; /* Ensure space above */
transition: background-color 0.3s ease, transform 0.1s;
user-select: none;
}
.btn-pronounce:hover {
background-color: #e68900; /* Darker orange */
}
.btn-pronounce:active {
transform: scale(0.95);
}
.btn-pronounce:disabled {
background-color: #ffcc80; /* Lighter orange when disabled */
cursor: not-allowed;
}
/* Star Rating & Sound effect container */
.interaction-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.8rem;
margin-bottom: 0.5rem; /* Space before pronounce button */
padding: 0 5px; /* Slight padding */
}
.stars {
display: flex;
gap: 0.2rem;
cursor: pointer;
user-select: none;
}
.star {
font-size: 1.3rem;
color: #ccc; /* Default grey stars */
transition: color 0.2s, transform 0.1s;
}
.star:hover {
transform: scale(1.2);
}
.star.filled {
color: #f39c12; /* Gold filled stars */
}
/* Sound effect icon */
.sound-effect-icon {
font-size: 1.6rem;
color: #2ecc71; /* Green */
user-select: none;
background: none;
border: none;
cursor: pointer;
padding: 0;
transition: transform 0.1s;
}
.sound-effect-icon:hover:not(:disabled) {
transform: scale(1.15);
}
.sound-effect-icon:disabled {
color: #a5d6a7; /* Lighter green when disabled */
cursor: not-allowed;
}
.sound-effect-icon:active:not(:disabled) {
transform: scale(0.9);
}
/* Test mode area */
#testModeSection {
width: 95%;
max-width: 800px;
background: #e0f7fa; /* Light cyan background */
border-radius: 15px;
padding: 1.5rem 2rem;
border: 1px solid #b2ebf2; /* Cyan border */
box-shadow: 0 5px 15px rgba(0, 188, 212, 0.2);
margin: 2rem auto; /* Center it below tabs */
user-select: none;
}
#testModeSection h2 {
text-align: center;
color: #00796b; /* Teal color */
margin-bottom: 1rem;
font-weight: 700;
font-size: 1.8rem;
}
#testQuestion {
font-size: 1.4rem;
margin-bottom: 1.5rem;
text-align: center;
font-weight: 600;
color: #37474f; /* Blue grey */
min-height: 40px;
}
#testOptions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
justify-items: center;
margin-bottom: 1.5rem;
}
#testOptions button {
background-color: #4fc3f7; /* Light blue options */
border-radius: 10px;
border: none;
padding: 0.8rem 1.2rem;
color: white;
font-weight: 600;
font-size: 1.1rem;
cursor: pointer;
box-shadow: 0 4px 7px rgba(79, 195, 247, 0.4);
transition: background-color 0.3s ease, transform 0.1s;
user-select: none;
width: 100%; /* Make buttons fill grid cell */
}
#testOptions button:hover {
background-color: #29b6f6; /* Slightly darker blue */
}
#testOptions button:active {
transform: scale(0.95);
}
#testResult {
text-align: center;
font-weight: 700;
font-size: 1.4rem;
margin-top: 1rem;
min-height: 30px;
user-select: none;
}
#testResult.correct {
color: #4caf50; /* Green for correct */
}
#testResult.wrong {
color: #f44336; /* Red for wrong */
}
.test-controls {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
#startTestBtn, #nextTestBtn, #resetTestBtn {
padding: 0.7rem 1.5rem;
font-size: 1.1rem;
font-weight: 700;
border: none;
border-radius: 30px;
color: white;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0,0,0, 0.2);
transition: background-color 0.3s, transform 0.1s;
user-select: none;
}
#startTestBtn:active, #nextTestBtn:active, #resetTestBtn:active {
transform: scale(0.95);
}
#startTestBtn { background-color: #4caf50; /* Green */ }
#startTestBtn:hover { background-color: #388e3c; }
#nextTestBtn { background-color: #ff9800; /* Orange */ }
#nextTestBtn:hover { background-color: #f57c00; }
#resetTestBtn { background-color: #f44336; /* Red */ }
#resetTestBtn:hover { background-color: #d32f2f; }
/* Scrollbar style for tab content */
.tab-content::-webkit-scrollbar { width: 10px; }
.tab-content::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 5px; }
.tab-content::-webkit-scrollbar-thumb { background: #3498db; border-radius: 5px; }
.tab-content::-webkit-scrollbar-thumb:hover { background: #2980b9; }
/* Responsive adjustments */
@media (max-width: 768px) {
h1 { font-size: 1.8rem; }
.tab-buttons button { font-size: 0.9rem; padding: 0.7rem 0.4rem; }
.category-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 1rem;}
.card { min-height: 250px; }
.card img { height: 100px; }
.card .word { font-size: 1.1rem; min-height: 40px;}
.btn-pronounce { font-size: 0.9rem; padding: 0.5rem 0.8rem;}
.star { font-size: 1.1rem; }
.sound-effect-icon { font-size: 1.4rem; }
#testOptions { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
#testOptions button { font-size: 1rem; padding: 0.7rem 1rem; }
}
@media (max-width: 480px) {
body { padding-bottom: 1rem; }
h1 { font-size: 1.6rem; }
#languageSwitcher { gap: 0.5rem; }
#languageSwitcher button { padding: 0.5rem 1rem; font-size: 0.9rem; }
.tabs { width: 100%; border-radius: 0; margin-bottom: 1rem;}
.tab-buttons button { font-size: 0.8rem; padding: 0.6rem 0.3rem; min-width: 80px;}
.tab-content { padding: 1rem 1rem; max-height: none; overflow-y: visible;}
.category-grid { grid-template-columns: repeat(2, 1fr); gap: 0.8rem;}
.card { min-height: 230px; padding: 10px;}
.card img { height: 80px; }
.card .word { font-size: 1rem; }
.interaction-row { margin-top: 0.5rem; }
.btn-pronounce { margin-top: 8px;}
#testModeSection { width: 100%; border-radius: 0; padding: 1rem; }
#testQuestion { font-size: 1.2rem; }
#testOptions { grid-template-columns: repeat(2, 1fr); gap: 0.5rem; }
#testOptions button { font-size: 0.9rem; padding: 0.6rem 0.8rem; }
.test-controls button { font-size: 1rem; padding: 0.6rem 1.2rem; }
}
</style>
</head>
<body>
<h1 id="appTitle">Educational Categories</h1>
<nav id="languageSwitcher" aria-label="Language Selection">
<button data-lang="en" class="active" aria-pressed="true" title="Switch to English">English</button>
<button data-lang="ms" aria-pressed="false" title="Switch to Malay">Malay</button>
<button data-lang="ar" aria-pressed="false" title="Switch to Arabic">Arabic</button>
</nav>
<div class="tabs" role="tablist" aria-label="Educational Categories Tabs">
<div class="tab-buttons" id="tabButtons">
<button data-tab="animals" role="tab" aria-selected="true" tabindex="0">Animals</button>
<button data-tab="fruits" role="tab" aria-selected="false" tabindex="-1">Fruits</button>
<button data-tab="vegetables" role="tab" aria-selected="false" tabindex="-1">Vegetables</button>
<button data-tab="shapes" role="tab" aria-selected="false" tabindex="-1">Shapes</button>
<button data-tab="colors" role="tab" aria-selected="false" tabindex="-1">Colors</button>
<button data-tab="tools" role="tab" aria-selected="false" tabindex="-1">Tools</button>
<button data-tab="numbers" role="tab" aria-selected="false" tabindex="-1">Numbers</button>
<button data-tab="groceries" role="tab" aria-selected="false" tabindex="-1">Groceries</button>
<button data-tab="clothes" role="tab" aria-selected="false" tabindex="-1">Clothes</button>
</div>
<section class="tab-content" id="tabContent" tabindex="0" aria-live="polite" aria-atomic="true">
<!-- Category content dynamically inserted here -->
<h2 style="text-align: center;">Loading...</h2>
</section>
</div>
<!-- Test Mode Section -->
<section id="testModeSection" aria-label="Test Your Learning">
<h2 id="testTitle">Test Mode</h2>
<p id="testQuestion" aria-live="polite" aria-atomic="true">Click 'Start Test' to begin!</p>
<div id="testOptions" role="list"></div>
<div id="testResult" aria-live="assertive"></div>
<div class="test-controls">
<button id="startTestBtn" aria-label="Start Test">Start Test</button>
<button id="nextTestBtn" aria-label="Next Question" hidden>Next</button>
<button id="resetTestBtn" aria-label="Reset Test" hidden>Reset Test</button>
</div>
</section>
<!-- Audio element for tap sound effects -->
<audio id="tapSound" src="https://actions.google.com/sounds/v1/ui/tap_start.ogg" preload="auto"></audio>
<script>
// Data Structure
const categories = {
animals: [
{id:"lion", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Lion_waiting_in_Namibia.jpg/640px-Lion_waiting_in_Namibia.jpg",
word: { en: "Lion", ms: "Singa", ar: "أسد" },
soundEffect: "https://actions.google.com/sounds/v1/animals/lion_roar.ogg"
},
{id:"elephant", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/6/63/African_elephant_warning_raised_trunk.jpg/640px-African_elephant_warning_raised_trunk.jpg",
word: { en: "Elephant", ms: "Gajah", ar: "فيل" },
soundEffect: "https://actions.google.com/sounds/v1/animals/elephant_trumpeting.ogg"
},
{id:"dog", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Black_and_tan_coonhound.jpg/640px-Black_and_tan_coonhound.jpg",
word: { en: "Dog", ms: "Anjing", ar: "كلب" },
soundEffect: "https://actions.google.com/sounds/v1/animals/dog_barking.ogg"
},
{id:"cat", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/640px-Cat_November_2010-1a.jpg",
word: { en: "Cat", ms: "Kucing", ar: "قطة" },
soundEffect: "https://actions.google.com/sounds/v1/animals/cat_meow.ogg"
},
{id:"bird", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/1/1d/Sparrow_in_flight_-_howardwebb.jpg/640px-Sparrow_in_flight_-_howardwebb.jpg",
word: { en: "Bird", ms: "Burung", ar: "طائر" },
soundEffect: "https://actions.google.com/sounds/v1/animals/bird_chirping.ogg"
}
],
fruits: [
{id:"apple", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Red_Apple.jpg/640px-Red_Apple.jpg",
word: { en: "Apple", ms: "Epal", ar: "تفاحة" },
soundEffect: ""
},
{id:"banana", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/Banana-Single.jpg/640px-Banana-Single.jpg",
word: { en: "Banana", ms: "Pisang", ar: "موز" },
soundEffect: ""
},
{id:"orange", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c4/Orange-Fruit-Pieces.jpg/640px-Orange-Fruit-Pieces.jpg",
word: { en: "Orange", ms: "Oren", ar: "برتقال" },
soundEffect: ""
},
{id:"grape", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Table_grapes_on_white.jpg/640px-Table_grapes_on_white.jpg",
word: { en: "Grape", ms: "Anggur", ar: "عنب" },
soundEffect: ""
},
{id:"pineapple", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Pineapple_and_cross_section.jpg/640px-Pineapple_and_cross_section.jpg",
word: { en: "Pineapple", ms: "Nanas", ar: "أناناس" },
soundEffect: ""
}
],
vegetables: [
{id:"carrot", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a2/Vegetable-Carrot-Bundle-wStalks.jpg/640px-Vegetable-Carrot-Bundle-wStalks.jpg",
word: { en: "Carrot", ms: "Lobak Merah", ar: "جزر" },
soundEffect: ""
},
{id:"tomato", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Bright_red_tomato_and_cross_section02.jpg/640px-Bright_red_tomato_and_cross_section02.jpg",
word: { en: "Tomato", ms: "Tomato", ar: "طماطم" },
soundEffect: ""
},
{id:"potato", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Patates.jpg/640px-Patates.jpg",
word: { en: "Potato", ms: "Kentang", ar: "بطاطس" },
soundEffect: ""
},
{id:"cabbage", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Cabbage_Bunch.jpg/640px-Cabbage_Bunch.jpg",
word: { en: "Cabbage", ms: "Kubis", ar: "ملفوف" },
soundEffect: ""
},
{id:"cucumber", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/ARS_cucumber.jpg/640px-ARS_cucumber.jpg",
word: { en: "Cucumber", ms: "Timun", ar: "خيار" },
soundEffect: ""
}
],
shapes: [
{id:"circle", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Circle_-_black_simple.svg/640px-Circle_-_black_simple.svg.png",
word: { en: "Circle", ms: "Bulatan", ar: "دائرة" },
soundEffect: ""
},
{id:"square", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/c/ca/Solid_black.svg/640px-Solid_black.svg.png", // Using a solid black square
word: { en: "Square", ms: "Segi Empat Sama", ar: "مربع" },
soundEffect: ""
},
{id:"triangle", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Black_triangle.svg/640px-Black_triangle.svg.png",
word: { en: "Triangle", ms: "Segitiga", ar: "مثلث" },
soundEffect: ""
},
{id:"rectangle", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Rectangle_example.svg/640px-Rectangle_example.svg.png",
word: { en: "Rectangle", ms: "Segi Empat Tepat", ar: "مستطيل" },
soundEffect: ""
},
{id:"star", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c7/Star_icon_black.svg/640px-Star_icon_black.svg.png",
word: { en: "Star", ms: "Bintang", ar: "نجمة" },
soundEffect: ""
}
],
colors: [
{id:"red", img:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAABlBMVEUAAAD/AAAAAAC/83XeAAAAAXRSTlMAQObYZgAAABVJREFUeNpjYBgFo2AUjIJRAAAAcgAABiAxQC0AAAAASUVORK5CYII=", // Base64 Red Square
word: { en: "Red", ms: "Merah", ar: "أحمر" },
soundEffect: ""
},
{id:"green", img:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAABlBMVEUAAAD/AAAAAP89mcUAAAAAXRSTlMAQObYZgAAABVJREFUeNpjYBgFo2AUjIJRAAAAcgAABiAxQC0AAAAASUVORK5CYII=", // Base64 Green Square
word: { en: "Green", ms: "Hijau", ar: "أخضر" },
soundEffect: ""
},
{id:"blue", img:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAABlBMVEUAAAAAAP+3TXi/AAAAAXRSTlMAQObYZgAAABVJREFUeNpjYBgFo2AUjIJRAAAAcgAABiAxQC0AAAAASUVORK5CYII=", // Base64 Blue Square
word: { en: "Blue", ms: "Biru", ar: "أزرق" },
soundEffect: ""
},
{id:"yellow", img:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAABlBMVEUAAAD///8AAABkPJOAAAAAAXRSTlMAQObYZgAAABVJREFUeNpjYBgFo2AUjIJRAAAAcgAABiAxQC0AAAAASUVORK5CYII=", // Base64 Yellow Square
word: { en: "Yellow", ms: "Kuning", ar: "أصفر" },
soundEffect: ""
},
{id:"purple", img:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAABlBMVEUAAAD/AAD/oV64AAAAAXRSTlMAQObYZgAAABVJREFUeNpjYBgFo2AUjIJRAAAAcgAABiAxQC0AAAAASUVORK5CYII=", // Base64 Purple Square
word: { en: "Purple", ms: "Ungu", ar: "أرجواني" }, // Or بنفسجي
soundEffect: ""
}
],
tools: [
{id:"hammer", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Hammer_clip_art.svg/640px-Hammer_clip_art.svg.png",
word: { en: "Hammer", ms: "Tukul", ar: "مطرقة" },
soundEffect: ""
},
{id:"screwdriver", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Screwdriver_clip_art.svg/640px-Screwdriver_clip_art.svg.png",
word: { en: "Screwdriver", ms: "Pemutar Skru", ar: "مفك براغي" },
soundEffect: ""
},
{id:"wrench", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Wrench_clip_art.svg/640px-Wrench_clip_art.svg.png",
word: { en: "Wrench", ms: "Sepana", ar: "مفتاح ربط" },
soundEffect: ""
},
{id:"pliers", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/Pliers_clip_art.svg/640px-Pliers_clip_art.svg.png",
word: { en: "Pliers", ms: "Playar", ar: "كماشة" },
soundEffect: ""
},
{id:"saw", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Saw_clip_art.svg/640px-Saw_clip_art.svg.png",
word: { en: "Saw", ms: "Gergaji", ar: "منشار" },
soundEffect: ""
}
],
numbers: [
{id:"1", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Number_1.svg/480px-Number_1.svg.png",
word: { en: "One", ms: "Satu", ar: "واحد" },
soundEffect: ""
},
{id:"2", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/8/86/Number_2.svg/480px-Number_2.svg.png",
word: { en: "Two", ms: "Dua", ar: "اثنان" },
soundEffect: ""
},
{id:"3", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Number_3.svg/480px-Number_3.svg.png",
word: { en: "Three", ms: "Tiga", ar: "ثلاثة" },
soundEffect: ""
},
{id:"4", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Number_4.svg/480px-Number_4.svg.png",
word: { en: "Four", ms: "Empat", ar: "أربعة" },
soundEffect: ""
},
{id:"5", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Number_5.svg/480px-Number_5.svg.png",
word: { en: "Five", ms: "Lima", ar: "خمسة" },
soundEffect: ""
}
// Add 6-10 if desired
],
groceries: [
{id:"milk", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/Milk_glass.jpg/640px-Milk_glass.jpg",
word: { en: "Milk", ms: "Susu", ar: "حليب" },
soundEffect: ""
},
{id:"bread", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Fresh_made_bread_05.jpg/640px-Fresh_made_bread_05.jpg",
word: { en: "Bread", ms: "Roti", ar: "خبز" },
soundEffect: ""
},
{id:"egg", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Brown_chicken_egg.jpg/640px-Brown_chicken_egg.jpg",
word: { en: "Egg", ms: "Telur", ar: "بيضة" }, // singular
soundEffect: ""
},
{id:"rice", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/White_uncooked_rice.jpg/640px-White_uncooked_rice.jpg",
word: { en: "Rice", ms: "Beras", ar: "أرز" }, // uncooked rice
soundEffect: ""
},
{id:"sugar", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Sugar_cubes.jpg/640px-Sugar_cubes.jpg",
word: { en: "Sugar", ms: "Gula", ar: "سكر" },
soundEffect: ""
}
],
clothes: [
{id:"shirt", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Polo_shirt_clip_art.svg/640px-Polo_shirt_clip_art.svg.png",
word: { en: "Shirt", ms: "Baju", ar: "قميص" },
soundEffect: ""
},
{id:"pants", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/7/7e/Pair_of_blue_jeans.jpg/640px-Pair_of_blue_jeans.jpg",
word: { en: "Pants", ms: "Seluar", ar: "بنطال" }, // or بنطلون
soundEffect: ""
},
{id:"hat", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Baseball_cap_from_Hanes.jpg/640px-Baseball_cap_from_Hanes.jpg",
word: { en: "Hat", ms: "Topi", ar: "قبعة" },
soundEffect: ""
},
{id:"shoes", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d8/Pair_of_running_shoes.jpg/640px-Pair_of_running_shoes.jpg",
word: { en: "Shoes", ms: "Kasut", ar: "حذاء" }, // singular or أحذية plural
soundEffect: ""
},
{id:"dress", img:"https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Little_black_dress_clip_art.svg/640px-Little_black_dress_clip_art.svg.png",
word: { en: "Dress", ms: "Gaun", ar: "فستان" },
soundEffect: ""
}
]
};
// Supported languages and directionality
const languageData = {
en: { label: "English", dir: "ltr", voiceLang: "en-US" }, // Specify voice variant
ms: { label: "Malay", dir: "ltr", voiceLang: "ms-MY" },
ar: { label: "Arabic", dir: "rtl", voiceLang: "ar-SA" }
};
// Elements cache
const languageButtons = document.querySelectorAll("#languageSwitcher button");
const tabButtonsContainer = document.getElementById("tabButtons");
const tabContent = document.getElementById("tabContent");
const appTitle = document.getElementById("appTitle");
const tapSound = document.getElementById('tapSound');
// Test mode elements
const testSection = document.getElementById("testModeSection");
const testTitle = document.getElementById("testTitle");
const testQuestion = document.getElementById("testQuestion");
const testOptions = document.getElementById("testOptions");
const testResult = document.getElementById("testResult");
const startTestBtn = document.getElementById("startTestBtn");
const nextTestBtn = document.getElementById("nextTestBtn");
const resetTestBtn = document.getElementById("resetTestBtn");
const testControlsDiv = document.querySelector('.test-controls');
// State management
let currentLang = "en"; // default English
let currentTab = "animals"; // default tab
let speechSynth = window.speechSynthesis;
let activeUtterance = null;
let starRatings = {}; // { "category_itemId": rating } - Load from localStorage
let soundEffectAudio = null; // For animal/item specific sounds
// Test mode variables
let testActive = false;
let testItemsPool = [];
let currentTestQuestion = null;
let currentTestItemIndex = 0;
let correctAnswers = 0;
const NUM_TEST_QUESTIONS = 10; // How many questions per test round
// Predefined voices cache for each language code for best enhanced voices
let voicesByLang = {};
let voicesLoaded = false;
// --- Utility Functions ---
// Play tap sound effect
function playTapSound() {
if(tapSound) {
tapSound.currentTime = 0; // Rewind if already playing
tapSound.play().catch(e => console.error("Error playing tap sound:", e));
}
}
// Load ratings from localStorage
function loadRatings() {
const storedRatings = localStorage.getItem('starRatings');
if (storedRatings) {
starRatings = JSON.parse(storedRatings);
} else {
starRatings = {};
}
}
// Save ratings to localStorage
function saveRatings() {
localStorage.setItem('starRatings', JSON.stringify(starRatings));
}
// Sanitize text for speech (especially Arabic)
function sanitizeText(text) {
return text.trim();
}
// Initialize: Fetch voices and index by lang code
function initializeVoices() {
if (!speechSynth) return;
const allVoices = speechSynth.getVoices();
if (allVoices.length === 0 && !voicesLoaded) {
// Voices might load asynchronously, try again later
voicesLoaded = false; // Mark as not yet loaded
return;
}
voicesLoaded = true; // Mark as loaded
voicesByLang = { en: [], ms: [], ar: [] }; // Reset
// Prioritize local, high-quality voices if possible
allVoices.forEach(v => {
if (v.lang.startsWith('en')) voicesByLang.en.push(v);
if (v.lang.startsWith('ms')) voicesByLang.ms.push(v);
if (v.lang.startsWith('ar')) voicesByLang.ar.push(v);
});
// Sort voices: prioritize local service, then perhaps specific voice names if known
const sortVoices = (a, b) => (b.localService - a.localService) || a.name.localeCompare(b.name);
voicesByLang.en.sort(sortVoices);
voicesByLang.ms.sort(sortVoices);
voicesByLang.ar.sort(sortVoices);
// Debug: Log available voices
// console.log("Available English Voices:", voicesByLang.en.map(v => v.name));
// console.log("Available Malay Voices:", voicesByLang.ms.map(v => v.name));
// console.log("Available Arabic Voices:", voicesByLang.ar.map(v => v.name));
}
// Wrapper to safely speak text
function speak(text, langCode) {
if (!speechSynth || !text) return;
// Cancel any ongoing speech
if (speechSynth.speaking) {
speechSynth.cancel();
}
// Ensure voices are loaded
if (!voicesLoaded) {
initializeVoices(); // Try loading again
// Add a small delay if voices are still loading
if (!voicesLoaded) {
console.warn("Voices not loaded yet, trying again shortly.");
setTimeout(() => speak(text, langCode), 500);
return;
}
}
const utter = new SpeechSynthesisUtterance(sanitizeText(text));
activeUtterance = utter;
// Assign voice based on langCode
const targetLang = languageData[langCode].voiceLang; // e.g., "en-US"
const candidateVoices = voicesByLang[langCode] || [];
// Find the best matching voice
let selectedVoice = candidateVoices.find(v => v.lang === targetLang && v.localService); // Prefer local US/MY/SA
if (!selectedVoice) selectedVoice = candidateVoices.find(v => v.lang === targetLang); // Any US/MY/SA
if (!selectedVoice) selectedVoice = candidateVoices.find(v => v.lang.startsWith(langCode) && v.localService); // Any local English/Malay/Arabic
if (!selectedVoice && candidateVoices.length > 0) selectedVoice = candidateVoices[0]; // Fallback to first available
if (selectedVoice) {
// console.log("Using voice:", selectedVoice.name, selectedVoice.lang);
utter.voice = selectedVoice;
} else {
// console.warn(`No specific voice found for ${targetLang}. Using browser default for ${langCode}.`);
utter.lang = targetLang; // Let browser pick default for the language code
}
utter.rate = 0.9; // Slightly slower for clarity
utter.pitch = 1.0; // Normal pitch
// Handle speech events
utter.onerror = (event) => {
console.error('SpeechSynthesisUtterance.onerror', event);
alert(`Sorry, an error occurred during speech synthesis: ${event.error}`);
};
speechSynth.speak(utter);
return utter;
}
// Play item-specific sound effect (e.g., animal call)
function playItemSoundEffect(url) {
if (!url) return;
// Stop previous effect if any
if (soundEffectAudio && !soundEffectAudio.paused) {
soundEffectAudio.pause();
}
soundEffectAudio = new Audio(url);
soundEffectAudio.volume = 0.6; // Slightly louder than tap
soundEffectAudio.play().catch(e => console.error("Error playing item sound effect:", e));
}
// --- Rendering Functions ---
// Render rating stars
function renderStarRating(container, catId, itemId) {
container.innerHTML = ''; // Clear previous stars
const ratingKey = `${catId}_${itemId}`;
let currentRating = starRatings[ratingKey] || 0;
for (let i = 1; i <= 5; i++) {
const starSpan = document.createElement('span');
starSpan.classList.add('star');
starSpan.setAttribute('role', 'radio');
starSpan.setAttribute('tabindex', '0');
starSpan.setAttribute('aria-label', i + " star" + (i > 1 ? "s" : ""));
starSpan.dataset.value = i;
if (i <= currentRating) {
starSpan.innerHTML = "★"; // Filled star
starSpan.classList.add('filled');
starSpan.setAttribute('aria-checked', 'true');
} else {
starSpan.innerHTML = "☆"; // Empty star
starSpan.setAttribute('aria-checked', 'false');
}
// Click event
starSpan.addEventListener("click", (e) => {
e.stopPropagation(); // Prevent card click event
playTapSound();
starRatings[ratingKey] = i;
saveRatings(); // Save to localStorage
renderStarRating(container, catId, itemId); // Re-render stars for this item
});
// Keyboard event (Enter or Space)
starSpan.addEventListener("keydown", e => {
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
playTapSound();
starRatings[ratingKey] = i;
saveRatings();
renderStarRating(container, catId, itemId);
}
});
container.appendChild(starSpan);
}
}
// Build and render items for a category
function renderCategory(catId) {
const items = categories[catId];
if (!items) {
tabContent.innerHTML = `<h2 style="color: red;">Error: Category '${catId}' not found.</h2>`;
return;
}
tabContent.innerHTML = ''; // Clear previous content
const categoryName = getTranslatedCategoryName(catId);
// Add category heading
const heading = document.createElement('h2');
heading.textContent = categoryName;
heading.style.textAlign = (languageData[currentLang].dir === 'rtl') ? 'right' : 'left';
heading.style.color = '#388278'; // Use h3 color
tabContent.appendChild(heading);
const grid = document.createElement('div');
grid.className = "category-grid";
items.forEach(item => {
const card = document.createElement('article');
card.className = "card";
card.setAttribute('tabindex', '0'); // Make card focusable
const currentWord = getCurrentWord(item.word);
card.setAttribute('aria-label', `${currentWord}. Tap image for sound effect, button for pronunciation.`);
card.addEventListener('click', playTapSound); // Play tap sound on card click
// Image
const img = document.createElement('img');
img.src = item.img;
img.alt = currentWord;
img.loading = "lazy";
// Play specific sound on image click if available
if (item.soundEffect) {
img.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent card tap sound
playItemSoundEffect(item.soundEffect);
});
}
card.appendChild(img);
// Word text
const wordDiv = document.createElement('div');
wordDiv.className = "word";
wordDiv.textContent = currentWord;
card.appendChild(wordDiv);
// Interaction row (sound effect icon + stars)
const interactionRow = document.createElement('div');
interactionRow.className = "interaction-row";
// Sound effect icon button
const soundEffectBtn = document.createElement('button');
soundEffectBtn.setAttribute('aria-label', 'Play sound effect of ' + currentWord);
soundEffectBtn.title = 'Play sound effect';
soundEffectBtn.className = 'sound-effect-icon';
soundEffectBtn.innerHTML = '🔊'; // Speaker emoji
soundEffectBtn.disabled = !item.soundEffect;
soundEffectBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent card tap sound
if (item.soundEffect) {
playItemSoundEffect(item.soundEffect);
}
});
interactionRow.appendChild(soundEffectBtn);
// Stars container
const starsContainer = document.createElement('div');
starsContainer.className = "stars";
starsContainer.setAttribute('role', 'radiogroup');
starsContainer.setAttribute('aria-label', 'Star rating for ' + currentWord);
renderStarRating(starsContainer, catId, item.id); // Initial render
interactionRow.appendChild(starsContainer);
card.appendChild(interactionRow);
// Pronounce button
const btn = document.createElement('button');
btn.className = "btn-pronounce";
btn.textContent = getLocalizedString("Hear Pronunciation");
btn.setAttribute('aria-label', 'Hear pronunciation of ' + currentWord);
btn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent card tap sound
playTapSound(); // Play tap sound on button click
speak(currentWord, currentLang);
});
card.appendChild(btn);
grid.appendChild(card);
});
tabContent.appendChild(grid);
}
// --- Language and UI Update Functions ---
function getTranslatedCategoryName(catId) {
const translations = {
en: { animals: "Animals", fruits: "Fruits", vegetables: "Vegetables", shapes: "Shapes", colors: "Colors", tools: "Tools", numbers: "Numbers", groceries: "Groceries", clothes: "Clothes" },
ms: { animals: "Haiwan", fruits: "Buah-buahan", vegetables: "Sayur-sayuran", shapes: "Bentuk", colors: "Warna", tools: "Alatan", numbers: "Nombor", groceries: "Barangan Runcit", clothes: "Pakaian" },
ar: { animals: "الحيوانات", fruits: "الفواكه", vegetables: "الخضروات", shapes: "الأشكال", colors: "الألوان", tools: "الأدوات", numbers: "الأرقام", groceries: "البقالة", clothes: "الملابس" }
};
return translations[currentLang]?.[catId] || catId.charAt(0).toUpperCase() + catId.slice(1); // Fallback
}
function getLocalizedString(key) {
const strings = {
en: { "Hear Pronunciation": "Hear Pronunciation", "Start Test": "Start Test", "Next": "Next", "Reset Test": "Reset Test", "Test Mode": "Test Mode", "Test Your Learning": "Test Your Learning", "Correct!": "Correct!", "Wrong!": "Wrong!", "Please select an answer.": "Please select an answer.", "Test Complete!": "Test Complete!", "Your Score:": "Your Score:", "Tap to hear sound": "Tap image for sound", "Loading...": "Loading...", "Click 'Start Test' to begin!": "Click 'Start Test' to begin!", "What is this?": "What is this?" },
ms: { "Hear Pronunciation": "Dengar Sebutan", "Start Test": "Mula Ujian", "Next": "Seterusnya", "Reset Test": "Tetap Semula Ujian", "Test Mode": "Mod Ujian", "Test Your Learning": "Uji Pembelajaran Anda", "Correct!": "Betul!", "Wrong!": "Salah!", "Please select an answer.": "Sila pilih jawapan.", "Test Complete!": "Ujian Selesai!", "Your Score:": "Skor Anda:", "Tap to hear sound": "Ketik imej untuk bunyi", "Loading...": "Memuatkan...", "Click 'Start Test' to begin!": "Klik 'Mula Ujian' untuk bermula!", "What is this?": "Apakah ini?" },
ar: { "Hear Pronunciation": "اسمع النطق", "Start Test": "ابدأ الاختبار", "Next": "التالي", "Reset Test": "إعادة الاختبار", "Test Mode": "وضع الاختبار", "Test Your Learning": "اختبر تعلمك", "Correct!": "صحيح!", "Wrong!": "خطأ!", "Please select an answer.": "الرجاء اختيار إجابة.", "Test Complete!": "اكتمل الاختبار!", "Your Score:": "نتيجتك:", "Tap to hear sound": "اضغط على الصورة للصوت", "Loading...": "جار التحميل...", "Click 'Start Test' to begin!": "اضغط على 'ابدأ الاختبار' للبدء!", "What is this?": "ما هذا؟" }
};
return strings[currentLang]?.[key] || key; // Fallback to key
}
function getCurrentWord(wordObj) {
if (!wordObj) return "";
let word = wordObj[currentLang];
// Handle Arabic Numbers (convert Western digits to Arabic-Indic if the word is purely digits)
if (currentLang === 'ar' && /^\d+$/.test(word)) {
const arabicIndicDigits = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
word = word.split('').map(d => arabicIndicDigits[parseInt(d)] || d).join('');
}
return word;
}
function updateUIForLanguage() {
const langInfo = languageData[currentLang];
document.documentElement.lang = currentLang;
document.documentElement.dir = langInfo.dir;
// Update language switcher buttons
languageButtons.forEach(btn => {
constisActive = btn.dataset.lang === currentLang;
btn.classList.toggle('active', isActive);
btn.setAttribute('aria-pressed', isActive.toString());
});
// Update tab button text
tabButtonsContainer.querySelectorAll('button').forEach(btn => {
const catId = btn.dataset.tab;
btn.textContent = getTranslatedCategoryName(catId);
});
// Update App Title
appTitle.textContent = getLocalizedString("Educational Categories") || "Educational Categories"; // Fallback needed here?
// Update test mode static text
testTitle.textContent = getLocalizedString("Test Mode");
startTestBtn.textContent = getLocalizedString("Start Test");
nextTestBtn.textContent = getLocalizedString("Next");
resetTestBtn.textContent = getLocalizedString("Reset Test");
// Update initial test question prompt if test not active
if (!testActive) {
testQuestion.textContent = getLocalizedString("Click 'Start Test' to begin!");
}
// Set direction for content areas
tabContent.dir = langInfo.dir;
testSection.dir = langInfo.dir;
// Re-render the currently active category to update card text etc.
renderCategory(currentTab);
// Update test question/options if test is active
if (testActive && currentTestQuestion) {
renderTestQuestionContent(currentTestQuestion);
}
}
// --- Event Handlers ---
// Tab switching
function switchTab(newTabId) {
if (currentTab === newTabId && tabContent.innerHTML !== '' && !tabContent.querySelector('h2')) return; // Avoid re-render if already active unless empty
playTapSound(); // Sound on tab switch
// Update button states
tabButtonsContainer.querySelectorAll('button').forEach(btn => {
const isActive = btn.dataset.tab === newTabId;
btn.classList.toggle('active', isActive);
btn.setAttribute('aria-selected', isActive.toString());
btn.tabIndex = isActive ? 0 : -1;
});
currentTab = newTabId;
renderCategory(currentTab); // Render the new category
tabContent.scrollTop = 0; // Scroll to top of new content
}
// Language button clicks
languageButtons.forEach(btn => {
btn.addEventListener("click", () => {
if (btn.dataset.lang === currentLang) return;
playTapSound();
currentLang = btn.dataset.lang;
updateUIForLanguage();
});
});
// Tab button clicks
tabButtonsContainer.addEventListener('click', e => {
if (e.target.tagName === 'BUTTON' && e.target.dataset.tab) {
switchTab(e.target.dataset.tab);
}
});
// Keyboard navigation for tabs
tabButtonsContainer.addEventListener('keydown', e => {
const buttons = Array.from(tabButtonsContainer.querySelectorAll('button'));
let focusIndex = buttons.indexOf(document.activeElement);
if (focusIndex === -1) return; // Only handle if focus is within tabs
let newIndex = focusIndex;
if (e.key === 'ArrowRight') {
newIndex = (focusIndex + 1) % buttons.length;
} else if (e.key === 'ArrowLeft') {
newIndex = (focusIndex - 1 + buttons.length) % buttons.length;
} else if (e.key === 'Home') {
newIndex = 0;
} else if (e.key === 'End') {
newIndex = buttons.length - 1;
} else {
return; // Ignore other keys
}
e.preventDefault(); // Prevent page scroll
buttons[newIndex].focus();
switchTab(buttons[newIndex].dataset.tab);
});
// --- Test Mode Logic ---
// Flatten all items into a single pool for testing
function prepareTestItemsPool() {
testItemsPool = [];
Object.entries(categories).forEach(([catId, items]) => {
items.forEach(item => {
testItemsPool.push({ category: catId, ...item }); // Spread item properties
});
});
}
// Shuffle array (Fisher-Yates)
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// Get random items from the pool, ensuring the correct item is included
function getTestOptions(correctItem, pool, count = 4) {
const options = new Set([correctItem]);
const shuffledPool = [...pool]; // Create a copy to shuffle
shuffleArray(shuffledPool);
for (const item of shuffledPool) {
if (options.size >= count) break;
if (item.id !== correctItem.id) { // Make sure not to add the correct item again
options.add(item);
}
}
// If somehow we didn't get enough options (e.g., small pool), fill with duplicates (less ideal)
let poolIndex = 0;
while (options.size < count && pool.length > 0) {
options.add(pool[poolIndex % pool.length]);
poolIndex++;
}
return Array.from(options);
}
// Render the content of the current test question
function renderTestQuestionContent(testItem) {
const wordToTest = getCurrentWord(testItem.word);
const questionPrompt = getLocalizedString("What is this?");
testQuestion.textContent = `${questionPrompt}: ${wordToTest}`;
testQuestion.setAttribute('aria-label', testQuestion.textContent); // Accessibility
testOptions.innerHTML = ''; // Clear previous options
testResult.textContent = ''; // Clear previous result
testResult.className = ''; // Reset result style
// Get options (1 correct, 3 wrong)
const options = getTestOptions(testItem, testItemsPool, 4);
shuffleArray(options); // Shuffle the final options array
// Create buttons for each option
options.forEach(optionItem => {
const btn = document.createElement('button');
const optionWord = getCurrentWord(optionItem.word);
btn.textContent = optionWord;
btn.setAttribute('data-item-id', optionItem.id);
btn.setAttribute('aria-label', `Option: ${optionWord}`);
btn.addEventListener('click', () => handleTestAnswer(optionItem.id, testItem.id, btn));
testOptions.appendChild(btn);
});
// Show/hide control buttons
startTestBtn.hidden = true;
nextTestBtn.hidden = true; // Hide next until answer is given
resetTestBtn.hidden = true;
testControlsDiv.style.display = 'flex'; // Ensure controls container is visible
// Speak the word being tested
speak(wordToTest, currentLang);
}
// Start the test
function startTest() {
playTapSound();
testActive = true;
correctAnswers = 0;
currentTestItemIndex = 0;
shuffleArray(testItemsPool); // Shuffle the pool for randomness each time
currentTestQuestion = testItemsPool[currentTestItemIndex];
if (!currentTestQuestion) {
testQuestion.textContent = "Error: No items to test!";
testOptions.innerHTML = '';
testResult.textContent = '';
resetTestBtn.hidden = false; // Allow reset
testActive = false;
return;
}
renderTestQuestionContent(currentTestQuestion);
}
// Handle user's answer selection
function handleTestAnswer(selectedItemId, correctItemId, clickedButton) {
playTapSound();
// Disable all option buttons
testOptions.querySelectorAll('button').forEach(btn => btn.disabled = true);
let isCorrect = selectedItemId === correctItemId;
if (isCorrect) {
correctAnswers++;
testResult.textContent = getLocalizedString("Correct!");
testResult.className = 'correct'; // Apply green style
clickedButton.style.backgroundColor = '#4caf50'; // Highlight correct answer green
} else {
testResult.textContent = getLocalizedString("Wrong!");
testResult.className = 'wrong'; // Apply red style
clickedButton.style.backgroundColor = '#f44336'; // Highlight wrong answer red
// Also highlight the correct one
const correctButton = testOptions.querySelector(`button[data-item-id="${correctItemId}"]`);
if(correctButton) correctButton.style.backgroundColor = '#a5d6a7'; // Lighter green for correct one
}
// Speak the result
speak(testResult.textContent, currentLang);
// Show the 'Next' or 'Reset' button
if (currentTestItemIndex < NUM_TEST_QUESTIONS - 1 && currentTestItemIndex < testItemsPool.length - 1) {
nextTestBtn.hidden = false;
nextTestBtn.focus(); // Focus next button
} else {
// Test finished
finishTest();
}
}
// Move to the next question
function nextTestQuestion() {
playTapSound();
currentTestItemIndex++;
if (currentTestItemIndex < NUM_TEST_QUESTIONS && currentTestItemIndex < testItemsPool.length) {
currentTestQuestion = testItemsPool[currentTestItemIndex];
renderTestQuestionContent(currentTestQuestion);
} else {
finishTest(); // Should not happen if logic is correct, but as fallback
}
}
// Finish the test and show score
function finishTest() {
testActive = false;
const score = `${correctAnswers} / ${Math.min(NUM_TEST_QUESTIONS, testItemsPool.length)}`;
testQuestion.textContent = getLocalizedString("Test Complete!");
testOptions.innerHTML = ''; // Clear options
testResult.textContent = `${getLocalizedString("Your Score:")} ${score}`;
testResult.className = 'final-score'; // Style differently if needed
startTestBtn.hidden = true;
nextTestBtn.hidden = true;
resetTestBtn.hidden = false; // Show reset button
resetTestBtn.focus();
// Speak final score
speak(`${getLocalizedString("Test Complete!")} ${getLocalizedString("Your Score:")} ${score}`, currentLang);
}
// Reset the test mode UI
function resetTest() {
playTapSound();
testActive = false;
currentTestQuestion = null;
testQuestion.textContent = getLocalizedString("Click 'Start Test' to begin!");
testOptions.innerHTML = '';
testResult.textContent = '';
testResult.className = '';
startTestBtn.hidden = false;
nextTestBtn.hidden = true;
resetTestBtn.hidden = true;
startTestBtn.focus();
}
// Event listeners for test buttons
startTestBtn.addEventListener('click', startTest);
nextTestBtn.addEventListener('click', nextTestQuestion);
resetTestBtn.addEventListener('click', resetTest);
// --- Initialization ---
function init() {
loadRatings(); // Load ratings from storage
prepareTestItemsPool(); // Prepare items for test mode
switchTab(currentTab); // Render the default tab
updateUIForLanguage(); // Set default language UI
// Attempt to load voices immediately and set up listener for changes
if (speechSynth.onvoiceschanged !== undefined) {
speechSynth.onvoiceschanged = initializeVoices;
}
initializeVoices(); // Initial attempt to load voices
// Initial state for test mode section
resetTest(); // Set the initial state correctly
}
// Run initialization when the DOM is ready
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
Visual Learning Example
High-quality, engaging images are crucial for capturing children's attention and aiding recognition. Below is an example of the type of visual used in this learning application, specifically for matching animal sounds.
Example of a visual aid for learning animal sounds.
Frequently Asked Questions (FAQ)
How do I change the language?
Click on the language buttons (English, Malay, Arabic) located near the top of the application. The interface and pronunciations will update instantly to the selected language.
How does the pronunciation work?
The application uses your web browser's built-in Text-to-Speech (TTS) engine. When you click the "Hear Pronunciation" button, the app instructs the browser to speak the word in the selected language using available synthesized voices. The quality of the "enhanced voices" depends on the voices installed on your system or provided by your browser.
Are the star ratings saved?
Yes, the star ratings you assign to each item are saved locally in your browser's storage. This means they will persist even if you close and reopen the browser tab, allowing you to track progress over time on the same device and browser.
What does the Test Mode do?
The Test Mode provides a quiz to check your learning. It randomly selects items from all categories, presents a word, and asks you to choose the correct matching word from multiple options. It provides immediate feedback (Correct/Wrong) and shows your total score at the end of the quiz round (typically 10 questions).
Why don't some items have a sound effect icon (🔊)?
Specific sound effects (beyond the standard pronunciation) are primarily included for categories where items have distinct, recognizable sounds, like the Animals category (e.g., a lion's roar, a cat's meow). Categories like Fruits, Colors, or Shapes generally don't have associated unique sounds, so the icon is disabled for those items.
Recommended Further Exploration
- Discover methods for integrating pre-recorded, high-quality audio files for pronunciation.
- Explore techniques for building more complex learning games using web technologies.
- Learn about using external APIs for dynamic translation and advanced text-to-speech features.
- Investigate more robust methods for storing user data and progress in web applications.
References
Last updated April 28, 2025