1/** 2 * i18n Translation System for Static HTML Pages 3 * Loads and applies translations based on language preference 4 */ 5(function() { 6 'use strict'; 7 8 let translations = {}; 9 let currentLang = 'en'; 10 11 /** 12 * Get current language 13 */ 14 function getCurrentLang() { 15 try { 16 // ä¼å ä» URL åæ°è·å 17 const urlParams = new URLSearchParams(window.location.search); 18 const langParam = urlParams.get('lang'); 19 if (langParam) { 20 return langParam.toLowerCase(); 21 } 22 23 // ä» localStorage è·å 24 const storedLang = localStorage.getItem('locale'); 25 if (storedLang) { 26 return storedLang.toLowerCase(); 27 } 28 29 // ä» cookie è·å 30 const cookies = document.cookie.split(';'); 31 for (let cookie of cookies) { 32 const [name, value] = cookie.trim().split('='); 33 if (name === 'locale' && value) { 34 return value.toLowerCase(); 35 } 36 } 37 38 // 仿µè§å¨è¯è¨è·å 39 const browserLang = navigator.language || navigator.userLanguage; 40 return browserLang.toLowerCase().startsWith('zh') ? 'zh' : 'en'; 41 } catch (e) { 42 return 'en'; 43 } 44 } 45 46 /** 47 * Load translation file 48 */ 49 async function loadTranslations(lang) { 50 try { 51 const response = await fetch(`/locales/${lang}.json`, { 52 headers: { 53 'Accept': 'application/json', 54 'Cache-Control': 'no-cache' 55 } 56 }); 57 58 if (!response.ok) { 59 throw new Error(`HTTP ${response.status}: ${response.statusText}`); 60 } 61 62 const data = await response.json(); 63 64 // éªè¯è¿åçæ¯ææç JSON 对象 65 if (typeof data !== 'object' || data === null) { 66 throw new Error('Invalid JSON format'); 67 } 68 69 return data; 70 } catch (error) { 71 console.error(`Failed to load translations for ${lang}:`, error); 72 73 // Fallback to Chinese if English fails 74 if (lang !== 'zh') { 75 try { 76 const response = await fetch('/locales/zh.json', { 77 headers: { 78 'Accept': 'application/json', 79 'Cache-Control': 'no-cache' 80 } 81 }); 82 if (response.ok) { 83 const data = await response.json(); 84 if (typeof data === 'object' && data !== null) { 85 console.warn(`Using Chinese translations as fallback for ${lang}`); 86 return data; 87 } 88 } 89 } catch (e) { 90 console.error('Failed to load fallback translations:', e); 91 } 92 } 93 94 // Return empty object if all fails 95 console.warn('No translations loaded, using empty object'); 96 return {}; 97 } 98 } 99 100 /** 101 * Get translation by key path (e.g., "header.login" or "services.creator.tags.0") 102 */ 103 function t(key, defaultValue = '') { 104 const keys = key.split('.'); 105 let value = translations; 106 107 for (const k of keys) { 108 if (value && typeof value === 'object') { 109 // Handle array indices 110 if (Array.isArray(value) && /^\d+$/.test(k)) { 111 const index = parseInt(k, 10); 112 if (index >= 0 && index < value.length) { 113 value = value[index]; 114 } else { 115 return defaultValue || key; 116 } 117 } else if (k in value) { 118 value = value[k]; 119 } else { 120 return defaultValue || key; 121 } 122 } else { 123 return defaultValue || key; 124 } 125 } 126 127 return value || defaultValue || key; 128 } 129 130 /** 131 * Reinitialize SplitType animations for reveal-type elements 132 * Only applies animation for Chinese, English shows plain text 133 */ 134 function reinitRevealTypeAnimations() { 135 // Check if SplitType and gsap are available 136 if (typeof SplitType === 'undefined' || typeof gsap === 'undefined' || typeof ScrollTrigger === 'undefined') { 137 // Wait a bit and try again if libraries are still loading 138 setTimeout(() => { 139 if (typeof SplitType !== 'undefined' && typeof gsap !== 'undefined' && typeof ScrollTrigger !== 'undefined') { 140 reinitRevealTypeAnimations(); 141 } 142 }, 100); 143 return; 144 } 145 146 // Only apply animation for Chinese, skip for English 147 if (currentLang !== 'zh') { 148 // For English, just ensure content is displayed without animation 149 const revealElements = document.querySelectorAll('.reveal-type[data-i18n]');
150 revealElements.forEach((element) => { 151 // Kill any existing ScrollTrigger instances 152 const triggers = ScrollTrigger.getAll().filter(trigger => { 153 return trigger.vars && trigger.vars.trigger === element; 154 }); 155 triggers.forEach(trigger => trigger.kill()); 156 157 // Clean up SplitType structure and restore plain text 158 const splitWrapper = element.querySelector('.split-type'); 159 if (splitWrapper) { 160 const key = element.getAttribute('data-i18n'); 161 const hasHtml = element.hasAttribute('data-i18n-html'); 162 if (key) { 163 const translation = t(key); 164 if (translation) { 165 if (hasHtml) { 166 element.innerHTML = translation; 167 } else { 168 element.textContent = translation; 169 } 170 } 171 } 172 } 173 }); 174 return; 175 } 176 177 // Find all reveal-type elements that have been translated 178 const revealElements = document.querySelectorAll('.reveal-type[data-i18n]'); 179 180 revealElements.forEach((element) => { 181 try { 182 // Kill existing ScrollTrigger instances for this element 183 const triggers = ScrollTrigger.getAll().filter(trigger => { 184 return trigger.vars && trigger.vars.trigger === element; 185 }); 186 triggers.forEach(trigger => trigger.kill()); 187 188 // Always restore the translated content before reinitializing SplitType 189 // Get translation key and current translation 190 const key = element.getAttribute('data-i18n'); 191 const hasHtml = element.hasAttribute('data-i18n-html'); 192 193 if (key) { 194 // Get the current translation (should already be applied, but ensure it's correct) 195 const translation = t(key); 196 197 if (translation) { 198 // Always restore the translated HTML/text content 199 if (hasHtml) { 200 element.innerHTML = translation; 201 } else { 202 element.textContent = translation; 203 } 204 } 205 } 206 207 // Reinitialize SplitType with the current (translated) content 208 const text = new SplitType(element, { types: 'words, chars' }); 209 210 // Reinitialize GSAP animation 211 if (text.chars && text.chars.length > 0) { 212 gsap.from(text.chars, { 213 scrollTrigger: { 214 trigger: element, 215 start: 'top 80%', 216 end: 'top 20%', 217 scrub: true, 218 markers: false 219 }, 220 opacity: 0.2, 221 stagger: 0.1 222 }); 223 } 224 } catch (e) { 225 console.warn('Failed to reinitialize SplitType for element:', e); 226 } 227 }); 228 } 229 230 /** 231 * Apply translations to elements with data-i18n attribute 232 */ 233 function applyTranslations() { 234 // Store elements that need animation reinit 235 const needsReinit = []; 236 237 // Apply to elements with data-i18n attribute 238 document.querySelectorAll('[data-i18n]').forEach(element => { 239 const key = element.getAttribute('data-i18n'); 240 const translation = t(key); 241 242 if (translation) { 243 // Check if element needs animation reinit 244 if (element.classList.contains('reveal-type')) { 245 needsReinit.push(element); 246 } 247 248 // Check if element should preserve HTML 249 if (element.hasAttribute('data-i18n-html')) { 250 element.innerHTML = translation; 251 } else { 252 element.textContent = translation; 253 } 254 } 255 }); 256 257 // Apply to elements with data-i18n-attr (for attributes like placeholder, title, etc.) 258 document.querySelectorAll('[data-i18n-attr]').forEach(element => { 259 const attrData = element.getAttribute('data-i18n-attr'); 260 const [attr, key] = attrData.split(':'); 261 const translation = t(key); 262 263 if (translation && attr) { 264 element.setAttribute(attr, translation); 265 } 266 }); 267 268 // Reinitialize animations after a short delay to ensure DOM is updated 269 if (needsReinit.length > 0) { 270 setTimeout(() => { 271 reinitRevealTypeAnimations(); 272 }, 50); 273 } 274 } 275 276 /** 277 * Initialize i18n system 278 */ 279 async function initI18n() { 280 currentLang = getCurrentLang(); 281 translations = await loadTranslations(currentLang); 282 283 // Update HTML lang attribute 284 document.documentElement.setAttribute('lang', currentLang === 'zh' ? 'zh-CN' : 'en'); 285 286 // Apply translations 287 applyTranslations(); 288 289 // Update language toggle button 290 updateLangToggleButton(); 291 292 // Dispatch custom event for other scripts 293 window.dispatchEvent(new CustomEvent('i18n:loaded', { 294 detail: { lang: currentLang, translations } 295 })); 296 } 297 298 /** 299 * Change language 300 */ 301 async function changeLanguage(lang) { 302 currentLang = lang; 303 translations = await loadTranslations(lang); 304 305 // Update HTML lang attribute 306 document.documentElement.setAttribute('lang', lang === 'zh' ? 'zh-CN' : 'en'); 307 308 // Apply translations 309 applyTranslations(); 310 311 // Update language toggle button text if exists 312 updateLangToggleButton(); 313 314 // Dispatch custom event
315 window.dispatchEvent(new CustomEvent('i18n:changed', { 316 detail: { lang, translations } 317 })); 318 319 return Promise.resolve(); 320 } 321 322 /** 323 * Update language toggle button text 324 */ 325 function updateLangToggleButton() { 326 const btn = document.getElementById('lang-toggle-btn'); 327 const textSpan = btn?.querySelector('.lang-toggle-text'); 328 329 if (btn && textSpan) { 330 const currentLang = getCurrentLang(); 331 textSpan.textContent = currentLang.startsWith('zh') ? 'EN' : '䏿'; 332 } 333 } 334 335 // Export to window for global access 336 window.i18n = { 337 t, 338 getCurrentLang, 339 changeLanguage, 340 initI18n, 341 updateLangToggleButton, 342 applyTranslations: (container) => { 343 if (container) { 344 // Apply translations to a specific container 345 container.querySelectorAll('[data-i18n]').forEach(element => { 346 const key = element.getAttribute('data-i18n'); 347 const translation = t(key); 348 349 if (translation) { 350 if (element.hasAttribute('data-i18n-html')) { 351 element.innerHTML = translation; 352 } else { 353 element.textContent = translation; 354 } 355 } 356 }); 357 358 container.querySelectorAll('[data-i18n-attr]').forEach(element => { 359 const attrData = element.getAttribute('data-i18n-attr'); 360 const [attr, key] = attrData.split(':'); 361 const translation = t(key); 362 363 if (translation && attr) { 364 element.setAttribute(attr, translation); 365 } 366 }); 367 } else { 368 // Apply to entire document 369 applyTranslations(); 370 } 371 }, 372 get translations() { return translations; }, 373 get lang() { return currentLang; } 374 }; 375 376 // Auto-initialize when DOM is ready 377 if (document.readyState === 'loading') { 378 document.addEventListener('DOMContentLoaded', initI18n); 379 } else { 380 initI18n(); 381 } 382})(); 383
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.