/** * Simple i18n system for Tractatus * Supports: en (English), de (German), fr (French), mi (Te Reo Māori - coming soon) */ const I18n = { currentLang: 'en', translations: {}, supportedLanguages: ['en', 'de', 'fr'], async init() { // 1. Detect language preference this.currentLang = this.detectLanguage(); // 2. Load translations await this.loadTranslations(this.currentLang); // 3. Apply to page this.applyTranslations(); // 4. Update language selector if present this.updateLanguageSelector(); console.log(`[i18n] Initialized with language: ${this.currentLang}`); }, detectLanguage() { // Priority order: // 1. User's manual selection (localStorage) - allows override via flag clicks // 2. Browser's language setting (automatic detection) // 3. Default to English (fallback) // 1. Check localStorage first (user override) const saved = localStorage.getItem('tractatus-lang'); if (saved && this.supportedLanguages.includes(saved)) { console.log(`[i18n] Language detected from user preference: ${saved}`); return saved; } // 2. Check browser language (automatic detection) const browserLang = (navigator.language || navigator.userLanguage).split('-')[0]; if (this.supportedLanguages.includes(browserLang)) { console.log(`[i18n] Language detected from browser: ${browserLang} (from ${navigator.language})`); return browserLang; } // 3. Default to English console.log(`[i18n] Language defaulted to: en (browser language '${navigator.language}' not supported)`); return 'en'; }, detectPageName() { // Try to get page name from data attribute first const pageAttr = document.documentElement.getAttribute('data-page'); if (pageAttr) { return pageAttr; } // Detect from URL path const path = window.location.pathname; // Map paths to translation file names const pageMap = { '/': 'homepage', '/index.html': 'homepage', '/researcher.html': 'researcher', '/leader.html': 'leader', '/implementer.html': 'implementer', '/about.html': 'about', '/about/values.html': 'values', '/about/values': 'values', '/faq.html': 'faq', '/koha.html': 'koha', '/koha/transparency.html': 'transparency', '/koha/transparency': 'transparency', '/privacy.html': 'privacy', '/privacy': 'privacy' }; return pageMap[path] || 'homepage'; }, async loadTranslations(lang) { try { // Always load common translations (footer, navbar, etc.) const commonResponse = await fetch(`/locales/${lang}/common.json`); let commonTranslations = {}; if (commonResponse.ok) { commonTranslations = await commonResponse.json(); } // Load page-specific translations const pageName = this.detectPageName(); const pageResponse = await fetch(`/locales/${lang}/${pageName}.json`); let pageTranslations = {}; if (pageResponse.ok) { pageTranslations = await pageResponse.json(); } else if (pageName !== 'homepage') { // If page-specific translations don't exist, that's okay for some pages console.warn(`[i18n] No translations found for ${lang}/${pageName}, using common only`); } else { throw new Error(`Failed to load translations for ${lang}/${pageName}`); } // Merge common and page-specific translations (page-specific takes precedence) this.translations = { ...commonTranslations, ...pageTranslations }; console.log(`[i18n] Loaded translations: common + ${pageName}`); } catch (error) { console.error(`[i18n] Error loading translations:`, error); // Fallback to English if loading fails if (lang !== 'en') { this.currentLang = 'en'; await this.loadTranslations('en'); } } }, t(key) { const keys = key.split('.'); let value = this.translations; for (const k of keys) { if (value && typeof value === 'object') { value = value[k]; } else { return key; // Return key if translation not found } } return value || key; }, applyTranslations() { // Find all elements with data-i18n attribute // Using innerHTML to preserve formatting like , , tags in translations document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.dataset.i18n; const translation = this.t(key); if (typeof translation === 'string') { el.innerHTML = translation; } }); // Handle data-i18n-html for HTML content (kept for backward compatibility) document.querySelectorAll('[data-i18n-html]').forEach(el => { const key = el.dataset.i18nHtml; const translation = this.t(key); if (typeof translation === 'string') { el.innerHTML = translation; } }); // Handle data-i18n-placeholder for input placeholders document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { const key = el.dataset.i18nPlaceholder; const translation = this.t(key); if (typeof translation === 'string') { el.placeholder = translation; } }); }, async setLanguage(lang) { if (!this.supportedLanguages.includes(lang)) { console.error(`[i18n] Unsupported language: ${lang}`); return; } // Save preference (overrides browser language detection) localStorage.setItem('tractatus-lang', lang); console.log(`[i18n] User manually selected language: ${lang} (saved to localStorage)`); // Update current language this.currentLang = lang; // Reload translations await this.loadTranslations(lang); // Reapply to page this.applyTranslations(); // Update selector this.updateLanguageSelector(); // Update HTML lang attribute document.documentElement.lang = lang; // Dispatch event for language change window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } })); console.log(`[i18n] Language changed to: ${lang} (will persist across pages)`); }, updateLanguageSelector() { const selector = document.getElementById('language-selector'); if (selector) { selector.value = this.currentLang; } }, getLanguageName(code) { const names = { 'en': 'English', 'de': 'Deutsch', 'fr': 'Français', 'mi': 'Te Reo Māori' }; return names[code] || code; } }; // Auto-initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => I18n.init()); } else { I18n.init(); } // Make available globally window.I18n = I18n;