PageSourceSearch

https://flowmuse.ai/js/i18n.js

js flowmuse.ai collected 2026-10-05 11:59:50 UTC 11,587 bytes, 383 lines download raw bytes

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.