|
| 1 | +// Fula API Documentation - Internationalization (i18n) System |
| 2 | +// Languages: English, Chinese, Farsi, Arabic, German, Spanish, Hindi, French |
| 3 | + |
| 4 | +const SUPPORTED_LANGUAGES = { |
| 5 | + en: { name: 'English', nativeName: 'English', dir: 'ltr' }, |
| 6 | + zh: { name: 'Chinese', nativeName: '中文', dir: 'ltr' }, |
| 7 | + fa: { name: 'Farsi', nativeName: 'فارسی', dir: 'rtl' }, |
| 8 | + ar: { name: 'Arabic', nativeName: 'العربية', dir: 'rtl' }, |
| 9 | + de: { name: 'German', nativeName: 'Deutsch', dir: 'ltr' }, |
| 10 | + es: { name: 'Spanish', nativeName: 'Español', dir: 'ltr' }, |
| 11 | + hi: { name: 'Hindi', nativeName: 'हिन्दी', dir: 'ltr' }, |
| 12 | + fr: { name: 'French', nativeName: 'Français', dir: 'ltr' } |
| 13 | +}; |
| 14 | + |
| 15 | +const STORAGE_KEY = 'fula-docs-language'; |
| 16 | +const DEFAULT_LANGUAGE = 'en'; |
| 17 | + |
| 18 | +class I18n { |
| 19 | + constructor() { |
| 20 | + this.translations = {}; |
| 21 | + this.currentLanguage = this.getSavedLanguage() || this.detectBrowserLanguage() || DEFAULT_LANGUAGE; |
| 22 | + this.listeners = []; |
| 23 | + } |
| 24 | + |
| 25 | + getSavedLanguage() { |
| 26 | + try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; } |
| 27 | + } |
| 28 | + |
| 29 | + detectBrowserLanguage() { |
| 30 | + const browserLang = (navigator.language || navigator.userLanguage || '').split('-')[0]; |
| 31 | + return SUPPORTED_LANGUAGES[browserLang] ? browserLang : null; |
| 32 | + } |
| 33 | + |
| 34 | + getLanguage() { return this.currentLanguage; } |
| 35 | + |
| 36 | + setLanguage(lang) { |
| 37 | + if (!SUPPORTED_LANGUAGES[lang]) lang = DEFAULT_LANGUAGE; |
| 38 | + this.currentLanguage = lang; |
| 39 | + try { localStorage.setItem(STORAGE_KEY, lang); } catch (e) {} |
| 40 | + this.applyLanguage(); |
| 41 | + this.listeners.forEach(cb => cb(lang)); |
| 42 | + } |
| 43 | + |
| 44 | + t(key, fallback = null) { |
| 45 | + const t = this.translations[this.currentLanguage]; |
| 46 | + if (t && t[key]) return t[key]; |
| 47 | + if (this.translations.en && this.translations.en[key]) return this.translations.en[key]; |
| 48 | + return fallback || key; |
| 49 | + } |
| 50 | + |
| 51 | + applyLanguage() { |
| 52 | + const langInfo = SUPPORTED_LANGUAGES[this.currentLanguage]; |
| 53 | + document.documentElement.setAttribute('lang', this.currentLanguage); |
| 54 | + document.documentElement.setAttribute('dir', langInfo.dir); |
| 55 | + document.body.classList.toggle('rtl', langInfo.dir === 'rtl'); |
| 56 | + |
| 57 | + document.querySelectorAll('[data-i18n]').forEach(el => { |
| 58 | + const key = el.getAttribute('data-i18n'); |
| 59 | + const translation = this.t(key); |
| 60 | + if (translation.includes('<')) el.innerHTML = translation; |
| 61 | + else el.textContent = translation; |
| 62 | + }); |
| 63 | + |
| 64 | + document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { |
| 65 | + el.placeholder = this.t(el.getAttribute('data-i18n-placeholder')); |
| 66 | + }); |
| 67 | + |
| 68 | + const selector = document.querySelector('.language-selector-current'); |
| 69 | + if (selector) selector.textContent = langInfo.nativeName; |
| 70 | + } |
| 71 | + |
| 72 | + onLanguageChange(callback) { this.listeners.push(callback); } |
| 73 | + getSupportedLanguages() { return SUPPORTED_LANGUAGES; } |
| 74 | + loadTranslations(t) { this.translations = t; } |
| 75 | +} |
| 76 | + |
| 77 | +const i18n = new I18n(); |
| 78 | + |
| 79 | +function createLanguageSelector() { |
| 80 | + const selector = document.createElement('div'); |
| 81 | + selector.className = 'language-selector'; |
| 82 | + selector.innerHTML = ` |
| 83 | + <button class="language-selector-btn" aria-label="Select language"> |
| 84 | + <span class="language-icon">🌐</span> |
| 85 | + <span class="language-selector-current">${SUPPORTED_LANGUAGES[i18n.getLanguage()].nativeName}</span> |
| 86 | + <span class="language-arrow">▼</span> |
| 87 | + </button> |
| 88 | + <div class="language-dropdown"> |
| 89 | + ${Object.entries(SUPPORTED_LANGUAGES).map(([code, info]) => ` |
| 90 | + <button class="language-option ${code === i18n.getLanguage() ? 'active' : ''}" |
| 91 | + data-lang="${code}" dir="${info.dir}"> |
| 92 | + <span class="lang-native">${info.nativeName}</span> |
| 93 | + <span class="lang-english">${info.name}</span> |
| 94 | + </button> |
| 95 | + `).join('')} |
| 96 | + </div> |
| 97 | + `; |
| 98 | + |
| 99 | + const btn = selector.querySelector('.language-selector-btn'); |
| 100 | + const dropdown = selector.querySelector('.language-dropdown'); |
| 101 | + |
| 102 | + btn.addEventListener('click', (e) => { e.stopPropagation(); dropdown.classList.toggle('open'); }); |
| 103 | + |
| 104 | + selector.querySelectorAll('.language-option').forEach(option => { |
| 105 | + option.addEventListener('click', (e) => { |
| 106 | + e.stopPropagation(); |
| 107 | + i18n.setLanguage(option.getAttribute('data-lang')); |
| 108 | + dropdown.classList.remove('open'); |
| 109 | + selector.querySelectorAll('.language-option').forEach(opt => { |
| 110 | + opt.classList.toggle('active', opt.getAttribute('data-lang') === i18n.getLanguage()); |
| 111 | + }); |
| 112 | + }); |
| 113 | + }); |
| 114 | + |
| 115 | + document.addEventListener('click', () => dropdown.classList.remove('open')); |
| 116 | + document.addEventListener('keydown', (e) => { if (e.key === 'Escape') dropdown.classList.remove('open'); }); |
| 117 | + |
| 118 | + return selector; |
| 119 | +} |
| 120 | + |
| 121 | +function initI18n() { |
| 122 | + const sidebarHeader = document.querySelector('.sidebar-header'); |
| 123 | + if (sidebarHeader) { |
| 124 | + const langSelector = createLanguageSelector(); |
| 125 | + const themeToggle = sidebarHeader.querySelector('.theme-toggle'); |
| 126 | + if (themeToggle) themeToggle.insertAdjacentElement('beforebegin', langSelector); |
| 127 | + else sidebarHeader.appendChild(langSelector); |
| 128 | + } |
| 129 | + |
| 130 | + const mobileHeaderActions = document.querySelector('.mobile-header-actions'); |
| 131 | + if (mobileHeaderActions) { |
| 132 | + const mobileLangSelector = createLanguageSelector(); |
| 133 | + mobileLangSelector.classList.add('mobile-lang-selector'); |
| 134 | + mobileHeaderActions.insertBefore(mobileLangSelector, mobileHeaderActions.firstChild); |
| 135 | + } |
| 136 | + |
| 137 | + i18n.applyLanguage(); |
| 138 | +} |
| 139 | + |
| 140 | +// Auto-init when DOM is ready |
| 141 | +if (document.readyState === 'loading') { |
| 142 | + document.addEventListener('DOMContentLoaded', initI18n); |
| 143 | +} else { |
| 144 | + initI18n(); |
| 145 | +} |
0 commit comments