Ask me anything
Ithy Logo
Sign in to save

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!

multilingual-educational-web-app-s5sskzux

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.

mindmap root["Educational Web App"] id1["Languages"] id1_1["English"] id1_2["Malay"] id1_3["Arabic (RTL)"] id2["Categories (Tabs)"] id2_1["Animals"] id2_2["Fruits"] id2_3["Vegetables"] id2_4["Geometric Shapes"] id2_5["Colors"] id2_6["Household Tools"] id2_7["Numbers"] id2_8["Groceries"] id2_9["Clothes"] id3["Item Card Features"] id3_1["Image Display"] id3_2["Multilingual Word"] id3_3["Pronunciation Button (TTS)"] id3_4["Specific Sound Effect (e.g., Animal Call)"] id3_5["Star Rating System"] id3_6["Tap Sound Effect"] id4["Global Features"] id4_1["Language Switching"] id4_2["Tab Navigation"] id4_3["Test Mode (Quiz)"] id4_4["Child-Friendly UI"]

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.

Animal Sound Match-Up Example

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


References

developer.mozilla.org
Web Speech API - MDN Web Docs

Last updated April 28, 2025

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI