tractatus/public/js/i18n-simple.js
TheFlow b2ac3dcbee feat: implement responsive mobile language selector
Mobile UX Improvements:
- Replace dropdown with icon-only flags on mobile (< 768px)
- Add 44x44px touch targets for better mobile interaction
- Add language selector to mobile menu drawer
- Desktop keeps full dropdown with language names (≥ 768px)

Language Selector Features:
- Mobile navbar: Icon-only buttons (🇬🇧 🇩🇪 🇫🇷)
- Desktop navbar: Dropdown with full text
- Mobile drawer: Full language list with checkmarks
- Active state: Blue ring around selected language
- Auto-close drawer after language selection

Accessibility:
- ARIA labels on all buttons
- aria-pressed state for current language
- Minimum 44x44px touch targets (WCAG AA)
- Keyboard navigation support maintained
- Screen reader support with role="group"

Technical Changes:
- language-selector.js: Rewritten with responsive versions
- navbar.js: Added mobile-menu-language-selector container
- i18n-simple.js: Added languageChanged event dispatch

UX Benefits:
- Space savings: ~87px saved in mobile navbar
- No crowding between language selector and hamburger menu
- Flag emojis are universally recognizable
- Touch-friendly buttons meet iOS/Android standards

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 14:59:58 +13:00

156 lines
3.8 KiB
JavaScript

/**
* 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() {
// Check localStorage first
const saved = localStorage.getItem('tractatus-lang');
if (saved && this.supportedLanguages.includes(saved)) {
return saved;
}
// Check browser language
const browserLang = (navigator.language || navigator.userLanguage).split('-')[0];
if (this.supportedLanguages.includes(browserLang)) {
return browserLang;
}
// Default to English
return 'en';
},
async loadTranslations(lang) {
try {
const response = await fetch(`/locales/${lang}/homepage.json`);
if (!response.ok) {
throw new Error(`Failed to load translations for ${lang}`);
}
this.translations = await response.json();
} 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
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
const translation = this.t(key);
if (typeof translation === 'string') {
el.textContent = translation;
}
});
// Handle data-i18n-html for HTML content
document.querySelectorAll('[data-i18n-html]').forEach(el => {
const key = el.dataset.i18nHtml;
const translation = this.t(key);
if (typeof translation === 'string') {
el.innerHTML = translation;
}
});
},
async setLanguage(lang) {
if (!this.supportedLanguages.includes(lang)) {
console.error(`[i18n] Unsupported language: ${lang}`);
return;
}
// Save preference
localStorage.setItem('tractatus-lang', lang);
// 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}`);
},
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;