1function googleTranslateElementInit() { 2 new google.translate.TranslateElement({ 3 pageLanguage: 'id', 4 includedLanguages: 'id,en,ar', 5 autoDisplay: false 6 }, 'google_translate_element'); 7} 8 9/** 10 * Custom Translator Script 11 * Handles Protected Terms (notranslate) and RTL Support 12 */ 13const Translator = { 14 protectedTerms: window.PROTECTED_TERMS || [], 15 16 init: function() { 17 this.applyProtection(); 18 19 // Handle RTL and UI sync immediately on load based on saved language 20 const savedLang = localStorage.getItem('user_lang') || 'id'; 21 this.handleRTL(savedLang); 22 this.updateSwitcherUI(savedLang); 23 24 // Watch for DOM changes to apply protection to dynamically loaded content 25 const observer = new MutationObserver((mutations) => { 26 if (this.isProcessing) return; 27 this.applyProtection(); 28 }); 29 30 observer.observe(document.body, { 31 childList: true, 32 subtree: true 33 }); 34 35 // Reinforce hiding the toolbar (Google Translate sometimes forces it back) 36 setInterval(() => { 37 // Reset body/html positioning 38 document.body.style.top = '0px'; 39 document.documentElement.style.marginTop = '0px'; 40 41 // Hide the toolbar iframe and skiptranslate divs 42 document.querySelectorAll('iframe.skiptranslate, body >
42 .skiptranslate, .VIpgJd-ZVi9od-ORHb-OEVmcd').forEach(el => { 43 el.style.display = 'none'; 44 el.style.height = '0'; 45 el.style.visibility = 'hidden'; 46 }); 47 }, 500); 48 }, 49 50 updateSwitcherUI: function(lang) { 51 // Find the link in the submenu that matches the language 52 const subMenuLink = document.querySelector(`.switcher-language .sub-menu a[onclick*="'${lang}'"]`); 53 const mainLink = document.querySelector('.switcher-language > li > a'); 54 55 if (subMenuLink && mainLink) { 56 const subImg = subMenuLink.querySelector('img'); 57 const subSpan = subMenuLink.querySelector('span'); 58 const mainImg = mainLink.querySelector('img'); 59 const mainSpan = mainLink.querySelector('span'); 60 61 if (subImg && mainImg) mainImg.src = subImg.src; 62 63 if (subSpan && mainSpan) { 64 // Get text safely, handling potential font tags from Google Translate 65 let text = subSpan.innerText || subSpan.textContent; 66 67 // If the language is ID, the user might prefer "Bahasa" as the label 68 // but usually syncing it is clearer. 69 mainSpan.innerText = text.trim(); 70 } 71 } 72 }, 73 74 isProcessing: false, 75 76 applyProtection: function() { 77 if (this.protectedTerms.length === 0 || this.isProcessing) return; 78 79 this.isProcessing = true; 80 81 // Sort terms by length descending to catch longer phrases first 82 const sortedTerms = [...this.protectedTerms].sort((a, b) => b.length - a.length); 83 84 const walker = document.createTreeWalker( 85 document.body, 86 NodeFilter.SHOW_TEXT, 87 { 88 acceptNode: function(node) { 89 const parent = node.parentElement; 90 if (!parent) return NodeFilter.FILTER_REJECT; 91 92 const tagName = parent.tagName.toLowerCase(); 93 if (['script', 'style', 'textarea', 'input', 'title', 'meta'].includes(tagName)) 94 return NodeFilter.FILTER_REJECT; 95 96 if (parent.closest('.notranslate')) return NodeFilter.FILTER_REJECT; 97 98 return NodeFilter.FILTER_ACCEPT; 99 } 100 } 101 ); 102 103 let node; 104 const nodesToProcess = []; 105 while (node = walker.nextNode()) { 106 nodesToProcess.push(node); 107 } 108 109 nodesToProcess.forEach(textNode => { 110 if (!textNode.parentNode) return; 111 112 let text = textNode.nodeValue; 113 let fragment = null; 114 let lastIdx = 0; 115 116 // Prepare a combined regex for efficiency, or loop through sorted terms 117 // For precision with word boundaries and case preservation, looping is safer 118 let currentText = text; 119 let parts = [{ type: 'text', val: currentText }]; 120 121 for (const term of sortedTerms) { 122 let newParts = []; 123 // Escape regex characters and replace spaces with \s+ to handle line breaks/multiple spaces 124 const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+'); 125 // Regex to capture optional leading/trailing space around whole words, case-insensitive 126 const regex = new RegExp(`(\\s?)\\b${escapedTerm}\\b(\\s?)`, 'gi'); 127 128 parts.forEach(part => { 129 if (part.type === 'protected') { 130 newParts.push(part); 131 return; 132 } 133 134 let lastMatchIdx = 0; 135 let match; 136 regex.lastIndex = 0; 137 138 while ((match = regex.exec(part.val)) !== null) { 139 // Text before the entire match (including potential leading space) 140 if (match.index > lastMatchIdx) { 141 newParts.push({ type: 'text', val: part.val.substring(lastMatchIdx, match.index) }); 142 } 143 144 // The entire match (preceding space + term + following space) 145 newParts.push({ type: 'protected', val: match[0] }); 146 lastMatchIdx = regex.lastIndex; 147 } 148 149 // Remaining text 150 if (lastMatchIdx < part.val.length) { 151 newParts.push({ type: 'text', val: part.val.substring(lastMatchIdx) }); 152 } 153 }); 154 parts = newParts; 155 } 156 157 // Only replace if we actually found matches 158 if (parts.length > 1 || (parts.length === 1 && parts[0].type === 'protected')) { 159 const fragment = document.createDocumentFragment();
160 parts.forEach(part => { 161 if (part.type === 'protected') { 162 const span = document.createElement('span'); 163 span.className = 'notranslate'; 164 span.setAttribute('translate', 'no'); 165 span.textContent = part.val; 166 fragment.appendChild(span); 167 } else { 168 fragment.appendChild(document.createTextNode(part.val)); 169 } 170 }); 171 textNode.parentNode.replaceChild(fragment, textNode); 172 } 173 }); 174 175 this.isProcessing = false; 176 }, 177 178 changeLanguage: function(lang) { 179 // Save the choice for persistence 180 if (lang === 'id') { 181 localStorage.removeItem('user_lang'); 182 } else { 183 localStorage.setItem('user_lang', lang); 184 } 185 186 // Always set cookies for Google Translate (needed for reload-based fallback) 187 if (lang === 'id') { 188 document.cookie = 'googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; 189 document.cookie = 'googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.' + location.hostname + ';'; 190 document.cookie = 'googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=' + location.hostname + ';'; 191 } else { 192 var cookieValue = '/id/' + lang; 193 document.cookie = 'googtrans=' + cookieValue + '; path=/;'; 194 document.cookie = 'googtrans=' + cookieValue + '; path=/; domain=.' + location.hostname + ';'; 195 document.cookie = 'googtrans=' + cookieValue + '; path=/; domain=' + location.hostname + ';'; 196 } 197 198 // Try to trigger Google Translate's select element directly 199 if (!this._triggerGoogleTranslate(lang)) { 200 // If Google Translate widget isn't ready, apply RTL and reload 201 this.handleRTL(lang); 202 location.reload(); 203 } 204 }, 205 206 /** 207 * Programmatically trigger the Google Translate select element. 208 * Returns true if successful, false if the widget is not yet available. 209 */ 210 _triggerGoogleTranslate: function(lang) { 211 var select = document.querySelector('.goog-te-combo'); 212 if (select) { 213 select.value = lang; 214 select.dispatchEvent(new Event('change')); 215 this.handleRTL(lang); 216 this.updateSwitcherUI(lang); 217 return true; 218 } 219 220 // Also try the iframe-based approach 221 var frame = document.querySelector('.goog-te-menu-frame'); 222 if (frame) { 223 try { 224 var innerDoc = frame.contentDocument || frame.contentWindow.document; 225 var items = innerDoc.querySelectorAll('.goog-te-menu2-item span.text'); 226 var langMap = { 'en': 'English', 'ar': 'Ø§ÙØ¹Ø±Ø¨ÙØ©', 'id': 'Indonesia' }; 227 var targetText = langMap[lang]; 228 for (var i = 0; i < items.length; i++) { 229 if (items[i].textContent === targetText) { 230 items[i].click(); 231 this.handleRTL(lang); 232 this.updateSwitcherUI(lang); 233 return true; 234 } 235 } 236 } catch(e) { 237 // Cross-origin iframe, fall through to reload 238 } 239 } 240 241 return false; 242 }, 243 244 /** 245 * Try to apply saved language on page load. 246 * Retries a few times since Google Translate loads asynchronously. 247 */ 248 _applySavedLanguage: function() { 249 var savedLang = localStorage.getItem('user_lang'); 250 if (!savedLang || savedLang === 'id') return; 251 252 var self = this; 253 var attempts = 0; 254 var maxAttempts = 20; 255 256 var interval = setInterval(function() { 257 attempts++; 258 if (self._triggerGoogleTranslate(savedLang)) { 259 clearInterval(interval); 260 } else if (attempts >= maxAttempts) { 261 clearInterval(interval); 262 } 263 }, 500); 264 }, 265 266 handleRTL: function(lang) { 267 if (lang === 'ar') { 268 document.documentElement.setAttribute('dir', 'rtl'); 269 document.documentElement.setAttribute('lang', 'ar'); 270 document.body.classList.add('rtl-mode'); 271 } else { 272 document.documentElement.setAttribute('dir', 'ltr'); 273 document.documentElement.setAttribute('lang', lang === 'id' ? 'id' : lang); 274 document.body.classList.remove('rtl-mode'); 275 } 276 } 277}; 278 279document.addEventListener('DOMContentLoaded', function() { 280 Translator.init(); 281 Translator._applySavedLanguage(); 282}); 283
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.