tractatus/public/js/i18n-simple.js
TheFlow c16cea80c8 feat(i18n): enhance browser language detection with clear priority logging
SUMMARY:
Enhanced i18n language detection to clearly show priority order and
provide better logging for debugging user language preferences.

PRIORITY ORDER:
1. User's manual selection (localStorage) - HIGHEST
   - Clicking language flags saves preference here
   - Allows users to override browser language

2. Browser's language setting (navigator.language) - AUTOMATIC
   - Detects from browser preferences
   - Only applies if no user override exists

3. Default to English (fallback)
   - Used when browser language not supported

CHANGES:

1. Enhanced detectLanguage():
   - Added clear comments documenting priority order
   - Added console.log for each detection path
   - Shows which source determined the language
   - Logs browser language even when not supported

2. Enhanced setLanguage():
   - Added log when user manually selects language
   - Clarifies that preference overrides browser detection
   - Shows that preference persists across pages

BENEFITS:
✓ Users see automatic language detection from browser
✓ Users can override via flag clicks (persists via localStorage)
✓ Clear logging helps debug language selection issues
✓ Developers can see exactly how language was determined

EXAMPLE LOGS:
- Browser detection: '[i18n] Language detected from browser: de (from de-DE)'
- User override: '[i18n] User manually selected language: fr (saved to localStorage)'
- Fallback: '[i18n] Language defaulted to: en (browser language 'ja-JP' not supported)'

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 15:01:14 +13:00

225 lines
6.6 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() {
// 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 <em>, <strong>, <a> 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;