Update project dependencies, documentation, and supporting files: - i18n improvements for multilingual support - Admin dashboard enhancements - Documentation updates for Koha/Stripe and deployment - Server middleware and model updates - Package dependency updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
198 lines
5.2 KiB
JavaScript
198 lines
5.2 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';
|
|
},
|
|
|
|
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'
|
|
};
|
|
|
|
return pageMap[path] || 'homepage';
|
|
},
|
|
|
|
async loadTranslations(lang) {
|
|
try {
|
|
const pageName = this.detectPageName();
|
|
const response = await fetch(`/locales/${lang}/${pageName}.json`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load translations for ${lang}/${pageName}`);
|
|
}
|
|
this.translations = await response.json();
|
|
console.log(`[i18n] Loaded translations for page: ${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
|
|
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;
|