const phpInitialLang = ''; let currentEntries = {}; const SUGGESTION_LIMIT_DISPLAY = 20; const searchCache = new Map(); const MAX_HISTORY_ITEMS = 50; const symbolMapping = { "[N1]": "/اسم/", "[N2]": "/صفة/", "[N3]": "/ظرف/", "[N4]": "/ضمير/", "[N5]": "/اسم الإشارة/", "[N6]": "/اسم الموصول/", "[N7]": "/كلمة مركبة/", "[N8]": "/اسم فعل/", "[N9]": "/علم/", "[V1]": "/فعل مجرد/", "[V2]": "/فعل مزيد/", "[V3]": "/فعل جامد/", "[P1]": "/حرف/", "[P2]": "/عبارة/", "[P3]": "/اسم استفهام/", "[P4]": "/عدد/", "[P5]": "/اسم شرط/", "[P6]": "/مصدر/", "[S7]": "/تركيب وصفي/", "[D2]": "/مصطلح/" }; const THEME_KEY = 'zotoThemePreference'; const LANGUAGE_KEY = 'zotoLanguagePreference'; let currentLang = phpInitialLang; let activeAudioElement = null; function getUIText(key) { const element = document.querySelector(`[data-key="${key}"]`) || document.getElementById(key); if (element) { return element.dataset[currentLang] || element.dataset['fr'] || element.textContent; } switch (key) { case 'copied': return currentLang === 'ar' ? "تم النسخ!" : (currentLang === 'mg' ? "Efa voadika!" : "Copié !"); case 'copy_error': return currentLang === 'ar' ? "خطأ في النسخ" : (currentLang === 'mg' ? "Misy tsy fetezana ny fandikana" : "Erreur de copie"); case 'copy_failed': return currentLang === 'ar' ? "فشل النسخ" : (currentLang === 'mg' ? "Tsy nahomby ny fandikana" : "Échec copie"); case 'confirm_clear_history': return currentLang === 'ar' ? "هل تريد بالتأكيد مسح كل السجل؟" : (currentLang === 'mg' ? "Tena hofafanaina daholo ve ny raki-pikarohana?" : "Voulez-vous vraiment effacer tout l'historique ?"); case 'history_cleared': return currentLang === 'ar' ? "تم مسح السجل." : (currentLang === 'mg' ? "Efa voafafa ny raki-pikarohana." : "Historique effacé."); case 'history_save_error': return currentLang === 'ar' ? "خطأ في حفظ السجل" : (currentLang === 'mg' ? "Misy tsy fetezana ny fampidirana ny raki-pikarohana" : "Erreur sauvegarde historique"); case 'history_empty': return currentLang === 'ar' ? "السجل فارغ" : (currentLang === 'mg' ? "Foana ny raki-pikarohana" : "L'historique est vide"); case 'tts_unavailable': return currentLang === 'ar' ? "النطق الصوتي غير متوفر" : (currentLang === 'mg' ? "Tsy misy feon-drindrambaiko ny navigateranao" : "Synthèse vocale indisponible"); case 'tts_error': return currentLang === 'ar' ? "خطأ في القراءة" : (currentLang === 'mg' ? "Misy tsy fetezana ny famakiana" : "Erreur de lecture"); case 'suggestion_error': return currentLang === 'ar' ? 'خطأ في الاقتراحات' : (currentLang === 'mg' ? 'Misy tsy fetezana ny sosokevitra' : 'Erreur suggestions'); case 'loading': return currentLang === 'ar' ? 'جار التحميل...' : (currentLang === 'mg' ? 'Eo am-pitelesarizena azy...' : 'Chargement...'); case 'no_results_for': return currentLang === 'ar' ? 'لم يتم العثور على نتائج لـ' : (currentLang === 'mg' ? 'Tsy misy vokatra hita ho an\'ny' : 'Aucun résultat trouvé pour'); case 'results_found': return (count) => currentLang === 'ar' ? `${count} ${count === 1 ? 'نتيجة' : (count === 2 ? 'نتيجتان' : 'نتائج')} لـ ` : (currentLang === 'mg' ? `${count} ny vokatra hita ho an'ny ` : `${count} résultat${count > 1 ? 's' : ''} pour `); case 'dictionary_not_loaded': return currentLang === 'ar' ? 'لم يتم تحميل القاموس.' : (currentLang === 'mg' ? 'Tsy tafiditra ny rakibolana.' : 'Dictionnaire non chargé.'); case 'definition_fetch_error': return currentLang === 'ar' ? 'خطأ في استرداد التعريف (التعريفات)' : (currentLang === 'mg' ? 'Misy tsy fetezana ny fakana famaritana' : 'Erreur récupération définition(s)'); case 'searching': return currentLang === 'ar' ? 'جار البحث...' : (currentLang === 'mg' ? 'Eo am-pitadiavana...' : 'Recherche...'); case 'search_error': return currentLang === 'ar' ? 'خطأ أثناء البحث' : (currentLang === 'mg' ? 'Misy tsy fetezana ny fikarohana' : 'Erreur lors de la recherche'); case 'listen': return currentLang === 'ar' ? 'استماع' : (currentLang === 'mg' ? 'Henoina' : 'Écouter'); case 'remove_item': return currentLang === 'ar' ? 'حذف هذا العنصر' : (currentLang === 'mg' ? 'Fafaina ity singa ity' : 'Supprimer cet élément'); case 'local_storage_warning': return currentLang === 'ar' ? "تحذير: الحفظ المحلي قد يفشل." : (currentLang === 'mg' ? "Fampitandremana: Mety tsy hahomby ny fitehirizana anaty." : "Attention: Sauvegarde locale pourrait échouer."); case 'local_storage_error': return currentLang === 'ar' ? "خطأ: الحفظ المحلي غير متوفر." : (currentLang === 'mg' ? "Tsy fetezana: Tsy misy ny fitehirizana anaty." : "Erreur: Sauvegarde locale indisponible."); case 'critical_error_startup': return currentLang === 'ar' ? 'خطأ في بدء تشغيل التطبيق' : (currentLang === 'mg' ? 'Misy tsy fetezana ny fandefasana ny rindrankajy' : 'Erreur Démarrage Application'); case 'preparing_app': return currentLang === 'ar' ? 'جار تحضير التطبيق...' : (currentLang === 'mg' ? 'Eo am-panomanana azy...' : 'Préparation application...'); case 'homeButton': return currentLang === 'ar' ? 'الرئيسية' : (currentLang === 'mg' ? 'Pejy fandraisana' : 'Accueil'); default: return key; } } function debounce(func, delay) { let t; return function (...a) { clearTimeout(t); t = setTimeout(() => func.apply(this, a), delay); }; } function removeAccents(str) { if(!str) return ''; try { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } catch (e) { return str.replace(/[àáâäãåā]/g, "a").replace(/[èéêëēėę]/g, "e").replace(/[îïíīįì]/g, "i").replace(/[ôöòóøōõ]/g, "o").replace(/[ûüùúū]/g, "u").replace(/[çćč]/g, "c"); } } function replaceSymbols(text) { if(!text) return ''; let r = text; for (const s in symbolMapping) { r = r.replace(s, symbolMapping[s]); } return r; } function removeParenthesesWords(text) { if(!text) return ''; return text.replace(/(?{t.classList.remove('show');}, duration); } function highlightSimple(text, searchTerm) { if (!text || !searchTerm) return document.createTextNode(text || ''); const fragment = document.createDocumentFragment(); const normalizedSearchTerm = removeAccents(searchTerm.toLowerCase()); const escapedTerm = normalizedSearchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const searchRegex = new RegExp(`(${escapedTerm})`, 'gi'); let lastIndex = 0; let match; const normalizedText = removeAccents(text.toLowerCase()); while ((match = searchRegex.exec(normalizedText)) !== null) { const startIndex = match.index; const matchedLength = match[1].length; const originalMatchedText = text.substring(startIndex, startIndex + matchedLength); if (startIndex > lastIndex) { fragment.appendChild(document.createTextNode(text.substring(lastIndex, startIndex))); } const span = document.createElement('span'); span.className = 'highlight'; span.textContent = originalMatchedText; fragment.appendChild(span); lastIndex = startIndex + matchedLength; searchRegex.lastIndex = lastIndex; } if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.substring(lastIndex))); } return fragment; } function updateURL(searchTerm) { try { const base = window.location.pathname; const params = new URLSearchParams(window.location.search); params.set('lang', currentLang); if (searchTerm) { params.set('query', searchTerm); } else { params.delete('query'); } const newQueryString = params.toString(); const newUrl = base + (newQueryString ? '?' + newQueryString : ''); if (window.location.href !== window.location.origin + newUrl && history.pushState) { history.pushState({ path: newUrl, searchTerm: searchTerm, lang: currentLang }, '', newUrl); } } catch (e) { console.warn("Échec mise à jour URL:", e)} } const searchHistory = { items: [], init() { try { const s = localStorage.getItem('zotoSearchHistory'); if (s) { try { this.items = JSON.parse(s); } catch (e) { console.error('Erreur chargement historique:', e); this.items = []; localStorage.removeItem('zotoSearchHistory');} } } catch (e) { console.error("LocalStorage indisponible pour historique:", e); } const hb = document.getElementById('historyButton'); const ch = document.getElementById('closeHistory'); const cl = document.getElementById('clearHistory'); const hp = document.getElementById('historyPopup'); if(hb) hb.addEventListener('click', () => this.showHistory()); if(ch) ch.addEventListener('click', () => this.hideHistory()); if(cl) cl.addEventListener('click', () => this.clearHistory()); if(hp) hp.addEventListener('click', (e) => { if(e.target === hp) this.hideHistory(); }); this.updateHistoryTexts(); }, updateHistoryTexts() { updateElementTexts({'historyTitle': {fr:"Historique de recherche",ar:"سجل البحث",mg:"Raki-pikarohana"},'clearHistory': {fr:"Effacer tout",ar:"مسح الكل",mg:"Fafaina daholo"},'#historyContent .history-empty':{fr:"Votre historique est vide",ar:"السجل فارغ",mg:"Foana ny raki-pikarohana"},'closeHistory':{frTitle:"Fermer",arTitle:"إغلاق",mgTitle:"Akatona"},'historyButton':{frTitle:"Historique",arTitle:"سجل البحث",mgTitle:"Raki-pikarohana"}}); document.querySelectorAll('.history-item-remove').forEach(b=>b.title=getUIText('remove_item')); }, addItem(word, flag) { if (!word || word.length < 1 || !flag) return; const lw = word.toLowerCase(); this.items = this.items.filter(i => i.word.toLowerCase() !== lw); this.items.unshift({ word: word, flag: flag, timestamp: Date.now() }); if (this.items.length > MAX_HISTORY_ITEMS) this.items = this.items.slice(0, MAX_HISTORY_ITEMS); this.saveToLocalStorage(); }, removeItem(word) { this.items = this.items.filter(i => i.word !== word); this.saveToLocalStorage(); this.renderHistory(); }, clearHistory() { if (confirm(getUIText('confirm_clear_history'))) { this.items = []; this.saveToLocalStorage(); this.renderHistory(); showToast(getUIText('history_cleared')); this.hideHistory(); } }, saveToLocalStorage() { try { localStorage.setItem('zotoSearchHistory', JSON.stringify(this.items)); } catch (e) { console.error('Erreur sauvegarde historique:', e); showToast(getUIText('history_save_error')); } }, formatTimestamp(t) { try { const d=new Date(t);const n=new Date();const diff=Math.floor((n-d)/864e5);const l=currentLang==='mg'?'fr-FR':(currentLang==='ar'?'ar-SA':'fr-FR');const tf={hour:'2-digit',minute:'2-digit',hour12:!1};const df={day:'2-digit',month:'2-digit',year:'numeric'};const ts=d.toLocaleTimeString(l,tf);if(diff===0){const td=currentLang==='ar'?'اليوم':(currentLang==='mg'?'Anio':'Auj.');return `${td} ${ts}`;}if(diff===1){const yd=currentLang==='ar'?'أمس':(currentLang==='mg'?'Omaly':'Hier');return `${yd} ${ts}`;}return d.toLocaleDateString(l,df);} catch (e) { return "Date invalide"; } }, showHistory() { this.renderHistory(); const p = document.getElementById('historyPopup'); if(p) p.classList.add('show'); }, hideHistory() { const p = document.getElementById('historyPopup'); if(p) p.classList.remove('show'); }, renderHistory() { const hc=document.getElementById('historyContent'); if(!hc)return; hc.innerHTML=''; const rt = getUIText('remove_item'); const cl=document.getElementById('clearHistory'); if(this.items.length===0){const e=document.createElement('div'); e.className='history-empty'; e.textContent=getUIText('history_empty'); hc.appendChild(e); if(cl)cl.disabled=!0; return;} if(cl)cl.disabled=!1; this.items.forEach(i=>{const d=document.createElement('div');d.className='history-item';d.innerHTML=`${i.flag}${i.word}${this.formatTimestamp(i.timestamp)}`;d.addEventListener('click',e=>{if(!e.target.classList.contains('history-item-remove')){fetchEntriesForTerm(i.word);this.hideHistory();}});const r=d.querySelector('.history-item-remove');if(r)r.onclick=e=>{e.stopPropagation();this.removeItem(i.word);};hc.appendChild(d);}); this.updateHistoryTexts(); } }; function speakText(text, buttonElement) { if(typeof SpeechSynthesisUtterance === 'undefined') return showToast(getUIText('tts_unavailable')); speechSynthesis.cancel(); const replacements = { 'ـعَـ\\،': 'فِعْلُ مَاضٍ ثلاثِي، ومُضارعه يأتي على وزن يَفْعَلُ.', 'ـعُـ\\،': 'فِعْلُ مَاضٍ ثلاثِي، ومُضارعه يأتي على وزن يَفْعُلُ.', 'ـعِـ\\،': 'فِعْلُ مَاضٍ ثلاثِي، ومُضارعه يأتي على وزن يَفْعِلُ.', 'س\\.': 'أما مصدره فَسَمَاعِيّ وهو.', 'ج\\.': 'وجمعه.', 'ج': 'جمع', 'مؤ\\.': 'ومؤنثه.', 'مؤ': 'مؤنث', 'مع\\.': 'بمعنى.', '~ة': 'وتُؤَنَّثُ بإضافة تاء التأنيث.', '~ون': 'وَتُجْمَع جَمْعًا مذكَّرًا سالما.', '~ات': 'وَتُجْمَع جَمْعًا مؤنَّثا سالما.', 'م{3}': 'اسم مؤنث مجازي مجرّد من علامة التأنيث، ولا يجوز أن يُذَكَّر.', 'ممذ': 'اسم مؤنث مجازي مجرد من علامة التأنيث، ويجوز أن يُذَكَّر.', 'مذمؤ': 'اسم مذكَّر، ويجوز أن يُؤَنَّث.', 'مذمذ': 'اسم مذكَّر رغم وجود علامة التأنيث فيه.' }; let pText = text; for (const [s, r] of Object.entries(replacements)) { pText = pText.replace(new RegExp(s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'), r); } const utt = new SpeechSynthesisUtterance(pText); utt.lang = /[ء-ي]/.test(text) ? 'ar-SA' : (currentLang === 'mg' ? 'fr-FR' : 'fr-FR'); utt.rate = 0.8; utt.pitch = 1; utt.volume = 1; if (activeAudioElement && activeAudioElement !== buttonElement) activeAudioElement.classList.remove('playing'); if (buttonElement) { utt.onstart = () => { buttonElement.classList.add('playing'); activeAudioElement = buttonElement; }; utt.onend = () => { buttonElement.classList.remove('playing'); activeAudioElement = null; }; utt.onerror = e => { console.error('TTS Error:', e.error); buttonElement.classList.remove('playing'); activeAudioElement = null; showToast(getUIText('tts_error')); }; } else { utt.onerror = e => { console.error('TTS Error:', e.error); showToast(getUIText('tts_error')); }; } speechSynthesis.speak(utt); } const settingsManager = { currentTheme: 'light', settingsButton: null, settingsPopup: null, closeSettingsButton: null, themeOptions: [], languageOptions: [], init() { try { this.settingsButton = document.getElementById('settingsButton'); this.settingsPopup = document.getElementById('settingsPopup'); this.closeSettingsButton = this.settingsPopup?.querySelector('.close-settings'); this.themeOptions = this.settingsPopup?.querySelectorAll('.theme-option[data-theme]') || []; this.languageOptions = this.settingsPopup?.querySelectorAll('.language-option[data-lang]') || []; if (!this.settingsButton || !this.settingsPopup || !this.closeSettingsButton) return; this.settingsButton.addEventListener('click', e => { e.stopPropagation(); this.showSettings(); }); this.closeSettingsButton.addEventListener('click', () => this.hideSettings()); document.addEventListener('click', e => { if (this.settingsPopup.classList.contains('show') && !this.settingsPopup.contains(e.target) && e.target !== this.settingsButton && !this.settingsButton.contains(e.target)) this.hideSettings(); }); this.themeOptions.forEach(o => { o.addEventListener('click', () => { const t = o.dataset.theme; if (t) this.applyTheme(t); }); }); this.languageOptions.forEach(o => { o.addEventListener('click', () => { const l = o.dataset.lang; if (l) { applyLanguage(l); this.hideSettings(); } }); }); this.loadInitialTheme(); this.updateSettingsPopupUI(); } catch (error) { console.error("Error init Settings Manager:", error); } }, showSettings() { this.updateSettingsPopupUI(); this.settingsPopup.classList.add('show'); }, hideSettings() { this.settingsPopup.classList.remove('show'); }, updateSettingsPopupUI() { this.themeOptions.forEach(o => o.classList.toggle('active', o.dataset.theme === this.currentTheme)); this.languageOptions.forEach(o => o.classList.toggle('active', o.dataset.lang === currentLang)); updateElementTexts({ '.settings-popup-header h4': {fr:"Paramètres",ar:"الإعدادات",mg:"Parametatra"}, '.close-settings': {frTitle:"Fermer",arTitle:"إغلاق",mgTitle:"Akatona"}, '.settings-section-title[data-fr="Thème d\'affichage"]': {fr:"Thème d'affichage",ar:"سمة العرض",mg:"Lokon'ny efijery"}, '.settings-section-title[data-fr="Langue de l\'interface"]': {fr:"Langue de l'interface",ar:"لغة الواجهة",mg:"Fiteny ampiasaina"}, '#lightThemeOption .theme-option-text': {fr:"Mode clair",ar:"الوضع الفاتح",mg:"Efijery mazava"}, '#darkThemeOption .theme-option-text': {fr:"Mode sombre",ar:"الوضع الداكن",mg:"Efijery maizina"}, '#softThemeOption .theme-option-text': {fr:"Mode doux",ar:"الوضع اللطيف",mg:"Efijery malefaka"} }); updateElementTexts({ '#settingsButton': {frTitle:"Paramètres",arTitle:"الإعدادات",mgTitle:"Parametatra"} }); }, applyTheme(theme) { if (!['light', 'dark', 'soft'].includes(theme)) theme = 'light'; const b = document.body; b.classList.remove('dark-mode', 'soft-mode'); if (theme === 'dark') b.classList.add('dark-mode'); else if (theme === 'soft') b.classList.add('soft-mode'); this.currentTheme = theme; try { localStorage.setItem(THEME_KEY, theme); } catch (e) {} this.updateSettingsPopupUI(); this.hideSettings(); }, loadInitialTheme() { let p = 'light'; try { const s = localStorage.getItem(THEME_KEY); if (s && ['light', 'dark', 'soft'].includes(s)) p = s; else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) p = 'dark'; } catch (e) {} const b = document.body; b.classList.remove('dark-mode', 'soft-mode'); if (p === 'dark') b.classList.add('dark-mode'); else if (p === 'soft') b.classList.add('soft-mode'); this.currentTheme = p; } }; function applyLanguage(langCode) { if (!['fr', 'ar', 'mg'].includes(langCode)) langCode = 'ar'; currentLang = langCode; const h = document.documentElement; const b = document.body; h.lang = langCode; b.classList.remove('rtl', 'fr', 'mg'); b.removeAttribute('dir'); if (langCode === 'ar') { b.dir = 'rtl'; b.classList.add('rtl'); } else { b.dir = 'ltr'; if (langCode === 'fr') b.classList.add('fr'); else if (langCode === 'mg') b.classList.add('mg'); } try { localStorage.setItem(LANGUAGE_KEY, langCode); } catch (e) {} updateUITexts(); settingsManager.updateSettingsPopupUI(); if (Object.keys(currentEntries).length > 0) { const searchTerm = document.getElementById('search')?.value || ''; const firstKey = Object.keys(currentEntries)[0]; const firstEntry = currentEntries[firstKey]; const flag = firstEntry ? getFlag(firstEntry.teny) : '❓'; displayMultipleEntries(currentEntries, searchTerm, flag); } } function loadInitialLanguage() { let langToApply = phpInitialLang; try { const savedLang = localStorage.getItem(LANGUAGE_KEY); if (savedLang && ['fr', 'ar', 'mg'].includes(savedLang)) { langToApply = savedLang; } } catch (e) { console.warn("LocalStorage non accessible pour la langue."); } applyLanguage(langToApply); } function displayMultipleEntries(entriesData, searchTermUsedForSearch, suggestionFlag) { const dictionaryDiv = document.getElementById('dictionary'); if (!dictionaryDiv) return; dictionaryDiv.textContent = ''; currentEntries = entriesData || {}; const entriesArray = Object.entries(currentEntries); const hasResults = entriesArray.length > 0; const flagToAdd = suggestionFlag || (searchTermUsedForSearch ? getFlag(searchTermUsedForSearch) : '❓'); if (searchTermUsedForSearch) { if (hasResults) { searchHistory.addItem(searchTermUsedForSearch, flagToAdd); updateURL(searchTermUsedForSearch); const rc = document.createElement('div'); rc.className = 'results-count'; const count = entriesArray.length; const rtf = getUIText('results_found'); rc.textContent = rtf(count) + `"${searchTermUsedForSearch}"`; dictionaryDiv.appendChild(rc); } else { dictionaryDiv.innerHTML = `