import Module from '../core/Module.js'; /** * LetterDiscovery - Interactive letter and word discovery game * Three phases: letter discovery, word exploration, and practice challenges */ class LetterDiscovery extends Module { constructor(name, dependencies, config = {}) { super(name, ['eventBus']); // Validate dependencies if (!dependencies.eventBus || !dependencies.content) { throw new Error('LetterDiscovery requires eventBus and content dependencies'); } this._eventBus = dependencies.eventBus; this._content = dependencies.content; this._config = { container: null, maxPracticeRounds: 8, autoPlayTTS: true, ttsSpeed: 0.8, ...config }; // Game state this._currentPhase = 'letter-discovery'; // letter-discovery, word-exploration, practice this._currentLetterIndex = 0; this._discoveredLetters = []; this._currentLetter = null; this._currentWordIndex = 0; this._discoveredWords = []; this._score = 0; this._lives = 3; // Content data this._letters = []; this._letterWords = {}; // Map letter -> words starting with that letter // Practice system this._practiceLevel = 1; this._practiceRound = 0; this._practiceCorrectAnswers = 0; this._practiceErrors = 0; this._currentPracticeItems = []; this._currentCorrectAnswer = null; Object.seal(this); } /** * Get game metadata * @returns {Object} Game metadata */ static getMetadata() { return { name: 'Letter Discovery', description: 'Discover letters and explore words that start with each letter', difficulty: 'beginner', category: 'letters', estimatedTime: 10, // minutes skills: ['alphabet', 'vocabulary', 'pronunciation'] }; } /** * Calculate compatibility score with content * @param {Object} content - Content to check compatibility with * @returns {Object} Compatibility score and details */ static getCompatibilityScore(content) { const letters = content?.letters || content?.rawContent?.letters; // Try to create letters from vocabulary if direct letters not found let lettersData = letters; if (!lettersData && content?.vocabulary) { lettersData = this._createLettersFromVocabulary(content.vocabulary); } if (!lettersData || Object.keys(lettersData).length === 0) { return { score: 0, reason: 'No letter structure found', requirements: ['letters'], details: 'Letter Discovery requires content with predefined letters system' }; } const letterCount = letters ? Object.keys(letters).length : 0; const totalWords = letters ? Object.values(letters).reduce((sum, words) => sum + (words?.length || 0), 0) : 0; if (totalWords === 0) { return { score: 0.2, reason: 'Letters found but no words', requirements: ['letters with words'], details: `Found ${letterCount} letters but no associated words` }; } // Perfect score at 26 letters, good score for 10+ letters const score = Math.min(letterCount / 26, 1); return { score, reason: `${letterCount} letters with ${totalWords} total words`, requirements: ['letters'], optimalLetters: 26, details: `Can create discovery experience with ${letterCount} letters and ${totalWords} words` }; } async init() { this._validateNotDestroyed(); try { // Validate container if (!this._config.container) { throw new Error('Game container is required'); } // Extract and validate content this._extractContent(); if (this._letters.length === 0) { throw new Error('No letter content found for discovery'); } // Set up event listeners this._eventBus.on('game:pause', this._handlePause.bind(this), this.name); this._eventBus.on('game:resume', this._handleResume.bind(this), this.name); // Initialize game interface this._injectCSS(); this._createGameInterface(); this._setupEventListeners(); // Start with first letter this._showLetterCard(); // Emit game ready event this._eventBus.emit('game:ready', { gameId: 'letter-discovery', instanceId: this.name, letters: this._letters.length, totalWords: Object.values(this._letterWords).reduce((sum, words) => sum + words.length, 0) }, this.name); this._setInitialized(); } catch (error) { this._showError(error.message); throw error; } } async destroy() { this._validateNotDestroyed(); // Clean up container if (this._config.container) { this._config.container.innerHTML = ''; } // Remove injected CSS this._removeInjectedCSS(); // Emit game end event this._eventBus.emit('game:ended', { gameId: 'letter-discovery', instanceId: this.name, score: this._score, lettersDiscovered: this._discoveredLetters.length, wordsLearned: this._discoveredWords.length }, this.name); this._setDestroyed(); } /** * Get current game state * @returns {Object} Current game state */ getGameState() { this._validateInitialized(); return { phase: this._currentPhase, score: this._score, lives: this._lives, currentLetter: this._currentLetter, lettersDiscovered: this._discoveredLetters.length, totalLetters: this._letters.length, wordsLearned: this._discoveredWords.length, practiceAccuracy: this._config.maxPracticeRounds > 0 ? (this._practiceCorrectAnswers / this._config.maxPracticeRounds) * 100 : 0 }; } // Private methods _extractContent() { const letters = this._content.letters || this._content.rawContent?.letters; if (letters && Object.keys(letters).length > 0) { this._letters = Object.keys(letters).sort(); this._letterWords = letters; } else { this._letters = []; this._letterWords = {}; } } _injectCSS() { if (document.getElementById('letter-discovery-styles')) return; const styleSheet = document.createElement('style'); styleSheet.id = 'letter-discovery-styles'; styleSheet.textContent = ` .letter-discovery-wrapper { background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e0 100%); min-height: 100vh; padding: 20px; position: relative; overflow-y: auto; box-sizing: border-box; } .letter-discovery-hud { display: flex; justify-content: space-between; align-items: center; background: rgba(255,255,255,0.1); padding: 15px 20px; border-radius: 15px; backdrop-filter: blur(10px); margin-bottom: 20px; flex-wrap: wrap; gap: 10px; } .hud-group { display: flex; align-items: center; gap: 15px; } .hud-item { color: white; font-weight: bold; font-size: 1.1em; } .phase-indicator { background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; font-size: 0.9em; color: white; backdrop-filter: blur(5px); } .letter-discovery-main { background: rgba(255,255,255,0.1); border-radius: 20px; padding: 30px; backdrop-filter: blur(10px); min-height: 60vh; display: flex; flex-direction: column; align-items: center; justify-content: center; } .game-content { width: 100%; max-width: 900px; text-align: center; } /* Letter Display Styles */ .letter-card { background: rgba(255,255,255,0.95); border-radius: 25px; padding: 60px 40px; margin: 30px auto; max-width: 400px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); transform: scale(0.8); animation: letterAppear 0.8s ease-out forwards; } @keyframes letterAppear { to { transform: scale(1); } } .letter-display { font-size: 8em; font-weight: bold; color: #2d3748; margin-bottom: 20px; text-shadow: 0 4px 8px rgba(0,0,0,0.1); font-family: 'Arial Black', Arial, sans-serif; } .letter-info { font-size: 1.5em; color: #333; margin-bottom: 15px; } .letter-pronunciation { font-size: 1.2em; color: #666; font-style: italic; margin-bottom: 25px; } .letter-controls { display: flex; gap: 15px; justify-content: center; margin-top: 30px; flex-wrap: wrap; } /* Word Exploration Styles */ .word-exploration-header { background: rgba(255,255,255,0.1); padding: 20px; border-radius: 15px; margin-bottom: 30px; backdrop-filter: blur(5px); } .exploring-letter { font-size: 3em; color: white; margin-bottom: 10px; font-weight: bold; } .word-progress { color: rgba(255,255,255,0.8); font-size: 1.1em; } .word-card { background: rgba(255,255,255,0.95); border-radius: 20px; padding: 40px 30px; margin: 25px auto; max-width: 500px; box-shadow: 0 15px 30px rgba(0,0,0,0.1); transform: translateY(20px); animation: wordSlideIn 0.6s ease-out forwards; } @keyframes wordSlideIn { to { transform: translateY(0); } } .word-text { font-size: 2.5em; color: #2d3748; margin-bottom: 15px; font-weight: bold; } .word-translation { font-size: 1.3em; color: #333; margin-bottom: 10px; } .word-pronunciation { font-size: 1.1em; color: #666; font-style: italic; margin-bottom: 10px; } .word-type { font-size: 0.9em; color: #2d3748; background: rgba(66, 153, 225, 0.1); padding: 4px 12px; border-radius: 15px; display: inline-block; margin-bottom: 15px; font-weight: 500; } .word-example { font-size: 1em; color: #555; font-style: italic; padding: 10px 15px; background: rgba(0, 0, 0, 0.05); border-left: 3px solid #4299e1; border-radius: 0 8px 8px 0; margin-bottom: 15px; } /* Practice Challenge Styles */ .practice-challenge { text-align: center; margin-bottom: 30px; } .challenge-text { font-size: 1.8em; color: white; margin-bottom: 25px; text-shadow: 0 2px 4px rgba(0,0,0,0.3); } .practice-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; max-width: 800px; margin: 0 auto; } .practice-option { background: rgba(255,255,255,0.9); border: none; border-radius: 15px; padding: 20px; font-size: 1.2em; cursor: pointer; transition: all 0.3s ease; color: #333; font-weight: 500; } .practice-option:hover:not(.correct):not(.incorrect) { background: rgba(255,255,255,1); transform: translateY(-3px); box-shadow: 0 8px 20px rgba(0,0,0,0.2); } .practice-option.correct { background: #4CAF50; color: white; animation: correctPulse 0.6s ease; } .practice-option.incorrect { background: #F44336; color: white; animation: incorrectShake 0.6s ease; } @keyframes correctPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } @keyframes incorrectShake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } .practice-stats { display: flex; justify-content: space-around; margin-top: 20px; color: white; font-size: 1.1em; gap: 10px; } .stat-item { text-align: center; padding: 10px; background: rgba(255,255,255,0.1); border-radius: 10px; backdrop-filter: blur(5px); flex: 1; } /* Control Buttons */ .discovery-btn { background: linear-gradient(45deg, #4299e1, #3182ce); color: white; border: none; padding: 15px 30px; border-radius: 25px; font-size: 1.1em; font-weight: bold; cursor: pointer; transition: all 0.3s ease; margin: 0 5px; } .discovery-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,0.3); } .discovery-btn:active { transform: translateY(0); } .audio-btn { background: none; border: none; font-size: 2em; cursor: pointer; color: #2d3748; transition: all 0.3s ease; padding: 10px; border-radius: 50%; } .audio-btn:hover { transform: scale(1.2); color: #3182ce; background: rgba(255,255,255,0.1); } /* Completion Message */ .completion-message { text-align: center; padding: 40px; background: rgba(255,255,255,0.1); border-radius: 20px; backdrop-filter: blur(10px); color: white; } .completion-title { font-size: 2.5em; margin-bottom: 20px; color: #00ff88; text-shadow: 0 2px 10px rgba(0,255,136,0.3); } .completion-stats { font-size: 1.3em; margin-bottom: 30px; line-height: 1.6; } .exit-btn { background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); padding: 10px 20px; border-radius: 15px; font-size: 1em; cursor: pointer; transition: all 0.3s ease; } .exit-btn:hover { background: rgba(255,255,255,0.3); } /* Responsive Design */ @media (max-width: 768px) { .letter-discovery-wrapper { padding: 15px; } .letter-display { font-size: 5em; } .word-text { font-size: 2em; } .challenge-text { font-size: 1.4em; } .practice-grid { grid-template-columns: 1fr; } .letter-controls { flex-direction: column; align-items: center; } .discovery-btn { margin: 5px 0; width: 100%; max-width: 250px; } .practice-stats { flex-direction: column; } } `; document.head.appendChild(styleSheet); } _removeInjectedCSS() { const styleSheet = document.getElementById('letter-discovery-styles'); if (styleSheet) { styleSheet.remove(); } } _createGameInterface() { this._config.container.innerHTML = `
Score: ${this._score}
Lives: ${this._lives}
Letter Discovery
Progress: 0/${this._letters.length}
`; } _setupEventListeners() { // Exit button document.getElementById('exit-btn').addEventListener('click', () => { this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name); }); } _updateHUD() { const scoreDisplay = document.getElementById('score-display'); const livesDisplay = document.getElementById('lives-display'); const progressDisplay = document.getElementById('progress-display'); const phaseIndicator = document.getElementById('phase-indicator'); if (scoreDisplay) scoreDisplay.textContent = this._score; if (livesDisplay) livesDisplay.textContent = this._lives; if (this._currentPhase === 'letter-discovery') { if (progressDisplay) progressDisplay.textContent = `${this._currentLetterIndex}/${this._letters.length}`; if (phaseIndicator) phaseIndicator.textContent = 'Letter Discovery'; } else if (this._currentPhase === 'word-exploration') { const words = this._letterWords[this._currentLetter] || []; if (progressDisplay) progressDisplay.textContent = `${this._currentWordIndex}/${words.length}`; if (phaseIndicator) phaseIndicator.textContent = `Exploring Letter "${this._currentLetter}"`; } else if (this._currentPhase === 'practice') { if (progressDisplay) progressDisplay.textContent = `Round ${this._practiceRound + 1}/${this._config.maxPracticeRounds}`; if (phaseIndicator) phaseIndicator.textContent = `Practice - Level ${this._practiceLevel}`; } } _showLetterCard() { if (this._currentLetterIndex >= this._letters.length) { this._showCompletion(); return; } const letter = this._letters[this._currentLetterIndex]; const gameContent = document.getElementById('game-content'); gameContent.innerHTML = `
${letter}
Letter "${letter}"
[${this._getLetterPronunciation(letter)}]
`; // Set up button listeners document.getElementById('discover-letter-btn').addEventListener('click', () => { this._discoverLetter(); }); document.getElementById('play-letter-btn').addEventListener('click', () => { this._playLetterSound(letter); }); this._updateHUD(); // Auto-play letter sound if enabled if (this._config.autoPlayTTS) { setTimeout(() => this._playLetterSound(letter), 500); } } _getLetterPronunciation(letter) { const pronunciations = { 'A': 'ay', 'B': 'bee', 'C': 'see', 'D': 'dee', 'E': 'ee', 'F': 'ef', 'G': 'gee', 'H': 'aych', 'I': 'eye', 'J': 'jay', 'K': 'kay', 'L': 'el', 'M': 'em', 'N': 'en', 'O': 'oh', 'P': 'pee', 'Q': 'cue', 'R': 'ar', 'S': 'ess', 'T': 'tee', 'U': 'you', 'V': 'vee', 'W': 'double-you', 'X': 'ex', 'Y': 'why', 'Z': 'zee' }; return pronunciations[letter] || letter.toLowerCase(); } _playLetterSound(letter) { this._speakText(letter, { rate: this._config.ttsSpeed * 0.8 }); // Slower for letters } _discoverLetter() { const letter = this._letters[this._currentLetterIndex]; this._discoveredLetters.push(letter); this._score += 10; // Emit score update event this._eventBus.emit('game:score-update', { gameId: 'letter-discovery', instanceId: this.name, score: this._score }, this.name); // Start word exploration for this letter this._currentLetter = letter; this._currentPhase = 'word-exploration'; this._currentWordIndex = 0; this._showWordExploration(); } _showWordExploration() { const words = this._letterWords[this._currentLetter]; if (!words || this._currentWordIndex >= words.length) { // Finished exploring words for this letter this._currentPhase = 'letter-discovery'; this._currentLetterIndex++; this._showLetterCard(); return; } const word = words[this._currentWordIndex]; const gameContent = document.getElementById('game-content'); gameContent.innerHTML = `
Letter "${this._currentLetter}"
Word ${this._currentWordIndex + 1} of ${words.length}
${word.word}
${word.translation}
${word.pronunciation ? `
[${word.pronunciation}]
` : ''} ${word.type ? `
${word.type}
` : ''} ${word.example ? `
"${word.example}"
` : ''}
`; // Set up button listeners document.getElementById('next-word-btn').addEventListener('click', () => { this._nextWord(); }); document.getElementById('play-word-btn').addEventListener('click', () => { this._playWordSound(word.word); }); // Add word to discovered list this._discoveredWords.push(word); this._updateHUD(); // Auto-play word sound if enabled if (this._config.autoPlayTTS) { setTimeout(() => this._playWordSound(word.word), 500); } } _playWordSound(word) { this._speakText(word, { rate: this._config.ttsSpeed }); } _nextWord() { this._currentWordIndex++; this._score += 5; // Emit score update event this._eventBus.emit('game:score-update', { gameId: 'letter-discovery', instanceId: this.name, score: this._score }, this.name); this._showWordExploration(); } _showCompletion() { const gameContent = document.getElementById('game-content'); const totalWords = Object.values(this._letterWords).reduce((sum, words) => sum + words.length, 0); gameContent.innerHTML = `
🎉 All Letters Discovered!
Letters Discovered: ${this._discoveredLetters.length}
Words Learned: ${this._discoveredWords.length}
Final Score: ${this._score}
`; // Set up button listeners document.getElementById('start-practice-btn').addEventListener('click', () => { this._startPractice(); }); document.getElementById('restart-btn').addEventListener('click', () => { this._restart(); }); this._updateHUD(); } _startPractice() { this._currentPhase = 'practice'; this._practiceLevel = 1; this._practiceRound = 0; this._practiceCorrectAnswers = 0; this._practiceErrors = 0; // Create shuffled practice items from all discovered words this._currentPracticeItems = this._shuffleArray([...this._discoveredWords]); this._showPracticeChallenge(); } _showPracticeChallenge() { if (this._practiceRound >= this._config.maxPracticeRounds) { this._endPractice(); return; } const currentItem = this._currentPracticeItems[this._practiceRound % this._currentPracticeItems.length]; const gameContent = document.getElementById('game-content'); // Generate options (correct + 3 random) const allWords = this._discoveredWords.filter(w => w.word !== currentItem.word); const randomOptions = this._shuffleArray([...allWords]).slice(0, 3); const options = this._shuffleArray([currentItem, ...randomOptions]); gameContent.innerHTML = `
What does "${currentItem.word}" mean?
${options.map((option, index) => ` `).join('')}
Correct: ${this._practiceCorrectAnswers}
Errors: ${this._practiceErrors}
Round: ${this._practiceRound + 1}/${this._config.maxPracticeRounds}
`; // Set up option listeners document.querySelectorAll('.practice-option').forEach(button => { button.addEventListener('click', (e) => { const selectedIndex = parseInt(e.target.dataset.optionIndex); const selectedWord = e.target.dataset.word; this._selectPracticeAnswer(selectedIndex, selectedWord); }); }); // Store correct answer for checking this._currentCorrectAnswer = currentItem.word; this._updateHUD(); // Auto-play word if enabled if (this._config.autoPlayTTS) { setTimeout(() => this._playWordSound(currentItem.word), 500); } } _selectPracticeAnswer(selectedIndex, selectedWord) { const buttons = document.querySelectorAll('.practice-option'); const isCorrect = selectedWord === this._currentCorrectAnswer; // Disable all buttons to prevent multiple clicks buttons.forEach(btn => btn.disabled = true); if (isCorrect) { buttons[selectedIndex].classList.add('correct'); this._practiceCorrectAnswers++; this._score += 10; // Emit score update event this._eventBus.emit('game:score-update', { gameId: 'letter-discovery', instanceId: this.name, score: this._score }, this.name); } else { buttons[selectedIndex].classList.add('incorrect'); this._practiceErrors++; // Show correct answer buttons.forEach((btn) => { if (btn.dataset.word === this._currentCorrectAnswer) { btn.classList.add('correct'); } }); } setTimeout(() => { this._practiceRound++; this._showPracticeChallenge(); }, 1500); } _endPractice() { const accuracy = Math.round((this._practiceCorrectAnswers / this._config.maxPracticeRounds) * 100); // Store best score const gameKey = 'letter-discovery'; const currentScore = this._score; const bestScore = parseInt(localStorage.getItem(`${gameKey}-best-score`) || '0'); const isNewBest = currentScore > bestScore; if (isNewBest) { localStorage.setItem(`${gameKey}-best-score`, currentScore.toString()); } // Show victory popup this._showVictoryPopup({ gameTitle: 'Letter Discovery', currentScore, bestScore: isNewBest ? currentScore : bestScore, isNewBest, stats: { 'Letters Found': this._discoveredLetters.length, 'Words Learned': this._discoveredWords.length, 'Practice Accuracy': `${accuracy}%`, 'Correct Answers': `${this._practiceCorrectAnswers}/${this._config.maxPracticeRounds}` } }); // Emit game completion event this._eventBus.emit('game:completed', { gameId: 'letter-discovery', instanceId: this.name, score: this._score, lettersDiscovered: this._discoveredLetters.length, wordsLearned: this._discoveredWords.length, practiceAccuracy: accuracy }, this.name); this._updateHUD(); } _restart() { // Reset all game state this._currentPhase = 'letter-discovery'; this._currentLetterIndex = 0; this._discoveredLetters = []; this._currentLetter = null; this._currentWordIndex = 0; this._discoveredWords = []; this._score = 0; this._lives = 3; this._practiceLevel = 1; this._practiceRound = 0; this._practiceCorrectAnswers = 0; this._practiceErrors = 0; this._currentPracticeItems = []; this._currentCorrectAnswer = null; this._showLetterCard(); } _shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } _speakText(text, options = {}) { if (!text) return; try { if ('speechSynthesis' in window) { speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = this._getContentLanguage(); utterance.rate = options.rate || this._config.ttsSpeed; utterance.volume = 1.0; speechSynthesis.speak(utterance); } } catch (error) { console.warn('TTS error:', error); } } _getContentLanguage() { if (this._content.language) { const langMap = { 'chinese': 'zh-CN', 'english': 'en-US', 'french': 'fr-FR', 'spanish': 'es-ES' }; return langMap[this._content.language] || this._content.language; } return 'en-US'; } _showError(message) { if (this._config.container) { this._config.container.innerHTML = `

Letter Discovery Error

${message}

`; } } _handlePause() { this._eventBus.emit('game:paused', { instanceId: this.name }, this.name); } _handleResume() { this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name); } _showVictoryPopup({ gameTitle, currentScore, bestScore, isNewBest, stats }) { const popup = document.createElement('div'); popup.className = 'victory-popup'; popup.innerHTML = `
🔤

${gameTitle} Complete!

${isNewBest ? '
🎉 New Best Score!
' : ''}
Your Score
${currentScore}
Best Score
${bestScore}
${Object.entries(stats).map(([key, value]) => `
${key}
${value}
`).join('')}
`; document.body.appendChild(popup); // Animate in requestAnimationFrame(() => { popup.classList.add('show'); }); // Add event listeners popup.querySelector('#play-again-btn').addEventListener('click', () => { popup.remove(); this._restart(); }); popup.querySelector('#different-game-btn').addEventListener('click', () => { popup.remove(); if (window.app && window.app.getCore().router) { window.app.getCore().router.navigate('/games'); // Force content reload by re-emitting navigation event setTimeout(() => { const chapterId = window.currentChapterId || 'sbs'; this._eventBus.emit('navigation:games', { path: `/games/${chapterId}`, data: { path: `/games/${chapterId}` } }, 'Application'); }, 100); } else { window.location.href = '/#/games'; } }); popup.querySelector('#main-menu-btn').addEventListener('click', () => { popup.remove(); if (window.app && window.app.getCore().router) { window.app.getCore().router.navigate('/'); } else { window.location.href = '/'; } }); // Close on backdrop click popup.addEventListener('click', (e) => { if (e.target === popup) { popup.remove(); if (window.app && window.app.getCore().router) { window.app.getCore().router.navigate('/games'); // Force content reload by re-emitting navigation event setTimeout(() => { const chapterId = window.currentChapterId || 'sbs'; this._eventBus.emit('navigation:games', { path: `/games/${chapterId}`, data: { path: `/games/${chapterId}` } }, 'Application'); }, 100); } else { window.location.href = '/#/games'; } } }); } // Helper method to convert vocabulary to letters format static _createLettersFromVocabulary(vocabulary) { const letters = {}; Object.entries(vocabulary).forEach(([word, data]) => { const firstLetter = word.charAt(0).toUpperCase(); if (!letters[firstLetter]) { letters[firstLetter] = []; } letters[firstLetter].push({ word: word, translation: data.user_language || data.translation || data, type: data.type || 'word', pronunciation: data.pronunciation }); }); return letters; } } export default LetterDiscovery;